#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 "hud/hud.h" #include "io/key.h" #include "libs/jansson.h" #include "mission/missioncampaign.h" #include "parse/parselo.h" #include "ship/ship.h" #include "weapon/beam.h" #include "weapon/weapon.h" #include #include 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; flag_def_list Script_conditions[] = { {"State", CHC_STATE, 0}, {"Campaign", CHC_CAMPAIGN, 0}, {"Mission", CHC_MISSION, 0}, {"Object Type", CHC_OBJECTTYPE, 0}, {"Ship", CHC_SHIP, 0}, {"Ship class", CHC_SHIPCLASS, 0}, {"Ship type", CHC_SHIPTYPE, 0}, {"Weapon class", CHC_WEAPONCLASS, 0}, {"KeyPress", CHC_KEYPRESS, 0}, {"Action", CHC_ACTION, 0}, {"Version", CHC_VERSION, 0}, {"Application", CHC_APPLICATION, 0}, }; int Num_script_conditions = sizeof(Script_conditions) / sizeof(flag_def_list); class BuiltinHook : public scripting::HookBase { public: BuiltinHook(SCP_string hookName, int32_t hookId) : HookBase(std::move(hookName), SCP_string(), SCP_vector(), hookId) { } ~BuiltinHook() override = default; bool isOverridable() const override { return true; } }; // clang-format off static USED_VARIABLE SCP_vector> Script_actions { std::make_shared("On Game Init", CHA_GAMEINIT ), std::make_shared("On Splash Screen", CHA_SPLASHSCREEN ), std::make_shared("On State Start", CHA_ONSTATESTART ), std::make_shared("On Action", CHA_ONACTION ), std::make_shared("On Action Stopped", CHA_ONACTIONSTOPPED ), std::make_shared("On Key Pressed", CHA_KEYPRESSED ), std::make_shared("On Key Released", CHA_KEYRELEASED ), std::make_shared("On Mouse Moved", CHA_MOUSEMOVED ), std::make_shared("On Mouse Pressed", CHA_MOUSEPRESSED ), std::make_shared("On Mouse Released", CHA_MOUSERELEASED ), std::make_shared("On State End", CHA_ONSTATEEND ), std::make_shared("On Mission Start", CHA_MISSIONSTART ), std::make_shared("On HUD Draw", CHA_HUDDRAW ), std::make_shared("On Ship Collision", CHA_COLLIDESHIP ), std::make_shared("On Weapon Collision", CHA_COLLIDEWEAPON ), std::make_shared("On Debris Collision", CHA_COLLIDEDEBRIS ), std::make_shared("On Asteroid Collision", CHA_COLLIDEASTEROID ), std::make_shared("On Object Render", CHA_OBJECTRENDER ), std::make_shared("On Death", CHA_DEATH ), std::make_shared("On Mission End", CHA_MISSIONEND ), std::make_shared("On Weapon Delete", CHA_ONWEAPONDELETE ), std::make_shared("On Weapon Equipped", CHA_ONWPEQUIPPED ), std::make_shared("On Weapon Fired", CHA_ONWPFIRED ), std::make_shared("On Weapon Selected", CHA_ONWPSELECTED ), std::make_shared("On Weapon Deselected", CHA_ONWPDESELECTED ), std::make_shared("On Gameplay Start", CHA_GAMEPLAYSTART ), std::make_shared("On Turret Fired", CHA_ONTURRETFIRED ), std::make_shared("On Primary Fire", CHA_PRIMARYFIRE ), std::make_shared("On Secondary Fire", CHA_SECONDARYFIRE ), std::make_shared("On Ship Arrive", CHA_ONSHIPARRIVE ), std::make_shared("On Beam Collision", CHA_COLLIDEBEAM ), std::make_shared("On Afterburner Engage", CHA_AFTERBURNSTART ), std::make_shared("On Afterburner Stop", CHA_AFTERBURNEND ), std::make_shared("On Beam Fire", CHA_BEAMFIRE ), std::make_shared("On Simulation", CHA_SIMULATION ), std::make_shared("On Load Screen", CHA_LOADSCREEN ), std::make_shared("On Campaign Mission Accept", CHA_CMISSIONACCEPT ), std::make_shared("On Ship Depart", CHA_ONSHIPDEPART ), std::make_shared("On Weapon Created", CHA_ONWEAPONCREATED ), std::make_shared("On Waypoints Done", CHA_ONWAYPOINTSDONE ), std::make_shared("On Subsystem Destroyed", CHA_ONSUBSYSDEATH ), std::make_shared("On Goals Cleared", CHA_ONGOALSCLEARED ), std::make_shared("On Briefing Stage", CHA_ONBRIEFSTAGE ), }; 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; HookVariableDocumentation::HookVariableDocumentation(const char* name_, ade_type_info type_, const char* description_) : name(name_), type(std::move(type_)), description(description_) { } //*************************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:")) { st->ParseGlobalChunk(CHA_ONFRAME, "Global"); } if (optional_string("$Splash:")) { st->ParseGlobalChunk(CHA_SPLASHSCREEN, "Splash"); } if (optional_string("$GameInit:")) { st->ParseGlobalChunk(CHA_GAMEINIT, "GameInit"); } if (optional_string("$Simulation:")) { st->ParseGlobalChunk(CHA_SIMULATION, "Simulation"); } if (optional_string("$HUD:")) { st->ParseGlobalChunk(CHA_HUDDRAW, "HUD"); } required_string("#End"); } if (optional_string("#Conditional Hooks")) { while (st->ParseCondition(filename)); required_string("#End"); } } 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", CFILE_NORMAL, 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 = function; Script_system.AddGameInitFunction(func); } catch (const LuaException& e) { LuaError(Script_system.GetLuaSession(), "Failed to parse %s: %s", filename, e.what()); } } static void json_doc_generate_class(json_t* elObj, const DocumentationElementClass* lib) { json_object_set_new(elObj, "superClass", json_string(lib->superClass.c_str())); } static json_t* json_doc_generate_return_type(const scripting::ade_type_info& type_info) { switch (type_info.getType()) { case ade_type_info_type::Empty: return json_string("void"); case ade_type_info_type::Simple: return json_string(type_info.getSimpleName()); case ade_type_info_type::Tuple: { json_t* tupleTypes = json_array(); for (const auto& type : type_info.elements()) { json_array_append_new(tupleTypes, json_doc_generate_return_type(type)); } return json_pack("{ssso}", "type", "tuple", "elements", tupleTypes); } case ade_type_info_type::Array: { return json_pack("{ssso}", "type", "list", "element", json_doc_generate_return_type(type_info.elements().front())); } case ade_type_info_type::Map: { return json_pack("{sssoso}", "type", "map", "key", json_doc_generate_return_type(type_info.elements()[0]), "value", json_doc_generate_return_type(type_info.elements()[1])); } case ade_type_info_type::Iterator: { return json_pack("{ssso}", "type", "iterator", "element", json_doc_generate_return_type(type_info.elements().front())); } case ade_type_info_type::Alternative: { json_t* alternativeTypes = json_array(); for (const auto& type : type_info.elements()) { json_array_append_new(alternativeTypes, json_doc_generate_return_type(type)); } return json_pack("{ssso}", "type", "alternative", "elements", alternativeTypes); } } UNREACHABLE("Unknown type type!"); return nullptr; } static void json_doc_generate_function(json_t* elObj, const DocumentationElementFunction* lib) { json_object_set_new(elObj, "returnType", json_doc_generate_return_type(lib->returnType)); json_object_set_new(elObj, "parameters", json_string(lib->parameters.c_str())); json_object_set_new(elObj, "returnDocumentation", json_string(lib->returnDocumentation.c_str())); } static void json_doc_generate_property(json_t* elObj, const DocumentationElementProperty* lib) { json_object_set_new(elObj, "getterType", json_doc_generate_return_type(lib->getterType)); json_object_set_new(elObj, "setterType", json_string(lib->setterType.c_str())); json_object_set_new(elObj, "returnDocumentation", json_string(lib->returnDocumentation.c_str())); } static json_t* json_doc_generate_elements(const SCP_vector>& elements); static json_t* json_doc_generate_element(const std::unique_ptr& element) { json_t* elementsObj = json_object(); json_object_set_new(elementsObj, "name", json_string(element->name.c_str())); json_object_set_new(elementsObj, "shortName", json_string(element->shortName.c_str())); json_object_set_new(elementsObj, "description", json_string(element->description.c_str())); switch (element->type) { case ElementType::Library: json_object_set_new(elementsObj, "type", json_string("library")); break; case ElementType::Class: json_object_set_new(elementsObj, "type", json_string("class")); json_doc_generate_class(elementsObj, static_cast(element.get())); break; case ElementType::Function: json_object_set_new(elementsObj, "type", json_string("function")); json_doc_generate_function(elementsObj, static_cast(element.get())); break; case ElementType::Operator: json_object_set_new(elementsObj, "type", json_string("operator")); json_doc_generate_function(elementsObj, static_cast(element.get())); break; case ElementType::Property: json_object_set_new(elementsObj, "type", json_string("property")); json_doc_generate_property(elementsObj, static_cast(element.get())); break; default: json_object_set_new(elementsObj, "type", json_string("unknown")); break; } json_object_set_new(elementsObj, "children", json_doc_generate_elements(element->children)); return elementsObj; } static json_t* json_doc_generate_elements(const SCP_vector>& elements) { json_t* elementsArray = json_array(); for (const auto& el : elements) { json_array_append_new(elementsArray, json_doc_generate_element(el)); } return elementsArray; } static json_t* json_doc_generate_param_element(const HookVariableDocumentation& param) { return json_pack("{s:s,s:s,s:o}", "name", param.name, "description", param.description, "type", json_doc_generate_return_type(param.type)); } static json_t* json_doc_generate_action_element(const DocumentationAction& action) { json_t* paramArray = json_array(); for (const auto& param : action.parameters) { json_array_append_new(paramArray, json_doc_generate_param_element(param)); } return json_pack("{s:s,s:s,s:o,s:b}", "name", action.name.c_str(), "description", action.description.c_str(), "hookVars", paramArray, "overridable", action.overridable); } static void documentation_to_json(const ScriptingDocumentation& doc) { std::unique_ptr root(json_object()); // Should be incremented every time an incompatible change is made json_object_set_new(root.get(), "version", json_integer(2)); { json_t* actionArray = json_array(); for (const auto& action : doc.actions) { json_array_append_new(actionArray, json_doc_generate_action_element(action)); } json_object_set_new(root.get(), "actions", actionArray); } { json_t* conditionArray = json_array(); for (const auto& cond : doc.conditions) { json_array_append_new(conditionArray, json_string(cond.c_str())); } json_object_set_new(root.get(), "conditions", conditionArray); } { json_t* enumObject = json_object(); for (const auto& enumVal : doc.enumerations) { json_object_set_new(enumObject, enumVal.name.c_str(), json_integer(enumVal.value)); } json_object_set_new(root.get(), "enums", enumObject); } { json_t* globalVariables = json_array(); for (const auto& param : doc.globalVariables) { json_array_append_new(globalVariables, json_doc_generate_param_element(param)); } json_object_set_new(root.get(), "globalVars", globalVariables); } json_object_set_new(root.get(), "elements", json_doc_generate_elements(doc.elements)); const auto jsonStr = json_dump_string(root.get(), JSON_INDENT(2) | JSON_SORT_KEYS); std::ofstream outStr("scripting.json"); outStr << jsonStr; } // 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) { const auto doc = Script_system.OutputDocumentation(); if (Output_scripting_meta) { mprintf(("SCRIPTING: Outputting scripting metadata...\n")); Script_system.OutputMeta("scripting.html"); } if (Output_scripting_json) { mprintf(("SCRIPTING: Outputting scripting metadata in JSON format...\n")); documentation_to_json(doc); exit(1); } } 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: Inititialization 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[]; bool ConditionedHook::AddCondition(script_condition *sc) { for(int i = 0; i < MAX_HOOK_CONDITIONS; i++) { if(Conditions[i].condition_type == CHC_NONE) { Conditions[i] = *sc; return true; } } return false; } bool ConditionedHook::AddAction(script_action *sa) { if(!script_hook_valid(&sa->hook)) return false; Actions.push_back(*sa); return true; } bool ConditionedHook::ConditionsValid(int action, object *objp, int more_data) { uint i; //Return false if any conditions are not met script_condition *scp; ship_info *sip; for(i = 0; i < MAX_HOOK_CONDITIONS; i++) { scp = &Conditions[i]; switch(scp->condition_type) { case CHC_STATE: if(gameseq_get_depth() < 0) return false; if(stricmp(GS_state_text[gameseq_get_state(0)], scp->condition_string.c_str()) != 0) return false; break; case CHC_SHIPTYPE: if(objp == NULL || objp->type != OBJ_SHIP) return false; sip = &Ship_info[Ships[objp->instance].ship_info_index]; if(sip->class_type < 0) return false; if(stricmp(Ship_types[sip->class_type].name, scp->condition_string.c_str()) != 0) return false; break; case CHC_SHIPCLASS: if(objp == NULL || objp->type != OBJ_SHIP) return false; if(stricmp(Ship_info[Ships[objp->instance].ship_info_index].name, scp->condition_string.c_str()) != 0) return false; break; case CHC_SHIP: if(objp == NULL || objp->type != OBJ_SHIP) return false; if(stricmp(Ships[objp->instance].ship_name, scp->condition_string.c_str()) != 0) 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(scp->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(scp->condition_string.c_str(), Mission_filename, len) != 0) return false; break; } case CHC_WEAPONCLASS: { if (action == CHA_COLLIDEWEAPON) { if (stricmp(Weapon_info[more_data].name, scp->condition_string.c_str()) != 0) return false; } else if (!(action == CHA_ONWPSELECTED || action == CHA_ONWPDESELECTED || action == CHA_ONWPEQUIPPED || action == CHA_ONWPFIRED || action == CHA_ONTURRETFIRED )) { if(objp == NULL || (objp->type != OBJ_WEAPON && objp->type != OBJ_BEAM)) return false; else if (( objp->type == OBJ_WEAPON) && (stricmp(Weapon_info[Weapons[objp->instance].weapon_info_index].name, scp->condition_string.c_str()) != 0 )) return false; else if (( objp->type == OBJ_BEAM) && (stricmp(Weapon_info[Beams[objp->instance].weapon_info_index].name, scp->condition_string.c_str()) != 0 )) return false; } else if(objp == NULL || objp->type != OBJ_SHIP) { return false; } else if (action == CHA_ONWEAPONCREATED) { if (objp == nullptr || objp->type != OBJ_WEAPON) return false; } else { // Okay, if we're still here, then objp is both valid and a ship ship* shipp = &Ships[objp->instance]; bool primary = false, secondary = false, prev_primary = false, prev_secondary = false; switch (action) { case CHA_ONWPSELECTED: if (shipp->weapons.current_primary_bank >= 0) { primary = stricmp(Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.current_primary_bank]].name, scp->condition_string.c_str()) == 0; } if (shipp->weapons.current_secondary_bank >= 0) { secondary = stricmp(Weapon_info[shipp->weapons.secondary_bank_weapons[shipp->weapons.current_secondary_bank]].name, scp->condition_string.c_str()) == 0; } if (!(primary || secondary)) { return false; } if ((shipp->flags[Ship::Ship_Flags::Primary_linked]) && primary && (Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.current_primary_bank]].wi_flags[Weapon::Info_Flags::Nolink])) { return false; } break; case CHA_ONWPDESELECTED: if (shipp->weapons.current_primary_bank >= 0) { primary = stricmp(Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.current_primary_bank]].name, scp->condition_string.c_str()) == 0; } if (shipp->weapons.previous_primary_bank >= 0) { prev_primary = stricmp(Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.previous_primary_bank]].name, scp->condition_string.c_str()) == 0; } if (shipp->weapons.current_secondary_bank >= 0) { secondary = stricmp(Weapon_info[shipp->weapons.secondary_bank_weapons[shipp->weapons.current_secondary_bank]].name, scp->condition_string.c_str()) == 0; } if (shipp->weapons.previous_secondary_bank >= 0) { prev_secondary = stricmp(Weapon_info[shipp->weapons.secondary_bank_weapons[shipp->weapons.previous_secondary_bank]].name, scp->condition_string.c_str()) == 0; } if ((shipp->flags[Ship::Ship_Flags::Primary_linked]) && prev_primary && (Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.previous_primary_bank]].wi_flags[Weapon::Info_Flags::Nolink])) return true; if ( !prev_secondary && ! secondary && !prev_primary && !primary ) return false; if ( (!prev_secondary && !secondary) && (prev_primary && primary) ) return false; if ( (!prev_secondary && !secondary) && (!prev_primary && primary) ) return false; if ( (!prev_primary && !primary) && (prev_secondary && secondary) ) return false; if ( (!prev_primary && !primary) && (!prev_secondary && secondary) ) return false; break; case CHA_ONWPEQUIPPED: { bool equipped = false; for(int j = 0; j < MAX_SHIP_PRIMARY_BANKS; j++) { if (!equipped && (shipp->weapons.primary_bank_weapons[j] >= 0) && (shipp->weapons.primary_bank_weapons[j] < weapon_info_size()) ) { if (!stricmp(Weapon_info[shipp->weapons.primary_bank_weapons[j]].name, scp->condition_string.c_str())) { equipped = true; break; } } } if (!equipped) { for(int j = 0; j < MAX_SHIP_SECONDARY_BANKS; j++) { if (!equipped && (shipp->weapons.secondary_bank_weapons[j] >= 0) && (shipp->weapons.secondary_bank_weapons[j] < weapon_info_size()) ) { if (!stricmp(Weapon_info[shipp->weapons.secondary_bank_weapons[j]].name, scp->condition_string.c_str())) { equipped = true; break; } } } } if (!equipped) return false; break; } case CHA_ONWPFIRED: { if (more_data == 1) { primary = stricmp(Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.current_primary_bank]].name, scp->condition_string.c_str()) == 0; secondary = false; } else { primary = false; secondary = stricmp(Weapon_info[shipp->weapons.secondary_bank_weapons[shipp->weapons.current_secondary_bank]].name, scp->condition_string.c_str()) == 0; } if ((shipp->flags[Ship::Ship_Flags::Primary_linked]) && primary && (Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.current_primary_bank]].wi_flags[Weapon::Info_Flags::Nolink])) return false; return more_data == 1 ? primary : secondary; break; } case CHA_ONTURRETFIRED: { if (! (stricmp(Weapon_info[shipp->last_fired_turret->last_fired_weapon_info_index].name, scp->condition_string.c_str()) == 0)) return false; break; } case CHA_PRIMARYFIRE: { if (stricmp(Weapon_info[shipp->weapons.primary_bank_weapons[shipp->weapons.current_primary_bank]].name, scp->condition_string.c_str()) != 0) return false; break; } case CHA_SECONDARYFIRE: { if (stricmp(Weapon_info[shipp->weapons.secondary_bank_weapons[shipp->weapons.current_secondary_bank]].name, scp->condition_string.c_str()) != 0) return false; break; } case CHA_BEAMFIRE: { if (!(stricmp(Weapon_info[more_data].name, scp->condition_string.c_str()) == 0)) return false; break; } default: break; } } // case CHC_WEAPONCLASS break; } case CHC_OBJECTTYPE: if(objp == NULL) return false; if(stricmp(Object_type_names[objp->type], scp->condition_string.c_str()) != 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; //WMC - could be more efficient, but whatever. if(stricmp(textify_scancode(Current_key_down), scp->condition_string.c_str()) != 0) return false; break; } case CHC_ACTION: { if(gameseq_get_depth() < 0) return false; int action_index = more_data; if (action_index <= 0 || stricmp(scp->condition_string.c_str(), Control_config[action_index].text) != 0) return false; break; } case CHC_VERSION: { // Goober5000: I'm going to assume scripting doesn't care about SVN revision char buf[32]; sprintf(buf, "%i.%i.%i", FS_VERSION_MAJOR, FS_VERSION_MINOR, FS_VERSION_BUILD); if(stricmp(buf, scp->condition_string.c_str()) != 0) { //In case some people are lazy and say "3.7" instead of "3.7.0" or something if(FS_VERSION_BUILD == 0) { sprintf(buf, "%i.%i", FS_VERSION_MAJOR, FS_VERSION_MINOR); if(stricmp(buf, scp->condition_string.c_str()) != 0) return false; } else { return false; } } break; } case CHC_APPLICATION: { if(Fred_running) { if(stricmp("FRED2_Open", scp->condition_string.c_str()) != 0 && stricmp("FRED2Open", scp->condition_string.c_str()) != 0 && stricmp("FRED 2", scp->condition_string.c_str()) != 0 && stricmp("FRED", scp->condition_string.c_str()) != 0) return false; } else { if(stricmp("FS2_Open", scp->condition_string.c_str()) != 0 && stricmp("FS2Open", scp->condition_string.c_str()) != 0 && stricmp("Freespace 2", scp->condition_string.c_str()) != 0 && stricmp("Freespace", scp->condition_string.c_str()) != 0) return false; } } default: break; } } return true; } bool ConditionedHook::IsOverride(script_state* sys, int action) { Assert(sys != NULL); // bool b = false; //Do the actions for(SCP_vector::iterator sap = Actions.begin(); sap != Actions.end(); ++sap) { if (sap->action_type == action) { if (sys->IsOverride(sap->hook)) return true; } } return false; } bool ConditionedHook::Run(class script_state* sys, int action) { Assert(sys != NULL); // Do the actions for (auto & Action : Actions) { if (Action.action_type == action) { sys->RunBytecode(Action.hook.hook_function); } } return true; } //*************************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); if(this->OpenHookVarTable()) { int amt_ldx = lua_gettop(LuaState); 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)); int data_ldx = lua_gettop(LuaState); lua_pushstring(LuaState, name); lua_pushvalue(LuaState, data_ldx); lua_rawset(LuaState, amt_ldx); lua_pop(LuaState, 1); //data_ldx } this->CloseHookVarTable(); } else { LuaError(LuaState, "Could not get HookVariable library to add hook variables - get a coder"); } va_end(vl); } //This pair of abstraction functions handles //getting the table of ade member functions //for the hook library. //Call CloseHookVarTable() only if OpenHookVarTable() //returns true. (see below) static int ohvt_poststack = 0; //Items on the stack prior to OHVT static int ohvt_isopen = 0; //Items OHVT puts on the stack bool script_state::OpenHookVarTable() { if(ohvt_isopen) Error(LOCATION, "OpenHookVarTable was called twice with no call to CloseHookVarTable - missing call ahoy!"); lua_pushstring(LuaState, "hv"); lua_gettable(LuaState, LUA_GLOBALSINDEX); int sv_ldx = lua_gettop(LuaState); if(lua_isuserdata(LuaState, sv_ldx)) { //Get ScriptVar metatable lua_getmetatable(LuaState, sv_ldx); int mtb_ldx = lua_gettop(LuaState); if(lua_istable(LuaState, mtb_ldx)) { //Get ScriptVar/metatable/__ademembers lua_pushstring(LuaState, "__ademembers"); lua_rawget(LuaState, mtb_ldx); int amt_ldx = lua_gettop(LuaState); if(lua_istable(LuaState, amt_ldx)) { ohvt_isopen = 3; ohvt_poststack = amt_ldx; return true; } lua_pop(LuaState, 1); //amt } lua_pop(LuaState, 1); //metatable } lua_pop(LuaState, 1); //Library return false; } //Call when you are done with CloseHookVarTable, //and you have removed all other objects 'above' //the stack objects generated by the call to OpenHookVarTable. bool script_state::CloseHookVarTable() { if(!ohvt_isopen) { Error(LOCATION, "CloseHookVarTable was called with no associated call to OpenHookVarTable"); } int top_ldx = lua_gettop(LuaState); if(top_ldx >= ohvt_poststack) { lua_pop(LuaState, ohvt_isopen); ohvt_isopen = 0; return true; } else { Error(LOCATION, "CloseHookVarTable() was called with too few objects on the stack; get a coder. (Stack: %d OHVT post: %d OHVT num: %d", top_ldx, ohvt_poststack, ohvt_isopen); return false; } } void script_state::RemHookVar(const char *name) { this->RemHookVars(1, name); } void script_state::RemHookVars(unsigned int num, ...) { if (LuaState != nullptr) { //WMC - Quick and clean. :) //WMC - *sigh* nostalgia //Get ScriptVar table if(this->OpenHookVarTable()) { int amt_ldx = lua_gettop(LuaState); va_list vl; va_start(vl, num); for(unsigned int i = 0; i < num; i++) { char *name = va_arg(vl, char*); lua_pushstring(LuaState, name); lua_pushnil(LuaState); lua_rawset(LuaState, amt_ldx); } va_end(vl); this->CloseHookVarTable(); } else { LuaError(LuaState, "Could not get HookVariable library to remove hook variables - get a coder"); } } } 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, object* objp, int more_data) { int num = 0; if (LuaState == nullptr) { return num; } for(SCP_vector::iterator chp = ConditionalHooks.begin(); chp != ConditionalHooks.end(); ++chp) { if(chp->ConditionsValid(action, objp, more_data)) { chp->Run(this, action); num++; } } return num; } bool script_state::IsConditionOverride(int action, object *objp) { //bool b = false; for(SCP_vector::iterator chp = ConditionalHooks.begin(); chp != ConditionalHooks.end(); ++chp) { if(chp->ConditionsValid(action, objp)) { if(chp->IsOverride(this, action)) return true; } } return false; } void script_state::EndFrame() { EndLuaFrame(); } void script_state::Clear() { // Free all lua value references ConditionalHooks.clear(); 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) { strncpy(StateName, name, sizeof(StateName)-1); 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() { ScriptingDocumentation doc; // 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->isOverridable()}); } if (Langs & SC_LUA) { OutputLuaDocumentation(doc); } return doc; } template static void output_hook_variable_list(FILE* fp, const Container& vars) { fputs("
", fp); for (const auto& param : vars) { fputs("
", fp); ade_output_type_link(fp, param.type); fprintf(fp, " %s
", param.name); fprintf(fp, "
Description: %s", param.description); } fputs("
", fp); } int script_state::OutputMeta(const char* filename) { FILE* fp = fopen(filename, "w"); int i; if (fp == nullptr) { return 0; } fprintf(fp, "\n\n\tScript Output - FSO v%s (%s)\n\n", FS_VERSION_FULL, StateName); fputs("", fp); fprintf(fp, "\t

Script Output - FSO v%s (%s)

\n", FS_VERSION_FULL, StateName); // Scripting languages links fputs("
", fp); //***Hooks fputs("

Conditional Hooks

", fp); fputs("
", fp); // Conditions fputs("
Conditions
", fp); for (i = 0; i < Num_script_conditions; i++) { fprintf(fp, "
%s
", Script_conditions[i].name); } // Actions fputs("
Actions
", fp); fputs("
", fp); 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) { fprintf(fp, "
%s
", hook->getHookName().c_str()); if (!hook->getDescription().empty()) { fprintf(fp, "
Description: %s", hook->getDescription().c_str()); // Only write this for hooks with descriptions since this information is useless for legacy hooks if (hook->isOverridable()) { fputs("
This hook is overridable", fp); } else { fputs("
This hook is not overridable", fp); } if (!hook->getParameters().empty()) { fputs("
Hook Variables:", fp); output_hook_variable_list(fp, hook->getParameters()); } fputs("
", fp); } } fputs("
", fp); fputs("
Global Hook Variables: (set globally independent of a specific hook)
", fp); output_hook_variable_list(fp, GlobalVariables); fputs("

", fp); //***Scripting langs fputs("

Scripting languages

", fp); if (Langs & SC_LUA) { fputs("
Lua
", fp); } fputs("
", fp); // Languages fputs("
", fp); if (Langs & SC_LUA) { fputs("

Lua

", fp); fputs("
", fp); OutputLuaMeta(fp); fputs("
", fp); } fputs("
", fp); fclose(fp); return 1; } 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", CFILE_NORMAL, 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 = 0; 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 = 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(); } catch (const LuaException&) { return false; } } catch (const LuaException& e) { LuaError(GetLuaSession(), "%s", e.what()); return false; } return true; } int script_state::RunBytecode(script_function& hd) { using namespace luacpp; if (!hd.function.isValid()) { return 1; } GR_DEBUG_SCOPE("Lua code"); try { hd.function.call(); } 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; } int32_t scripting_string_to_action(const char* action) { for (const auto& hook : scripting::getHooks()) { if (hook->getHookName() == action) return hook->getHookId(); } return CHA_NONE; } 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; } void script_state::ParseGlobalChunk(ConditionalActions hookType, const char* debug_str) { ConditionedHook hook; script_action sat; sat.action_type = hookType; ParseChunk(&sat.hook, debug_str); //Add the action hook.AddAction(&sat); ConditionalHooks.push_back(hook); } bool script_state::ParseCondition(const char *filename) { ConditionedHook *chp = nullptr; for(auto condition = script_parse_condition(); condition != CHC_NONE; condition = script_parse_condition()) { script_condition sct; sct.condition_type = condition; switch(condition) { case CHC_STATE: case CHC_SHIPCLASS: case CHC_SHIPTYPE: case CHC_SHIP: case CHC_MISSION: case CHC_CAMPAIGN: case CHC_WEAPONCLASS: case CHC_OBJECTTYPE: case CHC_VERSION: case CHC_APPLICATION: default: stuff_string(sct.condition_string, F_NAME); break; } if (chp == nullptr) { ConditionalHooks.push_back(ConditionedHook()); chp = &ConditionalHooks[ConditionalHooks.size() - 1]; } if (!chp->AddCondition(&sct)) { Warning(LOCATION, "Could not add condition to conditional hook in file '%s'; you may have more than %d", filename, MAX_HOOK_CONDITIONS); } } if (chp == NULL) { return false; } bool actions_added = false; for (const auto* action = script_parse_action(); action != nullptr; action = script_parse_action()) { script_action sat; sat.action_type = action->getHookId(); // WMC - build error string SCP_string buf; sprintf(buf, "%s - %s", filename, action->getHookName().c_str()); ParseChunk(&sat.hook, buf.c_str()); // Add the action if (chp->AddAction(&sat)) actions_added = true; } if(!actions_added) { Warning(LOCATION, "No actions specified for conditional hook in file '%s'", filename); ConditionalHooks.pop_back(); return false; } return true; } void script_state::AddConditionedHook(ConditionedHook hook) { ConditionalHooks.push_back(std::move(hook)); } void script_state::AddGameInitFunction(script_function func) { GameInitFunctions.push_back(std::move(func)); } //*************************CLASS: script_state************************* bool script_state::IsOverride(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(); } // 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*/) { // just incase something is wrong if (!scripting_state_inited) return; gr_reset_clip(); gr_clear(); gr_flip(); // process keys int k = game_check_key() & ~KEY_DEBUGGED; switch (k) { case KEY_ESC: gameseq_post_event(GS_EVENT_MAIN_MENU); return; } }