#include "scripting/scripting.h" #include "globalincs/systemvars.h" #include "globalincs/version.h" #include "ade.h" #include "ade_args.h" #include "freespace.h" #include "hook_api.h" #include "bmpman/bmpman.h" #include "controlconfig/controlsconfig.h" #include "gamesequence/gamesequence.h" #include "graphics/openxr.h" #include "hud/hud.h" #include "io/key.h" #include "mission/missioncampaign.h" #include "network/multi.h" #include "parse/parselo.h" #include "scripting/doc_html.h" #include "scripting/doc_json.h" #include "scripting/doc_luastub.h" #include "scripting/global_hooks.h" #include "scripting/scripting_doc.h" #include "ship/ship.h" #include "tracing/tracing.h" #include "weapon/beam.h" #include "weapon/weapon.h" using namespace scripting; // tehe. Declare the main event script_state Script_system("FS2_Open Scripting"); bool Output_scripting_meta = false; bool Output_scripting_json = false; bool Output_scripting_luastub = false; bool Scripting_game_init_run = false; flag_def_list Script_conditions[] = { {"State", CHC_STATE, 0}, {"Campaign", CHC_CAMPAIGN, 0}, {"Mission", CHC_MISSION, 0}, {"KeyPress", CHC_KEYPRESS, 0}, {"Version", CHC_VERSION, 0}, {"Application", CHC_APPLICATION, 0}, {"Multi type", CHC_MULTI_SERVER, 0}, {"VR device", CHC_VR_MODE, 0} }; int Num_script_conditions = sizeof(Script_conditions) / sizeof(flag_def_list); // clang-format off static HookVariableDocumentation GlobalVariables[] = { { "Player", "object", "The player object in a mission. Does not need to be a ship (e.g. in multiplayer). Not " "present if not in a game play state." }, }; // clang-format on int scripting_state_inited = 0; //*************************Scripting init and handling************************* // ditto bool script_hook_valid(script_hook* hook) { return hook->hook_function.function.isValid(); } void script_parse_table(const char* filename) { script_state* st = &Script_system; try { read_file_text(filename, CF_TYPE_TABLES); reset_parse(); if (optional_string("#Global Hooks")) { if (optional_string("$Global:")) { extern const std::shared_ptr> OnFrameHook; st->ParseGlobalChunk(CHA_ONFRAME, "Global", OnFrameHook); } if (optional_string("$Splash:")) { st->ParseGlobalChunk(CHA_SPLASHSCREEN, "Splash", hooks::OnSplashScreen); } if (optional_string("$GameInit:")) { st->ParseGlobalChunk(CHA_GAMEINIT, "GameInit", hooks::OnGameInit); } if (optional_string("$Simulation:")) { st->ParseGlobalChunk(CHA_SIMULATION, "Simulation", hooks::OnSimulation); } if (optional_string("$HUD:")) { st->ParseGlobalChunk(CHA_HUDDRAW, "HUD", hooks::OnHudDraw); } required_string("#End"); } if (optional_string("#Conditional Hooks")) { while (st->ParseCondition(filename)); required_string("#End"); } st->ProcessAddedHooks(); } catch (const parse::ParseException& e) { mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what())); return; } } void script_parse_lua_script(const char *filename) { using namespace luacpp; CFILE *cfp = cfopen(filename, "rb", CF_TYPE_TABLES); if(cfp == nullptr) { Warning(LOCATION, "Could not open lua script file '%s'", filename); return; } int len = cfilelength(cfp); SCP_string source; source.resize((size_t) len); cfread(&source[0], len, 1, cfp); cfclose(cfp); try { auto function = LuaFunction::createFromCode(Script_system.GetLuaSession(), source, filename); function.setErrorFunction(LuaFunction::createFromCFunction(Script_system.GetLuaSession(), ade_friendly_error)); script_function func; func.language = SC_LUA; func.function = std::move(function); Script_system.AddGameInitFunction(std::move(func)); } catch (const LuaException& e) { LuaError(Script_system.GetLuaSession(), "Failed to parse %s: %s", filename, e.what()); } } // Initializes the (global) scripting system, as well as any subsystems. // script_close is handled by destructors void script_init() { mprintf(("SCRIPTING: Beginning initialization sequence...\n")); mprintf(("SCRIPTING: Beginning Lua initialization...\n")); Script_system.CreateLuaState(); if (Output_scripting_meta || Output_scripting_json || Output_scripting_luastub) { const auto doc = Script_system.OutputDocumentation([](const SCP_string& error) { mprintf(("Scripting documentation: Error while parsing\n%s(This is only relevant for coders)\n\n", error.c_str())); }); if (Output_scripting_meta) { mprintf(("SCRIPTING: Outputting scripting metadata...\n")); scripting::output_html_doc(doc, "scripting.html"); } if (Output_scripting_json) { mprintf(("SCRIPTING: Outputting scripting metadata in JSON format...\n")); scripting::output_json_doc(doc, "scripting.json"); } if (Output_scripting_luastub) { mprintf(("SCRIPTING: Outputting scripting metadata in LUA Stub format...\n")); scripting::output_luastub_doc(doc, "scripting.lua"); } } mprintf(("SCRIPTING: Beginning main hook parse sequence....\n")); script_parse_table("scripting.tbl"); parse_modular_table(NOX("*-sct.tbm"), script_parse_table); mprintf(("SCRIPTING: Parsing pure Lua scripts\n")); parse_modular_table(NOX("*-sct.lua"), script_parse_lua_script); mprintf(("SCRIPTING: Initialization complete.\n")); } /* //WMC - Doesn't work as debug console interferes with any non-alphabetic chars. DCF(script, "Evaluates a line of scripting") { if(Dc_command) { dc_get_arg(ARG_STRING); Script_system.EvalString(Dc_arg); } if(Dc_help) { dc_printf("Usage: script -- Scripting to evaluate.\n"); } } */ //*************************CLASS: ConditionedScript************************* extern char Game_current_mission_filename[]; static bool global_condition_valid(const script_condition& condition) { switch (condition.condition_type) { case CHC_STATE: if (gameseq_get_depth() < 0) return false; if (gameseq_get_state() != condition.condition_cached_value) return false; break; case CHC_MISSION: { // WMC - Get mission filename with Mission_filename // I don't use Game_current_mission_filename, because // Mission_filename is valid in both fs2_open and FRED size_t len = strlen(Mission_filename); if (!len) return false; if (len > 4 && !stricmp(&Mission_filename[len - 4], ".fs2")) len -= 4; if (strnicmp(condition.condition_string.c_str(), Mission_filename, len) != 0) return false; break; } case CHC_CAMPAIGN: { size_t len = strlen(Campaign.filename); if (!len) return false; if (len > 4 && !stricmp(&Mission_filename[len - 4], ".fc2")) len -= 4; if (strnicmp(condition.condition_string.c_str(), Mission_filename, len) != 0) return false; break; } case CHC_KEYPRESS: { extern int Current_key_down; if (gameseq_get_depth() < 0) return false; if (Current_key_down == 0) return false; //Remove key masks that the API does not check against int key_down_modifier = ~KEY_CTRLED & ~KEY_MASK & Current_key_down; //Pretend that debug is the same as cheat if (key_down_modifier & KEY_DEBUGGED) key_down_modifier = (key_down_modifier & ~KEY_DEBUGGED) | KEY_DEBUGGED1; //For reasons only known to Volition, LCtrl and RCtrl are differentiated in name, while Alt and Shift are not. //As only the first of these identical names will be matched, replace the R versions with the L versions int key_down = Current_key_down & KEY_MASK; switch(key_down) { case KEY_RALT: key_down = KEY_LALT; break; case KEY_RSHIFT: key_down = KEY_LSHIFT; break; default: break; } return condition.condition_cached_value == (key_down | key_down_modifier); } case CHC_VERSION: { // Already evaluated on script load, stored value is 1 if application matches condition, 0 if not. if (condition.condition_cached_value == 0) { return false; } break; } case CHC_APPLICATION: { // Already evaluated on script load, stored value is 1 if application matches condition, 0 if not. if (condition.condition_cached_value == 0) { return false; } break; } case CHC_MULTI_SERVER: { // condition_cached_value is 0 if we execute on clients, 1 on servers return static_cast(condition.condition_cached_value == 1) == static_cast(MULTIPLAYER_MASTER); } case CHC_VR_MODE: { return static_cast(condition.condition_cached_value == 1) == openxr_enabled(); } default: break; } return true; } int cache_condition(ConditionalType type, const SCP_string& value){ //Since string comparisons are expensive and these hooks have to be checked very frequently //where possible whatever string comparison is done here and the outcome stored for later //nature of value stored depends on condition type. switch (type) { case CHC_STATE: return gameseq_get_state_idx(value.c_str()); case CHC_VERSION: { return gameversion::parse_version_inline() == gameversion::get_executable_version() ? 1 : 0; } case CHC_APPLICATION: { if (Fred_running) { if (stricmp("FRED2_Open", value.c_str()) != 0 && stricmp("FRED2Open", value.c_str()) != 0 && stricmp("FRED 2", value.c_str()) != 0 && stricmp("FRED", value.c_str()) != 0) return 0; else return 1; } else { if (stricmp("FS2_Open", value.c_str()) != 0 && stricmp("FS2Open", value.c_str()) != 0 && stricmp("Freespace 2", value.c_str()) != 0 && stricmp("Freespace", value.c_str()) != 0) return 0; else return 1; } } case CHC_MULTI_SERVER: { if (stricmp("Server", value.c_str()) == 0 || stricmp("Master", value.c_str()) == 0) { return 1; } else { return 0; } } case CHC_VR_MODE: { if (stricmp("VR", value.c_str()) == 0 || stricmp("HMD", value.c_str()) == 0 || stricmp("enabled", value.c_str()) == 0) { return 1; } else { return 0; } } case CHC_KEYPRESS: { int keycode = 0; //Technically, keys can be also CTRLED and DEBUGGED, but since the API never made a distinction, they will not be cached and filtered later if (value.find("Cheat") != SCP_string::npos) { keycode |= KEY_DEBUGGED1; } if (value.find("Alt") != SCP_string::npos) { keycode |= KEY_ALTED; } if (value.find("Shift") != SCP_string::npos) { keycode |= KEY_SHIFTED; } //Now, if Alt / Shift is ONLY the modifer, remove them here. If they are the only key pressed, the modifier still needs to be enabled, but the key also needs matching SCP_string key_copy = value; if (key_copy.rfind("Cheat-", 0) == 0){ key_copy = key_copy.substr(6); } if (key_copy.rfind("Alt-", 0) == 0){ key_copy = key_copy.substr(4); } if (key_copy.rfind("Shift-", 0) == 0){ key_copy = key_copy.substr(6); } bool foundKey = false; for (int key = 0; key < NUM_KEYS; key++){ extern const char *Scan_code_text_english[]; if (stricmp(Scan_code_text_english[key], key_copy.c_str()) == 0) { keycode |= key & KEY_MASK; foundKey = true; break; } } if (!foundKey) { Warning(LOCATION, "No key %s found for %s in conditional hook! The hook will not trigger!", key_copy.c_str(), value.c_str()); return -1; } return keycode; } default: return -1; } } //*************************CLASS: script_state************************* //Most of the icky stuff is here. Lots of #ifdefs //WMC - defined in parse/scripting.h void script_state::SetHookObject(const char *name, object *objp) { SetHookObjects(1, name, objp); } void script_state::SetHookObjects(int num, ...) { if (LuaState == nullptr) { return; } va_list vl; va_start(vl, num); for (int i = 0; i < num; i++) { char* name = va_arg(vl, char*); object* objp = va_arg(vl, object*); ade_set_object_with_breed(LuaState, OBJ_INDEX(objp)); auto reference = luacpp::UniqueLuaReference::create(LuaState); lua_pop(LuaState, 1); // Remove object value from the stack HookVariableValues[name].push_back(std::move(reference)); } va_end(vl); } void script_state::RemHookVar(const char* name) { this->RemHookVars({name}); } void script_state::RemHookVars(std::initializer_list names) { if (LuaState != nullptr) { for (const auto& hookVar : names) { if (HookVariableValues[hookVar].empty()) { // Nothing to do continue; } HookVariableValues[hookVar].pop_back(); } } } const SCP_unordered_map>& script_state::GetHookVariableReferences() { return HookVariableValues; } int script_state::LoadBm(const char* name) { for(int i = 0; i < (int)ScriptImages.size(); i++) { if(!stricmp(name, ScriptImages[i].fname)) return ScriptImages[i].handle; } image_desc id; int idx = bm_load(name); if(idx > -1) { id.handle = idx; strcpy_s(id.fname, name); ScriptImages.push_back(id); } return idx; } void script_state::UnloadImages() { for(int i = 0; i < (int)ScriptImages.size(); i++) { bm_release(ScriptImages[i].handle); } ScriptImages.clear(); } int script_state::RunCondition(int action_type, const std::any& local_condition_data) { TRACE_SCOPE(tracing::LuaHooks); int num = 0; if (LuaState == nullptr) { return num; } auto action_it = ConditionalHooks.find(action_type); if (action_it == ConditionalHooks.end()) return num; for(const auto& action : action_it->second) { if (action.ConditionsValid(local_condition_data)) { RunBytecode(action.hook.hook_function); num++; } } ProcessAddedHooks(); return num; } bool script_state::IsConditionOverride(int action_type, const std::any& local_condition_data) { auto action_it = ConditionalHooks.find(action_type); if (action_it == ConditionalHooks.end()) return false; for (const auto& action : action_it->second) { if (action.ConditionsValid(local_condition_data)) { if (IsOverride(action.hook)) return true; } } return false; } void script_state::Clear() { // Free all lua value references ConditionalHooks.clear(); HookVariableValues.clear(); AssayActions(); if (LuaState != nullptr) { OnStateDestroy(LuaState); lua_close(LuaState); } StateName[0] = '\0'; Langs = 0; //Don't close this yet LuaState = NULL; LuaLibs = NULL; } script_state::script_state(const char *name) { auto len = sizeof(StateName); strncpy(StateName, name, len); StateName[len - 1] = 0; Langs = 0; LuaState = NULL; LuaLibs = NULL; } script_state::~script_state() { Clear(); } void script_state::SetLuaSession(lua_State *L) { if (LuaState != nullptr) { lua_close(LuaState); } LuaState = L; if (LuaState != nullptr) { Langs |= SC_LUA; } else if(Langs & SC_LUA) { Langs &= ~SC_LUA; } } ScriptingDocumentation script_state::OutputDocumentation(const scripting::DocumentationErrorReporter& errorReporter) { ScriptingDocumentation doc; doc.name = StateName; // Conditions doc.conditions.reserve(static_cast(Num_script_conditions)); for (int32_t i = 0; i < Num_script_conditions; i++) { doc.conditions.emplace_back(Script_conditions[i].name); } // Global variables doc.globalVariables.assign(std::begin(GlobalVariables), std::end(GlobalVariables)); // Actions auto sortedHooks = scripting::getHooks(); std::sort(sortedHooks.begin(), sortedHooks.end(), [](const scripting::HookBase* left, const scripting::HookBase* right) { return left->getHookName() < right->getHookName(); }); for (const auto& hook : sortedHooks) { doc.actions.push_back( {hook->getHookName(), hook->getDescription(), hook->getParameters(), hook->_conditions, hook->getDeprecation(), hook->isOverridable()}); } OutputLuaDocumentation(doc, errorReporter); return doc; } void script_state::ParseChunkSub(script_function& script_func, const char* debug_str) { using namespace luacpp; Assert(debug_str != NULL); //Lua script_func.language = SC_LUA; std::string source; std::string function_name(debug_str); if(check_for_string("[[")) { //Lua from file char *filename = alloc_block("[[", "]]"); //Load from file CFILE *cfp = cfopen(filename, "rb", CF_TYPE_SCRIPTS ); //WMC - use filename instead of debug_str so that the filename gets passed. function_name = filename; vm_free(filename); if(cfp == NULL) { Warning(LOCATION, "Could not load lua script file '%s'", function_name.c_str()); return; } else { int len = cfilelength(cfp); source.resize((size_t) len); cfread(&source[0], len, 1, cfp); cfclose(cfp); } } else if(check_for_string("[")) { //Lua string // Determine the current line in the file so that the Lua source can begin at the same line as in the table // This will make sure that the line in the error message matches the line number in the table. auto line = get_line_num(); //Allocate raw script char* raw_lua = alloc_block("[", "]", 1); //WMC - minor hack to make sure that the last line gets //executed properly. In testing, I couldn't reproduce Nuke's //crash, so this is here just to be on the safe side. strcat(raw_lua, "\n"); for (auto i = 1; i <= line; ++i) { source += "\n"; } source += raw_lua; vm_free(raw_lua); } else { std::string buf; //Stuff it stuff_string(buf, F_RAW); source = "return "; source += buf; } try { auto function = LuaFunction::createFromCode(LuaState, source, function_name); function.setErrorFunction(LuaFunction::createFromCFunction(LuaState, ade_friendly_error)); script_func.function = std::move(function); } catch (const LuaException& e) { LuaError(GetLuaSession(), "%s", e.what()); } } void script_state::ParseChunk(script_hook *dest, const char *debug_str) { static int total_parse_calls = 0; char debug_buf[128]; total_parse_calls++; //DANGER! This code means the debug_str must be used only before parsing if(debug_str == NULL) { sprintf(debug_buf, "script_parse() count %d", total_parse_calls); debug_str = debug_buf; } ParseChunkSub(dest->hook_function, debug_str); if(optional_string("+Override:")) { size_t bufSize = strlen(debug_str) + 10; char *debug_str_over = (char*)vm_malloc(bufSize); strcpy_s(debug_str_over, bufSize, debug_str); strcat_s(debug_str_over, bufSize, " override"); ParseChunkSub(dest->override_function, debug_str_over); vm_free(debug_str_over); } } bool script_state::EvalString(const char* string, const char* debug_str) { using namespace luacpp; size_t string_size = strlen(string); char lastchar = string[string_size - 1]; if (string[0] == '{') { return false; } if (string[0] == '[' && lastchar != ']') { return false; } size_t s_bufSize = string_size + 8; std::string s; s.reserve(s_bufSize); if (string[0] != '[') { s += string; } else { s.assign(string + 1, string + string_size); } SCP_string debug_name; if (debug_str == nullptr) { debug_name = "String: "; debug_name += s; } else { debug_name = debug_str; } try { auto function = LuaFunction::createFromCode(LuaState, s, debug_name); function.setErrorFunction(LuaFunction::createFromCFunction(LuaState, scripting::ade_friendly_error)); try { function.call(LuaState); } catch (const LuaException&) { return false; } } catch (const LuaException& e) { LuaError(GetLuaSession(), "%s", e.what()); return false; } return true; } int script_state::RunBytecode(const script_function& hd) { using namespace luacpp; if (!hd.function.isValid()) { return 1; } GR_DEBUG_SCOPE("Lua code"); try { hd.function.call(LuaState); } catch (const LuaException&) { return 0; } return 1; } ConditionalType script_parse_condition() { char buf[NAME_LENGTH]; for (int i = 0; i < Num_script_conditions; i++) { sprintf(buf, "$%s:", Script_conditions[i].name); if(optional_string(buf)) return static_cast(Script_conditions[i].def); } return CHC_NONE; } const scripting::HookBase* script_parse_action() { for (const auto& action : scripting::getHooks()) { SCP_string buf; sprintf(buf, "$%s:", action->getHookName().c_str()); if (optional_string(buf.c_str())) return action; } return nullptr; } const HookBase* scripting_string_to_action(const char* action) { for (const auto& hook : scripting::getHooks()) { if (hook->getHookName() == action) return hook; } return nullptr; } ConditionalType scripting_string_to_condition(const char* condition) { for (int i = 0; i < Num_script_conditions; i++) { if (!stricmp(Script_conditions[i].name, condition)) { return static_cast(Script_conditions[i].def); } } return CHC_NONE; } bool script_action::ConditionsValid(const std::any& local_condition_data) const { for (const auto& global_condition : global_conditions) { if (!global_condition_valid(global_condition)) return false; } for (const auto& local_condition : local_conditions) { if (!local_condition->evaluate(local_condition_data)) return false; } return true; }; void script_state::ParseGlobalChunk(ConditionalActions hookType, const char* debug_str, const std::shared_ptr parentHook) { script_action sat; ParseChunk(&sat.hook, debug_str); if (parentHook && parentHook->getDeprecation()) { const auto& deprecation = *parentHook->getDeprecation(); bool shownWarn = false; if (sat.hook.hook_function.function.isValid()) { if (deprecation.level_hook == HookDeprecationOptions::DeprecationLevel::LEVEL_ERROR) { error_display(1, "Hook '%s' is removed since version %s and cannot be used!", parentHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else if (mod_supports_version(deprecation.deprecatedSince)) { error_display(1, "Hook '%s' is deprecated since version %s and cannot be used if the mod targets that version or higher!", parentHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else { error_display(0, "Hook '%s' is deprecated from version %s and should be replaced!", parentHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); shownWarn = true; } } if (sat.hook.override_function.function.isValid()) { if (deprecation.level_override == HookDeprecationOptions::DeprecationLevel::LEVEL_ERROR) { error_display(1, "Overriding Hook '%s' is removed since version %s and cannot be used!", parentHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else if (mod_supports_version(deprecation.deprecatedSince)) { error_display(1, "Overriding Hook '%s' is deprecated since version %s and cannot be used if the mod targets that version or higher!", parentHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else if (!shownWarn) { error_display(0, "Overriding Hook '%s' is deprecated from version %s and should be replaced!", parentHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } } } ConditionalHooks[hookType].emplace_back(std::move(sat)); } bool script_state::ParseCondition(const char *filename) { SCP_vector parsed_conditions; SCP_vector conditions; //First, is this a script condition? const HookBase* currHook = nullptr; //As long as we don't get hooks, its gotta be a condition while ((currHook = script_parse_action()) == nullptr) { auto condition = script_parse_condition(); if (condition != CHC_NONE) { //It's a global condition SCP_string condition_string; stuff_string(condition_string, F_NAME); int cache = cache_condition(condition, condition_string); parsed_conditions.emplace_back(script_condition{ condition, std::move(condition_string), cache }); } else { if (check_for_string("#End") || check_for_eof()) { //There was no action here, but an EOF if(!parsed_conditions.empty() || !conditions.empty()) error_display(1, "No actions specified for conditional hook in file '%s'", filename); return false; } //It's a local condition. Store it once we find hooks. SCP_string condition_string; stuff_string(condition_string, F_RAW); conditions.emplace_back(std::move(condition_string)); } } do { int hookId = currHook->getHookId(); script_action sat; // WMC - build error string SCP_string buf; sprintf(buf, "%s - %s", filename, currHook->getHookName().c_str()); ParseChunk(&sat.hook, buf.c_str()); sat.global_conditions = parsed_conditions; for (const SCP_string& local_condition : conditions) { bool found = false; pause_parse(); SCP_vm_unique_ptr parse{ vm_strdup(local_condition.c_str()) }; reset_parse(parse.get()); // coverity[escape:FALSE] - this is okay because the pointer escape only lasts until unpause_parse() restores the old state for (const auto& potential_condition : currHook->_conditions) { SCP_string bufCond; sprintf(bufCond, "$%s:", potential_condition.first.c_str()); if (optional_string(bufCond.c_str())) { SCP_string arg; stuff_string(arg, F_NAME); sat.local_conditions.emplace_back(potential_condition.second->parse(arg)); found = true; break; } } unpause_parse(); if (!found) { error_display(0, "Condition '%s' is not valid for hook '%s'. The hook will not evaluate!", local_condition.c_str(), currHook->getHookName().c_str()); sat.local_conditions.emplace_back(ParseableCondition().parse(local_condition)); continue; } } if (currHook->getDeprecation()) { const auto& deprecation = *currHook->getDeprecation(); bool shownWarn = false; if (sat.hook.hook_function.function.isValid()) { if (deprecation.level_hook == HookDeprecationOptions::DeprecationLevel::LEVEL_ERROR) { error_display(1, "Hook '%s' is removed since version %s and cannot be used!", currHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else if (mod_supports_version(deprecation.deprecatedSince)) { error_display(1, "Hook '%s' is deprecated since version %s and cannot be used if the mod targets that version or higher!", currHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else { error_display(0, "Hook '%s' is deprecated from version %s and should be replaced!", currHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); shownWarn = true; } } if (sat.hook.override_function.function.isValid()) { if (deprecation.level_override == HookDeprecationOptions::DeprecationLevel::LEVEL_ERROR) { error_display(1, "Overriding Hook '%s' is removed since version %s and cannot be used!", currHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else if (mod_supports_version(deprecation.deprecatedSince)) { error_display(1, "Overriding Hook '%s' is deprecated since version %s and cannot be used if the mod targets that version or higher!", currHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } else if(!shownWarn) { error_display(0, "Overriding Hook '%s' is deprecated from version %s and should be replaced!", currHook->getHookName().c_str(), gameversion::format_version(deprecation.deprecatedSince).c_str()); } } } AddConditionedHook(hookId, std::move(sat)); } while ((currHook = script_parse_action()) != nullptr); return true; } void script_state::AddConditionedHook(int action_id, script_action hook) { AddedHooks[action_id].emplace_back(std::move(hook)); } void script_state::ProcessAddedHooks() { for (auto& hook : AddedHooks) { auto& conditionalHooks = ConditionalHooks[hook.first]; conditionalHooks.insert(conditionalHooks.end(), std::make_move_iterator(hook.second.begin()), std::make_move_iterator(hook.second.end())); } AddedHooks.clear(); AssayActions(); } void script_state::AddGameInitFunction(script_function func) { GameInitFunctions.push_back(std::move(func)); } // For each possible script_action this maintains an array that records whether any scripts are actually using this action // This allows us to avoid significant overhead from checking everything at the potential hook sites, but you must call // AssayActions() after modifying ConditionalHooks before returning to normal operation of the scripting system! void script_state::AssayActions() { ActiveActions.clear(); for (const auto &hook : ConditionalHooks) { ActiveActions[hook.first] = !hook.second.empty(); } } bool script_state::IsActiveAction(int action_id) { auto entry = ActiveActions.find(action_id); if (entry != ActiveActions.end()) return entry->second; else return false; } bool script_state::IsOverride(const script_hook &hd) { if(!hd.hook_function.function.isValid()) return false; bool b=false; RunBytecode(hd.override_function, 'b', &b); return b; } void script_state::RunInitFunctions() { for (const auto& initFunc : GameInitFunctions) { initFunc.function(LuaState); } // We don't need this anymore so no need to keep references to those functions around anymore GameInitFunctions.clear(); } void scripting_state_init() { // nothing to do here if (scripting_state_inited) return; gr_set_clear_color(0, 0, 0); scripting_state_inited = 1; } void scripting_state_close() { if (!scripting_state_inited) return; game_flush(); scripting_state_inited = 0; } void scripting_state_do_frame(float /*frametime*/, bool doKeys) { // just incase something is wrong if (!scripting_state_inited) return; gr_reset_clip(); gr_clear(); gr_flip(); if (!doKeys) return; // process keys int k = game_check_key() & ~KEY_DEBUGGED; switch (k) { case KEY_ESC: gameseq_post_event(GS_EVENT_MAIN_MENU); return; } }