summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMichael Smith <mikesmiffy128@gmail.com>2021-12-27 03:14:47 +0000
committerMichael Smith <mikesmiffy128@gmail.com>2021-12-27 03:14:47 +0000
commit8eecc029568bbe8e2f3c0d9af218ad3f957251c9 (patch)
treecb2c8a75e051968aecfdc90b318634065c6986b0
parentcdb02fefa748dc05cc66b48ca973bfe21decc5e6 (diff)
Add custom demo packet stuff
This is more old code that wasn't part of the initial release. Figure I might as well throw it in for later.
-rw-r--r--src/demorec.c144
-rw-r--r--src/demorec.h11
-rw-r--r--src/sst.c2
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
diff --git a/src/sst.c b/src/sst.c
index 183a73a..176d5fb 100644
--- a/src/sst.c
+++ b/src/sst.c
@@ -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