Files
2026-01-15 15:27:48 +01:00

220 lines
8.3 KiB
GDScript

## GECS IO Utility Class[br]
##
## Provides functions for generating UUIDs, serializing/deserializing [Entity]s to/from [GecsData],
## and saving/loading [GecsData] to/from files.
class_name GECSIO
## Generates a custom GUID using random bytes.[br]
## The format uses 16 random bytes encoded to hex and formatted with hyphens.
static func uuid() -> String:
const BYTE_MASK: int = 0b11111111
# 16 random bytes with the bytes on index 6 and 8 modified
var b = [
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK,
((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
]
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [
# low
b[0], b[1], b[2], b[3],
# mid
b[4], b[5],
# hi
b[6], b[7],
# clock
b[8], b[9],
# clock
b[10], b[11], b[12], b[13], b[14], b[15]
]
## Serialize a [QueryBuilder] of [Entity](s) to [GecsData] format.[br]
## Optionally takes a [GECSSerializeConfig] to customize what gets serialized.
static func serialize(query: QueryBuilder, config: GECSSerializeConfig = null) -> GecsData:
return serialize_entities(query.execute() as Array[Entity], config)
## Serialize a list of [Entity](s) to [GecsData] format.[br]
## Optionally takes a [GECSSerializeConfig] to customize what gets serialized.
static func serialize_entities(entities: Array, config: GECSSerializeConfig = null) -> GecsData:
# Pass 1: Serialize entities from original query
var entity_data_array: Array[GecsEntityData] = []
var processed_entities: Dictionary = {} # id -> bool
var entity_id_mapping: Dictionary = {} # id -> Entity
for entity in entities:
var effective_config = _resolve_config(entity, config)
var entity_data = _serialize_entity(entity, false, effective_config)
entity_data_array.append(entity_data)
processed_entities[entity.id] = true
entity_id_mapping[entity.id] = entity
# Pass 2: Scan relationships and auto-include referenced entities (if enabled)
var entities_to_check = entities.duplicate()
var check_index = 0
while check_index < entities_to_check.size():
var entity = entities_to_check[check_index]
var effective_config = _resolve_config(entity, config)
# Only proceed if config allows including related entities
if effective_config.include_related_entities:
# Check all relationships of this entity
for relationship in entity.relationships:
if relationship.target is Entity:
var target_entity = relationship.target as Entity
var target_id = target_entity.id
# If this entity hasn't been processed yet, auto-include it
if not processed_entities.has(target_id):
var target_config = _resolve_config(target_entity, config)
var auto_entity_data = _serialize_entity(target_entity, true, target_config)
entity_data_array.append(auto_entity_data)
processed_entities[target_id] = true
entity_id_mapping[target_id] = target_entity
# Add to list for further relationship checking
entities_to_check.append(target_entity)
check_index += 1
return GecsData.new(entity_data_array)
## Save [GecsData] to a file at the specified path.[br]
## If binary is true, saves in binary format (.res), otherwise text format (.tres).
static func save(gecs_data: GecsData, filepath: String, binary: bool = false) -> bool:
var final_path = filepath
var flags = 0
if binary:
# Convert .tres to .res for binary format
final_path = filepath.replace(".tres", ".res")
flags = ResourceSaver.FLAG_COMPRESS # Binary format uses no flags, .res extension determines format
# else: text format (default flags = 0)
var result = ResourceSaver.save(gecs_data, final_path, flags)
if result != OK:
push_error("GECS save: Failed to save resource to: " + final_path)
return false
return true
## Load and deserialize [Entity](s) from a file at the specified path.[br]
## Supports both binary (.res) and text (.tres) formats, tries binary first.
static func deserialize(gecs_filepath: String) -> Array[Entity]:
# Try binary first (.res), then text (.tres)
var binary_path = gecs_filepath.replace(".tres", ".res")
if ResourceLoader.exists(binary_path):
return _load_from_path(binary_path)
elif ResourceLoader.exists(gecs_filepath):
return _load_from_path(gecs_filepath)
else:
push_error("GECS deserialize: File not found: " + gecs_filepath)
return []
## Deserialize [GecsData] into a list of [Entity](s).[br]
## This can be used so you can serialize entities to GECS Data and then Deserailize that [GecsSData] later
static func deserialize_gecs_data(gecs_data: GecsData) -> Array[Entity]:
var entities: Array[Entity] = []
var id_to_entity: Dictionary = {} # id -> Entity
# Pass 1: Create all entities and build ID mapping
for entity_data in gecs_data.entities:
var entity = _deserialize_entity(entity_data)
entities.append(entity)
id_to_entity[entity.id] = entity
# Pass 2: Restore relationships using ID mapping
for i in entities.size():
var entity = entities[i]
var entity_data = gecs_data.entities[i]
# Restore relationships
for rel_data in entity_data.relationships:
var relationship = rel_data.to_relationship(id_to_entity)
if relationship != null:
entity.add_relationship(relationship)
# Note: Invalid relationships are skipped with warning logged in to_relationship()
return entities
## Helper function to resolve the effective configuration for an entity
## Priority: provided_config > entity.serialize_config > world.default_serialize_config > fallback
static func _resolve_config(entity: Entity, provided_config: GECSSerializeConfig) -> GECSSerializeConfig:
if provided_config != null:
return provided_config
return entity.get_effective_serialize_config()
## Helper function to serialize a single entity with its components and relationships
static func _serialize_entity(entity: Entity, auto_included: bool, config: GECSSerializeConfig) -> GecsEntityData:
# Serialize components (filtered by config)
var components: Array[Component] = []
for component in entity.components.values():
if config.should_include_component(component):
# Duplicate the component to avoid modifying the original
components.append(component.duplicate(true))
# Serialize relationships (if enabled by config)
var relationships: Array[GecsRelationshipData] = []
if config.include_relationships:
for relationship in entity.relationships:
var rel_data = GecsRelationshipData.from_relationship(relationship)
relationships.append(rel_data)
return GecsEntityData.new(
entity.name,
entity.scene_file_path if entity.scene_file_path != "" else "",
components,
relationships,
auto_included,
entity.id
)
## Helper function to load and deserialize entities from a given file path
static func _load_from_path(file_path: String) -> Array[Entity]:
print("GECS _load_from_path: Loading file: ", file_path)
var gecs_data = load(file_path) as GecsData
if not gecs_data:
push_error("GECS deserialize: Could not load GecsData resource: " + file_path)
return []
print("GECS _load_from_path: Loaded GecsData with ", gecs_data.entities.size(), " entities")
return deserialize_gecs_data(gecs_data)
## Helper function to deserialize a single entity with its components and uuid
static func _deserialize_entity(entity_data: GecsEntityData) -> Entity:
var entity: Entity
# Check if this entity is a prefab (has scene file)
if entity_data.scene_path != "":
var scene_path = entity_data.scene_path
if ResourceLoader.exists(scene_path):
var packed_scene = load(scene_path) as PackedScene
if packed_scene:
entity = packed_scene.instantiate() as Entity
else:
push_warning("GECS deserialize: Could not load scene: " + scene_path + ", creating new entity")
entity = Entity.new()
else:
push_warning("GECS deserialize: Scene file not found: " + scene_path + ", creating new entity")
entity = Entity.new()
else:
# Create new entity
entity = Entity.new()
# Set entity name
entity.name = entity_data.entity_name
# Restore id (important: set this directly)
entity.id = entity_data.id
# Add components (they're already properly typed as Component resources)
for component in entity_data.components:
entity.add_component(component.duplicate(true))
return entity