// lot of implicit interdependent global state here and honestly any more votes // using the same framework would probably justify consolidating things into a // methodmap. #include #include #include #include #include public Plugin myinfo = { name = "yuuko's votes", author = "yuuko", description = "extra votes for open fortress", version = SOURCEMOD_VERSION, url = "https://www.partyvan.io/" }; // CBaseTFIssue appears pure virtual; idk if that matters at all but just to // be safe let's start with a known concrete vtable enum { base, CChangeMutatorIssue_typeinfo, CChangeMutatorIssue_D1, CChangeMutatorIssue_D0, // leave this alone; it's just the delete stub // this is what shows up in the vote setup menu CBaseIssue_GetTypeStringLocalized, CChangeMutatorIssue_GetDetailsString, // argv[1] to the vote CBaseIssue_SetIssueDetails, // writes this + YUUKO_VOTES_ISSUEDETAILS_OFFSET CBaseIssue_OnVoteFailed, CBaseIssue_OnVoteStarted, // runs before the corresponding message send CBaseIssue_OnVoteEnded, CChangeMutatorIssue_IsEnabled, CBaseIssue_CanTeamCallVote, CChangeMutatorIssue_CanCallVote, CBaseIssue_IsTeamRestrictedVote, CChangeMutatorIssue_GetDisplayString, CChangeMutatorIssue_ExecuteCommand, CBaseTFIssue_ListIssueDetails, CChangeMutatorIssue_GetVotePassedString, CBaseIssue_CountPotentialVoters, CBaseIssue_GetNumberVoteOptions, CBaseIssue_IsYesNoVote, CBaseIssue_SetYesNoVoteCount, CBaseIssue_GetVoteOptions, CBaseIssue_BRecordVoteFailureEventForEntity, CBaseIssue_GetQuorumRatio, CChangeMutatorIssue_vtable_length, CChangeMutatorIssue_length = 0xb4 } #define DH DynamicHook #define DH_NEW(%0,%1,%2) new DH(%0 - 2, HookType_Raw, ReturnType_%1, ThisPointer_%2) #define DH_HOOK(%1,%2,%3) %1.HookRaw(Hook_%2, %3, %1_Callback) #define DH_SIG(%1) MRESReturn %1_Callback #define VTABLE_SUB(%1,%2,%3,%4) WriteVal(%1 + %2 * 4, %3.GetMemSig(%4)) // ok so this is kind of cursed but if we make our display string resolve to // "%s1" we can show anything we want via the detail string. since that gets // overwritten when the vote goes through anyway, we can write to the object // instead of overriding the getter, which is just as well, given our apparent // lack of control over when sourcemod's native facility runs UserMessage // hooks besides "some time before it gets sent", meaning we wouldn't // confidently know when to fake it. #define FMTKEY "#TF_ScoreBoard_Points_Nolabel" #define SIMPLE_SCALAR(%1) (DHookReturn ret) { ret.Value = %1; return MRES_Supercede; } #define SIMPLE_STRING(%1) (DHookReturn ret) { ret.SetString(%1); return MRES_Supercede; } #define CPYLEN(%1) strlen(%1)+1 #define CSTRING(%1) StringToPtr(%1,CPYLEN(%1)) // globals --------------------------------------------------------------------- GameData GD; Handle CBaseIssue_CBaseIssue; int G_TopFrags_Cell[1]; ptr CFL_vtable; ptr ERT_vtable; ptr PCS_vtable; void g_Init() { GD = LoadGameConfigFile("yuuko_open_fortress"); CBaseIssue_CBaseIssue_Init(); CFL_vtable = CBaseTFIssue_vtable_Synth(); CFL_Init(); ERT_vtable = CBaseTFIssue_vtable_Synth(); ERT_Init(); PCS_vtable = CBaseTFIssue_vtable_Synth(); PCS_Init(); } void CBaseIssue_CBaseIssue_Init() { StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(GD, SDKConf_Signature, "CBaseIssue::CBaseIssue"); PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); CBaseIssue_CBaseIssue = EndPrepSDKCall(); } ptr CBaseTFIssue_Synth(const char[] name, ptr vtable) { ptr ret = calloc(CChangeMutatorIssue_length, 1); SDKCall(CBaseIssue_CBaseIssue, ret, name); WriteVal(ret, vtable + 4 * CChangeMutatorIssue_D1); return ret; } ptr CBaseTFIssue_vtable_Synth() { ptr ret = calloc(CChangeMutatorIssue_vtable_length, 4); memcpyf(ret, GD.GetMemSig("CChangeMutatorIssue::vtable"), CChangeMutatorIssue_vtable_length * 4); VTABLE_SUB(ret,CChangeMutatorIssue_typeinfo,GD,"CBaseTFIssue::typeinfo"); VTABLE_SUB(ret,CChangeMutatorIssue_D1,GD,"CBaseIssue::D1"); VTABLE_SUB(ret,CChangeMutatorIssue_D0,GD,"CBaseIssue::D0"); VTABLE_SUB(ret,CChangeMutatorIssue_GetDetailsString,GD,"CBaseIssue::GetDetailsString"); VTABLE_SUB(ret,CChangeMutatorIssue_IsEnabled,GD,"CBaseIssue::IsEnabled"); VTABLE_SUB(ret,CChangeMutatorIssue_CanCallVote,GD,"CBaseIssue::CanCallVote"); VTABLE_SUB(ret,CChangeMutatorIssue_ExecuteCommand,GD,"CBaseTFIssue::ExecuteCommand"); VTABLE_SUB(ret,CChangeMutatorIssue_GetVotePassedString,GD,"CBaseTFIssue::GetVotePassedString"); return ret; } // ChangeFraglimit ------------------------------------------------------------- int CFL_VoteOptions_Values[5]; ptr CFL_VoteOptions[5]; ptr CFL_Display; ConVar CFL_Allowed; ConVar CFL_Default; public bool OnClientConnect(int client, char[] rejectmsg, int maxlen) { if (GetClientCount(false) <= 1) FindConVar("mp_fraglimit").IntValue = CFL_Default.IntValue; return true; } void CFL_Init() { for (int i = 0; i < 5; i++) CFL_VoteOptions[i] = CSTRING(" "); CFL_Display = CSTRING("Change Frag Limit?"); CFL_Allowed = CreateConVar("sv_vote_issue_change_fraglimit_allowed", "0", "Can players call votes to change the frag limit?"); CFL_Default = CreateConVar("sm_default_fraglimit", "20", "Default frag limit to reset to when nobody is connected."); AutoExecConfig(); } DH_SIG(CFL_OnVoteStarted)(int pThis) { memcpy(pThis + YUUKO_VOTES_ISSUEDETAILS_OFFSET, CFL_Display, 19); return MRES_Handled; } // client doesn't call ConstructString so we're stuck with as-is l18n strings // here. this one isn't perfect but it's the best of the bunch. DH_SIG(CFL_GetTypeStringLocalized) SIMPLE_STRING("#TF_FragLimit") DH_SIG(CFL_IsEnabled) SIMPLE_SCALAR(CFL_Allowed.IntValue) // snip off any cosmetic suffix we wanted the vote panel to see DH_SIG(CFL_SetIssueDetails)(int pThis) { WriteByte(pThis + YUUKO_VOTES_ISSUEDETAILS_OFFSET + 2, '\0'); return MRES_Handled; } // "%s1" DH_SIG(CFL_GetDisplayString) SIMPLE_STRING(FMTKEY) DH_SIG(CFL_ExecuteCommand)(int pThis, DHookReturn ret) { char buf[0x40]; PtrToString(pThis + YUUKO_VOTES_ISSUEDETAILS_OFFSET, buf, 0x40); ServerCommand("mp_fraglimit %s", buf); return MRES_Supercede; } // "Playing to %s1 frags". this one is actually great! DH_SIG(CFL_GetVotePassedString) SIMPLE_STRING("#TF_ScoreBoard_Fraglimit") DH_SIG(CFL_GetNumberVoteOptions) SIMPLE_SCALAR(5) DH_SIG(CFL_IsYesNoVote) SIMPLE_SCALAR(0) int round10(int i) { return ((i + 5) / 10) * 10; } int max(int a, int b) { return a > b ? a : b; } void CFL_setopts() { int fraglimit = FindConVar("mp_fraglimit").IntValue; int midpoint = round10(fraglimit); if (midpoint == 0) { for (int i = 0; i < 5; i++) CFL_VoteOptions_Values[i] = (i + 1) * 10; return; } int lowerbound = max(10, round10(G_TopFrags_Cell[0] + 5)); int lowdiff = midpoint - lowerbound; if (lowdiff <= 10) { for (int i = 0; i < 5; i++) CFL_VoteOptions_Values[i] = (i + lowerbound / 10) * 10; return; } CFL_VoteOptions_Values[0] = lowerbound; CFL_VoteOptions_Values[1] = lowerbound + lowdiff / 2; CFL_VoteOptions_Values[2] = fraglimit; CFL_VoteOptions_Values[3] = midpoint + lowdiff / 2; CFL_VoteOptions_Values[4] = midpoint + lowdiff; } DH_SIG(CFL_GetVoteOptions)(DHookReturn ret, DHookParam param) { CUtlVector vec = CUtlVector(param.GetAddress(1)); char buf[32]; int fraglimit = FindConVar("mp_fraglimit").IntValue; CFL_setopts(); for (int i = 0; i < 5; i++) { FormatEx(buf, 32, CFL_VoteOptions_Values[i] == fraglimit ? "%d (No change)" : "%d", CFL_VoteOptions_Values[i] ); memcpy(CFL_VoteOptions[i], AddressOfString(buf), 32); vec.AddToTail(CFL_VoteOptions[i]); } ret.Value = 1; return MRES_Supercede; } void CFL_Install() { ptr issue = CBaseTFIssue_Synth("ChangeFraglimit", CFL_vtable); DH CFL_SetIssueDetails = DH_NEW(CBaseIssue_SetIssueDetails,Void,Address); CFL_SetIssueDetails.AddParam(HookParamType_CharPtr); DH_HOOK(CFL_SetIssueDetails,Post,issue); DH CFL_OnVoteStarted = DH_NEW(CBaseIssue_OnVoteStarted,Void,Address); DH_HOOK(CFL_OnVoteStarted,Post,issue); DH CFL_GetTypeStringLocalized = DH_NEW(CBaseIssue_GetTypeStringLocalized,CharPtr,Ignore); DH_HOOK(CFL_GetTypeStringLocalized,Pre,issue); DH CFL_IsEnabled = DH_NEW(CChangeMutatorIssue_IsEnabled,Int,Ignore); DH_HOOK(CFL_IsEnabled,Post,issue); DH CFL_GetDisplayString = DH_NEW(CChangeMutatorIssue_GetDisplayString,CharPtr,Ignore); DH_HOOK(CFL_GetDisplayString,Pre,issue); DH CFL_ExecuteCommand = DH_NEW(CChangeMutatorIssue_ExecuteCommand,Void,Address); DH_HOOK(CFL_ExecuteCommand,Pre,issue); DH CFL_GetVotePassedString = DH_NEW(CChangeMutatorIssue_GetVotePassedString,CharPtr,Ignore); DH_HOOK(CFL_GetVotePassedString,Pre,issue); DH CFL_GetNumberVoteOptions = DH_NEW(CBaseIssue_GetNumberVoteOptions,Int,Ignore); DH_HOOK(CFL_GetNumberVoteOptions,Pre,issue); DH CFL_IsYesNoVote = DH_NEW(CBaseIssue_IsYesNoVote,Int,Ignore); DH_HOOK(CFL_IsYesNoVote,Pre,issue); DH CFL_GetVoteOptions = DH_NEW(CBaseIssue_GetVoteOptions,Int,Ignore); CFL_GetVoteOptions.AddParam(HookParamType_ObjectPtr); DH_HOOK(CFL_GetVoteOptions,Pre,issue); } // ExtendRoundTimer ------------------------------------------------------------ ConVar ERT_Allowed; ptr ERT_Display; ptr ERT_Display2; void ERT_Init() { ERT_Allowed = CreateConVar("sv_vote_issue_extendtimer_allowed", "0", "Can players extend the timer?"); ERT_Display = StringToPtr("Add 10 minutes to the timer?", 35); ERT_Display2 = StringToPtr("Adding 10 minutes...", 21); } // "Max Game Time". yes that is the best we can do DH_SIG(ERT_GetTypeStringLocalized) SIMPLE_STRING("#TF_MatchOption_MaxTime") DH_SIG(ERT_OnVoteStarted)(int pThis) { memcpy(pThis + YUUKO_VOTES_ISSUEDETAILS_OFFSET, ERT_Display, 35); return MRES_Handled; } DH_SIG(ERT_IsEnabled) SIMPLE_SCALAR(ERT_Allowed.IntValue) DH_SIG(ERT_GetDisplayString) SIMPLE_STRING(FMTKEY) DH_SIG(ERT_ExecuteCommand)(int pThis, DHookReturn ret) { if (!ExtendMapTimeLimit(600)) LogError("failed to extend map time limit by vote"); return MRES_Supercede; } DH_SIG(ERT_GetVotePassedString)(int pThis, DHookReturn ret) { memcpy(pThis + YUUKO_VOTES_ISSUEDETAILS_OFFSET, ERT_Display2, 21); ret.SetString(FMTKEY); return MRES_Supercede; } void ERT_Install() { ptr issue = CBaseTFIssue_Synth("ExtendRoundTimer", ERT_vtable); DH ERT_GetTypeStringLocalized = DH_NEW(CBaseIssue_GetTypeStringLocalized,CharPtr,Ignore); DH_HOOK(ERT_GetTypeStringLocalized,Pre,issue); DH ERT_OnVoteStarted = DH_NEW(CBaseIssue_OnVoteStarted,Void,Address); DH_HOOK(ERT_OnVoteStarted,Post,issue); DH ERT_IsEnabled = DH_NEW(CChangeMutatorIssue_IsEnabled,Int,Ignore); DH_HOOK(ERT_IsEnabled,Post,issue); DH ERT_GetDisplayString = DH_NEW(CChangeMutatorIssue_GetDisplayString,CharPtr,Ignore); DH_HOOK(ERT_GetDisplayString,Pre,issue); DH ERT_ExecuteCommand = DH_NEW(CChangeMutatorIssue_ExecuteCommand,Void,Address); DH_HOOK(ERT_ExecuteCommand,Pre,issue); DH ERT_GetVotePassedString = DH_NEW(CChangeMutatorIssue_GetVotePassedString,CharPtr,Address); DH_HOOK(ERT_GetVotePassedString,Pre,issue); } // PlayerCountSurvey ----------------------------------------------------------- char PCS_VoteOptions_Cells[][] = { "Claustrophobic", "Cramped", "Just about right", "A bit empty", "Where's the other guy?" }; ptr PCS_VoteOptions[5]; char PCS_Display_Cell[] = "How well does this map fit the current player count?"; char PCS_Display2_Cell[] = "Thanks for your feedback."; ptr PCS_Display; ptr PCS_Display2; void PCS_Init() { for (int i = 0; i < 5; i++) PCS_VoteOptions[i] = CSTRING(PCS_VoteOptions_Cells[i]); PCS_Display = CSTRING(PCS_Display_Cell); PCS_Display2 = CSTRING(PCS_Display2_Cell); } DH_SIG(PCS_OnVoteStarted)(int pThis) { memcpy(pThis + YUUKO_VOTES_ISSUEDETAILS_OFFSET, PCS_Display, CPYLEN(PCS_Display_Cell)); return MRES_Handled; } // for internal use only. DH_SIG(PCS_IsEnabled) SIMPLE_SCALAR(0) DH_SIG(PCS_GetDisplayString) SIMPLE_STRING(FMTKEY) DH_SIG(PCS_ExecuteCommand)() { ServerCommand("say penis lol"); return MRES_Supercede; } DH_SIG(PCS_GetVotePassedString)(int pThis, DHookReturn ret) { memcpy(pThis + YUUKO_VOTES_ISSUEDETAILS_OFFSET, PCS_Display2, CPYLEN(PCS_Display2_Cell)); ret.SetString(FMTKEY); return MRES_Supercede; } DH_SIG(PCS_GetNumberVoteOptions) SIMPLE_SCALAR(5) DH_SIG(PCS_IsYesNoVote) SIMPLE_SCALAR(0) DH_SIG(PCS_GetVoteOptions)(DHookReturn ret, DHookParam param) { CUtlVector vec = CUtlVector(param.GetAddress(1)); for (int i = 0; i < 5; i++) vec.AddToTail(PCS_VoteOptions[i]); ret.Value = 1; return MRES_Supercede; } DH_SIG(PCS_GetQuorumRatio) SIMPLE_SCALAR(0.0) void PCS_Install() { ptr issue = CBaseTFIssue_Synth("PlayerCountSurvey", PCS_vtable); DH PCS_OnVoteStarted = DH_NEW(CBaseIssue_OnVoteStarted,Void,Address); DH_HOOK(PCS_OnVoteStarted,Post,issue); DH PCS_IsEnabled = DH_NEW(CChangeMutatorIssue_IsEnabled,Int,Ignore); DH_HOOK(PCS_IsEnabled,Post,issue); DH PCS_GetDisplayString = DH_NEW(CChangeMutatorIssue_GetDisplayString,CharPtr,Ignore); DH_HOOK(PCS_GetDisplayString,Pre,issue); DH PCS_ExecuteCommand = DH_NEW(CChangeMutatorIssue_ExecuteCommand,Void,Address); DH_HOOK(PCS_ExecuteCommand,Pre,issue); DH PCS_GetVotePassedString = DH_NEW(CChangeMutatorIssue_GetVotePassedString,CharPtr,Address); DH_HOOK(PCS_GetVotePassedString,Pre,issue); DH PCS_GetNumberVoteOptions = DH_NEW(CBaseIssue_GetNumberVoteOptions,Int,Ignore); DH_HOOK(PCS_GetNumberVoteOptions,Pre,issue); DH PCS_IsYesNoVote = DH_NEW(CBaseIssue_IsYesNoVote,Int,Ignore); DH_HOOK(PCS_IsYesNoVote,Pre,issue); DH PCS_GetVoteOptions = DH_NEW(CBaseIssue_GetVoteOptions,Int,Ignore); PCS_GetVoteOptions.AddParam(HookParamType_ObjectPtr); DH_HOOK(PCS_GetVoteOptions,Pre,issue); DH PCS_GetQuorumRatio = DH_NEW(CBaseIssue_GetQuorumRatio,Float,Ignore); DH_HOOK(PCS_GetQuorumRatio,Pre,issue); } // kill tracking --------------------------------------------------------------- MRESReturn UpdateTopFrags(int pThis) { memcpy(AddressOfArray(G_TopFrags_Cell), pThis + 0x908, 4); LogError("New Top Frags: %d", G_TopFrags_Cell[0]); return MRES_Handled; } // entry point ----------------------------------------------------------------- MRESReturn Issues_Install() { G_TopFrags_Cell[0] = 0; CFL_Install(); ERT_Install(); PCS_Install(); return MRES_Handled; } public void OnPluginStart() { g_Init(); DynamicDetour dKilled = new DynamicDetour(Address_Null, CallConv_THISCALL, ReturnType_Void, ThisPointer_Address); dKilled.SetFromConf(GD, SDKConf_Signature, "CTFGameRules::PlayerKilled"); dKilled.Enable(Hook_Post, UpdateTopFrags); DynamicDetour dCreate = new DynamicDetour(Address_Null, CallConv_CDECL, ReturnType_Void); dCreate.SetFromConf(GD, SDKConf_Signature, "CTFGameRules::CreateStandardEntities"); dCreate.Enable(Hook_Post, Issues_Install); } // resign to leaking a few vtable allocations here and there for the time being. // there are two different lifecycles: // - plugin gets unloaded, then map changes // - in OnPluginEnd just make our hand-allocated objects point to the old // static vtables again, then free them. // - map changes while plugin is loaded // - have destructors take care of freeing allocs respective to each object // both of these require manually undoing our hooks before free, so native // cleanup doesn't try to "restore" the contents of freed regions. // latter case presents a bootstrapping problem wrt unhooking the destructor. // // two paths to becoming a good citizen: // - figure out exactly how dhooks/sourcehook treat vtables and allocations. // might turn out we need to execute something post-cleanup which we don't // seem to be able to do? // - do away with dhooks for everything but the entry point detour and just // point to assembly. easy for the getters, annoying for complex stuff like // GetVoteOptions. // latter seems preferable eventually; in any case let's just get the server // running for now. it would take a stupid amount of uptime to start leaking // megs. // // public void OnPluginEnd() // { // do something? // }