autogen cached models
This commit is contained in:
@@ -269,6 +269,10 @@ native-topdata subset packaged as HAK resources. It builds only `.2da` output,
|
|||||||
injects those generated files into `build-haks`, and rejects TLK-backed text
|
injects those generated files into `build-haks`, and rejects TLK-backed text
|
||||||
because this path does not produce TLK packages.
|
because this path does not produce TLK packages.
|
||||||
|
|
||||||
|
For `cachedmodels_rows` generated assets, the configured autogen scan appends
|
||||||
|
discovered model stems to the selected `cachedmodels` dataset without `.mdl`
|
||||||
|
extensions and skips models already present in authored rows.
|
||||||
|
|
||||||
For `parts_rows` generated assets, `parts_rows.acbonus` can normalize final
|
For `parts_rows` generated assets, `parts_rows.acbonus` can normalize final
|
||||||
ACBONUS values before module overrides are applied. The
|
ACBONUS values before module overrides are applied. The
|
||||||
`ascending_row_id_sort_key` strategy writes toolset ordering values from row
|
`ascending_row_id_sort_key` strategy writes toolset ordering values from row
|
||||||
|
|||||||
@@ -1458,7 +1458,7 @@ func validateGeneratedConfig(cfg GeneratedConfig) []error {
|
|||||||
}
|
}
|
||||||
failures = append(failures, validateGlobList(fieldPrefix+".include_datasets", top2da.IncludeDatasets)...)
|
failures = append(failures, validateGlobList(fieldPrefix+".include_datasets", top2da.IncludeDatasets)...)
|
||||||
switch strings.TrimSpace(top2da.Autogen.Mode) {
|
switch strings.TrimSpace(top2da.Autogen.Mode) {
|
||||||
case "parts_rows":
|
case "parts_rows", "cachedmodels_rows":
|
||||||
case "":
|
case "":
|
||||||
failures = append(failures, fmt.Errorf("%s.autogen.mode is required", fieldPrefix))
|
failures = append(failures, fmt.Errorf("%s.autogen.mode is required", fieldPrefix))
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1078,6 +1078,20 @@ func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "cachedmodels",
|
||||||
|
Source: "topdata",
|
||||||
|
Output: "{paths.cache}/generated-assets/cachedmodels-2da",
|
||||||
|
IncludeDatasets: []string{"cachedmodels"},
|
||||||
|
PackageRoot: "vfxs",
|
||||||
|
Autogen: AutogenConsumerConfig{
|
||||||
|
ID: "cachedmodels",
|
||||||
|
Mode: "cachedmodels_rows",
|
||||||
|
Root: "vfxs",
|
||||||
|
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
|
||||||
|
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ func autogenConsumerTargetsCollectedDataset(collected []nativeCollectedDataset,
|
|||||||
if dataset.Dataset.Name == "visualeffects" {
|
if dataset.Dataset.Name == "visualeffects" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
case "cachedmodels_rows":
|
||||||
|
if dataset.Dataset.Name == "cachedmodels" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -603,6 +607,59 @@ func autogenPartsInventory(entries []autogenManifestEntry) map[string]map[int]st
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func augmentWithAutogeneratedCachedModels(collected []nativeCollectedDataset, entries []autogenManifestEntry) ([]nativeCollectedDataset, error) {
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return collected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := append([]nativeCollectedDataset(nil), collected...)
|
||||||
|
for i, dataset := range result {
|
||||||
|
if dataset.Dataset.Name != "cachedmodels" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !slices.Contains(dataset.Columns, "Model") {
|
||||||
|
return nil, fmt.Errorf("cachedmodels dataset must include Model column for cachedmodels_rows autogen")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]map[string]any, len(dataset.Rows), len(dataset.Rows)+len(entries))
|
||||||
|
seenModels := make(map[string]struct{}, len(dataset.Rows)+len(entries))
|
||||||
|
nextID := 0
|
||||||
|
for index, row := range dataset.Rows {
|
||||||
|
cloned := cloneRowMap(row)
|
||||||
|
rows[index] = cloned
|
||||||
|
if rowID, ok := cloned["id"].(int); ok && rowID >= nextID {
|
||||||
|
nextID = rowID + 1
|
||||||
|
}
|
||||||
|
if model, ok := cloned["Model"].(string); ok {
|
||||||
|
model = strings.TrimSpace(model)
|
||||||
|
if model != "" && model != nullValue {
|
||||||
|
seenModels[strings.ToLower(model)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
model := strings.TrimSpace(entry.ModelStem)
|
||||||
|
if model == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToLower(model)
|
||||||
|
if _, exists := seenModels[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows = append(rows, map[string]any{
|
||||||
|
"id": nextID,
|
||||||
|
"Model": model,
|
||||||
|
})
|
||||||
|
seenModels[key] = struct{}{}
|
||||||
|
nextID++
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i].Rows = rows
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDataset, entries []autogenManifestEntry) ([]nativeCollectedDataset, error) {
|
func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDataset, entries []autogenManifestEntry) ([]nativeCollectedDataset, error) {
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
return collected, nil
|
return collected, nil
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ func applyAutogenConsumersForGeneratedAssets(collected []nativeCollectedDataset,
|
|||||||
switch consumer.Mode {
|
switch consumer.Mode {
|
||||||
case "parts_rows":
|
case "parts_rows":
|
||||||
return augmentWithAutogeneratedPartsWithConfig(collected, autogenPartsInventory(entries), consumer.PartsRows), nil
|
return augmentWithAutogeneratedPartsWithConfig(collected, autogenPartsInventory(entries), consumer.PartsRows), nil
|
||||||
|
case "cachedmodels_rows":
|
||||||
|
return augmentWithAutogeneratedCachedModels(collected, entries)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported generated topdata 2DA autogen mode %q", consumer.Mode)
|
return nil, fmt.Errorf("unsupported generated topdata 2DA autogen mode %q", consumer.Mode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6294,6 +6294,74 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_accessories"))
|
||||||
|
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair"))
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "cachedmodels"))
|
||||||
|
writeFile(t, filepath.Join(root, "assets", "vfxs", "head_accessories", "hfx_bandana.mdl"), "mdl\n")
|
||||||
|
writeFile(t, filepath.Join(root, "assets", "vfxs", "head_accessories", "hfx_existing.mdl"), "mdl\n")
|
||||||
|
writeFile(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "mdl\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "cachedmodels", "base.json"), `{
|
||||||
|
"columns": ["Model"],
|
||||||
|
"rows": [
|
||||||
|
{"id": 0, "Model": "grn_m_bone"},
|
||||||
|
{"id": 1, "Model": "hfx_existing"}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
p := testProject(root)
|
||||||
|
p.Config.Paths.Assets = "assets"
|
||||||
|
p.Config.Generated.TopData2DA = []project.GeneratedTopData2DAConfig{
|
||||||
|
{
|
||||||
|
ID: "cachedmodels",
|
||||||
|
Source: "topdata",
|
||||||
|
Output: ".cache/generated-assets/cachedmodels-2da",
|
||||||
|
IncludeDatasets: []string{"cachedmodels"},
|
||||||
|
PackageRoot: "vfxs",
|
||||||
|
Autogen: project.AutogenConsumerConfig{
|
||||||
|
ID: "cachedmodels",
|
||||||
|
Mode: "cachedmodels_rows",
|
||||||
|
Root: "vfxs",
|
||||||
|
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
|
||||||
|
Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := BuildGenerated2DAAssets(p, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildGenerated2DAAssets failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(assets) != 1 {
|
||||||
|
t.Fatalf("expected one generated asset, got %#v", assets)
|
||||||
|
}
|
||||||
|
if assets[0].Rel != "vfxs/cachedmodels.2da" {
|
||||||
|
t.Fatalf("expected vfxs/cachedmodels.2da rel, got %q", assets[0].Rel)
|
||||||
|
}
|
||||||
|
bytes, err := os.ReadFile(assets[0].SourcePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read generated cachedmodels.2da: %v", err)
|
||||||
|
}
|
||||||
|
text := string(bytes)
|
||||||
|
for _, want := range []string{
|
||||||
|
"0\tgrn_m_bone",
|
||||||
|
"1\thfx_existing",
|
||||||
|
"2\thfx_bandana",
|
||||||
|
"3\thfx_hair_bangs",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Fatalf("expected generated cachedmodels output to contain %q, got:\n%s", want, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(text, ".mdl") {
|
||||||
|
t.Fatalf("expected cached model rows without .mdl extension, got:\n%s", text)
|
||||||
|
}
|
||||||
|
if strings.Count(text, "hfx_existing") != 1 {
|
||||||
|
t.Fatalf("expected duplicate discovered model to be skipped, got:\n%s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeProjectImportsLegacyLoadscreens(t *testing.T) {
|
func TestNormalizeProjectImportsLegacyLoadscreens(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
||||||
|
|||||||
Reference in New Issue
Block a user