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 <[email protected]>
This commit is contained in:
mo
2026-08-31 00:04:01 +02:00
committed by GitHub
co-authored by Jan Laupetin
parent 4d4921e230
commit 51770e7ae5
42 changed files with 3218 additions and 60 deletions
@@ -0,0 +1,51 @@
#include "MenuDumperIW3.h"
#include "MenuListDumperIW3.h"
#include "MenuWriterIW3.h"
#include "ObjWriting.h"
#include <string>
using namespace IW3;
namespace
{
std::string GetPathForMenu(menu::MenuDumpingZoneState* zoneState, const XAssetInfo<menuDef_t>& 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<AssetMenu::Type>& asset)
{
const auto* menu = asset.Asset();
auto* zoneState = context.GetZoneAssetDumperState<MenuDumpingZoneState>();
if (!ObjWriting::ShouldHandleAssetType(ASSET_TYPE_MENULIST))
{
// Make sure menu paths based on menu lists are created
auto menuListAssets = context.m_zone.m_pools.PoolAssets<AssetMenuList>();
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
@@ -0,0 +1,13 @@
#pragma once
#include "Dumping/AbstractAssetDumper.h"
#include "Game/IW3/IW3.h"
namespace menu
{
class MenuDumperIW3 final : public AbstractAssetDumper<IW3::AssetMenu>
{
protected:
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<IW3::AssetMenu::Type>& asset) override;
};
} // namespace menu
@@ -0,0 +1,120 @@
#include "MenuListDumperIW3.h"
#include "MenuWriterIW3.h"
#include <filesystem>
#include <format>
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<AssetMenuList::Type>& asset)
{
const auto* menuList = asset.Asset();
const auto assetFile = context.OpenAssetFile(asset.m_name);
if (!assetFile)
return;
auto* zoneState = context.GetZoneAssetDumperState<MenuDumpingZoneState>();
const auto menuWriter = CreateMenuWriterIW3(*assetFile);
menuWriter->Start();
DumpMenus(*menuWriter, zoneState, menuList);
menuWriter->End();
}
void MenuListDumperIW3::Dump(AssetDumpingContext& context)
{
auto* zoneState = context.GetZoneAssetDumperState<MenuDumpingZoneState>();
auto menuListAssets = context.m_zone.m_pools.PoolAssets<AssetMenuList>();
for (const auto* asset : menuListAssets)
CreateDumpingStateForMenuListIW3(zoneState, asset->Asset());
AbstractAssetDumper::Dump(context);
}
} // namespace menu
@@ -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<IW3::AssetMenuList>
{
public:
void Dump(AssetDumpingContext& context) override;
protected:
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<IW3::AssetMenuList::Type>& asset) override;
};
} // namespace menu
@@ -0,0 +1,681 @@
#include "MenuWriterIW3.h"
#include "Game/IW3/MenuConstantsIW3.h"
#include "Menu/AbstractMenuWriter.h"
#include "ObjWriting.h"
#include <cassert>
#include <cmath>
#include <limits>
#include <sstream>
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<size_t>(statement.numEntries));
const auto statementEnd = static_cast<size_t>(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<size_t>(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<size_t>(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<unsigned>(entry->data.op) < std::extent_v<decltype(g_expFunctionNames)>);
if (entry->data.op >= 0 && static_cast<unsigned>(entry->data.op) < std::extent_v<decltype(g_expFunctionNames)>)
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<unsigned>(expEntry->data.op) < std::extent_v<decltype(g_expFunctionNames)>)
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<size_t>(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<size_t>(statement.numEntries));
}
void WriteStatementSkipInitialUnnecessaryParenthesis(const statement_s& statement) const
{
if (!HasStatement(statement))
return;
const auto statementEnd = static_cast<size_t>(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<char>(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<size_t>(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<float>::epsilon()
|| std::fabs(-1.0f - editField.minVal) >= std::numeric_limits<float>::epsilon()
|| std::fabs(-1.0f - editField.maxVal) >= std::numeric_limits<float>::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<size_t>(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<IWriterIW3> CreateMenuWriterIW3(std::ostream& stream)
{
return std::make_unique<MenuWriter>(stream);
}
} // namespace menu
@@ -0,0 +1,18 @@
#pragma once
#include "Game/IW3/IW3.h"
#include "Menu/IMenuWriter.h"
#include <memory>
#include <ostream>
namespace menu
{
class IWriterIW3 : public IWriter
{
public:
virtual void WriteMenu(const IW3::menuDef_t& menu) = 0;
};
std::unique_ptr<IWriterIW3> CreateMenuWriterIW3(std::ostream& stream);
} // namespace menu
+4 -2
View File
@@ -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<light_def::DumperIW3>());
RegisterAssetDumper(std::make_unique<font::JsonDumperIW3>());
// REGISTER_DUMPER(AssetDumperMenuList)
// REGISTER_DUMPER(AssetDumpermenuDef_t)
RegisterAssetDumper(std::make_unique<menu::MenuListDumperIW3>());
RegisterAssetDumper(std::make_unique<menu::MenuDumperIW3>());
RegisterAssetDumper(std::make_unique<localize::DumperIW3>());
RegisterAssetDumper(std::make_unique<weapon::DumperIW3>());
// REGISTER_DUMPER(AssetDumperSndDriverGlobals)
+13 -3
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <sstream>
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";
}
}
}
+1 -1
View File
@@ -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;