diff options
Diffstat (limited to 'scripting/yuuko_votes.sp')
-rw-r--r-- | scripting/yuuko_votes.sp | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/scripting/yuuko_votes.sp b/scripting/yuuko_votes.sp new file mode 100644 index 0000000..4499d46 --- /dev/null +++ b/scripting/yuuko_votes.sp @@ -0,0 +1,506 @@ +// 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 <sourcemod> +#include <dhooks> +#include <smmem> +#include <timers> + +#include <yuuko_votes/of21> + +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? +// }
\ No newline at end of file |