From baa81f6cb7232ff709a14f5da2ec0f6b8e3e3537 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Thu, 14 May 2026 13:45:16 +0200 Subject: [PATCH] Praying for deliberately deleted entries to STOP BEING PARSED AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --- internal/topdata/native.go | 18 +++++ internal/topdata/topdata_test.go | 130 +++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/internal/topdata/native.go b/internal/topdata/native.go index 0d7d5d5..fd7f29a 100644 --- a/internal/topdata/native.go +++ b/internal/topdata/native.go @@ -971,6 +971,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) { } } if overrideRequestsNullRow(override) { + retireRowIdentity(row, lockData, retiredKeys) nullifyOverrideRow(row, columns, rowByKey) continue } @@ -1051,6 +1052,9 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) { } } for _, module := range generatedModules { + if module.ApplyAfterModules { + continue + } if err := applyModuleData(module.Name, module.Data); err != nil { return nativeCollectedDataset{}, err } @@ -4284,6 +4288,20 @@ func nullifyOverrideRow(row map[string]any, columns []string, rowByKey map[strin } } +func retireRowIdentity(row map[string]any, lockData map[string]int, retiredKeys map[string]struct{}) { + if row == nil { + return + } + oldKey, ok := row["key"].(string) + if !ok || oldKey == "" { + return + } + delete(lockData, oldKey) + if retiredKeys != nil { + retiredKeys[oldKey] = struct{}{} + } +} + func updateOverrideRowKey(datasetName string, row map[string]any, expanded map[string]any, rowByKey map[string]map[string]any, lockData map[string]int) (bool, string, error) { oldKey, _ := row["key"].(string) newKeyValue, hasKey := expanded["key"] diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index b7b84fd..7c22b86 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -6409,6 +6409,48 @@ func TestBuildOverridePrecedenceSuppressesBaseAndEntries(t *testing.T) { } } +func TestBuildNullRowRetiresLockIdentityGlobally(t *testing.T) { + root := testProjectRoot(t) + mkdirAll(t, filepath.Join(root, "topdata", "data", "dense", "modules")) + writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") + writeFile(t, filepath.Join(root, "topdata", "data", "dense", "base.json"), `{ + "output": "dense.2da", + "columns": ["Label", "Value"], + "rows": [ + {"id": 0, "key": "dense:one", "Label": "One", "Value": "one"} + ] +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "dense", "lock.json"), `{ + "dense:one": 0 +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "dense", "modules", "rmv_one.json"), `{ + "overrides": [ + {"id": 0, "key": null, "null": true} + ] +}`+"\n") + mkdirAll(t, filepath.Join(root, "reference")) + writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n") + + result, err := BuildNative(testProject(root), nil) + if err != nil { + t.Fatalf("BuildNative failed: %v", err) + } + lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "dense", "lock.json")) + if err != nil { + t.Fatalf("read dense lock: %v", err) + } + if strings.Contains(string(lockRaw), `"dense:one"`) { + t.Fatalf("expected nulled row to retire lock identity, got:\n%s", string(lockRaw)) + } + got, err := os.ReadFile(filepath.Join(result.Output2DADir, "dense.2da")) + if err != nil { + t.Fatalf("read dense.2da: %v", err) + } + if !strings.Contains(string(got), "0\t****\t****\n") { + t.Fatalf("expected nulled dense row in output, got:\n%s", string(got)) + } +} + func TestResolveOverrideTargetPrefersIDOverKey(t *testing.T) { keyRow := map[string]any{"id": 192, "key": "feat:skillfocus_intimidate"} idRow := map[string]any{"id": 916, "key": "feat:taunt"} @@ -8427,6 +8469,94 @@ func TestFeatGeneratedContextIgnoresExplicitlyRetiredFeatKeys(t *testing.T) { } } +func TestBuildGeneratedSkillFocusDoesNotReuseNulledFeatRow(t *testing.T) { + root := testProjectRoot(t) + writeFeatGeneratedHarness(t, root, map[string]string{ + "skill_focus.json": `{ + "family": "skill_focus", + "family_key": "skill_focus", + "template": "masterfeats:skillfocus", + "name_prefix": "Skill Focus", + "label_prefix": "FEAT_SKILL_FOCUS", + "constant_prefix": "FEAT_SKILL_FOCUS", + "default_fields": { + "CRValue": "0.5", + "ReqSkillMinRanks": "1", + "ALLCLASSESCANUSE": "1", + "TOOLSCATEGORIES": "6", + "PreReqEpic": "0", + "ReqAction": "1" + }, + "apply_after_modules": true, + "child_ref_field": "REQSKILL", + "child_source": { + "dataset": "skills", + "predicate": "accessible" + }, + "overrides": { + "skills:intimidate": { + "ICON": "ife_X2SkFInti" + } + } +}` + "\n", + }, map[string]int{ + "feat:skill_focus_intimidate": 916, + }) + writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{ + "output": "skills.2da", + "columns": ["Label", "Name", "Description", "Icon", "Untrained", "KeyAbility", "ArmorCheckPenalty", "AllClassesCanUse", "Category", "MaxCR", "Constant", "HostileSkill", "HideFromLevelUp"], + "rows": [ + {"id": 18, "key": "skills:intimidate", "Label": "Intimidate", "Name": "8756", "Description": "8786", "Icon": "isk_x2inti", "Untrained": "1", "KeyAbility": "CHA", "ArmorCheckPenalty": "0", "AllClassesCanUse": "1", "Category": "****", "MaxCR": "****", "Constant": "SKILL_INTIMIDATE", "HostileSkill": "1", "HideFromLevelUp": "0"} + ] +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:intimidate":18}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{ + "output": "feat.2da", + "compare_reference": false, + "columns": [ + "LABEL","FEAT","DESCRIPTION","ICON","MINATTACKBONUS","MINSTR","MINDEX","MININT","MINWIS","MINCON","MINCHA", + "MINSPELLLVL","PREREQFEAT1","PREREQFEAT2","GAINMULTIPLE","EFFECTSSTACK","ALLCLASSESCANUSE","CATEGORY","MAXCR", + "SPELLID","SUCCESSOR","CRValue","USESPERDAY","MASTERFEAT","TARGETSELF","OrReqFeat0","OrReqFeat1","OrReqFeat2", + "OrReqFeat3","OrReqFeat4","REQSKILL","ReqSkillMinRanks","REQSKILL2","ReqSkillMinRanks2","Constant","TOOLSCATEGORIES", + "HostileFeat","MinLevel","MinLevelClass","MaxLevel","MinFortSave","PreReqEpic","ReqAction" + ], + "rows": [ + {"id": 192, "key": "feat:skill_focus_taunt", "LABEL": "FEAT_SKILL_FOCUS_TAUNT", "REQSKILL": 18, "MASTERFEAT": 4, "Constant": "FEAT_SKILL_FOCUS_TAUNT"}, + {"id": 916, "key": "feat:skill_focus_intimidate", "LABEL": "FEAT_SKILL_FOCUS_INTIMIDATE", "REQSKILL": 24, "MASTERFEAT": 4, "Constant": "FEAT_SKILL_FOCUS_INTIMIDATE"} + ] +}`+"\n") + mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "modules", "removedandhidden")) + writeFile(t, filepath.Join(root, "topdata", "data", "feat", "modules", "removedandhidden", "rmv_feat_intimidate.json"), `{ + "overrides": [ + {"id": 916, "null": true, "key": null} + ] +}`+"\n") + + result, err := BuildNative(testProject(root), nil) + if err != nil { + t.Fatalf("BuildNative failed: %v", err) + } + lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "feat", "lock.json")) + if err != nil { + t.Fatalf("read feat lock: %v", err) + } + lockText := string(lockRaw) + if strings.Contains(lockText, `"feat:skill_focus_intimidate": 916`) { + t.Fatalf("expected generated skill focus intimidate to avoid nulled row 916, got:\n%s", lockText) + } + got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da")) + if err != nil { + t.Fatalf("read feat.2da: %v", err) + } + text := string(got) + if strings.Contains(text, "916\tFEAT_SKILL_FOCUS_INTIMIDATE\t") { + t.Fatalf("expected generated skill focus intimidate to avoid row 916, got:\n%s", text) + } + if !strings.Contains(text, "FEAT_SKILL_FOCUS_INTIMIDATE") { + t.Fatalf("expected generated skill focus intimidate row, got:\n%s", text) + } +} + func TestOverrideRequestsNullRowUsesExplicitNullFlag(t *testing.T) { if overrideRequestsNullRow(map[string]any{"key": nil}) { t.Fatal("key: null should not blank a row")