diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 16f9f8d..b4733a0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/adr/0002-domain-identity-namespaces.md b/docs/adr/0002-domain-identity-namespaces.md new file mode 100644 index 0000000..1b2deb6 --- /dev/null +++ b/docs/adr/0002-domain-identity-namespaces.md @@ -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` 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) diff --git a/docs/modules/README.md b/docs/modules/README.md index b6d2c46..ae72be8 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.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) | diff --git a/docs/modules/domain-identities.md b/docs/modules/domain-identities.md new file mode 100644 index 0000000..641113c --- /dev/null +++ b/docs/modules/domain-identities.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) diff --git a/src/domain/identity/content_id.gd b/src/domain/identity/content_id.gd new file mode 100644 index 0000000..40d7cae --- /dev/null +++ b/src/domain/identity/content_id.gd @@ -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") diff --git a/src/domain/identity/content_id.gd.uid b/src/domain/identity/content_id.gd.uid new file mode 100644 index 0000000..58bfc1e --- /dev/null +++ b/src/domain/identity/content_id.gd.uid @@ -0,0 +1 @@ +uid://dk6splyui2an1 diff --git a/src/domain/identity/entity_id.gd b/src/domain/identity/entity_id.gd new file mode 100644 index 0000000..043f972 --- /dev/null +++ b/src/domain/identity/entity_id.gd @@ -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 diff --git a/src/domain/identity/entity_id.gd.uid b/src/domain/identity/entity_id.gd.uid new file mode 100644 index 0000000..f0e0536 --- /dev/null +++ b/src/domain/identity/entity_id.gd.uid @@ -0,0 +1 @@ +uid://bqdd8el4me5ng diff --git a/src/domain/identity/server_entry_id.gd b/src/domain/identity/server_entry_id.gd new file mode 100644 index 0000000..6808b60 --- /dev/null +++ b/src/domain/identity/server_entry_id.gd @@ -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] diff --git a/src/domain/identity/server_entry_id.gd.uid b/src/domain/identity/server_entry_id.gd.uid new file mode 100644 index 0000000..969ada0 --- /dev/null +++ b/src/domain/identity/server_entry_id.gd.uid @@ -0,0 +1 @@ +uid://cg3mht5bhgsya diff --git a/src/domain/identity/wow_guid.gd b/src/domain/identity/wow_guid.gd new file mode 100644 index 0000000..79cec05 --- /dev/null +++ b/src/domain/identity/wow_guid.gd @@ -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 diff --git a/src/domain/identity/wow_guid.gd.uid b/src/domain/identity/wow_guid.gd.uid new file mode 100644 index 0000000..ee6036a --- /dev/null +++ b/src/domain/identity/wow_guid.gd.uid @@ -0,0 +1 @@ +uid://bmwlyacooq2lc diff --git a/src/tools/verify_domain_identities.gd b/src/tools/verify_domain_identities.gd new file mode 100644 index 0000000..bd4a2ab --- /dev/null +++ b/src/tools/verify_domain_identities.gd @@ -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) diff --git a/src/tools/verify_domain_identities.gd.uid b/src/tools/verify_domain_identities.gd.uid new file mode 100644 index 0000000..dd0ec59 --- /dev/null +++ b/src/tools/verify_domain_identities.gd.uid @@ -0,0 +1 @@ +uid://bmig1rwaur44r