Immutable Extracts

This commit is contained in:
2026-05-20 20:20:07 +02:00
parent b1b7a08848
commit c37b27b720
5 changed files with 603 additions and 12 deletions
+237 -1
View File
@@ -444,6 +444,7 @@ extract:
func TestExtractConsumeArchivesFalseOverridesLegacyDeleteFlag(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src", "module"))
mustMkdir(t, filepath.Join(root, "src", "areas"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
@@ -474,7 +475,6 @@ extract:
}
}
`)
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
@@ -3253,6 +3253,197 @@ func TestExtractOverwritesAndRemovesStaleFiles(t *testing.T) {
}
}
func TestExtractMergesConfiguredGFFJSONFieldsAndLists(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src", "module"))
mustMkdir(t, filepath.Join(root, "src", "areas"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
assets: assets
build: build
extract:
merge:
gff_json:
- target: module/module.ifo.json
preserve_fields:
- Mod_Entry_Area
- Mod_Entry_X
merge_lists:
- field: Mod_Area_list
key_field: Area_Name
strategy: preserve_existing_order_append_new
`)
mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
"file_type": "IFO ",
"file_version": "V3.2",
"root": {
"struct_type": 0,
"fields": [
{
"label": "Mod_Name",
"type": "CExoString",
"value": "Test Module"
},
{
"label": "Mod_Entry_Area",
"type": "ResRef",
"value": "area_a"
},
{
"label": "Mod_Entry_X",
"type": "Float",
"value": 1
},
{
"label": "Mod_Area_list",
"type": "List",
"value": [
{
"struct_type": 6,
"fields": [
{
"label": "Area_Name",
"type": "ResRef",
"value": "area_a"
}
]
},
{
"struct_type": 6,
"fields": [
{
"label": "Area_Name",
"type": "ResRef",
"value": "area_b"
}
]
},
{
"struct_type": 6,
"fields": [
{
"label": "Area_Name",
"type": "ResRef",
"value": "area_c"
}
]
}
]
}
]
}
}
`)
for _, area := range []string{"area_a", "area_b", "area_c"} {
mustWriteFile(t, filepath.Join(root, "src", "areas", area+".are.json"), `{
"file_type": "ARE ",
"file_version": "V3.2",
"root": {
"struct_type": 0,
"fields": []
}
}
`)
}
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
if err := p.ValidateLayout(); err != nil {
t.Fatalf("validate layout: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("scan: %v", err)
}
if _, err := BuildModule(p); err != nil {
t.Fatalf("build module: %v", err)
}
mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
"file_type": "IFO ",
"file_version": "V3.2",
"root": {
"struct_type": 0,
"fields": [
{
"label": "Mod_Entry_Area",
"type": "ResRef",
"value": "area_b"
},
{
"label": "Mod_Entry_X",
"type": "Float",
"value": 99
},
{
"label": "Mod_Area_list",
"type": "List",
"value": [
{
"struct_type": 6,
"fields": [
{
"label": "Area_Name",
"type": "ResRef",
"value": "area_b"
}
]
},
{
"struct_type": 6,
"fields": [
{
"label": "Area_Name",
"type": "ResRef",
"value": "area_a"
}
]
},
{
"struct_type": 6,
"fields": [
{
"label": "Area_Name",
"type": "ResRef",
"value": "removed_area"
}
]
}
]
}
]
}
}
`)
if err := p.Scan(); err != nil {
t.Fatalf("rescan before extract: %v", err)
}
if _, err := Extract(p); err != nil {
t.Fatalf("extract: %v", err)
}
document := readGFFJSON(t, filepath.Join(root, "src", "module", "module.ifo.json"))
if got, want := fieldValue(t, document.Root, "Mod_Entry_Area"), gff.ResRefValue("area_b"); got != want {
t.Fatalf("expected preserved Mod_Entry_Area %#v, got %#v", want, got)
}
if got, want := fieldValue(t, document.Root, "Mod_Entry_X"), gff.FloatValue(99); got != want {
t.Fatalf("expected preserved Mod_Entry_X %#v, got %#v", want, got)
}
if got, want := keyedListValues(t, document.Root, "Mod_Area_list", "Area_Name"), []string{"area_b", "area_a", "area_c"}; !slices.Equal(got, want) {
t.Fatalf("expected merged area order %#v, got %#v", want, got)
}
}
func TestExtractNormalizesResourceNamesToLowercase(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src", "blueprints", "items"))
@@ -3423,6 +3614,51 @@ func mustWriteFile(t *testing.T, path, data string) {
}
}
func readGFFJSON(t *testing.T, path string) gff.Document {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
var document gff.Document
if err := json.Unmarshal(raw, &document); err != nil {
t.Fatalf("parse %s: %v", path, err)
}
return document
}
func fieldValue(t *testing.T, s gff.Struct, label string) gff.Value {
t.Helper()
for _, field := range s.Fields {
if field.Label == label {
return field.Value
}
}
t.Fatalf("field %s not found", label)
return nil
}
func keyedListValues(t *testing.T, s gff.Struct, listLabel, keyLabel string) []string {
t.Helper()
list, ok := fieldValue(t, s, listLabel).(gff.ListValue)
if !ok {
t.Fatalf("field %s is not a list", listLabel)
}
values := make([]string, 0, len(list))
for _, item := range list {
value := fieldValue(t, item, keyLabel)
switch typed := value.(type) {
case gff.ResRefValue:
values = append(values, string(typed))
case gff.StringValue:
values = append(values, string(typed))
default:
t.Fatalf("field %s has unsupported key type %T", keyLabel, value)
}
}
return values
}
func minimalGFF(t *testing.T, fileType string) []byte {
t.Helper()
document := gff.Document{