135 lines
4.4 KiB
Markdown
135 lines
4.4 KiB
Markdown
# Portrait Metadata Contract
|
|
|
|
## Objective
|
|
|
|
Define the canonical behavior for portrait metadata embedded in topdata rows, enabling explicit opt-in portrait injection into the portraits.2da table.
|
|
|
|
---
|
|
|
|
## Portrait Metadata Shape
|
|
|
|
Portrait metadata is embedded in row `meta.portrait`:
|
|
|
|
```json
|
|
{
|
|
"meta": {
|
|
"portrait": {
|
|
"resref": "nw2book1_",
|
|
"sex": 4,
|
|
"race": 10,
|
|
"inanimate_type": 4,
|
|
"plot": 0,
|
|
"low_gore": "****"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
- `resref` — non-empty string identifying the portrait base resource reference
|
|
|
|
### Optional Fields
|
|
|
|
- `sex` — numeric or string value mapped to portraits.2da `Sex` column
|
|
- `race` — numeric or string value mapped to portraits.2da `Race` column
|
|
- `inanimate_type` — numeric or string value mapped to portraits.2da `InanimateType` column
|
|
- `plot` — numeric or string value mapped to portraits.2da `Plot` column
|
|
- `low_gore` — numeric or string value mapped to portraits.2da `LowGore` column
|
|
|
|
Field names are case-insensitive with hyphen/underscore normalization:
|
|
- `inanimate_type` = `inanimate-type` = `InanimateType`
|
|
|
|
---
|
|
|
|
## Merge Behavior
|
|
|
|
### Identity Matching
|
|
|
|
Portrait entries are matched by `resref`. Injection is only enabled when all of the following are true:
|
|
|
|
1. The row carries `meta.portrait.resref`
|
|
2. The row comes from a JSON module under `topdata/data/<dataset>/modules`
|
|
3. The dataset is one of `appearance`, `genericdoors`, or `placeables`
|
|
|
|
When those conditions are met:
|
|
|
|
1. If a portraits.2da row with matching `BaseResRef` already exists → merge into that row
|
|
2. If no matching row exists → create a new row with auto-generated identity `portraits:auto:<resref>`
|
|
|
|
### Field Mapping
|
|
|
|
Portrait metadata fields map to portraits.2da columns:
|
|
|
|
| Metadata Key | 2DA Column |
|
|
|---|---|
|
|
| `resref` | `BaseResRef` |
|
|
| `sex` | `Sex` |
|
|
| `race` | `Race` |
|
|
| `inanimate_type` | `InanimateType` |
|
|
| `plot` | `Plot` |
|
|
| `low_gore` | `LowGore` |
|
|
|
|
### Override Precedence
|
|
|
|
- First contributor wins for each column
|
|
- If a column already has a non-null value, subsequent contributions are rejected if they differ
|
|
- Identical values (after normalization) are silently accepted
|
|
- Conflicting values produce a build error
|
|
|
|
### Null-Like Values
|
|
|
|
Missing fields and `nullValue` ("****") are treated as unset. They do not override existing values and are not considered conflicts.
|
|
|
|
---
|
|
|
|
## Lock Data
|
|
|
|
Auto-generated portrait rows are tracked in lock data with key pattern:
|
|
|
|
```
|
|
portraits:auto:<resref>
|
|
```
|
|
|
|
For example: `portraits:auto:nw2book1_`
|
|
|
|
---
|
|
|
|
## Cross-Dataset Injection
|
|
|
|
Only module-authored rows from `appearance`, `genericdoors`, and `placeables` may contribute portrait metadata. The build pipeline:
|
|
|
|
1. Collects all datasets
|
|
2. Runs `mergePortraitMetadata()` after dataset collection
|
|
3. Injects portrait rows into the portraits.2da dataset
|
|
4. Updates lock data if new auto-generated rows were created
|
|
5. Prunes stale `portraits:auto:*` lock entries when no active auto-injected portrait row remains
|
|
|
|
This enables supported non-portrait datasets to define portrait associations without modifying the portraits dataset directly, while making the behavior explicit and removable with the originating module.
|
|
|
|
---
|
|
|
|
## Validation Rules
|
|
|
|
1. `resref` is required — missing resref produces a parse error
|
|
2. Unknown metadata fields in `meta.portrait` produce a parse error
|
|
3. Unknown top-level keys in `meta` produce a parse error (allowed keys: `family`, `wiki`, `portrait`)
|
|
4. Base rows do not inject portraits, even if they carry `meta.portrait`
|
|
5. Conflicting portrait field values from different contributors produce a build error
|
|
6. Portrait metadata is excluded from emitted 2DA output — it exists only at authoring/build time
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
Implementation is correct only if all of the following are true:
|
|
|
|
1. Portrait metadata is parsed from `meta.portrait` with case-insensitive field names
|
|
2. Only module-authored rows from `appearance`, `genericdoors`, and `placeables` inject into portraits.2da by `resref` matching
|
|
3. Base rows do not trigger portrait injection
|
|
4. Auto-generated rows receive `portraits:auto:<resref>` lock keys
|
|
5. First-contributor-wins precedence is enforced per column
|
|
6. Conflicting values produce build errors with clear diagnostic messages
|
|
7. Portrait metadata does not leak into emitted 2DA column output
|
|
8. Lock data is persisted when new auto-generated rows are created and stale auto-generated keys are pruned
|