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);