From 02ca86b8e0984485f96cdc3632b39b0f858b36df Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sun, 23 Feb 2025 00:51:48 +0100 Subject: [PATCH] :video_game: New mod type: '.gadget' Gadgets are essentially scripts wrapped as modcache entries. The name "gadgets" is inspired by Beyond All Reason RTS (running open source Recoil engine). Usual modcache treatment applies: * can be zipped or unzipped in directory * can have preview (aka 'mini') image * selectable via MainSelectorUI (open from TopMenubarUI, menu Tools, button BrowseGadgets All methods of loading a script were extended to recognize the .gadget suffix and load the gadget appropriately: * the 'loadscript' console command * the cvars 'app_custom_scripts' and 'app_recent_scripts' * the -runscript command line argument NOTE that although `ScriptCategory::GADGET` was added, all the above methods still load everything as `ScriptCategory::CUSTOM` and the game adjusts it based on extension. The ScriptMonitorUI was extended to display .gadget file name for `ScriptCategory::GADGET` script units. Example .gadget file: ``` ; This is a .gadget mod - basically a script wrapped as a modcache entry. ; The .gadget file must be present (even if empty) to get recognized. ; The script must have same base filename, i.e. 'foo.gadget' -> 'foo.as' ; Any extra include scripts or other resources can be bundled. ; ----------------------------------------------------------------------- ; Name to display in Selector UI. gadget_name "Engine Tool" ; Authors (Syntax: , , , []) - multiple authors can be given. gadget_author "base script" 351 ohlidalp ; Description to display in Selector UI. gadget_description "In-game engine diag and adjustment." ; Category for Selector UI (300 = Generic gadgets, 301 = Actor gadgets, 302 = Terrain gadgets). gadget_category 301 ``` --- source/main/Application.h | 5 ++ source/main/gui/panels/GUI_MainSelector.cpp | 11 +++- source/main/gui/panels/GUI_ScriptMonitor.cpp | 24 ++++++-- source/main/gui/panels/GUI_TopMenubar.cpp | 6 ++ source/main/main.cpp | 12 +++- source/main/resources/CacheSystem.cpp | 59 ++++++++++++++++++++ source/main/resources/CacheSystem.h | 6 ++ source/main/scripting/ScriptEngine.cpp | 37 +++++++++++- source/main/scripting/ScriptEngine.h | 12 ++-- 9 files changed, 157 insertions(+), 15 deletions(-) diff --git a/source/main/Application.h b/source/main/Application.h index 62bc440014..f3170ee8d2 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -322,6 +322,7 @@ enum LoaderType //!< Search mode for `ModCache::Query()` & Operation mode for `G LT_Tuneup, // No script alias, invoked manually, ext: tuneup LT_AssetPack, // No script alias, invoked manually, ext: assetpack LT_DashBoard, // No script alias, invoked manually, ext: dashboard + LT_Gadget, // No script alias, invoked manually, ext: gadget }; enum CacheCategoryId @@ -332,6 +333,10 @@ enum CacheCategoryId CID_DashboardsTruck = 201, CID_DashboardsBoat = 202, + CID_GadgetsGeneric = 300, + CID_GadgetsActor = 301, + CID_GadgetsTerrain = 302, + CID_Projects = 8000, //!< For truck files under 'projects/' directory, to allow listing from editors. CID_Tuneups = 8001, //!< For unsorted tuneup files. diff --git a/source/main/gui/panels/GUI_MainSelector.cpp b/source/main/gui/panels/GUI_MainSelector.cpp index 3d81585649..fe2f2b7565 100644 --- a/source/main/gui/panels/GUI_MainSelector.cpp +++ b/source/main/gui/panels/GUI_MainSelector.cpp @@ -31,6 +31,7 @@ #include "GUI_LoadingWindow.h" #include "InputEngine.h" #include "Language.h" +#include "ScriptEngine.h" #include "Utils.h" #include @@ -629,7 +630,15 @@ void MainSelector::Apply() ROR_ASSERT(m_selected_entry > -1); // Programmer error DisplayEntry& sd_entry = m_display_entries[m_selected_entry]; - if (m_loader_type == LT_Terrain && + if (m_loader_type == LT_Gadget) + { + auto request = new LoadScriptRequest(); + request->lsr_filename = sd_entry.sde_entry->fname; + request->lsr_category = ScriptCategory::GADGET; + App::GetGameContext()->PushMessage(Message(MSG_APP_LOAD_SCRIPT_REQUESTED, request)); + this->Close(); + } + else if (m_loader_type == LT_Terrain && App::app_state->getEnum() == AppState::MAIN_MENU) { App::GetGameContext()->PushMessage(Message(MSG_SIM_LOAD_TERRN_REQUESTED, sd_entry.sde_entry->fname)); diff --git a/source/main/gui/panels/GUI_ScriptMonitor.cpp b/source/main/gui/panels/GUI_ScriptMonitor.cpp index 293eb3ba20..c39ee9009a 100644 --- a/source/main/gui/panels/GUI_ScriptMonitor.cpp +++ b/source/main/gui/panels/GUI_ScriptMonitor.cpp @@ -58,7 +58,14 @@ void ScriptMonitor::Draw() ImGui::TextDisabled("%d", id); ImGui::NextColumn(); ImGui::AlignTextToFramePadding(); - ImGui::Text("%s", unit.scriptName.c_str()); + if (unit.scriptCategory == ScriptCategory::GADGET && unit.originatingGadget) + { + ImGui::Text("%s", unit.originatingGadget->fname.c_str()); + } + else + { + ImGui::Text("%s", unit.scriptName.c_str()); + } ImGui::NextColumn(); switch (unit.scriptCategory) { @@ -71,13 +78,20 @@ void ScriptMonitor::Draw() break; case ScriptCategory::CUSTOM: + case ScriptCategory::GADGET: { + std::string filename = unit.scriptName; + if (unit.scriptCategory == ScriptCategory::GADGET && unit.originatingGadget) + { + filename = unit.originatingGadget->fname; + } + if (ImGui::Button(_LC("ScriptMonitor", "Reload"))) { App::GetGameContext()->PushMessage(Message(MSG_APP_UNLOAD_SCRIPT_REQUESTED, new ScriptUnitID_t(id))); LoadScriptRequest* req = new LoadScriptRequest(); req->lsr_category = unit.scriptCategory; - req->lsr_filename = unit.scriptName; + req->lsr_filename = filename; App::GetGameContext()->ChainMessage(Message(MSG_APP_LOAD_SCRIPT_REQUESTED, req)); } ImGui::SameLine(); @@ -87,13 +101,13 @@ void ScriptMonitor::Draw() } ImGui::SameLine(); - bool autoload_set = std::find(autoload.begin(), autoload.end(), unit.scriptName) != autoload.end(); + bool autoload_set = std::find(autoload.begin(), autoload.end(), filename) != autoload.end(); if (ImGui::Checkbox(_LC("ScriptMonitor", "Autoload"), &autoload_set)) { if (autoload_set) - CvarAddFileToList(App::app_custom_scripts, unit.scriptName); + CvarAddFileToList(App::app_custom_scripts, filename); else - CvarRemoveFileFromList(App::app_custom_scripts, unit.scriptName); + CvarRemoveFileFromList(App::app_custom_scripts, filename); } break; } diff --git a/source/main/gui/panels/GUI_TopMenubar.cpp b/source/main/gui/panels/GUI_TopMenubar.cpp index 84784a7a4b..4f90e2df49 100644 --- a/source/main/gui/panels/GUI_TopMenubar.cpp +++ b/source/main/gui/panels/GUI_TopMenubar.cpp @@ -737,6 +737,12 @@ void TopMenubar::Draw(float dt) } } + if (ImGui::Button(_LC("TopMenubar", "Browse gadgets ..."))) + { + App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_SELECTOR_REQUESTED, new LoaderType(LT_Gadget))); + m_open_menu = TopMenu::TOPMENU_NONE; + } + ImGui::Separator(); ImGui::TextColored(GRAY_HINT_TEXT, "%s", _LC("TopMenubar", "Pre-spawn diag. options:")); diff --git a/source/main/main.cpp b/source/main/main.cpp index c1bcc2a397..0c513eba41 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -235,7 +235,11 @@ int main(int argc, char *argv[]) for (Ogre::String const& scriptname: script_names) { LOG(fmt::format("Loading startup script '{}' (from command line)", scriptname)); - App::GetScriptEngine()->loadScript(scriptname, ScriptCategory::CUSTOM); + // We cannot call `loadScript()` directly because modcache isn't up yet - gadgets cannot be resolved + LoadScriptRequest* req = new LoadScriptRequest(); + req->lsr_category = ScriptCategory::CUSTOM; + req->lsr_filename = scriptname; + App::GetGameContext()->PushMessage(Message(MSG_APP_LOAD_SCRIPT_REQUESTED, req)); // errors are logged by OGRE & AngelScript } } @@ -245,7 +249,11 @@ int main(int argc, char *argv[]) for (Ogre::String const& scriptname: script_names) { LOG(fmt::format("Loading startup script '{}' (from config file)", scriptname)); - App::GetScriptEngine()->loadScript(scriptname, ScriptCategory::CUSTOM); + // We cannot call `loadScript()` directly because modcache isn't up yet - gadgets cannot be resolved + LoadScriptRequest* req = new LoadScriptRequest(); + req->lsr_category = ScriptCategory::CUSTOM; + req->lsr_filename = scriptname; + App::GetGameContext()->PushMessage(Message(MSG_APP_LOAD_SCRIPT_REQUESTED, req)); // errors are logged by OGRE & AngelScript } } diff --git a/source/main/resources/CacheSystem.cpp b/source/main/resources/CacheSystem.cpp index eb1a32c5fe..8b9000d491 100644 --- a/source/main/resources/CacheSystem.cpp +++ b/source/main/resources/CacheSystem.cpp @@ -140,6 +140,7 @@ CacheSystem::CacheSystem() m_known_extensions.push_back("tuneup"); m_known_extensions.push_back("assetpack"); m_known_extensions.push_back("dashboard"); + m_known_extensions.push_back("gadget"); // register the dirs m_content_dirs.push_back("mods"); @@ -801,6 +802,12 @@ void CacheSystem::AddFile(String group, Ogre::FileInfo f, String ext) FillDashboardDetailInfo(entry, ds); new_entries.push_back(entry); } + else if (ext == "gadget") + { + CacheEntryPtr entry = new CacheEntry(); + FillGadgetDetailInfo(entry, ds); + new_entries.push_back(entry); + } else { CacheEntryPtr entry = new CacheEntry(); @@ -1330,6 +1337,46 @@ void CacheSystem::FillDashboardDetailInfo(CacheEntryPtr& entry, Ogre::DataStream } +void CacheSystem::FillGadgetDetailInfo(CacheEntryPtr& entry, Ogre::DataStreamPtr ds) +{ + GenericDocumentPtr doc = new GenericDocument(); + BitMask_t options + = GenericDocument::OPTION_ALLOW_SLASH_COMMENTS + | GenericDocument::OPTION_ALLOW_NAKED_STRINGS + | GenericDocument::OPTION_NAKEDSTR_USCORES_TO_SPACES; + doc->loadFromDataStream(ds, options); + + GenericDocContextPtr ctx = new GenericDocContext(doc); + while (!ctx->endOfFile()) + { + if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_name" && ctx->isTokString(1)) + { + entry->dname = ctx->getTokString(1); + } + else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_description" && ctx->isTokString(1)) + { + entry->description = ctx->getTokString(1); + } + else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_category" && ctx->isTokInt(1)) + { + entry->categoryid = ctx->getTokInt(1); + } + else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_author") + { + int n = ctx->countLineArgs(); + AuthorInfo author; + if (n > 1) { author.type = ctx->getTokString(1); } + if (n > 2) { author.id = ctx->getTokInt(2); } + if (n > 3) { author.name = ctx->getTokString(3); } + if (n > 4) { author.email = ctx->getTokString(4); } + entry->authors.push_back(author); + } + + ctx->seekNextLine(); + } + +} + void CacheSystem::FillTuneupDetailInfo(CacheEntryPtr &entry, TuneupDefPtr& tuneup_def) { if (!tuneup_def->author_name.empty()) @@ -1528,6 +1575,16 @@ void CacheSystem::LoadResource(CacheEntryPtr& entry) entry->resource_bundle_path, entry->resource_bundle_type, group, recursive, readonly); App::GetContentManager()->InitManagedMaterials(group); } + else if (entry->fext == "gadget") + { + // This is a .gadget bundle - use `inGlobalPool=false` to prevent resource name conflicts. + ResourceGroupManager::getSingleton().createResourceGroup(group, /*inGlobalPool=*/false); + ResourceGroupManager::getSingleton().addResourceLocation( + entry->resource_bundle_path, entry->resource_bundle_type, group, recursive, readonly); + App::GetContentManager()->InitManagedMaterials(group); + // Allow using builtin include scripts + App::GetContentManager()->AddResourcePack(ContentManager::ResourcePack::SCRIPTS, group); + } else { // A vehicle bundle - use `inGlobalPool=false` to prevent resource name conflicts. @@ -2187,6 +2244,8 @@ size_t CacheSystem::Query(CacheQuery& query) add = (query.cqy_filter_type == LT_AssetPack); else if (entry->fext == "dashboard") add = (query.cqy_filter_type == LT_DashBoard); + else if (entry->fext == "gadget") + add = (query.cqy_filter_type == LT_Gadget); else if (entry->fext == "truck") add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Vehicle || query.cqy_filter_type == LT_Truck); else if (entry->fext == "car") diff --git a/source/main/resources/CacheSystem.h b/source/main/resources/CacheSystem.h index 223e1cb7f1..9d74a552e3 100644 --- a/source/main/resources/CacheSystem.h +++ b/source/main/resources/CacheSystem.h @@ -356,6 +356,7 @@ class CacheSystem void FillTuneupDetailInfo(CacheEntryPtr &entry, TuneupDefPtr& tuneup_def); void FillAssetPackDetailInfo(CacheEntryPtr &entry, Ogre::DataStreamPtr ds); void FillDashboardDetailInfo(CacheEntryPtr& entry, Ogre::DataStreamPtr ds); + void FillGadgetDetailInfo(CacheEntryPtr& entry, Ogre::DataStreamPtr ds); /// @} void GenerateHashFromFilenames(); //!< For quick detection of added/removed content @@ -420,6 +421,11 @@ class CacheSystem {201, _LC("ModCategory", "Dashboards - Truck")}, {202, _LC("ModCategory", "Dashboards - Boat")}, + // gadgets + {CID_GadgetsGeneric, _LC("ModCategory", "Gadgets - Generic")}, + {CID_GadgetsActor, _LC("ModCategory", "Gadgets - Actor")}, + {CID_GadgetsTerrain, _LC("ModCategory", "Gadgets - Terrain")}, + // note: these categories are NOT in the repository: {5000, _LC("ModCategory", "Official Terrains")}, {5001, _LC("ModCategory", "Night Terrains")}, diff --git a/source/main/scripting/ScriptEngine.cpp b/source/main/scripting/ScriptEngine.cpp index 1605f9e544..661a0de632 100644 --- a/source/main/scripting/ScriptEngine.cpp +++ b/source/main/scripting/ScriptEngine.cpp @@ -826,7 +826,7 @@ String ScriptEngine::composeModuleName(String const& scriptName, ScriptCategory } ScriptUnitID_t ScriptEngine::loadScript( - String scriptName, ScriptCategory category/* = ScriptCategory::TERRAIN*/, + String scriptOrGadgetFileName, ScriptCategory category/* = ScriptCategory::TERRAIN*/, ActorPtr associatedActor /*= nullptr*/, std::string buffer /* =""*/) { // This function creates a new script unit, tries to set it up and removes it if setup fails. @@ -836,12 +836,41 @@ ScriptUnitID_t ScriptEngine::loadScript( // be created early, and removed if setup fails. static ScriptUnitID_t id_counter = 0; + std::string basename, ext, scriptName; + Ogre::StringUtil::splitBaseFilename(scriptOrGadgetFileName, basename, ext); + CacheEntryPtr originatingGadget; + if (ext == "gadget") + { + originatingGadget = App::GetCacheSystem()->FindEntryByFilename(LT_Gadget, /* partial: */false, scriptOrGadgetFileName); + if (!originatingGadget) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("Could not load script '{}' - gadget not found.", scriptOrGadgetFileName)); + return SCRIPTUNITID_INVALID; + } + App::GetCacheSystem()->LoadResource(originatingGadget); + scriptName = fmt::format("{}.as", basename); + // Ensure a .gadget file is always loaded as `GADGET`, even if requested as `CUSTOM` + category = ScriptCategory::GADGET; + } + else if (ext == "as") + { + scriptName = scriptOrGadgetFileName; + } + else + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("Could not load script '{}' - unknown file extension.", scriptOrGadgetFileName)); + return SCRIPTUNITID_INVALID; + } + ScriptUnitID_t unit_id = id_counter++; auto itor_pair = m_script_units.insert(std::make_pair(unit_id, ScriptUnit())); m_script_units[unit_id].uniqueId = unit_id; m_script_units[unit_id].scriptName = scriptName; m_script_units[unit_id].scriptCategory = category; m_script_units[unit_id].scriptBuffer = buffer; + m_script_units[unit_id].originatingGadget = originatingGadget; if (category == ScriptCategory::TERRAIN) { m_terrain_script_unit = unit_id; @@ -859,6 +888,10 @@ ScriptUnitID_t ScriptEngine::loadScript( { CvarAddFileToList(App::app_recent_scripts, scriptName); } + else if (category == ScriptCategory::GADGET && originatingGadget) + { + CvarAddFileToList(App::app_recent_scripts, originatingGadget->fname); + } // If setup failed, remove the unit. if (result != 0) @@ -879,7 +912,7 @@ int ScriptEngine::setupScriptUnit(int unit_id) int result=0; String moduleName = this->composeModuleName( - m_script_units[unit_id].scriptName, m_script_units[unit_id].scriptCategory, m_script_units[unit_id].uniqueId); + m_script_units[unit_id].scriptName, m_script_units[unit_id].scriptCategory, m_script_units[unit_id].uniqueId); // The builder is a helper class that will load the script file, // search for #include directives, and load any included files as diff --git a/source/main/scripting/ScriptEngine.h b/source/main/scripting/ScriptEngine.h index 18fff484d4..645efdefc1 100644 --- a/source/main/scripting/ScriptEngine.h +++ b/source/main/scripting/ScriptEngine.h @@ -60,7 +60,8 @@ enum class ScriptCategory INVALID, ACTOR, //!< Defined in truck file under 'scripts', contains global variable `BeamClass@ thisActor`. TERRAIN, //!< Defined in terrn2 file under '[Scripts]', receives terrain eventbox notifications. - CUSTOM //!< Loaded by user via either: A) ingame console 'loadscript'; B) RoR.cfg 'app_custom_scripts'; C) commandline '-runscript'. + GADGET, //!< Associated with a .gadget mod file, launched via UI or any method given below for CUSTOM scripts (use .gadget suffix - game will fix up category to `GADGET`). + CUSTOM //!< Loaded by user via either: A) ingame console 'loadscript'; B) RoR.cfg 'app_custom_scripts'; C) commandline '-runscript'; If used with .gadget file, game will fix up category to `GADGET`. }; const char* ScriptCategoryToString(ScriptCategory c); @@ -80,7 +81,8 @@ struct ScriptUnit AngelScript::asIScriptFunction* eventCallbackExFunctionPtr = nullptr; //!< script function pointer to the event callback function AngelScript::asIScriptFunction* defaultEventCallbackFunctionPtr = nullptr; //!< script function pointer for spawner events ActorPtr associatedActor; //!< For ScriptCategory::ACTOR - Ogre::String scriptName; + CacheEntryPtr originatingGadget; //!< For ScriptCategory::GADGET ~ determines resource group + Ogre::String scriptName; //!< Name of the '.as' file exclusively. Ogre::String scriptHash; Ogre::String scriptBuffer; }; @@ -89,7 +91,7 @@ typedef std::map ScriptUnitMap; struct LoadScriptRequest { - std::string lsr_filename; //!< Load from resource (file). If buffer is supplied, use this as display name only. + std::string lsr_filename; //!< Load from resource ('.as' file or '.gadget' file); If buffer is supplied, use this as display name only. std::string lsr_buffer; //!< Load from memory buffer. ScriptCategory lsr_category = ScriptCategory::TERRAIN; ActorInstanceID_t lsr_associated_actor = ACTORINSTANCEID_INVALID; //!< For ScriptCategory::ACTOR @@ -181,13 +183,13 @@ class ScriptEngine : public Ogre::LogListener /** * Loads a script - * @param scriptname filename to load; if buffer is supplied, this is only a display name. + * @param filename '.as' file or '.gadget' file to load; if buffer is supplied, this is only a display name. * @param category How to treat the script? * @param associatedActor Only for category ACTOR * @param buffer String with full script body; if empty, a file will be loaded as usual. * @return Unique ID of the script unit (because one script file can be loaded multiple times). */ - ScriptUnitID_t loadScript(Ogre::String scriptname, ScriptCategory category = ScriptCategory::TERRAIN, + ScriptUnitID_t loadScript(Ogre::String filename, ScriptCategory category = ScriptCategory::TERRAIN, ActorPtr associatedActor = nullptr, std::string buffer = ""); /**