class_name EventService extends RefCounted ## Deterministic event candidate queue, foreground actor locking and transactional ## choice authority. Narrative text is deliberately outside this service; only ## localization keys, content ratings/tags and safe replacement IDs are retained. signal effect_transaction_started signal effect_transaction_finished signal event_registered(definition: EventDefinition) signal event_enqueued(instance: EventInstance) signal event_started(instance: EventInstance) signal event_finished(instance: EventInstance) signal event_cancelled(instance: EventInstance, reason: StringName) signal choice_resolved(instance_id: StringName, choice_id: StringName, result: Dictionary) const EventDefinitionType = preload("res://scripts/events/event_definition.gd") const EventInstanceType = preload("res://scripts/events/event_instance.gd") const EventActionType = preload("res://scripts/events/event_action.gd") const CommandResultType = preload("res://scripts/core/command_result.gd") const SNAPSHOT_VERSION := 1 const MAX_UNINTERRUPTIBLE_EXTENSION := 60 var last_error := "" var pause_requested := false var last_recovery_repair_report: Dictionary = {} var _definitions: Dictionary = {} var _instances: Dictionary = {} var _queue: Array[StringName] = [] var _actor_locks: Dictionary = {} var _foreground_instance_id: StringName = &"" var _active_deduplication_keys: Dictionary = {} var _processed_choices: Dictionary = {} var _completed_scope_keys: Dictionary = {} var _flags: Dictionary = {} var _next_event_sequence := 1 var _next_choice_sequence := 1 var _filter_mode: EventDefinition.FilterBehavior = EventDefinition.FilterBehavior.NONE var _maximum_content_rating: EventDefinition.ContentRating = EventDefinition.ContentRating.GENERAL var _disabled_content_tags: Array[StringName] = [] var _actor_exists := Callable() var _action_executor := Callable() var _cost_reserver := Callable() var _cost_releaser := Callable() var _cost_committer := Callable() var _condition_callbacks: Dictionary = {} var _relationship_service: Variant = null var _candidate_eligibility_guard := Callable() func configure_callbacks( actor_exists: Callable = Callable(), action_executor: Callable = Callable(), cost_reserver: Callable = Callable(), cost_releaser: Callable = Callable(), cost_committer: Callable = Callable(), entity_exists: Callable = Callable(), path_exists: Callable = Callable() ) -> EventService: _actor_exists = actor_exists _action_executor = action_executor _cost_reserver = cost_reserver _cost_releaser = cost_releaser _cost_committer = cost_committer _condition_callbacks = { &"entity_exists": entity_exists, &"path_exists": path_exists, } return self ## A domain-owned, read-only guard runs before queue mutation and again before ## a queued event acquires actors or executes actions. It is rebuilt on restore. func configure_candidate_eligibility_guard(guard: Callable = Callable()) -> void: _candidate_eligibility_guard = guard func _candidate_permitted(definition: EventDefinition, context: Dictionary) -> bool: if not _candidate_eligibility_guard.is_valid(): return true var result: Variant = _candidate_eligibility_guard.call(definition, context.duplicate(true)) return typeof(result) == TYPE_BOOL and result func configure_relationship_service(service: Variant) -> EventService: _relationship_service = service return self func configure_content_filter( mode: EventDefinition.FilterBehavior, maximum_rating: EventDefinition.ContentRating = EventDefinition.ContentRating.GENERAL, disabled_tags: Array = [] ) -> EventService: _filter_mode = mode _maximum_content_rating = maximum_rating _disabled_content_tags = _sorted_unique_names(disabled_tags) return self ## A small, immutable boundary used by load safety. Callers must not need ## access to EventService's internal filter fields in order to preserve a ## stricter locally configured profile across snapshot restoration. func get_content_filter_profile() -> Dictionary: return { &"filter_mode": _filter_mode, &"filter_mode_name": EventDefinitionType.FilterBehavior.keys()[_filter_mode], &"maximum_content_rating": _maximum_content_rating, &"maximum_content_rating_name": EventDefinitionType.ContentRating.keys()[_maximum_content_rating], &"disabled_content_tags": _disabled_content_tags.duplicate(), } func register_definition(value: Variant) -> CommandResult: if value == null or not value is EventDefinition: return _invalid("EventDefinition is missing or invalid.") var definition: EventDefinition = value if not definition.is_valid(): return _invalid("EventDefinition fails schema validation.") if _definitions.has(definition.event_id): return _invalid("Duplicate EventDefinition: %s" % definition.event_id) _definitions[definition.event_id] = definition event_registered.emit(definition) last_error = "" return _success({&"event_id": definition.event_id}) func create_definition(event_id: StringName, options: Dictionary = {}) -> CommandResult: return register_definition(EventDefinitionType.new().configure(event_id, options)) func get_definition(event_id: StringName) -> EventDefinition: return _definitions.get(event_id) as EventDefinition func get_definitions() -> Array[EventDefinition]: var result: Array[EventDefinition] = [] for event_id: StringName in _sorted_name_keys(_definitions): result.append(_definitions[event_id]) return result func validate_definitions() -> Dictionary: var errors: Array[String] = [] for definition: EventDefinition in get_definitions(): if not definition.is_valid(): errors.append("Invalid definition: %s" % definition.event_id) if definition.filter_behavior == EventDefinitionType.FilterBehavior.REPLACE: if not _definitions.has(definition.replacement_event_id): errors.append("Replacement missing: %s -> %s" % [definition.event_id, definition.replacement_event_id]) elif _replacement_cycle_exists(definition.event_id): errors.append("Replacement cycle: %s" % definition.event_id) return {&"ok": errors.is_empty(), &"errors": errors} func resolve_content_event(event_id: StringName) -> Dictionary: var source := get_definition(event_id) if source == null: return {&"enabled": false, &"reason": &"UNKNOWN_EVENT"} if _filter_mode == EventDefinitionType.FilterBehavior.NONE or not _is_content_restricted(source): return { &"enabled": true, &"event_id": event_id, &"presentation_mode": &"FULL", } var behavior := source.filter_behavior if behavior == EventDefinitionType.FilterBehavior.NONE: behavior = _filter_mode match behavior: EventDefinitionType.FilterBehavior.FADE: return { &"enabled": true, &"event_id": event_id, &"presentation_mode": &"FADE", } EventDefinitionType.FilterBehavior.REPLACE: var replacement := get_definition(source.replacement_event_id) if replacement == null or replacement.event_id == source.event_id: return {&"enabled": false, &"reason": &"REPLACEMENT_MISSING"} if _is_content_restricted(replacement): return {&"enabled": false, &"reason": &"REPLACEMENT_UNSAFE"} return { &"enabled": true, &"event_id": replacement.event_id, &"presentation_mode": &"REPLACED", } EventDefinitionType.FilterBehavior.DISABLE: return {&"enabled": false, &"reason": &"CONTENT_DISABLED"} return {&"enabled": false, &"reason": &"CONTENT_DISABLED"} func enqueue_candidate( event_id: StringName, actor_ids: Array, absolute_minute: int, options: Dictionary = {} ) -> CommandResult: var source := get_definition(event_id) if source == null: return _not_found("EventDefinition", event_id) if absolute_minute < 0: return _invalid("Event candidate requires a non-negative minute.") var content_resolution := resolve_content_event(event_id) if not bool(content_resolution.get(&"enabled", false)): return _invalid("Event candidate was disabled by Content Filter.", content_resolution) var resolved_event_id := StringName(content_resolution[&"event_id"]) var definition := get_definition(resolved_event_id) if definition == null: return _invalid("Resolved EventDefinition is unavailable.") var normalized_actor_ids := _sorted_unique_names(actor_ids) var deduplication_key := StringName(_value( options, &"deduplication_key", _default_deduplication_key(event_id, normalized_actor_ids) )) if not deduplication_key.is_empty() and _active_deduplication_keys.has(deduplication_key): return _invalid("Duplicate active Event candidate.", { &"existing_instance_id": _active_deduplication_keys[deduplication_key], }) var scope_key := _scope_key(source, normalized_actor_ids, absolute_minute) if not scope_key.is_empty() and _completed_scope_keys.has(scope_key): return _invalid("Event once-scope has already completed.", {&"scope_key": scope_key}) var instance_id := StringName(_value(options, &"event_instance_id", &"")) var generated_id := instance_id.is_empty() if generated_id: instance_id = StringName("event_instance_%08d" % _next_event_sequence) if _instances.has(instance_id): return _invalid("Duplicate Event instance ID: %s" % instance_id) var expires_at := int(_value(options, &"expires_at", -1)) if expires_at < 0 and definition.expires_after > 0: expires_at = absolute_minute + definition.expires_after if expires_at >= 0 and expires_at < absolute_minute: return _invalid("Event expiry cannot precede creation.") var context := _dictionary(_value(options, &"context", {})).duplicate(true) context[&"absolute_minute"] = absolute_minute context[&"actor_ids"] = normalized_actor_ids.duplicate() if not _candidate_permitted(source, context) or not _candidate_permitted(definition, context): return _invalid("Event candidate is prevented by placement protection.", {&"reason": &"PLACEMENT_PROTECTION"}) var instance := EventInstanceType.new().configure( instance_id, event_id, resolved_event_id, normalized_actor_ids, definition.priority, absolute_minute, expires_at, definition.foreground, deduplication_key, context, StringName(content_resolution.get(&"presentation_mode", &"FULL")) ) _instances[instance_id] = instance _queue.append(instance_id) if not deduplication_key.is_empty(): _active_deduplication_keys[deduplication_key] = instance_id if generated_id: _next_event_sequence += 1 _sort_queue() event_enqueued.emit(instance) last_error = "" return _success({ &"event_instance_id": instance_id, &"resolved_event_id": resolved_event_id, &"presentation_mode": instance.presentation_mode, }) func start_next_event(absolute_minute: int, extra_context: Dictionary = {}) -> CommandResult: if absolute_minute < 0: return _invalid("Event processing requires a non-negative minute.") process_expired(absolute_minute) audit_actor_availability(absolute_minute) if pause_requested: return _invalid("Event processing stopped after a fatal failure.", {&"fatal": true}) if not _foreground_instance_id.is_empty(): return _invalid("A foreground Event is already active.", { &"event_instance_id": _foreground_instance_id, }) _sort_queue() for instance_id: StringName in _queue.duplicate(): var instance := get_instance(instance_id) if instance == null or instance.state != EventInstanceType.State.QUEUED: _queue.erase(instance_id) continue var definition := get_definition(instance.resolved_event_id) if definition == null: _cancel_instance(instance, &"DEFINITION_MISSING", EventInstanceType.State.ERROR) continue var missing := _missing_actor_ids(instance.actor_ids) if not missing.is_empty(): _handle_missing_actors(instance, definition, missing, absolute_minute) continue var context := _merged_context(instance, absolute_minute, extra_context) if not definition.conditions_match(context, _condition_callbacks): continue var original_definition := get_definition(instance.source_event_id) if original_definition == null or not _candidate_permitted(original_definition, context) or not _candidate_permitted(definition, context): _cancel_instance(instance, &"PLACEMENT_PROTECTION", EventInstanceType.State.CANCELLED) continue if instance.foreground and not _acquire_actor_locks(instance): continue _queue.erase(instance_id) instance.state = EventInstanceType.State.RUNNING if instance.foreground: _foreground_instance_id = instance.event_instance_id event_started.emit(instance) var automatic_result := _execute_actions( definition.automatic_actions, definition.transaction_policy, definition.failure_actions, context ) if not bool(automatic_result.get(&"ok", false)): instance.failure_reason = String(automatic_result.get(&"error", "Automatic Event Action failed.")) if bool(automatic_result.get(&"fatal", false)): instance.state = EventInstanceType.State.ERROR pause_requested = true var finish := _finish_instance(instance, false) if not bool(finish.get(&"ok", false)): automatic_result[&"cost_release"] = finish else: var cancelled := _cancel_instance(instance, &"ACTION_FAILED", EventInstanceType.State.CANCELLED) if not bool(cancelled.get(&"ok", false)): automatic_result[&"fatal"] = true automatic_result[&"cost_release"] = cancelled return _invalid(instance.failure_reason, automatic_result) if not definition.choices.is_empty(): instance.state = EventInstanceType.State.WAITING_CHOICE _advance_choice_attempt(instance) last_error = "" return _success({ &"event_instance_id": instance.event_instance_id, &"choice_instance_id": instance.choice_instance_id, &"waiting_for_choice": true, }) var finish := _finish_instance(instance, true) if not bool(finish.get(&"ok", false)): return _invalid(String(finish.get(&"error", "Event cost release failed.")), finish) last_error = "" return _success({ &"event_instance_id": instance.event_instance_id, &"completed": true, }) return _invalid("No eligible Event candidate is ready.") func process_expired(absolute_minute: int) -> Array[Dictionary]: var results: Array[Dictionary] = [] for instance_id: StringName in _queue.duplicate(): var instance := get_instance(instance_id) if instance == null or instance.expires_at < 0 or absolute_minute <= instance.expires_at: continue var definition := get_definition(instance.resolved_event_id) var context := _merged_context(instance, absolute_minute, {}) var fallback_result := {&"ok": true} if definition != null and not definition.fallback_actions.is_empty(): fallback_result = _execute_actions( definition.fallback_actions, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [], context ) var cancelled := _cancel_instance(instance, &"EXPIRED", EventInstanceType.State.EXPIRED) results.append({ &"event_instance_id": instance_id, &"fallback_ok": bool(fallback_result.get(&"ok", false)), &"cost_release": cancelled, &"fatal": bool(cancelled.get(&"fatal", false)), }) return results func reserve_choice_cost( event_instance_id: StringName, choice_id: StringName, absolute_minute: int, extra_context: Dictionary = {} ) -> CommandResult: effect_transaction_started.emit() var result := _reserve_choice_cost_inside_transaction(event_instance_id, choice_id, absolute_minute, extra_context) effect_transaction_finished.emit() return result func _reserve_choice_cost_inside_transaction( event_instance_id: StringName, choice_id: StringName, absolute_minute: int, extra_context: Dictionary = {} ) -> CommandResult: var instance := get_instance(event_instance_id) if instance == null: return _not_found("EventInstance", event_instance_id) if instance.state != EventInstanceType.State.WAITING_CHOICE: return _invalid("Event is not waiting for a Choice.") var definition := get_definition(instance.resolved_event_id) var choice := definition.get_choice(choice_id) if definition != null else null if choice == null: return _not_found("EventChoice", choice_id) var missing := _missing_actor_ids(instance.actor_ids) if not missing.is_empty(): _handle_missing_actors(instance, definition, missing, absolute_minute) return _invalid("Event actor is unavailable.", { &"missing_actor_ids": missing, &"fatal": instance.state == EventInstanceType.State.ERROR, &"failure_reason": instance.failure_reason, }) var context := _merged_context(instance, absolute_minute, extra_context) if not choice.conditions_match(context, _condition_callbacks): return _invalid("Choice conditions changed and no longer pass.") if instance.reserved_costs.has(choice_id): var existing: Dictionary = instance.reserved_costs[choice_id] return _success({&"reservation": existing.duplicate(true), &"is_replay": true}) var reserve_result := _reserve_cost(choice.cost, context) if not bool(reserve_result.get(&"ok", false)): return _invalid(String(reserve_result.get(&"error", "Choice cost is unavailable.")), reserve_result) instance.reserved_costs[choice_id] = { &"cost": choice.cost.duplicate(true), &"token": _copy(reserve_result.get(&"token", choice.cost)), } last_error = "" return _success({&"reservation": instance.reserved_costs[choice_id].duplicate(true)}) func submit_choice( event_instance_id: StringName, choice_instance_id: StringName, choice_id: StringName, absolute_minute: int, extra_context: Dictionary = {} ) -> CommandResult: effect_transaction_started.emit() var result := _submit_choice_inside_transaction(event_instance_id, choice_instance_id, choice_id, absolute_minute, extra_context) effect_transaction_finished.emit() return result func _submit_choice_inside_transaction( event_instance_id: StringName, choice_instance_id: StringName, choice_id: StringName, absolute_minute: int, extra_context: Dictionary = {} ) -> CommandResult: if _processed_choices.has(choice_instance_id): var replay := CommandResultType.from_dictionary(_processed_choices[choice_instance_id]) replay.is_replay = true return replay var instance := get_instance(event_instance_id) if instance == null: return _not_found("EventInstance", event_instance_id) if choice_instance_id.is_empty() or instance.choice_instance_id != choice_instance_id: return _invalid("Choice instance token does not match the active Event.") if instance.state != EventInstanceType.State.WAITING_CHOICE: return _invalid("Event is not waiting for a Choice.") var definition := get_definition(instance.resolved_event_id) if definition == null: return _invalid("EventDefinition is missing.") var missing := _missing_actor_ids(instance.actor_ids) if not missing.is_empty(): _handle_missing_actors(instance, definition, missing, absolute_minute) return _invalid("Event actor is unavailable.", { &"missing_actor_ids": missing, &"fatal": instance.state == EventInstanceType.State.ERROR, &"failure_reason": instance.failure_reason, }) var choice := definition.get_choice(choice_id) if choice == null: return _not_found("EventChoice", choice_id) var context := _merged_context(instance, absolute_minute, extra_context) if not choice.conditions_match(context, _condition_callbacks): return _invalid("Choice conditions changed and no longer pass.") if not instance.reserved_costs.has(choice_id): var reserve_result := reserve_choice_cost(event_instance_id, choice_id, absolute_minute, extra_context) if not reserve_result.is_success(): return reserve_result var transaction_result := _execute_actions( choice.actions, definition.transaction_policy, definition.failure_actions, context ) if not bool(transaction_result.get(&"ok", false)): var release := _release_reserved_cost(instance, choice_id, context) if not bool(release.get(&"ok", false)): transaction_result[&"fatal"] = true transaction_result[&"cost_release"] = release transaction_result[&"error"] = "Choice action failed and cost release failed: %s" % release.get(&"error", "") if bool(transaction_result.get(&"fatal", false)): var other_release := _release_all_reserved_costs(instance, context, [choice_id]) if not bool(other_release.get(&"ok", false)): transaction_result[&"other_cost_release"] = other_release instance.state = EventInstanceType.State.ERROR instance.failure_reason = String(transaction_result.get(&"error", "Failure branch failed.")) pause_requested = true _detach_instance(instance) var failed_result := _invalid( String(transaction_result.get(&"error", "Choice Action transaction failed.")), transaction_result ) _cache_choice_result(choice_instance_id, failed_result) if instance.state == EventInstanceType.State.WAITING_CHOICE: _advance_choice_attempt(instance) return failed_result var commit_result := _commit_reserved_cost(instance, choice_id, context) if not bool(commit_result.get(&"ok", false)): var rollback := _rollback_records(transaction_result.get(&"records", []), context) var release := _release_reserved_cost(instance, choice_id, context) if not bool(release.get(&"ok", false)): commit_result[&"fatal"] = true commit_result[&"cost_release"] = release commit_result[&"error"] = "Choice cost commit and release failed: %s" % release.get(&"error", "") if not bool(rollback.get(&"ok", false)): commit_result[&"fatal"] = true commit_result[&"rollback"] = rollback commit_result[&"error"] = "Choice cost failed and action rollback failed: %s" % rollback.get(&"error", "") if bool(commit_result.get(&"fatal", false)): var other_release := _release_all_reserved_costs(instance, context, [choice_id]) if not bool(other_release.get(&"ok", false)): commit_result[&"other_cost_release"] = other_release instance.state = EventInstanceType.State.ERROR instance.failure_reason = String(commit_result[&"error"]) pause_requested = true _detach_instance(instance) var commit_failure := _invalid( String(commit_result.get(&"error", "Choice cost commit failed.")), commit_result ) _cache_choice_result(choice_instance_id, commit_failure) if instance.state == EventInstanceType.State.WAITING_CHOICE: _advance_choice_attempt(instance) return commit_failure instance.selected_choice_id = choice_id var finish := _finish_instance(instance, true) if not bool(finish.get(&"ok", false)): var finish_failure := _invalid(String(finish.get(&"error", "Choice final cost release failed.")), finish) _cache_choice_result(choice_instance_id, finish_failure) return finish_failure var result := _success({ &"event_instance_id": event_instance_id, &"choice_instance_id": choice_instance_id, &"choice_id": choice_id, &"completed": true, &"skipped_action_ids": transaction_result.get(&"skipped_action_ids", []), &"branched_failure": bool(transaction_result.get(&"branched_failure", false)), }) _cache_choice_result(choice_instance_id, result) choice_resolved.emit(event_instance_id, choice_id, result.to_dictionary()) last_error = "" return result func audit_actor_availability(absolute_minute: int) -> Array[Dictionary]: var reports: Array[Dictionary] = [] for instance: EventInstance in get_instances(): if instance.state not in [EventInstanceType.State.RUNNING, EventInstanceType.State.WAITING_CHOICE, EventInstanceType.State.INTERRUPTED]: continue var missing := _missing_actor_ids(instance.actor_ids) if missing.is_empty(): continue var definition := get_definition(instance.resolved_event_id) _handle_missing_actors(instance, definition, missing, absolute_minute) reports.append({ &"event_instance_id": instance.event_instance_id, &"missing_actor_ids": missing, &"state": EventInstanceType.State.keys()[instance.state], }) return reports func cancel_event(event_instance_id: StringName, reason: StringName = &"CANCELLED") -> CommandResult: var instance := get_instance(event_instance_id) if instance == null: return _not_found("EventInstance", event_instance_id) if instance.is_terminal(): return _invalid("Event is already terminal.") var cancelled := _cancel_instance(instance, reason, EventInstanceType.State.CANCELLED) if not bool(cancelled.get(&"ok", false)): return _invalid(String(cancelled.get(&"error", "Event cost release failed.")), cancelled) last_error = "" return _success({&"event_instance_id": event_instance_id}) func get_instance(event_instance_id: StringName) -> EventInstance: return _instances.get(event_instance_id) as EventInstance func get_instances() -> Array[EventInstance]: var result: Array[EventInstance] = [] for instance_id: StringName in _sorted_name_keys(_instances): result.append(_instances[instance_id]) return result func get_queued_instances() -> Array[EventInstance]: _sort_queue() var result: Array[EventInstance] = [] for instance_id: StringName in _queue: var instance := get_instance(instance_id) if instance != null: result.append(instance) return result func get_foreground_instance() -> EventInstance: return get_instance(_foreground_instance_id) func get_actor_locks() -> Dictionary: return _actor_locks.duplicate(true) func has_flag(flag_id: StringName) -> bool: return bool(_flags.get(flag_id, false)) func validate_invariants() -> Dictionary: var errors: Array[String] = [] var queued_seen: Dictionary = {} for instance_id: StringName in _queue: if queued_seen.has(instance_id): errors.append("Duplicate queue instance: %s" % instance_id) queued_seen[instance_id] = true var instance := get_instance(instance_id) if instance == null or instance.state != EventInstanceType.State.QUEUED: errors.append("Queue references a non-QUEUED instance: %s" % instance_id) for instance: EventInstance in get_instances(): if not instance.is_valid() or not _definitions.has(instance.resolved_event_id): errors.append("Invalid EventInstance: %s" % instance.event_instance_id) if not _foreground_instance_id.is_empty(): var foreground := get_instance(_foreground_instance_id) if foreground == null or not foreground.foreground or foreground.state not in [EventInstanceType.State.RUNNING, EventInstanceType.State.WAITING_CHOICE, EventInstanceType.State.INTERRUPTED]: errors.append("Foreground pointer is invalid.") for raw_actor_id: Variant in _actor_locks: var actor_id := StringName(raw_actor_id) var instance_id := StringName(_actor_locks[raw_actor_id]) var instance := get_instance(instance_id) if instance == null or not instance.actor_ids.has(actor_id) or instance_id != _foreground_instance_id: errors.append("Actor Lock is orphaned: %s" % actor_id) return {&"ok": errors.is_empty(), &"errors": errors} func create_snapshot() -> Dictionary: var instances: Array[Dictionary] = [] for instance: EventInstance in get_instances(): instances.append(instance.create_snapshot()) var queue_strings: Array[String] = [] for instance_id: StringName in _queue: queue_strings.append(String(instance_id)) return { &"version": SNAPSHOT_VERSION, &"instances": instances, &"queue": queue_strings, &"actor_locks": _string_key_dictionary(_actor_locks), &"foreground_instance_id": String(_foreground_instance_id), &"active_deduplication_keys": _string_key_dictionary(_active_deduplication_keys), &"processed_choices": _string_key_dictionary(_processed_choices), &"completed_scope_keys": _string_key_dictionary(_completed_scope_keys), &"flags": _string_key_dictionary(_flags), &"next_event_sequence": _next_event_sequence, &"next_choice_sequence": _next_choice_sequence, &"pause_requested": pause_requested, &"filter_mode": EventDefinitionType.FilterBehavior.keys()[_filter_mode], &"maximum_content_rating": EventDefinitionType.ContentRating.keys()[_maximum_content_rating], &"disabled_content_tags": _names_to_strings(_disabled_content_tags), } func restore_snapshot(snapshot: Dictionary) -> bool: var parsed := _parse_snapshot(snapshot) if not bool(parsed.get(&"ok", false)): last_error = String(parsed.get(&"error", "Invalid EventService snapshot.")) return false _instances = parsed[&"instances"] _queue = parsed[&"queue"] _actor_locks = parsed[&"actor_locks"] _foreground_instance_id = parsed[&"foreground_instance_id"] _active_deduplication_keys = parsed[&"active_deduplication_keys"] _processed_choices = parsed[&"processed_choices"] _completed_scope_keys = parsed[&"completed_scope_keys"] _flags = parsed[&"flags"] _next_event_sequence = parsed[&"next_event_sequence"] _next_choice_sequence = parsed[&"next_choice_sequence"] pause_requested = parsed[&"pause_requested"] _filter_mode = parsed[&"filter_mode"] _maximum_content_rating = parsed[&"maximum_content_rating"] _disabled_content_tags = parsed[&"disabled_content_tags"] last_error = "" return true ## Recovery-only boundary. It can release stale Actor Locks and the matching ## foreground pointer only when that foreground instance is absent or already ## terminal. Active-foreground lock mismatches remain hard validation errors; ## this routine never invents actors, events, choices, or route ownership. func restore_snapshot_with_lock_recovery(snapshot: Dictionary) -> bool: last_recovery_repair_report = { &"automatic_repairs_applied": false, &"released_actor_ids": [], &"released_foreground_instance_id": &"", &"reason": &"NONE", } var raw_instances: Variant = _value(snapshot, &"instances", null) var raw_locks: Variant = _value(snapshot, &"actor_locks", null) if not raw_instances is Array or not raw_locks is Dictionary: return restore_snapshot(snapshot) var foreground_id := StringName(_value(snapshot, &"foreground_instance_id", &"")) if foreground_id.is_empty(): return restore_snapshot(snapshot) var matching_foreground: Dictionary = {} for raw_instance: Variant in raw_instances: if raw_instance is Dictionary and StringName(_value(raw_instance, &"event_instance_id", &"")) == foreground_id: matching_foreground = raw_instance break var repair_reason: StringName = &"MISSING_FOREGROUND" var known_foreground_actors: Array[StringName] = [] if not matching_foreground.is_empty(): var state_name := String(_value(matching_foreground, &"state", "")).to_upper() var state_index := EventInstanceType.State.keys().find(state_name) if state_index < 0: return restore_snapshot(snapshot) var state := state_index as EventInstance.State if not bool(_value(matching_foreground, &"foreground", true)) or state not in [ EventInstanceType.State.COMPLETED, EventInstanceType.State.CANCELLED, EventInstanceType.State.EXPIRED, EventInstanceType.State.ERROR, ]: return restore_snapshot(snapshot) repair_reason = &"TERMINAL_FOREGROUND" var raw_actor_ids: Variant = _value(matching_foreground, &"actor_ids", null) if not raw_actor_ids is Array: return restore_snapshot(snapshot) known_foreground_actors = _sorted_unique_names(raw_actor_ids) var candidate := snapshot.duplicate(true) var released_actor_ids: Array[StringName] = [] var retained_locks: Dictionary = {} for raw_actor_id: Variant in raw_locks: var actor_id := StringName(raw_actor_id) var lock_instance_id := StringName(raw_locks[raw_actor_id]) var belongs_to_known_actor_set := ( matching_foreground.is_empty() or known_foreground_actors.has(actor_id) ) if actor_id.is_empty() or lock_instance_id != foreground_id or not belongs_to_known_actor_set: last_error = "EventService recovery found unrelated Actor Lock corruption." return false released_actor_ids.append(actor_id) released_actor_ids.sort_custom( func(left: StringName, right: StringName) -> bool: return String(left) < String(right) ) candidate[&"foreground_instance_id"] = "" candidate[&"actor_locks"] = retained_locks if not restore_snapshot(candidate): return false last_recovery_repair_report = { &"automatic_repairs_applied": true, &"released_actor_ids": released_actor_ids, &"released_foreground_instance_id": foreground_id, &"reason": repair_reason, } return true func _parse_snapshot(snapshot: Dictionary) -> Dictionary: if int(_value(snapshot, &"version", 0)) != SNAPSHOT_VERSION: return {&"ok": false, &"error": "Unsupported EventService snapshot version."} var raw_instances: Variant = _value(snapshot, &"instances", null) var raw_queue: Variant = _value(snapshot, &"queue", null) var raw_locks: Variant = _value(snapshot, &"actor_locks", null) if not raw_instances is Array or not raw_queue is Array or not raw_locks is Dictionary: return {&"ok": false, &"error": "EventService snapshot collections are malformed."} var instances: Dictionary = {} for raw_instance: Variant in raw_instances: if not raw_instance is Dictionary: return {&"ok": false, &"error": "EventInstance snapshot is malformed."} var instance := EventInstanceType.from_snapshot(raw_instance) if not instance.is_valid() or instances.has(instance.event_instance_id) or not _definitions.has(instance.resolved_event_id): return {&"ok": false, &"error": "EventInstance snapshot reference is invalid."} instances[instance.event_instance_id] = instance var queue: Array[StringName] = [] var queue_seen: Dictionary = {} for raw_instance_id: Variant in raw_queue: var instance_id := StringName(raw_instance_id) var instance: EventInstance = instances.get(instance_id) if instance == null or instance.state != EventInstanceType.State.QUEUED or queue_seen.has(instance_id): return {&"ok": false, &"error": "Event queue reference is invalid."} queue_seen[instance_id] = true queue.append(instance_id) var foreground_id := StringName(_value(snapshot, &"foreground_instance_id", &"")) if not foreground_id.is_empty(): var foreground: EventInstance = instances.get(foreground_id) if foreground == null or not foreground.foreground or foreground.state not in [EventInstanceType.State.RUNNING, EventInstanceType.State.WAITING_CHOICE, EventInstanceType.State.INTERRUPTED]: return {&"ok": false, &"error": "Foreground Event reference is invalid."} var locks: Dictionary = {} for raw_actor_id: Variant in raw_locks: var actor_id := StringName(raw_actor_id) var instance_id := StringName(raw_locks[raw_actor_id]) var locked_instance: EventInstance = instances.get(instance_id) if actor_id.is_empty() or locked_instance == null or instance_id != foreground_id or not locked_instance.actor_ids.has(actor_id): return {&"ok": false, &"error": "Actor Lock snapshot is invalid."} locks[actor_id] = instance_id if not foreground_id.is_empty(): var active: EventInstance = instances[foreground_id] for actor_id: StringName in active.actor_ids: if locks.get(actor_id, &"") != foreground_id: return {&"ok": false, &"error": "Foreground Actor Lock is incomplete."} var active_dedup := _name_dictionary(_value(snapshot, &"active_deduplication_keys", {})) for raw_key: Variant in active_dedup: var instance_id := StringName(active_dedup[raw_key]) var instance: EventInstance = instances.get(instance_id) if instance == null or instance.is_terminal() or instance.deduplication_key != StringName(raw_key): return {&"ok": false, &"error": "Active deduplication index is invalid."} var next_event := int(_value(snapshot, &"next_event_sequence", 0)) var next_choice := int(_value(snapshot, &"next_choice_sequence", 0)) if next_event < 1 or next_choice < 1: return {&"ok": false, &"error": "Event sequence counters are invalid."} var filter_mode_value := _strict_enum_value( EventDefinitionType.FilterBehavior.keys(), _value(snapshot, &"filter_mode", null) ) if filter_mode_value < 0: return {&"ok": false, &"error": "EventService filter_mode snapshot enum is invalid."} var maximum_rating_value := _strict_enum_value( EventDefinitionType.ContentRating.keys(), _value(snapshot, &"maximum_content_rating", null) ) if maximum_rating_value < 0: return {&"ok": false, &"error": "EventService maximum_content_rating snapshot enum is invalid."} var raw_disabled_tags: Variant = _value(snapshot, &"disabled_content_tags", null) if not raw_disabled_tags is Array: return {&"ok": false, &"error": "EventService disabled_content_tags snapshot is malformed."} var filter_mode := filter_mode_value as EventDefinition.FilterBehavior var maximum_rating := maximum_rating_value as EventDefinition.ContentRating return { &"ok": true, &"instances": instances, &"queue": queue, &"actor_locks": locks, &"foreground_instance_id": foreground_id, &"active_deduplication_keys": active_dedup, &"processed_choices": _dictionary(_value(snapshot, &"processed_choices", {})).duplicate(true), &"completed_scope_keys": _dictionary(_value(snapshot, &"completed_scope_keys", {})).duplicate(true), &"flags": _dictionary(_value(snapshot, &"flags", {})).duplicate(true), &"next_event_sequence": next_event, &"next_choice_sequence": next_choice, &"pause_requested": bool(_value(snapshot, &"pause_requested", false)), &"filter_mode": filter_mode, &"maximum_content_rating": maximum_rating, &"disabled_content_tags": _sorted_unique_names(raw_disabled_tags), } func _execute_actions( actions: Array, policy: EventDefinition.TransactionPolicy, failure_actions: Array, context: Dictionary ) -> Dictionary: effect_transaction_started.emit() var result := _execute_actions_inside_transaction(actions, policy, failure_actions, context) effect_transaction_finished.emit() return result func _execute_actions_inside_transaction( actions: Array, policy: EventDefinition.TransactionPolicy, failure_actions: Array, context: Dictionary ) -> Dictionary: match policy: EventDefinitionType.TransactionPolicy.SKIP_FAILED: var records: Array[Dictionary] = [] var skipped: Array[StringName] = [] for action: EventAction in actions: var validation := _execute_action(action, &"validate", context, null) if not bool(validation.get(&"ok", false)): skipped.append(action.action_id) continue var applied := _execute_action(action, &"apply", context, null) if bool(applied.get(&"ok", false)): records.append({&"action": action, &"token": _copy(applied.get(&"token", null))}) else: if bool(applied.get(&"fatal", false)): return applied skipped.append(action.action_id) return {&"ok": true, &"records": records, &"skipped_action_ids": skipped} EventDefinitionType.TransactionPolicy.BRANCH_FAILURE: var primary := _run_rollback_all(actions, context) if bool(primary.get(&"ok", false)) or bool(primary.get(&"fatal", false)): return primary var branch := _run_rollback_all(failure_actions, context) if bool(branch.get(&"ok", false)): branch[&"branched_failure"] = true branch[&"primary_error"] = primary.get(&"error", "") return branch return { &"ok": false, &"fatal": true, &"error": "Failure branch failed: %s" % branch.get(&"error", "unknown error"), } return _run_rollback_all(actions, context) func _run_rollback_all(actions: Array, context: Dictionary) -> Dictionary: for action: EventAction in actions: var validation := _execute_action(action, &"validate", context, null) if not bool(validation.get(&"ok", false)): return {&"ok": false, &"error": validation.get(&"error", "Action validation failed."), &"records": []} var records: Array[Dictionary] = [] for action: EventAction in actions: var applied := _execute_action(action, &"apply", context, null) if not bool(applied.get(&"ok", false)): var rollback := _rollback_records(records, context) var failed := {&"ok": false, &"error": applied.get(&"error", "Action apply failed."), &"records": []} if bool(applied.get(&"fatal", false)) or not bool(rollback.get(&"ok", false)): failed[&"fatal"] = true failed[&"rollback"] = rollback failed[&"error"] = "Action transaction rollback failed: %s; %s" % [failed[&"error"], rollback.get(&"error", "")] return failed records.append({&"action": action, &"token": _copy(applied.get(&"token", null))}) return {&"ok": true, &"records": records, &"skipped_action_ids": []} func _rollback_records(raw_records: Variant, context: Dictionary) -> Dictionary: if not raw_records is Array: return {&"ok": false, &"error": "Rollback records are malformed."} var failed_actions: Array[StringName] = [] var errors: Array[String] = [] # Ask every participant even after a failure; retain reverse apply order. for index: int in range(raw_records.size() - 1, -1, -1): var record: Dictionary = raw_records[index] var action: EventAction = record[&"action"] var result := _execute_action(action, &"rollback", context, record.get(&"token")) if not bool(result.get(&"ok", false)): failed_actions.append(action.action_id) errors.append(String(result.get(&"error", "Rollback participant returned failure."))) return {&"ok": failed_actions.is_empty(), &"failed_action_ids": failed_actions, &"error": "; ".join(errors)} func _execute_action( action: EventAction, phase: StringName, context: Dictionary, rollback_token: Variant ) -> Dictionary: if _action_executor.is_valid(): var custom_result: Variant = _action_executor.call(action, phase, context, rollback_token) var normalized := _normalize_operation_result(custom_result) if not bool(normalized.get(&"not_handled", false)): return normalized return _execute_builtin_action(action, phase, context, rollback_token) func _execute_builtin_action( action: EventAction, phase: StringName, context: Dictionary, rollback_token: Variant ) -> Dictionary: match action.type: EventActionType.Type.SET_FLAG: var flag_id := StringName(_value(action.payload, &"flag_id", &"")) if flag_id.is_empty(): return {&"ok": false, &"error": "SET_FLAG requires flag_id."} if phase == &"validate": return {&"ok": true} if phase == &"apply": var token := {&"had": _flags.has(flag_id), &"value": _flags.get(flag_id)} _flags[flag_id] = bool(_value(action.payload, &"enabled", true)) return {&"ok": true, &"token": token} if phase == &"rollback" and rollback_token is Dictionary: if bool(rollback_token.get(&"had", false)): _flags[flag_id] = rollback_token.get(&"value") else: _flags.erase(flag_id) return {&"ok": true} EventActionType.Type.SCHEDULE_EVENT: var event_id := StringName(_value(action.payload, &"event_id", &"")) if event_id.is_empty() or not _definitions.has(event_id): return {&"ok": false, &"error": "SCHEDULE_EVENT references an unknown event."} if phase == &"validate": return {&"ok": true} if phase == &"apply": var result := enqueue_candidate( event_id, _array(_value(action.payload, &"actor_ids", _value(context, &"actor_ids", []))), int(_value(context, &"absolute_minute", 0)), _dictionary(_value(action.payload, &"options", {})) ) return {&"ok": result.is_success(), &"error": result.message, &"token": result.details.get(&"event_instance_id", &"")} if phase == &"rollback": var instance_id := StringName(rollback_token) var scheduled := get_instance(instance_id) if scheduled != null and not scheduled.is_terminal(): return _cancel_instance(scheduled, &"TRANSACTION_ROLLBACK", EventInstanceType.State.CANCELLED) return {&"ok": true} EventActionType.Type.ADD_RELATIONSHIP_VALUE, EventActionType.Type.DAMAGE_SHIELD, EventActionType.Type.SET_ROUTE_LOCK: return _execute_relationship_action(action, phase, context, rollback_token) return {&"ok": false, &"error": "No executor is configured for Action type %s." % EventActionType.Type.keys()[action.type]} func _execute_relationship_action( action: EventAction, phase: StringName, context: Dictionary, rollback_token: Variant ) -> Dictionary: if _relationship_service == null: return {&"ok": false, &"error": "RelationshipService is not configured."} var relationship_id := StringName(_value(action.payload, &"relationship_id", &"")) if relationship_id.is_empty() or _relationship_service.get_relationship(relationship_id) == null: return {&"ok": false, &"error": "Relationship Action references an unknown state."} if phase == &"validate": return {&"ok": true} if phase == &"rollback": if rollback_token is Dictionary and rollback_token.has(&"snapshot"): return {&"ok": bool(_relationship_service.restore_snapshot(rollback_token[&"snapshot"]))} return {&"ok": false, &"error": "Relationship rollback token is missing."} var token := {&"snapshot": _relationship_service.create_snapshot()} var result: Variant match action.type: EventActionType.Type.ADD_RELATIONSHIP_VALUE: result = _relationship_service.apply_value_change( relationship_id, StringName(_value(action.payload, &"metric_id", &"")), int(_value(action.payload, &"delta", 0)), int(_value(context, &"absolute_minute", 0)), _dictionary(_value(action.payload, &"options", {&"important": true, &"allow_threshold_crossing": true})) ) EventActionType.Type.DAMAGE_SHIELD: result = _relationship_service.apply_shield_attack( relationship_id, StringName(_value(action.payload, &"action_key", action.action_id)), int(_value(context, &"absolute_minute", 0)), _dictionary(_value(action.payload, &"terms", action.payload)) ) EventActionType.Type.SET_ROUTE_LOCK: var lock_value := int(_value(action.payload, &"route_lock", 0)) result = _relationship_service.request_route_lock( relationship_id, lock_value, StringName(_value(action.payload, &"route_id", &"")), bool(_value(action.payload, &"irreversible_event", false)) ) return {&"ok": result.is_success(), &"error": result.message, &"token": token} func _handle_missing_actors( instance: EventInstance, definition: EventDefinition, missing_actor_ids: Array[StringName], absolute_minute: int ) -> void: var release := _release_all_reserved_costs(instance, _merged_context(instance, absolute_minute, {})) if not bool(release.get(&"ok", false)): _mark_release_failure(instance, release) return if definition == null: _cancel_instance(instance, &"DEFINITION_MISSING", EventInstanceType.State.ERROR) return match definition.missing_actor_policy: EventDefinitionType.MissingActorPolicy.CONTINUE_WITHOUT: for actor_id: StringName in missing_actor_ids: instance.actor_ids.erase(actor_id) _actor_locks.erase(actor_id) if instance.actor_ids.is_empty() and instance.foreground: _cancel_instance(instance, &"ALL_ACTORS_MISSING", EventInstanceType.State.CANCELLED) EventDefinitionType.MissingActorPolicy.REPLACE_WITH_ROLE: var replacements: Dictionary = _dictionary(_value(instance.context, &"replacement_actor_ids", {})) var replacement_failed := false for actor_id: StringName in missing_actor_ids: var replacement_id := StringName(_value(replacements, actor_id, &"")) if replacement_id.is_empty() or not _actor_is_available(replacement_id): replacement_failed = true break instance.actor_ids.erase(actor_id) instance.actor_ids.append(replacement_id) _actor_locks.erase(actor_id) _actor_locks[replacement_id] = instance.event_instance_id instance.actor_ids = _sorted_unique_names(instance.actor_ids) if replacement_failed: _cancel_instance(instance, &"ACTOR_REPLACEMENT_FAILED", EventInstanceType.State.CANCELLED) EventDefinitionType.MissingActorPolicy.BRANCH_FAILURE: var branch := _execute_actions( definition.failure_actions, EventDefinitionType.TransactionPolicy.ROLLBACK_ALL, [], _merged_context(instance, absolute_minute, {&"missing_actor_ids": missing_actor_ids}) ) if bool(branch.get(&"ok", false)): _cancel_instance(instance, &"ACTOR_MISSING_BRANCH", EventInstanceType.State.CANCELLED) else: instance.failure_reason = String(branch.get(&"error", "Missing Actor failure branch failed.")) _cancel_instance(instance, &"ACTOR_MISSING_BRANCH_FAILED", EventInstanceType.State.ERROR) pause_requested = true _: _cancel_instance(instance, &"ACTOR_MISSING", EventInstanceType.State.CANCELLED) func _acquire_actor_locks(instance: EventInstance) -> bool: for actor_id: StringName in instance.actor_ids: if _actor_locks.has(actor_id): return false for actor_id: StringName in instance.actor_ids: _actor_locks[actor_id] = instance.event_instance_id return true func _release_actor_locks(instance: EventInstance) -> void: for actor_id: StringName in instance.actor_ids: if _actor_locks.get(actor_id, &"") == instance.event_instance_id: _actor_locks.erase(actor_id) if _foreground_instance_id == instance.event_instance_id: _foreground_instance_id = &"" func _detach_instance(instance: EventInstance) -> void: _release_actor_locks(instance) _queue.erase(instance.event_instance_id) if not instance.deduplication_key.is_empty(): _active_deduplication_keys.erase(instance.deduplication_key) func _mark_release_failure(instance: EventInstance, result: Dictionary) -> void: instance.state = EventInstanceType.State.ERROR instance.failure_reason = String(result.get(&"error", "Event cost release failed.")) pause_requested = true _detach_instance(instance) func _finish_instance(instance: EventInstance, completed: bool) -> Dictionary: var release := _release_all_reserved_costs(instance, instance.context) if not bool(release.get(&"ok", false)): _mark_release_failure(instance, release) return release _detach_instance(instance) if completed: instance.state = EventInstanceType.State.COMPLETED var source := get_definition(instance.source_event_id) if source != null: var scope_key := _scope_key(source, instance.actor_ids, instance.created_at) if not scope_key.is_empty(): _completed_scope_keys[scope_key] = instance.event_instance_id event_finished.emit(instance) return release func _cancel_instance(instance: EventInstance, reason: StringName, terminal_state: EventInstance.State) -> Dictionary: var release := _release_all_reserved_costs(instance, instance.context) if not bool(release.get(&"ok", false)): _mark_release_failure(instance, release) event_cancelled.emit(instance, &"COST_RELEASE_FAILED") return release _detach_instance(instance) instance.state = terminal_state instance.failure_reason = String(reason) event_cancelled.emit(instance, reason) return release func _reserve_cost(cost: Dictionary, context: Dictionary) -> Dictionary: if cost.is_empty(): return {&"ok": true, &"token": {}} if not _cost_reserver.is_valid(): return {&"ok": false, &"error": "Choice Cost reserver is not configured."} return _normalize_operation_result(_cost_reserver.call(cost.duplicate(true), context)) func _release_reserved_cost(instance: EventInstance, choice_id: StringName, context: Dictionary) -> Dictionary: if not instance.reserved_costs.has(choice_id): return {&"ok": true} var reservation: Dictionary = instance.reserved_costs[choice_id] var result := {&"ok": true} if _cost_releaser.is_valid(): result = _normalize_operation_result(_cost_releaser.call(_copy(reservation.get(&"token")), context)) elif not _dictionary(reservation.get(&"cost", {})).is_empty(): result = {&"ok": false, &"error": "Choice Cost releaser is not configured."} if bool(result.get(&"ok", false)): instance.reserved_costs.erase(choice_id) else: result[&"fatal"] = true result[&"failed_choice_id"] = choice_id return result func _release_all_reserved_costs(instance: EventInstance, context: Dictionary, excluded_choice_ids: Array[StringName] = []) -> Dictionary: var failed_ids: Array[StringName] = [] var failures: Array[Dictionary] = [] for choice_id: StringName in _sorted_name_keys(instance.reserved_costs): if choice_id in excluded_choice_ids: continue var released := _release_reserved_cost(instance, choice_id, context) if not bool(released.get(&"ok", false)): failed_ids.append(choice_id) failures.append(released) if failed_ids.is_empty(): return {&"ok": true} return {&"ok": false, &"fatal": true, &"failed_choice_ids": failed_ids, &"failures": failures, &"error": "Event cost release failed for choices: %s" % [failed_ids]} func _commit_reserved_cost(instance: EventInstance, choice_id: StringName, context: Dictionary) -> Dictionary: if not instance.reserved_costs.has(choice_id): return {&"ok": true} var reservation: Dictionary = instance.reserved_costs[choice_id] var result := {&"ok": true} if _cost_committer.is_valid(): result = _normalize_operation_result(_cost_committer.call(_copy(reservation.get(&"token")), context)) if bool(result.get(&"ok", false)): instance.reserved_costs.erase(choice_id) return result func _cache_choice_result(choice_instance_id: StringName, result: CommandResult) -> void: _processed_choices[choice_instance_id] = result.to_dictionary() func _missing_actor_ids(actor_ids: Array[StringName]) -> Array[StringName]: var missing: Array[StringName] = [] for actor_id: StringName in actor_ids: if not _actor_is_available(actor_id): missing.append(actor_id) return missing func _actor_is_available(actor_id: StringName) -> bool: return bool(_actor_exists.call(actor_id)) if _actor_exists.is_valid() else true func _merged_context(instance: EventInstance, absolute_minute: int, extra: Dictionary) -> Dictionary: var result := instance.context.duplicate(true) for raw_key: Variant in extra: result[raw_key] = _copy(extra[raw_key]) result[&"absolute_minute"] = absolute_minute result[&"event_instance_id"] = instance.event_instance_id result[&"actor_ids"] = instance.actor_ids.duplicate() return result func _is_content_restricted(definition: EventDefinition) -> bool: if definition.content_rating > _maximum_content_rating: return true for tag: StringName in definition.content_tags: if _disabled_content_tags.has(tag): return true return false func _replacement_cycle_exists(event_id: StringName) -> bool: var visited: Dictionary = {} var current_id := event_id while not current_id.is_empty(): if visited.has(current_id): return true visited[current_id] = true var definition := get_definition(current_id) if definition == null or definition.filter_behavior != EventDefinitionType.FilterBehavior.REPLACE: return false current_id = definition.replacement_event_id return false func _scope_key(definition: EventDefinition, actor_ids: Array[StringName], absolute_minute: int) -> StringName: match definition.once_scope: EventDefinitionType.OnceScope.PER_SAVE: return StringName("save:%s" % definition.event_id) EventDefinitionType.OnceScope.PER_DAY: return StringName("day:%s:%d" % [definition.event_id, absolute_minute / 1_440]) EventDefinitionType.OnceScope.PER_ACTOR_PAIR: return StringName("pair:%s:%s" % [definition.event_id, ",".join(_names_to_strings(actor_ids))]) return &"" func _sort_queue() -> void: _queue.sort_custom( func(a_id: StringName, b_id: StringName) -> bool: var a := get_instance(a_id) var b := get_instance(b_id) if a == null: return false if b == null: return true if a.priority != b.priority: return a.priority > b.priority var a_expiry := a.expires_at if a.expires_at >= 0 else 9_223_372_036_854_775_000 var b_expiry := b.expires_at if b.expires_at >= 0 else 9_223_372_036_854_775_000 if a_expiry != b_expiry: return a_expiry < b_expiry return String(a.event_instance_id) < String(b.event_instance_id) ) static func _default_deduplication_key(event_id: StringName, actor_ids: Array[StringName]) -> StringName: return StringName("%s:%s" % [event_id, ",".join(_names_to_strings(actor_ids))]) static func _normalize_operation_result(raw_result: Variant) -> Dictionary: if raw_result is Dictionary: var result: Dictionary = raw_result.duplicate(true) if bool(result.get(&"handled", result.get("handled", true))) == false: return {&"ok": false, &"not_handled": true} if not result.has(&"ok") and not result.has("ok"): result[&"ok"] = true return result if raw_result is bool: return {&"ok": raw_result, &"error": "Operation returned false." if not raw_result else ""} if raw_result is String or raw_result is StringName: var message := String(raw_result) return {&"ok": message.is_empty(), &"error": message} if raw_result == null: return {&"ok": true} return {&"ok": true, &"token": _copy(raw_result)} static func _enum_value(keys: Array, raw_value: Variant, fallback: int) -> int: if raw_value is int: return clampi(int(raw_value), 0, keys.size() - 1) var index := keys.find(String(raw_value).to_upper()) return index if index >= 0 else fallback static func _strict_enum_value(keys: Array, raw_value: Variant) -> int: if raw_value is int: var numeric := int(raw_value) return numeric if numeric >= 0 and numeric < keys.size() else -1 if raw_value is String or raw_value is StringName: return keys.find(String(raw_value).to_upper()) return -1 static func _sorted_unique_names(raw_value: Variant) -> Array[StringName]: var result: Array[StringName] = [] if raw_value is Array: for raw_entry: Variant in raw_value: var entry := StringName(raw_entry) if not entry.is_empty() and not result.has(entry): result.append(entry) result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) return result static func _sorted_name_keys(source: Dictionary) -> Array[StringName]: var result: Array[StringName] = [] for raw_key: Variant in source: result.append(StringName(raw_key)) result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) return result static func _names_to_strings(values: Array[StringName]) -> Array[String]: var result: Array[String] = [] for value: StringName in values: result.append(String(value)) return result static func _string_key_dictionary(source: Dictionary) -> Dictionary: var result: Dictionary = {} var keys: Array[String] = [] for raw_key: Variant in source: keys.append(String(raw_key)) keys.sort() for key: String in keys: var value: Variant = source.get(StringName(key), source.get(key)) result[key] = _copy(value) return result static func _name_dictionary(raw_value: Variant) -> Dictionary: var result: Dictionary = {} if raw_value is Dictionary: for raw_key: Variant in raw_value: result[StringName(raw_key)] = StringName(raw_value[raw_key]) return result static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant: if source.has(key): return source[key] return source.get(String(key), fallback) static func _array(value: Variant) -> Array: return value if value is Array else [] static func _dictionary(value: Variant) -> Dictionary: return value if value is Dictionary else {} static func _copy(value: Variant) -> Variant: return value.duplicate(true) if value is Dictionary or value is Array else value func _success(details: Dictionary = {}) -> CommandResult: return CommandResultType.succeeded(0, details) func _invalid(message: String, details: Dictionary = {}) -> CommandResult: last_error = message return CommandResultType.failed( CommandResultType.Code.VALIDATION_FAILED, 0, message, details ) func _not_found(kind: String, entity_id: StringName) -> CommandResult: return _invalid("%s not found: %s" % [kind, entity_id], {&"entity_id": entity_id}) func _advance_choice_attempt(instance: EventInstance) -> void: instance.choice_instance_id = StringName("choice_instance_%08d" % _next_choice_sequence) _next_choice_sequence += 1