diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/demorec.c | 144 | ||||
| -rw-r--r-- | src/demorec.h | 11 | ||||
| -rw-r--r-- | src/sst.c | 2 | 
3 files changed, 152 insertions, 5 deletions
diff --git a/src/demorec.c b/src/demorec.c index 60966e5..c4ac504 100644 --- a/src/demorec.c +++ b/src/demorec.c @@ -16,9 +16,13 @@   */  #include <stdbool.h> +#include <string.h> +#include "bitbuf.h"  #include "con_.h" +#include "demorec.h"  #include "hook.h" +#include "factory.h"  #include "gamedata.h"  #include "intdefs.h"  #include "mem.h" @@ -39,6 +43,8 @@ static int *demonum;  static f_SetSignonState orig_SetSignonState;  static f_StopRecording orig_StopRecording;  static con_cmdcb orig_stop_callback; +static int nbits_msgtype; +static int nbits_datalen;  static int auto_demonum = 1;  static bool auto_recording = false; @@ -89,6 +95,51 @@ static void hook_stop_callback(const struct con_cmdargs *args) {  	orig_stop_callback(args);  } + +// The engine allows usermessages up to 255 bytes, we add 2 bytes of overhead, +// and then there's the leading bits before that too (see create_message) +static char bb_buf[DEMOREC_CUSTOM_MSG_MAX + 4]; +static struct bitbuf bb = { +	bb_buf, sizeof(bb_buf), sizeof(bb_buf) * 8, 0, false, false, "SST" +}; + +static void create_message(struct bitbuf *msg, const void *buf, int len) { +	// The way we pack our custom demo data is via a user message packet with +	// type "HudText" - this causes the client to do a text lookup which will +	// simply silently fail on invalid keys. By making the first byte null +	// (creating an empty string), we get the rest of the packet to stick in +	// whatever other data we want. +	// +	// Notes from Uncrafted: +	// > But yeah the data you want to append is as follows: +	// > - 6 bits (5 bits in older versions) for the message type - should be 23 +	// >   for user message +	bitbuf_appendbits(msg, 23, nbits_msgtype); +	// > - 1 byte for the user message type - should be 2 for HudText +	bitbuf_appendbyte(msg, 2); +	// > - ~~an int~~ 11 or 12 bits for the length of your data in bits, +	// NOTE: this assumes len <= 254 +	bitbuf_appendbits(msg, len * 8, nbits_datalen); +	// > - your data +	// [first the aforementioned null byte, plus an arbitrary marker byte to +	// avoid confusion when parsing the demo later... +	bitbuf_appendbyte(msg, 0); +	bitbuf_appendbyte(msg, 0xAC); +	// ... and then just the data itself] +	bitbuf_appendbuf(msg, buf, len); +	// Thanks Uncrafted, very cool! +} + +typedef void (*VCALLCONV WriteMessages_func)(void *this, struct bitbuf *msg); +static WriteMessages_func WriteMessages = 0; + +void demorec_writecustom(void *buf, int len) { +	create_message(&bb, buf, len); +	WriteMessages(demorecorder, &bb); +	bitbuf_reset(&bb); +} + +  // This finds the "demorecorder" global variable (the engine-wide CDemoRecorder  // instance).  static inline void *find_demorecorder(struct con_cmd *cmd_stop) { @@ -117,8 +168,8 @@ static inline void *find_demorecorder(struct con_cmd *cmd_stop) {  }  // This finds "m_bRecording" and "m_nDemoNumber" using the pointer to the -// original "StopRecording" demorecorder function -static inline bool find_recmembers(void *stop_recording_func, void *demorec) { +// original "StopRecording" demorecorder function. +static inline bool find_recmembers(void *stop_recording_func) {  	struct ud udis;  	ud_init(&udis);  	ud_set_mode(&udis, 32); @@ -138,10 +189,12 @@ static inline bool find_recmembers(void *stop_recording_func, void *demorec) {  			// the byte immediate refers to m_bRecording  			if (src->type == UD_OP_IMM && src->lval.ubyte == 0) {  				if (src->size == 8) { -					recording = (bool *)mem_offset(demorec, dest->lval.udword); +					recording = (bool *)mem_offset(demorecorder, +							dest->lval.udword);  				}  				else { -					demonum = (int *)mem_offset(demorec, dest->lval.udword); +					demonum = (int *)mem_offset(demorecorder, +							dest->lval.udword);  				}  				if (recording && demonum) return true; // blegh  			} @@ -156,6 +209,37 @@ static inline bool find_recmembers(void *stop_recording_func, void *demorec) {  	return false;  } +// This finds the CDemoRecorder::WriteMessages() function, which takes a raw +// network packet, wraps it up in the appropriate demo framing format and writes +// it out to the demo file being recorded. +static bool find_WriteMessages(void) { +	const uchar *insns = (*(uchar ***)demorecorder)[gamedata_vtidx_RecordPacket]; +	// RecordPacket calls WriteMessages pretty much right away: +	// 56           push  esi +	// 57           push  edi +	// 8B F1        mov   esi,ecx +	// 8D BE        lea   edi,[esi + 0x68c] +	// 8C 06 00 00 +	// 57           push  edi +	// E8           call  CDemoRecorder_WriteMessages +	// B0 EF FF FF +	// So we just double check the byte pattern... +	static const uchar bytes[] = +#ifdef _WIN32 +{0x56, 0x57, 0x8B, 0xF1, 0x8D, 0xBE, 0x8C, 0x06, 0x00, 0x00, 0x57, 0xE8}; +#else +#error This is possibly different on Linux too, have a look! +#endif +	if (!memcmp(insns, bytes, sizeof(bytes))) { +		ssize off = mem_loadoffset(insns + sizeof(bytes)); +		// ... and then offset is relative to the address of whatever is _after_ +		// the call instruction... because x86. +		WriteMessages = (WriteMessages_func)(insns + sizeof(bytes) + 4 + off); +		return true; +	} +	return false; +} +  bool demorec_init(void) {  	if (!gamedata_has_vtidx_SetSignonState ||  			!gamedata_has_vtidx_StopRecording) { @@ -189,7 +273,7 @@ bool demorec_init(void) {  		return false;  	} -	if (!find_recmembers(vtable[7], demorecorder)) { +	if (!find_recmembers(vtable[7])) {  		con_warn("demorec: couldn't find m_bRecording and m_nDemoNumber\n");  		return false;  	} @@ -206,6 +290,56 @@ bool demorec_init(void) {  	return true;  } +// make custom data a separate feature so we don't lose autorecording if we +// can't find the WriteMessage stuff +bool demorec_custom_init(void) {  +	if (!gamedata_has_vtidx_GetEngineBuildNumber || +			!gamedata_has_vtidx_RecordPacket) { +		con_warn("demorec: custom: missing gamedata entries for this engine\n"); +		return false; +	} +	// TODO(featgen): auto-check this factory +	if (!factory_engine) { +		con_warn("demorec: missing required interfaces\n"); +		return false; +	} + +	// More UncraftedkNowledge: +	// > yeah okay so [the usermessage length is] 11 bits if the demo protocol +	// > is 11 or if the game is l4d2 and the network protocol is 2042. +	// > otherwise it's 12 bits +	// > there might be some other l4d2 versions where it's 11 but idk +	// So here we have to figure out the network protocol version! +	void *clientiface; +	uint buildnum; +	// TODO(compat): probably expose VEngineClient/VEngineServer some other way +	// if it's useful elsewhere later!? +	if (clientiface = factory_engine("VEngineClient013", 0)) { +		typedef uint (*VCALLCONV GetEngineBuildNumber_func)(void *this); +		buildnum = (*(GetEngineBuildNumber_func **)clientiface)[ +				gamedata_vtidx_GetEngineBuildNumber](clientiface); +	} +	// add support for other interfaces here: +	// else if (clientiface = factory_engine("VEngineClient0XX", 0)) { +	//     ... +	// } +	else { +		return false; +	} +	// condition is redundant until other GetEngineBuildNumber offsets are added +	// if (GAMETYPE_MATCHES(L4D2)) { +		nbits_msgtype = 6; +		// based on Some Code I Read, buildnum *should* be the protocol version, +		// however L4D2 returns the actual game version instead, because sure +		// why not. The only practical difference though is that the network +		// protocol froze after 2042, so we just have to do a >=. No big deal +		// really. +		if (buildnum >= 2042) nbits_datalen = 11; else nbits_datalen = 12; +	// } +	 +	return find_WriteMessages(); +} +  void demorec_end(void) {  	void **vtable = *(void ***)demorecorder;  	unhook_vtable(vtable, gamedata_vtidx_SetSignonState, diff --git a/src/demorec.h b/src/demorec.h index 9d8e73e..d739393 100644 --- a/src/demorec.h +++ b/src/demorec.h @@ -23,6 +23,17 @@  bool demorec_init(void);  void demorec_end(void); +bool demorec_custom_init(void); + +/* maximum length of a custom demo message, in bytes */ +#define DEMOREC_CUSTOM_MSG_MAX 253 + +/* + * Write a block of up to DEMOWRITER_MSG_MAX bytes into the currently recording + * demo - NOT bounds checked, caller MUST ensure length is okay! + */ +void demorec_writecustom(void *buf, int len); +  #endif  // vi: sw=4 ts=4 noet tw=80 cc=80 @@ -68,6 +68,7 @@ ifacefactory factory_client = 0, factory_server = 0, factory_engine = 0;  // plonking ~~some bools~~ one bool here and worrying about it later. :^)  static bool has_autojump = false;  static bool has_demorec = false; +static bool has_demorec_custom = false;  // HACK: later versions of L4D2 show an annoying dialog on every plugin_load.  // We can suppress this by catching the message string that's passed from @@ -126,6 +127,7 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) {  nc:	gamedata_init();  	has_autojump = autojump_init();  	has_demorec = demorec_init(); +	if (has_demorec) has_demorec_custom = demorec_custom_init();  	fixes_apply();  	// NOTE: this is technically redundant for early versions but I CBA writing  | 
