aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoryuuko <yuuko@partyvan.io>2024-08-29 19:43:58 -0700
committeryuuko <yuuko@partyvan.io>2024-08-29 19:43:58 -0700
commitb4651d5aa991c25467077838b082fa73ee6921d0 (patch)
treed75e40fca46fe86207e57952b53b9faf88fd1c45
dress up for public release
-rw-r--r--README.md86
-rw-r--r--gamedata/yuuko_open_fortress.txt65
-rw-r--r--scripting/include/yuuko_votes/of19.inc1
-rw-r--r--scripting/include/yuuko_votes/of21.inc1
-rw-r--r--scripting/yuuko_callvote_fix.sp46
-rw-r--r--scripting/yuuko_fake_rtv.sp31
-rw-r--r--scripting/yuuko_maptimer.sp81
-rw-r--r--scripting/yuuko_votes.sp506
8 files changed, 817 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f1791bc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+### WARNING: THIS IS EXTREMELY HORRIBLE AND BAD
+
+you know how in open source licenses there's that bit that goes like
+
+> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+> SOFTWARE.
+
+yea
+
+* leaks a little bit of memory every map load (as a treat)
+* is almost guaranteed to break with every open fortress revision
+* locks you into certain server settings lest there be jank
+
+anyway with that said
+
+yuuko_votes
+===========
+
+* `yuuko_votes`, the headline item. yuuko's custom open fortress votes, that
+ integrate into the native vote menu.
+ * `sv_vote_issue_change_fraglimit_allowed`
+ * presents 5 values, with the smallest guaranteed to be current fraglimit
+ rounded to the next highest 10, so the vote can never terminate the
+ match immediately upon success.
+ * when running this plugin, you need to set a new convar called
+ `sm_default_fraglimit` instead of `mp_fraglimit`. this is down to init
+ order but basically i couldn't find a good place to set `mp_fraglimit`
+ that doesn't get run on every map load, which makes the voted-on value
+ fail to persist across map loads. however, there's also a hook in this
+ plugin that resets `mp_fraglimit` *to* `sm_default_fraglimit` whenever
+ the server goes from 0 to 1 gamers, so that players aren't blindsided by
+ something insane like 10 or 90 frags the morning after. anyway long story
+ short is this plugin takes complete ownership of `mp_fraglimit` dealwithit
+ * `sv_vote_issue_extendtimer_allowed`
+ * simple yes/no vote to tack 10 minutes onto the *current* timer, i.e. it
+ does not persist
+ * you probably want `mp_maxrounds{,_ffa}` set to 1 if you enable this.
+
+* `yuuko_callvote_fix`, lets the server console initiate votes. by default
+ open fortress is hardcoded to forbid this. you probably do not need this
+ as it was written for another experiment (cf. PlayerCountSurvey in
+ `yuuko_votes.sp`)
+
+* `yuuko_maptimer`, lets you mess with the map timer from the console. was
+ written for debugging purposes but hey have fun go nuts
+
+* `yuuko_fake_rtv`, `yuuko_votes` used to implement a native rtv but open
+ fortress just has one of those now! this just whispers back to anyone who
+ types rtv in chat that they can use the vote menu instead.
+
+building
+--------
+
+requires [SM-Memory], then something like:
+
+ mkdir plugins
+ # example selection
+ for plug in yuuko_votes yuuko_fake_rtv; do
+ spcomp \
+ -i /path/to/SM-Memory/pawn/sourcemod/scripting/include \
+ $plug.sp \
+ -o plugins/$plug.smx
+ done
+
+installing
+----------
+
+merge `plugins` and `gamedata` over your production sourcemod install
+
+thanks, and have fun
+
+license
+-------
+snore zzzzzz honk shooo mimimimimimimi
+https://svn.alliedmods.net/viewvc.cgi/trunk/public/licenses/LICENSE.txt?revision=2255&root=sourcemod
+
+they technically have no grounds to mandate this but gpl linking
+""""violations"""" have resulted in out-of-court settlements and i dont want
+any smoke so w/e.
+
+[SM-Memory]: https://github.com/Scags/SM-Memory
diff --git a/gamedata/yuuko_open_fortress.txt b/gamedata/yuuko_open_fortress.txt
new file mode 100644
index 0000000..a46e7ee
--- /dev/null
+++ b/gamedata/yuuko_open_fortress.txt
@@ -0,0 +1,65 @@
+"Games" {
+ "open_fortress" {
+ "Signatures" {
+ "CTFGameRules::CreateStandardEntities" {
+ "library" "server"
+ "linux" "@_ZN12CTFGameRules22CreateStandardEntitiesEv"
+ }
+ "CTFGameRules::PlayerKilled" {
+ "library" "server"
+ "linux" "@_ZN12CTFGameRules12PlayerKilledEP11CBasePlayerRK15CTakeDamageInfo"
+ }
+ "CBaseIssue::CBaseIssue" {
+ "library" "server"
+ "linux" "@_ZN10CBaseIssueC1EPKc"
+ }
+ "CChangeMutatorIssue::vtable" {
+ "library" "server"
+ "linux" "@_ZTV19CChangeMutatorIssue"
+ }
+ "CBaseTFIssue::typeinfo" {
+ "library" "server"
+ "linux" "@_ZTI12CBaseTFIssue"
+ }
+ "CBaseIssue::D1" {
+ "library" "server"
+ "linux" "@_ZN10CBaseIssueD1Ev"
+ }
+ "CBaseIssue::D0" {
+ "library" "server"
+ "linux" "@_ZN10CBaseIssueD0Ev"
+ }
+ "CBaseIssue::GetDetailsString" {
+ "library" "server"
+ "linux" "@_ZN10CBaseIssue16GetDetailsStringEv"
+ }
+ "CBaseIssue::IsEnabled" {
+ "library" "server"
+ "linux" "@_ZN10CBaseIssue9IsEnabledEv"
+ }
+ "CBaseIssue::CanCallVote" {
+ "library" "server"
+ "linux" "@_ZN10CBaseIssue11CanCallVoteEiPKcR20vote_create_failed_tRi"
+ }
+ // GetDisplayString only has concrete impls!!
+ "CBaseTFIssue::ExecuteCommand" {
+ "library" "server"
+ "linux" "@_ZN12CBaseTFIssue14ExecuteCommandEv"
+ }
+ "CBaseIssue::GetVotePassedString" {
+ "library" "server"
+ "linux" "@_ZN10CBaseIssue19GetVotePassedStringEv"
+ }
+ "CBaseTFIssue::vtable" {
+ "library" "server"
+ "linux" "@_ZTV12CBaseTFIssue"
+ }
+ "CVoteController::CreateVote" {
+ "library" "server"
+ "linux" "@_ZN15CVoteController10CreateVoteEiPKcS1_"
+ }
+ }
+ }
+}
+
+ \ No newline at end of file
diff --git a/scripting/include/yuuko_votes/of19.inc b/scripting/include/yuuko_votes/of19.inc
new file mode 100644
index 0000000..c0aed9d
--- /dev/null
+++ b/scripting/include/yuuko_votes/of19.inc
@@ -0,0 +1 @@
+#define YUUKO_VOTES_ISSUEDETAILS_OFFSET 100
diff --git a/scripting/include/yuuko_votes/of21.inc b/scripting/include/yuuko_votes/of21.inc
new file mode 100644
index 0000000..15e6c1d
--- /dev/null
+++ b/scripting/include/yuuko_votes/of21.inc
@@ -0,0 +1 @@
+#define YUUKO_VOTES_ISSUEDETAILS_OFFSET 108
diff --git a/scripting/yuuko_callvote_fix.sp b/scripting/yuuko_callvote_fix.sp
new file mode 100644
index 0000000..17d4137
--- /dev/null
+++ b/scripting/yuuko_callvote_fix.sp
@@ -0,0 +1,46 @@
+#include <sourcemod>
+#include <sdktools>
+
+public Plugin myinfo = {
+ name = "yuuko's callvote fix",
+ author = "yuuko",
+ description = "makes the open fortress callvote command console-accessible",
+ version = SOURCEMOD_VERSION,
+ url = "https://www.partyvan.io/"
+};
+
+Handle CVoteController_CreateVote;
+int vote_controller_id;
+
+// OF's callvote unceremoniously returns early if run by the dedicated server.
+// we need to fix that if our server-only votes are to be worth anything.
+Action callvote_Callback(int client, const char[] command, int argc)
+{
+ if (strcmp(command, "callvote") != 0 || client != 0)
+ return Plugin_Continue;
+ char buf1[128], buf2[128];
+ if (argc > 0)
+ GetCmdArg(1, buf1, 128);
+ if (argc > 1)
+ GetCmdArg(2, buf2, 128);
+ SDKCall(CVoteController_CreateVote, vote_controller_id, 129, buf1, buf2);
+ return Plugin_Handled;
+}
+
+public void OnPluginStart()
+{
+ {
+ GameData GD = LoadGameConfigFile("yuuko_open_fortress");
+
+ StartPrepSDKCall(SDKCall_Entity);
+ PrepSDKCall_SetFromConf(GD, SDKConf_Signature, "CVoteController::CreateVote");
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
+ PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
+
+ CVoteController_CreateVote = EndPrepSDKCall();
+ vote_controller_id = FindEntityByClassname(-1, "vote_controller");
+ }
+ AddCommandListener(callvote_Callback, "callvote");
+} \ No newline at end of file
diff --git a/scripting/yuuko_fake_rtv.sp b/scripting/yuuko_fake_rtv.sp
new file mode 100644
index 0000000..23628ac
--- /dev/null
+++ b/scripting/yuuko_fake_rtv.sp
@@ -0,0 +1,31 @@
+#include <sourcemod>
+#include <console>
+#include <timers>
+
+public Plugin myinfo = {
+ name = "yuuko's fake rtv",
+ author = "yuuko",
+ description = "stubs out rtv to tell players they can vote for it instead",
+ version = SOURCEMOD_VERSION,
+ url = "https://www.partyvan.io/"
+};
+
+public Action Command_FakeRTV(int client, int args)
+{
+ PrintToChat(client, "NOTICE: Open Fortress has a native equivalent to RTV as of Revision 20; see \"End map and start vote\" in the vote menu.");
+ return Plugin_Handled;
+}
+
+public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
+{
+ if (strcmp(sArgs, "rtv", false) == 0 || strcmp(sArgs, "rockthevote", false) == 0)
+ return Command_FakeRTV(client, 0);
+ return Plugin_Continue;
+}
+
+public void OnPluginStart()
+{
+ RegConsoleCmd("sm_rtv", Command_FakeRTV, "rock the vote (not really)");
+ RegConsoleCmd("sm_rockthevote", Command_FakeRTV, "rock the vote (not really)");
+}
+
diff --git a/scripting/yuuko_maptimer.sp b/scripting/yuuko_maptimer.sp
new file mode 100644
index 0000000..a90222f
--- /dev/null
+++ b/scripting/yuuko_maptimer.sp
@@ -0,0 +1,81 @@
+#include <sourcemod>
+#include <console>
+#include <timers>
+
+public Plugin myinfo = {
+ name = "yuuko's map timer commands",
+ author = "yuuko",
+ description = "exposes the map-related bits of <timers> to the console",
+ version = SOURCEMOD_VERSION,
+ url = "https://www.partyvan.io/"
+};
+
+public Action Command_ExtendMapTimeLimit(int client, int args)
+{
+ if (args != 1) {
+ PrintToConsole(client, "Usage: sm_extendmaptimelimit <seconds>");
+ return Plugin_Handled;
+ }
+ int seconds = 0;
+ if (!GetCmdArgIntEx(1, seconds)) {
+ PrintToConsole(client, "argument must be an integer");
+ return Plugin_Handled;
+ }
+ if (!ExtendMapTimeLimit(seconds)) {
+ PrintToConsole(client, "operation not supported");
+ return Plugin_Handled;
+ }
+ PrintToConsole(client, "map time limit extended");
+ return Plugin_Handled;
+}
+
+public Action Command_GetMapTimeLimit(int client, int args)
+{
+ if (args != 0) {
+ PrintToConsole(client, "Usage: sm_getmaptimelimit");
+ return Plugin_Handled;
+ }
+ int minutes = 0;
+ if (!GetMapTimeLimit(minutes)) {
+ PrintToConsole(client, "operation not supported");
+ return Plugin_Handled;
+ }
+ PrintToConsole(client, "%d", minutes);
+ return Plugin_Handled;
+}
+
+public Action Command_GetMapTimeLeft(int client, int args)
+{
+ if (args != 0) {
+ PrintToConsole(client, "Usage: sm_getmaptimeleft");
+ return Plugin_Handled;
+ }
+ int seconds = 0;
+ if (!GetMapTimeLeft(seconds)) {
+ PrintToConsole(client, "operation not supported");
+ return Plugin_Handled;
+ }
+ PrintToConsole(client, "%d", seconds);
+ return Plugin_Handled;
+}
+
+public void OnPluginStart()
+{
+ RegAdminCmd(
+ "sm_extendmaptimelimit",
+ Command_ExtendMapTimeLimit,
+ ADMFLAG_CHANGEMAP,
+ "extend map time limit by argv[1] seconds"
+ );
+ RegConsoleCmd(
+ "sm_getmaptimelimit",
+ Command_GetMapTimeLimit,
+ "get map time limit, in minutes for some reason (api docs are a lie)"
+ );
+ RegConsoleCmd(
+ "sm_getmaptimeleft",
+ Command_GetMapTimeLeft,
+ "get (approximate) time left out of map time limit, in seconds"
+ );
+}
+
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