Support for unique prefixes on 2da conversion
This commit is contained in:
+82
-10
@@ -17,12 +17,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type convertOptions struct {
|
type convertOptions struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
KeyFields []string
|
KeyFields []string
|
||||||
CollisionMode string
|
CollisionMode string
|
||||||
BaseDialog string
|
BaseDialog string
|
||||||
Type string
|
Type string
|
||||||
Name string
|
Name string
|
||||||
|
FilenamePrefixes map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunConvertCommand(args []string, stdout io.Writer) error {
|
func RunConvertCommand(args []string, stdout io.Writer) error {
|
||||||
@@ -107,7 +108,8 @@ func printConvertUsage(stdout io.Writer) {
|
|||||||
_, _ = fmt.Fprintln(stdout, "")
|
_, _ = fmt.Fprintln(stdout, "")
|
||||||
_, _ = fmt.Fprintln(stdout, "2da-to-module notes:")
|
_, _ = fmt.Fprintln(stdout, "2da-to-module notes:")
|
||||||
_, _ = fmt.Fprintln(stdout, " - --namespace is optional when topdata/templates/config.yaml declares it for the input table")
|
_, _ = fmt.Fprintln(stdout, " - --namespace is optional when topdata/templates/config.yaml declares it for the input table")
|
||||||
_, _ = fmt.Fprintln(stdout, " - --name controls inferred output naming: add_<namespace>_<name>.json")
|
_, _ = fmt.Fprintln(stdout, " - --name controls inferred output naming: <prefix>_<namespace>_<name>.json")
|
||||||
|
_, _ = fmt.Fprintln(stdout, " - topdata/templates/config.yaml module_output.filename_prefixes can set entries/override prefixes")
|
||||||
_, _ = fmt.Fprintln(stdout, " - flags accept both --flag value and --flag=value forms")
|
_, _ = fmt.Fprintln(stdout, " - flags accept both --flag value and --flag=value forms")
|
||||||
_, _ = fmt.Fprintln(stdout, " - unkeyed rows now fail conversion instead of being skipped")
|
_, _ = fmt.Fprintln(stdout, " - unkeyed rows now fail conversion instead of being skipped")
|
||||||
}
|
}
|
||||||
@@ -238,6 +240,9 @@ func parseConvertArgs(args []string, allowFormat bool, defaultCollision string)
|
|||||||
if len(opts.KeyFields) == 0 && len(ctx.Template.KeyFields) > 0 {
|
if len(opts.KeyFields) == 0 && len(ctx.Template.KeyFields) > 0 {
|
||||||
opts.KeyFields = append(opts.KeyFields, ctx.Template.KeyFields...)
|
opts.KeyFields = append(opts.KeyFields, ctx.Template.KeyFields...)
|
||||||
}
|
}
|
||||||
|
if len(opts.FilenamePrefixes) == 0 && len(ctx.Config.ModuleOutput.FilenamePrefixes) > 0 {
|
||||||
|
opts.FilenamePrefixes = cloneStringMap(ctx.Config.ModuleOutput.FilenamePrefixes)
|
||||||
|
}
|
||||||
if strings.TrimSpace(opts.Namespace) == "" {
|
if strings.TrimSpace(opts.Namespace) == "" {
|
||||||
return opts, "", "", errors.New("2da-to-module requires --namespace or a topdata/templates/config.yaml entry for the input table")
|
return opts, "", "", errors.New("2da-to-module requires --namespace or a topdata/templates/config.yaml entry for the input table")
|
||||||
}
|
}
|
||||||
@@ -550,11 +555,17 @@ type convertTemplateTable struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type convertTemplateConfig struct {
|
type convertTemplateConfig struct {
|
||||||
Tables map[string]convertTemplateTable `json:"tables" yaml:"tables"`
|
ModuleOutput convertModuleOutputConfig `json:"module_output" yaml:"module_output"`
|
||||||
|
Tables map[string]convertTemplateTable `json:"tables" yaml:"tables"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type convertModuleOutputConfig struct {
|
||||||
|
FilenamePrefixes map[string]string `json:"filename_prefixes" yaml:"filename_prefixes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type convertContext struct {
|
type convertContext struct {
|
||||||
Project *project.Project
|
Project *project.Project
|
||||||
|
Config convertTemplateConfig
|
||||||
Template convertTemplateTable
|
Template convertTemplateTable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,12 +585,17 @@ func resolveConvertContext(input string) (convertContext, error) {
|
|||||||
if !p.HasTopData() {
|
if !p.HasTopData() {
|
||||||
return convertContext{Project: p}, nil
|
return convertContext{Project: p}, nil
|
||||||
}
|
}
|
||||||
table, err := loadTemplateTableConfig(p, input)
|
config, err := loadConvertTemplateConfig(p)
|
||||||
|
if err != nil {
|
||||||
|
return convertContext{}, err
|
||||||
|
}
|
||||||
|
table, err := templateTableConfig(config, p, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return convertContext{}, err
|
return convertContext{}, err
|
||||||
}
|
}
|
||||||
return convertContext{
|
return convertContext{
|
||||||
Project: p,
|
Project: p,
|
||||||
|
Config: config,
|
||||||
Template: table,
|
Template: table,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -589,6 +605,10 @@ func loadTemplateTableConfig(p *project.Project, input string) (convertTemplateT
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return convertTemplateTable{}, err
|
return convertTemplateTable{}, err
|
||||||
}
|
}
|
||||||
|
return templateTableConfig(config, p, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templateTableConfig(config convertTemplateConfig, p *project.Project, input string) (convertTemplateTable, error) {
|
||||||
if len(config.Tables) == 0 {
|
if len(config.Tables) == 0 {
|
||||||
return convertTemplateTable{}, nil
|
return convertTemplateTable{}, nil
|
||||||
}
|
}
|
||||||
@@ -640,11 +660,28 @@ func loadConvertTemplateConfig(p *project.Project) (convertTemplateConfig, error
|
|||||||
return convertTemplateConfig{}, fmt.Errorf("parse %s: %w", configPath, err)
|
return convertTemplateConfig{}, fmt.Errorf("parse %s: %w", configPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := validateConvertTemplateConfig(config); err != nil {
|
||||||
|
return convertTemplateConfig{}, fmt.Errorf("validate %s: %w", configPath, err)
|
||||||
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
return convertTemplateConfig{}, nil
|
return convertTemplateConfig{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateConvertTemplateConfig(config convertTemplateConfig) error {
|
||||||
|
for moduleType, prefix := range config.ModuleOutput.FilenamePrefixes {
|
||||||
|
switch moduleType {
|
||||||
|
case "entries", "override":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported module_output.filename_prefixes key %q; expected entries or override", moduleType)
|
||||||
|
}
|
||||||
|
if normalizeModuleFilenamePrefix(prefix) == "" {
|
||||||
|
return fmt.Errorf("module_output.filename_prefixes.%s must contain at least one filename-safe character", moduleType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func resolveConvertOutputContext(p *project.Project, output string) (convertTemplateTable, error) {
|
func resolveConvertOutputContext(p *project.Project, output string) (convertTemplateTable, error) {
|
||||||
if p == nil || !p.HasTopData() || strings.TrimSpace(output) == "" {
|
if p == nil || !p.HasTopData() || strings.TrimSpace(output) == "" {
|
||||||
return convertTemplateTable{}, nil
|
return convertTemplateTable{}, nil
|
||||||
@@ -724,7 +761,8 @@ func defaultModuleFileName(input string, opts convertOptions, p *project.Project
|
|||||||
return "", fmt.Errorf("invalid --name %q", opts.Name)
|
return "", fmt.Errorf("invalid --name %q", opts.Name)
|
||||||
}
|
}
|
||||||
namespace := normalizeModuleFilenameNamespace(opts.Namespace)
|
namespace := normalizeModuleFilenameNamespace(opts.Namespace)
|
||||||
return fmt.Sprintf("add_%s_%s.json", namespace, name), nil
|
prefix := moduleFilenamePrefix(opts)
|
||||||
|
return fmt.Sprintf("%s_%s_%s.json", prefix, namespace, name), nil
|
||||||
}
|
}
|
||||||
if isTemplateInput(p, input) {
|
if isTemplateInput(p, input) {
|
||||||
return "", errors.New("2da-to-module requires --name when converting from topdata/templates without an explicit output path")
|
return "", errors.New("2da-to-module requires --name when converting from topdata/templates without an explicit output path")
|
||||||
@@ -732,6 +770,32 @@ func defaultModuleFileName(input string, opts convertOptions, p *project.Project
|
|||||||
return strings.TrimSuffix(filepath.Base(input), filepath.Ext(input)) + ".json", nil
|
return strings.TrimSuffix(filepath.Base(input), filepath.Ext(input)) + ".json", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func moduleFilenamePrefix(opts convertOptions) string {
|
||||||
|
moduleType := strings.TrimSpace(opts.Type)
|
||||||
|
if moduleType == "entry" {
|
||||||
|
moduleType = "entries"
|
||||||
|
}
|
||||||
|
if moduleType == "overrides" {
|
||||||
|
moduleType = "override"
|
||||||
|
}
|
||||||
|
if configured := strings.TrimSpace(opts.FilenamePrefixes[moduleType]); configured != "" {
|
||||||
|
if prefix := normalizeModuleFilenamePrefix(configured); prefix != "" {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "add"
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeModuleFilenamePrefix(prefix string) string {
|
||||||
|
prefix = strings.TrimSpace(strings.ToLower(prefix))
|
||||||
|
prefix = strings.ReplaceAll(prefix, "/", "_")
|
||||||
|
prefix = strings.ReplaceAll(prefix, "\\", "_")
|
||||||
|
prefix = convertKeyWhitespace.ReplaceAllString(prefix, "_")
|
||||||
|
prefix = convertKeyCleaner.ReplaceAllString(prefix, "_")
|
||||||
|
prefix = strings.Trim(prefix, "_")
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeModuleFilenameNamespace(namespace string) string {
|
func normalizeModuleFilenameNamespace(namespace string) string {
|
||||||
namespace = strings.TrimSpace(strings.ToLower(namespace))
|
namespace = strings.TrimSpace(strings.ToLower(namespace))
|
||||||
namespace = strings.ReplaceAll(namespace, "/", "_")
|
namespace = strings.ReplaceAll(namespace, "/", "_")
|
||||||
@@ -814,3 +878,11 @@ func write2DAFile(data parsed2DA, path string) error {
|
|||||||
}
|
}
|
||||||
return write2DA(table, path, false)
|
return write2DA(table, path, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneStringMap(in map[string]string) map[string]string {
|
||||||
|
out := make(map[string]string, len(in))
|
||||||
|
for key, value := range in {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,71 @@ tables:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunConvertCommandInfersOverrideOutputPrefixFromTemplateConfig(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||||
|
module:
|
||||||
|
name: Test
|
||||||
|
resref: test
|
||||||
|
paths:
|
||||||
|
source: src
|
||||||
|
assets: assets
|
||||||
|
build: build
|
||||||
|
topdata:
|
||||||
|
source: topdata
|
||||||
|
build: build/topdata
|
||||||
|
`)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "templates"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "templates", "config.yaml"), `
|
||||||
|
module_output:
|
||||||
|
filename_prefixes:
|
||||||
|
entries: add
|
||||||
|
override: ovr
|
||||||
|
tables:
|
||||||
|
template_appearance:
|
||||||
|
namespace: appearance
|
||||||
|
key_fields:
|
||||||
|
- LABEL
|
||||||
|
`)
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "templates", "template_appearance.2da"), "2DA V2.0\n\nLABEL STRING_REF NAME\n960 \"Zombie, Ash, Hot\" **** Zombie_Ash_Hot\n")
|
||||||
|
|
||||||
|
oldCwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getwd: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Chdir(oldCwd)
|
||||||
|
})
|
||||||
|
if err := os.Chdir(root); err != nil {
|
||||||
|
t.Fatalf("chdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
if err := RunConvertCommand([]string{
|
||||||
|
"2da-to-module",
|
||||||
|
"--type", "override",
|
||||||
|
"--name", "ashzombies",
|
||||||
|
"topdata/templates/template_appearance.2da",
|
||||||
|
}, &stdout); err != nil {
|
||||||
|
t.Fatalf("RunConvertCommand failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath := filepath.Join(root, "topdata", "data", "appearance", "modules", "ovr_appearance_ashzombies.json")
|
||||||
|
raw, err := os.ReadFile(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read output: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(raw), `"overrides"`) {
|
||||||
|
t.Fatalf("expected override payload, got:\n%s", string(raw))
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(root, "topdata", "data", "appearance", "modules", "add_appearance_ashzombies.json")); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected add-prefixed output to be absent, stat err: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout.String(), "converted overrides: 1") {
|
||||||
|
t.Fatalf("unexpected stdout: %s", stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunConvertCommand2DAToJSONInfersTemplateKeys(t *testing.T) {
|
func TestRunConvertCommand2DAToJSONInfersTemplateKeys(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||||
@@ -470,6 +535,54 @@ topdata:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunConvertCommandRejectsUnknownTemplateFilenamePrefixType(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||||
|
module:
|
||||||
|
name: Test
|
||||||
|
resref: test
|
||||||
|
paths:
|
||||||
|
source: src
|
||||||
|
assets: assets
|
||||||
|
build: build
|
||||||
|
topdata:
|
||||||
|
source: topdata
|
||||||
|
build: build/topdata
|
||||||
|
`)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "templates"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "templates", "config.yaml"), `
|
||||||
|
module_output:
|
||||||
|
filename_prefixes:
|
||||||
|
additions: add
|
||||||
|
tables:
|
||||||
|
template_appearance:
|
||||||
|
namespace: appearance
|
||||||
|
key_fields:
|
||||||
|
- LABEL
|
||||||
|
`)
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "templates", "template_appearance.2da"), "2DA V2.0\n\nLABEL\n960 Zombie_Ash_Hot\n")
|
||||||
|
|
||||||
|
oldCwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getwd: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Chdir(oldCwd)
|
||||||
|
})
|
||||||
|
if err := os.Chdir(root); err != nil {
|
||||||
|
t.Fatalf("chdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = RunConvertCommand([]string{
|
||||||
|
"2da-to-module",
|
||||||
|
"--name", "ashzombies",
|
||||||
|
"topdata/templates/template_appearance.2da",
|
||||||
|
}, &bytes.Buffer{})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), `unsupported module_output.filename_prefixes key "additions"`) {
|
||||||
|
t.Fatalf("expected invalid filename prefix type error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConvert2DAToModuleFailsOnDuplicateGeneratedKeysByDefault(t *testing.T) {
|
func TestConvert2DAToModuleFailsOnDuplicateGeneratedKeysByDefault(t *testing.T) {
|
||||||
_, err := convert2DAToModule(parsed2DA{
|
_, err := convert2DAToModule(parsed2DA{
|
||||||
Columns: []string{"LABEL"},
|
Columns: []string{"LABEL"},
|
||||||
|
|||||||
Reference in New Issue
Block a user