feat(topdata): add any-present injection conditions
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package topdata
|
package topdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -680,6 +682,466 @@ func TestValidateProjectAcceptsCanonicalJSONContracts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildNativeAnyPresent(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rows string
|
||||||
|
wantInjected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "first alternative",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "later alternative",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no alternatives",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"SkillLabel": "ProfessionFarmer",
|
||||||
|
"SkillIndex": {"id": "skills:profession_farmer"},
|
||||||
|
"ClassSkill": "1"
|
||||||
|
},
|
||||||
|
"any_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:athletics"},
|
||||||
|
{"field": "SkillIndex", "id": "skills:tumble"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||||
|
if got := tableContainsCellValue(table, "SkillLabel", "ProfessionFarmer"); got != tc.wantInjected {
|
||||||
|
t.Fatalf("ProfessionFarmer presence = %v, want %v", got, tc.wantInjected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildNativeCombinesGlobalConditionGroups(t *testing.T) {
|
||||||
|
t.Run("require_present and any_present", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rows string
|
||||||
|
wantInjected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both pass",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||||
|
{"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "require_present fails",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any_present fails",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"SkillLabel": "DualGate",
|
||||||
|
"SkillIndex": {"id": "skills:lore"},
|
||||||
|
"ClassSkill": "1"
|
||||||
|
},
|
||||||
|
"require_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:athletics"}
|
||||||
|
],
|
||||||
|
"any_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:tumble"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||||
|
if got := tableContainsCellValue(table, "SkillLabel", "DualGate"); got != tc.wantInjected {
|
||||||
|
t.Fatalf("DualGate presence = %v, want %v", got, tc.wantInjected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("any_present and unless_present", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rows string
|
||||||
|
wantInjected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both pass",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any_present fails",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unless_present fails",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||||
|
{"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"SkillLabel": "NeedsAnyWithoutBlocked",
|
||||||
|
"SkillIndex": {"id": "skills:lore"},
|
||||||
|
"ClassSkill": "1"
|
||||||
|
},
|
||||||
|
"any_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:athletics"}
|
||||||
|
],
|
||||||
|
"unless_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:profession_farmer"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||||
|
if got := tableContainsCellValue(table, "SkillLabel", "NeedsAnyWithoutBlocked"); got != tc.wantInjected {
|
||||||
|
t.Fatalf("NeedsAnyWithoutBlocked presence = %v, want %v", got, tc.wantInjected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("require_present keeps all semantics", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rows string
|
||||||
|
wantInjected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all present",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||||
|
{"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing one required row",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"SkillLabel": "AllRequired",
|
||||||
|
"SkillIndex": {"id": "skills:lore"},
|
||||||
|
"ClassSkill": "1"
|
||||||
|
},
|
||||||
|
"require_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:athletics"},
|
||||||
|
{"field": "SkillIndex", "id": "skills:tumble"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||||
|
if got := tableContainsCellValue(table, "SkillLabel", "AllRequired"); got != tc.wantInjected {
|
||||||
|
t.Fatalf("AllRequired presence = %v, want %v", got, tc.wantInjected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unless_present keeps none semantics", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rows string
|
||||||
|
wantInjected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "none present",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one blocked row present",
|
||||||
|
rows: `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||||
|
{"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"}
|
||||||
|
]`,
|
||||||
|
wantInjected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"SkillLabel": "NoneBlocked",
|
||||||
|
"SkillIndex": {"id": "skills:lore"},
|
||||||
|
"ClassSkill": "1"
|
||||||
|
},
|
||||||
|
"unless_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:profession_farmer"},
|
||||||
|
{"field": "SkillIndex", "id": "skills:stealth"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||||
|
if got := tableContainsCellValue(table, "SkillLabel", "NoneBlocked"); got != tc.wantInjected {
|
||||||
|
t.Fatalf("NoneBlocked presence = %v, want %v", got, tc.wantInjected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildNativeUsesCurrentRowsForAnyPresent(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalPlainSkillsFixture(t, root, `[
|
||||||
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||||
|
]`, `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"SkillLabel": "ProfessionFarmer",
|
||||||
|
"SkillIndex": {"id": "skills:profession_farmer"},
|
||||||
|
"ClassSkill": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"SkillLabel": "Lore",
|
||||||
|
"SkillIndex": {"id": "skills:lore"},
|
||||||
|
"ClassSkill": "1"
|
||||||
|
},
|
||||||
|
"any_present": [
|
||||||
|
{"field": "SkillIndex", "id": "skills:profession_farmer"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||||
|
for _, label := range []string{"ProfessionFarmer", "Lore"} {
|
||||||
|
if !tableContainsCellValue(table, "SkillLabel", label) {
|
||||||
|
t.Fatalf("expected %s injection to apply after earlier mutation", label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildNativeRejectsUnsupportedGlobalControlSyntax(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
globalJSON string
|
||||||
|
fragments []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "when_present injection key",
|
||||||
|
globalJSON: `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {"ImpactScript": "ss_new_spell"},
|
||||||
|
"when_present": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}` + "\n",
|
||||||
|
fragments: []string{"global injection 0", "when_present", "unsupported"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown condition key",
|
||||||
|
globalJSON: `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {"ImpactScript": "ss_new_spell"},
|
||||||
|
"require_present": [
|
||||||
|
{"field": "FeatIndex", "id": "feat:gate", "typo": true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}` + "\n",
|
||||||
|
fragments: []string{"require_present[0]", "typo", "unsupported"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty any_present",
|
||||||
|
globalJSON: `{
|
||||||
|
"injections": [
|
||||||
|
{
|
||||||
|
"row": {"ImpactScript": "ss_new_spell"},
|
||||||
|
"any_present": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}` + "\n",
|
||||||
|
fragments: []string{"any_present", "at least one condition"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalContractScaffold(t, root)
|
||||||
|
writeCanonicalSpellsDataset(t, root)
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), tc.globalJSON)
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
_, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected BuildNativeWithOptions to fail")
|
||||||
|
}
|
||||||
|
for _, fragment := range tc.fragments {
|
||||||
|
if !strings.Contains(err.Error(), fragment) {
|
||||||
|
t.Fatalf("error %q does not contain %q", err, fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) {
|
||||||
|
t.Fatalf("expected spells.2da to remain absent, got err %v", statErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildNativeRejectsUnsupportedCanonicalRoot(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeCanonicalContractScaffold(t, root)
|
||||||
|
writeCanonicalSpellsBase(t, root, `{
|
||||||
|
"output": "spells.2da",
|
||||||
|
"columns": ["Label"],
|
||||||
|
"rows": [
|
||||||
|
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"}
|
||||||
|
],
|
||||||
|
"typo": true
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
_, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected BuildNativeWithOptions to fail")
|
||||||
|
}
|
||||||
|
for _, fragment := range []string{"base.json root", "typo", "unsupported"} {
|
||||||
|
if !strings.Contains(err.Error(), fragment) {
|
||||||
|
t.Fatalf("error %q does not contain %q", err, fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) {
|
||||||
|
t.Fatalf("expected spells.2da to remain absent, got err %v", statErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableContainsCellValue(table twoDATable, column, want string) bool {
|
||||||
|
for _, row := range table.rows {
|
||||||
|
if row[column] == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func writeCanonicalContractScaffold(t *testing.T, root string) {
|
func writeCanonicalContractScaffold(t *testing.T, root string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
||||||
@@ -717,6 +1179,40 @@ func writeCanonicalRepadjustDataset(t *testing.T, root string) {
|
|||||||
}`+"\n")
|
}`+"\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeCanonicalPlainSkillsFixture(t *testing.T, root, datasetRows, globalJSON string) {
|
||||||
|
t.Helper()
|
||||||
|
writeCanonicalContractScaffold(t, root)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "global.json"), globalJSON)
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "guard.json"), `{
|
||||||
|
"key": "classes/skills:guard",
|
||||||
|
"output": "cls_skill_guard.2da",
|
||||||
|
"columns": ["SkillLabel", "SkillIndex", "ClassSkill"],
|
||||||
|
"rows": `+datasetRows+`
|
||||||
|
}`+"\n")
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
|
||||||
|
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:tumble", "Label": "Tumble"},
|
||||||
|
{"id": 3, "key": "skills:concentration", "Label": "Concentration"},
|
||||||
|
{"id": 4, "key": "skills:profession_farmer", "Label": "ProfessionFarmer"},
|
||||||
|
{"id": 5, "key": "skills:lore", "Label": "Lore"},
|
||||||
|
{"id": 6, "key": "skills:stealth", "Label": "Stealth"}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{
|
||||||
|
"skills:athletics": 1,
|
||||||
|
"skills:tumble": 2,
|
||||||
|
"skills:concentration": 3,
|
||||||
|
"skills:profession_farmer": 4,
|
||||||
|
"skills:lore": 5,
|
||||||
|
"skills:stealth": 6
|
||||||
|
}`+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
func assertDiagnosticTextContainsAll(t *testing.T, diags []Diagnostic, fragments ...string) {
|
func assertDiagnosticTextContainsAll(t *testing.T, diags []Diagnostic, fragments ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
text := diagnosticsText(diags)
|
text := diagnosticsText(diags)
|
||||||
|
|||||||
@@ -3886,48 +3886,11 @@ func globalManifestPosition(module nativeGeneratedModule) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func globalInjectionConditionsMatch(injection map[string]any, rows []map[string]any) (bool, error) {
|
func globalInjectionConditionsMatch(injection map[string]any, rows []map[string]any) (bool, error) {
|
||||||
if rawRequired, ok := injection["require_present"]; ok {
|
groups, err := parseGlobalConditionGroups(injection)
|
||||||
matches, err := globalConditionListMatches("require_present", rawRequired, rows, true)
|
if err != nil {
|
||||||
if err != nil || !matches {
|
return false, err
|
||||||
return matches, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if rawBlocked, ok := injection["unless_present"]; ok {
|
return globalConditionGroupsMatch(groups, rows), nil
|
||||||
matches, err := globalConditionListMatches("unless_present", rawBlocked, rows, false)
|
|
||||||
if err != nil || !matches {
|
|
||||||
return matches, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func globalConditionListMatches(name string, raw any, rows []map[string]any, required bool) (bool, error) {
|
|
||||||
conditions, ok := raw.([]any)
|
|
||||||
if !ok {
|
|
||||||
return false, fmt.Errorf("%s must be an array", name)
|
|
||||||
}
|
|
||||||
for index, rawCondition := range conditions {
|
|
||||||
condition, ok := rawCondition.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return false, fmt.Errorf("%s[%d] must be an object", name, index)
|
|
||||||
}
|
|
||||||
field, _ := condition["field"].(string)
|
|
||||||
id, _ := condition["id"].(string)
|
|
||||||
if strings.TrimSpace(field) == "" {
|
|
||||||
return false, fmt.Errorf("%s[%d].field is required", name, index)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(id) == "" {
|
|
||||||
return false, fmt.Errorf("%s[%d].id is required", name, index)
|
|
||||||
}
|
|
||||||
found := globalReferencePresent(rows, field, id)
|
|
||||||
if required && !found {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if !required && found {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func globalRowIdentityPresent(row map[string]any, rows []map[string]any) bool {
|
func globalRowIdentityPresent(row map[string]any, rows []map[string]any) bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user