mirror of
https://github.com/Laupetin/OpenAssetTools.git
synced 2026-09-08 06:57:05 +00:00
feat: add T4 menu dumping and loading (#986)
* feat: add T4 menu dumping * chore: align with latest changes * feat: add T4 menu loading Load T4 menulists and menu files through the shared menu parser. Preserve T4-specific menu and listbox fields and cover loading, dumping, linking, and round-trip behaviour. * chore: make flag fields unsigned * chore: rename `expOperationEnum` -> `operationEnum` * chore: use default enum numeration for T4 operationEnum * chore: remove unused members of operationEnum * chore: remove redundant op from operationEnum * refactor: align T4 expression operand names with IW3 * chore: small adjustment to iw3,t4 types --------- Co-authored-by: Jan Laupetin <[email protected]>
This commit is contained in:
@@ -182,8 +182,8 @@ using `Linker`):
|
|||||||
| GfxWorld | ❌ | ❌ | |
|
| GfxWorld | ❌ | ❌ | |
|
||||||
| GfxLightDef | ❌ | ❌ | |
|
| GfxLightDef | ❌ | ❌ | |
|
||||||
| Font_s | ✅ | ✅ | |
|
| Font_s | ✅ | ✅ | |
|
||||||
| MenuList | ❌ | ❌ | |
|
| MenuList | ✅ | ✅ | The output is decompiled. The result will not be the same as the input. |
|
||||||
| menuDef_t | ❌ | ❌ | |
|
| menuDef_t | ✅ | ✅ | See menulist. |
|
||||||
| LocalizeEntry | ✅ | ✅ | |
|
| LocalizeEntry | ✅ | ✅ | |
|
||||||
| WeaponDef | ✅ | ✅ | |
|
| WeaponDef | ✅ | ✅ | |
|
||||||
| FxEffectDef | ❌ | ❌ | |
|
| FxEffectDef | ❌ | ❌ | |
|
||||||
|
|||||||
@@ -2812,7 +2812,7 @@ namespace IW3
|
|||||||
int gameMsgWindowIndex;
|
int gameMsgWindowIndex;
|
||||||
int gameMsgWindowMode;
|
int gameMsgWindowMode;
|
||||||
const char* text;
|
const char* text;
|
||||||
int itemFlags;
|
unsigned int itemFlags;
|
||||||
menuDef_t* parent;
|
menuDef_t* parent;
|
||||||
const char* mouseEnterText;
|
const char* mouseEnterText;
|
||||||
const char* mouseExitText;
|
const char* mouseExitText;
|
||||||
|
|||||||
+137
-10
@@ -2781,6 +2781,26 @@ namespace T4
|
|||||||
int vertAlign;
|
int vertAlign;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum WindowDefStaticFlag : unsigned int
|
||||||
|
{
|
||||||
|
WINDOW_FLAG_DECORATION = 0x100000,
|
||||||
|
WINDOW_FLAG_HORIZONTAL_SCROLL = 0x200000,
|
||||||
|
WINDOW_FLAG_AUTO_WRAPPED = 0x800000,
|
||||||
|
WINDOW_FLAG_POPUP = 0x1000000,
|
||||||
|
WINDOW_FLAG_OUT_OF_BOUNDS_CLICK = 0x2000000,
|
||||||
|
WINDOW_FLAG_LEGACY_SPLIT_SCREEN_SCALE = 0x4000000,
|
||||||
|
WINDOW_FLAG_HIDDEN_DURING_FLASH_BANG = 0x10000000,
|
||||||
|
WINDOW_FLAG_HIDDEN_DURING_SCOPE = 0x20000000,
|
||||||
|
WINDOW_FLAG_HIDDEN_DURING_UI = 0x40000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum WindowDefDynamicFlag : unsigned int
|
||||||
|
{
|
||||||
|
WINDOW_FLAG_VISIBLE = 0x4,
|
||||||
|
WINDOW_FLAG_NON_DEFAULT_BACKCOLOR = 0x8000,
|
||||||
|
WINDOW_FLAG_NON_DEFAULT_FORECOLOR = 0x10000,
|
||||||
|
};
|
||||||
|
|
||||||
struct windowDef_t
|
struct windowDef_t
|
||||||
{
|
{
|
||||||
const char* name;
|
const char* name;
|
||||||
@@ -2792,8 +2812,8 @@ namespace T4
|
|||||||
int ownerDraw;
|
int ownerDraw;
|
||||||
int ownerDrawFlags;
|
int ownerDrawFlags;
|
||||||
float borderSize;
|
float borderSize;
|
||||||
int staticFlags;
|
unsigned int staticFlags;
|
||||||
int dynamicFlags[1];
|
unsigned int dynamicFlags[1];
|
||||||
int nextTime;
|
int nextTime;
|
||||||
float foreColor[4];
|
float foreColor[4];
|
||||||
float backColor[4];
|
float backColor[4];
|
||||||
@@ -2813,7 +2833,7 @@ namespace T4
|
|||||||
{
|
{
|
||||||
int intVal;
|
int intVal;
|
||||||
float floatVal;
|
float floatVal;
|
||||||
const char* string;
|
const char* stringVal;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum expDataType
|
enum expDataType
|
||||||
@@ -2823,11 +2843,109 @@ namespace T4
|
|||||||
VAL_STRING = 0x2,
|
VAL_STRING = 0x2,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum expOperationEnum
|
enum operationEnum : int
|
||||||
{
|
{
|
||||||
OP_NEGATE = 0x7,
|
OP_NOOP,
|
||||||
NUM_EXPRESSION_OPERATORS = 0x18,
|
OP_RIGHTPAREN,
|
||||||
MAX_OPERATOR_VALUE = 0x4000,
|
OP_MULTIPLY,
|
||||||
|
OP_DIVIDE,
|
||||||
|
OP_MODULUS,
|
||||||
|
OP_ADD,
|
||||||
|
OP_SUBTRACT,
|
||||||
|
OP_NOT,
|
||||||
|
OP_LESSTHAN,
|
||||||
|
OP_LESSTHANEQUALTO,
|
||||||
|
OP_GREATERTHAN,
|
||||||
|
OP_GREATERTHANEQUALTO,
|
||||||
|
OP_EQUALS,
|
||||||
|
OP_NOTEQUAL,
|
||||||
|
OP_AND,
|
||||||
|
OP_OR,
|
||||||
|
OP_LEFTPAREN,
|
||||||
|
OP_COMMA,
|
||||||
|
OP_BITWISEAND,
|
||||||
|
OP_BITWISEOR,
|
||||||
|
OP_BITWISENOT,
|
||||||
|
OP_BITSHIFTLEFT,
|
||||||
|
OP_BITSHIFTRIGHT,
|
||||||
|
|
||||||
|
OP_FIRSTFUNCTIONCALL,
|
||||||
|
OP_SIN = OP_FIRSTFUNCTIONCALL,
|
||||||
|
OP_COS,
|
||||||
|
OP_MIN,
|
||||||
|
OP_MAX,
|
||||||
|
OP_MILLISECONDS,
|
||||||
|
OP_DVARINT,
|
||||||
|
OP_DVARBOOL,
|
||||||
|
OP_DVARFLOAT,
|
||||||
|
OP_DVARSTRING,
|
||||||
|
OP_STAT,
|
||||||
|
OP_UIACTIVE,
|
||||||
|
OP_FLASHBANGED,
|
||||||
|
OP_SCOPED,
|
||||||
|
OP_SCOREBOARDVISIBLE,
|
||||||
|
OP_INKILLCAM,
|
||||||
|
OP_PLAYERFIELD,
|
||||||
|
OP_SELECTINGLOCATION,
|
||||||
|
OP_TEAMFIELD,
|
||||||
|
OP_OTHERTEAMFIELD,
|
||||||
|
OP_MARINESFIELD,
|
||||||
|
OP_OPFORFIELD,
|
||||||
|
OP_MENUISOPEN,
|
||||||
|
OP_WRITINGDATA,
|
||||||
|
OP_INLOBBY,
|
||||||
|
OP_INPRIVATEPARTY,
|
||||||
|
OP_PRIVATEPARTYHOST,
|
||||||
|
OP_PRIVATEPARTYHOSTINLOBBY,
|
||||||
|
OP_ALONEINPARTY,
|
||||||
|
OP_ADSJAVELIN,
|
||||||
|
OP_WEAPLOCKBLINK,
|
||||||
|
OP_WEAPATTACKTOP,
|
||||||
|
OP_WEAPATTACKDIRECT,
|
||||||
|
OP_SECONDSASTIME,
|
||||||
|
OP_TABLELOOKUP,
|
||||||
|
OP_LOCALIZESTRING,
|
||||||
|
OP_LOCALVARINT,
|
||||||
|
OP_LOCALVARBOOL,
|
||||||
|
OP_LOCALVARFLOAT,
|
||||||
|
OP_LOCALVARSTRING,
|
||||||
|
OP_TIMELEFT,
|
||||||
|
OP_SECONDSASCOUNTDOWN,
|
||||||
|
OP_GAMEMSGWNDACTIVE,
|
||||||
|
OP_TOINT,
|
||||||
|
OP_TOSTRING,
|
||||||
|
OP_TOFLOAT,
|
||||||
|
OP_GAMETYPENAME,
|
||||||
|
OP_GAMETYPE,
|
||||||
|
OP_GAMETYPEDESCRIPTION,
|
||||||
|
OP_SCORE,
|
||||||
|
OP_FRIENDSONLINE,
|
||||||
|
OP_FOLLOWING,
|
||||||
|
OP_STATRANGEBITSSET,
|
||||||
|
OP_KEYBINDING,
|
||||||
|
OP_ACTIONSLOTUSABLE,
|
||||||
|
OP_HUDFADE,
|
||||||
|
OP_MAXPLAYERS,
|
||||||
|
OP_ACCEPTINGINVITE,
|
||||||
|
OP_GAMEHOST,
|
||||||
|
OP_ISSPLITSCREEN,
|
||||||
|
OP_ISSPLITSCREENHOST,
|
||||||
|
OP_SPLITSCREENNUM,
|
||||||
|
OP_ISCINEMATICFINISHED,
|
||||||
|
OP_ISSQUADLEADER,
|
||||||
|
OP_GETSQUADID,
|
||||||
|
OP_ISARTILLERY,
|
||||||
|
OP_GETPLAYERSQUADID,
|
||||||
|
OP_HASFRIENDS,
|
||||||
|
OP_HASPENDINGFRIENDS,
|
||||||
|
OP_HASINVITES,
|
||||||
|
OP_SQUADINVITESENT,
|
||||||
|
OP_ISINTERMISSION,
|
||||||
|
OP_PARTYISMISSINGMAPPACK,
|
||||||
|
OP_PARTYMISSINGMAPPACKERROR,
|
||||||
|
OP_ANYNEWMAPPACKS,
|
||||||
|
|
||||||
|
NUM_OPERATORS,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Operand
|
struct Operand
|
||||||
@@ -2839,7 +2957,7 @@ namespace T4
|
|||||||
union entryInternalData
|
union entryInternalData
|
||||||
{
|
{
|
||||||
Operand operand;
|
Operand operand;
|
||||||
expOperationEnum op;
|
operationEnum op;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum expressionEntryType : int
|
enum expressionEntryType : int
|
||||||
@@ -2879,7 +2997,7 @@ namespace T4
|
|||||||
int elementStyle;
|
int elementStyle;
|
||||||
int numColumns;
|
int numColumns;
|
||||||
columnInfo_s columnInfo[16];
|
columnInfo_s columnInfo[16];
|
||||||
const char* doubleClick;
|
const char* onDoubleClick;
|
||||||
int notselectable;
|
int notselectable;
|
||||||
int noScrollBars;
|
int noScrollBars;
|
||||||
int usePaging;
|
int usePaging;
|
||||||
@@ -2945,6 +3063,15 @@ namespace T4
|
|||||||
ITEM_TYPE_GAME_MESSAGE_WINDOW = 0x13
|
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
|
struct itemDef_s
|
||||||
{
|
{
|
||||||
windowDef_t window;
|
windowDef_t window;
|
||||||
@@ -2961,7 +3088,7 @@ namespace T4
|
|||||||
int gameMsgWindowIndex;
|
int gameMsgWindowIndex;
|
||||||
int gameMsgWindowMode;
|
int gameMsgWindowMode;
|
||||||
const char* text;
|
const char* text;
|
||||||
int itemFlags;
|
unsigned int itemFlags;
|
||||||
menuDef_t* parent;
|
menuDef_t* parent;
|
||||||
const char* mouseEnterText;
|
const char* mouseEnterText;
|
||||||
const char* mouseExitText;
|
const char* mouseExitText;
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace T4
|
||||||
|
{
|
||||||
|
inline constexpr const char* g_expFunctionNames[]{
|
||||||
|
"NOOP",
|
||||||
|
")",
|
||||||
|
"*",
|
||||||
|
"/",
|
||||||
|
"%",
|
||||||
|
"+",
|
||||||
|
"-",
|
||||||
|
"!",
|
||||||
|
"<",
|
||||||
|
"<=",
|
||||||
|
">",
|
||||||
|
">=",
|
||||||
|
"==",
|
||||||
|
"!=",
|
||||||
|
"&&",
|
||||||
|
"||",
|
||||||
|
"(",
|
||||||
|
",",
|
||||||
|
"&",
|
||||||
|
"|",
|
||||||
|
"~",
|
||||||
|
"<<",
|
||||||
|
">>",
|
||||||
|
"sin",
|
||||||
|
"cos",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
"milliseconds",
|
||||||
|
"dvarint",
|
||||||
|
"dvarbool",
|
||||||
|
"dvarfloat",
|
||||||
|
"dvarstring",
|
||||||
|
"stat",
|
||||||
|
"ui_active",
|
||||||
|
"flashbanged",
|
||||||
|
"scoped",
|
||||||
|
"scoreboard_visible",
|
||||||
|
"inkillcam",
|
||||||
|
"player",
|
||||||
|
"selecting_location",
|
||||||
|
"team",
|
||||||
|
"otherteam",
|
||||||
|
"marinesfield",
|
||||||
|
"opforfield",
|
||||||
|
"menuisopen",
|
||||||
|
"writingdata",
|
||||||
|
"inlobby",
|
||||||
|
"inprivateparty",
|
||||||
|
"privatepartyhost",
|
||||||
|
"privatepartyhostinlobby",
|
||||||
|
"aloneinparty",
|
||||||
|
"adsjavelin",
|
||||||
|
"weaplockblink",
|
||||||
|
"weapattacktop",
|
||||||
|
"weapattackdirect",
|
||||||
|
"secondsastime",
|
||||||
|
"tablelookup",
|
||||||
|
"locstring",
|
||||||
|
"localvarint",
|
||||||
|
"localvarbool",
|
||||||
|
"localvarfloat",
|
||||||
|
"localvarstring",
|
||||||
|
"timeleft",
|
||||||
|
"secondsascountdown",
|
||||||
|
"gamemsgwndactive",
|
||||||
|
"int",
|
||||||
|
"string",
|
||||||
|
"float",
|
||||||
|
"gametypename",
|
||||||
|
"gametype",
|
||||||
|
"gametypedescription",
|
||||||
|
"scoreatrank",
|
||||||
|
"friendsonline",
|
||||||
|
"spectatingclient",
|
||||||
|
"statrangeanybitsset",
|
||||||
|
"keybinding",
|
||||||
|
"actionslotusable",
|
||||||
|
"hudfade",
|
||||||
|
"maxrecommendedplayers",
|
||||||
|
"acceptinginvite",
|
||||||
|
"gamehost",
|
||||||
|
"issplitscreen",
|
||||||
|
"issplitscreenhost",
|
||||||
|
"splitscreennum",
|
||||||
|
"iscinematicfinished",
|
||||||
|
"issquadleader",
|
||||||
|
"getsquadid",
|
||||||
|
"isartillery",
|
||||||
|
"getplayersquadid",
|
||||||
|
"hasfriends",
|
||||||
|
"haspendingfriends",
|
||||||
|
"hasinvites",
|
||||||
|
"squadinvitesent",
|
||||||
|
"isintermission",
|
||||||
|
"partyismissingmappack",
|
||||||
|
"partymissingmappackerror",
|
||||||
|
"anynewmappacks",
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(std::extent_v<decltype(g_expFunctionNames)> == static_cast<unsigned>(NUM_OPERATORS));
|
||||||
|
} // namespace T4
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
#include "LoaderMenuListT4.h"
|
||||||
|
|
||||||
|
#include "Game/T4/Menu/MenuConversionZoneStateT4.h"
|
||||||
|
#include "Game/T4/Menu/MenuConverterT4.h"
|
||||||
|
#include "ObjLoading.h"
|
||||||
|
#include "Parsing/Menu/MenuAssetZoneState.h"
|
||||||
|
#include "Parsing/Menu/MenuFileReader.h"
|
||||||
|
#include "Utils/Logging/Log.h"
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class MenuListLoader final : public AssetCreator<AssetMenuList>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MenuListLoader(MemoryManager& memory, ISearchPath& searchPath)
|
||||||
|
: m_memory(memory),
|
||||||
|
m_search_path(searchPath)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override
|
||||||
|
{
|
||||||
|
std::vector<menuDef_t*> menus;
|
||||||
|
AssetRegistration<AssetMenuList> registration(assetName);
|
||||||
|
|
||||||
|
auto& zoneState = context.GetZoneAssetCreationState<menu::MenuAssetZoneState>();
|
||||||
|
auto& conversionState = context.GetZoneAssetCreationState<MenuConversionZoneState>();
|
||||||
|
|
||||||
|
std::deque<std::string> 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<MenuList>();
|
||||||
|
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<menuDef_t*>& menus,
|
||||||
|
AssetRegistration<AssetMenuList>& 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<menuDef_t*>& menus,
|
||||||
|
AssetRegistration<AssetMenuList>& 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<XAssetInfo<menuDef_t>*> 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<menuDef_t>();
|
||||||
|
AssetRegistration<AssetMenu> 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<menuDef_t*>& menus) const
|
||||||
|
{
|
||||||
|
menuList.menuCount = static_cast<int>(menus.size());
|
||||||
|
|
||||||
|
if (menuList.menuCount > 0)
|
||||||
|
{
|
||||||
|
menuList.menus = m_memory.Alloc<menuDef_t*>(menuList.menuCount);
|
||||||
|
for (auto i = 0; i < menuList.menuCount; i++)
|
||||||
|
menuList.menus[i] = menus[i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
menuList.menus = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<menu::ParsingResult>
|
||||||
|
ParseMenuFile(std::istream& stream, const std::string& menuFileName, const menu::MenuAssetZoneState& zoneState) const
|
||||||
|
{
|
||||||
|
menu::MenuFileReader reader(stream, menuFileName, menu::FeatureLevel::T4, m_search_path);
|
||||||
|
|
||||||
|
reader.IncludeZoneState(zoneState);
|
||||||
|
reader.SetPermissiveMode(ObjLoading::Configuration.MenuPermissiveParsing);
|
||||||
|
|
||||||
|
return reader.ReadMenuFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryManager& m_memory;
|
||||||
|
ISearchPath& m_search_path;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
std::unique_ptr<AssetCreator<AssetMenuList>> CreateMenuListLoaderT4(MemoryManager& memory, ISearchPath& searchPath)
|
||||||
|
{
|
||||||
|
return std::make_unique<MenuListLoader>(memory, searchPath);
|
||||||
|
}
|
||||||
|
} // namespace menu
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Asset/IAssetCreator.h"
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
#include "SearchPath/ISearchPath.h"
|
||||||
|
#include "Utils/MemoryManager.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
std::unique_ptr<AssetCreator<T4::AssetMenuList>> CreateMenuListLoaderT4(MemoryManager& memory, ISearchPath& searchPath);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include "MenuConversionZoneStateT4.h"
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
|
||||||
|
void MenuConversionZoneState::AddLoadedFile(std::string loadedFileName, std::vector<XAssetInfo<menuDef_t>*> menusOfFile)
|
||||||
|
{
|
||||||
|
m_menus_by_filename.emplace(std::move(loadedFileName), std::move(menusOfFile));
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Asset/IZoneAssetCreationState.h"
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace T4
|
||||||
|
{
|
||||||
|
class MenuConversionZoneState final : public IZoneAssetCreationState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void AddLoadedFile(std::string loadedFileName, std::vector<XAssetInfo<menuDef_t>*> menusOfFile);
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::vector<XAssetInfo<menuDef_t>*>> m_menus_by_filename;
|
||||||
|
};
|
||||||
|
} // namespace T4
|
||||||
@@ -0,0 +1,735 @@
|
|||||||
|
#include "MenuConverterT4.h"
|
||||||
|
|
||||||
|
#include "Menu/AbstractMenuConverter.h"
|
||||||
|
#include "Parsing/Menu/Domain/EventHandler/CommonEventHandlerScript.h"
|
||||||
|
#include "Parsing/Menu/Domain/Expression/CommonExpressionBaseFunctionCall.h"
|
||||||
|
#include "Parsing/Menu/Domain/Expression/CommonExpressionCustomFunctionCall.h"
|
||||||
|
#include "Parsing/Simple/Expression/SimpleExpressionBinaryOperation.h"
|
||||||
|
#include "Parsing/Simple/Expression/SimpleExpressionConditionalOperator.h"
|
||||||
|
#include "Parsing/Simple/Expression/SimpleExpressionUnaryOperation.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstring>
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
using namespace menu;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class MenuConverter final : public AbstractMenuConverter, public IMenuConverter
|
||||||
|
{
|
||||||
|
[[nodiscard]] static rectDef_s ConvertRectDef(const CommonRect& rect)
|
||||||
|
{
|
||||||
|
return rectDef_s{
|
||||||
|
.x = static_cast<float>(rect.x),
|
||||||
|
.y = static_cast<float>(rect.y),
|
||||||
|
.w = static_cast<float>(rect.w),
|
||||||
|
.h = static_cast<float>(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<float>(rect.w),
|
||||||
|
.h = static_cast<float>(rect.h),
|
||||||
|
.horzAlign = rect.horzAlign,
|
||||||
|
.vertAlign = rect.vertAlign,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ConvertColor(float (&output)[4], const CommonColor& input)
|
||||||
|
{
|
||||||
|
output[0] = static_cast<float>(input.r);
|
||||||
|
output[1] = static_cast<float>(input.g);
|
||||||
|
output[2] = static_cast<float>(input.b);
|
||||||
|
output[3] = static_cast<float>(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<AssetMaterial>(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<AssetSound>(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<unsigned>(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<unsigned>(SimpleBinaryOperationId::COUNT));
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsOperation(const ISimpleExpression* expression) const
|
||||||
|
{
|
||||||
|
if (!m_disable_optimizations && expression->IsStatic())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return dynamic_cast<const SimpleExpressionBinaryOperation*>(expression) != nullptr
|
||||||
|
|| dynamic_cast<const SimpleExpressionUnaryOperation*>(expression) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConvertExpressionEntryBaseFunctionCall(std::vector<expressionEntry>& entries,
|
||||||
|
const CommonExpressionBaseFunctionCall* baseFunction,
|
||||||
|
const CommonMenuDef* menu,
|
||||||
|
const CommonItemDef* item) const
|
||||||
|
{
|
||||||
|
entries.emplace_back(expressionEntry{.type = EET_OPERATOR, .data = {.op = static_cast<operationEnum>(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<expressionEntry>& entries,
|
||||||
|
const SimpleExpressionUnaryOperation* unary,
|
||||||
|
const CommonMenuDef* menu,
|
||||||
|
const CommonItemDef* item) const
|
||||||
|
{
|
||||||
|
assert(static_cast<unsigned>(unary->m_operation_type->m_id) < static_cast<unsigned>(SimpleUnaryOperationId::COUNT));
|
||||||
|
|
||||||
|
entries.emplace_back(expressionEntry{
|
||||||
|
.type = EET_OPERATOR,
|
||||||
|
.data = {.op = UNARY_OPERATION_MAPPING[static_cast<unsigned>(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<expressionEntry>& 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<unsigned>(binary->m_operation_type->m_id) < static_cast<unsigned>(SimpleBinaryOperationId::COUNT));
|
||||||
|
entries.emplace_back(expressionEntry{
|
||||||
|
.type = EET_OPERATOR,
|
||||||
|
.data = {.op = BINARY_OPERATION_MAPPING[static_cast<unsigned>(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<expressionEntry>& 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<float>(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<expressionEntry>& 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<const SimpleExpressionValue*>(expression))
|
||||||
|
{
|
||||||
|
ConvertExpressionValue(entries, *value);
|
||||||
|
}
|
||||||
|
else if (const auto* binary = dynamic_cast<const SimpleExpressionBinaryOperation*>(expression))
|
||||||
|
{
|
||||||
|
ConvertExpressionEntryBinaryOperation(entries, binary, menu, item);
|
||||||
|
}
|
||||||
|
else if (const auto* unary = dynamic_cast<const SimpleExpressionUnaryOperation*>(expression))
|
||||||
|
{
|
||||||
|
ConvertExpressionEntryUnaryOperation(entries, unary, menu, item);
|
||||||
|
}
|
||||||
|
else if (const auto* baseFunction = dynamic_cast<const CommonExpressionBaseFunctionCall*>(expression))
|
||||||
|
{
|
||||||
|
ConvertExpressionEntryBaseFunctionCall(entries, baseFunction, menu, item);
|
||||||
|
}
|
||||||
|
else if (dynamic_cast<const CommonExpressionCustomFunctionCall*>(expression))
|
||||||
|
{
|
||||||
|
throw MenuConversionException("T4 does not support custom menu functions", menu, item);
|
||||||
|
}
|
||||||
|
else if (dynamic_cast<const SimpleExpressionConditionalOperator*>(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<expressionEntry> entries;
|
||||||
|
ConvertExpressionEntry(entries, expression, menu, item);
|
||||||
|
|
||||||
|
statement.numEntries = static_cast<int>(entries.size());
|
||||||
|
statement.entries = m_memory.Alloc<expressionEntry*>(entries.size());
|
||||||
|
for (auto i = 0u; i < entries.size(); i++)
|
||||||
|
{
|
||||||
|
statement.entries[i] = m_memory.Alloc<expressionEntry>();
|
||||||
|
*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<float>(value.m_int_value);
|
||||||
|
else if (value.m_type == SimpleExpressionValue::Type::DOUBLE)
|
||||||
|
staticValue = static_cast<float>(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<const SimpleExpressionValue*>(expression);
|
||||||
|
isStatic = staticValue != nullptr;
|
||||||
|
isTruthy = isStatic && (staticValue->m_type == SimpleExpressionValue::Type::INT || staticValue->m_type == SimpleExpressionValue::Type::DOUBLE)
|
||||||
|
&& staticValue->IsTruthy();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isStatic = expression->IsStatic();
|
||||||
|
isTruthy = isStatic && expression->EvaluateStatic().IsTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStatic)
|
||||||
|
{
|
||||||
|
if (isTruthy)
|
||||||
|
window.dynamicFlags[0] |= WINDOW_FLAG_VISIBLE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
window.dynamicFlags[0] |= WINDOW_FLAG_VISIBLE;
|
||||||
|
ConvertExpression(statement, expression, commonMenu, commonItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const char*
|
||||||
|
ConvertEventHandlerSet(const CommonEventHandlerSet* handlers, const CommonMenuDef* menu, const CommonItemDef* item = nullptr) const
|
||||||
|
{
|
||||||
|
if (!handlers)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
std::ostringstream ss;
|
||||||
|
for (const auto& element : handlers->m_elements)
|
||||||
|
{
|
||||||
|
if (element->GetType() != CommonEventHandlerElementType::SCRIPT)
|
||||||
|
throw MenuConversionException("T4 event handlers must compile to a script string", menu, item);
|
||||||
|
|
||||||
|
ss << dynamic_cast<const CommonEventHandlerScript*>(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<int, std::unique_ptr<CommonEventHandlerSet>>& keyHandlers,
|
||||||
|
const CommonMenuDef* menu,
|
||||||
|
const CommonItemDef* item = nullptr) const
|
||||||
|
{
|
||||||
|
if (keyHandlers.empty())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
const auto keyHandlerCount = keyHandlers.size();
|
||||||
|
auto* output = m_memory.Alloc<ItemKeyHandler>(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<std::string>& 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<listBoxDef_s>();
|
||||||
|
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<float>(commonListBox->m_element_width);
|
||||||
|
listBox->elementHeight = static_cast<float>(commonListBox->m_element_height);
|
||||||
|
item->special = static_cast<float>(commonListBox->m_feeder);
|
||||||
|
listBox->elementStyle = commonListBox->m_element_style;
|
||||||
|
listBox->onDoubleClick = ConvertEventHandlerSet(commonListBox->m_on_double_click.get(), &parentMenu, &commonItem);
|
||||||
|
item->onListboxSelectionChange = ConvertEventHandlerSet(commonListBox->m_on_selection_change.get(), &parentMenu, &commonItem);
|
||||||
|
ConvertColor(listBox->selectBorder, commonListBox->m_select_border);
|
||||||
|
ConvertColor(listBox->disableColor, commonItem.m_disable_color);
|
||||||
|
ConvertColor(listBox->focusColor, commonListBox->m_focus_color);
|
||||||
|
listBox->selectIcon = ConvertMaterial(commonListBox->m_select_icon, &parentMenu, &commonItem);
|
||||||
|
listBox->backgroundItemListbox = ConvertMaterial(commonListBox->m_background_item, &parentMenu, &commonItem);
|
||||||
|
listBox->highlightTexture = ConvertMaterial(commonListBox->m_highlight_texture, &parentMenu, &commonItem);
|
||||||
|
|
||||||
|
listBox->numColumns = static_cast<int>(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<editFieldDef_s>();
|
||||||
|
editField->defVal = static_cast<float>(commonEditField->m_def_val);
|
||||||
|
editField->minVal = static_cast<float>(commonEditField->m_min_val);
|
||||||
|
editField->maxVal = static_cast<float>(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<multiDef_s>();
|
||||||
|
multiValue->count = static_cast<int>(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<unsigned>(i))
|
||||||
|
multiValue->dvarStr[i] = ConvertString(commonMultiValue->m_string_values[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (commonMultiValue->m_double_values.size() > static_cast<unsigned>(i))
|
||||||
|
multiValue->dvarValue[i] = static_cast<float>(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<itemDef_s>();
|
||||||
|
|
||||||
|
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<float>(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<float>(commonItem.m_text_align_x);
|
||||||
|
item->textaligny = static_cast<float>(commonItem.m_text_align_y);
|
||||||
|
item->textscale = static_cast<float>(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<itemDef_s*>(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<int>(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<AssetMenu>& 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<float>(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<float>(commonMenu.m_fade_clamp);
|
||||||
|
menu.fadeCycle = commonMenu.m_fade_cycle;
|
||||||
|
menu.fadeAmount = static_cast<float>(commonMenu.m_fade_amount);
|
||||||
|
menu.fadeInAmount = static_cast<float>(commonMenu.m_fade_in_amount);
|
||||||
|
menu.blurRadius = static_cast<float>(commonMenu.m_blur_radius);
|
||||||
|
ApplyFlag(menu.window.staticFlags, commonMenu.m_legacy_split_screen_scale, WINDOW_FLAG_LEGACY_SPLIT_SCREEN_SCALE);
|
||||||
|
ApplyFlag(menu.window.staticFlags, commonMenu.m_hidden_during_scope, WINDOW_FLAG_HIDDEN_DURING_SCOPE);
|
||||||
|
ApplyFlag(menu.window.staticFlags, commonMenu.m_hidden_during_flashbang, WINDOW_FLAG_HIDDEN_DURING_FLASH_BANG);
|
||||||
|
ApplyFlag(menu.window.staticFlags, commonMenu.m_hidden_during_ui, WINDOW_FLAG_HIDDEN_DURING_UI);
|
||||||
|
menu.allowedBinding = ConvertString(commonMenu.m_allowed_binding);
|
||||||
|
ConvertVisibleExpression(menu.window, menu.visibleExp, commonMenu.m_visible_expression.get(), &commonMenu);
|
||||||
|
ConvertOrApplyStatement(menu.window.rect.x, menu.rectXExp, commonMenu.m_rect_x_exp.get(), &commonMenu);
|
||||||
|
ConvertOrApplyStatement(menu.window.rect.y, menu.rectYExp, commonMenu.m_rect_y_exp.get(), &commonMenu);
|
||||||
|
menu.onOpen = ConvertEventHandlerSet(commonMenu.m_on_open.get(), &commonMenu);
|
||||||
|
menu.onFocus = ConvertEventHandlerSet(commonMenu.m_on_focus.get(), &commonMenu);
|
||||||
|
menu.onClose = ConvertEventHandlerSet(commonMenu.m_on_close.get(), &commonMenu);
|
||||||
|
menu.onESC = ConvertEventHandlerSet(commonMenu.m_on_esc.get(), &commonMenu);
|
||||||
|
menu.onKey = ConvertKeyHandler(commonMenu.m_key_handlers, &commonMenu);
|
||||||
|
menu.items = ConvertMenuItems(commonMenu, menu, menu.itemCount);
|
||||||
|
}
|
||||||
|
catch (const MenuConversionException& e)
|
||||||
|
{
|
||||||
|
PrintConversionExceptionDetails(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::unique_ptr<IMenuConverter>
|
||||||
|
IMenuConverter::Create(const bool disableOptimizations, ISearchPath& searchPath, MemoryManager& memory, AssetCreationContext& context)
|
||||||
|
{
|
||||||
|
return std::make_unique<MenuConverter>(disableOptimizations, searchPath, memory, context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Asset/AssetCreationContext.h"
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
#include "Parsing/Menu/Domain/CommonMenuDef.h"
|
||||||
|
#include "SearchPath/ISearchPath.h"
|
||||||
|
#include "Utils/MemoryManager.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace T4
|
||||||
|
{
|
||||||
|
class IMenuConverter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IMenuConverter() = default;
|
||||||
|
virtual ~IMenuConverter() = default;
|
||||||
|
|
||||||
|
virtual bool ConvertMenu(const menu::CommonMenuDef& commonMenu, menuDef_t& menu, AssetRegistration<AssetMenu>& registration) = 0;
|
||||||
|
|
||||||
|
static std::unique_ptr<IMenuConverter> Create(bool disableOptimizations, ISearchPath& searchPath, MemoryManager& memory, AssetCreationContext& context);
|
||||||
|
};
|
||||||
|
} // namespace T4
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "Game/T4/Font/FontLoaderT4.h"
|
#include "Game/T4/Font/FontLoaderT4.h"
|
||||||
#include "Game/T4/Image/ImageLoaderEmbeddedT4.h"
|
#include "Game/T4/Image/ImageLoaderEmbeddedT4.h"
|
||||||
#include "Game/T4/Image/ImageLoaderExternalT4.h"
|
#include "Game/T4/Image/ImageLoaderExternalT4.h"
|
||||||
|
#include "Game/T4/Menu/LoaderMenuListT4.h"
|
||||||
#include "Game/T4/T4.h"
|
#include "Game/T4/T4.h"
|
||||||
#include "Game/T4/Weapon/AccuracyGraphLoaderT4.h"
|
#include "Game/T4/Weapon/AccuracyGraphLoaderT4.h"
|
||||||
#include "Game/T4/XAnim/XAnimLoaderT4.h"
|
#include "Game/T4/XAnim/XAnimLoaderT4.h"
|
||||||
@@ -104,6 +105,7 @@ namespace
|
|||||||
collection.AddAssetCreator(image::CreateLoaderExternalT4(memory, searchPath));
|
collection.AddAssetCreator(image::CreateLoaderExternalT4(memory, searchPath));
|
||||||
collection.AddAssetCreator(material::CreateLoaderT4(memory, searchPath));
|
collection.AddAssetCreator(material::CreateLoaderT4(memory, searchPath));
|
||||||
collection.AddAssetCreator(font::CreateLoaderT4(memory, searchPath));
|
collection.AddAssetCreator(font::CreateLoaderT4(memory, searchPath));
|
||||||
|
collection.AddAssetCreator(menu::CreateMenuListLoaderT4(memory, searchPath));
|
||||||
collection.AddAssetCreator(localize::CreateLoaderT4(memory, searchPath, zone));
|
collection.AddAssetCreator(localize::CreateLoaderT4(memory, searchPath, zone));
|
||||||
collection.AddAssetCreator(map_ents::CreateLoaderT4(memory, searchPath));
|
collection.AddAssetCreator(map_ents::CreateLoaderT4(memory, searchPath));
|
||||||
collection.AddAssetCreator(phys_preset::CreateRawLoaderT4(memory, searchPath, zone));
|
collection.AddAssetCreator(phys_preset::CreateRawLoaderT4(memory, searchPath, zone));
|
||||||
|
|||||||
@@ -43,9 +43,13 @@ namespace menu
|
|||||||
double m_feeder = 0;
|
double m_feeder = 0;
|
||||||
int m_element_style = 0;
|
int m_element_style = 0;
|
||||||
CommonColor m_select_border;
|
CommonColor m_select_border;
|
||||||
|
CommonColor m_focus_color;
|
||||||
std::string m_select_icon;
|
std::string m_select_icon;
|
||||||
|
std::string m_background_item;
|
||||||
|
std::string m_highlight_texture;
|
||||||
|
|
||||||
std::unique_ptr<CommonEventHandlerSet> m_on_double_click;
|
std::unique_ptr<CommonEventHandlerSet> m_on_double_click;
|
||||||
|
std::unique_ptr<CommonEventHandlerSet> m_on_selection_change;
|
||||||
std::unique_ptr<ISimpleExpression> m_element_height_expression;
|
std::unique_ptr<ISimpleExpression> m_element_height_expression;
|
||||||
std::vector<Column> m_columns;
|
std::vector<Column> m_columns;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace menu
|
|||||||
std::unique_ptr<ISimpleExpression> m_open_sound_exp;
|
std::unique_ptr<ISimpleExpression> m_open_sound_exp;
|
||||||
std::unique_ptr<ISimpleExpression> m_close_sound_exp;
|
std::unique_ptr<ISimpleExpression> m_close_sound_exp;
|
||||||
std::unique_ptr<CommonEventHandlerSet> m_on_open;
|
std::unique_ptr<CommonEventHandlerSet> m_on_open;
|
||||||
|
std::unique_ptr<CommonEventHandlerSet> m_on_focus;
|
||||||
std::unique_ptr<CommonEventHandlerSet> m_on_close;
|
std::unique_ptr<CommonEventHandlerSet> m_on_close;
|
||||||
std::unique_ptr<CommonEventHandlerSet> m_on_request_close;
|
std::unique_ptr<CommonEventHandlerSet> m_on_request_close;
|
||||||
std::unique_ptr<CommonEventHandlerSet> m_on_esc;
|
std::unique_ptr<CommonEventHandlerSet> m_on_esc;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace menu
|
|||||||
{
|
{
|
||||||
IW3,
|
IW3,
|
||||||
IW4,
|
IW4,
|
||||||
IW5
|
IW5,
|
||||||
|
T4
|
||||||
};
|
};
|
||||||
} // namespace menu
|
} // namespace menu
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "Game/IW4/MenuConstantsIW4.h"
|
#include "Game/IW4/MenuConstantsIW4.h"
|
||||||
#include "Game/IW5/IW5.h"
|
#include "Game/IW5/IW5.h"
|
||||||
#include "Game/IW5/MenuConstantsIW5.h"
|
#include "Game/IW5/MenuConstantsIW5.h"
|
||||||
|
#include "Game/T4/MenuConstantsT4.h"
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
#include "MenuMatcherFactory.h"
|
#include "MenuMatcherFactory.h"
|
||||||
#include "Parsing/Menu/Domain/Expression/CommonExpressionBaseFunctionCall.h"
|
#include "Parsing/Menu/Domain/Expression/CommonExpressionBaseFunctionCall.h"
|
||||||
#include "Parsing/Menu/Domain/Expression/CommonExpressionCustomFunctionCall.h"
|
#include "Parsing/Menu/Domain/Expression/CommonExpressionCustomFunctionCall.h"
|
||||||
@@ -70,6 +72,24 @@ const std::unordered_map<std::string, size_t>& MenuExpressionMatchers::GetBaseFu
|
|||||||
return iw3FunctionMap;
|
return iw3FunctionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (featureLevel == FeatureLevel::T4)
|
||||||
|
{
|
||||||
|
static std::unordered_map<std::string, size_t> t4FunctionMap;
|
||||||
|
static bool t4FunctionMapInitialized = false;
|
||||||
|
|
||||||
|
if (!t4FunctionMapInitialized)
|
||||||
|
{
|
||||||
|
for (size_t i = T4::OP_FIRSTFUNCTIONCALL; i < std::extent_v<decltype(T4::g_expFunctionNames)>; i++)
|
||||||
|
{
|
||||||
|
std::string functionName(T4::g_expFunctionNames[i]);
|
||||||
|
utils::MakeStringLowerCase(functionName);
|
||||||
|
t4FunctionMap.emplace(std::make_pair(std::move(functionName), i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t4FunctionMap;
|
||||||
|
}
|
||||||
|
|
||||||
if (featureLevel == FeatureLevel::IW4)
|
if (featureLevel == FeatureLevel::IW4)
|
||||||
{
|
{
|
||||||
static std::unordered_map<std::string, size_t> iw4FunctionMap;
|
static std::unordered_map<std::string, size_t> iw4FunctionMap;
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ void MenuFileReader::SetupDefinesProxy()
|
|||||||
case FeatureLevel::IW5:
|
case FeatureLevel::IW5:
|
||||||
defines->AddDefine(DefinesStreamProxy::Define("FEATURE_LEVEL_IW5", "1"));
|
defines->AddDefine(DefinesStreamProxy::Define("FEATURE_LEVEL_IW5", "1"));
|
||||||
break;
|
break;
|
||||||
|
case FeatureLevel::T4:
|
||||||
|
defines->AddDefine(DefinesStreamProxy::Define("FEATURE_LEVEL_T4", "1"));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
assert(false);
|
assert(false);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -929,8 +929,8 @@ void EventHandlerSetScopeSequences::AddSequences(const FeatureLevel featureLevel
|
|||||||
{
|
{
|
||||||
AddSequence(std::make_unique<SequenceSkipEmptyStatements>());
|
AddSequence(std::make_unique<SequenceSkipEmptyStatements>());
|
||||||
|
|
||||||
// IW3 stores event handlers as opaque script strings rather than structured handlers.
|
// IW3 and T4 store event handlers as opaque script strings rather than structured handlers.
|
||||||
if (featureLevel == FeatureLevel::IW3)
|
if (featureLevel == FeatureLevel::IW3 || featureLevel == FeatureLevel::T4)
|
||||||
{
|
{
|
||||||
AddSequence(std::make_unique<SequenceCloseBlock>());
|
AddSequence(std::make_unique<SequenceCloseBlock>());
|
||||||
AddSequence(std::make_unique<SequenceSkipScriptToken>());
|
AddSequence(std::make_unique<SequenceSkipScriptToken>());
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ GlobalScopeSequences::GlobalScopeSequences(std::vector<std::unique_ptr<MenuFileP
|
|||||||
void GlobalScopeSequences::AddSequences(const FeatureLevel featureLevel, const bool permissive) const
|
void GlobalScopeSequences::AddSequences(const FeatureLevel featureLevel, const bool permissive) const
|
||||||
{
|
{
|
||||||
AddSequence(std::make_unique<SequenceCloseBlock>());
|
AddSequence(std::make_unique<SequenceCloseBlock>());
|
||||||
if (featureLevel != FeatureLevel::IW3)
|
if (featureLevel != FeatureLevel::IW3 && featureLevel != FeatureLevel::T4)
|
||||||
AddSequence(std::make_unique<SequenceFunctionDef>());
|
AddSequence(std::make_unique<SequenceFunctionDef>());
|
||||||
AddSequence(std::make_unique<SequenceMenuDef>());
|
AddSequence(std::make_unique<SequenceMenuDef>());
|
||||||
AddSequence(std::make_unique<SequenceLoadMenu>());
|
AddSequence(std::make_unique<SequenceLoadMenu>());
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace
|
|||||||
|
|
||||||
class ItemScopeOperations
|
class ItemScopeOperations
|
||||||
{
|
{
|
||||||
inline static const CommonItemFeatureType IW3_IW4_FEATURE_TYPE_BY_TYPE[0x18]{
|
inline static const CommonItemFeatureType IW3_IW4_T4_FEATURE_TYPE_BY_TYPE[0x18]{
|
||||||
CommonItemFeatureType::EDIT_FIELD, // ITEM_TYPE_TEXT
|
CommonItemFeatureType::EDIT_FIELD, // ITEM_TYPE_TEXT
|
||||||
CommonItemFeatureType::NONE, // ITEM_TYPE_BUTTON
|
CommonItemFeatureType::NONE, // ITEM_TYPE_BUTTON
|
||||||
CommonItemFeatureType::NONE, // ITEM_TYPE_RADIOBUTTON
|
CommonItemFeatureType::NONE, // ITEM_TYPE_RADIOBUTTON
|
||||||
@@ -84,9 +84,10 @@ namespace
|
|||||||
{
|
{
|
||||||
case FeatureLevel::IW3:
|
case FeatureLevel::IW3:
|
||||||
case FeatureLevel::IW4:
|
case FeatureLevel::IW4:
|
||||||
if (static_cast<unsigned>(type) >= std::extent_v<decltype(IW3_IW4_FEATURE_TYPE_BY_TYPE)>)
|
case FeatureLevel::T4:
|
||||||
|
if (static_cast<unsigned>(type) >= std::extent_v<decltype(IW3_IW4_T4_FEATURE_TYPE_BY_TYPE)>)
|
||||||
throw ParsingException(pos, "Invalid item type");
|
throw ParsingException(pos, "Invalid item type");
|
||||||
return IW3_IW4_FEATURE_TYPE_BY_TYPE[static_cast<unsigned>(type)];
|
return IW3_IW4_T4_FEATURE_TYPE_BY_TYPE[static_cast<unsigned>(type)];
|
||||||
|
|
||||||
case FeatureLevel::IW5:
|
case FeatureLevel::IW5:
|
||||||
default:
|
default:
|
||||||
@@ -464,8 +465,8 @@ namespace
|
|||||||
static constexpr auto CAPTURE_FIRST_TOKEN = 1;
|
static constexpr auto CAPTURE_FIRST_TOKEN = 1;
|
||||||
static constexpr auto CAPTURE_COLUMN_COUNT = 2;
|
static constexpr auto CAPTURE_COLUMN_COUNT = 2;
|
||||||
|
|
||||||
static constexpr const char* SYNTAX_IW3_IW4 = "<xpos> <width> <maxChars> [alignment]";
|
static constexpr const char* SYNTAX_IW3_IW4_T4 = "<xpos> <width> <maxChars> [alignment]";
|
||||||
static constexpr auto BASE_COLUMN_COUNT_IW3_IW4 = 3;
|
static constexpr auto BASE_COLUMN_COUNT_IW3_IW4_T4 = 3;
|
||||||
|
|
||||||
static constexpr const char* SYNTAX_IW5 = "<xpos> <ypos> <width> <height> <maxChars> [alignment]";
|
static constexpr const char* SYNTAX_IW5 = "<xpos> <ypos> <width> <height> <maxChars> [alignment]";
|
||||||
static constexpr auto BASE_COLUMN_COUNT_IW5 = 5;
|
static constexpr auto BASE_COLUMN_COUNT_IW5 = 5;
|
||||||
@@ -487,7 +488,8 @@ namespace
|
|||||||
void ProcessMatch(MenuFileParserState* state, SequenceResult<SimpleParserValue>& result) const override
|
void ProcessMatch(MenuFileParserState* state, SequenceResult<SimpleParserValue>& result) const override
|
||||||
{
|
{
|
||||||
assert(state->m_current_item);
|
assert(state->m_current_item);
|
||||||
assert(state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4 || state->m_feature_level == FeatureLevel::IW5);
|
assert(state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4 || state->m_feature_level == FeatureLevel::IW5
|
||||||
|
|| state->m_feature_level == FeatureLevel::T4);
|
||||||
|
|
||||||
const auto& firstToken = result.NextCapture(CAPTURE_FIRST_TOKEN);
|
const auto& firstToken = result.NextCapture(CAPTURE_FIRST_TOKEN);
|
||||||
ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, firstToken.GetPos());
|
ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, firstToken.GetPos());
|
||||||
@@ -500,10 +502,10 @@ namespace
|
|||||||
size_t baseColumnCount;
|
size_t baseColumnCount;
|
||||||
const char* syntax;
|
const char* syntax;
|
||||||
bool hasHeightValues;
|
bool hasHeightValues;
|
||||||
if (state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4)
|
if (state->m_feature_level == FeatureLevel::IW3 || state->m_feature_level == FeatureLevel::IW4 || state->m_feature_level == FeatureLevel::T4)
|
||||||
{
|
{
|
||||||
baseColumnCount = BASE_COLUMN_COUNT_IW3_IW4;
|
baseColumnCount = BASE_COLUMN_COUNT_IW3_IW4_T4;
|
||||||
syntax = SYNTAX_IW3_IW4;
|
syntax = SYNTAX_IW3_IW4_T4;
|
||||||
hasHeightValues = false;
|
hasHeightValues = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1143,6 +1145,35 @@ void ItemScopeSequences::AddSequences(const FeatureLevel featureLevel, const boo
|
|||||||
state->m_current_item->m_list_box_features->m_select_icon = value;
|
state->m_current_item->m_list_box_features->m_select_icon = value;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (featureLevel == FeatureLevel::T4)
|
||||||
|
{
|
||||||
|
AddSequence(std::make_unique<GenericMenuEventHandlerSetPropertySequence>(
|
||||||
|
"onListboxSelectionChange",
|
||||||
|
[](const MenuFileParserState* state, const TokenPos& pos) -> std::unique_ptr<CommonEventHandlerSet>&
|
||||||
|
{
|
||||||
|
ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos);
|
||||||
|
return state->m_current_item->m_list_box_features->m_on_selection_change;
|
||||||
|
}));
|
||||||
|
AddSequence(std::make_unique<GenericColorPropertySequence>("focusColor",
|
||||||
|
[](const MenuFileParserState* state, const TokenPos& pos, const CommonColor value)
|
||||||
|
{
|
||||||
|
ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos);
|
||||||
|
state->m_current_item->m_list_box_features->m_focus_color = value;
|
||||||
|
}));
|
||||||
|
AddSequence(std::make_unique<GenericStringPropertySequence>("backgroundItemListbox",
|
||||||
|
[](const MenuFileParserState* state, const TokenPos& pos, const std::string& value)
|
||||||
|
{
|
||||||
|
ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos);
|
||||||
|
state->m_current_item->m_list_box_features->m_background_item = value;
|
||||||
|
}));
|
||||||
|
AddSequence(std::make_unique<GenericStringPropertySequence>("highlightTexture",
|
||||||
|
[](const MenuFileParserState* state, const TokenPos& pos, const std::string& value)
|
||||||
|
{
|
||||||
|
ItemScopeOperations::EnsureHasListboxFeatures(*state->m_current_item, pos);
|
||||||
|
state->m_current_item->m_list_box_features->m_highlight_texture = value;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (featureLevel == FeatureLevel::IW5)
|
if (featureLevel == FeatureLevel::IW5)
|
||||||
{
|
{
|
||||||
AddSequence(
|
AddSequence(
|
||||||
|
|||||||
@@ -261,6 +261,15 @@ void MenuScopeSequences::AddSequences(const FeatureLevel featureLevel, const boo
|
|||||||
{
|
{
|
||||||
return state->m_current_menu->m_on_open;
|
return state->m_current_menu->m_on_open;
|
||||||
}));
|
}));
|
||||||
|
if (featureLevel == FeatureLevel::T4)
|
||||||
|
{
|
||||||
|
AddSequence(std::make_unique<GenericMenuEventHandlerSetPropertySequence>(
|
||||||
|
"onFocus",
|
||||||
|
[](const MenuFileParserState* state, const TokenPos&) -> std::unique_ptr<CommonEventHandlerSet>&
|
||||||
|
{
|
||||||
|
return state->m_current_menu->m_on_focus;
|
||||||
|
}));
|
||||||
|
}
|
||||||
AddSequence(std::make_unique<GenericMenuEventHandlerSetPropertySequence>(
|
AddSequence(std::make_unique<GenericMenuEventHandlerSetPropertySequence>(
|
||||||
"onClose",
|
"onClose",
|
||||||
[](const MenuFileParserState* state, const TokenPos&) -> std::unique_ptr<CommonEventHandlerSet>&
|
[](const MenuFileParserState* state, const TokenPos&) -> std::unique_ptr<CommonEventHandlerSet>&
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#include "MenuDumperT4.h"
|
||||||
|
|
||||||
|
#include "MenuListDumperT4.h"
|
||||||
|
#include "MenuWriterT4.h"
|
||||||
|
#include "ObjWriting.h"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
|
||||||
|
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 std::format("ui_mp/{}.menu", asset.Asset()->window.name);
|
||||||
|
|
||||||
|
return menuDumpingState->second.m_path;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
void MenuDumperT4::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
|
||||||
|
const auto menuListAssets = context.m_zone.m_pools.PoolAssets<AssetMenuList>();
|
||||||
|
for (auto* menuListAsset : menuListAssets)
|
||||||
|
CreateDumpingStateForMenuListT4(zoneState, menuListAsset->Asset());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto menuFilePath = GetPathForMenu(zoneState, asset);
|
||||||
|
const auto assetFile = context.OpenAssetFile(menuFilePath);
|
||||||
|
|
||||||
|
if (!assetFile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto menuWriter = CreateMenuWriterT4(*assetFile);
|
||||||
|
|
||||||
|
menuWriter->Start();
|
||||||
|
menuWriter->WriteMenu(*menu);
|
||||||
|
menuWriter->End();
|
||||||
|
}
|
||||||
|
} // namespace menu
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Dumping/AbstractAssetDumper.h"
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
class MenuDumperT4 final : public AbstractAssetDumper<T4::AssetMenu>
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<T4::AssetMenu::Type>& asset) override;
|
||||||
|
};
|
||||||
|
} // namespace menu
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#include "MenuListDumperT4.h"
|
||||||
|
|
||||||
|
#include "MenuWriterT4.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void DumpMenus(menu::IWriterT4& menuDumper, menu::MenuDumpingZoneState* zoneState, const MenuList* menuList)
|
||||||
|
{
|
||||||
|
if (!menuList->menus)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (auto menuNum = 0; menuNum < menuList->menuCount; menuNum++)
|
||||||
|
{
|
||||||
|
const auto* menu = menuList->menus[menuNum];
|
||||||
|
if (!menu)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto menuDumpingState = zoneState->m_menu_dumping_state_map.find(menu);
|
||||||
|
if (menuDumpingState == zoneState->m_menu_dumping_state_map.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto* menuAssetName = menu->window.name;
|
||||||
|
const auto isReference = menuAssetName && menuAssetName[0] == ',';
|
||||||
|
|
||||||
|
// If the menu was embedded directly as menu list write its data in the menu list file
|
||||||
|
if (!isReference && menuDumpingState->second.m_alias_menu_list == menuList)
|
||||||
|
menuDumper.WriteMenu(*menu);
|
||||||
|
else
|
||||||
|
menuDumper.IncludeMenu(menuDumpingState->second.m_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PathForMenu(const std::string& menuListParentPath, const menuDef_t* menu)
|
||||||
|
{
|
||||||
|
const auto* menuAssetName = menu->window.name;
|
||||||
|
|
||||||
|
if (!menuAssetName)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (menuAssetName[0] == ',')
|
||||||
|
menuAssetName = &menuAssetName[1];
|
||||||
|
|
||||||
|
return std::format("{}{}.menu", menuListParentPath, menuAssetName);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
void CreateDumpingStateForMenuListT4(MenuDumpingZoneState* zoneState, const MenuList* menuList)
|
||||||
|
{
|
||||||
|
if (!menuList || menuList->menuCount <= 0 || !menuList->menus || !menuList->name)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::string menuListName(menuList->name);
|
||||||
|
const fs::path p(menuListName);
|
||||||
|
std::string parentPath;
|
||||||
|
if (p.has_parent_path())
|
||||||
|
parentPath = p.parent_path().generic_string() + "/";
|
||||||
|
|
||||||
|
for (auto i = 0; i < menuList->menuCount; i++)
|
||||||
|
{
|
||||||
|
const auto* menu = menuList->menus[i];
|
||||||
|
|
||||||
|
if (!menu)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto menuPath = PathForMenu(parentPath, menu);
|
||||||
|
if (menuPath.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto existingState = zoneState->m_menu_dumping_state_map.find(menu);
|
||||||
|
if (existingState == zoneState->m_menu_dumping_state_map.end())
|
||||||
|
{
|
||||||
|
const auto isTheSameAsMenuList = menuPath == menuListName;
|
||||||
|
zoneState->CreateMenuDumpingState(menu, std::move(menuPath), isTheSameAsMenuList ? menuList : nullptr);
|
||||||
|
}
|
||||||
|
else if (!existingState->second.m_alias_menu_list)
|
||||||
|
{
|
||||||
|
const auto isTheSameAsMenuList = menuPath == menuListName;
|
||||||
|
if (isTheSameAsMenuList)
|
||||||
|
{
|
||||||
|
existingState->second.m_alias_menu_list = menuList;
|
||||||
|
existingState->second.m_path = std::move(menuPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuListDumperT4::DumpAsset(AssetDumpingContext& context, const XAssetInfo<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 = CreateMenuWriterT4(*assetFile);
|
||||||
|
|
||||||
|
menuWriter->Start();
|
||||||
|
DumpMenus(*menuWriter, zoneState, menuList);
|
||||||
|
menuWriter->End();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuListDumperT4::Dump(AssetDumpingContext& context)
|
||||||
|
{
|
||||||
|
auto* zoneState = context.GetZoneAssetDumperState<MenuDumpingZoneState>();
|
||||||
|
|
||||||
|
const auto menuListAssets = context.m_zone.m_pools.PoolAssets<AssetMenuList>();
|
||||||
|
for (const auto* asset : menuListAssets)
|
||||||
|
CreateDumpingStateForMenuListT4(zoneState, asset->Asset());
|
||||||
|
|
||||||
|
AbstractAssetDumper::Dump(context);
|
||||||
|
}
|
||||||
|
} // namespace menu
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Dumping/AbstractAssetDumper.h"
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
#include "Menu/MenuDumpingZoneState.h"
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
void CreateDumpingStateForMenuListT4(MenuDumpingZoneState* zoneState, const T4::MenuList* menuList);
|
||||||
|
|
||||||
|
class MenuListDumperT4 final : public AbstractAssetDumper<T4::AssetMenuList>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void Dump(AssetDumpingContext& context) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<T4::AssetMenuList::Type>& asset) override;
|
||||||
|
};
|
||||||
|
} // namespace menu
|
||||||
@@ -0,0 +1,686 @@
|
|||||||
|
#include "MenuWriterT4.h"
|
||||||
|
|
||||||
|
#include "Game/T4/MenuConstantsT4.h"
|
||||||
|
#include "Menu/AbstractMenuWriter.h"
|
||||||
|
#include "ObjWriting.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Set this to true to skip interpretative expression dumping
|
||||||
|
constexpr auto DUMP_NAIVE = false;
|
||||||
|
|
||||||
|
size_t FindStatementClosingParenthesis(const statement_s& statement, const size_t openingParenthesisPosition)
|
||||||
|
{
|
||||||
|
assert(statement.numEntries >= 0);
|
||||||
|
assert(openingParenthesisPosition < static_cast<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::IWriterT4
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit MenuWriter(std::ostream& stream)
|
||||||
|
: AbstractBaseWriter(stream)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteMenu(const menuDef_t& menu) override
|
||||||
|
{
|
||||||
|
StartMenuDefScope();
|
||||||
|
WriteMenuData(menu);
|
||||||
|
EndScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start() override
|
||||||
|
{
|
||||||
|
AbstractBaseWriter::Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void End() override
|
||||||
|
{
|
||||||
|
AbstractBaseWriter::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
void IncludeMenu(const std::string& menuPath) const override
|
||||||
|
{
|
||||||
|
AbstractBaseWriter::IncludeMenu(menuPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool HasStatement(const statement_s& statement)
|
||||||
|
{
|
||||||
|
return statement.numEntries > 0 && statement.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteStatementNaive(const statement_s& statement) const
|
||||||
|
{
|
||||||
|
const auto entryCount = static_cast<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);
|
||||||
|
WriteScriptProperty("onListboxSelectionChange", item.onListboxSelectionChange);
|
||||||
|
WriteColorProperty("selectBorder", listBox.selectBorder, COLOR_0000);
|
||||||
|
WriteColorProperty("disableColor", listBox.disableColor, COLOR_0000);
|
||||||
|
WriteColorProperty("focusColor", listBox.focusColor, COLOR_0000);
|
||||||
|
WriteMaterialProperty("selectIcon", listBox.selectIcon);
|
||||||
|
WriteMaterialProperty("backgroundItemListbox", listBox.backgroundItemListbox);
|
||||||
|
WriteMaterialProperty("highlightTexture", listBox.highlightTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteDvarFloatProperty(const itemDef_s& item, const editFieldDef_s& editField) const
|
||||||
|
{
|
||||||
|
if (!item.dvar)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Indent();
|
||||||
|
WriteKey("dvarFloat");
|
||||||
|
WriteEscapedString(item.dvar);
|
||||||
|
m_stream << " " << editField.defVal << " " << editField.minVal << " " << editField.maxVal << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteEditFieldProperties(const itemDef_s& item) const
|
||||||
|
{
|
||||||
|
switch (item.type)
|
||||||
|
{
|
||||||
|
case ITEM_TYPE_TEXT:
|
||||||
|
case ITEM_TYPE_EDITFIELD:
|
||||||
|
case ITEM_TYPE_NUMERICFIELD:
|
||||||
|
case ITEM_TYPE_SLIDER:
|
||||||
|
case ITEM_TYPE_YESNO:
|
||||||
|
case ITEM_TYPE_BIND:
|
||||||
|
case ITEM_TYPE_VALIDFILEFIELD:
|
||||||
|
case ITEM_TYPE_DECIMALFIELD:
|
||||||
|
case ITEM_TYPE_UPREDITFIELD:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.typeData.editField)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& editField = *item.typeData.editField;
|
||||||
|
if (std::fabs(-1.0f - editField.defVal) >= std::numeric_limits<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
|
||||||
|
{
|
||||||
|
// T4 distinguishes explicitly empty text from null text, which falls back to the item's dvar.
|
||||||
|
if (!text)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Indent();
|
||||||
|
WriteKey("text");
|
||||||
|
WriteEscapedString(text);
|
||||||
|
m_stream << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteItemData(const itemDef_s& item)
|
||||||
|
{
|
||||||
|
WriteStringProperty("name", item.window.name);
|
||||||
|
WriteItemTextProperty(item.text);
|
||||||
|
WriteStringProperty("group", item.window.group);
|
||||||
|
WriteRectProperty("rect", item.window.rectClient);
|
||||||
|
WriteIntProperty("style", item.window.style, 0);
|
||||||
|
WriteKeywordProperty("decoration", item.window.staticFlags & WINDOW_FLAG_DECORATION);
|
||||||
|
WriteKeywordProperty("autowrapped", item.window.staticFlags & WINDOW_FLAG_AUTO_WRAPPED);
|
||||||
|
WriteKeywordProperty("horizontalscroll", item.window.staticFlags & WINDOW_FLAG_HORIZONTAL_SCROLL);
|
||||||
|
WriteIntProperty("type", item.type, ITEM_TYPE_TEXT);
|
||||||
|
WriteIntProperty("border", item.window.border, 0);
|
||||||
|
WriteFloatProperty("borderSize", item.window.borderSize, 0.0f);
|
||||||
|
|
||||||
|
if (HasStatement(item.visibleExp))
|
||||||
|
WriteStatementProperty("visible", item.visibleExp, true);
|
||||||
|
else if (item.window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE)
|
||||||
|
WriteIntProperty("visible", 1, 0);
|
||||||
|
|
||||||
|
WriteIntProperty("ownerdraw", item.window.ownerDraw, 0);
|
||||||
|
WriteFlagsProperty("ownerdrawFlag", item.window.ownerDrawFlags);
|
||||||
|
WriteIntProperty("align", item.alignment, 0);
|
||||||
|
WriteIntProperty("textalign", item.textAlignMode, 0);
|
||||||
|
WriteFloatProperty("textalignx", item.textalignx, 0.0f);
|
||||||
|
WriteFloatProperty("textaligny", item.textaligny, 0.0f);
|
||||||
|
WriteFloatProperty("textscale", item.textscale, 0.0f);
|
||||||
|
WriteIntProperty("textstyle", item.textStyle, 0);
|
||||||
|
WriteIntProperty("textfont", item.fontEnum, 0);
|
||||||
|
WriteColorProperty("backcolor", item.window.backColor, COLOR_0000);
|
||||||
|
WriteColorProperty("forecolor", item.window.foreColor, COLOR_1111);
|
||||||
|
WriteColorProperty("bordercolor", item.window.borderColor, COLOR_0000);
|
||||||
|
WriteColorProperty("outlinecolor", item.window.outlineColor, COLOR_0000);
|
||||||
|
WriteMaterialProperty("background", item.window.background);
|
||||||
|
WriteScriptProperty("onFocus", item.onFocus);
|
||||||
|
WriteScriptProperty("leaveFocus", item.leaveFocus);
|
||||||
|
WriteScriptProperty("mouseEnter", item.mouseEnter);
|
||||||
|
WriteScriptProperty("mouseExit", item.mouseExit);
|
||||||
|
WriteScriptProperty("mouseEnterText", item.mouseEnterText);
|
||||||
|
WriteScriptProperty("mouseExitText", item.mouseExitText);
|
||||||
|
WriteScriptProperty("action", item.action);
|
||||||
|
WriteScriptProperty("accept", item.onAccept);
|
||||||
|
WriteSoundAliasProperty("focusSound", item.focusSound);
|
||||||
|
WriteStringProperty("dvarTest", item.dvarTest);
|
||||||
|
|
||||||
|
if (item.dvarFlags & ITEM_DVAR_FLAG_ENABLE)
|
||||||
|
WriteMultiTokenStringProperty("enableDvar", item.enableDvar);
|
||||||
|
else if (item.dvarFlags & ITEM_DVAR_FLAG_DISABLE)
|
||||||
|
WriteMultiTokenStringProperty("disableDvar", item.enableDvar);
|
||||||
|
else if (item.dvarFlags & ITEM_DVAR_FLAG_SHOW)
|
||||||
|
WriteMultiTokenStringProperty("showDvar", item.enableDvar);
|
||||||
|
else if (item.dvarFlags & ITEM_DVAR_FLAG_HIDE)
|
||||||
|
WriteMultiTokenStringProperty("hideDvar", item.enableDvar);
|
||||||
|
else if (item.dvarFlags & ITEM_DVAR_FLAG_FOCUS)
|
||||||
|
WriteMultiTokenStringProperty("focusDvar", item.enableDvar);
|
||||||
|
|
||||||
|
WriteItemKeyHandlers(item.onKey);
|
||||||
|
WriteStatementProperty("exp text", item.textExp, false);
|
||||||
|
WriteStatementProperty("exp material", item.materialExp, false);
|
||||||
|
WriteStatementProperty("exp rect X", item.rectXExp, false);
|
||||||
|
WriteStatementProperty("exp rect Y", item.rectYExp, false);
|
||||||
|
WriteStatementProperty("exp rect W", item.rectWExp, false);
|
||||||
|
WriteStatementProperty("exp rect H", item.rectHExp, false);
|
||||||
|
WriteStatementProperty("exp forecolor A", item.forecolorAExp, false);
|
||||||
|
WriteIntProperty("gamemsgwindowindex", item.gameMsgWindowIndex, 0);
|
||||||
|
WriteIntProperty("gamemsgwindowmode", item.gameMsgWindowMode, 0);
|
||||||
|
|
||||||
|
WriteListBoxProperties(item);
|
||||||
|
WriteEditFieldProperties(item);
|
||||||
|
WriteMultiProperties(item);
|
||||||
|
WriteEnumDvarProperties(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteItemDefs(const itemDef_s* const* items, const size_t itemCount)
|
||||||
|
{
|
||||||
|
if (!items || itemCount <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (size_t itemIndex = 0u; itemIndex < itemCount; itemIndex++)
|
||||||
|
{
|
||||||
|
const auto* item = items[itemIndex];
|
||||||
|
if (!item)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
StartItemDefScope();
|
||||||
|
WriteItemData(*item);
|
||||||
|
EndScope();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteMenuData(const menuDef_t& menu)
|
||||||
|
{
|
||||||
|
WriteStringProperty("name", menu.window.name);
|
||||||
|
WriteBoolProperty("fullscreen", menu.fullScreen != 0, false);
|
||||||
|
WriteKeywordProperty("decoration", menu.window.staticFlags & WINDOW_FLAG_DECORATION);
|
||||||
|
WriteRectProperty("rect", menu.window.rect);
|
||||||
|
WriteIntProperty("style", menu.window.style, 0);
|
||||||
|
WriteIntProperty("border", menu.window.border, 0);
|
||||||
|
WriteFloatProperty("borderSize", menu.window.borderSize, 0.0f);
|
||||||
|
WriteColorProperty("backcolor", menu.window.backColor, COLOR_0000);
|
||||||
|
WriteColorProperty("forecolor", menu.window.foreColor, COLOR_1111);
|
||||||
|
WriteColorProperty("bordercolor", menu.window.borderColor, COLOR_0000);
|
||||||
|
WriteColorProperty("focuscolor", menu.focusColor, COLOR_0000);
|
||||||
|
WriteColorProperty("disablecolor", menu.disableColor, COLOR_0000);
|
||||||
|
WriteColorProperty("outlinecolor", menu.window.outlineColor, COLOR_0000);
|
||||||
|
WriteMaterialProperty("background", menu.window.background);
|
||||||
|
WriteIntProperty("ownerdraw", menu.window.ownerDraw, 0);
|
||||||
|
WriteFlagsProperty("ownerdrawFlag", menu.window.ownerDrawFlags);
|
||||||
|
WriteKeywordProperty("outOfBoundsClick", menu.window.staticFlags & WINDOW_FLAG_OUT_OF_BOUNDS_CLICK);
|
||||||
|
WriteStringProperty("soundLoop", menu.soundName);
|
||||||
|
WriteKeywordProperty("popup", menu.window.staticFlags & WINDOW_FLAG_POPUP);
|
||||||
|
WriteFloatProperty("fadeClamp", menu.fadeClamp, 0.0f);
|
||||||
|
WriteIntProperty("fadeCycle", menu.fadeCycle, 0);
|
||||||
|
WriteFloatProperty("fadeAmount", menu.fadeAmount, 0.0f);
|
||||||
|
WriteFloatProperty("fadeInAmount", menu.fadeInAmount, 0.0f);
|
||||||
|
WriteFloatProperty("blurWorld", menu.blurRadius, 0.0f);
|
||||||
|
WriteKeywordProperty("legacySplitScreenScale", menu.window.staticFlags & WINDOW_FLAG_LEGACY_SPLIT_SCREEN_SCALE);
|
||||||
|
WriteKeywordProperty("hiddenDuringScope", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_SCOPE);
|
||||||
|
WriteKeywordProperty("hiddenDuringFlashbang", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_FLASH_BANG);
|
||||||
|
WriteKeywordProperty("hiddenDuringUI", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_UI);
|
||||||
|
WriteStringProperty("allowedBinding", menu.allowedBinding);
|
||||||
|
|
||||||
|
if (HasStatement(menu.visibleExp))
|
||||||
|
WriteStatementProperty("visible", menu.visibleExp, true);
|
||||||
|
else if (menu.window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE)
|
||||||
|
WriteIntProperty("visible", 1, 0);
|
||||||
|
|
||||||
|
WriteStatementProperty("exp rect X", menu.rectXExp, false);
|
||||||
|
WriteStatementProperty("exp rect Y", menu.rectYExp, false);
|
||||||
|
WriteScriptProperty("onOpen", menu.onOpen);
|
||||||
|
WriteScriptProperty("onFocus", menu.onFocus);
|
||||||
|
WriteScriptProperty("onClose", menu.onClose);
|
||||||
|
WriteScriptProperty("onESC", menu.onESC);
|
||||||
|
WriteItemKeyHandlers(menu.onKey);
|
||||||
|
WriteItemDefs(menu.items, menu.itemCount);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
std::unique_ptr<IWriterT4> CreateMenuWriterT4(std::ostream& stream)
|
||||||
|
{
|
||||||
|
return std::make_unique<MenuWriter>(stream);
|
||||||
|
}
|
||||||
|
} // namespace menu
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
#include "Menu/IMenuWriter.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <ostream>
|
||||||
|
|
||||||
|
namespace menu
|
||||||
|
{
|
||||||
|
class IWriterT4 : public IWriter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void WriteMenu(const T4::menuDef_t& menu) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<IWriterT4> CreateMenuWriterT4(std::ostream& stream);
|
||||||
|
} // namespace menu
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "Game/T4/XAnim/XAnimDumperT4.h"
|
#include "Game/T4/XAnim/XAnimDumperT4.h"
|
||||||
#include "Game/T4/XModel/XModelDumperT4.h"
|
#include "Game/T4/XModel/XModelDumperT4.h"
|
||||||
#include "Localize/LocalizeDumperT4.h"
|
#include "Localize/LocalizeDumperT4.h"
|
||||||
|
#include "Menu/MenuDumperT4.h"
|
||||||
|
#include "Menu/MenuListDumperT4.h"
|
||||||
#include "PhysConstraints/PhysConstraintsInfoStringDumperT4.h"
|
#include "PhysConstraints/PhysConstraintsInfoStringDumperT4.h"
|
||||||
#include "PhysPreset/PhysPresetInfoStringDumperT4.h"
|
#include "PhysPreset/PhysPresetInfoStringDumperT4.h"
|
||||||
#include "RawFile/RawFileDumperT4.h"
|
#include "RawFile/RawFileDumperT4.h"
|
||||||
@@ -27,6 +29,8 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
|
|||||||
RegisterAssetDumper(std::make_unique<sound::LoadedSoundDumperT4>());
|
RegisterAssetDumper(std::make_unique<sound::LoadedSoundDumperT4>());
|
||||||
RegisterAssetDumper(std::make_unique<map_ents::DumperT4>());
|
RegisterAssetDumper(std::make_unique<map_ents::DumperT4>());
|
||||||
RegisterAssetDumper(std::make_unique<font::JsonDumperT4>());
|
RegisterAssetDumper(std::make_unique<font::JsonDumperT4>());
|
||||||
|
RegisterAssetDumper(std::make_unique<menu::MenuListDumperT4>());
|
||||||
|
RegisterAssetDumper(std::make_unique<menu::MenuDumperT4>());
|
||||||
RegisterAssetDumper(std::make_unique<localize::DumperT4>());
|
RegisterAssetDumper(std::make_unique<localize::DumperT4>());
|
||||||
RegisterAssetDumper(std::make_unique<weapon::DumperT4>());
|
RegisterAssetDumper(std::make_unique<weapon::DumperT4>());
|
||||||
RegisterAssetDumper(std::make_unique<raw_file::DumperT4>());
|
RegisterAssetDumper(std::make_unique<raw_file::DumperT4>());
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ set condition expressionEntry::data::operand type == EET_OPERAND;
|
|||||||
use Operand;
|
use Operand;
|
||||||
set condition internals::intVal dataType == VAL_INT;
|
set condition internals::intVal dataType == VAL_INT;
|
||||||
set condition internals::floatVal dataType == VAL_FLOAT;
|
set condition internals::floatVal dataType == VAL_FLOAT;
|
||||||
set condition internals::string dataType == VAL_STRING;
|
set condition internals::stringVal dataType == VAL_STRING;
|
||||||
set string internals::string;
|
set string internals::stringVal;
|
||||||
|
|
||||||
// itemDef_s
|
// itemDef_s
|
||||||
use itemDef_s;
|
use itemDef_s;
|
||||||
@@ -72,7 +72,7 @@ set condition multi itemDef_s::type == ITEM_TYPE_MULTI;
|
|||||||
set condition enumDvarName itemDef_s::type == ITEM_TYPE_DVARENUM;
|
set condition enumDvarName itemDef_s::type == ITEM_TYPE_DVARENUM;
|
||||||
|
|
||||||
// listBoxDef_s
|
// listBoxDef_s
|
||||||
set string listBoxDef_s::doubleClick;
|
set string listBoxDef_s::onDoubleClick;
|
||||||
|
|
||||||
// multiDef_s
|
// multiDef_s
|
||||||
use multiDef_s;
|
use multiDef_s;
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
#include "Game/T4/Menu/LoaderMenuListT4.h"
|
||||||
|
|
||||||
|
#include "Game/T4/GameT4.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_approx.hpp>
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
using namespace std::literals;
|
||||||
|
using namespace Catch;
|
||||||
|
using namespace Catch::Matchers;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class MenuLoadingTestHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MenuLoadingTestHelper()
|
||||||
|
: m_zone("MockZone", 0, GameId::T4, GamePlatform::PC),
|
||||||
|
m_creator_collection(m_zone),
|
||||||
|
m_context(m_zone, &m_creator_collection, &m_ignored_asset_lookup)
|
||||||
|
{
|
||||||
|
m_loader = menu::CreateMenuListLoaderT4(m_zone.Memory(), m_search_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddFile(std::string name, std::string contents)
|
||||||
|
{
|
||||||
|
m_search_path.AddFileData(std::move(name), std::move(contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
Material* AddMaterial(const std::string& name)
|
||||||
|
{
|
||||||
|
auto* material = m_zone.Memory().Alloc<Material>();
|
||||||
|
material->info.name = m_zone.Memory().Dup(name.c_str());
|
||||||
|
m_context.AddAsset<AssetMaterial>(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<menuDef_t*>(asset->m_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Zone m_zone;
|
||||||
|
MockSearchPath m_search_path;
|
||||||
|
std::unique_ptr<IAssetCreator> m_loader;
|
||||||
|
AssetCreatorCollection m_creator_collection;
|
||||||
|
IgnoredAssetLookup m_ignored_asset_lookup;
|
||||||
|
AssetCreationContext m_context;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("MenuListLoaderT4: Loads dumped menus and preserves T4 data", "[t4][menu][assetloader]")
|
||||||
|
{
|
||||||
|
MenuLoadingTestHelper helper;
|
||||||
|
const auto* backgroundMaterial = helper.AddMaterial("background_material");
|
||||||
|
const auto* selectMaterial = helper.AddMaterial("select_material");
|
||||||
|
const auto* listboxBackgroundMaterial = helper.AddMaterial("listbox_background_material");
|
||||||
|
const auto* highlightMaterial = helper.AddMaterial("highlight_material");
|
||||||
|
|
||||||
|
helper.AddFile("ui_mp/menus.txt", R"MENU({
|
||||||
|
loadMenu { "ui_mp/main.menu" }
|
||||||
|
loadMenu { "ui_mp/popup.menu" }
|
||||||
|
}
|
||||||
|
)MENU");
|
||||||
|
helper.AddFile("ui_mp/main.menu", R"MENU({
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "main"
|
||||||
|
fullscreen 1
|
||||||
|
rect 0 0 640 480 0 0
|
||||||
|
background "background_material"
|
||||||
|
forecolor 0.8 0.7 0.6 0.5
|
||||||
|
disablecolor 0.2 0.3 0.4 0.5
|
||||||
|
visible when(dvarbool("ui_show_main"));
|
||||||
|
onOpen
|
||||||
|
{
|
||||||
|
play menu_open;
|
||||||
|
setLocalVarInt ui_highlight 5;
|
||||||
|
}
|
||||||
|
onFocus
|
||||||
|
{
|
||||||
|
setdvar ui_menu_focused 1;
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "empty_text"
|
||||||
|
text ""
|
||||||
|
rect 10 20 180 24 1 2
|
||||||
|
type 1
|
||||||
|
visible 1
|
||||||
|
forecolor 0.9 0.9 1 0.07
|
||||||
|
dvar "ui_fallback_text"
|
||||||
|
action
|
||||||
|
{
|
||||||
|
setItemColor empty_text borderColor 0.1 0.1 0.12 0.5;
|
||||||
|
execondvarstringvalue ui_zfeather 0 "set cg_laserLight 0";
|
||||||
|
}
|
||||||
|
exp material (localvarstring("ui_material"));
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "server_list"
|
||||||
|
rect 20 60 300 200 0 0
|
||||||
|
type 6
|
||||||
|
columns 2 0 120 32 0 120 180 48 1
|
||||||
|
elementwidth 300
|
||||||
|
elementheight 20
|
||||||
|
feeder 2
|
||||||
|
elementtype 1
|
||||||
|
notselectable
|
||||||
|
noscrollbars
|
||||||
|
usepaging
|
||||||
|
disablecolor 0.6 0.5 0.4 0.3
|
||||||
|
selectBorder 1 1 1 1
|
||||||
|
focusColor 0.1 0.2 0.3 0.4
|
||||||
|
selectIcon "select_material"
|
||||||
|
backgroundItemListbox "listbox_background_material"
|
||||||
|
highlightTexture "highlight_material"
|
||||||
|
doubleclick
|
||||||
|
{
|
||||||
|
play mouse_click;
|
||||||
|
}
|
||||||
|
onListboxSelectionChange
|
||||||
|
{
|
||||||
|
setdvar ui_server_selected 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "default_text"
|
||||||
|
text "-"
|
||||||
|
maxChars 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)MENU");
|
||||||
|
helper.AddFile("ui_mp/popup.menu", R"MENU({
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "popup"
|
||||||
|
rect 40 40 200 100 0 0
|
||||||
|
popup
|
||||||
|
visible when(partyismissingmappack());
|
||||||
|
onESC
|
||||||
|
{
|
||||||
|
close self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)MENU");
|
||||||
|
|
||||||
|
const auto result = helper.LoadMenuList("ui_mp/menus.txt");
|
||||||
|
REQUIRE(result.HasBeenSuccessful());
|
||||||
|
|
||||||
|
const auto* menuList = static_cast<MenuList*>(result.GetAssetInfo()->m_ptr);
|
||||||
|
const auto* mainMenu = helper.GetMenu("main");
|
||||||
|
const auto* popupMenu = helper.GetMenu("popup");
|
||||||
|
|
||||||
|
REQUIRE(menuList->name == "ui_mp/menus.txt"s);
|
||||||
|
REQUIRE(menuList->menuCount == 2);
|
||||||
|
REQUIRE(menuList->menus[0] == mainMenu);
|
||||||
|
REQUIRE(menuList->menus[1] == popupMenu);
|
||||||
|
|
||||||
|
REQUIRE(mainMenu->window.background == backgroundMaterial);
|
||||||
|
REQUIRE(mainMenu->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR);
|
||||||
|
REQUIRE_THAT(mainMenu->disableColor[0], WithinRel(0.2f));
|
||||||
|
REQUIRE_THAT(mainMenu->disableColor[3], WithinRel(0.5f));
|
||||||
|
REQUIRE(mainMenu->visibleExp.numEntries == 3);
|
||||||
|
REQUIRE(mainMenu->visibleExp.entries[0]->type == EET_OPERATOR);
|
||||||
|
REQUIRE(mainMenu->visibleExp.entries[0]->data.op == OP_DVARBOOL);
|
||||||
|
REQUIRE(mainMenu->visibleExp.entries[1]->data.operand.dataType == VAL_STRING);
|
||||||
|
REQUIRE(mainMenu->visibleExp.entries[1]->data.operand.internals.stringVal == "ui_show_main"s);
|
||||||
|
REQUIRE(mainMenu->visibleExp.entries[2]->data.op == OP_RIGHTPAREN);
|
||||||
|
REQUIRE(mainMenu->onOpen == R"("play" "menu_open" ; "setLocalVarInt" "ui_highlight" "5" ; )"s);
|
||||||
|
REQUIRE(mainMenu->onFocus == R"("setdvar" "ui_menu_focused" "1" ; )"s);
|
||||||
|
|
||||||
|
REQUIRE(mainMenu->itemCount == 3);
|
||||||
|
const auto* emptyTextItem = mainMenu->items[0];
|
||||||
|
REQUIRE(emptyTextItem->parent == mainMenu);
|
||||||
|
REQUIRE(emptyTextItem->text != nullptr);
|
||||||
|
REQUIRE(emptyTextItem->text == ""s);
|
||||||
|
REQUIRE(emptyTextItem->dvar == "ui_fallback_text"s);
|
||||||
|
REQUIRE(emptyTextItem->window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE);
|
||||||
|
REQUIRE(emptyTextItem->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR);
|
||||||
|
REQUIRE(
|
||||||
|
emptyTextItem->action
|
||||||
|
== R"("setItemColor" "empty_text" "borderColor" "0.1" "0.1" "0.12" "0.5" ; "execondvarstringvalue" "ui_zfeather" "0" "set cg_laserLight 0" ; )"s);
|
||||||
|
REQUIRE(emptyTextItem->materialExp.numEntries == 3);
|
||||||
|
REQUIRE(emptyTextItem->materialExp.entries[0]->data.op == OP_LOCALVARSTRING);
|
||||||
|
|
||||||
|
const auto* listItem = mainMenu->items[1];
|
||||||
|
REQUIRE(listItem->typeData.listBox);
|
||||||
|
REQUIRE(listItem->typeData.listBox->numColumns == 2);
|
||||||
|
REQUIRE(listItem->typeData.listBox->columnInfo[1].pos == 120);
|
||||||
|
REQUIRE(listItem->typeData.listBox->columnInfo[1].width == 180);
|
||||||
|
REQUIRE(listItem->typeData.listBox->notselectable == 1);
|
||||||
|
REQUIRE(listItem->typeData.listBox->noScrollBars == 1);
|
||||||
|
REQUIRE(listItem->typeData.listBox->usePaging == 1);
|
||||||
|
REQUIRE_THAT(listItem->typeData.listBox->disableColor[0], WithinRel(0.6f));
|
||||||
|
REQUIRE_THAT(listItem->typeData.listBox->focusColor[2], WithinRel(0.3f));
|
||||||
|
REQUIRE(listItem->typeData.listBox->selectIcon == selectMaterial);
|
||||||
|
REQUIRE(listItem->typeData.listBox->backgroundItemListbox == listboxBackgroundMaterial);
|
||||||
|
REQUIRE(listItem->typeData.listBox->highlightTexture == highlightMaterial);
|
||||||
|
REQUIRE(listItem->typeData.listBox->onDoubleClick == R"("play" "mouse_click" ; )"s);
|
||||||
|
REQUIRE(listItem->onListboxSelectionChange == R"("setdvar" "ui_server_selected" "1" ; )"s);
|
||||||
|
|
||||||
|
const auto* defaultTextItem = mainMenu->items[2];
|
||||||
|
REQUIRE(defaultTextItem->type == 0);
|
||||||
|
REQUIRE(defaultTextItem->typeData.editField);
|
||||||
|
REQUIRE(defaultTextItem->typeData.editField->maxChars == 2);
|
||||||
|
|
||||||
|
REQUIRE(popupMenu->window.staticFlags & WINDOW_FLAG_POPUP);
|
||||||
|
REQUIRE(popupMenu->visibleExp.numEntries == 2);
|
||||||
|
REQUIRE(popupMenu->visibleExp.entries[0]->data.op == OP_PARTYISMISSINGMAPPACK);
|
||||||
|
REQUIRE(popupMenu->visibleExp.entries[1]->data.op == OP_RIGHTPAREN);
|
||||||
|
REQUIRE(popupMenu->onESC == R"("close" "self" ; )"s);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("MenuListLoaderT4: Ensure optimizing item rect considers relative positions", "[t4][menu][assetloader]")
|
||||||
|
{
|
||||||
|
MenuLoadingTestHelper helper;
|
||||||
|
|
||||||
|
helper.AddFile("ui_mp/test.menu", R"MENU({
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "test"
|
||||||
|
rect 2 4 100 100
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
rect 1 2 20 20
|
||||||
|
exp rect x (5)
|
||||||
|
exp rect y (7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)MENU");
|
||||||
|
|
||||||
|
const auto result = helper.LoadMenuList("ui_mp/test.menu");
|
||||||
|
REQUIRE(result.HasBeenSuccessful());
|
||||||
|
|
||||||
|
const auto* testMenu = helper.GetMenu("test");
|
||||||
|
|
||||||
|
REQUIRE(testMenu->window.rectClient.x == Approx(2));
|
||||||
|
REQUIRE(testMenu->window.rectClient.y == Approx(4));
|
||||||
|
REQUIRE(testMenu->window.rect.x == Approx(2));
|
||||||
|
REQUIRE(testMenu->window.rect.y == Approx(4));
|
||||||
|
|
||||||
|
REQUIRE(testMenu->itemCount == 1);
|
||||||
|
const auto* item = testMenu->items[0];
|
||||||
|
REQUIRE(item->rectXExp.numEntries == 0);
|
||||||
|
REQUIRE(item->rectYExp.numEntries == 0);
|
||||||
|
REQUIRE(item->window.rectClient.x == Approx(5));
|
||||||
|
REQUIRE(item->window.rectClient.y == Approx(7));
|
||||||
|
REQUIRE(item->window.rect.x == Approx(7));
|
||||||
|
REQUIRE(item->window.rect.y == Approx(11));
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,616 @@
|
|||||||
|
#include "Game/T4/Menu/MenuDumperT4.h"
|
||||||
|
|
||||||
|
#include "Game/T4/Menu/MenuListDumperT4.h"
|
||||||
|
#include "Parsing/Menu/MenuFileReader.h"
|
||||||
|
#include "SearchPath/MockOutputPath.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
expressionEntry Operator(const operationEnum operation)
|
||||||
|
{
|
||||||
|
expressionEntry entry{};
|
||||||
|
entry.type = EET_OPERATOR;
|
||||||
|
entry.data.op = operation;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
expressionEntry IntOperand(const int value)
|
||||||
|
{
|
||||||
|
expressionEntry entry{};
|
||||||
|
entry.type = EET_OPERAND;
|
||||||
|
entry.data.operand.dataType = VAL_INT;
|
||||||
|
entry.data.operand.internals.intVal = value;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
expressionEntry StringOperand(const char* value)
|
||||||
|
{
|
||||||
|
expressionEntry entry{};
|
||||||
|
entry.type = EET_OPERAND;
|
||||||
|
entry.data.operand.dataType = VAL_STRING;
|
||||||
|
entry.data.operand.internals.stringVal = value;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::size_t EntryCount> std::array<expressionEntry*, EntryCount> EntryPointers(std::array<expressionEntry, EntryCount>& entries)
|
||||||
|
{
|
||||||
|
std::array<expressionEntry*, EntryCount> result{};
|
||||||
|
for (auto entryIndex = 0u; entryIndex < EntryCount; entryIndex++)
|
||||||
|
result[entryIndex] = &entries[entryIndex];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("MenuDumperT4: Can dump menu with expressions and scripts", "[t4][menu][assetdumper]")
|
||||||
|
{
|
||||||
|
constexpr auto UI_SHOW_FAVORITE_SERVERS = 0x00000004;
|
||||||
|
constexpr auto UI_SHOW_NOT_FAVORITE_SERVERS = 0x00001000;
|
||||||
|
|
||||||
|
std::array menuVisibleValues{
|
||||||
|
Operator(OP_DVARBOOL),
|
||||||
|
StringOperand("ui_test"),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
};
|
||||||
|
auto menuVisibleEntries = EntryPointers(menuVisibleValues);
|
||||||
|
|
||||||
|
std::array buttonVisibleValues{
|
||||||
|
Operator(OP_LEFTPAREN),
|
||||||
|
Operator(OP_LOCALVARINT),
|
||||||
|
StringOperand("ui_highlight"),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
Operator(OP_EQUALS),
|
||||||
|
IntOperand(5),
|
||||||
|
Operator(OP_AND),
|
||||||
|
Operator(OP_LOCALVARSTRING),
|
||||||
|
StringOperand("ui_choicegroup"),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
Operator(OP_EQUALS),
|
||||||
|
StringOperand("popmenu"),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
};
|
||||||
|
auto buttonVisibleEntries = EntryPointers(buttonVisibleValues);
|
||||||
|
|
||||||
|
std::array materialExpressionValues{
|
||||||
|
Operator(OP_LEFTPAREN),
|
||||||
|
StringOperand("expression_material"),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
};
|
||||||
|
auto materialExpressionEntries = EntryPointers(materialExpressionValues);
|
||||||
|
|
||||||
|
Material backgroundMaterial{};
|
||||||
|
backgroundMaterial.info.name = "background_material";
|
||||||
|
|
||||||
|
ItemKeyHandler buttonKeyHandler{
|
||||||
|
.key = 'a',
|
||||||
|
.action = "open advanced;",
|
||||||
|
.next = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
itemDef_s button{};
|
||||||
|
button.window.name = "button_test";
|
||||||
|
button.window.group = "buttons";
|
||||||
|
button.window.rectClient = {
|
||||||
|
.x = 10.0f,
|
||||||
|
.y = 20.0f,
|
||||||
|
.w = 180.0f,
|
||||||
|
.h = 24.0f,
|
||||||
|
.horzAlign = 1,
|
||||||
|
.vertAlign = 2,
|
||||||
|
};
|
||||||
|
button.window.style = 1;
|
||||||
|
button.window.ownerDrawFlags = UI_SHOW_NOT_FAVORITE_SERVERS;
|
||||||
|
button.window.backColor[0] = 0.1f;
|
||||||
|
button.window.backColor[1] = 0.1f;
|
||||||
|
button.window.backColor[2] = 0.1f;
|
||||||
|
button.window.backColor[3] = 0.25f;
|
||||||
|
button.window.foreColor[0] = 0.69f;
|
||||||
|
button.window.foreColor[1] = 0.69f;
|
||||||
|
button.window.foreColor[2] = 0.69f;
|
||||||
|
button.window.foreColor[3] = 1.0f;
|
||||||
|
button.window.background = &backgroundMaterial;
|
||||||
|
button.type = ITEM_TYPE_BUTTON;
|
||||||
|
button.text = "@MENU_TEST";
|
||||||
|
button.textAlignMode = 10;
|
||||||
|
button.textalignx = -6.0f;
|
||||||
|
button.textscale = 0.4f;
|
||||||
|
button.textStyle = 6;
|
||||||
|
button.fontEnum = 1;
|
||||||
|
button.onFocus = "play mouse_over; setlocalvarint ui_highlight 5;";
|
||||||
|
button.leaveFocus = "setlocalvarint ui_highlight 0;";
|
||||||
|
button.mouseExit = R"(setitemcolor button_test bordercolor "0.1" "0.1" "0.12" "0.5";)";
|
||||||
|
button.action = "play mouse_click; open options;";
|
||||||
|
button.onKey = &buttonKeyHandler;
|
||||||
|
button.visibleExp = {.numEntries = static_cast<int>(buttonVisibleEntries.size()), .entries = buttonVisibleEntries.data()};
|
||||||
|
button.materialExp = {.numEntries = static_cast<int>(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<int>(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<int>(std::size(items));
|
||||||
|
menu.items = items;
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC);
|
||||||
|
zone.m_pools.AddAsset(std::make_unique<XAssetInfo<menuDef_t>>(ASSET_TYPE_MENU, menu.window.name, &menu));
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
|
||||||
|
menu::MenuDumperT4 dumper;
|
||||||
|
dumper.Dump(context);
|
||||||
|
|
||||||
|
const auto* file = mockOutput.GetMockedFile("ui_mp/test_menu.menu");
|
||||||
|
REQUIRE(file);
|
||||||
|
|
||||||
|
const auto output = file->AsString();
|
||||||
|
constexpr auto expectedOutput = R"({
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "test_menu"
|
||||||
|
fullscreen 1
|
||||||
|
rect 0 0 640 480 0 0
|
||||||
|
style 1
|
||||||
|
border 1
|
||||||
|
borderSize 2
|
||||||
|
backcolor 0.1 0.2 0.3 0.75
|
||||||
|
focuscolor 1 0.8 0.2 1
|
||||||
|
ownerdrawFlag 4
|
||||||
|
soundLoop "menu_music"
|
||||||
|
fadeClamp 1
|
||||||
|
fadeCycle 16
|
||||||
|
fadeAmount 0.1
|
||||||
|
fadeInAmount 0.2
|
||||||
|
blurWorld 2
|
||||||
|
allowedBinding "+activate"
|
||||||
|
visible when(dvarbool("ui_test"));
|
||||||
|
onOpen
|
||||||
|
{
|
||||||
|
play mouse_open;
|
||||||
|
setfocus button_test;
|
||||||
|
}
|
||||||
|
onClose
|
||||||
|
{
|
||||||
|
play mouse_close;
|
||||||
|
}
|
||||||
|
onESC
|
||||||
|
{
|
||||||
|
close self;
|
||||||
|
}
|
||||||
|
execKeyInt 13
|
||||||
|
{
|
||||||
|
open key_help;
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "button_test"
|
||||||
|
text "@MENU_TEST"
|
||||||
|
group "buttons"
|
||||||
|
rect 10 20 180 24 1 2
|
||||||
|
style 1
|
||||||
|
type 1
|
||||||
|
visible when(localvarint("ui_highlight") == 5 && localvarstring("ui_choicegroup") == "popmenu");
|
||||||
|
ownerdrawFlag 4096
|
||||||
|
textalign 10
|
||||||
|
textalignx -6
|
||||||
|
textscale 0.4
|
||||||
|
textstyle 6
|
||||||
|
textfont 1
|
||||||
|
backcolor 0.1 0.1 0.1 0.25
|
||||||
|
forecolor 0.69 0.69 0.69 1
|
||||||
|
background "background_material"
|
||||||
|
onFocus
|
||||||
|
{
|
||||||
|
play mouse_over;
|
||||||
|
setlocalvarint ui_highlight 5;
|
||||||
|
}
|
||||||
|
leaveFocus
|
||||||
|
{
|
||||||
|
setlocalvarint ui_highlight 0;
|
||||||
|
}
|
||||||
|
mouseExit
|
||||||
|
{
|
||||||
|
setitemcolor button_test bordercolor 0.1 0.1 0.12 0.5;
|
||||||
|
}
|
||||||
|
action
|
||||||
|
{
|
||||||
|
play mouse_click;
|
||||||
|
open options;
|
||||||
|
}
|
||||||
|
execKey "a"
|
||||||
|
{
|
||||||
|
open advanced;
|
||||||
|
}
|
||||||
|
exp material ("expression_material");
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "name_field"
|
||||||
|
text ""
|
||||||
|
group "fields"
|
||||||
|
rect 10 56 180 24 1 2
|
||||||
|
style 1
|
||||||
|
type 18
|
||||||
|
border 1
|
||||||
|
borderSize 1
|
||||||
|
visible 1
|
||||||
|
textalign 1
|
||||||
|
textalignx -28
|
||||||
|
textaligny 14
|
||||||
|
textscale 0.3
|
||||||
|
textfont 1
|
||||||
|
backcolor 0.1 0.1 0.1 0.25
|
||||||
|
bordercolor 0.5 0.5 0.5 1
|
||||||
|
accept
|
||||||
|
{
|
||||||
|
setdvar ui_name player_name;
|
||||||
|
}
|
||||||
|
dvar "player_name"
|
||||||
|
maxChars 16
|
||||||
|
maxPaintChars 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
REQUIRE(output == expectedOutput);
|
||||||
|
|
||||||
|
menu::MenuAssetZoneState zoneState;
|
||||||
|
std::istringstream input(output);
|
||||||
|
menu::MenuFileReader reader(input, "ui_mp/test_menu.menu", menu::FeatureLevel::IW4, mockObjPath);
|
||||||
|
reader.IncludeZoneState(zoneState);
|
||||||
|
|
||||||
|
const auto parsed = reader.ReadMenuFile();
|
||||||
|
REQUIRE(parsed);
|
||||||
|
REQUIRE(parsed->m_menus.size() == 1);
|
||||||
|
REQUIRE(parsed->m_menus[0]->m_name == "test_menu");
|
||||||
|
REQUIRE(parsed->m_menus[0]->m_owner_draw_flags == UI_SHOW_FAVORITE_SERVERS);
|
||||||
|
REQUIRE(parsed->m_menus[0]->m_items.size() == 2);
|
||||||
|
REQUIRE(parsed->m_menus[0]->m_items[0]->m_name == "button_test");
|
||||||
|
REQUIRE(parsed->m_menus[0]->m_items[0]->m_owner_draw_flags == UI_SHOW_NOT_FAVORITE_SERVERS);
|
||||||
|
REQUIRE(parsed->m_menus[0]->m_items[1]->m_name == "name_field");
|
||||||
|
REQUIRE(parsed->m_menus[0]->m_items[1]->m_dvar == "player_name");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("MenuDumperT4: Prefers parent menu list path over ui_mp fallback", "[t4][menu][assetdumper]")
|
||||||
|
{
|
||||||
|
menuDef_t menu{};
|
||||||
|
menu.window.name = "test_menu";
|
||||||
|
|
||||||
|
menuDef_t* menus[]{&menu};
|
||||||
|
|
||||||
|
MenuList menuList{};
|
||||||
|
menuList.name = "ui/menus.txt";
|
||||||
|
menuList.menuCount = static_cast<int>(std::size(menus));
|
||||||
|
menuList.menus = menus;
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC);
|
||||||
|
zone.m_pools.AddAsset(std::make_unique<XAssetInfo<menuDef_t>>(ASSET_TYPE_MENU, menu.window.name, &menu));
|
||||||
|
zone.m_pools.AddAsset(std::make_unique<XAssetInfo<MenuList>>(ASSET_TYPE_MENULIST, menuList.name, &menuList));
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
|
||||||
|
menu::MenuListDumperT4 menuListDumper;
|
||||||
|
menuListDumper.Dump(context);
|
||||||
|
|
||||||
|
menu::MenuDumperT4 menuDumper;
|
||||||
|
menuDumper.Dump(context);
|
||||||
|
|
||||||
|
REQUIRE(mockOutput.GetMockedFile("ui/test_menu.menu"));
|
||||||
|
REQUIRE_FALSE(mockOutput.GetMockedFile("ui_mp/test_menu.menu"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("MenuDumperT4: Expressions are valid when dumped", "[t4][menu][assetdumper]")
|
||||||
|
{
|
||||||
|
std::array item0VisibleValues{
|
||||||
|
Operator(OP_LEFTPAREN),
|
||||||
|
Operator(OP_LOCALVARSTRING),
|
||||||
|
StringOperand("ui_highlight"),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
};
|
||||||
|
auto item0Entries = EntryPointers(item0VisibleValues);
|
||||||
|
|
||||||
|
itemDef_s item0{};
|
||||||
|
item0.window.foreColor[0] = 1.0f;
|
||||||
|
item0.window.foreColor[1] = 1.0f;
|
||||||
|
item0.window.foreColor[2] = 1.0f;
|
||||||
|
item0.window.foreColor[3] = 1.0f;
|
||||||
|
item0.window.rectClient = {.x = 1, .y = 2, .w = 3, .h = 4, .horzAlign = 0, .vertAlign = 0};
|
||||||
|
item0.textExp = {.numEntries = static_cast<int>(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<int>(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<int>(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<int>(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<int>(std::size(items));
|
||||||
|
menu.items = items;
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC);
|
||||||
|
zone.m_pools.AddAsset(std::make_unique<XAssetInfo<menuDef_t>>(ASSET_TYPE_MENU, menu.window.name, &menu));
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
|
||||||
|
menu::MenuDumperT4 dumper;
|
||||||
|
dumper.Dump(context);
|
||||||
|
|
||||||
|
const auto* file = mockOutput.GetMockedFile("ui_mp/test_menu.menu");
|
||||||
|
REQUIRE(file);
|
||||||
|
|
||||||
|
const auto output = file->AsString();
|
||||||
|
constexpr auto expectedOutput = R"({
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "test_menu"
|
||||||
|
rect 1 2 3 4 0 0
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
rect 1 2 3 4 0 0
|
||||||
|
exp text (localvarstring("ui_highlight"));
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
rect 1 2 3 4 0 0
|
||||||
|
exp text (2);
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
rect 1 2 3 4 0 0
|
||||||
|
exp text (3 + 7);
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
rect 1 2 3 4 0 0
|
||||||
|
exp text (-3 + 7 * !sin(cos(7)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
REQUIRE(output == expectedOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("MenuDumperT4: Can dump T4-specific menu properties", "[t4][menu][assetdumper]")
|
||||||
|
{
|
||||||
|
std::array visibleValues{
|
||||||
|
Operator(OP_PARTYISMISSINGMAPPACK),
|
||||||
|
Operator(OP_RIGHTPAREN),
|
||||||
|
};
|
||||||
|
auto visibleEntries = EntryPointers(visibleValues);
|
||||||
|
|
||||||
|
Material selectIcon{};
|
||||||
|
selectIcon.info.name = "listbox_select_icon";
|
||||||
|
|
||||||
|
Material listboxBackground{};
|
||||||
|
listboxBackground.info.name = "listbox_background";
|
||||||
|
|
||||||
|
Material highlightTexture{};
|
||||||
|
highlightTexture.info.name = "listbox_highlight";
|
||||||
|
|
||||||
|
listBoxDef_s listBox{};
|
||||||
|
listBox.onDoubleClick = "play mouse_click;";
|
||||||
|
listBox.focusColor[0] = 1.0f;
|
||||||
|
listBox.focusColor[1] = 0.8f;
|
||||||
|
listBox.focusColor[2] = 0.2f;
|
||||||
|
listBox.focusColor[3] = 1.0f;
|
||||||
|
listBox.selectIcon = &selectIcon;
|
||||||
|
listBox.backgroundItemListbox = &listboxBackground;
|
||||||
|
listBox.highlightTexture = &highlightTexture;
|
||||||
|
|
||||||
|
itemDef_s serverList{};
|
||||||
|
serverList.window.name = "server_list";
|
||||||
|
serverList.window.rectClient = {.x = 20.0f, .y = 80.0f, .w = 400.0f, .h = 180.0f, .horzAlign = 1, .vertAlign = 2};
|
||||||
|
serverList.window.foreColor[0] = 1.0f;
|
||||||
|
serverList.window.foreColor[1] = 1.0f;
|
||||||
|
serverList.window.foreColor[2] = 1.0f;
|
||||||
|
serverList.window.foreColor[3] = 1.0f;
|
||||||
|
serverList.type = ITEM_TYPE_LISTBOX;
|
||||||
|
serverList.onListboxSelectionChange = "setdvar ui_server_selected 1;";
|
||||||
|
serverList.typeData.listBox = &listBox;
|
||||||
|
|
||||||
|
itemDef_s* items[]{&serverList};
|
||||||
|
|
||||||
|
menuDef_t menu{};
|
||||||
|
menu.window.name = "t4_menu";
|
||||||
|
menu.window.rect = {.x = 0.0f, .y = 0.0f, .w = 640.0f, .h = 480.0f, .horzAlign = 0, .vertAlign = 0};
|
||||||
|
menu.window.foreColor[0] = 1.0f;
|
||||||
|
menu.window.foreColor[1] = 1.0f;
|
||||||
|
menu.window.foreColor[2] = 1.0f;
|
||||||
|
menu.window.foreColor[3] = 1.0f;
|
||||||
|
menu.onFocus = "setdvar ui_hint t4_menu;";
|
||||||
|
menu.visibleExp = {.numEntries = static_cast<int>(visibleEntries.size()), .entries = visibleEntries.data()};
|
||||||
|
menu.itemCount = static_cast<int>(std::size(items));
|
||||||
|
menu.items = items;
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC);
|
||||||
|
zone.m_pools.AddAsset(std::make_unique<XAssetInfo<menuDef_t>>(ASSET_TYPE_MENU, menu.window.name, &menu));
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
|
||||||
|
menu::MenuDumperT4 dumper;
|
||||||
|
dumper.Dump(context);
|
||||||
|
|
||||||
|
const auto* file = mockOutput.GetMockedFile("ui_mp/t4_menu.menu");
|
||||||
|
REQUIRE(file);
|
||||||
|
|
||||||
|
constexpr auto expectedOutput = R"({
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "t4_menu"
|
||||||
|
rect 0 0 640 480 0 0
|
||||||
|
visible when(partyismissingmappack());
|
||||||
|
onFocus
|
||||||
|
{
|
||||||
|
setdvar ui_hint t4_menu;
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "server_list"
|
||||||
|
rect 20 80 400 180 1 2
|
||||||
|
type 6
|
||||||
|
doubleclick
|
||||||
|
{
|
||||||
|
play mouse_click;
|
||||||
|
}
|
||||||
|
onListboxSelectionChange
|
||||||
|
{
|
||||||
|
setdvar ui_server_selected 1;
|
||||||
|
}
|
||||||
|
focusColor 1 0.8 0.2 1
|
||||||
|
selectIcon "listbox_select_icon"
|
||||||
|
backgroundItemListbox "listbox_background"
|
||||||
|
highlightTexture "listbox_highlight"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
REQUIRE(file->AsString() == expectedOutput);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#include "Game/T4/Menu/MenuListDumperT4.h"
|
||||||
|
|
||||||
|
#include "SearchPath/MockOutputPath.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
using namespace T4;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TEST_CASE("MenuListDumperT4: Can dump several menu entries", "[t4][menu][menulist][assetdumper]")
|
||||||
|
{
|
||||||
|
menuDef_t mainMenu{};
|
||||||
|
mainMenu.window.name = "main";
|
||||||
|
|
||||||
|
menuDef_t optionsMenu{};
|
||||||
|
optionsMenu.window.name = "options";
|
||||||
|
|
||||||
|
menuDef_t confirmationMenu{};
|
||||||
|
confirmationMenu.window.name = ",confirmation";
|
||||||
|
|
||||||
|
menuDef_t* menus[]{&mainMenu, &optionsMenu, &confirmationMenu};
|
||||||
|
|
||||||
|
MenuList menuList{};
|
||||||
|
menuList.name = "ui_mp/menus.txt";
|
||||||
|
menuList.menuCount = static_cast<int>(std::size(menus));
|
||||||
|
menuList.menus = menus;
|
||||||
|
|
||||||
|
Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC);
|
||||||
|
zone.m_pools.AddAsset(std::make_unique<XAssetInfo<MenuList>>(ASSET_TYPE_MENULIST, menuList.name, &menuList));
|
||||||
|
|
||||||
|
MockSearchPath mockObjPath;
|
||||||
|
MockOutputPath mockOutput;
|
||||||
|
AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt);
|
||||||
|
|
||||||
|
menu::MenuListDumperT4 dumper;
|
||||||
|
dumper.Dump(context);
|
||||||
|
|
||||||
|
const auto* file = mockOutput.GetMockedFile("ui_mp/menus.txt");
|
||||||
|
REQUIRE(file);
|
||||||
|
|
||||||
|
constexpr auto expectedOutput = R"({
|
||||||
|
loadMenu { "ui_mp/main.menu" }
|
||||||
|
loadMenu { "ui_mp/options.menu" }
|
||||||
|
loadMenu { "ui_mp/confirmation.menu" }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
REQUIRE(file->AsString() == expectedOutput);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
>game,T4
|
||||||
|
|
||||||
|
menulist,ui_mp/menus.txt
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "main"
|
||||||
|
fullscreen 1
|
||||||
|
rect 0 0 640 480 0 0
|
||||||
|
disablecolor 0.2 0.3 0.4 0.5
|
||||||
|
forecolor 0.8 0.7 0.6 0.5
|
||||||
|
visible when(dvarbool("ui_show_main"));
|
||||||
|
onOpen
|
||||||
|
{
|
||||||
|
play menu_open;
|
||||||
|
setLocalVarInt ui_highlight 5;
|
||||||
|
}
|
||||||
|
onFocus
|
||||||
|
{
|
||||||
|
setdvar ui_menu_focused 1;
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "empty_text"
|
||||||
|
text ""
|
||||||
|
rect 10 20 180 24 1 2
|
||||||
|
type 1
|
||||||
|
visible 1
|
||||||
|
forecolor 0.9 0.9 1 0.07
|
||||||
|
dvar "ui_fallback_text"
|
||||||
|
action
|
||||||
|
{
|
||||||
|
setItemColor empty_text borderColor 0.1 0.1 0.12 0.5;
|
||||||
|
}
|
||||||
|
exp material localvarstring("ui_material");
|
||||||
|
}
|
||||||
|
itemDef
|
||||||
|
{
|
||||||
|
name "server_list"
|
||||||
|
rect 20 60 300 200 0 0
|
||||||
|
type 6
|
||||||
|
focusColor 0.1 0.2 0.3 0.4
|
||||||
|
onListboxSelectionChange
|
||||||
|
{
|
||||||
|
setdvar ui_server_selected 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
loadMenu { "ui_mp/main.menu" }
|
||||||
|
loadMenu { "ui_mp/popup.menu" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
menuDef
|
||||||
|
{
|
||||||
|
name "popup"
|
||||||
|
rect 40 40 200 100 0 0
|
||||||
|
popup
|
||||||
|
visible when(partyismissingmappack());
|
||||||
|
onESC
|
||||||
|
{
|
||||||
|
close self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "Game/T4/Menu/MenuDumperT4.h"
|
||||||
|
#include "Game/T4/Menu/MenuListDumperT4.h"
|
||||||
|
#include "Game/T4/T4.h"
|
||||||
|
#include "Linker.h"
|
||||||
|
#include "OatTestPaths.h"
|
||||||
|
#include "SearchPath/MockOutputPath.h"
|
||||||
|
#include "SearchPath/MockSearchPath.h"
|
||||||
|
#include "SystemTestsPaths.h"
|
||||||
|
#include "ZoneLoading.h"
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace T4;
|
||||||
|
using namespace std::literals;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TEST_CASE("Menu loading and dumping (T4)", "[t4][menu][system]")
|
||||||
|
{
|
||||||
|
const auto testDataPath = oat::paths::GetSystemTestsDirectory() / "Game/T4/MenuRoundTrip";
|
||||||
|
const auto testDataPathString = testDataPath.string();
|
||||||
|
const auto outputPath = oat::paths::GetTempDirectory("MenuRoundTripT4");
|
||||||
|
const auto outputPathString = outputPath.string();
|
||||||
|
|
||||||
|
const char* arguments[]{
|
||||||
|
"SystemTests",
|
||||||
|
"--verbose",
|
||||||
|
"--base-folder",
|
||||||
|
outputPathString.c_str(),
|
||||||
|
"--asset-search-path",
|
||||||
|
testDataPathString.c_str(),
|
||||||
|
"--source-search-path",
|
||||||
|
testDataPathString.c_str(),
|
||||||
|
"--output-folder",
|
||||||
|
outputPathString.c_str(),
|
||||||
|
"MenuRoundTripT4",
|
||||||
|
};
|
||||||
|
|
||||||
|
LinkerArgs linkerArgs;
|
||||||
|
auto shouldContinue = true;
|
||||||
|
REQUIRE(linkerArgs.ParseArgs(std::size(arguments), arguments, shouldContinue));
|
||||||
|
REQUIRE(shouldContinue);
|
||||||
|
|
||||||
|
const auto linker = Linker::Create(std::move(linkerArgs));
|
||||||
|
REQUIRE(linker->Start());
|
||||||
|
|
||||||
|
const auto zonePath = outputPath / "MenuRoundTripT4.ff";
|
||||||
|
auto maybeZone = ZoneLoading::LoadZone(zonePath.string(), std::nullopt);
|
||||||
|
REQUIRE(maybeZone);
|
||||||
|
auto zone = std::move(*maybeZone);
|
||||||
|
|
||||||
|
const auto* menuList = zone->m_pools.GetAsset<AssetMenuList>("ui_mp/menus.txt");
|
||||||
|
const auto* mainMenu = zone->m_pools.GetAsset<AssetMenu>("main");
|
||||||
|
const auto* popupMenu = zone->m_pools.GetAsset<AssetMenu>("popup");
|
||||||
|
REQUIRE(menuList);
|
||||||
|
REQUIRE(mainMenu);
|
||||||
|
REQUIRE(popupMenu);
|
||||||
|
REQUIRE(menuList->Asset()->menuCount == 2);
|
||||||
|
REQUIRE(mainMenu->Asset()->visibleExp.numEntries == 3);
|
||||||
|
REQUIRE(mainMenu->Asset()->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR);
|
||||||
|
REQUIRE(mainMenu->Asset()->onFocus == R"("setdvar" "ui_menu_focused" "1" ; )"s);
|
||||||
|
REQUIRE(mainMenu->Asset()->items[0]->text != nullptr);
|
||||||
|
REQUIRE(mainMenu->Asset()->items[0]->text == ""s);
|
||||||
|
REQUIRE(mainMenu->Asset()->items[0]->window.dynamicFlags[0] & WINDOW_FLAG_NON_DEFAULT_FORECOLOR);
|
||||||
|
REQUIRE(mainMenu->Asset()->items[0]->action == R"("setItemColor" "empty_text" "borderColor" "0.1" "0.1" "0.12" "0.5" ; )"s);
|
||||||
|
REQUIRE(mainMenu->Asset()->items[1]->onListboxSelectionChange == R"("setdvar" "ui_server_selected" "1" ; )"s);
|
||||||
|
REQUIRE(mainMenu->Asset()->items[1]->typeData.listBox->focusColor[2] == 0.3f);
|
||||||
|
REQUIRE(popupMenu->Asset()->visibleExp.entries[0]->data.op == OP_PARTYISMISSINGMAPPACK);
|
||||||
|
|
||||||
|
MockSearchPath objPath;
|
||||||
|
MockOutputPath dumpOutput;
|
||||||
|
AssetDumpingContext dumpingContext(*zone, "", dumpOutput, objPath, std::nullopt);
|
||||||
|
menu::MenuListDumperT4 menuListDumper;
|
||||||
|
menuListDumper.Dump(dumpingContext);
|
||||||
|
menu::MenuDumperT4 menuDumper;
|
||||||
|
menuDumper.Dump(dumpingContext);
|
||||||
|
|
||||||
|
const auto* dumpedMenuList = dumpOutput.GetMockedFile("ui_mp/menus.txt");
|
||||||
|
const auto* dumpedMainMenu = dumpOutput.GetMockedFile("ui_mp/main.menu");
|
||||||
|
const auto* dumpedPopupMenu = dumpOutput.GetMockedFile("ui_mp/popup.menu");
|
||||||
|
REQUIRE(dumpedMenuList);
|
||||||
|
REQUIRE(dumpedMainMenu);
|
||||||
|
REQUIRE(dumpedPopupMenu);
|
||||||
|
REQUIRE(dumpedMenuList->AsString().find("loadMenu { \"ui_mp/main.menu\" }") != std::string::npos);
|
||||||
|
REQUIRE(dumpedMenuList->AsString().find("loadMenu { \"ui_mp/popup.menu\" }") != std::string::npos);
|
||||||
|
REQUIRE(dumpedMainMenu->AsString().find("text \"\"") != std::string::npos);
|
||||||
|
REQUIRE(dumpedMainMenu->AsString().find("visible when(dvarbool(\"ui_show_main\"));") != std::string::npos);
|
||||||
|
REQUIRE(dumpedMainMenu->AsString().find("setItemColor empty_text borderColor 0.1 0.1 0.12 0.5;") != std::string::npos);
|
||||||
|
REQUIRE(dumpedMainMenu->AsString().find("onListboxSelectionChange") != std::string::npos);
|
||||||
|
REQUIRE(dumpedMainMenu->AsString().find("focusColor 0.1 0.2 0.3 0.4") != std::string::npos);
|
||||||
|
REQUIRE(dumpedPopupMenu->AsString().find("partyismissingmappack()") != std::string::npos);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user