Support for global.json files

This commit is contained in:
2026-06-05 10:36:47 +02:00
parent d0e065536c
commit c672a1ff33
5 changed files with 804 additions and 264 deletions
+230
View File
@@ -1722,6 +1722,7 @@ func TestBuildNativeExpandsClassesFeatMasterfeatRowsWithFilter(t *testing.T) {
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "feats", "fighter.json"), `{
"key": "classes/feats:fighter",
@@ -1797,6 +1798,7 @@ func TestBuildNativeExpandsClassesFeatClassskillsFilter(t *testing.T) {
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "feats", "fighter.json"), `{
"key": "classes/feats:fighter",
@@ -1870,6 +1872,7 @@ func TestBuildNativeAppliesConfiguredClassFeatInjections(t *testing.T) {
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "feats", "fighter.json"), `{
"key": "classes/feats:fighter",
@@ -1917,6 +1920,15 @@ func TestBuildNativeAppliesConfiguredClassFeatInjections(t *testing.T) {
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "lock.json"), `{"masterfeats:skillfocus": 4}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
"output": "skills.2da",
"columns": ["Label"],
"rows": [
{"id": 1, "key": "skills:athletics", "Label": "Athletics"},
{"id": 2, "key": "skills:alchemy", "Label": "Alchemy"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:athletics":1,"skills:alchemy":2}`+"\n")
p := testProject(root)
p.Config.TopData.ReferenceBuilder = ""
@@ -1953,6 +1965,108 @@ func TestBuildNativeAppliesConfiguredClassFeatInjections(t *testing.T) {
}
}
func TestBuildNativeAppliesClassFeatGlobalJSONInjections(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "feats"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "feats", "global.json"), `{
"position": "prepend",
"injections": [
{
"row": {"FeatIndex": {"id": "feat:literate"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "0"},
"unless_present": [{"field": "FeatIndex", "id": "feat:illiterate"}]
},
{
"row": {"FeatIndex": {"id": "feat:customglobal"}, "List": "3", "GrantedOnLevel": "2", "OnMenu": "1"}
},
{
"row": {"FeatIndex": {"id": "masterfeats:skillfocus", "filter": "classskills"}, "List": "1", "GrantedOnLevel": "2", "OnMenu": "0"}
}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "feats", "fighter.json"), `{
"key": "classes/feats:fighter",
"output": "cls_feat_fighter.2da",
"columns": ["FeatLabel", "FeatIndex", "List", "GrantedOnLevel", "OnMenu"],
"rows": [
{"FeatIndex": {"id": "feat:illiterate"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "0"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "fighter.json"), `{
"key": "classes/skills:fighter",
"output": "cls_skill_fighter.2da",
"columns": ["SkillLabel", "SkillIndex", "ClassSkill"],
"rows": [
{"SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
{"SkillLabel": "Alchemy", "SkillIndex": {"id": "skills:alchemy"}, "ClassSkill": "0"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
"output": "feat.2da",
"columns": ["LABEL", "FEAT", "DESCRIPTION", "MASTERFEAT", "REQSKILL", "SUCCESSOR"],
"rows": [
{"id": 1, "key": "feat:illiterate", "LABEL": "Illiterate", "FEAT": "100", "DESCRIPTION": "101", "MASTERFEAT": "****", "REQSKILL": "****", "SUCCESSOR": "****"},
{"id": 2, "key": "feat:literate", "LABEL": "Literate", "FEAT": "102", "DESCRIPTION": "103", "MASTERFEAT": "****", "REQSKILL": "****", "SUCCESSOR": "****"},
{"id": 3, "key": "feat:customglobal", "LABEL": "CustomGlobal", "FEAT": "104", "DESCRIPTION": "105", "MASTERFEAT": "****", "REQSKILL": "****", "SUCCESSOR": "****"},
{"id": 10, "key": "feat:skillfocus_athletics", "LABEL": "SkillFocusAthletics", "FEAT": "200", "DESCRIPTION": "201", "MASTERFEAT": "4", "REQSKILL": "skills:athletics", "SUCCESSOR": "****"},
{"id": 11, "key": "feat:skillfocus_alchemy", "LABEL": "SkillFocusAlchemy", "FEAT": "204", "DESCRIPTION": "205", "MASTERFEAT": "4", "REQSKILL": "skills:alchemy", "SUCCESSOR": "****"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{
"feat:illiterate": 1,
"feat:literate": 2,
"feat:customglobal": 3,
"feat:skillfocus_athletics": 10,
"feat:skillfocus_alchemy": 11
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "base.json"), `{
"output": "masterfeats.2da",
"columns": ["LABEL", "STRREF", "DESCRIPTION", "ICON"],
"rows": [
{"id": 4, "key": "masterfeats:skillfocus", "LABEL": "SkillFocus", "STRREF": "6493", "DESCRIPTION": "426", "ICON": "ife_skfoc"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "lock.json"), `{"masterfeats:skillfocus": 4}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
"output": "skills.2da",
"columns": ["Label"],
"rows": [
{"id": 1, "key": "skills:athletics", "Label": "Athletics"},
{"id": 2, "key": "skills:alchemy", "Label": "Alchemy"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:athletics":1,"skills:alchemy":2}`+"\n")
p := testProject(root)
p.Config.TopData.ReferenceBuilder = ""
result, err := BuildNativeWithOptions(p, NativeBuildOptions{BuildWiki: false}, nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "cls_feat_fighter.2da"))
if err != nil {
t.Fatalf("read cls_feat_fighter.2da: %v", err)
}
text := string(got)
if !strings.Contains(text, "CustomGlobal\t3\t3\t2\t1\n") {
t.Fatalf("expected global.json feat injection, got:\n%s", text)
}
if strings.Contains(text, "Literate\t2\t3\t1\t0\n") {
t.Fatalf("expected literate global injection to be skipped when illiterate is present, got:\n%s", text)
}
if !strings.Contains(text, "SkillFocusAthletics\t10\t1\t2\t0\n") {
t.Fatalf("expected global.json class-skill masterfeat expansion, got:\n%s", text)
}
if strings.Contains(text, "SkillFocusAlchemy") {
t.Fatalf("expected non-class skill focus to be filtered out, got:\n%s", text)
}
}
func TestResolveCanonicalDatasetKeyMatchesMasterfeatAliases(t *testing.T) {
keyToID := map[string]int{
"masterfeats:skill_focus": 4,
@@ -13859,6 +13973,122 @@ func TestBuildNativeCreatesRootLockfileForImplicitRootPlainTableKeys(t *testing.
}
}
func TestBuildNativeAppliesGlobalJSONAllOverrideToBaseDataset(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), `{
"output": "spells.2da",
"columns": ["Label", "Metamagic"],
"rows": [
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog", "Metamagic": "255"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "lock.json"), `{"spells:acid_fog":0}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "extra.json"), `{
"entries": {
"spells:burning_hands": {"Label": "BurningHands", "Metamagic": "127"}
}
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
"overrides": [
{"match": "all", "Metamagic": "0"}
]
}`+"\n")
proj := testProject(root)
proj.Config.TopData.ReferenceBuilder = ""
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "spells.2da"))
if err != nil {
t.Fatalf("read spells.2da: %v", err)
}
text := string(got)
if !strings.Contains(text, "0\tAcidFog\t0\n") {
t.Fatalf("expected base spell metamagic to be globally zeroed, got:\n%s", text)
}
if !strings.Contains(text, "1\tBurningHands\t0\n") {
t.Fatalf("expected module-added spell metamagic to be globally zeroed, got:\n%s", text)
}
if strings.Contains(text, "global") {
t.Fatalf("global.json must not be emitted as a separate dataset, got:\n%s", text)
}
}
func TestBuildNativeAppliesRecursiveGlobalJSONPlainDatasetInjections(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills", "baseclasses"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills", "prestigeclasses"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "global.json"), `{
"injections": [
{
"row": {
"SkillLabel": "ProfessionFarmer",
"SkillIndex": {"id": "skills:profession_farmer"},
"ClassSkill": "1"
}
}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "baseclasses", "fighter.json"), `{
"key": "classes/skills:fighter",
"output": "cls_skill_fighter.2da",
"columns": ["SkillLabel", "SkillIndex", "ClassSkill"],
"rows": [
{"SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "prestigeclasses", "assassin.json"), `{
"key": "classes/skills:assassin",
"output": "cls_skill_assassin.2da",
"columns": ["SkillLabel", "SkillIndex", "ClassSkill"],
"rows": [
{"SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
"output": "skills.2da",
"columns": ["Label"],
"rows": [
{"id": 1, "key": "skills:athletics", "Label": "Athletics"},
{"id": 2, "key": "skills:profession_farmer", "Label": "ProfessionFarmer"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:athletics":1,"skills:profession_farmer":2}`+"\n")
proj := testProject(root)
proj.Config.TopData.ReferenceBuilder = ""
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
fighterBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "cls_skill_fighter.2da"))
if err != nil {
t.Fatalf("read cls_skill_fighter.2da: %v", err)
}
fighterText := string(fighterBytes)
if !strings.Contains(fighterText, "ProfessionFarmer\t2\t1\n") {
t.Fatalf("expected recursive global skill injection, got:\n%s", fighterText)
}
assassinBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "cls_skill_assassin.2da"))
if err != nil {
t.Fatalf("read cls_skill_assassin.2da: %v", err)
}
if got := strings.Count(string(assassinBytes), "ProfessionFarmer"); got != 1 {
t.Fatalf("expected authored profession row to dedupe global injection, got %d occurrences:\n%s", got, string(assassinBytes))
}
if _, err := os.Stat(filepath.Join(result.Output2DADir, "global.2da")); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("global.json must not emit global.2da, got err %v", err)
}
}
func TestBuildNativeExtendsConfiguredPlainRowsByRepeatingLastLevel(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "spellsgained"))