From 4da55a351af0c44426336f6f0efd96c8566fcfe0 Mon Sep 17 00:00:00 2001 From: mo Date: Thu, 3 Sep 2026 22:19:03 +0100 Subject: [PATCH] feat: add T4 menu dumping and loading (#986) * feat: add T4 menu dumping * chore: align with latest changes * feat: add T4 menu loading Load T4 menulists and menu files through the shared menu parser. Preserve T4-specific menu and listbox fields and cover loading, dumping, linking, and round-trip behaviour. * chore: make flag fields unsigned * chore: rename `expOperationEnum` -> `operationEnum` * chore: use default enum numeration for T4 operationEnum * chore: remove unused members of operationEnum * chore: remove redundant op from operationEnum * refactor: align T4 expression operand names with IW3 * chore: small adjustment to iw3,t4 types --------- Co-authored-by: Jan Laupetin --- docs/SupportedAssetTypes.md | 4 +- src/Common/Game/IW3/IW3_Assets.h | 2 +- src/Common/Game/T4/T4_Assets.h | 147 +++- src/ObjCommon/Game/T4/MenuConstantsT4.h | 110 +++ .../Game/T4/Menu/LoaderMenuListT4.cpp | 215 +++++ .../Game/T4/Menu/LoaderMenuListT4.h | 13 + .../T4/Menu/MenuConversionZoneStateT4.cpp | 8 + .../Game/T4/Menu/MenuConversionZoneStateT4.h | 19 + .../Game/T4/Menu/MenuConverterT4.cpp | 735 ++++++++++++++++++ src/ObjLoading/Game/T4/Menu/MenuConverterT4.h | 23 + src/ObjLoading/Game/T4/ObjLoaderT4.cpp | 2 + .../Parsing/Menu/Domain/CommonItemDef.h | 4 + .../Parsing/Menu/Domain/CommonMenuDef.h | 1 + .../Parsing/Menu/Domain/MenuFeatureLevel.h | 3 +- .../Menu/Matcher/MenuExpressionMatchers.cpp | 20 + .../Parsing/Menu/MenuFileReader.cpp | 3 + .../EventHandlerSetScopeSequences.cpp | 4 +- .../Menu/Sequence/GlobalScopeSequences.cpp | 2 +- .../Menu/Sequence/ItemScopeSequences.cpp | 49 +- .../Menu/Sequence/MenuScopeSequences.cpp | 9 + src/ObjWriting/Game/T4/Menu/MenuDumperT4.cpp | 52 ++ src/ObjWriting/Game/T4/Menu/MenuDumperT4.h | 13 + .../Game/T4/Menu/MenuListDumperT4.cpp | 123 +++ .../Game/T4/Menu/MenuListDumperT4.h | 19 + src/ObjWriting/Game/T4/Menu/MenuWriterT4.cpp | 686 ++++++++++++++++ src/ObjWriting/Game/T4/Menu/MenuWriterT4.h | 18 + src/ObjWriting/Game/T4/ObjWriterT4.cpp | 4 + src/ZoneCode/Game/T4/XAssets/menuDef_t.txt | 6 +- .../Game/T4/Menu/LoaderMenuListT4Test.cpp | 269 +++++++ .../Game/T4/Menu/MenuDumperT4Test.cpp | 616 +++++++++++++++ .../Game/T4/Menu/MenuListDumperT4Test.cpp | 53 ++ .../T4/MenuRoundTrip/MenuRoundTripT4.zone | 3 + .../Game/T4/MenuRoundTrip/ui_mp/main.menu | 46 ++ .../Game/T4/MenuRoundTrip/ui_mp/menus.txt | 4 + .../Game/T4/MenuRoundTrip/ui_mp/popup.menu | 13 + test/SystemTests/Game/T4/MenuRoundTripT4.cpp | 97 +++ 36 files changed, 3366 insertions(+), 29 deletions(-) create mode 100644 src/ObjCommon/Game/T4/MenuConstantsT4.h create mode 100644 src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.cpp create mode 100644 src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.h create mode 100644 src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.cpp create mode 100644 src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.h create mode 100644 src/ObjLoading/Game/T4/Menu/MenuConverterT4.cpp create mode 100644 src/ObjLoading/Game/T4/Menu/MenuConverterT4.h create mode 100644 src/ObjWriting/Game/T4/Menu/MenuDumperT4.cpp create mode 100644 src/ObjWriting/Game/T4/Menu/MenuDumperT4.h create mode 100644 src/ObjWriting/Game/T4/Menu/MenuListDumperT4.cpp create mode 100644 src/ObjWriting/Game/T4/Menu/MenuListDumperT4.h create mode 100644 src/ObjWriting/Game/T4/Menu/MenuWriterT4.cpp create mode 100644 src/ObjWriting/Game/T4/Menu/MenuWriterT4.h create mode 100644 test/ObjLoadingTests/Game/T4/Menu/LoaderMenuListT4Test.cpp create mode 100644 test/ObjWritingTests/Game/T4/Menu/MenuDumperT4Test.cpp create mode 100644 test/ObjWritingTests/Game/T4/Menu/MenuListDumperT4Test.cpp create mode 100644 test/SystemTests/Game/T4/MenuRoundTrip/MenuRoundTripT4.zone create mode 100644 test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/main.menu create mode 100644 test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/menus.txt create mode 100644 test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/popup.menu create mode 100644 test/SystemTests/Game/T4/MenuRoundTripT4.cpp diff --git a/docs/SupportedAssetTypes.md b/docs/SupportedAssetTypes.md index 6e7190ae..1c57aa57 100644 --- a/docs/SupportedAssetTypes.md +++ b/docs/SupportedAssetTypes.md @@ -182,8 +182,8 @@ using `Linker`): | GfxWorld | ❌ | ❌ | | | GfxLightDef | ❌ | ❌ | | | Font_s | ✅ | ✅ | | -| MenuList | ❌ | ❌ | | -| menuDef_t | ❌ | ❌ | | +| MenuList | ✅ | ✅ | The output is decompiled. The result will not be the same as the input. | +| menuDef_t | ✅ | ✅ | See menulist. | | LocalizeEntry | ✅ | ✅ | | | WeaponDef | ✅ | ✅ | | | FxEffectDef | ❌ | ❌ | | diff --git a/src/Common/Game/IW3/IW3_Assets.h b/src/Common/Game/IW3/IW3_Assets.h index 05d8832e..6e97a936 100644 --- a/src/Common/Game/IW3/IW3_Assets.h +++ b/src/Common/Game/IW3/IW3_Assets.h @@ -2812,7 +2812,7 @@ namespace IW3 int gameMsgWindowIndex; int gameMsgWindowMode; const char* text; - int itemFlags; + unsigned int itemFlags; menuDef_t* parent; const char* mouseEnterText; const char* mouseExitText; diff --git a/src/Common/Game/T4/T4_Assets.h b/src/Common/Game/T4/T4_Assets.h index 4b572bfd..35cd7ff1 100644 --- a/src/Common/Game/T4/T4_Assets.h +++ b/src/Common/Game/T4/T4_Assets.h @@ -2781,6 +2781,26 @@ namespace T4 int vertAlign; }; + enum WindowDefStaticFlag : unsigned int + { + WINDOW_FLAG_DECORATION = 0x100000, + WINDOW_FLAG_HORIZONTAL_SCROLL = 0x200000, + WINDOW_FLAG_AUTO_WRAPPED = 0x800000, + WINDOW_FLAG_POPUP = 0x1000000, + WINDOW_FLAG_OUT_OF_BOUNDS_CLICK = 0x2000000, + WINDOW_FLAG_LEGACY_SPLIT_SCREEN_SCALE = 0x4000000, + WINDOW_FLAG_HIDDEN_DURING_FLASH_BANG = 0x10000000, + WINDOW_FLAG_HIDDEN_DURING_SCOPE = 0x20000000, + WINDOW_FLAG_HIDDEN_DURING_UI = 0x40000000, + }; + + enum WindowDefDynamicFlag : unsigned int + { + WINDOW_FLAG_VISIBLE = 0x4, + WINDOW_FLAG_NON_DEFAULT_BACKCOLOR = 0x8000, + WINDOW_FLAG_NON_DEFAULT_FORECOLOR = 0x10000, + }; + struct windowDef_t { const char* name; @@ -2792,8 +2812,8 @@ namespace T4 int ownerDraw; int ownerDrawFlags; float borderSize; - int staticFlags; - int dynamicFlags[1]; + unsigned int staticFlags; + unsigned int dynamicFlags[1]; int nextTime; float foreColor[4]; float backColor[4]; @@ -2813,7 +2833,7 @@ namespace T4 { int intVal; float floatVal; - const char* string; + const char* stringVal; }; enum expDataType @@ -2823,11 +2843,109 @@ namespace T4 VAL_STRING = 0x2, }; - enum expOperationEnum + enum operationEnum : int { - OP_NEGATE = 0x7, - NUM_EXPRESSION_OPERATORS = 0x18, - MAX_OPERATOR_VALUE = 0x4000, + OP_NOOP, + OP_RIGHTPAREN, + OP_MULTIPLY, + OP_DIVIDE, + OP_MODULUS, + OP_ADD, + OP_SUBTRACT, + OP_NOT, + OP_LESSTHAN, + OP_LESSTHANEQUALTO, + OP_GREATERTHAN, + OP_GREATERTHANEQUALTO, + OP_EQUALS, + OP_NOTEQUAL, + OP_AND, + OP_OR, + OP_LEFTPAREN, + OP_COMMA, + OP_BITWISEAND, + OP_BITWISEOR, + OP_BITWISENOT, + OP_BITSHIFTLEFT, + OP_BITSHIFTRIGHT, + + OP_FIRSTFUNCTIONCALL, + OP_SIN = OP_FIRSTFUNCTIONCALL, + OP_COS, + OP_MIN, + OP_MAX, + OP_MILLISECONDS, + OP_DVARINT, + OP_DVARBOOL, + OP_DVARFLOAT, + OP_DVARSTRING, + OP_STAT, + OP_UIACTIVE, + OP_FLASHBANGED, + OP_SCOPED, + OP_SCOREBOARDVISIBLE, + OP_INKILLCAM, + OP_PLAYERFIELD, + OP_SELECTINGLOCATION, + OP_TEAMFIELD, + OP_OTHERTEAMFIELD, + OP_MARINESFIELD, + OP_OPFORFIELD, + OP_MENUISOPEN, + OP_WRITINGDATA, + OP_INLOBBY, + OP_INPRIVATEPARTY, + OP_PRIVATEPARTYHOST, + OP_PRIVATEPARTYHOSTINLOBBY, + OP_ALONEINPARTY, + OP_ADSJAVELIN, + OP_WEAPLOCKBLINK, + OP_WEAPATTACKTOP, + OP_WEAPATTACKDIRECT, + OP_SECONDSASTIME, + OP_TABLELOOKUP, + OP_LOCALIZESTRING, + OP_LOCALVARINT, + OP_LOCALVARBOOL, + OP_LOCALVARFLOAT, + OP_LOCALVARSTRING, + OP_TIMELEFT, + OP_SECONDSASCOUNTDOWN, + OP_GAMEMSGWNDACTIVE, + OP_TOINT, + OP_TOSTRING, + OP_TOFLOAT, + OP_GAMETYPENAME, + OP_GAMETYPE, + OP_GAMETYPEDESCRIPTION, + OP_SCORE, + OP_FRIENDSONLINE, + OP_FOLLOWING, + OP_STATRANGEBITSSET, + OP_KEYBINDING, + OP_ACTIONSLOTUSABLE, + OP_HUDFADE, + OP_MAXPLAYERS, + OP_ACCEPTINGINVITE, + OP_GAMEHOST, + OP_ISSPLITSCREEN, + OP_ISSPLITSCREENHOST, + OP_SPLITSCREENNUM, + OP_ISCINEMATICFINISHED, + OP_ISSQUADLEADER, + OP_GETSQUADID, + OP_ISARTILLERY, + OP_GETPLAYERSQUADID, + OP_HASFRIENDS, + OP_HASPENDINGFRIENDS, + OP_HASINVITES, + OP_SQUADINVITESENT, + OP_ISINTERMISSION, + OP_PARTYISMISSINGMAPPACK, + OP_PARTYMISSINGMAPPACKERROR, + OP_ANYNEWMAPPACKS, + + NUM_OPERATORS, }; struct Operand @@ -2839,7 +2957,7 @@ namespace T4 union entryInternalData { Operand operand; - expOperationEnum op; + operationEnum op; }; enum expressionEntryType : int @@ -2879,7 +2997,7 @@ namespace T4 int elementStyle; int numColumns; columnInfo_s columnInfo[16]; - const char* doubleClick; + const char* onDoubleClick; int notselectable; int noScrollBars; int usePaging; @@ -2945,6 +3063,15 @@ namespace T4 ITEM_TYPE_GAME_MESSAGE_WINDOW = 0x13 }; + enum ItemDefDvarFlag + { + ITEM_DVAR_FLAG_ENABLE = 0x1, + ITEM_DVAR_FLAG_DISABLE = 0x2, + ITEM_DVAR_FLAG_SHOW = 0x4, + ITEM_DVAR_FLAG_HIDE = 0x8, + ITEM_DVAR_FLAG_FOCUS = 0x10, + }; + struct itemDef_s { windowDef_t window; @@ -2961,7 +3088,7 @@ namespace T4 int gameMsgWindowIndex; int gameMsgWindowMode; const char* text; - int itemFlags; + unsigned int itemFlags; menuDef_t* parent; const char* mouseEnterText; const char* mouseExitText; diff --git a/src/ObjCommon/Game/T4/MenuConstantsT4.h b/src/ObjCommon/Game/T4/MenuConstantsT4.h new file mode 100644 index 00000000..c0d4405b --- /dev/null +++ b/src/ObjCommon/Game/T4/MenuConstantsT4.h @@ -0,0 +1,110 @@ +#pragma once + +#include "Game/T4/T4.h" + +#include + +namespace T4 +{ + inline constexpr const char* g_expFunctionNames[]{ + "NOOP", + ")", + "*", + "/", + "%", + "+", + "-", + "!", + "<", + "<=", + ">", + ">=", + "==", + "!=", + "&&", + "||", + "(", + ",", + "&", + "|", + "~", + "<<", + ">>", + "sin", + "cos", + "min", + "max", + "milliseconds", + "dvarint", + "dvarbool", + "dvarfloat", + "dvarstring", + "stat", + "ui_active", + "flashbanged", + "scoped", + "scoreboard_visible", + "inkillcam", + "player", + "selecting_location", + "team", + "otherteam", + "marinesfield", + "opforfield", + "menuisopen", + "writingdata", + "inlobby", + "inprivateparty", + "privatepartyhost", + "privatepartyhostinlobby", + "aloneinparty", + "adsjavelin", + "weaplockblink", + "weapattacktop", + "weapattackdirect", + "secondsastime", + "tablelookup", + "locstring", + "localvarint", + "localvarbool", + "localvarfloat", + "localvarstring", + "timeleft", + "secondsascountdown", + "gamemsgwndactive", + "int", + "string", + "float", + "gametypename", + "gametype", + "gametypedescription", + "scoreatrank", + "friendsonline", + "spectatingclient", + "statrangeanybitsset", + "keybinding", + "actionslotusable", + "hudfade", + "maxrecommendedplayers", + "acceptinginvite", + "gamehost", + "issplitscreen", + "issplitscreenhost", + "splitscreennum", + "iscinematicfinished", + "issquadleader", + "getsquadid", + "isartillery", + "getplayersquadid", + "hasfriends", + "haspendingfriends", + "hasinvites", + "squadinvitesent", + "isintermission", + "partyismissingmappack", + "partymissingmappackerror", + "anynewmappacks", + }; + + static_assert(std::extent_v == static_cast(NUM_OPERATORS)); +} // namespace T4 diff --git a/src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.cpp b/src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.cpp new file mode 100644 index 00000000..0885f7f9 --- /dev/null +++ b/src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.cpp @@ -0,0 +1,215 @@ +#include "LoaderMenuListT4.h" + +#include "Game/T4/Menu/MenuConversionZoneStateT4.h" +#include "Game/T4/Menu/MenuConverterT4.h" +#include "ObjLoading.h" +#include "Parsing/Menu/MenuAssetZoneState.h" +#include "Parsing/Menu/MenuFileReader.h" +#include "Utils/Logging/Log.h" + +#include + +using namespace T4; + +namespace +{ + class MenuListLoader final : public AssetCreator + { + public: + MenuListLoader(MemoryManager& memory, ISearchPath& searchPath) + : m_memory(memory), + m_search_path(searchPath) + { + } + + AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override + { + std::vector menus; + AssetRegistration registration(assetName); + + auto& zoneState = context.GetZoneAssetCreationState(); + auto& conversionState = context.GetZoneAssetCreationState(); + + std::deque menuLoadQueue; + const auto alreadyLoadedMenuListFileMenus = conversionState.m_menus_by_filename.find(assetName); + + if (alreadyLoadedMenuListFileMenus == conversionState.m_menus_by_filename.end()) + { + const auto file = m_search_path.Open(assetName); + if (!file.IsOpen()) + return AssetCreationResult::NoAction(); + + const auto menuListResult = ParseMenuFile(*file.m_stream, assetName, zoneState); + if (menuListResult) + { + if (!ProcessParsedResults(assetName, context, *menuListResult, zoneState, conversionState, menus, registration)) + return AssetCreationResult::Failure(); + + for (const auto& menuToLoad : menuListResult->m_menus_to_load) + menuLoadQueue.emplace_back(menuToLoad); + + zoneState.AddMenusToLoad(assetName, std::move(menuListResult->m_menus_to_load)); + } + else + return AssetCreationResult::Failure(); + } + else + { + for (auto* menu : alreadyLoadedMenuListFileMenus->second) + { + menus.emplace_back(menu->Asset()); + registration.AddDependency(menu); + } + } + + while (!menuLoadQueue.empty()) + { + const auto& menuFileToLoad = menuLoadQueue.front(); + + if (!LoadMenuFileFromQueue(menuFileToLoad, context, zoneState, conversionState, menus, registration)) + return AssetCreationResult::Failure(); + + menuLoadQueue.pop_front(); + } + + auto* menuListAsset = m_memory.Alloc(); + menuListAsset->name = m_memory.Dup(assetName.c_str()); + registration.SetAsset(menuListAsset); + + CreateMenuListAsset(*menuListAsset, menus); + + return AssetCreationResult::Success(context.AddAsset(std::move(registration))); + } + + private: + bool LoadMenuFileFromQueue(const std::string& menuFilePath, + AssetCreationContext& context, + menu::MenuAssetZoneState& zoneState, + MenuConversionZoneState& conversionState, + std::vector& menus, + AssetRegistration& registration) const + { + const auto alreadyLoadedMenuFile = conversionState.m_menus_by_filename.find(menuFilePath); + if (alreadyLoadedMenuFile != conversionState.m_menus_by_filename.end()) + { + con::debug("Already loaded \"{}\", skipping", menuFilePath); + for (auto* menu : alreadyLoadedMenuFile->second) + { + menus.emplace_back(menu->Asset()); + registration.AddDependency(menu); + } + return true; + } + + const auto file = m_search_path.Open(menuFilePath); + if (!file.IsOpen()) + { + con::error("Could not open menu file \"{}\"", menuFilePath); + return false; + } + + const auto menuFileResult = ParseMenuFile(*file.m_stream, menuFilePath, zoneState); + if (menuFileResult) + { + if (!ProcessParsedResults(menuFilePath, context, *menuFileResult, zoneState, conversionState, menus, registration)) + return false; + if (!menuFileResult->m_menus_to_load.empty()) + con::warn("Menu file has menus to load even though it is not a menu list, ignoring: \"{}\"", menuFilePath); + + return true; + } + else + con::error("Could not read menu file \"{}\"", menuFilePath); + + return false; + } + + bool ProcessParsedResults(const std::string& fileName, + AssetCreationContext& context, + menu::ParsingResult& parsingResult, + menu::MenuAssetZoneState& zoneState, + MenuConversionZoneState& conversionState, + std::vector& menus, + AssetRegistration& registration) const + { + const auto menuCount = parsingResult.m_menus.size(); + const auto menuLoadCount = parsingResult.m_menus_to_load.size(); + auto totalItemCount = 0uz; + for (const auto& menu : parsingResult.m_menus) + totalItemCount += menu->m_items.size(); + + con::info("Successfully read menu file \"{}\" ({} loads, {} menus, {} items)", fileName, menuLoadCount, menuCount, totalItemCount); + + // Prepare a list of all menus of this file + std::vector*> allMenusOfFile; + allMenusOfFile.reserve(parsingResult.m_menus.size()); + + // Convert all menus and add them as assets + for (auto& commonMenu : parsingResult.m_menus) + { + auto converter = IMenuConverter::Create(ObjLoading::Configuration.MenuNoOptimization, m_search_path, m_memory, context); + + auto* menuAsset = m_memory.Alloc(); + AssetRegistration menuRegistration(commonMenu->m_name, menuAsset); + + if (!converter->ConvertMenu(*commonMenu, *menuAsset, menuRegistration)) + { + con::error("Failed to convert menu file \"{}\"", commonMenu->m_name); + return false; + } + + menus.emplace_back(menuAsset); + auto* menuAssetInfo = context.AddAsset(std::move(menuRegistration)); + + if (menuAssetInfo) + { + allMenusOfFile.emplace_back(menuAssetInfo); + registration.AddDependency(menuAssetInfo); + } + + zoneState.AddMenu(std::move(commonMenu)); + } + + // Register this file with all loaded menus + conversionState.AddLoadedFile(fileName, std::move(allMenusOfFile)); + + return true; + } + + void CreateMenuListAsset(MenuList& menuList, const std::vector& menus) const + { + menuList.menuCount = static_cast(menus.size()); + + if (menuList.menuCount > 0) + { + menuList.menus = m_memory.Alloc(menuList.menuCount); + for (auto i = 0; i < menuList.menuCount; i++) + menuList.menus[i] = menus[i]; + } + else + menuList.menus = nullptr; + } + + std::unique_ptr + ParseMenuFile(std::istream& stream, const std::string& menuFileName, const menu::MenuAssetZoneState& zoneState) const + { + menu::MenuFileReader reader(stream, menuFileName, menu::FeatureLevel::T4, m_search_path); + + reader.IncludeZoneState(zoneState); + reader.SetPermissiveMode(ObjLoading::Configuration.MenuPermissiveParsing); + + return reader.ReadMenuFile(); + } + + MemoryManager& m_memory; + ISearchPath& m_search_path; + }; +} // namespace + +namespace menu +{ + std::unique_ptr> CreateMenuListLoaderT4(MemoryManager& memory, ISearchPath& searchPath) + { + return std::make_unique(memory, searchPath); + } +} // namespace menu diff --git a/src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.h b/src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.h new file mode 100644 index 00000000..84b121e8 --- /dev/null +++ b/src/ObjLoading/Game/T4/Menu/LoaderMenuListT4.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Asset/IAssetCreator.h" +#include "Game/T4/T4.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +namespace menu +{ + std::unique_ptr> CreateMenuListLoaderT4(MemoryManager& memory, ISearchPath& searchPath); +} diff --git a/src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.cpp b/src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.cpp new file mode 100644 index 00000000..b489c73c --- /dev/null +++ b/src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.cpp @@ -0,0 +1,8 @@ +#include "MenuConversionZoneStateT4.h" + +using namespace T4; + +void MenuConversionZoneState::AddLoadedFile(std::string loadedFileName, std::vector*> menusOfFile) +{ + m_menus_by_filename.emplace(std::move(loadedFileName), std::move(menusOfFile)); +} diff --git a/src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.h b/src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.h new file mode 100644 index 00000000..16adfb88 --- /dev/null +++ b/src/ObjLoading/Game/T4/Menu/MenuConversionZoneStateT4.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Asset/IZoneAssetCreationState.h" +#include "Game/T4/T4.h" + +#include +#include +#include + +namespace T4 +{ + class MenuConversionZoneState final : public IZoneAssetCreationState + { + public: + void AddLoadedFile(std::string loadedFileName, std::vector*> menusOfFile); + + std::unordered_map*>> m_menus_by_filename; + }; +} // namespace T4 diff --git a/src/ObjLoading/Game/T4/Menu/MenuConverterT4.cpp b/src/ObjLoading/Game/T4/Menu/MenuConverterT4.cpp new file mode 100644 index 00000000..479706db --- /dev/null +++ b/src/ObjLoading/Game/T4/Menu/MenuConverterT4.cpp @@ -0,0 +1,735 @@ +#include "MenuConverterT4.h" + +#include "Menu/AbstractMenuConverter.h" +#include "Parsing/Menu/Domain/EventHandler/CommonEventHandlerScript.h" +#include "Parsing/Menu/Domain/Expression/CommonExpressionBaseFunctionCall.h" +#include "Parsing/Menu/Domain/Expression/CommonExpressionCustomFunctionCall.h" +#include "Parsing/Simple/Expression/SimpleExpressionBinaryOperation.h" +#include "Parsing/Simple/Expression/SimpleExpressionConditionalOperator.h" +#include "Parsing/Simple/Expression/SimpleExpressionUnaryOperation.h" + +#include +#include +#include +#include +#include +#include + +using namespace T4; +using namespace menu; + +namespace +{ + class MenuConverter final : public AbstractMenuConverter, public IMenuConverter + { + [[nodiscard]] static rectDef_s ConvertRectDef(const CommonRect& rect) + { + return rectDef_s{ + .x = static_cast(rect.x), + .y = static_cast(rect.y), + .w = static_cast(rect.w), + .h = static_cast(rect.h), + .horzAlign = rect.horizontalAlign, + .vertAlign = rect.verticalAlign, + }; + } + + [[nodiscard]] static rectDef_s ConvertRectDefRelativeTo(const rectDef_s& rect, const rectDef_s& relativeTo) + { + return rectDef_s{ + .x = relativeTo.x + rect.x, + .y = relativeTo.y + rect.y, + .w = static_cast(rect.w), + .h = static_cast(rect.h), + .horzAlign = rect.horzAlign, + .vertAlign = rect.vertAlign, + }; + } + + static void ConvertColor(float (&output)[4], const CommonColor& input) + { + output[0] = static_cast(input.r); + output[1] = static_cast(input.g); + output[2] = static_cast(input.b); + output[3] = static_cast(input.a); + } + + static void ApplyFlag(unsigned& flags, const bool shouldApply, const unsigned flagValue) + { + if (!shouldApply) + return; + + flags |= flagValue; + } + + [[nodiscard]] Material* ConvertMaterial(const std::string& materialName, const CommonMenuDef* menu, const CommonItemDef* item = nullptr) const + { + if (materialName.empty()) + return nullptr; + + auto* materialDependency = m_context.LoadDependency(materialName); + if (!materialDependency) + throw MenuConversionException(std::format("Failed to load material \"{}\"", materialName), menu, item); + + return materialDependency->Asset(); + } + + [[nodiscard]] snd_alias_list_t* ConvertSound(const std::string& soundName, const CommonMenuDef* menu, const CommonItemDef* item = nullptr) const + { + if (soundName.empty()) + return nullptr; + + auto* soundDependency = m_context.LoadDependency(soundName); + if (!soundDependency) + throw MenuConversionException(std::format("Failed to load sound \"{}\"", soundName), menu, item); + + return soundDependency->Asset(); + } + + constexpr static operationEnum UNARY_OPERATION_MAPPING[]{ + OP_NOT, + OP_BITWISENOT, + OP_SUBTRACT, + }; + static_assert(std::size(UNARY_OPERATION_MAPPING) == static_cast(SimpleUnaryOperationId::COUNT)); + + constexpr static operationEnum BINARY_OPERATION_MAPPING[]{ + OP_ADD, + OP_SUBTRACT, + OP_MULTIPLY, + OP_DIVIDE, + OP_MODULUS, + OP_BITWISEAND, + OP_BITWISEOR, + OP_BITSHIFTLEFT, + OP_BITSHIFTRIGHT, + OP_GREATERTHAN, + OP_GREATERTHANEQUALTO, + OP_LESSTHAN, + OP_LESSTHANEQUALTO, + OP_EQUALS, + OP_NOTEQUAL, + OP_AND, + OP_OR, + }; + static_assert(std::size(BINARY_OPERATION_MAPPING) == static_cast(SimpleBinaryOperationId::COUNT)); + + [[nodiscard]] bool IsOperation(const ISimpleExpression* expression) const + { + if (!m_disable_optimizations && expression->IsStatic()) + return false; + + return dynamic_cast(expression) != nullptr + || dynamic_cast(expression) != nullptr; + } + + void ConvertExpressionEntryBaseFunctionCall(std::vector& entries, + const CommonExpressionBaseFunctionCall* baseFunction, + const CommonMenuDef* menu, + const CommonItemDef* item) const + { + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = static_cast(baseFunction->m_function_index)}}); + + auto firstArg = true; + for (const auto& arg : baseFunction->m_args) + { + if (firstArg) + firstArg = false; + else + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_COMMA}}); + + ConvertExpressionEntry(entries, arg.get(), menu, item); + } + + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_RIGHTPAREN}}); + } + + void ConvertExpressionEntryUnaryOperation(std::vector& entries, + const SimpleExpressionUnaryOperation* unary, + const CommonMenuDef* menu, + const CommonItemDef* item) const + { + assert(static_cast(unary->m_operation_type->m_id) < static_cast(SimpleUnaryOperationId::COUNT)); + + entries.emplace_back(expressionEntry{ + .type = EET_OPERATOR, + .data = {.op = UNARY_OPERATION_MAPPING[static_cast(unary->m_operation_type->m_id)]}, + }); + + const auto wrapOperand = IsOperation(unary->m_operand.get()); + if (wrapOperand) + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_LEFTPAREN}}); + ConvertExpressionEntry(entries, unary->m_operand.get(), menu, item); + if (wrapOperand) + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_RIGHTPAREN}}); + } + + void ConvertExpressionEntryBinaryOperation(std::vector& entries, + const SimpleExpressionBinaryOperation* binary, + const CommonMenuDef* menu, + const CommonItemDef* item) const + { + // Game needs all nested operations to have parenthesis + const auto wrapLeft = IsOperation(binary->m_operand1.get()); + if (wrapLeft) + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_LEFTPAREN}}); + ConvertExpressionEntry(entries, binary->m_operand1.get(), menu, item); + if (wrapLeft) + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_RIGHTPAREN}}); + + assert(static_cast(binary->m_operation_type->m_id) < static_cast(SimpleBinaryOperationId::COUNT)); + entries.emplace_back(expressionEntry{ + .type = EET_OPERATOR, + .data = {.op = BINARY_OPERATION_MAPPING[static_cast(binary->m_operation_type->m_id)]}, + }); + + // Game needs all nested operations to have parenthesis + const auto wrapRight = IsOperation(binary->m_operand2.get()); + if (wrapRight) + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_LEFTPAREN}}); + ConvertExpressionEntry(entries, binary->m_operand2.get(), menu, item); + if (wrapRight) + entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = OP_RIGHTPAREN}}); + } + + void ConvertExpressionValue(std::vector& entries, const SimpleExpressionValue& value) const + { + expressionEntry entry{}; + entry.type = EET_OPERAND; + + switch (value.m_type) + { + case SimpleExpressionValue::Type::INT: + entry.data.operand.dataType = VAL_INT; + entry.data.operand.internals.intVal = value.m_int_value; + break; + case SimpleExpressionValue::Type::DOUBLE: + entry.data.operand.dataType = VAL_FLOAT; + entry.data.operand.internals.floatVal = static_cast(value.m_double_value); + break; + case SimpleExpressionValue::Type::STRING: + entry.data.operand.dataType = VAL_STRING; + entry.data.operand.internals.stringVal = m_memory.Dup(value.m_string_value->c_str()); + break; + } + + entries.emplace_back(entry); + } + + void ConvertExpressionEntry(std::vector& entries, + const ISimpleExpression* expression, + const CommonMenuDef* menu, + const CommonItemDef* item) const + { + if (!m_disable_optimizations && expression->IsStatic()) + { + const auto staticValue = expression->EvaluateStatic(); + ConvertExpressionValue(entries, staticValue); + } + else if (const auto* value = dynamic_cast(expression)) + { + ConvertExpressionValue(entries, *value); + } + else if (const auto* binary = dynamic_cast(expression)) + { + ConvertExpressionEntryBinaryOperation(entries, binary, menu, item); + } + else if (const auto* unary = dynamic_cast(expression)) + { + ConvertExpressionEntryUnaryOperation(entries, unary, menu, item); + } + else if (const auto* baseFunction = dynamic_cast(expression)) + { + ConvertExpressionEntryBaseFunctionCall(entries, baseFunction, menu, item); + } + else if (dynamic_cast(expression)) + { + throw MenuConversionException("T4 does not support custom menu functions", menu, item); + } + else if (dynamic_cast(expression)) + { + throw MenuConversionException("Cannot use conditional expression in menu expressions", menu, item); + } + else + { + assert(false); + throw MenuConversionException("Unknown expression entry type in menu expressions", menu, item); + } + } + + void + ConvertExpression(statement_s& statement, const ISimpleExpression* expression, const CommonMenuDef* menu, const CommonItemDef* item = nullptr) const + { + if (!expression) + return; + + std::vector entries; + ConvertExpressionEntry(entries, expression, menu, item); + + statement.numEntries = static_cast(entries.size()); + statement.entries = m_memory.Alloc(entries.size()); + for (auto i = 0u; i < entries.size(); i++) + { + statement.entries[i] = m_memory.Alloc(); + *statement.entries[i] = entries[i]; + } + } + + void ConvertOrApplyStatement(float& staticValue, + statement_s& statement, + const ISimpleExpression* expression, + const CommonMenuDef* menu, + const CommonItemDef* item = nullptr) const + { + if (!expression) + return; + + if (!m_disable_optimizations && expression->IsStatic()) + { + const auto value = expression->EvaluateStatic(); + + if (value.m_type == SimpleExpressionValue::Type::INT) + staticValue = static_cast(value.m_int_value); + else if (value.m_type == SimpleExpressionValue::Type::DOUBLE) + staticValue = static_cast(value.m_double_value); + else + throw MenuConversionException("Cannot convert string expression value to floating point", menu, item); + } + else + { + ConvertExpression(statement, expression, menu, item); + } + } + + void ConvertOrApplyStatement(const char*& staticValue, + statement_s& statement, + const ISimpleExpression* expression, + const CommonMenuDef* menu, + const CommonItemDef* item = nullptr) const + { + if (!expression) + return; + + if (!m_disable_optimizations && expression->IsStatic()) + { + const auto value = expression->EvaluateStatic(); + if (value.m_type != SimpleExpressionValue::Type::STRING) + throw MenuConversionException("Cannot convert numeric expression value to string", menu, item); + + staticValue = m_memory.Dup(value.m_string_value->c_str()); + } + else + { + ConvertExpression(statement, expression, menu, item); + } + } + + void ConvertOrApplyStatement(Material*& staticValue, + statement_s& statement, + const ISimpleExpression* expression, + const CommonMenuDef* menu, + const CommonItemDef* item = nullptr) const + { + if (!expression) + return; + + if (!m_disable_optimizations && expression->IsStatic()) + { + const auto value = expression->EvaluateStatic(); + if (value.m_type != SimpleExpressionValue::Type::STRING) + throw MenuConversionException("Cannot convert numeric expression value to material", menu, item); + + staticValue = ConvertMaterial(*value.m_string_value, menu, item); + } + else + { + ConvertExpression(statement, expression, menu, item); + } + } + + void ConvertVisibleExpression(windowDef_t& window, + statement_s& statement, + const ISimpleExpression* expression, + const CommonMenuDef* commonMenu, + const CommonItemDef* commonItem = nullptr) const + { + if (!expression) + return; + + bool isStatic; + bool isTruthy; + if (m_disable_optimizations) + { + const auto* staticValue = dynamic_cast(expression); + isStatic = staticValue != nullptr; + isTruthy = isStatic && (staticValue->m_type == SimpleExpressionValue::Type::INT || staticValue->m_type == SimpleExpressionValue::Type::DOUBLE) + && staticValue->IsTruthy(); + } + else + { + isStatic = expression->IsStatic(); + isTruthy = isStatic && expression->EvaluateStatic().IsTruthy(); + } + + if (isStatic) + { + if (isTruthy) + window.dynamicFlags[0] |= WINDOW_FLAG_VISIBLE; + } + else + { + window.dynamicFlags[0] |= WINDOW_FLAG_VISIBLE; + ConvertExpression(statement, expression, commonMenu, commonItem); + } + } + + [[nodiscard]] const char* + ConvertEventHandlerSet(const CommonEventHandlerSet* handlers, const CommonMenuDef* menu, const CommonItemDef* item = nullptr) const + { + if (!handlers) + return nullptr; + + std::ostringstream ss; + for (const auto& element : handlers->m_elements) + { + if (element->GetType() != CommonEventHandlerElementType::SCRIPT) + throw MenuConversionException("T4 event handlers must compile to a script string", menu, item); + + ss << dynamic_cast(element.get())->m_script; + } + + const auto script = ss.str(); + return script.empty() ? nullptr : m_memory.Dup(script.c_str()); + } + + [[nodiscard]] ItemKeyHandler* ConvertKeyHandler(const std::multimap>& keyHandlers, + const CommonMenuDef* menu, + const CommonItemDef* item = nullptr) const + { + if (keyHandlers.empty()) + return nullptr; + + const auto keyHandlerCount = keyHandlers.size(); + auto* output = m_memory.Alloc(keyHandlerCount); + auto currentKeyHandler = keyHandlers.cbegin(); + for (auto i = 0u; i < keyHandlerCount; i++) + { + output[i].key = currentKeyHandler->first; + output[i].action = ConvertEventHandlerSet(currentKeyHandler->second.get(), menu, item); + + if (i + 1 < keyHandlerCount) + output[i].next = &output[i + 1]; + else + output[i].next = nullptr; + ++currentKeyHandler; + } + + return output; + } + + [[nodiscard]] const char* CreateEnableDvarString(const std::vector& stringElements) const + { + std::ostringstream ss; + + for (const auto& element : stringElements) + { + ss << "\"" << element << "\" "; + } + + return m_memory.Dup(ss.str().c_str()); + } + + [[nodiscard]] const char* ConvertEnableDvar(const CommonItemDef& commonItem, int& dvarFlags) const + { + dvarFlags = 0; + + if (!commonItem.m_enable_dvar.empty()) + { + dvarFlags |= ITEM_DVAR_FLAG_ENABLE; + return CreateEnableDvarString(commonItem.m_enable_dvar); + } + + if (!commonItem.m_disable_dvar.empty()) + { + dvarFlags |= ITEM_DVAR_FLAG_DISABLE; + return CreateEnableDvarString(commonItem.m_disable_dvar); + } + + if (!commonItem.m_show_dvar.empty()) + { + dvarFlags |= ITEM_DVAR_FLAG_SHOW; + return CreateEnableDvarString(commonItem.m_show_dvar); + } + + if (!commonItem.m_hide_dvar.empty()) + { + dvarFlags |= ITEM_DVAR_FLAG_HIDE; + return CreateEnableDvarString(commonItem.m_hide_dvar); + } + + if (!commonItem.m_focus_dvar.empty()) + { + dvarFlags |= ITEM_DVAR_FLAG_FOCUS; + return CreateEnableDvarString(commonItem.m_focus_dvar); + } + + return nullptr; + } + + [[nodiscard]] listBoxDef_s* ConvertListBoxFeatures(itemDef_s* item, + CommonItemFeaturesListBox* commonListBox, + const CommonMenuDef& parentMenu, + const CommonItemDef& commonItem) const + { + if (!commonListBox) + return nullptr; + + auto* listBox = m_memory.Alloc(); + listBox->notselectable = commonListBox->m_not_selectable ? 1 : 0; + listBox->noScrollBars = commonListBox->m_no_scrollbars ? 1 : 0; + listBox->usePaging = commonListBox->m_use_paging ? 1 : 0; + listBox->elementWidth = static_cast(commonListBox->m_element_width); + listBox->elementHeight = static_cast(commonListBox->m_element_height); + item->special = static_cast(commonListBox->m_feeder); + listBox->elementStyle = commonListBox->m_element_style; + listBox->onDoubleClick = ConvertEventHandlerSet(commonListBox->m_on_double_click.get(), &parentMenu, &commonItem); + item->onListboxSelectionChange = ConvertEventHandlerSet(commonListBox->m_on_selection_change.get(), &parentMenu, &commonItem); + ConvertColor(listBox->selectBorder, commonListBox->m_select_border); + ConvertColor(listBox->disableColor, commonItem.m_disable_color); + ConvertColor(listBox->focusColor, commonListBox->m_focus_color); + listBox->selectIcon = ConvertMaterial(commonListBox->m_select_icon, &parentMenu, &commonItem); + listBox->backgroundItemListbox = ConvertMaterial(commonListBox->m_background_item, &parentMenu, &commonItem); + listBox->highlightTexture = ConvertMaterial(commonListBox->m_highlight_texture, &parentMenu, &commonItem); + + listBox->numColumns = static_cast(std::min(std::size(listBox->columnInfo), commonListBox->m_columns.size())); + for (auto i = 0; i < listBox->numColumns; i++) + { + auto& col = listBox->columnInfo[i]; + const auto& commonCol = commonListBox->m_columns[i]; + + col.pos = commonCol.m_x_pos; + col.width = commonCol.m_width; + col.maxChars = commonCol.m_max_chars; + col.alignment = commonCol.m_alignment; + } + + return listBox; + } + + [[nodiscard]] editFieldDef_s* ConvertEditFieldFeatures(CommonItemFeaturesEditField* commonEditField) const + { + if (!commonEditField) + return nullptr; + + auto* editField = m_memory.Alloc(); + editField->defVal = static_cast(commonEditField->m_def_val); + editField->minVal = static_cast(commonEditField->m_min_val); + editField->maxVal = static_cast(commonEditField->m_max_val); + editField->maxChars = commonEditField->m_max_chars; + editField->maxCharsGotoNext = commonEditField->m_max_chars_goto_next ? 1 : 0; + editField->maxPaintChars = commonEditField->m_max_paint_chars; + + return editField; + } + + [[nodiscard]] multiDef_s* ConvertMultiValueFeatures(CommonItemFeaturesMultiValue* commonMultiValue) const + { + if (!commonMultiValue) + return nullptr; + + auto* multiValue = m_memory.Alloc(); + multiValue->count = static_cast(std::min(std::size(multiValue->dvarList), commonMultiValue->m_step_names.size())); + multiValue->strDef = !commonMultiValue->m_string_values.empty() ? 1 : 0; + + for (auto i = 0; i < multiValue->count; i++) + { + multiValue->dvarList[i] = ConvertString(commonMultiValue->m_step_names[i]); + + if (multiValue->strDef) + { + if (commonMultiValue->m_string_values.size() > static_cast(i)) + multiValue->dvarStr[i] = ConvertString(commonMultiValue->m_string_values[i]); + } + else + { + if (commonMultiValue->m_double_values.size() > static_cast(i)) + multiValue->dvarValue[i] = static_cast(commonMultiValue->m_double_values[i]); + } + } + + return multiValue; + } + + [[nodiscard]] itemDef_s* ConvertItem(const CommonMenuDef& commonParentMenu, menuDef_t& parentMenu, const CommonItemDef& commonItem) const + { + auto* item = m_memory.Alloc(); + + item->window.name = ConvertString(commonItem.m_name); + item->text = commonItem.m_text ? m_memory.Dup(commonItem.m_text->c_str()) : nullptr; + item->window.group = ConvertString(commonItem.m_group); + item->window.rectClient = ConvertRectDef(commonItem.m_rect); + item->window.style = commonItem.m_style; + ApplyFlag(item->window.staticFlags, commonItem.m_decoration, WINDOW_FLAG_DECORATION); + ApplyFlag(item->window.staticFlags, commonItem.m_auto_wrapped, WINDOW_FLAG_AUTO_WRAPPED); + ApplyFlag(item->window.staticFlags, commonItem.m_horizontal_scroll, WINDOW_FLAG_HORIZONTAL_SCROLL); + item->type = commonItem.m_type; + item->dataType = commonItem.m_type; + item->window.border = commonItem.m_border; + item->window.borderSize = static_cast(commonItem.m_border_size); + ConvertVisibleExpression(item->window, item->visibleExp, commonItem.m_visible_expression.get(), &commonParentMenu, &commonItem); + item->window.ownerDraw = commonItem.m_owner_draw; + item->window.ownerDrawFlags = commonItem.m_owner_draw_flags; + item->alignment = commonItem.m_align; + item->textAlignMode = commonItem.m_text_align; + item->textalignx = static_cast(commonItem.m_text_align_x); + item->textaligny = static_cast(commonItem.m_text_align_y); + item->textscale = static_cast(commonItem.m_text_scale); + item->textStyle = commonItem.m_text_style; + item->fontEnum = commonItem.m_text_font; + ConvertColor(item->window.backColor, commonItem.m_back_color); + + ConvertColor(item->window.foreColor, commonItem.m_fore_color); + ApplyFlag(item->window.dynamicFlags[0], !commonItem.m_fore_color.Equals(CommonColor(1.0, 1.0, 1.0, 1.0)), WINDOW_FLAG_NON_DEFAULT_FORECOLOR); + + ConvertColor(item->window.borderColor, commonItem.m_border_color); + ConvertColor(item->window.outlineColor, commonItem.m_outline_color); + item->window.background = ConvertMaterial(commonItem.m_background, &commonParentMenu, &commonItem); + item->onFocus = ConvertEventHandlerSet(commonItem.m_on_focus.get(), &commonParentMenu, &commonItem); + item->leaveFocus = ConvertEventHandlerSet(commonItem.m_on_leave_focus.get(), &commonParentMenu, &commonItem); + item->mouseEnter = ConvertEventHandlerSet(commonItem.m_on_mouse_enter.get(), &commonParentMenu, &commonItem); + item->mouseExit = ConvertEventHandlerSet(commonItem.m_on_mouse_exit.get(), &commonParentMenu, &commonItem); + item->mouseEnterText = ConvertEventHandlerSet(commonItem.m_on_mouse_enter_text.get(), &commonParentMenu, &commonItem); + item->mouseExitText = ConvertEventHandlerSet(commonItem.m_on_mouse_exit_text.get(), &commonParentMenu, &commonItem); + item->action = ConvertEventHandlerSet(commonItem.m_on_action.get(), &commonParentMenu, &commonItem); + item->onAccept = ConvertEventHandlerSet(commonItem.m_on_accept.get(), &commonParentMenu, &commonItem); + item->focusSound = ConvertSound(commonItem.m_focus_sound, &commonParentMenu, &commonItem); + item->dvar = ConvertString(commonItem.m_dvar); + item->dvarTest = ConvertString(commonItem.m_dvar_test); + item->enableDvar = ConvertEnableDvar(commonItem, item->dvarFlags); + item->onKey = ConvertKeyHandler(commonItem.m_key_handlers, &commonParentMenu, &commonItem); + ConvertOrApplyStatement(item->text, item->textExp, commonItem.m_text_expression.get(), &commonParentMenu, &commonItem); + ConvertOrApplyStatement(item->window.background, item->materialExp, commonItem.m_material_expression.get(), &commonParentMenu, &commonItem); + ConvertOrApplyStatement(item->window.rectClient.x, item->rectXExp, commonItem.m_rect_x_exp.get(), &commonParentMenu, &commonItem); + ConvertOrApplyStatement(item->window.rectClient.y, item->rectYExp, commonItem.m_rect_y_exp.get(), &commonParentMenu, &commonItem); + ConvertOrApplyStatement(item->window.rectClient.w, item->rectWExp, commonItem.m_rect_w_exp.get(), &commonParentMenu, &commonItem); + ConvertOrApplyStatement(item->window.rectClient.h, item->rectHExp, commonItem.m_rect_h_exp.get(), &commonParentMenu, &commonItem); + ConvertOrApplyStatement( + item->window.foreColor[3], item->forecolorAExp, commonItem.m_forecolor_expressions.m_a_exp.get(), &commonParentMenu, &commonItem); + item->gameMsgWindowIndex = commonItem.m_game_message_window_index; + item->gameMsgWindowMode = commonItem.m_game_message_window_mode; + + switch (commonItem.m_feature_type) + { + case CommonItemFeatureType::LISTBOX: + item->typeData.listBox = ConvertListBoxFeatures(item, commonItem.m_list_box_features.get(), commonParentMenu, commonItem); + break; + + case CommonItemFeatureType::EDIT_FIELD: + item->typeData.editField = ConvertEditFieldFeatures(commonItem.m_edit_field_features.get()); + break; + + case CommonItemFeatureType::MULTI_VALUE: + item->typeData.multi = ConvertMultiValueFeatures(commonItem.m_multi_value_features.get()); + break; + + case CommonItemFeatureType::ENUM_DVAR: + item->typeData.enumDvarName = ConvertString(commonItem.m_enum_dvar_name); + break; + + case CommonItemFeatureType::NONE: + default: + break; + } + + // Do this last so any optimizations are considered + item->window.rect = ConvertRectDefRelativeTo(item->window.rectClient, parentMenu.window.rect); + + item->parent = &parentMenu; + + return item; + } + + itemDef_s** ConvertMenuItems(const CommonMenuDef& commonMenu, menuDef_t& menu, int& itemCount) const + { + if (commonMenu.m_items.empty()) + { + itemCount = 0; + return nullptr; + } + + auto* items = m_memory.Alloc(commonMenu.m_items.size()); + for (auto i = 0u; i < commonMenu.m_items.size(); i++) + items[i] = ConvertItem(commonMenu, menu, *commonMenu.m_items[i]); + + itemCount = static_cast(commonMenu.m_items.size()); + + return items; + } + + public: + MenuConverter(const bool disableOptimizations, ISearchPath& searchPath, MemoryManager& memory, AssetCreationContext& context) + : AbstractMenuConverter(disableOptimizations, searchPath, memory, context) + { + } + + bool ConvertMenu(const CommonMenuDef& commonMenu, menuDef_t& menu, AssetRegistration& registration) override + { + try + { + menu.window.name = m_memory.Dup(commonMenu.m_name.c_str()); + menu.fullScreen = commonMenu.m_full_screen ? 1 : 0; + ApplyFlag(menu.window.staticFlags, commonMenu.m_decoration, WINDOW_FLAG_DECORATION); + menu.window.rect = ConvertRectDef(commonMenu.m_rect); + menu.window.rectClient = menu.window.rect; + menu.window.style = commonMenu.m_style; + menu.window.border = commonMenu.m_border; + menu.window.borderSize = static_cast(commonMenu.m_border_size); + ConvertColor(menu.window.backColor, commonMenu.m_back_color); + ConvertColor(menu.window.foreColor, commonMenu.m_fore_color); + ApplyFlag(menu.window.dynamicFlags[0], !commonMenu.m_fore_color.Equals(CommonColor(1.0, 1.0, 1.0, 1.0)), WINDOW_FLAG_NON_DEFAULT_FORECOLOR); + ConvertColor(menu.window.borderColor, commonMenu.m_border_color); + ConvertColor(menu.window.outlineColor, commonMenu.m_outline_color); + ConvertColor(menu.focusColor, commonMenu.m_focus_color); + ConvertColor(menu.disableColor, commonMenu.m_disable_color); + menu.window.background = ConvertMaterial(commonMenu.m_background, &commonMenu); + menu.window.ownerDraw = commonMenu.m_owner_draw; + menu.window.ownerDrawFlags = commonMenu.m_owner_draw_flags; + ApplyFlag(menu.window.staticFlags, commonMenu.m_out_of_bounds_click, WINDOW_FLAG_OUT_OF_BOUNDS_CLICK); + menu.soundName = ConvertString(commonMenu.m_sound_loop); + ApplyFlag(menu.window.staticFlags, commonMenu.m_popup, WINDOW_FLAG_POPUP); + menu.fadeClamp = static_cast(commonMenu.m_fade_clamp); + menu.fadeCycle = commonMenu.m_fade_cycle; + menu.fadeAmount = static_cast(commonMenu.m_fade_amount); + menu.fadeInAmount = static_cast(commonMenu.m_fade_in_amount); + menu.blurRadius = static_cast(commonMenu.m_blur_radius); + ApplyFlag(menu.window.staticFlags, commonMenu.m_legacy_split_screen_scale, WINDOW_FLAG_LEGACY_SPLIT_SCREEN_SCALE); + ApplyFlag(menu.window.staticFlags, commonMenu.m_hidden_during_scope, WINDOW_FLAG_HIDDEN_DURING_SCOPE); + ApplyFlag(menu.window.staticFlags, commonMenu.m_hidden_during_flashbang, WINDOW_FLAG_HIDDEN_DURING_FLASH_BANG); + ApplyFlag(menu.window.staticFlags, commonMenu.m_hidden_during_ui, WINDOW_FLAG_HIDDEN_DURING_UI); + menu.allowedBinding = ConvertString(commonMenu.m_allowed_binding); + ConvertVisibleExpression(menu.window, menu.visibleExp, commonMenu.m_visible_expression.get(), &commonMenu); + ConvertOrApplyStatement(menu.window.rect.x, menu.rectXExp, commonMenu.m_rect_x_exp.get(), &commonMenu); + ConvertOrApplyStatement(menu.window.rect.y, menu.rectYExp, commonMenu.m_rect_y_exp.get(), &commonMenu); + menu.onOpen = ConvertEventHandlerSet(commonMenu.m_on_open.get(), &commonMenu); + menu.onFocus = ConvertEventHandlerSet(commonMenu.m_on_focus.get(), &commonMenu); + menu.onClose = ConvertEventHandlerSet(commonMenu.m_on_close.get(), &commonMenu); + menu.onESC = ConvertEventHandlerSet(commonMenu.m_on_esc.get(), &commonMenu); + menu.onKey = ConvertKeyHandler(commonMenu.m_key_handlers, &commonMenu); + menu.items = ConvertMenuItems(commonMenu, menu, menu.itemCount); + } + catch (const MenuConversionException& e) + { + PrintConversionExceptionDetails(e); + return false; + } + + return true; + } + }; +} // namespace + +std::unique_ptr + IMenuConverter::Create(const bool disableOptimizations, ISearchPath& searchPath, MemoryManager& memory, AssetCreationContext& context) +{ + return std::make_unique(disableOptimizations, searchPath, memory, context); +} diff --git a/src/ObjLoading/Game/T4/Menu/MenuConverterT4.h b/src/ObjLoading/Game/T4/Menu/MenuConverterT4.h new file mode 100644 index 00000000..987922bb --- /dev/null +++ b/src/ObjLoading/Game/T4/Menu/MenuConverterT4.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Asset/AssetCreationContext.h" +#include "Game/T4/T4.h" +#include "Parsing/Menu/Domain/CommonMenuDef.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +namespace T4 +{ + class IMenuConverter + { + public: + IMenuConverter() = default; + virtual ~IMenuConverter() = default; + + virtual bool ConvertMenu(const menu::CommonMenuDef& commonMenu, menuDef_t& menu, AssetRegistration& registration) = 0; + + static std::unique_ptr Create(bool disableOptimizations, ISearchPath& searchPath, MemoryManager& memory, AssetCreationContext& context); + }; +} // namespace T4 diff --git a/src/ObjLoading/Game/T4/ObjLoaderT4.cpp b/src/ObjLoading/Game/T4/ObjLoaderT4.cpp index 2d64b6f0..b0e166aa 100644 --- a/src/ObjLoading/Game/T4/ObjLoaderT4.cpp +++ b/src/ObjLoading/Game/T4/ObjLoaderT4.cpp @@ -5,6 +5,7 @@ #include "Game/T4/Font/FontLoaderT4.h" #include "Game/T4/Image/ImageLoaderEmbeddedT4.h" #include "Game/T4/Image/ImageLoaderExternalT4.h" +#include "Game/T4/Menu/LoaderMenuListT4.h" #include "Game/T4/T4.h" #include "Game/T4/Weapon/AccuracyGraphLoaderT4.h" #include "Game/T4/XAnim/XAnimLoaderT4.h" @@ -104,6 +105,7 @@ namespace collection.AddAssetCreator(image::CreateLoaderExternalT4(memory, searchPath)); collection.AddAssetCreator(material::CreateLoaderT4(memory, searchPath)); collection.AddAssetCreator(font::CreateLoaderT4(memory, searchPath)); + collection.AddAssetCreator(menu::CreateMenuListLoaderT4(memory, searchPath)); collection.AddAssetCreator(localize::CreateLoaderT4(memory, searchPath, zone)); collection.AddAssetCreator(map_ents::CreateLoaderT4(memory, searchPath)); collection.AddAssetCreator(phys_preset::CreateRawLoaderT4(memory, searchPath, zone)); diff --git a/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h b/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h index f4ce2cfb..4e08915f 100644 --- a/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h +++ b/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h @@ -43,9 +43,13 @@ namespace menu double m_feeder = 0; int m_element_style = 0; CommonColor m_select_border; + CommonColor m_focus_color; std::string m_select_icon; + std::string m_background_item; + std::string m_highlight_texture; std::unique_ptr m_on_double_click; + std::unique_ptr m_on_selection_change; std::unique_ptr m_element_height_expression; std::vector m_columns; }; diff --git a/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h b/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h index ac211ccb..c7171a8c 100644 --- a/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h +++ b/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h @@ -45,6 +45,7 @@ namespace menu std::unique_ptr m_open_sound_exp; std::unique_ptr m_close_sound_exp; std::unique_ptr m_on_open; + std::unique_ptr m_on_focus; std::unique_ptr m_on_close; std::unique_ptr m_on_request_close; std::unique_ptr m_on_esc; diff --git a/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h b/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h index e7435bff..de5d6618 100644 --- a/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h +++ b/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h @@ -6,6 +6,7 @@ namespace menu { IW3, IW4, - IW5 + IW5, + T4 }; } // namespace menu diff --git a/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp b/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp index d0a9f11e..c5fe013d 100644 --- a/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp +++ b/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp @@ -6,6 +6,8 @@ #include "Game/IW4/MenuConstantsIW4.h" #include "Game/IW5/IW5.h" #include "Game/IW5/MenuConstantsIW5.h" +#include "Game/T4/MenuConstantsT4.h" +#include "Game/T4/T4.h" #include "MenuMatcherFactory.h" #include "Parsing/Menu/Domain/Expression/CommonExpressionBaseFunctionCall.h" #include "Parsing/Menu/Domain/Expression/CommonExpressionCustomFunctionCall.h" @@ -70,6 +72,24 @@ const std::unordered_map& MenuExpressionMatchers::GetBaseFu return iw3FunctionMap; } + if (featureLevel == FeatureLevel::T4) + { + static std::unordered_map t4FunctionMap; + static bool t4FunctionMapInitialized = false; + + if (!t4FunctionMapInitialized) + { + for (size_t i = T4::OP_FIRSTFUNCTIONCALL; i < std::extent_v; i++) + { + std::string functionName(T4::g_expFunctionNames[i]); + utils::MakeStringLowerCase(functionName); + t4FunctionMap.emplace(std::make_pair(std::move(functionName), i)); + } + } + + return t4FunctionMap; + } + if (featureLevel == FeatureLevel::IW4) { static std::unordered_map iw4FunctionMap; diff --git a/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp b/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp index bf9f8e7b..d56baf98 100644 --- a/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp +++ b/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp @@ -48,6 +48,9 @@ void MenuFileReader::SetupDefinesProxy() case FeatureLevel::IW5: defines->AddDefine(DefinesStreamProxy::Define("FEATURE_LEVEL_IW5", "1")); break; + case FeatureLevel::T4: + defines->AddDefine(DefinesStreamProxy::Define("FEATURE_LEVEL_T4", "1")); + break; default: assert(false); break; diff --git a/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp b/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp index 095faed2..8bff9b27 100644 --- a/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp +++ b/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp @@ -929,8 +929,8 @@ void EventHandlerSetScopeSequences::AddSequences(const FeatureLevel featureLevel { AddSequence(std::make_unique()); - // IW3 stores event handlers as opaque script strings rather than structured handlers. - if (featureLevel == FeatureLevel::IW3) + // IW3 and T4 store event handlers as opaque script strings rather than structured handlers. + if (featureLevel == FeatureLevel::IW3 || featureLevel == FeatureLevel::T4) { AddSequence(std::make_unique()); AddSequence(std::make_unique()); diff --git a/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp b/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp index 95e4a0e5..77f13987 100644 --- a/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp +++ b/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp @@ -109,7 +109,7 @@ GlobalScopeSequences::GlobalScopeSequences(std::vector()); - if (featureLevel != FeatureLevel::IW3) + if (featureLevel != FeatureLevel::IW3 && featureLevel != FeatureLevel::T4) AddSequence(std::make_unique()); AddSequence(std::make_unique()); AddSequence(std::make_unique()); diff --git a/src/ObjLoading/Parsing/Menu/Sequence/ItemScopeSequences.cpp b/src/ObjLoading/Parsing/Menu/Sequence/ItemScopeSequences.cpp index 4873fa30..0a50eedc 100644 --- a/src/ObjLoading/Parsing/Menu/Sequence/ItemScopeSequences.cpp +++ b/src/ObjLoading/Parsing/Menu/Sequence/ItemScopeSequences.cpp @@ -24,7 +24,7 @@ namespace class ItemScopeOperations { - inline static const CommonItemFeatureType IW3_IW4_FEATURE_TYPE_BY_TYPE[0x18]{ + inline static const CommonItemFeatureType IW3_IW4_T4_FEATURE_TYPE_BY_TYPE[0x18]{ CommonItemFeatureType::EDIT_FIELD, // ITEM_TYPE_TEXT CommonItemFeatureType::NONE, // ITEM_TYPE_BUTTON CommonItemFeatureType::NONE, // ITEM_TYPE_RADIOBUTTON @@ -84,9 +84,10 @@ namespace { case FeatureLevel::IW3: case FeatureLevel::IW4: - if (static_cast(type) >= std::extent_v) + case FeatureLevel::T4: + if (static_cast(type) >= std::extent_v) throw ParsingException(pos, "Invalid item type"); - return IW3_IW4_FEATURE_TYPE_BY_TYPE[static_cast(type)]; + return IW3_IW4_T4_FEATURE_TYPE_BY_TYPE[static_cast(type)]; case FeatureLevel::IW5: default: @@ -464,8 +465,8 @@ namespace static constexpr auto CAPTURE_FIRST_TOKEN = 1; static constexpr auto CAPTURE_COLUMN_COUNT = 2; - static constexpr const char* SYNTAX_IW3_IW4 = " [alignment]"; - static constexpr auto BASE_COLUMN_COUNT_IW3_IW4 = 3; + static constexpr const char* SYNTAX_IW3_IW4_T4 = " [alignment]"; + static constexpr auto BASE_COLUMN_COUNT_IW3_IW4_T4 = 3; static constexpr const char* SYNTAX_IW5 = " [alignment]"; static constexpr auto BASE_COLUMN_COUNT_IW5 = 5; @@ -487,7 +488,8 @@ namespace void ProcessMatch(MenuFileParserState* state, SequenceResult& result) const override { assert(state->m_current_item); - assert(state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4 || state->m_feature_level == FeatureLevel::IW5); + assert(state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4 || state->m_feature_level == FeatureLevel::IW5 + || state->m_feature_level == FeatureLevel::T4); const auto& firstToken = result.NextCapture(CAPTURE_FIRST_TOKEN); ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, firstToken.GetPos()); @@ -500,10 +502,10 @@ namespace size_t baseColumnCount; const char* syntax; bool hasHeightValues; - if (state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4) + if (state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4 || state->m_feature_level == FeatureLevel::T4) { - baseColumnCount = BASE_COLUMN_COUNT_IW3_IW4; - syntax = SYNTAX_IW3_IW4; + baseColumnCount = BASE_COLUMN_COUNT_IW3_IW4_T4; + syntax = SYNTAX_IW3_IW4_T4; hasHeightValues = false; } else @@ -1143,6 +1145,35 @@ void ItemScopeSequences::AddSequences(const FeatureLevel featureLevel, const boo state->m_current_item->m_list_box_features->m_select_icon = value; })); + if (featureLevel == FeatureLevel::T4) + { + AddSequence(std::make_unique( + "onListboxSelectionChange", + [](const MenuFileParserState* state, const TokenPos& pos) -> std::unique_ptr& + { + ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos); + return state->m_current_item->m_list_box_features->m_on_selection_change; + })); + AddSequence(std::make_unique("focusColor", + [](const MenuFileParserState* state, const TokenPos& pos, const CommonColor value) + { + ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos); + state->m_current_item->m_list_box_features->m_focus_color = value; + })); + AddSequence(std::make_unique("backgroundItemListbox", + [](const MenuFileParserState* state, const TokenPos& pos, const std::string& value) + { + ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos); + state->m_current_item->m_list_box_features->m_background_item = value; + })); + AddSequence(std::make_unique("highlightTexture", + [](const MenuFileParserState* state, const TokenPos& pos, const std::string& value) + { + ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos); + state->m_current_item->m_list_box_features->m_highlight_texture = value; + })); + } + if (featureLevel == FeatureLevel::IW5) { AddSequence( diff --git a/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp b/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp index 5b6e7bf5..dac12f8c 100644 --- a/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp +++ b/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp @@ -261,6 +261,15 @@ void MenuScopeSequences::AddSequences(const FeatureLevel featureLevel, const boo { return state->m_current_menu->m_on_open; })); + if (featureLevel == FeatureLevel::T4) + { + AddSequence(std::make_unique( + "onFocus", + [](const MenuFileParserState* state, const TokenPos&) -> std::unique_ptr& + { + return state->m_current_menu->m_on_focus; + })); + } AddSequence(std::make_unique( "onClose", [](const MenuFileParserState* state, const TokenPos&) -> std::unique_ptr& diff --git a/src/ObjWriting/Game/T4/Menu/MenuDumperT4.cpp b/src/ObjWriting/Game/T4/Menu/MenuDumperT4.cpp new file mode 100644 index 00000000..bd978ff8 --- /dev/null +++ b/src/ObjWriting/Game/T4/Menu/MenuDumperT4.cpp @@ -0,0 +1,52 @@ +#include "MenuDumperT4.h" + +#include "MenuListDumperT4.h" +#include "MenuWriterT4.h" +#include "ObjWriting.h" + +#include +#include + +using namespace T4; + +namespace +{ + std::string GetPathForMenu(menu::MenuDumpingZoneState* zoneState, const XAssetInfo& asset) + { + const auto menuDumpingState = zoneState->m_menu_dumping_state_map.find(asset.Asset()); + + if (menuDumpingState == zoneState->m_menu_dumping_state_map.end()) + return std::format("ui_mp/{}.menu", asset.Asset()->window.name); + + return menuDumpingState->second.m_path; + } +} // namespace + +namespace menu +{ + void MenuDumperT4::DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) + { + const auto* menu = asset.Asset(); + auto* zoneState = context.GetZoneAssetDumperState(); + + if (!ObjWriting::ShouldHandleAssetType(ASSET_TYPE_MENULIST)) + { + // Make sure menu paths based on menu lists are created + const auto menuListAssets = context.m_zone.m_pools.PoolAssets(); + for (auto* menuListAsset : menuListAssets) + CreateDumpingStateForMenuListT4(zoneState, menuListAsset->Asset()); + } + + const auto menuFilePath = GetPathForMenu(zoneState, asset); + const auto assetFile = context.OpenAssetFile(menuFilePath); + + if (!assetFile) + return; + + const auto menuWriter = CreateMenuWriterT4(*assetFile); + + menuWriter->Start(); + menuWriter->WriteMenu(*menu); + menuWriter->End(); + } +} // namespace menu diff --git a/src/ObjWriting/Game/T4/Menu/MenuDumperT4.h b/src/ObjWriting/Game/T4/Menu/MenuDumperT4.h new file mode 100644 index 00000000..eadd2b47 --- /dev/null +++ b/src/ObjWriting/Game/T4/Menu/MenuDumperT4.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Dumping/AbstractAssetDumper.h" +#include "Game/T4/T4.h" + +namespace menu +{ + class MenuDumperT4 final : public AbstractAssetDumper + { + protected: + void DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) override; + }; +} // namespace menu diff --git a/src/ObjWriting/Game/T4/Menu/MenuListDumperT4.cpp b/src/ObjWriting/Game/T4/Menu/MenuListDumperT4.cpp new file mode 100644 index 00000000..bf9f6d92 --- /dev/null +++ b/src/ObjWriting/Game/T4/Menu/MenuListDumperT4.cpp @@ -0,0 +1,123 @@ +#include "MenuListDumperT4.h" + +#include "MenuWriterT4.h" + +#include +#include + +namespace fs = std::filesystem; + +using namespace T4; + +namespace +{ + void DumpMenus(menu::IWriterT4& menuDumper, menu::MenuDumpingZoneState* zoneState, const MenuList* menuList) + { + if (!menuList->menus) + return; + + for (auto menuNum = 0; menuNum < menuList->menuCount; menuNum++) + { + const auto* menu = menuList->menus[menuNum]; + if (!menu) + continue; + + const auto menuDumpingState = zoneState->m_menu_dumping_state_map.find(menu); + if (menuDumpingState == zoneState->m_menu_dumping_state_map.end()) + continue; + + const auto* menuAssetName = menu->window.name; + const auto isReference = menuAssetName && menuAssetName[0] == ','; + + // If the menu was embedded directly as menu list write its data in the menu list file + if (!isReference && menuDumpingState->second.m_alias_menu_list == menuList) + menuDumper.WriteMenu(*menu); + else + menuDumper.IncludeMenu(menuDumpingState->second.m_path); + } + } + + std::string PathForMenu(const std::string& menuListParentPath, const menuDef_t* menu) + { + const auto* menuAssetName = menu->window.name; + + if (!menuAssetName) + return {}; + + if (menuAssetName[0] == ',') + menuAssetName = &menuAssetName[1]; + + return std::format("{}{}.menu", menuListParentPath, menuAssetName); + } +} // namespace + +namespace menu +{ + void CreateDumpingStateForMenuListT4(MenuDumpingZoneState* zoneState, const MenuList* menuList) + { + if (!menuList || menuList->menuCount <= 0 || !menuList->menus || !menuList->name) + return; + + const std::string menuListName(menuList->name); + const fs::path p(menuListName); + std::string parentPath; + if (p.has_parent_path()) + parentPath = p.parent_path().generic_string() + "/"; + + for (auto i = 0; i < menuList->menuCount; i++) + { + const auto* menu = menuList->menus[i]; + + if (!menu) + continue; + + auto menuPath = PathForMenu(parentPath, menu); + if (menuPath.empty()) + continue; + + auto existingState = zoneState->m_menu_dumping_state_map.find(menu); + if (existingState == zoneState->m_menu_dumping_state_map.end()) + { + const auto isTheSameAsMenuList = menuPath == menuListName; + zoneState->CreateMenuDumpingState(menu, std::move(menuPath), isTheSameAsMenuList ? menuList : nullptr); + } + else if (!existingState->second.m_alias_menu_list) + { + const auto isTheSameAsMenuList = menuPath == menuListName; + if (isTheSameAsMenuList) + { + existingState->second.m_alias_menu_list = menuList; + existingState->second.m_path = std::move(menuPath); + } + } + } + } + + void MenuListDumperT4::DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) + { + const auto* menuList = asset.Asset(); + const auto assetFile = context.OpenAssetFile(asset.m_name); + + if (!assetFile) + return; + + auto* zoneState = context.GetZoneAssetDumperState(); + + const auto menuWriter = CreateMenuWriterT4(*assetFile); + + menuWriter->Start(); + DumpMenus(*menuWriter, zoneState, menuList); + menuWriter->End(); + } + + void MenuListDumperT4::Dump(AssetDumpingContext& context) + { + auto* zoneState = context.GetZoneAssetDumperState(); + + const auto menuListAssets = context.m_zone.m_pools.PoolAssets(); + for (const auto* asset : menuListAssets) + CreateDumpingStateForMenuListT4(zoneState, asset->Asset()); + + AbstractAssetDumper::Dump(context); + } +} // namespace menu diff --git a/src/ObjWriting/Game/T4/Menu/MenuListDumperT4.h b/src/ObjWriting/Game/T4/Menu/MenuListDumperT4.h new file mode 100644 index 00000000..22ca9014 --- /dev/null +++ b/src/ObjWriting/Game/T4/Menu/MenuListDumperT4.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Dumping/AbstractAssetDumper.h" +#include "Game/T4/T4.h" +#include "Menu/MenuDumpingZoneState.h" + +namespace menu +{ + void CreateDumpingStateForMenuListT4(MenuDumpingZoneState* zoneState, const T4::MenuList* menuList); + + class MenuListDumperT4 final : public AbstractAssetDumper + { + public: + void Dump(AssetDumpingContext& context) override; + + protected: + void DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) override; + }; +} // namespace menu diff --git a/src/ObjWriting/Game/T4/Menu/MenuWriterT4.cpp b/src/ObjWriting/Game/T4/Menu/MenuWriterT4.cpp new file mode 100644 index 00000000..262e156b --- /dev/null +++ b/src/ObjWriting/Game/T4/Menu/MenuWriterT4.cpp @@ -0,0 +1,686 @@ +#include "MenuWriterT4.h" + +#include "Game/T4/MenuConstantsT4.h" +#include "Menu/AbstractMenuWriter.h" +#include "ObjWriting.h" + +#include +#include +#include +#include + +using namespace T4; + +namespace +{ + // Set this to true to skip interpretative expression dumping + constexpr auto DUMP_NAIVE = false; + + size_t FindStatementClosingParenthesis(const statement_s& statement, const size_t openingParenthesisPosition) + { + assert(statement.numEntries >= 0); + assert(openingParenthesisPosition < static_cast(statement.numEntries)); + + const auto statementEnd = static_cast(statement.numEntries); + + // The openingParenthesisPosition does not necessarily point to an actual opening parenthesis operator. That's fine though. + // We will pretend it does since the game does sometimes leave out opening parenthesis from the entries. + auto currentParenthesisDepth = 1; + for (auto currentSearchPosition = openingParenthesisPosition + 1; currentSearchPosition < statementEnd; currentSearchPosition++) + { + const auto* expEntry = statement.entries[currentSearchPosition]; + if (!expEntry || expEntry->type != EET_OPERATOR) + continue; + + // Any function means a "left out" left paren + if (expEntry->data.op == OP_LEFTPAREN || expEntry->data.op >= OP_FIRSTFUNCTIONCALL) + { + currentParenthesisDepth++; + } + else if (expEntry->data.op == OP_RIGHTPAREN) + { + if (currentParenthesisDepth > 0) + currentParenthesisDepth--; + if (currentParenthesisDepth == 0) + return currentSearchPosition; + } + } + + return statementEnd; + } + + class MenuWriter final : public menu::AbstractBaseWriter, public menu::IWriterT4 + { + public: + explicit MenuWriter(std::ostream& stream) + : AbstractBaseWriter(stream) + { + } + + void WriteMenu(const menuDef_t& menu) override + { + StartMenuDefScope(); + WriteMenuData(menu); + EndScope(); + } + + void Start() override + { + AbstractBaseWriter::Start(); + } + + void End() override + { + AbstractBaseWriter::End(); + } + + void IncludeMenu(const std::string& menuPath) const override + { + AbstractBaseWriter::IncludeMenu(menuPath); + } + + private: + static bool HasStatement(const statement_s& statement) + { + return statement.numEntries > 0 && statement.entries; + } + + void WriteStatementNaive(const statement_s& statement) const + { + const auto entryCount = static_cast(statement.numEntries); + + const auto missingClosingParenthesis = statement.numEntries > 0 && statement.entries[0]->type == EET_OPERATOR + && statement.entries[0]->data.op == OP_LEFTPAREN + && FindStatementClosingParenthesis(statement, 0) >= static_cast(statement.numEntries); + + for (auto i = 0uz; i < entryCount; i++) + { + const auto& entry = statement.entries[i]; + if (entry->type == EET_OPERAND) + { + size_t pos = i; + bool discard = false; + WriteStatementOperand(statement, pos, discard); + } + else + { + assert(entry->data.op >= 0 && static_cast(entry->data.op) < std::extent_v); + if (entry->data.op >= 0 && static_cast(entry->data.op) < std::extent_v) + m_stream << g_expFunctionNames[entry->data.op]; + if (entry->data.op >= OP_FIRSTFUNCTIONCALL) + m_stream << "("; + } + } + + if (missingClosingParenthesis) + m_stream << ")"; + } + + void WriteStatementOperator(const statement_s& statement, size_t& currentPos, bool& spaceNext) const + { + const auto& expEntry = statement.entries[currentPos]; + + if (spaceNext && expEntry->data.op != OP_COMMA) + m_stream << " "; + + if (expEntry->data.op == OP_LEFTPAREN) + { + const auto closingParenPos = FindStatementClosingParenthesis(statement, currentPos); + m_stream << "("; + WriteStatementEntryRange(statement, currentPos + 1, closingParenPos); + m_stream << ")"; + + currentPos = closingParenPos + 1; + spaceNext = true; + } + else + { + if (expEntry->data.op >= 0 && static_cast(expEntry->data.op) < std::extent_v) + m_stream << g_expFunctionNames[expEntry->data.op]; + + if (expEntry->data.op >= OP_FIRSTFUNCTIONCALL) + { + // Functions do not have opening parenthesis in the entries. We can just pretend they do though + const auto closingParenPos = FindStatementClosingParenthesis(statement, currentPos); + m_stream << "("; + WriteStatementEntryRange(statement, currentPos + 1, closingParenPos); + m_stream << ")"; + currentPos = closingParenPos + 1; + } + else + currentPos++; + + spaceNext = expEntry->data.op != OP_NOT; + } + } + + void WriteStatementOperand(const statement_s& statement, size_t& currentPos, bool& spaceNext) const + { + const auto& expEntry = statement.entries[currentPos]; + + if (spaceNext) + m_stream << " "; + + const auto& operand = expEntry->data.operand; + + switch (operand.dataType) + { + case VAL_FLOAT: + m_stream << operand.internals.floatVal; + break; + + case VAL_INT: + m_stream << operand.internals.intVal; + break; + + case VAL_STRING: + WriteEscapedString(operand.internals.stringVal); + break; + + default: + break; + } + + currentPos++; + spaceNext = true; + } + + void WriteStatementEntryRange(const statement_s& statement, const size_t startOffset, const size_t endOffset) const + { + assert(startOffset <= endOffset); + assert(endOffset <= static_cast(statement.numEntries)); + + auto currentPos = startOffset; + auto spaceNext = false; + while (currentPos < endOffset) + { + const auto& expEntry = statement.entries[currentPos]; + + if (expEntry->type == EET_OPERATOR) + { + WriteStatementOperator(statement, currentPos, spaceNext); + } + else + { + WriteStatementOperand(statement, currentPos, spaceNext); + } + } + } + + void WriteStatement(const statement_s& statement) const + { + if (!HasStatement(statement)) + return; + + WriteStatementEntryRange(statement, 0, static_cast(statement.numEntries)); + } + + void WriteStatementSkipInitialUnnecessaryParenthesis(const statement_s& statement) const + { + if (!HasStatement(statement)) + return; + + const auto statementEnd = static_cast(statement.numEntries); + + if (statement.numEntries >= 1 && statement.entries[0]->type == EET_OPERATOR && statement.entries[0]->data.op == OP_LEFTPAREN) + { + const auto parenthesisEnd = FindStatementClosingParenthesis(statement, 0); + + if (parenthesisEnd >= statementEnd) + WriteStatementEntryRange(statement, 1, statementEnd); + else if (parenthesisEnd == statementEnd - 1) + WriteStatementEntryRange(statement, 1, statementEnd - 1); + else + WriteStatementEntryRange(statement, 0, statementEnd); + } + else + { + WriteStatementEntryRange(statement, 0, statementEnd); + } + } + + void WriteStatementProperty(const std::string& propertyKey, const statement_s& statement, const bool isBooleanStatement) const + { + if (!HasStatement(statement)) + return; + + Indent(); + WriteKey(propertyKey); + + if (isBooleanStatement) + { + m_stream << "when("; + if constexpr (DUMP_NAIVE) + WriteStatementNaive(statement); + else + WriteStatementSkipInitialUnnecessaryParenthesis(statement); + m_stream << ");\n"; + } + else + { + if constexpr (DUMP_NAIVE) + WriteStatementNaive(statement); + else + WriteStatement(statement); + m_stream << ";\n"; + } + } + + // #define WRITE_ORIGINAL_SCRIPT + void WriteUnconditionalScript(const char* script) const + { +#ifdef WRITE_ORIGINAL_SCRIPT + Indent(); + m_stream << script << "\n"; + return; +#endif + + const auto tokenList = CreateScriptTokenList(script); + + auto isNewStatement = true; + for (const auto& token : tokenList) + { + if (isNewStatement) + { + if (token == ";") + continue; + Indent(); + } + + if (token == ";") + { + m_stream << ";\n"; + isNewStatement = true; + continue; + } + + if (!isNewStatement) + m_stream << " "; + else + isNewStatement = false; + + if (DoesTokenNeedQuotationMarks(token)) + WriteEscapedString(token); + else + m_stream << token; + } + + if (!isNewStatement) + m_stream << ";\n"; + } + + void WriteScriptProperty(const std::string& propertyKey, const char* script) + { + if (!script || !script[0]) + return; + + Indent(); + m_stream << propertyKey << "\n"; + Indent(); + m_stream << "{\n"; + IncIndent(); + WriteUnconditionalScript(script); + DecIndent(); + Indent(); + m_stream << "}\n"; + } + + void WriteRectProperty(const std::string& propertyKey, const rectDef_s& rect) const + { + Indent(); + WriteKey(propertyKey); + m_stream << rect.x << " " << rect.y << " " << rect.w << " " << rect.h << " " << rect.horzAlign << " " << rect.vertAlign << "\n"; + } + + void WriteMaterialProperty(const std::string& propertyKey, const Material* material) const + { + if (!material || !material->info.name) + return; + + const auto* materialName = material->info.name; + if (materialName[0] == ',') + materialName++; + WriteStringProperty(propertyKey, materialName); + } + + void WriteSoundAliasProperty(const std::string& propertyKey, const snd_alias_list_t* soundAlias) const + { + if (soundAlias) + WriteStringProperty(propertyKey, soundAlias->aliasName); + } + + void WriteItemKeyHandlers(const ItemKeyHandler* handler) + { + for (const auto* current = handler; current; current = current->next) + { + std::string key; + if (current->key >= '!' && current->key <= '~' && current->key != '"') + key = std::format("execKey \"{}\"", static_cast(current->key)); + else + key = std::format("execKeyInt {}", current->key); + + WriteScriptProperty(key, current->action); + } + } + + void WriteMultiTokenStringProperty(const std::string& propertyKey, const char* value) const + { + if (!value) + return; + + Indent(); + WriteKey(propertyKey); + m_stream << "{ "; + + const auto tokenList = CreateScriptTokenList(value); + auto firstToken = true; + for (const auto& token : tokenList) + { + if (firstToken) + firstToken = false; + else + m_stream << ";"; + + WriteEscapedString(token); + } + if (!firstToken) + m_stream << " "; + m_stream << "}\n"; + } + + void WriteColumnProperty(const listBoxDef_s& listBox) const + { + if (listBox.numColumns <= 0) + return; + + Indent(); + WriteKey("columns"); + m_stream << listBox.numColumns << "\n"; + + const auto columnCount = std::min(listBox.numColumns, std::size(listBox.columnInfo)); + for (size_t columnIndex = 0u; columnIndex < columnCount; columnIndex++) + { + const auto& column = listBox.columnInfo[columnIndex]; + + Indent(); + for (auto i = 0u; i < MENU_KEY_SPACING; i++) + m_stream << " "; + + m_stream << column.pos << " " << column.width << " " << column.maxChars << " " << column.alignment << "\n"; + } + } + + void WriteListBoxProperties(const itemDef_s& item) + { + if (item.type != ITEM_TYPE_LISTBOX || !item.typeData.listBox) + return; + + const auto& listBox = *item.typeData.listBox; + WriteKeywordProperty("notselectable", listBox.notselectable != 0); + WriteKeywordProperty("noscrollbars", listBox.noScrollBars != 0); + WriteKeywordProperty("usepaging", listBox.usePaging != 0); + WriteFloatProperty("elementwidth", listBox.elementWidth, 0.0f); + WriteFloatProperty("elementheight", listBox.elementHeight, 0.0f); + WriteFloatProperty("feeder", item.special, 0.0f); + WriteIntProperty("elementtype", listBox.elementStyle, 0); + WriteColumnProperty(listBox); + WriteScriptProperty("doubleclick", listBox.onDoubleClick); + WriteScriptProperty("onListboxSelectionChange", item.onListboxSelectionChange); + WriteColorProperty("selectBorder", listBox.selectBorder, COLOR_0000); + WriteColorProperty("disableColor", listBox.disableColor, COLOR_0000); + WriteColorProperty("focusColor", listBox.focusColor, COLOR_0000); + WriteMaterialProperty("selectIcon", listBox.selectIcon); + WriteMaterialProperty("backgroundItemListbox", listBox.backgroundItemListbox); + WriteMaterialProperty("highlightTexture", listBox.highlightTexture); + } + + void WriteDvarFloatProperty(const itemDef_s& item, const editFieldDef_s& editField) const + { + if (!item.dvar) + return; + + Indent(); + WriteKey("dvarFloat"); + WriteEscapedString(item.dvar); + m_stream << " " << editField.defVal << " " << editField.minVal << " " << editField.maxVal << "\n"; + } + + void WriteEditFieldProperties(const itemDef_s& item) const + { + switch (item.type) + { + case ITEM_TYPE_TEXT: + case ITEM_TYPE_EDITFIELD: + case ITEM_TYPE_NUMERICFIELD: + case ITEM_TYPE_SLIDER: + case ITEM_TYPE_YESNO: + case ITEM_TYPE_BIND: + case ITEM_TYPE_VALIDFILEFIELD: + case ITEM_TYPE_DECIMALFIELD: + case ITEM_TYPE_UPREDITFIELD: + break; + + default: + return; + } + + if (!item.typeData.editField) + return; + + const auto& editField = *item.typeData.editField; + if (std::fabs(-1.0f - editField.defVal) >= std::numeric_limits::epsilon() + || std::fabs(-1.0f - editField.minVal) >= std::numeric_limits::epsilon() + || std::fabs(-1.0f - editField.maxVal) >= std::numeric_limits::epsilon()) + { + WriteDvarFloatProperty(item, editField); + } + else + { + WriteStringProperty("dvar", item.dvar); + } + + WriteIntProperty("maxChars", editField.maxChars, 0); + WriteKeywordProperty("maxCharsGotoNext", editField.maxCharsGotoNext != 0); + WriteIntProperty("maxPaintChars", editField.maxPaintChars, 0); + } + + void WriteMultiValueProperty(const multiDef_s& multi) const + { + if (multi.count <= 0) + return; + + Indent(); + WriteKey(multi.strDef ? "dvarStrList" : "dvarFloatList"); + m_stream << "{"; + const auto valueCount = std::min(multi.count, std::size(multi.dvarValue)); + for (size_t valueIndex = 0u; valueIndex < valueCount; valueIndex++) + { + if (!multi.dvarList[valueIndex] || (multi.strDef && !multi.dvarStr[valueIndex])) + continue; + + m_stream << " "; + WriteEscapedString(multi.dvarList[valueIndex]); + m_stream << " "; + if (multi.strDef) + WriteEscapedString(multi.dvarStr[valueIndex]); + else + m_stream << multi.dvarValue[valueIndex]; + } + m_stream << " }\n"; + } + + void WriteMultiProperties(const itemDef_s& item) const + { + if (item.type != ITEM_TYPE_MULTI || !item.typeData.multi) + return; + + WriteStringProperty("dvar", item.dvar); + WriteMultiValueProperty(*item.typeData.multi); + } + + void WriteEnumDvarProperties(const itemDef_s& item) const + { + if (item.type != ITEM_TYPE_DVARENUM) + return; + + WriteStringProperty("dvar", item.dvar); + WriteStringProperty("dvarEnumList", item.typeData.enumDvarName); + } + + void WriteItemTextProperty(const char* text) const + { + // T4 distinguishes explicitly empty text from null text, which falls back to the item's dvar. + if (!text) + return; + + Indent(); + WriteKey("text"); + WriteEscapedString(text); + m_stream << "\n"; + } + + void WriteItemData(const itemDef_s& item) + { + WriteStringProperty("name", item.window.name); + WriteItemTextProperty(item.text); + WriteStringProperty("group", item.window.group); + WriteRectProperty("rect", item.window.rectClient); + WriteIntProperty("style", item.window.style, 0); + WriteKeywordProperty("decoration", item.window.staticFlags & WINDOW_FLAG_DECORATION); + WriteKeywordProperty("autowrapped", item.window.staticFlags & WINDOW_FLAG_AUTO_WRAPPED); + WriteKeywordProperty("horizontalscroll", item.window.staticFlags & WINDOW_FLAG_HORIZONTAL_SCROLL); + WriteIntProperty("type", item.type, ITEM_TYPE_TEXT); + WriteIntProperty("border", item.window.border, 0); + WriteFloatProperty("borderSize", item.window.borderSize, 0.0f); + + if (HasStatement(item.visibleExp)) + WriteStatementProperty("visible", item.visibleExp, true); + else if (item.window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE) + WriteIntProperty("visible", 1, 0); + + WriteIntProperty("ownerdraw", item.window.ownerDraw, 0); + WriteFlagsProperty("ownerdrawFlag", item.window.ownerDrawFlags); + WriteIntProperty("align", item.alignment, 0); + WriteIntProperty("textalign", item.textAlignMode, 0); + WriteFloatProperty("textalignx", item.textalignx, 0.0f); + WriteFloatProperty("textaligny", item.textaligny, 0.0f); + WriteFloatProperty("textscale", item.textscale, 0.0f); + WriteIntProperty("textstyle", item.textStyle, 0); + WriteIntProperty("textfont", item.fontEnum, 0); + WriteColorProperty("backcolor", item.window.backColor, COLOR_0000); + WriteColorProperty("forecolor", item.window.foreColor, COLOR_1111); + WriteColorProperty("bordercolor", item.window.borderColor, COLOR_0000); + WriteColorProperty("outlinecolor", item.window.outlineColor, COLOR_0000); + WriteMaterialProperty("background", item.window.background); + WriteScriptProperty("onFocus", item.onFocus); + WriteScriptProperty("leaveFocus", item.leaveFocus); + WriteScriptProperty("mouseEnter", item.mouseEnter); + WriteScriptProperty("mouseExit", item.mouseExit); + WriteScriptProperty("mouseEnterText", item.mouseEnterText); + WriteScriptProperty("mouseExitText", item.mouseExitText); + WriteScriptProperty("action", item.action); + WriteScriptProperty("accept", item.onAccept); + WriteSoundAliasProperty("focusSound", item.focusSound); + WriteStringProperty("dvarTest", item.dvarTest); + + if (item.dvarFlags & ITEM_DVAR_FLAG_ENABLE) + WriteMultiTokenStringProperty("enableDvar", item.enableDvar); + else if (item.dvarFlags & ITEM_DVAR_FLAG_DISABLE) + WriteMultiTokenStringProperty("disableDvar", item.enableDvar); + else if (item.dvarFlags & ITEM_DVAR_FLAG_SHOW) + WriteMultiTokenStringProperty("showDvar", item.enableDvar); + else if (item.dvarFlags & ITEM_DVAR_FLAG_HIDE) + WriteMultiTokenStringProperty("hideDvar", item.enableDvar); + else if (item.dvarFlags & ITEM_DVAR_FLAG_FOCUS) + WriteMultiTokenStringProperty("focusDvar", item.enableDvar); + + WriteItemKeyHandlers(item.onKey); + WriteStatementProperty("exp text", item.textExp, false); + WriteStatementProperty("exp material", item.materialExp, false); + WriteStatementProperty("exp rect X", item.rectXExp, false); + WriteStatementProperty("exp rect Y", item.rectYExp, false); + WriteStatementProperty("exp rect W", item.rectWExp, false); + WriteStatementProperty("exp rect H", item.rectHExp, false); + WriteStatementProperty("exp forecolor A", item.forecolorAExp, false); + WriteIntProperty("gamemsgwindowindex", item.gameMsgWindowIndex, 0); + WriteIntProperty("gamemsgwindowmode", item.gameMsgWindowMode, 0); + + WriteListBoxProperties(item); + WriteEditFieldProperties(item); + WriteMultiProperties(item); + WriteEnumDvarProperties(item); + } + + void WriteItemDefs(const itemDef_s* const* items, const size_t itemCount) + { + if (!items || itemCount <= 0) + return; + + for (size_t itemIndex = 0u; itemIndex < itemCount; itemIndex++) + { + const auto* item = items[itemIndex]; + if (!item) + continue; + + StartItemDefScope(); + WriteItemData(*item); + EndScope(); + } + } + + void WriteMenuData(const menuDef_t& menu) + { + WriteStringProperty("name", menu.window.name); + WriteBoolProperty("fullscreen", menu.fullScreen != 0, false); + WriteKeywordProperty("decoration", menu.window.staticFlags & WINDOW_FLAG_DECORATION); + WriteRectProperty("rect", menu.window.rect); + WriteIntProperty("style", menu.window.style, 0); + WriteIntProperty("border", menu.window.border, 0); + WriteFloatProperty("borderSize", menu.window.borderSize, 0.0f); + WriteColorProperty("backcolor", menu.window.backColor, COLOR_0000); + WriteColorProperty("forecolor", menu.window.foreColor, COLOR_1111); + WriteColorProperty("bordercolor", menu.window.borderColor, COLOR_0000); + WriteColorProperty("focuscolor", menu.focusColor, COLOR_0000); + WriteColorProperty("disablecolor", menu.disableColor, COLOR_0000); + WriteColorProperty("outlinecolor", menu.window.outlineColor, COLOR_0000); + WriteMaterialProperty("background", menu.window.background); + WriteIntProperty("ownerdraw", menu.window.ownerDraw, 0); + WriteFlagsProperty("ownerdrawFlag", menu.window.ownerDrawFlags); + WriteKeywordProperty("outOfBoundsClick", menu.window.staticFlags & WINDOW_FLAG_OUT_OF_BOUNDS_CLICK); + WriteStringProperty("soundLoop", menu.soundName); + WriteKeywordProperty("popup", menu.window.staticFlags & WINDOW_FLAG_POPUP); + WriteFloatProperty("fadeClamp", menu.fadeClamp, 0.0f); + WriteIntProperty("fadeCycle", menu.fadeCycle, 0); + WriteFloatProperty("fadeAmount", menu.fadeAmount, 0.0f); + WriteFloatProperty("fadeInAmount", menu.fadeInAmount, 0.0f); + WriteFloatProperty("blurWorld", menu.blurRadius, 0.0f); + WriteKeywordProperty("legacySplitScreenScale", menu.window.staticFlags & WINDOW_FLAG_LEGACY_SPLIT_SCREEN_SCALE); + WriteKeywordProperty("hiddenDuringScope", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_SCOPE); + WriteKeywordProperty("hiddenDuringFlashbang", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_FLASH_BANG); + WriteKeywordProperty("hiddenDuringUI", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_UI); + WriteStringProperty("allowedBinding", menu.allowedBinding); + + if (HasStatement(menu.visibleExp)) + WriteStatementProperty("visible", menu.visibleExp, true); + else if (menu.window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE) + WriteIntProperty("visible", 1, 0); + + WriteStatementProperty("exp rect X", menu.rectXExp, false); + WriteStatementProperty("exp rect Y", menu.rectYExp, false); + WriteScriptProperty("onOpen", menu.onOpen); + WriteScriptProperty("onFocus", menu.onFocus); + WriteScriptProperty("onClose", menu.onClose); + WriteScriptProperty("onESC", menu.onESC); + WriteItemKeyHandlers(menu.onKey); + WriteItemDefs(menu.items, menu.itemCount); + } + }; +} // namespace + +namespace menu +{ + std::unique_ptr CreateMenuWriterT4(std::ostream& stream) + { + return std::make_unique(stream); + } +} // namespace menu diff --git a/src/ObjWriting/Game/T4/Menu/MenuWriterT4.h b/src/ObjWriting/Game/T4/Menu/MenuWriterT4.h new file mode 100644 index 00000000..7358c67f --- /dev/null +++ b/src/ObjWriting/Game/T4/Menu/MenuWriterT4.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Game/T4/T4.h" +#include "Menu/IMenuWriter.h" + +#include +#include + +namespace menu +{ + class IWriterT4 : public IWriter + { + public: + virtual void WriteMenu(const T4::menuDef_t& menu) = 0; + }; + + std::unique_ptr CreateMenuWriterT4(std::ostream& stream); +} // namespace menu diff --git a/src/ObjWriting/Game/T4/ObjWriterT4.cpp b/src/ObjWriting/Game/T4/ObjWriterT4.cpp index de5cd741..fe9eb4cd 100644 --- a/src/ObjWriting/Game/T4/ObjWriterT4.cpp +++ b/src/ObjWriting/Game/T4/ObjWriterT4.cpp @@ -7,6 +7,8 @@ #include "Game/T4/XAnim/XAnimDumperT4.h" #include "Game/T4/XModel/XModelDumperT4.h" #include "Localize/LocalizeDumperT4.h" +#include "Menu/MenuDumperT4.h" +#include "Menu/MenuListDumperT4.h" #include "PhysConstraints/PhysConstraintsInfoStringDumperT4.h" #include "PhysPreset/PhysPresetInfoStringDumperT4.h" #include "RawFile/RawFileDumperT4.h" @@ -27,6 +29,8 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); + RegisterAssetDumper(std::make_unique()); + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); diff --git a/src/ZoneCode/Game/T4/XAssets/menuDef_t.txt b/src/ZoneCode/Game/T4/XAssets/menuDef_t.txt index 38198715..9b114141 100644 --- a/src/ZoneCode/Game/T4/XAssets/menuDef_t.txt +++ b/src/ZoneCode/Game/T4/XAssets/menuDef_t.txt @@ -34,8 +34,8 @@ set condition expressionEntry::data::operand type == EET_OPERAND; use Operand; set condition internals::intVal dataType == VAL_INT; set condition internals::floatVal dataType == VAL_FLOAT; -set condition internals::string dataType == VAL_STRING; -set string internals::string; +set condition internals::stringVal dataType == VAL_STRING; +set string internals::stringVal; // itemDef_s use itemDef_s; @@ -72,7 +72,7 @@ set condition multi itemDef_s::type == ITEM_TYPE_MULTI; set condition enumDvarName itemDef_s::type == ITEM_TYPE_DVARENUM; // listBoxDef_s -set string listBoxDef_s::doubleClick; +set string listBoxDef_s::onDoubleClick; // multiDef_s use multiDef_s; diff --git a/test/ObjLoadingTests/Game/T4/Menu/LoaderMenuListT4Test.cpp b/test/ObjLoadingTests/Game/T4/Menu/LoaderMenuListT4Test.cpp new file mode 100644 index 00000000..b84b401a --- /dev/null +++ b/test/ObjLoadingTests/Game/T4/Menu/LoaderMenuListT4Test.cpp @@ -0,0 +1,269 @@ +#include "Game/T4/Menu/LoaderMenuListT4.h" + +#include "Game/T4/GameT4.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include + +using namespace T4; +using namespace std::literals; +using namespace Catch; +using namespace Catch::Matchers; + +namespace +{ + class MenuLoadingTestHelper + { + public: + MenuLoadingTestHelper() + : m_zone("MockZone", 0, GameId::T4, GamePlatform::PC), + m_creator_collection(m_zone), + m_context(m_zone, &m_creator_collection, &m_ignored_asset_lookup) + { + m_loader = menu::CreateMenuListLoaderT4(m_zone.Memory(), m_search_path); + } + + void AddFile(std::string name, std::string contents) + { + m_search_path.AddFileData(std::move(name), std::move(contents)); + } + + Material* AddMaterial(const std::string& name) + { + auto* material = m_zone.Memory().Alloc(); + material->info.name = m_zone.Memory().Dup(name.c_str()); + m_context.AddAsset(name, material); + return material; + } + + AssetCreationResult LoadMenuList(const std::string& name) + { + return m_loader->CreateAsset(name, m_context); + } + + menuDef_t* GetMenu(const std::string& name) + { + const auto* asset = m_zone.m_pools.GetAsset(ASSET_TYPE_MENU, name); + REQUIRE(asset); + return static_cast(asset->m_ptr); + } + + private: + Zone m_zone; + MockSearchPath m_search_path; + std::unique_ptr m_loader; + AssetCreatorCollection m_creator_collection; + IgnoredAssetLookup m_ignored_asset_lookup; + AssetCreationContext m_context; + }; + + TEST_CASE("MenuListLoaderT4: Loads dumped menus and preserves T4 data", "[t4][menu][assetloader]") + { + MenuLoadingTestHelper helper; + const auto* backgroundMaterial = helper.AddMaterial("background_material"); + const auto* selectMaterial = helper.AddMaterial("select_material"); + const auto* listboxBackgroundMaterial = helper.AddMaterial("listbox_background_material"); + const auto* highlightMaterial = helper.AddMaterial("highlight_material"); + + helper.AddFile("ui_mp/menus.txt", R"MENU({ + loadMenu { "ui_mp/main.menu" } + loadMenu { "ui_mp/popup.menu" } +} +)MENU"); + helper.AddFile("ui_mp/main.menu", R"MENU({ + menuDef + { + name "main" + fullscreen 1 + rect 0 0 640 480 0 0 + background "background_material" + forecolor 0.8 0.7 0.6 0.5 + disablecolor 0.2 0.3 0.4 0.5 + visible when(dvarbool("ui_show_main")); + onOpen + { + play menu_open; + setLocalVarInt ui_highlight 5; + } + onFocus + { + setdvar ui_menu_focused 1; + } + itemDef + { + name "empty_text" + text "" + rect 10 20 180 24 1 2 + type 1 + visible 1 + forecolor 0.9 0.9 1 0.07 + dvar "ui_fallback_text" + action + { + setItemColor empty_text borderColor 0.1 0.1 0.12 0.5; + execondvarstringvalue ui_zfeather 0 "set cg_laserLight 0"; + } + exp material (localvarstring("ui_material")); + } + itemDef + { + name "server_list" + rect 20 60 300 200 0 0 + type 6 + columns 2 0 120 32 0 120 180 48 1 + elementwidth 300 + elementheight 20 + feeder 2 + elementtype 1 + notselectable + noscrollbars + usepaging + disablecolor 0.6 0.5 0.4 0.3 + selectBorder 1 1 1 1 + focusColor 0.1 0.2 0.3 0.4 + selectIcon "select_material" + backgroundItemListbox "listbox_background_material" + highlightTexture "highlight_material" + doubleclick + { + play mouse_click; + } + onListboxSelectionChange + { + setdvar ui_server_selected 1; + } + } + itemDef + { + name "default_text" + text "-" + maxChars 2 + } + } +} +)MENU"); + helper.AddFile("ui_mp/popup.menu", R"MENU({ + menuDef + { + name "popup" + rect 40 40 200 100 0 0 + popup + visible when(partyismissingmappack()); + onESC + { + close self; + } + } +} +)MENU"); + + const auto result = helper.LoadMenuList("ui_mp/menus.txt"); + REQUIRE(result.HasBeenSuccessful()); + + const auto* menuList = static_cast(result.GetAssetInfo()->m_ptr); + const auto* mainMenu = helper.GetMenu("main"); + const auto* popupMenu = helper.GetMenu("popup"); + + REQUIRE(menuList->name == "ui_mp/menus.txt"s); + REQUIRE(menuList->menuCount == 2); + REQUIRE(menuList->menus[0] == mainMenu); + REQUIRE(menuList->menus[1] == popupMenu); + + REQUIRE(mainMenu->window.background == backgroundMaterial); + REQUIRE(mainMenu->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR); + REQUIRE_THAT(mainMenu->disableColor[0], WithinRel(0.2f)); + REQUIRE_THAT(mainMenu->disableColor[3], WithinRel(0.5f)); + REQUIRE(mainMenu->visibleExp.numEntries == 3); + REQUIRE(mainMenu->visibleExp.entries[0]->type == EET_OPERATOR); + REQUIRE(mainMenu->visibleExp.entries[0]->data.op == OP_DVARBOOL); + REQUIRE(mainMenu->visibleExp.entries[1]->data.operand.dataType == VAL_STRING); + REQUIRE(mainMenu->visibleExp.entries[1]->data.operand.internals.stringVal == "ui_show_main"s); + REQUIRE(mainMenu->visibleExp.entries[2]->data.op == OP_RIGHTPAREN); + REQUIRE(mainMenu->onOpen == R"("play" "menu_open" ; "setLocalVarInt" "ui_highlight" "5" ; )"s); + REQUIRE(mainMenu->onFocus == R"("setdvar" "ui_menu_focused" "1" ; )"s); + + REQUIRE(mainMenu->itemCount == 3); + const auto* emptyTextItem = mainMenu->items[0]; + REQUIRE(emptyTextItem->parent == mainMenu); + REQUIRE(emptyTextItem->text != nullptr); + REQUIRE(emptyTextItem->text == ""s); + REQUIRE(emptyTextItem->dvar == "ui_fallback_text"s); + REQUIRE(emptyTextItem->window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE); + REQUIRE(emptyTextItem->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR); + REQUIRE( + emptyTextItem->action + == R"("setItemColor" "empty_text" "borderColor" "0.1" "0.1" "0.12" "0.5" ; "execondvarstringvalue" "ui_zfeather" "0" "set cg_laserLight 0" ; )"s); + REQUIRE(emptyTextItem->materialExp.numEntries == 3); + REQUIRE(emptyTextItem->materialExp.entries[0]->data.op == OP_LOCALVARSTRING); + + const auto* listItem = mainMenu->items[1]; + REQUIRE(listItem->typeData.listBox); + REQUIRE(listItem->typeData.listBox->numColumns == 2); + REQUIRE(listItem->typeData.listBox->columnInfo[1].pos == 120); + REQUIRE(listItem->typeData.listBox->columnInfo[1].width == 180); + REQUIRE(listItem->typeData.listBox->notselectable == 1); + REQUIRE(listItem->typeData.listBox->noScrollBars == 1); + REQUIRE(listItem->typeData.listBox->usePaging == 1); + REQUIRE_THAT(listItem->typeData.listBox->disableColor[0], WithinRel(0.6f)); + REQUIRE_THAT(listItem->typeData.listBox->focusColor[2], WithinRel(0.3f)); + REQUIRE(listItem->typeData.listBox->selectIcon == selectMaterial); + REQUIRE(listItem->typeData.listBox->backgroundItemListbox == listboxBackgroundMaterial); + REQUIRE(listItem->typeData.listBox->highlightTexture == highlightMaterial); + REQUIRE(listItem->typeData.listBox->onDoubleClick == R"("play" "mouse_click" ; )"s); + REQUIRE(listItem->onListboxSelectionChange == R"("setdvar" "ui_server_selected" "1" ; )"s); + + const auto* defaultTextItem = mainMenu->items[2]; + REQUIRE(defaultTextItem->type == 0); + REQUIRE(defaultTextItem->typeData.editField); + REQUIRE(defaultTextItem->typeData.editField->maxChars == 2); + + REQUIRE(popupMenu->window.staticFlags & WINDOW_FLAG_POPUP); + REQUIRE(popupMenu->visibleExp.numEntries == 2); + REQUIRE(popupMenu->visibleExp.entries[0]->data.op == OP_PARTYISMISSINGMAPPACK); + REQUIRE(popupMenu->visibleExp.entries[1]->data.op == OP_RIGHTPAREN); + REQUIRE(popupMenu->onESC == R"("close" "self" ; )"s); + } + + TEST_CASE("MenuListLoaderT4: Ensure optimizing item rect considers relative positions", "[t4][menu][assetloader]") + { + MenuLoadingTestHelper helper; + + helper.AddFile("ui_mp/test.menu", R"MENU({ + menuDef + { + name "test" + rect 2 4 100 100 + itemDef + { + rect 1 2 20 20 + exp rect x (5) + exp rect y (7) + } + } +} +)MENU"); + + const auto result = helper.LoadMenuList("ui_mp/test.menu"); + REQUIRE(result.HasBeenSuccessful()); + + const auto* testMenu = helper.GetMenu("test"); + + REQUIRE(testMenu->window.rectClient.x == Approx(2)); + REQUIRE(testMenu->window.rectClient.y == Approx(4)); + REQUIRE(testMenu->window.rect.x == Approx(2)); + REQUIRE(testMenu->window.rect.y == Approx(4)); + + REQUIRE(testMenu->itemCount == 1); + const auto* item = testMenu->items[0]; + REQUIRE(item->rectXExp.numEntries == 0); + REQUIRE(item->rectYExp.numEntries == 0); + REQUIRE(item->window.rectClient.x == Approx(5)); + REQUIRE(item->window.rectClient.y == Approx(7)); + REQUIRE(item->window.rect.x == Approx(7)); + REQUIRE(item->window.rect.y == Approx(11)); + } +} // namespace diff --git a/test/ObjWritingTests/Game/T4/Menu/MenuDumperT4Test.cpp b/test/ObjWritingTests/Game/T4/Menu/MenuDumperT4Test.cpp new file mode 100644 index 00000000..554c6874 --- /dev/null +++ b/test/ObjWritingTests/Game/T4/Menu/MenuDumperT4Test.cpp @@ -0,0 +1,616 @@ +#include "Game/T4/Menu/MenuDumperT4.h" + +#include "Game/T4/Menu/MenuListDumperT4.h" +#include "Parsing/Menu/MenuFileReader.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include +#include + +using namespace T4; + +namespace +{ + expressionEntry Operator(const operationEnum operation) + { + expressionEntry entry{}; + entry.type = EET_OPERATOR; + entry.data.op = operation; + return entry; + } + + expressionEntry IntOperand(const int value) + { + expressionEntry entry{}; + entry.type = EET_OPERAND; + entry.data.operand.dataType = VAL_INT; + entry.data.operand.internals.intVal = value; + return entry; + } + + expressionEntry StringOperand(const char* value) + { + expressionEntry entry{}; + entry.type = EET_OPERAND; + entry.data.operand.dataType = VAL_STRING; + entry.data.operand.internals.stringVal = value; + return entry; + } + + template std::array EntryPointers(std::array& entries) + { + std::array result{}; + for (auto entryIndex = 0u; entryIndex < EntryCount; entryIndex++) + result[entryIndex] = &entries[entryIndex]; + return result; + } + + TEST_CASE("MenuDumperT4: Can dump menu with expressions and scripts", "[t4][menu][assetdumper]") + { + constexpr auto UI_SHOW_FAVORITE_SERVERS = 0x00000004; + constexpr auto UI_SHOW_NOT_FAVORITE_SERVERS = 0x00001000; + + std::array menuVisibleValues{ + Operator(OP_DVARBOOL), + StringOperand("ui_test"), + Operator(OP_RIGHTPAREN), + }; + auto menuVisibleEntries = EntryPointers(menuVisibleValues); + + std::array buttonVisibleValues{ + Operator(OP_LEFTPAREN), + Operator(OP_LOCALVARINT), + StringOperand("ui_highlight"), + Operator(OP_RIGHTPAREN), + Operator(OP_EQUALS), + IntOperand(5), + Operator(OP_AND), + Operator(OP_LOCALVARSTRING), + StringOperand("ui_choicegroup"), + Operator(OP_RIGHTPAREN), + Operator(OP_EQUALS), + StringOperand("popmenu"), + Operator(OP_RIGHTPAREN), + }; + auto buttonVisibleEntries = EntryPointers(buttonVisibleValues); + + std::array materialExpressionValues{ + Operator(OP_LEFTPAREN), + StringOperand("expression_material"), + Operator(OP_RIGHTPAREN), + }; + auto materialExpressionEntries = EntryPointers(materialExpressionValues); + + Material backgroundMaterial{}; + backgroundMaterial.info.name = "background_material"; + + ItemKeyHandler buttonKeyHandler{ + .key = 'a', + .action = "open advanced;", + .next = nullptr, + }; + + itemDef_s button{}; + button.window.name = "button_test"; + button.window.group = "buttons"; + button.window.rectClient = { + .x = 10.0f, + .y = 20.0f, + .w = 180.0f, + .h = 24.0f, + .horzAlign = 1, + .vertAlign = 2, + }; + button.window.style = 1; + button.window.ownerDrawFlags = UI_SHOW_NOT_FAVORITE_SERVERS; + button.window.backColor[0] = 0.1f; + button.window.backColor[1] = 0.1f; + button.window.backColor[2] = 0.1f; + button.window.backColor[3] = 0.25f; + button.window.foreColor[0] = 0.69f; + button.window.foreColor[1] = 0.69f; + button.window.foreColor[2] = 0.69f; + button.window.foreColor[3] = 1.0f; + button.window.background = &backgroundMaterial; + button.type = ITEM_TYPE_BUTTON; + button.text = "@MENU_TEST"; + button.textAlignMode = 10; + button.textalignx = -6.0f; + button.textscale = 0.4f; + button.textStyle = 6; + button.fontEnum = 1; + button.onFocus = "play mouse_over; setlocalvarint ui_highlight 5;"; + button.leaveFocus = "setlocalvarint ui_highlight 0;"; + button.mouseExit = R"(setitemcolor button_test bordercolor "0.1" "0.1" "0.12" "0.5";)"; + button.action = "play mouse_click; open options;"; + button.onKey = &buttonKeyHandler; + button.visibleExp = {.numEntries = static_cast(buttonVisibleEntries.size()), .entries = buttonVisibleEntries.data()}; + button.materialExp = {.numEntries = static_cast(materialExpressionEntries.size()), .entries = materialExpressionEntries.data()}; + + editFieldDef_s editField{}; + editField.minVal = -1.0f; + editField.maxVal = -1.0f; + editField.defVal = -1.0f; + editField.maxChars = 16; + editField.maxPaintChars = 16; + + itemDef_s editItem{}; + editItem.window.name = "name_field"; + editItem.window.group = "fields"; + editItem.window.rectClient = {.x = 10.0f, .y = 56.0f, .w = 180.0f, .h = 24.0f, .horzAlign = 1, .vertAlign = 2}; + editItem.window.style = 1; + editItem.window.border = 1; + editItem.window.borderSize = 1.0f; + editItem.window.dynamicFlags[0] = WINDOW_FLAG_VISIBLE; + editItem.window.backColor[0] = 0.1f; + editItem.window.backColor[1] = 0.1f; + editItem.window.backColor[2] = 0.1f; + editItem.window.backColor[3] = 0.25f; + editItem.window.foreColor[0] = 1.0f; + editItem.window.foreColor[1] = 1.0f; + editItem.window.foreColor[2] = 1.0f; + editItem.window.foreColor[3] = 1.0f; + editItem.window.borderColor[0] = 0.5f; + editItem.window.borderColor[1] = 0.5f; + editItem.window.borderColor[2] = 0.5f; + editItem.window.borderColor[3] = 1.0f; + editItem.type = ITEM_TYPE_UPREDITFIELD; + editItem.text = ""; + editItem.textAlignMode = 1; + editItem.textalignx = -28.0f; + editItem.textaligny = 14.0f; + editItem.textscale = 0.3f; + editItem.fontEnum = 1; + editItem.onAccept = "setdvar ui_name player_name;"; + editItem.dvar = "player_name"; + editItem.typeData.editField = &editField; + + itemDef_s* items[]{&button, &editItem}; + ItemKeyHandler menuKeyHandler{ + .key = 13, + .action = "open key_help;", + .next = nullptr, + }; + + menuDef_t menu{}; + menu.window.name = "test_menu"; + menu.window.rect = {.x = 0.0f, .y = 0.0f, .w = 640.0f, .h = 480.0f, .horzAlign = 0, .vertAlign = 0}; + menu.window.style = 1; + menu.window.ownerDrawFlags = UI_SHOW_FAVORITE_SERVERS; + menu.window.border = 1; + menu.window.borderSize = 2.0f; + menu.window.backColor[0] = 0.1f; + menu.window.backColor[1] = 0.2f; + menu.window.backColor[2] = 0.3f; + menu.window.backColor[3] = 0.75f; + menu.window.foreColor[0] = 1.0f; + menu.window.foreColor[1] = 1.0f; + menu.window.foreColor[2] = 1.0f; + menu.window.foreColor[3] = 1.0f; + menu.fullScreen = 1; + menu.fadeCycle = 16; + menu.fadeClamp = 1.0f; + menu.fadeAmount = 0.1f; + menu.fadeInAmount = 0.2f; + menu.blurRadius = 2.0f; + menu.onOpen = "play mouse_open; setfocus button_test;"; + menu.onClose = "play mouse_close;"; + menu.onESC = "close self;"; + menu.onKey = &menuKeyHandler; + menu.visibleExp = {.numEntries = static_cast(menuVisibleEntries.size()), .entries = menuVisibleEntries.data()}; + menu.allowedBinding = "+activate"; + menu.soundName = "menu_music"; + menu.focusColor[0] = 1.0f; + menu.focusColor[1] = 0.8f; + menu.focusColor[2] = 0.2f; + menu.focusColor[3] = 1.0f; + menu.itemCount = static_cast(std::size(items)); + menu.items = items; + + Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC); + zone.m_pools.AddAsset(std::make_unique>(ASSET_TYPE_MENU, menu.window.name, &menu)); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt); + + menu::MenuDumperT4 dumper; + dumper.Dump(context); + + const auto* file = mockOutput.GetMockedFile("ui_mp/test_menu.menu"); + REQUIRE(file); + + const auto output = file->AsString(); + constexpr auto expectedOutput = R"({ + menuDef + { + name "test_menu" + fullscreen 1 + rect 0 0 640 480 0 0 + style 1 + border 1 + borderSize 2 + backcolor 0.1 0.2 0.3 0.75 + focuscolor 1 0.8 0.2 1 + ownerdrawFlag 4 + soundLoop "menu_music" + fadeClamp 1 + fadeCycle 16 + fadeAmount 0.1 + fadeInAmount 0.2 + blurWorld 2 + allowedBinding "+activate" + visible when(dvarbool("ui_test")); + onOpen + { + play mouse_open; + setfocus button_test; + } + onClose + { + play mouse_close; + } + onESC + { + close self; + } + execKeyInt 13 + { + open key_help; + } + itemDef + { + name "button_test" + text "@MENU_TEST" + group "buttons" + rect 10 20 180 24 1 2 + style 1 + type 1 + visible when(localvarint("ui_highlight") == 5 && localvarstring("ui_choicegroup") == "popmenu"); + ownerdrawFlag 4096 + textalign 10 + textalignx -6 + textscale 0.4 + textstyle 6 + textfont 1 + backcolor 0.1 0.1 0.1 0.25 + forecolor 0.69 0.69 0.69 1 + background "background_material" + onFocus + { + play mouse_over; + setlocalvarint ui_highlight 5; + } + leaveFocus + { + setlocalvarint ui_highlight 0; + } + mouseExit + { + setitemcolor button_test bordercolor 0.1 0.1 0.12 0.5; + } + action + { + play mouse_click; + open options; + } + execKey "a" + { + open advanced; + } + exp material ("expression_material"); + } + itemDef + { + name "name_field" + text "" + group "fields" + rect 10 56 180 24 1 2 + style 1 + type 18 + border 1 + borderSize 1 + visible 1 + textalign 1 + textalignx -28 + textaligny 14 + textscale 0.3 + textfont 1 + backcolor 0.1 0.1 0.1 0.25 + bordercolor 0.5 0.5 0.5 1 + accept + { + setdvar ui_name player_name; + } + dvar "player_name" + maxChars 16 + maxPaintChars 16 + } + } +} +)"; + REQUIRE(output == expectedOutput); + + menu::MenuAssetZoneState zoneState; + std::istringstream input(output); + menu::MenuFileReader reader(input, "ui_mp/test_menu.menu", menu::FeatureLevel::IW4, mockObjPath); + reader.IncludeZoneState(zoneState); + + const auto parsed = reader.ReadMenuFile(); + REQUIRE(parsed); + REQUIRE(parsed->m_menus.size() == 1); + REQUIRE(parsed->m_menus[0]->m_name == "test_menu"); + REQUIRE(parsed->m_menus[0]->m_owner_draw_flags == UI_SHOW_FAVORITE_SERVERS); + REQUIRE(parsed->m_menus[0]->m_items.size() == 2); + REQUIRE(parsed->m_menus[0]->m_items[0]->m_name == "button_test"); + REQUIRE(parsed->m_menus[0]->m_items[0]->m_owner_draw_flags == UI_SHOW_NOT_FAVORITE_SERVERS); + REQUIRE(parsed->m_menus[0]->m_items[1]->m_name == "name_field"); + REQUIRE(parsed->m_menus[0]->m_items[1]->m_dvar == "player_name"); + } + + TEST_CASE("MenuDumperT4: Prefers parent menu list path over ui_mp fallback", "[t4][menu][assetdumper]") + { + menuDef_t menu{}; + menu.window.name = "test_menu"; + + menuDef_t* menus[]{&menu}; + + MenuList menuList{}; + menuList.name = "ui/menus.txt"; + menuList.menuCount = static_cast(std::size(menus)); + menuList.menus = menus; + + Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC); + zone.m_pools.AddAsset(std::make_unique>(ASSET_TYPE_MENU, menu.window.name, &menu)); + zone.m_pools.AddAsset(std::make_unique>(ASSET_TYPE_MENULIST, menuList.name, &menuList)); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt); + + menu::MenuListDumperT4 menuListDumper; + menuListDumper.Dump(context); + + menu::MenuDumperT4 menuDumper; + menuDumper.Dump(context); + + REQUIRE(mockOutput.GetMockedFile("ui/test_menu.menu")); + REQUIRE_FALSE(mockOutput.GetMockedFile("ui_mp/test_menu.menu")); + } + + TEST_CASE("MenuDumperT4: Expressions are valid when dumped", "[t4][menu][assetdumper]") + { + std::array item0VisibleValues{ + Operator(OP_LEFTPAREN), + Operator(OP_LOCALVARSTRING), + StringOperand("ui_highlight"), + Operator(OP_RIGHTPAREN), + Operator(OP_RIGHTPAREN), + }; + auto item0Entries = EntryPointers(item0VisibleValues); + + itemDef_s item0{}; + item0.window.foreColor[0] = 1.0f; + item0.window.foreColor[1] = 1.0f; + item0.window.foreColor[2] = 1.0f; + item0.window.foreColor[3] = 1.0f; + item0.window.rectClient = {.x = 1, .y = 2, .w = 3, .h = 4, .horzAlign = 0, .vertAlign = 0}; + item0.textExp = {.numEntries = static_cast(item0Entries.size()), .entries = item0Entries.data()}; + + std::array item1VisibleValues{ + Operator(OP_LEFTPAREN), + IntOperand(2), + Operator(OP_RIGHTPAREN), + }; + auto item1Entries = EntryPointers(item1VisibleValues); + + itemDef_s item1{}; + item1.window.foreColor[0] = 1.0f; + item1.window.foreColor[1] = 1.0f; + item1.window.foreColor[2] = 1.0f; + item1.window.foreColor[3] = 1.0f; + item1.window.rectClient = {.x = 1, .y = 2, .w = 3, .h = 4, .horzAlign = 0, .vertAlign = 0}; + item1.textExp = {.numEntries = static_cast(item1Entries.size()), .entries = item1Entries.data()}; + + std::array item2VisibleValues{ + Operator(OP_LEFTPAREN), + IntOperand(3), + Operator(OP_ADD), + IntOperand(7), + Operator(OP_RIGHTPAREN), + }; + auto item2Entries = EntryPointers(item2VisibleValues); + + itemDef_s item2{}; + item2.window.foreColor[0] = 1.0f; + item2.window.foreColor[1] = 1.0f; + item2.window.foreColor[2] = 1.0f; + item2.window.foreColor[3] = 1.0f; + item2.window.rectClient = {.x = 1, .y = 2, .w = 3, .h = 4, .horzAlign = 0, .vertAlign = 0}; + item2.textExp = {.numEntries = static_cast(item2Entries.size()), .entries = item2Entries.data()}; + + std::array item3VisibleValues{ + Operator(OP_LEFTPAREN), + IntOperand(-3), + Operator(OP_ADD), + IntOperand(7), + Operator(OP_MULTIPLY), + Operator(OP_NOT), + Operator(OP_SIN), + Operator(OP_COS), + IntOperand(7), + Operator(OP_RIGHTPAREN), + Operator(OP_RIGHTPAREN), + Operator(OP_RIGHTPAREN), + }; + auto item3Entries = EntryPointers(item3VisibleValues); + + itemDef_s item3{}; + item3.window.foreColor[0] = 1.0f; + item3.window.foreColor[1] = 1.0f; + item3.window.foreColor[2] = 1.0f; + item3.window.foreColor[3] = 1.0f; + item3.window.rectClient = {.x = 1, .y = 2, .w = 3, .h = 4, .horzAlign = 0, .vertAlign = 0}; + item3.textExp = {.numEntries = static_cast(item3Entries.size()), .entries = item3Entries.data()}; + + itemDef_s* items[]{&item0, &item1, &item2, &item3}; + + menuDef_t menu{}; + menu.window.name = "test_menu"; + menu.window.foreColor[0] = 1.0f; + menu.window.foreColor[1] = 1.0f; + menu.window.foreColor[2] = 1.0f; + menu.window.foreColor[3] = 1.0f; + menu.window.rect = {.x = 1, .y = 2, .w = 3, .h = 4, .horzAlign = 0, .vertAlign = 0}; + menu.itemCount = static_cast(std::size(items)); + menu.items = items; + + Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC); + zone.m_pools.AddAsset(std::make_unique>(ASSET_TYPE_MENU, menu.window.name, &menu)); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt); + + menu::MenuDumperT4 dumper; + dumper.Dump(context); + + const auto* file = mockOutput.GetMockedFile("ui_mp/test_menu.menu"); + REQUIRE(file); + + const auto output = file->AsString(); + constexpr auto expectedOutput = R"({ + menuDef + { + name "test_menu" + rect 1 2 3 4 0 0 + itemDef + { + rect 1 2 3 4 0 0 + exp text (localvarstring("ui_highlight")); + } + itemDef + { + rect 1 2 3 4 0 0 + exp text (2); + } + itemDef + { + rect 1 2 3 4 0 0 + exp text (3 + 7); + } + itemDef + { + rect 1 2 3 4 0 0 + exp text (-3 + 7 * !sin(cos(7))); + } + } +} +)"; + REQUIRE(output == expectedOutput); + } + + TEST_CASE("MenuDumperT4: Can dump T4-specific menu properties", "[t4][menu][assetdumper]") + { + std::array visibleValues{ + Operator(OP_PARTYISMISSINGMAPPACK), + Operator(OP_RIGHTPAREN), + }; + auto visibleEntries = EntryPointers(visibleValues); + + Material selectIcon{}; + selectIcon.info.name = "listbox_select_icon"; + + Material listboxBackground{}; + listboxBackground.info.name = "listbox_background"; + + Material highlightTexture{}; + highlightTexture.info.name = "listbox_highlight"; + + listBoxDef_s listBox{}; + listBox.onDoubleClick = "play mouse_click;"; + listBox.focusColor[0] = 1.0f; + listBox.focusColor[1] = 0.8f; + listBox.focusColor[2] = 0.2f; + listBox.focusColor[3] = 1.0f; + listBox.selectIcon = &selectIcon; + listBox.backgroundItemListbox = &listboxBackground; + listBox.highlightTexture = &highlightTexture; + + itemDef_s serverList{}; + serverList.window.name = "server_list"; + serverList.window.rectClient = {.x = 20.0f, .y = 80.0f, .w = 400.0f, .h = 180.0f, .horzAlign = 1, .vertAlign = 2}; + serverList.window.foreColor[0] = 1.0f; + serverList.window.foreColor[1] = 1.0f; + serverList.window.foreColor[2] = 1.0f; + serverList.window.foreColor[3] = 1.0f; + serverList.type = ITEM_TYPE_LISTBOX; + serverList.onListboxSelectionChange = "setdvar ui_server_selected 1;"; + serverList.typeData.listBox = &listBox; + + itemDef_s* items[]{&serverList}; + + menuDef_t menu{}; + menu.window.name = "t4_menu"; + menu.window.rect = {.x = 0.0f, .y = 0.0f, .w = 640.0f, .h = 480.0f, .horzAlign = 0, .vertAlign = 0}; + menu.window.foreColor[0] = 1.0f; + menu.window.foreColor[1] = 1.0f; + menu.window.foreColor[2] = 1.0f; + menu.window.foreColor[3] = 1.0f; + menu.onFocus = "setdvar ui_hint t4_menu;"; + menu.visibleExp = {.numEntries = static_cast(visibleEntries.size()), .entries = visibleEntries.data()}; + menu.itemCount = static_cast(std::size(items)); + menu.items = items; + + Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC); + zone.m_pools.AddAsset(std::make_unique>(ASSET_TYPE_MENU, menu.window.name, &menu)); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt); + + menu::MenuDumperT4 dumper; + dumper.Dump(context); + + const auto* file = mockOutput.GetMockedFile("ui_mp/t4_menu.menu"); + REQUIRE(file); + + constexpr auto expectedOutput = R"({ + menuDef + { + name "t4_menu" + rect 0 0 640 480 0 0 + visible when(partyismissingmappack()); + onFocus + { + setdvar ui_hint t4_menu; + } + itemDef + { + name "server_list" + rect 20 80 400 180 1 2 + type 6 + doubleclick + { + play mouse_click; + } + onListboxSelectionChange + { + setdvar ui_server_selected 1; + } + focusColor 1 0.8 0.2 1 + selectIcon "listbox_select_icon" + backgroundItemListbox "listbox_background" + highlightTexture "listbox_highlight" + } + } +} +)"; + REQUIRE(file->AsString() == expectedOutput); + } +} // namespace diff --git a/test/ObjWritingTests/Game/T4/Menu/MenuListDumperT4Test.cpp b/test/ObjWritingTests/Game/T4/Menu/MenuListDumperT4Test.cpp new file mode 100644 index 00000000..9ddb7ad9 --- /dev/null +++ b/test/ObjWritingTests/Game/T4/Menu/MenuListDumperT4Test.cpp @@ -0,0 +1,53 @@ +#include "Game/T4/Menu/MenuListDumperT4.h" + +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include + +using namespace T4; + +namespace +{ + TEST_CASE("MenuListDumperT4: Can dump several menu entries", "[t4][menu][menulist][assetdumper]") + { + menuDef_t mainMenu{}; + mainMenu.window.name = "main"; + + menuDef_t optionsMenu{}; + optionsMenu.window.name = "options"; + + menuDef_t confirmationMenu{}; + confirmationMenu.window.name = ",confirmation"; + + menuDef_t* menus[]{&mainMenu, &optionsMenu, &confirmationMenu}; + + MenuList menuList{}; + menuList.name = "ui_mp/menus.txt"; + menuList.menuCount = static_cast(std::size(menus)); + menuList.menus = menus; + + Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC); + zone.m_pools.AddAsset(std::make_unique>(ASSET_TYPE_MENULIST, menuList.name, &menuList)); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt); + + menu::MenuListDumperT4 dumper; + dumper.Dump(context); + + const auto* file = mockOutput.GetMockedFile("ui_mp/menus.txt"); + REQUIRE(file); + + constexpr auto expectedOutput = R"({ + loadMenu { "ui_mp/main.menu" } + loadMenu { "ui_mp/options.menu" } + loadMenu { "ui_mp/confirmation.menu" } +} +)"; + REQUIRE(file->AsString() == expectedOutput); + } +} // namespace diff --git a/test/SystemTests/Game/T4/MenuRoundTrip/MenuRoundTripT4.zone b/test/SystemTests/Game/T4/MenuRoundTrip/MenuRoundTripT4.zone new file mode 100644 index 00000000..161f6809 --- /dev/null +++ b/test/SystemTests/Game/T4/MenuRoundTrip/MenuRoundTripT4.zone @@ -0,0 +1,3 @@ +>game,T4 + +menulist,ui_mp/menus.txt diff --git a/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/main.menu b/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/main.menu new file mode 100644 index 00000000..106d86d3 --- /dev/null +++ b/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/main.menu @@ -0,0 +1,46 @@ +{ + menuDef + { + name "main" + fullscreen 1 + rect 0 0 640 480 0 0 + disablecolor 0.2 0.3 0.4 0.5 + forecolor 0.8 0.7 0.6 0.5 + visible when(dvarbool("ui_show_main")); + onOpen + { + play menu_open; + setLocalVarInt ui_highlight 5; + } + onFocus + { + setdvar ui_menu_focused 1; + } + itemDef + { + name "empty_text" + text "" + rect 10 20 180 24 1 2 + type 1 + visible 1 + forecolor 0.9 0.9 1 0.07 + dvar "ui_fallback_text" + action + { + setItemColor empty_text borderColor 0.1 0.1 0.12 0.5; + } + exp material localvarstring("ui_material"); + } + itemDef + { + name "server_list" + rect 20 60 300 200 0 0 + type 6 + focusColor 0.1 0.2 0.3 0.4 + onListboxSelectionChange + { + setdvar ui_server_selected 1; + } + } + } +} diff --git a/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/menus.txt b/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/menus.txt new file mode 100644 index 00000000..9c3fb51f --- /dev/null +++ b/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/menus.txt @@ -0,0 +1,4 @@ +{ + loadMenu { "ui_mp/main.menu" } + loadMenu { "ui_mp/popup.menu" } +} diff --git a/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/popup.menu b/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/popup.menu new file mode 100644 index 00000000..e2195244 --- /dev/null +++ b/test/SystemTests/Game/T4/MenuRoundTrip/ui_mp/popup.menu @@ -0,0 +1,13 @@ +{ + menuDef + { + name "popup" + rect 40 40 200 100 0 0 + popup + visible when(partyismissingmappack()); + onESC + { + close self; + } + } +} diff --git a/test/SystemTests/Game/T4/MenuRoundTripT4.cpp b/test/SystemTests/Game/T4/MenuRoundTripT4.cpp new file mode 100644 index 00000000..c882d004 --- /dev/null +++ b/test/SystemTests/Game/T4/MenuRoundTripT4.cpp @@ -0,0 +1,97 @@ +#include "Game/T4/Menu/MenuDumperT4.h" +#include "Game/T4/Menu/MenuListDumperT4.h" +#include "Game/T4/T4.h" +#include "Linker.h" +#include "OatTestPaths.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" +#include "SystemTestsPaths.h" +#include "ZoneLoading.h" + +#include +#include +#include +#include + +namespace fs = std::filesystem; +using namespace T4; +using namespace std::literals; + +namespace +{ + TEST_CASE("Menu loading and dumping (T4)", "[t4][menu][system]") + { + const auto testDataPath = oat::paths::GetSystemTestsDirectory() / "Game/T4/MenuRoundTrip"; + const auto testDataPathString = testDataPath.string(); + const auto outputPath = oat::paths::GetTempDirectory("MenuRoundTripT4"); + const auto outputPathString = outputPath.string(); + + const char* arguments[]{ + "SystemTests", + "--verbose", + "--base-folder", + outputPathString.c_str(), + "--asset-search-path", + testDataPathString.c_str(), + "--source-search-path", + testDataPathString.c_str(), + "--output-folder", + outputPathString.c_str(), + "MenuRoundTripT4", + }; + + LinkerArgs linkerArgs; + auto shouldContinue = true; + REQUIRE(linkerArgs.ParseArgs(std::size(arguments), arguments, shouldContinue)); + REQUIRE(shouldContinue); + + const auto linker = Linker::Create(std::move(linkerArgs)); + REQUIRE(linker->Start()); + + const auto zonePath = outputPath / "MenuRoundTripT4.ff"; + auto maybeZone = ZoneLoading::LoadZone(zonePath.string(), std::nullopt); + REQUIRE(maybeZone); + auto zone = std::move(*maybeZone); + + const auto* menuList = zone->m_pools.GetAsset("ui_mp/menus.txt"); + const auto* mainMenu = zone->m_pools.GetAsset("main"); + const auto* popupMenu = zone->m_pools.GetAsset("popup"); + REQUIRE(menuList); + REQUIRE(mainMenu); + REQUIRE(popupMenu); + REQUIRE(menuList->Asset()->menuCount == 2); + REQUIRE(mainMenu->Asset()->visibleExp.numEntries == 3); + REQUIRE(mainMenu->Asset()->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR); + REQUIRE(mainMenu->Asset()->onFocus == R"("setdvar" "ui_menu_focused" "1" ; )"s); + REQUIRE(mainMenu->Asset()->items[0]->text != nullptr); + REQUIRE(mainMenu->Asset()->items[0]->text == ""s); + REQUIRE(mainMenu->Asset()->items[0]->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR); + REQUIRE(mainMenu->Asset()->items[0]->action == R"("setItemColor" "empty_text" "borderColor" "0.1" "0.1" "0.12" "0.5" ; )"s); + REQUIRE(mainMenu->Asset()->items[1]->onListboxSelectionChange == R"("setdvar" "ui_server_selected" "1" ; )"s); + REQUIRE(mainMenu->Asset()->items[1]->typeData.listBox->focusColor[2] == 0.3f); + REQUIRE(popupMenu->Asset()->visibleExp.entries[0]->data.op == OP_PARTYISMISSINGMAPPACK); + + MockSearchPath objPath; + MockOutputPath dumpOutput; + AssetDumpingContext dumpingContext(*zone, "", dumpOutput, objPath, std::nullopt); + menu::MenuListDumperT4 menuListDumper; + menuListDumper.Dump(dumpingContext); + menu::MenuDumperT4 menuDumper; + menuDumper.Dump(dumpingContext); + + const auto* dumpedMenuList = dumpOutput.GetMockedFile("ui_mp/menus.txt"); + const auto* dumpedMainMenu = dumpOutput.GetMockedFile("ui_mp/main.menu"); + const auto* dumpedPopupMenu = dumpOutput.GetMockedFile("ui_mp/popup.menu"); + REQUIRE(dumpedMenuList); + REQUIRE(dumpedMainMenu); + REQUIRE(dumpedPopupMenu); + REQUIRE(dumpedMenuList->AsString().find("loadMenu { \"ui_mp/main.menu\" }") != std::string::npos); + REQUIRE(dumpedMenuList->AsString().find("loadMenu { \"ui_mp/popup.menu\" }") != std::string::npos); + REQUIRE(dumpedMainMenu->AsString().find("text \"\"") != std::string::npos); + REQUIRE(dumpedMainMenu->AsString().find("visible when(dvarbool(\"ui_show_main\"));") != std::string::npos); + REQUIRE(dumpedMainMenu->AsString().find("setItemColor empty_text borderColor 0.1 0.1 0.12 0.5;") != std::string::npos); + REQUIRE(dumpedMainMenu->AsString().find("onListboxSelectionChange") != std::string::npos); + REQUIRE(dumpedMainMenu->AsString().find("focusColor 0.1 0.2 0.3 0.4") != std::string::npos); + REQUIRE(dumpedPopupMenu->AsString().find("partyismissingmappack()") != std::string::npos); + } +} // namespace