feat: add emergent world foundations
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
class_name ActionCommand
|
||||
extends RefCounted
|
||||
|
||||
var _command_id: StringName
|
||||
var _actor_id: StringName
|
||||
var _action_id: StringName
|
||||
var _target: WorldTargetHandle
|
||||
var _parameters: Dictionary
|
||||
|
||||
|
||||
func _init(
|
||||
command_id: StringName = &"",
|
||||
actor_id: StringName = &"",
|
||||
action_id: StringName = &"",
|
||||
target: WorldTargetHandle = null,
|
||||
parameters: Dictionary = {}
|
||||
) -> void:
|
||||
_command_id = command_id
|
||||
_actor_id = actor_id
|
||||
_action_id = action_id
|
||||
_target = target
|
||||
_parameters = parameters.duplicate(true)
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return (
|
||||
not _command_id.is_empty()
|
||||
and not _actor_id.is_empty()
|
||||
and not _action_id.is_empty()
|
||||
and _target != null
|
||||
and _target.is_well_formed()
|
||||
and not WorldTargetCapability._contains_object(_parameters)
|
||||
)
|
||||
|
||||
|
||||
func get_command_id() -> StringName:
|
||||
return _command_id
|
||||
|
||||
|
||||
func get_actor_id() -> StringName:
|
||||
return _actor_id
|
||||
|
||||
|
||||
func get_action_id() -> StringName:
|
||||
return _action_id
|
||||
|
||||
|
||||
func get_target() -> WorldTargetHandle:
|
||||
return _target
|
||||
|
||||
|
||||
func get_parameters() -> Dictionary:
|
||||
return _parameters.duplicate(true)
|
||||
@@ -0,0 +1 @@
|
||||
uid://doxwel0w8eqx5
|
||||
@@ -0,0 +1,10 @@
|
||||
class_name ActionCommandService
|
||||
extends RefCounted
|
||||
|
||||
signal command_resolved(result: ActionResult)
|
||||
|
||||
|
||||
# Implementations must resolve the transient target handle and revalidate every
|
||||
# precondition. An earlier ActionOffer never grants authority by itself.
|
||||
func submit_command(_command: ActionCommand) -> ActionResult:
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfbj1brsbira5
|
||||
@@ -0,0 +1,74 @@
|
||||
class_name ActionOffer
|
||||
extends RefCounted
|
||||
|
||||
var _offer_id: StringName
|
||||
var _action_id: StringName
|
||||
var _target: WorldTargetHandle
|
||||
var _display_name: String
|
||||
var _enabled: bool
|
||||
var _rejection_reason: String
|
||||
var _parameters: Dictionary
|
||||
var _priority: float
|
||||
|
||||
|
||||
func _init(
|
||||
offer_id: StringName = &"",
|
||||
action_id: StringName = &"",
|
||||
target: WorldTargetHandle = null,
|
||||
display_name: String = "",
|
||||
enabled: bool = true,
|
||||
rejection_reason: String = "",
|
||||
parameters: Dictionary = {},
|
||||
priority: float = 0.0
|
||||
) -> void:
|
||||
_offer_id = offer_id
|
||||
_action_id = action_id
|
||||
_target = target
|
||||
_display_name = display_name
|
||||
_enabled = enabled
|
||||
_rejection_reason = rejection_reason
|
||||
_parameters = parameters.duplicate(true)
|
||||
_priority = priority
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return (
|
||||
not _offer_id.is_empty()
|
||||
and not _action_id.is_empty()
|
||||
and _target != null
|
||||
and _target.is_well_formed()
|
||||
and is_finite(_priority)
|
||||
and not WorldTargetCapability._contains_object(_parameters)
|
||||
)
|
||||
|
||||
|
||||
func get_offer_id() -> StringName:
|
||||
return _offer_id
|
||||
|
||||
|
||||
func get_action_id() -> StringName:
|
||||
return _action_id
|
||||
|
||||
|
||||
func get_target() -> WorldTargetHandle:
|
||||
return _target
|
||||
|
||||
|
||||
func get_display_name() -> String:
|
||||
return _display_name
|
||||
|
||||
|
||||
func is_enabled() -> bool:
|
||||
return _enabled
|
||||
|
||||
|
||||
func get_rejection_reason() -> String:
|
||||
return _rejection_reason
|
||||
|
||||
|
||||
func get_parameters() -> Dictionary:
|
||||
return _parameters.duplicate(true)
|
||||
|
||||
|
||||
func get_priority() -> float:
|
||||
return _priority
|
||||
@@ -0,0 +1 @@
|
||||
uid://c34gchiomlnpy
|
||||
@@ -0,0 +1,88 @@
|
||||
class_name ActionResult
|
||||
extends RefCounted
|
||||
|
||||
const STATUS_ACCEPTED := &"accepted"
|
||||
const STATUS_REJECTED := &"rejected"
|
||||
const STATUS_COMPLETED := &"completed"
|
||||
const STATUS_FAILED := &"failed"
|
||||
const VALID_STATUSES := [STATUS_ACCEPTED, STATUS_REJECTED, STATUS_COMPLETED, STATUS_FAILED]
|
||||
|
||||
var _command_id: StringName
|
||||
var _status: StringName
|
||||
var _reason_code: StringName
|
||||
var _message: String
|
||||
var _payload: Dictionary
|
||||
|
||||
|
||||
func _init(
|
||||
command_id: StringName = &"",
|
||||
status: StringName = &"",
|
||||
reason_code: StringName = &"",
|
||||
message: String = "",
|
||||
payload: Dictionary = {}
|
||||
) -> void:
|
||||
_command_id = command_id
|
||||
_status = status
|
||||
_reason_code = reason_code
|
||||
_message = message
|
||||
_payload = payload.duplicate(true)
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return (
|
||||
not _command_id.is_empty()
|
||||
and _status in VALID_STATUSES
|
||||
and not WorldTargetCapability._contains_object(_payload)
|
||||
)
|
||||
|
||||
|
||||
func is_accepted() -> bool:
|
||||
return _status in [STATUS_ACCEPTED, STATUS_COMPLETED]
|
||||
|
||||
|
||||
func is_terminal() -> bool:
|
||||
return _status in [STATUS_REJECTED, STATUS_COMPLETED, STATUS_FAILED]
|
||||
|
||||
|
||||
func did_succeed() -> bool:
|
||||
return _status == STATUS_COMPLETED
|
||||
|
||||
|
||||
func get_command_id() -> StringName:
|
||||
return _command_id
|
||||
|
||||
|
||||
func get_status() -> StringName:
|
||||
return _status
|
||||
|
||||
|
||||
func get_reason_code() -> StringName:
|
||||
return _reason_code
|
||||
|
||||
|
||||
func get_message() -> String:
|
||||
return _message
|
||||
|
||||
|
||||
func get_payload() -> Dictionary:
|
||||
return _payload.duplicate(true)
|
||||
|
||||
|
||||
static func accepted(command_id: StringName, payload: Dictionary = {}) -> ActionResult:
|
||||
return ActionResult.new(command_id, STATUS_ACCEPTED, &"", "", payload)
|
||||
|
||||
|
||||
static func rejected(
|
||||
command_id: StringName, reason_code: StringName, message: String = ""
|
||||
) -> ActionResult:
|
||||
return ActionResult.new(command_id, STATUS_REJECTED, reason_code, message)
|
||||
|
||||
|
||||
static func completed(command_id: StringName, payload: Dictionary = {}) -> ActionResult:
|
||||
return ActionResult.new(command_id, STATUS_COMPLETED, &"", "", payload)
|
||||
|
||||
|
||||
static func failed(
|
||||
command_id: StringName, reason_code: StringName, message: String = ""
|
||||
) -> ActionResult:
|
||||
return ActionResult.new(command_id, STATUS_FAILED, reason_code, message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bs85tiv8cs3gi
|
||||
@@ -0,0 +1,8 @@
|
||||
class_name InteractionService
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# Offers are advisory presentation values. Implementations must not mutate
|
||||
# authoritative state while answering this query.
|
||||
func get_action_offers(_actor_id: StringName, _target: WorldTargetHandle) -> Array[ActionOffer]:
|
||||
return []
|
||||
@@ -0,0 +1 @@
|
||||
uid://bgd28t7y4x3mf
|
||||
@@ -0,0 +1,340 @@
|
||||
class_name ContentCatalog
|
||||
extends RefCounted
|
||||
|
||||
const CORE_PACK_PATH := "res://simulation/definitions/packs/core.tres"
|
||||
|
||||
var _valid := false
|
||||
var _errors: Array[String] = []
|
||||
var _packs: Array[SimulationContentPack] = []
|
||||
var _actions: Array[ActionDefinition] = []
|
||||
var _professions: Array[ProfessionDefinition] = []
|
||||
var _items: Array[ItemDefinition] = []
|
||||
var _enemies: Array[EnemyDefinition] = []
|
||||
var _packs_by_id: Dictionary = {}
|
||||
var _actions_by_id: Dictionary = {}
|
||||
var _professions_by_id: Dictionary = {}
|
||||
var _items_by_id: Dictionary = {}
|
||||
var _enemies_by_id: Dictionary = {}
|
||||
|
||||
|
||||
static func create_core(supported_handler_ids: Array[StringName] = []) -> ContentCatalog:
|
||||
var catalog := ContentCatalog.new()
|
||||
var pack := load(CORE_PACK_PATH) as SimulationContentPack
|
||||
if pack == null:
|
||||
catalog._errors = ["Core content pack failed to load from '%s'" % CORE_PACK_PATH]
|
||||
return catalog
|
||||
var packs: Array[SimulationContentPack] = [pack]
|
||||
catalog.rebuild(packs, supported_handler_ids)
|
||||
return catalog
|
||||
|
||||
|
||||
func rebuild(
|
||||
content_packs: Array[SimulationContentPack], supported_handler_ids: Array[StringName] = []
|
||||
) -> Array[String]:
|
||||
_clear_published_content()
|
||||
var errors: Array[String] = []
|
||||
var pack_list := content_packs.duplicate()
|
||||
pack_list.sort_custom(_sort_packs)
|
||||
var supported_handlers := {}
|
||||
for handler_id in supported_handler_ids:
|
||||
if handler_id.is_empty():
|
||||
errors.append("Supported handler ID is empty")
|
||||
elif supported_handlers.has(handler_id):
|
||||
errors.append("Duplicate supported handler '%s'" % handler_id)
|
||||
else:
|
||||
supported_handlers[handler_id] = true
|
||||
|
||||
var packs_by_id := {}
|
||||
var actions_by_id := {}
|
||||
var professions_by_id := {}
|
||||
var items_by_id := {}
|
||||
var enemies_by_id := {}
|
||||
var actions: Array[ActionDefinition] = []
|
||||
var professions: Array[ProfessionDefinition] = []
|
||||
var items: Array[ItemDefinition] = []
|
||||
var enemies: Array[EnemyDefinition] = []
|
||||
|
||||
for pack in pack_list:
|
||||
if pack == null:
|
||||
errors.append("Content pack list contains a null pack")
|
||||
continue
|
||||
for error in pack.validate():
|
||||
errors.append("Pack '%s': %s" % [pack.pack_id, error])
|
||||
if packs_by_id.has(pack.pack_id):
|
||||
errors.append("Duplicate pack_id '%s'" % pack.pack_id)
|
||||
else:
|
||||
packs_by_id[pack.pack_id] = pack
|
||||
_collect_actions(pack, actions, actions_by_id, errors)
|
||||
_collect_professions(pack, professions, professions_by_id, errors)
|
||||
_collect_items(pack, items, items_by_id, errors)
|
||||
_collect_enemies(pack, enemies, enemies_by_id, errors)
|
||||
|
||||
for pack in pack_list:
|
||||
if pack == null:
|
||||
continue
|
||||
for required_pack_id in pack.required_pack_ids:
|
||||
if not required_pack_id.is_empty() and not packs_by_id.has(required_pack_id):
|
||||
errors.append(
|
||||
(
|
||||
"Pack '%s' references unknown required pack '%s'"
|
||||
% [pack.pack_id, required_pack_id]
|
||||
)
|
||||
)
|
||||
for required_handler_id in pack.required_handler_ids:
|
||||
if (
|
||||
not required_handler_id.is_empty()
|
||||
and not supported_handlers.has(required_handler_id)
|
||||
):
|
||||
errors.append(
|
||||
(
|
||||
"Pack '%s' references unknown required handler '%s'"
|
||||
% [pack.pack_id, required_handler_id]
|
||||
)
|
||||
)
|
||||
|
||||
_validate_action_references(actions, actions_by_id, professions_by_id, items_by_id, errors)
|
||||
_validate_enemy_references(enemies, items_by_id, errors)
|
||||
errors.sort()
|
||||
_errors = errors
|
||||
if not errors.is_empty():
|
||||
return get_errors()
|
||||
|
||||
actions.sort_custom(_sort_actions)
|
||||
professions.sort_custom(_sort_professions)
|
||||
items.sort_custom(_sort_items)
|
||||
enemies.sort_custom(_sort_enemies)
|
||||
_packs = pack_list
|
||||
_actions = actions
|
||||
_professions = professions
|
||||
_items = items
|
||||
_enemies = enemies
|
||||
_packs_by_id = packs_by_id
|
||||
_actions_by_id = actions_by_id
|
||||
_professions_by_id = professions_by_id
|
||||
_items_by_id = items_by_id
|
||||
_enemies_by_id = enemies_by_id
|
||||
_valid = true
|
||||
return []
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return _valid
|
||||
|
||||
|
||||
func get_errors() -> Array[String]:
|
||||
return _errors.duplicate()
|
||||
|
||||
|
||||
func get_packs() -> Array[SimulationContentPack]:
|
||||
return _packs.duplicate()
|
||||
|
||||
|
||||
func get_actions() -> Array[ActionDefinition]:
|
||||
return _actions.duplicate()
|
||||
|
||||
|
||||
func get_professions() -> Array[ProfessionDefinition]:
|
||||
return _professions.duplicate()
|
||||
|
||||
|
||||
func get_items() -> Array[ItemDefinition]:
|
||||
return _items.duplicate()
|
||||
|
||||
|
||||
func get_enemies() -> Array[EnemyDefinition]:
|
||||
return _enemies.duplicate()
|
||||
|
||||
|
||||
func get_pack(pack_id: StringName) -> SimulationContentPack:
|
||||
return _packs_by_id.get(pack_id) as SimulationContentPack
|
||||
|
||||
|
||||
func get_action(action_id: StringName) -> ActionDefinition:
|
||||
return _actions_by_id.get(action_id) as ActionDefinition
|
||||
|
||||
|
||||
func get_profession(profession_id: StringName) -> ProfessionDefinition:
|
||||
return _professions_by_id.get(profession_id) as ProfessionDefinition
|
||||
|
||||
|
||||
func get_item(item_id: StringName) -> ItemDefinition:
|
||||
return _items_by_id.get(item_id) as ItemDefinition
|
||||
|
||||
|
||||
func get_enemy(enemy_id: StringName) -> EnemyDefinition:
|
||||
return _enemies_by_id.get(enemy_id) as EnemyDefinition
|
||||
|
||||
|
||||
func _clear_published_content() -> void:
|
||||
_valid = false
|
||||
_errors.clear()
|
||||
_packs.clear()
|
||||
_actions.clear()
|
||||
_professions.clear()
|
||||
_items.clear()
|
||||
_enemies.clear()
|
||||
_packs_by_id.clear()
|
||||
_actions_by_id.clear()
|
||||
_professions_by_id.clear()
|
||||
_items_by_id.clear()
|
||||
_enemies_by_id.clear()
|
||||
|
||||
|
||||
static func _collect_actions(
|
||||
pack: SimulationContentPack,
|
||||
definitions: Array[ActionDefinition],
|
||||
definitions_by_id: Dictionary,
|
||||
errors: Array[String]
|
||||
) -> void:
|
||||
for definition in pack.actions:
|
||||
if definition == null:
|
||||
continue
|
||||
if definitions_by_id.has(definition.action_id):
|
||||
errors.append("Duplicate action_id '%s'" % definition.action_id)
|
||||
continue
|
||||
definitions_by_id[definition.action_id] = definition
|
||||
definitions.append(definition)
|
||||
|
||||
|
||||
static func _collect_professions(
|
||||
pack: SimulationContentPack,
|
||||
definitions: Array[ProfessionDefinition],
|
||||
definitions_by_id: Dictionary,
|
||||
errors: Array[String]
|
||||
) -> void:
|
||||
for definition in pack.professions:
|
||||
if definition == null:
|
||||
continue
|
||||
if definitions_by_id.has(definition.profession_id):
|
||||
errors.append("Duplicate profession_id '%s'" % definition.profession_id)
|
||||
continue
|
||||
definitions_by_id[definition.profession_id] = definition
|
||||
definitions.append(definition)
|
||||
|
||||
|
||||
static func _collect_items(
|
||||
pack: SimulationContentPack,
|
||||
definitions: Array[ItemDefinition],
|
||||
definitions_by_id: Dictionary,
|
||||
errors: Array[String]
|
||||
) -> void:
|
||||
for definition in pack.items:
|
||||
if definition == null:
|
||||
continue
|
||||
if definitions_by_id.has(definition.item_id):
|
||||
errors.append("Duplicate item_id '%s'" % definition.item_id)
|
||||
continue
|
||||
definitions_by_id[definition.item_id] = definition
|
||||
definitions.append(definition)
|
||||
|
||||
|
||||
static func _collect_enemies(
|
||||
pack: SimulationContentPack,
|
||||
definitions: Array[EnemyDefinition],
|
||||
definitions_by_id: Dictionary,
|
||||
errors: Array[String]
|
||||
) -> void:
|
||||
for definition in pack.enemies:
|
||||
if definition == null:
|
||||
continue
|
||||
if definitions_by_id.has(definition.enemy_id):
|
||||
errors.append("Duplicate enemy_id '%s'" % definition.enemy_id)
|
||||
continue
|
||||
definitions_by_id[definition.enemy_id] = definition
|
||||
definitions.append(definition)
|
||||
|
||||
|
||||
static func _validate_action_references(
|
||||
actions: Array[ActionDefinition],
|
||||
actions_by_id: Dictionary,
|
||||
professions_by_id: Dictionary,
|
||||
items_by_id: Dictionary,
|
||||
errors: Array[String]
|
||||
) -> void:
|
||||
for definition in actions:
|
||||
if (
|
||||
not definition.preferred_profession_id.is_empty()
|
||||
and not professions_by_id.has(definition.preferred_profession_id)
|
||||
):
|
||||
errors.append(
|
||||
(
|
||||
"Action '%s' references unknown profession '%s'"
|
||||
% [definition.action_id, definition.preferred_profession_id]
|
||||
)
|
||||
)
|
||||
if (
|
||||
definition.target_type == SimulationIds.TARGET_RESOURCE
|
||||
and not actions_by_id.has(definition.resource_action_id)
|
||||
):
|
||||
errors.append(
|
||||
(
|
||||
"Action '%s' references unknown resource action '%s'"
|
||||
% [definition.action_id, definition.resource_action_id]
|
||||
)
|
||||
)
|
||||
if (
|
||||
not definition.completion_cost_resource_id.is_empty()
|
||||
and not items_by_id.has(definition.completion_cost_resource_id)
|
||||
):
|
||||
errors.append(
|
||||
(
|
||||
"Action '%s' references unknown completion cost item '%s'"
|
||||
% [definition.action_id, definition.completion_cost_resource_id]
|
||||
)
|
||||
)
|
||||
elif not definition.completion_cost_resource_id.is_empty():
|
||||
var cost_item := (
|
||||
items_by_id.get(definition.completion_cost_resource_id) as ItemDefinition
|
||||
)
|
||||
if cost_item != null and not cost_item.is_resource():
|
||||
errors.append(
|
||||
(
|
||||
"Action '%s' references non-resource completion cost item '%s'"
|
||||
% [definition.action_id, definition.completion_cost_resource_id]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func _validate_enemy_references(
|
||||
enemies: Array[EnemyDefinition], items_by_id: Dictionary, errors: Array[String]
|
||||
) -> void:
|
||||
for definition in enemies:
|
||||
var weapon := items_by_id.get(definition.weapon_id) as ItemDefinition
|
||||
if weapon == null:
|
||||
errors.append(
|
||||
(
|
||||
"Enemy '%s' references unknown weapon '%s'"
|
||||
% [definition.enemy_id, definition.weapon_id]
|
||||
)
|
||||
)
|
||||
elif not weapon.is_weapon():
|
||||
errors.append(
|
||||
(
|
||||
"Enemy '%s' references non-weapon item '%s'"
|
||||
% [definition.enemy_id, definition.weapon_id]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func _sort_packs(first: SimulationContentPack, second: SimulationContentPack) -> bool:
|
||||
if first == null:
|
||||
return second != null
|
||||
if second == null:
|
||||
return false
|
||||
return String(first.pack_id) < String(second.pack_id)
|
||||
|
||||
|
||||
static func _sort_actions(first: ActionDefinition, second: ActionDefinition) -> bool:
|
||||
return String(first.action_id) < String(second.action_id)
|
||||
|
||||
|
||||
static func _sort_professions(first: ProfessionDefinition, second: ProfessionDefinition) -> bool:
|
||||
return String(first.profession_id) < String(second.profession_id)
|
||||
|
||||
|
||||
static func _sort_items(first: ItemDefinition, second: ItemDefinition) -> bool:
|
||||
return String(first.item_id) < String(second.item_id)
|
||||
|
||||
|
||||
static func _sort_enemies(first: EnemyDefinition, second: EnemyDefinition) -> bool:
|
||||
return String(first.enemy_id) < String(second.enemy_id)
|
||||
@@ -0,0 +1 @@
|
||||
uid://csvigy66784n
|
||||
@@ -1,13 +1,17 @@
|
||||
class_name ItemDefinition
|
||||
extends Resource
|
||||
|
||||
const CATEGORY_RESOURCE := &"resource"
|
||||
const CATEGORY_WEAPON := SimulationIds.ITEM_CATEGORY_WEAPON
|
||||
const VALID_CATEGORIES := [CATEGORY_RESOURCE, CATEGORY_WEAPON]
|
||||
|
||||
@export var item_id: StringName
|
||||
@export var display_name: String
|
||||
@export var category: StringName = SimulationIds.ITEM_CATEGORY_WEAPON
|
||||
@export var equip_slot: StringName = SimulationIds.EQUIP_SLOT_HAND
|
||||
@export var category: StringName = CATEGORY_RESOURCE
|
||||
@export var equip_slot: StringName
|
||||
@export_range(0.0, 1000.0, 0.1) var damage := 0.0
|
||||
@export_range(0.0, 50.0, 0.1) var reach := 1.5
|
||||
@export_range(0.0, 5.0, 0.05) var attack_cooldown := 0.6
|
||||
@export_range(0.0, 50.0, 0.1) var reach := 0.0
|
||||
@export_range(0.0, 5.0, 0.05) var attack_cooldown := 0.0
|
||||
|
||||
|
||||
func validate() -> Array[String]:
|
||||
@@ -16,10 +20,27 @@ func validate() -> Array[String]:
|
||||
errors.append("item_id is empty")
|
||||
if display_name.is_empty():
|
||||
errors.append("display_name is empty for '%s'" % item_id)
|
||||
if damage <= 0.0:
|
||||
errors.append("damage must be positive for '%s'" % item_id)
|
||||
if category not in VALID_CATEGORIES:
|
||||
errors.append("category '%s' is invalid for '%s'" % [category, item_id])
|
||||
elif category == CATEGORY_WEAPON:
|
||||
if equip_slot != SimulationIds.EQUIP_SLOT_HAND:
|
||||
errors.append(
|
||||
"weapon '%s' must equip in '%s'" % [item_id, SimulationIds.EQUIP_SLOT_HAND]
|
||||
)
|
||||
if not is_finite(damage) or damage <= 0.0:
|
||||
errors.append("damage must be positive for '%s'" % item_id)
|
||||
if not is_finite(reach) or reach <= 0.0:
|
||||
errors.append("reach must be positive for '%s'" % item_id)
|
||||
if not is_finite(attack_cooldown) or attack_cooldown <= 0.0:
|
||||
errors.append("attack_cooldown must be positive for '%s'" % item_id)
|
||||
elif not equip_slot.is_empty():
|
||||
errors.append("resource item '%s' cannot declare equip_slot '%s'" % [item_id, equip_slot])
|
||||
return errors
|
||||
|
||||
|
||||
func is_weapon() -> bool:
|
||||
return category == SimulationIds.ITEM_CATEGORY_WEAPON
|
||||
return category == CATEGORY_WEAPON
|
||||
|
||||
|
||||
func is_resource() -> bool:
|
||||
return category == CATEGORY_RESOURCE
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
class_name SimulationContentPack
|
||||
extends Resource
|
||||
|
||||
@export var pack_id: StringName
|
||||
@export var display_name: String
|
||||
@export var required_pack_ids: Array[StringName] = []
|
||||
@export var required_handler_ids: Array[StringName] = []
|
||||
@export var actions: Array[ActionDefinition] = []
|
||||
@export var professions: Array[ProfessionDefinition] = []
|
||||
@export var items: Array[ItemDefinition] = []
|
||||
@export var enemies: Array[EnemyDefinition] = []
|
||||
|
||||
|
||||
func validate() -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
if pack_id.is_empty():
|
||||
errors.append("pack_id is empty")
|
||||
if display_name.is_empty():
|
||||
errors.append("display_name is empty for pack '%s'" % pack_id)
|
||||
_validate_dependency_ids(required_pack_ids, "required pack", errors)
|
||||
_validate_dependency_ids(required_handler_ids, "required handler", errors)
|
||||
if pack_id in required_pack_ids:
|
||||
errors.append("pack '%s' cannot require itself" % pack_id)
|
||||
_validate_actions(errors)
|
||||
_validate_professions(errors)
|
||||
_validate_items(errors)
|
||||
_validate_enemies(errors)
|
||||
return errors
|
||||
|
||||
|
||||
func _validate_actions(errors: Array[String]) -> void:
|
||||
for definition in actions:
|
||||
if definition == null:
|
||||
errors.append("actions contains a null definition")
|
||||
continue
|
||||
for error in definition.validate():
|
||||
errors.append("ActionDefinition: " + error)
|
||||
|
||||
|
||||
func _validate_professions(errors: Array[String]) -> void:
|
||||
for definition in professions:
|
||||
if definition == null:
|
||||
errors.append("professions contains a null definition")
|
||||
continue
|
||||
for error in definition.validate():
|
||||
errors.append("ProfessionDefinition: " + error)
|
||||
|
||||
|
||||
func _validate_items(errors: Array[String]) -> void:
|
||||
for definition in items:
|
||||
if definition == null:
|
||||
errors.append("items contains a null definition")
|
||||
continue
|
||||
for error in definition.validate():
|
||||
errors.append("ItemDefinition: " + error)
|
||||
|
||||
|
||||
func _validate_enemies(errors: Array[String]) -> void:
|
||||
for definition in enemies:
|
||||
if definition == null:
|
||||
errors.append("enemies contains a null definition")
|
||||
continue
|
||||
for error in definition.validate():
|
||||
errors.append("EnemyDefinition: " + error)
|
||||
|
||||
|
||||
static func _validate_dependency_ids(
|
||||
ids: Array[StringName], label: String, errors: Array[String]
|
||||
) -> void:
|
||||
var seen := {}
|
||||
for dependency_id in ids:
|
||||
if dependency_id.is_empty():
|
||||
errors.append("%s ID is empty" % label)
|
||||
continue
|
||||
if seen.has(dependency_id):
|
||||
errors.append("duplicate %s '%s'" % [label, dependency_id])
|
||||
continue
|
||||
seen[dependency_id] = true
|
||||
@@ -0,0 +1 @@
|
||||
uid://j65xpgmfakin
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_resource type="Resource" script_class="EnemyDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/EnemyDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
enemy_id = &"enemy_raider"
|
||||
display_name = "Raider"
|
||||
kind = &"raider"
|
||||
weapon_id = &"item_sword"
|
||||
health = 60.0
|
||||
move_speed = 3.5
|
||||
body_color = Color(0.4, 0.13, 0.1, 1)
|
||||
accent_color = Color(0.55, 0.55, 0.58, 1)
|
||||
@@ -0,0 +1,15 @@
|
||||
[gd_resource type="Resource" script_class="EnemyDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/EnemyDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
enemy_id = &"enemy_wolf"
|
||||
display_name = "Wolf"
|
||||
kind = &"wolf"
|
||||
weapon_id = &"item_claw"
|
||||
health = 40.0
|
||||
move_speed = 4.2
|
||||
body_color = Color(0.32, 0.32, 0.34, 1)
|
||||
accent_color = Color(0.95, 0.5, 0.12, 1)
|
||||
visual_scale = Vector3(0.8, 0.8, 0.8)
|
||||
@@ -0,0 +1,13 @@
|
||||
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
item_id = &"item_claw"
|
||||
display_name = "Claw"
|
||||
category = &"weapon"
|
||||
equip_slot = &"hand"
|
||||
damage = 10.0
|
||||
reach = 1.2
|
||||
attack_cooldown = 0.4
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
item_id = &"food"
|
||||
display_name = "Food"
|
||||
category = &"resource"
|
||||
@@ -0,0 +1,13 @@
|
||||
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
item_id = &"item_sword"
|
||||
display_name = "Sword"
|
||||
category = &"weapon"
|
||||
equip_slot = &"hand"
|
||||
damage = 24.0
|
||||
reach = 2.4
|
||||
attack_cooldown = 0.55
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
item_id = &"wood"
|
||||
display_name = "Wood"
|
||||
category = &"resource"
|
||||
@@ -0,0 +1,36 @@
|
||||
[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=27 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/SimulationContentPack.gd" id="1_pack"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/defend.tres" id="2_defend"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/deposit_food.tres" id="3_deposit_food"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/deposit_wood.tres" id="4_deposit_wood"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/eat.tres" id="5_eat"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/feed_animal.tres" id="6_feed_animal"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/gather_food.tres" id="7_gather_food"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/gather_wood.tres" id="8_gather_wood"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/patrol.tres" id="9_patrol"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/rest.tres" id="10_rest"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/sleep.tres" id="11_sleep"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/study.tres" id="12_study"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/wander.tres" id="13_wander"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/withdraw_food.tres" id="14_withdraw_food"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/professions/farmer.tres" id="15_farmer"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/professions/guard.tres" id="16_guard"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/professions/scholar.tres" id="17_scholar"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/professions/wanderer.tres" id="18_wanderer"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/professions/woodcutter.tres" id="19_woodcutter"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/items/claw.tres" id="20_claw"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/items/food.tres" id="21_food"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/items/sword.tres" id="22_sword"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/items/wood.tres" id="23_wood"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/enemies/raider.tres" id="24_raider"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/enemies/wolf.tres" id="25_wolf"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pack")
|
||||
pack_id = &"core"
|
||||
display_name = "The Steward Core"
|
||||
actions = [ExtResource("2_defend"), ExtResource("3_deposit_food"), ExtResource("4_deposit_wood"), ExtResource("5_eat"), ExtResource("6_feed_animal"), ExtResource("7_gather_food"), ExtResource("8_gather_wood"), ExtResource("9_patrol"), ExtResource("10_rest"), ExtResource("11_sleep"), ExtResource("12_study"), ExtResource("13_wander"), ExtResource("14_withdraw_food")]
|
||||
professions = [ExtResource("15_farmer"), ExtResource("16_guard"), ExtResource("17_scholar"), ExtResource("18_wanderer"), ExtResource("19_woodcutter")]
|
||||
items = [ExtResource("20_claw"), ExtResource("21_food"), ExtResource("22_sword"), ExtResource("23_wood")]
|
||||
enemies = [ExtResource("24_raider"), ExtResource("25_wolf")]
|
||||
@@ -0,0 +1,204 @@
|
||||
class_name WorldEventStore
|
||||
extends RefCounted
|
||||
|
||||
signal event_recorded(event: WorldEventRecord)
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
|
||||
var _events_by_id: Dictionary = {}
|
||||
var _ordered_event_ids: Array[int] = []
|
||||
var _event_type_index: Dictionary = {}
|
||||
var _participant_index: Dictionary = {}
|
||||
var _location_index: Dictionary = {}
|
||||
var _tick_index: Dictionary = {}
|
||||
var _ordered_ticks: Array[int] = []
|
||||
|
||||
|
||||
static func from_dictionary(store_data: Dictionary) -> WorldEventStore:
|
||||
var store := WorldEventStore.new()
|
||||
return store if store.restore_from_dictionary(store_data) else null
|
||||
|
||||
|
||||
func size() -> int:
|
||||
return _events_by_id.size()
|
||||
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _events_by_id.is_empty()
|
||||
|
||||
|
||||
func has_event(event_id: int) -> bool:
|
||||
return _events_by_id.has(event_id)
|
||||
|
||||
|
||||
func append(event: WorldEventRecord) -> bool:
|
||||
return _append(event, true)
|
||||
|
||||
|
||||
func append_economic(
|
||||
economic_event: EconomicEventRecord, world_id: StringName = &"", location_id: StringName = &""
|
||||
) -> WorldEventRecord:
|
||||
var world_event := WorldEventRecord.from_economic_event(economic_event, world_id, location_id)
|
||||
if world_event == null or not append(world_event):
|
||||
return null
|
||||
return get_by_id(world_event.get_event_id())
|
||||
|
||||
|
||||
func get_by_id(event_id: int) -> WorldEventRecord:
|
||||
return _events_by_id.get(event_id) as WorldEventRecord
|
||||
|
||||
|
||||
func get_by_ids(event_ids: Array[int]) -> Array[WorldEventRecord]:
|
||||
var results: Array[WorldEventRecord] = []
|
||||
for event_id in event_ids:
|
||||
var event := get_by_id(event_id)
|
||||
if event != null:
|
||||
results.append(event)
|
||||
return results
|
||||
|
||||
|
||||
func get_all() -> Array[WorldEventRecord]:
|
||||
return _records_for_ids(_ordered_event_ids)
|
||||
|
||||
|
||||
func get_for_type(event_type: StringName) -> Array[WorldEventRecord]:
|
||||
return _records_for_ids(_event_type_index.get(event_type, []))
|
||||
|
||||
|
||||
func get_for_participant(participant: WorldEntityRef) -> Array[WorldEventRecord]:
|
||||
if participant == null or not participant.is_valid():
|
||||
return []
|
||||
return _records_for_ids(_participant_index.get(participant.index_key(), []))
|
||||
|
||||
|
||||
func get_for_location(location: SpatialAddress) -> Array[WorldEventRecord]:
|
||||
if location == null or not location.is_valid():
|
||||
return []
|
||||
return _records_for_ids(_location_index.get(location.index_key(), []))
|
||||
|
||||
|
||||
func get_for_tick(tick: int) -> Array[WorldEventRecord]:
|
||||
return _records_for_ids(_tick_index.get(tick, []))
|
||||
|
||||
|
||||
func get_between_ticks(first_tick: int, last_tick: int) -> Array[WorldEventRecord]:
|
||||
var results: Array[WorldEventRecord] = []
|
||||
if first_tick < 0 or last_tick < first_tick:
|
||||
return results
|
||||
for tick in _ordered_ticks:
|
||||
if tick < first_tick:
|
||||
continue
|
||||
if tick > last_tick:
|
||||
break
|
||||
results.append_array(_records_for_ids(_tick_index[tick]))
|
||||
return results
|
||||
|
||||
|
||||
func restore(restored_events: Array[WorldEventRecord]) -> bool:
|
||||
var candidate := WorldEventStore.new()
|
||||
for event in restored_events:
|
||||
if not candidate._append(event, false):
|
||||
return false
|
||||
_adopt_indexes(candidate)
|
||||
return true
|
||||
|
||||
|
||||
func restore_from_dictionary(store_data: Dictionary) -> bool:
|
||||
if int(store_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return false
|
||||
var saved_events = store_data.get("events")
|
||||
if not saved_events is Array:
|
||||
return false
|
||||
var restored_events: Array[WorldEventRecord] = []
|
||||
for saved_event in saved_events:
|
||||
if not saved_event is Dictionary:
|
||||
return false
|
||||
var event := WorldEventRecord.from_dictionary(saved_event)
|
||||
if event == null:
|
||||
return false
|
||||
restored_events.append(event)
|
||||
return restore(restored_events)
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
var serialized_events: Array[Dictionary] = []
|
||||
for event in get_all():
|
||||
serialized_events.append(event.to_dictionary())
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"events": serialized_events,
|
||||
}
|
||||
|
||||
|
||||
func _append(event: WorldEventRecord, emit_signal: bool) -> bool:
|
||||
if event == null or not event.is_valid() or _events_by_id.has(event.get_event_id()):
|
||||
return false
|
||||
var stored_event := WorldEventRecord.from_dictionary(event.to_dictionary())
|
||||
if stored_event == null:
|
||||
return false
|
||||
var event_id := stored_event.get_event_id()
|
||||
_events_by_id[event_id] = stored_event
|
||||
_insert_event_id(_ordered_event_ids, event_id)
|
||||
_index_event_id(_event_type_index, stored_event.get_event_type(), event_id)
|
||||
for participant_key in stored_event.get_participant_index_keys():
|
||||
_index_event_id(_participant_index, participant_key, event_id)
|
||||
if stored_event.has_location():
|
||||
_index_event_id(_location_index, stored_event.get_location().index_key(), event_id)
|
||||
_index_event_id(_tick_index, stored_event.get_tick(), event_id)
|
||||
_insert_tick(stored_event.get_tick())
|
||||
if emit_signal:
|
||||
event_recorded.emit(stored_event)
|
||||
return true
|
||||
|
||||
|
||||
func _index_event_id(index: Dictionary, key: Variant, event_id: int) -> void:
|
||||
if not index.has(key):
|
||||
index[key] = []
|
||||
var indexed_ids: Array = index[key]
|
||||
_insert_event_id(indexed_ids, event_id)
|
||||
|
||||
|
||||
func _insert_event_id(event_ids: Array, event_id: int) -> void:
|
||||
var low := 0
|
||||
var high := event_ids.size()
|
||||
while low < high:
|
||||
var middle := (low + high) / 2
|
||||
if _event_precedes(event_id, int(event_ids[middle])):
|
||||
high = middle
|
||||
else:
|
||||
low = middle + 1
|
||||
event_ids.insert(low, event_id)
|
||||
|
||||
|
||||
func _event_precedes(left_id: int, right_id: int) -> bool:
|
||||
var left := get_by_id(left_id)
|
||||
var right := get_by_id(right_id)
|
||||
if left.get_tick() != right.get_tick():
|
||||
return left.get_tick() < right.get_tick()
|
||||
return left_id < right_id
|
||||
|
||||
|
||||
func _insert_tick(tick: int) -> void:
|
||||
var insert_at := _ordered_ticks.bsearch(tick)
|
||||
if insert_at < _ordered_ticks.size() and _ordered_ticks[insert_at] == tick:
|
||||
return
|
||||
_ordered_ticks.insert(insert_at, tick)
|
||||
|
||||
|
||||
func _records_for_ids(event_ids: Array) -> Array[WorldEventRecord]:
|
||||
var results: Array[WorldEventRecord] = []
|
||||
for event_id in event_ids:
|
||||
var event := get_by_id(int(event_id))
|
||||
if event != null:
|
||||
results.append(event)
|
||||
return results
|
||||
|
||||
|
||||
func _adopt_indexes(source: WorldEventStore) -> void:
|
||||
_events_by_id = source._events_by_id
|
||||
_ordered_event_ids = source._ordered_event_ids
|
||||
_event_type_index = source._event_type_index
|
||||
_participant_index = source._participant_index
|
||||
_location_index = source._location_index
|
||||
_tick_index = source._tick_index
|
||||
_ordered_ticks = source._ordered_ticks
|
||||
@@ -0,0 +1 @@
|
||||
uid://dss5liteqydyd
|
||||
@@ -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