feat(M01): add typed domain identities
Work-Package: M01-FND-DOMAIN-IDENTITIES-001 Agent: sindo-main-codex
This commit is contained in:
@@ -36,7 +36,12 @@ TrinityCore/AzerothCore ◄── Network Adapter ◄── Runtime Client
|
||||
|
||||
### Domain
|
||||
|
||||
Содержит идентификаторы, value objects, команды и события: `EntityId`, `WowGuid`, `MapPosition`, `SpellId`, `MoveIntent`, `EntityMoved`. Не содержит side effects.
|
||||
Содержит идентификаторы, value objects, команды и события: stable authored
|
||||
`ContentId`, session-local `EntityId`, opaque wire `WowGuid`, namespaced
|
||||
`ServerEntryId`, typed coordinates, `SpellId`, `MoveIntent`, `EntityMoved`.
|
||||
Эти identity namespaces не конвертируются неявно и не содержат side effects;
|
||||
их lifecycle и mapping определены в
|
||||
[`modules/domain-identities.md`](modules/domain-identities.md).
|
||||
|
||||
### Gameplay
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# ADR 0002: Separate domain identity namespaces
|
||||
|
||||
- Status: Accepted for M01 contract implementation
|
||||
- Date: 2026-07-13
|
||||
- Work package: `M01-FND-DOMAIN-IDENTITIES-001`
|
||||
- Decision owners: M01 foundation/integration owners
|
||||
|
||||
## Context
|
||||
|
||||
OpenWC must relate authored content, runtime objects, WoW protocol objects and
|
||||
TrinityCore/AzerothCore rows. All four domains can expose an integer or string,
|
||||
but their identity lifetimes and collision rules are different. A raw `int`, a
|
||||
generic `id` field or one universal ID class would allow an entity sequence to
|
||||
be persisted as content identity, a database entry to be sent as a wire GUID,
|
||||
or equal entry numbers from different tables to collide.
|
||||
|
||||
The Content Project requires stable references before server entries are
|
||||
allocated. Runtime entities require cheap session-local identity even when no
|
||||
server object exists. WoW 3.3.5a GUIDs carry opaque 64 wire bits whose semantic
|
||||
decomposition must be established by protocol fixtures. Server templates use
|
||||
numeric entries scoped by an adapter/table namespace.
|
||||
|
||||
## Decision
|
||||
|
||||
### ContentId
|
||||
|
||||
`ContentId` stores canonical lowercase UUID text in the `8-4-4-4-12` shape.
|
||||
It is the only identity in this decision intended for stable authored-content
|
||||
references and serialization. Construction normalizes whitespace and letter
|
||||
case; `is_valid()` reports malformed input without crashing an import.
|
||||
|
||||
The value validates text shape, not UUID version or variant bits. A future
|
||||
Content Project allocator chooses the UUID generation policy and owns duplicate
|
||||
detection. Every persisted schema that contains a `ContentId` still declares
|
||||
its own schema version and field migration.
|
||||
|
||||
### EntityId
|
||||
|
||||
`EntityId` stores a positive integer allocated by one runtime session owner.
|
||||
It is valid only inside that session and must not be persisted as content,
|
||||
protocol or server identity. Sequence reuse policy belongs to the future entity
|
||||
registry, not the value object.
|
||||
|
||||
### WowGuid
|
||||
|
||||
`WowGuid` stores opaque `high_32_bits` and `low_32_bits` integers. Each word is
|
||||
validated against `0..0xffffffff`, preserving every unsigned wire bit without
|
||||
combining it into GDScript's signed 64-bit integer. The all-zero value remains
|
||||
an observable protocol sentinel and receives no gameplay interpretation here.
|
||||
|
||||
High-type masks, packed-GUID codecs and object-kind extraction are explicitly
|
||||
deferred to the build-12340 protocol package and require packet fixtures.
|
||||
|
||||
### ServerEntryId
|
||||
|
||||
`ServerEntryId` stores a positive numeric entry together with a normalized,
|
||||
non-empty adapter/table namespace. Equal numbers in different namespaces are
|
||||
different identities. Mapping a server entry to a `ContentId` is explicit
|
||||
adapter/snapshot data; neither value derives the other.
|
||||
|
||||
### Equality and keys
|
||||
|
||||
The types expose domain-named `equals` methods because `RefCounted` `==`
|
||||
compares object references. Stable keys are available only where their lifetime
|
||||
allows them: `ContentId.to_key()`, `WowGuid.to_hex_string()` and
|
||||
`ServerEntryId.to_key()`. `EntityId` exposes only `to_debug_key()` to discourage
|
||||
persistence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### One generic `Id<T>` abstraction
|
||||
|
||||
Rejected because GDScript has no compile-time generic type distinction and the
|
||||
shared API would obscure different validation, lifetime and persistence rules.
|
||||
|
||||
### Use strings everywhere
|
||||
|
||||
Rejected because prefixes are a convention rather than a type boundary and
|
||||
would still permit accidental cross-layer assignment.
|
||||
|
||||
### Store WowGuid in one integer
|
||||
|
||||
Rejected because unsigned 64-bit GUIDs can exceed GDScript's positive signed
|
||||
integer range. Split words preserve the wire representation losslessly.
|
||||
|
||||
### Use server entries as ContentId
|
||||
|
||||
Rejected because entries are allocated by a particular backend/table, can
|
||||
collide between namespaces and do not exist while authoring offline content.
|
||||
|
||||
### Generate UUIDs inside ContentId
|
||||
|
||||
Rejected because randomness/allocation is a side effect requiring duplicate,
|
||||
fixture and deterministic-build policy. This value object only normalizes and
|
||||
validates an allocator-provided value.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Cross-layer contracts must name the identity type they accept.
|
||||
- Content references remain stable before and after server deployment.
|
||||
- Runtime entity registries need explicit mappings to wire GUIDs/content IDs.
|
||||
- Server adapters must supply a namespace and maintain entry-to-content maps.
|
||||
- Malformed external values remain representable long enough for adapters to
|
||||
produce diagnostics, but consumers must call the relevant validation method.
|
||||
- Additional typed IDs such as map, spell, item and quest remain separate
|
||||
follow-up contracts rather than aliases of these four values.
|
||||
|
||||
## Verification
|
||||
|
||||
- UUID normalization, shape validation and value equality are fixture-tested.
|
||||
- Zero/positive session sequences establish `EntityId` validity boundaries.
|
||||
- Maximum unsigned GUID words round-trip to exactly sixteen hexadecimal digits;
|
||||
overflow is rejected and zero remains detectable.
|
||||
- Equal server entry numbers in creature/gameobject namespaces do not compare equal.
|
||||
- Dependency search verifies that identity values do not inherit Node/Resource
|
||||
or expose `Vector3`.
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 9562: Universally Unique IDentifiers](https://www.rfc-editor.org/rfc/rfc9562.html)
|
||||
- [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
|
||||
- [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md)
|
||||
@@ -8,6 +8,7 @@
|
||||
|---|---|---|
|
||||
| Foundation/data | Planned/Partial | [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md) |
|
||||
| Coordinate mapping | Implemented | [`coordinate-mapping.md`](coordinate-mapping.md) |
|
||||
| Domain identities | Implemented contract | [`domain-identities.md`](domain-identities.md) |
|
||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
|
||||
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# Domain Identities
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target/work package | `M01-FND-DOMAIN-IDENTITIES-001` |
|
||||
| Owners | Foundation/domain |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m01-domain-identities`, 2026-07-13 |
|
||||
| Profiles/capabilities | All profiles; identity contract version 1 |
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide non-interchangeable, scene-free identity values for authored content,
|
||||
runtime entities, WoW wire objects and namespaced server entries. Each value
|
||||
states its lifetime and serialization rules instead of exposing a generic `id`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Allocating, reserving or globally registering IDs.
|
||||
- Parsing packet bytes or server database rows.
|
||||
- Mapping GUID high types or server entries to gameplay object kinds.
|
||||
- Defining Content Project, snapshot or replay schemas.
|
||||
- Replacing future typed map/spell/item/quest/display/sound IDs.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Authoring[Content Project allocator] --> ContentId[ContentId]
|
||||
Session[Entity registry] --> EntityId[EntityId]
|
||||
Wire[Build 12340 packet adapter] --> WowGuid[WowGuid]
|
||||
Server[Core snapshot adapter] --> ServerEntry[ServerEntryId]
|
||||
ContentId --> Mapping[Explicit identity mapping records]
|
||||
EntityId --> Mapping
|
||||
WowGuid --> Mapping
|
||||
ServerEntry --> Mapping
|
||||
Mapping --> Consumers[Gameplay / network / authoring consumers]
|
||||
Raw[Raw int or string] -. forbidden across boundary .-> Consumers
|
||||
```
|
||||
|
||||
Allowed dependencies:
|
||||
|
||||
- GDScript scalar/string operations and `RefCounted` lifetime;
|
||||
- callers in content, gameplay, network and server adapter layers.
|
||||
|
||||
Forbidden dependencies:
|
||||
|
||||
- Node, Resource, SceneTree, renderer and UI state;
|
||||
- packet buffers, SQL connections or database table implementations;
|
||||
- implicit conversion between identity namespaces;
|
||||
- random/global allocation inside the value objects.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `ContentId` | Immutable value | Stable authored identity as canonical lowercase UUID text | Any thread; persisted content lifetime | `is_valid()` reports malformed shape |
|
||||
| `ContentId.equals` | Pure method | Value equality across references | Call duration | Null compares false |
|
||||
| `ContentId.to_key` | Pure method | Canonical serialization/dictionary key | Stable across schema lifetime | Caller must validate first |
|
||||
| `EntityId` | Immutable value | Positive session-local runtime sequence | One runtime session | `is_valid()` rejects zero/negative |
|
||||
| `EntityId.equals` | Pure method | Equality within an explicitly shared session | Call duration | Cross-session comparison is a caller error |
|
||||
| `EntityId.to_debug_key` | Pure method | Non-persistent diagnostic label | Session/log lifetime | Must not be serialized as content |
|
||||
| `WowGuid` | Immutable value | Opaque unsigned high/low wire words | Packet/replay/entity mapping lifetime | `has_valid_word_ranges()` reports overflow |
|
||||
| `WowGuid.is_empty` | Pure method | Detect all-zero wire sentinel | Call duration | Does not assign semantic meaning |
|
||||
| `WowGuid.to_hex_string` | Pure method | Sixteen-digit diagnostic/replay key | Wire/replay lifetime | Caller validates ranges first |
|
||||
| `ServerEntryId` | Immutable value | Adapter/table namespace plus positive entry | Snapshot/deployment mapping lifetime | `is_valid()` rejects missing namespace/non-positive entry |
|
||||
| `ServerEntryId.to_key` | Pure method | Namespaced mapping key | Snapshot/schema lifetime | Caller validates first |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | UUID text | Future Content Project allocator/parser | `ContentId` | Copied normalized string | Build/content lifetime |
|
||||
| Input | Session sequence | Future entity registry | `EntityId` | Copied integer | One runtime session |
|
||||
| Input | Two unsigned 32-bit words | Future build-12340 packet codec | `WowGuid` | Copied integers | Packet/replay/entity lifetime |
|
||||
| Input | Namespace and numeric entry | Future core snapshot adapter | `ServerEntryId` | Copied normalized string/integer | Snapshot/deployment lifetime |
|
||||
| Output | Validation/equality/key results | Identity values | Adapters/registries/schemas | Returned scalar/string | Call duration |
|
||||
|
||||
Side effects:
|
||||
|
||||
- none;
|
||||
- no allocation registry, random generation, file/cache write, packet read,
|
||||
database query, scene mutation or logging.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ContentSource[Authored document] -->|UUID text| ContentId
|
||||
ServerRow[Backend table row] -->|adapter + table + entry| ServerEntryId
|
||||
Packet[World packet] -->|high/low wire words| WowGuid
|
||||
Registry[Session entity allocator] -->|positive sequence| EntityId
|
||||
ServerEntryId --> DeployMap[Explicit deployment mapping]
|
||||
ContentId --> DeployMap
|
||||
WowGuid --> RuntimeMap[Explicit runtime mapping]
|
||||
EntityId --> RuntimeMap
|
||||
DeployMap --> Snapshot[Versioned snapshot/change set]
|
||||
RuntimeMap --> WorldState[Session world state]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
state ContentIdentity {
|
||||
[*] --> Parsed
|
||||
Parsed --> Persisted: valid ContentId
|
||||
Persisted --> Referenced
|
||||
}
|
||||
state RuntimeIdentity {
|
||||
[*] --> Allocated
|
||||
Allocated --> Active
|
||||
Active --> Released: session owner removes entity
|
||||
Released --> [*]
|
||||
}
|
||||
state ExternalIdentity {
|
||||
[*] --> Observed
|
||||
Observed --> Mapped: WowGuid or ServerEntryId
|
||||
Mapped --> Stale: disconnect or snapshot replacement
|
||||
Stale --> [*]
|
||||
}
|
||||
```
|
||||
|
||||
The values themselves are stateless and immutable. Allocation, release,
|
||||
mapping and stale-state ownership belong to future registries/adapters.
|
||||
|
||||
## Main sequence
|
||||
|
||||
Identity construction and validation are synchronous. An external adapter reads
|
||||
its format, constructs the matching value, validates it, then publishes an
|
||||
explicit mapping or typed domain event. No value object performs I/O or retries.
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Values own only copied strings/integers and are caller-owned `RefCounted` references.
|
||||
- Public methods do not mutate values or shared state and are worker-safe.
|
||||
- Content/session/wire/server mapping records are owned by their future layer,
|
||||
not by these values.
|
||||
- No Node, RID, file, socket, database, lock or cache resource is acquired.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Malformed ContentId | `is_valid()` | Invalid value remains inspectable | Source field/path supplied by parser | Regenerate or repair authored ID |
|
||||
| Invalid EntityId sequence | `is_valid()` | Registry must reject publication | Session/correlation context | Fix allocator/session mapping |
|
||||
| GUID word overflow | `has_valid_word_ranges()` | Codec must reject packet/replay value | Packet offset/opcode context | Reject malformed input; do not truncate |
|
||||
| Empty GUID | `is_empty()` | Preserved as protocol sentinel | Adapter decides whether field permits empty | Apply opcode/field-specific rule |
|
||||
| Missing server namespace/entry | `is_valid()` | Snapshot adapter rejects mapping | Backend/table/row context | Correct adapter schema mapping |
|
||||
| Unknown/stale mapping | Registry/snapshot lookup | No implicit numeric fallback | Typed IDs in diagnostic | Refresh snapshot/session or reject event |
|
||||
|
||||
There is no cancellation inside constant-time value operations.
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Identity contract version | 1 | All | No | Four separate namespaces and validation rules |
|
||||
| ContentId text form | Lowercase `8-4-4-4-12` UUID | Authoring/runtime/tools | No | Stable persisted key |
|
||||
| GUID decomposition | Opaque two-word storage | `wotlk_335a_build_12340` | No | Prevents unverified high-type assumptions |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
- `ContentId.uuid_text` is the only stable persisted identity introduced here.
|
||||
- Containing schemas must store their own schema version and duplicate/reference policy.
|
||||
- `EntityId` must never be persisted across sessions.
|
||||
- `WowGuid` may appear in versioned packet replays as explicit high/low words or
|
||||
the exact sixteen-digit hex form; a replay schema owns that choice.
|
||||
- `ServerEntryId` may appear in snapshots only with both namespace and entry.
|
||||
- This package is additive and migrates no existing data or cache.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Stable diagnostics use `ContentId.to_key`, `WowGuid.to_hex_string` and
|
||||
`ServerEntryId.to_key`.
|
||||
- Runtime diagnostics may use `EntityId.to_debug_key` with a session correlation ID.
|
||||
- Production metrics/logging belong to registries and adapters; values emit none.
|
||||
|
||||
## Verification
|
||||
|
||||
- Contract test: `src/tools/verify_domain_identities.gd`.
|
||||
- Fixtures cover UUID normalization/malformed shape, positive/zero session IDs,
|
||||
maximum/overflow GUID words, zero GUID and cross-table server entry collisions.
|
||||
- Dependency gate confirms no Node/Resource/Vector3 exposure.
|
||||
- Fidelity boundary: GUID bit meaning remains unclaimed until build-12340 packet fixtures.
|
||||
- Performance: constant-time integer checks and a bounded 36-character UUID scan.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Content Project allocator and duplicate/reference validation.
|
||||
- Session-owned entity registry and explicit `WowGuid ↔ EntityId` mappings.
|
||||
- Backend capability adapters that define server entry namespaces.
|
||||
- Separate typed map/spell/item/quest/display/sound values.
|
||||
- Fixture-backed build-12340 GUID high-type decoding in the protocol layer.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Stable authored ContentId | Implemented contract | UUID normalization/validation fixtures | Allocator and schema arrive with Content Project |
|
||||
| Session-local EntityId | Implemented contract | Positive/zero/equality fixtures | Registry lifecycle arrives with gameplay world state |
|
||||
| Lossless opaque WowGuid | Implemented contract | Full unsigned words and hex fixture | Packet codec/high-type semantics need build-12340 fixtures |
|
||||
| Namespaced ServerEntryId | Implemented contract | Cross-table collision fixture | Backend namespace capability mapping arrives with server adapters |
|
||||
| Cross-namespace mappings | Planned | Architecture/ADR boundary | Implement only in owning registries/adapters |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- No allocator or duplicate registry exists yet.
|
||||
- GDScript `==` compares references; consumers must use domain `equals` methods
|
||||
or canonical keys until a registry owns keying.
|
||||
- UUID shape validation deliberately does not enforce generation version/variant.
|
||||
- Server namespace vocabulary is adapter-owned and not yet centrally catalogued.
|
||||
- GUID high-type/object-kind semantics remain unverified rather than guessed.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/domain/identity/content_id.gd` | Stable canonical UUID content identity |
|
||||
| `src/domain/identity/entity_id.gd` | Session-local runtime identity |
|
||||
| `src/domain/identity/wow_guid.gd` | Opaque unsigned WoW wire identity |
|
||||
| `src/domain/identity/server_entry_id.gd` | Namespaced backend entry identity |
|
||||
| `src/tools/verify_domain_identities.gd` | Headless value/boundary regression |
|
||||
| `docs/adr/0002-domain-identity-namespaces.md` | Normative namespace/lifetime decision |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- ADR: [`../adr/0002-domain-identity-namespaces.md`](../adr/0002-domain-identity-namespaces.md)
|
||||
- Architecture: [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
|
||||
- Foundation plan: [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md)
|
||||
@@ -0,0 +1,43 @@
|
||||
class_name ContentId
|
||||
extends RefCounted
|
||||
|
||||
## Stable authored-content identity serialized as canonical lowercase UUID text.
|
||||
## It is independent of runtime entities, server database entries and wire GUIDs.
|
||||
|
||||
var uuid_text: String:
|
||||
get:
|
||||
return _uuid_text
|
||||
|
||||
var _uuid_text: String
|
||||
|
||||
|
||||
## Normalizes surrounding whitespace and hexadecimal letter case.
|
||||
func _init(uuid_text_value: String) -> void:
|
||||
_uuid_text = uuid_text_value.strip_edges().to_lower()
|
||||
|
||||
|
||||
## Reports whether the value has the canonical 8-4-4-4-12 UUID text shape.
|
||||
func is_valid() -> bool:
|
||||
if _uuid_text.length() != 36:
|
||||
return false
|
||||
for character_index in range(_uuid_text.length()):
|
||||
if character_index in [8, 13, 18, 23]:
|
||||
if _uuid_text[character_index] != "-":
|
||||
return false
|
||||
elif not _is_lowercase_hexadecimal_character(_uuid_text[character_index]):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Compares stable content identity by normalized UUID text.
|
||||
func equals(other_content_id: ContentId) -> bool:
|
||||
return other_content_id != null and _uuid_text == other_content_id.uuid_text
|
||||
|
||||
|
||||
## Returns the canonical dictionary/serialization key.
|
||||
func to_key() -> String:
|
||||
return _uuid_text
|
||||
|
||||
|
||||
func _is_lowercase_hexadecimal_character(character: String) -> bool:
|
||||
return (character >= "0" and character <= "9") or (character >= "a" and character <= "f")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dk6splyui2an1
|
||||
@@ -0,0 +1,31 @@
|
||||
class_name EntityId
|
||||
extends RefCounted
|
||||
|
||||
## Session-local runtime entity identity.
|
||||
## This sequence is never a stable content ID, server entry or WoW wire GUID.
|
||||
|
||||
var session_sequence: int:
|
||||
get:
|
||||
return _session_sequence
|
||||
|
||||
var _session_sequence: int
|
||||
|
||||
|
||||
## Stores an allocator-issued session sequence without persistence semantics.
|
||||
func _init(session_sequence_value: int) -> void:
|
||||
_session_sequence = session_sequence_value
|
||||
|
||||
|
||||
## Reports whether this is a positive allocator-issued sequence.
|
||||
func is_valid() -> bool:
|
||||
return _session_sequence > 0
|
||||
|
||||
|
||||
## Compares identity inside the same explicitly owned runtime session.
|
||||
func equals(other_entity_id: EntityId) -> bool:
|
||||
return other_entity_id != null and _session_sequence == other_entity_id.session_sequence
|
||||
|
||||
|
||||
## Returns a diagnostic key; it must not be persisted across sessions.
|
||||
func to_debug_key() -> String:
|
||||
return "entity:%d" % _session_sequence
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqdd8el4me5ng
|
||||
@@ -0,0 +1,41 @@
|
||||
class_name ServerEntryId
|
||||
extends RefCounted
|
||||
|
||||
## Adapter-namespaced numeric entry from a server database or canonical snapshot.
|
||||
## Equal numbers in different template tables are intentionally different IDs.
|
||||
|
||||
var entry_namespace: String:
|
||||
get:
|
||||
return _entry_namespace
|
||||
|
||||
var entry_value: int:
|
||||
get:
|
||||
return _entry_value
|
||||
|
||||
var _entry_namespace: String
|
||||
var _entry_value: int
|
||||
|
||||
|
||||
## Normalizes a table/adapter namespace and preserves its positive entry value.
|
||||
func _init(entry_namespace_value: String, entry_value_value: int) -> void:
|
||||
_entry_namespace = entry_namespace_value.strip_edges().to_lower()
|
||||
_entry_value = entry_value_value
|
||||
|
||||
|
||||
## Reports whether both collision-prevention fields are present.
|
||||
func is_valid() -> bool:
|
||||
return not _entry_namespace.is_empty() and _entry_value > 0
|
||||
|
||||
|
||||
## Compares both namespace and numeric entry.
|
||||
func equals(other_server_entry_id: ServerEntryId) -> bool:
|
||||
return (
|
||||
other_server_entry_id != null
|
||||
and _entry_namespace == other_server_entry_id.entry_namespace
|
||||
and _entry_value == other_server_entry_id.entry_value
|
||||
)
|
||||
|
||||
|
||||
## Returns the stable adapter key without implying a ContentId mapping.
|
||||
func to_key() -> String:
|
||||
return "%s:%d" % [_entry_namespace, _entry_value]
|
||||
@@ -0,0 +1 @@
|
||||
uid://cg3mht5bhgsya
|
||||
@@ -0,0 +1,53 @@
|
||||
class_name WowGuid
|
||||
extends RefCounted
|
||||
|
||||
## Opaque 64-bit WoW wire GUID stored as two lossless unsigned 32-bit words.
|
||||
## Build-specific high-type decoding belongs to the protocol adapter and must be
|
||||
## fixture-backed before becoming a domain contract.
|
||||
|
||||
const MAXIMUM_UNSIGNED_32_BIT_VALUE := 0xffffffff
|
||||
|
||||
var high_32_bits: int:
|
||||
get:
|
||||
return _high_32_bits
|
||||
|
||||
var low_32_bits: int:
|
||||
get:
|
||||
return _low_32_bits
|
||||
|
||||
var _high_32_bits: int
|
||||
var _low_32_bits: int
|
||||
|
||||
|
||||
## Preserves both wire words without signed 64-bit conversion.
|
||||
func _init(high_32_bits_value: int, low_32_bits_value: int) -> void:
|
||||
_high_32_bits = high_32_bits_value
|
||||
_low_32_bits = low_32_bits_value
|
||||
|
||||
|
||||
## Reports whether both words fit their unsigned wire ranges.
|
||||
func has_valid_word_ranges() -> bool:
|
||||
return _is_unsigned_32_bit_word(_high_32_bits) and _is_unsigned_32_bit_word(_low_32_bits)
|
||||
|
||||
|
||||
## Reports the all-zero protocol sentinel without assigning gameplay meaning.
|
||||
func is_empty() -> bool:
|
||||
return _high_32_bits == 0 and _low_32_bits == 0
|
||||
|
||||
|
||||
## Compares all 64 opaque wire bits.
|
||||
func equals(other_wow_guid: WowGuid) -> bool:
|
||||
return (
|
||||
other_wow_guid != null
|
||||
and _high_32_bits == other_wow_guid.high_32_bits
|
||||
and _low_32_bits == other_wow_guid.low_32_bits
|
||||
)
|
||||
|
||||
|
||||
## Returns exactly sixteen lowercase hexadecimal digits for diagnostics/replays.
|
||||
func to_hex_string() -> String:
|
||||
return "%08x%08x" % [_high_32_bits, _low_32_bits]
|
||||
|
||||
|
||||
func _is_unsigned_32_bit_word(word_value: int) -> bool:
|
||||
return word_value >= 0 and word_value <= MAXIMUM_UNSIGNED_32_BIT_VALUE
|
||||
@@ -0,0 +1 @@
|
||||
uid://bmwlyacooq2lc
|
||||
@@ -0,0 +1,87 @@
|
||||
extends SceneTree
|
||||
|
||||
## Headless M01 contract verification for non-interchangeable identity values.
|
||||
|
||||
const ContentIdScript = preload("res://src/domain/identity/content_id.gd")
|
||||
const EntityIdScript = preload("res://src/domain/identity/entity_id.gd")
|
||||
const WowGuidScript = preload("res://src/domain/identity/wow_guid.gd")
|
||||
const ServerEntryIdScript = preload("res://src/domain/identity/server_entry_id.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_content_id(failures)
|
||||
_verify_entity_id(failures)
|
||||
_verify_wow_guid(failures)
|
||||
_verify_server_entry_id(failures)
|
||||
_verify_scene_free_sources(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("DOMAIN_IDENTITIES: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("DOMAIN_IDENTITIES PASS content=5 entity=5 wow_guid=6 server_entry=5")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_content_id(failures: Array[String]) -> void:
|
||||
var normalized = ContentIdScript.new(" 123E4567-E89B-42D3-A456-426614174000 ")
|
||||
_expect_true(normalized.is_valid(), "normalized ContentId is valid", failures)
|
||||
_expect_equal(normalized.to_key(), "123e4567-e89b-42d3-a456-426614174000", "ContentId canonical key", failures)
|
||||
_expect_true(normalized.equals(ContentIdScript.new("123e4567-e89b-42d3-a456-426614174000")), "equal ContentIds", failures)
|
||||
_expect_true(not normalized.equals(ContentIdScript.new("123e4567-e89b-42d3-a456-426614174001")), "different ContentIds", failures)
|
||||
_expect_true(not ContentIdScript.new("123e4567e89b42d3a456426614174000").is_valid(), "malformed ContentId", failures)
|
||||
|
||||
|
||||
func _verify_entity_id(failures: Array[String]) -> void:
|
||||
var entity_id = EntityIdScript.new(42)
|
||||
_expect_true(entity_id.is_valid(), "positive EntityId", failures)
|
||||
_expect_true(not EntityIdScript.new(0).is_valid(), "zero EntityId", failures)
|
||||
_expect_true(not EntityIdScript.new(-1).is_valid(), "negative EntityId", failures)
|
||||
_expect_true(entity_id.equals(EntityIdScript.new(42)), "equal EntityIds", failures)
|
||||
_expect_equal(entity_id.to_debug_key(), "entity:42", "EntityId diagnostic key", failures)
|
||||
|
||||
|
||||
func _verify_wow_guid(failures: Array[String]) -> void:
|
||||
var guid = WowGuidScript.new(0xf1300000, 0xffffffff)
|
||||
_expect_true(guid.has_valid_word_ranges(), "maximum GUID low word", failures)
|
||||
_expect_equal(guid.to_hex_string(), "f1300000ffffffff", "GUID hexadecimal form", failures)
|
||||
_expect_true(guid.equals(WowGuidScript.new(0xf1300000, 0xffffffff)), "equal GUID words", failures)
|
||||
_expect_true(WowGuidScript.new(0, 0).is_empty(), "zero GUID sentinel", failures)
|
||||
_expect_true(not WowGuidScript.new(-1, 0).has_valid_word_ranges(), "negative GUID word", failures)
|
||||
_expect_true(not WowGuidScript.new(0x100000000, 0).has_valid_word_ranges(), "overflow GUID word", failures)
|
||||
|
||||
|
||||
func _verify_server_entry_id(failures: Array[String]) -> void:
|
||||
var creature_entry = ServerEntryIdScript.new(" Creature_Template ", 123)
|
||||
var gameobject_entry = ServerEntryIdScript.new("gameobject_template", 123)
|
||||
_expect_true(creature_entry.is_valid(), "namespaced server entry", failures)
|
||||
_expect_equal(creature_entry.to_key(), "creature_template:123", "server entry key", failures)
|
||||
_expect_true(not creature_entry.equals(gameobject_entry), "cross-table entry collision prevented", failures)
|
||||
_expect_true(not ServerEntryIdScript.new("", 123).is_valid(), "missing entry namespace", failures)
|
||||
_expect_true(not ServerEntryIdScript.new("creature_template", 0).is_valid(), "zero server entry", failures)
|
||||
|
||||
|
||||
func _verify_scene_free_sources(failures: Array[String]) -> void:
|
||||
for file_name in ["content_id.gd", "entity_id.gd", "wow_guid.gd", "server_entry_id.gd"]:
|
||||
var path := "res://src/domain/identity/%s" % file_name
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
continue
|
||||
var source := file.get_as_text()
|
||||
_expect_true(not source.contains("extends Node"), "%s does not inherit Node" % file_name, failures)
|
||||
_expect_true(not source.contains("extends Resource"), "%s does not inherit Resource" % file_name, failures)
|
||||
_expect_true(not source.contains("Vector3"), "%s does not expose Vector3" % file_name, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual_value, expected_value, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bmig1rwaur44r
|
||||
Reference in New Issue
Block a user