Portrait reservation fix?
This commit is contained in:
@@ -90,6 +90,10 @@ func mergeExpansionData(collected []nativeCollectedDataset) ([]nativeCollectedDa
|
||||
return nil, fmt.Errorf("expansion targets unknown dataset %q", targetDatasetName)
|
||||
}
|
||||
targetDS := collected[targetIndex]
|
||||
originalLockData, _ := loadLockfile(targetDS.Dataset.LockPath)
|
||||
if originalLockData == nil {
|
||||
originalLockData = map[string]int{}
|
||||
}
|
||||
lockModified := false
|
||||
usedIDs := map[int]struct{}{}
|
||||
usedKeys := map[string]struct{}{}
|
||||
@@ -135,6 +139,12 @@ func mergeExpansionData(collected []nativeCollectedDataset) ([]nativeCollectedDa
|
||||
if existingID, exists := targetDS.LockData[key]; exists {
|
||||
rowID = existingID
|
||||
hasID = true
|
||||
} else if existingID, exists := originalLockData[key]; exists {
|
||||
rowID = existingID
|
||||
hasID = true
|
||||
targetDS.LockData[key] = existingID
|
||||
lockModified = true
|
||||
targetDS.LockModified = true
|
||||
}
|
||||
if _, seen := usedKeys[key]; seen {
|
||||
continue
|
||||
|
||||
@@ -1169,6 +1169,10 @@ func nativeDatasetSourceDir(dataset nativeDataset) string {
|
||||
}
|
||||
|
||||
func collectReferencedLockKeysFromValue(value any, referenced map[string]struct{}) {
|
||||
collectReferencedLockKeysFromValueWithExpansion(value, referenced, false)
|
||||
}
|
||||
|
||||
func collectReferencedLockKeysFromValueWithExpansion(value any, referenced map[string]struct{}, inExpansionSubtree bool) {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if strings.Contains(typed, ":") {
|
||||
@@ -1176,17 +1180,26 @@ func collectReferencedLockKeysFromValue(value any, referenced map[string]struct{
|
||||
}
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
collectReferencedLockKeysFromValue(item, referenced)
|
||||
collectReferencedLockKeysFromValueWithExpansion(item, referenced, inExpansionSubtree)
|
||||
}
|
||||
case map[string]any:
|
||||
_, hasValue := typed["value"]
|
||||
_, hasData := typed["data"]
|
||||
isExpansion := hasValue && hasData
|
||||
|
||||
for key, item := range typed {
|
||||
if strings.Contains(key, ":") {
|
||||
referenced[key] = struct{}{}
|
||||
}
|
||||
if key == "key" {
|
||||
if inExpansionSubtree {
|
||||
if s, ok := item.(string); ok && strings.Contains(s, ":") {
|
||||
referenced[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
collectReferencedLockKeysFromValue(item, referenced)
|
||||
collectReferencedLockKeysFromValueWithExpansion(item, referenced, inExpansionSubtree || isExpansion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package topdata
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -7124,6 +7125,215 @@ func TestBuildExpansionDataIsGlobalAndPrunesStaleInjectedLocks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildExpansionDataPreservesExistingLockfileIDs(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"))
|
||||
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", "portraits", "base.json"), `{
|
||||
"output": "portraits.2da",
|
||||
"columns": ["BaseResRef", "Sex", "Race", "InanimateType", "Plot", "LowGore"],
|
||||
"rows": [
|
||||
{"id": 0, "BaseResRef": "****", "Sex": 4},
|
||||
{"id": 1, "BaseResRef": "existing_", "Sex": 2, "key": "portraits:existing"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "modules", "placeables.json"), `{
|
||||
"entries": {
|
||||
"portraits:plc_scala": {
|
||||
"BaseResRef": "plc_scala",
|
||||
"Sex": 4,
|
||||
"Race": 10,
|
||||
"Plot": 0
|
||||
},
|
||||
"portraits:plc_scalb": {
|
||||
"BaseResRef": "plc_scalb",
|
||||
"Sex": 4,
|
||||
"Race": 10,
|
||||
"Plot": 0
|
||||
}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "portraits", "lock.json"), `{
|
||||
"portraits:existing": 1,
|
||||
"portraits:grue_air_": 16632,
|
||||
"portraits:grue_fire_": 16633,
|
||||
"portraits:eyeball_": 16634,
|
||||
"portraits:zk_": 16635,
|
||||
"portraits:plc_scala": 16636,
|
||||
"portraits:plc_scalb": 16637
|
||||
}`)
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "base.json"), `{
|
||||
"output": "appearance.2da",
|
||||
"columns": ["LABEL", "PORTRAIT"],
|
||||
"rows": [
|
||||
{"key": "appearance:grue_air", "id": 0, "LABEL": "Air Grue", "PORTRAIT": "****"},
|
||||
{"key": "appearance:grue_fire", "id": 1, "LABEL": "Fire Grue", "PORTRAIT": "****"},
|
||||
{"key": "appearance:eyeball", "id": 2, "LABEL": "Eyeball", "PORTRAIT": "****"},
|
||||
{"key": "appearance:zk", "id": 3, "LABEL": "Zombie Knight", "PORTRAIT": "****"},
|
||||
{"key": "appearance:displacer", "id": 4, "LABEL": "Displacer", "PORTRAIT": "****"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "lock.json"), `{
|
||||
"appearance:grue_air": 0,
|
||||
"appearance:grue_fire": 1,
|
||||
"appearance:eyeball": 2,
|
||||
"appearance:zk": 3,
|
||||
"appearance:displacer": 4
|
||||
}`)
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "modules", "add_grues.json"), `{
|
||||
"entries": {
|
||||
"appearance:grue_air": {
|
||||
"LABEL": "Air Grue",
|
||||
"PORTRAIT": {
|
||||
"value": "po_grue_air_",
|
||||
"data": {
|
||||
"portraits": {
|
||||
"key": "portraits:grue_air_",
|
||||
"BaseResRef": "grue_air_",
|
||||
"Sex": 4,
|
||||
"Race": 10,
|
||||
"Plot": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"appearance:grue_fire": {
|
||||
"LABEL": "Fire Grue",
|
||||
"PORTRAIT": {
|
||||
"value": "po_grue_fire_",
|
||||
"data": {
|
||||
"portraits": {
|
||||
"key": "portraits:grue_fire_",
|
||||
"BaseResRef": "grue_fire_",
|
||||
"Sex": 4,
|
||||
"Race": 10,
|
||||
"Plot": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "modules", "add_eyeball.json"), `{
|
||||
"entries": {
|
||||
"appearance:eyeball": {
|
||||
"LABEL": "Eyeball",
|
||||
"PORTRAIT": {
|
||||
"value": "po_eyeball_",
|
||||
"data": {
|
||||
"portraits": {
|
||||
"key": "portraits:eyeball_",
|
||||
"BaseResRef": "eyeball_",
|
||||
"Sex": 4,
|
||||
"Race": 10,
|
||||
"Plot": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "modules", "add_zk.json"), `{
|
||||
"entries": {
|
||||
"appearance:zk": {
|
||||
"LABEL": "Zombie Knight",
|
||||
"PORTRAIT": {
|
||||
"value": "po_zk_",
|
||||
"data": {
|
||||
"portraits": {
|
||||
"key": "portraits:zk_",
|
||||
"BaseResRef": "zk_",
|
||||
"Sex": 4,
|
||||
"Race": 10,
|
||||
"Plot": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "appearance", "modules", "add_displacer.json"), `{
|
||||
"entries": {
|
||||
"appearance:displacer": {
|
||||
"LABEL": "Displacer",
|
||||
"PORTRAIT": {
|
||||
"value": "po_displacer1_",
|
||||
"data": {
|
||||
"portraits": {
|
||||
"key": "portraits:displacer1_",
|
||||
"BaseResRef": "displacer1_",
|
||||
"Sex": 4,
|
||||
"Race": 19,
|
||||
"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)
|
||||
}
|
||||
|
||||
lockBytes, err := os.ReadFile(filepath.Join(root, "topdata", "data", "portraits", "lock.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read portraits lock.json: %v", err)
|
||||
}
|
||||
lockText := string(lockBytes)
|
||||
|
||||
// All existing lockfile entries must preserve their IDs — no shifting
|
||||
expected := map[string]string{
|
||||
"portraits:grue_air_": "16632",
|
||||
"portraits:grue_fire_": "16633",
|
||||
"portraits:eyeball_": "16634",
|
||||
"portraits:zk_": "16635",
|
||||
"portraits:plc_scala": "16636",
|
||||
"portraits:plc_scalb": "16637",
|
||||
}
|
||||
for key, wantID := range expected {
|
||||
entry := fmt.Sprintf(`"%s": %s`, key, wantID)
|
||||
if !strings.Contains(lockText, entry) {
|
||||
t.Fatalf("lockfile entry %s was not preserved:\n%s", entry, lockText)
|
||||
}
|
||||
}
|
||||
|
||||
// New entry must NOT conflict with any existing ID
|
||||
var lockData map[string]int
|
||||
if err := json.Unmarshal([]byte(lockText), &lockData); err != nil {
|
||||
t.Fatalf("unmarshal lock.json: %v", err)
|
||||
}
|
||||
newID, ok := lockData["portraits:displacer1_"]
|
||||
if !ok {
|
||||
t.Fatalf("expected lockfile entry for portraits:displacer1_, got:\n%s", lockText)
|
||||
}
|
||||
for key, id := range lockData {
|
||||
if key != "portraits:displacer1_" && id == newID {
|
||||
t.Fatalf("new portrait displacer1_ got ID %d which conflicts with existing %s=%d", newID, key, id)
|
||||
}
|
||||
}
|
||||
if newID < 2 {
|
||||
t.Fatalf("new portrait displacer1_ got unreasonably low ID %d", newID)
|
||||
}
|
||||
|
||||
portraitsBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "portraits.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read portraits.2da: %v", err)
|
||||
}
|
||||
text := string(portraitsBytes)
|
||||
if !strings.Contains(text, "displacer1_") {
|
||||
t.Fatalf("expected injected portrait in portraits.2da, got:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProjectRejectsUnknownMetadataKey(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "placeables"))
|
||||
|
||||
Reference in New Issue
Block a user