iw4x-client/src/Components/Modules/Script.cpp

731 lines
18 KiB
C++
Raw Normal View History

2017-01-19 16:23:59 -05:00
#include "STDInclude.hpp"
namespace Components
{
std::string Script::ScriptName;
std::vector<int> Script::ScriptHandles;
std::vector<Script::Function> Script::ScriptFunctions;
2017-01-19 16:23:59 -05:00
std::vector<std::string> Script::ScriptNameStack;
unsigned short Script::FunctionName;
std::unordered_map<std::string, std::string> Script::ScriptStorage;
std::unordered_map<int, std::string> Script::ScriptBaseProgramNum;
2021-11-13 08:15:27 -05:00
std::unordered_map<const char*, const char*> Script::ReplacedFunctions;
const char* Script::ReplacedPos = 0;
2020-12-19 17:50:51 -05:00
int Script::LastFrameTime = -1;
2017-01-19 16:23:59 -05:00
Utils::Signal<Scheduler::Callback> Script::VMShutdownSignal;
2017-05-14 14:14:52 -04:00
2017-01-19 16:23:59 -05:00
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(Game::ERR_SCRIPT_DROP, "script compile error\nunknown function %s\n%s\n\n", funcName.data(), Script::ScriptName.data());
2017-01-19 16:23:59 -05:00
}
__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
}
}
2022-01-16 12:25:51 -05:00
void Script::RuntimeError(const char* codePos, unsigned int index, const char* msg, const char* dialogMessage)
{
const auto developer = Dvar::Var("developer").get<int>();
// Allow error messages to be printed if developer mode is on
// Should check scrVarPub.developer but it's absent in this version of the game
if (!Game::scrVmPub->terminal_error && !developer)
return;
// If were are developing let's just print a brief message
2022-01-17 19:21:25 -05:00
// scrVmPub.debugCode seems to be always false
2022-01-16 12:25:51 -05:00
if (Game::scrVmPub->debugCode || Game::scrVarPub->developer_script)
{
Logger::Print(23, "%s\n", msg);
if (!Game::scrVmPub->terminal_error)
return;
}
else
{
Game::RuntimeErrorInternal(23, codePos, index, msg);
// Let's not throw error unless we have to
if (!Game::scrVmPub->abort_on_error && !Game::scrVmPub->terminal_error)
return;
}
if (dialogMessage == nullptr)
dialogMessage = "";
const auto errorLevel = (Game::scrVmPub->terminal_error) ? Game::ERR_SCRIPT_DROP : Game::ERR_SCRIPT;
Logger::Error(errorLevel, "\x15script runtime error\n(see console for details)\n%s\n%s", msg, dialogMessage);
}
2017-01-19 16:23:59 -05:00
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
{
2017-02-01 07:44:25 -05:00
pushad
lea ecx, [esp + 30h]
2017-01-19 16:23:59 -05:00
push ecx
call Script::StoreScriptName
add esp, 4h
2017-02-01 07:44:25 -05:00
popad
2017-01-19 16:23:59 -05:00
push ebp
mov ebp, ds:1CDEAA8h
2017-02-01 07:44:25 -05:00
push 427DC3h
retn
2017-01-19 16:23:59 -05:00
}
}
void Script::RestoreScriptName()
{
Script::ScriptName = Script::ScriptNameStack.back();
Script::ScriptNameStack.pop_back();
}
__declspec(naked) void Script::RestoreScriptNameStub()
{
__asm
{
2017-02-01 07:44:25 -05:00
pushad
2017-01-19 16:23:59 -05:00
call Script::RestoreScriptName
2017-02-01 07:44:25 -05:00
popad
2017-01-19 16:23:59 -05:00
mov ds:1CDEAA8h, ebp
2017-02-01 07:44:25 -05:00
push 427E77h
retn
2017-01-19 16:23:59 -05:00
}
}
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(Game::ERR_SCRIPT_DROP, "script compile error\n%s\n%s\n(see console for actual details)\n", msgbuf, Script::ScriptName.data());
2017-01-19 16:23:59 -05:00
}
2018-12-17 08:29:18 -05:00
int Script::LoadScriptAndLabel(const std::string& script, const std::string& label)
2017-01-19 16:23:59 -05:00
{
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(Game::ERR_DROP, reinterpret_cast<char*>(0x70B810), script.data());
2017-01-19 16:23:59 -05:00
}
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
2017-01-19 16:23:59 -05:00
{
handle = Script::LoadScriptAndLabel(file, "main");
if (handle) Script::ScriptHandles.push_back(handle);
2017-01-19 16:23:59 -05:00
}
}
Game::GScr_LoadGameTypeScript();
}
2018-12-17 08:29:18 -05:00
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 });
}
2017-05-14 14:14:52 -04:00
Game::scr_function_t Script::GetFunction(void* caller, const char** name, int* isDev)
{
for (auto& function : Script::ScriptFunctions)
{
2017-05-14 14:14:52 -04:00
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<void*>(0x465781))
{
Game::Scr_RegisterFunction(function.getFunction());
}
}
return nullptr;
}
__declspec(naked) void Script::GetFunctionStub()
{
__asm
{
test eax, eax
jnz returnSafe
2017-05-14 14:14:52 -04:00
sub esp, 8h
push [esp + 10h]
call Script::GetFunction
2017-05-14 14:14:52 -04:00
add esp, 0Ch
2020-12-08 17:18:09 -05:00
returnSafe:
2017-05-14 14:14:52 -04:00
pop edi
2020-12-08 17:18:09 -05:00
pop esi
retn
}
}
void Script::StoreScriptBaseProgramNum()
{
Script::ScriptBaseProgramNum.insert_or_assign(Utils::Hook::Get<int>(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
2017-05-14 14:14:52 -04:00
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<Scheduler::Callback> callback)
{
Script::ScriptBaseProgramNum.clear();
Script::VMShutdownSignal.connect(callback);
}
2017-05-14 14:14:52 -04:00
void Script::ScrShutdownSystemStub(int num)
{
Script::VMShutdownSignal();
// Scr_ShutdownSystem
Utils::Hook::Call<void(int)>(0x421EE0)(num);
}
2021-11-13 08:22:06 -05:00
unsigned int Script::SetExpFogStub()
2017-04-29 17:08:41 -04:00
{
2021-11-13 08:22:06 -05:00
if (Game::Scr_GetNumParam() == 6u)
2017-04-29 17:08:41 -04:00
{
std::memmove(&Game::scrVmPub->top[-4], &Game::scrVmPub->top[-5], sizeof(Game::VariableValue) * 6);
Game::scrVmPub->top += 1;
Game::scrVmPub->top[-6].type = Game::VAR_FLOAT;
Game::scrVmPub->top[-6].u.floatValue = 0.0f;
2017-05-07 13:04:57 -04:00
++Game::scrVmPub->outparamcount;
2017-04-29 17:08:41 -04:00
}
return Game::Scr_GetNumParam();
}
const char* Script::GetCodePosForParam(int index)
2021-11-13 08:15:27 -05:00
{
2021-11-13 15:55:22 -05:00
if (static_cast<unsigned int>(index) >= Game::scrVmPub->outparamcount)
{
Game::Scr_ParamError(static_cast<unsigned int>(index), "^1GetCodePosForParam: Index is out of range!\n");
return "";
}
const auto value = &Game::scrVmPub->top[-index];
if (value->type != Game::VAR_FUNCTION)
{
Game::Scr_ParamError(static_cast<unsigned int>(index), "^1GetCodePosForParam: Expects a function as parameter!\n");
return "";
}
return value->u.codePosValue;
2021-11-13 08:15:27 -05:00
}
void Script::GetReplacedPos(const char* pos)
{
if (Script::ReplacedFunctions.find(pos) != Script::ReplacedFunctions.end())
{
2021-11-13 08:22:06 -05:00
Script::ReplacedPos = Script::ReplacedFunctions[pos];
2021-11-13 08:15:27 -05:00
}
}
void Script::SetReplacedPos(const char* what, const char* with)
{
if (what[0] == '\0' || with[0] == '\0')
{
Logger::Print("Warning: Invalid paramters passed to ReplacedFunctions\n");
return;
}
2021-11-13 08:15:27 -05:00
if (Script::ReplacedFunctions.find(what) != Script::ReplacedFunctions.end())
{
Logger::Print("Warning: ReplacedFunctions already contains codePosValue for a function\n");
2021-11-13 08:15:27 -05:00
}
Script::ReplacedFunctions[what] = with;
}
__declspec(naked) void Script::VMExecuteInternalStub()
{
__asm
{
pushad
push edx
call Script::GetReplacedPos
pop edx
popad
cmp Script::ReplacedPos, 0
jne SetPos
movzx eax, byte ptr [edx]
inc edx
Loc1:
cmp eax, 0x8B
push ecx
mov ecx, 0x2045094
mov [ecx], eax
mov ecx, 0x2040CD4
mov [ecx], edx
pop ecx
push 0x61E944
2021-11-13 08:15:27 -05:00
retn
SetPos:
mov edx, Script::ReplacedPos
mov Script::ReplacedPos, 0
movzx eax, byte ptr [edx]
inc edx
jmp Loc1
}
}
2022-01-07 16:00:44 -05:00
Game::gentity_t* Script::GetEntFromEntRef(const Game::scr_entref_t entref)
{
if (entref.classnum != 0)
2022-01-07 16:00:44 -05:00
{
Game::Scr_ObjectError("Not an entity");
return nullptr;
}
assert(entref.entnum < Game::MAX_GENTITIES);
return &Game::g_entities[entref.entnum];
}
2022-01-07 16:00:44 -05:00
Game::client_t* Script::GetClientFromEnt(const Game::gentity_t* gentity)
{
2022-01-07 16:00:44 -05:00
if (gentity->client == nullptr)
{
2022-01-07 16:00:44 -05:00
Game::Scr_ObjectError(Utils::String::VA("Entity %i is not a player", gentity->s.number));
return nullptr;
}
2022-01-07 16:00:44 -05:00
return &Game::svs_clients[gentity->s.number];
}
void Script::AddFunctions()
{
2021-11-13 08:32:20 -05:00
Script::AddFunction("ReplaceFunc", [](Game::scr_entref_t) // gsc: ReplaceFunc(<function>, <function>)
2021-11-13 08:15:27 -05:00
{
2021-11-13 08:22:06 -05:00
if (Game::Scr_GetNumParam() != 2u)
2021-11-13 08:15:27 -05:00
{
Game::Scr_Error("^1ReplaceFunc: Needs two parameters!\n");
return;
}
const auto what = Script::GetCodePosForParam(0);
const auto with = Script::GetCodePosForParam(1);
2021-11-13 08:15:27 -05:00
Script::SetReplacedPos(what, with);
});
// System time
Script::AddFunction("GetSystemTime", [](Game::scr_entref_t) // gsc: GetSystemTime()
2020-12-08 17:18:09 -05:00
{
SYSTEMTIME time;
GetSystemTime(&time);
2020-12-08 17:18:09 -05:00
Game::Scr_AddInt(time.wSecond);
});
Script::AddFunction("GetSystemMilliseconds", [](Game::scr_entref_t) // gsc: GetSystemMilliseconds()
2020-12-08 17:18:09 -05:00
{
SYSTEMTIME time;
GetSystemTime(&time);
2020-12-08 17:18:09 -05:00
Game::Scr_AddInt(time.wMilliseconds);
});
// Print to console, even without being in 'developer 1'.
Script::AddFunction("PrintConsole", [](Game::scr_entref_t) // gsc: PrintConsole(<string>)
2020-12-08 17:18:09 -05:00
{
2022-01-07 16:00:44 -05:00
const auto str = Game::Scr_GetString(0);
2020-12-08 17:18:09 -05:00
Game::Com_Printf(0, str);
});
// Executes command to the console
Script::AddFunction("Exec", [](Game::scr_entref_t) // gsc: Exec(<string>)
2020-12-08 17:18:09 -05:00
{
2022-01-07 16:00:44 -05:00
const auto str = Game::Scr_GetString(0);
2020-12-08 17:18:09 -05:00
Command::Execute(str, false);
});
// Script Storage Funcs
Script::AddFunction("StorageSet", [](Game::scr_entref_t) // gsc: StorageSet(<str key>, <str data>);
2020-12-08 17:18:09 -05:00
{
std::string key = Game::Scr_GetString(0);
std::string data = Game::Scr_GetString(1);
2020-12-08 17:18:09 -05:00
Script::ScriptStorage.insert_or_assign(key, data);
});
Script::AddFunction("StorageRemove", [](Game::scr_entref_t) // gsc: StorageRemove(<str key>);
2020-12-08 17:18:09 -05:00
{
std::string key = Game::Scr_GetString(0);
if (!Script::ScriptStorage.count(key))
2020-12-08 17:18:09 -05:00
{
Game::Scr_Error(Utils::String::VA("^1StorageRemove: Store does not have key '%s'!\n", key.data()));
2020-12-08 17:18:09 -05:00
return;
}
2020-12-08 17:18:09 -05:00
Script::ScriptStorage.erase(key);
});
Script::AddFunction("StorageGet", [](Game::scr_entref_t) // gsc: StorageGet(<str key>);
2020-12-08 17:18:09 -05:00
{
std::string key = Game::Scr_GetString(0);
if (!Script::ScriptStorage.count(key))
2020-12-08 17:18:09 -05:00
{
Game::Scr_Error(Utils::String::VA("^1StorageGet: Store does not have key '%s'!\n", key.data()));
2020-12-08 17:18:09 -05:00
return;
}
const auto& data = Script::ScriptStorage.at(key);
Game::Scr_AddString(data.data());
2020-12-08 17:18:09 -05:00
});
Script::AddFunction("StorageHas", [](Game::scr_entref_t) // gsc: StorageHas(<str key>);
2020-12-08 17:18:09 -05:00
{
std::string key = Game::Scr_GetString(0);
Game::Scr_AddBool(Script::ScriptStorage.count(key));
2020-12-08 17:18:09 -05:00
});
Script::AddFunction("StorageClear", [](Game::scr_entref_t) // gsc: StorageClear();
2020-12-08 17:18:09 -05:00
{
Script::ScriptStorage.clear();
});
2022-01-15 08:49:33 -05:00
// PlayerCmd_AreControlsFrozen GSC function from Black Ops 2
Script::AddFunction("AreControlsFrozen", [](Game::scr_entref_t entref) // Usage: self AreControlsFrozen();
{
2022-01-15 08:49:33 -05:00
const auto* ent = Script::GetEntFromEntRef(entref);
if (ent->client == nullptr)
{
Game::Scr_ObjectError(Utils::String::VA("Entity %i is not a player", ent->s.number));
return;
}
Game::Scr_AddBool((ent->client->flags & Game::PLAYER_FLAG_FROZEN) != 0);
});
}
2017-01-19 16:23:59 -05:00
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();
2022-01-16 12:25:51 -05:00
Utils::Hook(0x61E3AD, Script::RuntimeError, HOOK_CALL).install()->quick();
Utils::Hook(0x621976, Script::RuntimeError, HOOK_CALL).install()->quick();
Utils::Hook(0x62246E, Script::RuntimeError, HOOK_CALL).install()->quick();
2017-01-19 16:23:59 -05:00
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();
2017-04-29 17:08:41 -04:00
2017-05-14 14:14:52 -04:00
Utils::Hook(0x44E736, Script::GetFunctionStub, HOOK_JUMP).install()->quick(); // Scr_GetFunction
Utils::Hook(0x4EC8E5, Script::GetFunctionStub, HOOK_JUMP).install()->quick(); // Scr_GetMethod
2017-04-29 17:08:41 -04:00
Utils::Hook(0x5F41A3, Script::SetExpFogStub, HOOK_CALL).install()->quick();
2017-05-14 14:14:52 -04:00
2021-11-13 08:15:27 -05:00
Utils::Hook(0x61E92E, Script::VMExecuteInternalStub, HOOK_JUMP).install()->quick();
Utils::Hook::Nop(0x61E933, 1);
2017-05-14 14:14:52 -04:00
Utils::Hook(0x47548B, Script::ScrShutdownSystemStub, HOOK_CALL).install()->quick();
Utils::Hook(0x4D06BA, Script::ScrShutdownSystemStub, HOOK_CALL).install()->quick();
2020-12-19 17:50:51 -05:00
Scheduler::OnFrame([]()
{
if (!Game::SV_Loaded())
return;
int nowMs = Game::Sys_Milliseconds();
if (Script::LastFrameTime != -1)
{
int timeTaken = static_cast<int>((nowMs - Script::LastFrameTime) * Dvar::Var("timescale").get<float>());
if (timeTaken >= 500)
Logger::Print(23, "Hitch warning: %i msec frame time\n", timeTaken);
}
Script::LastFrameTime = nowMs;
});
2017-05-14 14:14:52 -04:00
Script::AddFunction("debugBox", [](Game::scr_entref_t)
2020-12-08 17:18:09 -05:00
{
MessageBoxA(nullptr, Game::Scr_GetString(0), "DEBUG", 0);
}, true);
Script::AddFunctions();
Script::OnVMShutdown([]
{
Script::ReplacedFunctions.clear();
});
2017-01-19 16:23:59 -05:00
}
Script::~Script()
{
Script::ScriptName.clear();
Script::ScriptHandles.clear();
Script::ScriptNameStack.clear();
Script::ScriptFunctions.clear();
2021-11-13 08:15:27 -05:00
Script::ReplacedFunctions.clear();
2017-05-14 14:14:52 -04:00
Script::VMShutdownSignal.clear();
Script::ScriptStorage.clear();
Script::ScriptBaseProgramNum.clear();
2017-01-19 16:23:59 -05:00
}
}