feat(M01): add typed domain identities
Work-Package: M01-FND-DOMAIN-IDENTITIES-001 Agent: sindo-main-codex
This commit is contained in:
@@ -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