From 35a3e0871c5a98110847765f10a3c2c604323ee6 Mon Sep 17 00:00:00 2001 From: Willian Henrique Date: Thu, 19 Jun 2025 21:28:10 -0300 Subject: Add a way to cancel "ISG" in Portal runs This is a stopgap solution approved by the Portal moderators until we have a better way to handle timing and verifying closing/restarting the game mid-run. Essentially, the agreed-upon short-term solution is to somewhat emulate restarting the game by simply turning off the glitch and sending the player back to the main menu. Committer's note: this wasn't added to the Linux compile script since it doesn't work and I honestly just couldn't be bothered adding in the usual ifdef-errors. And in fact I'm starting to wonder if we should just be leaving out features that don't work on Linux to avoid all the ifdefs, but that can happen later because the main goal of committing this is to get a release out, so I don't want to faff about with that right now. --- src/portalisg.c | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/portalisg.c (limited to 'src/portalisg.c') diff --git a/src/portalisg.c b/src/portalisg.c new file mode 100644 index 0000000..ffa80f2 --- /dev/null +++ b/src/portalisg.c @@ -0,0 +1,152 @@ +/* + * Copyright © Willian Henrique + * + * 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. + */ + +#include "con_.h" +#include "engineapi.h" +#include "errmsg.h" +#include "feature.h" +#include "gamedata.h" +#include "intdefs.h" +#include "langext.h" +#include "mem.h" +#include "x86.h" +#include "x86util.h" + +FEATURE("Portal \"ISG\" state reset (experimental)") +GAMESPECIFIC(Portal1) +REQUIRE_GAMEDATA(vtidx_CreateEnvironment) +REQUIRE_GAMEDATA(vtidx_CreatePolyObject) +REQUIRE_GAMEDATA(vtidx_RecheckCollisionFilter) + +static bool *isg_flag; +static con_cmdcbv2 disconnect_cb; + +DEF_FEAT_CCMD_HERE(sst_portal_resetisg, + "Remove \"ISG\" state and disconnect from the server", 0) { + struct con_cmdargs disconnect_args = {0}; + disconnect_cb(&disconnect_args); + *isg_flag = false; +} + +static void **find_physenv_vtable(void *CreateEnvironment) { + const uchar *insns = (uchar *)CreateEnvironment; + for (const uchar *p = insns; p - insns < 16;) { + if (*p == X86_CALL) { p = insns = p + 5 + mem_loads32(p + 1); goto _1; } + NEXT_INSN(p, "call to CreateEnvironment"); + } + return 0; +_1: for (const uchar *p = insns; p - insns < 32;) { + // tail call to the constructor + if (*p == X86_JMPIW) { insns = p + 5 + mem_loads32(p + 1); goto _2; } + NEXT_INSN(p, "call to CPhysicsEnvironment constructor"); + } + return 0; +_2: for (const uchar *p = insns; p - insns < 16;) { + // the vtable is loaded pretty early on: + // mov dword ptr [reg], + if (*p == X86_MOVMIW && (p[1] & 0xF8) == 0) return mem_loadptr(p + 2); + NEXT_INSN(p, "CPhysicsEnvironment vtable"); + } + return 0; +} + +static void **find_physobj_vtable(void *CreatePolyObject) { + const uchar *insns = (uchar *)CreatePolyObject; + for (const uchar *p = insns; p - insns < 64;) { + // first thing in the method is a call (after pushing a million params) + if (*p == X86_CALL) { + insns = p + 5 + mem_loads32(p + 1); + goto _1; + } + NEXT_INSN(p, "call to CreatePhysicsObject"); + } + return 0; +_1: for (const uchar *p = insns; p - insns < 768;) { + // there's a call to "new CPhysicsObject" somewhere down the line. + // the (always inlined) constructor calls memset on the obj to init it. + // the obj's vtable being loaded in is interleaved with pushing args + // for memset and the order for all the instructions varies between + // versions. the consistent bit is that `push 72` always happens shortly + // before the vtable is loaded. + if (*p == X86_PUSHI8 && p[1] == 72) { insns = p + 2; goto _2; } + NEXT_INSN(p, "push before CPhysicsObject vtable load"); + } + return 0; +_2: for (const uchar *p = insns; p - insns < 16;) { + // mov dword ptr [reg], + if (*p == X86_MOVMIW && (p[1] & 0xF8) == 0) return mem_loadptr(p + 2); + NEXT_INSN(p, "CPhysicsObject vtable"); + } + return 0; +} + +static bool find_isg_flag(void *RecheckCollisionFilter) { + const uchar *insns = (uchar *)RecheckCollisionFilter, *p = insns; + while (p - insns < 32) { + // besides some flag handling, the only thing this function does is + // call m_pObject->recheck_collision_filter() + if (*p == X86_CALL) { + p = p + 5 + mem_loads32(p + 1); + goto _1; + } + NEXT_INSN(p, "call to RecheckCollisionFilter"); + } + return false; +_1: for (insns = p; p - insns < 32;) { + // recheck_collision_filter pretty much just calls a function + if (*p == X86_CALL) { + p = p + 5 + mem_loads32(p + 1); + goto _2; + } + NEXT_INSN(p, "call to recheck_ov_element"); + } + return false; +_2: for (insns = p; p - insns < 0x300;) { + // mov byte ptr [g_fDeferDeleteMindist] + if (*p == X86_MOVMI8 && p[1] == X86_MODRM(0, 0, 5) && p[6] == 1) { + isg_flag = mem_loadptr(p + 2); + return true; + } + NEXT_INSN(p, "g_fDeferDeleteMindist"); + } + return false; +} + +INIT { + disconnect_cb = con_getcmdcbv2(con_findcmd("disconnect")); + if_cold(!disconnect_cb) return FEAT_INCOMPAT; + void *phys = factory_engine("VPhysics031", 0); + if_cold (phys == 0) { + errmsg_errorx("couldn't get IPhysics interface"); + return FEAT_INCOMPAT; + } + void **vtable = mem_loadptr(phys); + vtable = find_physenv_vtable(vtable[vtidx_CreateEnvironment]); + if_cold (!vtable) { + errmsg_errorx("couldn't find CPhysicsEnvironment vtable"); + return FEAT_INCOMPAT; + } + vtable = find_physobj_vtable(vtable[vtidx_CreatePolyObject]); + if_cold (!vtable) { + errmsg_errorx("couldn't find CPhysicsObject vtable"); + return FEAT_INCOMPAT; + } + if_cold (!find_isg_flag(vtable[vtidx_RecheckCollisionFilter])) { + errmsg_errorx("couldn't find ISG flag"); + return FEAT_INCOMPAT; + } + return FEAT_OK; +} -- cgit v1.2.3-54-g00ecf