Feature/set get stat (#485)

* [Script]: GetStat & SetStat from CoD4(x)

* [Stats] Remove limitation

* [Bullet] Clean this up
This commit is contained in:
Edo 2022-09-13 20:39:45 +02:00 committed by GitHub
parent be1bc5fd10
commit e49194ff65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 185 additions and 43 deletions

View File

@ -20,7 +20,7 @@ namespace Components
}
// Game's code
if (surfaceType != Game::materialSurfType_t::SURF_TYPE_DEFAULT)
if (surfaceType != Game::SURF_TYPE_DEFAULT)
{
return (*Game::penetrationDepthTable)[weapDef->penetrateType][surfaceType];
}

View File

@ -272,18 +272,5 @@ namespace Components
// clanName in CG_Obituary
Utils::Hook(0x586DD6, PlayerName::GetClientName, HOOK_CALL).install()->quick();
Utils::Hook(0x586E2A, PlayerName::GetClientName, HOOK_CALL).install()->quick();
Command::Add("statGet", [](Command::Params* params)
{
if (params->size() < 2)
{
Logger::PrintError(Game::CON_CHANNEL_SERVER, "statget usage: statget <index>\n");
return;
}
const auto index = std::atoi(params->get(1));
const auto stat = Game::LiveStorage_GetStat(0, index);
Logger::Print(Game::CON_CHANNEL_SYSTEM, "Stat {}: {}\n", index, stat);
});
}
}

View File

@ -246,7 +246,7 @@ namespace Components
void ScriptExtension::AddMethods()
{
// ScriptExtension methods
Script::AddMethod("GetIp", [](Game::scr_entref_t entref) // gsc: self GetIp()
Script::AddMethod("GetIp", [](const Game::scr_entref_t entref) // gsc: self GetIp()
{
const auto* ent = Game::GetPlayerEntity(entref);
const auto* client = Script::GetClient(ent);
@ -259,7 +259,7 @@ namespace Components
Game::Scr_AddString(ip.data());
});
Script::AddMethod("GetPing", [](Game::scr_entref_t entref) // gsc: self GetPing()
Script::AddMethod("GetPing", [](const Game::scr_entref_t entref) // gsc: self GetPing()
{
const auto* ent = Game::GetPlayerEntity(entref);
const auto* client = Script::GetClient(ent);
@ -267,7 +267,7 @@ namespace Components
Game::Scr_AddInt(client->ping);
});
Script::AddMethod("SetPing", [](Game::scr_entref_t entref) // gsc: self SetPing(<int>)
Script::AddMethod("SetPing", [](const Game::scr_entref_t entref) // gsc: self SetPing(<int>)
{
auto ping = Game::Scr_GetInt(0);
@ -306,7 +306,7 @@ namespace Components
void ScriptExtension::AddEntityFields()
{
AddEntityField("entityflags", Game::fieldtype_t::F_INT,
AddEntityField("entityflags", Game::F_INT,
[](Game::gentity_s* ent, [[maybe_unused]] int offset)
{
ent->flags = Game::Scr_GetInt(0);
@ -319,7 +319,7 @@ namespace Components
void ScriptExtension::AddClientFields()
{
AddClientField("clientflags", Game::fieldtype_t::F_INT,
AddClientField("clientflags", Game::F_INT,
[](Game::gclient_s* pSelf, [[maybe_unused]] const Game::client_fields_s* pField)
{
pSelf->flags = Game::Scr_GetInt(0);

View File

@ -2,18 +2,18 @@
namespace Components
{
std::unordered_map<std::int32_t, std::function<bool(Command::Params*)>> ServerCommands::Commands;
std::unordered_map<std::int32_t, ServerCommands::serverCommandHandler> ServerCommands::Commands;
void ServerCommands::OnCommand(std::int32_t cmd, std::function<bool(Command::Params*)> callback)
void ServerCommands::OnCommand(std::int32_t cmd, const serverCommandHandler& callback)
{
ServerCommands::Commands.insert_or_assign(cmd, std::move(callback));
Commands.insert_or_assign(cmd, callback);
}
bool ServerCommands::OnServerCommand()
{
Command::ClientParams params;
for (const auto& [id, callback] : ServerCommands::Commands)
for (const auto& [id, callback] : Commands)
{
if (params.size() >= 1)
{
@ -33,7 +33,10 @@ namespace Components
{
push eax
pushad
// Missing localClientNum!
call ServerCommands::OnServerCommand
mov [esp + 20h], eax
popad
pop eax
@ -63,7 +66,7 @@ namespace Components
ServerCommands::ServerCommands()
{
// Server command receive hook
Utils::Hook(0x59449F, ServerCommands::CG_DeployServerCommand_Stub).install()->quick();
Utils::Hook(0x59449F, CG_DeployServerCommand_Stub).install()->quick();
Utils::Hook::Nop(0x5944A4, 6);
}
}

View File

@ -7,10 +7,11 @@ namespace Components
public:
ServerCommands();
static void OnCommand(std::int32_t cmd, std::function<bool(Command::Params*)> callback);
using serverCommandHandler = std::function<bool(Command::Params*)>;
static void OnCommand(std::int32_t cmd, const serverCommandHandler& callback);
private:
static std::unordered_map<std::int32_t, std::function<bool(Command::Params*)>> Commands;
static std::unordered_map<std::int32_t, serverCommandHandler> Commands;
static bool OnServerCommand();
static void CG_DeployServerCommand_Stub();

View File

@ -1,10 +1,11 @@
#include <STDInclude.hpp>
#include "GSC/Script.hpp"
namespace Components
{
int64_t* Stats::GetStatsID()
std::int64_t* Stats::GetStatsID()
{
static int64_t id = 0x110000100001337;
static std::int64_t id = 0x110000100001337;
return &id;
}
@ -58,7 +59,7 @@ namespace Components
void Stats::UpdateClasses([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info)
{
Stats::SendStats();
SendStats();
}
int Stats::SaveStats(char* dest, const char* folder, const char* buffer, size_t length)
@ -73,18 +74,64 @@ namespace Components
return Utils::Hook::Call<int(char*, const char*, const char*, size_t)>(0x426450)(dest, folder, buffer, length);
}
void Stats::AddScriptFunctions()
{
Script::AddMethod("GetStat", [](const Game::scr_entref_t entref)
{
const auto* ent = Game::GetPlayerEntity(entref);
const auto index = Game::Scr_GetInt(0);
if (index < 0 || index > 3499)
{
Game::Scr_ParamError(0, Utils::String::VA("GetStat: invalid index %i", index));
}
if (ent->client->sess.connected <= Game::CON_DISCONNECTED)
{
Game::Scr_Error("GetStat: called on a disconnected player");
}
Game::Scr_AddInt(Game::SV_GetClientStat(ent->s.number, index));
});
Script::AddMethod("SetStat", [](const Game::scr_entref_t entref)
{
const auto* ent = Game::GetPlayerEntity(entref);
const auto iNumParms = Game::Scr_GetNumParam();
if (iNumParms != 2)
{
Game::Scr_Error(Utils::String::VA("GetStat: takes 2 arguments, got %u.\n", iNumParms));
}
const auto index = Game::Scr_GetInt(0);
if (index < 0 || index > 3499)
{
Game::Scr_ParamError(0, Utils::String::VA("setstat: invalid index %i", index));
}
const auto value = Game::Scr_GetInt(1);
if (index < 2000 && (value < 0 || value > 255))
{
Game::Scr_ParamError(1, Utils::String::VA("setstat: index %i is a byte value, and you're trying to set it to %i", index, value));
}
Game::SV_SetClientStat(ent->s.number, index, value);
});
}
Stats::Stats()
{
// This UIScript should be added in the onClose code of the cac_popup menu,
// so everytime the create-a-class window is closed, and a client is connected
// to a server, the stats data of the client will be reuploaded to the server.
// allowing the player to change their classes while connected to a server.
UIScript::Add("UpdateClasses", Stats::UpdateClasses);
UIScript::Add("UpdateClasses", UpdateClasses);
// Allow playerdata to be changed while connected to a server
Utils::Hook::Set<BYTE>(0x4376FD, 0xEB);
// ToDo: Allow playerdata changes in setPlayerData UI script.
// TODO: Allow playerdata changes in setPlayerData UI script.
// Rename stat file
Utils::Hook::SetString(0x71C048, "iw4x.stat");
@ -92,8 +139,9 @@ namespace Components
// Patch stats steamid
Utils::Hook::Nop(0x682EBF, 20);
Utils::Hook::Nop(0x6830B1, 20);
Utils::Hook(0x682EBF, Stats::GetStatsID, HOOK_CALL).install()->quick();
Utils::Hook(0x6830B1, Stats::GetStatsID, HOOK_CALL).install()->quick();
Utils::Hook(0x682EBF, GetStatsID, HOOK_CALL).install()->quick();
Utils::Hook(0x6830B1, GetStatsID, HOOK_CALL).install()->quick();
//Utils::Hook::Set<BYTE>(0x68323A, 0xEB);
// Never use remote stat saving
@ -103,6 +151,34 @@ namespace Components
Utils::Hook::Nop(0x402CE6, 2);
// Write stats to mod folder if a mod is loaded
Utils::Hook(0x682F7B, Stats::SaveStats, HOOK_CALL).install()->quick();
Utils::Hook(0x682F7B, SaveStats, HOOK_CALL).install()->quick();
AddScriptFunctions();
// Skip silly Com_Error (LiveStorage_SetStat)
Utils::Hook::Set<BYTE>(0x4CC5F9, 0xEB);
// 'M' Seems to be used on Xbox only for parsing platform specific ranks
ServerCommands::OnCommand('M', [](Command::Params* params)
{
const auto* arg1 = params->get(1);
const auto* arg2 = params->get(2);
Game::LiveStorage_SetStat(Game::CL_ControllerIndexFromClientNum(0), std::atoi(arg1), std::atoi(arg2));
return true;
});
Command::Add("statGet", []([[maybe_unused]] Command::Params* params)
{
if (params->size() < 2)
{
Logger::PrintError(Game::CON_CHANNEL_SERVER, "statget usage: statget <index>\n");
return;
}
const auto index = std::atoi(params->get(1));
const auto stat = Game::LiveStorage_GetStat(0, index);
Logger::Print(Game::CON_CHANNEL_SYSTEM, "Stat {}: {}\n", index, stat);
});
}
}

View File

@ -11,9 +11,12 @@ namespace Components
private:
static void UpdateClasses([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info);
static void SendStats();
static int SaveStats(char* dest, const char* folder, const char* buffer, size_t length);
static int64_t* GetStatsID();
static std::int64_t* GetStatsID();
static void AddScriptFunctions();
};
}

View File

@ -138,6 +138,7 @@ namespace Game
Live_GetLocalClientName_t Live_GetLocalClientName = Live_GetLocalClientName_t(0x441FC0);
LiveStorage_GetStat_t LiveStorage_GetStat = LiveStorage_GetStat_t(0x471F60);
LiveStorage_SetStat_t LiveStorage_SetStat = LiveStorage_SetStat_t(0x4CC5D0);
Scr_AddSourceBuffer_t Scr_AddSourceBuffer = Scr_AddSourceBuffer_t(0x61ABC0);

View File

@ -347,6 +347,9 @@ namespace Game
typedef int(*LiveStorage_GetStat_t)(int controllerIndex, int index);
extern LiveStorage_GetStat_t LiveStorage_GetStat;
typedef void(*LiveStorage_SetStat_t)(int controllerIndex, int index, int value);
extern LiveStorage_SetStat_t LiveStorage_SetStat;
typedef char*(*Scr_AddSourceBuffer_t)(const char* filename, const char* extFilename, const char* codePos, bool archive);
extern Scr_AddSourceBuffer_t Scr_AddSourceBuffer;

View File

@ -6,6 +6,7 @@ namespace Game
SV_IsTestClient_t SV_IsTestClient = SV_IsTestClient_t(0x4D6E40);
SV_GameClientNum_Score_t SV_GameClientNum_Score = SV_GameClientNum_Score_t(0x469AC0);
SV_GameSendServerCommand_t SV_GameSendServerCommand = SV_GameSendServerCommand_t(0x4BC3A0);
SV_SendServerCommand_t SV_SendServerCommand = SV_SendServerCommand_t(0x4255A0);
SV_Cmd_TokenizeString_t SV_Cmd_TokenizeString = SV_Cmd_TokenizeString_t(0x4B5780);
SV_Cmd_EndTokenizedString_t SV_Cmd_EndTokenizedString = SV_Cmd_EndTokenizedString_t(0x464750);
SV_Cmd_ArgvBuffer_t SV_Cmd_ArgvBuffer = SV_Cmd_ArgvBuffer_t(0x40BB60);
@ -52,6 +53,60 @@ namespace Game
}
}
int SV_GetClientStat(int clientNum, int index)
{
assert(svs_clients[clientNum].statPacketsReceived == (1 << MAX_STATPACKETS) - 1);
assert(svs_clients[clientNum].header.state >= CS_CONNECTED);
if (index < 2000)
{
// Skip first 4 bytes of the binary blob
return svs_clients[clientNum].stats.__s0.binary[index];
}
if (index < 3498)
{
return svs_clients[clientNum].stats.__s0.data[index - 2000];
}
assert(0 && "Unhandled stat index");
return 0;
}
void SV_SetClientStat(int clientNum, int index, int value)
{
assert(svs_clients[clientNum].statPacketsReceived == (1 << MAX_STATPACKETS) - 1);
assert(svs_clients[clientNum].header.state >= CS_CONNECTED);
if (index < 2000)
{
assert(value >= 0 && value <= 255);
if (svs_clients[clientNum].stats.__s0.binary[index] == value)
{
return;
}
svs_clients[clientNum].stats.__s0.binary[index] = static_cast<unsigned char>(value);
}
else if (index < 3498)
{
if (svs_clients[clientNum].stats.__s0.data[index - 2000] == value)
{
return;
}
svs_clients[clientNum].stats.__s0.data[index - 2000] = value;
}
else
{
assert(0 && "Unhandled stat index");
return;
}
SV_SendServerCommand(&svs_clients[clientNum], SV_CMD_RELIABLE, "%c %i %i", 'M', index, value);
}
void SV_BotUserMove(client_t* client)
{
static DWORD SV_BotUserMove_t = 0x626E50;

View File

@ -14,6 +14,9 @@ namespace Game
typedef void(*SV_GameSendServerCommand_t)(int clientNum, svscmd_type type, const char* text);
extern SV_GameSendServerCommand_t SV_GameSendServerCommand;
typedef void(*SV_SendServerCommand_t)(client_t* cl, svscmd_type type, const char* fmt, ...);
extern SV_SendServerCommand_t SV_SendServerCommand;
typedef void(*SV_Cmd_TokenizeString_t)(const char* string);
extern SV_Cmd_TokenizeString_t SV_Cmd_TokenizeString;
@ -47,6 +50,8 @@ namespace Game
typedef client_t* (*SV_FindClientByAddress_t)(netadr_t from, int qport, int remoteClientIndex);
extern SV_FindClientByAddress_t SV_FindClientByAddress;
constexpr auto MAX_STATPACKETS = 7;
extern int* svs_time;
extern int* sv_serverId_value;
extern int* svs_clientCount;
@ -57,5 +62,7 @@ namespace Game
extern int SV_GetServerThreadOwnsGame();
extern void SV_GameDropClient(int clientNum, const char* reason);
extern void SV_DropAllBots();
extern int SV_GetClientStat(int clientNum, int index);
extern void SV_SetClientStat(int clientNum, int index, int value);
extern void SV_BotUserMove(client_t* client);
}

View File

@ -6023,12 +6023,6 @@ namespace Game
float keys[1];
};
union $81775853B5F1E1C6748A82ED93FC367C
{
FxElemVisuals visuals[32];
FxElemMarkVisuals markVisuals[16];
};
struct FxEditorTrailDef
{
FxTrailVertex verts[64];
@ -6081,7 +6075,11 @@ namespace Game
FxFloatRange emitDistVariance;
char elemType;
int visualCount;
$81775853B5F1E1C6748A82ED93FC367C ___u42;
union
{
FxElemVisuals visuals[32];
FxElemMarkVisuals markVisuals[16];
} ___u42;
int trailSplitDist;
int trailSplitArcDist;
int trailSplitTime;
@ -6583,7 +6581,15 @@ namespace Game
int bIsTestClient; // 269040
int serverID; // 269044
bool usingOnlineStatsOffline;
char stats[8192];
struct
{
unsigned int checksum;
struct
{
unsigned char binary[2000];
int data[1547];
} __s0;
} stats;
char statsModifiedFlags[1024];
bool statsModified;
char statPacketsReceived;