In-line Portrait Generation

This commit is contained in:
2026-04-16 12:04:28 +02:00
parent ef431bc8ae
commit 376de5d67a
7 changed files with 426 additions and 722 deletions
+124 -315
View File
@@ -6147,157 +6147,49 @@ func TestBuildBaseDatasetAllowsModuleDeclaredColumns(t *testing.T) {
}
}
func TestNormalizeProjectImportsLegacyAppearanceAndPortraits(t *testing.T) {
func TestBuildInjectsExpansionDataIntoTargetDataset(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data"))
mkdirAll(t, filepath.Join(root, "reference", "data", "appearance", "modules"))
mkdirAll(t, filepath.Join(root, "reference", "data", "portraits", "modules"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
writeFile(t, filepath.Join(root, "reference", "data", "appearance", "base.json"), `{
"columns": ["LABEL", "PORTRAIT"],
"rows": [
{"key": "appearance:dwarf", "id": 0, "LABEL": "Dwarf", "PORTRAIT": "****"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "appearance", "lock.json"), `{
"appearance:blacklagoonreaver": 15105,
"appearance:crawlingclaw": 15107,
"appearance:halfogre": 15108,
"appearance:zombieknight3": 15109,
"appearance:zombieknight4": 15110,
"appearance:blacklagoonreaverswimming": 15111
}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "appearance", "modules", "cotblreaver.json"), `{"entries":{"appearance:blacklagoonreaver":{"LABEL":"Black Lagoon Reaver","PORTRAIT":"po_cotbl"},"appearance:blacklagoonreaverswimming":{"LABEL":"Black Lagoon Reaver, Swimming","PORTRAIT":"po_cotbl"}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "appearance", "modules", "crawlingclaw.json"), `{"entries":{"appearance:crawlingclaw":{"LABEL":"Crawling Claw","PORTRAIT":"po_cclaw"}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "appearance", "modules", "halfogre.json"), `{"entries":{"appearance:halfogre":{"LABEL":"Half-Ogre","PORTRAIT":"****"}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "appearance", "modules", "zombieknight.json"), `{"entries":{"appearance:zombieknight3":{"LABEL":"Zombie_Knight3","PORTRAIT":"po_zk"},"appearance:zombieknight4":{"LABEL":"Zombie_Knight4","PORTRAIT":"po_zk"}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "portraits", "base.json"), `{
"columns": ["BaseResRef", "Sex", "Race", "InanimateType", "Plot", "LowGore"],
"rows": [
{"id": 0, "BaseResRef": "****", "Sex": 4, "Race": "****", "InanimateType": "****", "Plot": "****", "LowGore": "****"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "portraits", "lock.json"), `{
"zarathustra:id_572": 16001,
"tiberiusmorguhn:zknight": 16002,
"shemsuheru:id_1513": 16003,
"cepnwn2lib:id_10041": 16004,
"ceporiental:id_3854": 16020
}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "portraits", "modules", "add_cclaw.json"), `{"entries":{"zarathustra:id_572":{"BaseResRef":"cclaw_","Sex":4,"Race":10,"Plot":0}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "portraits", "modules", "add_cotblreaver.json"), `{"entries":{"shemsuheru:id_1513":{"BaseResRef":"cotbl_","Sex":4,"Race":15,"Plot":0}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "portraits", "modules", "add_nwn2lib.json"), `{"entries":{"cepnwn2lib:id_10041":{"BaseResRef":"nw2book1_","Sex":4,"InanimateType":4}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "portraits", "modules", "add_orientalplcs.json"), `{"entries":{"ceporiental:id_3854":{"BaseResRef":"ccc_adori01","Sex":4,"InanimateType":1}}}`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "portraits", "modules", "add_zknight.json"), `{"entries":{"tiberiusmorguhn:zknight":{"BaseResRef":"po_zk_","Sex":4,"Race":10,"Plot":0}}}`+"\n")
result, err := NormalizeProject(testProject(root))
if err != nil {
t.Fatalf("NormalizeProject failed: %v", err)
}
if result.UpdatedFiles < 13 {
t.Fatalf("expected appearance and portraits files to be imported, got %#v", result)
}
for _, path := range []string{
filepath.Join(root, "topdata", "data", "appearance", "base.json"),
filepath.Join(root, "topdata", "data", "appearance", "lock.json"),
filepath.Join(root, "topdata", "data", "appearance", "modules", "cotblreaver.json"),
filepath.Join(root, "topdata", "data", "appearance", "modules", "crawlingclaw.json"),
filepath.Join(root, "topdata", "data", "appearance", "modules", "halfogre.json"),
filepath.Join(root, "topdata", "data", "appearance", "modules", "zombieknight.json"),
filepath.Join(root, "topdata", "data", "portraits", "base.json"),
filepath.Join(root, "topdata", "data", "portraits", "lock.json"),
filepath.Join(root, "topdata", "data", "portraits", "modules", "add_cclaw.json"),
filepath.Join(root, "topdata", "data", "portraits", "modules", "add_cotblreaver.json"),
filepath.Join(root, "topdata", "data", "portraits", "modules", "add_nwn2lib.json"),
filepath.Join(root, "topdata", "data", "portraits", "modules", "add_orientalplcs.json"),
filepath.Join(root, "topdata", "data", "portraits", "modules", "add_zknight.json"),
} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected imported path %s: %v", path, err)
}
}
appearanceBase, err := os.ReadFile(filepath.Join(root, "topdata", "data", "appearance", "base.json"))
if err != nil {
t.Fatalf("read appearance base: %v", err)
}
if !strings.Contains(string(appearanceBase), `"output": "appearance.2da"`) {
t.Fatalf("expected imported appearance output name, got:\n%s", string(appearanceBase))
}
portraitsBase, err := os.ReadFile(filepath.Join(root, "topdata", "data", "portraits", "base.json"))
if err != nil {
t.Fatalf("read portraits base: %v", err)
}
if !strings.Contains(string(portraitsBase), `"output": "portraits.2da"`) {
t.Fatalf("expected imported portraits output name, got:\n%s", string(portraitsBase))
}
}
func TestBuildDenseGenerationDefaultsMissingColumnsAndPortraitFields(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "dense", "modules"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "plain"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "appearance"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "appearance", "modules"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "portraits"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables", "modules"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "portraits", "modules"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "dense", "base.json"), `{
"output": "dense.2da",
"columns": ["A", "B"],
"rows": [
{"id": 0, "A": "base"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "dense", "lock.json"), `{"dense:add":1}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "dense", "modules", "add.json"), `{
"entries": {
"dense:add": {
"A": "entry"
}
}
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "plain", "table.json"), `{
"output": "table.2da",
"columns": ["A", "B"],
"rows": [
{"id": 0, "A": "plain"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "base.json"), `{
"output": "portraits.2da",
"columns": ["BaseResRef", "Sex", "Race", "InanimateType", "Plot", "LowGore"],
"rows": []
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "lock.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "base.json"), `{
"output": "placeables.2da",
"columns": ["Label"],
"rows": [
{
"key": "placeables:bookshelf_special",
"id": 10,
"Label": "BookshelfSpecial"
}
{"id": 0, "BaseResRef": "****", "Sex": 4}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "lock.json"), `{"placeables:bookshelf_special":10}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "modules", "add_bookshelf_special.json"), `{
"overrides": [
{
"key": "placeables:bookshelf_special",
"meta": {
"portrait": {
"resref": "nw2book1_",
"sex": 4,
"inanimate_type": 4
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "lock.json"), "{}")
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "base.json"), `{
"output": "appearance.2da",
"columns": ["LABEL", "PORTRAIT"],
"rows": [
{"key": "appearance:crawlingclaw", "id": 0, "LABEL": "Crawling Claw", "PORTRAIT": "****"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "lock.json"), `{"appearance:crawlingclaw":0}`)
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "modules", "add_crawlingclaw.json"), `{
"entries": {
"appearance:crawlingclaw": {
"LABEL": "Crawling Claw",
"PORTRAIT": {
"value": "po_cclaw_",
"data": {
"portraits": {
"BaseResRef": "cclaw_",
"Sex": 4,
"Race": 10,
"Plot": 0
}
}
}
}
]
}
}`+"\n")
mkdirAll(t, filepath.Join(root, "reference"))
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
@@ -6306,40 +6198,112 @@ func TestBuildDenseGenerationDefaultsMissingColumnsAndPortraitFields(t *testing.
t.Fatalf("BuildNative failed: %v", err)
}
denseBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "dense.2da"))
appearanceBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "appearance.2da"))
if err != nil {
t.Fatalf("read dense.2da: %v", err)
t.Fatalf("read appearance.2da: %v", err)
}
if string(denseBytes) != "2DA V2.0\n\nA\tB\n0\tbase\t****\n1\tentry\t****\n" {
t.Fatalf("unexpected dense.2da output:\n%s", string(denseBytes))
}
plainBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "table.2da"))
if err != nil {
t.Fatalf("read table.2da: %v", err)
}
if string(plainBytes) != "2DA V2.0\n\nA\tB\n0\tplain\t****\n" {
t.Fatalf("unexpected table.2da output:\n%s", string(plainBytes))
if !strings.Contains(string(appearanceBytes), "po_cclaw_") {
t.Fatalf("expected PORTRAIT value in appearance.2da, got:\n%s", string(appearanceBytes))
}
portraitsBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "portraits.2da"))
if err != nil {
t.Fatalf("read portraits.2da: %v", err)
}
wantPortraits := "2DA V2.0\n\nBaseResRef\tSex\tRace\tInanimateType\tPlot\tLowGore\n0\tnw2book1_\t4\t****\t4\t****\t****\n"
if string(portraitsBytes) != wantPortraits {
t.Fatalf("unexpected portraits.2da output:\n%s", string(portraitsBytes))
text := string(portraitsBytes)
if !strings.Contains(text, "cclaw_") || !strings.Contains(text, "Sex\t") || !strings.Contains(text, "Race\t") {
t.Fatalf("expected injected portrait data in portraits.2da, got:\n%s", text)
}
if !strings.Contains(text, "4\t10\t") {
t.Fatalf("expected Sex=4 Race=10 in portraits.2da, got:\n%s", text)
}
placeablesBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "placeables.2da"))
lockBytes, err := os.ReadFile(filepath.Join(root, "topdata", "data", "portraits", "lock.json"))
if err != nil {
t.Fatalf("read placeables.2da: %v", err)
t.Fatalf("read portraits lock.json: %v", err)
}
if !strings.Contains(string(placeablesBytes), "Label\n") || !strings.Contains(string(placeablesBytes), "10\tBookshelfSpecial\n") {
t.Fatalf("unexpected placeables.2da output:\n%s", string(placeablesBytes))
lockText := string(lockBytes)
if !strings.Contains(lockText, "portraits:cclaw_") {
t.Fatalf("expected lockfile entry for portraits:cclaw_, got:\n%s", lockText)
}
if strings.Contains(string(placeablesBytes), "portrait") || strings.Contains(string(placeablesBytes), "meta") {
t.Fatalf("expected metadata to stay out of placeables.2da, got:\n%s", string(placeablesBytes))
}
func TestBuildExpansionDataDoesNotDuplicatePortraitRows(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "appearance"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "appearance", "modules"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "portraits"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "base.json"), `{
"output": "portraits.2da",
"columns": ["BaseResRef", "Sex", "Race", "InanimateType", "Plot", "LowGore"],
"rows": [
{"id": 0, "BaseResRef": "****", "Sex": 4}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "lock.json"), "{}")
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "base.json"), `{
"output": "appearance.2da",
"columns": ["LABEL", "PORTRAIT"],
"rows": []
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "lock.json"), "{}")
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "modules", "add_zombieknight.json"), `{
"entries": {
"appearance:zombieknight3": {
"LABEL": "Zombie Knight 3",
"PORTRAIT": {
"value": "po_zk_",
"data": {
"portraits": {
"BaseResRef": "zk_",
"Sex": 4,
"Race": 23,
"Plot": 0
}
}
}
},
"appearance:zombieknight4": {
"LABEL": "Zombie Knight 4",
"PORTRAIT": {
"value": "po_zk_",
"data": {
"portraits": {
"BaseResRef": "zk_",
"Sex": 4,
"Race": 23,
"Plot": 0
}
}
}
}
}
}`+"\n")
mkdirAll(t, filepath.Join(root, "reference"))
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
result, err := BuildNative(testProject(root), nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
portraitsBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "portraits.2da"))
if err != nil {
t.Fatalf("read portraits.2da: %v", err)
}
text := string(portraitsBytes)
lines := strings.Split(text, "\n")
zkCount := 0
for _, line := range lines {
if strings.Contains(line, "zk_") {
zkCount++
}
}
if zkCount != 1 {
t.Fatalf("expected exactly 1 portrait row with zk_, got %d:\n%s", zkCount, text)
}
}
@@ -6350,8 +6314,7 @@ func TestValidateProjectRejectsUnknownMetadataKey(t *testing.T) {
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "base.json"), `{
"output": "placeables.2da",
"columns": ["Label"],
"rows": [
{"id": 0, "Label": "Shelf", "meta": {"unknown": {}}}
"rows": [{"id": 0, "Label": "Shelf", "meta": {"unknown": {}}}
]
}`+"\n")
@@ -6424,58 +6387,6 @@ func TestBuildNativeTreatsMetaCaseInsensitive(t *testing.T) {
}
}
func TestBuildRejectsConflictingPortraitMetadata(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "portraits"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "genericdoors"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables", "modules"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "genericdoors", "modules"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "base.json"), `{
"output": "portraits.2da",
"columns": ["BaseResRef", "Sex", "Race", "InanimateType", "Plot", "LowGore"],
"rows": []
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "lock.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "base.json"), `{
"output": "placeables.2da",
"columns": ["Label"],
"rows": [
{"key": "placeables:shelf", "id": 1, "Label": "Shelf"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "lock.json"), `{"placeables:shelf":1}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "genericdoors", "base.json"), `{
"output": "genericdoors.2da",
"columns": ["Label"],
"rows": [
{"key": "genericdoors:door", "id": 2, "Label": "Door"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "genericdoors", "lock.json"), `{"genericdoors:door":2}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "modules", "ovr_shelf.json"), `{
"overrides": [
{"key": "placeables:shelf", "meta": {"portrait": {"resref": "nw2book1_", "sex": 4}}}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "genericdoors", "modules", "ovr_door.json"), `{
"overrides": [
{"key": "genericdoors:door", "meta": {"portrait": {"resref": "nw2book1_", "sex": 1}}}
]
}`+"\n")
mkdirAll(t, filepath.Join(root, "reference"))
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
_, err := BuildNative(testProject(root), nil)
if err == nil {
t.Fatalf("expected BuildNative to fail on conflicting portrait metadata")
}
if !strings.Contains(err.Error(), "conflicts for column Sex") {
t.Fatalf("expected portrait conflict error, got: %v", err)
}
}
func TestBuildCanonicalAppearanceMatchesReference(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data"))
@@ -7828,108 +7739,6 @@ with open(os.path.join(tlk, "sow_tlk.tlk"), "wb") as f:
}
}
func TestBuildSupportsCanonicalPortraitsFromDatasetAndMetadata(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "portraits", "modules"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables", "modules"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "base.json"), `{
"output": "portraits.2da",
"columns": ["BaseResRef", "Sex", "Race", "InanimateType", "Plot", "LowGore"],
"rows": [
{"id": 0, "BaseResRef": "****", "Sex": 4}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "lock.json"), `{"portraits:auto:nw2book1_":16004,"shemsuheru:id_1513":16003}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "modules", "add_cotblreaver.json"), `{
"entries": {
"shemsuheru:id_1513": {
"BaseResRef": "cotbl_",
"Sex": 4,
"Race": 15,
"Plot": 0
}
}
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "base.json"), `{
"output": "placeables.2da",
"columns": ["Label"],
"rows": [
{"key": "placeables:bookshelf", "id": 1, "Label": "Bookshelf"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "lock.json"), `{"placeables:bookshelf":1}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "modules", "ovr_bookshelf.json"), `{
"overrides": [
{"key": "placeables:bookshelf", "meta": {"portrait": {"resref": "nw2book1_", "sex": 4, "inanimate_type": 4}}}
]
}`+"\n")
mkdirAll(t, filepath.Join(root, "reference"))
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
result, err := BuildNative(testProject(root), nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "portraits.2da"))
if err != nil {
t.Fatalf("read portraits.2da: %v", err)
}
text := string(got)
if !strings.Contains(text, "BaseResRef\tSex\tRace\tInanimateType\tPlot\tLowGore\n") ||
!strings.Contains(text, "0\t****\t4\t****\t****\t****\t****\n") ||
!strings.Contains(text, "16003\tcotbl_\t4\t15\t****\t0\t****\n") ||
!strings.Contains(text, "16004\tnw2book1_\t4\t****\t4\t****\t****\n") {
t.Fatalf("unexpected portraits.2da output:\n%s", string(got))
}
}
func TestBuildIgnoresBasePortraitMetadataWithoutModuleOptInAndPrunesAutoLocks(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "portraits"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "base.json"), `{
"output": "portraits.2da",
"columns": ["BaseResRef", "Sex", "Race", "InanimateType", "Plot", "LowGore"],
"rows": [
{"id": 0, "BaseResRef": "****", "Sex": 4}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "lock.json"), `{"portraits:auto:nw2book1_":16004}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "base.json"), `{
"output": "placeables.2da",
"columns": ["Label"],
"rows": [
{"key": "placeables:bookshelf", "id": 1, "Label": "Bookshelf", "meta": {"portrait": {"resref": "nw2book1_", "sex": 4, "inanimate_type": 4}}}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "placeables", "lock.json"), `{"placeables:bookshelf":1}`+"\n")
mkdirAll(t, filepath.Join(root, "reference"))
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
result, err := BuildNative(testProject(root), nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "portraits.2da"))
if err != nil {
t.Fatalf("read portraits.2da: %v", err)
}
want := "2DA V2.0\n\nBaseResRef\tSex\tRace\tInanimateType\tPlot\tLowGore\n0\t****\t4\t****\t****\t****\t****\n"
if string(got) != want {
t.Fatalf("unexpected portraits.2da output:\n%s", string(got))
}
lockBytes, err := os.ReadFile(filepath.Join(root, "topdata", "data", "portraits", "lock.json"))
if err != nil {
t.Fatalf("read portraits lock: %v", err)
}
if strings.TrimSpace(string(lockBytes)) != "{}" {
t.Fatalf("expected stale auto portrait lock to be pruned, got:\n%s", string(lockBytes))
}
}
func TestNormalizeProjectImportsRacialtypesRegistry(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data"))