Class feat injections moved to yaml
This commit is contained in:
@@ -151,17 +151,18 @@ type EffectiveMusicDataset struct {
|
||||
}
|
||||
|
||||
type EffectiveTopDataConfig struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Build string `json:"build" yaml:"build"`
|
||||
ReferenceBuilder string `json:"reference_builder" yaml:"reference_builder"`
|
||||
Assets string `json:"assets" yaml:"assets"`
|
||||
Compiled2DADir string `json:"compiled_2da_dir" yaml:"compiled_2da_dir"`
|
||||
CompiledTLK string `json:"compiled_tlk" yaml:"compiled_tlk"`
|
||||
PackageHAK string `json:"package_hak" yaml:"package_hak"`
|
||||
PackageTLK string `json:"package_tlk" yaml:"package_tlk"`
|
||||
ValueEncodings []TopDataValueEncodingConfig `json:"value_encodings" yaml:"value_encodings"`
|
||||
ValueDefaults []TopDataValueDefaultConfig `json:"value_defaults" yaml:"value_defaults"`
|
||||
Wiki TopDataWikiConfig `json:"wiki" yaml:"wiki"`
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Build string `json:"build" yaml:"build"`
|
||||
ReferenceBuilder string `json:"reference_builder" yaml:"reference_builder"`
|
||||
Assets string `json:"assets" yaml:"assets"`
|
||||
Compiled2DADir string `json:"compiled_2da_dir" yaml:"compiled_2da_dir"`
|
||||
CompiledTLK string `json:"compiled_tlk" yaml:"compiled_tlk"`
|
||||
PackageHAK string `json:"package_hak" yaml:"package_hak"`
|
||||
PackageTLK string `json:"package_tlk" yaml:"package_tlk"`
|
||||
ValueEncodings []TopDataValueEncodingConfig `json:"value_encodings" yaml:"value_encodings"`
|
||||
ValueDefaults []TopDataValueDefaultConfig `json:"value_defaults" yaml:"value_defaults"`
|
||||
ClassFeatInjections TopDataClassFeatInjectionConfig `json:"class_feat_injections" yaml:"class_feat_injections"`
|
||||
Wiki TopDataWikiConfig `json:"wiki" yaml:"wiki"`
|
||||
}
|
||||
|
||||
type EffectiveAutogenConfig struct {
|
||||
@@ -231,16 +232,17 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
}
|
||||
topBuild := expandPathTemplate(defaultString(p.Config.TopData.Build, DefaultTopDataBuild), paths)
|
||||
top := EffectiveTopDataConfig{
|
||||
Source: strings.TrimSpace(p.Config.TopData.Source),
|
||||
Build: topBuild,
|
||||
ReferenceBuilder: strings.TrimSpace(p.Config.TopData.ReferenceBuilder),
|
||||
Assets: strings.TrimSpace(p.Config.TopData.Assets),
|
||||
Compiled2DADir: defaultString(p.Config.TopData.Compiled2DADir, DefaultTopDataCompiled2DADir),
|
||||
CompiledTLK: defaultString(p.Config.TopData.CompiledTLK, DefaultTopDataCompiledTLK),
|
||||
PackageHAK: defaultString(p.Config.TopData.PackageHAK, DefaultTopDataPackageHAK),
|
||||
PackageTLK: defaultString(p.Config.TopData.PackageTLK, DefaultTopDataPackageTLK),
|
||||
ValueEncodings: cloneTopDataValueEncodings(p.Config.TopData.ValueEncodings),
|
||||
ValueDefaults: cloneTopDataValueDefaults(p.Config.TopData.ValueDefaults),
|
||||
Source: strings.TrimSpace(p.Config.TopData.Source),
|
||||
Build: topBuild,
|
||||
ReferenceBuilder: strings.TrimSpace(p.Config.TopData.ReferenceBuilder),
|
||||
Assets: strings.TrimSpace(p.Config.TopData.Assets),
|
||||
Compiled2DADir: defaultString(p.Config.TopData.Compiled2DADir, DefaultTopDataCompiled2DADir),
|
||||
CompiledTLK: defaultString(p.Config.TopData.CompiledTLK, DefaultTopDataCompiledTLK),
|
||||
PackageHAK: defaultString(p.Config.TopData.PackageHAK, DefaultTopDataPackageHAK),
|
||||
PackageTLK: defaultString(p.Config.TopData.PackageTLK, DefaultTopDataPackageTLK),
|
||||
ValueEncodings: cloneTopDataValueEncodings(p.Config.TopData.ValueEncodings),
|
||||
ValueDefaults: cloneTopDataValueDefaults(p.Config.TopData.ValueDefaults),
|
||||
ClassFeatInjections: cloneTopDataClassFeatInjections(p.Config.TopData.ClassFeatInjections),
|
||||
Wiki: TopDataWikiConfig{
|
||||
OutputRoot: defaultString(p.Config.TopData.Wiki.OutputRoot, DefaultTopDataWikiOutputRoot),
|
||||
PagesDir: defaultString(p.Config.TopData.Wiki.PagesDir, DefaultTopDataWikiPagesDir),
|
||||
@@ -429,6 +431,18 @@ func cloneTopDataValueDefaults(values []TopDataValueDefaultConfig) []TopDataValu
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTopDataClassFeatInjections(value TopDataClassFeatInjectionConfig) TopDataClassFeatInjectionConfig {
|
||||
out := TopDataClassFeatInjectionConfig{
|
||||
GlobalFeats: slices.Clone(value.GlobalFeats),
|
||||
ClassSkillMasterfeats: slices.Clone(value.ClassSkillMasterfeats),
|
||||
}
|
||||
for index := range out.GlobalFeats {
|
||||
out.GlobalFeats[index].RequirePresent = slices.Clone(value.GlobalFeats[index].RequirePresent)
|
||||
out.GlobalFeats[index].UnlessPresent = slices.Clone(value.GlobalFeats[index].UnlessPresent)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *Project) EffectiveConfigJSON() ([]byte, error) {
|
||||
return json.MarshalIndent(p.EffectiveConfig(), "", " ")
|
||||
}
|
||||
|
||||
+92
-11
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -206,17 +207,39 @@ type MusicDatasetConfig struct {
|
||||
}
|
||||
|
||||
type TopDataConfig struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Build string `json:"build" yaml:"build"`
|
||||
ReferenceBuilder string `json:"reference_builder" yaml:"reference_builder"`
|
||||
Assets string `json:"assets" yaml:"assets"`
|
||||
Compiled2DADir string `json:"compiled_2da_dir" yaml:"compiled_2da_dir"`
|
||||
CompiledTLK string `json:"compiled_tlk" yaml:"compiled_tlk"`
|
||||
PackageHAK string `json:"package_hak" yaml:"package_hak"`
|
||||
PackageTLK string `json:"package_tlk" yaml:"package_tlk"`
|
||||
ValueEncodings []TopDataValueEncodingConfig `json:"value_encodings" yaml:"value_encodings"`
|
||||
ValueDefaults []TopDataValueDefaultConfig `json:"value_defaults" yaml:"value_defaults"`
|
||||
Wiki TopDataWikiConfig `json:"wiki" yaml:"wiki"`
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Build string `json:"build" yaml:"build"`
|
||||
ReferenceBuilder string `json:"reference_builder" yaml:"reference_builder"`
|
||||
Assets string `json:"assets" yaml:"assets"`
|
||||
Compiled2DADir string `json:"compiled_2da_dir" yaml:"compiled_2da_dir"`
|
||||
CompiledTLK string `json:"compiled_tlk" yaml:"compiled_tlk"`
|
||||
PackageHAK string `json:"package_hak" yaml:"package_hak"`
|
||||
PackageTLK string `json:"package_tlk" yaml:"package_tlk"`
|
||||
ValueEncodings []TopDataValueEncodingConfig `json:"value_encodings" yaml:"value_encodings"`
|
||||
ValueDefaults []TopDataValueDefaultConfig `json:"value_defaults" yaml:"value_defaults"`
|
||||
ClassFeatInjections TopDataClassFeatInjectionConfig `json:"class_feat_injections" yaml:"class_feat_injections"`
|
||||
Wiki TopDataWikiConfig `json:"wiki" yaml:"wiki"`
|
||||
}
|
||||
|
||||
type TopDataClassFeatInjectionConfig struct {
|
||||
GlobalFeats []TopDataClassFeatGlobalRule `json:"global_feats" yaml:"global_feats"`
|
||||
ClassSkillMasterfeats []TopDataClassFeatMasterfeatRule `json:"class_skill_masterfeats" yaml:"class_skill_masterfeats"`
|
||||
}
|
||||
|
||||
type TopDataClassFeatGlobalRule struct {
|
||||
Feat string `json:"feat" yaml:"feat"`
|
||||
List string `json:"list" yaml:"list"`
|
||||
GrantedOnLevel string `json:"granted_on_level" yaml:"granted_on_level"`
|
||||
OnMenu string `json:"on_menu" yaml:"on_menu"`
|
||||
RequirePresent []string `json:"require_present,omitempty" yaml:"require_present,omitempty"`
|
||||
UnlessPresent []string `json:"unless_present,omitempty" yaml:"unless_present,omitempty"`
|
||||
}
|
||||
|
||||
type TopDataClassFeatMasterfeatRule struct {
|
||||
Masterfeat string `json:"masterfeat" yaml:"masterfeat"`
|
||||
List string `json:"list" yaml:"list"`
|
||||
GrantedOnLevel string `json:"granted_on_level" yaml:"granted_on_level"`
|
||||
OnMenu string `json:"on_menu" yaml:"on_menu"`
|
||||
}
|
||||
|
||||
type TopDataValueEncodingConfig struct {
|
||||
@@ -583,6 +606,7 @@ func (p *Project) ValidateLayout() error {
|
||||
failures = append(failures, validateGeneratedConfig(effective.Generated)...)
|
||||
failures = append(failures, validateTopDataValueEncodings(effective.TopData.ValueEncodings)...)
|
||||
failures = append(failures, validateTopDataValueDefaults(effective.TopData.ValueDefaults)...)
|
||||
failures = append(failures, validateTopDataClassFeatInjections(effective.TopData.ClassFeatInjections)...)
|
||||
failures = append(failures, validateRelativePath("scripts.cache", effective.Scripts.Cache)...)
|
||||
failures = append(failures, validateRelativePath("scripts.source_dir", effective.Scripts.SourceDir)...)
|
||||
failures = append(failures, validateRelativePath("topdata.compiled_2da_dir", effective.TopData.Compiled2DADir)...)
|
||||
@@ -781,6 +805,63 @@ func validateTopDataValueDefaults(defaults []TopDataValueDefaultConfig) []error
|
||||
return failures
|
||||
}
|
||||
|
||||
func validateTopDataClassFeatInjections(config TopDataClassFeatInjectionConfig) []error {
|
||||
failures := []error{}
|
||||
for index, rule := range config.GlobalFeats {
|
||||
prefix := fmt.Sprintf("topdata.class_feat_injections.global_feats[%d]", index)
|
||||
if feat := strings.TrimSpace(rule.Feat); feat == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.feat is required", prefix))
|
||||
} else if !strings.HasPrefix(feat, "feat:") {
|
||||
failures = append(failures, fmt.Errorf("%s.feat must use a feat: reference", prefix))
|
||||
}
|
||||
failures = append(failures, validateClassFeatInjectionFields(prefix, rule.List, rule.GrantedOnLevel, rule.OnMenu)...)
|
||||
failures = append(failures, validateFeatReferenceList(prefix+".require_present", rule.RequirePresent)...)
|
||||
failures = append(failures, validateFeatReferenceList(prefix+".unless_present", rule.UnlessPresent)...)
|
||||
}
|
||||
for index, rule := range config.ClassSkillMasterfeats {
|
||||
prefix := fmt.Sprintf("topdata.class_feat_injections.class_skill_masterfeats[%d]", index)
|
||||
if masterfeat := strings.TrimSpace(rule.Masterfeat); masterfeat == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.masterfeat is required", prefix))
|
||||
} else if !strings.HasPrefix(masterfeat, "masterfeats:") {
|
||||
failures = append(failures, fmt.Errorf("%s.masterfeat must use a masterfeats: reference", prefix))
|
||||
}
|
||||
failures = append(failures, validateClassFeatInjectionFields(prefix, rule.List, rule.GrantedOnLevel, rule.OnMenu)...)
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func validateClassFeatInjectionFields(prefix, list, grantedOnLevel, onMenu string) []error {
|
||||
failures := []error{}
|
||||
for field, value := range map[string]string{
|
||||
"list": list,
|
||||
"granted_on_level": grantedOnLevel,
|
||||
"on_menu": onMenu,
|
||||
} {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.%s is required", prefix, field))
|
||||
continue
|
||||
}
|
||||
if _, err := strconv.Atoi(trimmed); err != nil {
|
||||
failures = append(failures, fmt.Errorf("%s.%s must be an integer", prefix, field))
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func validateFeatReferenceList(prefix string, refs []string) []error {
|
||||
failures := []error{}
|
||||
for index, ref := range refs {
|
||||
trimmed := strings.TrimSpace(ref)
|
||||
if trimmed == "" {
|
||||
failures = append(failures, fmt.Errorf("%s[%d] is required", prefix, index))
|
||||
} else if !strings.HasPrefix(trimmed, "feat:") {
|
||||
failures = append(failures, fmt.Errorf("%s[%d] must use a feat: reference", prefix, index))
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func intPow(base, exponent int) int {
|
||||
result := 1
|
||||
for i := 0; i < exponent; i++ {
|
||||
|
||||
@@ -191,6 +191,45 @@ topdata:
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveConfigIncludesTopDataClassFeatInjections(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
topdata:
|
||||
source: topdata
|
||||
class_feat_injections:
|
||||
global_feats:
|
||||
- feat: feat:literate
|
||||
list: 3
|
||||
granted_on_level: 1
|
||||
on_menu: 0
|
||||
unless_present: [feat:illiterate]
|
||||
class_skill_masterfeats:
|
||||
- masterfeat: masterfeats:skillfocus
|
||||
list: 1
|
||||
granted_on_level: -1
|
||||
on_menu: 0
|
||||
`)
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
injections := proj.EffectiveConfig().TopData.ClassFeatInjections
|
||||
if len(injections.GlobalFeats) != 1 || len(injections.ClassSkillMasterfeats) != 1 {
|
||||
t.Fatalf("expected configured class feat injections, got %#v", injections)
|
||||
}
|
||||
if got := injections.GlobalFeats[0]; got.Feat != "feat:literate" || got.List != "3" || got.GrantedOnLevel != "1" || got.OnMenu != "0" || len(got.UnlessPresent) != 1 || got.UnlessPresent[0] != "feat:illiterate" {
|
||||
t.Fatalf("unexpected global class feat injection: %#v", got)
|
||||
}
|
||||
if got := injections.ClassSkillMasterfeats[0]; got.Masterfeat != "masterfeats:skillfocus" || got.List != "1" || got.GrantedOnLevel != "-1" || got.OnMenu != "0" {
|
||||
t.Fatalf("unexpected class skill masterfeat injection: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsInvalidTopDataValueEncodings(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "topdata"))
|
||||
@@ -239,6 +278,50 @@ func TestValidateLayoutRejectsInvalidTopDataValueEncodings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsInvalidTopDataClassFeatInjections(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "topdata"))
|
||||
mkdirAll(t, filepath.Join(root, "build"))
|
||||
|
||||
proj := &Project{
|
||||
Root: root,
|
||||
Config: Config{
|
||||
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
||||
Paths: PathConfig{Build: "build"},
|
||||
TopData: TopDataConfig{
|
||||
Source: "topdata",
|
||||
ClassFeatInjections: TopDataClassFeatInjectionConfig{
|
||||
GlobalFeats: []TopDataClassFeatGlobalRule{
|
||||
{Feat: "literate", List: "3", GrantedOnLevel: "1", OnMenu: "0", UnlessPresent: []string{"illiterate"}},
|
||||
{Feat: "feat:valid", List: "", GrantedOnLevel: "x", OnMenu: "0"},
|
||||
},
|
||||
ClassSkillMasterfeats: []TopDataClassFeatMasterfeatRule{
|
||||
{Masterfeat: "skillfocus", List: "1", GrantedOnLevel: "-1", OnMenu: ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := proj.ValidateLayout()
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid class feat injection validation error")
|
||||
}
|
||||
text := err.Error()
|
||||
for _, want := range []string{
|
||||
"topdata.class_feat_injections.global_feats[0].feat",
|
||||
"topdata.class_feat_injections.global_feats[0].unless_present[0]",
|
||||
"topdata.class_feat_injections.global_feats[1].list",
|
||||
"topdata.class_feat_injections.global_feats[1].granted_on_level",
|
||||
"topdata.class_feat_injections.class_skill_masterfeats[0].masterfeat",
|
||||
"topdata.class_feat_injections.class_skill_masterfeats[0].on_menu",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected validation error %q, got %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanUsesYAMLMusicConvertExtensionsWithinConfiguredDatasetRoots(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "assets", "audio", "westgate"), 0o755); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user