Compare commits
2 Commits
master
...
fd1df6e40e
Author | SHA1 | Date | |
---|---|---|---|
fd1df6e40e | |||
1a8c0b9f90 |
4
go.mod
4
go.mod
@@ -21,6 +21,10 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
9
go.sum
9
go.sum
@@ -36,6 +36,15 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
|||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,26 +86,216 @@ func ProcessJSON(content string, command utils.ModifyCommand, filename string) (
|
|||||||
return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err)
|
return commands, fmt.Errorf("failed to convert Lua table back to Go: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal back to JSON
|
// Use surgical JSON editing instead of full replacement
|
||||||
modifiedJSON, err := json.MarshalIndent(goData, "", " ")
|
commands, err = applySurgicalJSONChanges(content, jsonData, goData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
processJsonLogger.Error("Failed to marshal modified data to JSON: %v", err)
|
processJsonLogger.Error("Failed to apply surgical JSON changes: %v", err)
|
||||||
return commands, fmt.Errorf("failed to marshal modified data to JSON: %v", err)
|
return commands, fmt.Errorf("failed to apply surgical JSON changes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create replacement command for the entire file
|
|
||||||
// For JSON mode, we always replace the entire content
|
|
||||||
commands = append(commands, utils.ReplaceCommand{
|
|
||||||
From: 0,
|
|
||||||
To: len(content),
|
|
||||||
With: string(modifiedJSON),
|
|
||||||
})
|
|
||||||
|
|
||||||
processJsonLogger.Debug("Total JSON processing time: %v", time.Since(startTime))
|
processJsonLogger.Debug("Total JSON processing time: %v", time.Since(startTime))
|
||||||
processJsonLogger.Debug("Generated %d total modifications", len(commands))
|
processJsonLogger.Debug("Generated %d total modifications", len(commands))
|
||||||
return commands, nil
|
return commands, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applySurgicalJSONChanges compares original and modified data and applies changes surgically
|
||||||
|
func applySurgicalJSONChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
|
||||||
|
var commands []utils.ReplaceCommand
|
||||||
|
|
||||||
|
// Convert both to JSON for comparison
|
||||||
|
originalJSON, err := json.Marshal(originalData)
|
||||||
|
if err != nil {
|
||||||
|
return commands, fmt.Errorf("failed to marshal original data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiedJSON, err := json.Marshal(modifiedData)
|
||||||
|
if err != nil {
|
||||||
|
return commands, fmt.Errorf("failed to marshal modified data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no changes, return empty commands
|
||||||
|
if string(originalJSON) == string(modifiedJSON) {
|
||||||
|
return commands, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try true surgical approach that preserves formatting
|
||||||
|
surgicalCommands, err := applyTrueSurgicalChanges(content, originalData, modifiedData)
|
||||||
|
if err == nil && len(surgicalCommands) > 0 {
|
||||||
|
return surgicalCommands, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to full replacement with proper formatting
|
||||||
|
modifiedJSONIndented, err := json.MarshalIndent(modifiedData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return commands, fmt.Errorf("failed to marshal modified data with indentation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = append(commands, utils.ReplaceCommand{
|
||||||
|
From: 0,
|
||||||
|
To: len(content),
|
||||||
|
With: string(modifiedJSONIndented),
|
||||||
|
})
|
||||||
|
|
||||||
|
return commands, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyTrueSurgicalChanges attempts to make surgical changes while preserving exact formatting
|
||||||
|
func applyTrueSurgicalChanges(content string, originalData, modifiedData interface{}) ([]utils.ReplaceCommand, error) {
|
||||||
|
var commands []utils.ReplaceCommand
|
||||||
|
|
||||||
|
// Find changes by comparing the data structures
|
||||||
|
changes := findDeepChanges("", originalData, modifiedData)
|
||||||
|
|
||||||
|
if len(changes) == 0 {
|
||||||
|
return commands, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply changes surgically using sjson.Set() to preserve formatting
|
||||||
|
modifiedContent := content
|
||||||
|
for path, newValue := range changes {
|
||||||
|
var err error
|
||||||
|
modifiedContent, err = sjson.Set(modifiedContent, path, newValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to apply surgical change at path %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we successfully made changes, create a replacement command
|
||||||
|
if modifiedContent != content {
|
||||||
|
commands = append(commands, utils.ReplaceCommand{
|
||||||
|
From: 0,
|
||||||
|
To: len(content),
|
||||||
|
With: modifiedContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findDeepChanges recursively finds all paths that need to be changed
|
||||||
|
func findDeepChanges(basePath string, original, modified interface{}) map[string]interface{} {
|
||||||
|
changes := make(map[string]interface{})
|
||||||
|
|
||||||
|
switch orig := original.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if mod, ok := modified.(map[string]interface{}); ok {
|
||||||
|
// Check each key in the modified data
|
||||||
|
for key, modValue := range mod {
|
||||||
|
var currentPath string
|
||||||
|
if basePath == "" {
|
||||||
|
currentPath = key
|
||||||
|
} else {
|
||||||
|
currentPath = basePath + "." + key
|
||||||
|
}
|
||||||
|
|
||||||
|
if origValue, exists := orig[key]; exists {
|
||||||
|
// Key exists in both, check if value changed
|
||||||
|
if !deepEqual(origValue, modValue) {
|
||||||
|
// If it's a nested object/array, recurse
|
||||||
|
switch modValue.(type) {
|
||||||
|
case map[string]interface{}, []interface{}:
|
||||||
|
nestedChanges := findDeepChanges(currentPath, origValue, modValue)
|
||||||
|
for nestedPath, nestedValue := range nestedChanges {
|
||||||
|
changes[nestedPath] = nestedValue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Primitive value changed
|
||||||
|
changes[currentPath] = modValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New key added
|
||||||
|
changes[currentPath] = modValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
if mod, ok := modified.([]interface{}); ok {
|
||||||
|
// For arrays, check each index
|
||||||
|
for i, modValue := range mod {
|
||||||
|
var currentPath string
|
||||||
|
if basePath == "" {
|
||||||
|
currentPath = fmt.Sprintf("%d", i)
|
||||||
|
} else {
|
||||||
|
currentPath = fmt.Sprintf("%s.%d", basePath, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < len(orig) {
|
||||||
|
// Index exists in both, check if value changed
|
||||||
|
if !deepEqual(orig[i], modValue) {
|
||||||
|
// If it's a nested object/array, recurse
|
||||||
|
switch modValue.(type) {
|
||||||
|
case map[string]interface{}, []interface{}:
|
||||||
|
nestedChanges := findDeepChanges(currentPath, orig[i], modValue)
|
||||||
|
for nestedPath, nestedValue := range nestedChanges {
|
||||||
|
changes[nestedPath] = nestedValue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Primitive value changed
|
||||||
|
changes[currentPath] = modValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New array element added
|
||||||
|
changes[currentPath] = modValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// For primitive types, compare directly
|
||||||
|
if !deepEqual(original, modified) {
|
||||||
|
if basePath == "" {
|
||||||
|
changes[""] = modified
|
||||||
|
} else {
|
||||||
|
changes[basePath] = modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// deepEqual performs deep comparison of two values
|
||||||
|
func deepEqual(a, b interface{}) bool {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch av := a.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if bv, ok := b.(map[string]interface{}); ok {
|
||||||
|
if len(av) != len(bv) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range av {
|
||||||
|
if !deepEqual(v, bv[k]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
case []interface{}:
|
||||||
|
if bv, ok := b.([]interface{}); ok {
|
||||||
|
if len(av) != len(bv) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, v := range av {
|
||||||
|
if !deepEqual(v, bv[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return a == b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ToLuaTable converts a Go interface{} to a Lua table recursively
|
// ToLuaTable converts a Go interface{} to a Lua table recursively
|
||||||
func ToLuaTable(L *lua.LState, data interface{}) (*lua.LTable, error) {
|
func ToLuaTable(L *lua.LState, data interface{}) (*lua.LTable, error) {
|
||||||
toLuaTableLogger := jsonLogger.WithPrefix("ToLuaTable")
|
toLuaTableLogger := jsonLogger.WithPrefix("ToLuaTable")
|
||||||
|
283
processor/surgical_json_test.go
Normal file
283
processor/surgical_json_test.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cook/utils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of exact string comparison, check that key values are present
|
||||||
|
// This accounts for field ordering differences in JSON
|
||||||
|
if !contains(result, `"value": 84`) && tt.name == "Modify single field" {
|
||||||
|
t.Errorf("Expected value to be 84, got:\n%s", result)
|
||||||
|
}
|
||||||
|
if !contains(result, `"newField": "added"`) && tt.name == "Add new field" {
|
||||||
|
t.Errorf("Expected newField to be added, got:\n%s", result)
|
||||||
|
}
|
||||||
|
if !contains(result, `"enabled": true`) && tt.name == "Modify nested field" {
|
||||||
|
t.Errorf("Expected enabled to be true, got:\n%s", result)
|
||||||
|
}
|
||||||
|
if !contains(result, `"timeout": 60`) && tt.name == "Modify nested field" {
|
||||||
|
t.Errorf("Expected timeout to be 60, got:\n%s", 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
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:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the weight was changed
|
||||||
|
if !contains(result, `"Weight": 500`) {
|
||||||
|
t.Errorf("Expected weight to be changed to 500, got:\n%s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that formatting is preserved (should have proper indentation)
|
||||||
|
if !contains(result, " \"Weight\": 500") {
|
||||||
|
t.Errorf("Expected proper indentation, got:\n%s", 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:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the weight was changed
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected:\n%s\nGot:\n%s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && (s == substr ||
|
||||||
|
(len(s) > len(substr) && (s[:len(substr)] == substr ||
|
||||||
|
s[len(s)-len(substr):] == substr ||
|
||||||
|
containsSubstring(s, substr))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSubstring(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
11
test_surgical.yml
Normal file
11
test_surgical.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
- name: SurgicalWeightTest
|
||||||
|
json: true
|
||||||
|
lua: |
|
||||||
|
-- This demonstrates surgical JSON editing
|
||||||
|
-- Only the Weight field of Item_Fiber will be modified
|
||||||
|
data.Rows[1].Weight = 999
|
||||||
|
modified = true
|
||||||
|
files:
|
||||||
|
- 'D_Itemable.json'
|
||||||
|
reset: false
|
||||||
|
loglevel: INFO
|
Reference in New Issue
Block a user