diff options
-rw-r--r-- | README.md | 86 | ||||
-rw-r--r-- | gamedata/yuuko_open_fortress.txt | 65 | ||||
-rw-r--r-- | scripting/include/yuuko_votes/of19.inc | 1 | ||||
-rw-r--r-- | scripting/include/yuuko_votes/of21.inc | 1 | ||||
-rw-r--r-- | scripting/yuuko_callvote_fix.sp | 46 | ||||
-rw-r--r-- | scripting/yuuko_fake_rtv.sp | 31 | ||||
-rw-r--r-- | scripting/yuuko_maptimer.sp | 81 | ||||
-rw-r--r-- | scripting/yuuko_votes.sp | 506 |
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 |