command and help ux pass
This commit is contained in:
@@ -8,8 +8,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/music"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,11 +19,6 @@ const (
|
||||
DefaultHAKArchiveTemplate = "{hak.name}.hak"
|
||||
DefaultSourceJSONPattern = "{resref}.{extension}.json"
|
||||
DefaultValidationProfile = "nwn_module"
|
||||
DefaultMusicStageRoot = "{paths.cache}/music"
|
||||
DefaultCreditsRoot = "{paths.cache}/credits"
|
||||
DefaultCreditsOverlayFile = "CREDITS.md"
|
||||
DefaultMusicNamingScheme = "nwn_bmu"
|
||||
DefaultMusicOutputExtension = ".bmu"
|
||||
DefaultTopDataBuild = "{paths.build}/topdata"
|
||||
DefaultTopDataCompiled2DADir = "2da"
|
||||
DefaultTopDataCompiledTLK = "sow_tlk.tlk"
|
||||
@@ -85,7 +78,6 @@ type EffectiveConfig struct {
|
||||
Generated GeneratedConfig `json:"generated_assets" yaml:"generated_assets"`
|
||||
Inventory InventoryConfig `json:"inventory" yaml:"inventory"`
|
||||
Validation ValidationConfig `json:"validation" yaml:"validation"`
|
||||
Music EffectiveMusicConfig `json:"music" yaml:"music"`
|
||||
TopData EffectiveTopDataConfig `json:"topdata" yaml:"topdata"`
|
||||
Extract ExtractConfig `json:"extract" yaml:"extract"`
|
||||
Autogen EffectiveAutogenConfig `json:"autogen" yaml:"autogen"`
|
||||
@@ -108,32 +100,6 @@ type EffectiveOutputConfig struct {
|
||||
HAKArchive string `json:"hak_archive" yaml:"hak_archive"`
|
||||
}
|
||||
|
||||
type EffectiveMusicConfig struct {
|
||||
Prefixes map[string]string `json:"prefixes" yaml:"prefixes"`
|
||||
Tools MusicToolsConfig `json:"tools" yaml:"tools"`
|
||||
StageRoot string `json:"stage_root" yaml:"stage_root"`
|
||||
CreditsRoot string `json:"credits_root" yaml:"credits_root"`
|
||||
CreditsOverlay string `json:"credits_overlay" yaml:"credits_overlay"`
|
||||
MaxStemLength int `json:"max_stem_length" yaml:"max_stem_length"`
|
||||
NamingScheme string `json:"naming_scheme" yaml:"naming_scheme"`
|
||||
OutputExtension string `json:"output_extension" yaml:"output_extension"`
|
||||
ConvertExtensions []string `json:"convert_extensions" yaml:"convert_extensions"`
|
||||
Datasets map[string]EffectiveMusicDataset `json:"datasets,omitempty" yaml:"datasets,omitempty"`
|
||||
}
|
||||
|
||||
type EffectiveMusicDataset struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Output string `json:"output" yaml:"output"`
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
NamingScheme string `json:"naming_scheme" yaml:"naming_scheme"`
|
||||
OutputExtension string `json:"output_extension" yaml:"output_extension"`
|
||||
MaxStemLength int `json:"max_stem_length" yaml:"max_stem_length"`
|
||||
PackageMode string `json:"package_mode" yaml:"package_mode"`
|
||||
ConvertExtensions []string `json:"convert_extensions" yaml:"convert_extensions"`
|
||||
}
|
||||
|
||||
type EffectiveTopDataConfig struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Build string `json:"build" yaml:"build"`
|
||||
@@ -275,81 +241,6 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
extract.ConsumeArchives = &defaultConsume
|
||||
}
|
||||
|
||||
musicStageRoot := expandPathTemplate(DefaultMusicStageRoot, paths)
|
||||
musicCreditsRoot := expandPathTemplate(DefaultCreditsRoot, paths)
|
||||
musicCreditsOverlay := DefaultCreditsOverlayFile
|
||||
musicMaxStemLength := 16
|
||||
musicNamingScheme := DefaultMusicNamingScheme
|
||||
musicOutputExtension := DefaultMusicOutputExtension
|
||||
musicConvertExtensions := music.DefaultConvertExtensions()
|
||||
if d := p.Config.Music.Defaults; d != nil {
|
||||
if d.StageRoot != "" {
|
||||
musicStageRoot = expandPathTemplate(d.StageRoot, paths)
|
||||
}
|
||||
if d.CreditsRoot != "" {
|
||||
musicCreditsRoot = expandPathTemplate(d.CreditsRoot, paths)
|
||||
}
|
||||
if d.CreditsOverlay != "" {
|
||||
musicCreditsOverlay = d.CreditsOverlay
|
||||
}
|
||||
if d.MaxStemLength != nil {
|
||||
musicMaxStemLength = *d.MaxStemLength
|
||||
}
|
||||
if d.NamingScheme != "" {
|
||||
musicNamingScheme = d.NamingScheme
|
||||
}
|
||||
if d.OutputExtension != "" {
|
||||
musicOutputExtension = normalizeExtension(d.OutputExtension)
|
||||
}
|
||||
if len(d.ConvertExtensions) > 0 {
|
||||
musicConvertExtensions = normalizeExtensionSlice(d.ConvertExtensions)
|
||||
}
|
||||
}
|
||||
musicPrefixes := cloneStringMap(p.Config.Music.Prefixes)
|
||||
if len(musicPrefixes) == 0 {
|
||||
musicPrefixes = make(map[string]string, len(p.Config.Music.Datasets))
|
||||
for _, ds := range p.Config.Music.Datasets {
|
||||
if ds.Source != "" && ds.Prefix != "" {
|
||||
musicPrefixes[ds.Source] = ds.Prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
effectiveDatasets := make(map[string]EffectiveMusicDataset, len(p.Config.Music.Datasets))
|
||||
for id, ds := range p.Config.Music.Datasets {
|
||||
dsStageRoot := ds.StageRoot
|
||||
if dsStageRoot == "" {
|
||||
dsStageRoot = musicStageRoot
|
||||
}
|
||||
dsCreditsRoot := ds.CreditsRoot
|
||||
if dsCreditsRoot == "" {
|
||||
dsCreditsRoot = musicCreditsRoot
|
||||
}
|
||||
dsOutput := strings.TrimSpace(ds.Output)
|
||||
if dsOutput == "" {
|
||||
dsOutput = ds.Source
|
||||
}
|
||||
dsNamingScheme := defaultString(ds.NamingScheme, musicNamingScheme)
|
||||
dsOutputExtension := musicOutputExtension
|
||||
if ds.OutputExtension != "" {
|
||||
dsOutputExtension = normalizeExtension(ds.OutputExtension)
|
||||
}
|
||||
dsConvertExtensions := musicConvertExtensions
|
||||
if len(ds.ConvertExtensions) > 0 {
|
||||
dsConvertExtensions = normalizeExtensionSlice(ds.ConvertExtensions)
|
||||
}
|
||||
effectiveDatasets[id] = EffectiveMusicDataset{
|
||||
Source: ds.Source,
|
||||
Output: dsOutput,
|
||||
Prefix: ds.Prefix,
|
||||
StageRoot: dsStageRoot,
|
||||
CreditsRoot: dsCreditsRoot,
|
||||
NamingScheme: dsNamingScheme,
|
||||
OutputExtension: dsOutputExtension,
|
||||
MaxStemLength: musicMaxStemLength,
|
||||
PackageMode: defaultString(ds.PackageMode, "hak_asset"),
|
||||
ConvertExtensions: dsConvertExtensions,
|
||||
}
|
||||
}
|
||||
effective := EffectiveConfig{
|
||||
ConfigSource: p.ConfigSource,
|
||||
Module: p.Config.Module,
|
||||
@@ -359,20 +250,8 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
Generated: generated,
|
||||
Inventory: inventory,
|
||||
Validation: validation,
|
||||
Music: EffectiveMusicConfig{
|
||||
Prefixes: musicPrefixes,
|
||||
Tools: p.Config.Music.Tools,
|
||||
StageRoot: musicStageRoot,
|
||||
CreditsRoot: musicCreditsRoot,
|
||||
CreditsOverlay: musicCreditsOverlay,
|
||||
MaxStemLength: musicMaxStemLength,
|
||||
NamingScheme: musicNamingScheme,
|
||||
OutputExtension: musicOutputExtension,
|
||||
ConvertExtensions: musicConvertExtensions,
|
||||
Datasets: effectiveDatasets,
|
||||
},
|
||||
TopData: top,
|
||||
Extract: extract,
|
||||
TopData: top,
|
||||
Extract: extract,
|
||||
Autogen: EffectiveAutogenConfig{
|
||||
Producers: slices.Clone(p.Config.Autogen.Producers),
|
||||
Consumers: slices.Clone(p.Config.Autogen.Consumers),
|
||||
@@ -517,13 +396,6 @@ func markMissingDefaults(provenance ConfigProvenance) {
|
||||
"validation.profile": DefaultValidationProfile,
|
||||
"validation.builtin_script_prefixes": "NWN built-in script prefixes",
|
||||
"validation.required_fields": "NWN required GFF fields",
|
||||
"music.stage_root": DefaultMusicStageRoot,
|
||||
"music.credits_root": DefaultCreditsRoot,
|
||||
"music.credits_overlay": DefaultCreditsOverlayFile,
|
||||
"music.max_stem_length": "16",
|
||||
"music.naming_scheme": DefaultMusicNamingScheme,
|
||||
"music.output_extension": DefaultMusicOutputExtension,
|
||||
"music.convert_extensions": strings.Join(music.DefaultConvertExtensions(), ", "),
|
||||
"topdata.build": DefaultTopDataBuild,
|
||||
"topdata.compiled_2da_dir": DefaultTopDataCompiled2DADir,
|
||||
"topdata.compiled_tlk": DefaultTopDataCompiledTLK,
|
||||
@@ -690,8 +562,6 @@ func activeOverrides(effective EffectiveConfig) []ConfigOverride {
|
||||
"autogen.cache.parts_manifest_refresh": effective.Autogen.Cache.PartsManifestRefreshEnv,
|
||||
"autogen.release_source.server_url": effective.Autogen.ReleaseSource.ServerURLEnv,
|
||||
"autogen.release_source.repo": effective.Autogen.ReleaseSource.RepoEnv,
|
||||
"music.ffmpeg": "SOW_FFMPEG",
|
||||
"music.ffprobe": "SOW_FFPROBE",
|
||||
"wiki.endpoint": "NODEBB_API_ENDPOINT",
|
||||
"wiki.token": "NODEBB_API_TOKEN",
|
||||
"wiki.categories": "NODEBB_WIKI_CATEGORIES",
|
||||
|
||||
+29
-222
@@ -31,7 +31,7 @@ var SourceExtensions = []string{
|
||||
}
|
||||
|
||||
var AssetExtensions = []string{
|
||||
".2da", ".bik", ".bmp", ".bmu", ".dds", ".dwk", ".gr2", ".itp", ".jpg", ".lod", ".lyt", ".mdb", ".mdl", ".mdx", ".mp3", ".mtr", ".ogg", ".plt", ".png", ".pwk", ".set", ".shd", ".tga", ".txi", ".uti", ".vis", ".wav", ".wlk", ".wok", ".xml",
|
||||
".2da", ".bik", ".bmp", ".bmu", ".dds", ".dwk", ".gr2", ".itp", ".jpg", ".lod", ".lyt", ".mdb", ".mdl", ".mdx", ".mtr", ".plt", ".png", ".pwk", ".set", ".shd", ".tga", ".txi", ".uti", ".vis", ".wav", ".wlk", ".wok", ".xml",
|
||||
}
|
||||
|
||||
var BuiltinScriptPrefixes = []string{
|
||||
@@ -88,7 +88,6 @@ type Config struct {
|
||||
HAKs []HAKConfig `json:"haks" yaml:"haks"`
|
||||
Inventory InventoryConfig `json:"inventory" yaml:"inventory"`
|
||||
Validation ValidationConfig `json:"validation" yaml:"validation"`
|
||||
Music MusicConfig `json:"music" yaml:"music"`
|
||||
TopData TopDataConfig `json:"topdata" yaml:"topdata"`
|
||||
Extract ExtractConfig `json:"extract" yaml:"extract"`
|
||||
Autogen AutogenConfig `json:"autogen" yaml:"autogen"`
|
||||
@@ -153,40 +152,6 @@ type HAKConfig struct {
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
}
|
||||
|
||||
type MusicConfig struct {
|
||||
Prefixes map[string]string `json:"prefixes,omitempty" yaml:"prefixes,omitempty"`
|
||||
Tools MusicToolsConfig `json:"tools,omitempty" yaml:"tools,omitempty"`
|
||||
Defaults *MusicDefaultsConfig `json:"defaults,omitempty" yaml:"defaults,omitempty"`
|
||||
Datasets map[string]MusicDatasetConfig `json:"datasets,omitempty" yaml:"datasets,omitempty"`
|
||||
}
|
||||
|
||||
type MusicToolsConfig struct {
|
||||
FFmpeg string `json:"ffmpeg,omitempty" yaml:"ffmpeg,omitempty"`
|
||||
FFprobe string `json:"ffprobe,omitempty" yaml:"ffprobe,omitempty"`
|
||||
}
|
||||
|
||||
type MusicDefaultsConfig struct {
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
CreditsOverlay string `json:"credits_overlay,omitempty" yaml:"credits_overlay,omitempty"`
|
||||
MaxStemLength *int `json:"max_stem_length,omitempty" yaml:"max_stem_length,omitempty"`
|
||||
NamingScheme string `json:"naming_scheme,omitempty" yaml:"naming_scheme,omitempty"`
|
||||
OutputExtension string `json:"output_extension,omitempty" yaml:"output_extension,omitempty"`
|
||||
ConvertExtensions []string `json:"convert_extensions,omitempty" yaml:"convert_extensions,omitempty"`
|
||||
}
|
||||
|
||||
type MusicDatasetConfig struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Output string `json:"output,omitempty" yaml:"output,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
StageRoot string `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
|
||||
CreditsRoot string `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
|
||||
NamingScheme string `json:"naming_scheme,omitempty" yaml:"naming_scheme,omitempty"`
|
||||
OutputExtension string `json:"output_extension,omitempty" yaml:"output_extension,omitempty"`
|
||||
PackageMode string `json:"package_mode,omitempty" yaml:"package_mode,omitempty"`
|
||||
ConvertExtensions []string `json:"convert_extensions,omitempty" yaml:"convert_extensions,omitempty"`
|
||||
}
|
||||
|
||||
type TopDataConfig struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Build string `json:"build" yaml:"build"`
|
||||
@@ -344,42 +309,42 @@ type AutogenReleaseSourceConfig struct {
|
||||
}
|
||||
|
||||
type AutogenProducerConfig struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Root string `json:"root" yaml:"root"`
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
Derive AutogenDeriveConfig `json:"derive" yaml:"derive"`
|
||||
Manifest AutogenManifestConfig `json:"manifest" yaml:"manifest"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Root string `json:"root" yaml:"root"`
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
Derive AutogenDeriveConfig `json:"derive" yaml:"derive"`
|
||||
Manifest AutogenManifestConfig `json:"manifest" yaml:"manifest"`
|
||||
AccessoryVisualeffects AccessoryVisualeffectsConfig `json:"accessory_visualeffects" yaml:"accessory_visualeffects"`
|
||||
}
|
||||
|
||||
type AutogenConsumerConfig struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Producer string `json:"producer" yaml:"producer"`
|
||||
Dataset string `json:"dataset" yaml:"dataset"`
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
Optional bool `json:"optional,omitempty" yaml:"optional,omitempty"`
|
||||
Root string `json:"root" yaml:"root"`
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
Derive AutogenDeriveConfig `json:"derive" yaml:"derive"`
|
||||
PartsRows PartsRowsConfig `json:"parts_rows" yaml:"parts_rows"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Producer string `json:"producer" yaml:"producer"`
|
||||
Dataset string `json:"dataset" yaml:"dataset"`
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
Optional bool `json:"optional,omitempty" yaml:"optional,omitempty"`
|
||||
Root string `json:"root" yaml:"root"`
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
Derive AutogenDeriveConfig `json:"derive" yaml:"derive"`
|
||||
PartsRows PartsRowsConfig `json:"parts_rows" yaml:"parts_rows"`
|
||||
AccessoryVisualeffects AccessoryVisualeffectsConfig `json:"accessory_visualeffects" yaml:"accessory_visualeffects"`
|
||||
Manifest AutogenManifestConfig `json:"manifest" yaml:"manifest"`
|
||||
LocalOverrideRoot string `json:"local_override_root,omitempty" yaml:"local_override_root,omitempty"`
|
||||
ManifestFile string `json:"manifest_file,omitempty" yaml:"manifest_file,omitempty"`
|
||||
Source AutogenSourceConfig `json:"source,omitempty" yaml:"source,omitempty"`
|
||||
Manifest AutogenManifestConfig `json:"manifest" yaml:"manifest"`
|
||||
LocalOverrideRoot string `json:"local_override_root,omitempty" yaml:"local_override_root,omitempty"`
|
||||
ManifestFile string `json:"manifest_file,omitempty" yaml:"manifest_file,omitempty"`
|
||||
Source AutogenSourceConfig `json:"source,omitempty" yaml:"source,omitempty"`
|
||||
}
|
||||
|
||||
type AccessoryVisualeffectsConfig struct {
|
||||
Groups map[string]AccessoryVisualeffectGroupConfig `json:"groups,omitempty" yaml:"groups"`
|
||||
GroupTokenSource string `json:"group_token_source,omitempty" yaml:"group_token_source"`
|
||||
CategoryFrom string `json:"category_from,omitempty" yaml:"category_from"`
|
||||
Delimiter string `json:"delimiter,omitempty" yaml:"delimiter"`
|
||||
Case string `json:"case,omitempty" yaml:"case"`
|
||||
StripModelPrefixes []string `json:"strip_model_prefixes,omitempty" yaml:"strip_model_prefixes"`
|
||||
KeyFormat string `json:"key_format,omitempty" yaml:"key_format"`
|
||||
LabelFormat string `json:"label_format,omitempty" yaml:"label_format"`
|
||||
ModelColumn string `json:"model_column,omitempty" yaml:"model_column"`
|
||||
RowDefaults map[string]string `json:"row_defaults,omitempty" yaml:"row_defaults"`
|
||||
GroupTokenSource string `json:"group_token_source,omitempty" yaml:"group_token_source"`
|
||||
CategoryFrom string `json:"category_from,omitempty" yaml:"category_from"`
|
||||
Delimiter string `json:"delimiter,omitempty" yaml:"delimiter"`
|
||||
Case string `json:"case,omitempty" yaml:"case"`
|
||||
StripModelPrefixes []string `json:"strip_model_prefixes,omitempty" yaml:"strip_model_prefixes"`
|
||||
KeyFormat string `json:"key_format,omitempty" yaml:"key_format"`
|
||||
LabelFormat string `json:"label_format,omitempty" yaml:"label_format"`
|
||||
ModelColumn string `json:"model_column,omitempty" yaml:"model_column"`
|
||||
RowDefaults map[string]string `json:"row_defaults,omitempty" yaml:"row_defaults"`
|
||||
}
|
||||
|
||||
type AccessoryVisualeffectGroupConfig struct {
|
||||
@@ -750,8 +715,6 @@ func (p *Project) ValidateLayout() error {
|
||||
}
|
||||
}
|
||||
failures = append(failures, validateAutogenConfig(p.Config.Autogen)...)
|
||||
failures = append(failures, validateMusicConfig(p.Config.Music)...)
|
||||
|
||||
hakNames := map[string]struct{}{}
|
||||
for index, hak := range p.Config.HAKs {
|
||||
name := strings.TrimSpace(hak.Name)
|
||||
@@ -1059,46 +1022,9 @@ func (p *Project) Scan() error {
|
||||
assetDir := p.AssetsDir()
|
||||
var assetFiles []string
|
||||
if assetDir != "" && filepath.Clean(assetDir) != filepath.Clean(p.Root) {
|
||||
musicSourceExts := make(map[string]struct{})
|
||||
for _, ext := range effective.Music.ConvertExtensions {
|
||||
musicSourceExts[ext] = struct{}{}
|
||||
}
|
||||
musicSourceRoots := make(map[string]map[string]struct{})
|
||||
for _, ds := range effective.Music.Datasets {
|
||||
if strings.TrimSpace(ds.Source) == "" {
|
||||
continue
|
||||
}
|
||||
exts := musicSourceRoots[ds.Source]
|
||||
if exts == nil {
|
||||
exts = make(map[string]struct{})
|
||||
musicSourceRoots[ds.Source] = exts
|
||||
}
|
||||
for _, ext := range ds.ConvertExtensions {
|
||||
exts[ext] = struct{}{}
|
||||
}
|
||||
for ext := range musicSourceExts {
|
||||
exts[ext] = struct{}{}
|
||||
}
|
||||
}
|
||||
var err error
|
||||
assetFiles, _, err = scanDir(assetDir, func(path string) bool {
|
||||
rel, err := filepath.Rel(assetDir, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
ext := strings.ToLower(filepath.Ext(rel))
|
||||
if slices.Contains(effective.Inventory.AssetExtensions, ext) {
|
||||
return true
|
||||
}
|
||||
for root, exts := range musicSourceRoots {
|
||||
if !pathIsUnder(root, rel) {
|
||||
continue
|
||||
}
|
||||
_, ok := exts[ext]
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
return slices.Contains(effective.Inventory.AssetExtensions, strings.ToLower(filepath.Ext(path)))
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -1343,33 +1269,6 @@ func (p *Project) TopDataPackageTLKPath() string {
|
||||
return filepath.Join(p.BuildDir(), p.TopDataPackageTLKName())
|
||||
}
|
||||
|
||||
func (p *Project) MusicStageDir() string {
|
||||
return p.rootPath(p.EffectiveConfig().Music.StageRoot)
|
||||
}
|
||||
|
||||
func (p *Project) CreditsDir() string {
|
||||
return p.rootPath(p.EffectiveConfig().Music.CreditsRoot)
|
||||
}
|
||||
|
||||
func (p *Project) MusicToolPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(p.Root, filepath.FromSlash(path))
|
||||
}
|
||||
|
||||
func (p *Project) MusicFFmpegPath() string {
|
||||
return p.MusicToolPath(p.EffectiveConfig().Music.Tools.FFmpeg)
|
||||
}
|
||||
|
||||
func (p *Project) MusicFFprobePath() string {
|
||||
return p.MusicToolPath(p.EffectiveConfig().Music.Tools.FFprobe)
|
||||
}
|
||||
|
||||
func (p *Project) AutogenCacheDir() string {
|
||||
return p.rootPath(p.EffectiveConfig().Autogen.Cache.Root)
|
||||
}
|
||||
@@ -1459,36 +1358,6 @@ func normalizeConfig(cfg *Config) {
|
||||
for i := range cfg.Autogen.Consumers {
|
||||
cfg.Autogen.Consumers[i].Include = normalizeStringSlice(cfg.Autogen.Consumers[i].Include)
|
||||
}
|
||||
normalizeMusicConfig(&cfg.Music)
|
||||
}
|
||||
|
||||
func normalizeMusicConfig(music *MusicConfig) {
|
||||
if music.Defaults != nil {
|
||||
music.Defaults.ConvertExtensions = normalizeExtensionSlice(music.Defaults.ConvertExtensions)
|
||||
music.Defaults.OutputExtension = normalizeExtension(music.Defaults.OutputExtension)
|
||||
}
|
||||
if len(music.Prefixes) > 0 && len(music.Datasets) == 0 {
|
||||
datasets := make(map[string]MusicDatasetConfig, len(music.Prefixes))
|
||||
for dir, prefix := range music.Prefixes {
|
||||
source := trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(dir)))
|
||||
if source == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ReplaceAll(source, "/", "_")
|
||||
datasets[key] = MusicDatasetConfig{
|
||||
Source: source,
|
||||
Prefix: strings.TrimSpace(prefix),
|
||||
}
|
||||
}
|
||||
music.Datasets = datasets
|
||||
}
|
||||
for id, ds := range music.Datasets {
|
||||
ds.Source = trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Source)))
|
||||
ds.Output = trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Output)))
|
||||
ds.OutputExtension = normalizeExtension(ds.OutputExtension)
|
||||
ds.ConvertExtensions = normalizeExtensionSlice(ds.ConvertExtensions)
|
||||
music.Datasets[id] = ds
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeStringSlice(input []string) []string {
|
||||
@@ -1982,68 +1851,6 @@ func validatePartsRowsFormat(format string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMusicConfig(cfg MusicConfig) []error {
|
||||
var failures []error
|
||||
if cfg.Defaults != nil {
|
||||
if cfg.Defaults.MaxStemLength != nil && *cfg.Defaults.MaxStemLength < 3 {
|
||||
failures = append(failures, errors.New("music.defaults.max_stem_length must be at least 3"))
|
||||
}
|
||||
if cfg.Defaults.NamingScheme != "" && cfg.Defaults.NamingScheme != "nwn_bmu" && cfg.Defaults.NamingScheme != "passthrough" {
|
||||
failures = append(failures, fmt.Errorf("music.defaults.naming_scheme %q is not supported", cfg.Defaults.NamingScheme))
|
||||
}
|
||||
if cfg.Defaults.OutputExtension != "" && !strings.HasPrefix(cfg.Defaults.OutputExtension, ".") {
|
||||
failures = append(failures, errors.New("music.defaults.output_extension must start with ."))
|
||||
}
|
||||
}
|
||||
normalizedRoots := map[string]string{}
|
||||
for root, prefix := range cfg.Prefixes {
|
||||
normalizedRoot := trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(root)))
|
||||
if normalizedRoot == "" {
|
||||
failures = append(failures, errors.New("music.prefixes contains an empty root"))
|
||||
continue
|
||||
}
|
||||
if prior, exists := normalizedRoots[normalizedRoot]; exists {
|
||||
failures = append(failures, fmt.Errorf("music.prefixes roots %q and %q normalize to the same path", prior, root))
|
||||
}
|
||||
normalizedRoots[normalizedRoot] = root
|
||||
if strings.TrimSpace(prefix) == "" {
|
||||
failures = append(failures, fmt.Errorf("music.prefixes[%q] must not be empty", root))
|
||||
}
|
||||
}
|
||||
seenSources := map[string]string{}
|
||||
for id, ds := range cfg.Datasets {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
failures = append(failures, errors.New("music.datasets contains an empty key"))
|
||||
} else if strings.Contains(id, "/") || strings.Contains(id, "\\") {
|
||||
failures = append(failures, fmt.Errorf("music.datasets id %q must not contain path separators", id))
|
||||
}
|
||||
source := trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Source)))
|
||||
if source == "" {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].source is required", id))
|
||||
} else if pathEscapesRoot(source) {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].source must not escape project root", id))
|
||||
} else if prior, exists := seenSources[source]; exists {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].source %q conflicts with dataset %q", id, ds.Source, prior))
|
||||
} else {
|
||||
seenSources[source] = id
|
||||
}
|
||||
output := trimConfigSlashes(filepath.ToSlash(strings.TrimSpace(ds.Output)))
|
||||
if output != "" && pathEscapesRoot(output) {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].output must not escape project root", id))
|
||||
}
|
||||
if ds.NamingScheme != "" && ds.NamingScheme != "nwn_bmu" && ds.NamingScheme != "passthrough" {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].naming_scheme %q is not supported", id, ds.NamingScheme))
|
||||
}
|
||||
if ds.PackageMode != "" && ds.PackageMode != "hak_asset" && ds.PackageMode != "none" {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].package_mode %q is not supported", id, ds.PackageMode))
|
||||
}
|
||||
if ds.OutputExtension != "" && !strings.HasPrefix(ds.OutputExtension, ".") {
|
||||
failures = append(failures, fmt.Errorf("music.datasets[%q].output_extension must start with .", id))
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func pathEscapesRoot(path string) bool {
|
||||
clean := filepath.Clean(filepath.FromSlash(path))
|
||||
return clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator))
|
||||
|
||||
@@ -384,50 +384,30 @@ func TestValidateLayoutRejectsInvalidTopDataClassFeatInjections(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanUsesYAMLMusicConvertExtensionsWithinConfiguredDatasetRoots(t *testing.T) {
|
||||
func TestDefaultAssetExtensionsKeepAuthoredBMUWithoutConversionSources(t *testing.T) {
|
||||
for _, extension := range []string{".bmu", ".wav"} {
|
||||
if !slices.Contains(AssetExtensions, extension) {
|
||||
t.Errorf("default asset extensions must retain %s", extension)
|
||||
}
|
||||
}
|
||||
for _, extension := range []string{".mp3", ".ogg"} {
|
||||
if slices.Contains(AssetExtensions, extension) {
|
||||
t.Errorf("default asset extensions must not include conversion source %s", extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsRemovedMusicConfig(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "assets", "audio", "westgate"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir westgate audio: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, "assets", "audio", "other"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir other audio: %v", err)
|
||||
}
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
paths:
|
||||
source: src
|
||||
assets: assets
|
||||
music:
|
||||
defaults:
|
||||
convert_extensions:
|
||||
- .flac
|
||||
datasets:
|
||||
westgate_audio:
|
||||
source: audio/westgate
|
||||
convert_extensions:
|
||||
- .aac
|
||||
datasets: {}
|
||||
`)
|
||||
writeProjectFile(t, filepath.Join(root, "assets", "audio", "westgate", "theme.aac"), "aac")
|
||||
writeProjectFile(t, filepath.Join(root, "assets", "audio", "westgate", "theme.flac"), "flac")
|
||||
writeProjectFile(t, filepath.Join(root, "assets", "audio", "other", "ignored.aac"), "aac")
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if err := proj.Scan(); err != nil {
|
||||
t.Fatalf("Scan returned error: %v", err)
|
||||
}
|
||||
if !slices.Contains(proj.Inventory.AssetFiles, "audio/westgate/theme.aac") {
|
||||
t.Fatalf("expected configured dataset AAC file to be discovered, got %#v", proj.Inventory.AssetFiles)
|
||||
}
|
||||
if !slices.Contains(proj.Inventory.AssetFiles, "audio/westgate/theme.flac") {
|
||||
t.Fatalf("expected configured dataset FLAC file to be discovered, got %#v", proj.Inventory.AssetFiles)
|
||||
}
|
||||
if slices.Contains(proj.Inventory.AssetFiles, "audio/other/ignored.aac") {
|
||||
t.Fatalf("did not expect non-dataset AAC file to be discovered, got %#v", proj.Inventory.AssetFiles)
|
||||
if _, err := Load(root); err == nil || !strings.Contains(err.Error(), "music") {
|
||||
t.Fatalf("expected strict YAML decoding to reject removed music config, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1780,34 +1760,6 @@ func TestValidateLayoutRejectsDuplicateHAKNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsAmbiguousMusicPrefixes(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "assets"))
|
||||
mkdirAll(t, filepath.Join(root, "build"))
|
||||
|
||||
proj := &Project{
|
||||
Root: root,
|
||||
Config: Config{
|
||||
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
||||
Paths: PathConfig{Assets: "assets", Build: "build"},
|
||||
Music: MusicConfig{
|
||||
Prefixes: map[string]string{
|
||||
"envi/music": "mus_",
|
||||
"/envi/music": "mus2_",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := proj.ValidateLayout()
|
||||
if err == nil {
|
||||
t.Fatal("expected music prefix validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "normalize to the same path") {
|
||||
t.Fatalf("expected ambiguous prefix validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanAllowsMissingAssetsDir(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "src"))
|
||||
|
||||
Reference in New Issue
Block a user