Skip to content

Commit 7d0d768

Browse files
committed
🎮 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: <credit>, <forumID>, <name>, [<email>]) - 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 ```
1 parent 49be281 commit 7d0d768

8 files changed

+158
-28
lines changed

source/main/Application.h

+20
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,26 @@ enum LoaderType //!< Search mode for `ModCache::Query()` & Operation mode for `G
321321
LT_AddonPart, // No script alias, invoked manually, ext: addonpart
322322
LT_Tuneup, // No script alias, invoked manually, ext: tuneup
323323
LT_AssetPack, // No script alias, invoked manually, ext: assetpack
324+
LT_Gadget, // No script alias, invoked manually, ext: gadget
325+
};
326+
327+
enum CacheCategoryId
328+
{
329+
CID_None = 0,
330+
331+
CID_GadgetsGeneric = 300,
332+
CID_GadgetsActor = 301,
333+
CID_GadgetsTerrain = 302,
334+
335+
CID_Projects = 8000, //!< For truck files under 'projects/' directory, to allow listing from editors.
336+
CID_Tuneups = 8001, //!< For unsorted tuneup files.
337+
338+
CID_Max = 9000, //!< SPECIAL VALUE - Maximum allowed to be present in any mod files.
339+
CID_Unsorted = 9990,
340+
CID_All = 9991,
341+
CID_Fresh = 9992,
342+
CID_Hidden = 9993,
343+
CID_SearchResults = 9994,
324344
};
325345

326346
enum class TObjSpecialObject

source/main/gui/panels/GUI_MainSelector.cpp

+10-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "GUI_LoadingWindow.h"
3232
#include "InputEngine.h"
3333
#include "Language.h"
34+
#include "ScriptEngine.h"
3435
#include "Utils.h"
3536

3637
#include <MyGUI.h>
@@ -622,7 +623,15 @@ void MainSelector::Apply()
622623
ROR_ASSERT(m_selected_entry > -1); // Programmer error
623624
DisplayEntry& sd_entry = m_display_entries[m_selected_entry];
624625

625-
if (m_loader_type == LT_Terrain &&
626+
if (m_loader_type == LT_Gadget)
627+
{
628+
auto request = new LoadScriptRequest();
629+
request->lsr_filename = sd_entry.sde_entry->fname;
630+
request->lsr_category = ScriptCategory::GADGET;
631+
App::GetGameContext()->PushMessage(Message(MSG_APP_LOAD_SCRIPT_REQUESTED, request));
632+
this->Close();
633+
}
634+
else if (m_loader_type == LT_Terrain &&
626635
App::app_state->getEnum<AppState>() == AppState::MAIN_MENU)
627636
{
628637
App::GetGameContext()->PushMessage(Message(MSG_SIM_LOAD_TERRN_REQUESTED, sd_entry.sde_entry->fname));

source/main/gui/panels/GUI_ScriptMonitor.cpp

+19-5
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,14 @@ void ScriptMonitor::Draw()
5858
ImGui::TextDisabled("%d", id);
5959
ImGui::NextColumn();
6060
ImGui::AlignTextToFramePadding();
61-
ImGui::Text("%s", unit.scriptName.c_str());
61+
if (unit.scriptCategory == ScriptCategory::GADGET && unit.originatingGadget)
62+
{
63+
ImGui::Text("%s", unit.originatingGadget->fname.c_str());
64+
}
65+
else
66+
{
67+
ImGui::Text("%s", unit.scriptName.c_str());
68+
}
6269
ImGui::NextColumn();
6370
switch (unit.scriptCategory)
6471
{
@@ -71,13 +78,20 @@ void ScriptMonitor::Draw()
7178
break;
7279

7380
case ScriptCategory::CUSTOM:
81+
case ScriptCategory::GADGET:
7482
{
83+
std::string filename = unit.scriptName;
84+
if (unit.scriptCategory == ScriptCategory::GADGET && unit.originatingGadget)
85+
{
86+
filename = unit.originatingGadget->fname;
87+
}
88+
7589
if (ImGui::Button(_LC("ScriptMonitor", "Reload")))
7690
{
7791
App::GetGameContext()->PushMessage(Message(MSG_APP_UNLOAD_SCRIPT_REQUESTED, new ScriptUnitID_t(id)));
7892
LoadScriptRequest* req = new LoadScriptRequest();
7993
req->lsr_category = unit.scriptCategory;
80-
req->lsr_filename = unit.scriptName;
94+
req->lsr_filename = filename;
8195
App::GetGameContext()->ChainMessage(Message(MSG_APP_LOAD_SCRIPT_REQUESTED, req));
8296
}
8397
ImGui::SameLine();
@@ -87,13 +101,13 @@ void ScriptMonitor::Draw()
87101
}
88102

89103
ImGui::SameLine();
90-
bool autoload_set = std::find(autoload.begin(), autoload.end(), unit.scriptName) != autoload.end();
104+
bool autoload_set = std::find(autoload.begin(), autoload.end(), filename) != autoload.end();
91105
if (ImGui::Checkbox(_LC("ScriptMonitor", "Autoload"), &autoload_set))
92106
{
93107
if (autoload_set)
94-
CvarAddFileToList(App::app_custom_scripts, unit.scriptName);
108+
CvarAddFileToList(App::app_custom_scripts, filename);
95109
else
96-
CvarRemoveFileFromList(App::app_custom_scripts, unit.scriptName);
110+
CvarRemoveFileFromList(App::app_custom_scripts, filename);
97111
}
98112
break;
99113
}

source/main/gui/panels/GUI_TopMenubar.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,12 @@ void TopMenubar::Draw(float dt)
737737
}
738738
}
739739

740+
if (ImGui::Button(_LC("TopMenubar", "Browse gadgets ...")))
741+
{
742+
App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_SELECTOR_REQUESTED, new LoaderType(LT_Gadget)));
743+
m_open_menu = TopMenu::TOPMENU_NONE;
744+
}
745+
740746
ImGui::Separator();
741747
ImGui::TextColored(GRAY_HINT_TEXT, "%s", _LC("TopMenubar", "Pre-spawn diag. options:"));
742748

source/main/resources/CacheSystem.cpp

+59
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ CacheSystem::CacheSystem()
139139
m_known_extensions.push_back("addonpart");
140140
m_known_extensions.push_back("tuneup");
141141
m_known_extensions.push_back("assetpack");
142+
m_known_extensions.push_back("gadget");
142143

143144
// register the dirs
144145
m_content_dirs.push_back("mods");
@@ -793,6 +794,12 @@ void CacheSystem::AddFile(String group, Ogre::FileInfo f, String ext)
793794
FillAssetPackDetailInfo(entry, ds);
794795
new_entries.push_back(entry);
795796
}
797+
else if (ext == "gadget")
798+
{
799+
CacheEntryPtr entry = new CacheEntry();
800+
FillGadgetDetailInfo(entry, ds);
801+
new_entries.push_back(entry);
802+
}
796803
else
797804
{
798805
CacheEntryPtr entry = new CacheEntry();
@@ -1285,6 +1292,46 @@ void CacheSystem::FillAssetPackDetailInfo(CacheEntryPtr &entry, Ogre::DataStream
12851292
}
12861293
}
12871294

1295+
void CacheSystem::FillGadgetDetailInfo(CacheEntryPtr& entry, Ogre::DataStreamPtr ds)
1296+
{
1297+
GenericDocumentPtr doc = new GenericDocument();
1298+
BitMask_t options
1299+
= GenericDocument::OPTION_ALLOW_SLASH_COMMENTS
1300+
| GenericDocument::OPTION_ALLOW_NAKED_STRINGS
1301+
| GenericDocument::OPTION_NAKEDSTR_USCORES_TO_SPACES;
1302+
doc->loadFromDataStream(ds, options);
1303+
1304+
GenericDocContextPtr ctx = new GenericDocContext(doc);
1305+
while (!ctx->endOfFile())
1306+
{
1307+
if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_name" && ctx->isTokString(1))
1308+
{
1309+
entry->dname = ctx->getTokString(1);
1310+
}
1311+
else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_description" && ctx->isTokString(1))
1312+
{
1313+
entry->description = ctx->getTokString(1);
1314+
}
1315+
else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_category" && ctx->isTokInt(1))
1316+
{
1317+
entry->categoryid = ctx->getTokInt(1);
1318+
}
1319+
else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "gadget_author")
1320+
{
1321+
int n = ctx->countLineArgs();
1322+
AuthorInfo author;
1323+
if (n > 1) { author.type = ctx->getTokString(1); }
1324+
if (n > 2) { author.id = ctx->getTokInt(2); }
1325+
if (n > 3) { author.name = ctx->getTokString(3); }
1326+
if (n > 4) { author.email = ctx->getTokString(4); }
1327+
entry->authors.push_back(author);
1328+
}
1329+
1330+
ctx->seekNextLine();
1331+
}
1332+
1333+
}
1334+
12881335
void CacheSystem::FillTuneupDetailInfo(CacheEntryPtr &entry, TuneupDefPtr& tuneup_def)
12891336
{
12901337
if (!tuneup_def->author_name.empty())
@@ -1483,6 +1530,16 @@ void CacheSystem::LoadResource(CacheEntryPtr& entry)
14831530
entry->resource_bundle_path, entry->resource_bundle_type, group, recursive, readonly);
14841531
App::GetContentManager()->InitManagedMaterials(group);
14851532
}
1533+
else if (entry->fext == "gadget")
1534+
{
1535+
// This is a .gadget bundle - use `inGlobalPool=false` to prevent resource name conflicts.
1536+
ResourceGroupManager::getSingleton().createResourceGroup(group, /*inGlobalPool=*/false);
1537+
ResourceGroupManager::getSingleton().addResourceLocation(
1538+
entry->resource_bundle_path, entry->resource_bundle_type, group, recursive, readonly);
1539+
App::GetContentManager()->InitManagedMaterials(group);
1540+
// Allow using builtin include scripts
1541+
App::GetContentManager()->AddResourcePack(ContentManager::ResourcePack::SCRIPTS, group);
1542+
}
14861543
else
14871544
{
14881545
// A vehicle bundle - use `inGlobalPool=false` to prevent resource name conflicts.
@@ -2140,6 +2197,8 @@ size_t CacheSystem::Query(CacheQuery& query)
21402197
add = (query.cqy_filter_type == LT_Tuneup);
21412198
else if (entry->fext == "assetpack")
21422199
add = (query.cqy_filter_type == LT_AssetPack);
2200+
else if (entry->fext == "gadget")
2201+
add = (query.cqy_filter_type == LT_Gadget);
21432202
else if (entry->fext == "truck")
21442203
add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Vehicle || query.cqy_filter_type == LT_Truck);
21452204
else if (entry->fext == "car")

source/main/resources/CacheSystem.h

+6-15
Original file line numberDiff line numberDiff line change
@@ -144,21 +144,6 @@ class CacheEntry: public RefCountingObject<CacheEntry>
144144

145145
typedef RefCountingObjectPtr<CacheEntry> CacheEntryPtr;
146146

147-
enum CacheCategoryId
148-
{
149-
CID_None = 0,
150-
151-
CID_Projects = 8000, //!< For truck files under 'projects/' directory, to allow listing from editors.
152-
CID_Tuneups = 8001, //!< For unsorted tuneup files.
153-
154-
CID_Max = 9000, //!< SPECIAL VALUE - Maximum allowed to be present in any mod files.
155-
CID_Unsorted = 9990,
156-
CID_All = 9991,
157-
CID_Fresh = 9992,
158-
CID_Hidden = 9993,
159-
CID_SearchResults = 9994,
160-
};
161-
162147
struct CacheQueryResult
163148
{
164149
CacheQueryResult(CacheEntryPtr entry, size_t score);
@@ -370,6 +355,7 @@ class CacheSystem
370355
void FillAddonPartDetailInfo(CacheEntryPtr &entry, Ogre::DataStreamPtr ds);
371356
void FillTuneupDetailInfo(CacheEntryPtr &entry, TuneupDefPtr& tuneup_def);
372357
void FillAssetPackDetailInfo(CacheEntryPtr &entry, Ogre::DataStreamPtr ds);
358+
void FillGadgetDetailInfo(CacheEntryPtr& entry, Ogre::DataStreamPtr ds);
373359
/// @}
374360

375361
void GenerateHashFromFilenames(); //!< For quick detection of added/removed content
@@ -429,6 +415,11 @@ class CacheSystem
429415

430416
{875, _LC("ModCategory", "Submarine")},
431417

418+
// gadgets
419+
{CID_GadgetsGeneric, _LC("ModCategory", "Gadgets - Generic")},
420+
{CID_GadgetsActor, _LC("ModCategory", "Gadgets - Actor")},
421+
{CID_GadgetsTerrain, _LC("ModCategory", "Gadgets - Terrain")},
422+
432423
// note: these categories are NOT in the repository:
433424
{5000, _LC("ModCategory", "Official Terrains")},
434425
{5001, _LC("ModCategory", "Night Terrains")},

source/main/scripting/ScriptEngine.cpp

+31-2
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ String ScriptEngine::composeModuleName(String const& scriptName, ScriptCategory
826826
}
827827

828828
ScriptUnitID_t ScriptEngine::loadScript(
829-
String scriptName, ScriptCategory category/* = ScriptCategory::TERRAIN*/,
829+
String scriptOrGadgetFileName, ScriptCategory category/* = ScriptCategory::TERRAIN*/,
830830
ActorPtr associatedActor /*= nullptr*/, std::string buffer /* =""*/)
831831
{
832832
// 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(
836836
// be created early, and removed if setup fails.
837837
static ScriptUnitID_t id_counter = 0;
838838

839+
std::string basename, ext, scriptName;
840+
Ogre::StringUtil::splitBaseFilename(scriptOrGadgetFileName, basename, ext);
841+
CacheEntryPtr originatingGadget;
842+
if (ext == "gadget")
843+
{
844+
originatingGadget = App::GetCacheSystem()->FindEntryByFilename(LT_Gadget, /* partial: */false, scriptOrGadgetFileName);
845+
if (!originatingGadget)
846+
{
847+
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
848+
fmt::format("Could not load script '{}' - gadget not found.", scriptOrGadgetFileName));
849+
return SCRIPTUNITID_INVALID;
850+
}
851+
App::GetCacheSystem()->LoadResource(originatingGadget);
852+
scriptName = fmt::format("{}.as", basename);
853+
// Ensure a .gadget file is always loaded as `GADGET`, even if requested as `CUSTOM`
854+
category = ScriptCategory::GADGET;
855+
}
856+
else if (ext == "as")
857+
{
858+
scriptName = scriptOrGadgetFileName;
859+
}
860+
else
861+
{
862+
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
863+
fmt::format("Could not load script '{}' - unknown file extension.", scriptOrGadgetFileName));
864+
return SCRIPTUNITID_INVALID;
865+
}
866+
839867
ScriptUnitID_t unit_id = id_counter++;
840868
auto itor_pair = m_script_units.insert(std::make_pair(unit_id, ScriptUnit()));
841869
m_script_units[unit_id].uniqueId = unit_id;
842870
m_script_units[unit_id].scriptName = scriptName;
843871
m_script_units[unit_id].scriptCategory = category;
844872
m_script_units[unit_id].scriptBuffer = buffer;
873+
m_script_units[unit_id].originatingGadget = originatingGadget;
845874
if (category == ScriptCategory::TERRAIN)
846875
{
847876
m_terrain_script_unit = unit_id;
@@ -879,7 +908,7 @@ int ScriptEngine::setupScriptUnit(int unit_id)
879908
int result=0;
880909

881910
String moduleName = this->composeModuleName(
882-
m_script_units[unit_id].scriptName, m_script_units[unit_id].scriptCategory, m_script_units[unit_id].uniqueId);
911+
m_script_units[unit_id].scriptName, m_script_units[unit_id].scriptCategory, m_script_units[unit_id].uniqueId);
883912

884913
// The builder is a helper class that will load the script file,
885914
// search for #include directives, and load any included files as

source/main/scripting/ScriptEngine.h

+7-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ enum class ScriptCategory
6060
INVALID,
6161
ACTOR, //!< Defined in truck file under 'scripts', contains global variable `BeamClass@ thisActor`.
6262
TERRAIN, //!< Defined in terrn2 file under '[Scripts]', receives terrain eventbox notifications.
63-
CUSTOM //!< Loaded by user via either: A) ingame console 'loadscript'; B) RoR.cfg 'app_custom_scripts'; C) commandline '-runscript'.
63+
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`).
64+
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`.
6465
};
6566

6667
const char* ScriptCategoryToString(ScriptCategory c);
@@ -80,7 +81,8 @@ struct ScriptUnit
8081
AngelScript::asIScriptFunction* eventCallbackExFunctionPtr = nullptr; //!< script function pointer to the event callback function
8182
AngelScript::asIScriptFunction* defaultEventCallbackFunctionPtr = nullptr; //!< script function pointer for spawner events
8283
ActorPtr associatedActor; //!< For ScriptCategory::ACTOR
83-
Ogre::String scriptName;
84+
CacheEntryPtr originatingGadget; //!< For ScriptCategory::GADGET ~ determines resource group
85+
Ogre::String scriptName; //!< Name of the '.as' file exclusively.
8486
Ogre::String scriptHash;
8587
Ogre::String scriptBuffer;
8688
};
@@ -89,7 +91,7 @@ typedef std::map<ScriptUnitID_t, ScriptUnit> ScriptUnitMap;
8991

9092
struct LoadScriptRequest
9193
{
92-
std::string lsr_filename; //!< Load from resource (file). If buffer is supplied, use this as display name only.
94+
std::string lsr_filename; //!< Load from resource ('.as' file or '.gadget' file); If buffer is supplied, use this as display name only.
9395
std::string lsr_buffer; //!< Load from memory buffer.
9496
ScriptCategory lsr_category = ScriptCategory::TERRAIN;
9597
ActorInstanceID_t lsr_associated_actor = ACTORINSTANCEID_INVALID; //!< For ScriptCategory::ACTOR
@@ -181,13 +183,13 @@ class ScriptEngine : public Ogre::LogListener
181183

182184
/**
183185
* Loads a script
184-
* @param scriptname filename to load; if buffer is supplied, this is only a display name.
186+
* @param filename '.as' file or '.gadget' file to load; if buffer is supplied, this is only a display name.
185187
* @param category How to treat the script?
186188
* @param associatedActor Only for category ACTOR
187189
* @param buffer String with full script body; if empty, a file will be loaded as usual.
188190
* @return Unique ID of the script unit (because one script file can be loaded multiple times).
189191
*/
190-
ScriptUnitID_t loadScript(Ogre::String scriptname, ScriptCategory category = ScriptCategory::TERRAIN,
192+
ScriptUnitID_t loadScript(Ogre::String filename, ScriptCategory category = ScriptCategory::TERRAIN,
191193
ActorPtr associatedActor = nullptr, std::string buffer = "");
192194

193195
/**

0 commit comments

Comments
 (0)