package project import ( "encoding/json" "fmt" "os" "path/filepath" "slices" "strings" "time" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/music" ) const ( DefaultBuildPath = "build" DefaultCachePath = ".cache" DefaultToolsPath = "tools" DefaultModuleArchiveTemplate = "{module.resref}.mod" DefaultHAKManifest = "haks.json" DefaultHAKArchiveTemplate = "{hak.name}.hak" DefaultSourceJSONPattern = "{resref}.{extension}.json" DefaultValidationProfile = "nwn_module" DefaultScriptsSourceDir = "scripts" DefaultScriptsCache = "{paths.cache}/ncs" DefaultScriptCompilerEnvPath = "SOW_NWN_SCRIPT_COMPILER" 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" DefaultTopDataWikiOutputRoot = "wiki" DefaultTopDataWikiPagesDir = "pages" DefaultTopDataWikiStateFile = "state.json" DefaultTopDataWikiSource = "topdata/wiki" DefaultTopDataWikiRenderer = "nodebb_tiptap_html" DefaultTopDataWikiLinkStrategy = "preserve_westgate_wiki_links" DefaultTopDataWikiNamespacesFile = "namespaces.yaml" DefaultTopDataWikiTablesFile = "tables.yaml" DefaultTopDataWikiTemplatesDir = "templates" DefaultTopDataWikiManualSectionsDir = "manual-sections" DefaultTopDataWikiStaleDefault = "report" DefaultTopDataWikiStaleLiveCleanup = "archive" DefaultTopDataWikiMarkerFormat = "html_comments" DefaultTopDataWikiPageMarkerPrefix = "sow-topdata-wiki" DefaultTopDataWikiTitlePrefixMinLength = 3 DefaultTopDataWikiStatusListingScope = "all" DefaultWikiDeployManifest = ".wiki_deploy_manifest.json" DefaultWikiDeployEditSummary = "Auto-generated from native builder" DefaultTopDataPackageHAK = "sow_top.hak" DefaultTopDataPackageTLK = "sow_tlk.tlk" DefaultAutogenCacheRoot = "{paths.cache}" DefaultAutogenCacheMaxAge = time.Hour DefaultAutogenRefreshEnv = "SOW_AUTOGEN_MANIFEST_REFRESH" DefaultPartsManifestRefreshEnv = "SOW_PARTS_MANIFEST_REFRESH" DefaultAutogenReleaseProvider = "gitea" DefaultAutogenServerURLEnv = "SOW_ASSETS_SERVER_URL" DefaultAutogenRepoEnv = "SOW_ASSETS_REPO" DefaultExtractLayout = "nwn_canonical_json" DefaultExtractHAKDiscovery = "build_glob" ) type ConfigProvenance map[string]ConfigValueProvenance type ConfigValueProvenance struct { Source string `json:"source" yaml:"source"` Detail string `json:"detail,omitempty" yaml:"detail,omitempty"` Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` } type EffectiveConfig struct { ConfigSource ConfigSource `json:"config_source" yaml:"config_source"` Module ModuleConfig `json:"module" yaml:"module"` Paths EffectivePathConfig `json:"paths" yaml:"paths"` Outputs EffectiveOutputConfig `json:"outputs" yaml:"outputs"` Build BuildConfig `json:"build" yaml:"build"` Generated GeneratedConfig `json:"generated_assets" yaml:"generated_assets"` Inventory InventoryConfig `json:"inventory" yaml:"inventory"` Validation ValidationConfig `json:"validation" yaml:"validation"` Scripts EffectiveScriptsConfig `json:"scripts" yaml:"scripts"` Music EffectiveMusicConfig `json:"music" yaml:"music"` TopData EffectiveTopDataConfig `json:"topdata" yaml:"topdata"` Extract ExtractConfig `json:"extract" yaml:"extract"` Autogen EffectiveAutogenConfig `json:"autogen" yaml:"autogen"` HAKs []HAKConfig `json:"haks" yaml:"haks"` Provenance ConfigProvenance `json:"provenance" yaml:"provenance"` Overrides []ConfigOverride `json:"overrides,omitempty" yaml:"overrides,omitempty"` } type EffectivePathConfig struct { Source string `json:"source" yaml:"source"` Assets string `json:"assets" yaml:"assets"` Build string `json:"build" yaml:"build"` Cache string `json:"cache" yaml:"cache"` Tools string `json:"tools" yaml:"tools"` } type EffectiveOutputConfig struct { ModuleArchive string `json:"module_archive" yaml:"module_archive"` HAKManifest string `json:"hak_manifest" yaml:"hak_manifest"` HAKArchive string `json:"hak_archive" yaml:"hak_archive"` } type EffectiveScriptsConfig struct { SourceDir string `json:"source_dir" yaml:"source_dir"` Cache string `json:"cache" yaml:"cache"` Compiler EffectiveScriptCompilerConfig `json:"compiler" yaml:"compiler"` } type EffectiveScriptCompilerConfig struct { Path string `json:"path" yaml:"path"` Search []string `json:"search" yaml:"search"` Env EffectiveScriptCompilerEnvConfig `json:"env" yaml:"env"` } type EffectiveScriptCompilerEnvConfig struct { Path string `json:"path" yaml:"path"` NWNRoot []string `json:"nwn_root" yaml:"nwn_root"` NWNUserDirectory []string `json:"nwn_user_directory" yaml:"nwn_user_directory"` } 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"` 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"` Wiki TopDataWikiConfig `json:"wiki" yaml:"wiki"` } type EffectiveAutogenConfig struct { Producers []AutogenProducerConfig `json:"producers" yaml:"producers"` Consumers []AutogenConsumerConfig `json:"consumers" yaml:"consumers"` Cache EffectiveAutogenCacheConfig `json:"cache" yaml:"cache"` ReleaseSource EffectiveAutogenReleaseSourceConfig `json:"release_source" yaml:"release_source"` } type EffectiveAutogenCacheConfig struct { Root string `json:"root" yaml:"root"` MaxAge string `json:"max_age" yaml:"max_age"` RefreshEnv string `json:"refresh_env" yaml:"refresh_env"` PartsManifestRefreshEnv string `json:"parts_manifest_refresh_env" yaml:"parts_manifest_refresh_env"` } type EffectiveAutogenReleaseSourceConfig struct { Provider string `json:"provider" yaml:"provider"` ServerURLEnv string `json:"server_url_env" yaml:"server_url_env"` RepoEnv string `json:"repo_env" yaml:"repo_env"` } type ConfigOverride struct { Key string `json:"key" yaml:"key"` Value string `json:"value" yaml:"value"` Source string `json:"source" yaml:"source"` } func (p *Project) EffectiveConfig() EffectiveConfig { provenance := cloneProvenance(p.Provenance) markMissingDefaults(provenance) paths := EffectivePathConfig{ Source: strings.TrimSpace(p.Config.Paths.Source), Assets: strings.TrimSpace(p.Config.Paths.Assets), Build: defaultString(p.Config.Paths.Build, DefaultBuildPath), Cache: defaultString(p.Config.Paths.Cache, DefaultCachePath), Tools: defaultString(p.Config.Paths.Tools, DefaultToolsPath), } outputs := EffectiveOutputConfig{ ModuleArchive: defaultString(p.Config.Outputs.ModuleArchive, DefaultModuleArchiveTemplate), HAKManifest: defaultString(p.Config.Outputs.HAKManifest, DefaultHAKManifest), HAKArchive: defaultString(p.Config.Outputs.HAKArchive, DefaultHAKArchiveTemplate), } inventory := InventoryConfig{ SourceExtensions: defaultStringSlice(p.Config.Inventory.SourceExtensions, SourceExtensions), AssetExtensions: defaultStringSlice(p.Config.Inventory.AssetExtensions, AssetExtensions), SourceJSONPattern: defaultString(p.Config.Inventory.SourceJSONPattern, DefaultSourceJSONPattern), } validation := ValidationConfig{ Profile: defaultString(p.Config.Validation.Profile, DefaultValidationProfile), BuiltinScriptPrefixes: normalizeLowerStringSlice(defaultStringSlice(p.Config.Validation.BuiltinScriptPrefixes, DefaultBuiltinScriptPrefixes())), RequiredFields: defaultRequiredFields(p.Config.Validation.RequiredFields), } scripts := EffectiveScriptsConfig{ SourceDir: defaultString(p.Config.Scripts.SourceDir, DefaultScriptsSourceDir), Cache: expandPathTemplate(defaultString(p.Config.Scripts.Cache, DefaultScriptsCache), paths), Compiler: EffectiveScriptCompilerConfig{ Path: strings.TrimSpace(p.Config.Scripts.Compiler.Path), Search: expandPathTemplates(defaultStringSlice(p.Config.Scripts.Compiler.Search, defaultScriptCompilerSearch()), paths), Env: EffectiveScriptCompilerEnvConfig{ Path: defaultString(p.Config.Scripts.Compiler.Env.Path, DefaultScriptCompilerEnvPath), NWNRoot: defaultStringSlice(p.Config.Scripts.Compiler.Env.NWNRoot, []string{"SOW_NWN_ROOT", "NWN_ROOT"}), NWNUserDirectory: defaultStringSlice(p.Config.Scripts.Compiler.Env.NWNUserDirectory, []string{"SOW_NWN_USER_DIRECTORY", "NWN_HOME", "NWN_USER_DIRECTORY"}), }, }, } 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), Wiki: TopDataWikiConfig{ OutputRoot: defaultString(p.Config.TopData.Wiki.OutputRoot, DefaultTopDataWikiOutputRoot), PagesDir: defaultString(p.Config.TopData.Wiki.PagesDir, DefaultTopDataWikiPagesDir), StateFile: defaultString(p.Config.TopData.Wiki.StateFile, DefaultTopDataWikiStateFile), ManagedNamespaces: defaultStringSlice(p.Config.TopData.Wiki.ManagedNamespaces, []string{"classes", "feat", "itemtypes", "races", "skills", "spells", "meta"}), DeployManifest: defaultString(p.Config.TopData.Wiki.DeployManifest, DefaultWikiDeployManifest), DeployEditSummary: defaultString(p.Config.TopData.Wiki.DeployEditSummary, DefaultWikiDeployEditSummary), Source: defaultString(p.Config.TopData.Wiki.Source, DefaultTopDataWikiSource), Renderer: defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer), LinkStrategy: defaultString(p.Config.TopData.Wiki.LinkStrategy, DefaultTopDataWikiLinkStrategy), NamespacesFile: defaultString(p.Config.TopData.Wiki.NamespacesFile, DefaultTopDataWikiNamespacesFile), TablesFile: defaultString(p.Config.TopData.Wiki.TablesFile, DefaultTopDataWikiTablesFile), TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir), ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir), TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength), StatusListingScope: defaultString(p.Config.TopData.Wiki.StatusListingScope, DefaultTopDataWikiStatusListingScope), StalePages: TopDataWikiStalePagesConfig{ Default: defaultString(p.Config.TopData.Wiki.StalePages.Default, DefaultTopDataWikiStaleDefault), LiveCleanup: defaultString(p.Config.TopData.Wiki.StalePages.LiveCleanup, DefaultTopDataWikiStaleLiveCleanup), }, ManagedRegion: TopDataWikiManagedRegionConfig{ MarkerFormat: defaultString(p.Config.TopData.Wiki.ManagedRegion.MarkerFormat, DefaultTopDataWikiMarkerFormat), PageMarkerPrefix: defaultString(p.Config.TopData.Wiki.ManagedRegion.PageMarkerPrefix, DefaultTopDataWikiPageMarkerPrefix), }, }, } generated := p.Config.Generated for index := range generated.TopData2DA { generated.TopData2DA[index].Output = expandPathTemplate(defaultString(generated.TopData2DA[index].Output, "{paths.cache}/generated-assets/"+generated.TopData2DA[index].ID), paths) } extract := p.Config.Extract extract.Layout = defaultString(extract.Layout, DefaultExtractLayout) extract.HAKDiscovery = defaultString(extract.HAKDiscovery, DefaultExtractHAKDiscovery) if len(extract.Archives) == 0 { extract.Archives = []string{outputs.ModuleArchiveName(p.Config.Module)} } if extract.CleanupStale == nil { defaultCleanup := true extract.CleanupStale = &defaultCleanup } if extract.ConsumeArchives == nil { defaultConsume := false 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, Paths: paths, Outputs: outputs, Build: p.Config.Build, Generated: generated, Inventory: inventory, Validation: validation, Scripts: scripts, 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, Autogen: EffectiveAutogenConfig{ Producers: slices.Clone(p.Config.Autogen.Producers), Consumers: slices.Clone(p.Config.Autogen.Consumers), Cache: EffectiveAutogenCacheConfig{ Root: expandPathTemplate(defaultString(p.Config.Autogen.Cache.Root, DefaultAutogenCacheRoot), paths), MaxAge: defaultString(p.Config.Autogen.Cache.MaxAge, DefaultAutogenCacheMaxAge.String()), RefreshEnv: defaultString(p.Config.Autogen.Cache.RefreshEnv, DefaultAutogenRefreshEnv), PartsManifestRefreshEnv: DefaultPartsManifestRefreshEnv, }, ReleaseSource: EffectiveAutogenReleaseSourceConfig{ Provider: defaultString(p.Config.Autogen.ReleaseSource.Provider, DefaultAutogenReleaseProvider), ServerURLEnv: defaultString(p.Config.Autogen.ReleaseSource.ServerURLEnv, DefaultAutogenServerURLEnv), RepoEnv: defaultString(p.Config.Autogen.ReleaseSource.RepoEnv, DefaultAutogenRepoEnv), }, }, HAKs: slices.Clone(p.Config.HAKs), Provenance: provenance, } effective.Overrides = activeOverrides(effective) return effective } func (p *Project) EffectiveConfigJSON() ([]byte, error) { return json.MarshalIndent(p.EffectiveConfig(), "", " ") } func (p *Project) ExplainConfig(key string) (any, ConfigValueProvenance, bool) { effective := p.EffectiveConfig() value, ok := lookupEffectiveValue(effective, key) if !ok { return nil, ConfigValueProvenance{}, false } prov := effective.Provenance[key] if prov.Source == "" { prov = ConfigValueProvenance{Source: "yaml", Detail: p.ConfigSource.Name} } return value, prov, true } func lookupEffectiveValue(effective EffectiveConfig, key string) (any, bool) { raw, err := json.Marshal(effective) if err != nil { return nil, false } var data any if err := json.Unmarshal(raw, &data); err != nil { return nil, false } current := data for _, part := range strings.Split(key, ".") { object, ok := current.(map[string]any) if !ok { return nil, false } current, ok = object[part] if !ok { return nil, false } } return current, true } func markMissingDefaults(provenance ConfigProvenance) { defaults := map[string]string{ "paths.build": "build", "paths.cache": ".cache", "paths.tools": "tools", "outputs.module_archive": DefaultModuleArchiveTemplate, "outputs.hak_manifest": DefaultHAKManifest, "outputs.hak_archive": DefaultHAKArchiveTemplate, "generated_assets.topdata_2da": "no generated topdata 2DA assets", "inventory.source_extensions": "NWN source resource extensions", "inventory.asset_extensions": "NWN asset resource extensions", "inventory.source_json_pattern": DefaultSourceJSONPattern, "validation.profile": DefaultValidationProfile, "validation.builtin_script_prefixes": "NWN built-in script prefixes", "validation.required_fields": "NWN required GFF fields", "scripts.source_dir": DefaultScriptsSourceDir, "scripts.cache": DefaultScriptsCache, "scripts.compiler.env.path": DefaultScriptCompilerEnvPath, "scripts.compiler.search": "repo tools and PATH:nwn_script_comp", "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, "topdata.package_hak": DefaultTopDataPackageHAK, "topdata.package_tlk": DefaultTopDataPackageTLK, "topdata.wiki.output_root": DefaultTopDataWikiOutputRoot, "topdata.wiki.pages_dir": DefaultTopDataWikiPagesDir, "topdata.wiki.state_file": DefaultTopDataWikiStateFile, "topdata.wiki.managed_namespaces": "NWN topdata wiki namespaces", "topdata.wiki.deploy_manifest": DefaultWikiDeployManifest, "topdata.wiki.deploy_edit_summary": DefaultWikiDeployEditSummary, "topdata.wiki.source": DefaultTopDataWikiSource, "topdata.wiki.renderer": DefaultTopDataWikiRenderer, "topdata.wiki.link_strategy": DefaultTopDataWikiLinkStrategy, "topdata.wiki.namespaces_file": DefaultTopDataWikiNamespacesFile, "topdata.wiki.tables_file": DefaultTopDataWikiTablesFile, "topdata.wiki.templates_dir": DefaultTopDataWikiTemplatesDir, "topdata.wiki.manual_sections_dir": DefaultTopDataWikiManualSectionsDir, "topdata.wiki.title_prefix_min_length": "3", "topdata.wiki.status_listing_scope": DefaultTopDataWikiStatusListingScope, "topdata.wiki.stale_pages.default": DefaultTopDataWikiStaleDefault, "topdata.wiki.stale_pages.live_cleanup": DefaultTopDataWikiStaleLiveCleanup, "topdata.wiki.managed_region.marker_format": DefaultTopDataWikiMarkerFormat, "topdata.wiki.managed_region.page_marker_prefix": DefaultTopDataWikiPageMarkerPrefix, "extract.layout": DefaultExtractLayout, "extract.hak_discovery": DefaultExtractHAKDiscovery, "extract.archives": DefaultModuleArchiveTemplate, "extract.cleanup_stale": "true", "extract.consume_archives": "false", "autogen.cache.root": DefaultAutogenCacheRoot, "autogen.cache.max_age": DefaultAutogenCacheMaxAge.String(), "autogen.cache.refresh_env": DefaultAutogenRefreshEnv, "autogen.cache.parts_manifest_refresh_env": DefaultPartsManifestRefreshEnv, "autogen.release_source.provider": DefaultAutogenReleaseProvider, "autogen.release_source.server_url_env": DefaultAutogenServerURLEnv, "autogen.release_source.repo_env": DefaultAutogenRepoEnv, } for key, detail := range defaults { if _, ok := provenance[key]; !ok { provenance[key] = ConfigValueProvenance{Source: "toolkit default", Detail: detail} } } } func cloneProvenance(input ConfigProvenance) ConfigProvenance { out := ConfigProvenance{} for key, value := range input { out[key] = value } return out } func defaultString(value, fallback string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return fallback } return trimmed } func defaultInt(value, fallback int) int { if value == 0 { return fallback } return value } func defaultStringSlice(value, fallback []string) []string { if len(value) == 0 { return slices.Clone(fallback) } return slices.Clone(value) } func defaultRequiredFields(configured map[string][]string) map[string][]string { if len(configured) > 0 { return cloneStringSliceMap(normalizeRequiredFields(configured)) } return DefaultRequiredFields() } func cloneStringSliceMap(input map[string][]string) map[string][]string { out := make(map[string][]string, len(input)) for key, values := range input { out[key] = slices.Clone(values) } return out } func cloneStringMap(input map[string]string) map[string]string { if len(input) == 0 { return nil } out := make(map[string]string, len(input)) for key, value := range input { out[key] = value } return out } func expandPathTemplate(template string, paths EffectivePathConfig) string { replacer := strings.NewReplacer( "{paths.source}", paths.Source, "{paths.assets}", paths.Assets, "{paths.build}", paths.Build, "{paths.cache}", paths.Cache, "{paths.tools}", paths.Tools, ) return filepath.ToSlash(replacer.Replace(template)) } func expandPathTemplates(templates []string, paths EffectivePathConfig) []string { out := make([]string, 0, len(templates)) for _, template := range templates { out = append(out, expandPathTemplate(template, paths)) } return out } func defaultScriptCompilerSearch() []string { return []string{ "{paths.tools}/script-compiler/nwn_script_comp", "{paths.tools}/script-compiler/nwn_script_comp.exe", "{paths.tools}/nwn_script_comp", "{paths.tools}/nwn_script_comp.exe", "PATH:nwn_script_comp", "PATH:nwn_script_comp.exe", } } func (p *Project) ActiveOverrides() []ConfigOverride { return activeOverrides(p.EffectiveConfig()) } func activeOverrides(effective EffectiveConfig) []ConfigOverride { envs := map[string]string{ "build.keep_existing_haks": effectiveBuildKeepExistingEnv, "scripts.compiler.path": effective.Scripts.Compiler.Env.Path, "autogen.cache.refresh": effective.Autogen.Cache.RefreshEnv, "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", "release.version": "GITHUB_REF_NAME", } for _, key := range effective.Scripts.Compiler.Env.NWNRoot { envs["scripts.compiler.env.nwn_root."+key] = key } for _, key := range effective.Scripts.Compiler.Env.NWNUserDirectory { envs["scripts.compiler.env.nwn_user_directory."+key] = key } keys := make([]string, 0, len(envs)) for key := range envs { keys = append(keys, key) } slices.Sort(keys) overrides := make([]ConfigOverride, 0) for _, key := range keys { env := envs[key] if value := strings.TrimSpace(os.Getenv(env)); value != "" { if isSensitiveOverrideKey(key) || isSensitiveOverrideKey(env) { value = "" } overrides = append(overrides, ConfigOverride{Key: key, Value: value, Source: "env:" + env}) } } return overrides } const effectiveBuildKeepExistingEnv = "SOW_BUILD_HAKS_KEEP_EXISTING" func isSensitiveOverrideKey(key string) bool { lower := strings.ToLower(key) return strings.Contains(lower, "token") || strings.Contains(lower, "password") } func (o EffectiveOutputConfig) ModuleArchiveName(module ModuleConfig) string { return strings.NewReplacer("{module.resref}", strings.TrimSpace(module.ResRef)).Replace(o.ModuleArchive) } func (o EffectiveOutputConfig) HAKArchiveName(name string) string { return strings.NewReplacer("{hak.name}", strings.TrimSpace(name)).Replace(o.HAKArchive) } func (p ConfigValueProvenance) String() string { if p.Detail == "" { return p.Source } return fmt.Sprintf("%s (%s)", p.Source, p.Detail) }