feat: extract set_fields rule forces Int fields on gff_json merge targets
Glob targets now match via matchPathPattern, so one rule can cover areas/*.are.json. set_fields runs after preserve/merge so the forced value always wins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -350,12 +350,10 @@ func mergeExtractedGFFJSON(p *project.Project, target string, extracted *gff.Doc
|
|||||||
}
|
}
|
||||||
|
|
||||||
raw, err := os.ReadFile(target)
|
raw, err := os.ReadFile(target)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("read existing source %s: %w", target, err)
|
return fmt.Errorf("read existing source %s: %w", target, err)
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
var existing gff.Document
|
var existing gff.Document
|
||||||
if err := json.Unmarshal(raw, &existing); err != nil {
|
if err := json.Unmarshal(raw, &existing); err != nil {
|
||||||
return fmt.Errorf("parse existing source %s: %w", target, err)
|
return fmt.Errorf("parse existing source %s: %w", target, err)
|
||||||
@@ -371,6 +369,30 @@ func mergeExtractedGFFJSON(p *project.Project, target string, extracted *gff.Doc
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set_fields runs last so a forced value also wins over preserve_fields.
|
||||||
|
for _, setRule := range rule.SetFields {
|
||||||
|
if err := setGFFIntField(&extracted.Root, setRule); err != nil {
|
||||||
|
return fmt.Errorf("set_fields on %s: %w", target, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setGFFIntField forces an existing Int field to a fixed value; a field the
|
||||||
|
// document does not have is left absent rather than invented.
|
||||||
|
func setGFFIntField(s *gff.Struct, rule project.ExtractSetFieldRule) error {
|
||||||
|
for index, field := range s.Fields {
|
||||||
|
if field.Label != rule.Field {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := field.Value.(gff.IntValue); !ok {
|
||||||
|
return fmt.Errorf("field %q is %s, not Int", rule.Field, field.Type)
|
||||||
|
}
|
||||||
|
s.Fields[index].Value = gff.IntValue(rule.Value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +406,7 @@ func extractGFFJSONMergeRule(p *project.Project, target string) (project.Extract
|
|||||||
return project.ExtractGFFJSONMergeRule{}, false, nil
|
return project.ExtractGFFJSONMergeRule{}, false, nil
|
||||||
}
|
}
|
||||||
for _, rule := range p.EffectiveConfig().Extract.Merge.GFFJSON {
|
for _, rule := range p.EffectiveConfig().Extract.Merge.GFFJSON {
|
||||||
if rule.Target == rel {
|
if rule.Target == rel || matchPathPattern(rel, rule.Target) {
|
||||||
return rule, true, nil
|
return rule, true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3503,6 +3503,100 @@ extract:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractSetsConfiguredGFFJSONFields(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: areas/*.are.json
|
||||||
|
set_fields:
|
||||||
|
- field: ChanceRain
|
||||||
|
value: 0
|
||||||
|
- field: ChanceSnow
|
||||||
|
value: 0
|
||||||
|
`)
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
mustWriteFile(t, filepath.Join(root, "src", "areas", "area_a.are.json"), `{
|
||||||
|
"file_type": "ARE ",
|
||||||
|
"file_version": "V3.2",
|
||||||
|
"root": {
|
||||||
|
"struct_type": 0,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"label": "ChanceRain",
|
||||||
|
"type": "Int",
|
||||||
|
"value": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "WindPower",
|
||||||
|
"type": "Int",
|
||||||
|
"value": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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", "areas", "area_a.are.json"))
|
||||||
|
if got, want := fieldValue(t, document.Root, "ChanceRain"), gff.IntValue(0); got != want {
|
||||||
|
t.Fatalf("expected forced ChanceRain %#v, got %#v", want, got)
|
||||||
|
}
|
||||||
|
if got, want := fieldValue(t, document.Root, "WindPower"), gff.IntValue(2); got != want {
|
||||||
|
t.Fatalf("expected untouched WindPower %#v, got %#v", want, got)
|
||||||
|
}
|
||||||
|
if _, ok := gffField(document.Root, "ChanceSnow"); ok {
|
||||||
|
t.Fatalf("expected absent ChanceSnow to stay absent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractNormalizesResourceNamesToLowercase(t *testing.T) {
|
func TestExtractNormalizesResourceNamesToLowercase(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
mustMkdir(t, filepath.Join(root, "src", "blueprints", "items"))
|
mustMkdir(t, filepath.Join(root, "src", "blueprints", "items"))
|
||||||
|
|||||||
@@ -281,6 +281,15 @@ type ExtractGFFJSONMergeRule struct {
|
|||||||
Target string `json:"target" yaml:"target"`
|
Target string `json:"target" yaml:"target"`
|
||||||
PreserveFields []string `json:"preserve_fields" yaml:"preserve_fields"`
|
PreserveFields []string `json:"preserve_fields" yaml:"preserve_fields"`
|
||||||
MergeLists []ExtractListMergeRule `json:"merge_lists" yaml:"merge_lists"`
|
MergeLists []ExtractListMergeRule `json:"merge_lists" yaml:"merge_lists"`
|
||||||
|
SetFields []ExtractSetFieldRule `json:"set_fields" yaml:"set_fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractSetFieldRule forces an Int field to a fixed value on every extract,
|
||||||
|
// overriding whatever the toolset saved. Fields absent from the extracted
|
||||||
|
// document are left absent.
|
||||||
|
type ExtractSetFieldRule struct {
|
||||||
|
Field string `json:"field" yaml:"field"`
|
||||||
|
Value int32 `json:"value" yaml:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtractListMergeRule struct {
|
type ExtractListMergeRule struct {
|
||||||
@@ -980,6 +989,12 @@ func validateExtractMergeConfig(config ExtractMergeConfig) []error {
|
|||||||
seenTargets[normalized] = struct{}{}
|
seenTargets[normalized] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for setIndex, setRule := range rule.SetFields {
|
||||||
|
if strings.TrimSpace(setRule.Field) == "" {
|
||||||
|
failures = append(failures, fmt.Errorf("%s.set_fields[%d].field must not be empty", prefix, setIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for listIndex, listRule := range rule.MergeLists {
|
for listIndex, listRule := range rule.MergeLists {
|
||||||
listPrefix := fmt.Sprintf("%s.merge_lists[%d]", prefix, listIndex)
|
listPrefix := fmt.Sprintf("%s.merge_lists[%d]", prefix, listIndex)
|
||||||
if strings.TrimSpace(listRule.Field) == "" {
|
if strings.TrimSpace(listRule.Field) == "" {
|
||||||
@@ -1346,6 +1361,9 @@ func normalizeConfig(cfg *Config) {
|
|||||||
}
|
}
|
||||||
cfg.Extract.Merge.GFFJSON[i].Target = target
|
cfg.Extract.Merge.GFFJSON[i].Target = target
|
||||||
cfg.Extract.Merge.GFFJSON[i].PreserveFields = normalizeStringSlice(cfg.Extract.Merge.GFFJSON[i].PreserveFields)
|
cfg.Extract.Merge.GFFJSON[i].PreserveFields = normalizeStringSlice(cfg.Extract.Merge.GFFJSON[i].PreserveFields)
|
||||||
|
for j := range cfg.Extract.Merge.GFFJSON[i].SetFields {
|
||||||
|
cfg.Extract.Merge.GFFJSON[i].SetFields[j].Field = strings.TrimSpace(cfg.Extract.Merge.GFFJSON[i].SetFields[j].Field)
|
||||||
|
}
|
||||||
for j := range cfg.Extract.Merge.GFFJSON[i].MergeLists {
|
for j := range cfg.Extract.Merge.GFFJSON[i].MergeLists {
|
||||||
cfg.Extract.Merge.GFFJSON[i].MergeLists[j].Field = strings.TrimSpace(cfg.Extract.Merge.GFFJSON[i].MergeLists[j].Field)
|
cfg.Extract.Merge.GFFJSON[i].MergeLists[j].Field = strings.TrimSpace(cfg.Extract.Merge.GFFJSON[i].MergeLists[j].Field)
|
||||||
cfg.Extract.Merge.GFFJSON[i].MergeLists[j].KeyField = strings.TrimSpace(cfg.Extract.Merge.GFFJSON[i].MergeLists[j].KeyField)
|
cfg.Extract.Merge.GFFJSON[i].MergeLists[j].KeyField = strings.TrimSpace(cfg.Extract.Merge.GFFJSON[i].MergeLists[j].KeyField)
|
||||||
|
|||||||
Reference in New Issue
Block a user