From 51770e7ae56a7470d7392e90380ffe69a34fd78b Mon Sep 17 00:00:00 2001 From: mo Date: Sun, 30 Aug 2026 23:04:01 +0100 Subject: [PATCH] feat: IW3 menu dumping and loading (#909) * feat: IW3 menu dumping * fix: IW3 menu dumper preserve menu ownerdraw flag masks * fix: IW3 menu dumper preserve numeric script arguments Emit numeric tokens without quotes so the linker keeps colour arguments separate. Before: `"0.1" "0.1" "0.12" "0.5"` becomes `"0.10.10.120.5"` After: `0.1 0.1 0.12 0.5` This fixes `mouseExit` colour commands failing to clear menu hover borders. * fix: IW3 menu dumper preserve empty item text Keep explicit empty text distinct from null text when dumping menu items. This fixes CD-key values overflowing their input boxes. * test: cover IW3 menu dumper material case * test: cover IW3 menu list path precedence * chore: clang format * fix: reverse order of union to avoid edge case bug in zcg * feat: IW3 menu loading * fix: validate menu expression name table * chore: explain numeric token quoting in comment * refactor: simplify menu dump fallback path * chore: add more dynamic window flags * refactor: reuse IW3 menu window flag constants * refactor: use explicit menu item feature lookup * refactor: represent menu item text as optional * refactor: clarify shared IW3 and IW4 menu constants IW4 only extends the type enum so they can be shared * refactor: clarify IW3 menu zone state naming * fix: log IW3 menu conversion failures * refactor: align IW3 menu list loader with IW4 * refactor: align IW3 menu dumpers with IW4 * refactor: align IW3 menu converter with IW4 * chore: use default enum numeration for operationEnum * chore: small optimization on edit field capability initialization * fix: iw3 menu optimizations for rect not considering relative position to parent * chore: align iw3 menu converting closer to iw4 * chore: align iw3 menu dumping closer to iw4 * fix: not dumping menu flags properties correctly * chore: adjust iw3 expression dumping to work similar to iw4 * chore: adjust iw3 expression converting to work similar to iw4 * chore: use unordered_map for MenuExpressionMatchers --------- Co-authored-by: Jan Laupetin --- docs/SupportedAssetTypes.md | 4 +- src/Common/Game/IW3/IW3_Assets.h | 139 +++- src/Common/Game/IW4/IW4_Assets.h | 2 +- src/Common/Game/IW5/IW5_Assets.h | 2 +- src/ObjCommon/Game/IW3/MenuConstantsIW3.h | 94 +++ .../Game/IW3/Menu/LoaderMenuListIW3.cpp | 215 ++++++ .../Game/IW3/Menu/LoaderMenuListIW3.h | 13 + .../IW3/Menu/MenuConversionZoneStateIW3.cpp | 8 + .../IW3/Menu/MenuConversionZoneStateIW3.h | 19 + .../Game/IW3/Menu/MenuConverterIW3.cpp | 729 ++++++++++++++++++ .../Game/IW3/Menu/MenuConverterIW3.h | 23 + src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp | 3 +- .../Game/IW4/Menu/MenuConverterIW4.cpp | 2 +- .../Game/IW5/Menu/MenuConverterIW5.cpp | 2 +- .../Parsing/Menu/Domain/CommonItemDef.h | 5 +- .../Parsing/Menu/Domain/CommonMenuDef.h | 3 +- .../Parsing/Menu/Domain/MenuFeatureLevel.h | 1 + .../Menu/Matcher/MenuExpressionMatchers.cpp | 26 +- .../Menu/Matcher/MenuExpressionMatchers.h | 10 +- .../Parsing/Menu/MenuFileReader.cpp | 3 + .../EventHandlerSetScopeSequences.cpp | 11 +- .../Menu/Sequence/GlobalScopeSequences.cpp | 3 +- .../Menu/Sequence/ItemScopeSequences.cpp | 71 +- .../Menu/Sequence/MenuScopeSequences.cpp | 5 + .../Game/IW3/Menu/MenuDumperIW3.cpp | 51 ++ src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.h | 13 + .../Game/IW3/Menu/MenuListDumperIW3.cpp | 120 +++ .../Game/IW3/Menu/MenuListDumperIW3.h | 19 + .../Game/IW3/Menu/MenuWriterIW3.cpp | 681 ++++++++++++++++ src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.h | 18 + src/ObjWriting/Game/IW3/ObjWriterIW3.cpp | 6 +- src/ObjWriting/Menu/AbstractMenuWriter.cpp | 16 +- src/ObjWriting/Menu/AbstractMenuWriter.h | 2 +- src/ZoneCode/Game/IW3/XAssets/menuDef_t.txt | 2 +- .../Game/IW3/Menu/LoaderMenuListIW3Test.cpp | 247 ++++++ .../Game/IW3/Menu/MenuDumperIW3Test.cpp | 517 +++++++++++++ .../Game/IW3/Menu/MenuListDumperIW3Test.cpp | 53 ++ .../IW3/MenuRoundTrip/MenuRoundTripIW3.zone | 3 + .../Game/IW3/MenuRoundTrip/ui_mp/main.menu | 31 + .../Game/IW3/MenuRoundTrip/ui_mp/menus.txt | 4 + .../Game/IW3/MenuRoundTrip/ui_mp/popup.menu | 12 + .../SystemTests/Game/IW3/MenuRoundTripIW3.cpp | 90 +++ 42 files changed, 3218 insertions(+), 60 deletions(-) create mode 100644 src/ObjCommon/Game/IW3/MenuConstantsIW3.h create mode 100644 src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.cpp create mode 100644 src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.h create mode 100644 src/ObjLoading/Game/IW3/Menu/MenuConversionZoneStateIW3.cpp create mode 100644 src/ObjLoading/Game/IW3/Menu/MenuConversionZoneStateIW3.h create mode 100644 src/ObjLoading/Game/IW3/Menu/MenuConverterIW3.cpp create mode 100644 src/ObjLoading/Game/IW3/Menu/MenuConverterIW3.h create mode 100644 src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.cpp create mode 100644 src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.h create mode 100644 src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.cpp create mode 100644 src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.h create mode 100644 src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.cpp create mode 100644 src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.h create mode 100644 test/ObjLoadingTests/Game/IW3/Menu/LoaderMenuListIW3Test.cpp create mode 100644 test/ObjWritingTests/Game/IW3/Menu/MenuDumperIW3Test.cpp create mode 100644 test/ObjWritingTests/Game/IW3/Menu/MenuListDumperIW3Test.cpp create mode 100644 test/SystemTests/Game/IW3/MenuRoundTrip/MenuRoundTripIW3.zone create mode 100644 test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/main.menu create mode 100644 test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/menus.txt create mode 100644 test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/popup.menu create mode 100644 test/SystemTests/Game/IW3/MenuRoundTripIW3.cpp diff --git a/docs/SupportedAssetTypes.md b/docs/SupportedAssetTypes.md index ac112451..6e7190ae 100644 --- a/docs/SupportedAssetTypes.md +++ b/docs/SupportedAssetTypes.md @@ -29,8 +29,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 7741f3dc..05d8832e 100644 --- a/src/Common/Game/IW3/IW3_Assets.h +++ b/src/Common/Game/IW3/IW3_Assets.h @@ -2513,6 +2513,37 @@ namespace IW3 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_1 = 0x1, + WINDOW_FLAG_FOCUSED = 0x2, + WINDOW_FLAG_VISIBLE = 0x4, + WINDOW_FLAG_FADING_OUT = 0x10, + WINDOW_FLAG_FADING_IN = 0x20, + WINDOW_FLAG_HOVERED = 0x40, + WINDOW_FLAG_LISTBOX_HOVER_100 = 0x100, + WINDOW_FLAG_LISTBOX_HOVER_200 = 0x200, + WINDOW_FLAG_LISTBOX_HOVER_400 = 0x400, + WINDOW_FLAG_LISTBOX_HOVER_800 = 0x800, + WINDOW_FLAG_LISTBOX_HOVER_1000 = 0x1000, + WINDOW_FLAG_4000 = 0x4000, + WINDOW_FLAG_NON_DEFAULT_BACKCOLOR = 0x8000, + WINDOW_FLAG_NON_DEFAULT_FORECOLOR = 0x10000, + }; + struct windowDef_t { const char* name; @@ -2522,10 +2553,10 @@ namespace IW3 int style; int border; int ownerDraw; - int ownerDrawFlags; + unsigned int ownerDrawFlags; float borderSize; - int staticFlags; - int dynamicFlags[1]; + unsigned int staticFlags; + unsigned int dynamicFlags[1]; int nextTime; float foreColor[4]; float backColor[4]; @@ -2561,10 +2592,99 @@ namespace IW3 operandInternalDataUnion internals; }; + enum operationEnum : int + { + 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_ISINTERMISSION, + + NUM_OPERATORS, + }; + union entryInternalData { Operand operand; - int op; + operationEnum op; }; enum expressionEntryType : int @@ -2604,7 +2724,7 @@ namespace IW3 int elementStyle; int numColumns; columnInfo_s columnInfo[16]; - const char* doubleClick; + const char* onDoubleClick; int notselectable; int noScrollBars; int usePaging; @@ -2667,6 +2787,15 @@ namespace IW3 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; diff --git a/src/Common/Game/IW4/IW4_Assets.h b/src/Common/Game/IW4/IW4_Assets.h index c8aa64cb..67b3a1e1 100644 --- a/src/Common/Game/IW4/IW4_Assets.h +++ b/src/Common/Game/IW4/IW4_Assets.h @@ -2313,7 +2313,7 @@ namespace IW4 int style; int border; int ownerDraw; - int ownerDrawFlags; + unsigned int ownerDrawFlags; float borderSize; int staticFlags; unsigned int dynamicFlags[1]; diff --git a/src/Common/Game/IW5/IW5_Assets.h b/src/Common/Game/IW5/IW5_Assets.h index 927aa35b..61146ec2 100644 --- a/src/Common/Game/IW5/IW5_Assets.h +++ b/src/Common/Game/IW5/IW5_Assets.h @@ -3233,7 +3233,7 @@ namespace IW5 int style; int border; int ownerDraw; - int ownerDrawFlags; + unsigned int ownerDrawFlags; float borderSize; int staticFlags; int dynamicFlags[1]; diff --git a/src/ObjCommon/Game/IW3/MenuConstantsIW3.h b/src/ObjCommon/Game/IW3/MenuConstantsIW3.h new file mode 100644 index 00000000..78209be4 --- /dev/null +++ b/src/ObjCommon/Game/IW3/MenuConstantsIW3.h @@ -0,0 +1,94 @@ +#pragma once + +#include "Game/IW3/IW3.h" + +#include + +namespace IW3 +{ + 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", + "isintermission", + }; + + static_assert(std::extent_v == static_cast(NUM_OPERATORS)); +} // namespace IW3 diff --git a/src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.cpp b/src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.cpp new file mode 100644 index 00000000..bd57470c --- /dev/null +++ b/src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.cpp @@ -0,0 +1,215 @@ +#include "LoaderMenuListIW3.h" + +#include "Game/IW3/Menu/MenuConversionZoneStateIW3.h" +#include "Game/IW3/Menu/MenuConverterIW3.h" +#include "ObjLoading.h" +#include "Parsing/Menu/MenuAssetZoneState.h" +#include "Parsing/Menu/MenuFileReader.h" +#include "Utils/Logging/Log.h" + +#include + +using namespace IW3; + +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::IW3, 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> CreateMenuListLoaderIW3(MemoryManager& memory, ISearchPath& searchPath) + { + return std::make_unique(memory, searchPath); + } +} // namespace menu diff --git a/src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.h b/src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.h new file mode 100644 index 00000000..7bb5ca8c --- /dev/null +++ b/src/ObjLoading/Game/IW3/Menu/LoaderMenuListIW3.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Asset/IAssetCreator.h" +#include "Game/IW3/IW3.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +namespace menu +{ + std::unique_ptr> CreateMenuListLoaderIW3(MemoryManager& memory, ISearchPath& searchPath); +} diff --git a/src/ObjLoading/Game/IW3/Menu/MenuConversionZoneStateIW3.cpp b/src/ObjLoading/Game/IW3/Menu/MenuConversionZoneStateIW3.cpp new file mode 100644 index 00000000..42f204f7 --- /dev/null +++ b/src/ObjLoading/Game/IW3/Menu/MenuConversionZoneStateIW3.cpp @@ -0,0 +1,8 @@ +#include "MenuConversionZoneStateIW3.h" + +using namespace IW3; + +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/IW3/Menu/MenuConversionZoneStateIW3.h b/src/ObjLoading/Game/IW3/Menu/MenuConversionZoneStateIW3.h new file mode 100644 index 00000000..fca7b28f --- /dev/null +++ b/src/ObjLoading/Game/IW3/Menu/MenuConversionZoneStateIW3.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Asset/IZoneAssetCreationState.h" +#include "Game/IW3/IW3.h" + +#include +#include +#include + +namespace IW3 +{ + class MenuConversionZoneState final : public IZoneAssetCreationState + { + public: + void AddLoadedFile(std::string loadedFileName, std::vector*> menusOfFile); + + std::unordered_map*>> m_menus_by_filename; + }; +} // namespace IW3 diff --git a/src/ObjLoading/Game/IW3/Menu/MenuConverterIW3.cpp b/src/ObjLoading/Game/IW3/Menu/MenuConverterIW3.cpp new file mode 100644 index 00000000..8e77db1b --- /dev/null +++ b/src/ObjLoading/Game/IW3/Menu/MenuConverterIW3.cpp @@ -0,0 +1,729 @@ +#include "MenuConverterIW3.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 + +using namespace IW3; +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("IW3 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("IW3 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); + ConvertColor(listBox->selectBorder, commonListBox->m_select_border); + ConvertColor(listBox->disableColor, commonItem.m_disable_color); + listBox->selectIcon = ConvertMaterial(commonListBox->m_select_icon, &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.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/IW3/Menu/MenuConverterIW3.h b/src/ObjLoading/Game/IW3/Menu/MenuConverterIW3.h new file mode 100644 index 00000000..5e67e90f --- /dev/null +++ b/src/ObjLoading/Game/IW3/Menu/MenuConverterIW3.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Asset/AssetCreationContext.h" +#include "Game/IW3/IW3.h" +#include "Parsing/Menu/Domain/CommonMenuDef.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +namespace IW3 +{ + 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 IW3 diff --git a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp index 2a7fca49..c68726ac 100644 --- a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp +++ b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp @@ -7,6 +7,7 @@ #include "Game/IW3/IW3.h" #include "Game/IW3/Image/ImageLoaderEmbeddedIW3.h" #include "Game/IW3/Image/ImageLoaderExternalIW3.h" +#include "Game/IW3/Menu/LoaderMenuListIW3.h" #include "Game/IW3/Techset/PixelShaderLoaderIW3.h" #include "Game/IW3/Techset/VertexShaderLoaderIW3.h" #include "Game/IW3/Weapon/AccuracyGraphLoaderIW3.h" @@ -121,7 +122,7 @@ namespace // collection.AddAssetCreator(std::make_unique(memory)); collection.AddAssetCreator(light_def::CreateLoaderIW3(memory, searchPath)); collection.AddAssetCreator(font::CreateLoaderIW3(memory, searchPath)); - // collection.AddAssetCreator(std::make_unique(memory)); + collection.AddAssetCreator(menu::CreateMenuListLoaderIW3(memory, searchPath)); // collection.AddAssetCreator(std::make_unique(memory)); collection.AddAssetCreator(localize::CreateLoaderIW3(memory, searchPath, zone)); collection.AddAssetCreator(weapon::CreateRawLoaderIW3(memory, searchPath, zone)); diff --git a/src/ObjLoading/Game/IW4/Menu/MenuConverterIW4.cpp b/src/ObjLoading/Game/IW4/Menu/MenuConverterIW4.cpp index 7f0fa52e..4ba7b3a5 100644 --- a/src/ObjLoading/Game/IW4/Menu/MenuConverterIW4.cpp +++ b/src/ObjLoading/Game/IW4/Menu/MenuConverterIW4.cpp @@ -1117,7 +1117,7 @@ namespace auto* item = m_memory.Alloc(); item->window.name = ConvertString(commonItem.m_name); - item->text = ConvertString(commonItem.m_text); + item->text = commonItem.m_text ? m_memory.Dup(commonItem.m_text->c_str()) : nullptr; ApplyFlag(item->itemFlags, commonItem.m_text_save_game, ITEM_FLAG_SAVE_GAME_INFO); ApplyFlag(item->itemFlags, commonItem.m_text_cinematic_subtitle, ITEM_FLAG_CINEMATIC_SUBTITLE); item->window.group = ConvertString(commonItem.m_group); diff --git a/src/ObjLoading/Game/IW5/Menu/MenuConverterIW5.cpp b/src/ObjLoading/Game/IW5/Menu/MenuConverterIW5.cpp index a01d66fd..e86b0a3c 100644 --- a/src/ObjLoading/Game/IW5/Menu/MenuConverterIW5.cpp +++ b/src/ObjLoading/Game/IW5/Menu/MenuConverterIW5.cpp @@ -1123,7 +1123,7 @@ namespace memset(item, 0, sizeof(itemDef_s)); item->window.name = ConvertString(commonItem.m_name); - item->text = ConvertString(commonItem.m_text); + item->text = commonItem.m_text ? m_memory.Dup(commonItem.m_text->c_str()) : nullptr; ApplyFlag(item->itemFlags, commonItem.m_text_save_game, ITEM_FLAG_SAVE_GAME_INFO); ApplyFlag(item->itemFlags, commonItem.m_text_cinematic_subtitle, ITEM_FLAG_CINEMATIC_SUBTITLE); item->window.group = ConvertString(commonItem.m_group); diff --git a/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h b/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h index 98a22764..f4ce2cfb 100644 --- a/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h +++ b/src/ObjLoading/Parsing/Menu/Domain/CommonItemDef.h @@ -5,6 +5,7 @@ #include "Parsing/Simple/Expression/ISimpleExpression.h" #include +#include #include #include @@ -92,7 +93,7 @@ namespace menu }; std::string m_name; - std::string m_text; + std::optional m_text; bool m_text_save_game = false; bool m_text_cinematic_subtitle = false; std::string m_group; @@ -106,7 +107,7 @@ namespace menu int m_border = 0; double m_border_size = 0; int m_owner_draw = 0; - int m_owner_draw_flags = 0; + unsigned m_owner_draw_flags = 0; int m_align = 0; int m_text_align = 0; double m_text_align_x = 0; diff --git a/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h b/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h index 07b6b7d0..ac211ccb 100644 --- a/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h +++ b/src/ObjLoading/Parsing/Menu/Domain/CommonMenuDef.h @@ -24,10 +24,11 @@ namespace menu CommonColor m_fore_color = CommonColor(1.0, 1.0, 1.0, 1.0); CommonColor m_border_color; CommonColor m_focus_color; + CommonColor m_disable_color; CommonColor m_outline_color; std::string m_background; int m_owner_draw = 0; - int m_owner_draw_flags = 0; + unsigned m_owner_draw_flags = 0; std::string m_sound_loop; std::unique_ptr m_sound_loop_exp; double m_fade_clamp = 0; diff --git a/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h b/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h index 35090180..e7435bff 100644 --- a/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h +++ b/src/ObjLoading/Parsing/Menu/Domain/MenuFeatureLevel.h @@ -4,6 +4,7 @@ namespace menu { enum class FeatureLevel { + IW3, IW4, IW5 }; diff --git a/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp b/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp index b5acb8ac..d0a9f11e 100644 --- a/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp +++ b/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.cpp @@ -1,5 +1,7 @@ #include "MenuExpressionMatchers.h" +#include "Game/IW3/IW3.h" +#include "Game/IW3/MenuConstantsIW3.h" #include "Game/IW4/IW4.h" #include "Game/IW4/MenuConstantsIW4.h" #include "Game/IW5/IW5.h" @@ -48,11 +50,29 @@ std::unique_ptr MenuExpressionMatchers::Par }); } -const std::map& MenuExpressionMatchers::GetBaseFunctionMapForFeatureLevel(const FeatureLevel featureLevel) +const std::unordered_map& MenuExpressionMatchers::GetBaseFunctionMapForFeatureLevel(const FeatureLevel featureLevel) { + if (featureLevel == FeatureLevel::IW3) + { + static std::unordered_map iw3FunctionMap; + static bool iw3FunctionMapInitialized = false; + + if (!iw3FunctionMapInitialized) + { + for (size_t i = IW3::OP_FIRSTFUNCTIONCALL; i < std::extent_v; i++) + { + std::string functionName(IW3::g_expFunctionNames[i]); + utils::MakeStringLowerCase(functionName); + iw3FunctionMap.emplace(std::make_pair(std::move(functionName), i)); + } + } + + return iw3FunctionMap; + } + if (featureLevel == FeatureLevel::IW4) { - static std::map iw4FunctionMap; + static std::unordered_map iw4FunctionMap; static bool iw4FunctionMapInitialized = false; if (!iw4FunctionMapInitialized) @@ -69,7 +89,7 @@ const std::map& MenuExpressionMatchers::GetBaseFunctionMapF } if (featureLevel == FeatureLevel::IW5) { - static std::map iw5FunctionMap; + static std::unordered_map iw5FunctionMap; static bool iw5FunctionMapInitialized = false; if (!iw5FunctionMapInitialized) diff --git a/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.h b/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.h index 4704c6ed..b87f85e2 100644 --- a/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.h +++ b/src/ObjLoading/Parsing/Menu/Matcher/MenuExpressionMatchers.h @@ -4,15 +4,12 @@ #include "Parsing/Simple/Expression/SimpleExpressionMatchers.h" #include +#include namespace menu { class MenuExpressionMatchers final : public SimpleExpressionMatchers { - const MenuFileParserState* m_state; - - static const std::map& GetBaseFunctionMapForFeatureLevel(FeatureLevel featureLevel); - public: MenuExpressionMatchers(); explicit MenuExpressionMatchers(const MenuFileParserState* state); @@ -20,5 +17,10 @@ namespace menu protected: std::unique_ptr ParseOperandExtension(const supplier_t* labelSupplier) const override; std::unique_ptr ProcessOperandExtension(SequenceResult& result) const override; + + private: + const MenuFileParserState* m_state; + + static const std::unordered_map& GetBaseFunctionMapForFeatureLevel(FeatureLevel featureLevel); }; } // namespace menu diff --git a/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp b/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp index 6790807d..bf9f8e7b 100644 --- a/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp +++ b/src/ObjLoading/Parsing/Menu/MenuFileReader.cpp @@ -39,6 +39,9 @@ void MenuFileReader::SetupDefinesProxy() defines->AddDefine(DefinesStreamProxy::Define("PC", "1")); switch (m_feature_level) { + case FeatureLevel::IW3: + defines->AddDefine(DefinesStreamProxy::Define("FEATURE_LEVEL_IW3", "1")); + break; case FeatureLevel::IW4: defines->AddDefine(DefinesStreamProxy::Define("FEATURE_LEVEL_IW4", "1")); break; diff --git a/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp b/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp index cb2eef8f..095faed2 100644 --- a/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp +++ b/src/ObjLoading/Parsing/Menu/Sequence/EventHandlerSetScopeSequences.cpp @@ -281,7 +281,7 @@ namespace AddMatchers({ create .Or({ - create.Numeric(), + create.ScriptStrictNumeric(), create.String(), create.Identifier(), create.Type(SimpleParserValueType::CHARACTER), @@ -928,6 +928,15 @@ EventHandlerSetScopeSequences::EventHandlerSetScopeSequences(std::vector()); + + // IW3 stores event handlers as opaque script strings rather than structured handlers. + if (featureLevel == FeatureLevel::IW3) + { + AddSequence(std::make_unique()); + AddSequence(std::make_unique()); + return; + } + // If else and stuff // Creating factory with no label supplier. Cannot use labels with it. diff --git a/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp b/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp index 5a62a750..95e4a0e5 100644 --- a/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp +++ b/src/ObjLoading/Parsing/Menu/Sequence/GlobalScopeSequences.cpp @@ -109,7 +109,8 @@ GlobalScopeSequences::GlobalScopeSequences(std::vector()); - AddSequence(std::make_unique()); + if (featureLevel != FeatureLevel::IW3) + 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 05f2b5e9..4873fa30 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 IW4_FEATURE_TYPE_BY_TYPE[0x18]{ + inline static const CommonItemFeatureType IW3_IW4_FEATURE_TYPE_BY_TYPE[0x18]{ CommonItemFeatureType::EDIT_FIELD, // ITEM_TYPE_TEXT CommonItemFeatureType::NONE, // ITEM_TYPE_BUTTON CommonItemFeatureType::NONE, // ITEM_TYPE_RADIOBUTTON @@ -78,32 +78,34 @@ namespace CommonItemFeatureType::EDIT_FIELD, // ITEM_TYPE_PASSWORDFIELD }; - public: - static void SetItemType(CommonItemDef& item, const FeatureLevel featureLevel, const TokenPos& pos, const int type) + static CommonItemFeatureType GetFeatureTypeForItemType(const FeatureLevel featureLevel, const TokenPos& pos, const int type) { - if (type < 0) - throw ParsingException(pos, "Invalid item type"); - - if (item.m_feature_type != CommonItemFeatureType::NONE) - throw ParsingException(pos, "Item type has already been set"); - - item.m_type = type; - switch (featureLevel) { + case FeatureLevel::IW3: case FeatureLevel::IW4: - if (static_cast(type) >= std::extent_v) + if (static_cast(type) >= std::extent_v) throw ParsingException(pos, "Invalid item type"); - item.m_feature_type = IW4_FEATURE_TYPE_BY_TYPE[static_cast(type)]; - break; + return IW3_IW4_FEATURE_TYPE_BY_TYPE[static_cast(type)]; case FeatureLevel::IW5: default: if (static_cast(type) >= std::extent_v) throw ParsingException(pos, "Invalid item type"); - item.m_feature_type = IW5_FEATURE_TYPE_BY_TYPE[static_cast(type)]; - break; + return IW5_FEATURE_TYPE_BY_TYPE[static_cast(type)]; } + } + + public: + static void SetItemType(CommonItemDef& item, const FeatureLevel featureLevel, const TokenPos& pos, const int type) + { + const auto featureType = GetFeatureTypeForItemType(featureLevel, pos, type); + + if (item.m_feature_type != CommonItemFeatureType::NONE) + throw ParsingException(pos, "Item type has already been set"); + + item.m_type = type; + item.m_feature_type = featureType; switch (item.m_feature_type) { @@ -130,9 +132,16 @@ namespace throw ParsingException(pos, "Item must have be listbox to use this declaration"); } - static void EnsureHasEditFieldFeatures(const CommonItemDef& item, const TokenPos& pos) + static void EnsureHasEditFieldFeatures(CommonItemDef& item, const FeatureLevel featureLevel, const TokenPos& pos) { - if (item.m_feature_type != CommonItemFeatureType::EDIT_FIELD || !item.m_edit_field_features) + // Due to default item types, edit field capabilities might not have been initialized + if (item.m_feature_type == CommonItemFeatureType::NONE + && GetFeatureTypeForItemType(featureLevel, pos, item.m_type) == CommonItemFeatureType::EDIT_FIELD) + { + item.m_feature_type = CommonItemFeatureType::EDIT_FIELD; + item.m_edit_field_features = std::make_unique(); + } + else if (item.m_feature_type != CommonItemFeatureType::EDIT_FIELD || !item.m_edit_field_features) throw ParsingException(pos, "Item must have be edit field to use this declaration"); } @@ -360,7 +369,7 @@ namespace { assert(state->m_current_item); - ItemScopeOperations::EnsureHasEditFieldFeatures(*state->m_current_item, result.NextCapture(CAPTURE_FIRST_TOKEN).GetPos()); + ItemScopeOperations::EnsureHasEditFieldFeatures(*state->m_current_item, state->m_feature_level, result.NextCapture(CAPTURE_FIRST_TOKEN).GetPos()); state->m_current_item->m_dvar = MenuMatcherFactory::TokenTextValue(result.NextCapture(CAPTURE_DVAR_NAME)); state->m_current_item->m_edit_field_features->m_def_val = MenuMatcherFactory::TokenNumericExpressionValue(state, result); state->m_current_item->m_edit_field_features->m_min_val = MenuMatcherFactory::TokenNumericExpressionValue(state, result); @@ -455,8 +464,8 @@ namespace static constexpr auto CAPTURE_FIRST_TOKEN = 1; static constexpr auto CAPTURE_COLUMN_COUNT = 2; - static constexpr const char* SYNTAX_IW4 = " [alignment]"; - static constexpr auto BASE_COLUMN_COUNT_IW4 = 3; + static constexpr const char* SYNTAX_IW3_IW4 = " [alignment]"; + static constexpr auto BASE_COLUMN_COUNT_IW3_IW4 = 3; static constexpr const char* SYNTAX_IW5 = " [alignment]"; static constexpr auto BASE_COLUMN_COUNT_IW5 = 5; @@ -478,7 +487,7 @@ namespace void ProcessMatch(MenuFileParserState* state, SequenceResult& result) const override { assert(state->m_current_item); - assert(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); const auto& firstToken = result.NextCapture(CAPTURE_FIRST_TOKEN); ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, firstToken.GetPos()); @@ -491,10 +500,10 @@ namespace size_t baseColumnCount; const char* syntax; bool hasHeightValues; - if (state->m_feature_level == FeatureLevel::IW4) + if (state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4) { - baseColumnCount = BASE_COLUMN_COUNT_IW4; - syntax = SYNTAX_IW4; + baseColumnCount = BASE_COLUMN_COUNT_IW3_IW4; + syntax = SYNTAX_IW3_IW4; hasHeightValues = false; } else @@ -1151,25 +1160,29 @@ void ItemScopeSequences::AddSequences(const FeatureLevel featureLevel, const boo AddSequence(std::make_unique("localvar", [](const MenuFileParserState* state, const TokenPos& pos, const std::string& value) { - ItemScopeOperations::EnsureHasEditFieldFeatures(*state->m_current_item, pos); + ItemScopeOperations::EnsureHasEditFieldFeatures( + *state->m_current_item, state->m_feature_level, pos); state->m_current_item->m_edit_field_features->m_local_var = value; })); AddSequence(std::make_unique("maxChars", [](const MenuFileParserState* state, const TokenPos& pos, const int value) { - ItemScopeOperations::EnsureHasEditFieldFeatures(*state->m_current_item, pos); + ItemScopeOperations::EnsureHasEditFieldFeatures( + *state->m_current_item, state->m_feature_level, pos); state->m_current_item->m_edit_field_features->m_max_chars = value; })); AddSequence(std::make_unique("maxPaintChars", [](const MenuFileParserState* state, const TokenPos& pos, const int value) { - ItemScopeOperations::EnsureHasEditFieldFeatures(*state->m_current_item, pos); + ItemScopeOperations::EnsureHasEditFieldFeatures( + *state->m_current_item, state->m_feature_level, pos); state->m_current_item->m_edit_field_features->m_max_paint_chars = value; })); AddSequence(std::make_unique("maxCharsGotoNext", [](const MenuFileParserState* state, const TokenPos& pos) { - ItemScopeOperations::EnsureHasEditFieldFeatures(*state->m_current_item, pos); + ItemScopeOperations::EnsureHasEditFieldFeatures( + *state->m_current_item, state->m_feature_level, pos); state->m_current_item->m_edit_field_features->m_max_chars_goto_next = true; })); diff --git a/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp b/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp index e44b56e3..5b6e7bf5 100644 --- a/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp +++ b/src/ObjLoading/Parsing/Menu/Sequence/MenuScopeSequences.cpp @@ -309,6 +309,11 @@ void MenuScopeSequences::AddSequences(const FeatureLevel featureLevel, const boo { state->m_current_menu->m_focus_color = value; })); + AddSequence(std::make_unique("disablecolor", + [](const MenuFileParserState* state, const TokenPos&, const CommonColor value) + { + state->m_current_menu->m_disable_color = value; + })); AddSequence(std::make_unique("outlinecolor", [](const MenuFileParserState* state, const TokenPos&, const CommonColor& value) { diff --git a/src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.cpp b/src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.cpp new file mode 100644 index 00000000..13d7ca19 --- /dev/null +++ b/src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.cpp @@ -0,0 +1,51 @@ +#include "MenuDumperIW3.h" + +#include "MenuListDumperIW3.h" +#include "MenuWriterIW3.h" +#include "ObjWriting.h" + +#include + +using namespace IW3; + +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 "ui_mp/" + std::string(asset.Asset()->window.name) + ".menu"; + + return menuDumpingState->second.m_path; + } +} // namespace + +namespace menu +{ + void MenuDumperIW3::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 + auto menuListAssets = context.m_zone.m_pools.PoolAssets(); + for (auto* menuListAsset : menuListAssets) + CreateDumpingStateForMenuListIW3(zoneState, menuListAsset->Asset()); + } + + const auto menuFilePath = GetPathForMenu(zoneState, asset); + const auto assetFile = context.OpenAssetFile(menuFilePath); + + if (!assetFile) + return; + + auto menuWriter = CreateMenuWriterIW3(*assetFile); + + menuWriter->Start(); + menuWriter->WriteMenu(*menu); + menuWriter->End(); + } +} // namespace menu diff --git a/src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.h b/src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.h new file mode 100644 index 00000000..97ca08a2 --- /dev/null +++ b/src/ObjWriting/Game/IW3/Menu/MenuDumperIW3.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Dumping/AbstractAssetDumper.h" +#include "Game/IW3/IW3.h" + +namespace menu +{ + class MenuDumperIW3 final : public AbstractAssetDumper + { + protected: + void DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) override; + }; +} // namespace menu diff --git a/src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.cpp b/src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.cpp new file mode 100644 index 00000000..24e45565 --- /dev/null +++ b/src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.cpp @@ -0,0 +1,120 @@ +#include "MenuListDumperIW3.h" + +#include "MenuWriterIW3.h" + +#include +#include + +namespace fs = std::filesystem; + +using namespace IW3; + +namespace +{ + void DumpMenus(menu::IWriterIW3& 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; + + // If the menu was embedded directly as menu list write its data in the menu list file + if (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 CreateDumpingStateForMenuListIW3(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 MenuListDumperIW3::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 = CreateMenuWriterIW3(*assetFile); + + menuWriter->Start(); + DumpMenus(*menuWriter, zoneState, menuList); + menuWriter->End(); + } + + void MenuListDumperIW3::Dump(AssetDumpingContext& context) + { + auto* zoneState = context.GetZoneAssetDumperState(); + + auto menuListAssets = context.m_zone.m_pools.PoolAssets(); + for (const auto* asset : menuListAssets) + CreateDumpingStateForMenuListIW3(zoneState, asset->Asset()); + + AbstractAssetDumper::Dump(context); + } +} // namespace menu diff --git a/src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.h b/src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.h new file mode 100644 index 00000000..d107b2b3 --- /dev/null +++ b/src/ObjWriting/Game/IW3/Menu/MenuListDumperIW3.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Dumping/AbstractAssetDumper.h" +#include "Game/IW3/IW3.h" +#include "Menu/MenuDumpingZoneState.h" + +namespace menu +{ + void CreateDumpingStateForMenuListIW3(MenuDumpingZoneState* zoneState, const IW3::MenuList* menuList); + + class MenuListDumperIW3 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/IW3/Menu/MenuWriterIW3.cpp b/src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.cpp new file mode 100644 index 00000000..e9a2667d --- /dev/null +++ b/src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.cpp @@ -0,0 +1,681 @@ +#include "MenuWriterIW3.h" + +#include "Game/IW3/MenuConstantsIW3.h" +#include "Menu/AbstractMenuWriter.h" +#include "ObjWriting.h" + +#include +#include +#include +#include + +using namespace IW3; + +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::IWriterIW3 + { + 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); + WriteColorProperty("selectBorder", listBox.selectBorder, COLOR_0000); + WriteColorProperty("disableColor", listBox.disableColor, COLOR_0000); + WriteMaterialProperty("selectIcon", listBox.selectIcon); + } + + 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 + { + // IW3 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("onClose", menu.onClose); + WriteScriptProperty("onESC", menu.onESC); + WriteItemKeyHandlers(menu.onKey); + WriteItemDefs(menu.items, menu.itemCount); + } + }; +} // namespace + +namespace menu +{ + std::unique_ptr CreateMenuWriterIW3(std::ostream& stream) + { + return std::make_unique(stream); + } +} // namespace menu diff --git a/src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.h b/src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.h new file mode 100644 index 00000000..664e8ba1 --- /dev/null +++ b/src/ObjWriting/Game/IW3/Menu/MenuWriterIW3.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Game/IW3/IW3.h" +#include "Menu/IMenuWriter.h" + +#include +#include + +namespace menu +{ + class IWriterIW3 : public IWriter + { + public: + virtual void WriteMenu(const IW3::menuDef_t& menu) = 0; + }; + + std::unique_ptr CreateMenuWriterIW3(std::ostream& stream); +} // namespace menu diff --git a/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp b/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp index 2c7af7a9..2fd0cf6d 100644 --- a/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp +++ b/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp @@ -9,6 +9,8 @@ #include "Game/IW3/XModel/XModelDumperIW3.h" #include "LightDef/LightDefDumperIW3.h" #include "Localize/LocalizeDumperIW3.h" +#include "Menu/MenuDumperIW3.h" +#include "Menu/MenuListDumperIW3.h" #include "PhysPreset/PhysPresetInfoStringDumperIW3.h" #include "RawFile/RawFileDumperIW3.h" #include "Sound/LoadedSoundDumperIW3.h" @@ -43,8 +45,8 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) // REGISTER_DUMPER(AssetDumperGfxWorld) RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); - // REGISTER_DUMPER(AssetDumperMenuList) - // REGISTER_DUMPER(AssetDumpermenuDef_t) + RegisterAssetDumper(std::make_unique()); + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); // REGISTER_DUMPER(AssetDumperSndDriverGlobals) diff --git a/src/ObjWriting/Menu/AbstractMenuWriter.cpp b/src/ObjWriting/Menu/AbstractMenuWriter.cpp index 34c1b567..a272d4eb 100644 --- a/src/ObjWriting/Menu/AbstractMenuWriter.cpp +++ b/src/ObjWriting/Menu/AbstractMenuWriter.cpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace menu @@ -117,6 +118,14 @@ namespace menu if (token.empty()) return true; + // The menu linker concatenates adjacent quoted numeric arguments. For example, + // `"0.1" "0.1" "0.12" "0.5"` becomes `"0.10.10.120.5"` instead of four + // color components. + char* numericEnd; + (void)std::strtof(token.c_str(), &numericEnd); + if (numericEnd == token.c_str() + token.size()) + return false; + const auto hasAlNumCharacter = std::ranges::any_of(token, [](const char& c) { @@ -263,15 +272,16 @@ namespace menu m_stream << "\n"; } - void AbstractBaseWriter::WriteFlagsProperty(const std::string& propertyKey, const int flagsValue) const + void AbstractBaseWriter::WriteFlagsProperty(const std::string& propertyKey, const unsigned flagsValue) const { for (auto i = 0u; i < sizeof(flagsValue) * 8; i++) { - if (flagsValue & (1 << i)) + const unsigned mask = 1u << i; + if (flagsValue & mask) { Indent(); WriteKey(propertyKey); - m_stream << i << "\n"; + m_stream << mask << "\n"; } } } diff --git a/src/ObjWriting/Menu/AbstractMenuWriter.h b/src/ObjWriting/Menu/AbstractMenuWriter.h index 3127a3e8..f20be167 100644 --- a/src/ObjWriting/Menu/AbstractMenuWriter.h +++ b/src/ObjWriting/Menu/AbstractMenuWriter.h @@ -51,7 +51,7 @@ namespace menu void WriteFloatProperty(const std::string& propertyKey, float propertyValue, float defaultValue) const; void WriteColorProperty(const std::string& propertyKey, const float (&propertyValue)[4], const float (&defaultValue)[4]) const; void WriteKeywordProperty(const std::string& propertyKey, bool shouldWrite) const; - void WriteFlagsProperty(const std::string& propertyKey, int flagsValue) const; + void WriteFlagsProperty(const std::string& propertyKey, unsigned flagsValue) const; std::ostream& m_stream; size_t m_indent; diff --git a/src/ZoneCode/Game/IW3/XAssets/menuDef_t.txt b/src/ZoneCode/Game/IW3/XAssets/menuDef_t.txt index 6c629643..a0c20760 100644 --- a/src/ZoneCode/Game/IW3/XAssets/menuDef_t.txt +++ b/src/ZoneCode/Game/IW3/XAssets/menuDef_t.txt @@ -70,7 +70,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/IW3/Menu/LoaderMenuListIW3Test.cpp b/test/ObjLoadingTests/Game/IW3/Menu/LoaderMenuListIW3Test.cpp new file mode 100644 index 00000000..e4a8009c --- /dev/null +++ b/test/ObjLoadingTests/Game/IW3/Menu/LoaderMenuListIW3Test.cpp @@ -0,0 +1,247 @@ +#include "Game/IW3/Menu/LoaderMenuListIW3.h" + +#include "Game/IW3/GameIW3.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include + +using namespace IW3; +using namespace std::literals; +using namespace Catch; +using namespace Catch::Matchers; + +namespace +{ + class MenuLoadingTestHelper + { + public: + MenuLoadingTestHelper() + : m_zone("MockZone", 0, GameId::IW3, GamePlatform::PC), + m_creator_collection(m_zone), + m_context(m_zone, &m_creator_collection, &m_ignored_asset_lookup) + { + m_loader = menu::CreateMenuListLoaderIW3(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("MenuListLoaderIW3: Loads dumped menus and preserves IW3 data", "[iw3][menu][assetloader]") + { + MenuLoadingTestHelper helper; + const auto* backgroundMaterial = helper.AddMaterial("background_material"); + const auto* selectMaterial = helper.AddMaterial("select_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; + } + 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 + selectIcon "select_material" + doubleclick + { + play mouse_click; + } + } + 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 + 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] & 0x10000); + 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->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] & 0x4); + REQUIRE(emptyTextItem->window.dynamicFlags[0] & 0x10000); + 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(listItem->typeData.listBox->selectIcon == selectMaterial); + REQUIRE(listItem->typeData.listBox->onDoubleClick == R"("play" "mouse_click" ; )"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 & 0x01000000); + REQUIRE(popupMenu->onESC == R"("close" "self" ; )"s); + } + + TEST_CASE("MenuListLoaderIW3: Ensure optimizing item rect considers relative positions", "[iw3][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/IW3/Menu/MenuDumperIW3Test.cpp b/test/ObjWritingTests/Game/IW3/Menu/MenuDumperIW3Test.cpp new file mode 100644 index 00000000..c2365384 --- /dev/null +++ b/test/ObjWritingTests/Game/IW3/Menu/MenuDumperIW3Test.cpp @@ -0,0 +1,517 @@ +#include "Game/IW3/Menu/MenuDumperIW3.h" + +#include "Game/IW3/Menu/MenuListDumperIW3.h" +#include "Parsing/Menu/MenuFileReader.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include +#include + +using namespace IW3; + +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("MenuDumperIW3: Can dump menu with expressions and scripts", "[iw3][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::IW3, 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::MenuDumperIW3 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("MenuDumperIW3: Prefers parent menu list path over ui_mp fallback", "[iw3][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::IW3, 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::MenuListDumperIW3 menuListDumper; + menuListDumper.Dump(context); + + menu::MenuDumperIW3 menuDumper; + menuDumper.Dump(context); + + REQUIRE(mockOutput.GetMockedFile("ui/test_menu.menu")); + REQUIRE_FALSE(mockOutput.GetMockedFile("ui_mp/test_menu.menu")); + } + + TEST_CASE("MenuDumperIW3: Expressions are valid when dumped", "[iw3][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::IW3, 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::MenuDumperIW3 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); + } +} // namespace diff --git a/test/ObjWritingTests/Game/IW3/Menu/MenuListDumperIW3Test.cpp b/test/ObjWritingTests/Game/IW3/Menu/MenuListDumperIW3Test.cpp new file mode 100644 index 00000000..17998094 --- /dev/null +++ b/test/ObjWritingTests/Game/IW3/Menu/MenuListDumperIW3Test.cpp @@ -0,0 +1,53 @@ +#include "Game/IW3/Menu/MenuListDumperIW3.h" + +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include + +using namespace IW3; + +namespace +{ + TEST_CASE("MenuListDumperIW3: Can dump several menu entries", "[iw3][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::IW3, 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::MenuListDumperIW3 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/IW3/MenuRoundTrip/MenuRoundTripIW3.zone b/test/SystemTests/Game/IW3/MenuRoundTrip/MenuRoundTripIW3.zone new file mode 100644 index 00000000..5590cb1c --- /dev/null +++ b/test/SystemTests/Game/IW3/MenuRoundTrip/MenuRoundTripIW3.zone @@ -0,0 +1,3 @@ +>game,IW3 + +menulist,ui_mp/menus.txt diff --git a/test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/main.menu b/test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/main.menu new file mode 100644 index 00000000..369a878f --- /dev/null +++ b/test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/main.menu @@ -0,0 +1,31 @@ +{ + 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; + } + 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"); + } + } +} diff --git a/test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/menus.txt b/test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/menus.txt new file mode 100644 index 00000000..9c3fb51f --- /dev/null +++ b/test/SystemTests/Game/IW3/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/IW3/MenuRoundTrip/ui_mp/popup.menu b/test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/popup.menu new file mode 100644 index 00000000..83699219 --- /dev/null +++ b/test/SystemTests/Game/IW3/MenuRoundTrip/ui_mp/popup.menu @@ -0,0 +1,12 @@ +{ + menuDef + { + name "popup" + rect 40 40 200 100 0 0 + popup + onESC + { + close self; + } + } +} diff --git a/test/SystemTests/Game/IW3/MenuRoundTripIW3.cpp b/test/SystemTests/Game/IW3/MenuRoundTripIW3.cpp new file mode 100644 index 00000000..0fccebab --- /dev/null +++ b/test/SystemTests/Game/IW3/MenuRoundTripIW3.cpp @@ -0,0 +1,90 @@ +#include "Game/IW3/IW3.h" +#include "Game/IW3/Menu/MenuDumperIW3.h" +#include "Game/IW3/Menu/MenuListDumperIW3.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 IW3; +using namespace std::literals; + +namespace +{ + TEST_CASE("Menu loading and dumping (IW3)", "[iw3][menu][system]") + { + const auto testDataPath = oat::paths::GetSystemTestsDirectory() / "Game/IW3/MenuRoundTrip"; + const auto testDataPathString = testDataPath.string(); + const auto outputPath = oat::paths::GetTempDirectory("MenuRoundTripIW3"); + 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(), + "MenuRoundTripIW3", + }; + + 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 / "MenuRoundTripIW3.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] & 0x10000); + REQUIRE(mainMenu->Asset()->items[0]->text != nullptr); + REQUIRE(mainMenu->Asset()->items[0]->text == ""s); + REQUIRE(mainMenu->Asset()->items[0]->window.dynamicFlags[0] & 0x10000); + REQUIRE(mainMenu->Asset()->items[0]->action == R"("setItemColor" "empty_text" "borderColor" "0.1" "0.1" "0.12" "0.5" ; )"s); + + MockSearchPath objPath; + MockOutputPath dumpOutput; + AssetDumpingContext dumpingContext(*zone, "", dumpOutput, objPath, std::nullopt); + menu::MenuListDumperIW3 menuListDumper; + menuListDumper.Dump(dumpingContext); + menu::MenuDumperIW3 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); + } +} // namespace