Preserve approved pixel tower prototype as development baseline

This commit is contained in:
Codex
2026-09-09 07:51:16 +09:00
commit 423fd6dd2e
915 changed files with 95193 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
class_name SimulationCommand
extends RefCounted
var command_id := 0
var issued_tick := 0
var execute_tick := 0
var priority := 0
var command_type: StringName = &""
var actor_id: StringName = &""
var target_id: StringName = &""
var payload: Dictionary = {}
var idempotency_key: StringName = &""
func _init(
p_command_id: int = 0,
p_issued_tick: int = 0,
p_execute_tick: int = 0,
p_priority: int = 0,
p_command_type: StringName = &"",
p_actor_id: StringName = &"",
p_target_id: StringName = &"",
p_payload: Dictionary = {},
p_idempotency_key: StringName = &""
) -> void:
command_id = p_command_id
issued_tick = p_issued_tick
execute_tick = p_execute_tick
priority = p_priority
command_type = p_command_type
actor_id = p_actor_id
target_id = p_target_id
payload = p_payload.duplicate(true)
idempotency_key = p_idempotency_key
func validation_error() -> String:
if command_id <= 0:
return "command_id must be greater than zero."
if issued_tick < 0:
return "issued_tick cannot be negative."
if execute_tick < issued_tick:
return "execute_tick cannot be earlier than issued_tick."
return ""
func is_valid() -> bool:
return validation_error().is_empty()
func copy() -> SimulationCommand:
return SimulationCommand.new(
command_id,
issued_tick,
execute_tick,
priority,
command_type,
actor_id,
target_id,
payload,
idempotency_key
)
func to_dictionary() -> Dictionary:
return {
"command_id": command_id,
"issued_tick": issued_tick,
"execute_tick": execute_tick,
"priority": priority,
"command_type": String(command_type),
"actor_id": String(actor_id),
"target_id": String(target_id),
"payload": payload.duplicate(true),
"idempotency_key": String(idempotency_key),
}
static func from_dictionary(data: Dictionary) -> SimulationCommand:
return SimulationCommand.new(
int(data.get("command_id", 0)),
int(data.get("issued_tick", 0)),
int(data.get("execute_tick", 0)),
int(data.get("priority", 0)),
StringName(data.get("command_type", "")),
StringName(data.get("actor_id", "")),
StringName(data.get("target_id", "")),
data.get("payload", {}) as Dictionary,
StringName(data.get("idempotency_key", ""))
)
+1
View File
@@ -0,0 +1 @@
uid://crauxmerklv67
+407
View File
@@ -0,0 +1,407 @@
class_name CommandQueue
extends RefCounted
const SNAPSHOT_VERSION := 2
const LEGACY_SNAPSHOT_VERSION := 1
var _pending: Array[SimulationCommand] = []
var _in_flight: Dictionary = {}
var _known_command_ids: Dictionary = {}
var _idempotency_to_command_id: Dictionary = {}
var _processed_results: Dictionary = {}
var _accepted_metadata: Dictionary = {}
func enqueue(command: SimulationCommand) -> CommandResult:
if command == null:
return CommandResult.failed(
CommandResult.Code.INVALID_COMMAND,
0,
"Command is null."
)
var validation_error := command.validation_error()
if not validation_error.is_empty():
return CommandResult.failed(
CommandResult.Code.INVALID_COMMAND,
command.command_id,
validation_error
)
if _known_command_ids.has(command.command_id):
return CommandResult.failed(
CommandResult.Code.DUPLICATE_COMMAND_ID,
command.command_id,
"command_id has already been accepted.",
{"original_command_id": command.command_id}
)
if not command.idempotency_key.is_empty():
var key := command.idempotency_key
if _idempotency_to_command_id.has(key):
var original_id: int = _idempotency_to_command_id[key]
var duplicate_details := {
"idempotency_key": String(key),
"original_command_id": original_id,
"processed": _processed_results.has(original_id),
}
if _processed_results.has(original_id):
var prior: CommandResult = _processed_results[original_id]
duplicate_details["original_result"] = prior.to_dictionary()
return CommandResult.failed(
CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY,
command.command_id,
"idempotency_key has already been accepted.",
duplicate_details
)
var queued_command := command.copy()
_pending.append(queued_command)
_known_command_ids[queued_command.command_id] = true
_accepted_metadata[queued_command.command_id] = {
"command_id": queued_command.command_id,
"idempotency_key": String(queued_command.idempotency_key),
}
if not queued_command.idempotency_key.is_empty():
_idempotency_to_command_id[queued_command.idempotency_key] = queued_command.command_id
_pending.sort_custom(_comes_before)
return CommandResult.succeeded(queued_command.command_id)
func drain_ready(current_tick: int) -> Array[SimulationCommand]:
var ready: Array[SimulationCommand] = []
while not _pending.is_empty():
var command := _pending[0]
if command.execute_tick > current_tick:
break
_pending.pop_front()
_in_flight[command.command_id] = command
ready.append(command)
return ready
func execute_ready(current_tick: int, executor: Callable) -> Array[CommandResult]:
var results: Array[CommandResult] = []
for command in drain_ready(current_tick):
var result: CommandResult
if not executor.is_valid():
result = CommandResult.failed(
CommandResult.Code.EXECUTOR_FAILED,
command.command_id,
"Executor is not callable."
)
else:
var returned: Variant = executor.call(command)
if returned is CommandResult:
result = returned
elif returned is bool and returned:
result = CommandResult.succeeded(command.command_id)
else:
result = CommandResult.failed(
CommandResult.Code.EXECUTOR_FAILED,
command.command_id,
"Executor did not return a successful CommandResult."
)
if result.command_id == 0:
result.command_id = command.command_id
complete(command.command_id, result)
results.append(result.copy())
return results
func complete(command_id: int, result: CommandResult) -> CommandResult:
if not _in_flight.has(command_id):
return CommandResult.failed(
CommandResult.Code.NOT_FOUND,
command_id,
"Command is not in flight."
)
if result == null:
return CommandResult.failed(
CommandResult.Code.INVALID_COMMAND,
command_id,
"Result is null."
)
var stored_result := result.copy()
stored_result.command_id = command_id
_processed_results[command_id] = stored_result
_in_flight.erase(command_id)
return stored_result.copy()
func get_processed_result(command_id: int) -> CommandResult:
if not _processed_results.has(command_id):
return null
var result: CommandResult = _processed_results[command_id]
return result.copy()
func get_result_by_idempotency_key(key: StringName) -> CommandResult:
if not _idempotency_to_command_id.has(key):
return null
var command_id: int = _idempotency_to_command_id[key]
if not _processed_results.has(command_id):
return null
var result: CommandResult = _processed_results[command_id]
return result.replay_copy()
func has_idempotency_key(key: StringName) -> bool:
return _idempotency_to_command_id.has(key)
func pending_count() -> int:
return _pending.size()
func in_flight_count() -> int:
return _in_flight.size()
func processed_count() -> int:
return _processed_results.size()
func get_accepted_command_metadata() -> Array[Dictionary]:
var result: Array[Dictionary] = []
var command_ids: Array = _accepted_metadata.keys()
command_ids.sort()
for command_id: Variant in command_ids:
result.append((_accepted_metadata[command_id] as Dictionary).duplicate(true))
return result
func next_execute_tick() -> int:
if _pending.is_empty():
return -1
return _pending[0].execute_tick
func create_snapshot() -> Dictionary:
var pending_data: Array[Dictionary] = []
for command in _pending:
pending_data.append(command.to_dictionary())
var in_flight_ids: Array = _in_flight.keys()
in_flight_ids.sort()
var in_flight_data: Array[Dictionary] = []
for command_id: Variant in in_flight_ids:
var command: SimulationCommand = _in_flight[command_id]
in_flight_data.append(command.to_dictionary())
var processed_ids: Array = _processed_results.keys()
processed_ids.sort()
var processed_data: Array[Dictionary] = []
for command_id: Variant in processed_ids:
var result: CommandResult = _processed_results[command_id]
processed_data.append(result.to_dictionary())
var idempotency_data := {}
var idempotency_keys: Array = _idempotency_to_command_id.keys()
idempotency_keys.sort()
for key: Variant in idempotency_keys:
idempotency_data[String(key)] = int(_idempotency_to_command_id[key])
return {
"version": SNAPSHOT_VERSION,
"pending": pending_data,
"in_flight": in_flight_data,
"processed": processed_data,
"idempotency": idempotency_data,
"accepted": get_accepted_command_metadata(),
}
func restore_snapshot(snapshot: Dictionary) -> bool:
var version := LEGACY_SNAPSHOT_VERSION
if snapshot.has("version"):
if typeof(snapshot["version"]) != TYPE_INT:
return false
version = int(snapshot["version"])
if version not in [LEGACY_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
return false
if version == SNAPSHOT_VERSION and not _has_exact_keys(
snapshot,
["version", "pending", "in_flight", "processed", "idempotency", "accepted"]
):
return false
for required_key in ["pending", "in_flight", "processed", "idempotency"]:
if not snapshot.has(required_key):
return false
if not snapshot["pending"] is Array \
or not snapshot["in_flight"] is Array \
or not snapshot["processed"] is Array \
or not snapshot["idempotency"] is Dictionary \
or (version == SNAPSHOT_VERSION and not snapshot.get("accepted", null) is Array):
return false
var restored_pending: Array[SimulationCommand] = []
var restored_in_flight := {}
var restored_known_ids := {}
var restored_processed := {}
var restored_idempotency := {}
var command_keys_by_id := {}
for command_data: Variant in snapshot["pending"]:
if not command_data is Dictionary:
return false
if version == SNAPSHOT_VERSION and not _strict_command_dictionary(command_data):
return false
var command := SimulationCommand.from_dictionary(command_data)
if not command.is_valid() or restored_known_ids.has(command.command_id):
return false
restored_pending.append(command)
restored_known_ids[command.command_id] = true
command_keys_by_id[command.command_id] = command.idempotency_key
for command_data: Variant in snapshot["in_flight"]:
if not command_data is Dictionary:
return false
if version == SNAPSHOT_VERSION and not _strict_command_dictionary(command_data):
return false
var command := SimulationCommand.from_dictionary(command_data)
if not command.is_valid() or restored_known_ids.has(command.command_id):
return false
restored_in_flight[command.command_id] = command
restored_known_ids[command.command_id] = true
command_keys_by_id[command.command_id] = command.idempotency_key
for result_data: Variant in snapshot["processed"]:
if not result_data is Dictionary:
return false
if version == SNAPSHOT_VERSION and not _strict_result_dictionary(result_data):
return false
var result := CommandResult.from_dictionary(result_data)
if result.command_id <= 0 or restored_known_ids.has(result.command_id):
return false
restored_processed[result.command_id] = result
restored_known_ids[result.command_id] = true
var mapped_key_by_id := {}
for key: Variant in snapshot["idempotency"]:
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(key).is_empty() \
or typeof(snapshot["idempotency"][key]) != TYPE_INT:
return false
var mapped_command_id := int(snapshot["idempotency"][key])
if not restored_known_ids.has(mapped_command_id) \
or mapped_key_by_id.has(mapped_command_id):
return false
restored_idempotency[StringName(key)] = mapped_command_id
mapped_key_by_id[mapped_command_id] = StringName(key)
for command_id: Variant in command_keys_by_id:
var command_key := StringName(command_keys_by_id[command_id])
if command_key.is_empty():
if mapped_key_by_id.has(command_id):
return false
elif mapped_key_by_id.get(command_id, &"") != command_key:
return false
var restored_metadata := {}
if version == SNAPSHOT_VERSION:
var previous_command_id := 0
for metadata_value: Variant in snapshot["accepted"]:
if not metadata_value is Dictionary or not _has_exact_keys(
metadata_value, ["command_id", "idempotency_key"]
):
return false
var command_id_value: Variant = metadata_value.get("command_id", null)
var idempotency_value: Variant = metadata_value.get("idempotency_key", null)
if typeof(command_id_value) != TYPE_INT \
or int(command_id_value) <= previous_command_id \
or typeof(idempotency_value) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var command_id := int(command_id_value)
var idempotency_key := StringName(idempotency_value)
if not restored_known_ids.has(command_id) or restored_metadata.has(command_id):
return false
if command_keys_by_id.has(command_id) \
and StringName(command_keys_by_id[command_id]) != idempotency_key:
return false
if idempotency_key.is_empty():
if mapped_key_by_id.has(command_id):
return false
elif mapped_key_by_id.get(command_id, &"") != idempotency_key:
return false
restored_metadata[command_id] = {
"command_id": command_id,
"idempotency_key": String(idempotency_key),
}
previous_command_id = command_id
if restored_metadata.size() != restored_known_ids.size():
return false
else:
# Legacy snapshots did not retain processed Command input. Their reverse
# idempotency map still lets migration preserve the exactly-once key.
var legacy_ids: Array = restored_known_ids.keys()
legacy_ids.sort()
for command_id: Variant in legacy_ids:
var idempotency_key := StringName(command_keys_by_id.get(
command_id, mapped_key_by_id.get(command_id, &"")
))
restored_metadata[command_id] = {
"command_id": int(command_id),
"idempotency_key": String(idempotency_key),
}
restored_pending.sort_custom(_comes_before)
_pending = restored_pending
_in_flight = restored_in_flight
_known_command_ids = restored_known_ids
_processed_results = restored_processed
_idempotency_to_command_id = restored_idempotency
_accepted_metadata = restored_metadata
return true
func _strict_command_dictionary(data: Dictionary) -> bool:
if not _has_exact_keys(data, [
"command_id", "issued_tick", "execute_tick", "priority", "command_type",
"actor_id", "target_id", "payload", "idempotency_key",
]):
return false
for key: String in ["command_id", "issued_tick", "execute_tick", "priority"]:
if typeof(data.get(key, null)) != TYPE_INT:
return false
for key: String in ["command_type", "actor_id", "target_id", "idempotency_key"]:
if typeof(data.get(key, null)) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
return data.get("payload", null) is Dictionary
func _strict_result_dictionary(data: Dictionary) -> bool:
if not _has_exact_keys(data, [
"code", "command_id", "target_id", "message", "resolution_hints",
"details", "is_replay",
]):
return false
var code_value: Variant = data.get("code", null)
if typeof(code_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or CommandResult.Code.keys().find(String(code_value)) < 0 \
or typeof(data.get("command_id", null)) != TYPE_INT \
or typeof(data.get("target_id", null)) not in [TYPE_STRING, TYPE_STRING_NAME] \
or typeof(data.get("message", null)) != TYPE_STRING \
or not data.get("resolution_hints", null) is Array \
or not data.get("details", null) is Dictionary \
or typeof(data.get("is_replay", null)) != TYPE_BOOL:
return false
for hint: Variant in data["resolution_hints"]:
if typeof(hint) != TYPE_STRING:
return false
return true
func _has_exact_keys(source: Dictionary, expected: Array) -> bool:
if source.size() != expected.size():
return false
var seen: Dictionary = {}
for raw_key: Variant in source:
if typeof(raw_key) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var key := String(raw_key)
if key not in expected or seen.has(key):
return false
seen[key] = true
return seen.size() == expected.size()
func _comes_before(left: SimulationCommand, right: SimulationCommand) -> bool:
if left.execute_tick != right.execute_tick:
return left.execute_tick < right.execute_tick
if left.priority != right.priority:
return left.priority > right.priority
return left.command_id < right.command_id
+1
View File
@@ -0,0 +1 @@
uid://dscjo2hyabf7
+104
View File
@@ -0,0 +1,104 @@
class_name CommandResult
extends RefCounted
enum Code {
OK,
INVALID_COMMAND,
INVALID_PAYLOAD,
DUPLICATE_COMMAND_ID,
DUPLICATE_IDEMPOTENCY_KEY,
NOT_FOUND,
VALIDATION_FAILED,
TRANSACTION_CLOSED,
TRANSACTION_APPLY_FAILED,
EXECUTOR_FAILED,
}
var code: Code = Code.OK
var command_id := 0
var target_id: StringName = &""
var message := ""
var resolution_hints: Array[String] = []
var details: Dictionary = {}
var is_replay := false
func _init(
p_code: Code = Code.OK,
p_command_id: int = 0,
p_message: String = "",
p_details: Dictionary = {}
) -> void:
code = p_code
command_id = p_command_id
message = p_message
details = p_details.duplicate(true)
static func succeeded(
p_command_id: int = 0,
p_details: Dictionary = {}
) -> CommandResult:
return CommandResult.new(Code.OK, p_command_id, "", p_details)
static func failed(
p_code: Code,
p_command_id: int,
p_message: String,
p_details: Dictionary = {}
) -> CommandResult:
assert(p_code != Code.OK, "Use CommandResult.succeeded() for a successful result.")
return CommandResult.new(p_code, p_command_id, p_message, p_details)
func is_success() -> bool:
return code == Code.OK
func copy() -> CommandResult:
var result := CommandResult.new(code, command_id, message, details)
result.target_id = target_id
result.resolution_hints.assign(resolution_hints)
result.is_replay = is_replay
return result
func replay_copy() -> CommandResult:
var result := copy()
result.is_replay = true
return result
func to_dictionary() -> Dictionary:
return {
"code": Code.keys()[code],
"command_id": command_id,
"target_id": String(target_id),
"message": message,
"resolution_hints": resolution_hints.duplicate(),
"details": details.duplicate(true),
"is_replay": is_replay,
}
static func from_dictionary(data: Dictionary) -> CommandResult:
var code_name := String(data.get("code", "INVALID_COMMAND"))
var parsed_code := Code.INVALID_COMMAND
var code_names := Code.keys()
var code_index := code_names.find(code_name)
if code_index >= 0:
parsed_code = code_index as Code
var result := CommandResult.new(
parsed_code,
int(data.get("command_id", 0)),
String(data.get("message", "")),
data.get("details", {}) as Dictionary
)
result.target_id = StringName(data.get("target_id", ""))
var saved_hints: Variant = data.get("resolution_hints", [])
if saved_hints is Array:
for hint: Variant in saved_hints:
result.resolution_hints.append(String(hint))
result.is_replay = bool(data.get("is_replay", false))
return result
+1
View File
@@ -0,0 +1 @@
uid://b4gb8xmpwer3o
+762
View File
@@ -0,0 +1,762 @@
class_name DefinitionRegistry
extends RefCounted
## Loads, validates, and indexes immutable game definitions.
##
## Invalid definitions are reported but never exposed through lookup/query APIs.
## All returned definition and issue lists use a deterministic ID-first order.
const DEFAULT_FACILITY_DIRECTORY := "res://data/facilities"
const FACILITY_FILE_EXTENSION := "tres"
const SEVERITY_ERROR := &"error"
const SEVERITY_WARNING := &"warning"
var _facilities_by_id: Dictionary = {}
var _ordered_facility_ids: Array[StringName] = []
var _errors: Array[Dictionary] = []
var _warnings: Array[Dictionary] = []
var _source_count := 0
## Scans the default facility directory and rebuilds the registry.
func rebuild(directory_path: String = DEFAULT_FACILITY_DIRECTORY) -> Dictionary:
_reset()
var paths := _discover_facility_paths(directory_path)
if not _errors.is_empty():
_finish_rebuild()
return get_report()
_load_and_index(paths)
_finish_rebuild()
return get_report()
## Rebuilds from an explicit path list. Primarily useful for content packs and tests.
func rebuild_from_paths(paths: Array[String]) -> Dictionary:
_reset()
var ordered_paths := paths.duplicate()
ordered_paths.sort()
_load_and_index(ordered_paths)
_finish_rebuild()
return get_report()
func is_valid() -> bool:
return _errors.is_empty()
func has_fatal_errors() -> bool:
return not _errors.is_empty()
func facility_count() -> int:
return _ordered_facility_ids.size()
func source_count() -> int:
return _source_count
func has_facility(facility_id: StringName) -> bool:
return _facilities_by_id.has(facility_id)
func get_facility(facility_id: StringName) -> FacilityDefinition:
return _facilities_by_id.get(facility_id) as FacilityDefinition
func get_facility_ids() -> Array[StringName]:
return _ordered_facility_ids.duplicate()
func get_all_facilities() -> Array[FacilityDefinition]:
var result: Array[FacilityDefinition] = []
for facility_id in _ordered_facility_ids:
result.append(_facilities_by_id[facility_id] as FacilityDefinition)
return result
func query_facilities_by_tag(tag: StringName) -> Array[FacilityDefinition]:
var result: Array[FacilityDefinition] = []
for facility_id in _ordered_facility_ids:
var definition := _facilities_by_id[facility_id] as FacilityDefinition
if tag in definition.tags:
result.append(definition)
return result
func query_facilities_by_zone(zone: int) -> Array[FacilityDefinition]:
var result: Array[FacilityDefinition] = []
for facility_id in _ordered_facility_ids:
var definition := _facilities_by_id[facility_id] as FacilityDefinition
if definition.zone == zone:
result.append(definition)
return result
func query_facilities_by_layer(layer: int) -> Array[FacilityDefinition]:
var result: Array[FacilityDefinition] = []
for facility_id in _ordered_facility_ids:
var definition := _facilities_by_id[facility_id] as FacilityDefinition
if definition.layer == layer:
result.append(definition)
return result
func get_fatal_errors() -> Array[Dictionary]:
return _errors.duplicate(true)
func get_warnings() -> Array[Dictionary]:
return _warnings.duplicate(true)
func get_report() -> Dictionary:
return {
&"valid": is_valid(),
&"source_count": _source_count,
&"definition_count": facility_count(),
&"error_count": _errors.size(),
&"warning_count": _warnings.size(),
&"errors": get_fatal_errors(),
&"warnings": get_warnings(),
}
func _reset() -> void:
_facilities_by_id.clear()
_ordered_facility_ids.clear()
_errors.clear()
_warnings.clear()
_source_count = 0
func _discover_facility_paths(directory_path: String) -> Array[String]:
var result: Array[String] = []
var directory := DirAccess.open(directory_path)
if directory == null:
_add_error(
&"directory_unreadable",
&"",
directory_path,
&"resource_path",
"Facility definition directory cannot be opened."
)
return result
directory.list_dir_begin()
var file_name := directory.get_next()
while not file_name.is_empty():
if not directory.current_is_dir() and file_name.get_extension().to_lower() == FACILITY_FILE_EXTENSION:
result.append(directory_path.path_join(file_name))
file_name = directory.get_next()
directory.list_dir_end()
result.sort()
if result.is_empty():
_add_warning(
&"no_definitions_found",
&"",
directory_path,
&"resource_path",
"No facility definitions were found."
)
return result
func _load_and_index(paths: Array[String]) -> void:
var candidates: Array[Dictionary] = []
var seen_paths: Dictionary = {}
for path in paths:
_source_count += 1
if seen_paths.has(path):
_add_warning(
&"duplicate_source_path",
&"",
path,
&"resource_path",
"The same source path was supplied more than once."
)
seen_paths[path] = true
if not _validate_requested_path(path):
continue
var resource := ResourceLoader.load(path)
if resource == null:
_add_error(
&"resource_load_failed",
&"",
path,
&"resource_path",
"The resource could not be loaded."
)
continue
if not resource is FacilityDefinition:
_add_error(
&"wrong_resource_type",
&"",
path,
&"resource",
"Expected FacilityDefinition, got %s." % resource.get_class()
)
continue
var definition := resource as FacilityDefinition
candidates.append({
&"definition": definition,
&"path": path,
&"valid": _validate_facility(definition, path),
})
candidates.sort_custom(_candidate_less)
var counts_by_id: Dictionary = {}
var paths_by_id: Dictionary = {}
for candidate in candidates:
var definition := candidate[&"definition"] as FacilityDefinition
if definition.id.is_empty():
continue
counts_by_id[definition.id] = int(counts_by_id.get(definition.id, 0)) + 1
if not paths_by_id.has(definition.id):
paths_by_id[definition.id] = []
var id_paths := paths_by_id[definition.id] as Array
id_paths.append(String(candidate[&"path"]))
for candidate in candidates:
var definition := candidate[&"definition"] as FacilityDefinition
var path := String(candidate[&"path"])
if definition.id.is_empty():
continue
if int(counts_by_id[definition.id]) > 1:
var duplicate_paths := paths_by_id[definition.id] as Array
_add_error(
&"duplicate_id",
definition.id,
path,
&"id",
"Facility ID is duplicated by: %s." % ", ".join(duplicate_paths)
)
continue
if not bool(candidate[&"valid"]):
continue
_facilities_by_id[definition.id] = definition
_ordered_facility_ids.append(definition.id)
func _validate_requested_path(path: String) -> bool:
var valid := true
if path.is_empty():
_add_error(&"empty_resource_path", &"", path, &"resource_path", "Resource path is empty.")
return false
if not path.begins_with("res://"):
_add_error(
&"non_project_resource_path",
&"",
path,
&"resource_path",
"Facility definitions must use a res:// project path."
)
valid = false
if path.get_extension().to_lower() != FACILITY_FILE_EXTENSION:
_add_error(
&"wrong_resource_extension",
&"",
path,
&"resource_path",
"Facility definitions must use the .tres extension."
)
valid = false
if not FileAccess.file_exists(path):
_add_error(
&"resource_not_found",
&"",
path,
&"resource_path",
"Facility definition file does not exist."
)
valid = false
return valid
func _validate_facility(definition: FacilityDefinition, requested_path: String) -> bool:
var valid := true
var facility_id := definition.id
var actual_path := definition.resource_path
if facility_id.is_empty():
_add_error(&"missing_required_value", facility_id, requested_path, &"id", "Facility ID is required.")
valid = false
elif not _is_snake_case_id(String(facility_id)):
_add_error(
&"invalid_id_format",
facility_id,
requested_path,
&"id",
"Facility ID must be lowercase ASCII snake_case and begin with a letter."
)
valid = false
if definition.display_name.strip_edges().is_empty():
_add_error(
&"missing_required_value",
facility_id,
requested_path,
&"display_name",
"Display name is required."
)
valid = false
if definition.description.strip_edges().is_empty():
_add_warning(
&"empty_description",
facility_id,
requested_path,
&"description",
"Description is empty."
)
if actual_path.is_empty():
_add_error(
&"missing_resource_path",
facility_id,
requested_path,
&"resource_path",
"Loaded FacilityDefinition has no resource_path."
)
valid = false
elif actual_path != requested_path:
_add_error(
&"resource_path_mismatch",
facility_id,
requested_path,
&"resource_path",
"Loaded resource_path '%s' does not match its requested path." % actual_path
)
valid = false
if definition.zone < FacilityDefinition.MapZone.ANY \
or definition.zone > FacilityDefinition.MapZone.UNDERGROUND:
_add_error(&"value_out_of_range", facility_id, requested_path, &"zone", "Zone enum value is out of range.")
valid = false
if definition.layer < FacilityDefinition.ViewLayer.FRONT \
or definition.layer > FacilityDefinition.ViewLayer.BACK:
_add_error(&"value_out_of_range", facility_id, requested_path, &"layer", "Layer enum value is out of range.")
valid = false
if definition.footprint.x <= 0 or definition.footprint.y <= 0:
_add_error(
&"value_out_of_range",
facility_id,
requested_path,
&"footprint",
"Footprint dimensions must both be greater than zero."
)
valid = false
if definition.build_minutes <= 0:
_add_error(&"value_out_of_range", facility_id, requested_path, &"build_minutes", "Build time must be greater than zero.")
valid = false
if definition.weight_per_cell not in [1, 2, 3]:
_add_error(&"value_out_of_range", facility_id, requested_path, &"weight_per_cell", "Structural weight must be 1, 2, or 3.")
valid = false
if definition.allowed_rotations.is_empty():
_add_error(&"missing_required_value", facility_id, requested_path, &"allowed_rotations", "At least one rotation is required.")
valid = false
else:
var seen_rotations := {}
for rotation in definition.allowed_rotations:
if rotation not in [0, 90, 180, 270] or seen_rotations.has(rotation):
_add_error(&"invalid_rotation", facility_id, requested_path, &"allowed_rotations", "Rotations must be unique quarter turns.")
valid = false
seen_rotations[rotation] = true
if definition.staff_min < 0 or definition.staff_max < definition.staff_min:
_add_error(&"value_out_of_range", facility_id, requested_path, &"staff_min", "Staff limits are inconsistent.")
valid = false
if definition.capacity < 0 or definition.queue_capacity < 0:
_add_error(&"value_out_of_range", facility_id, requested_path, &"capacity", "Service and queue capacities cannot be negative.")
valid = false
if definition.base_service_minutes <= 0 or definition.base_price < 0:
_add_error(&"value_out_of_range", facility_id, requested_path, &"base_service_minutes", "Service duration must be positive and price cannot be negative.")
valid = false
for item_id: Variant in definition.inventory_inputs:
if StringName(item_id).is_empty() or int(definition.inventory_inputs[item_id]) <= 0:
_add_error(&"invalid_inventory_requirement", facility_id, requested_path, &"inventory_inputs", "Inventory input ids and quantities must be positive.")
valid = false
for item_id: Variant in definition.inventory_outputs:
if StringName(item_id).is_empty() or int(definition.inventory_outputs[item_id]) <= 0:
_add_error(&"invalid_inventory_output", facility_id, requested_path, &"inventory_outputs", "Inventory output ids and quantities must be positive.")
valid = false
if not _validate_construction_cost(definition.construction_cost, facility_id, requested_path):
valid = false
if definition.maintenance_mana < 0:
_add_error(
&"value_out_of_range",
facility_id,
requested_path,
&"maintenance_mana",
"Maintenance mana cannot be negative."
)
valid = false
if definition.staff_min < 0:
_add_error(&"value_out_of_range", facility_id, requested_path, &"staff_min", "Minimum staff cannot be negative.")
valid = false
if definition.staff_max < 0:
_add_error(&"value_out_of_range", facility_id, requested_path, &"staff_max", "Maximum staff cannot be negative.")
valid = false
if definition.staff_max < definition.staff_min:
_add_error(
&"invalid_value_relation",
facility_id,
requested_path,
&"staff_max",
"Maximum staff must be greater than or equal to minimum staff."
)
valid = false
if definition.capacity < 0:
_add_error(&"value_out_of_range", facility_id, requested_path, &"capacity", "Capacity cannot be negative.")
valid = false
if not _validate_tags(definition.tags, facility_id, requested_path):
valid = false
if not _validate_detailed_profile(definition, requested_path):
valid = false
if not facility_id.is_empty() and requested_path.get_file().get_basename() != String(facility_id):
_add_warning(
&"id_filename_mismatch",
facility_id,
requested_path,
&"id",
"Facility ID does not match the resource filename."
)
return valid
func _validate_detailed_profile(definition: FacilityDefinition, path: String) -> bool:
var facility_id := definition.id
if definition.definition_schema_version == FacilityDefinition.LEGACY_SCHEMA_VERSION:
return true
if definition.definition_schema_version != FacilityDefinition.DETAILED_SCHEMA_VERSION:
_add_error(&"unsupported_definition_schema", facility_id, path, &"definition_schema_version", "Facility definition schema is unsupported.")
return false
var valid := true
if definition.category.is_empty() or not _is_snake_case_id(String(definition.category)):
_add_error(&"invalid_category", facility_id, path, &"category", "Detailed facilities require one snake_case category.")
valid = false
if not _validate_service_profiles(definition, path):
valid = false
for field_row: Array in [
[&"service_types", definition.service_types, true],
[&"required_connection_tags", definition.required_connection_tags, false],
[&"preferred_adjacency_tags", definition.preferred_adjacency_tags, false],
[&"forbidden_adjacency_tags", definition.forbidden_adjacency_tags, false],
[&"allowed_guest_tags", definition.allowed_guest_tags, false],
[&"forbidden_guest_tags", definition.forbidden_guest_tags, false],
[&"research_requirements", definition.research_requirements, false],
[&"statistics_keys", definition.statistics_keys, true],
]:
if not _validate_name_list(field_row[1], bool(field_row[2]), facility_id, path, field_row[0]):
valid = false
if not _validate_access_offsets(definition, definition.guest_access_offsets, &"guest_access_offsets", path):
valid = false
if not _validate_access_offsets(definition, definition.staff_access_offsets, &"staff_access_offsets", path):
valid = false
if not definition.pass_through and definition.guest_access_offsets.is_empty() and definition.staff_access_offsets.is_empty():
_add_error(&"missing_access_point", facility_id, path, &"guest_access_offsets", "A non-route detailed facility requires a guest or staff access point.")
valid = false
if definition.max_level < 1 or definition.max_level > 5 or definition.upgrade_costs.size() != definition.max_level - 1:
_add_error(&"invalid_upgrade_ladder", facility_id, path, &"upgrade_costs", "Upgrade costs must contain exactly max_level - 1 entries.")
valid = false
for index: int in definition.upgrade_costs.size():
if not _validate_construction_cost(definition.upgrade_costs[index], facility_id, path):
valid = false
if definition.demolition_refund_percent < 0 or definition.demolition_refund_percent > 100:
_add_error(&"value_out_of_range", facility_id, path, &"demolition_refund_percent", "Demolition refund percent must be from 0 through 100.")
valid = false
if definition.unlock_rank < 1 or definition.unlock_rank > 7:
_add_error(&"value_out_of_range", facility_id, path, &"unlock_rank", "Unlock rank must be from 1 through 7.")
valid = false
var expected_schedule_keys: Array[StringName] = [&"morning", &"day", &"night"]
if definition.opening_schedule.size() != expected_schedule_keys.size() \
or not definition.opening_schedule.has(&"morning") \
or not definition.opening_schedule.has(&"day") \
or not definition.opening_schedule.has(&"night"):
_add_error(&"invalid_opening_schedule", facility_id, path, &"opening_schedule", "Opening schedule must contain exactly morning, day, and night.")
valid = false
else:
for time_band: StringName in expected_schedule_keys:
if not definition.opening_schedule[time_band] is bool:
_add_error(&"wrong_value_type", facility_id, path, &"opening_schedule", "Opening schedule values must be booleans.")
valid = false
if not _validate_nonnegative_int_map(definition.incident_weights, facility_id, path, &"incident_weights"):
valid = false
if not _validate_policy_schema(definition.policy_schema, facility_id, path):
valid = false
if definition.adult_content and (not definition.tags.has(&"adult_only") or definition.content_replacement_id.is_empty()):
_add_error(&"unsafe_adult_content_profile", facility_id, path, &"adult_content", "Adult content requires adult_only and a safe content replacement ID.")
valid = false
if not definition.adult_content and not definition.content_replacement_id.is_empty():
_add_warning(&"unused_content_replacement", facility_id, path, &"content_replacement_id", "A non-adult facility does not use a content replacement.")
return valid
func _validate_service_profiles(definition: FacilityDefinition, path: String) -> bool:
var valid := true
if definition.default_service_type.is_empty() or not definition.service_types.has(definition.default_service_type):
_add_error(&"invalid_default_service", definition.id, path, &"default_service_type", "Default service must reference one declared service type.")
valid = false
if definition.service_profiles.size() != definition.service_types.size():
_add_error(&"service_profile_mismatch", definition.id, path, &"service_profiles", "Every declared service type requires exactly one profile.")
valid = false
for service_type: StringName in definition.service_types:
if not definition.service_profiles.has(service_type) or not definition.service_profiles[service_type] is Dictionary:
_add_error(&"missing_service_profile", definition.id, path, &"service_profiles", "Service '%s' requires a Dictionary profile." % service_type)
valid = false
continue
var profile := definition.service_profiles[service_type] as Dictionary
for required_key: StringName in [&"duration_minutes", &"base_quality", &"base_price", &"capacity_units", &"need_effects"]:
if not profile.has(required_key):
_add_error(&"missing_service_profile_field", definition.id, path, &"service_profiles", "Service '%s' is missing '%s'." % [service_type, required_key])
valid = false
if profile.has(&"duration_minutes") and (not profile[&"duration_minutes"] is int or int(profile[&"duration_minutes"]) <= 0):
_add_error(&"invalid_service_profile_value", definition.id, path, &"service_profiles", "Service '%s' duration_minutes must be a positive integer." % service_type)
valid = false
if profile.has(&"base_quality") and (not profile[&"base_quality"] is int or int(profile[&"base_quality"]) < 0 or int(profile[&"base_quality"]) > 100):
_add_error(&"invalid_service_profile_value", definition.id, path, &"service_profiles", "Service '%s' base_quality must be an integer from 0 through 100." % service_type)
valid = false
if profile.has(&"base_price") and (not profile[&"base_price"] is int or int(profile[&"base_price"]) < 0):
_add_error(&"invalid_service_profile_value", definition.id, path, &"service_profiles", "Service '%s' base_price must be a non-negative integer." % service_type)
valid = false
if profile.has(&"capacity_units") and (not profile[&"capacity_units"] is int or int(profile[&"capacity_units"]) <= 0):
_add_error(&"invalid_service_profile_value", definition.id, path, &"service_profiles", "Service '%s' capacity_units must be a positive integer." % service_type)
valid = false
if not profile.has(&"need_effects"):
pass # The missing-field issue was emitted above.
elif not profile[&"need_effects"] is Dictionary:
_add_error(&"invalid_service_profile_value", definition.id, path, &"service_profiles", "Service '%s' need_effects must be a Dictionary." % service_type)
valid = false
elif not _validate_signed_int_map(profile[&"need_effects"], -100, 100, definition.id, path, &"service_profiles"):
valid = false
return valid
func _validate_name_list(values: Array[StringName], required: bool, facility_id: StringName, path: String, field: StringName) -> bool:
if required and values.is_empty():
_add_error(&"missing_required_value", facility_id, path, field, "%s requires at least one ID." % field)
return false
var valid := true
var seen: Dictionary = {}
for value: StringName in values:
if value.is_empty() or not _is_snake_case_id(String(value)) or seen.has(value):
_add_error(&"invalid_id_list", facility_id, path, field, "%s must contain unique snake_case IDs." % field)
valid = false
seen[value] = true
return valid
func _validate_access_offsets(definition: FacilityDefinition, offsets: Array[Vector2i], field: StringName, path: String) -> bool:
var valid := true
var seen: Dictionary = {}
for offset: Vector2i in offsets:
if offset.x < 0 or offset.y < 0 or offset.x >= definition.footprint.x or offset.y >= definition.footprint.y or seen.has(offset):
_add_error(&"invalid_access_offset", definition.id, path, field, "Access points must be unique cells inside the unrotated footprint.")
valid = false
seen[offset] = true
return valid
func _validate_nonnegative_int_map(values: Dictionary, facility_id: StringName, path: String, field: StringName) -> bool:
var valid := true
for raw_key: Variant in values:
var key := StringName(raw_key)
if key.is_empty() or not _is_snake_case_id(String(key)) or not values[raw_key] is int or int(values[raw_key]) < 0:
_add_error(&"invalid_weight_map", facility_id, path, field, "%s requires snake_case IDs and non-negative integer weights." % field)
valid = false
return valid
func _validate_signed_int_map(values: Dictionary, minimum: int, maximum: int, facility_id: StringName, path: String, field: StringName) -> bool:
var valid := true
for raw_key: Variant in values:
var key := StringName(raw_key)
if key.is_empty() or not _is_snake_case_id(String(key)) or not values[raw_key] is int or int(values[raw_key]) < minimum or int(values[raw_key]) > maximum:
_add_error(&"invalid_signed_value_map", facility_id, path, field, "%s requires snake_case IDs and integer values from %d through %d." % [field, minimum, maximum])
valid = false
return valid
func _validate_policy_schema(values: Dictionary, facility_id: StringName, path: String) -> bool:
var valid := true
for raw_key: Variant in values:
var key := StringName(raw_key)
var row: Variant = values[raw_key]
if key.is_empty() or not _is_snake_case_id(String(key)) or not row is Dictionary:
_add_error(&"invalid_policy_schema", facility_id, path, &"policy_schema", "Policy entries require snake_case IDs and Dictionary schemas.")
valid = false
continue
var schema := row as Dictionary
if not schema.has(&"type") or not schema.has(&"default") or StringName(schema[&"type"]) not in [&"bool", &"enum", &"int"]:
_add_error(&"invalid_policy_schema", facility_id, path, &"policy_schema", "Every policy requires type and default; type must be bool, enum, or int.")
valid = false
return valid
func _validate_construction_cost(cost: Dictionary, facility_id: StringName, path: String) -> bool:
var valid := true
for required_key in [&"gold", &"materials"]:
if not cost.has(required_key):
_add_error(
&"missing_required_key",
facility_id,
path,
&"construction_cost",
"Construction cost is missing '%s'." % required_key
)
valid = false
continue
var value: Variant = cost[required_key]
if not value is int:
_add_error(
&"wrong_value_type",
facility_id,
path,
&"construction_cost",
"Construction cost '%s' must be an integer." % required_key
)
valid = false
elif int(value) < 0:
_add_error(
&"value_out_of_range",
facility_id,
path,
&"construction_cost",
"Construction cost '%s' cannot be negative." % required_key
)
valid = false
var cost_keys := cost.keys()
cost_keys.sort()
for key in cost_keys:
if key != &"gold" and key != &"materials":
_add_warning(
&"unknown_cost_key",
facility_id,
path,
&"construction_cost",
"Unknown construction cost key '%s' is ignored by the base game." % key
)
return valid
func _validate_tags(tags: Array[StringName], facility_id: StringName, path: String) -> bool:
var valid := true
var seen: Dictionary = {}
for tag in tags:
if tag.is_empty() or not _is_snake_case_id(String(tag)):
_add_error(
&"invalid_tag_format",
facility_id,
path,
&"tags",
"Tags must be non-empty lowercase ASCII snake_case identifiers."
)
valid = false
if seen.has(tag):
_add_warning(&"duplicate_tag", facility_id, path, &"tags", "Duplicate tag '%s'." % tag)
seen[tag] = true
return valid
func _is_snake_case_id(value: String) -> bool:
if value.is_empty():
return false
var first := value.unicode_at(0)
if first < 97 or first > 122:
return false
var previous_was_underscore := false
for index in value.length():
var character := value.unicode_at(index)
var is_letter := character >= 97 and character <= 122
var is_number := character >= 48 and character <= 57
var is_underscore := character == 95
if not is_letter and not is_number and not is_underscore:
return false
if is_underscore and previous_was_underscore:
return false
previous_was_underscore = is_underscore
return not previous_was_underscore
func _finish_rebuild() -> void:
_ordered_facility_ids.sort_custom(_id_less)
_errors.sort_custom(_issue_less)
_warnings.sort_custom(_issue_less)
func _add_error(
code: StringName,
definition_id: StringName,
resource_path: String,
field: StringName,
message: String
) -> void:
_errors.append(_make_issue(SEVERITY_ERROR, code, definition_id, resource_path, field, message))
func _add_warning(
code: StringName,
definition_id: StringName,
resource_path: String,
field: StringName,
message: String
) -> void:
_warnings.append(_make_issue(SEVERITY_WARNING, code, definition_id, resource_path, field, message))
func _make_issue(
severity: StringName,
code: StringName,
definition_id: StringName,
resource_path: String,
field: StringName,
message: String
) -> Dictionary:
return {
&"severity": severity,
&"code": code,
&"definition_id": definition_id,
&"resource_path": resource_path,
&"field": field,
&"message": message,
}
func _candidate_less(left: Dictionary, right: Dictionary) -> bool:
var left_definition := left[&"definition"] as FacilityDefinition
var right_definition := right[&"definition"] as FacilityDefinition
var left_id := String(left_definition.id)
var right_id := String(right_definition.id)
if left_id != right_id:
return left_id < right_id
return String(left[&"path"]) < String(right[&"path"])
func _id_less(left: StringName, right: StringName) -> bool:
return String(left) < String(right)
func _issue_less(left: Dictionary, right: Dictionary) -> bool:
var left_id := String(left[&"definition_id"])
var right_id := String(right[&"definition_id"])
if left_id != right_id:
return left_id < right_id
var left_path := String(left[&"resource_path"])
var right_path := String(right[&"resource_path"])
if left_path != right_path:
return left_path < right_path
var left_field := String(left[&"field"])
var right_field := String(right[&"field"])
if left_field != right_field:
return left_field < right_field
var left_code := String(left[&"code"])
var right_code := String(right[&"code"])
if left_code != right_code:
return left_code < right_code
return String(left[&"message"]) < String(right[&"message"])
+1
View File
@@ -0,0 +1 @@
uid://c51liwjesdyql
+154
View File
@@ -0,0 +1,154 @@
class_name DeterministicRng
extends RefCounted
## A small deterministic random stream for simulation code.
##
## Each subsystem must create its own stream from the shared root seed and a
## stable stream ID. Saving both state and counter allows an exact continuation
## without coupling unrelated systems through one global random sequence.
const ALGORITHM_ID: StringName = &"xorshift32-sha256-v1"
const SNAPSHOT_VERSION := 1
const UINT32_MASK := 0xffffffff
const UINT32_RANGE := 0x100000000
const FALLBACK_NON_ZERO_STATE := 0x6d2b79f5
var _root_seed := 0
var _stream_id: StringName = &"default"
var _state := FALLBACK_NON_ZERO_STATE
var _counter := 0
static func from_root_seed(root_seed: int, stream_id: StringName) -> Variant:
# Loading by resource path keeps this factory usable on a clean checkout,
# before Godot has generated its global class-name cache.
var result: Variant = load("res://scripts/core/deterministic_rng.gd").new()
result.reset(root_seed, stream_id)
return result
func reset(root_seed: int, stream_id: StringName) -> void:
_root_seed = root_seed
_stream_id = stream_id
_state = _derive_non_zero_state(root_seed, stream_id)
_counter = 0
func next_u32() -> int:
# xorshift32 operates only on non-negative 32-bit values. Intermediates stay
# below the signed 64-bit limit used by GDScript integers.
var value := _state
value ^= (value << 13) & UINT32_MASK
value ^= value >> 17
value ^= (value << 5) & UINT32_MASK
_state = value & UINT32_MASK
_counter += 1
return _state
func randi_range(minimum: int, maximum: int) -> int:
if minimum > maximum:
push_error("DeterministicRng range minimum must not exceed maximum")
return minimum
# A crossing-zero subtraction can overflow only for ranges much larger than
# this generator supports, so reject it before evaluating the subtraction.
var width := 0
if minimum < 0 and maximum > 0:
if maximum > minimum + UINT32_MASK:
push_error("DeterministicRng supports ranges of at most 2^32 values")
return minimum
width = maximum - minimum
else:
width = maximum - minimum
if width > UINT32_MASK:
push_error("DeterministicRng supports ranges of at most 2^32 values")
return minimum
var span := width + 1
if span == 1:
return minimum
# Rejection sampling avoids modulo bias for spans which do not divide 2^32.
var acceptance_limit := UINT32_RANGE - (UINT32_RANGE % span)
while true:
var sample := next_u32()
if sample < acceptance_limit:
return minimum + (sample % span)
return minimum
func snapshot() -> Dictionary:
return {
"version": SNAPSHOT_VERSION,
"algorithm": String(ALGORITHM_ID),
"root_seed": _root_seed,
"stream_id": String(_stream_id),
"state": _state,
"counter": _counter,
}
func restore(saved_state: Dictionary) -> bool:
if int(saved_state.get("version", -1)) != SNAPSHOT_VERSION:
push_error("Unsupported DeterministicRng snapshot version")
return false
if StringName(saved_state.get("algorithm", "")) != ALGORITHM_ID:
push_error("DeterministicRng snapshot algorithm does not match")
return false
var restored_state := int(saved_state.get("state", 0))
var restored_counter := int(saved_state.get("counter", -1))
if restored_state <= 0 or restored_state > UINT32_MASK:
push_error("DeterministicRng snapshot contains an invalid state")
return false
if restored_counter < 0:
push_error("DeterministicRng snapshot contains an invalid counter")
return false
_root_seed = int(saved_state.get("root_seed", 0))
_stream_id = StringName(saved_state.get("stream_id", "default"))
_state = restored_state
_counter = restored_counter
return true
func get_root_seed() -> int:
return _root_seed
func get_stream_id() -> StringName:
return _stream_id
func get_state() -> int:
return _state
func get_counter() -> int:
return _counter
static func _derive_non_zero_state(root_seed: int, stream_id: StringName) -> int:
var stream_bytes := String(stream_id).to_utf8_buffer()
var context := HashingContext.new()
if context.start(HashingContext.HASH_SHA256) != OK:
push_error("Could not initialize the deterministic RNG seed hash")
return FALLBACK_NON_ZERO_STATE
context.update("the-tree:deterministic-rng:v1\n".to_utf8_buffer())
context.update(("%d\n%d\n" % [root_seed, stream_bytes.size()]).to_utf8_buffer())
context.update(stream_bytes)
var digest := context.finish()
var derived_state := 0
for byte_index in range(4):
derived_state = (derived_state << 8) | int(digest[byte_index])
if derived_state == 0:
# The all-zero state is absorbing for xorshift32. A second digest word is
# already independently distributed; retain a literal fallback as a final
# guard for the astronomically unlikely double-zero case.
for byte_index in range(4, 8):
derived_state = (derived_state << 8) | int(digest[byte_index])
if derived_state == 0:
derived_state = FALLBACK_NON_ZERO_STATE
return derived_state
+1
View File
@@ -0,0 +1 @@
uid://d3kcft1dnys2a
+75
View File
@@ -0,0 +1,75 @@
class_name SimulationDomainEvent
extends RefCounted
var event_id: StringName = &""
var event_type: StringName = &""
var absolute_minute := 0
var source_service: StringName = &""
var entity_ids: Array[StringName] = []
var payload: Dictionary = {}
var causation_command_id := 0
func _init(
p_event_id: StringName = &"",
p_event_type: StringName = &"",
p_absolute_minute := 0,
p_source_service: StringName = &"",
p_entity_ids: Array[StringName] = [],
p_payload: Dictionary = {},
p_causation_command_id := 0
) -> void:
event_id = p_event_id
event_type = p_event_type
absolute_minute = p_absolute_minute
source_service = p_source_service
entity_ids = p_entity_ids.duplicate()
payload = p_payload.duplicate(true)
causation_command_id = p_causation_command_id
func is_valid() -> bool:
return not event_id.is_empty() \
and not event_type.is_empty() \
and not source_service.is_empty() \
and absolute_minute >= 0 \
and causation_command_id >= 0
func copy() -> SimulationDomainEvent:
return SimulationDomainEvent.new(
event_id,
event_type,
absolute_minute,
source_service,
entity_ids,
payload,
causation_command_id
)
func to_dictionary() -> Dictionary:
var saved_entity_ids: Array[String] = []
for entity_id in entity_ids:
saved_entity_ids.append(String(entity_id))
return {
"event_id": String(event_id),
"event_type": String(event_type),
"absolute_minute": absolute_minute,
"source_service": String(source_service),
"entity_ids": saved_entity_ids,
"payload": payload.duplicate(true),
"causation_command_id": causation_command_id,
}
static func from_dictionary(data: Dictionary) -> SimulationDomainEvent:
var restored_entity_ids: Array[StringName] = []
var saved_entity_ids: Variant = data.get("entity_ids", [])
if saved_entity_ids is Array:
for entity_id: Variant in saved_entity_ids:
restored_entity_ids.append(StringName(entity_id))
return SimulationDomainEvent.new(
StringName(data.get("event_id", "")),
StringName(data.get("event_type", "")),
int(data.get("absolute_minute", -1)),
StringName(data.get("source_service", "")),
restored_entity_ids,
data.get("payload", {}) as Dictionary,
int(data.get("causation_command_id", 0))
)
+1
View File
@@ -0,0 +1 @@
uid://y0x57c1kmwj7
+144
View File
@@ -0,0 +1,144 @@
class_name DomainEventStream
extends RefCounted
const SNAPSHOT_VERSION := 1
const WILDCARD_EVENT_TYPE: StringName = &"*"
const RECENT_EVENT_LIMIT := 256
const MAX_BATCH_SIZE := 4_096
var _subscribers: Dictionary = {}
var _published_ids: Dictionary = {}
var _recent_events: Array[SimulationDomainEvent] = []
var _pending_deliveries: Array[SimulationDomainEvent] = []
var _delivery_active := false
func subscribe(event_type: StringName, subscriber_id: StringName, callback: Callable) -> bool:
if event_type.is_empty() or subscriber_id.is_empty() or not callback.is_valid():
return false
if not _subscribers.has(event_type):
_subscribers[event_type] = {}
var subscribers: Dictionary = _subscribers[event_type]
if subscribers.has(subscriber_id):
return false
subscribers[subscriber_id] = callback
return true
func unsubscribe(event_type: StringName, subscriber_id: StringName) -> void:
if _subscribers.has(event_type):
var subscribers: Dictionary = _subscribers[event_type]
subscribers.erase(subscriber_id)
func publish(event: SimulationDomainEvent) -> bool:
return publish_batch([event])
## Validate and commit the entire event set before the first subscriber runs.
## Reentrant publications join the end of the delivery queue, so callbacks can
## never observe or deliver only a prefix of the committed batch.
func publish_batch(events: Array) -> bool:
if events.size() > MAX_BATCH_SIZE:
return false
var committed: Array[SimulationDomainEvent] = []
var batch_ids: Dictionary = {}
for event_value: Variant in events:
if not event_value is SimulationDomainEvent:
return false
var event: SimulationDomainEvent = event_value
if event == null or not event.is_valid() \
or _published_ids.has(event.event_id) or batch_ids.has(event.event_id):
return false
batch_ids[event.event_id] = true
committed.append(event.copy())
for event: SimulationDomainEvent in committed:
_published_ids[event.event_id] = true
_recent_events.append(event.copy())
if _recent_events.size() > RECENT_EVENT_LIMIT:
_recent_events.pop_front()
_pending_deliveries.append(event.copy())
_drain_deliveries()
return true
func _drain_deliveries() -> void:
if _delivery_active:
return
_delivery_active = true
while not _pending_deliveries.is_empty():
var event: SimulationDomainEvent = _pending_deliveries.pop_front()
_deliver(event)
_delivery_active = false
func _deliver(event: SimulationDomainEvent) -> void:
var callbacks := {}
_collect_callbacks(event.event_type, callbacks)
_collect_callbacks(WILDCARD_EVENT_TYPE, callbacks)
var subscriber_ids: Array = callbacks.keys()
subscriber_ids.sort()
for subscriber_id: Variant in subscriber_ids:
var callback: Callable = callbacks[subscriber_id]
if callback.is_valid():
callback.call(event.copy())
func has_published(event_id: StringName) -> bool:
return _published_ids.has(event_id)
func recent_events() -> Array[SimulationDomainEvent]:
var result: Array[SimulationDomainEvent] = []
for event in _recent_events:
result.append(event.copy())
return result
func create_snapshot() -> Dictionary:
var published_ids: Array = _published_ids.keys()
published_ids.sort_custom(func(left: Variant, right: Variant) -> bool:
return String(left) < String(right)
)
var saved_ids: Array[String] = []
for event_id: Variant in published_ids:
saved_ids.append(String(event_id))
var saved_recent: Array[Dictionary] = []
for event in _recent_events:
saved_recent.append(event.to_dictionary())
return {
"version": SNAPSHOT_VERSION,
"published_ids": saved_ids,
"recent_events": saved_recent,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
if _delivery_active or not _pending_deliveries.is_empty():
return false
if int(snapshot.get("version", -1)) != SNAPSHOT_VERSION:
return false
if not snapshot.get("published_ids", null) is Array \
or not snapshot.get("recent_events", null) is Array:
return false
var restored_ids := {}
for event_id: Variant in snapshot["published_ids"]:
var normalized_id := StringName(event_id)
if normalized_id.is_empty() or restored_ids.has(normalized_id):
return false
restored_ids[normalized_id] = true
var restored_recent: Array[SimulationDomainEvent] = []
for event_data: Variant in snapshot["recent_events"]:
if not event_data is Dictionary:
return false
var event := SimulationDomainEvent.from_dictionary(event_data)
if not event.is_valid() or not restored_ids.has(event.event_id):
return false
restored_recent.append(event)
if restored_recent.size() > RECENT_EVENT_LIMIT:
return false
_published_ids = restored_ids
_recent_events = restored_recent
_pending_deliveries.clear()
return true
func _collect_callbacks(event_type: StringName, target: Dictionary) -> void:
if not _subscribers.has(event_type):
return
var source: Dictionary = _subscribers[event_type]
for subscriber_id: Variant in source:
if not target.has(subscriber_id):
target[subscriber_id] = source[subscriber_id]
+1
View File
@@ -0,0 +1 @@
uid://d21vwgov273if
+174
View File
@@ -0,0 +1,174 @@
class_name SimulationPerformanceBudget
extends RefCounted
## Lightweight runtime profiler with explicit integer-microsecond samples.
## It is diagnostic state only and deliberately stays outside Save snapshots.
signal budget_exceeded(metric_id: StringName, report: Dictionary)
enum Aggregation {
AVERAGE,
P95,
MAXIMUM,
}
const DEFAULT_SAMPLE_CAPACITY := 512
const DEFAULT_BUDGETS := {
&"simulation_x1": {&"aggregation": Aggregation.AVERAGE, &"limit_us": 4_000},
&"simulation_x4": {&"aggregation": Aggregation.P95, &"limit_us": 12_000},
&"path_query": {&"aggregation": Aggregation.P95, &"limit_us": 2_000},
&"main_thread_stall": {&"aggregation": Aggregation.MAXIMUM, &"limit_us": 50_000},
&"save": {&"aggregation": Aggregation.MAXIMUM, &"limit_us": 5_000_000},
&"load": {&"aggregation": Aggregation.MAXIMUM, &"limit_us": 8_000_000},
}
var sample_capacity := DEFAULT_SAMPLE_CAPACITY
var _budgets: Dictionary = {}
var _samples: Dictionary = {}
var _totals: Dictionary = {}
var _sample_counts: Dictionary = {}
func _init(capacity := DEFAULT_SAMPLE_CAPACITY) -> void:
sample_capacity = maxi(int(capacity), 1)
for raw_metric_id: Variant in DEFAULT_BUDGETS:
_budgets[StringName(raw_metric_id)] = (
DEFAULT_BUDGETS[raw_metric_id] as Dictionary
).duplicate(true)
func set_budget(
metric_id: StringName,
limit_us: int,
aggregation: Aggregation = Aggregation.P95
) -> bool:
if metric_id.is_empty() or limit_us <= 0 \
or aggregation < Aggregation.AVERAGE or aggregation > Aggregation.MAXIMUM:
return false
_budgets[metric_id] = {
&"aggregation": aggregation,
&"limit_us": limit_us,
}
return true
func record_sample(metric_id: StringName, duration_us: int) -> Dictionary:
if metric_id.is_empty() or duration_us < 0:
return {&"ok": false, &"error": "Metric ID and non-negative duration are required."}
if not _samples.has(metric_id):
_samples[metric_id] = []
var values: Array = _samples[metric_id]
values.append(duration_us)
if values.size() > sample_capacity:
values.pop_front()
_totals[metric_id] = int(_totals.get(metric_id, 0)) + duration_us
_sample_counts[metric_id] = int(_sample_counts.get(metric_id, 0)) + 1
var report := get_report(metric_id)
if bool(report.get(&"budget_exceeded", false)):
budget_exceeded.emit(metric_id, report.duplicate(true))
return {&"ok": true, &"report": report}
func record_simulation_tick(duration_us: int, speed: int) -> Dictionary:
var metric_id := &"simulation_x4" if speed == 4 else &"simulation_x1"
return record_sample(metric_id, duration_us)
## Records the full main-thread cost of one rendered/process frame, including
## all game-minute ticks consumed at accelerated speed. This keeps x4 from
## accidentally reporting four individually-fast ticks while the frame stalls.
func record_simulation_frame(duration_us: int, speed: int, ticks_run: int) -> Dictionary:
if ticks_run <= 0:
return {&"ok": false, &"error": "At least one completed tick is required."}
var simulation_result := record_simulation_tick(duration_us, speed)
var stall_result := record_sample(&"main_thread_stall", duration_us)
return {
&"ok": bool(simulation_result[&"ok"]) and bool(stall_result[&"ok"]),
&"ticks_run": ticks_run,
&"simulation": simulation_result,
&"main_thread": stall_result,
}
func get_report(metric_id: StringName) -> Dictionary:
var values: Array = _samples.get(metric_id, [])
var count := values.size()
var lifetime_count := int(_sample_counts.get(metric_id, 0))
var lifetime_total := int(_totals.get(metric_id, 0))
var average_us := lifetime_total / lifetime_count if lifetime_count > 0 else 0
var maximum_us := 0
for raw_value: Variant in values:
maximum_us = maxi(maximum_us, int(raw_value))
var p95_us := _percentile(values, 95)
var budget: Dictionary = _budgets.get(metric_id, {})
var aggregation := int(budget.get(&"aggregation", Aggregation.P95))
var measured_us := _aggregated_value(aggregation, average_us, p95_us, maximum_us)
var limit_us := int(budget.get(&"limit_us", 0))
return {
&"metric_id": metric_id,
&"window_sample_count": count,
&"lifetime_sample_count": lifetime_count,
&"average_us": average_us,
&"p95_us": p95_us,
&"maximum_us": maximum_us,
&"aggregation": aggregation,
&"measured_us": measured_us,
&"limit_us": limit_us,
&"budget_exceeded": limit_us > 0 and measured_us > limit_us,
}
func get_all_reports() -> Array[Dictionary]:
var metric_ids: Array[StringName] = []
for raw_metric_id: Variant in _budgets:
metric_ids.append(StringName(raw_metric_id))
for raw_metric_id: Variant in _samples:
var metric_id := StringName(raw_metric_id)
if not metric_ids.has(metric_id):
metric_ids.append(metric_id)
metric_ids.sort_custom(func(a: StringName, b: StringName) -> bool:
return String(a) < String(b)
)
var reports: Array[Dictionary] = []
for metric_id: StringName in metric_ids:
reports.append(get_report(metric_id))
return reports
func clear(metric_id: StringName = &"") -> void:
if metric_id.is_empty():
_samples.clear()
_totals.clear()
_sample_counts.clear()
return
_samples.erase(metric_id)
_totals.erase(metric_id)
_sample_counts.erase(metric_id)
func _aggregated_value(
aggregation: int,
average_us: int,
p95_us: int,
maximum_us: int
) -> int:
match aggregation:
Aggregation.AVERAGE:
return average_us
Aggregation.MAXIMUM:
return maximum_us
_:
return p95_us
func _percentile(raw_values: Array, percentile: int) -> int:
if raw_values.is_empty():
return 0
var values: Array[int] = []
for raw_value: Variant in raw_values:
values.append(int(raw_value))
values.sort()
var rank_numerator := clampi(percentile, 0, 100) * values.size()
var index := maxi((rank_numerator + 99) / 100 - 1, 0)
return values[mini(index, values.size() - 1)]
+1
View File
@@ -0,0 +1 @@
uid://dt8ia8mm76hc1
+142
View File
@@ -0,0 +1,142 @@
class_name SaveMigrationRegistry
extends RefCounted
## A deterministic, in-memory registry for adjacent save-format migrations.
##
## Every registered step migrates exactly N -> N + 1. The callback receives a
## deep copy of the current value plus the two version numbers and must return
## either the migrated Dictionary, or { ok = true, data = Dictionary }. A
## callback can reject a migration with { ok = false, code = ..., message = ... }.
## The caller-owned source Dictionary is never passed to migration code.
var _steps: Dictionary = {}
var _migration_ids: Dictionary = {}
func register_step(from_version: int, migration_id: StringName, migrator: Callable) -> Dictionary:
if from_version < 0:
return _failure(&"INVALID_FROM_VERSION", "from_version must be non-negative")
if migration_id.is_empty():
return _failure(&"INVALID_MIGRATION_ID", "migration_id must not be empty")
if not migrator.is_valid():
return _failure(&"INVALID_MIGRATOR", str(migration_id))
if _steps.has(from_version):
return _failure(&"DUPLICATE_MIGRATION_STEP", _required_step_id(from_version))
if _migration_ids.has(migration_id):
return _failure(&"DUPLICATE_MIGRATION_ID", str(migration_id))
_steps[from_version] = {
&"id": migration_id,
&"migrator": migrator,
}
_migration_ids[migration_id] = from_version
return {
&"ok": true,
&"code": &"OK",
&"migration_id": migration_id,
&"from_version": from_version,
&"to_version": from_version + 1,
}
func has_step(from_version: int) -> bool:
return _steps.has(from_version)
func get_step_id(from_version: int) -> StringName:
if not _steps.has(from_version):
return StringName()
return StringName(_steps[from_version][&"id"])
func get_registered_steps() -> Array[Dictionary]:
var versions: Array = _steps.keys()
versions.sort()
var result: Array[Dictionary] = []
for version_value: Variant in versions:
var from_version := int(version_value)
result.append({
&"id": get_step_id(from_version),
&"from_version": from_version,
&"to_version": from_version + 1,
})
return result
func migrate(source: Dictionary, from_version: int, target_version: int) -> Dictionary:
if from_version < 0 or target_version < 0 or target_version < from_version:
return _failure(&"INVALID_VERSION_RANGE", "%d -> %d" % [from_version, target_version])
var source_guard := source.duplicate(true)
var working := source.duplicate(true)
var applied_ids: Array[StringName] = []
for version in range(from_version, target_version):
if not _steps.has(version):
return {
&"ok": false,
&"code": &"MIGRATION_STEP_MISSING",
&"message": "missing adjacent migration %d -> %d" % [version, version + 1],
&"failed_from_version": version,
&"failed_to_version": version + 1,
&"required_migration_id": _required_step_id(version),
&"applied_migration_ids": applied_ids,
}
var step: Dictionary = _steps[version]
var migration_id := StringName(step[&"id"])
var migrator: Callable = step[&"migrator"]
# Each callback receives its own deep copy so even a failing mutator cannot
# contaminate the last accepted intermediate value.
var callback_value: Variant = migrator.call(working.duplicate(true), version, version + 1)
var unpacked := _unpack_callback_result(callback_value)
if not unpacked[&"ok"]:
return {
&"ok": false,
&"code": StringName(unpacked.get(&"code", &"MIGRATION_STEP_FAILED")),
&"message": str(unpacked.get(&"message", migration_id)),
&"failed_migration_id": migration_id,
&"failed_from_version": version,
&"failed_to_version": version + 1,
&"applied_migration_ids": applied_ids,
}
working = unpacked[&"data"].duplicate(true)
applied_ids.append(migration_id)
if source != source_guard:
# This should be unreachable because callbacks only see deep copies. Keep
# the guard explicit so purity regressions fail closed.
return _failure(&"SOURCE_MUTATED", "migration source changed unexpectedly")
return {
&"ok": true,
&"code": &"OK",
&"data": working.duplicate(true),
&"from_version": from_version,
&"target_version": target_version,
&"applied_migration_ids": applied_ids,
}
func _unpack_callback_result(value: Variant) -> Dictionary:
if not value is Dictionary:
return _failure(&"MIGRATION_INVALID_RESULT", "migration callback must return a Dictionary")
var result: Dictionary = value
if result.has(&"ok") or result.has("ok"):
var result_ok := bool(result.get(&"ok", result.get("ok", false)))
if not result_ok:
return _failure(
StringName(result.get(&"code", result.get("code", &"MIGRATION_STEP_FAILED"))),
str(result.get(&"message", result.get("message", "migration rejected")))
)
var data_value: Variant = result.get(&"data", result.get("data", null))
if not data_value is Dictionary:
return _failure(&"MIGRATION_INVALID_RESULT", "successful migration has no Dictionary data")
return {&"ok": true, &"code": &"OK", &"data": data_value}
return {&"ok": true, &"code": &"OK", &"data": result}
func _required_step_id(from_version: int) -> StringName:
return StringName("format_%d_to_%d" % [from_version, from_version + 1])
func _failure(code: StringName, message: String) -> Dictionary:
return {
&"ok": false,
&"code": code,
&"message": message,
}
@@ -0,0 +1 @@
uid://dclgekguin7m2
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
uid://chwd86m6gactn
+240
View File
@@ -0,0 +1,240 @@
class_name SimulationDigest
extends RefCounted
## Produces a stable SHA-256 digest for snapshot-shaped Variant data.
##
## Dictionary insertion order never affects the result. Type tags and byte
## lengths make the canonical form unambiguous, and Array order remains
## significant. Runtime Objects and other process-local values are rejected;
## callers must first export them to Dictionaries, Arrays and value types.
const FORMAT_VERSION := 1
const MAX_NESTING_DEPTH := 128
static func compute(value: Variant) -> String:
var canonical := canonical_bytes(value)
if canonical.is_empty():
return ""
var context := HashingContext.new()
if context.start(HashingContext.HASH_SHA256) != OK:
push_error("Could not initialize the simulation digest hash")
return ""
context.update(canonical)
return context.finish().hex_encode()
static func canonical_bytes(value: Variant) -> PackedByteArray:
var output := PackedByteArray()
_append_ascii(output, "the-tree:simulation-digest:v%d;" % FORMAT_VERSION)
if not _encode_value(value, output, 0):
return PackedByteArray()
return output
static func _encode_value(value: Variant, output: PackedByteArray, depth: int) -> bool:
if depth > MAX_NESTING_DEPTH:
push_error("SimulationDigest data exceeds the maximum nesting depth")
return false
match typeof(value):
TYPE_NIL:
_append_ascii(output, "n")
TYPE_BOOL:
_append_ascii(output, "b1" if bool(value) else "b0")
TYPE_INT:
_append_ascii(output, "i%d;" % int(value))
TYPE_FLOAT:
_append_ascii(output, "f")
return _append_float64(output, float(value))
TYPE_STRING:
_append_sized_bytes(output, "s", String(value).to_utf8_buffer())
TYPE_STRING_NAME:
_append_sized_bytes(output, "q", String(value).to_utf8_buffer())
TYPE_VECTOR2:
var vector: Vector2 = value
_append_ascii(output, "v2")
return _append_float64(output, vector.x) and _append_float64(output, vector.y)
TYPE_VECTOR2I:
var vector: Vector2i = value
_append_ascii(output, "w2")
_append_ascii(output, "i%d;i%d;" % [vector.x, vector.y])
TYPE_RECT2:
var rect: Rect2 = value
_append_ascii(output, "r2")
return (
_append_float64(output, rect.position.x)
and _append_float64(output, rect.position.y)
and _append_float64(output, rect.size.x)
and _append_float64(output, rect.size.y)
)
TYPE_RECT2I:
var rect: Rect2i = value
_append_ascii(output, "x2")
_append_ascii(output, "i%d;i%d;i%d;i%d;" % [rect.position.x, rect.position.y, rect.size.x, rect.size.y])
TYPE_VECTOR3:
var vector: Vector3 = value
_append_ascii(output, "v3")
return (
_append_float64(output, vector.x)
and _append_float64(output, vector.y)
and _append_float64(output, vector.z)
)
TYPE_VECTOR3I:
var vector: Vector3i = value
_append_ascii(output, "w3")
_append_ascii(output, "i%d;i%d;i%d;" % [vector.x, vector.y, vector.z])
TYPE_VECTOR4:
var vector: Vector4 = value
_append_ascii(output, "v4")
return (
_append_float64(output, vector.x)
and _append_float64(output, vector.y)
and _append_float64(output, vector.z)
and _append_float64(output, vector.w)
)
TYPE_VECTOR4I:
var vector: Vector4i = value
_append_ascii(output, "w4")
_append_ascii(output, "i%d;i%d;i%d;i%d;" % [vector.x, vector.y, vector.z, vector.w])
TYPE_COLOR:
var color: Color = value
_append_ascii(output, "c")
return (
_append_float64(output, color.r)
and _append_float64(output, color.g)
and _append_float64(output, color.b)
and _append_float64(output, color.a)
)
TYPE_ARRAY:
return _encode_array(value, output, depth)
TYPE_DICTIONARY:
return _encode_dictionary(value, output, depth)
TYPE_PACKED_BYTE_ARRAY:
_append_sized_bytes(output, "pb", value)
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY:
return _encode_packed_int_array(value, output)
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY:
return _encode_packed_float_array(value, output)
TYPE_PACKED_STRING_ARRAY:
return _encode_packed_string_array(value, output)
TYPE_PACKED_VECTOR2_ARRAY:
return _encode_packed_vector2_array(value, output)
TYPE_PACKED_VECTOR3_ARRAY:
return _encode_packed_vector3_array(value, output)
TYPE_PACKED_COLOR_ARRAY:
return _encode_packed_color_array(value, output)
_:
push_error("SimulationDigest does not support Variant type %s" % type_string(typeof(value)))
return false
return true
static func _encode_array(value: Array, output: PackedByteArray, depth: int) -> bool:
_append_ascii(output, "a%d:" % value.size())
for item in value:
if not _encode_value(item, output, depth + 1):
return false
return true
static func _encode_dictionary(value: Dictionary, output: PackedByteArray, depth: int) -> bool:
var entries: Array[Dictionary] = []
for key in value:
var encoded_key := PackedByteArray()
if not _encode_value(key, encoded_key, depth + 1):
return false
entries.append({
"sort_key": encoded_key.hex_encode(),
"encoded_key": encoded_key,
"value": value[key],
})
entries.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
return String(left["sort_key"]) < String(right["sort_key"])
)
_append_ascii(output, "d%d:" % entries.size())
for entry in entries:
var encoded_key: PackedByteArray = entry["encoded_key"]
output.append_array(encoded_key)
if not _encode_value(entry["value"], output, depth + 1):
return false
return true
static func _encode_packed_int_array(value: Variant, output: PackedByteArray) -> bool:
var tag := "pi32" if typeof(value) == TYPE_PACKED_INT32_ARRAY else "pi64"
_append_ascii(output, "%s%d:" % [tag, value.size()])
for item in value:
_append_ascii(output, "i%d;" % int(item))
return true
static func _encode_packed_float_array(value: Variant, output: PackedByteArray) -> bool:
var tag := "pf32" if typeof(value) == TYPE_PACKED_FLOAT32_ARRAY else "pf64"
_append_ascii(output, "%s%d:" % [tag, value.size()])
for item in value:
if not _append_float64(output, float(item)):
return false
return true
static func _encode_packed_string_array(value: PackedStringArray, output: PackedByteArray) -> bool:
_append_ascii(output, "ps%d:" % value.size())
for item in value:
_append_sized_bytes(output, "s", item.to_utf8_buffer())
return true
static func _encode_packed_vector2_array(value: PackedVector2Array, output: PackedByteArray) -> bool:
_append_ascii(output, "pv2%d:" % value.size())
for item in value:
if not _append_float64(output, item.x) or not _append_float64(output, item.y):
return false
return true
static func _encode_packed_vector3_array(value: PackedVector3Array, output: PackedByteArray) -> bool:
_append_ascii(output, "pv3%d:" % value.size())
for item in value:
if (
not _append_float64(output, item.x)
or not _append_float64(output, item.y)
or not _append_float64(output, item.z)
):
return false
return true
static func _encode_packed_color_array(value: PackedColorArray, output: PackedByteArray) -> bool:
_append_ascii(output, "pc%d:" % value.size())
for item in value:
if (
not _append_float64(output, item.r)
or not _append_float64(output, item.g)
or not _append_float64(output, item.b)
or not _append_float64(output, item.a)
):
return false
return true
static func _append_float64(output: PackedByteArray, value: float) -> bool:
if is_nan(value) or is_inf(value):
push_error("SimulationDigest rejects NaN and infinite float values")
return false
var normalized := 0.0 if value == 0.0 else value
var encoded := PackedByteArray()
encoded.resize(8)
encoded.encode_double(0, normalized)
output.append_array(encoded)
return true
static func _append_sized_bytes(output: PackedByteArray, tag: String, value: PackedByteArray) -> void:
_append_ascii(output, "%s%d:" % [tag, value.size()])
output.append_array(value)
static func _append_ascii(output: PackedByteArray, value: String) -> void:
output.append_array(value.to_utf8_buffer())
+1
View File
@@ -0,0 +1 @@
uid://dxqv2isl2o0le
+258
View File
@@ -0,0 +1,258 @@
class_name SimulationKernel
extends Node
signal time_changed(day: int, hour: int, minute: int)
signal day_started(day: int)
signal settlement_requested(day: int)
signal tick_started(absolute_minute: int)
signal tick_phase_started(phase: TickPhase, absolute_minute: int)
signal tick_completed(absolute_minute: int)
signal simulation_overloaded(backlog_minutes: int)
enum TickPhase {
APPLY_COMMANDS,
TIME_BOUNDARIES,
MANA,
CONSTRUCTION,
LOGISTICS,
FACILITY_COMPLETION,
VITALS_AND_ENVIRONMENT,
INCIDENTS_AND_EVENTS,
FACILITY_QUEUES,
AI_DECISIONS,
PUBLISH_SNAPSHOT,
}
const MINUTES_PER_DAY := 24 * 60
const START_ABSOLUTE_MINUTE := 6 * 60
const REAL_SECONDS_PER_GAME_HOUR := 30.0
const REAL_SECONDS_PER_GAME_MINUTE := REAL_SECONDS_PER_GAME_HOUR / 60.0
const MAX_TICKS_PER_FRAME := 8
const MAX_BACKLOG_MINUTES := 120
var absolute_minute := START_ABSOLUTE_MINUTE
var day := 1
var minute_of_day := START_ABSOLUTE_MINUTE
var speed := 1
var paused := false
var snapshot_version := 0
var performance_budget := SimulationPerformanceBudget.new()
var _accumulator := 0.0
var _skip_next_process := false
var _phase_handlers: Dictionary = {}
var _pause_request_serial := 0
var _abort_current_tick_requested := false
var _last_tick_advanced := false
func _ready() -> void:
_emit_current_time()
func _process(delta: float) -> void:
process_real_delta(delta)
func process_real_delta(delta: float) -> int:
if _skip_next_process:
_skip_next_process = false
return 0
if paused or speed <= 0 or delta <= 0.0:
return 0
var frame_started_at_us := Time.get_ticks_usec()
_accumulator += delta * speed
var due_ticks := int(_accumulator / REAL_SECONDS_PER_GAME_MINUTE)
var tick_budget := mini(due_ticks, MAX_TICKS_PER_FRAME)
var ticks_run := 0
for _index in range(tick_budget):
if paused:
break
advance_one_minute()
if not _last_tick_advanced:
break
_accumulator -= REAL_SECONDS_PER_GAME_MINUTE
ticks_run += 1
# A boundary handler may pause in the middle of a catch-up frame. Never
# consume another queued minute after that decision.
if paused:
break
if ticks_run > 0:
performance_budget.record_simulation_frame(
int(Time.get_ticks_usec() - frame_started_at_us),
speed,
ticks_run
)
var backlog_minutes := get_backlog_minutes()
if backlog_minutes > MAX_BACKLOG_MINUTES:
paused = true
simulation_overloaded.emit(backlog_minutes)
return ticks_run
func reset(start_day := 1, start_hour := 6, start_minute := 0) -> void:
var safe_day := maxi(start_day, 1)
var safe_minute := posmod(start_hour * 60 + start_minute, MINUTES_PER_DAY)
absolute_minute = (safe_day - 1) * MINUTES_PER_DAY + safe_minute
_sync_calendar_fields()
_accumulator = 0.0
paused = false
snapshot_version = 0
performance_budget.clear()
_abort_current_tick_requested = false
_last_tick_advanced = false
_emit_current_time()
func advance_minutes(amount: int) -> void:
if amount <= 0:
return
# Tests and deterministic tooling historically use an already-paused clock as
# an explicit manual stepping mode. Preserve that contract, while a pause
# raised during an initially-running batch is a hard boundary stop.
var manual_paused_stepping := paused
var pause_serial_before := _pause_request_serial
for _index in range(amount):
if not manual_paused_stepping and paused:
break
advance_one_minute()
if not _last_tick_advanced:
break
if _pause_request_serial != pause_serial_before \
or (not manual_paused_stepping and paused):
break
func advance_one_minute() -> void:
_abort_current_tick_requested = false
_last_tick_advanced = false
tick_started.emit(absolute_minute)
_run_phase(TickPhase.APPLY_COMMANDS)
# A Runtime gate can reject the tick before the Clock changes. This is
# distinct from an ordinary pause raised by a command that already committed
# for the upcoming minute.
if _abort_current_tick_requested:
_abort_current_tick_requested = false
return
var previous_absolute_minute := absolute_minute
absolute_minute += 1
_sync_calendar_fields()
_handle_time_boundaries(previous_absolute_minute)
_run_phase(TickPhase.TIME_BOUNDARIES)
for phase in [
TickPhase.MANA,
TickPhase.CONSTRUCTION,
TickPhase.LOGISTICS,
TickPhase.FACILITY_COMPLETION,
TickPhase.VITALS_AND_ENVIRONMENT,
TickPhase.INCIDENTS_AND_EVENTS,
TickPhase.FACILITY_QUEUES,
TickPhase.AI_DECISIONS,
TickPhase.PUBLISH_SNAPSHOT,
]:
_run_phase(phase)
snapshot_version += 1
tick_completed.emit(absolute_minute)
_emit_current_time()
_abort_current_tick_requested = false
_last_tick_advanced = true
func request_current_tick_abort() -> void:
_abort_current_tick_requested = true
func set_speed(value: int) -> bool:
if value not in [1, 2, 4]:
return false
speed = value
paused = false
return true
func toggle_pause() -> bool:
paused = not paused
if paused:
_pause_request_serial += 1
return paused
func set_paused(value: bool) -> void:
paused = value
if value:
_pause_request_serial += 1
func get_hour() -> int:
return minute_of_day / 60
func get_minute() -> int:
return minute_of_day % 60
func get_backlog_minutes() -> int:
return int(_accumulator / REAL_SECONDS_PER_GAME_MINUTE)
func get_performance_reports() -> Array[Dictionary]:
return performance_budget.get_all_reports()
func register_tick_handler(phase: TickPhase, handler_id: StringName, callback: Callable) -> bool:
if handler_id.is_empty() or not callback.is_valid():
return false
if not _phase_handlers.has(phase):
_phase_handlers[phase] = {}
var handlers: Dictionary = _phase_handlers[phase]
if handlers.has(handler_id):
return false
handlers[handler_id] = callback
return true
func unregister_tick_handler(phase: TickPhase, handler_id: StringName) -> void:
if not _phase_handlers.has(phase):
return
var handlers: Dictionary = _phase_handlers[phase]
handlers.erase(handler_id)
func clock_snapshot() -> Dictionary:
return {
&"absolute_minute": absolute_minute,
&"speed": speed,
&"paused": paused,
&"snapshot_version": snapshot_version,
}
func restore_clock(snapshot: Dictionary) -> bool:
if not snapshot.has(&"absolute_minute"):
return false
var restored_speed := int(snapshot.get(&"speed", 1))
if restored_speed not in [1, 2, 4]:
return false
absolute_minute = maxi(int(snapshot[&"absolute_minute"]), 0)
speed = restored_speed
paused = bool(snapshot.get(&"paused", true))
snapshot_version = maxi(int(snapshot.get(&"snapshot_version", 0)), 0)
_accumulator = 0.0
_skip_next_process = true
_sync_calendar_fields()
_emit_current_time()
return true
func _run_phase(phase: TickPhase) -> void:
tick_phase_started.emit(phase, absolute_minute)
if not _phase_handlers.has(phase):
return
var handlers: Dictionary = _phase_handlers[phase]
var handler_ids: Array = handlers.keys()
handler_ids.sort_custom(func(left: Variant, right: Variant) -> bool:
return String(left) < String(right)
)
for handler_id: Variant in handler_ids:
var callback: Callable = handlers[handler_id]
if callback.is_valid():
callback.call(absolute_minute)
func _sync_calendar_fields() -> void:
day = absolute_minute / MINUTES_PER_DAY + 1
minute_of_day = absolute_minute % MINUTES_PER_DAY
func _handle_time_boundaries(previous_absolute_minute: int) -> void:
if minute_of_day == 0:
var completed_day := previous_absolute_minute / MINUTES_PER_DAY + 1
settlement_requested.emit(completed_day)
day_started.emit(day)
func _emit_current_time() -> void:
time_changed.emit(day, get_hour(), get_minute())
+1
View File
@@ -0,0 +1 @@
uid://vj3mrw0wb5bx
+47
View File
@@ -0,0 +1,47 @@
class_name SimulationSnapshot
extends RefCounted
const SCHEMA_VERSION := 1
static func capture(clock: SimulationKernel, state: Node, extra := {}) -> Dictionary:
var snapshot := {
&"schema_version": SCHEMA_VERSION,
&"clock": clock.clock_snapshot(),
&"game_state": state.create_snapshot(),
}
if extra is Dictionary and not extra.is_empty():
snapshot[&"systems"] = extra.duplicate(true)
return snapshot
static func validate(snapshot: Dictionary) -> Dictionary:
var errors: Array[String] = []
if int(snapshot.get(&"schema_version", -1)) != SCHEMA_VERSION:
errors.append("unsupported_schema_version")
if not snapshot.get(&"clock", null) is Dictionary:
errors.append("missing_clock")
else:
var clock_data: Dictionary = snapshot[&"clock"]
if int(clock_data.get(&"absolute_minute", -1)) < 0:
errors.append("invalid_absolute_minute")
if int(clock_data.get(&"speed", 0)) not in [1, 2, 4]:
errors.append("invalid_speed")
if not snapshot.get(&"game_state", null) is Dictionary:
errors.append("missing_game_state")
return {
&"ok": errors.is_empty(),
&"errors": errors,
}
static func restore(snapshot: Dictionary, clock: SimulationKernel, state: Node) -> bool:
var validation := validate(snapshot)
if not validation[&"ok"]:
return false
var previous_clock := clock.clock_snapshot()
var previous_state: Dictionary = state.create_snapshot()
if not clock.restore_clock(snapshot[&"clock"]):
return false
if not state.restore_snapshot(snapshot[&"game_state"]):
clock.restore_clock(previous_clock)
state.restore_snapshot(previous_state)
return false
return true
+1
View File
@@ -0,0 +1 @@
uid://c0vxraq44s1wu
+60
View File
@@ -0,0 +1,60 @@
class_name StableIdGenerator
extends RefCounted
const SNAPSHOT_VERSION := 1
const MAX_COUNTER := 999999999999
var _counters: Dictionary = {}
func next_id(prefix: StringName) -> StringName:
if not _is_valid_prefix(String(prefix)):
push_error("Stable ID prefix must be lowercase ASCII snake_case")
return &""
var next_value := int(_counters.get(prefix, 0)) + 1
if next_value > MAX_COUNTER:
push_error("Stable ID counter exhausted for '%s'" % prefix)
return &""
_counters[prefix] = next_value
return StringName("%s.%012d" % [prefix, next_value])
func get_counter(prefix: StringName) -> int:
return int(_counters.get(prefix, 0))
func create_snapshot() -> Dictionary:
var counters := {}
var prefixes: Array = _counters.keys()
prefixes.sort()
for prefix: Variant in prefixes:
counters[String(prefix)] = int(_counters[prefix])
return {
"version": SNAPSHOT_VERSION,
"counters": counters,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
if int(snapshot.get("version", -1)) != SNAPSHOT_VERSION:
return false
var saved_counters: Variant = snapshot.get("counters", null)
if not saved_counters is Dictionary:
return false
var restored := {}
for prefix: Variant in saved_counters:
var normalized_prefix := String(prefix)
var counter := int(saved_counters[prefix])
if not _is_valid_prefix(normalized_prefix) or counter < 0 or counter > MAX_COUNTER:
return false
restored[StringName(normalized_prefix)] = counter
_counters = restored
return true
func reset() -> void:
_counters.clear()
static func _is_valid_prefix(prefix: String) -> bool:
if prefix.is_empty() or prefix[0] < "a" or prefix[0] > "z":
return false
for character in prefix:
if (character >= "a" and character <= "z") or (character >= "0" and character <= "9") or character == "_":
continue
return false
return true
+1
View File
@@ -0,0 +1 @@
uid://6dn6weviv7a8
+266
View File
@@ -0,0 +1,266 @@
class_name StateTransaction
extends RefCounted
enum Status {
OPEN,
COMMITTED,
ROLLED_BACK,
FAILED,
}
class StagedChange:
extends RefCounted
var label := ""
var validate_action := Callable()
var apply_action := Callable()
var rollback_action := Callable()
func _init(
p_label: String,
p_validate_action: Callable,
p_apply_action: Callable,
p_rollback_action: Callable
) -> void:
label = p_label
validate_action = p_validate_action
apply_action = p_apply_action
rollback_action = p_rollback_action
var status: Status = Status.OPEN
var _changes: Array[StagedChange] = []
var _validators: Array[Callable] = []
var _configuration_errors: Array[String] = []
func stage_dictionary_set(
target: Dictionary,
key: Variant,
value: Variant,
label: String = ""
) -> bool:
if not _require_open("stage_dictionary_set"):
return false
var had_key := target.has(key)
var original_value: Variant = target.get(key)
var original_snapshot: Variant = _copy_value(original_value)
var staged_value: Variant = _copy_value(value)
var change_label := label if not label.is_empty() else "set dictionary key %s" % str(key)
return stage_custom(
change_label,
func() -> Variant:
if target.has(key) != had_key:
return "Dictionary key presence changed before commit."
if had_key and target[key] != original_snapshot:
return "Dictionary value changed before commit."
return true,
func() -> Variant:
target[key] = _copy_value(staged_value)
return true,
func() -> void:
if had_key:
target[key] = original_value
else:
target.erase(key)
)
func stage_dictionary_erase(
target: Dictionary,
key: Variant,
label: String = ""
) -> bool:
if not _require_open("stage_dictionary_erase"):
return false
var had_key := target.has(key)
var original_value: Variant = target.get(key)
var original_snapshot: Variant = _copy_value(original_value)
var change_label := label if not label.is_empty() else "erase dictionary key %s" % str(key)
return stage_custom(
change_label,
func() -> Variant:
if target.has(key) != had_key:
return "Dictionary key presence changed before commit."
if had_key and target[key] != original_snapshot:
return "Dictionary value changed before commit."
return true,
func() -> Variant:
target.erase(key)
return true,
func() -> void:
if had_key:
target[key] = original_value
)
func stage_dictionary_increment(
target: Dictionary,
key: Variant,
delta: Variant,
minimum: Variant = null,
maximum: Variant = null,
label: String = ""
) -> bool:
if not _require_open("stage_dictionary_increment"):
return false
if not target.has(key) or not _is_number(target[key]) or not _is_number(delta):
_configuration_errors.append("Increment requires an existing numeric value and numeric delta.")
return false
var next_value: Variant = target[key] + delta
if minimum != null and (not _is_number(minimum) or next_value < minimum):
_configuration_errors.append("Increment result is below its minimum.")
return false
if maximum != null and (not _is_number(maximum) or next_value > maximum):
_configuration_errors.append("Increment result is above its maximum.")
return false
return stage_dictionary_set(target, key, next_value, label)
func add_validator(validator: Callable) -> bool:
if not _require_open("add_validator"):
return false
if not validator.is_valid():
_configuration_errors.append("Validator is not callable.")
return false
_validators.append(validator)
return true
func stage_custom(
label: String,
validate_action: Callable,
apply_action: Callable,
rollback_action: Callable
) -> bool:
if not _require_open("stage_custom"):
return false
if not apply_action.is_valid() or not rollback_action.is_valid():
_configuration_errors.append("Custom change '%s' requires apply and rollback actions." % label)
return false
_changes.append(StagedChange.new(label, validate_action, apply_action, rollback_action))
return true
func validate() -> CommandResult:
if status != Status.OPEN:
return CommandResult.failed(
CommandResult.Code.TRANSACTION_CLOSED,
0,
"Transaction is not open."
)
if not _configuration_errors.is_empty():
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
0,
_configuration_errors[0],
{"errors": _configuration_errors.duplicate()}
)
for index in _validators.size():
var validator_result := _validation_result(_validators[index].call(), "validator %d" % index)
if validator_result != null:
return validator_result
for change in _changes:
if not change.validate_action.is_valid():
continue
var change_result := _validation_result(change.validate_action.call(), change.label)
if change_result != null:
return change_result
return CommandResult.succeeded()
func commit() -> CommandResult:
var validation_result := validate()
if not validation_result.is_success():
if status == Status.OPEN:
status = Status.FAILED
return validation_result
var applied: Array[StagedChange] = []
for change in _changes:
var returned: Variant = change.apply_action.call()
var apply_failure := _apply_result(returned, change.label)
if apply_failure != null:
change.rollback_action.call()
for index in range(applied.size() - 1, -1, -1):
applied[index].rollback_action.call()
status = Status.ROLLED_BACK
apply_failure.details["rolled_back"] = true
return apply_failure
applied.append(change)
status = Status.COMMITTED
return CommandResult.succeeded(0, {"change_count": _changes.size()})
func rollback() -> CommandResult:
if status != Status.OPEN:
return CommandResult.failed(
CommandResult.Code.TRANSACTION_CLOSED,
0,
"Only an open transaction can be cancelled."
)
status = Status.ROLLED_BACK
return CommandResult.succeeded()
func change_count() -> int:
return _changes.size()
func _require_open(operation: String) -> bool:
if status == Status.OPEN:
return true
_configuration_errors.append("%s cannot run on a closed transaction." % operation)
return false
func _validation_result(returned: Variant, label: String) -> CommandResult:
if returned == null or (returned is bool and returned):
return null
if returned is CommandResult:
var command_result: CommandResult = returned
if command_result.is_success():
return null
return command_result.copy()
var message := str(returned) if returned is String and not returned.is_empty() else "Validation failed."
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
0,
"%s: %s" % [label, message],
{"change": label}
)
func _apply_result(returned: Variant, label: String) -> CommandResult:
if returned == null or (returned is bool and returned):
return null
if returned is CommandResult:
var command_result: CommandResult = returned
if command_result.is_success():
return null
return command_result.copy()
var message := str(returned) if returned is String and not returned.is_empty() else "Apply action failed."
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
0,
"%s: %s" % [label, message],
{"change": label}
)
static func _copy_value(value: Variant) -> Variant:
if value is Dictionary or value is Array:
return value.duplicate(true)
return value
static func _is_number(value: Variant) -> bool:
return value is int or value is float
+1
View File
@@ -0,0 +1 @@
uid://drilrnwk4am7v