ci / ci (pull_request) Successful in 3m44s
*palcus.itp files are Toolset-generated snapshots: category skeleton plus one descriptor per custom blueprint. Treat them as derived data. Module source now commits only descriptor-free skeletons; build strips any stale descriptors and re-inserts descriptors computed from the blueprints in the source tree (name/STRREF, resref, and CR/faction for creatures), so the DM Creator lists always match what actually ships in the .mod. Compare applies the same projection. Extract skips *palcus.itp entirely, so local Toolset palette churn can no longer leak into module source. Blueprints with PaletteID 255 (hidden) or no PaletteID are omitted, per engine convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
192 lines
5.8 KiB
Go
192 lines
5.8 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/gff"
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
|
)
|
|
|
|
const paletteTestSkeleton = `{
|
|
"file_type": "ITP ",
|
|
"file_version": "V3.2",
|
|
"root": {
|
|
"struct_type": 4294967295,
|
|
"fields": [
|
|
{
|
|
"label": "MAIN",
|
|
"type": "List",
|
|
"value": [
|
|
{
|
|
"struct_type": 0,
|
|
"fields": [
|
|
{"label": "STRREF", "type": "DWord", "value": 500},
|
|
{
|
|
"label": "LIST",
|
|
"type": "List",
|
|
"value": [
|
|
{
|
|
"struct_type": 0,
|
|
"fields": [
|
|
{"label": "STRREF", "type": "DWord", "value": 6699},
|
|
{"label": "ID", "type": "Byte", "value": 23},
|
|
{
|
|
"label": "LIST",
|
|
"type": "List",
|
|
"value": [
|
|
{
|
|
"struct_type": 0,
|
|
"fields": [
|
|
{"label": "NAME", "type": "CExoString", "value": "Stale Junk"},
|
|
{"label": "RESREF", "type": "ResRef", "value": "stalejunk"}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"struct_type": 0,
|
|
"fields": [
|
|
{"label": "STRREF", "type": "DWord", "value": 6753},
|
|
{"label": "ID", "type": "Byte", "value": 24}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
`
|
|
|
|
func paletteTestItem(resref, name string, paletteID int) string {
|
|
return `{
|
|
"file_type": "UTI ",
|
|
"file_version": "V3.2",
|
|
"root": {
|
|
"struct_type": 4294967295,
|
|
"fields": [
|
|
{"label": "TemplateResRef", "type": "ResRef", "value": "` + resref + `"},
|
|
{
|
|
"label": "LocalizedName",
|
|
"type": "CExoLocString",
|
|
"value": {"string_ref": 4294967295, "entries": [{"id": 0, "value": "` + name + `"}]}
|
|
},
|
|
{"label": "PaletteID", "type": "Byte", "value": ` + itoa(paletteID) + `}
|
|
]
|
|
}
|
|
}
|
|
`
|
|
}
|
|
|
|
func itoa(v int) string {
|
|
data, _ := json.Marshal(v)
|
|
return string(data)
|
|
}
|
|
|
|
func TestBuildProjectsPaletteDescriptorsAndExtractSkipsThem(t *testing.T) {
|
|
root := t.TempDir()
|
|
mustMkdir(t, filepath.Join(root, "src", "module"))
|
|
mustMkdir(t, filepath.Join(root, "src", "palettes"))
|
|
mustMkdir(t, filepath.Join(root, "src", "blueprints", "items"))
|
|
mustMkdir(t, filepath.Join(root, "build"))
|
|
|
|
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
|
|
"module": {"name": "Test Module", "resref": "testmod"},
|
|
"paths": {"source": "src", "build": "build"}
|
|
}
|
|
`)
|
|
mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
|
|
"file_type": "IFO ",
|
|
"file_version": "V3.2",
|
|
"root": {"struct_type": 4294967295, "fields": [{"label": "Mod_Name", "type": "CExoString", "value": "Test Module"}]}
|
|
}
|
|
`)
|
|
mustWriteFile(t, filepath.Join(root, "src", "palettes", "itempalcus.itp.json"), paletteTestSkeleton)
|
|
mustWriteFile(t, filepath.Join(root, "src", "blueprints", "items", "i_b.uti.json"), paletteTestItem("i_b", "Bravo Item", 23))
|
|
mustWriteFile(t, filepath.Join(root, "src", "blueprints", "items", "i_a.uti.json"), paletteTestItem("i_a", "Alpha Item", 23))
|
|
mustWriteFile(t, filepath.Join(root, "src", "blueprints", "items", "i_hidden.uti.json"), paletteTestItem("i_hidden", "Hidden Item", 255))
|
|
|
|
p, err := project.Load(root)
|
|
if err != nil {
|
|
t.Fatalf("load project: %v", err)
|
|
}
|
|
if err := p.Scan(); err != nil {
|
|
t.Fatalf("scan: %v", err)
|
|
}
|
|
if _, err := BuildModule(p); err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
|
|
archiveFile, err := os.Open(p.ModuleArchivePath())
|
|
if err != nil {
|
|
t.Fatalf("open module archive: %v", err)
|
|
}
|
|
defer archiveFile.Close()
|
|
archive, err := erf.Read(archiveFile)
|
|
if err != nil {
|
|
t.Fatalf("read module archive: %v", err)
|
|
}
|
|
|
|
var palette *gff.Document
|
|
for _, resource := range archive.Resources {
|
|
if resource.Name == "itempalcus" {
|
|
document, err := gff.Read(bytes.NewReader(resource.Data))
|
|
if err != nil {
|
|
t.Fatalf("decode itempalcus: %v", err)
|
|
}
|
|
palette = &document
|
|
}
|
|
}
|
|
if palette == nil {
|
|
t.Fatal("itempalcus missing from built module")
|
|
}
|
|
|
|
canonical, err := json.Marshal(palette)
|
|
if err != nil {
|
|
t.Fatalf("marshal palette: %v", err)
|
|
}
|
|
text := string(canonical)
|
|
if bytes.Contains(canonical, []byte("stalejunk")) {
|
|
t.Fatalf("stale descriptor survived projection: %s", text)
|
|
}
|
|
for _, resref := range []string{"i_a", "i_b"} {
|
|
if !bytes.Contains(canonical, []byte(resref)) {
|
|
t.Fatalf("descriptor %s missing from projection: %s", resref, text)
|
|
}
|
|
}
|
|
if bytes.Contains(canonical, []byte("i_hidden")) {
|
|
t.Fatalf("PaletteID 255 blueprint leaked into projection: %s", text)
|
|
}
|
|
if a, b := bytes.Index(canonical, []byte("i_a")), bytes.Index(canonical, []byte("i_b")); a > b {
|
|
t.Fatalf("descriptors not name-sorted: %s", text)
|
|
}
|
|
|
|
if _, err := Compare(p); err != nil {
|
|
t.Fatalf("compare after build: %v", err)
|
|
}
|
|
|
|
// Extraction must never write palcus files back into source.
|
|
if err := os.Remove(filepath.Join(root, "src", "palettes", "itempalcus.itp.json")); err != nil {
|
|
t.Fatalf("remove skeleton: %v", err)
|
|
}
|
|
if err := p.Scan(); err != nil {
|
|
t.Fatalf("rescan: %v", err)
|
|
}
|
|
if _, err := Extract(p); err != nil {
|
|
t.Fatalf("extract: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "src", "palettes", "itempalcus.itp.json")); !os.IsNotExist(err) {
|
|
t.Fatalf("extract wrote palcus file back (stat err: %v)", err)
|
|
}
|
|
}
|