feat: add emergent world foundations
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
class_name SpatialAddress
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
|
||||
var _world_id: StringName
|
||||
var _location_id: StringName
|
||||
var _position: Vector3
|
||||
|
||||
|
||||
func _init(
|
||||
world_id: StringName = &"", location_id: StringName = &"", position: Vector3 = Vector3.ZERO
|
||||
) -> void:
|
||||
_world_id = world_id
|
||||
_location_id = location_id
|
||||
_position = position
|
||||
|
||||
|
||||
static func create(
|
||||
world_id: StringName = &"", location_id: StringName = &"", position: Vector3 = Vector3.ZERO
|
||||
) -> SpatialAddress:
|
||||
var address := SpatialAddress.new(world_id, location_id, position)
|
||||
return address if address.is_valid() else null
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> SpatialAddress:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if not record_data.has_all(["world_id", "location_id", "position"]):
|
||||
return null
|
||||
var saved_position = record_data["position"]
|
||||
if not saved_position is Array or saved_position.size() != 3:
|
||||
return null
|
||||
return create(
|
||||
StringName(record_data["world_id"]),
|
||||
StringName(record_data["location_id"]),
|
||||
Vector3(float(saved_position[0]), float(saved_position[1]), float(saved_position[2]))
|
||||
)
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return is_finite(_position.x) and is_finite(_position.y) and is_finite(_position.z)
|
||||
|
||||
|
||||
func get_world_id() -> StringName:
|
||||
return _world_id
|
||||
|
||||
|
||||
func get_location_id() -> StringName:
|
||||
return _location_id
|
||||
|
||||
|
||||
func get_position() -> Vector3:
|
||||
return _position
|
||||
|
||||
|
||||
func index_key() -> String:
|
||||
var address_parts: Array = [String(_world_id), String(_location_id)]
|
||||
if _location_id.is_empty():
|
||||
address_parts.append([_position.x, _position.y, _position.z])
|
||||
return JSON.stringify(address_parts)
|
||||
|
||||
|
||||
func equals(other: SpatialAddress) -> bool:
|
||||
return (
|
||||
other != null
|
||||
and _world_id == other.get_world_id()
|
||||
and _location_id == other.get_location_id()
|
||||
and (_location_id.is_empty() == false or _position == other.get_position())
|
||||
)
|
||||
|
||||
|
||||
func duplicate_address() -> SpatialAddress:
|
||||
return SpatialAddress.new(_world_id, _location_id, _position)
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"world_id": String(_world_id),
|
||||
"location_id": String(_location_id),
|
||||
"position": [_position.x, _position.y, _position.z],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ngd5wc8yftsv
|
||||
@@ -0,0 +1,63 @@
|
||||
class_name WorldEntityRef
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
|
||||
var _entity_type: StringName
|
||||
var _entity_id: StringName
|
||||
|
||||
|
||||
func _init(entity_type: StringName = &"", entity_id: StringName = &"") -> void:
|
||||
_entity_type = entity_type
|
||||
_entity_id = entity_id
|
||||
|
||||
|
||||
static func create(entity_type: StringName, entity_id: StringName) -> WorldEntityRef:
|
||||
var entity_ref := WorldEntityRef.new(entity_type, entity_id)
|
||||
return entity_ref if entity_ref.is_valid() else null
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> WorldEntityRef:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if not record_data.has_all(["entity_type", "entity_id"]):
|
||||
return null
|
||||
return create(StringName(record_data["entity_type"]), StringName(record_data["entity_id"]))
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return not _entity_type.is_empty() and not _entity_id.is_empty()
|
||||
|
||||
|
||||
func get_entity_type() -> StringName:
|
||||
return _entity_type
|
||||
|
||||
|
||||
func get_entity_id() -> StringName:
|
||||
return _entity_id
|
||||
|
||||
|
||||
func index_key() -> String:
|
||||
var type_text := String(_entity_type)
|
||||
var id_text := String(_entity_id)
|
||||
return "%d:%s%d:%s" % [type_text.length(), type_text, id_text.length(), id_text]
|
||||
|
||||
|
||||
func equals(other: WorldEntityRef) -> bool:
|
||||
return (
|
||||
other != null
|
||||
and _entity_type == other.get_entity_type()
|
||||
and _entity_id == other.get_entity_id()
|
||||
)
|
||||
|
||||
|
||||
func duplicate_ref() -> WorldEntityRef:
|
||||
return WorldEntityRef.new(_entity_type, _entity_id)
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"entity_type": String(_entity_type),
|
||||
"entity_id": String(_entity_id),
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgy40n4mbhb65
|
||||
@@ -0,0 +1,318 @@
|
||||
class_name WorldEventRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const ENTITY_TYPE_NPC := &"npc"
|
||||
const ENTITY_TYPE_WORLD := &"world_entity"
|
||||
const ROLE_ACTOR := &"actor"
|
||||
const ROLE_SOURCE := &"source"
|
||||
const ROLE_DESTINATION := &"destination"
|
||||
const ECONOMIC_CORE_FIELDS := [
|
||||
"schema_version",
|
||||
"event_id",
|
||||
"event_type",
|
||||
"tick",
|
||||
"actor_id",
|
||||
"source_id",
|
||||
"destination_id",
|
||||
"world_position",
|
||||
]
|
||||
|
||||
var _event_id := -1
|
||||
var _event_type: StringName
|
||||
var _tick := -1
|
||||
var _participants: Dictionary = {}
|
||||
var _location: SpatialAddress
|
||||
var _payload: Dictionary = {}
|
||||
|
||||
|
||||
static func create(
|
||||
event_id: int,
|
||||
event_type: StringName,
|
||||
tick: int,
|
||||
participants: Dictionary = {},
|
||||
location: SpatialAddress = null,
|
||||
payload: Dictionary = {}
|
||||
) -> WorldEventRecord:
|
||||
var normalized_participants: Variant = _normalize_participants(participants)
|
||||
if normalized_participants == null:
|
||||
return null
|
||||
var payload_status := [true]
|
||||
var normalized_payload: Variant = _normalize_serializable_value(payload, payload_status)
|
||||
if not payload_status[0] or not normalized_payload is Dictionary:
|
||||
return null
|
||||
var event := WorldEventRecord.new()
|
||||
event._event_id = event_id
|
||||
event._event_type = event_type
|
||||
event._tick = tick
|
||||
event._participants = normalized_participants
|
||||
event._location = location.duplicate_address() if location != null else null
|
||||
event._payload = normalized_payload
|
||||
return event if event.is_valid() else null
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> WorldEventRecord:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if not record_data.has_all(
|
||||
["event_id", "event_type", "tick", "participants", "location", "payload"]
|
||||
):
|
||||
return null
|
||||
var saved_participants = record_data["participants"]
|
||||
if not saved_participants is Array:
|
||||
return null
|
||||
var participants: Dictionary = {}
|
||||
for participant_data in saved_participants:
|
||||
if not participant_data is Dictionary or not participant_data.has_all(["role", "entity"]):
|
||||
return null
|
||||
var role := StringName(participant_data["role"])
|
||||
if (
|
||||
role.is_empty()
|
||||
or participants.has(role)
|
||||
or not participant_data["entity"] is Dictionary
|
||||
):
|
||||
return null
|
||||
var entity_ref := WorldEntityRef.from_dictionary(participant_data["entity"])
|
||||
if entity_ref == null:
|
||||
return null
|
||||
participants[role] = entity_ref
|
||||
var location: SpatialAddress
|
||||
var saved_location = record_data["location"]
|
||||
if saved_location != null:
|
||||
if not saved_location is Dictionary:
|
||||
return null
|
||||
location = SpatialAddress.from_dictionary(saved_location)
|
||||
if location == null:
|
||||
return null
|
||||
if not record_data["payload"] is Dictionary:
|
||||
return null
|
||||
return create(
|
||||
int(record_data["event_id"]),
|
||||
StringName(record_data["event_type"]),
|
||||
int(record_data["tick"]),
|
||||
participants,
|
||||
location,
|
||||
record_data["payload"]
|
||||
)
|
||||
|
||||
|
||||
static func from_economic_event(
|
||||
economic_event: EconomicEventRecord, world_id: StringName = &"", location_id: StringName = &""
|
||||
) -> WorldEventRecord:
|
||||
if economic_event == null:
|
||||
return null
|
||||
var economic_data := economic_event.to_dictionary()
|
||||
var participants: Dictionary = {
|
||||
ROLE_ACTOR:
|
||||
WorldEntityRef.create(ENTITY_TYPE_NPC, StringName(str(int(economic_data["actor_id"]))))
|
||||
}
|
||||
var source_id := StringName(economic_data["source_id"])
|
||||
if not source_id.is_empty():
|
||||
participants[ROLE_SOURCE] = WorldEntityRef.create(ENTITY_TYPE_WORLD, source_id)
|
||||
var destination_id := StringName(economic_data["destination_id"])
|
||||
if not destination_id.is_empty():
|
||||
participants[ROLE_DESTINATION] = WorldEntityRef.create(ENTITY_TYPE_WORLD, destination_id)
|
||||
var payload: Dictionary = {}
|
||||
for field in economic_data:
|
||||
var field_name := String(field)
|
||||
if field_name in ECONOMIC_CORE_FIELDS:
|
||||
continue
|
||||
payload[field_name] = economic_data[field]
|
||||
var location := SpatialAddress.create(
|
||||
world_id, location_id, economic_event.get_world_position()
|
||||
)
|
||||
return create(
|
||||
int(economic_data["event_id"]),
|
||||
StringName(economic_data["event_type"]),
|
||||
int(economic_data["tick"]),
|
||||
participants,
|
||||
location,
|
||||
payload
|
||||
)
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
if _event_id < 0 or _tick < 0 or _event_type.is_empty():
|
||||
return false
|
||||
if _location != null and not _location.is_valid():
|
||||
return false
|
||||
for role in _participants:
|
||||
if StringName(role).is_empty():
|
||||
return false
|
||||
var entity_ref := _participants[role] as WorldEntityRef
|
||||
if entity_ref == null or not entity_ref.is_valid():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func get_event_id() -> int:
|
||||
return _event_id
|
||||
|
||||
|
||||
func get_event_type() -> StringName:
|
||||
return _event_type
|
||||
|
||||
|
||||
func get_tick() -> int:
|
||||
return _tick
|
||||
|
||||
|
||||
func has_location() -> bool:
|
||||
return _location != null
|
||||
|
||||
|
||||
func get_location() -> SpatialAddress:
|
||||
return _location.duplicate_address() if _location != null else null
|
||||
|
||||
|
||||
func get_payload() -> Dictionary:
|
||||
return _payload.duplicate(true)
|
||||
|
||||
|
||||
func get_participant(role: StringName) -> WorldEntityRef:
|
||||
var entity_ref := _participants.get(role) as WorldEntityRef
|
||||
return entity_ref.duplicate_ref() if entity_ref != null else null
|
||||
|
||||
|
||||
func get_participant_roles() -> Array[StringName]:
|
||||
var roles: Array[StringName] = []
|
||||
for role in _participants:
|
||||
roles.append(StringName(role))
|
||||
roles.sort_custom(
|
||||
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
|
||||
)
|
||||
return roles
|
||||
|
||||
|
||||
func get_participant_refs() -> Array[WorldEntityRef]:
|
||||
var refs: Array[WorldEntityRef] = []
|
||||
for role in get_participant_roles():
|
||||
var entity_ref := _participants[role] as WorldEntityRef
|
||||
refs.append(entity_ref.duplicate_ref())
|
||||
return refs
|
||||
|
||||
|
||||
func get_participant_index_keys() -> Array[String]:
|
||||
var keys: Array[String] = []
|
||||
var seen: Dictionary = {}
|
||||
for role in get_participant_roles():
|
||||
var entity_ref := _participants[role] as WorldEntityRef
|
||||
var key := entity_ref.index_key()
|
||||
if seen.has(key):
|
||||
continue
|
||||
seen[key] = true
|
||||
keys.append(key)
|
||||
keys.sort()
|
||||
return keys
|
||||
|
||||
|
||||
func to_economic_event() -> EconomicEventRecord:
|
||||
var actor := _participants.get(ROLE_ACTOR) as WorldEntityRef
|
||||
if actor == null:
|
||||
return null
|
||||
var actor_text := String(actor.get_entity_id())
|
||||
if not actor_text.is_valid_int():
|
||||
return null
|
||||
var source := _participants.get(ROLE_SOURCE) as WorldEntityRef
|
||||
var destination := _participants.get(ROLE_DESTINATION) as WorldEntityRef
|
||||
var economic_data := _payload.duplicate(true)
|
||||
economic_data["schema_version"] = EconomicEventRecord.SCHEMA_VERSION
|
||||
economic_data["event_id"] = _event_id
|
||||
economic_data["event_type"] = String(_event_type)
|
||||
economic_data["tick"] = _tick
|
||||
economic_data["actor_id"] = actor_text.to_int()
|
||||
economic_data["source_id"] = String(source.get_entity_id()) if source != null else ""
|
||||
economic_data["destination_id"] = (
|
||||
String(destination.get_entity_id()) if destination != null else ""
|
||||
)
|
||||
economic_data["item_id"] = String(economic_data.get("item_id", ""))
|
||||
economic_data["amount"] = float(economic_data.get("amount", 0.0))
|
||||
economic_data["action_id"] = String(economic_data.get("action_id", ""))
|
||||
economic_data["required_amount"] = float(economic_data.get("required_amount", 0.0))
|
||||
var position := _location.get_position() if _location != null else Vector3.ZERO
|
||||
economic_data["world_position"] = [position.x, position.y, position.z]
|
||||
return EconomicEventRecord.from_dictionary(economic_data)
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
var serialized_participants: Array[Dictionary] = []
|
||||
for role in get_participant_roles():
|
||||
var entity_ref := _participants[role] as WorldEntityRef
|
||||
(
|
||||
serialized_participants
|
||||
. append(
|
||||
{
|
||||
"role": String(role),
|
||||
"entity": entity_ref.to_dictionary(),
|
||||
}
|
||||
)
|
||||
)
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"event_id": _event_id,
|
||||
"event_type": String(_event_type),
|
||||
"tick": _tick,
|
||||
"participants": serialized_participants,
|
||||
"location": _location.to_dictionary() if _location != null else null,
|
||||
"payload": _payload.duplicate(true),
|
||||
}
|
||||
|
||||
|
||||
static func _normalize_participants(participants: Dictionary) -> Variant:
|
||||
var normalized: Dictionary = {}
|
||||
var role_names: Dictionary = {}
|
||||
for raw_role in participants:
|
||||
if not raw_role is String and not raw_role is StringName:
|
||||
return null
|
||||
var role := StringName(raw_role)
|
||||
var role_text := String(role)
|
||||
if role.is_empty() or role_names.has(role_text):
|
||||
return null
|
||||
var entity_ref := participants[raw_role] as WorldEntityRef
|
||||
if entity_ref == null or not entity_ref.is_valid():
|
||||
return null
|
||||
role_names[role_text] = true
|
||||
normalized[role] = entity_ref.duplicate_ref()
|
||||
return normalized
|
||||
|
||||
|
||||
static func _normalize_serializable_value(value: Variant, status: Array) -> Variant:
|
||||
match typeof(value):
|
||||
TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_STRING:
|
||||
return value
|
||||
TYPE_FLOAT:
|
||||
if not is_finite(float(value)):
|
||||
status[0] = false
|
||||
return value
|
||||
TYPE_STRING_NAME:
|
||||
return String(value)
|
||||
TYPE_ARRAY:
|
||||
var normalized_array: Array = []
|
||||
for element in value:
|
||||
normalized_array.append(_normalize_serializable_value(element, status))
|
||||
return normalized_array
|
||||
TYPE_DICTIONARY:
|
||||
return _normalize_serializable_dictionary(value, status)
|
||||
_:
|
||||
status[0] = false
|
||||
return null
|
||||
|
||||
|
||||
static func _normalize_serializable_dictionary(value: Dictionary, status: Array) -> Dictionary:
|
||||
var source_keys: Dictionary = {}
|
||||
for raw_key in value:
|
||||
if not raw_key is String and not raw_key is StringName:
|
||||
status[0] = false
|
||||
return {}
|
||||
var key_text := String(raw_key)
|
||||
if source_keys.has(key_text):
|
||||
status[0] = false
|
||||
return {}
|
||||
source_keys[key_text] = raw_key
|
||||
var sorted_keys: Array = source_keys.keys()
|
||||
sorted_keys.sort()
|
||||
var normalized: Dictionary = {}
|
||||
for key_text in sorted_keys:
|
||||
var raw_key = source_keys[key_text]
|
||||
normalized[key_text] = _normalize_serializable_value(value[raw_key], status)
|
||||
return normalized
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2txwx7jgo8rx
|
||||
Reference in New Issue
Block a user