Skip to content

Commit 1d1c65c

Browse files
authored
Merge pull request #9 from Bit-Crust/master
Fix constant references in preparation for semantic change in Pre7
2 parents 884c32d + 6f714c9 commit 1d1c65c

File tree

6 files changed

+363
-4
lines changed

6 files changed

+363
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ zig-cache
44
Cortex-Command-Mod-Converter-Engine.exe
55
Cortex-Command-Mod-Converter-Engine.pdb
66
imgui.ini
7+
/.vs

rules/ini_deinlining_rules.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[
2+
"GibParticle",
3+
"EmittedParticle",
4+
"DeliveryCraft",
5+
"AddCargoItem",
6+
"Particle",
7+
"Shell",
8+
"DebrisMaterial",
9+
"TargetMaterial",
10+
"FrostingMaterial",
11+
"TargetMaterial",
12+
"Material",
13+
"BreakWound",
14+
"ParentBreakWound",
15+
"RegularRound",
16+
"TracerRound",
17+
"EntryWound",
18+
"ExitWound"
19+
]

rules/ini_property_rules.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
"CollidesWithTerrainWhenAttached": "CollidesWithTerrainWhileAttached",
3131
"DrawWhenOpen": "DrawMaterialLayerWhenOpen",
3232
"DrawWhenClosed": "DrawMaterialLayerWhenClosed",
33-
"AffectedByPitch": "AffectedByGlobalPitch"
33+
"AffectedByPitch": "AffectedByGlobalPitch",
34+
"InstanceName": "PresetName"
3435
}

src/main.zig

Lines changed: 250 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const page_allocator = std.heap.page_allocator;
99
const Allocator = std.mem.Allocator;
1010
const ArenaAllocator = std.heap.ArenaAllocator;
1111
const ArrayList = std.ArrayList;
12-
const HashMap = std.hash_map.HashMap;
12+
const StringArrayHashMap = std.StringArrayHashMap;
1313
const MultiArrayList = std.MultiArrayList;
1414
const Scanner = std.json.Scanner;
1515
const StringHashMap = std.hash_map.StringHashMap;
@@ -20,6 +20,7 @@ const bufferedReader = std.io.bufferedReader;
2020
const bufferedWriter = std.io.bufferedWriter;
2121
const copyFileAbsolute = std.fs.copyFileAbsolute;
2222
const endsWith = std.mem.endsWith;
23+
const indexOf = std.mem.indexOf;
2324
const eql = std.mem.eql;
2425
const expectEqualStrings = std.testing.expectEqualStrings;
2526
const 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.
128136
pub 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+
13871626
fn 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+
23852632
fn 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();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
AddEffect = MOSRotating
2+
CopyOf = LikeACasingOrSomething
3+
PresetName = InThisMod
4+
5+
AddEffect = MOSRotating
6+
CopyOf = LikeACasingOrSomething
7+
PresetName = InThisModAndBase
8+
9+
AddEffect = MOSRotating
10+
CopyOf = InThisMod
11+
PresetName = DefinedInline
12+
13+
AddEffect = MOSRotating
14+
PresetName = DefinedWrongOrder
15+
CopyOf = InThisMod
16+
17+
AddEffect = PEmitter
18+
CopyOf = Whatever Really
19+
AddEmission = Emission
20+
EmittedParticle = MOSRotating/Base.rte/NotInThisMod
21+
AddEmission = Emission
22+
EmittedParticle = MOSRotating/Bar.rte/InThisMod
23+
AddEmission = Emission
24+
EmittedParticle = MOSRotating/Bar.rte/DefinedInline
25+
AddEmission = Emission
26+
EmittedParticle = MOSRotating/Bar.rte/DefinedWrongOrder
27+
AddEmission = Emission
28+
EmittedParticle = MOSRotating/Bar.rte/InThisMod
29+
AddEmission = Emission
30+
EmittedParticle = MOSRotating/Bar.rte/InThisModAndBase
31+
AddEmission = Emission
32+
EmittedParticle = MOSRotating/Base.rte/InThisModAndBase
33+
AddEmission = Emission
34+
EmittedParticle = MOSRotating/Browncoats.rte/InBrowncoats
35+
AddEmission = Emission
36+
EmittedParticle = MOSRotating/Base.rte/InBase
37+
38+
AddLoadout = Loadout
39+
AddCargoItem = MOSRotating/Base.rte/AssumeBase
40+
AddCargoItem = MOSRotating/Base.rte/DontDefineBecauseLoadoutAssumeBase
41+
AddCargoItem = MOSRotating/Bar.rte/UseGivenModule
42+
AddCargoItem = MOSRotating/Browncoats.rte/UseOtherGivenModule

0 commit comments

Comments
 (0)