From f488920e76473af78970de63a7e43372c6424004 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 16 Jun 2026 11:33:14 +0100 Subject: [PATCH] feat(IT4 MP): add `unlockstats` command --- codxe.vcxproj | 2 + src/game/t4/mp/components/events.cpp | 25 +++ src/game/t4/mp/components/events.h | 5 + src/game/t4/mp/components/stats.cpp | 240 +++++++++++++++++++++++++++ src/game/t4/mp/components/stats.h | 21 +++ src/game/t4/mp/main.cpp | 2 + src/game/t4/mp/structs.h | 21 ++- src/game/t4/mp/symbols.h | 9 + 8 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 src/game/t4/mp/components/stats.cpp create mode 100644 src/game/t4/mp/components/stats.h diff --git a/codxe.vcxproj b/codxe.vcxproj index 574163c..59dc162 100644 --- a/codxe.vcxproj +++ b/codxe.vcxproj @@ -161,6 +161,7 @@ + @@ -284,6 +285,7 @@ + diff --git a/src/game/t4/mp/components/events.cpp b/src/game/t4/mp/components/events.cpp index 13cd812..9c13484 100644 --- a/src/game/t4/mp/components/events.cpp +++ b/src/game/t4/mp/components/events.cpp @@ -7,6 +7,7 @@ namespace mp { std::vector> Events::dvarinit_callbacks; +std::vector> Events::cmdinit_callbacks; std::vector> Events::vmshutdown_callbacks; std::vector> Events::ui_refresh_callbacks; @@ -29,6 +30,25 @@ void Events::OnDvarInit(const std::function &callback) Detour Events::Com_InitDvars_Detour; +void Events::Cmd_Init_Hook() +{ + Cmd_Init_Detour.GetOriginal()(); + + for (auto it = cmdinit_callbacks.begin(); it != cmdinit_callbacks.end(); ++it) + { + (*it)(); + } + + cmdinit_callbacks.clear(); +} + +void Events::OnCmdInit(const std::function &callback) +{ + cmdinit_callbacks.emplace_back(callback); +} + +Detour Events::Cmd_Init_Detour; + void *Events::Scr_ShutdownSystem_Hook(scriptInstance_t inst, int sys, int bComplete) { for (auto it = vmshutdown_callbacks.begin(); it != vmshutdown_callbacks.end(); ++it) @@ -70,6 +90,9 @@ Events::Events() Com_InitDvars_Detour = Detour(Com_InitDvars, Com_InitDvars_Hook); Com_InitDvars_Detour.Install(); + Cmd_Init_Detour = Detour(Cmd_Init, Cmd_Init_Hook); + Cmd_Init_Detour.Install(); + Scr_ShutdownSystem_Detour = Detour(Scr_ShutdownSystem, Scr_ShutdownSystem_Hook); Scr_ShutdownSystem_Detour.Install(); @@ -80,10 +103,12 @@ Events::Events() Events::~Events() { Com_InitDvars_Detour.Remove(); + Cmd_Init_Detour.Remove(); Scr_ShutdownSystem_Detour.Remove(); UI_Refresh_Detour.Remove(); dvarinit_callbacks.clear(); + cmdinit_callbacks.clear(); vmshutdown_callbacks.clear(); ui_refresh_callbacks.clear(); } diff --git a/src/game/t4/mp/components/events.h b/src/game/t4/mp/components/events.h index 54773df..e8e6141 100644 --- a/src/game/t4/mp/components/events.h +++ b/src/game/t4/mp/components/events.h @@ -18,6 +18,7 @@ class Events : public Module }; static void OnDvarInit(const std::function &callback); + static void OnCmdInit(const std::function &callback); static void OnVMShutdown(const std::function &callback); static void OnUIRefresh(const std::function &callback); @@ -26,6 +27,10 @@ class Events : public Module static Detour Com_InitDvars_Detour; static void Com_InitDvars_Hook(); + static std::vector> cmdinit_callbacks; + static Detour Cmd_Init_Detour; + static void Cmd_Init_Hook(); + static std::vector> vmshutdown_callbacks; static Detour Scr_ShutdownSystem_Detour; static void *Scr_ShutdownSystem_Hook(scriptInstance_t inst, int sys, int bComplete); diff --git a/src/game/t4/mp/components/stats.cpp b/src/game/t4/mp/components/stats.cpp new file mode 100644 index 0000000..a8b0e8e --- /dev/null +++ b/src/game/t4/mp/components/stats.cpp @@ -0,0 +1,240 @@ +#include "pch.h" +#include +#include +#include "events.h" +#include "stats.h" + +namespace t4 +{ +namespace mp +{ +namespace +{ +cmd_function_s Cmd_UnlockStats_VAR; + +const char *TableLookup(const StringTable *table, int row, int column) +{ + if (!table || row < 0 || column < 0 || row >= table->rowCount || column >= table->columnCount || !table->values) + { + return ""; + } + + const char *value = table->values[row * table->columnCount + column]; + return value ? value : ""; +} + +bool TryParseInt(const char *text, int *out) +{ + if (!text || !*text) + { + return false; + } + + const char *p = text; + if (*p == '-') + { + ++p; + } + + if (*p < '0' || *p > '9') + { + return false; + } + + *out = atoi(text); + return true; +} + +bool HasText(const char *text) +{ + return text && *text; +} + +std::string ToLower(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), + [](char c) { return static_cast(std::tolower(static_cast(c))); }); + return value; +} + +const StringTable *FindStringTable(const char *name) +{ + StringTable *table = nullptr; + if (StringTable_GetAsset(name, &table) && table) + { + return table; + } + + const std::string lowerName = ToLower(name); + if (StringTable_GetAsset(lowerName.c_str(), &table) && table) + { + return table; + } + + return nullptr; +} + +void ExecuteCommand(const char *command) +{ + char commandLine[1024] = {}; + const size_t commandLength = std::strlen(command); + + if (commandLength >= sizeof(commandLine) - 1) + { + Com_Printf(0, "unlockstats: command too long: %s\n", command); + return; + } + + std::memcpy(commandLine, command, commandLength); + commandLine[commandLength] = '\n'; + + Cbuf_ExecuteBuffer(0, 0, commandLine); +} + +void StatSet(int stat, int value) +{ + ExecuteCommand(va(const_cast("statset %i %i"), stat, value)); +} + +void UnlockRank() +{ + const StringTable *rankTable = FindStringTable("mp/ranktable.csv"); + int maxRank = 64; + int minXp = 148680; + int maxXp = 153950; + + if (rankTable) + { + for (int row = 1; row < rankTable->rowCount; ++row) + { + if (std::strcmp(TableLookup(rankTable, row, 0), "maxrank") == 0) + { + TryParseInt(TableLookup(rankTable, row, 1), &maxRank); + continue; + } + + int rank = 0; + int xp = 0; + if (TryParseInt(TableLookup(rankTable, row, 0), &rank) && + TryParseInt(TableLookup(rankTable, row, 7), &xp) && xp >= maxXp) + { + maxRank = rank; + TryParseInt(TableLookup(rankTable, row, 2), &minXp); + maxXp = xp; + } + } + } + + StatSet(2301, maxXp); + StatSet(2326, 10); + StatSet(2350, maxRank); + StatSet(2351, minXp); + StatSet(2352, maxXp); + StatSet(2353, maxXp); + StatSet(251, maxRank); + StatSet(252, maxRank); +} + +void FlushChallengeGroup(int stateStat, int progressStat, int maxProgress, int &challengeCount) +{ + if (stateStat <= 0 || progressStat <= 0) + { + return; + } + + // GSC treats 255 as a completed challenge state; 1..n are active tiers. + StatSet(stateStat, 255); + StatSet(progressStat, maxProgress); + ++challengeCount; +} + +void UnlockChallengeTable(const StringTable *challengeTable, int &challengeCount) +{ + int stateStat = 0; + int progressStat = 0; + int maxProgress = 0; + + for (int row = 1; row < challengeTable->rowCount; ++row) + { + int newStateStat = 0; + if (TryParseInt(TableLookup(challengeTable, row, 2), &newStateStat)) + { + FlushChallengeGroup(stateStat, progressStat, maxProgress, challengeCount); + + stateStat = newStateStat; + TryParseInt(TableLookup(challengeTable, row, 3), &progressStat); + maxProgress = 0; + } + + if (stateStat <= 0) + { + continue; + } + + int target = 0; + if (TryParseInt(TableLookup(challengeTable, row, 4), &target) && target > maxProgress) + { + maxProgress = target; + } + } + + FlushChallengeGroup(stateStat, progressStat, maxProgress, challengeCount); +} + +void UnlockChallenges(int &challengeCount) +{ + const StringTable *challengeList = FindStringTable("mp/challengetable.csv"); + if (!challengeList) + { + Com_Printf(0, "unlockstats: mp/challengetable.csv not found\n"); + return; + } + + for (int row = 1; row < challengeList->rowCount; ++row) + { + const char *challengeTableName = TableLookup(challengeList, row, 4); + if (!HasText(challengeTableName)) + { + continue; + } + + const StringTable *challengeTable = FindStringTable(challengeTableName); + if (!challengeTable) + { + Com_Printf(0, "unlockstats: %s not found\n", challengeTableName); + continue; + } + + UnlockChallengeTable(challengeTable, challengeCount); + } +} + +void Cmd_UnlockStats_f() +{ + int challengeCount = 0; + + ExecuteCommand("exec mp/unlock_init.cfg"); + UnlockRank(); + UnlockChallenges(challengeCount); + ExecuteCommand("updategamerprofile"); + + Com_Printf(0, "unlockstats: queued stats and %i challenge groups\n", challengeCount); +} + +void RegisterCommands() +{ + Cmd_AddCommandInternal("unlockstats", Cmd_UnlockStats_f, &Cmd_UnlockStats_VAR); +} +} // namespace + +stats::stats() +{ + Events::OnCmdInit(RegisterCommands); +} + +stats::~stats() +{ +} + +} // namespace mp +} // namespace t4 diff --git a/src/game/t4/mp/components/stats.h b/src/game/t4/mp/components/stats.h new file mode 100644 index 0000000..1e9d027 --- /dev/null +++ b/src/game/t4/mp/components/stats.h @@ -0,0 +1,21 @@ +#pragma once + +#include "pch.h" + +namespace t4 +{ +namespace mp +{ +class stats : public Module +{ + public: + stats(); + ~stats(); + + const char *get_name() override + { + return "stats"; + } +}; +} // namespace mp +} // namespace t4 diff --git a/src/game/t4/mp/main.cpp b/src/game/t4/mp/main.cpp index 5e30de3..39a0ca1 100644 --- a/src/game/t4/mp/main.cpp +++ b/src/game/t4/mp/main.cpp @@ -11,6 +11,7 @@ #include "components/image_loader.h" #include "components/map.h" #include "components/patches.h" +#include "components/stats.h" #include "components/sv_bots.h" #include "components/ui.h" @@ -34,6 +35,7 @@ T4_MP_Plugin::T4_MP_Plugin() // RegisterModule(new ImageLoader()); RegisterModule(new Map()); RegisterModule(new Patches()); + RegisterModule(new stats()); RegisterModule(new ui()); } diff --git a/src/game/t4/mp/structs.h b/src/game/t4/mp/structs.h index c8fc25e..a04ebc3 100644 --- a/src/game/t4/mp/structs.h +++ b/src/game/t4/mp/structs.h @@ -12,6 +12,8 @@ enum XAssetType : __int32 { ASSET_TYPE_IMAGE = 0x9, ASSET_TYPE_FONT = 0x15, + ASSET_TYPE_RAWFILE = 0x21, + ASSET_TYPE_STRINGTABLE = 0x22, }; enum MapType : __int32 @@ -73,6 +75,23 @@ struct RawFile const char *buffer; }; +struct StringTable +{ + const char *name; + int columnCount; + int rowCount; + const char **values; +}; + +struct cmd_function_s +{ + cmd_function_s *next; + const char *name; + const char *autoCompleteDir; + const char *autoCompleteExt; + void (*function)(); +}; + struct Material; struct Glyph @@ -142,7 +161,7 @@ union XAssetHeader // const FxEffectDef *fx; // FxImpactTable *impactFx; RawFile *rawfile; - // StringTable *stringTable; + StringTable *stringTable; void *data; }; diff --git a/src/game/t4/mp/symbols.h b/src/game/t4/mp/symbols.h index d11aec2..605df41 100644 --- a/src/game/t4/mp/symbols.h +++ b/src/game/t4/mp/symbols.h @@ -62,6 +62,12 @@ static auto Cbuf_ExecuteBuffer = typedef void (*Cbuf_AddText_t)(int localClientNum, const char *text); static Cbuf_AddText_t Cbuf_AddText = reinterpret_cast(0x8226FF08); +typedef void (*Cmd_AddCommandInternal_t)(const char *cmdName, void (*function)(), cmd_function_s *allocedCmd); +static Cmd_AddCommandInternal_t Cmd_AddCommandInternal = reinterpret_cast(0x822708D0); + +typedef void (*Cmd_Init_t)(); +static Cmd_Init_t Cmd_Init = reinterpret_cast(0x82271570); + static auto CL_WritePacket = reinterpret_cast(0x821B0F30); typedef void (*Com_Printf_t)(int channel, const char *fmt, ...); @@ -149,6 +155,9 @@ static Scr_ShutdownSystem_t Scr_ShutdownSystem = reinterpret_cast(0x822A8680); +typedef bool (*StringTable_GetAsset_t)(const char *filename, StringTable **tablePtr); +static StringTable_GetAsset_t StringTable_GetAsset = reinterpret_cast(0x822BB280); + static auto SV_ClientThink = reinterpret_cast(0x82284D50); typedef void (*SV_BotUserMove_t)(client_t *cl); static SV_BotUserMove_t SV_BotUserMove = reinterpret_cast(0x8228AB98);