@@ -9,7 +9,7 @@ const page_allocator = std.heap.page_allocator;
99const Allocator = std .mem .Allocator ;
1010const ArenaAllocator = std .heap .ArenaAllocator ;
1111const ArrayList = std .ArrayList ;
12- const HashMap = std .hash_map . HashMap ;
12+ const StringArrayHashMap = std .StringArrayHashMap ;
1313const MultiArrayList = std .MultiArrayList ;
1414const Scanner = std .json .Scanner ;
1515const StringHashMap = std .hash_map .StringHashMap ;
@@ -20,6 +20,7 @@ const bufferedReader = std.io.bufferedReader;
2020const bufferedWriter = std .io .bufferedWriter ;
2121const copyFileAbsolute = std .fs .copyFileAbsolute ;
2222const endsWith = std .mem .endsWith ;
23+ const indexOf = std .mem .indexOf ;
2324const eql = std .mem .eql ;
2425const expectEqualStrings = std .testing .expectEqualStrings ;
2526const extension = std .fs .path .extension ;
@@ -123,6 +124,13 @@ const IniFolder = struct {
123124 folders : ArrayList (IniFolder ),
124125};
125126
127+ // A module space is a list of mappings, ClassName to PresetName-set.
128+ // As well as a module name, taken from the actual folder name.
129+ const ModuleSpace = struct {
130+ entityDefinitions : * StringArrayHashMap (* StringArrayHashMap (void )),
131+ moduleName : []const u8 ,
132+ };
133+
126134/// Updated by `convert()` to record what it is doing.
127135/// If `convert()` crashed, look inside this struct to see why and where it did.
128136pub const Diagnostics = struct {
@@ -172,6 +180,10 @@ pub fn main() !void {
172180 std .log .info ("Error: Invalid output path" , .{});
173181 return err ;
174182 },
183+ error .InvalidRulesPath = > {
184+ std .log .info ("Error: Invalid rules path" , .{});
185+ return err ;
186+ },
175187 error .UnexpectedToken = > {
176188 std .log .info ("Error: Unexpected '{s}' at {s}:{}:{}\n " , .{
177189 diagnostics .token orelse "null" ,
@@ -216,7 +228,7 @@ pub fn convert(input_folder_path_: []const u8, output_folder_path_: []const u8,
216228 // https://github.com/ziglang/zig/issues/15607#issue-1698930560
217229 if (! try isValidDirPath (input_folder_path_ )) return error .InvalidInputPath ;
218230 if (! try isValidDirPath (output_folder_path_ )) return error .InvalidOutputPath ;
219- if (! try isValidDirPath (rules_folder_path_ )) return error .InvalidOutputPath ;
231+ if (! try isValidDirPath (rules_folder_path_ )) return error .InvalidRulesPath ;
220232
221233 const input_folder_path = try std .fs .realpathAlloc (allocator , input_folder_path_ );
222234 const output_folder_path = try std .fs .realpathAlloc (allocator , output_folder_path_ );
@@ -285,6 +297,13 @@ pub fn convert(input_folder_path_: []const u8, output_folder_path_: []const u8,
285297 std .log .info ("Applying INI SoundContainer rules...\n " , .{});
286298 applyIniSoundContainerRules (ini_sound_container_rules , & file_tree );
287299
300+ const ini_deinlining_rules = try parseIniDeinliningRules (rules_folder_path , allocator );
301+ std .log .info ("Applying INI de-inlining rules...\n " , .{});
302+ const entityDefinitionSets : * ArrayList (* ModuleSpace ) = try allocator .create (ArrayList (* ModuleSpace ));
303+ entityDefinitionSets .* = ArrayList (* ModuleSpace ).init (allocator );
304+ try populateEntityDefinitionSets (allocator , & file_tree , entityDefinitionSets );
305+ try applyIniDeinliningRules (allocator , ini_deinlining_rules , & file_tree , entityDefinitionSets );
306+
288307 std .log .info ("Updating INI file tree...\n " , .{});
289308 try updateIniFileTree (& file_tree , allocator );
290309
@@ -1384,6 +1403,226 @@ fn applyIniSoundContainerRulesRecursivelyNode(node: *Node, property: []const u8)
13841403 }
13851404}
13861405
1406+ fn parseIniDeinliningRules (rules_folder_path : []const u8 , allocator : Allocator ) ! [][]const u8 {
1407+ const ini_deinlining_rules_path = try join (allocator , &.{ rules_folder_path , "ini_deinlining_rules.json" });
1408+ const text = try readFile (ini_deinlining_rules_path , allocator );
1409+ return try parseFromSliceLeaky ([][]const u8 , allocator , text , .{});
1410+ }
1411+
1412+ fn populateEntityDefinitionSets (allocator : Allocator , file_tree : * IniFolder , modules : * ArrayList (* ModuleSpace )) ! void {
1413+ for (file_tree .folders .items ) | * folder | {
1414+ const entityDefinitions : * StringArrayHashMap (* StringArrayHashMap (void )) = try allocator .create (StringArrayHashMap (* StringArrayHashMap (void )));
1415+ entityDefinitions .* = StringArrayHashMap (* StringArrayHashMap (void )).init (allocator );
1416+
1417+ try populateEntityDefinitionSetsRecursively (allocator , folder , entityDefinitions );
1418+
1419+ const module : * ModuleSpace = try allocator .create (ModuleSpace );
1420+ module .* = ModuleSpace {
1421+ .moduleName = folder .name ,
1422+ .entityDefinitions = entityDefinitions ,
1423+ };
1424+
1425+ try modules .append (module );
1426+ }
1427+ }
1428+
1429+ fn populateEntityDefinitionSetsRecursively (allocator : Allocator , moduleFolder : * IniFolder , entitySpace : * StringArrayHashMap (* StringArrayHashMap (void ))) ! void {
1430+ for (moduleFolder .folders .items ) | * folder | {
1431+ try populateEntityDefinitionSetsRecursively (allocator , folder , entitySpace );
1432+ }
1433+
1434+ for (moduleFolder .files .items ) | * file | {
1435+ for (file .ast .items ) | * node | {
1436+ try populateEntityDefinitionSetsNode (allocator , node , entitySpace );
1437+ }
1438+ }
1439+ }
1440+
1441+ fn populateEntityDefinitionSetsNode (allocator : Allocator , serialNode : * Node , entitySpace : * StringArrayHashMap (* StringArrayHashMap (void ))) ! void {
1442+ // Assume this isn't an Entity with a PresetName.
1443+ var presetName : ? []const u8 = null ;
1444+
1445+ // Find out if it does specify a PresetName, and record it if so.
1446+ for (serialNode .children .items ) | * node | {
1447+ if (node .property ) | nodeProperty | {
1448+ if (strEql (nodeProperty , "PresetName" )) {
1449+ if (node .value ) | nodeValue | {
1450+ presetName = nodeValue ;
1451+ }
1452+ }
1453+ }
1454+ }
1455+
1456+ // If this has a specified PresetName, then this is an Entity from this module, which will be useful for accurate de-inlining.
1457+ if (presetName ) | presetNameNonOptional | {
1458+ if (serialNode .value ) | serialNodeValue | {
1459+ var presetSpace : * StringArrayHashMap (void ) = entitySpace .get (serialNodeValue ) orelse emplaceEntitySpace : {
1460+ const presetSpace : * StringArrayHashMap (void ) = try allocator .create (StringArrayHashMap (void ));
1461+ presetSpace .* = StringArrayHashMap (void ).init (allocator );
1462+ try entitySpace .put (serialNodeValue , presetSpace );
1463+ break :emplaceEntitySpace presetSpace ;
1464+ };
1465+
1466+ try presetSpace .put (presetNameNonOptional , {});
1467+ }
1468+ }
1469+
1470+ for (serialNode .children .items ) | * node | {
1471+ try populateEntityDefinitionSetsNode (allocator , node , entitySpace );
1472+ }
1473+ }
1474+
1475+ fn applyIniDeinliningRules (allocator : Allocator , ini_deinlining_rules : [][]const u8 , file_tree : * IniFolder , modules : * ArrayList (* ModuleSpace )) ! void {
1476+ for (file_tree .folders .items , 0.. ) | * moduleFolder , moduleID | {
1477+ for (ini_deinlining_rules ) | property | {
1478+ try applyIniDeinliningRulesRecursivelyFolder (allocator , moduleFolder , property , modules .items [moduleID ]);
1479+ }
1480+ }
1481+ }
1482+
1483+ fn applyIniDeinliningRulesRecursivelyFolder (allocator : Allocator , file_tree : * IniFolder , property : []const u8 , moduleSpace : * ModuleSpace ) ! void {
1484+ for (file_tree .folders .items ) | * folder | {
1485+ try applyIniDeinliningRulesRecursivelyFolder (allocator , folder , property , moduleSpace );
1486+ }
1487+
1488+ for (file_tree .files .items ) | * file | {
1489+ // This is kind of hideous. Loop through each item in the list.
1490+ var i : usize = 0 ;
1491+ while (i < file .ast .items .len ) {
1492+ const node : * Node = & file .ast .items [i ];
1493+ i = i + 1 ;
1494+
1495+ // Instead of looping over all nodes, loop over the children of each node.
1496+ // Why necessary? Because some unused inis use the word "Particle" as a root property.
1497+ // This is safe, however, because there should never be any lone constant references at root property level.
1498+ for (node .children .items ) | * child | {
1499+ // Deinlining a definition into the file will push the current object further forward.
1500+ const insertionApplied : bool = try applyIniDeinliningRulesRecursivelyNode (allocator , child , property , file , node .* , moduleSpace );
1501+
1502+ // We want to read the object again, since we stopped after the deinline, but we also want to read the definition that was deinlined.
1503+ // So decrement i.
1504+ if (insertionApplied ) {
1505+ i = i - 1 ;
1506+ break ;
1507+ }
1508+ }
1509+ }
1510+ }
1511+ }
1512+
1513+ fn applyIniDeinliningRulesRecursivelyNode (allocator : Allocator , node : * Node , property : []const u8 , file : * IniFile , rootParent : Node , moduleSpace : * ModuleSpace ) ! bool {
1514+ if (node .property ) | nodeProperty | {
1515+ if (node .value ) | nodeValue | {
1516+ if (strEql (nodeProperty , property )) {
1517+ if (node .children .items .len > 0 ) {
1518+ const className : []const u8 = nodeValue ;
1519+ var moduleNameOptional : ? []const u8 = null ;
1520+ var presetNameOptional : ? []const u8 = null ;
1521+
1522+ var isOriginalPreset : bool = false ;
1523+ var isCopyOf : bool = false ;
1524+
1525+ // A CopyOf changes what the constant reference will point to,
1526+ // unless it is following a PresetName, which is assumed to be a mistake.
1527+ // A PresetName always changes the pointing of the constant reference.
1528+ for (node .children .items ) | * child | {
1529+ if (child .property ) | childProperty | {
1530+ const readingCopyOf : bool = strEql (childProperty , "CopyOf" );
1531+ const readingOriginalPreset : bool = strEql (childProperty , "PresetName" );
1532+ isCopyOf = isCopyOf or readingCopyOf ;
1533+ isOriginalPreset = isOriginalPreset or readingOriginalPreset ;
1534+
1535+ if ((readingCopyOf and ! isOriginalPreset ) or readingOriginalPreset ) {
1536+ presetNameOptional = child .value ;
1537+ }
1538+ }
1539+ }
1540+
1541+ // Sometimes both CopyOfs and PresetNames list the module name with the preset name, detect this.
1542+ // This lets it respect when well written mods intentionally reference non-Base data entities.
1543+ if (presetNameOptional ) | presetName | {
1544+ if (indexOf (u8 , presetName , "/" )) | modulePrefixEnd | {
1545+ moduleNameOptional = presetName [0.. modulePrefixEnd ];
1546+ presetNameOptional = presetName [modulePrefixEnd + 1 .. ];
1547+ }
1548+ }
1549+
1550+ // If this is an original preset, unless it's in a loadout, where all sorts of things seem to go wrong,
1551+ // then we're going to deinline it.
1552+ const deinliningFlag = isOriginalPreset and ! strEql (rootParent .property .? , "AddLoadout" );
1553+
1554+ // If we're set to deinline, then we're making a new preset definition in this module.
1555+ // This means the constant reference should certainly point to this module.
1556+ if (deinliningFlag ) {
1557+ moduleNameOptional = moduleSpace .moduleName ;
1558+
1559+ const duplicateNode = Node {
1560+ .property = "AddEffect" ,
1561+ .value = node .value ,
1562+ .comments = try node .comments .clone (),
1563+ .children = try node .children .clone (),
1564+ };
1565+
1566+ const determinedIndex = index_of (Node , file .ast .items , rootParent ) orelse 0 ;
1567+ try file .ast .insert (determinedIndex , duplicateNode );
1568+ }
1569+
1570+ // As long as we ever encountered the implication of a preset name, we can construct a constant reference.
1571+ if (presetNameOptional ) | presetName | {
1572+ // If this was copied from something, and it isn't an original preset,
1573+ // then check if it is pointing to something within this module.
1574+ const moduleName : []const u8 = moduleNameOptional orelse moduleNameDeterminingBlock : {
1575+ if (isCopyOf and ! isOriginalPreset ) {
1576+ if (moduleSpace .entityDefinitions .get (className )) | presetNameSpace | {
1577+ if (presetNameSpace .get (presetName ) != null ) {
1578+ break :moduleNameDeterminingBlock moduleSpace .moduleName ;
1579+ }
1580+ }
1581+ }
1582+ break :moduleNameDeterminingBlock "Base.rte" ;
1583+ };
1584+
1585+ node .value = try allocPrint (allocator , "{s}/{s}/{s}" , .{ className , moduleName , presetName });
1586+ } else {
1587+ node .value = "None" ;
1588+ }
1589+
1590+ node .children .clearRetainingCapacity ();
1591+ node .comments .clearRetainingCapacity ();
1592+
1593+ // In any case, this was a malformed constant reference, and it no longer has children to correct.
1594+ // We signal to the caller if a definition has been deinlined, so that it can step backwards to proofread that one as well.
1595+ return deinliningFlag ;
1596+ }
1597+ }
1598+ }
1599+ }
1600+
1601+ // Since this isn't a malformed constant reference, check the same for this' children.
1602+ for (node .children .items ) | * child | {
1603+ const sequenceInvalidated = try applyIniDeinliningRulesRecursivelyNode (allocator , child , property , file , rootParent , moduleSpace );
1604+
1605+ // We have discovered an inline definition, and corrected it, which we must immediately signal to the caller.
1606+ if (sequenceInvalidated ) {
1607+ return true ;
1608+ }
1609+ }
1610+
1611+ // This is not a malformed constant reference, neither does it contain any.
1612+ return false ;
1613+ }
1614+
1615+ // Stolen off stack exchange, surprised the std library hasn't been updated to remedy the specific problem solved by this
1616+ fn index_of (comptime T : type , slice : []const T , value : T ) ? usize {
1617+ for (slice , 0.. ) | element , index | {
1618+ if (std .meta .eql (value , element )) {
1619+ return index ;
1620+ }
1621+ } else {
1622+ return null ;
1623+ }
1624+ }
1625+
13871626fn updateIniFileTree (file_tree : * IniFolder , allocator : Allocator ) ! void {
13881627 try applyOnNodesAlloc (addGetsHitByMosWhenHeldToShields , file_tree , allocator );
13891628 try applyOnNodesAlloc (addGripStrength , file_tree , allocator );
@@ -2327,10 +2566,14 @@ fn writeAstRecursively(node: *Node, buffered_writer: anytype, depth: usize) !voi
23272566
23282567 if (node .property ) | property | {
23292568 try writeBuffered (buffered_writer , property );
2569+
2570+ // Named properties with values, and AddLine (for empty lines in MultiLineText), require the equality symbol.
2571+ if (node .value != null or strEql (property , "AddLine" )) {
2572+ try writeBuffered (buffered_writer , " = " );
2573+ }
23302574 }
23312575
23322576 if (node .value ) | value | {
2333- try writeBuffered (buffered_writer , " = " );
23342577 try writeBuffered (buffered_writer , value );
23352578 }
23362579
@@ -2382,6 +2625,10 @@ test "updated" {
23822625 try testDirectory ("updated" , false );
23832626}
23842627
2628+ test "constant_reference" {
2629+ try testDirectory ("constant_reference" , false );
2630+ }
2631+
23852632fn testDirectory (comptime directory_name : []const u8 , is_invalid_test : bool ) ! void {
23862633 var iterable_tests = try std .fs .cwd ().openDir ("tests/" ++ directory_name , .{ .iterate = true });
23872634 defer iterable_tests .close ();
0 commit comments