#include "STDInclude.hpp" namespace Components { std::string Script::ScriptName; std::vector Script::ScriptHandles; std::vector Script::ScriptFunctions; std::vector Script::ScriptNameStack; unsigned short Script::FunctionName; std::unordered_map Script::ScriptStorage; std::unordered_map Script::ScriptBaseProgramNum; int Script::LastFrameTime = -1; Utils::Signal Script::VMShutdownSignal; void Script::FunctionError() { std::string funcName = Game::SL_ConvertToString(Script::FunctionName); Game::Scr_ShutdownAllocNode(); Logger::Print(23, "\n"); Logger::Print(23, "******* script compile error *******\n"); Logger::Print(23, "Error: unknown function %s in %s\n", funcName.data(), Script::ScriptName.data()); Logger::Print(23, "************************************\n"); Logger::Error(5, "script compile error\nunknown function %s\n%s\n\n", funcName.data(), Script::ScriptName.data()); } __declspec(naked) void Script::StoreFunctionNameStub() { __asm { mov eax, [esp - 8h] mov Script::FunctionName, ax sub esp, 0Ch push 0 push edi mov eax, 612DB6h jmp eax } } void Script::StoreScriptName(const char* name) { Script::ScriptNameStack.push_back(Script::ScriptName); Script::ScriptName = name; if (!Utils::String::EndsWith(Script::ScriptName, ".gsc")) { Script::ScriptName.append(".gsc"); } } __declspec(naked) void Script::StoreScriptNameStub() { __asm { pushad lea ecx, [esp + 30h] push ecx call Script::StoreScriptName add esp, 4h popad push ebp mov ebp, ds:1CDEAA8h push 427DC3h retn } } void Script::RestoreScriptName() { Script::ScriptName = Script::ScriptNameStack.back(); Script::ScriptNameStack.pop_back(); } __declspec(naked) void Script::RestoreScriptNameStub() { __asm { pushad call Script::RestoreScriptName popad mov ds:1CDEAA8h, ebp push 427E77h retn } } void Script::PrintSourcePos(const char* filename, unsigned int offset) { FileSystem::File script(filename); if (script.exists()) { std::string buffer = script.getBuffer(); Utils::String::Replace(buffer, "\t", " "); int line = 1; int lineOffset = 0; int inlineOffset = 0; for (unsigned int i = 0; i < buffer.size(); ++i) { // Terminate line if (i == offset) { while (buffer[i] != '\r' && buffer[i] != '\n' && buffer[i] != '\0') { ++i; } buffer[i] = '\0'; break; } if (buffer[i] == '\n') { ++line; lineOffset = i; // Includes the line break! inlineOffset = 0; } else { ++inlineOffset; } } Logger::Print(23, "in file %s, line %d:", filename, line); Logger::Print(23, "%s\n", buffer.data() + lineOffset); for (int i = 0; i < (inlineOffset - 1); ++i) { Logger::Print(23, " "); } Logger::Print(23, "*\n"); } else { Logger::Print(23, "in file %s, offset %d\n", filename, offset); } } void Script::CompileError(unsigned int offset, const char* message, ...) { char msgbuf[1024] = { 0 }; va_list v; va_start(v, message); _vsnprintf_s(msgbuf, sizeof(msgbuf), message, v); va_end(v); Game::Scr_ShutdownAllocNode(); Logger::Print(23, "\n"); Logger::Print(23, "******* script compile error *******\n"); Logger::Print(23, "Error: %s ", msgbuf); Script::PrintSourcePos(Script::ScriptName.data(), offset); Logger::Print(23, "************************************\n\n"); Logger::Error(5, "script compile error\n%s\n%s\n(see console for actual details)\n", msgbuf, Script::ScriptName.data()); } int Script::LoadScriptAndLabel(const std::string& script, const std::string& label) { Logger::Print("Loading script %s.gsc...\n", script.data()); if (!Game::Scr_LoadScript(script.data())) { Logger::Print("Script %s encountered an error while loading. (doesn't exist?)", script.data()); Logger::Error(1, reinterpret_cast(0x70B810), script.data()); } else { Logger::Print("Script %s.gsc loaded successfully.\n", script.data()); } Logger::Print("Finding script handle %s::%s...\n", script.data(), label.data()); int handle = Game::Scr_GetFunctionHandle(script.data(), label.data()); if (handle) { Logger::Print("Script handle %s::%s loaded successfully.\n", script.data(), label.data()); return handle; } Logger::Print("Script handle %s::%s couldn't be loaded. (file with no entry point?)\n", script.data(), label.data()); return handle; } void Script::LoadGameType() { for (auto handle : Script::ScriptHandles) { Game::Scr_FreeThread(Game::Scr_ExecThread(handle, 0)); } Game::Scr_LoadGameType(); } void Script::LoadGameTypeScript() { Script::ScriptHandles.clear(); auto list = FileSystem::GetFileList("scripts/", "gsc"); for (auto file : list) { file = "scripts/" + file; if (Utils::String::EndsWith(file, ".gsc")) { file = file.substr(0, file.size() - 4); } int handle = Script::LoadScriptAndLabel(file, "init"); if (handle) Script::ScriptHandles.push_back(handle); else { handle = Script::LoadScriptAndLabel(file, "main"); if (handle) Script::ScriptHandles.push_back(handle); } } Game::GScr_LoadGameTypeScript(); } void Script::AddFunction(const std::string& name, Game::scr_function_t function, bool isDev) { for (auto i = Script::ScriptFunctions.begin(); i != Script::ScriptFunctions.end();) { if (i->getName() == name) { i = Script::ScriptFunctions.erase(i); continue; } ++i; } Script::ScriptFunctions.push_back({ name, function, isDev }); } Game::scr_function_t Script::GetFunction(void* caller, const char** name, int* isDev) { for (auto& function : Script::ScriptFunctions) { if (name && *name) { if (Utils::String::ToLower(*name) == Utils::String::ToLower(function.getName())) { *name = function.getName(); *isDev = function.isDev(); return function.getFunction(); } } else if (caller == reinterpret_cast(0x465781)) { Game::Scr_RegisterFunction(function.getFunction()); } } return nullptr; } __declspec(naked) void Script::GetFunctionStub() { __asm { test eax, eax jnz returnSafe sub esp, 8h push [esp + 10h] call Script::GetFunction add esp, 0Ch returnSafe: pop edi pop esi retn } } void Script::StoreScriptBaseProgramNum() { Script::ScriptBaseProgramNum.insert_or_assign(Utils::Hook::Get(0x1CFEEF8), Script::ScriptName); } void Script::Scr_PrintPrevCodePos(int scriptPos) { int bestCodePos = -1; int nextCodePos = -1; int offset = -1; std::string file; for (auto kv : Script::ScriptBaseProgramNum) { int codePos = kv.first; if (codePos > scriptPos) { if (nextCodePos == -1 || codePos < nextCodePos) nextCodePos = codePos; continue; } if (codePos < bestCodePos) continue; bestCodePos = codePos; file = kv.second; offset = scriptPos - bestCodePos; } if (bestCodePos == -1) return; float onehundred = 100.0; Logger::Print(23, "\n@ %d (%d - %d)\n", scriptPos, bestCodePos, nextCodePos); Logger::Print(23, "in %s (%.1f%% through the source)\n\n", file.c_str(), ((offset * onehundred) / (nextCodePos - bestCodePos))); } __declspec(naked) void Script::Scr_PrintPrevCodePosStub() { __asm { push esi call Script::Scr_PrintPrevCodePos add esp, 4h pop esi retn } } __declspec(naked) void Script::StoreScriptBaseProgramNumStub() { __asm { // execute our hook pushad call Script::StoreScriptBaseProgramNum popad // execute overwritten code caused by the jump hook sub eax, ds:201A460h // gScrVarPub_programBuffer add esp, 0Ch mov ds : 1CFEEF8h, eax // gScrCompilePub_programLen // jump back to the original code push 426C3Bh retn } } void Script::OnVMShutdown(Utils::Slot callback) { Script::ScriptBaseProgramNum.clear(); Script::VMShutdownSignal.connect(callback); } void Script::ScrShutdownSystemStub(int num) { Script::VMShutdownSignal(); // Scr_ShutdownSystem Utils::Hook::Call(0x421EE0)(num); } int Script::SetExpFogStub() { if (Game::Scr_GetNumParam() == 6) { std::memmove(&Game::scriptContainer->stack[-4], &Game::scriptContainer->stack[-5], sizeof(Game::VariableValue) * 6); Game::scriptContainer->stack += 1; Game::scriptContainer->stack[-6].type = Game::VAR_FLOAT; Game::scriptContainer->stack[-6].u.floatValue = 0; ++Game::scriptContainer->numParam; } return Game::Scr_GetNumParam(); } Game::gentity_t* Script::getEntFromEntRef(Game::scr_entref_t entref) { Game::gentity_t* gentity = &Game::g_entities[entref]; return gentity; } Game::client_t* Script::getClientFromEnt(Game::gentity_t* gentity) { if (!gentity->client) { Logger::Error(5, "Entity: %i is not a client", gentity); } return &Game::svs_clients[gentity->s.number]; } void Script::AddFunctions() { // System time Script::AddFunction("GetSystemTime", [](Game::scr_entref_t) // gsc: GetSystemTime() { SYSTEMTIME time; GetSystemTime(&time); Game::Scr_AddInt(time.wSecond); }); Script::AddFunction("GetSystemMilliseconds", [](Game::scr_entref_t) // gsc: GetSystemMilliseconds() { SYSTEMTIME time; GetSystemTime(&time); Game::Scr_AddInt(time.wMilliseconds); }); // Print to console, even without being in 'developer 1'. Script::AddFunction("PrintConsole", [](Game::scr_entref_t) // gsc: PrintConsole() { if (Game::Scr_GetNumParam() != 1 || Game::Scr_GetType(0) != Game::VAR_STRING) { Game::Scr_Error("^1PrintConsole: Needs one string parameter!\n"); return; } auto str = Game::Scr_GetString(0); Game::Com_Printf(0, str); }); // Executes command to the console Script::AddFunction("Exec", [](Game::scr_entref_t) // gsc: Exec() { if (Game::Scr_GetNumParam() != 1 || Game::Scr_GetType(0) != Game::VAR_STRING) { Game::Scr_Error("^1Exec: Needs one string parameter!\n"); return; } auto str = Game::Scr_GetString(0); Command::Execute(str, false); }); // Script Storage Funcs Script::AddFunction("StorageSet", [](Game::scr_entref_t) // gsc: StorageSet(, ); { if (Game::Scr_GetNumParam() != 2 || Game::Scr_GetType(0) != Game::VAR_STRING || Game::Scr_GetType(1) != Game::VAR_STRING) { Game::Scr_Error("^1StorageSet: Needs two string parameters!\n"); return; } std::string key = Game::Scr_GetString(0); std::string data = Game::Scr_GetString(1); Script::ScriptStorage.insert_or_assign(key, data); }); Script::AddFunction("StorageRemove", [](Game::scr_entref_t) // gsc: StorageRemove(); { if (Game::Scr_GetNumParam() != 1 || Game::Scr_GetType(0) != Game::VAR_STRING) { Game::Scr_Error("^1StorageRemove: Needs one string parameter!\n"); return; } std::string key = Game::Scr_GetString(0); if (!Script::ScriptStorage.count(key)) { Game::Scr_Error(Utils::String::VA("^1StorageRemove: Store does not have key '%s'!\n", key.c_str())); return; } Script::ScriptStorage.erase(key); }); Script::AddFunction("StorageGet", [](Game::scr_entref_t) // gsc: StorageGet(); { if (Game::Scr_GetNumParam() != 1 || Game::Scr_GetType(0) != Game::VAR_STRING) { Game::Scr_Error("^1StorageGet: Needs one string parameter!\n"); return; } std::string key = Game::Scr_GetString(0); if (!Script::ScriptStorage.count(key)) { Game::Scr_Error(Utils::String::VA("^1StorageGet: Store does not have key '%s'!\n", key.c_str())); return; } auto data = Script::ScriptStorage.at(key); Game::Scr_AddString(data.c_str()); }); Script::AddFunction("StorageHas", [](Game::scr_entref_t) // gsc: StorageHas(); { if (Game::Scr_GetNumParam() != 1 || Game::Scr_GetType(0) != Game::VAR_STRING) { Game::Scr_Error("^1StorageHas: Needs one string parameter!\n"); return; } std::string key = Game::Scr_GetString(0); Game::Scr_AddInt(Script::ScriptStorage.count(key)); }); Script::AddFunction("StorageClear", [](Game::scr_entref_t) // gsc: StorageClear(); { Script::ScriptStorage.clear(); }); Script::AddFunction("Noclip", [](Game::scr_entref_t entref) { if (entref >= Game::MAX_GENTITIES || Game::g_entities[entref].client == nullptr) { Game::Scr_Error(Utils::String::VA("^1NoClip: entity %u is not a client\n", entref)); return; } if (Game::Scr_GetNumParam() == 1 && Game::Scr_GetType(0) == Game::VAR_INTEGER) { if (Game::Scr_GetInt(0)) { Game::g_entities[entref].client->flags |= Game::PLAYER_FLAG_NOCLIP; } else { Game::g_entities[entref].client->flags &= ~Game::PLAYER_FLAG_NOCLIP; } } else { Game::g_entities[entref].client->flags ^= Game::PLAYER_FLAG_NOCLIP; } }); Script::AddFunction("Ufo", [](Game::scr_entref_t entref) { if (entref >= Game::MAX_GENTITIES || Game::g_entities[entref].client == nullptr) { Game::Scr_Error(Utils::String::VA("^1Ufo: entity %u is not a client\n", entref)); return; } if (Game::Scr_GetNumParam() == 1 && Game::Scr_GetType(0) == Game::VAR_INTEGER) { if (Game::Scr_GetInt(0)) { Game::g_entities[entref].client->flags |= Game::PLAYER_FLAG_UFO; } else { Game::g_entities[entref].client->flags &= ~Game::PLAYER_FLAG_UFO; } } else { Game::g_entities[entref].client->flags ^= Game::PLAYER_FLAG_UFO; } }); } Script::Script() { Utils::Hook(0x612DB0, Script::StoreFunctionNameStub, HOOK_JUMP).install()->quick(); Utils::Hook(0x427E71, Script::RestoreScriptNameStub, HOOK_JUMP).install()->quick(); Utils::Hook(0x427DBC, Script::StoreScriptNameStub, HOOK_JUMP).install()->quick(); Utils::Hook(0x426C2D, Script::StoreScriptBaseProgramNumStub, HOOK_JUMP).install()->quick(); Utils::Hook(0x42281B, Script::Scr_PrintPrevCodePosStub, HOOK_JUMP).install()->quick(); // enable scr_error printing if in developer Dvar::OnInit([]() { int developer = Dvar::Var("developer").get(); if (developer > 0 && Dedicated::IsEnabled()) Utils::Hook::Set(0x48D8C7, 0x75); }); Utils::Hook(0x612E8D, Script::FunctionError, HOOK_CALL).install()->quick(); Utils::Hook(0x612EA2, Script::FunctionError, HOOK_CALL).install()->quick(); Utils::Hook(0x434260, Script::CompileError, HOOK_JUMP).install()->quick(); Utils::Hook(0x48EFFE, Script::LoadGameType, HOOK_CALL).install()->quick(); Utils::Hook(0x45D44A, Script::LoadGameTypeScript, HOOK_CALL).install()->quick(); Utils::Hook(0x44E736, Script::GetFunctionStub, HOOK_JUMP).install()->quick(); // Scr_GetFunction Utils::Hook(0x4EC8E5, Script::GetFunctionStub, HOOK_JUMP).install()->quick(); // Scr_GetMethod Utils::Hook(0x5F41A3, Script::SetExpFogStub, HOOK_CALL).install()->quick(); Utils::Hook(0x47548B, Script::ScrShutdownSystemStub, HOOK_CALL).install()->quick(); Utils::Hook(0x4D06BA, Script::ScrShutdownSystemStub, HOOK_CALL).install()->quick(); Scheduler::OnFrame([]() { if (!Game::SV_Loaded()) return; int nowMs = Game::Sys_Milliseconds(); if (Script::LastFrameTime != -1) { int timeTaken = static_cast((nowMs - Script::LastFrameTime) * Dvar::Var("timescale").get()); if (timeTaken >= 500) Logger::Print(23, "Hitch warning: %i msec frame time\n", timeTaken); } Script::LastFrameTime = nowMs; }); Script::AddFunction("debugBox", [](Game::scr_entref_t) { MessageBoxA(nullptr, Game::Scr_GetString(0), "DEBUG", 0); }, true); Script::AddFunctions(); // Script::AddFunction("playviewmodelfx", [](Game::scr_entref_t /*index*/) // { // /*auto Scr_Error = Utils::Hook::Call(0x42EF40); // if (index >> 16) // { // Scr_Error("not an entity"); // return; // }*/ // // obtain FX name // auto fxName = Game::Scr_GetString(0); // auto fx = Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_FX, fxName).fx; // auto tagName = Game::Scr_GetString(1); // auto tagIndex = Game::SL_GetString(tagName, 0); // /*char boneIndex = -2; // if (!Game::CG_GetBoneIndex(2048, tagIndex, &boneIndex)) // { // Scr_Error(Utils::String::VA("Unknown bone %s.\n", tagName)); // return; // }*/ // Game::CG_PlayBoltedEffect(0, fx, 2048, tagIndex); // }); } Script::~Script() { Script::ScriptName.clear(); Script::ScriptHandles.clear(); Script::ScriptNameStack.clear(); Script::ScriptFunctions.clear(); Script::VMShutdownSignal.clear(); Script::ScriptStorage.clear(); Script::ScriptBaseProgramNum.clear(); } }