diff --git a/ModVerify.slnx b/ModVerify.slnx index 10c1b6db..9cecc54d 100644 --- a/ModVerify.slnx +++ b/ModVerify.slnx @@ -38,6 +38,8 @@ + + @@ -46,4 +48,5 @@ + diff --git a/modules/ModdingToolBase b/modules/ModdingToolBase index 837d2cc1..6f010371 160000 --- a/modules/ModdingToolBase +++ b/modules/ModdingToolBase @@ -1 +1 @@ -Subproject commit 837d2cc1b72e280c6c5da8e6e841b44920c6c251 +Subproject commit 6f010371a67c8e427a9a1dbeaad659e0fcc5f422 diff --git a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs index 544112aa..50101588 100644 --- a/src/ModVerify.CliApp/Reporting/BaselineSelector.cs +++ b/src/ModVerify.CliApp/Reporting/BaselineSelector.cs @@ -159,7 +159,7 @@ private bool TryPromptForEmbeddedBaseline(GameEngineType engineType, ? $"Apply the default baseline for engine '{engineType}' as a base? Findings already covered by it will be excluded from your new baseline." : $"Do you want to load the default baseline for game engine '{engineType}'?"; - if (!ConsoleUtilities.UserYesNoQuestion(question)) + if (!ConsoleUtilities.UserYesNoQuestion(question, defaultAnswer: true)) return false; return TryLoadEmbeddedBaseline(engineType, out baseline, out identifier); @@ -197,6 +197,6 @@ private bool ShouldUseBaseline(VerificationBaseline baseline, string baselinePat ? "Use it as a base? Findings already covered by it will be excluded from your new baseline." : "Do you want to use it?"; Console.ResetColor(); - return ConsoleUtilities.UserYesNoQuestion(question); + return ConsoleUtilities.UserYesNoQuestion(question, defaultAnswer: true); } } diff --git a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json index fb8cb646..99d339ba 100644 --- a/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json +++ b/src/ModVerify.CliApp/Resources/Baselines/baseline-foc.json @@ -8,26 +8,15 @@ }, "minSeverity": "Information", "errors": [ - { - "id": "XML04", - "severity": "Warning", - "asset": "Probability", - "message": "Expected integer but got \u002780, 20\u0027. File=\u0027DATA\\XML\\SFXEVENTSWEAPONS.XML #90\u0027", - "context": [ - "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlIntegerParser", - "File: DATA\\XML\\SFXEVENTSWEAPONS.XML", - "Unit_TIE_Fighter_Fire" - ] - }, { "id": "XML10", "severity": "Information", "asset": "Disabled_Darken", - "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8569\u0027", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8608\u0027", "context": [ "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", - "b_fast_forward" + "b_fast_forward_t" ] }, { @@ -41,45 +30,55 @@ ] }, { - "id": "XML04", - "severity": "Warning", - "asset": "Size", - "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", + "id": "XML10", + "severity": "Information", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8569\u0027", "context": [ - "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlFloatParser", + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", - "bm_text_steal" + "b_fast_forward" ] }, { "id": "XML08", "severity": "Information", - "asset": "DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML", - "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML #0\u0027", + "asset": "DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML #0\u0027", "context": [ "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", - "File: DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML" + "File: DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML" ] }, { - "id": "XML10", + "id": "XML08", "severity": "Information", - "asset": "Disabled_Darken", - "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8550\u0027", + "asset": "DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML #0\u0027", "context": [ - "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", - "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", - "b_play_pause" + "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", + "File: DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML" ] }, { "id": "XML08", "severity": "Information", - "asset": "DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML", - "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML #0\u0027", + "asset": "DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML", + "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML #0\u0027", "context": [ "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", - "File: DATA\\XML\\UNITS_HERO_REBEL_ROGUE_SQUADRON.XML" + "File: DATA\\XML\\UNITS_LAND_REBEL_GALLOFREE_HTT.XML" + ] + }, + { + "id": "XML04", + "severity": "Warning", + "asset": "Probability", + "message": "Expected integer but got \u002780, 20\u0027. File=\u0027DATA\\XML\\SFXEVENTSWEAPONS.XML #90\u0027", + "context": [ + "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlIntegerParser", + "File: DATA\\XML\\SFXEVENTSWEAPONS.XML", + "Unit_TIE_Fighter_Fire" ] }, { @@ -105,116 +104,126 @@ { "id": "XML10", "severity": "Information", - "asset": "Disabled_Darken", - "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8608\u0027", + "asset": "Mega_Texture_Name", + "message": "The node \u0027Mega_Texture_Name\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8\u0027", "context": [ "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", - "b_fast_forward_t" + "i_main_commandbar" ] }, { "id": "XML10", "severity": "Information", - "asset": "Mega_Texture_Name", - "message": "The node \u0027Mega_Texture_Name\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8\u0027", + "asset": "Disabled_Darken", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8589\u0027", "context": [ "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", - "i_main_commandbar" + "b_play_pause_t" ] }, { "id": "XML10", "severity": "Information", "asset": "Disabled_Darken", - "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8589\u0027", + "message": "The node \u0027Disabled_Darken\u0027 is not supported. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #8550\u0027", "context": [ "Parser: PG.StarWarsGame.Engine.Xml.Parsers.CommandBarComponentParser", "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", - "b_play_pause_t" + "b_play_pause" ] }, { - "id": "XML08", - "severity": "Information", - "asset": "DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML", - "message": "XML header is not the first entry of the XML file. File=\u0027DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML #0\u0027", + "id": "XML04", + "severity": "Warning", + "asset": "Size", + "message": "Expected double but got value \u002737\u0060\u0027. File=\u0027DATA\\XML\\COMMANDBARCOMPONENTS.XML #11571\u0027", "context": [ - "Parser: PG.StarWarsGame.Engine.Xml.Parsers.GameObjectFileParser", - "File: DATA\\XML\\UNITS_SPACE_EMPIRE_TIE_DEFENDER.XML" + "Parser: PG.StarWarsGame.Files.XML.Parsers.PetroglyphXmlFloatParser", + "File: DATA\\XML\\COMMANDBARCOMPONENTS.XML", + "bm_text_steal" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0315_ENG.WAV", - "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", + "asset": "U000_ARC3106_ENG.WAV", + "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Complete_Troops_Arc_Hammer" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0202_ENG.WAV", - "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0404_ENG.WAV", + "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Guard_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0110_ENG.WAV", - "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0309_ENG.WAV", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0203_ENG.WAV", - "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0602_ENG.WAV", + "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Fleet_Move_Leia" + "Unit_Increase_Production_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0301_ENG.WAV", - "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0304_ENG.WAV", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0306_ENG.WAV", - "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", + "asset": "U000_ARC3105_ENG.WAV", + "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Complete_Troops_Arc_Hammer" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0602_ENG.WAV", - "message": "Audio file \u0027U000_LEI0602_ENG.WAV\u0027 could not be found.", + "asset": "AMB_DES_CLEAR_LOOP_1.WAV", + "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", "context": [ - "Unit_Increase_Production_Leia" + "Weather_Ambient_Clear_Sandstorm_Loop" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0104_ENG.WAV", - "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0502_ENG.WAV", + "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Remove_Corruption_Leia" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Move_Leia" ] }, { @@ -229,71 +238,80 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_ARC3106_ENG.WAV", - "message": "Audio file \u0027U000_ARC3106_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0314_ENG.WAV", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Complete_Troops_Arc_Hammer" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0208_ENG.WAV", - "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0115_ENG.WAV", + "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_DEF3006_ENG.WAV", - "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0312_ENG.WAV", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Corrupt_Sabateur" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0211_ENG.WAV", - "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", + "asset": "U000_MAL0503_ENG.WAV", + "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Assist_Move_Missile_Launcher" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0501_ENG.WAV", - "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Fleet_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0207_ENG.WAV", - "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Group_Move_Leia" ] }, + { + "id": "FILE00", + "severity": "Error", + "asset": "U000_LEI0103_ENG.WAV", + "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Select_Leia" + ] + }, { "id": "FILE00", "severity": "Error", "asset": "U000_LEI0215_ENG.WAV", "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0203_ENG.WAV", - "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0212_ENG.WAV", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Group_Move_Leia" ] @@ -301,82 +319,82 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0402_ENG.WAV", - "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0311_ENG.WAV", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0401_ENG.WAV", - "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", + "asset": "U000_DEF3006_ENG.WAV", + "message": "Audio file \u0027U000_DEF3006_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" + "Unit_Corrupt_Sabateur" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0215_ENG.WAV", - "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Fleet_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "AMB_DES_CLEAR_LOOP_1.WAV", - "message": "Audio file \u0027AMB_DES_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "context": [ - "Weather_Ambient_Clear_Sandstorm_Loop" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0202_ENG.WAV", - "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0501_ENG.WAV", + "message": "Audio file \u0027U000_LEI0501_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Remove_Corruption_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0115_ENG.WAV", - "message": "Audio file \u0027U000_LEI0115_ENG.WAV\u0027 could not be found.", + "asset": "EGL_STAR_VIPER_SPINNING_1.WAV", + "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Star_Viper_Spinning_By" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0303_ENG.WAV", - "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0604_ENG.WAV", - "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0203_ENG.WAV", + "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0312_ENG.WAV", - "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "asset": "C000_DST0102_ENG.WAV", + "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "EHD_Death_Star_Activate" ] }, { @@ -391,8 +409,8 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0308_ENG.WAV", - "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0305_ENG.WAV", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Group_Attack_Leia" ] @@ -400,125 +418,125 @@ { "id": "FILE00", "severity": "Error", - "asset": "FS_BEETLE_3.WAV", - "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", + "asset": "U000_LEI0311_ENG.WAV", + "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "FS_BEETLE_4.WAV", - "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", + "asset": "U000_LEI0108_ENG.WAV", + "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0208_ENG.WAV", - "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0312_ENG.WAV", + "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Fleet_Move_Leia" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0101_ENG.WAV", - "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0207_ENG.WAV", + "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "FS_BEETLE_2.WAV", - "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", + "asset": "U000_LEI0306_ENG.WAV", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0109_ENG.WAV", - "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0113_ENG.WAV", - "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", + "asset": "TESTUNITMOVE_ENG.WAV", + "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Move_Gneneric_Test" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0403_ENG.WAV", - "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0313_ENG.WAV", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0206_ENG.WAV", - "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", + "asset": "FS_BEETLE_4.WAV", + "message": "Audio file \u0027FS_BEETLE_4.WAV\u0027 could not be found.", "context": [ - "Unit_Fleet_Move_Leia" + "SFX_Anim_Beetle_Footsteps" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0209_ENG.WAV", - "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", + "asset": "FS_BEETLE_1.WAV", + "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "SFX_Anim_Beetle_Footsteps" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0312_ENG.WAV", - "message": "Audio file \u0027U000_LEI0312_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0304_ENG.WAV", - "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0102_ENG.WAV", + "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_TMC0212_ENG.WAV", - "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Assist_Move_Tie_Mauler" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0315_ENG.WAV", - "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0314_ENG.WAV", + "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Group_Attack_Leia" ] @@ -526,134 +544,134 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0308_ENG.WAV", - "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0206_ENG.WAV", + "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0303_ENG.WAV", - "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0111_ENG.WAV", + "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0111_ENG.WAV", - "message": "Audio file \u0027U000_LEI0111_ENG.WAV\u0027 could not be found.", + "asset": "AMB_URB_CLEAR_LOOP_1.WAV", + "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Weather_Ambient_Clear_Urban_Loop" ] }, { "id": "FILE00", "severity": "Error", - "asset": "EGL_STAR_VIPER_SPINNING_1.WAV", - "message": "Audio file \u0027EGL_STAR_VIPER_SPINNING_1.WAV\u0027 could not be found.", + "asset": "U000_LEI0315_ENG.WAV", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Star_Viper_Spinning_By" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0201_ENG.WAV", - "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0402_ENG.WAV", + "message": "Audio file \u0027U000_LEI0402_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Guard_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "FS_BEETLE_1.WAV", - "message": "Audio file \u0027FS_BEETLE_1.WAV\u0027 could not be found.", + "asset": "U000_LEI0604_ENG.WAV", + "message": "Audio file \u0027U000_LEI0604_ENG.WAV\u0027 could not be found.", "context": [ - "SFX_Anim_Beetle_Footsteps" + "Unit_Increase_Production_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0313_ENG.WAV", - "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "asset": "U000_TMC0212_ENG.WAV", + "message": "Audio file \u0027U000_TMC0212_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Assist_Move_Tie_Mauler" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0305_ENG.WAV", - "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", + "asset": "FS_BEETLE_2.WAV", + "message": "Audio file \u0027FS_BEETLE_2.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "SFX_Anim_Beetle_Footsteps" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0307_ENG.WAV", - "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0313_ENG.WAV", - "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0401_ENG.WAV", + "message": "Audio file \u0027U000_LEI0401_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Guard_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0108_ENG.WAV", - "message": "Audio file \u0027U000_LEI0108_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0305_ENG.WAV", + "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0503_ENG.WAV", - "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0114_ENG.WAV", + "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0205_ENG.WAV", - "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0113_ENG.WAV", + "message": "Audio file \u0027U000_LEI0113_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Fleet_Move_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0204_ENG.WAV", - "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0104_ENG.WAV", + "message": "Audio file \u0027U000_LEI0104_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0205_ENG.WAV", - "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0215_ENG.WAV", + "message": "Audio file \u0027U000_LEI0215_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Move_Leia" ] @@ -661,98 +679,98 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_DEF3106_ENG.WAV", - "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0504_ENG.WAV", + "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Weaken_Sabateur" + "Unit_Remove_Corruption_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_MAL0503_ENG.WAV", - "message": "Audio file \u0027U000_MAL0503_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0303_ENG.WAV", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Assist_Move_Missile_Launcher" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0114_ENG.WAV", - "message": "Audio file \u0027U000_LEI0114_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0301_ENG.WAV", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0504_ENG.WAV", - "message": "Audio file \u0027U000_LEI0504_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0209_ENG.WAV", + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0211_ENG.WAV", - "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", + "asset": "U000_ARC3104_ENG.WAV", + "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Produce_Troops_Arc_Hammer" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0502_ENG.WAV", - "message": "Audio file \u0027U000_LEI0502_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0304_ENG.WAV", + "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Remove_Corruption_Leia" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_ARC3104_ENG.WAV", - "message": "Audio file \u0027U000_ARC3104_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0204_ENG.WAV", + "message": "Audio file \u0027U000_LEI0204_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Produce_Troops_Arc_Hammer" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "TESTUNITMOVE_ENG.WAV", - "message": "Audio file \u0027TESTUNITMOVE_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0308_ENG.WAV", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Gneneric_Test" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0202_ENG.WAV", - "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0503_ENG.WAV", + "message": "Audio file \u0027U000_LEI0503_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Fleet_Move_Leia" + "Unit_Remove_Corruption_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0309_ENG.WAV", - "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0210_ENG.WAV", + "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0210_ENG.WAV", - "message": "Audio file \u0027U000_LEI0210_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0211_ENG.WAV", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Move_Leia" ] @@ -760,53 +778,53 @@ { "id": "FILE00", "severity": "Error", - "asset": "AMB_URB_CLEAR_LOOP_1.WAV", - "message": "Audio file \u0027AMB_URB_CLEAR_LOOP_1.WAV\u0027 could not be found.", + "asset": "U000_LEI0307_ENG.WAV", + "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", "context": [ - "Weather_Ambient_Clear_Urban_Loop" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0304_ENG.WAV", - "message": "Audio file \u0027U000_LEI0304_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0601_ENG.WAV", + "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Increase_Production_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0311_ENG.WAV", - "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0109_ENG.WAV", + "message": "Audio file \u0027U000_LEI0109_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "C000_DST0102_ENG.WAV", - "message": "Audio file \u0027C000_DST0102_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0603_ENG.WAV", + "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", "context": [ - "EHD_Death_Star_Activate" + "Unit_Increase_Production_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_MCF1601_ENG.WAV", - "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0212_ENG.WAV", + "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_StarDest_MC30_Frigate" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0212_ENG.WAV", - "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Move_Leia" ] @@ -814,44 +832,44 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0205_ENG.WAV", - "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0107_ENG.WAV", + "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0601_ENG.WAV", - "message": "Audio file \u0027U000_LEI0601_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0403_ENG.WAV", + "message": "Audio file \u0027U000_LEI0403_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Guard_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0404_ENG.WAV", - "message": "Audio file \u0027U000_LEI0404_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Guard_Leia" + "Unit_Fleet_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0314_ENG.WAV", - "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0201_ENG.WAV", + "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0208_ENG.WAV", - "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0211_ENG.WAV", + "message": "Audio file \u0027U000_LEI0211_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Group_Move_Leia" ] @@ -859,17 +877,17 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0103_ENG.WAV", - "message": "Audio file \u0027U000_LEI0103_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0309_ENG.WAV", + "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0107_ENG.WAV", - "message": "Audio file \u0027U000_LEI0107_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0110_ENG.WAV", + "message": "Audio file \u0027U000_LEI0110_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Select_Leia" ] @@ -877,35 +895,35 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0203_ENG.WAV", - "message": "Audio file \u0027U000_LEI0203_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0306_ENG.WAV", + "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0603_ENG.WAV", - "message": "Audio file \u0027U000_LEI0603_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Increase_Production_Leia" + "Unit_Fleet_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0206_ENG.WAV", - "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0106_ENG.WAV", + "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0112_ENG.WAV", - "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0105_ENG.WAV", + "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Select_Leia" ] @@ -913,8 +931,8 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0209_ENG.WAV", - "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0202_ENG.WAV", + "message": "Audio file \u0027U000_LEI0202_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Move_Leia" ] @@ -924,6 +942,15 @@ "severity": "Error", "asset": "U000_LEI0307_ENG.WAV", "message": "Audio file \u0027U000_LEI0307_ENG.WAV\u0027 could not be found.", + "context": [ + "Unit_Group_Attack_Leia" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "U000_LEI0313_ENG.WAV", + "message": "Audio file \u0027U000_LEI0313_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Attack_Leia" ] @@ -931,17 +958,17 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0201_ENG.WAV", - "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "asset": "U000_MCF1601_ENG.WAV", + "message": "Audio file \u0027U000_MCF1601_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Fleet_Move_Leia" + "Unit_StarDest_MC30_Frigate" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0311_ENG.WAV", - "message": "Audio file \u0027U000_LEI0311_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0303_ENG.WAV", + "message": "Audio file \u0027U000_LEI0303_ENG.WAV\u0027 could not be found.", "context": [ "Unit_Attack_Leia" ] @@ -949,19 +976,19 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0301_ENG.WAV", - "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", + "asset": "U000_DEF3106_ENG.WAV", + "message": "Audio file \u0027U000_DEF3106_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Weaken_Sabateur" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0213_ENG.WAV", - "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0112_ENG.WAV", + "message": "Audio file \u0027U000_LEI0112_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Select_Leia" ] }, { @@ -976,64 +1003,64 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0309_ENG.WAV", - "message": "Audio file \u0027U000_LEI0309_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0213_ENG.WAV", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Group_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0207_ENG.WAV", - "message": "Audio file \u0027U000_LEI0207_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0315_ENG.WAV", + "message": "Audio file \u0027U000_LEI0315_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_ARC3105_ENG.WAV", - "message": "Audio file \u0027U000_ARC3105_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0213_ENG.WAV", + "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Complete_Troops_Arc_Hammer" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0212_ENG.WAV", - "message": "Audio file \u0027U000_LEI0212_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0101_ENG.WAV", + "message": "Audio file \u0027U000_LEI0101_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Move_Leia" + "Unit_Select_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0102_ENG.WAV", - "message": "Audio file \u0027U000_LEI0102_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0208_ENG.WAV", + "message": "Audio file \u0027U000_LEI0208_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Fleet_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0306_ENG.WAV", - "message": "Audio file \u0027U000_LEI0306_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0209_ENG.WAV", + "message": "Audio file \u0027U000_LEI0209_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Group_Attack_Leia" + "Unit_Move_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0206_ENG.WAV", - "message": "Audio file \u0027U000_LEI0206_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0205_ENG.WAV", + "message": "Audio file \u0027U000_LEI0205_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Move_Leia" + "Unit_Fleet_Move_Leia" ] }, { @@ -1048,55 +1075,55 @@ { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0314_ENG.WAV", - "message": "Audio file \u0027U000_LEI0314_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0308_ENG.WAV", + "message": "Audio file \u0027U000_LEI0308_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Attack_Leia" + "Unit_Group_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0105_ENG.WAV", - "message": "Audio file \u0027U000_LEI0105_ENG.WAV\u0027 could not be found.", + "asset": "FS_BEETLE_3.WAV", + "message": "Audio file \u0027FS_BEETLE_3.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "SFX_Anim_Beetle_Footsteps" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0106_ENG.WAV", - "message": "Audio file \u0027U000_LEI0106_ENG.WAV\u0027 could not be found.", + "asset": "U000_LEI0301_ENG.WAV", + "message": "Audio file \u0027U000_LEI0301_ENG.WAV\u0027 could not be found.", "context": [ - "Unit_Select_Leia" + "Unit_Attack_Leia" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0213_ENG.WAV", - "message": "Audio file \u0027U000_LEI0213_ENG.WAV\u0027 could not be found.", + "asset": "underworld_logo_off.tga", + "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 of type \u0027ButtonMiddle\u0027 at origin \u0027MegaTexture\u0027 for component \u0027IDC_PLAY_FACTION_A_BUTTON_BIG\u0027.", "context": [ - "Unit_Group_Move_Leia" + "IDC_PLAY_FACTION_A_BUTTON_BIG" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0201_ENG.WAV", - "message": "Audio file \u0027U000_LEI0201_ENG.WAV\u0027 could not be found.", + "asset": "i_dialogue_button_large_middle_off.tga", + "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 of type \u0027ButtonMiddleDisabled\u0027 at origin \u0027Repository\u0027 for component \u0027IDC_PLAY_FACTION_B_BUTTON_BIG\u0027.", "context": [ - "Unit_Move_Leia" + "IDC_PLAY_FACTION_B_BUTTON_BIG" ] }, { "id": "FILE00", "severity": "Error", - "asset": "U000_LEI0305_ENG.WAV", - "message": "Audio file \u0027U000_LEI0305_ENG.WAV\u0027 could not be found.", + "asset": "underworld_logo_selected.tga", + "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 of type \u0027ButtonMiddlePressed\u0027 at origin \u0027MegaTexture\u0027 for component \u0027IDC_PLAY_FACTION_A_BUTTON_BIG\u0027.", "context": [ - "Unit_Attack_Leia" + "IDC_PLAY_FACTION_A_BUTTON_BIG" ] }, { @@ -1111,226 +1138,260 @@ { "id": "FILE00", "severity": "Error", - "asset": "i_dialogue_button_large_middle_off.tga", - "message": "Could not find GUI texture \u0027i_dialogue_button_large_middle_off.tga\u0027 of type \u0027ButtonMiddleDisabled\u0027 at origin \u0027Repository\u0027 for component \u0027IDC_PLAY_FACTION_B_BUTTON_BIG\u0027.", + "asset": "CIN_NAVYTROOPER_ROW.ALO", + "message": "Unable to find Alamo file \u0027CIN_NavyTrooper_Row.alo\u0027", "context": [ - "IDC_PLAY_FACTION_B_BUTTON_BIG" + "Cin_NavyTrooper_Row", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "underworld_logo_off.tga", - "message": "Could not find GUI texture \u0027underworld_logo_off.tga\u0027 of type \u0027ButtonMiddle\u0027 at origin \u0027MegaTexture\u0027 for component \u0027IDC_PLAY_FACTION_A_BUTTON_BIG\u0027.", + "asset": "CIN_DEATHSTAR_HIGH.ALO", + "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG" + "UM05_PROP_DSTAR", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "underworld_logo_selected.tga", - "message": "Could not find GUI texture \u0027underworld_logo_selected.tga\u0027 of type \u0027ButtonMiddlePressed\u0027 at origin \u0027MegaTexture\u0027 for component \u0027IDC_PLAY_FACTION_A_BUTTON_BIG\u0027.", + "asset": "p_prison_light", + "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027NB_PRISON.ALO\u0027", "context": [ - "IDC_PLAY_FACTION_A_BUTTON_BIG" + "Imperial_Prison_Facility", + "Tag: Land_Model_Name", + "NB_PRISON.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_SHUTTLE_TYDERIUM.ALO", - "message": "Unable to find Alamo file \u0027Cin_Shuttle_Tyderium.alo\u0027", + "asset": "P_heat_small01", + "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027NB_VCH.ALO\u0027", "context": [ - "Intro2_Shuttle_Tyderium", - "Tag: Space_Model_Name" + "Volcanic_Civilian_Spawn_House_Independent_AI", + "Tag: Land_Model_Name", + "NB_VCH.ALO" ] }, { "id": "FILE00", "severity": "Error", "asset": "Default.fx", - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027UV_SKIPRAY.ALO\u0027.", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027EV_TIE_LANCET.ALO\u0027.", "context": [ - "Skipray_Bombing_Run", + "Lancet_Air_Artillery", "Tag: Land_Model_Name", - "UV_SKIPRAY.ALO" + "EV_TIE_LANCET.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_desert_ground_dust", - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027UI_IG88.ALO\u0027", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_LOW.ALO\u0027", "context": [ - "IG-88", - "Tag: Land_Model_Name", - "UI_IG88.ALO" + "Stars_Low", + "Tag: Space_Model_Name", + "W_STARS_LOW.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_EV_STARDESTROYER_WARP.ALO", - "message": "Unable to find Alamo file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", + "asset": "W_SITH_ARCH.ALO", + "message": "Unable to find Alamo file \u0027w_sith_arch.alo\u0027", "context": [ - "Star_Destroyer_Warp", - "Tag: Space_Model_Name" + "Cin_sith_arch", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_RBEL_GREYGROUP.ALO", - "message": "Unable to find Alamo file \u0027CIN_Rbel_GreyGroup.alo\u0027", + "asset": "CIN_DSTAR_PROTONS.ALO", + "message": "Unable to find Alamo file \u0027Cin_DStar_protons.alo\u0027", "context": [ - "Cin_Rebel_GreyGroup", - "Tag: Land_Model_Name" + "Protons_DStar_Xplode", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_desert_ground_dust", - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027RI_KYLEKATARN.ALO\u0027", + "asset": "CIN_RBEL_SOLDIER.ALO", + "message": "Unable to find Alamo file \u0027CIN_Rbel_Soldier.alo\u0027", "context": [ - "Kyle_Katarn", - "Tag: Land_Model_Name", - "RI_KYLEKATARN.ALO" + "Cin_Rebel_soldier", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DSTAR_TURRETLASERS.ALO", - "message": "Unable to find Alamo file \u0027Cin_DStar_TurretLasers.alo\u0027", + "asset": "P_mptl-2a_Die", + "message": "Proxy particle \u0027P_mptl-2a_Die\u0027 not found for model \u0027RV_MPTL-2A.ALO\u0027", "context": [ - "TurretLasers_DStar_Xplode", - "Tag: Space_Model_Name" + "MPTL", + "Tag: Land_Model_Name", + "RV_MPTL-2A.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_EI_PALPATINE.ALO", - "message": "Unable to find Alamo file \u0027Cin_EI_Palpatine.alo\u0027", + "asset": "CIN_EI_VADER.ALO", + "message": "Unable to find Alamo file \u0027Cin_EI_Vader.alo\u0027", "context": [ - "Cin_Emperor_Shot_5", + "Cin_Vader", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_BRIDGE.ALO", - "message": "Unable to find Alamo file \u0027Cin_bridge.alo\u0027", + "asset": "W_KAMINO_REFLECT.ALO", + "message": "Unable to find Alamo file \u0027W_Kamino_Reflect.ALO\u0027", "context": [ - "Imperial_Bridge", - "Tag: Space_Model_Name" + "Prop_Kamino_Reflection_01", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_P_PROTON_TORPEDO.ALO", - "message": "Unable to find Alamo file \u0027CIN_p_proton_torpedo.alo\u0027", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_03_STATION_D.ALO\u0027", "context": [ - "Cin_Proj_Ground_Proton_Torpedo", + "Underworld_Star_Base_3_Death_Clone", + "Tag: Space_Model_Name", + "UB_03_STATION_D.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_w_tile--\u003ETag: Land_Model_Name--\u003EW_TILE.ALO].", + "context": [ + "Cin_w_tile", + "Tag: Land_Model_Name", + "W_TILE.ALO" + ] + }, + { + "id": "FILE00", + "severity": "Error", + "asset": "W_VOL_STEAM01.ALO", + "message": "Unable to find Alamo file \u0027W_Vol_Steam01.ALO\u0027", + "context": [ + "Prop_Vol_Steam01", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_hp_archammer-damage", - "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027EV_ARCHAMMER.ALO\u0027", + "asset": "CIN_REB_CELEBCHARACTERS.ALO", + "message": "Unable to find Alamo file \u0027CIN_REb_CelebCharacters.alo\u0027", "context": [ - "Arc_Hammer", - "Tag: Space_Model_Name", - "EV_ARCHAMMER.ALO" + "REb_CelebCharacters", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_cold_tiny01", - "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027NB_SCH.ALO\u0027", + "asset": "CIN_DEATHSTAR_HIGH.ALO", + "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "Arctic_Civilian_Spawn_House", - "Tag: Land_Model_Name", - "NB_SCH.ALO" + "Death_Star_Whole", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Cin_Reb_CelebHall_Wall_B.tga", - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_console--\u003ETag: Land_Model_Name--\u003EW_SITH_CONSOLE.ALO].", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027EI_MARAJADE.ALO\u0027", "context": [ - "Cin_sith_console", + "Mara_Jade", "Tag: Land_Model_Name", - "W_SITH_CONSOLE.ALO" + "EI_MARAJADE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "P_heat_small01", - "message": "Proxy particle \u0027P_heat_small01\u0027 not found for model \u0027NB_VCH.ALO\u0027", + "asset": "CIN_EI_PALPATINE.ALO", + "message": "Unable to find Alamo file \u0027Cin_EI_Palpatine.alo\u0027", "context": [ - "Volcanic_Civilian_Spawn_House_Independent_AI", - "Tag: Land_Model_Name", - "NB_VCH.ALO" + "Cin_Emperor_Shot_6-9", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_IMPERIALCRAFT.ALO", - "message": "Unable to find Alamo file \u0027Cin_ImperialCraft.alo\u0027", + "asset": "CIN_DEATHSTAR_WALL.ALO", + "message": "Unable to find Alamo file \u0027Cin_DeathStar_Wall.alo\u0027", "context": [ - "Intro2_ImperialCraft", + "Death_Star_Hangar_Outside", "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Cin_Reb_CelebHall_Wall.tga", - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_console--\u003ETag: Land_Model_Name--\u003EW_SITH_CONSOLE.ALO].", + "asset": "CIN_PLANET_ALDERAAN_HIGH.ALO", + "message": "Unable to find Alamo file \u0027Cin_Planet_Alderaan_High.alo\u0027", "context": [ - "Cin_sith_console", - "Tag: Land_Model_Name", - "W_SITH_CONSOLE.ALO" + "Alderaan_Backdrop_Large 6x", + "Tag: Space_Model_Name" + ] + }, + { + "id": "FILE00", + "severity": "Warning", + "asset": "i_button_ni_nightsister_ranger.tga", + "message": "Could not find icon \u0027i_button_ni_nightsister_ranger.tga\u0027 for game object type \u0027Dathomir_Night_Sister\u0027.", + "context": [ + "Dathomir_Night_Sister" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DSTAR_DISH_CLOSE.ALO", - "message": "Unable to find Alamo file \u0027Cin_DStar_Dish_close.alo\u0027", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027RI_KYLEKATARN.ALO\u0027", "context": [ - "Death_Star_Dish_Close", - "Tag: Space_Model_Name" + "Kyle_Katarn", + "Tag: Land_Model_Name", + "RI_KYLEKATARN.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_BRIDGE.ALO", - "message": "Unable to find Alamo file \u0027Cin_bridge.alo\u0027", + "asset": "W_PLANET_VOLCANIC.ALO", + "message": "Unable to find Alamo file \u0027w_planet_volcanic.alo\u0027", "context": [ - "UM05_PROP_BRIDGE", - "Tag: Land_Model_Name" + "Volcanic_Backdrop_Large", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_REB_CELEBCHARACTERS.ALO", - "message": "Unable to find Alamo file \u0027CIN_REb_CelebCharacters.alo\u0027", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_01_STATION_D.ALO\u0027", "context": [ - "REb_CelebCharacters", - "Tag: Land_Model_Name" + "Underworld_Star_Base_1_Death_Clone", + "Tag: Space_Model_Name", + "UB_01_STATION_D.ALO" ] }, { @@ -1347,34 +1408,32 @@ { "id": "FILE00", "severity": "Error", - "asset": "p_ewok_drag_dirt", - "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027UI_EWOK_HANDLER.ALO\u0027", + "asset": "CIN_REB_CELEBHALL.ALO", + "message": "Unable to find Alamo file \u0027CIN_Reb_CelebHall.alo\u0027", "context": [ - "Ewok_Handler", - "Tag: Land_Model_Name", - "UI_EWOK_HANDLER.ALO" + "REb_CelebHall", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_explosion_small_delay00", - "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027EB_COMMANDCENTER.ALO\u0027", + "asset": "Cin_DeathStar.tga", + "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [Test_Base_Hector--\u003ETag: Land_Model_Name--\u003EALTTEST.ALO].", "context": [ - "Imperial_Command_Center", + "Test_Base_Hector", "Tag: Land_Model_Name", - "EB_COMMANDCENTER.ALO" + "ALTTEST.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "UB_girder_B.tga", - "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [Underworld_Ysalamiri_Cage--\u003ETag: Land_Model_Name--\u003EUV_MDU_CAGE.ALO].", + "asset": "P_SPLASH_WAKE_LAVA.ALO", + "message": "Unable to find Alamo file \u0027p_splash_wake_lava.alo\u0027", "context": [ - "Underworld_Ysalamiri_Cage", - "Tag: Land_Model_Name", - "UV_MDU_CAGE.ALO" + "Splash_Wake_Lava", + "Tag: Land_Model_Name" ] }, { @@ -1391,149 +1450,125 @@ { "id": "FILE00", "severity": "Error", - "asset": "p_bomb_spin", - "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", + "asset": "CIN_PROBE_DROID.ALO", + "message": "Unable to find Alamo file \u0027CIN_Probe_Droid.alo\u0027", "context": [ - "TIE_Bomber_Bombing_Run_Bomb", - "Tag: Land_Model_Name", - "W_THERMAL_DETONATOR_EMPIRE.ALO" + "Empire_Droid", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_OFFICER_ROW.ALO", - "message": "Unable to find Alamo file \u0027CIN_Officer_Row.alo\u0027", + "asset": "CIN_TROOPER_ROW.ALO", + "message": "Unable to find Alamo file \u0027CIN_Trooper_Row.alo\u0027", "context": [ - "Cin_Officer_Row", + "Cin_Trooper_Row", "Tag: Land_Model_Name" ] }, { - "id": "FILE00", - "severity": "Error", - "asset": "W_TE_Rock_f_02_b.tga", - "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [Vengeance_Frigate--\u003ETag: Space_Model_Name--\u003EUV_VENGEANCE.ALO].", + "id": "FILE03", + "severity": "Information", + "asset": "MOV_EMPIRE_INTRO_SHUTTLE_FIRE_DIE_00.ALA", + "message": "Possible file CRC32 collision: \u0027MOV_EMPIRE_INTRO_SHUTTLE_FIRE_DIE_00.ALA\u0027 was requested but \u0027U000_EMP0212_ENG.WAV\u0027 was found by the engine.", "context": [ - "Vengeance_Frigate", - "Tag: Space_Model_Name", - "UV_VENGEANCE.ALO" + "Shuttle_Tyderium_Lua_Cinematic", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_RBEL_GREY.ALO", - "message": "Unable to find Alamo file \u0027CIN_Rbel_grey.alo\u0027", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027UI_SABOTEUR.ALO\u0027", "context": [ - "Cin_Rebel_Grey", - "Tag: Land_Model_Name" + "Underworld_Saboteur", + "Tag: Land_Model_Name", + "UI_SABOTEUR.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Cin_Reb_CelebHall_Wall.tga", - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_w_tile--\u003ETag: Land_Model_Name--\u003EW_TILE.ALO].", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027EV_MDU_SENSORNODE.ALO\u0027.", "context": [ - "Cin_w_tile", + "Empire_Offensive_Sensor_Node", "Tag: Land_Model_Name", - "W_TILE.ALO" + "EV_MDU_SENSORNODE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_uwstation_death", - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_01_STATION_D.ALO\u0027", + "asset": "W_ALLSHADERS.ALO", + "message": "Unable to find Alamo file \u0027W_AllShaders.ALO\u0027", "context": [ - "Underworld_Star_Base_1_Death_Clone", - "Tag: Space_Model_Name", - "UB_01_STATION_D.ALO" + "Prop_AllShaders", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_LAMBDA_HEAD.ALO", - "message": "Unable to find Alamo file \u0027CIN_Lambda_Head.alo\u0027", + "asset": "CIN_FIRE_HUGE.ALO", + "message": "Unable to find Alamo file \u0027CIN_Fire_Huge.alo\u0027", "context": [ - "Cin_Lambda_Head", + "Fin_Fire_Huge", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_smoke_small_thin2", - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027NB_MONCAL_BUILDING.ALO\u0027", + "asset": "p_explosion_smoke_small_thin5", + "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027NB_NOGHRI_HUT.ALO\u0027", "context": [ - "MonCalamari_Spawn_House", + "Noghri_Spawn_House", "Tag: Land_Model_Name", - "NB_MONCAL_BUILDING.ALO" + "NB_NOGHRI_HUT.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_ssd_debris", - "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027UV_ECLIPSE_UC_DC.ALO\u0027", + "asset": "CIN_IMPERIALCRAFT.ALO", + "message": "Unable to find Alamo file \u0027Cin_ImperialCraft.alo\u0027", "context": [ - "Eclipse_Super_Star_Destroyer_Death_Clone", - "Tag: Space_Model_Name", - "UV_ECLIPSE_UC_DC.ALO" + "Intro2_ImperialCraft", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_smoke_small_thin2", - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027RB_HYPERVELOCITYGUN.ALO\u0027", + "asset": "CIN_DSTAR_TURRETLASERS.ALO", + "message": "Unable to find Alamo file \u0027Cin_DStar_TurretLasers.alo\u0027", "context": [ - "Ground_Empire_Hypervelocity_Gun", - "Tag: Land_Model_Name", - "RB_HYPERVELOCITYGUN.ALO" + "TurretLasers_DStar_Xplode", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_EV_TIEADVANCED.ALO", - "message": "Unable to find Alamo file \u0027Cin_EV_TieAdvanced.alo\u0027", + "asset": "CIN_FIRE_MEDIUM.ALO", + "message": "Unable to find Alamo file \u0027CIN_Fire_Medium.alo\u0027", "context": [ - "Fin_Vader_TIE", - "Tag: Space_Model_Name" + "Fin_Fire_Medium", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "pe_bwing_yellow", - "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027RV_BWING.ALO\u0027", - "context": [ - "B-Wing", - "Tag: Space_Model_Name", - "RV_BWING.ALO" - ] - }, - { - "id": "FILE03", - "severity": "Information", - "asset": "MOV_EMPIRE_INTRO_SHUTTLE.ALO", - "message": "Possible file CRC32 collision: \u0027MOV_Empire_Intro_Shuttle.ALO\u0027 was requested but \u0027U000_EMP0212_ENG.WAV\u0027 was found by the engine.", - "context": [ - "Shuttle_Tyderium_Lua_Cinematic", - "Tag: Land_Model_Name" - ] - }, - { - "id": "FILE00", - "severity": "Error", - "asset": "CIN_NAVYTROOPER_ROW.ALO", - "message": "Unable to find Alamo file \u0027CIN_NavyTrooper_Row.alo\u0027", + "asset": "p_ewok_drag_dirt", + "message": "Proxy particle \u0027p_ewok_drag_dirt\u0027 not found for model \u0027UI_EWOK_HANDLER.ALO\u0027", "context": [ - "Cin_NavyTrooper_Row", - "Tag: Land_Model_Name" + "Ewok_Handler", + "Tag: Land_Model_Name", + "UI_EWOK_HANDLER.ALO" ] }, { @@ -1550,203 +1585,210 @@ { "id": "FILE00", "severity": "Error", - "asset": "p_desert_ground_dust", - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027EI_MARAJADE.ALO\u0027", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_console--\u003ETag: Land_Model_Name--\u003EW_SITH_CONSOLE.ALO].", "context": [ - "Mara_Jade", + "Cin_sith_console", "Tag: Land_Model_Name", - "EI_MARAJADE.ALO" + "W_SITH_CONSOLE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_LAMBDA_MOUTH.ALO", - "message": "Unable to find Alamo file \u0027CIN_Lambda_Mouth.alo\u0027", + "asset": "CIN_OFFICER_ROW.ALO", + "message": "Unable to find Alamo file \u0027CIN_Officer_Row.alo\u0027", "context": [ - "Cin_Lambda_Mouth", + "Cin_Officer_Row", "Tag: Land_Model_Name" ] }, { "id": "FILE00", - "severity": "Warning", - "asset": "i_button_general_dodonna.tga", - "message": "Could not find icon \u0027i_button_general_dodonna.tga\u0027 for game object type \u0027General_Dodonna\u0027.", + "severity": "Error", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_CINE.ALO\u0027", "context": [ - "General_Dodonna" + "Stars_Cinematic", + "Tag: Space_Model_Name", + "W_STARS_CINE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Lensflare0", - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_MEDIUM.ALO\u0027", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [Vengeance_Frigate--\u003ETag: Space_Model_Name--\u003EUV_VENGEANCE.ALO].", "context": [ - "Stars_Medium", + "Vengeance_Frigate", "Tag: Space_Model_Name", - "W_STARS_MEDIUM.ALO" + "UV_VENGEANCE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Cin_Reb_CelebHall_Wall.tga", - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_lefthall--\u003ETag: Land_Model_Name--\u003EW_SITH_LEFTHALL.ALO].", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_05_STATION_D.ALO\u0027", "context": [ - "Cin_sith_lefthall", - "Tag: Land_Model_Name", - "W_SITH_LEFTHALL.ALO" + "Underworld_Star_Base_5_Death_Clone", + "Tag: Space_Model_Name", + "UB_05_STATION_D.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_DROID_STEAM.ALO", - "message": "Unable to find Alamo file \u0027W_droid_steam.alo\u0027", + "asset": "p_explosion_small_delay00", + "message": "Proxy particle \u0027p_explosion_small_delay00\u0027 not found for model \u0027EB_COMMANDCENTER.ALO\u0027", "context": [ - "Prop_Droid_Steam", - "Tag: Land_Model_Name" + "Imperial_Command_Center", + "Tag: Land_Model_Name", + "EB_COMMANDCENTER.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_REB_CELEBHALL.ALO", - "message": "Unable to find Alamo file \u0027CIN_Reb_CelebHall.alo\u0027", + "asset": "p_steam_small", + "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027RB_HEAVYVEHICLEFACTORY.ALO\u0027", "context": [ - "REb_CelebHall", - "Tag: Land_Model_Name" + "R_Ground_Heavy_Vehicle_Factory", + "Tag: Land_Model_Name", + "RB_HEAVYVEHICLEFACTORY.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_EI_PALPATINE.ALO", - "message": "Unable to find Alamo file \u0027Cin_EI_Palpatine.alo\u0027", + "asset": "CIN_RBEL_NAVYROW.ALO", + "message": "Unable to find Alamo file \u0027CIN_Rbel_NavyRow.alo\u0027", "context": [ - "Cin_Emperor_Shot_6-9", + "Cin_Rebel_NavyRow", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_uwstation_death", - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_05_STATION_D.ALO\u0027", + "asset": "CIN_LAMBDA_MOUTH.ALO", + "message": "Unable to find Alamo file \u0027CIN_Lambda_Mouth.alo\u0027", "context": [ - "Underworld_Star_Base_5_Death_Clone", - "Tag: Space_Model_Name", - "UB_05_STATION_D.ALO" + "Cin_Lambda_Mouth", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_TROOPER_ROW.ALO", - "message": "Unable to find Alamo file \u0027CIN_Trooper_Row.alo\u0027", + "asset": "CIN_RBEL_SOLDIER_GROUP.ALO", + "message": "Unable to find Alamo file \u0027CIN_Rbel_Soldier_Group.alo\u0027", "context": [ - "Cin_Trooper_Row", + "Cin_Rebel_SoldierRow", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DEATHSTAR_WALL.ALO", - "message": "Unable to find Alamo file \u0027Cin_DeathStar_Wall.alo\u0027", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_lefthall--\u003ETag: Land_Model_Name--\u003EW_SITH_LEFTHALL.ALO].", "context": [ - "Death_Star_Hangar_Outside", - "Tag: Space_Model_Name" + "Cin_sith_lefthall", + "Tag: Land_Model_Name", + "W_SITH_LEFTHALL.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_prison_light", - "message": "Proxy particle \u0027p_prison_light\u0027 not found for model \u0027NB_PRISON.ALO\u0027", + "asset": "CIN_EI_VADER.ALO", + "message": "Unable to find Alamo file \u0027Cin_EI_Vader.alo\u0027", "context": [ - "Imperial_Prison_Facility", - "Tag: Land_Model_Name", - "NB_PRISON.ALO" + "Cin_Vader_Shot_6-9", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", - "severity": "Warning", - "asset": "i_button_ni_nightsister_ranger.tga", - "message": "Could not find icon \u0027i_button_ni_nightsister_ranger.tga\u0027 for game object type \u0027Dathomir_Night_Sister\u0027.", + "severity": "Error", + "asset": "CIN_DSTAR_LEVERPANEL.ALO", + "message": "Unable to find Alamo file \u0027Cin_DStar_LeverPanel.alo\u0027", "context": [ - "Dathomir_Night_Sister" + "Death_Star_LeverPanel", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DEATHSTAR_HIGH.ALO", - "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", + "asset": "Default.fx", + "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027UV_SKIPRAY.ALO\u0027.", "context": [ - "Death_Star_Whole_Vsmall", - "Tag: Space_Model_Name" + "Skipray_Bombing_Run", + "Tag: Land_Model_Name", + "UV_SKIPRAY.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_EV_LAMBDASHUTTLE_150.ALO", - "message": "Unable to find Alamo file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "asset": "CINE_EV_STARDESTROYER.ALO", + "message": "Unable to find Alamo file \u0027CINE_EV_StarDestroyer.ALO\u0027", "context": [ - "Lambda_Shuttle_150X6-9", - "Tag: Land_Model_Name" + "CIN_Star_Destroyer3X", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_FIRE_MEDIUM.ALO", - "message": "Unable to find Alamo file \u0027CIN_Fire_Medium.alo\u0027", + "asset": "pe_bwing_yellow", + "message": "Proxy particle \u0027pe_bwing_yellow\u0027 not found for model \u0027RV_BWING.ALO\u0027", "context": [ - "Fin_Fire_Medium", - "Tag: Land_Model_Name" + "B-Wing", + "Tag: Space_Model_Name", + "RV_BWING.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_VOLCANO_ROCK02.ALO", - "message": "Unable to find Alamo file \u0027W_Volcano_Rock02.ALO\u0027", + "asset": "NB_YsalamiriTree_B.tga", + "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [Ysalamiri_Tree--\u003ETag: Land_Model_Name--\u003ENB_YSALAMIRI_TREE.ALO].", "context": [ - "Prop_Volcano_RockForm03", - "Tag: Land_Model_Name" + "Ysalamiri_Tree", + "Tag: Land_Model_Name", + "NB_YSALAMIRI_TREE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_EI_VADER.ALO", - "message": "Unable to find Alamo file \u0027Cin_EI_Vader.alo\u0027", + "asset": "CIN_EV_LAMBDASHUTTLE_150.ALO", + "message": "Unable to find Alamo file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", "context": [ - "Cin_Vader", + "Lambda_Shuttle_150X6-9", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_VOL_STEAM01.ALO", - "message": "Unable to find Alamo file \u0027W_Vol_Steam01.ALO\u0027", + "asset": "CIN_LAMBDA_HEAD.ALO", + "message": "Unable to find Alamo file \u0027CIN_Lambda_Head.alo\u0027", "context": [ - "Prop_Vol_Steam01", + "Cin_Lambda_Head", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DEATHSTAR_HIGH.ALO", - "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", + "asset": "W_DROID_STEAM.ALO", + "message": "Unable to find Alamo file \u0027W_droid_steam.alo\u0027", "context": [ - "UM05_PROP_DSTAR", + "Prop_Droid_Steam", "Tag: Land_Model_Name" ] }, @@ -1756,81 +1798,82 @@ "asset": "CIN_RV_XWINGPROP.ALO", "message": "Unable to find Alamo file \u0027Cin_rv_XWingProp.alo\u0027", "context": [ - "Grounded_Xwing", + "Cin_X-WingProp", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Default.fx", - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027EV_MDU_SENSORNODE.ALO\u0027.", + "asset": "p_hp_archammer-damage", + "message": "Proxy particle \u0027p_hp_archammer-damage\u0027 not found for model \u0027EV_ARCHAMMER.ALO\u0027", "context": [ - "Empire_Offensive_Sensor_Node", - "Tag: Land_Model_Name", - "EV_MDU_SENSORNODE.ALO" + "Arc_Hammer", + "Tag: Space_Model_Name", + "EV_ARCHAMMER.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_particle_master", - "message": "Could not find texture \u0027p_particle_master\u0027 for context: [Test_Particle--\u003ETag: Land_Model_Name--\u003EP_DIRT_EMITTER_TEST1.ALO].", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [The_Peacebringer--\u003ETag: Space_Model_Name--\u003EUV_KRAYTCLASSDESTROYER_TYBER.ALO].", "context": [ - "Test_Particle", - "Tag: Land_Model_Name", - "P_DIRT_EMITTER_TEST1.ALO" + "The_Peacebringer", + "Tag: Space_Model_Name", + "UV_KRAYTCLASSDESTROYER_TYBER.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_BIKER_ROW.ALO", - "message": "Unable to find Alamo file \u0027CIN_Biker_Row.alo\u0027", + "asset": "W_VOLCANO_ROCK02.ALO", + "message": "Unable to find Alamo file \u0027W_Volcano_Rock02.ALO\u0027", "context": [ - "Cin_Biker_Row", + "Prop_Volcano_RockForm03", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DEATHSTAR_HIGH.ALO", - "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", + "asset": "p_ssd_debris", + "message": "Proxy particle \u0027p_ssd_debris\u0027 not found for model \u0027UV_ECLIPSE_UC_DC.ALO\u0027", "context": [ - "Death_Star_Whole_small", - "Tag: Space_Model_Name" + "Eclipse_Super_Star_Destroyer_Death_Clone", + "Tag: Space_Model_Name", + "UV_ECLIPSE_UC_DC.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_FIRE_HUGE.ALO", - "message": "Unable to find Alamo file \u0027CIN_Fire_Huge.alo\u0027", + "asset": "CIN_RV_XWINGPROP.ALO", + "message": "Unable to find Alamo file \u0027Cin_rv_XWingProp.alo\u0027", "context": [ - "Fin_Fire_Huge", + "Grounded_Xwing", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_RBEL_NAVYROW.ALO", - "message": "Unable to find Alamo file \u0027CIN_Rbel_NavyRow.alo\u0027", + "asset": "CIN_DEATHSTAR_HIGH.ALO", + "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "Cin_Rebel_NavyRow", - "Tag: Land_Model_Name" + "Death_Star_Whole_Vsmall", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "lookat", - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027UV_ECLIPSE.ALO\u0027", + "asset": "W_TE_Rock_f_02_b.tga", + "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [TIE_Phantom--\u003ETag: Space_Model_Name--\u003EEV_TIE_PHANTOM.ALO].", "context": [ - "Eclipse_Prop", + "TIE_Phantom", "Tag: Space_Model_Name", - "UV_ECLIPSE.ALO" + "EV_TIE_PHANTOM.ALO" ] }, { @@ -1846,743 +1889,703 @@ { "id": "FILE00", "severity": "Error", - "asset": "W_TE_Rock_f_02_b.tga", - "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [TIE_Phantom--\u003ETag: Space_Model_Name--\u003EEV_TIE_PHANTOM.ALO].", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_MEDIUM.ALO\u0027", "context": [ - "TIE_Phantom", + "Stars_Medium", "Tag: Space_Model_Name", - "EV_TIE_PHANTOM.ALO" + "W_STARS_MEDIUM.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "P_SPLASH_WAKE_LAVA.ALO", - "message": "Unable to find Alamo file \u0027p_splash_wake_lava.alo\u0027", + "asset": "CIN_RBEL_GREYGROUP.ALO", + "message": "Unable to find Alamo file \u0027CIN_Rbel_GreyGroup.alo\u0027", "context": [ - "Splash_Wake_Lava", + "Cin_Rebel_GreyGroup", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_PLANET_ALDERAAN_HIGH.ALO", - "message": "Unable to find Alamo file \u0027Cin_Planet_Alderaan_High.alo\u0027", - "context": [ - "Alderaan_Backdrop_Large 6x", - "Tag: Space_Model_Name" - ] - }, - { - "id": "FILE00", - "severity": "Error", - "asset": "CIN_EV_LAMBDASHUTTLE_150.ALO", - "message": "Unable to find Alamo file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", + "asset": "CIN_DEATHSTAR_HANGAR.ALO", + "message": "Unable to find Alamo file \u0027CIN_DeathStar_Hangar.alo\u0027", "context": [ - "Lambda_Shuttle_150", + "Cin_DeathStar_Hangar", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_PLANET_VOLCANIC.ALO", - "message": "Unable to find Alamo file \u0027w_planet_volcanic.alo\u0027", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027RB_HYPERVELOCITYGUN.ALO\u0027", "context": [ - "Volcanic_Backdrop_Large", - "Tag: Space_Model_Name" + "Ground_Empire_Hypervelocity_Gun", + "Tag: Land_Model_Name", + "RB_HYPERVELOCITYGUN.ALO" ] }, { "id": "FILE00", - "severity": "Error", - "asset": "CIN_EI_VADER.ALO", - "message": "Unable to find Alamo file \u0027Cin_EI_Vader.alo\u0027", + "severity": "Warning", + "asset": "i_button_general_dodonna.tga", + "message": "Could not find icon \u0027i_button_general_dodonna.tga\u0027 for game object type \u0027General_Dodonna\u0027.", "context": [ - "Cin_Vader_Shot_6-9", - "Tag: Land_Model_Name" + "General_Dodonna" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_uwstation_death", - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_02_STATION_D.ALO\u0027", + "asset": "CIN_EV_LAMBDASHUTTLE_150.ALO", + "message": "Unable to find Alamo file \u0027Cin_EV_lambdaShuttle_150.alo\u0027", "context": [ - "Underworld_Star_Base_2_Death_Clone", - "Tag: Space_Model_Name", - "UB_02_STATION_D.ALO" + "Lambda_Shuttle_150", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CINE_EV_STARDESTROYER.ALO", - "message": "Unable to find Alamo file \u0027CINE_EV_StarDestroyer.ALO\u0027", + "asset": "CIN_BRIDGE.ALO", + "message": "Unable to find Alamo file \u0027Cin_bridge.alo\u0027", "context": [ - "CIN_Star_Destroyer3X", + "Imperial_Bridge", "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_smoke_small_thin2", - "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027NB_PRISON.ALO\u0027", + "asset": "CIN_BIKER_ROW.ALO", + "message": "Unable to find Alamo file \u0027CIN_Biker_Row.alo\u0027", "context": [ - "Imperial_Prison_Facility", - "Tag: Land_Model_Name", - "NB_PRISON.ALO" + "Cin_Biker_Row", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_CORUSCANT.ALO", - "message": "Unable to find Alamo file \u0027Cin_Coruscant.alo\u0027", + "asset": "CIN_EI_PALPATINE.ALO", + "message": "Unable to find Alamo file \u0027Cin_EI_Palpatine.alo\u0027", "context": [ - "Corusant_Backdrop_Large 6x", - "Tag: Space_Model_Name" + "Cin_Emperor_Shot_5", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Cin_Reb_CelebHall_Wall_B.tga", - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_lefthall--\u003ETag: Land_Model_Name--\u003EW_SITH_LEFTHALL.ALO].", + "asset": "CIN_DEATHSTAR_HIGH.ALO", + "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", "context": [ - "Cin_sith_lefthall", - "Tag: Land_Model_Name", - "W_SITH_LEFTHALL.ALO" + "Death_Star_Whole_small", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_uwstation_death", - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_03_STATION_D.ALO\u0027", + "asset": "CIN_SHUTTLE_TYDERIUM.ALO", + "message": "Unable to find Alamo file \u0027Cin_Shuttle_Tyderium.alo\u0027", "context": [ - "Underworld_Star_Base_3_Death_Clone", - "Tag: Space_Model_Name", - "UB_03_STATION_D.ALO" + "Intro2_Shuttle_Tyderium", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Default.fx", - "message": "Shader effect \u0027Default.fx\u0027 not found for model \u0027EV_TIE_LANCET.ALO\u0027.", + "asset": "CIN_OFFICER.ALO", + "message": "Unable to find Alamo file \u0027Cin_Officer.alo\u0027", "context": [ - "Lancet_Air_Artillery", - "Tag: Land_Model_Name", - "EV_TIE_LANCET.ALO" + "FIN_Officer", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Lensflare0", - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_CINE_LUA.ALO\u0027", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027NB_PRISON.ALO\u0027", "context": [ - "Stars_Lua_Cinematic", + "Imperial_Prison_Facility", "Tag: Land_Model_Name", - "W_STARS_CINE_LUA.ALO" + "NB_PRISON.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_SWAMPGASEMIT.ALO", - "message": "Unable to find Alamo file \u0027W_SwampGasEmit.ALO\u0027", + "asset": "CIN_RBEL_GREY.ALO", + "message": "Unable to find Alamo file \u0027CIN_Rbel_grey.alo\u0027", "context": [ - "Prop_SwampGasEmitter", + "Cin_Rebel_Grey", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_smoke_small_thin4", - "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027NB_PRISON.ALO\u0027", + "asset": "p_cold_tiny01", + "message": "Proxy particle \u0027p_cold_tiny01\u0027 not found for model \u0027NB_SCH.ALO\u0027", "context": [ - "Imperial_Prison_Facility", + "Arctic_Civilian_Spawn_House", "Tag: Land_Model_Name", - "NB_PRISON.ALO" - ] - }, - { - "id": "FILE00", - "severity": "Error", - "asset": "CIN_DEATHSTAR_HIGH.ALO", - "message": "Unable to find Alamo file \u0027Cin_DeathStar_High.alo\u0027", - "context": [ - "Death_Star_Whole", - "Tag: Space_Model_Name" + "NB_SCH.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_OFFICER.ALO", - "message": "Unable to find Alamo file \u0027Cin_Officer.alo\u0027", + "asset": "lookat", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027UV_ECLIPSE.ALO\u0027", "context": [ - "FIN_Officer", - "Tag: Space_Model_Name" + "Eclipse_Prop", + "Tag: Space_Model_Name", + "UV_ECLIPSE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_RV_XWINGPROP.ALO", - "message": "Unable to find Alamo file \u0027Cin_rv_XWingProp.alo\u0027", + "asset": "W_KAMINO_REFLECT.ALO", + "message": "Unable to find Alamo file \u0027W_Kamino_Reflect.ALO\u0027", "context": [ - "Cin_X-WingProp", + "Prop_Kamino_Reflection_00", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Lensflare0", - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_LOW.ALO\u0027", + "asset": "CIN_DSTAR_DISH_CLOSE.ALO", + "message": "Unable to find Alamo file \u0027Cin_DStar_Dish_close.alo\u0027", "context": [ - "Stars_Low", - "Tag: Space_Model_Name", - "W_STARS_LOW.ALO" + "Death_Star_Dish_Close", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DEATHSTAR_HANGAR.ALO", - "message": "Unable to find Alamo file \u0027CIN_DeathStar_Hangar.alo\u0027", + "asset": "W_BUSH_SWMP00.ALO", + "message": "Unable to find Alamo file \u0027W_Bush_Swmp00.ALO\u0027", "context": [ - "Cin_DeathStar_Hangar", + "Prop_Swamp_Bush00", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "P_mptl-2a_Die", - "message": "Proxy particle \u0027P_mptl-2a_Die\u0027 not found for model \u0027RV_MPTL-2A.ALO\u0027", - "context": [ - "MPTL", - "Tag: Land_Model_Name", - "RV_MPTL-2A.ALO" - ] - }, - { - "id": "FILE00", - "severity": "Error", - "asset": "Cin_Reb_CelebHall_Wall_B.tga", - "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_w_tile--\u003ETag: Land_Model_Name--\u003EW_TILE.ALO].", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_HIGH.ALO\u0027", "context": [ - "Cin_w_tile", - "Tag: Land_Model_Name", - "W_TILE.ALO" + "Stars_High", + "Tag: Space_Model_Name", + "W_STARS_HIGH.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_SITH_ARCH.ALO", - "message": "Unable to find Alamo file \u0027w_sith_arch.alo\u0027", + "asset": "lookat", + "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027UV_ECLIPSE_UC.ALO\u0027", "context": [ - "Cin_sith_arch", - "Tag: Land_Model_Name" + "Eclipse_Super_Star_Destroyer", + "Tag: Space_Model_Name", + "UV_ECLIPSE_UC.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_ALLSHADERS.ALO", - "message": "Unable to find Alamo file \u0027W_AllShaders.ALO\u0027", + "asset": "CIN_BRIDGE.ALO", + "message": "Unable to find Alamo file \u0027Cin_bridge.alo\u0027", "context": [ - "Prop_AllShaders", + "UM05_PROP_BRIDGE", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_steam_small", - "message": "Proxy particle \u0027p_steam_small\u0027 not found for model \u0027RB_HEAVYVEHICLEFACTORY.ALO\u0027", - "context": [ - "R_Ground_Heavy_Vehicle_Factory", - "Tag: Land_Model_Name", - "RB_HEAVYVEHICLEFACTORY.ALO" - ] - }, - { - "id": "FILE00", - "severity": "Error", - "asset": "NB_YsalamiriTree_B.tga", - "message": "Could not find texture \u0027NB_YsalamiriTree_B.tga\u0027 for context: [Ysalamiri_Tree--\u003ETag: Land_Model_Name--\u003ENB_YSALAMIRI_TREE.ALO].", + "asset": "p_bomb_spin", + "message": "Proxy particle \u0027p_bomb_spin\u0027 not found for model \u0027W_THERMAL_DETONATOR_EMPIRE.ALO\u0027", "context": [ - "Ysalamiri_Tree", + "TIE_Bomber_Bombing_Run_Bomb", "Tag: Land_Model_Name", - "NB_YSALAMIRI_TREE.ALO" + "W_THERMAL_DETONATOR_EMPIRE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "w_grenade.tga", - "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [Proj_Merc_Concussion_Grenade--\u003ETag: Land_Model_Name--\u003EW_GRENADE.ALO].", + "asset": "Lensflare0", + "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_CINE_LUA.ALO\u0027", "context": [ - "Proj_Merc_Concussion_Grenade", + "Stars_Lua_Cinematic", "Tag: Land_Model_Name", - "W_GRENADE.ALO" + "W_STARS_CINE_LUA.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_KAMINO_REFLECT.ALO", - "message": "Unable to find Alamo file \u0027W_Kamino_Reflect.ALO\u0027", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_02_STATION_D.ALO\u0027", "context": [ - "Prop_Kamino_Reflection_01", - "Tag: Land_Model_Name" + "Underworld_Star_Base_2_Death_Clone", + "Tag: Space_Model_Name", + "UB_02_STATION_D.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DSTAR_PROTONS.ALO", - "message": "Unable to find Alamo file \u0027Cin_DStar_protons.alo\u0027", + "asset": "CIN_EV_STARDESTROYER_WARP.ALO", + "message": "Unable to find Alamo file \u0027Cin_EV_Stardestroyer_Warp.alo\u0027", "context": [ - "Protons_DStar_Xplode", + "Star_Destroyer_Warp", "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Cin_DeathStar.tga", - "message": "Could not find texture \u0027Cin_DeathStar.tga\u0027 for context: [Test_Base_Hector--\u003ETag: Land_Model_Name--\u003EALTTEST.ALO].", + "asset": "p_smoke_small_thin4", + "message": "Proxy particle \u0027p_smoke_small_thin4\u0027 not found for model \u0027NB_PRISON.ALO\u0027", "context": [ - "Test_Base_Hector", + "Imperial_Prison_Facility", "Tag: Land_Model_Name", - "ALTTEST.ALO" - ] - }, - { - "id": "FILE00", - "severity": "Error", - "asset": "lookat", - "message": "Proxy particle \u0027lookat\u0027 not found for model \u0027UV_ECLIPSE_UC.ALO\u0027", - "context": [ - "Eclipse_Super_Star_Destroyer", - "Tag: Space_Model_Name", - "UV_ECLIPSE_UC.ALO" + "NB_PRISON.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_explosion_smoke_small_thin5", - "message": "Proxy particle \u0027p_explosion_smoke_small_thin5\u0027 not found for model \u0027NB_NOGHRI_HUT.ALO\u0027", + "asset": "w_grenade.tga", + "message": "Could not find texture \u0027w_grenade.tga\u0027 for context: [Proj_Merc_Concussion_Grenade--\u003ETag: Land_Model_Name--\u003EW_GRENADE.ALO].", "context": [ - "Noghri_Spawn_House", + "Proj_Merc_Concussion_Grenade", "Tag: Land_Model_Name", - "NB_NOGHRI_HUT.ALO" + "W_GRENADE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_KAMINO_REFLECT.ALO", - "message": "Unable to find Alamo file \u0027W_Kamino_Reflect.ALO\u0027", + "asset": "CIN_CORUSCANT.ALO", + "message": "Unable to find Alamo file \u0027Cin_Coruscant.alo\u0027", "context": [ - "Prop_Kamino_Reflection_00", - "Tag: Land_Model_Name" + "Corusant_Backdrop_Large 6x", + "Tag: Space_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_PROBE_DROID.ALO", - "message": "Unable to find Alamo file \u0027CIN_Probe_Droid.alo\u0027", + "asset": "UB_girder_B.tga", + "message": "Could not find texture \u0027UB_girder_B.tga\u0027 for context: [Underworld_Ysalamiri_Cage--\u003ETag: Land_Model_Name--\u003EUV_MDU_CAGE.ALO].", "context": [ - "Empire_Droid", - "Tag: Land_Model_Name" + "Underworld_Ysalamiri_Cage", + "Tag: Land_Model_Name", + "UV_MDU_CAGE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Lensflare0", - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_HIGH.ALO\u0027", + "asset": "Cin_Reb_CelebHall_Wall_B.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall_B.tga\u0027 for context: [Cin_sith_console--\u003ETag: Land_Model_Name--\u003EW_SITH_CONSOLE.ALO].", "context": [ - "Stars_High", - "Tag: Space_Model_Name", - "W_STARS_HIGH.ALO" + "Cin_sith_console", + "Tag: Land_Model_Name", + "W_SITH_CONSOLE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "Lensflare0", - "message": "Proxy particle \u0027Lensflare0\u0027 not found for model \u0027W_STARS_CINE.ALO\u0027", + "asset": "p_uwstation_death", + "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_04_STATION_D.ALO\u0027", "context": [ - "Stars_Cinematic", + "Underworld_Star_Base_4_Death_Clone", "Tag: Space_Model_Name", - "W_STARS_CINE.ALO" + "UB_04_STATION_D.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_desert_ground_dust", - "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027UI_SABOTEUR.ALO\u0027", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_sith_lefthall--\u003ETag: Land_Model_Name--\u003EW_SITH_LEFTHALL.ALO].", "context": [ - "Underworld_Saboteur", + "Cin_sith_lefthall", "Tag: Land_Model_Name", - "UI_SABOTEUR.ALO" + "W_SITH_LEFTHALL.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "p_uwstation_death", - "message": "Proxy particle \u0027p_uwstation_death\u0027 not found for model \u0027UB_04_STATION_D.ALO\u0027", + "asset": "p_smoke_small_thin2", + "message": "Proxy particle \u0027p_smoke_small_thin2\u0027 not found for model \u0027NB_MONCAL_BUILDING.ALO\u0027", "context": [ - "Underworld_Star_Base_4_Death_Clone", - "Tag: Space_Model_Name", - "UB_04_STATION_D.ALO" + "MonCalamari_Spawn_House", + "Tag: Land_Model_Name", + "NB_MONCAL_BUILDING.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_DSTAR_LEVERPANEL.ALO", - "message": "Unable to find Alamo file \u0027Cin_DStar_LeverPanel.alo\u0027", + "asset": "W_SWAMPGASEMIT.ALO", + "message": "Unable to find Alamo file \u0027W_SwampGasEmit.ALO\u0027", "context": [ - "Death_Star_LeverPanel", - "Tag: Space_Model_Name" + "Prop_SwampGasEmitter", + "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_TE_Rock_f_02_b.tga", - "message": "Could not find texture \u0027W_TE_Rock_f_02_b.tga\u0027 for context: [The_Peacebringer--\u003ETag: Space_Model_Name--\u003EUV_KRAYTCLASSDESTROYER_TYBER.ALO].", + "asset": "Cin_Reb_CelebHall_Wall.tga", + "message": "Could not find texture \u0027Cin_Reb_CelebHall_Wall.tga\u0027 for context: [Cin_w_tile--\u003ETag: Land_Model_Name--\u003EW_TILE.ALO].", "context": [ - "The_Peacebringer", - "Tag: Space_Model_Name", - "UV_KRAYTCLASSDESTROYER_TYBER.ALO" + "Cin_w_tile", + "Tag: Land_Model_Name", + "W_TILE.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "W_BUSH_SWMP00.ALO", - "message": "Unable to find Alamo file \u0027W_Bush_Swmp00.ALO\u0027", + "asset": "CIN_P_PROTON_TORPEDO.ALO", + "message": "Unable to find Alamo file \u0027CIN_p_proton_torpedo.alo\u0027", "context": [ - "Prop_Swamp_Bush00", + "Cin_Proj_Ground_Proton_Torpedo", "Tag: Land_Model_Name" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_RBEL_SOLDIER.ALO", - "message": "Unable to find Alamo file \u0027CIN_Rbel_Soldier.alo\u0027", + "asset": "p_desert_ground_dust", + "message": "Proxy particle \u0027p_desert_ground_dust\u0027 not found for model \u0027UI_IG88.ALO\u0027", "context": [ - "Cin_Rebel_soldier", - "Tag: Land_Model_Name" + "IG-88", + "Tag: Land_Model_Name", + "UI_IG88.ALO" ] }, { "id": "FILE00", "severity": "Error", - "asset": "CIN_RBEL_SOLDIER_GROUP.ALO", - "message": "Unable to find Alamo file \u0027CIN_Rbel_Soldier_Group.alo\u0027", + "asset": "CIN_EV_TIEADVANCED.ALO", + "message": "Unable to find Alamo file \u0027Cin_EV_TieAdvanced.alo\u0027", "context": [ - "Cin_Rebel_SoldierRow", - "Tag: Land_Model_Name" + "Fin_Vader_TIE", + "Tag: Space_Model_Name" ] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "zoomed_back", - "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", + "asset": "g_planet_value", + "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "zoomed_header_text", - "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", + "asset": "st_power", + "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_planet_value", - "message": "The CommandBar component \u0027g_planet_value\u0027 is not connected to a shell component.", + "asset": "g_planet_land_forces", + "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "generic_collision", - "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", + "asset": "st_bracket_large", + "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_corruption_icon", - "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", + "asset": "objective_text", + "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "encyclopedia_right_text", - "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", + "asset": "zoomed_center_text", + "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_ground_level_pips", - "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", + "asset": "objective_header_text", + "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "zoomed_center_text", - "message": "The CommandBar component \u0027zoomed_center_text\u0027 is not connected to a shell component.", + "asset": "st_hero_health", + "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_planet_name", - "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", + "asset": "zoomed_back", + "message": "The CommandBar component \u0027zoomed_back\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "generic_flytext", - "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", + "asset": "st_control_group", + "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_health_medium", - "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", + "asset": "tooltip_back", + "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_bracket_medium", - "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", + "asset": "bm_title_4011", + "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "objective_header_text", - "message": "The CommandBar component \u0027objective_header_text\u0027 is not connected to a shell component.", + "asset": "g_planet_name", + "message": "The CommandBar component \u0027g_planet_name\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "help_back", - "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", + "asset": "skirmish_upgrade", + "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tooltip_price", - "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", + "asset": "g_conflict", + "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_political_control", - "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", + "asset": "objective_back", + "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_health_bar", - "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", + "asset": "g_radar_blip", + "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "radar_blip", - "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", + "asset": "st_bracket_medium", + "message": "The CommandBar component \u0027st_bracket_medium\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_space_level_pips", - "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "asset": "st_health", + "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "cs_ability_button", - "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", + "asset": "g_space_icon", + "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "zoomed_cost_text", - "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", + "asset": "st_shields_large", + "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_planet_fleet", - "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", + "asset": "generic_flytext", + "message": "The CommandBar component \u0027generic_flytext\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_space_level", - "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", + "asset": "encyclopedia_text", + "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "remote_bomb_icon", - "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", + "asset": "st_hero_icon", + "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_ground_sell", - "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", + "asset": "bribe_display", + "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "encyclopedia_cost_text", - "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", + "asset": "st_health_medium", + "message": "The CommandBar component \u0027st_health_medium\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_special_ability", - "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", + "asset": "tactical_sell", + "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_planet_ability", - "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", + "asset": "g_planet_ring", + "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_garrison_icon", - "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", + "asset": "balance_pip", + "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_space_icon", - "message": "The CommandBar component \u0027g_space_icon\u0027 is not connected to a shell component.", + "asset": "g_ground_sell", + "message": "The CommandBar component \u0027g_ground_sell\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "garrison_slot_icon", - "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", + "asset": "bm_title_4010", + "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "encyclopedia_icon", - "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", + "asset": "gui_dialog_tooltip", + "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_planet_ring", - "message": "The CommandBar component \u0027g_planet_ring\u0027 is not connected to a shell component.", + "asset": "b_planet_left", + "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_radar_blip", - "message": "The CommandBar component \u0027g_radar_blip\u0027 is not connected to a shell component.", + "asset": "encyclopedia_right_text", + "message": "The CommandBar component \u0027encyclopedia_right_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tooltip_left_text", - "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", + "asset": "help_back", + "message": "The CommandBar component \u0027help_back\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "objective_text", - "message": "The CommandBar component \u0027objective_text\u0027 is not connected to a shell component.", + "asset": "g_ground_level_pips", + "message": "The CommandBar component \u0027g_ground_level_pips\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "zoomed_right_text", - "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", + "asset": "objective_icon", + "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "context": [] + }, + { + "id": "CMDBAR04", + "severity": "Warning", + "asset": "g_space_level_pips", + "message": "The CommandBar component \u0027g_space_level_pips\u0027 is not connected to a shell component.", + "context": [] + }, + { + "id": "CMDBAR04", + "severity": "Warning", + "asset": "g_planet_fleet", + "message": "The CommandBar component \u0027g_planet_fleet\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tutorial_text", - "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", + "asset": "g_planet_ability", + "message": "The CommandBar component \u0027g_planet_ability\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "b_planet_right", - "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", + "asset": "garrison_respawn_counter", + "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_health", - "message": "The CommandBar component \u0027st_health\u0027 is not connected to a shell component.", + "asset": "encyclopedia_back", + "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", "context": [] }, { @@ -2592,252 +2595,235 @@ "message": "The CommandBar component \u0027g_ground_level\u0027 is not connected to a shell component.", "context": [] }, - { - "id": "CMDBAR03", - "severity": "Information", - "asset": "g_credit_bar", - "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", - "context": [] - }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_conflict", - "message": "The CommandBar component \u0027g_conflict\u0027 is not connected to a shell component.", + "asset": "lt_weather_icon", + "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "encyclopedia_header_text", - "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", + "asset": "g_radar_view", + "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_hero_icon", - "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", + "asset": "g_smuggler", + "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "bribe_display", - "message": "The CommandBar component \u0027bribe_display\u0027 is not connected to a shell component.", + "asset": "zoomed_text", + "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_bracket_large", - "message": "The CommandBar component \u0027st_bracket_large\u0027 is not connected to a shell component.", + "asset": "tooltip_name", + "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_build", - "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", + "asset": "g_credit_bar", + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "lt_weather_icon", - "message": "The CommandBar component \u0027lt_weather_icon\u0027 is not connected to a shell component.", + "asset": "tooltip_left_text", + "message": "The CommandBar component \u0027tooltip_left_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_bracket_small", - "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", + "asset": "st_ability_icon", + "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "objective_icon", - "message": "The CommandBar component \u0027objective_icon\u0027 is not connected to a shell component.", + "asset": "tutorial_text", + "message": "The CommandBar component \u0027tutorial_text\u0027 is not connected to a shell component.", "context": [] }, - { - "id": "FILE00", - "severity": "Error", - "asset": "i_button_blank.tga", - "message": "Could not find texture \u0027i_button_blank.tga\u0027 for context: [map_shell--\u003EPLANETARY_MODE.ALO].", - "context": [ - "map_shell", - "PLANETARY_MODE.ALO" - ] - }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_radar_view", - "message": "The CommandBar component \u0027g_radar_view\u0027 is not connected to a shell component.", + "asset": "g_corruption_text", + "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_hero", - "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", + "asset": "tooltip_icon_land", + "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "gui_dialog_tooltip", - "message": "The CommandBar component \u0027gui_dialog_tooltip\u0027 is not connected to a shell component.", + "asset": "st_bracket_small", + "message": "The CommandBar component \u0027st_bracket_small\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_shields_large", - "message": "The CommandBar component \u0027st_shields_large\u0027 is not connected to a shell component.", + "asset": "b_beacon_t", + "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_control_group", - "message": "The CommandBar component \u0027st_control_group\u0027 is not connected to a shell component.", + "asset": "tutorial_text_back", + "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", "context": [] }, { - "id": "CMDBAR04", - "severity": "Warning", - "asset": "b_planet_left", - "message": "The CommandBar component \u0027b_planet_left\u0027 is not connected to a shell component.", + "id": "CMDBAR03", + "severity": "Information", + "asset": "g_credit_bar", + "message": "The CommandBar component \u0027g_credit_bar\u0027 is not supported by the game.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "objective_back", - "message": "The CommandBar component \u0027objective_back\u0027 is not connected to a shell component.", + "asset": "g_enemy_hero", + "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tooltip_icon_land", - "message": "The CommandBar component \u0027tooltip_icon_land\u0027 is not connected to a shell component.", + "asset": "g_political_control", + "message": "The CommandBar component \u0027g_political_control\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "balance_pip", - "message": "The CommandBar component \u0027balance_pip\u0027 is not connected to a shell component.", + "asset": "bribed_icon", + "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tooltip_icon", - "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", + "asset": "st_health_bar", + "message": "The CommandBar component \u0027st_health_bar\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_planet_land_forces", - "message": "The CommandBar component \u0027g_planet_land_forces\u0027 is not connected to a shell component.", + "asset": "g_hero", + "message": "The CommandBar component \u0027g_hero\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_hero_health", - "message": "The CommandBar component \u0027st_hero_health\u0027 is not connected to a shell component.", + "asset": "st_health_large", + "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "skirmish_upgrade", - "message": "The CommandBar component \u0027skirmish_upgrade\u0027 is not connected to a shell component.", + "asset": "tooltip_icon", + "message": "The CommandBar component \u0027tooltip_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_health_large", - "message": "The CommandBar component \u0027st_health_large\u0027 is not connected to a shell component.", + "asset": "b_planet_right", + "message": "The CommandBar component \u0027b_planet_right\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tactical_sell", - "message": "The CommandBar component \u0027tactical_sell\u0027 is not connected to a shell component.", + "asset": "cs_ability_text", + "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "encyclopedia_text", - "message": "The CommandBar component \u0027encyclopedia_text\u0027 is not connected to a shell component.", + "asset": "encyclopedia_center_text", + "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_weather", - "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", + "asset": "zoomed_right_text", + "message": "The CommandBar component \u0027zoomed_right_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_power", - "message": "The CommandBar component \u0027st_power\u0027 is not connected to a shell component.", + "asset": "g_bounty_hunter", + "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "b_quick_ref", - "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", + "asset": "g_hero_icon", + "message": "The CommandBar component \u0027g_hero_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_ground_icon", - "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", + "asset": "b_quick_ref", + "message": "The CommandBar component \u0027b_quick_ref\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "reinforcement_counter", - "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", + "asset": "generic_collision", + "message": "The CommandBar component \u0027generic_collision\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "bm_title_4011", - "message": "The CommandBar component \u0027bm_title_4011\u0027 is not connected to a shell component.", + "asset": "g_build", + "message": "The CommandBar component \u0027g_build\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_ability_icon", - "message": "The CommandBar component \u0027st_ability_icon\u0027 is not connected to a shell component.", + "asset": "g_corruption_icon", + "message": "The CommandBar component \u0027g_corruption_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "encyclopedia_back", - "message": "The CommandBar component \u0027encyclopedia_back\u0027 is not connected to a shell component.", + "asset": "g_smuggled", + "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", "context": [] }, { @@ -2850,141 +2836,144 @@ { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_credit_bar", - "message": "The CommandBar component \u0027g_credit_bar\u0027 is not connected to a shell component.", + "asset": "reinforcement_counter", + "message": "The CommandBar component \u0027reinforcement_counter\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_hero_icon", - "message": "The CommandBar component \u0027st_hero_icon\u0027 is not connected to a shell component.", + "asset": "g_space_level", + "message": "The CommandBar component \u0027g_space_level\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_corruption_text", - "message": "The CommandBar component \u0027g_corruption_text\u0027 is not connected to a shell component.", + "asset": "remote_bomb_icon", + "message": "The CommandBar component \u0027remote_bomb_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "cs_ability_text", - "message": "The CommandBar component \u0027cs_ability_text\u0027 is not connected to a shell component.", + "asset": "st_garrison_icon", + "message": "The CommandBar component \u0027st_garrison_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tutorial_text_back", - "message": "The CommandBar component \u0027tutorial_text_back\u0027 is not connected to a shell component.", + "asset": "cs_ability_button", + "message": "The CommandBar component \u0027cs_ability_button\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_shields_medium", - "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", + "asset": "zoomed_cost_text", + "message": "The CommandBar component \u0027zoomed_cost_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "bribed_icon", - "message": "The CommandBar component \u0027bribed_icon\u0027 is not connected to a shell component.", + "asset": "encyclopedia_icon", + "message": "The CommandBar component \u0027encyclopedia_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_enemy_hero", - "message": "The CommandBar component \u0027g_enemy_hero\u0027 is not connected to a shell component.", + "asset": "surface_mod_icon", + "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_smuggler", - "message": "The CommandBar component \u0027g_smuggler\u0027 is not connected to a shell component.", + "asset": "g_special_ability", + "message": "The CommandBar component \u0027g_special_ability\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "st_shields", - "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", + "asset": "tooltip_price", + "message": "The CommandBar component \u0027tooltip_price\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_smuggled", - "message": "The CommandBar component \u0027g_smuggled\u0027 is not connected to a shell component.", + "asset": "encyclopedia_header_text", + "message": "The CommandBar component \u0027encyclopedia_header_text\u0027 is not connected to a shell component.", "context": [] }, { - "id": "CMDBAR04", - "severity": "Warning", - "asset": "garrison_respawn_counter", - "message": "The CommandBar component \u0027garrison_respawn_counter\u0027 is not connected to a shell component.", - "context": [] + "id": "FILE00", + "severity": "Error", + "asset": "i_button_blank.tga", + "message": "Could not find texture \u0027i_button_blank.tga\u0027 for context: [map_shell--\u003EPLANETARY_MODE.ALO].", + "context": [ + "map_shell", + "PLANETARY_MODE.ALO" + ] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "b_beacon_t", - "message": "The CommandBar component \u0027b_beacon_t\u0027 is not connected to a shell component.", + "asset": "garrison_slot_icon", + "message": "The CommandBar component \u0027garrison_slot_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "bm_title_4010", - "message": "The CommandBar component \u0027bm_title_4010\u0027 is not connected to a shell component.", + "asset": "st_shields_medium", + "message": "The CommandBar component \u0027st_shields_medium\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "zoomed_text", - "message": "The CommandBar component \u0027zoomed_text\u0027 is not connected to a shell component.", + "asset": "g_ground_icon", + "message": "The CommandBar component \u0027g_ground_icon\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "encyclopedia_center_text", - "message": "The CommandBar component \u0027encyclopedia_center_text\u0027 is not connected to a shell component.", + "asset": "encyclopedia_cost_text", + "message": "The CommandBar component \u0027encyclopedia_cost_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "surface_mod_icon", - "message": "The CommandBar component \u0027surface_mod_icon\u0027 is not connected to a shell component.", + "asset": "g_weather", + "message": "The CommandBar component \u0027g_weather\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tooltip_back", - "message": "The CommandBar component \u0027tooltip_back\u0027 is not connected to a shell component.", + "asset": "zoomed_header_text", + "message": "The CommandBar component \u0027zoomed_header_text\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "tooltip_name", - "message": "The CommandBar component \u0027tooltip_name\u0027 is not connected to a shell component.", + "asset": "radar_blip", + "message": "The CommandBar component \u0027radar_blip\u0027 is not connected to a shell component.", "context": [] }, { "id": "CMDBAR04", "severity": "Warning", - "asset": "g_bounty_hunter", - "message": "The CommandBar component \u0027g_bounty_hunter\u0027 is not connected to a shell component.", + "asset": "st_shields", + "message": "The CommandBar component \u0027st_shields\u0027 is not connected to a shell component.", "context": [] } ] diff --git a/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs b/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs index e3af949e..03166fee 100644 --- a/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs +++ b/src/ModVerify.CliApp/Updates/ModVerifyUpdater.cs @@ -85,7 +85,7 @@ await updater.CheckForUpdateAsync(branch, CancellationToken.None), if (mode == ModVerifyUpdateMode.InteractiveUpdate) { - var shallUpdate = ConsoleUtilities.UserYesNoQuestion("Do you want to update now?"); + var shallUpdate = ConsoleUtilities.UserYesNoQuestion("Do you want to update now?", defaultAnswer: true); if (!shallUpdate) return; } diff --git a/src/ModVerify/AssemblyInfo.cs b/src/ModVerify/AssemblyInfo.cs new file mode 100644 index 00000000..b2805cc0 --- /dev/null +++ b/src/ModVerify/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ModVerify.Test")] diff --git a/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs index c8cae051..6410e8d4 100644 --- a/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs +++ b/src/ModVerify/Reporting/Engine/XmlParseErrorReporter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO.Abstractions; -using System.Xml; using AET.ModVerify.Utilities; using AET.ModVerify.Verifiers; using Microsoft.Extensions.DependencyInjection; diff --git a/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs index 7b4433db..79892f70 100644 --- a/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs +++ b/src/ModVerify/Reporting/Reporters/Console/ConsoleReporter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -24,6 +24,9 @@ private void PrintErrorStats(VerificationResult verificationResult, List x.Value.Count); + if (resolvedCount == 0) + return; + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine( + $"Reduced issues: {resolvedCount} error(s) present in the baseline are no longer reported."); + Console.ResetColor(); + + if (Settings.Verbose +#if DEBUG + || true +#endif + ) + { + foreach (var baseline in resolvedErrors) + { + foreach (var error in baseline.Value) + Console.WriteLine($" [Resolved] [{baseline.Key}] [{error.Id}] Message={error.Message}"); + } + } + + Console.WriteLine(); + } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs b/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs index 690d2505..86a7676c 100644 --- a/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs +++ b/src/ModVerify/Verifiers/Commons/SingleModelVerifier.cs @@ -236,7 +236,7 @@ private void VerifyModelClass(ModelClass modelClass, IReadOnlyCollection if (!CheckBinaryCorruptedFileIsActuallyRenderable(alaFileName, out var actualFilePath)) { var message = - $"Possible file CRC32 collision: '{fileName}' was requested but '{actualFilePath}' was found by the engine."; + $"Possible file CRC32 collision: '{alaFileName}' was requested but '{actualFilePath}' was found by the engine."; AddError(VerificationError.Create( this, VerifierErrorCodes.UnexpectedFileLoad, @@ -246,7 +246,7 @@ private void VerifyModelClass(ModelClass modelClass, IReadOnlyCollection // there are simply more chances for a CRC32 collision. VerificationSeverity.Information, contextInfo, - NormalizeFileName(fileName))); + NormalizeFileName(alaFileName))); } else { diff --git a/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs b/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs index e254a782..58c8753c 100644 --- a/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs +++ b/src/ModVerify/Verifiers/Engine/HardcodedAssetsVerifier.cs @@ -1,8 +1,9 @@ -using System; -using System.Threading; -using AET.ModVerify.Settings; +using AET.ModVerify.Settings; using AET.ModVerify.Verifiers.Commons; using PG.StarWarsGame.Engine; +using System; +using System.Threading; +using AET.ModVerify.Reporting; namespace AET.ModVerify.Verifiers.Engine; @@ -18,7 +19,9 @@ public HardcodedAssetsVerifier(IStarWarsGameEngine gameEngine, GameVerifySetting public override void Verify(CancellationToken token) { - OnProgress(0.0d, "Verifying Hardcoded Models"); + OnProgress(0.0d, "Verifying Hardcoded Shaders"); + VerifyShaders(token); + OnProgress(0.5d, "Verifying Hardcoded Models"); VerifyModels(token); OnProgress(1.0, null); } @@ -33,4 +36,31 @@ private void VerifyModels(CancellationToken token) foreach (var error in _modelVerifier.VerifyErrors) AddError(error); } + + // TODO: Create a shader verifier that reports a warning if a shader is located at the game's root + // as this can cause compatibility issues with mods and in general is not recommended. + + private void VerifyShaders(CancellationToken token) + { + var repo = GameEngine.GameRepository.EffectsRepository; + // The engine loads the following shaders at startup + foreach (var shadersName in HardcodedEngineAssets.HardcodedEngineShadersNames) + { + token.ThrowIfCancellationRequested(); + + if (!repo.FileExists(shadersName)) + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Unable to find shader '{shadersName}'", VerificationSeverity.Error, [], shadersName)); + } + + // The engine loads the following shaders on terrain load. For simplicity, we try to find them once here + foreach (var shadersName in HardcodedEngineAssets.HardcodedTerrainShadersNames) + { + token.ThrowIfCancellationRequested(); + + if (!repo.FileExists(shadersName)) + AddError(VerificationError.Create(this, VerifierErrorCodes.FileNotFound, + $"Unable to find terrain shader '{shadersName}'", VerificationSeverity.Error, [], shadersName)); + } + } } \ No newline at end of file diff --git a/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs b/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs index 5c567f07..1bbc5b03 100644 --- a/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs +++ b/src/ModVerify/Verifiers/GuiDialogs/GuiDialogsVerifier.cs @@ -14,7 +14,7 @@ namespace AET.ModVerify.Verifiers.GuiDialogs; -sealed class GuiDialogsVerifier : GameVerifier +public sealed class GuiDialogsVerifier : GameVerifier { internal const string DefaultComponentIdentifier = "<>"; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/IO/PetroglyphFileSystemTests.CombineJoin.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/IO/PetroglyphFileSystemTests.CombineJoin.cs index 5da9a79c..a2e62d5b 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/IO/PetroglyphFileSystemTests.CombineJoin.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/IO/PetroglyphFileSystemTests.CombineJoin.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using PG.StarWarsGame.Engine.IO; using PG.StarWarsGame.Engine.Utilities; using Xunit; #if NETFRAMEWORK @@ -10,34 +11,37 @@ namespace PG.StarWarsGame.Engine.FileSystem.Test.IO; public partial class PetroglyphFileSystemTests { + private static string Sep = PetroglyphFileSystem.DirectorySeparatorChar.ToString(); + private static string AltSep = PetroglyphFileSystem.AltDirectorySeparatorChar.ToString(); + private static string OsSep = Path.DirectorySeparatorChar.ToString(); + + // Mirrors TestData_JoinTwoPaths, but with CombinePath semantics: a rooted second path replaces the + // first entirely. Null cases are omitted because CombinePath throws on null (see the dedicated facts). + public static TheoryData TestData_CombineTwoPaths = new() + { + { "", "", "" }, + { Sep, "", Sep }, + { AltSep, "", AltSep }, + { "", Sep, Sep }, + { "", AltSep, AltSep }, + { Sep, Sep, Sep }, + { AltSep, AltSep, AltSep }, + { "a", "", "a" }, + { "", "a", "a" }, + { "a", "a", $"a{OsSep}a" }, + { $"a{Sep}", "a", $"a{Sep}a" }, + { "a", $"{Sep}a", $"{Sep}a" }, + { $"a{Sep}", $"{Sep}a", $"{Sep}a" }, + { "a", $"a{Sep}", $"a{OsSep}a{Sep}" }, + { $"a{AltSep}", "a", $"a{AltSep}a" }, + { "a", $"{AltSep}a", $"{AltSep}a" }, + { $"a{Sep}", $"{AltSep}a", $"{AltSep}a" }, + { $"a{AltSep}", $"{AltSep}a", $"{AltSep}a" }, + { "a", $"a{AltSep}", $"a{OsSep}a{AltSep}" } + }; + [Theory] -#if Windows - [InlineData("a", "b", "a\\b")] - [InlineData("a/", "b", "a/b")] - [InlineData("a\\", "b", "a\\b")] - [InlineData("", "b", "b")] - [InlineData("a", "", "a")] - [InlineData("/", "b", "/b")] - [InlineData("a", "/b", "/b")] - [InlineData("a", "\\b", "\\b")] - [InlineData("a/b", "c/d", "a/b\\c/d")] - [InlineData("a\\b", "c\\d", "a\\b\\c\\d")] - [InlineData("a/b/", "c/d", "a/b/c/d")] - [InlineData("a\\b\\", "c\\d", "a\\b\\c\\d")] -#else - [InlineData("a", "b", "a/b")] - [InlineData("a/", "b", "a/b")] - [InlineData("a\\", "b", "a\\b")] - [InlineData("", "b", "b")] - [InlineData("a", "", "a")] - [InlineData("/", "b", "/b")] - [InlineData("a", "/b", "/b")] - [InlineData("a", "\\b", "\\b")] - [InlineData("a/b", "c/d", "a/b/c/d")] - [InlineData("a\\b", "c\\d", "a\\b/c\\d")] - [InlineData("a/b/", "c/d", "a/b/c/d")] - [InlineData("a\\b\\", "c\\d", "a\\b\\c\\d")] -#endif + [MemberData(nameof(TestData_CombineTwoPaths))] public void CombinePath(string pathA, string pathB, string expected) { var result = _pgFileSystem.CombinePath(pathA, pathB); @@ -47,31 +51,35 @@ public void CombinePath(string pathA, string pathB, string expected) #endif } + public static TheoryData TestData_JoinTwoPaths = new() + { + { "", "", "" }, + { Sep, "", Sep }, + { AltSep, "", AltSep }, + { "", Sep, Sep }, + { "", AltSep, AltSep }, + { Sep, Sep, $"{Sep}{Sep}" }, + { AltSep, AltSep, $"{AltSep}{AltSep}" }, + { "a", "", "a" }, + { "", "a", "a" }, + { "a", "a", $"a{OsSep}a" }, + { $"a{Sep}", "a", $"a{Sep}a" }, + { "a", $"{Sep}a", $"a{Sep}a" }, + { $"a{Sep}", $"{Sep}a", $"a{Sep}{Sep}a" }, + { "a", $"a{Sep}", $"a{OsSep}a{Sep}" }, + { $"a{AltSep}", "a", $"a{AltSep}a" }, + { "a", $"{AltSep}a", $"a{AltSep}a" }, + { $"a{Sep}", $"{AltSep}a", $"a{Sep}{AltSep}a" }, + { $"a{AltSep}", $"{AltSep}a", $"a{AltSep}{AltSep}a" }, + { "a", $"a{AltSep}", $"a{OsSep}a{AltSep}" }, + { null, null, ""}, + { null, "a", "a"}, + { "a", null, "a"} + }; + [Theory] -#if Windows - [InlineData("a", "b", "a\\b")] - [InlineData("a/", "b", "a/b")] - [InlineData("a\\", "b", "a\\b")] - [InlineData("", "b", "b")] - [InlineData("a", "", "a")] - [InlineData("/", "b", "/b")] - [InlineData("a", "/b", "a/b")] - [InlineData("a", "\\b", "a\\b")] - [InlineData("a/b", "c/d", "a/b\\c/d")] - [InlineData("a\\b", "c\\d", "a\\b\\c\\d")] -#else - [InlineData("a", "b", "a/b")] - [InlineData("a/", "b", "a/b")] - [InlineData("a\\", "b", "a\\b")] - [InlineData("", "b", "b")] - [InlineData("a", "", "a")] - [InlineData("/", "b", "/b")] - [InlineData("a", "/b", "a/b")] - [InlineData("a", "\\b", "a\\b")] - [InlineData("a/b", "c/d", "a/b/c/d")] - [InlineData("a\\b", "c\\d", "a\\b/c\\d")] -#endif - public void JoinPath(string path1, string path2, string expected) + [MemberData(nameof(TestData_JoinTwoPaths))] + public void JoinPath(string? path1, string? path2, string expected) { var vsb = new ValueStringBuilder(); try @@ -89,6 +97,54 @@ public void JoinPath(string path1, string path2, string expected) } } + public static TheoryData TestData_JoinThreePaths = new() + { + { "", "", "", "" }, + { Sep, Sep, Sep, $"{Sep}{Sep}{Sep}" }, + { AltSep, AltSep, AltSep, $"{AltSep}{AltSep}{AltSep}" }, + { "a", "", "", "a" }, + { "", "a", "", "a" }, + { "", "", "a", "a" }, + { "a", "", "a", $"a{OsSep}a" }, + { "a", "a", "", $"a{OsSep}a" }, + { "", "a", "a", $"a{OsSep}a" }, + { "a", "a", "a", $"a{OsSep}a{OsSep}a" }, + { "a", Sep, "a", $"a{Sep}a" }, + { $"a{Sep}", "", "a", $"a{Sep}a" }, + { $"a{Sep}", "a", "", $"a{Sep}a" }, + { "", $"a{Sep}", "a", $"a{Sep}a" }, + { "a", "", $"{Sep}a", $"a{Sep}a" }, + { $"a{AltSep}", "", "a", $"a{AltSep}a" }, + { $"a{AltSep}", "a", "", $"a{AltSep}a" }, + { "", $"a{AltSep}", "a", $"a{AltSep}a" }, + { "a", "", $"{AltSep}a", $"a{AltSep}a" }, + { null, null, null, "" }, + { "a", null, null, "a" }, + { null, "a", null, "a" }, + { null, null, "a", "a" }, + { "a", null, "a", $"a{OsSep}a" } + }; + + [Theory] + [MemberData(nameof(TestData_JoinThreePaths))] + public void JoinPath_ThreePaths(string? path1, string? path2, string? path3, string expected) + { + var vsb = new ValueStringBuilder(); + try + { + _pgFileSystem.JoinPath(path1.AsSpan(), path2.AsSpan(), path3.AsSpan(), ref vsb); + var result = vsb.ToString(); + Assert.Equal(expected, result); +#if Windows + Assert.Equal(result, _fileSystem.Path.Join(path1, path2, path3)); +#endif + } + finally + { + vsb.Dispose(); + } + } + [Fact] public void CombinePath_FirstArgNull_Throws() { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/PG.StarWarsGame.Engine.FileSystem.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/PG.StarWarsGame.Engine.FileSystem.Test.csproj index 826878ed..cf1c3501 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/PG.StarWarsGame.Engine.FileSystem.Test.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem.Test/PG.StarWarsGame.Engine.FileSystem.Test.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.CombineJoin.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.CombineJoin.cs index b89a8efb..e7d4afbd 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.CombineJoin.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.CombineJoin.cs @@ -38,7 +38,7 @@ public string CombinePath(string pathA, string pathB) throw new ArgumentNullException(nameof(pathB)); return CombineInternal(pathA, pathB); } - + internal void JoinPath(ReadOnlySpan path1, ReadOnlySpan path2, ref ValueStringBuilder stringBuilder) { if (path1.Length == 0 && path2.Length == 0) @@ -59,7 +59,44 @@ internal void JoinPath(ReadOnlySpan path1, ReadOnlySpan path2, ref V stringBuilder.Append(path2); } - + + internal void JoinPath(ReadOnlySpan path1, ReadOnlySpan path2, ReadOnlySpan path3, ref ValueStringBuilder stringBuilder) + { + if (path1.IsEmpty) + { + JoinPath(path2, path3, ref stringBuilder); + return; + } + + if (path2.IsEmpty) + { + JoinPath(path1, path3, ref stringBuilder); + return; + } + + if (path3.IsEmpty) + { + JoinPath(path1, path2, ref stringBuilder); + return; + } + + stringBuilder.Append(path1); + + var firstHasSeparator = IsDirectorySeparator(path1[path1.Length - 1]) || IsDirectorySeparator(path2[0]); + var secondHasSeparator = IsDirectorySeparator(path2[path2.Length - 1]) || IsDirectorySeparator(path3[0]); + + if (!firstHasSeparator) + stringBuilder.Append(UnderlyingFileSystem.Path.DirectorySeparatorChar); + + stringBuilder.Append(path2); + + if (!secondHasSeparator) + stringBuilder.Append(UnderlyingFileSystem.Path.DirectorySeparatorChar); + + stringBuilder.Append(path3); + } + + private string CombineInternal(string first, string second) { if (string.IsNullOrEmpty(first)) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.cs index 09bcf729..b0969ca3 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.cs @@ -16,8 +16,8 @@ namespace PG.StarWarsGame.Engine.IO; /// public sealed partial class PetroglyphFileSystem { - private const char DirectorySeparatorChar = '/'; - private const char AltDirectorySeparatorChar = '\\'; + internal const char DirectorySeparatorChar = '/'; + internal const char AltDirectorySeparatorChar = '\\'; // ReSharper disable once InconsistentNaming private static readonly PathNormalizeOptions PGFileSystemDirectorySeparatorNormalizeOptions = new() diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/CaseInsensitivityFixture.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/CaseInsensitivityFixture.cs new file mode 100644 index 00000000..867b83cf --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/CaseInsensitivityFixture.cs @@ -0,0 +1,22 @@ +using System; +using PG.StarWarsGame.Engine.IO; +using PG.StarWarsGame.Engine.Testing; + +namespace PG.StarWarsGame.Engine.Test; + +/// +/// Represents a test fixture for verifying that repositories resolve requests in a case-insensitive manner. +/// +/// Callback that writes fixtures into the virtual game's ConfigureGame origin. +/// Picks the repository under test from the constructed . +/// Lookup key the repository should resolve to the filesystem-backed fixture. +/// Expected content at . +/// Lookup key the repository should resolve to the MEG-backed fixture. +/// Expected content at . +public sealed record CaseInsensitivityFixture( + Action PopulateGame, + Func SelectRepository, + string FilesystemLookup, + string FilesystemContent, + string MegLookup, + string MegContent); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/EngineRepositoryTestBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/EngineRepositoryTestBase.cs new file mode 100644 index 00000000..f1e9553f --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/EngineRepositoryTestBase.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Text; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.IO; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Testing; +using Testably.Abstractions; +using Xunit; + +namespace PG.StarWarsGame.Engine.Test; + +/// +/// Base class for engine-bound repository tests. +/// +public abstract class EngineRepositoryTestBase : EngineTestBase +{ + /// Gets the engine targeted by this test class. + protected abstract GameEngineType Engine { get; } + + /// Whether the repository resolves a file requested by name only, without its directory. + protected abstract bool ResolvesFileNameWithoutDirectory { get; } + + /// Whether the repository surfaces the path-too-long condition for an overlong request. + protected abstract bool SurfacesPathTooLong { get; } + + /// + /// The repository origins in descending lookup priority for this test class's . + /// + private IReadOnlyList ExpectedLoadOrder => Engine switch + { + GameEngineType.Foc => + [ + RepositoryLayer.Mod, + RepositoryLayer.Game, + RepositoryLayer.MasterMeg, + RepositoryLayer.Fallback, + ], + GameEngineType.Eaw => + [ + RepositoryLayer.Mod, + RepositoryLayer.Game, + RepositoryLayer.Fallback, + RepositoryLayer.MasterMeg, + ], + _ => throw new ArgumentOutOfRangeException() + }; + + /// + /// Builds a instance that provides test data and logic + /// for verifying case and separator insensitivity in repository lookups. + /// + /// + /// Path casing and separators automatically randomized by the underlying test logic. + /// + /// + /// A containing the configuration and data + /// necessary for case insensitivity tests. + /// + protected abstract CaseInsensitivityFixture BuildCaseInsensitivityFixture(); + + /// A simple fixture describing basic file existence tests for this test class's repository. + protected abstract RepositoryFixture RepositoryFixture { get; } + + protected sealed override IFileSystem CreateFileSystem() + { + // Real file system is required to test integration + // with PG.StarWarsGame.Engine.FileSystem + return new RealFileSystem(); + } + + /// Creates a builder bound to the test base's service provider. + protected VirtualGameRepoBuilder CreateBuilder() + { + return new VirtualGameRepoBuilder(ServiceProvider); + } + + /// Constructs an for this test class's . + /// The returned repository is sealed against further MEG modifications, matching the engine-init lifecycle. + protected IGameRepository CreateRepository(VirtualGameRepo repo) + { + return CreateRepository(Engine, repo, errorReporter: null); + } + + /// Constructs an for this test class's with a custom error reporter + /// to observe init-time assertions (e.g. for missing patches). + protected IGameRepository CreateRepository(VirtualGameRepo repo, IGameEngineErrorReporter? errorReporter) + { + return CreateRepository(Engine, repo, errorReporter); + } + + private GameRepository CreateRepository(GameEngineType engine, VirtualGameRepo repo, IGameEngineErrorReporter? errorReporter) + { + if (repo == null) + throw new ArgumentNullException(nameof(repo)); + + var factory = new GameRepositoryFactory(ServiceProvider); + var wrapper = new GameEngineErrorReporterWrapper(errorReporter); + var gameRepo = factory.Create(engine, repo.GameLocations, wrapper); + gameRepo.Seal(); + return gameRepo; + } + + /// Reads the stream to end as UTF-8 text and disposes it. Convenient for inspecting OpenFile results. + protected static string ReadAll(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using (stream) + using (var reader = new StreamReader(stream, Encoding.UTF8)) + return reader.ReadToEnd(); + } + + public static TheoryData SupportedFileSystemStrategies() + { + var data = new TheoryData(); + foreach (var strategy in PetroglyphFileSystemTestHelpers.SupportedForCurrentOS()) + data.Add(strategy); + return data; + } + + [Theory] + [MemberData(nameof(SupportedFileSystemStrategies))] + public void Lookup_IsCaseAndSeparatorInsensitive_AcrossFilesystemAndMeg(PetroglyphFileSystemStrategy strategy) + { + var fixture = BuildCaseInsensitivityFixture(); + + using var virt = CreateBuilder() + .ConfigureGame(fixture.PopulateGame) + .Build(); + var gameRepo = CreateRepository(virt); + gameRepo.PGFileSystem.ApplyStrategy(strategy); + var repoUnderTest = fixture.SelectRepository(gameRepo); + + var separatorRandom = new Random(42); + for (var i = 0; i < 32; i++) + { + var fsVariant = JitterSeparators(string.ShuffleCasing(fixture.FilesystemLookup), separatorRandom); + var megVariant = JitterSeparators(string.ShuffleCasing(fixture.MegLookup), separatorRandom); + + Assert.True(repoUnderTest.FileExists(fsVariant), $"Filesystem variant '{fsVariant}' should resolve."); + Assert.True(repoUnderTest.FileExists(megVariant), $"MEG variant '{megVariant}' should resolve."); + Assert.Equal(fixture.FilesystemContent, ReadAll(repoUnderTest.OpenFile(fsVariant))); + Assert.Equal(fixture.MegContent, ReadAll(repoUnderTest.OpenFile(megVariant))); + } + } + + #region Asset existence + + public static TheoryData AllOrigins() + { + return [RepositoryLayer.Mod, RepositoryLayer.Game, RepositoryLayer.MasterMeg, RepositoryLayer.Fallback]; + } + + [Theory] + [MemberData(nameof(AllOrigins))] + public void FileExists_ResolvesFromOrigin(RepositoryLayer origin) + { + var fixture = RepositoryFixture; + + var builder = CreateBuilder(); + WriteLayer(builder, origin, fixture.ResolvablePath, "content"); + using var repo = builder.Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + Assert.True(repoUnderTest.FileExists(fixture.ResolvablePath)); + } + + [Theory] + [MemberData(nameof(AllOrigins))] + public void FileExists_EmptyPath_ReturnsFalse(RepositoryLayer origin) + { + var fixture = RepositoryFixture; + + var builder = CreateBuilder(); + WriteLayer(builder, origin, fixture.ResolvablePath, "content"); + using var repo = builder.Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + Assert.False(repoUnderTest.FileExists("")); + } + + [Theory] + [MemberData(nameof(AllOrigins))] + public void FileExists_NullPath_ReturnsFalse(RepositoryLayer origin) + { + var fixture = RepositoryFixture; + + var builder = CreateBuilder(); + WriteLayer(builder, origin, fixture.ResolvablePath, "content"); + using var repo = builder.Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + Assert.False(repoUnderTest.FileExists(null!)); + } + + [Fact] + public void FileExists_MissingAsset_ReturnsFalse() + { + var fixture = RepositoryFixture; + + using var repo = CreateBuilder().Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + Assert.False(repoUnderTest.FileExists(fixture.ResolvablePath)); + } + + [Fact] + public void FileExists_FileNameWithoutDirectory_ResolvesWhenSupported() + { + // The file is written at its full path; whether its name alone resolves depends on the repository + // prepending a built-in directory (effects, textures) or not (models, base lookup). + var fixture = RepositoryFixture; + var fileName = FileSystem.Path.GetFileName(fixture.ResolvablePath); + + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write(fixture.ResolvablePath, "content")) + .Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + Assert.Equal(ResolvesFileNameWithoutDirectory, repoUnderTest.FileExists(fileName)); + } + + [Fact] + public void FileExists_OverlongPath_IsMissingAndFlagsPathTooLongWhenSupported() + { + var fixture = RepositoryFixture; + + using var repo = CreateBuilder().Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + var overlong = new string('a', 300); + var found = repoUnderTest.FileExists(overlong.AsSpan(), megFileOnly: false, out var pathTooLong); + + Assert.False(found); + Assert.Equal(SurfacesPathTooLong, pathTooLong); + } + + #endregion + + #region Default loading chain priority + + [Theory] + [MemberData(nameof(SupportedFileSystemStrategies))] + public void Priority_ResolvesAccordingToEngineLoadOrder(PetroglyphFileSystemStrategy strategy) + { + var fixture = RepositoryFixture; + var order = ExpectedLoadOrder; + + // Sliding 'top' down the list makes each origin, + // in turn, the highest-priority one holding the file (Example for FOC): + // top = mod -> all four origins hold it -> mod must win + // top = game -> game, MEG, fallback hold it -> game must win (mod is empty) + // top = MEG -> MEG, fallback hold it -> MEG must win (mod, game empty) + // top = fallback -> only fallback holds it -> fallback wins + for (var top = 0; top < order.Count; top++) + { + var builder = CreateBuilder(); + for (var i = top; i < order.Count; i++) + WriteLayer(builder, order[i], fixture.ResolvablePath, order[i].ToString()); + + using var repo = builder.Build(); + var gameRepo = CreateRepository(repo); + gameRepo.PGFileSystem.ApplyStrategy(strategy); + var repoUnderTest = fixture.SelectRepository(gameRepo); + + var winner = order[top]; + Assert.Equal(winner.ToString(), ReadAll(repoUnderTest.OpenFile(fixture.ResolvablePath))); + } + } + + private static void WriteLayer(VirtualGameRepoBuilder builder, RepositoryLayer layer, string path, string content) + { + switch (layer) + { + case RepositoryLayer.Mod: + builder.WithMod("Mod", w => w.Write(path, content)); + break; + case RepositoryLayer.Game: + builder.ConfigureGame(g => g.Write(path, content)); + break; + case RepositoryLayer.MasterMeg: + builder.ConfigureGame(g => g.WriteMeg("Data/Patch.meg", meg => meg.Add(path, content))); + break; + case RepositoryLayer.Fallback: + builder.WithFallbackGame(f => f.Write(path, content)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(layer), layer, null); + } + } + + [Fact] + public void Priority_ModDeclarationOrderIsRespected() + { + var fixture = RepositoryFixture; + + using var repo = CreateBuilder() + .WithMod("ModA", m => m.Write(fixture.ResolvablePath, "A")) + .WithMod("ModB", m => m.Write(fixture.ResolvablePath, "B")) + .Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + Assert.Equal("A", ReadAll(repoUnderTest.OpenFile(fixture.ResolvablePath))); + } + + [Fact] + public void Priority_FallbackDeclarationOrderIsRespected() + { + var fixture = RepositoryFixture; + + using var repo = CreateBuilder() + .WithFallback("FallbackA", w => w.Write(fixture.ResolvablePath, "A")) + .WithFallback("FallbackB", w => w.Write(fixture.ResolvablePath, "B")) + .Build(); + var repoUnderTest = fixture.SelectRepository(CreateRepository(repo)); + + Assert.Equal("A", ReadAll(repoUnderTest.OpenFile(fixture.ResolvablePath))); + } + + #endregion + + private static string JitterSeparators(string path, Random random) + { + var chars = path.ToCharArray(); + for (var i = 0; i < chars.Length; i++) + { + if (chars[i] == '/' || chars[i] == '\\') + chars[i] = random.Next(2) == 0 ? '/' : '\\'; + } + return new string(chars); + } + + /// + /// Returns the last segment of an engine path, treating both '/' and '\' as separators. + /// + // Engine paths always use '\'; System.IO.Path.GetFileName only splits on '\' on Windows. + protected static string EngineFileName(string path) + { + return path.Substring(path.LastIndexOfAny(['/', '\\']) + 1); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/EngineTestBase.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/EngineTestBase.cs new file mode 100644 index 00000000..a97fe3f3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/EngineTestBase.cs @@ -0,0 +1,30 @@ +using AnakinRaW.CommonUtilities.Hashing; +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using PG.StarWarsGame.Files.ALO; +using PG.StarWarsGame.Files.MEG; +using PG.StarWarsGame.Files.MTD; +using PG.StarWarsGame.Files.XML; + +namespace PG.StarWarsGame.Engine.Test; + +/// +/// Represents a base class for engine-bound tests, providing the necessary service +/// registrations for constructing game repositories and related components. +/// +public abstract class EngineTestBase : TestBaseWithFileSystem +{ + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(sp => new HashingService(sp)); + + serviceCollection.SupportMTD(); + serviceCollection.SupportMEG(); + serviceCollection.SupportALO(); + serviceCollection.SupportXML(); + PetroglyphCommons.ContributeServices(serviceCollection); + PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/EffectsRepositoryTests.Existence.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/EffectsRepositoryTests.Existence.cs new file mode 100644 index 00000000..8a76e17f --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/EffectsRepositoryTests.Existence.cs @@ -0,0 +1,110 @@ +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract partial class EffectsRepositoryTests : EngineRepositoryTestBase +{ + protected override bool ResolvesFileNameWithoutDirectory => true; + + protected override bool SurfacesPathTooLong => false; + + protected override CaseInsensitivityFixture BuildCaseInsensitivityFixture() + { + return new CaseInsensitivityFixture( + PopulateGame: g => + { + g.Write("Data/Art/Shaders/MyShader.fx", "fs-fx"); + g.RegisterAndWriteMeg("Data/Shaders.meg", meg => meg.Add("Data/Art/Shaders/OtherShader.fx", "meg-fx")); + }, + SelectRepository: gameRepo => gameRepo.EffectsRepository, + FilesystemLookup: "MyShader", + FilesystemContent: "fs-fx", + MegLookup: "OtherShader", + MegContent: "meg-fx"); + } + + protected override RepositoryFixture RepositoryFixture => new( + SelectRepository: gameRepo => gameRepo.EffectsRepository, + ResolvablePath: "Data/Art/Shaders/MyShader.fx"); + + public static TheoryData ResolvableShaderLocations_Root() + { + return ShaderLocationsTheoryData( + "MyShader.fx", + "MyShader.fxo", + "MyShader.fxh"); + } + + public static TheoryData ResolvableShaderLocations_DataArts() + { + return ShaderLocationsTheoryData( + "Data/Art/Shaders/MyShader.fx", + "Data/Art/Shaders/Terrain/MyShader.fx"); + } + + [Theory] + [MemberData(nameof(ResolvableShaderLocations_Root))] + [MemberData(nameof(ResolvableShaderLocations_DataArts))] + public void FileExists_ResolvesShaderAtSupportedLocation(string writtenPath, string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write(writtenPath, "x")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.EffectsRepository.FileExists(input)); + } + + [Theory] + [InlineData("Engine\\MyShader")] + [InlineData("Engine/MyShader")] + [InlineData("Engine\\MyShader.fx")] + [InlineData("Engine\\MyShader.bogus")] + public void FileExists_ShaderAddressedWithEngineSubdirPrefix_Resolves(string input) + { + // The Engine directory is not one of the hardcoded shader search paths, but a shader placed there is + // still reachable when the request carries the "Engine\" prefix: it resolves under the SHADERS base. + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/Art/Shaders/Engine/MyShader.fx", "engine-fx")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.EffectsRepository.FileExists(input)); + Assert.Equal("engine-fx", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(ResolvableShaderLocations_Root))] + public void FileExists_ShaderInModRoot_ShouldNotResolve(string writtenPath, string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => { }) + .WithMod("MyMod", w => w.Write(writtenPath, "fx-in-mod")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.False(gameRepo.EffectsRepository.FileExists(input)); + } + + [Theory] + [MemberData(nameof(ResolvableShaderLocations_Root))] + public void FileExists_ShaderInFallbackRoot_ShouldNotResolve(string writtenPath, string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => { }) + .WithFallback("fallback", w => w.Write(writtenPath, "fx-in-fallback")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.False(gameRepo.EffectsRepository.FileExists(input)); + } + + private static TheoryData ShaderLocationsTheoryData(params string[] locations) + { + var data = new TheoryData(); + foreach (var location in locations) + foreach (var input in RepositoryTestData.EquivalentShaderNames) + data.Add(location, input); + return data; + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/EffectsRepositoryTests.Priority.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/EffectsRepositoryTests.Priority.cs new file mode 100644 index 00000000..8cb583ef --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/EffectsRepositoryTests.Priority.cs @@ -0,0 +1,215 @@ +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract partial class EffectsRepositoryTests +{ + #region Extension priority (outer loop) + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void Extension_FxBeatsOtherExtensions_SameLocation(string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("MyShader.fx", "fx"); + g.Write("MyShader.fxo", "fxo"); + g.Write("MyShader.fxh", "fxh"); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("fx", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void Extension_FxoBeatsFxh_SameLocation(string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("MyShader.fxo", "fxo"); + g.Write("MyShader.fxh", "fxh"); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("fxo", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void Extension_FxInDeepestDir_BeatsFxoAtBare(string input) + { + // Extension is the outer key in the lookup loop: + // .fx is probed in every directory before .fxo is tried anywhere. + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/Art/Shaders/MyShader.fx", "fx-deepest"); + g.Write("MyShader.fxo", "fxo-bare"); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("fx-deepest", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void Extension_FxInFallback_BeatsFxoInModSameDir(string input) + { + // Extension priority dominates over chain position: + // .fx in fallback beats .fxo in a mod, because .fx is probed everywhere first. + using var repo = CreateBuilder() + .WithMod("Mod", m => m.Write("Data/Art/Shaders/MyShader.fxo", "mod-fxo")) + .WithFallbackGame(f => f.Write("Data/Art/Shaders/MyShader.fx", "fb-fx")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("fb-fx", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + #endregion + + #region Directory priority (middle loop) + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void Directory_BareBeatsShaders_SameExtension(string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("MyShader.fx", "bare"); + g.Write("Data/Art/Shaders/MyShader.fx", "shaders"); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("bare", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void Directory_ShadersBeatsTerrain(string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/Art/Shaders/MyShader.fx", "shaders"); + g.Write("Data/Art/Shaders/Terrain/MyShader.fx", "terrain"); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("shaders", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + #endregion + + #region Root vs Data priority (inner loop) + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void RootLevel_ModIsInvisible_GameDirWins(string input) + { + using var repo = CreateBuilder() + .WithMod("Mod", m => m.Write("MyShader.fx", "mod-root-unreachable")) + .ConfigureGame(g => g.Write("MyShader.fx", "game-root")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("game-root", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void RootLevel_FallbackIsInvisible_MegWins(string input) + { + using var repo = CreateBuilder() + .WithFallbackGame(f => f.Write("MyShader.fx", "fb-root-unreachable")) + .ConfigureGame(g => g.RegisterAndWriteMeg("Data/Shaders.meg", m => m.Add("MyShader.fx", "meg"))) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("meg", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void RootLevel_GameBeatsMeg(string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("MyShader.fx", "game-root"); + g.RegisterAndWriteMeg("Data/Shaders.meg", m => m.Add("MyShader.fx", "meg-root")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("game-root", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + #endregion + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void FullMatrix_HighestReachablePriorityWins(string input) + { + using var repo = CreateBuilder() + .WithMod("Mod", m => + { + m.Write("Data/Art/Shaders/MyShader.fx", "WINNER"); + m.Write("Data/Art/Shaders/Terrain/MyShader.fx", "L1"); + m.Write("Data/Art/Shaders/MyShader.fxo", "L2"); + }) + .ConfigureGame(g => + { + g.Write("Data/Art/Shaders/MyShader.fx", "L3"); + g.Write("Data/Art/Shaders/Engine/MyShader.fx", "L4"); + g.RegisterAndWriteMeg("Data/Shaders.meg", meg => + { + meg.Add("Data/Art/Shaders/MyShader.fx", "L5"); + meg.Add("Data/Art/Shaders/MyShader.fxo", "L6"); + }); + }) + .WithFallbackGame(f => + { + f.Write("Data/Art/Shaders/MyShader.fx", "L7"); + f.Write("Data/Art/Shaders/MyShader.fxh", "L8"); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("WINNER", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void GameRootFx_BeatsModShadersFx(string input) + { + using var repo = CreateBuilder() + .WithMod("Mod", m => m.Write("Data/Art/Shaders/MyShader.fx", "mod-shaders")) + .ConfigureGame(g => g.Write("MyShader.fx", "game-root")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("game-root", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } + + [Theory] + [MemberData(nameof(RepositoryTestData.ShaderInputs), MemberType = typeof(RepositoryTestData))] + public void OnlyFxh_StillResolves(string input) + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/Art/Shaders/MyShader.fxh", "fxh-deepest")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("fxh-deepest", ReadAll(gameRepo.EffectsRepository.OpenFile(input))); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/ExtensionFallbackRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/ExtensionFallbackRepositoryTests.cs new file mode 100644 index 00000000..8d38651a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/ExtensionFallbackRepositoryTests.cs @@ -0,0 +1,146 @@ +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract class ExtensionFallbackRepositoryTests : EngineRepositoryTestBase +{ + /// The extension a request falls back to when its own extension is not present. + protected abstract string FallbackExtension { get; } + + /// A supported extension that resolves by its own name but is never a fallback target. + protected abstract string SecondaryExtension { get; } + + [Fact] + public void FileExists_EachSupportedExtension_ResolvesByItsOwnName() + { + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write(AssetPath(FallbackExtension), "fallback"); + g.Write(AssetPath(SecondaryExtension), "secondary"); + }) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.True(repoUnderTest.FileExists(AssetPath(FallbackExtension))); + Assert.Equal("fallback", ReadAll(repoUnderTest.OpenFile(AssetPath(FallbackExtension)))); + Assert.True(repoUnderTest.FileExists(AssetPath(SecondaryExtension))); + Assert.Equal("secondary", ReadAll(repoUnderTest.OpenFile(AssetPath(SecondaryExtension)))); + } + + [Fact] + public void FileExists_UnsupportedExtension_FallsBackToFallbackExtension() + { + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write(AssetPath(FallbackExtension), "fallback")) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.True(repoUnderTest.FileExists(AssetPath(".unsupported"))); + } + + [Fact] + public void FileExists_UnsupportedExtension_FallsBackToFallbackExtensionInMeg() + { + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .ConfigureGame(g => g.RegisterAndWriteMeg("Data/Assets.meg", + meg => meg.Add(AssetPath(FallbackExtension), "fallback"))) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.True(repoUnderTest.FileExists(AssetPath(".unsupported"))); + } + + [Fact] + public void FileExists_UnsupportedExtension_DoesNotResolveSecondaryFile() + { + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write(AssetPath(SecondaryExtension), "secondary")) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.False(repoUnderTest.FileExists(AssetPath(".unsupported"))); + } + + [Fact] + public void FileExists_ExtensionlessRequest_FallsBackToFallbackExtension() + { + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write(AssetPath(FallbackExtension), "fallback")) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.True(repoUnderTest.FileExists(AssetPath(string.Empty))); + } + + [Fact] + public void FileExists_FileNameWithoutDirectory_FallsBackWhenSupported() + { + // Resolving a bare name with a non-fallback extension needs both the implicit directory and the + // extension fallback, so it succeeds only where the repository has an implicit directory. + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write(AssetPath(FallbackExtension), "fallback")) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + var bareName = FileSystem.Path.GetFileName(AssetPath(SecondaryExtension)); + Assert.Equal(ResolvesFileNameWithoutDirectory, repoUnderTest.FileExists(bareName)); + } + + [Fact] + public void Priority_ExactExtensionBeatsFallbackExtension_SameOrigin() + { + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write(AssetPath(SecondaryExtension), "secondary"); + g.Write(AssetPath(FallbackExtension), "fallback"); + }) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.Equal("secondary", ReadAll(repoUnderTest.OpenFile(AssetPath(SecondaryExtension)))); + } + + [Fact] + public void Priority_ExactExtensionInFallbackOrigin_BeatsFallbackExtensionInMod() + { + // The exact-extension pass walks the whole chain before the fallback-extension pass, so an exact hit + // in the fallback origin outranks a fallback-extension hit in a mod — extension dominates chain position. + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .WithMod("Mod", m => m.Write(AssetPath(FallbackExtension), "mod-fallback")) + .WithFallbackGame(f => f.Write(AssetPath(SecondaryExtension), "fb-secondary")) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.Equal("fb-secondary", ReadAll(repoUnderTest.OpenFile(AssetPath(SecondaryExtension)))); + } + + [Fact] + public void Priority_FallbackExtension_RespectsChainOrder() + { + // With no exact match anywhere, the fallback-extension pass still honors the chain: a mod's copy wins. + var select = RepositoryFixture.SelectRepository; + using var repo = CreateBuilder() + .WithMod("Mod", m => m.Write(AssetPath(FallbackExtension), "mod-fallback")) + .ConfigureGame(g => g.Write(AssetPath(FallbackExtension), "game-fallback")) + .Build(); + var repoUnderTest = select(CreateRepository(repo)); + + Assert.Equal("mod-fallback", ReadAll(repoUnderTest.OpenFile(AssetPath(".unsupported")))); + } + + private string AssetPath(string extension) + { + var resolvable = RepositoryFixture.ResolvablePath; + return resolvable.Substring(0, resolvable.LastIndexOf('.')) + extension; + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocEffectsRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocEffectsRepositoryTests.cs new file mode 100644 index 00000000..b3bcf355 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocEffectsRepositoryTests.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Engine.Test.IO.Foc; + +public class FocEffectsRepositoryTests : EffectsRepositoryTests +{ + protected override GameEngineType Engine => GameEngineType.Foc; +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocGameRepositoryTests.Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocGameRepositoryTests.Initialization.cs new file mode 100644 index 00000000..007e65a1 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocGameRepositoryTests.Initialization.cs @@ -0,0 +1,147 @@ +using System.Linq; +using PG.StarWarsGame.Engine.ErrorReporting; +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO.Foc; + +public partial class FocGameRepositoryTests +{ + [Fact] + public void Init_LoadsEawFallbackPatchMegs() + { + using var repo = CreateBuilder() + .WithFallbackGame(f => + { + f.WriteMeg("Data/Patch.meg", m => m.Add("Init/EawPatch.bin", "EawPatch")); + f.WriteMeg("Data/Patch2.meg", m => m.Add("Init/EawPatch2.bin", "EawPatch2")); + f.WriteMeg("Data/64Patch.meg", m => m.Add("Init/Eaw64Patch.bin", "Eaw64Patch")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("EawPatch", ReadAll(gameRepo.OpenFile("Init/EawPatch.bin", megFileOnly: true))); + Assert.Equal("EawPatch2", ReadAll(gameRepo.OpenFile("Init/EawPatch2.bin", megFileOnly: true))); + Assert.Equal("Eaw64Patch", ReadAll(gameRepo.OpenFile("Init/Eaw64Patch.bin", megFileOnly: true))); + } + + [Fact] + public void Init_LoadsEawFallbackMegaFilesXmlMegs() + { + const string megaFilesXml = """ + + + Data/EawCustom.meg + + """; + using var repo = CreateBuilder() + .WithFallbackGame(f => + { + f.Write("Data/MegaFiles.xml", megaFilesXml); + f.WriteMeg("Data/EawCustom.meg", m => m.Add("Init/InEawCustom.bin", "EawCustom")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("EawCustom", ReadAll(gameRepo.OpenFile("Init/InEawCustom.bin", megFileOnly: true))); + } + + [Fact] + public void MasterMeg_EawPatch2OverridesEawPatch() + { + using var repo = CreateBuilder() + .WithFallbackGame(f => + { + f.WriteMeg("Data/Patch.meg", m => m.Add("Init/EawConflict.bin", "EawPatch")); + f.WriteMeg("Data/Patch2.meg", m => m.Add("Init/EawConflict.bin", "EawPatch2")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("EawPatch2", ReadAll(gameRepo.OpenFile("Init/EawConflict.bin", megFileOnly: true))); + } + + [Fact] + public void MasterMeg_FocPatchOverridesEaw64Patch() + { + // All four EaW slots are loaded before any FoC slot, so even the latest EaW (64Patch) + // gets overridden by the earliest FoC (Patch). + // + // The empty FoC Patch2/64Patch are necessary: the FoC ctor probes "Data/Patch2.meg" and + // "Data/64Patch.meg" via the full lookup chain (mods → game → master → fallback). Without + // empty game-dir shadows, the foc 64Patch probe would fall through to the fallback's 64Patch + // and re-load the EaW entry as the very last write, defeating the test. + using var repo = CreateBuilder() + .WithFallbackGame(f => f.WriteMeg("Data/64Patch.meg", + m => m.Add("Init/CrossConflict.bin", "Eaw64Patch"))) + .ConfigureGame(g => + { + g.WriteMeg("Data/Patch.meg", m => m.Add("Init/CrossConflict.bin", "FocPatch")); + g.WriteMeg("Data/Patch2.meg", _ => { }); + g.WriteMeg("Data/64Patch.meg", _ => { }); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("FocPatch", ReadAll(gameRepo.OpenFile("Init/CrossConflict.bin", megFileOnly: true))); + } + + [Fact] + public void MasterMeg_FocMegaFilesXmlOverridesEaw64Patch() + { + const string focMegaFilesXml = """ + + + Data/FocCustom.meg + + """; + using var repo = CreateBuilder() + .WithFallbackGame(f => f.WriteMeg("Data/64Patch.meg", + m => m.Add("Init/CrossConflict.bin", "Eaw64Patch"))) + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", focMegaFilesXml); + g.WriteMeg("Data/FocCustom.meg", m => m.Add("Init/CrossConflict.bin", "FocCustom")); + g.WriteMeg("Data/Patch.meg", _ => { }); + g.WriteMeg("Data/Patch2.meg", _ => { }); + g.WriteMeg("Data/64Patch.meg", _ => { }); // shadow fallback's 64Patch + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("FocCustom", ReadAll(gameRepo.OpenFile("Init/CrossConflict.bin", megFileOnly: true))); + } + + [Fact] + public void ErrorReporter_MissingEawPatches_AssertsFileNotFound_WhenFallbackConfigured() + { + using var repo = CreateBuilder() + .WithFallbackGame(_ => { }) + .Build(); + var reporter = new RecordingErrorReporter(); + + _ = CreateRepository(repo, reporter); + + var names = reporter.Asserts + .Where(a => a.Kind == EngineAssertKind.FileNotFound) + .Select(a => EngineFileName(a.Value)) + .ToList(); + // 6 missing: Patch / Patch2 / 64Patch in both the fallback and the FoC dir. + Assert.Equal(2, names.Count(v => v == "Patch.meg")); + Assert.Equal(2, names.Count(v => v == "Patch2.meg")); + Assert.Equal(2, names.Count(v => v == "64Patch.meg")); + } + + [Fact] + public void ErrorReporter_NoFallbackConfigured_NoEawAttempts() + { + using var repo = CreateBuilder().Build(); + var reporter = new RecordingErrorReporter(); + + _ = CreateRepository(repo, reporter); + + var fileNotFound = reporter.Asserts + .Where(a => a.Kind == EngineAssertKind.FileNotFound) + .ToList(); + Assert.Equal(3, fileNotFound.Count); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocGameRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocGameRepositoryTests.cs new file mode 100644 index 00000000..0c81ae50 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocGameRepositoryTests.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Engine.Test.IO.Foc; + +public partial class FocGameRepositoryTests : GameRepositoryTests +{ + protected override GameEngineType Engine => GameEngineType.Foc; +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocModelRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocModelRepositoryTests.cs new file mode 100644 index 00000000..5209a92c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocModelRepositoryTests.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Engine.Test.IO.Foc; + +public class FocModelRepositoryTests : ModelRepositoryTests +{ + protected override GameEngineType Engine => GameEngineType.Foc; +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocTextureRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocTextureRepositoryTests.cs new file mode 100644 index 00000000..6650c664 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/Foc/FocTextureRepositoryTests.cs @@ -0,0 +1,6 @@ +namespace PG.StarWarsGame.Engine.Test.IO.Foc; + +public class FocTextureRepositoryTests : TextureRepositoryTests +{ + protected override GameEngineType Engine => GameEngineType.Foc; +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryFactoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryFactoryTests.cs new file mode 100644 index 00000000..a4a03d6b --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryFactoryTests.cs @@ -0,0 +1,40 @@ +using System; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Engine.IO; +using PG.StarWarsGame.Engine.IO.Repositories; +using PG.StarWarsGame.Engine.Testing; +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +/// +/// Tests engine dispatch in isolation. The factory is engine-agnostic +/// infrastructure, so it is a standalone test class rather than an engine-bound repository test. +/// +public class GameRepositoryFactoryTests : EngineTestBase +{ + private GameRepository Create(GameEngineType engine, VirtualGameRepo repo) + { + var factory = new GameRepositoryFactory(ServiceProvider); + return factory.Create(engine, repo.GameLocations, new GameEngineErrorReporterWrapper(null)); + } + + [Fact] + public void Create_Foc_ReturnsFocGameRepositoryWithMatchingEngineType() + { + using var repo = new VirtualGameRepoBuilder(ServiceProvider).Build(); + + var gameRepo = Create(GameEngineType.Foc, repo); + + Assert.IsType(gameRepo); + Assert.Equal(GameEngineType.Foc, gameRepo.EngineType); + } + + [Fact] + public void Create_Eaw_ThrowsNotImplemented() + { + using var repo = new VirtualGameRepoBuilder(ServiceProvider).Build(); + + Assert.Throws(() => Create(GameEngineType.Eaw, repo)); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.FileLookup.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.FileLookup.cs new file mode 100644 index 00000000..4005e8a3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.FileLookup.cs @@ -0,0 +1,209 @@ +using System.IO; +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract partial class GameRepositoryTests +{ + [Fact] + public void FileExists_MissingFile_ReturnsFalse() + { + using var repo = CreateBuilder().Build(); + var gameRepo = CreateRepository(repo); + + Assert.False(gameRepo.FileExists("Data/XML/DoesNotExist.xml")); + } + + [Fact] + public void FileExists_FileInGameDir_ReturnsTrue() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/XML/Foo.xml", "")) + .ConfigureGame(g => g.WriteXml("Bar.xml", "")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("Data/XML/Foo.xml")); + Assert.True(gameRepo.FileExists("Data/XML/Bar.xml")); + } + + [Fact] + public void FileExists_OutParams_FileInGameDir_NotInMeg() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/XML/Foo.xml", "")) + .Build(); + var gameRepo = CreateRepository(repo); + + var found = gameRepo.FileExists("Data/XML/Foo.xml", megFileOnly: false, out var inMeg, out var actualPath); + + Assert.True(found); + Assert.False(inMeg); + Assert.NotNull(actualPath); + Assert.EndsWith("Foo.xml", actualPath); + } + + [Fact] + public void FileExists_OutParams_MissingFile_ActualPathIsNull() + { + using var repo = CreateBuilder().Build(); + var gameRepo = CreateRepository(repo); + + var found = gameRepo.FileExists("Data/XML/Missing.xml", megFileOnly: false, out _, out var actualPath); + + Assert.False(found); + Assert.Null(actualPath); + } + + [Fact] + public void FileExists_MegFileOnlyFlag_FileSystemHitIsIgnored() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/XML/Foo.xml", "")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("Data/XML/Foo.xml")); + Assert.False(gameRepo.FileExists("Data/XML/Foo.xml", megFileOnly: true)); + } + + [Fact] + public void OpenFile_FileInGameDir_ReturnsStreamWithContent() + { + const string payload = "hello-world"; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/XML/Foo.xml", payload)) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal(payload, ReadAll(gameRepo.OpenFile("Data/XML/Foo.xml"))); + } + + [Fact] + public void OpenFile_Missing_ThrowsFileNotFound() + { + using var repo = CreateBuilder().Build(); + var gameRepo = CreateRepository(repo); + + Assert.Throws(() => gameRepo.OpenFile("Data/XML/Missing.xml")); + } + + [Fact] + public void TryOpenFile_Missing_ReturnsNull() + { + using var repo = CreateBuilder().Build(); + var gameRepo = CreateRepository(repo); + + Assert.Null(gameRepo.TryOpenFile("Data/XML/Missing.xml")); + } + + [Fact] + public void TryOpenFile_Present_ReturnsStream() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/XML/Foo.xml", "x")) + .Build(); + var gameRepo = CreateRepository(repo); + + using var stream = gameRepo.TryOpenFile("Data/XML/Foo.xml"); + Assert.NotNull(stream); + Assert.Equal("x", ReadAll(stream)); + } + + [Fact] + public void FileExists_ModOverridesGame() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/XML/Foo.xml", "from-game")) + .WithMod("MyMod", m => m.Write("Data/XML/Foo.xml", "from-mod")) + .Build(); + var gameRepo = CreateRepository(repo); + + gameRepo.FileExists("Data/XML/Foo.xml", megFileOnly: false, out _, out var actualPath); + Assert.NotNull(actualPath); + Assert.Contains("MyMod", actualPath); + + Assert.Equal("from-mod", ReadAll(gameRepo.OpenFile("Data/XML/Foo.xml"))); + } + + [Fact] + public void FileExists_NonDataPath_DoesNotConsultModOrFallback() + { + using var repo = CreateBuilder() + .WithMod("MyMod", f => f.Write("Other/Hidden.xml", "mod")) + .WithFallbackGame(f => f.Write("Other/Hidden.xml", "fbg")) + .WithFallback("Fallback", f => f.Write("Other/Hidden.xml", "fb")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.False(gameRepo.FileExists("Other/Hidden.xml")); + } + + [Fact] + public void FileExists_NonDataPath_TakeFromGame() + { + using var repo = CreateBuilder() + .ConfigureGame(f => f.Write("Other/Foo.xml", "game")) + .WithMod("MyMod", f => f.Write("Other/Foo.xml", "mod")) + .WithFallbackGame(f => f.Write("Other/Foo.xml", "fbg")) + .WithFallback("Fallback", f => f.Write("Other/Foo.xml", "fb")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("Other/Foo.xml")); + Assert.Equal("game", ReadAll(gameRepo.OpenFile("Other/Foo.xml"))); + + } + + [Fact] + public void FileExists_ForwardSlashAndBackslash_BothResolve() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/XML/Foo.xml", "x")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("Data/XML/Foo.xml")); + Assert.True(gameRepo.FileExists("Data\\XML\\Foo.xml")); + Assert.True(gameRepo.FileExists("Data\\XML/Foo.xml")); + Assert.True(gameRepo.FileExists("Data/XML\\Foo.xml")); + } + + [Fact] + public void FileExists_DataPathWithDotPrefix_ModHit() + { + using var repo = CreateBuilder() + .WithMod("MyMod", f => f.Write("Data/XML/Foo.xml", "fb")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("./Data/XML/Foo.xml")); + Assert.True(gameRepo.FileExists(".\\Data\\XML\\Foo.xml")); + Assert.True(gameRepo.FileExists("./Data\\XML\\Foo.xml")); + Assert.True(gameRepo.FileExists(".\\Data/XML\\Foo.xml")); + } + + [Fact] + public void FileExists_DataPathWithDotPrefix_FallbackHit() + { + using var repo = CreateBuilder() + .WithFallbackGame(f => f.Write("Data/XML/Foo.xml", "fb")) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("./Data/XML/Foo.xml")); + Assert.True(gameRepo.FileExists(".\\Data\\XML\\Foo.xml")); + Assert.True(gameRepo.FileExists("./Data\\XML\\Foo.xml")); + Assert.True(gameRepo.FileExists(".\\Data/XML\\Foo.xml")); + } + + [Fact] + public void EmptyRepository_NoErrors_LookupReturnsFalse() + { + using var repo = CreateBuilder().Build(); + var gameRepo = CreateRepository(repo); + + Assert.False(gameRepo.FileExists("anything.txt")); + Assert.Null(gameRepo.TryOpenFile("anything.txt")); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.Initialization.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.Initialization.cs new file mode 100644 index 00000000..b7e0adc5 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.Initialization.cs @@ -0,0 +1,214 @@ +using System.Linq; +using PG.StarWarsGame.Engine.ErrorReporting; +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract partial class GameRepositoryTests +{ + // ----------------------- pre-load: which MEGs are loaded at construction time ----------------------- + + [Fact] + public void Init_LoadsAllPatchMegs() + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.WriteMeg("Data/Patch.meg", m => m.Add("Init/InPatch.bin", "Patch")); + g.WriteMeg("Data/Patch2.meg", m => m.Add("Init/InPatch2.bin", "Patch2")); + g.WriteMeg("Data/64Patch.meg", m => m.Add("Init/In64Patch.bin", "64Patch")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("Patch", ReadAll(gameRepo.OpenFile("Init/InPatch.bin", megFileOnly: true))); + Assert.Equal("Patch2", ReadAll(gameRepo.OpenFile("Init/InPatch2.bin", megFileOnly: true))); + Assert.Equal("64Patch", ReadAll(gameRepo.OpenFile("Init/In64Patch.bin", megFileOnly: true))); + } + + [Fact] + public void Init_LoadsMegsListedInMegaFilesXml() + { + const string megaFilesXml = """ + + + Data/First.meg + Data/Second.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", megaFilesXml); + g.WriteMeg("Data/First.meg", m => m.Add("Init/InFirst.bin", "First")); + g.WriteMeg("Data/Second.meg", m => m.Add("Init/InSecond.bin", "Second")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("First", ReadAll(gameRepo.OpenFile("Init/InFirst.bin", megFileOnly: true))); + Assert.Equal("Second", ReadAll(gameRepo.OpenFile("Init/InSecond.bin", megFileOnly: true))); + } + + // ----------------------- master MEG ordering: later load wins ----------------------- + + [Fact] + public void MasterMeg_Patch2OverridesPatch() + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.WriteMeg("Data/Patch.meg", m => m.Add("Init/Conflict.bin", "Patch")); + g.WriteMeg("Data/Patch2.meg", m => m.Add("Init/Conflict.bin", "Patch2")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("Patch2", ReadAll(gameRepo.OpenFile("Init/Conflict.bin", megFileOnly: true))); + } + + [Fact] + public void MasterMeg_64PatchOverridesPatch2() + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.WriteMeg("Data/Patch2.meg", m => m.Add("Init/Conflict.bin", "Patch2")); + g.WriteMeg("Data/64Patch.meg", m => m.Add("Init/Conflict.bin", "64Patch")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("64Patch", ReadAll(gameRepo.OpenFile("Init/Conflict.bin", megFileOnly: true))); + } + + [Fact] + public void MasterMeg_PatchOverridesMegaFilesXmlEntries() + { + const string megaFilesXml = """ + + + Data/Custom.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", megaFilesXml); + g.WriteMeg("Data/Custom.meg", m => m.Add("Init/Conflict.bin", "Custom")); + g.WriteMeg("Data/Patch.meg", m => m.Add("Init/Conflict.bin", "Patch")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("Patch", ReadAll(gameRepo.OpenFile("Init/Conflict.bin", megFileOnly: true))); + } + + [Fact] + public void MasterMeg_MegaFilesXml_LaterMegOverridesEarlier() + { + const string megaFilesXml = """ + + + Data/First.meg + Data/Second.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", megaFilesXml); + g.WriteMeg("Data/First.meg", m => m.Add("Init/Conflict.bin", "First")); + g.WriteMeg("Data/Second.meg", m => m.Add("Init/Conflict.bin", "Second")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("Second", ReadAll(gameRepo.OpenFile("Init/Conflict.bin", megFileOnly: true))); + } + + // ----------------------- error reporter signals at init ----------------------- + + [Fact] + public void ErrorReporter_MissingPatchMegs_AssertsFileNotFound() + { + // Empty repo (no fallback): the ctor will probe each patch slot and miss all three. + using var repo = CreateBuilder().Build(); + var reporter = new RecordingErrorReporter(); + + _ = CreateRepository(repo, reporter); + + var fileNotFoundNames = reporter.Asserts + .Where(a => a.Kind == EngineAssertKind.FileNotFound) + .Select(a => EngineFileName(a.Value)) + .ToList(); + Assert.Contains("Patch.meg", fileNotFoundNames); + Assert.Contains("Patch2.meg", fileNotFoundNames); + Assert.Contains("64Patch.meg", fileNotFoundNames); + } + + [Fact] + public void ErrorReporter_AllPatchesPresent_NoFileNotFoundForPatches() + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.WriteMeg("Data/Patch.meg", _ => { }); + g.WriteMeg("Data/Patch2.meg", _ => { }); + g.WriteMeg("Data/64Patch.meg", _ => { }); + }) + .Build(); + var reporter = new RecordingErrorReporter(); + + _ = CreateRepository(repo, reporter); + + var fileNotFoundNames = reporter.Asserts + .Where(a => a.Kind == EngineAssertKind.FileNotFound) + .Select(a => EngineFileName(a.Value)) + .ToList(); + Assert.DoesNotContain("Patch.meg", fileNotFoundNames); + Assert.DoesNotContain("Patch2.meg", fileNotFoundNames); + Assert.DoesNotContain("64Patch.meg", fileNotFoundNames); + } + + [Fact] + public void ErrorReporter_MegaFilesXmlReferencesMissingMeg_AssertsFileNotFound() + { + const string megaFilesXml = """ + + + Data/DoesNotExist.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/MegaFiles.xml", megaFilesXml)) + .Build(); + var reporter = new RecordingErrorReporter(); + + _ = CreateRepository(repo, reporter); + + Assert.Contains(reporter.Asserts, a => + a.Kind == EngineAssertKind.FileNotFound && EngineFileName(a.Value) == "DoesNotExist.meg"); + } + + [Fact] + public void ErrorReporter_MissingSpeechMeg_DoesNotAssert() + { + // Speech.meg paths are intentionally silent: missing speech files only emit a debug log. + const string megaFilesXml = """ + + + Data/EnglishSpeech.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/MegaFiles.xml", megaFilesXml)) + .Build(); + var reporter = new RecordingErrorReporter(); + + _ = CreateRepository(repo, reporter); + + Assert.DoesNotContain(reporter.Asserts, a => + a.Kind == EngineAssertKind.FileNotFound && EngineFileName(a.Value) == "EnglishSpeech.meg"); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.MegLoading.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.MegLoading.cs new file mode 100644 index 00000000..41e54260 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.MegLoading.cs @@ -0,0 +1,171 @@ +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract partial class GameRepositoryTests +{ + private const string TextEntry = "Data/Text/text.txt"; + + #region Intra-origin: several MEGs in the same game + + [Fact] + public void MegaFilesXml_ThreeListedMegs_LastListedWins() + { + const string megaFilesXml = """ + + + Data/A.meg + Data/B.meg + Data/C.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", megaFilesXml); + g.WriteMeg("Data/A.meg", m => m.Add(TextEntry, "A")); + g.WriteMeg("Data/B.meg", m => m.Add(TextEntry, "B")); + g.WriteMeg("Data/C.meg", m => m.Add(TextEntry, "C")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("C", ReadAll(gameRepo.OpenFile(TextEntry, megFileOnly: true))); + } + + [Fact] + public void MasterMeg_PatchSlotsOverrideMegaFilesXml_64PatchWinsOverall() + { + // Full precedence for one entry present in every slot: MegaFiles.xml-listed < Patch < Patch2 < 64Patch. + const string megaFilesXml = """ + + + Data/Custom.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", megaFilesXml); + g.WriteMeg("Data/Custom.meg", m => m.Add(TextEntry, "Custom")); + g.WriteMeg("Data/Patch.meg", m => m.Add(TextEntry, "Patch")); + g.WriteMeg("Data/Patch2.meg", m => m.Add(TextEntry, "Patch2")); + g.WriteMeg("Data/64Patch.meg", m => m.Add(TextEntry, "64Patch")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("64Patch", ReadAll(gameRepo.OpenFile(TextEntry, megFileOnly: true))); + } + + #endregion + + #region Inter-origin: mod MEGs shadow game MEGs + + [Fact] + public void ModPatchMeg_ShadowsGamePatchMeg() + { + // A patch slot is resolved through the file-lookup chain, so the mod's Data/Patch.meg is loaded + // instead of the game's — the game's copy never reaches the master MEG. + using var repo = CreateBuilder() + .ConfigureGame(g => g.WriteMeg("Data/Patch.meg", m => m.Add(TextEntry, "game"))) + .WithMod("Mod", m => m.WriteMeg("Data/Patch.meg", meg => meg.Add(TextEntry, "mod"))) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("mod", ReadAll(gameRepo.OpenFile(TextEntry, megFileOnly: true))); + } + + [Fact] + public void ModMegaFilesXml_ShadowsGameMegaFilesXml() + { + // Data/MegaFiles.xml is itself resolved through the chain, so only the mod's list is read; a MEG + // listed solely in the game's MegaFiles.xml is never loaded. + const string modXml = """ + + + Data/ModListed.meg + + """; + const string gameXml = """ + + + Data/GameListed.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", gameXml); + g.WriteMeg("Data/GameListed.meg", m => m.Add("Data/Text/game.txt", "game")); + }) + .WithMod("Mod", m => + { + m.Write("Data/MegaFiles.xml", modXml); + m.WriteMeg("Data/ModListed.meg", meg => meg.Add("Data/Text/mod.txt", "mod")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("mod", ReadAll(gameRepo.OpenFile("Data/Text/mod.txt", megFileOnly: true))); + Assert.False(gameRepo.FileExists("Data/Text/game.txt", megFileOnly: true)); + } + + [Fact] + public void MegaFilesXml_MixesModAndGameMegs_LaterListedWins() + { + const string megaFilesXml = """ + + + Data/FromMod.meg + Data/FromGame.meg + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/MegaFiles.xml", megaFilesXml); + g.WriteMeg("Data/FromGame.meg", m => m.Add(TextEntry, "game")); + }) + .WithMod("Mod", m => m.WriteMeg("Data/FromMod.meg", meg => meg.Add(TextEntry, "mod"))) + .Build(); + var gameRepo = CreateRepository(repo); + + // FromGame.meg is listed after FromMod.meg, so the game entry wins for the shared path. + Assert.Equal("game", ReadAll(gameRepo.OpenFile(TextEntry, megFileOnly: true))); + } + + #endregion + + // Both of these normalized paths hash to CRC32 0x2AAF63A4: + // model : DATA\ART\MODELS\MOV_EMPIRE_INTRO_SHUTTLE_FIRE_DIE_00.ALA + // WAV entry : U000_EMP0212_ENG.WAV + // The model request collides with the bare WAV entry once the engine has prefixed the model directory, + // which is why the lookup is driven through DATA\ART\MODELS below. + private const string ModelLookup = @"Data\Art\Models\MOV_EMPIRE_INTRO_SHUTTLE_FIRE_DIE_00.ALA"; + private const string CollidingWav = "U000_EMP0212_ENG.WAV"; + + [Fact] + public void Crc32Collision_ModelLoadedFirst_ShadowedByLaterWav() + { + // Both files genuinely exist in the master MEG. The model's MEG (First.meg) is loaded before the + // WAV's MEG (Second.meg), so the later WAV takes over the shared CRC32 slot. + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.RegisterAndWriteMeg("Data/First.meg", meg => meg.Add(ModelLookup, "model-bytes")); + g.RegisterAndWriteMeg("Data/Second.meg", meg => meg.Add(CollidingWav, "wav-bytes")); + }) + .Build(); + var modelRepo = CreateRepository(repo).ModelRepository; + + var found = modelRepo.FileExists(ModelLookup, megFileOnly: false, out var inMeg, out var actualFilePath); + + // The model exists, yet requesting it resolves to the colliding WAV that was loaded later: the + // engine reports the WAV's path and hands back the WAV's bytes instead of the model. + Assert.True(found); + Assert.True(inMeg); + Assert.Equal(CollidingWav, actualFilePath); + Assert.Equal("wav-bytes", ReadAll(modelRepo.OpenFile(ModelLookup))); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.MegLookup.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.MegLookup.cs new file mode 100644 index 00000000..c5977a33 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.MegLookup.cs @@ -0,0 +1,155 @@ +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract partial class GameRepositoryTests +{ + [Fact] + public void FileExists_OutParams_HitInMeg_SetsInMegAndNormalizedPath() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.WriteMeg("Data/Patch.meg", + meg => meg.Add("Data/Audio/foo.wav", "audio"))) + .Build(); + var gameRepo = CreateRepository(repo); + + var found = gameRepo.FileExists("Data/Audio/foo.wav", megFileOnly: false, out var inMeg, out var actualPath); + + Assert.True(found); + Assert.True(inMeg); + Assert.Equal(@"DATA\AUDIO\FOO.WAV", actualPath); + } + + [Fact] + public void OpenFile_FromPatchMeg_ReturnsEntryBytes() + { + const string payload = "wav-payload"; + using var repo = CreateBuilder() + .ConfigureGame(g => g.WriteMeg("Data/Patch.meg", + meg => meg.Add("Data/Audio/foo.wav", payload))) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal(payload, ReadAll(gameRepo.OpenFile("Data/Audio/foo.wav"))); + } + + [Fact] + public void FileExists_MegLookup_CaseInsensitive() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.WriteMeg("Data/Patch.meg", + meg => meg.Add("Data/Audio/foo.wav", "x"))) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("DATA/AUDIO/FOO.WAV")); + Assert.True(gameRepo.FileExists("data/audio/foo.wav")); + Assert.True(gameRepo.FileExists("Data/Audio/Foo.Wav")); + } + + [Fact] + public void FileExists_MegLookup_SeparatorInsensitive() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.WriteMeg("Data/Patch.meg", + meg => meg.Add("Data/Audio/foo.wav", "x"))) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.True(gameRepo.FileExists("Data/Audio/foo.wav")); + Assert.True(gameRepo.FileExists(@"Data\Audio\foo.wav")); + } + + [Fact] + public void FileExists_FileSystemWinsOverMeg() + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/Audio/foo.wav", "from-fs"); + g.WriteMeg("Data/Patch.meg", meg => meg.Add("Data/Audio/foo.wav", "from-meg")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + var found = gameRepo.FileExists("Data/Audio/foo.wav", megFileOnly: false, out var inMeg, out _); + Assert.True(found); + Assert.False(inMeg); + + Assert.Equal("from-fs", ReadAll(gameRepo.OpenFile("Data/Audio/foo.wav"))); + } + + [Fact] + public void FileExists_MegFileOnlyFlag_SkipsFilesystemEvenIfPresent() + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("Data/Audio/foo.wav", "from-fs"); + g.WriteMeg("Data/Patch.meg", meg => meg.Add("Data/Audio/foo.wav", "from-meg")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + var found = gameRepo.FileExists("Data/Audio/foo.wav", megFileOnly: true, out var inMeg, out _); + Assert.True(found); + Assert.True(inMeg); + + Assert.Equal("from-meg", ReadAll(gameRepo.OpenFile("Data/Audio/foo.wav", megFileOnly: true))); + } + + [Fact] + public void FileExists_MissingMegEntry_ReturnsFalse() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.WriteMeg("Data/Patch.meg", + meg => meg.Add("Data/Audio/foo.wav", "x"))) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.False(gameRepo.FileExists("Data/Audio/missing.wav")); + } + + [Fact] + public void EmptyMegaFilesXml_DoesNotCrash() + { + const string megaFilesXml = """ + + + + """; + using var repo = CreateBuilder() + .ConfigureGame(g => g.Write("Data/MegaFiles.xml", megaFilesXml)) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.False(gameRepo.FileExists("anything.txt")); + } + + [Fact] + public void RegisterAndWriteMeg_LoadsMegViaGeneratedMegaFilesXml() + { + using var repo = CreateBuilder() + .ConfigureGame(g => g.RegisterAndWriteMeg("Data/Custom.meg", + m => m.Add(TextEntry, "registered"))) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("registered", ReadAll(gameRepo.OpenFile(TextEntry, megFileOnly: true))); + } + + [Fact] + public void RegisterAndWriteMeg_RegistrationOrderIsLoadOrder() + { + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.RegisterAndWriteMeg("Data/First.meg", m => m.Add(TextEntry, "first")); + g.RegisterAndWriteMeg("Data/Second.meg", m => m.Add(TextEntry, "second")); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("second", ReadAll(gameRepo.OpenFile(TextEntry, megFileOnly: true))); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.cs new file mode 100644 index 00000000..c0e347ab --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/GameRepositoryTests.cs @@ -0,0 +1,62 @@ +using PG.StarWarsGame.Engine.IO; +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +/// +/// Engine-agnostic tests for the base file lookup. +/// +public abstract partial class GameRepositoryTests : EngineRepositoryTestBase +{ + protected override bool ResolvesFileNameWithoutDirectory => false; + + protected override bool SurfacesPathTooLong => false; + + protected override CaseInsensitivityFixture BuildCaseInsensitivityFixture() + { + return new CaseInsensitivityFixture( + PopulateGame: g => + { + g.Write("Data/XML/Foo.xml", "fs-content"); + g.RegisterAndWriteMeg("Data/Content.meg", meg => meg.Add("Data/Audio/Bar.wav", "meg-content")); + }, + SelectRepository: gameRepo => gameRepo, + FilesystemLookup: "Data/XML/Foo.xml", + FilesystemContent: "fs-content", + MegLookup: "Data/Audio/Bar.wav", + MegContent: "meg-content"); + } + + protected override RepositoryFixture RepositoryFixture => new( + SelectRepository: gameRepo => gameRepo, + ResolvablePath: "Data/XML/Foo.xml"); + + [Fact] + public void Path_NoModConfigured_PointsToGameDirectory() + { + using var repo = CreateBuilder().Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal(RootPathOf(gameRepo, repo.GameLocations.GamePath), gameRepo.Path); + } + + [Fact] + public void Path_ModsConfigured_PointsToFirstMod() + { + using var repo = CreateBuilder() + .WithMod("FirstMod", _ => { }) + .WithMod("SecondMod", _ => { }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal(RootPathOf(gameRepo, repo.GameLocations.ModPaths[0]), gameRepo.Path); + } + + // The repository's Path is the fully-qualified top-most root (first mod, else game directory) with a + // trailing directory separator, resolved through the same file system the repository uses. + private static string RootPathOf(IGameRepository gameRepo, string rawRoot) + { + var path = gameRepo.PGFileSystem.UnderlyingFileSystem.Path; + return path.GetFullPath(rawRoot) + path.DirectorySeparatorChar; + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/ModelRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/ModelRepositoryTests.cs new file mode 100644 index 00000000..a8ce3eb7 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/ModelRepositoryTests.cs @@ -0,0 +1,31 @@ +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract class ModelRepositoryTests : ExtensionFallbackRepositoryTests +{ + protected override bool ResolvesFileNameWithoutDirectory => false; + + protected override bool SurfacesPathTooLong => true; + + protected override string FallbackExtension => ".alo"; + + protected override string SecondaryExtension => ".ala"; + + protected override CaseInsensitivityFixture BuildCaseInsensitivityFixture() + { + return new CaseInsensitivityFixture( + PopulateGame: g => + { + g.Write("Data/Art/Models/Ship.alo", "fs-alo"); + g.RegisterAndWriteMeg("Data/Models.meg", meg => meg.Add("Data/Art/Models/OtherShip.alo", "meg-alo")); + }, + SelectRepository: gameRepo => gameRepo.ModelRepository, + FilesystemLookup: "Data/Art/Models/Ship.alo", + FilesystemContent: "fs-alo", + MegLookup: "Data/Art/Models/OtherShip.alo", + MegContent: "meg-alo"); + } + + protected override RepositoryFixture RepositoryFixture => new( + SelectRepository: gameRepo => gameRepo.ModelRepository, + ResolvablePath: "Data/Art/Models/Ship.alo"); +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/TextureRepositoryTests.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/TextureRepositoryTests.cs new file mode 100644 index 00000000..3d750f23 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/IO/TextureRepositoryTests.cs @@ -0,0 +1,50 @@ +using Xunit; + +namespace PG.StarWarsGame.Engine.Test.IO; + +public abstract class TextureRepositoryTests : ExtensionFallbackRepositoryTests +{ + protected override bool ResolvesFileNameWithoutDirectory => true; + + protected override bool SurfacesPathTooLong => true; + + protected override string FallbackExtension => ".dds"; + + protected override string SecondaryExtension => ".tga"; + + protected override CaseInsensitivityFixture BuildCaseInsensitivityFixture() + { + return new CaseInsensitivityFixture( + PopulateGame: g => + { + g.Write("Data/Art/Textures/MyTex.tga", "fs-tga"); + g.RegisterAndWriteMeg("Data/Textures.meg", meg => meg.Add("Data/Art/Textures/OtherTex.tga", "meg-tga")); + }, + SelectRepository: gameRepo => gameRepo.TextureRepository, + FilesystemLookup: "Data/Art/Textures/MyTex.tga", + FilesystemContent: "fs-tga", + MegLookup: "Data/Art/Textures/OtherTex.tga", + MegContent: "meg-tga"); + } + + protected override RepositoryFixture RepositoryFixture => new( + SelectRepository: gameRepo => gameRepo.TextureRepository, + ResolvablePath: "Data/Art/Textures/MyTex.tga"); + + [Fact] + public void Priority_AsIsLocationBeatsTexturesDirectory() + { + // For a bare request the path is probed as-is (here: the game root) before it is retried under the + // implicit ./Data/Art/Textures/ directory. + using var repo = CreateBuilder() + .ConfigureGame(g => + { + g.Write("MyTex.tga", "root"); + g.Write("Data/Art/Textures/MyTex.tga", "textures-dir"); + }) + .Build(); + var gameRepo = CreateRepository(repo); + + Assert.Equal("root", ReadAll(gameRepo.TextureRepository.OpenFile("MyTex.tga"))); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/PG.StarWarsGame.Engine.Test.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/PG.StarWarsGame.Engine.Test.csproj new file mode 100644 index 00000000..eeb1d920 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/PG.StarWarsGame.Engine.Test.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + $(TargetFrameworks);net481 + preview + enable + + + + false + true + Exe + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RecordingErrorReporter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RecordingErrorReporter.cs new file mode 100644 index 00000000..3b1278ed --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RecordingErrorReporter.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using PG.StarWarsGame.Engine.ErrorReporting; +using PG.StarWarsGame.Files.XML.ErrorHandling; + +namespace PG.StarWarsGame.Engine.Test; + +internal sealed class RecordingErrorReporter : IGameEngineErrorReporter +{ + public List Asserts { get; } = []; + + public List InitializationErrors { get; } = []; + + public List XmlErrors { get; } = []; + + public void Assert(EngineAssert assert) + { + Asserts.Add(assert); + } + + public void Report(InitializationError error) + { + InitializationErrors.Add(error); + } + + public void Report(XmlError error) + { + XmlErrors.Add(error); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryFixture.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryFixture.cs new file mode 100644 index 00000000..6d6490a1 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryFixture.cs @@ -0,0 +1,13 @@ +using System; +using PG.StarWarsGame.Engine.IO; + +namespace PG.StarWarsGame.Engine.Test; + +/// +/// Represents a test fixture for verifying that repositories resolve simple file requests. +/// +/// Picks the repository under test from the constructed . +/// Lookup key the repository should resolve to the filesystem-backed fixture. +public sealed record RepositoryFixture( + Func SelectRepository, + string ResolvablePath); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryLayer.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryLayer.cs new file mode 100644 index 00000000..999b415d --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryLayer.cs @@ -0,0 +1,22 @@ +namespace PG.StarWarsGame.Engine.Test; + +/// +/// Specifies an origin layer of the repository loading chain. +/// +/// +/// The relative priority of these origins is engine-specific. +/// +public enum RepositoryLayer +{ + /// A mod path. + Mod, + + /// The base game directory. + Game, + + /// The master MEG archive. + MasterMeg, + + /// A fallback game directory. + Fallback, +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryTestData.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryTestData.cs new file mode 100644 index 00000000..7f7b620c --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Test/RepositoryTestData.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace PG.StarWarsGame.Engine.Test; + +/// Shared theory data for repository lookup tests. +internal static class RepositoryTestData +{ + /// + /// All shader-name forms that resolve identically: the effects repository strips the input's extension + /// before probing, so "MyShader.X" resolves the same regardless of X. + /// + public static readonly string[] EquivalentShaderNames = + [ + "MyShader", + "MyShader.fx", + "MyShader.fxo", + "MyShader.fxh", + "MyShader.bogus", + ]; + + public static IEnumerable ShaderInputs() + { + foreach (var input in EquivalentShaderNames) + yield return [input]; + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/AssemblyInfo.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/AssemblyInfo.cs new file mode 100644 index 00000000..0bc51f77 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("PG.StarWarsGame.Engine.Testing.Test")] diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/EmbeddedFixtures.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/EmbeddedFixtures.cs new file mode 100644 index 00000000..9a3dc567 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/EmbeddedFixtures.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Reflection; + +namespace PG.StarWarsGame.Engine.Testing; + +/// Loads bytes from embedded resources. +public static class EmbeddedFixtures +{ + /// Returns the bytes of an embedded resource. + /// The fully qualified resource name. + /// The assembly that contains the resource. uses the calling assembly. + /// was not found in . + public static byte[] Load(string resourceName, Assembly? source = null) + { + if (resourceName == null) + throw new ArgumentNullException(nameof(resourceName)); + + var asm = source ?? Assembly.GetCallingAssembly(); + using var stream = asm.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Embedded resource '{resourceName}' was not found in {asm.FullName}."); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/IMegContentBuilder.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/IMegContentBuilder.cs new file mode 100644 index 00000000..e0fa6428 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/IMegContentBuilder.cs @@ -0,0 +1,21 @@ +using System.Reflection; + +namespace PG.StarWarsGame.Engine.Testing; + +/// Composes the entries of a MEG archive. +public interface IMegContentBuilder +{ + /// Adds an entry with binary content. + /// The entry name is normalized to canonical MEG form (uppercase, backslash-separated). + IMegContentBuilder Add(string entryName, byte[] content); + + /// Adds an entry with text content. + /// Encoded as UTF-8. The entry name is normalized to canonical MEG form. + IMegContentBuilder Add(string entryName, string content); + + /// Adds an entry whose content is loaded from an embedded resource. + /// The entry name (will be normalized). + /// The fully qualified resource name. + /// The assembly containing the resource. uses the calling assembly. + IMegContentBuilder AddEmbedded(string entryName, string resourceName, Assembly? source = null); +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/IRepoOriginWriter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/IRepoOriginWriter.cs new file mode 100644 index 00000000..1dc128c2 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/IRepoOriginWriter.cs @@ -0,0 +1,42 @@ +using System; +using System.Reflection; + +namespace PG.StarWarsGame.Engine.Testing; + +/// Provides per-origin write operations against the underlying file system. +public interface IRepoOriginWriter +{ + /// Writes the text content to a file relative to the origin root. + void Write(string relativePath, string content); + + /// Writes the binary content to a file relative to the origin root. + void Write(string relativePath, byte[] content); + + /// Writes the bytes of an embedded resource to a file relative to the origin root. + /// The destination path relative to the origin root. + /// The fully qualified resource name. + /// The assembly that contains the resource. uses the calling assembly. + void WriteEmbedded(string relativePath, string resourceName, Assembly? source = null); + + /// Removes a file relative to the origin root, if it exists. + void Remove(string relativePath); + + /// Writes the XML content to Data/XML/<name> relative to the origin root. + /// may contain subpath separators. + void WriteXml(string name, string content); + + /// Writes a MEG archive composed via the configure callback to a file relative to the origin root. + /// The destination path relative to the origin root. + /// The callback that populates the archive entries. + void WriteMeg(string relativePath, Action configure); + + /// Writes a MEG archive and registers it in this origin's Data/MegaFiles.xml, + /// which the builder emits when the repository is built. + /// The destination path relative to the origin root, also listed in MegaFiles.xml. + /// The callback that populates the archive entries. + void RegisterAndWriteMeg(string relativePath, Action configure); + + /// Writes an empty MEG archive to a file relative to the origin root. + /// The destination path relative to the origin root. + void WriteEmptyMeg(string relativePath); +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/MegContentBuilder.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/MegContentBuilder.cs new file mode 100644 index 00000000..389bba20 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/MegContentBuilder.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using System.Text; +using PG.StarWarsGame.Files.MEG.Services.Builder; + +namespace PG.StarWarsGame.Engine.Testing; + +internal sealed class MegContentBuilder(IMegBuilder inner) : IMegContentBuilder +{ + private readonly IMegBuilder _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + + public IMegContentBuilder Add(string entryName, byte[] content) + { + if (entryName == null) + throw new ArgumentNullException(nameof(entryName)); + if (content == null) + throw new ArgumentNullException(nameof(content)); + + var result = _inner.AddBytes(content, entryName, encrypt: false); + return !result.Added + ? throw new InvalidOperationException($"Failed to add MEG entry '{entryName}': {result.Status} ({result.Message ?? "no message"}).") + : this; + } + + public IMegContentBuilder Add(string entryName, string content) + { + if (content == null) + throw new ArgumentNullException(nameof(content)); + + return Add(entryName, Encoding.UTF8.GetBytes(content)); + } + + public IMegContentBuilder AddEmbedded(string entryName, string resourceName, Assembly? source = null) + { + var asm = source ?? Assembly.GetCallingAssembly(); + return Add(entryName, EmbeddedFixtures.Load(resourceName, asm)); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PG.StarWarsGame.Engine.Testing.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PG.StarWarsGame.Engine.Testing.csproj new file mode 100644 index 00000000..bdb86af9 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PG.StarWarsGame.Engine.Testing.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0;netstandard2.1;net10.0 + preview + enable + disable + true + latest + false + Test scaffolding for the Petroglyph Star Wars game engine library. + + + + + + + + + + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PetroglyphFileSystemStrategy.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PetroglyphFileSystemStrategy.cs new file mode 100644 index 00000000..e34eeb6a --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PetroglyphFileSystemStrategy.cs @@ -0,0 +1,28 @@ +using PG.StarWarsGame.Engine.IO; + +namespace PG.StarWarsGame.Engine.Testing; + +/// +/// A selectable file-exists strategy. The Windows-backed strategies are +/// only supported on Windows hosts. +/// +public enum PetroglyphFileSystemStrategy +{ + /// Win32 CreateFileA per lookup (Windows only). + Windows, + + /// Case-folding, component-by-component walk. + Wine, + + /// Game-directory snapshot, delegating outside-game lookups to the Windows strategy (Windows only). + VirtualWindowsFallback, + + /// Game-directory snapshot, delegating outside-game lookups to the Wine strategy. + VirtualWineFallback, + + /// Watcher-backed game-directory snapshot, with the Windows fallback (Windows only). + LiveVirtualWindowsFallback, + + /// Watcher-backed game-directory snapshot, with the Wine fallback. + LiveVirtualWineFallback, +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PetroglyphFileSystemTestHelpers.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PetroglyphFileSystemTestHelpers.cs new file mode 100644 index 00000000..8c386586 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/PetroglyphFileSystemTestHelpers.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using PG.StarWarsGame.Engine.IO; + +namespace PG.StarWarsGame.Engine.Testing; + +/// Helpers for selecting file-exists strategies in tests. +public static class PetroglyphFileSystemTestHelpers +{ + /// The file-exists strategies supported on the current OS (Windows-backed strategies are Windows-only). + public static IReadOnlyList SupportedForCurrentOS() + { + var strategies = new List + { + PetroglyphFileSystemStrategy.Wine, + PetroglyphFileSystemStrategy.VirtualWineFallback, + PetroglyphFileSystemStrategy.LiveVirtualWineFallback, + }; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + strategies.Add(PetroglyphFileSystemStrategy.Windows); + strategies.Add(PetroglyphFileSystemStrategy.VirtualWindowsFallback); + strategies.Add(PetroglyphFileSystemStrategy.LiveVirtualWindowsFallback); + } + return strategies; + } + + /// Switches to the given file-exists strategy. + /// is . + /// A Windows-backed strategy is selected on a non-Windows host. + public static void ApplyStrategy(this PetroglyphFileSystem fileSystem, PetroglyphFileSystemStrategy strategy) + { + if (fileSystem is null) + throw new ArgumentNullException(nameof(fileSystem)); + + switch (strategy) + { + case PetroglyphFileSystemStrategy.Windows: fileSystem.UseWindowsStrategy(); break; + case PetroglyphFileSystemStrategy.Wine: fileSystem.UseWineStrategy(); break; + case PetroglyphFileSystemStrategy.VirtualWindowsFallback: fileSystem.UseVirtualStrategy(windowsFallback: true); break; + case PetroglyphFileSystemStrategy.VirtualWineFallback: fileSystem.UseVirtualStrategy(windowsFallback: false); break; + case PetroglyphFileSystemStrategy.LiveVirtualWindowsFallback: fileSystem.UseLiveVirtualStrategy(windowsFallback: true); break; + case PetroglyphFileSystemStrategy.LiveVirtualWineFallback: fileSystem.UseLiveVirtualStrategy(windowsFallback: false); break; + default: throw new ArgumentOutOfRangeException(nameof(strategy), strategy, null); + } + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/RepoOriginWriter.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/RepoOriginWriter.cs new file mode 100644 index 00000000..74bb2d19 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/RepoOriginWriter.cs @@ -0,0 +1,102 @@ +using System; +using System.IO.Abstractions; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using PG.StarWarsGame.Files.MEG.Files; +using PG.StarWarsGame.Files.MEG.Services.Builder; + +namespace PG.StarWarsGame.Engine.Testing; + +internal sealed class RepoOriginWriter(IServiceProvider services, string originPath, Action registerMeg) : IRepoOriginWriter +{ + private readonly IServiceProvider _services = services ?? throw new ArgumentNullException(nameof(services)); + private readonly IFileSystem _fileSystem = services.GetRequiredService(); + private readonly string _originPath = originPath ?? throw new ArgumentNullException(nameof(originPath)); + private readonly Action _registerMeg = registerMeg ?? throw new ArgumentNullException(nameof(registerMeg)); + + public void Write(string relativePath, string content) + { + if (content == null) + throw new ArgumentNullException(nameof(content)); + + var dst = Resolve(relativePath); + EnsureParent(dst); + _fileSystem.File.WriteAllText(dst, content); + } + + public void Write(string relativePath, byte[] content) + { + if (content == null) + throw new ArgumentNullException(nameof(content)); + + var dst = Resolve(relativePath); + EnsureParent(dst); + _fileSystem.File.WriteAllBytes(dst, content); + } + + public void WriteEmbedded(string relativePath, string resourceName, Assembly? source = null) + { + var asm = source ?? Assembly.GetCallingAssembly(); + var bytes = EmbeddedFixtures.Load(resourceName, asm); + Write(relativePath, bytes); + } + + public void Remove(string relativePath) + { + var dst = Resolve(relativePath); + if (_fileSystem.File.Exists(dst)) + _fileSystem.File.Delete(dst); + } + + public void WriteXml(string name, string content) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + Write(_fileSystem.Path.Combine("Data", "XML", Normalize(name)), content); + } + + public void WriteMeg(string relativePath, Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + var dst = Resolve(relativePath); + EnsureParent(dst); + + using var megBuilder = new EmpireAtWarMegBuilder(_fileSystem.Path.GetDirectoryName(dst)!, _services); + configure(new MegContentBuilder(megBuilder)); + using var fileInfo = new MegFileInformation(dst, MegFileVersion.V1, encryptionData: null); + megBuilder.Build(fileInfo, overwrite: true); + } + + public void WriteEmptyMeg(string relativePath) + { + WriteMeg(relativePath, _ => { }); + } + + public void RegisterAndWriteMeg(string relativePath, Action configure) + { + WriteMeg(relativePath, configure); + _registerMeg(relativePath); + } + + private string Resolve(string relativePath) + { + return relativePath == null + ? throw new ArgumentNullException(nameof(relativePath)) + : _fileSystem.Path.Combine(_originPath, Normalize(relativePath)); + } + + private string Normalize(string path) + { + return path.Replace('\\', _fileSystem.Path.DirectorySeparatorChar).Replace('/', _fileSystem.Path.DirectorySeparatorChar); + } + + private void EnsureParent(string fullPath) + { + var dir = _fileSystem.Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(dir)) + _fileSystem.Directory.CreateDirectory(dir!); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/RepoOriginWriterExtensions.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/RepoOriginWriterExtensions.cs new file mode 100644 index 00000000..fc0179ed --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/RepoOriginWriterExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Reflection; + +namespace PG.StarWarsGame.Engine.Testing; + +/// Provides convenience extensions for . +public static class RepoOriginWriterExtensions +{ + /// Writes every embedded resource whose name starts with under the origin, stripping the prefix to form the destination path. + /// The origin writer. + /// The resource name prefix that identifies a tree of fixtures (e.g., "MinimalFoc"). + /// The assembly containing the resources. uses the calling assembly. + /// or is . + public static void WriteEmbeddedTree(this IRepoOriginWriter writer, string resourcePrefix, Assembly? source = null) + { + if (writer == null) + throw new ArgumentNullException(nameof(writer)); + if (resourcePrefix == null) + throw new ArgumentNullException(nameof(resourcePrefix)); + + var asm = source ?? Assembly.GetCallingAssembly(); + var prefix = resourcePrefix.TrimEnd('/') + "/"; + foreach (var name in asm.GetManifestResourceNames()) + { + if (!name.StartsWith(prefix, StringComparison.Ordinal)) + continue; + writer.WriteEmbedded(name.Substring(prefix.Length), name, asm); + } + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/VirtualGameRepo.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/VirtualGameRepo.cs new file mode 100644 index 00000000..5a26d587 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/VirtualGameRepo.cs @@ -0,0 +1,35 @@ +using System; +using System.IO.Abstractions; + +namespace PG.StarWarsGame.Engine.Testing; + +/// Represents a disposable temp-directory-backed virtual game repository. +public sealed class VirtualGameRepo : IDisposable +{ + private readonly IFileSystem _fileSystem; + + /// Gets the absolute path of the temp directory that backs this repository. + public string TempPath { get; } + + /// Gets the game locations describing the virtual layout. + public GameLocations GameLocations { get; } + + /// Initializes a new instance of the class. + /// The file system used to access and clean up the backing directory. + /// The absolute path of the temp directory that backs this repository. + /// The game locations describing the virtual layout. + /// , , or is . + public VirtualGameRepo(IFileSystem fileSystem, string tempPath, GameLocations gameLocations) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + TempPath = tempPath ?? throw new ArgumentNullException(nameof(tempPath)); + GameLocations = gameLocations ?? throw new ArgumentNullException(nameof(gameLocations)); + } + + /// + public void Dispose() + { + if (_fileSystem.Directory.Exists(TempPath)) + _fileSystem.Directory.Delete(TempPath, recursive: true); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/VirtualGameRepoBuilder.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/VirtualGameRepoBuilder.cs new file mode 100644 index 00000000..54671da3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine.Testing/VirtualGameRepoBuilder.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Text; +using AnakinRaW.CommonUtilities.Collections; +using Microsoft.Extensions.DependencyInjection; + +namespace PG.StarWarsGame.Engine.Testing; + +/// Builds a temp-directory-backed from raw file content. +public sealed class VirtualGameRepoBuilder +{ + private readonly IServiceProvider _services; + private readonly IFileSystem _fs; + private readonly string _tempRoot; + private readonly string _gameRoot; + private readonly List _modPaths = []; + private readonly List _fallbackPaths = []; + private readonly ValueListDictionary _registeredMegsByOrigin = new(); + private string? _fallbackGamePath; + + /// Initializes a new instance of the class. + /// The service provider supplying the file system and file-format services used by the builder. + /// is . + public VirtualGameRepoBuilder(IServiceProvider services) + { + _services = services ?? throw new ArgumentNullException(nameof(services)); + _fs = services.GetRequiredService(); + _tempRoot = _fs.Path.Combine(_fs.Path.GetTempPath(), + $"PG.StarWarsGame.Engine.Testing.{Guid.NewGuid():N}"); + _gameRoot = _fs.Path.Combine(_tempRoot, "game"); + _fs.Directory.CreateDirectory(_gameRoot); + } + + /// Configures files under the base game directory. + /// The base game directory always exists; this populates it. Unlike and + /// , it does not add an optional origin. + /// The writer callback. + /// is . + public VirtualGameRepoBuilder ConfigureGame(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + configure(CreateWriter(_gameRoot)); + return this; + } + + /// Configures files under the primary fallback game directory. + /// The writer callback. + /// is . + public VirtualGameRepoBuilder WithFallbackGame(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + if (_fallbackGamePath == null) + { + _fallbackGamePath = _fs.Path.Combine(_tempRoot, "fallback", "_primary"); + _fs.Directory.CreateDirectory(_fallbackGamePath); + } + configure(CreateWriter(_fallbackGamePath)); + return this; + } + + /// Configures files under a named additional fallback path. + /// A unique name identifying this fallback. + /// The writer callback. + /// is or empty. + /// is . + public VirtualGameRepoBuilder WithFallback(string name, Action configure) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Fallback name must be non-empty.", nameof(name)); + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + var dir = _fs.Path.Combine(_tempRoot, "fallback", name); + if (!_fallbackPaths.Contains(dir)) + { + _fs.Directory.CreateDirectory(dir); + _fallbackPaths.Add(dir); + } + configure(CreateWriter(dir)); + return this; + } + + /// Configures files under a named mod path. + /// Mods are independent path roots. Declaration order is preserved as the order in . + /// A unique name identifying this mod. + /// The writer callback. + /// is or empty. + /// is . + public VirtualGameRepoBuilder WithMod(string name, Action configure) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Mod name must be non-empty.", nameof(name)); + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + var dir = _fs.Path.Combine(_tempRoot, "mods", name); + if (!_modPaths.Contains(dir)) + { + _fs.Directory.CreateDirectory(dir); + _modPaths.Add(dir); + } + configure(CreateWriter(dir)); + return this; + } + + /// Builds the configured repository. + public VirtualGameRepo Build() + { + WriteRegisteredMegaFiles(); + + var fallbacks = new List(); + if (_fallbackGamePath != null) + fallbacks.Add(_fallbackGamePath); + fallbacks.AddRange(_fallbackPaths); + var locations = new GameLocations(_modPaths, _gameRoot, fallbacks); + return new VirtualGameRepo(_fs, _tempRoot, locations); + } + + private RepoOriginWriter CreateWriter(string originRoot) + { + return new RepoOriginWriter(_services, originRoot, relativePath => _registeredMegsByOrigin.Add(originRoot, relativePath)); + } + + // Emits a Data/MegaFiles.xml per origin that had MEGs registered via RegisterAndWriteMeg, in + // registration order (which is the master-MEG load order). + private void WriteRegisteredMegaFiles() + { + foreach (var originRoot in _registeredMegsByOrigin.Keys) + CreateWriter(originRoot).Write("Data/MegaFiles.xml", BuildMegaFilesXml(_registeredMegsByOrigin.GetValues(originRoot))); + } + + private static string BuildMegaFilesXml(IReadOnlyList megs) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + foreach (var meg in megs) + sb.AppendLine($" {meg}"); + sb.Append(""); + return sb.ToString(); + } +} diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs index e84d2833..1befb96f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/HardcodedEngineAssets.cs @@ -5,10 +5,53 @@ namespace PG.StarWarsGame.Engine; public static class HardcodedEngineAssets { + public static IReadOnlyList HardcodedEngineShadersNames { get; } = new List + { + "Engine\\Global", + "AlamoEngine", + "RSkinGloss", + "MeshGloss", + "FillRateTest", + "Engine\\FrameEffectWipe", + "Engine\\FrameEffectAlpha", + "Engine\\PrimOpaque", + "Engine\\PrimAdditive", + "Engine\\PrimAlpha", + "Engine\\PrimModulate", + "Engine\\PrimDepthSpriteAdditive", + "Engine\\PrimDepthSpriteAlpha", + "Engine\\PrimDepthSpriteModulate", + "Engine\\StencilDarkenToAlpha", + "Engine\\StencilDarkenFinalBlur", + "Engine\\PrimDiffuseAlpha", + "Engine\\PrimHeat", + "Engine\\PrimParticleBumpAlpha", + "Engine\\PrimDecalBumpAlpha", + "Engine\\PrimAlphaScanlines", + "Engine\\SceneBloom", + "Engine\\SceneHeat", + "Engine\\FrameEffectFakeMotionBlur", + "SpaceFogOfWar", + "MeshOccludedUnit", + "RSkinOccludedUnit" + }; + + public static IReadOnlyList HardcodedTerrainShadersNames { get; } = new List + { + "TerrainRenderBump", + "TerrainRenderBumpDual", + "TerrainPassability", + "TerrainWater", + "TerrainLava", + "TerrainIce", + "TerrainFogOfWar", + "TerrainRenderBaked" + }; + /// /// These models / particles are hardcoded into StarWarsG.exe. /// - public static IList HardcodedFocModelsParticles { get; } = new List + public static IReadOnlyList HardcodedFocModelsParticles { get; } = new List { "i_tutorial_arrow.alo", "p_hero_empire_fx.alo", @@ -24,7 +67,7 @@ public static class HardcodedEngineAssets /// /// These models / particles are hardcoded into StarWarsG.exe. /// - public static IList HardcodedEawModelsParticles { get; } = new List + public static IReadOnlyList HardcodedEawModelsParticles { get; } = new List { "i_tutorial_arrow.alo", "p_hero_empire_fx.alo", @@ -34,7 +77,7 @@ public static class HardcodedEngineAssets "W_TextScroll.alo" }; - public static IList HardcodedFocTextures { get; } = new List + public static IReadOnlyList HardcodedFocTextures { get; } = new List { "splash.tga", "SPLASH_E3.tga", @@ -172,7 +215,7 @@ public static class HardcodedEngineAssets "W_Space_Reinforce_FOW_Grid.tga", }; - public static IList HardcodedEawTextures { get; } = new List + public static IReadOnlyList HardcodedEawTextures { get; } = new List { "splash.tga", "i_button_temporary.tga", @@ -280,7 +323,7 @@ public static class HardcodedEngineAssets "W_Space_Reinforce_FOW_Grid.tga", }; - public static IList GetHardcodedModelsAndParticles(GameEngineType engine) + public static IReadOnlyList GetHardcodedModelsAndParticles(GameEngineType engine) { return engine switch { @@ -290,7 +333,7 @@ public static IList GetHardcodedModelsAndParticles(GameEngineType engine }; } - public static IList GetHardcodedTextures(GameEngineType engine) + public static IReadOnlyList GetHardcodedTextures(GameEngineType engine) { return engine switch { diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IGameRepository.cs index 09183c80..0eb4e345 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IGameRepository.cs @@ -1,11 +1,12 @@ -using PG.StarWarsGame.Engine.Localization; +using PG.StarWarsGame.Engine.Localization; namespace PG.StarWarsGame.Engine.IO; public interface IGameRepository : IRepository { /// - /// Gets the full qualified path of this repository with a trailing directory separator + /// Gets the fully qualified path of the repository's top-most root — the first mod directory when mods + /// are configured, otherwise the base game directory — with a trailing directory separator. /// string Path { get; } @@ -20,7 +21,5 @@ public interface IGameRepository : IRepository IRepository ModelRepository { get; } - bool FileExists(string filePath, string[] extensions, bool megFileOnly = false); - bool IsLanguageInstalled(LanguageType languageType); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs index 44a8b11d..39769bcc 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs @@ -23,6 +23,27 @@ public Stream OpenFile(ReadOnlySpan filePath, bool megFileOnly = false) return fileStream; } + public Stream? TryOpenFile(string filePath, bool megFileOnly = false) + { + return TryOpenFile(filePath.AsSpan(), megFileOnly); + } + + public Stream? TryOpenFile(ReadOnlySpan filePath, bool megFileOnly = false) + { + var multiPassSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + var destinationSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); + try + { + var fileFound = MultiPassAction(filePath, ref multiPassSb, ref destinationSb, megFileOnly); + return BaseRepository.OpenFileCore(fileFound); + } + finally + { + multiPassSb.Dispose(); + destinationSb.Dispose(); + } + } + public bool FileExists(string filePath, bool megFileOnly = false) { return FileExists(filePath.AsSpan(), megFileOnly); @@ -73,29 +94,8 @@ public bool FileExists(ReadOnlySpan filePath, bool megFileOnly, out bool p } private protected abstract FileFoundInfo MultiPassAction( - ReadOnlySpan filePath, + ReadOnlySpan filePath, ref ValueStringBuilder reusableStringBuilder, ref ValueStringBuilder destination, bool megFileOnly); - - public Stream? TryOpenFile(string filePath, bool megFileOnly = false) - { - return TryOpenFile(filePath.AsSpan(), megFileOnly); - } - - public Stream? TryOpenFile(ReadOnlySpan filePath, bool megFileOnly = false) - { - var multiPassSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); - var destinationSb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); - try - { - var fileFound = MultiPassAction(filePath, ref multiPassSb, ref destinationSb, megFileOnly); - return BaseRepository.OpenFileCore(fileFound); - } - finally - { - multiPassSb.Dispose(); - destinationSb.Dispose(); - } - } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs index 1557a7e9..e102dd7f 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs @@ -10,7 +10,7 @@ internal class EffectsRepository(GameRepository baseRepository) : MultiPassRepos "DATA\\ART\\SHADERS", "DATA\\ART\\SHADERS\\TERRAIN", // This path is not coded to the engine - "DATA\\ART\\SHADERS\\ENGINE", + //"DATA\\ART\\SHADERS\\ENGINE", ]; // The engine does not support ".fxh" as a shader lookup, but as there might be some pre-compiling going on, this should be OK. diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs index fbe98122..bcc4f040 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using PG.StarWarsGame.Engine.ErrorReporting; @@ -25,9 +25,9 @@ public FocGameRepository(GameLocations gameLocations, GameEngineErrorReporterWra if (firstFallback is not null) { var eawMegs = LoadMegArchivesFromXml(firstFallback); - var eawPatch = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data/Patch.meg")); - var eawPatch2 = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data/Patch2.meg")); - var eaw64Patch = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data/64Patch.meg")); + var eawPatch = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data\\Patch.meg")); + var eawPatch2 = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data\\Patch2.meg")); + var eaw64Patch = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data\\64Patch.meg")); megsToConsider.AddRange(eawMegs); if (eawPatch is not null) @@ -39,9 +39,9 @@ public FocGameRepository(GameLocations gameLocations, GameEngineErrorReporterWra } var focOrModMegs = LoadMegArchivesFromXml("."); - var focPatch = LoadMegArchive("Data/Patch.meg"); - var focPatch2 = LoadMegArchive("Data/Patch2.meg"); - var foc64Patch = LoadMegArchive("Data/64Patch.meg"); + var focPatch = LoadMegArchive("Data\\Patch.meg"); + var focPatch2 = LoadMegArchive("Data\\Patch2.meg"); + var foc64Patch = LoadMegArchive("Data\\64Patch.meg"); megsToConsider.AddRange(focOrModMegs); if (focPatch is not null) @@ -56,6 +56,9 @@ public FocGameRepository(GameLocations gameLocations, GameEngineErrorReporterWra protected internal override FileFoundInfo FindFile(ReadOnlySpan filePath, ref ValueStringBuilder pathStringBuilder, bool megFileOnly = false) { + if (filePath.IsEmpty) + return default; + if (!megFileOnly) { var fileFoundInfo = FileFromAltExists(filePath, ModPaths, ref pathStringBuilder); diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs index 7d0f1a12..b3589018 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.Files.cs @@ -13,17 +13,6 @@ internal partial class GameRepository { private static readonly string[] DataPathPrefixes = ["DATA/", "DATA\\", "./DATA/", ".\\DATA\\"]; - public bool FileExists(string filePath, string[] extensions, bool megFileOnly = false) - { - foreach (var extension in extensions) - { - var newPath = PGFileSystem.ChangeExtension(filePath, extension); - if (FileExists(newPath, megFileOnly)) - return true; - } - return false; - } - public bool FileExists(string filePath, bool megFileOnly = false) { return FileExists(filePath.AsSpan(), megFileOnly); @@ -120,7 +109,7 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan filePath) if (filePath.Length > PGConstants.MaxMegEntryPathLength) { _logger.LogWarning("Trying to open a MEG entry which is longer than 259 characters: '{FileName}'", filePath.ToString()); - return default; + return new FileFoundInfo { PathTooLong = true }; } var sb = new ValueStringBuilder(stackalloc char[PGConstants.MaxMegEntryPathLength]); @@ -172,7 +161,7 @@ protected FileFoundInfo FileFromAltExists(ReadOnlySpan filePath, IList filePath, IList path, out int cutoffLength) { - cutoffLength = 0; - if (path.Length < 5) - return false; - var sb = new ValueStringBuilder(stackalloc char[265]); sb.Append(path); + + // Normalizing is necessary because a previous Join/Combine uses the systems directory separator, + // while hardcoded paths usually use backslashes. This might lead to "asymmetric" separator usage. + // DataPathPrefixes paths only cover symmetric cases. PGFileSystem.NormalizePath(ref sb); try { @@ -198,11 +187,12 @@ private bool PathStartsWithDataDirectory(ReadOnlySpan path, out int cutoff { if (sb.AsSpan().StartsWith(prefix.AsSpan(), StringComparison.OrdinalIgnoreCase)) { - if (path[0] == '.') - cutoffLength = 2; + cutoffLength = prefix.Length; return true; } } + + cutoffLength = 0; return false; } finally diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs index c96cd67f..bce45793 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/GameRepository.cs @@ -177,9 +177,9 @@ public IEnumerable InitializeInstalledSfxMegFiles() if (firstFallback is not null) { var fallback2dNonLocalized = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path - .Combine(firstFallback, "DATA/AUDIO/SFX/SFX2D_NON_LOCALIZED.MEG")); + .Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG")); var fallback3dNonLocalized = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path - .Combine(firstFallback, "DATA/AUDIO/SFX/SFX3D_NON_LOCALIZED.MEG")); + .Combine(firstFallback, "DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG")); if (fallback2dNonLocalized is not null) megsToAdd.Add(fallback2dNonLocalized); @@ -188,8 +188,8 @@ public IEnumerable InitializeInstalledSfxMegFiles() megsToAdd.Add(fallback3dNonLocalized); } - var nonLocalized2d = LoadMegArchive("DATA/AUDIO/SFX/SFX2D_NON_LOCALIZED.MEG"); - var nonLocalized3d = LoadMegArchive("DATA/AUDIO/SFX/SFX3D_NON_LOCALIZED.MEG"); + var nonLocalized2d = LoadMegArchive("DATA\\AUDIO\\SFX\\SFX2D_NON_LOCALIZED.MEG"); + var nonLocalized3d = LoadMegArchive("DATA\\AUDIO\\SFX\\SFX3D_NON_LOCALIZED.MEG"); if (nonLocalized2d is not null) megsToAdd.Add(nonLocalized2d); @@ -231,7 +231,7 @@ public IEnumerable InitializeInstalledSfxMegFiles() protected IList LoadMegArchivesFromXml(string lookupPath) { - var megFilesXmlPath = PGFileSystem.CombinePath(lookupPath, "Data/MegaFiles.xml"); + var megFilesXmlPath = PGFileSystem.CombinePath(lookupPath, "Data\\MegaFiles.xml"); using var xmlStream = TryOpenFile(megFilesXmlPath); @@ -314,8 +314,8 @@ public LanguageFiles(LanguageType language) { Language = language; var languageString = language.ToString().ToUpperInvariant(); - MasterTextDatFilePath = $"DATA/TEXT/MasterTextFile_{languageString}.DAT"; - Sfx2dMegFilePath = $"DATA/AUDIO/SFX/SFX2D_{languageString}.MEG"; + MasterTextDatFilePath = $"DATA\\TEXT\\MasterTextFile_{languageString}.DAT"; + Sfx2dMegFilePath = $"DATA\\AUDIO\\SFX\\SFX2D_{languageString}.MEG"; SpeechMegFileName = $"{languageString}SPEECH.MEG"; } } diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/ModelRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/ModelRepository.cs index e7368527..7089d052 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/ModelRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/ModelRepository.cs @@ -12,8 +12,8 @@ private protected override FileFoundInfo MultiPassAction( ref ValueStringBuilder destination, bool megFileOnly) { - if (!IsValidSize(filePath)) - return default; + if (!IsValidSize(filePath, out var pathTooLong)) + return new FileFoundInfo { PathTooLong = pathTooLong }; var fileInfo = BaseRepository.FindFile(filePath, ref destination, megFileOnly); if (fileInfo.FileFound) @@ -28,15 +28,26 @@ private protected override FileFoundInfo MultiPassAction( reusableStringBuilder.Append(".ALO"); var alternatePath = reusableStringBuilder.AsSpan(); - return !IsValidSize(alternatePath) - ? default + return !IsValidSize(alternatePath, out pathTooLong) + ? new FileFoundInfo { PathTooLong = pathTooLong } : BaseRepository.FindFile(alternatePath, ref destination, megFileOnly); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsValidSize(ReadOnlySpan path) - { - return path.Length != 0 && path.Length < PGConstants.MaxModelFileName; + private static bool IsValidSize(ReadOnlySpan path, out bool pathTooLong) + { + switch (path.Length) + { + case 0: + pathTooLong = false; + return false; + case >= PGConstants.MaxModelFileName: + pathTooLong = true; + return false; + default: + pathTooLong = false; + return true; + } } private static ReadOnlySpan StripFileName(ReadOnlySpan src) diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs index a8e14326..36f34863 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/TextureRepository.cs @@ -1,12 +1,12 @@ -using PG.StarWarsGame.Engine.Utilities; +using PG.StarWarsGame.Engine.Utilities; using System; namespace PG.StarWarsGame.Engine.IO.Repositories; internal class TextureRepository(GameRepository baseRepository) : MultiPassRepository(baseRepository) { - private static readonly string DdsExtension = ".dds"; - private static readonly string TexturePath = "./Data/art/Textures/"; + private const string DdsExtension = ".dds"; + private const string TexturePath = "./Data/art/Textures/"; private protected override FileFoundInfo MultiPassAction( ReadOnlySpan filePath, @@ -46,10 +46,9 @@ private FileFoundInfo FindTexture(ref ValueStringBuilder multiPassStringBuilder, multiPassStringBuilder.Insert(0, TexturePath); - if (multiPassStringBuilder.AsSpan().Length > PGConstants.MaxTextureFileName) - return new FileFoundInfo { PathTooLong = true }; - - return BaseRepository.FindFile(multiPassStringBuilder.AsSpan(), ref pathStringBuilder); + return multiPassStringBuilder.AsSpan().Length > PGConstants.MaxTextureFileName + ? new FileFoundInfo { PathTooLong = true } + : BaseRepository.FindFile(multiPassStringBuilder.AsSpan(), ref pathStringBuilder); } private static void ChangeExtensionTo(ref ValueStringBuilder stringBuilder, ReadOnlySpan extension) @@ -60,11 +59,12 @@ private static void ChangeExtensionTo(ref ValueStringBuilder stringBuilder, Read // Also, while there are many cases, where this method breaks (such as "c:/test.abc/path.dds"), // it's the way how the engine works 🤷‍ + // The engine does strtok(name, ".") + strcat(".dds"): truncate at the first '.' if there is one, + // then always append the extension — so a name with no '.' still gets the extension appended. var firstPeriod = stringBuilder.AsSpan().IndexOf('.'); - if (firstPeriod == -1) - return; + if (firstPeriod != -1) + stringBuilder.Length = firstPeriod; - stringBuilder.Length = firstPeriod; stringBuilder.Append(extension); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 2ed89db2..fd4974c8 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -36,4 +36,8 @@ + + + + \ No newline at end of file diff --git a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj index b3efb42e..fc80dff4 100644 --- a/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj +++ b/test/ModVerify.CliApp.Test/ModVerify.CliApp.Test.csproj @@ -18,7 +18,7 @@ - + diff --git a/test/ModVerify.Test/AssemblyInfo.cs b/test/ModVerify.Test/AssemblyInfo.cs new file mode 100644 index 00000000..21712008 --- /dev/null +++ b/test/ModVerify.Test/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/CommandBarComponentFiles.xml b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/CommandBarComponentFiles.xml new file mode 100644 index 00000000..c900f1e9 --- /dev/null +++ b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/CommandBarComponentFiles.xml @@ -0,0 +1,4 @@ + + + Empty_CommandBarComponents.xml + diff --git a/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_CommandBarComponents.xml b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_CommandBarComponents.xml new file mode 100644 index 00000000..03dd08bd --- /dev/null +++ b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_CommandBarComponents.xml @@ -0,0 +1,6 @@ + + + + Icon + + diff --git a/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_GameObjects.xml b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_GameObjects.xml new file mode 100644 index 00000000..c3bcf1b4 --- /dev/null +++ b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_GameObjects.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_SFXEvents.xml b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_SFXEvents.xml new file mode 100644 index 00000000..728570e0 --- /dev/null +++ b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/Empty_SFXEvents.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/GameObjectFiles.xml b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/GameObjectFiles.xml new file mode 100644 index 00000000..de8cdede --- /dev/null +++ b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/GameObjectFiles.xml @@ -0,0 +1,4 @@ + + + Empty_GameObjects.xml + diff --git a/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/GuiDialogs.xml b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/GuiDialogs.xml new file mode 100644 index 00000000..dcf8336a --- /dev/null +++ b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/GuiDialogs.xml @@ -0,0 +1,8 @@ + + + + + placeholder + + + diff --git a/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/SFXEventFiles.xml b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/SFXEventFiles.xml new file mode 100644 index 00000000..656cfb22 --- /dev/null +++ b/test/ModVerify.Test/Fixtures/MinimalFoc/Data/XML/SFXEventFiles.xml @@ -0,0 +1,4 @@ + + + Empty_SFXEvents.xml + diff --git a/test/ModVerify.Test/Framework/ErrorAssertions.cs b/test/ModVerify.Test/Framework/ErrorAssertions.cs new file mode 100644 index 00000000..a2fad0b9 --- /dev/null +++ b/test/ModVerify.Test/Framework/ErrorAssertions.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using AET.ModVerify.Reporting; +using Xunit; + +namespace ModVerify.Test.Framework; + +internal static class ErrorAssertions +{ + public static VerificationError Single( + IEnumerable errors, string id, + string? asset = null, string? contextContains = null) + { + var matches = errors.Where(e => Match(e, id, asset, contextContains)).ToList(); + Assert.Single(matches); + return matches[0]; + } + + public static void None(IEnumerable errors, string id) + { + Assert.DoesNotContain(errors, e => e.Id == id); + } + + public static void Exactly(IEnumerable errors, int expected, string id) + { + Assert.Equal(expected, errors.Count(e => e.Id == id)); + } + + private static bool Match(VerificationError e, string id, string? asset, string? contextContains) + { + if (e.Id != id) + return false; + if (asset != null && e.Asset != asset) + return false; + return contextContains == null || e.ContextEntries.Any(c => c.Contains(contextContains)); + } +} diff --git a/test/ModVerify.Test/Framework/MinimalFoc.cs b/test/ModVerify.Test/Framework/MinimalFoc.cs new file mode 100644 index 00000000..c4bc2378 --- /dev/null +++ b/test/ModVerify.Test/Framework/MinimalFoc.cs @@ -0,0 +1,34 @@ +using System; +using System.Reflection; +using PG.StarWarsGame.Engine.Testing; + +namespace ModVerify.Test.Framework; + +/// Provides the minimal FoC skeleton as an extension on . +public static class MinimalFoc +{ + private static readonly Assembly Asm = typeof(MinimalFoc).Assembly; + + /// Scaffolds the committed minimal FoC skeleton onto the builder's game origin. + /// + /// Empty MEG archives are produced at scaffold time using the production MEG writer; the resulting bytes are written through the builder. + /// Hand-authoring binary MEG files in Fixtures/ would require committing platform-specific bytes. + /// + /// The builder to populate. + public static VirtualGameRepoBuilder WithMinimalFoc(this VirtualGameRepoBuilder builder) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + return builder.ConfigureGame(g => + { + g.WriteEmptyMeg("Data/Patch.meg"); + g.WriteEmptyMeg("Data/Patch2.meg"); + g.WriteEmptyMeg("Data/64Patch.meg"); + g.WriteEmptyMeg("Data/Audio/SFX/SFX2D_NON_LOCALIZED.MEG"); + g.WriteEmptyMeg("Data/Audio/SFX/SFX3D_NON_LOCALIZED.MEG"); + + g.WriteEmbeddedTree("MinimalFoc", Asm); + }); + } +} diff --git a/test/ModVerify.Test/Framework/ModVerifyTestBase.cs b/test/ModVerify.Test/Framework/ModVerifyTestBase.cs new file mode 100644 index 00000000..90e367b2 --- /dev/null +++ b/test/ModVerify.Test/Framework/ModVerifyTestBase.cs @@ -0,0 +1,93 @@ +using System; +using System.IO.Abstractions; +using System.Threading.Tasks; +using AET.ModVerify; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Settings; +using AnakinRaW.CommonUtilities.Hashing; +using AnakinRaW.CommonUtilities.Testing; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Testing; +using PG.StarWarsGame.Files.ALO; +using PG.StarWarsGame.Files.MEG; +using PG.StarWarsGame.Files.MTD; +using PG.StarWarsGame.Files.XML; +using Testably.Abstractions; +using Xunit; + +namespace ModVerify.Test.Framework; + +public abstract class ModVerifyTestBase : TestBaseWithFileSystem +{ + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + + serviceCollection.AddSingleton(sp => new HashingService(sp)); + + serviceCollection.SupportMTD(); + serviceCollection.SupportMEG(); + serviceCollection.SupportALO(); + serviceCollection.SupportXML(); + PetroglyphCommons.ContributeServices(serviceCollection); + PetroglyphEngineServiceContribution.ContributeServices(serviceCollection); + } + + protected override IFileSystem CreateFileSystem() + { + return new RealFileSystem(); + } + + /// Creates a builder bound to the test base's service provider. + protected VirtualGameRepoBuilder CreateBuilder() + { + return new VirtualGameRepoBuilder(ServiceProvider); + } + + /// Creates the default for a pipeline run. + /// defaults to 1 so the order of verifier invocations is deterministic. + protected virtual VerifierServiceSettings CreateDefaultSettings(IGameVerifiersProvider verifiers) + { + return new VerifierServiceSettings + { + VerifiersProvider = verifiers, + ParallelVerifiers = 1, + UseLiveVirtualFileSystem = false, + GameVerifySettings = GameVerifySettings.Default, + FailFastSettings = FailFastSetting.NoFailFast, + }; + } + + protected async Task RunPipelineAsync( + VirtualGameRepo repo, + IGameVerifiersProvider? verifiers = null, + BaselineCollection? baselines = null, + SuppressionList? suppressions = null, + VerifierServiceSettings? settings = null) + { + if (repo == null) + throw new ArgumentNullException(nameof(repo)); + + var serviceSettings = settings ?? CreateDefaultSettings( + verifiers ?? new DefaultGameVerifiersProvider()); + var target = new VerificationTarget + { + Engine = GameEngineType.Foc, + Location = repo.GameLocations, + Name = "test-target" + }; + + using var pipeline = new GameVerifyPipeline( + target, + serviceSettings, + ServiceProvider, + baselines ?? BaselineCollection.Empty, + suppressions ?? SuppressionList.Empty); + await pipeline.RunAsync(TestContext.Current.CancellationToken).ConfigureAwait(false); + return pipeline.Errors; + } +} diff --git a/test/ModVerify.Test/Framework/Providers/ErrorThenTrackingProvider.cs b/test/ModVerify.Test/Framework/Providers/ErrorThenTrackingProvider.cs new file mode 100644 index 00000000..5e431d01 --- /dev/null +++ b/test/ModVerify.Test/Framework/Providers/ErrorThenTrackingProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using ModVerify.Test.Framework.Verifiers; +using PG.StarWarsGame.Engine; + +namespace ModVerify.Test.Framework.Providers; + +internal sealed class ErrorThenTrackingProvider( + Func errorFactory) + : IGameVerifiersProvider +{ + public TrackingVerifier? Tracker { get; private set; } + + public IEnumerable GetVerifiers( + IStarWarsGameEngine engine, GameVerifySettings settings, IServiceProvider sp) + { + yield return errorFactory(engine, settings, sp); + Tracker = new TrackingVerifier(engine, settings, sp); + yield return Tracker; + } + + public static ErrorThenTrackingProvider Create( + string id, + string asset, + string[] context, + VerificationSeverity severity, + string message = "static error") + { + var spec = new StaticErrorSpec(id, asset, context, severity, message); + return new ErrorThenTrackingProvider((engine, settings, sp) => new StaticErrorVerifier([spec], engine, settings, sp)); + } +} diff --git a/test/ModVerify.Test/Framework/Providers/NoVerifiersProvider.cs b/test/ModVerify.Test/Framework/Providers/NoVerifiersProvider.cs new file mode 100644 index 00000000..37a3d3a6 --- /dev/null +++ b/test/ModVerify.Test/Framework/Providers/NoVerifiersProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using PG.StarWarsGame.Engine; + +namespace ModVerify.Test.Framework.Providers; + +internal sealed class NoVerifiersProvider : IGameVerifiersProvider +{ + public IEnumerable GetVerifiers( + IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider sp) + { + return []; + } +} diff --git a/test/ModVerify.Test/Framework/Providers/SingleVerifierProvider.cs b/test/ModVerify.Test/Framework/Providers/SingleVerifierProvider.cs new file mode 100644 index 00000000..1ff3b529 --- /dev/null +++ b/test/ModVerify.Test/Framework/Providers/SingleVerifierProvider.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using AET.ModVerify; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using PG.StarWarsGame.Engine; + +namespace ModVerify.Test.Framework.Providers; + +/// Provides a single verifier instance produced by the given factory. +internal sealed class SingleVerifierProvider( + Func factory) + : IGameVerifiersProvider + where TVerifier : GameVerifier +{ + private readonly Func _factory = factory + ?? throw new ArgumentNullException(nameof(factory)); + + public IEnumerable GetVerifiers( + IStarWarsGameEngine gameEngine, GameVerifySettings settings, IServiceProvider serviceProvider) + { + yield return _factory(gameEngine, settings, serviceProvider); + } +} diff --git a/test/ModVerify.Test/Framework/Providers/StaticErrorProvider.cs b/test/ModVerify.Test/Framework/Providers/StaticErrorProvider.cs new file mode 100644 index 00000000..253c926a --- /dev/null +++ b/test/ModVerify.Test/Framework/Providers/StaticErrorProvider.cs @@ -0,0 +1,23 @@ +using AET.ModVerify.Reporting; +using ModVerify.Test.Framework.Verifiers; + +namespace ModVerify.Test.Framework.Providers; + +internal static class StaticErrorProvider +{ + public static SingleVerifierProvider Create( + string id, + string asset, + string[] context, + string message = "static error", + VerificationSeverity severity = VerificationSeverity.Warning) + { + return Create(new StaticErrorSpec(id, asset, context, severity, message)); + } + + public static SingleVerifierProvider Create(params StaticErrorSpec[] errors) + { + return new SingleVerifierProvider( + (engine, settings, sp) => new StaticErrorVerifier(errors, engine, settings, sp)); + } +} diff --git a/test/ModVerify.Test/Framework/VerifierTestBase.cs b/test/ModVerify.Test/Framework/VerifierTestBase.cs new file mode 100644 index 00000000..bb7ccfb1 --- /dev/null +++ b/test/ModVerify.Test/Framework/VerifierTestBase.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using ModVerify.Test.Framework.Providers; +using PG.StarWarsGame.Engine; +using PG.StarWarsGame.Engine.Testing; + +namespace ModVerify.Test.Framework; + +public abstract class VerifierTestBase : ModVerifyTestBase + where TVerifier : GameVerifier +{ + protected async Task> RunAsync( + VirtualGameRepo repo, + Func factory, + VerifierServiceSettings? settings = null) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + var provider = new SingleVerifierProvider(factory); + var result = await RunPipelineAsync(repo, verifiers: provider, settings: settings).ConfigureAwait(false); + return result.NewErrors.Concat(result.ExistingErrors.Values.SelectMany(v => v)).ToList(); + } +} diff --git a/test/ModVerify.Test/Framework/Verifiers/StaticErrorVerifier.cs b/test/ModVerify.Test/Framework/Verifiers/StaticErrorVerifier.cs new file mode 100644 index 00000000..38b58f17 --- /dev/null +++ b/test/ModVerify.Test/Framework/Verifiers/StaticErrorVerifier.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using AET.ModVerify.Reporting; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using PG.StarWarsGame.Engine; + +namespace ModVerify.Test.Framework.Verifiers; + +/// Emits a fixed set of errors on each call. +internal sealed class StaticErrorVerifier( + IReadOnlyList errors, + IStarWarsGameEngine gameEngine, + GameVerifySettings settings, + IServiceProvider sp) + : GameVerifier(gameEngine, settings, sp) +{ + public override void Verify(CancellationToken token) + { + foreach (var spec in errors) + AddError(VerificationError.Create(this, spec.Id, spec.Message, spec.Severity, spec.Context, spec.Asset)); + } +} + +internal sealed record StaticErrorSpec( + string Id, + string Asset, + string[] Context, + VerificationSeverity Severity = VerificationSeverity.Warning, + string Message = "static error"); diff --git a/test/ModVerify.Test/Framework/Verifiers/TrackingVerifier.cs b/test/ModVerify.Test/Framework/Verifiers/TrackingVerifier.cs new file mode 100644 index 00000000..3f958fb4 --- /dev/null +++ b/test/ModVerify.Test/Framework/Verifiers/TrackingVerifier.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using AET.ModVerify.Settings; +using AET.ModVerify.Verifiers; +using PG.StarWarsGame.Engine; + +namespace ModVerify.Test.Framework.Verifiers; + +/// A test verifier that records whether has been called. +internal sealed class TrackingVerifier(IStarWarsGameEngine engine, GameVerifySettings settings, IServiceProvider sp) + : GameVerifier(engine, settings, sp) +{ + public bool WasInvoked { get; private set; } + + public override void Verify(CancellationToken token) + { + WasInvoked = true; + } +} diff --git a/test/ModVerify.Test/ModVerify.Test.csproj b/test/ModVerify.Test/ModVerify.Test.csproj new file mode 100644 index 00000000..a4b23d98 --- /dev/null +++ b/test/ModVerify.Test/ModVerify.Test.csproj @@ -0,0 +1,44 @@ + + + + net10.0 + $(TargetFrameworks);net481 + preview + enable + + + + false + true + Exe + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + $([System.String]::Copy('%(RecursiveDir)%(Filename)%(Extension)').Replace('\','/')) + + + + diff --git a/test/ModVerify.Test/Pipeline/BaselineCategorizationTest.cs b/test/ModVerify.Test/Pipeline/BaselineCategorizationTest.cs new file mode 100644 index 00000000..1a3514e5 --- /dev/null +++ b/test/ModVerify.Test/Pipeline/BaselineCategorizationTest.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Baseline; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Verifiers; +using ModVerify.Test.Framework; +using ModVerify.Test.Framework.Providers; +using ModVerify.Test.Framework.Verifiers; +using Xunit; + +namespace ModVerify.Test.Pipeline; + +public class BaselineCategorizationTest : ModVerifyTestBase +{ + [Fact] + public async Task Categorize_ErrorInRunAndBaseline_IsExisting() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + + var baseline = BuildBaseline(("TEST00", "asset-1", ["ctx"])); + var provider = StaticErrorProvider.Create( + id: "TEST00", asset: "asset-1", context: ["ctx"]); + + var result = await RunPipelineAsync(repo, verifiers: provider, baselines: baseline); + + Assert.Empty(result.NewErrors); + Assert.Single(result.ExistingErrors.Values.SelectMany(v => v)); + } + + [Fact] + public async Task Categorize_ErrorOnlyInRun_IsNew() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + var provider = StaticErrorProvider.Create( + id: "TEST00", asset: "asset-1", context: ["ctx"]); + + var result = await RunPipelineAsync(repo, verifiers: provider); + + Assert.Single(result.NewErrors, e => e is { Id: "TEST00", Asset: "asset-1" }); + } + + [Fact] + public async Task Categorize_ErrorOnlyInBaseline_IsResolved() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + var baseline = BuildBaseline(("TEST00", "asset-1", ["ctx"])); + + var result = await RunPipelineAsync(repo, verifiers: new NoVerifiersProvider(), baselines: baseline); + + Assert.Single(result.ResolvedErrors.Values.SelectMany(v => v), + e => e is { Id: "TEST00", Asset: "asset-1" }); + Assert.Empty(result.NewErrors); + } + + [Fact] + public async Task Categorize_SuppressedErrorAlsoInBaseline_IsResolved() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + + var baseline = BuildBaseline(("TEST00", "asset-1", ["ctx"])); + var suppressions = new SuppressionList( + [ + new SuppressionFilter(id: "TEST00", verifier: null, asset: "asset-1"), + ]); + var provider = StaticErrorProvider.Create( + id: "TEST00", asset: "asset-1", context: ["ctx"]); + + var result = await RunPipelineAsync(repo, verifiers: provider, baselines: baseline, suppressions: suppressions); + + Assert.Empty(result.NewErrors); + Assert.Empty(result.ExistingErrors.Values.SelectMany(v => v)); + Assert.Single(result.ResolvedErrors.Values.SelectMany(v => v), + e => e is { Id: "TEST00", Asset: "asset-1" }); + } + + [Fact] + public async Task Categorize_OneOfTwoEmittedErrorsSuppressed_BothInBaseline_SuppressedIsResolved_OtherIsExisting() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + + var baseline = BuildBaseline( + ("TEST00", "asset-1", ["ctx"]), + ("TEST01", "asset-2", ["ctx"])); + var suppressions = new SuppressionList( + [ + new SuppressionFilter(id: "TEST00", verifier: null, asset: "asset-1"), + ]); + var provider = StaticErrorProvider.Create( + new StaticErrorSpec("TEST00", "asset-1", ["ctx"]), + new StaticErrorSpec("TEST01", "asset-2", ["ctx"])); + + var result = await RunPipelineAsync(repo, verifiers: provider, baselines: baseline, suppressions: suppressions); + + Assert.Empty(result.NewErrors); + Assert.Single(result.ExistingErrors.Values.SelectMany(v => v), + e => e is { Id: "TEST01", Asset: "asset-2" }); + Assert.Single(result.ResolvedErrors.Values.SelectMany(v => v), + e => e is { Id: "TEST00", Asset: "asset-1" }); + } + + [Fact] + public async Task Categorize_NewErrorAndSuppressedError_OnlyUnsuppressedAppearsAsNew() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + + var suppressions = new SuppressionList( + [ + new SuppressionFilter(id: "TEST00", verifier: null, asset: "asset-1"), + ]); + var provider = StaticErrorProvider.Create( + new StaticErrorSpec("TEST00", "asset-1", ["ctx"]), + new StaticErrorSpec("TEST01", "asset-2", ["ctx"])); + + var result = await RunPipelineAsync(repo, verifiers: provider, suppressions: suppressions); + + Assert.Single(result.NewErrors, e => e is { Id: "TEST01", Asset: "asset-2" }); + Assert.DoesNotContain(result.NewErrors, e => e.Id == "TEST00"); + } + + private static BaselineCollection BuildBaseline(params (string Id, string Asset, string[] Context)[] entries) + { + var verifierInfo = new StubVerifierInfo("stub"); + var errors = entries + .Select(e => new VerificationError( + e.Id, "baseline error", verifierInfo, e.Context, e.Asset, VerificationSeverity.Warning)) + .ToList(); + var baseline = new VerificationBaseline(VerificationSeverity.Information, errors, target: null); + return new BaselineCollection([ + new IdentifiedBaseline("test-baseline", baseline, BaselineSource.File) + ]); + } + + private sealed class StubVerifierInfo(string name) : IGameVerifierInfo + { + public IGameVerifierInfo? Parent => null; + public IReadOnlyList VerifierChain => [this]; + public string Name { get; } = name; + public string FriendlyName => Name; + } +} diff --git a/test/ModVerify.Test/Pipeline/EngineErrorSurfacingTest.cs b/test/ModVerify.Test/Pipeline/EngineErrorSurfacingTest.cs new file mode 100644 index 00000000..729078a9 --- /dev/null +++ b/test/ModVerify.Test/Pipeline/EngineErrorSurfacingTest.cs @@ -0,0 +1,27 @@ +using System.Linq; +using System.Threading.Tasks; +using AET.ModVerify.Verifiers; +using ModVerify.Test.Framework; +using ModVerify.Test.Framework.Providers; +using Xunit; + +namespace ModVerify.Test.Pipeline; + +public class EngineErrorSurfacingTest : ModVerifyTestBase +{ + [Fact] + public async Task Verify_MalformedXml_SurfacesAsVerificationError() + { + using var repo = CreateBuilder() + .WithMinimalFoc() + .ConfigureGame(g => g.WriteXml("SFXEventFiles.xml", "< e.Id is VerifierErrorCodes.GenericXmlError or VerifierErrorCodes.EmptyXmlRoot) + .ToList(); + Assert.NotEmpty(xmlErrors); + } +} diff --git a/test/ModVerify.Test/Pipeline/FailFastTest.cs b/test/ModVerify.Test/Pipeline/FailFastTest.cs new file mode 100644 index 00000000..7f90b3c4 --- /dev/null +++ b/test/ModVerify.Test/Pipeline/FailFastTest.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using AET.ModVerify; +using AET.ModVerify.Reporting; +using AET.ModVerify.Reporting.Suppressions; +using AET.ModVerify.Settings; +using ModVerify.Test.Framework; +using ModVerify.Test.Framework.Providers; +using Xunit; + +namespace ModVerify.Test.Pipeline; + +public class FailFastTest : ModVerifyTestBase +{ + [Fact] + public async Task RunPipeline_ErrorAboveThreshold_AbortsBeforeSubsequentVerifiers() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + + var provider = ErrorThenTrackingProvider.Create( + id: "TEST00", asset: "asset-1", context: ["ctx"], + severity: VerificationSeverity.Error); + + var settings = BuildFailFastSettings(provider); + + await Assert.ThrowsAnyAsync( + () => RunPipelineAsync(repo, settings: settings)); + + Assert.False(provider.Tracker!.WasInvoked, + "TrackingVerifier ran despite fail-fast — pipeline did not actually short-circuit."); + } + + [Fact] + public async Task RunPipeline_SuppressedErrorAboveThreshold_DoesNotAbort() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + + var provider = ErrorThenTrackingProvider.Create( + id: "TEST00", asset: "asset-1", context: ["ctx"], + severity: VerificationSeverity.Error); + + var suppressions = new SuppressionList( + [ + new SuppressionFilter(id: "TEST00", verifier: null, asset: "asset-1"), + ]); + + var settings = BuildFailFastSettings(provider); + + var result = await RunPipelineAsync(repo, suppressions: suppressions, settings: settings); + + Assert.True(provider.Tracker!.WasInvoked, + "TrackingVerifier did not run despite the error being suppressed."); + Assert.Empty(result.NewErrors); + } + + private static VerifierServiceSettings BuildFailFastSettings(IGameVerifiersProvider provider) + { + return new VerifierServiceSettings + { + VerifiersProvider = provider, + ParallelVerifiers = 1, + UseLiveVirtualFileSystem = false, + FailFastSettings = new FailFastSetting(VerificationSeverity.Error), + GameVerifySettings = GameVerifySettings.Default with + { + ThrowsOnMinimumSeverity = VerificationSeverity.Error, + }, + }; + } +} diff --git a/test/ModVerify.Test/Pipeline/MinimalFocTest.cs b/test/ModVerify.Test/Pipeline/MinimalFocTest.cs new file mode 100644 index 00000000..529817eb --- /dev/null +++ b/test/ModVerify.Test/Pipeline/MinimalFocTest.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Threading.Tasks; +using ModVerify.Test.Framework; +using ModVerify.Test.Framework.Providers; +using Xunit; + +namespace ModVerify.Test.Pipeline; + +public class MinimalFocTest : ModVerifyTestBase +{ + [Fact] + public async Task Verify_MinimalFoc_BootsCleanWithoutInitErrors() + { + using var repo = CreateBuilder() + .WithMinimalFoc() + .Build(); + + var result = await RunPipelineAsync(repo, verifiers: new NoVerifiersProvider()); + + Assert.Empty(result.NewErrors); + Assert.Empty(result.ExistingErrors.Values.SelectMany(v => v)); + } +} diff --git a/test/ModVerify.Test/Pipeline/SuppressionFilteringTest.cs b/test/ModVerify.Test/Pipeline/SuppressionFilteringTest.cs new file mode 100644 index 00000000..cc3fe1bc --- /dev/null +++ b/test/ModVerify.Test/Pipeline/SuppressionFilteringTest.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Threading.Tasks; +using AET.ModVerify.Reporting.Suppressions; +using ModVerify.Test.Framework; +using ModVerify.Test.Framework.Providers; +using Xunit; + +namespace ModVerify.Test.Pipeline; + +public class SuppressionFilteringTest : ModVerifyTestBase +{ + [Fact] + public async Task RunPipeline_SuppressedError_IsFilteredBeforeBaselineCategorization() + { + using var repo = CreateBuilder().WithMinimalFoc().Build(); + + var suppressions = new SuppressionList( + [ + new SuppressionFilter(id: "TEST00", verifier: null, asset: "asset-1"), + ]); + + var provider = StaticErrorProvider.Create(id: "TEST00", asset: "asset-1", context: ["ctx"]); + + var result = await RunPipelineAsync(repo, verifiers: provider, suppressions: suppressions); + + Assert.Empty(result.NewErrors); + Assert.Empty(result.ExistingErrors.Values.SelectMany(v => v)); + } +} diff --git a/test/ModVerify.Test/Verifiers/CommandBarVerifierTest.cs b/test/ModVerify.Test/Verifiers/CommandBarVerifierTest.cs new file mode 100644 index 00000000..9c864a17 --- /dev/null +++ b/test/ModVerify.Test/Verifiers/CommandBarVerifierTest.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using AET.ModVerify.Verifiers.CommandBar; +using ModVerify.Test.Framework; +using Xunit; + +namespace ModVerify.Test.Verifiers; + +public class CommandBarVerifierTest : VerifierTestBase +{ + [Fact] + public async Task Verify_NoShellsGroup_EmitsCmdBarError() + { + using var repo = CreateBuilder() + .WithMinimalFoc() + .Build(); + + var errors = await RunAsync(repo, + (engine, settings, sp) => new CommandBarVerifier(engine, settings, sp)); + + ErrorAssertions.Single(errors, + id: CommandBarVerifier.CommandBarNoShellsGroup, + asset: "GameCommandBar"); + } +} diff --git a/test/ModVerify.Test/Verifiers/GameObjectTypeVerifierTest.cs b/test/ModVerify.Test/Verifiers/GameObjectTypeVerifierTest.cs new file mode 100644 index 00000000..2184bcbf --- /dev/null +++ b/test/ModVerify.Test/Verifiers/GameObjectTypeVerifierTest.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.GameObjects; +using ModVerify.Test.Framework; +using Xunit; + +namespace ModVerify.Test.Verifiers; + +public class GameObjectTypeVerifierTest : VerifierTestBase +{ + [Fact] + public async Task Verify_GameObjectWithMissingLandModel_EmitsFileNotFound() + { + using var repo = CreateBuilder() + .WithMinimalFoc() + .ConfigureGame(g => + { + g.WriteXml("Empty_GameObjects.xml", """ + + + + does_not_exist.alo + + + """); + }) + .Build(); + + var errors = await RunAsync(repo, + (engine, settings, sp) => new GameObjectTypeVerifier(engine, settings, sp)); + + ErrorAssertions.Single(errors, + id: VerifierErrorCodes.FileNotFound, + asset: "DOES_NOT_EXIST.ALO", + contextContains: "INFANTRY_TROOPER_TEST"); + } +} diff --git a/test/ModVerify.Test/Verifiers/GuiDialogsVerifierTest.cs b/test/ModVerify.Test/Verifiers/GuiDialogsVerifierTest.cs new file mode 100644 index 00000000..d28ddbc2 --- /dev/null +++ b/test/ModVerify.Test/Verifiers/GuiDialogsVerifierTest.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.GuiDialogs; +using ModVerify.Test.Framework; +using Xunit; + +namespace ModVerify.Test.Verifiers; + +public class GuiDialogsVerifierTest : VerifierTestBase +{ + [Fact] + public async Task Verify_MissingMtdFile_EmitsFileNotFound() + { + using var repo = CreateBuilder() + .WithMinimalFoc() + .Build(); + + var errors = await RunAsync(repo, + (engine, settings, sp) => new GuiDialogsVerifier(engine, settings, sp)); + + ErrorAssertions.Single(errors, + id: VerifierErrorCodes.FileNotFound, + asset: "empty"); + } +} diff --git a/test/ModVerify.Test/Verifiers/HardcodedAssetsVerifierTest.cs b/test/ModVerify.Test/Verifiers/HardcodedAssetsVerifierTest.cs new file mode 100644 index 00000000..a814b6ff --- /dev/null +++ b/test/ModVerify.Test/Verifiers/HardcodedAssetsVerifierTest.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.Engine; +using ModVerify.Test.Framework; +using Xunit; + +namespace ModVerify.Test.Verifiers; + +public class HardcodedAssetsVerifierTest : VerifierTestBase +{ + [Fact] + public async Task Verify_MissingHardcodedAsset_EmitsFileNotFound() + { + using var repo = CreateBuilder() + .WithMinimalFoc() + .Build(); + + var errors = await RunAsync(repo, + (engine, settings, sp) => new HardcodedAssetsVerifier(engine, settings, sp)); + + Assert.Contains(errors, e => + e.Id == VerifierErrorCodes.FileNotFound && + e.Asset == "I_TUTORIAL_ARROW.ALO"); + } +} diff --git a/test/ModVerify.Test/Verifiers/SfxEventVerifierTest.cs b/test/ModVerify.Test/Verifiers/SfxEventVerifierTest.cs new file mode 100644 index 00000000..da9b0c93 --- /dev/null +++ b/test/ModVerify.Test/Verifiers/SfxEventVerifierTest.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using AET.ModVerify.Verifiers; +using AET.ModVerify.Verifiers.SfxEvents; +using ModVerify.Test.Framework; +using Xunit; + +namespace ModVerify.Test.Verifiers; + +public class SfxEventVerifierTest : VerifierTestBase +{ + [Fact] + public async Task Verify_SfxEventWithMissingSample_EmitsFileNotFound() + { + using var repo = CreateBuilder() + .WithMinimalFoc() + .ConfigureGame(g => + { + g.WriteXml("Empty_SFXEvents.xml", """ + + + + missing_sample.wav + + + """); + }) + .Build(); + + var errors = await RunAsync(repo, + (engine, settings, sp) => new SfxEventVerifier(engine, settings, sp)); + + ErrorAssertions.Single(errors, + id: VerifierErrorCodes.FileNotFound, + asset: "MISSING_SAMPLE.WAV", + contextContains: "TestEvent"); + } +}