/* * Copyright © 2024 Matthew Wozniak * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ #define NO_EXTERNS #include "api.h" #undef NO_EXTERNS #include "os.h" #include "log.h" #include "sst/x86.h" struct engserver *engserver = 0; struct engclient *engclient = 0; struct engineapi *engineapi = 0; struct enginetools *enginetools = 0; struct cvar *cvar = 0; struct demoplayer *demoplayer = 0; struct videomode **videomode = 0; struct movieinfo *movieinfo = 0; struct audio_device **audio_device = 0; struct soundstate *snd = 0; void (*snd_recordbuffer)(void) = 0; void (*cbuf_addtext)(char *) = 0; bool steampipe = false; // This macro is Copyright 2024 Michael Smith under the same license as above. #define NEXT_INSN(p, tgt) do { \ int _len = x86_len(p); \ if (_len == -1) { \ bail("unknown or invalid instruction looking for %s", tgt); \ return false; \ } \ (p) += _len; \ } while (0) // TODO: should this be split up? bool api_init(bool _steampipe) { steampipe = _steampipe; void *engine_dll = os_dlhandle("engine"); createinterface_func engine_factory = (createinterface_func)os_dlsym(engine_dll, "CreateInterface"); void *vstdlib_dll = os_dlhandle("vstdlib"); createinterface_func vstdlib_factory = (createinterface_func)os_dlsym(vstdlib_dll, "CreateInterface"); if (!engine_factory) bail("couldn't get engine factory"); engserver = engine_factory(VENGINE_SERVER_INTERFACE_VERSION, 0); if (!engserver) bail("couldn't get IVEngineServer from engine"); engclient = engine_factory(VENGINE_CLIENT_INTERFACE_VERSION, 0); if (!engclient) bail("couldn't get IVEngineClient from engine"); engineapi = engine_factory(VENGINE_LAUNCHER_INTERFACE_VERSION, 0); if (!engineapi) bail("couldn't get IEngineAPI from engine"); enginetools = engine_factory(VENGINE_TOOL_INTERFACE_VERSION, 0); if (!engineapi) bail("couldn't get IEngineTools from engine"); cvar = vstdlib_factory(VENGINE_CVAR_INTERFACE_VERSION, 0); if (!cvar) bail("couldn't get ICVar from vstdlib"); // find cbuf_addtext const u8 *instr = (const u8 *)engserver->vt->server_command; // ServerCommand() calls a few small functions before Cbuf_AddText but they // get inlined. look for 'push esi' and then a call. for (const u8 *p = instr; p - instr < 64;) { if (*p == X86_PUSHESI && *++p == X86_CALL) { // jump is relative to after the instruction cbuf_addtext = (void (*)(char *))(p + 5 + *(i32 *)(p + 1)); } NEXT_INSN(p, "CBuf_AddText"); } if (!cbuf_addtext) bail("couldn't find cbuf_addtext"); // find demoplayer instr = steampipe ? (const u8 *)engclient->vt->steampipe_is_playing_demo : (const u8 *)engclient->vt->is_playing_demo; // CEngineClient::IsPlayingDemo is a wrapper around a demoplayer call // The first thing it does should be load a ptr to demoplayer into ECX if (instr[0] != X86_MOVRMW || instr[1] != X86_MODRM(0, 1, 5)) bail("couldn't get demoplayer"); demoplayer = **(struct demoplayer ***)(instr + 2); // find videomode // CEngineAPI::SetEngineWindow calls videomode->SetGameWindow after // detaching the current window with another vcall. Look for the second one. // This, like the demoplayer, is a pointer. We need to keep the double // pointer because when we get this, it is null. It is set by the engine // later. instr = (const u8 *)engineapi->vt->set_engine_window; int mov_ecx_counter = 0; for (const u8 *p = instr; p - instr < 64;) { if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5) && ++mov_ecx_counter == 2) { videomode = *(struct videomode ***)(p + 2); break; } NEXT_INSN(p, "videomode"); } // find cl_movieinfo { // step 1: find CL_IsRecordingMovie() in // CEngineTools::StartMovieRecording instr = (const u8 *)enginetools->vt->start_movie_recording; void *cl_isrecordingmovie = 0; for (const u8 *p = instr; p - instr < 64;) { // this is the first call in the function if (*p == X86_CALL) { cl_isrecordingmovie = (void *)(p + 5 + *(i32 *)(p + 1)); } NEXT_INSN(p, "CL_IsRecordingMovie"); } if (!cl_isrecordingmovie) bail("couldn't find cbuf_addtext CL_IsRecordingMovie"); // step 2: get the pointer to cl_movieinfo // should be in the first instruction instr = (const u8 *)cl_isrecordingmovie; if (instr[0] != X86_ALUMI8 || instr[1] != X86_MODRM(0, 7, 5)) bail("couldn't get movieinfo"); movieinfo = *(struct movieinfo **)(instr + 2); } // find SND_RecordBuffer... this is something! { // step 1: Find S_Init() in snd_restart struct concommand *snd_restart = cvar->vt->find_command(cvar, "snd_restart"); instr = (const u8 *)snd_restart->callback; void *s_init = 0; // S_Init is called after setting snd_firsttime to true. Look for the // second call after the mov bool snd_firsttime_set = false; int call_count = 0; for (const u8 *p = instr; p - instr < 80;) { // mov byte ptr [snd_firsttime], 1 if (p[0] == X86_MOVMI8 && p[1] == 0x05 && p[6] == 1) snd_firsttime_set = true; if (p[0] == X86_CALL && snd_firsttime_set && ++call_count == 2) { s_init = (void *)(p + 5 + *(i32 *)(p + 1)); break; } NEXT_INSN(p, "S_Init"); } if (!s_init) bail("couldn't find S_Init"); // step 2: find g_AudioDevice in S_Init // g_AudioDevice is the first variable assignment after the check for // the -nosound command line arg. look for that. instr = (const u8 *)s_init; bool nosound_push = false; for (const u8 *p = instr; p - instr < 180;) { if (p[0] == X86_PUSHIW && !strcmp(*(const char **)(p + 1), "-nosound")) nosound_push = true; if (nosound_push && p[0] == X86_MOVIIEAX) { audio_device = *(struct audio_device ***)(p + 1); break; } NEXT_INSN(p, "g_AudioDevice"); } if (!audio_device) bail("couldn't get ptr to g_AudioDevice"); } return true; } bool api_find_snd_recordbuffer(void) { // continuation, we must do this part later // step 3: Find S_TransferStereo16 in CAudioDirectSound::TransferSamples void *transferstereo16 = 0; const u8 *instr = steampipe ? (*audio_device)->vt->steampipe_transfer_samples : (*audio_device)->vt->transfer_samples; // S_TransferStereo16 is the 8th call in TransferSamples int call_count = 0; for (const u8 *p = instr; p - instr < 384 /* big func! */;) { if (*p == X86_CALL && ++call_count == 8) transferstereo16 = (void *)(p + 5 + *(i32 *)(p + 1)); NEXT_INSN(p, "S_TransferStereo16"); } if (!transferstereo16) bail("couldn't find transferstereo16"); // step 4: find SND_RecordBuffer in S_TransferStereo16 // it should be the next call after an 'add ecx, ecx' // We are also going to get Snd_WriteLinearBlastStereo16 to get some other // variables we need. instr = (const u8 *)transferstereo16; bool found_double = false; bool found_recbuf = false; for (const u8 *p = instr; p - instr < 192;) { if (p[0] == X86_ADDRMW && p[1] == X86_MODRM(3, 1, 1)) found_double = true; else if (p[0] == X86_CALL && found_double && !found_recbuf) { found_recbuf = true; snd_recordbuffer = (void (*)(void))(p + 5 + *(i32 *)(p + 1)); } // this will be the next call else if (p[0] == X86_CALL && found_double && found_recbuf) { // this looks outrageous! that is because it is. snd = (struct soundstate *)(p + 5 + *(i32 *)(p + 1) + 3); break; } NEXT_INSN(p, "SND_RecordBuffer"); } if (!snd_recordbuffer) bail("couldn't find snd_recordbuffer"); return true; } // vi: sw=4 ts=4 noet tw=80 cc=80