package processor import ( "cook/utils" "testing" "github.com/google/go-cmp/cmp" ) func TestSurgicalJSONEditing(t *testing.T) { tests := []struct { name string content string luaCode string expected string }{ { name: "Modify single field", content: `{ "name": "test", "value": 42, "description": "original" }`, luaCode: ` data.value = 84 modified = true `, expected: `{ "name": "test", "value": 84, "description": "original" }`, }, { name: "Add new field", content: `{ "name": "test", "value": 42 }`, luaCode: ` data.newField = "added" modified = true `, expected: `{ "name": "test", "value": 42 ,"newField": "added"}`, // sjson.Set() adds new fields in compact format }, { name: "Modify nested field", content: `{ "config": { "settings": { "enabled": false, "timeout": 30 } } }`, luaCode: ` data.config.settings.enabled = true data.config.settings.timeout = 60 modified = true `, expected: `{ "config": { "settings": { "enabled": true, "timeout": 60 } } }`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { command := utils.ModifyCommand{ Name: "test", Lua: tt.luaCode, } commands, err := ProcessJSON(tt.content, command, "test.json") if err != nil { t.Fatalf("ProcessJSON failed: %v", err) } if len(commands) == 0 { t.Fatal("Expected at least one command") } // Apply the commands result := tt.content for _, cmd := range commands { result = result[:cmd.From] + cmd.With + result[cmd.To:] } diff := cmp.Diff(result, tt.expected) if diff != "" { t.Errorf("Differences:\n%s", diff) } // Check the actual result matches expected if result != tt.expected { t.Errorf("Expected:\n%s\n\nGot:\n%s", tt.expected, result) } }) } } func TestSurgicalJSONPreservesFormatting(t *testing.T) { // Test that surgical editing preserves the original formatting structure content := `{ "Defaults": { "Behaviour": "None", "Description": "", "DisplayName": "", "FlavorText": "", "Icon": "None", "MaxStack": 1, "Override_Glow_Icon": "None", "Weight": 0, "bAllowZeroWeight": false }, "RowStruct": "/Script/Icarus.ItemableData", "Rows": [ { "Description": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-Description\", \"A bundle of soft fiber, highly useful.\")", "DisplayName": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-DisplayName\", \"Fiber\")", "FlavorText": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-FlavorText\", \"Fiber is collected from bast, the strong inner bark of certain flowering plants.\")", "Icon": "/Game/Assets/2DArt/UI/Items/Item_Icons/Resources/ITEM_Fibre.ITEM_Fibre", "MaxStack": 1000000, "Name": "Item_Fiber", "Weight": 10 } ] }` expected := `{ "Defaults": { "Behaviour": "None", "Description": "", "DisplayName": "", "FlavorText": "", "Icon": "None", "MaxStack": 1, "Override_Glow_Icon": "None", "Weight": 0, "bAllowZeroWeight": false }, "RowStruct": "/Script/Icarus.ItemableData", "Rows": [ { "Description": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-Description\", \"A bundle of soft fiber, highly useful.\")", "DisplayName": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-DisplayName\", \"Fiber\")", "FlavorText": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-FlavorText\", \"Fiber is collected from bast, the strong inner bark of certain flowering plants.\")", "Icon": "/Game/Assets/2DArt/UI/Items/Item_Icons/Resources/ITEM_Fibre.ITEM_Fibre", "MaxStack": 1000000, "Name": "Item_Fiber", "Weight": 500 } ] }` command := utils.ModifyCommand{ Name: "test", Lua: ` -- Modify the weight of the first item data.Rows[1].Weight = 500 modified = true `, } commands, err := ProcessJSON(content, command, "test.json") if err != nil { t.Fatalf("ProcessJSON failed: %v", err) } if len(commands) == 0 { t.Fatal("Expected at least one command") } // Apply the commands result := content for _, cmd := range commands { result = result[:cmd.From] + cmd.With + result[cmd.To:] } diff := cmp.Diff(result, expected) if diff != "" { t.Errorf("Differences:\n%s", diff) } // Check that the result matches expected (preserves formatting and changes weight) if result != expected { t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result) } } func TestSurgicalJSONPreservesFormatting2(t *testing.T) { // Test that surgical editing preserves the original formatting structure content := ` { "RowStruct": "/Script/Icarus.ProcessorRecipe", "Defaults": { "bForceDisableRecipe": false, "Requirement": { "RowName": "None", "DataTableName": "D_Talents" }, "SessionRequirement": { "RowName": "None", "DataTableName": "D_CharacterFlags" }, "CharacterRequirement": { "RowName": "None", "DataTableName": "D_CharacterFlags" }, "RequiredMillijoules": 2500, "RecipeSets": [], "ResourceCostMultipliers": [], "Inputs": [ { "Element": { "RowName": "None", "DataTableName": "D_ItemsStatic" }, "Count": 1, "DynamicProperties": [] } ], "Container": { "Value": "None" }, "ResourceInputs": [], "bSelectOutputItemRandomly": false, "bContainsContainer": false, "ItemIconOverride": { "ItemStaticData": { "RowName": "None", "DataTableName": "D_ItemsStatic" }, "ItemDynamicData": [], "ItemCustomStats": [], "CustomProperties": { "StaticWorldStats": [], "StaticWorldHeldStats": [], "Stats": [], "Alterations": [], "LivingItemSlots": [] }, "DatabaseGUID": "", "ItemOwnerLookupId": -1, "RuntimeTags": { "GameplayTags": [] } }, "Outputs": [ { "Element": { "RowName": "None", "DataTableName": "D_ItemTemplate" }, "Count": 1, "DynamicProperties": [] } ], "ResourceOutputs": [], "Refundable": "Inherit", "ExperienceMultiplier": 1, "Audio": { "RowName": "None", "DataTableName": "D_CraftingAudioData" } }, "Rows": [ { "Name": "Biofuel1", "RecipeSets": [ { "RowName": "Composter", "DataTableName": "D_RecipeSets" } ], "Inputs": [ { "Element": { "RowName": "Raw_Meat", "DataTableName": "D_ItemsStatic" }, "Count": 2, "DynamicProperties": [] }, { "Element": { "RowName": "Tree_Sap", "DataTableName": "D_ItemsStatic" }, "Count": 1, "DynamicProperties": [] } ], "Outputs": [], "Audio": { "RowName": "Composter" }, "ResourceOutputs": [ { "Type": { "Value": "Biofuel" }, "RequiredUnits": 100 } ] } ] } ` expected := ` { "RowStruct": "/Script/Icarus.ProcessorRecipe", "Defaults": { "bForceDisableRecipe": false, "Requirement": { "RowName": "None", "DataTableName": "D_Talents" }, "SessionRequirement": { "RowName": "None", "DataTableName": "D_CharacterFlags" }, "CharacterRequirement": { "RowName": "None", "DataTableName": "D_CharacterFlags" }, "RequiredMillijoules": 2500, "RecipeSets": [], "ResourceCostMultipliers": [], "Inputs": [ { "Element": { "RowName": "None", "DataTableName": "D_ItemsStatic" }, "Count": 1, "DynamicProperties": [] } ], "Container": { "Value": "None" }, "ResourceInputs": [], "bSelectOutputItemRandomly": false, "bContainsContainer": false, "ItemIconOverride": { "ItemStaticData": { "RowName": "None", "DataTableName": "D_ItemsStatic" }, "ItemDynamicData": [], "ItemCustomStats": [], "CustomProperties": { "StaticWorldStats": [], "StaticWorldHeldStats": [], "Stats": [], "Alterations": [], "LivingItemSlots": [] }, "DatabaseGUID": "", "ItemOwnerLookupId": -1, "RuntimeTags": { "GameplayTags": [] } }, "Outputs": [ { "Element": { "RowName": "None", "DataTableName": "D_ItemTemplate" }, "Count": 1, "DynamicProperties": [] } ], "ResourceOutputs": [], "Refundable": "Inherit", "ExperienceMultiplier": 1, "Audio": { "RowName": "None", "DataTableName": "D_CraftingAudioData" } }, "Rows": [ { "Name": "Biofuel1", "RecipeSets": [ { "RowName": "Composter", "DataTableName": "D_RecipeSets" } ], "Inputs": [ { "Element": { "RowName": "Raw_Meat", "DataTableName": "D_ItemsStatic" }, "Count": 2, "DynamicProperties": [] } ], "Outputs": [], "Audio": { "RowName": "Composter" }, "ResourceOutputs": [ { "Type": { "Value": "Biofuel" }, "RequiredUnits": 100 } ] } ] } ` command := utils.ModifyCommand{ Name: "test", Lua: ` -- Define regex patterns for matching recipe names local function matchesPattern(name, pattern) local matches = re(pattern, name) -- Check if matches table has any content (index 0 or 1 should exist if there's a match) return matches and (matches[0] or matches[1]) end -- Selection pattern for recipes that get multiplied local selectionPattern = "(?-s)(Bulk_)?(Pistol|Rifle).*?Round.*?|(Carbon|Composite)_Paste.*|(Gold|Copper)_Wire|(Ironw|Copper)_Nail|(Platinum|Steel|Cold_Steel|Titanium)_Ingot|.*?Shotgun_Shell.*?|.*_Arrow|.*_Bolt|.*_Fertilizer_?\\d*|.*_Grenade|.*_Pill|.*_Tonic|Aluminum|Ammo_Casing|Animal_Fat|Carbon_Fiber|Composites|Concrete_Mix|Cured_Leather_?\\d?|Electronics|Epoxy_?\\d?|Glass\\d?|Gunpowder\\w*|Health_.*|Titanium_Plate|Organic_Resin|Platinum_Sheath|Refined_[a-zA-Z]+|Rope|Shotgun_Casing|Steel_Bloom\\d?|Tree_Sap\\w*" -- Ingot pattern for recipes that get count set to 1 local ingotPattern = "(?-s)(Platinum|Steel|Cold_Steel|Titanium)_Ingot|Aluminum|Refined_[a-zA-Z]+|Glass\\d?" local factor = 16 local bonus = 0.5 for _, row in ipairs(data.Rows) do local recipeName = row.Name -- Special case: Biofuel recipes - remove Tree_Sap input if string.find(recipeName, "Biofuel") then if row.Inputs then for i = #row.Inputs, 1, -1 do local input = row.Inputs[i] if input.Element and input.Element.RowName and string.find(input.Element.RowName, "Tree_Sap") then table.remove(row.Inputs, i) print("Removing input 'Tree_Sap' from processor recipe '" .. recipeName .. "'") end end end end -- Ingot recipes: set input and output counts to 1 if matchesPattern(recipeName, ingotPattern) then if row.Inputs then for _, input in ipairs(row.Inputs) do input.Count = 1 end end if row.Outputs then for _, output in ipairs(row.Outputs) do output.Count = 1 end end end -- Selected recipes: multiply inputs by factor, outputs by factor * (1 + bonus) if matchesPattern(recipeName, selectionPattern) then if row.Inputs then for _, input in ipairs(row.Inputs) do local oldCount = input.Count input.Count = input.Count * factor print("Recipe " .. recipeName .. " Input.Count: " .. oldCount .. " -> " .. input.Count) end end if row.Outputs then for _, output in ipairs(row.Outputs) do local oldCount = output.Count output.Count = math.floor(output.Count * factor * (1 + bonus)) print("Recipe " .. recipeName .. " Output.Count: " .. oldCount .. " -> " .. output.Count) end end end end `, } commands, err := ProcessJSON(content, command, "test.json") if err != nil { t.Fatalf("ProcessJSON failed: %v", err) } if len(commands) == 0 { t.Fatal("Expected at least one command") } // Apply the commands result := content for _, cmd := range commands { result = result[:cmd.From] + cmd.With + result[cmd.To:] } diff := cmp.Diff(result, expected) if diff != "" { t.Errorf("Differences:\n%s", diff) } // Check that the result matches expected (preserves formatting and changes weight) if result != expected { t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result) } } func TestRetardedJSONEditing(t *testing.T) { original := `{ "RowStruct": "/Script/Icarus.ItemableData", "Defaults": { "Behaviour": "None", "DisplayName": "", "Icon": "None", "Override_Glow_Icon": "None", "Description": "", "FlavorText": "", "Weight": 0, "bAllowZeroWeight": false, "MaxStack": 1 }, "Rows": [ { "DisplayName": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-DisplayName\", \"Fiber\")", "Icon": "/Game/Assets/2DArt/UI/Items/Item_Icons/Resources/ITEM_Fibre.ITEM_Fibre", "Description": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-Description\", \"A bundle of soft fiber, highly useful.\")", "FlavorText": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-FlavorText\", \"Fiber is collected from bast, the strong inner bark of certain flowering plants.\")", "Weight": 10, "MaxStack": 200, "Name": "Item_Fiber" } ] }` expected := `{ "RowStruct": "/Script/Icarus.ItemableData", "Defaults": { "Behaviour": "None", "DisplayName": "", "Icon": "None", "Override_Glow_Icon": "None", "Description": "", "FlavorText": "", "Weight": 0, "bAllowZeroWeight": false, "MaxStack": 1 }, "Rows": [ { "DisplayName": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-DisplayName\", \"Fiber\")", "Icon": "/Game/Assets/2DArt/UI/Items/Item_Icons/Resources/ITEM_Fibre.ITEM_Fibre", "Description": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-Description\", \"A bundle of soft fiber, highly useful.\")", "FlavorText": "NSLOCTEXT(\"D_Itemable\", \"Item_Fiber-FlavorText\", \"Fiber is collected from bast, the strong inner bark of certain flowering plants.\")", "Weight": 10, "MaxStack": 1000000, "Name": "Item_Fiber" } ] }` command := utils.ModifyCommand{ Name: "test", Lua: ` for _, row in ipairs(data.Rows) do if row.MaxStack then if string.find(row.Name, "Carrot") or string.find(row.Name, "Potato") then row.MaxStack = 25 else row.MaxStack = row.MaxStack * 10000 if row.MaxStack > 1000000 then row.MaxStack = 1000000 end end end end `, } commands, err := ProcessJSON(original, command, "test.json") if err != nil { t.Fatalf("ProcessJSON failed: %v", err) } if len(commands) == 0 { t.Fatal("Expected at least one command") } // Apply the commands result := original for _, cmd := range commands { result = result[:cmd.From] + cmd.With + result[cmd.To:] } diff := cmp.Diff(result, expected) if diff != "" { t.Errorf("Differences:\n%s", diff) } // Check that the weight was changed if result != expected { t.Errorf("Expected:\n%s\nGot:\n%s", expected, result) } } func TestRetardedJSONEditing2(t *testing.T) { original := ` { "Rows": [ { "Name": "Deep_Mining_Drill_Biofuel", "Meshable": { "RowName": "Mesh_Deep_Mining_Drill_Biofuel" }, "Itemable": { "RowName": "Item_Deep_Mining_Drill_Biofuel" }, "Interactable": { "RowName": "Deployable" }, "Focusable": { "RowName": "Focusable_1H" }, "Highlightable": { "RowName": "Generic" }, "Actionable": { "RowName": "Deployable" }, "Usable": { "RowName": "Place" }, "Deployable": { "RowName": "Deep_Mining_Drill_Biofuel" }, "Durable": { "RowName": "Deployable_750" }, "Inventory": { "RowName": "Deep_Mining_Drill_Biofuel" }, "Decayable": { "RowName": "Decay_MetaItem" }, "Generator": { "RowName": "Deep_Mining_Biofuel_Drill" }, "Resource": { "RowName": "Simple_Internal_Flow_Only" }, "Manual_Tags": { "GameplayTags": [ { "TagName": "Item.Machine" } ] }, "Generated_Tags": { "GameplayTags": [ { "TagName": "Item.Machine" }, { "TagName": "Traits.Meshable" }, { "TagName": "Traits.Itemable" }, { "TagName": "Traits.Interactable" }, { "TagName": "Traits.Highlightable" }, { "TagName": "Traits.Actionable" }, { "TagName": "Traits.Usable" }, { "TagName": "Traits.Deployable" }, { "TagName": "Traits.Durable" }, { "TagName": "Traits.Inventory" } ], "ParentTags": [] } } ] } ` expected := ` { "Rows": [ { "Name": "Deep_Mining_Drill_Biofuel", "Meshable": { "RowName": "Mesh_Deep_Mining_Drill_Biofuel" }, "Itemable": { "RowName": "Item_Deep_Mining_Drill_Biofuel" }, "Interactable": { "RowName": "Deployable" }, "Focusable": { "RowName": "Focusable_1H" }, "Highlightable": { "RowName": "Generic" }, "Actionable": { "RowName": "Deployable" }, "Usable": { "RowName": "Place" }, "Deployable": { "RowName": "Deep_Mining_Drill_Biofuel" }, "Durable": { "RowName": "Deployable_750" }, "Inventory": { "RowName": "Deep_Mining_Drill_Biofuel" }, "Decayable": { "RowName": "Decay_MetaItem" }, "Generator": { "RowName": "Deep_Mining_Biofuel_Drill" }, "Resource": { "RowName": "Simple_Internal_Flow_Only" }, "Manual_Tags": { "GameplayTags": [ { "TagName": "Item.Machine" } ] }, "Generated_Tags": { "GameplayTags": [ { "TagName": "Item.Machine" }, { "TagName": "Traits.Meshable" }, { "TagName": "Traits.Itemable" }, { "TagName": "Traits.Interactable" }, { "TagName": "Traits.Highlightable" }, { "TagName": "Traits.Actionable" }, { "TagName": "Traits.Usable" }, { "TagName": "Traits.Deployable" }, { "TagName": "Traits.Durable" }, { "TagName": "Traits.Inventory" } ], "ParentTags": [] } ,"AdditionalStats": {"(Value=\"BaseDeepMiningDrillSpeed_+%\")":4000}} ] } ` command := utils.ModifyCommand{ Name: "test", Lua: ` for i, row in ipairs(data.Rows) do -- Special case: Deep_Mining_Drill_Biofuel if string.find(row.Name, "Deep_Mining_Drill_Biofuel") then print("[DEBUG] Special case: Deep_Mining_Drill_Biofuel") if not row.AdditionalStats then print("[DEBUG] Creating AdditionalStats table for Deep_Mining_Drill_Biofuel") row.AdditionalStats = {} end print("[DEBUG] Setting BaseDeepMiningDrillSpeed_+% to 4000") row.AdditionalStats["(Value=\\\"BaseDeepMiningDrillSpeed_+%\\\")"] = 4000 end end `, } commands, err := ProcessJSON(original, command, "test.json") if err != nil { t.Fatalf("ProcessJSON failed: %v", err) } if len(commands) == 0 { t.Fatal("Expected at least one command") } // Apply the commands result := original for _, cmd := range commands { result = result[:cmd.From] + cmd.With + result[cmd.To:] } diff := cmp.Diff(result, expected) if diff != "" { t.Errorf("Differences:\n%s", diff) } if result != expected { t.Errorf("Expected:\n%s\nGot:\n%s", expected, result) } }