aboutsummaryrefslogtreecommitdiff
path: root/scripting/yuuko_votes.sp
diff options
context:
space:
mode:
Diffstat (limited to 'scripting/yuuko_votes.sp')
-rw-r--r--scripting/yuuko_votes.sp506
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