From 7e81c09265d20215cdaa6a406f70f042bf26baaf Mon Sep 17 00:00:00 2001 From: agent-bot Date: Mon, 17 Aug 2026 17:02:03 +0000 Subject: [PATCH 01/20] WIP checkpoint: Implement chunk streaming system in GDScript --- project.godot | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 project.godot diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..b323d0f --- /dev/null +++ b/project.godot @@ -0,0 +1,20 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; but it can also be edited by hand. + +config_version=5 + +[application] + +config/name="AGP-Game" +run/main_scene="res://scenes/main.tscn" +config/features=PackedStringArray("4.6.2.stable") + +[autoload] + +ChunkStream="*res://scripts/chunk_stream.gd" + +[display] + +window/size/viewport_width=1280 +window/size/viewport_height=720 From 2ad8e3497172d947c567ec2eff8cc65ef5937493 Mon Sep 17 00:00:00 2001 From: agent-bot Date: Mon, 17 Aug 2026 17:04:38 +0000 Subject: [PATCH 02/20] WIP checkpoint: Implement chunk streaming system in GDScript --- project.godot | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/project.godot b/project.godot index b323d0f..dc4b3c2 100644 --- a/project.godot +++ b/project.godot @@ -6,15 +6,10 @@ config_version=5 [application] -config/name="AGP-Game" -run/main_scene="res://scenes/main.tscn" +config/name="AGP Game" +run/main_scene="" config/features=PackedStringArray("4.6.2.stable") [autoload] -ChunkStream="*res://scripts/chunk_stream.gd" - -[display] - -window/size/viewport_width=1280 -window/size/viewport_height=720 +ChunkStreamer="*res://scripts/chunk_streamer.gd" From 2d4a7e91cb36eefedc7cdc859f1bba42e0873cec Mon Sep 17 00:00:00 2001 From: agent-bot Date: Mon, 17 Aug 2026 17:06:15 +0000 Subject: [PATCH 03/20] WIP checkpoint: Implement chunk streaming system in GDScript --- project.godot | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project.godot b/project.godot index dc4b3c2..7963ea5 100644 --- a/project.godot +++ b/project.godot @@ -7,9 +7,9 @@ config_version=5 [application] config/name="AGP Game" -run/main_scene="" -config/features=PackedStringArray("4.6.2.stable") +run/main_scene="res://scenes/main.tscn" +config/features=PackedStringArray("4.6.2") -[autoload] +[rendering] -ChunkStreamer="*res://scripts/chunk_streamer.gd" +renderer/rendering_method="forward_plus" From 8e2f17f159ddd8fedca069f66c87cd3771cb3faf Mon Sep 17 00:00:00 2001 From: agent-bot Date: Mon, 17 Aug 2026 17:08:19 +0000 Subject: [PATCH 04/20] WIP checkpoint: Implement chunk streaming system in GDScript --- project.godot | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project.godot b/project.godot index 7963ea5..331cf39 100644 --- a/project.godot +++ b/project.godot @@ -10,6 +10,7 @@ config/name="AGP Game" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.6.2") -[rendering] +[display] -renderer/rendering_method="forward_plus" +window/size/viewport_width=1280 +window/size/viewport_height=720 From ce2690e8b9a764303d495fb8db8b6e3f1bc26b25 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 21:23:26 +0000 Subject: [PATCH 05/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/chunk.gd | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 scripts/chunk.gd diff --git a/scripts/chunk.gd b/scripts/chunk.gd new file mode 100644 index 0000000..1cbc21d --- /dev/null +++ b/scripts/chunk.gd @@ -0,0 +1,82 @@ +class_name Chunk +extends Node2D +## A single loaded chunk in the world. +## +## A chunk is a fixed-size, axis-aligned region of a room. It is the atomic +## unit of load, save, and stream. This node is instantiated by the +## [ChunkStreamer] when a chunk is loaded from the server and freed when the +## chunk is unloaded. + +## World-space size of a chunk along X (world units / tiles). +const CHUNK_SIZE_X := 32.0 +## World-space size of a chunk along Y (world units / tiles). +const CHUNK_SIZE_Y := 32.0 +## World-space size of a chunk along Z (world units / tiles). +const CHUNK_SIZE_Z := 1.0 + +## Canonical chunk key: "{world_id}:{room_id}:{cx}:{cy}:{cz}". +var key: String = "" +## World id this chunk belongs to. +var world_id: String = "" +## Room id this chunk belongs to. +var room_id: String = "" +## Integer chunk coordinate along X. +var cx: int = 0 +## Integer chunk coordinate along Y. +var cy: int = 0 +## Integer chunk coordinate along Z. +var cz: int = 0 + +## Whether this chunk is currently loaded and active in the scene tree. +var is_loaded := false + +## Emitted when the chunk has finished loading its content. +signal chunk_loaded(chunk: Chunk) +## Emitted when the chunk is about to be unloaded. +signal chunk_unloaded(chunk: Chunk) + + +## Build the canonical chunk key from the chunk's coordinates. +func build_key() -> String: + return "%s:%s:%d:%d:%d" % [world_id, room_id, cx, cy, cz] + + +## Compute the world-space origin (min corner) of this chunk. +func world_origin() -> Vector2: + return Vector2(cx * CHUNK_SIZE_X, cy * CHUNK_SIZE_Y) + + +## Compute the world-space bounding box of this chunk. +func world_bounds() -> Rect2: + var origin := world_origin() + return Rect2(origin, Vector2(CHUNK_SIZE_X, CHUNK_SIZE_Y)) + + +## Compute the integer chunk coordinate for a world-space position. +static func coord_for_position(pos: Vector2) -> Vector2i: + return Vector2i( + floori(pos.x / CHUNK_SIZE_X), + floori(pos.y / CHUNK_SIZE_Y) + ) + + +## Compute the canonical key for a world-space position. +static func key_for_position(pos: Vector2, p_world_id: String, p_room_id: String) -> String: + var c := coord_for_position(pos) + return "%s:%s:%d:%d:0" % [p_world_id, p_room_id, c.x, c.y] + + +## Load this chunk's content. In a real deployment this would fetch the chunk +## payload from the server (see world.md §5.3 `request_chunk` / `chunk_data`) +## and instantiate tiles/objects. For now it just marks the chunk loaded. +func load_chunk() -> void: + is_loaded = true + chunk_loaded.emit(self) + + +## Unload this chunk's content and free any child nodes. +func unload_chunk() -> void: + chunk_unloaded.emit(self) + for child in get_children(): + child.queue_free() + is_loaded = false From 62172e0ee4e50e7eb25aa2dae53d267c552c47bb Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 21:23:53 +0000 Subject: [PATCH 06/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/chunk.gd | 139 +++++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/scripts/chunk.gd b/scripts/chunk.gd index 1cbc21d..bb9d8cf 100644 --- a/scripts/chunk.gd +++ b/scripts/chunk.gd @@ -1,82 +1,105 @@ class_name Chunk -extends Node2D -## A single loaded chunk in the world. +extends Node3D +## A single world chunk. Holds the chunk coordinate, bounds, and the +## visual/gameplay content for that region of the world. ## -## A chunk is a fixed-size, axis-aligned region of a room. It is the atomic -## unit of load, save, and stream. This node is instantiated by the -## [ChunkStreamer] when a chunk is loaded from the server and freed when the -## chunk is unloaded. +## Chunks are the atomic unit of world geometry, gameplay objects, and +## persistence (see docs/architecture/world.md). The server loads and saves +## whole chunks; clients stream the chunks around them. -## World-space size of a chunk along X (world units / tiles). -const CHUNK_SIZE_X := 32.0 -## World-space size of a chunk along Y (world units / tiles). -const CHUNK_SIZE_Y := 32.0 -## World-space size of a chunk along Z (world units / tiles). -const CHUNK_SIZE_Z := 1.0 +## Default chunk dimensions in world units (tiles). Matches the constants in +## docs/architecture/world.md section 2.2. +const CHUNK_SIZE_X: int = 32 +const CHUNK_SIZE_Y: int = 32 +const CHUNK_SIZE_Z: int = 1 -## Canonical chunk key: "{world_id}:{room_id}:{cx}:{cy}:{cz}". +## World-space origin of chunk coordinate (0,0). +const CHUNK_ORIGIN := Vector3(0.0, 0.0, 0.0) + +## Chunk coordinate (cx, cy, cz) in chunk space. +var coord := Vector3i.ZERO + +## The canonical chunk key: "{world_id}:{room_id}:{cx}:{cy}:{cz}". var key: String = "" -## World id this chunk belongs to. + +## World and room identifiers this chunk belongs to. var world_id: String = "" -## Room id this chunk belongs to. var room_id: String = "" -## Integer chunk coordinate along X. -var cx: int = 0 -## Integer chunk coordinate along Y. -var cy: int = 0 -## Integer chunk coordinate along Z. -var cz: int = 0 -## Whether this chunk is currently loaded and active in the scene tree. -var is_loaded := false +## Format version of the chunk payload (currently 1). +var format_version: int = 1 -## Emitted when the chunk has finished loading its content. -signal chunk_loaded(chunk: Chunk) -## Emitted when the chunk is about to be unloaded. -signal chunk_unloaded(chunk: Chunk) +## Revision counter, incremented on every write. Used for optimistic +## concurrency and change detection. +var revision: int = 0 + +## Whether this chunk has been modified in memory but not yet flushed. +var dirty: bool = false + +## Container for static objects (props, geometry) in this chunk. +var objects: Array[Node3D] = [] + +## Container for dynamic object templates (spawnable entities). +var dynamic_templates: Array[Node3D] = [] + +## The world-space bounding box of this chunk. +var bounds: AABB -## Build the canonical chunk key from the chunk's coordinates. -func build_key() -> String: - return "%s:%s:%d:%d:%d" % [world_id, room_id, cx, cy, cz] +func _init(p_coord: Vector3i = Vector3i.ZERO, p_world_id: String = "", p_room_id: String = "") -> void: + coord = p_coord + world_id = p_world_id + room_id = p_room_id + key = build_key(world_id, room_id, coord) + bounds = compute_bounds(coord) -## Compute the world-space origin (min corner) of this chunk. -func world_origin() -> Vector2: - return Vector2(cx * CHUNK_SIZE_X, cy * CHUNK_SIZE_Y) +## Build the canonical chunk key from its components. +static func build_key(p_world_id: String, p_room_id: String, p_coord: Vector3i) -> String: + return "%s:%s:%d:%d:%d" % [p_world_id, p_room_id, p_coord.x, p_coord.y, p_coord.z] -## Compute the world-space bounding box of this chunk. -func world_bounds() -> Rect2: - var origin := world_origin() - return Rect2(origin, Vector2(CHUNK_SIZE_X, CHUNK_SIZE_Y)) +## Compute the world-space bounding box for a chunk coordinate. +static func compute_bounds(p_coord: Vector3i) -> AABB: + var min_pos := Vector3( + CHUNK_ORIGIN.x + p_coord.x * CHUNK_SIZE_X, + CHUNK_ORIGIN.y + p_coord.y * CHUNK_SIZE_Y, + CHUNK_ORIGIN.z + p_coord.z * CHUNK_SIZE_Z + ) + return AABB(min_pos, Vector3(CHUNK_SIZE_X, CHUNK_SIZE_Y, CHUNK_SIZE_Z)) -## Compute the integer chunk coordinate for a world-space position. -static func coord_for_position(pos: Vector2) -> Vector2i: - return Vector2i( - floori(pos.x / CHUNK_SIZE_X), - floori(pos.y / CHUNK_SIZE_Y) +## Compute the chunk coordinate that contains a world-space position. +static func coord_for_position(p_pos: Vector3) -> Vector3i: + return Vector3i( + int(floor((p_pos.x - CHUNK_ORIGIN.x) / CHUNK_SIZE_X)), + int(floor((p_pos.y - CHUNK_ORIGIN.y) / CHUNK_SIZE_Y)), + int(floor((p_pos.z - CHUNK_ORIGIN.z) / CHUNK_SIZE_Z)) ) -## Compute the canonical key for a world-space position. -static func key_for_position(pos: Vector2, p_world_id: String, p_room_id: String) -> String: - var c := coord_for_position(pos) - return "%s:%s:%d:%d:0" % [p_world_id, p_room_id, c.x, c.y] +## Add a static object to this chunk. +func add_object(p_object: Node3D) -> void: + objects.append(p_object) + add_child(p_object) -## Load this chunk's content. In a real deployment this would fetch the chunk -## payload from the server (see world.md §5.3 `request_chunk` / `chunk_data`) -## and instantiate tiles/objects. For now it just marks the chunk loaded. -func load_chunk() -> void: - is_loaded = true - chunk_loaded.emit(self) +## Add a dynamic object template to this chunk. +func add_dynamic_template(p_template: Node3D) -> void: + dynamic_templates.append(p_template) + add_child(p_template) -## Unload this chunk's content and free any child nodes. -func unload_chunk() -> void: - chunk_unloaded.emit(self) - for child in get_children(): - child.queue_free() - is_loaded = false +## Remove all objects from this chunk. +func clear_objects() -> void: + for obj in objects: + if is_instance_valid(obj): + obj.queue_free() + objects.clear() + dynamic_templates.clear() + + +## Mark this chunk as dirty (modified but not yet persisted). +func mark_dirty() -> void: + dirty = true + revision += 1 From cc27d73ca661ba2c069f60653526624771926796 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 21:36:13 +0000 Subject: [PATCH 07/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/chunk_stream_manager.gd | 275 ++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 scripts/chunk_stream_manager.gd diff --git a/scripts/chunk_stream_manager.gd b/scripts/chunk_stream_manager.gd new file mode 100644 index 0000000..40b833a --- /dev/null +++ b/scripts/chunk_stream_manager.gd @@ -0,0 +1,275 @@ +class_name ChunkStreamManager +extends Node +## Manages chunk load/unload based on player position. +## +## Chunks are loaded from the server and instantiated as [ChunkScene] nodes. +## The manager maintains a view radius around the player and only loads chunks +## within that radius. Chunks outside the radius are unloaded to save memory. +## +## This implements the streaming system described in docs/architecture/world.md +## section 5.6 (Streaming & Caching). + +## Radius (in chunks) around the player within which chunks are loaded. +const VIEW_RADIUS: int = 4 + +## Minimum distance (in chunks) the player must move before the manager +## re-evaluates which chunks need to be loaded/unloaded. This prevents +## excessive chunk churn when the player is standing still. +const MIN_MOVE_DISTANCE: float = 1.0 + +## Number of chunks to load/unload per frame (prevents frame spikes). +const CHUNKS_PER_FRAME: int = 4 + +## Signal emitted when a chunk is successfully loaded. +signal chunk_loaded(chunk_key: String, chunk_scene: Node3D) + +## Signal emitted when a chunk is unloaded and freed. +signal chunk_unloaded(chunk_key: String) + +## Signal emitted when a chunk load fails. +signal chunk_load_failed(chunk_key: String, error: String) + +## The player node whose position drives chunk streaming. +var player: Node3D = null + +## Set of currently loaded chunk keys. +var loaded_chunks: Dictionary = {} + +## Set of chunk keys that are currently being loaded (async). +var loading_chunks: Dictionary = {} + +## The player's position at the last evaluation. +var last_player_position: Vector3 = Vector3.ZERO + +## Whether streaming is active (player exists and is in-game). +var is_active: bool = false + +## Reference to the server connection for chunk requests. +var server: NetworkedMultiplayerENet = null + + +func _ready() -> void: + last_player_position = Vector3.ZERO + + +## Set the player node that drives chunk streaming. +func set_player(p_player: Node3D) -> void: + player = p_player + if player != null: + last_player_position = player.global_position + is_active = true + else: + is_active = false + + +## Set the network server reference for chunk requests. +func set_server(p_server: NetworkedMultiplayerENet) -> void: + server = p_server + + +## Called every frame to evaluate chunk loading/unloading. +func _process(_delta: float) -> void: + if not is_active or player == null: + return + + # Check if player has moved enough to warrant re-evaluation. + var current_pos := player.global_position + var move_distance := (current_pos - last_player_position).length() + if move_distance < MIN_MOVE_DISTANCE: + return + + last_player_position = current_pos + + # Evaluate which chunks need to be loaded/unloaded. + _evaluate_chunks() + + +## Evaluate which chunks to load and which to unload. +func _evaluate_chunks() -> void: + if player == null: + return + + var player_chunk_coord := Chunk.coord_for_position(player.global_position) + var chunks_to_load: PackedStringArray = [] + var chunks_to_unload: PackedStringArray = [] + + # Determine which chunks should be loaded. + for loaded_key in loaded_chunks.keys(): + var coord := _parse_chunk_key(loaded_key) + if coord == null: + continue + + var dist := _chunk_distance(player_chunk_coord, coord) + if dist > VIEW_RADIUS: + chunks_to_unload.append(loaded_key) + + # Determine which chunks should be loaded. + for dx in range(-VIEW_RADIUS, VIEW_RADIUS + 1): + for dy in range(-VIEW_RADIUS, VIEW_RADIUS + 1): + for dz in range(-1, 2): + var target_coord := Vector3i( + player_chunk_coord.x + dx, + player_chunk_coord.y + dy, + player_chunk_coord.z + dz + ) + + # Skip chunks outside the spherical view radius. + if _chunk_distance(player_chunk_coord, target_coord) > VIEW_RADIUS: + continue + + var world_id := _get_world_id() + var room_id := _get_room_id() + var key := Chunk.build_key(world_id, room_id, target_coord) + + if key not in loaded_chunks and key not in loading_chunks: + chunks_to_load.append(key) + + # Unload chunks (up to CHUNKS_PER_FRAME per frame). + var unload_count := 0 + for key in chunks_to_unload: + if unload_count >= CHUNKS_PER_FRAME: + break + _unload_chunk(key) + unload_count += 1 + + # Load chunks (up to CHUNKS_PER_FRAME per frame). + var load_count := 0 + for key in chunks_to_load: + if load_count >= CHUNKS_PER_FRAME: + break + _load_chunk(key) + load_count += 1 + + +## Load a chunk by requesting it from the server and instantiating it. +func _load_chunk(chunk_key: String) -> void: + if server == null: + # No server available; create a placeholder chunk. + _instantiate_chunk(chunk_key) + return + + loading_chunks[chunk_key] = true + + # Request chunk from server via RPC. + server.rpc("request_chunk", chunk_key) + + +## Handle the server's chunk_data response. +@rpc("any_peer", "call_local", "reliable") +func _rpc_chunk_data(chunk_key: String, payload: PackedByteArray, checksum: String) -> void: + loading_chunks.erase(chunk_key) + _instantiate_chunk(chunk_key, payload, checksum) + + +## Handle chunk load failure from server. +@rpc("any_peer", "call_local", "reliable") +func _rpc_chunk_load_error(chunk_key: String, error_msg: String) -> void: + loading_chunks.erase(chunk_key) + chunk_load_failed.emit(chunk_key, error_msg) + + +## Instantiate a chunk scene from data (either cached or from server). +func _instantiate_chunk(chunk_key: String, payload: PackedByteArray = PackedByteArray(), checksum: String = "") -> void: + var coord := _parse_chunk_key(chunk_key) + if coord == null: + push_error("Invalid chunk key: %s" % chunk_key) + return + + var world_id := _get_world_id() + var room_id := _get_room_id() + + # Create the chunk scene. + var chunk_scene := ChunkScene.new() + chunk_scene.chunk_key = chunk_key + chunk_scene.coord = coord + chunk_scene.world_id = world_id + chunk_scene.room_id = room_id + + # Decode payload if available. + if not payload.is_empty(): + chunk_scene.decode_payload(payload, checksum) + + # Add to world tree. + add_child(chunk_scene) + loaded_chunks[chunk_key] = chunk_scene + loading_chunks.erase(chunk_key) + + chunk_loaded.emit(chunk_key, chunk_scene) + + +## Unload a chunk by freeing its scene. +func _unload_chunk(chunk_key: String) -> void: + var chunk_scene = loaded_chunks.get(chunk_key) + if chunk_scene == null: + return + + loaded_chunks.erase(chunk_key) + + if is_instance_valid(chunk_scene): + chunk_scene.queue_free() + # Wait for tree removal before freeing. + await chunk_scene.tree_exited + + chunk_unloaded.emit(chunk_key) + + +## Get the distance between two chunk coordinates (Manhattan distance in chunk space). +static func _chunk_distance(a: Vector3i, b: Vector3i) -> int: + var dx := absi(a.x - b.x) + var dy := absi(a.y - b.y) + var dz := absi(a.z - b.z) + # Use Chebyshev distance (max of axis distances) for square view radius. + return maxi(dx, maxi(dy, dz)) + + +## Parse a chunk key string into its coordinate components. +static func _parse_chunk_key(key: String) -> Vector3i: + var parts := key.split(":") + if parts.size() < 5: + return null + return Vector3i( + int(parts[2]), + int(parts[3]), + int(parts[4]) + ) + + +## Get the current world ID (override in subclass or set via property). +func _get_world_id() -> String: + return "w_0001" + + +## Get the current room ID (override in subclass or set via property). +func _get_room_id() -> String: + return "r_0001" + + +## Get the list of currently loaded chunk keys. +func get_loaded_chunk_keys() -> PackedStringArray: + return loaded_chunks.keys() + + +## Check if a specific chunk is currently loaded. +func is_chunk_loaded(chunk_key: String) -> bool: + return chunk_key in loaded_chunks + + +## Force reload a specific chunk (unloads and re-loads it). +func reload_chunk(chunk_key: String) -> void: + if chunk_key in loaded_chunks: + _unload_chunk(chunk_key) + if chunk_key not in loading_chunks: + _load_chunk(chunk_key) + + +## Clear all loaded chunks (useful for world transitions). +func clear_all_chunks() -> void: + var keys := loaded_chunks.keys() + for key in keys: + _unload_chunk(key) + loading_chunks.clear() + + +## Get the number of currently loaded chunks. +func get_loaded_chunk_count() -> int: + return loaded_chunks.size() From 5c0a61870f20f32af6e997d25ccc9ec759dff00d Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 21:46:23 +0000 Subject: [PATCH 08/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/chunk_scene.gd | 170 ++++++++++++ scripts/chunk_stream_manager.gd | 477 ++++++++++++++++++-------------- 2 files changed, 443 insertions(+), 204 deletions(-) create mode 100644 scripts/chunk_scene.gd diff --git a/scripts/chunk_scene.gd b/scripts/chunk_scene.gd new file mode 100644 index 0000000..f19cf9b --- /dev/null +++ b/scripts/chunk_scene.gd @@ -0,0 +1,170 @@ +class_name ChunkScene +extends Node3D +## A visual representation of a single loaded world chunk. +## +## This node is instantiated by [ChunkStreamManager] when a chunk is loaded. +## It holds the tile layers, world objects, and metadata for one chunk of the +## world. The scene is responsible for rendering the chunk's visual content +## and exposing its gameplay objects to the rest of the game. +## +## ## Structure +## +## ``` +## ChunkScene (Node3D) +## ├── TileLayer (Node3D) # one per tile layer (ground, walls, decor, ...) +## ├── StaticObject (Node3D) # props, geometry, interactables +## └── DynamicObject (Node3D) # spawnable entities (templates) +## ``` +## +## ## Lifecycle +## +## 1. [ChunkStreamManager] requests chunk data from the server. +## 2. Server responds with binary payload. +## 3. [ChunkStreamManager] decodes the payload and creates a [ChunkScene]. +## 4. [ChunkScene] populates itself with tile layers and objects. +## 5. When the player moves out of range, [ChunkStreamManager] calls +## [method ChunkScene.queue_free] to unload it. + +## The chunk coordinate this scene represents. +var coord: Vector3i = Vector3i.ZERO + +## The canonical chunk key. +var key: String = "" + +## World-space bounding box of this chunk. +var bounds: AABB = AABB() + +## Format version of the chunk data. +var format_version: int = 1 + +## Revision counter from the server. +var revision: int = 0 + +## List of tile layers in this chunk. +var tile_layers: Array[TileLayer] = [] + +## List of world objects (static + dynamic) in this chunk. +var world_objects: Array[WorldObject] = [] + +## Content moderation flags for this chunk. +var content_flags: ChunkDataDecoder.ContentFlags = ChunkDataDecoder.ContentFlags.new() + +## Whether this chunk has been modified locally (not yet synced to server). +var is_dirty: bool = false + + +func _ready() -> void: + # Position this chunk scene at its world-space origin + global_position = bounds.position + # Set up collision shape for the chunk bounds + _setup_collision() + + +func _setup_collision() -> void: + # Add a collision shape for the chunk's bounding box + var collision := CollisionShape3D.new() + var shape := BoxShape3D.new() + shape.size = bounds.size + collision.shape = shape + collision.name = "ChunkBounds" + add_child(collision) + + +## Add a tile layer to this chunk scene. +func add_tile_layer(layer: TileLayer) -> void: + tile_layers.append(layer) + var layer_node := TileLayerNode.new() + layer_node.tile_layer = layer + layer_node.chunk_coord = coord + add_child(layer_node) + + +## Add a world object to this chunk scene. +func add_object(obj: WorldObject) -> void: + world_objects.append(obj) + if obj.is_dynamic: + _add_dynamic_object(obj) + else: + _add_static_object(obj) + + +func _add_static_object(obj: WorldObject) -> void: + var static_obj := StaticObjectNode.new() + static_obj.world_object = obj + static_obj.position = obj.position + static_obj.rotation = obj.rotation + static_obj.scale = obj.scale + static_obj.name = "static_%s" % obj.id + add_child(static_obj) + + +func _add_dynamic_object(obj: WorldObject) -> void: + var dynamic_obj := DynamicObjectNode.new() + dynamic_obj.world_object = obj + dynamic_obj.position = obj.position + dynamic_obj.rotation = obj.rotation + dynamic_obj.scale = obj.scale + dynamic_obj.name = "dynamic_%s" % obj.id + add_child(dynamic_obj) + + +## Get a world object by its ID. +func get_object_by_id(object_id: String) -> WorldObject: + for obj in world_objects: + if obj.id == object_id: + return obj + return null + + +## Get all static objects in this chunk. +func get_static_objects() -> Array[WorldObject]: + var result: Array[WorldObject] = [] + for obj in world_objects: + if not obj.is_dynamic: + result.append(obj) + return result + + +## Get all dynamic object templates in this chunk. +func get_dynamic_templates() -> Array[WorldObject]: + var result: Array[WorldObject] = [] + for obj in world_objects: + if obj.is_dynamic: + result.append(obj) + return result + + +## Mark this chunk as dirty (locally modified). +func mark_dirty() -> void: + is_dirty = true + + +## Clear all objects and tile layers from this chunk. +func clear() -> void: + for child in get_children(): + if child is TileLayerNode or child is StaticObjectNode or child is DynamicObjectNode: + child.queue_free() + tile_layers.clear() + world_objects.clear() + + +## Get the world-space position of a tile within this chunk. +func get_tile_world_position(tile_x: int, tile_y: int, tile_z: int = 0) -> Vector3: + return bounds.position + Vector3( + tile_x * 1.0, # Tile size is 1 world unit + tile_y * 1.0, + tile_z * 1.0 + ) + + +## Check if a world-space position is within this chunk's bounds. +func contains_position(pos: Vector3) -> bool: + return bounds.has_point(pos) + + +## Check if a chunk coordinate is adjacent to this chunk. +func is_adjacent(other_coord: Vector3i) -> bool: + var dx := abs(coord.x - other_coord.x) + var dy := abs(coord.y - other_coord.y) + var dz := abs(coord.z - other_coord.z) + return dx <= 1 and dy <= 1 and dz <= 1 and (dx + dy + dz) > 0 diff --git a/scripts/chunk_stream_manager.gd b/scripts/chunk_stream_manager.gd index 40b833a..27d04bc 100644 --- a/scripts/chunk_stream_manager.gd +++ b/scripts/chunk_stream_manager.gd @@ -1,229 +1,339 @@ class_name ChunkStreamManager extends Node -## Manages chunk load/unload based on player position. +## Manages loading and unloading of world chunks based on player position. ## -## Chunks are loaded from the server and instantiated as [ChunkScene] nodes. -## The manager maintains a view radius around the player and only loads chunks -## within that radius. Chunks outside the radius are unloaded to save memory. +## This is the central orchestrator for the chunk streaming system. It monitors +## the player's position, determines which chunks should be loaded, requests +## chunk data from the server, instantiates ChunkScene nodes, and unloads +## chunks that fall outside the streaming radius. ## -## This implements the streaming system described in docs/architecture/world.md -## section 5.6 (Streaming & Caching). +## ## Design +## +## - **Streaming radius** defines how many chunks around the player are kept +## loaded. Chunks beyond this radius are unloaded to save memory and CPU. +## - **Load priority** ensures chunks closer to the player are loaded first, +## preventing visual pop-in at the edge of the view. +## - **Server communication** uses the RPCs defined in docs/architecture/world.md +## (§5.3): `request_chunk`, `chunk_data`, `chunk_invalidated`. +## +## ## Usage +## +## Add this node to your scene tree and set the `player` reference: +## ```gdscript +## var stream_manager: ChunkStreamManager = $ChunkStreamManager +## stream_manager.player = $Player +## stream_manager.streaming_radius = 4 # chunks +## ``` -## Radius (in chunks) around the player within which chunks are loaded. -const VIEW_RADIUS: int = 4 +## Number of chunks (radius) to keep loaded around the player. +## A radius of 2 means a 5x5 grid of chunks centered on the player. +## Higher values increase memory usage but reduce streaming pop-in. +@export var streaming_radius: int = 3 -## Minimum distance (in chunks) the player must move before the manager -## re-evaluates which chunks need to be loaded/unloaded. This prevents -## excessive chunk churn when the player is standing still. -const MIN_MOVE_DISTANCE: float = 1.0 +## Maximum number of chunks to load per frame. Prevents frame spikes during +## rapid movement or when entering a new area. +@export var max_loads_per_frame: int = 4 -## Number of chunks to load/unload per frame (prevents frame spikes). -const CHUNKS_PER_FRAME: int = 4 +## Maximum number of chunks to unload per frame. +@export var max_unloads_per_frame: int = 4 -## Signal emitted when a chunk is successfully loaded. -signal chunk_loaded(chunk_key: String, chunk_scene: Node3D) +## Distance (in chunks) from the streaming radius edge before a chunk is +## considered for unloading. Prevents rapid load/unload cycling. +@export var unload_margin: int = 1 -## Signal emitted when a chunk is unloaded and freed. -signal chunk_unloaded(chunk_key: String) +## Emitted when a chunk finishes loading. +signal chunk_loaded(chunk_coord: Vector3i, chunk_scene: Node) -## Signal emitted when a chunk load fails. -signal chunk_load_failed(chunk_key: String, error: String) +## Emitted when a chunk is unloaded. +signal chunk_unloaded(chunk_coord: Vector3i) -## The player node whose position drives chunk streaming. +## Emitted when a chunk fails to load (e.g., server error). +signal chunk_load_failed(chunk_coord: Vector3i, error: String) + +## Emitted when the player moves far enough to require a streaming update. +signal streaming_updated + +## Reference to the player node. Must be set before the manager starts. var player: Node3D = null -## Set of currently loaded chunk keys. -var loaded_chunks: Dictionary = {} +## The current player chunk coordinate. +var _player_chunk_coord: Vector3i = Vector3i.ZERO -## Set of chunk keys that are currently being loaded (async). -var loading_chunks: Dictionary = {} +## Map of loaded chunk coordinates to their scene nodes. +var _loaded_chunks: Dictionary = {} -## The player's position at the last evaluation. -var last_player_position: Vector3 = Vector3.ZERO +## Queue of chunk coordinates pending load, sorted by distance from player. +var _load_queue: Array[Vector3i] = [] -## Whether streaming is active (player exists and is in-game). +## Queue of chunk coordinates pending unload. +var _unload_queue: Array[Vector3i] = [] + +## Whether streaming is currently active (player is set and in game). var is_active: bool = false -## Reference to the server connection for chunk requests. -var server: NetworkedMultiplayerENet = null +## The server RPC interface for chunk data. Set by the networking layer. +var _server_rpc: Object = null + +## Internal: track which chunks are currently being loaded (to avoid duplicates). +var _loading_in_progress: Dictionary = {} func _ready() -> void: - last_player_position = Vector3.ZERO + _process_thread = Thread.create(_streaming_loop, Thread.PRIORITY_LOW) -## Set the player node that drives chunk streaming. -func set_player(p_player: Node3D) -> void: - player = p_player - if player != null: - last_player_position = player.global_position - is_active = true - else: - is_active = false +func _streaming_loop() -> void: + while is_inside_tree(): + _update_streaming() + _process_load_queue() + _process_unload_queue() + streaming_updated.emit() + await get_tree().create_timer(0.1).timeout # Check every 100ms -## Set the network server reference for chunk requests. -func set_server(p_server: NetworkedMultiplayerENet) -> void: - server = p_server - - -## Called every frame to evaluate chunk loading/unloading. -func _process(_delta: float) -> void: - if not is_active or player == null: +func _update_streaming() -> void: + if player == null or not is_inside_tree(): return - # Check if player has moved enough to warrant re-evaluation. - var current_pos := player.global_position - var move_distance := (current_pos - last_player_position).length() - if move_distance < MIN_MOVE_DISTANCE: + var current_coord := Chunk.coord_for_position(player.global_position) + if current_coord == _player_chunk_coord: return - last_player_position = current_pos - - # Evaluate which chunks need to be loaded/unloaded. - _evaluate_chunks() + _player_chunk_coord = current_coord + _rebuild_queues() -## Evaluate which chunks to load and which to unload. -func _evaluate_chunks() -> void: - if player == null: - return +func _rebuild_queues() -> void: + var target_set := _compute_target_chunks() - var player_chunk_coord := Chunk.coord_for_position(player.global_position) - var chunks_to_load: PackedStringArray = [] - var chunks_to_unload: PackedStringArray = [] + # Determine which chunks need to be loaded + var to_load: Array[Vector3i] = [] + for coord in target_set: + if not _loaded_chunks.has(coord) and not _loading_in_progress.has(coord): + to_load.append(coord) - # Determine which chunks should be loaded. - for loaded_key in loaded_chunks.keys(): - var coord := _parse_chunk_key(loaded_key) - if coord == null: - continue + # Sort by distance from player (closest first) + to_load.sort_custom(_compare_distance) + _load_queue = to_load - var dist := _chunk_distance(player_chunk_coord, coord) - if dist > VIEW_RADIUS: - chunks_to_unload.append(loaded_key) + # Determine which chunks need to be unloaded + var to_unload: Array[Vector3i] = [] + for coord in _loaded_chunks: + if not target_set.has(coord): + to_unload.append(coord) - # Determine which chunks should be loaded. - for dx in range(-VIEW_RADIUS, VIEW_RADIUS + 1): - for dy in range(-VIEW_RADIUS, VIEW_RADIUS + 1): - for dz in range(-1, 2): - var target_coord := Vector3i( - player_chunk_coord.x + dx, - player_chunk_coord.y + dy, - player_chunk_coord.z + dz + to_unload.sort_custom(_compare_distance) + _unload_queue = to_unload + + +func _compute_target_chunks() -> Dictionary: + """Compute the set of chunk coordinates that should be loaded.""" + var target := {} + var half := streaming_radius + for dx in range(-half, half + 1): + for dy in range(-half, half + 1): + for dz in range(-Chunk.CHUNK_SIZE_Z, Chunk.CHUNK_SIZE_Z + 1): + var coord := Vector3i( + _player_chunk_coord.x + dx, + _player_chunk_coord.y + dy, + _player_chunk_coord.z + dz ) - - # Skip chunks outside the spherical view radius. - if _chunk_distance(player_chunk_coord, target_coord) > VIEW_RADIUS: - continue - - var world_id := _get_world_id() - var room_id := _get_room_id() - var key := Chunk.build_key(world_id, room_id, target_coord) - - if key not in loaded_chunks and key not in loading_chunks: - chunks_to_load.append(key) - - # Unload chunks (up to CHUNKS_PER_FRAME per frame). - var unload_count := 0 - for key in chunks_to_unload: - if unload_count >= CHUNKS_PER_FRAME: - break - _unload_chunk(key) - unload_count += 1 - - # Load chunks (up to CHUNKS_PER_FRAME per frame). - var load_count := 0 - for key in chunks_to_load: - if load_count >= CHUNKS_PER_FRAME: - break - _load_chunk(key) - load_count += 1 + target[coord] = coord + return target -## Load a chunk by requesting it from the server and instantiating it. -func _load_chunk(chunk_key: String) -> void: - if server == null: - # No server available; create a placeholder chunk. - _instantiate_chunk(chunk_key) +func _compare_distance(a: Vector3i, b: Vector3i) -> bool: + """Compare chunk coordinates by distance from player chunk.""" + var dist_a := _chunk_distance_squared(_player_chunk_coord, a) + var dist_b := _chunk_distance_squared(_player_chunk_coord, b) + return dist_a < dist_b + + +func _chunk_distance_squared(a: Vector3i, b: Vector3i) -> int: + var dx := a.x - b.x + var dy := a.y - b.y + var dz := a.z - b.z + return dx * dx + dy * dy + dz * dz + + +func _process_load_queue() -> void: + var count := 0 + while count < max_loads_per_frame and not _load_queue.is_empty(): + var coord := _load_queue.pop_front() + _load_chunk(coord) + count += 1 + + +func _process_unload_queue() -> void: + var count := 0 + while count < max_unloads_per_frame and not _unload_queue.is_empty(): + var coord := _unload_queue.pop_front() + _unload_chunk(coord) + count += 1 + + +func _load_chunk(coord: Vector3i) -> void: + if _loading_in_progress.has(coord): return - loading_chunks[chunk_key] = true + _loading_in_progress[coord] = true - # Request chunk from server via RPC. - server.rpc("request_chunk", chunk_key) + # Request chunk data from server via RPC + if _server_rpc != null and _server_rpc.has_method("request_chunk"): + _server_rpc.request_chunk(Chunk.build_key( + "", "", coord + )) + else: + # Fallback: try to load from local cache or create empty chunk + _instantiate_chunk_scene(coord) -## Handle the server's chunk_data response. -@rpc("any_peer", "call_local", "reliable") -func _rpc_chunk_data(chunk_key: String, payload: PackedByteArray, checksum: String) -> void: - loading_chunks.erase(chunk_key) - _instantiate_chunk(chunk_key, payload, checksum) - - -## Handle chunk load failure from server. -@rpc("any_peer", "call_local", "reliable") -func _rpc_chunk_load_error(chunk_key: String, error_msg: String) -> void: - loading_chunks.erase(chunk_key) - chunk_load_failed.emit(chunk_key, error_msg) - - -## Instantiate a chunk scene from data (either cached or from server). -func _instantiate_chunk(chunk_key: String, payload: PackedByteArray = PackedByteArray(), checksum: String = "") -> void: - var coord := _parse_chunk_key(chunk_key) +func _on_chunk_data_received(key: String, binary_data: PackedByteArray) -> void: + """Called by the networking layer when chunk data arrives from server.""" + var coord := _parse_coord_from_key(key) if coord == null: - push_error("Invalid chunk key: %s" % chunk_key) return - var world_id := _get_world_id() - var room_id := _get_room_id() + _loading_in_progress.erase(coord) - # Create the chunk scene. + # Decode the binary chunk data + var chunk_data := ChunkDataDecoder.decode_binary(binary_data) + if chunk_data == null: + chunk_load_failed.emit(coord, "Failed to decode chunk data") + return + + # Instantiate the chunk scene with the decoded data + _instantiate_chunk_scene_with_data(coord, chunk_data) + + +func _on_chunk_load_error(coord: Vector3i, error_msg: String) -> void: + """Called when the server reports a chunk load error.""" + _loading_in_progress.erase(coord) + chunk_load_failed.emit(coord, error_msg) + + +func _instantiate_chunk_scene(coord: Vector3i) -> void: + """Create an empty chunk scene for the given coordinate.""" var chunk_scene := ChunkScene.new() - chunk_scene.chunk_key = chunk_key chunk_scene.coord = coord - chunk_scene.world_id = world_id - chunk_scene.room_id = room_id + chunk_scene.key = Chunk.build_key("", "", coord) + chunk_scene.bounds = Chunk.compute_bounds(coord) + _add_loaded_chunk(coord, chunk_scene) - # Decode payload if available. - if not payload.is_empty(): - chunk_scene.decode_payload(payload, checksum) - # Add to world tree. +func _instantiate_chunk_scene_with_data(coord: Vector3i, data: ChunkDataDecoder.ChunkPayload) -> void: + """Create a chunk scene populated with server data.""" + var chunk_scene := ChunkScene.new() + chunk_scene.coord = coord + chunk_scene.key = data.key + chunk_scene.bounds = data.bounds + chunk_scene.format_version = data.format_version + chunk_scene.revision = data.revision + + # Add tiles + for layer in data.tile_layers: + var tile_layer := TileLayer.new() + tile_layer.name = layer.name + tile_layer.layer_id = layer.layer_id + tile_layer.width = layer.width + tile_layer.height = layer.height + tile_layer.depth = layer.depth + tile_layer.palette = layer.palette + tile_layer.tiles = layer.tiles + chunk_scene.add_tile_layer(tile_layer) + + # Add objects + for obj_data in data.objects: + var world_obj := WorldObject.new() + world_obj.id = obj_data.id + world_obj.type = obj_data.type + world_obj.asset_id = obj_data.asset_id + world_obj.position = obj_data.position + world_obj.rotation = obj_data.rotation + world_obj.scale = obj_data.scale + world_obj.properties = obj_data.properties + world_obj.is_dynamic = obj_data.dynamic + world_obj.flags = obj_data.flags + chunk_scene.add_object(world_obj) + + # Add content flags + chunk_scene.content_flags = data.flags + + _add_loaded_chunk(coord, chunk_scene) + + +func _add_loaded_chunk(coord: Vector3i, chunk_scene: Node) -> void: + _loaded_chunks[coord] = chunk_scene add_child(chunk_scene) - loaded_chunks[chunk_key] = chunk_scene - loading_chunks.erase(chunk_key) - - chunk_loaded.emit(chunk_key, chunk_scene) + _loading_in_progress.erase(coord) + chunk_loaded.emit(coord, chunk_scene) -## Unload a chunk by freeing its scene. -func _unload_chunk(chunk_key: String) -> void: - var chunk_scene = loaded_chunks.get(chunk_key) - if chunk_scene == null: +func _unload_chunk(coord: Vector3i) -> void: + if not _loaded_chunks.has(coord): return - loaded_chunks.erase(chunk_key) - + var chunk_scene = _loaded_chunks[coord] if is_instance_valid(chunk_scene): chunk_scene.queue_free() - # Wait for tree removal before freeing. - await chunk_scene.tree_exited - chunk_unloaded.emit(chunk_key) + _loaded_chunks.erase(coord) + chunk_unloaded.emit(coord) -## Get the distance between two chunk coordinates (Manhattan distance in chunk space). -static func _chunk_distance(a: Vector3i, b: Vector3i) -> int: - var dx := absi(a.x - b.x) - var dy := absi(a.y - b.y) - var dz := absi(a.z - b.z) - # Use Chebyshev distance (max of axis distances) for square view radius. - return maxi(dx, maxi(dy, dz)) +func _on_player_position_changed() -> void: + """Call this when the player position changes significantly.""" + if player != null: + _update_streaming() -## Parse a chunk key string into its coordinate components. -static func _parse_chunk_key(key: String) -> Vector3i: +func set_server_rpc(rpc_interface: Object) -> void: + """Set the server RPC interface for chunk requests.""" + _server_rpc = rpc_interface + + +func get_loaded_chunk(coord: Vector3i) -> Node: + """Get a loaded chunk scene by coordinate, or null if not loaded.""" + return _loaded_chunks.get(coord, null) + + +func is_chunk_loaded(coord: Vector3i) -> bool: + """Check if a chunk is currently loaded.""" + return _loaded_chunks.has(coord) + + +func get_loaded_chunk_count() -> int: + """Get the number of currently loaded chunks.""" + return _loaded_chunks.size() + + +func get_all_loaded_coords() -> Array[Vector3i]: + """Get all currently loaded chunk coordinates.""" + var coords: Array[Vector3i] = [] + for key in _loaded_chunks: + coords.append(key) + return coords + + +func force_reload_chunk(coord: Vector3i) -> void: + """Force reload a specific chunk (e.g., after server update).""" + if _loaded_chunks.has(coord): + _unload_chunk(coord) + if not _loading_in_progress.has(coord): + _load_chunk(coord) + + +func clear_all_chunks() -> void: + """Unload all chunks immediately.""" + var coords := get_all_loaded_coords() + for coord in coords: + _unload_chunk(coord) + _load_queue.clear() + _unload_queue.clear() + + +func _parse_coord_from_key(key: String) -> Vector3i: + """Parse chunk coordinate from a canonical chunk key.""" var parts := key.split(":") if parts.size() < 5: return null @@ -232,44 +342,3 @@ static func _parse_chunk_key(key: String) -> Vector3i: int(parts[3]), int(parts[4]) ) - - -## Get the current world ID (override in subclass or set via property). -func _get_world_id() -> String: - return "w_0001" - - -## Get the current room ID (override in subclass or set via property). -func _get_room_id() -> String: - return "r_0001" - - -## Get the list of currently loaded chunk keys. -func get_loaded_chunk_keys() -> PackedStringArray: - return loaded_chunks.keys() - - -## Check if a specific chunk is currently loaded. -func is_chunk_loaded(chunk_key: String) -> bool: - return chunk_key in loaded_chunks - - -## Force reload a specific chunk (unloads and re-loads it). -func reload_chunk(chunk_key: String) -> void: - if chunk_key in loaded_chunks: - _unload_chunk(chunk_key) - if chunk_key not in loading_chunks: - _load_chunk(chunk_key) - - -## Clear all loaded chunks (useful for world transitions). -func clear_all_chunks() -> void: - var keys := loaded_chunks.keys() - for key in keys: - _unload_chunk(key) - loading_chunks.clear() - - -## Get the number of currently loaded chunks. -func get_loaded_chunk_count() -> int: - return loaded_chunks.size() From 67f6a9c43fc2cced6b82a973b85058a07c25498f Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 21:54:29 +0000 Subject: [PATCH 09/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/chunk_data_decoder.gd | 688 ++++++++++++++++++++++++++++++++++ 1 file changed, 688 insertions(+) create mode 100644 scripts/chunk_data_decoder.gd diff --git a/scripts/chunk_data_decoder.gd b/scripts/chunk_data_decoder.gd new file mode 100644 index 0000000..b490ca2 --- /dev/null +++ b/scripts/chunk_data_decoder.gd @@ -0,0 +1,688 @@ +class_name ChunkDataDecoder +extends RefCounted +## Decodes chunk binary payloads (and JSON) into logical chunk data. +## +## Implements the binary format spec from docs/architecture/world.md §3.6. +## Both the Godot client and Python backend use this class to ensure +## identical encoding/decoding behavior. +## +## ## Binary Format Overview +## +## ``` +## Header (48 bytes): +## [0:4] magic "AGPC" +## [4:6] version (uint16 LE) +## [6:8] header_len (uint16 LE) +## [8:12] flags (uint32 LE) +## [12:16] payload_len (uint32 LE) +## [16:48] checksum (SHA-256 hex, 32 bytes) +## +## Payload Body (sections): +## [section_id:1][section_len:4][data:section_len] +## ``` + + +## Content flags parsed from a chunk. +class ContentFlags: + ## Severity levels: 0=none, 1=mild, 2=moderate, 3=severe + var violence: int = 0 + var sexual: int = 0 + var language: int = 0 + var substance: int = 0 + var gambling: int = 0 + var horror: int = 0 + var hate: int = 0 + var custom: int = 0 + var mature: bool = false + var sources: Array[String] = [] + + func is_mature() -> bool: + return mature + + func has_severe_content() -> bool: + return (violence >= 3 or sexual >= 3 or language >= 3 or + substance >= 3 or gambling >= 3 or horror >= 3 or + hate >= 3 or custom >= 3) + + func get_max_severity() -> int: + return maxi( + violence, maxi(sexual, maxi(language, maxi(substance, + maxi(gambling, maxi(horror, maxi(hate, custom))))))) + + func to_dict() -> Dictionary: + return { + "categories": { + "violence": violence, + "sexual": sexual, + "language": language, + "substance": substance, + "gambling": gambling, + "horror": horror, + "hate": hate, + "custom": custom, + }, + "mature": mature, + "sources": sources, + } + + func from_dict(d: Dictionary) -> void: + if d.has("categories"): + violence = d["categories"].get("violence", 0) + sexual = d["categories"].get("sexual", 0) + language = d["categories"].get("language", 0) + substance = d["categories"].get("substance", 0) + gambling = d["categories"].get("gambling", 0) + horror = d["categories"].get("horror", 0) + hate = d["categories"].get("hate", 0) + custom = d["categories"].get("custom", 0) + mature = d.get("mature", false) + sources = d.get("sources", []) + + +## A tile layer definition (palette + dense tile indices). +class TileLayerData: + var name: String = "" + var layer_id: int = 0 + var width: int = 0 + var height: int = 0 + var depth: int = 1 + var palette: Array[TileDef] = [] + var tiles: PackedInt32Array = PackedInt32Array() + + func tile_count() -> int: + return width * height * depth + + func get_tile(x: int, y: int, z: int = 0) -> int: + if x < 0 or x >= width or y < 0 or y >= height or z < 0 or z >= depth: + return 0 # Out of bounds returns empty tile + var idx := (z * height + y) * width + x + if idx < tiles.size(): + return tiles[idx] + return 0 + + func get_tile_def(tile_id: int) -> TileDef: + for def in palette: + if def.id == tile_id: + return def + return null + + +## A single tile definition (palette entry). +class TileDef: + var id: int = 0 + var asset_id: String = "" + var solid: bool = false + var flags: int = 0 + var variants: int = 1 + + func is_solid() -> bool: + return solid + + func is_walkable() -> bool: + return (flags & 0x01) != 0 + + func is_climbable() -> bool: + return (flags & 0x02) != 0 + + func is_liquid() -> bool: + return (flags & 0x04) != 0 + + +## A world object parsed from chunk data. +class WorldObjectData: + var id: String = "" + var type: String = "" + var asset_id: String = "" + var position: Vector3 = Vector3.ZERO + var rotation: Vector4 = Vector4(0, 0, 0, 1) + var scale: Vector3 = Vector3.ONE + var properties: Dictionary = {} + var dynamic: bool = false + var flags: ContentFlags = ContentFlags.new() + + +## The complete decoded chunk payload. +class ChunkPayload: + var version: int = 1 + var key: String = "" + var world_id: String = "" + var room_id: String = "" + var coord: Vector3i = Vector3i.ZERO + var bounds: AABB = AABB() + var tile_layers: Array[TileLayerData] = [] + var objects: Array[WorldObjectData] = [] + var flags: ContentFlags = ContentFlags.new() + var created_at: int = 0 # Unix timestamp + var updated_at: int = 0 # Unix timestamp + var revision: int = 0 + var author: String = "" + var checksum: String = "" + + +# ─── Binary Decoding ──────────────────────────────────────────────────────── + +static const MAGIC: PackedByteArray = PackedByteArray([0x41, 0x47, 0x50, 0x43]) # "AGPC" +static const HEADER_SIZE: int = 48 +static const SECTION_META: int = 0x01 +static const SECTION_TILE: int = 0x02 +static const SECTION_OBJECT: int = 0x03 +static const SECTION_FLAGS: int = 0x04 +static const SECTION_REVISION_META: int = 0x05 + + +## Decode a binary chunk payload into a ChunkPayload. +## Returns null if the data is invalid or version is unsupported. +static func decode_binary(data: PackedByteArray) -> ChunkPayload: + if data.size() < HEADER_SIZE: + push_error("ChunkDataDecoder: Data too short for header (%d bytes)" % data.size()) + return null + + # Verify magic + var magic := PackedByteArray(data.slice(0, 4)) + if magic != MAGIC: + push_error("ChunkDataDecoder: Invalid magic bytes") + return null + + # Read header fields + var version := _read_uint16(data, 4) + if version != 1: + push_error("ChunkDataDecoder: Unsupported chunk version %d" % version) + return null + + var header_len := _read_uint16(data, 6) + var flags := _read_uint32(data, 8) + var payload_len := _read_uint32(data, 12) + var checksum := _read_hex_string(data, 16, 32) + + # Verify payload length + if data.size() < HEADER_SIZE + payload_len: + push_error("ChunkDataDecoder: Payload too short") + return null + + var payload := data.slice(HEADER_SIZE, HEADER_SIZE + payload_len) + + # Verify checksum + var computed_checksum := _sha256_hex(payload) + if computed_checksum != checksum: + push_error("ChunkDataDecoder: Checksum mismatch") + # Continue anyway for resilience + + var result := ChunkPayload.new() + result.version = version + result.checksum = checksum + + # Parse sections + var offset := 0 + while offset < payload.size(): + if offset + 5 > payload.size(): + break + + var section_id := payload[offset] + var section_len := _read_uint32(payload, offset + 1) + + if offset + 5 + section_len > payload.size(): + break + + var section_data := payload.slice(offset + 5, offset + 5 + section_len) + + match section_id: + SECTION_META: + _parse_metadata_section(section_data, result) + SECTION_TILE: + _parse_tile_section(section_data, result) + SECTION_OBJECT: + _parse_object_section(section_data, result) + SECTION_FLAGS: + _parse_flags_section(section_data, result.flags) + SECTION_REVISION_META: + _parse_revision_meta_section(section_data, result) + + offset += 5 + section_len + + return result + + +## Encode a ChunkPayload into binary format. +static func encode_binary(payload: ChunkPayload) -> PackedByteArray: + var sections := PackedByteArray() + + # Build metadata section + sections.append_array(_build_metadata_section(payload)) + # Build tile sections + for layer in payload.tile_layers: + sections.append_array(_build_tile_section(layer)) + # Build object sections + for obj in payload.objects: + sections.append_array(_build_object_section(obj)) + # Build flags section + sections.append_array(_build_flags_section(payload.flags)) + # Build revision meta section + sections.append_array(_build_revision_meta_section(payload)) + + var payload_bytes := PackedByteArray(sections) + var checksum := _sha256_hex(payload_bytes) + + # Build header + var header := PackedByteArray() + header.append_array(MAGIC) + header.append_array(_write_uint16(1)) # version + header.append_array(_write_uint16(0)) # header_len (after this field) + header.append_array(_write_uint32(0)) # flags + header.append_array(_write_uint32(payload_bytes.size())) + header.append_array(_hex_to_bytes(checksum)) + + return header + payload_bytes + + +# ─── Section Parsers ──────────────────────────────────────────────────────── + +static func _parse_metadata_section(data: PackedByteArray, result: ChunkPayload) -> void: + var offset := 0 + + # world_id (length-prefixed string) + var world_id_len := _read_uint16(data, offset) + offset += 2 + result.world_id = data.slice(offset, offset + world_id_len).get_string_from_utf8() + offset += world_id_len + + # room_id (length-prefixed string) + var room_id_len := _read_uint16(data, offset) + offset += 2 + result.room_id = data.slice(offset, offset + room_id_len).get_string_from_utf8() + offset += room_id_len + + # coord (3 × int32) + result.coord = Vector3i( + _read_int32(data, offset), + _read_int32(data, offset + 4), + _read_int32(data, offset + 8) + ) + offset += 12 + + # bounds (6 × float32) + var min_pos := Vector3( + _read_float32(data, offset), + _read_float32(data, offset + 4), + _read_float32(data, offset + 8) + ) + var max_pos := Vector3( + _read_float32(data, offset + 12), + _read_float32(data, offset + 16), + _read_float32(data, offset + 20) + ) + result.bounds = AABB(min_pos, max_pos - min_pos) + + +static func _parse_tile_section(data: PackedByteArray, result: ChunkPayload) -> void: + var layer := TileLayerData.new() + var offset := 0 + + # name (length-prefixed string) + var name_len := _read_uint16(data, offset) + offset += 2 + layer.name = data.slice(offset, offset + name_len).get_string_from_utf8() + offset += name_len + + # layer_id (uint16) + layer.layer_id = _read_uint16(data, offset) + offset += 2 + + # dimensions (3 × uint16) + layer.width = _read_uint16(data, offset) + offset += 2 + layer.height = _read_uint16(data, offset) + offset += 2 + layer.depth = _read_uint16(data, offset) + offset += 2 + + # palette + var palette_count := _read_uint16(data, offset) + offset += 2 + for _i in range(palette_count): + var tile_def := TileDef.new() + tile_def.id = _read_uint16(data, offset) + offset += 2 + + var asset_id_len := _read_uint16(data, offset) + offset += 2 + tile_def.asset_id = data.slice(offset, offset + asset_id_len).get_string_from_utf8() + offset += asset_id_len + + tile_def.solid = data[offset] != 0 + offset += 1 + + tile_def.flags = _read_uint32(data, offset) + offset += 4 + + tile_def.variants = data[offset] + offset += 1 + + layer.palette.append(tile_def) + + # tiles + var tile_count := _read_uint32(data, offset) + offset += 4 + for i in range(tile_count): + layer.tiles.append(_read_uint16(data, offset)) + offset += 2 + + result.tile_layers.append(layer) + + +static func _parse_object_section(data: PackedByteArray, result: ChunkPayload) -> void: + var obj := WorldObjectData.new() + var offset := 0 + + # id (length-prefixed string) + var id_len := _read_uint16(data, offset) + offset += 2 + obj.id = data.slice(offset, offset + id_len).get_string_from_utf8() + offset += id_len + + # type (length-prefixed string) + var type_len := _read_uint16(data, offset) + offset += 2 + obj.type = data.slice(offset, offset + type_len).get_string_from_utf8() + offset += type_len + + # asset_id (length-prefixed string) + var asset_id_len := _read_uint16(data, offset) + offset += 2 + obj.asset_id = data.slice(offset, offset + asset_id_len).get_string_from_utf8() + offset += asset_id_len + + # position (3 × float32) + obj.position = Vector3( + _read_float32(data, offset), + _read_float32(data, offset + 4), + _read_float32(data, offset + 8) + ) + offset += 12 + + # rotation (4 × float32) + obj.rotation = Vector4( + _read_float32(data, offset), + _read_float32(data, offset + 4), + _read_float32(data, offset + 8), + _read_float32(data, offset + 12) + ) + offset += 16 + + # scale (3 × float32) + obj.scale = Vector3( + _read_float32(data, offset), + _read_float32(data, offset + 4), + _read_float32(data, offset + 8) + ) + offset += 12 + + # dynamic (uint8) + obj.dynamic = data[offset] != 0 + offset += 1 + + # properties (length-prefixed JSON string) + var props_len := _read_uint16(data, offset) + offset += 2 + var props_str := data.slice(offset, offset + props_len).get_string_from_utf8() + offset += props_len + obj.properties = JSON.parse_string(props_str) or {} + + # flags (ContentFlags) + obj.flags.violence = data[offset] + offset += 1 + obj.flags.sexual = data[offset] + offset += 1 + obj.flags.language = data[offset] + offset += 1 + obj.flags.substance = data[offset] + offset += 1 + obj.flags.gambling = data[offset] + offset += 1 + obj.flags.horror = data[offset] + offset += 1 + obj.flags.hate = data[offset] + offset += 1 + obj.flags.custom = data[offset] + offset += 1 + obj.flags.mature = data[offset] != 0 + offset += 1 + + var source_count := _read_uint16(data, offset) + offset += 2 + for _i in range(source_count): + var src_len := _read_uint16(data, offset) + offset += 2 + obj.flags.sources.append(data.slice(offset, offset + src_len).get_string_from_utf8()) + offset += src_len + + result.objects.append(obj) + + +static func _parse_flags_section(data: PackedByteArray, flags: ContentFlags) -> void: + flags.violence = data[0] + flags.sexual = data[1] + flags.language = data[2] + flags.substance = data[3] + flags.gambling = data[4] + flags.horror = data[5] + flags.hate = data[6] + flags.custom = data[7] + flags.mature = data[8] != 0 + + var offset := 9 + var source_count := _read_uint16(data, offset) + offset += 2 + for _i in range(source_count): + var src_len := _read_uint16(data, offset) + offset += 2 + flags.sources.append(data.slice(offset, offset + src_len).get_string_from_utf8()) + offset += src_len + + +static func _parse_revision_meta_section(data: PackedByteArray, result: ChunkPayload) -> void: + var offset := 0 + # created_at (int64 unix) + result.created_at = _read_int64(data, offset) + offset += 8 + # updated_at (int64 unix) + result.updated_at = _read_int64(data, offset) + offset += 8 + # revision (uint32) + result.revision = _read_uint32(data, offset) + offset += 4 + # author (length-prefixed string) + var author_len := _read_uint16(data, offset) + offset += 2 + result.author = data.slice(offset, offset + author_len).get_string_from_utf8() + + +# ─── Section Builders ─────────────────────────────────────────────────────── + +static func _build_metadata_section(payload: ChunkPayload) -> PackedByteArray: + var data := PackedByteArray() + data.append_array(_write_string(payload.world_id)) + data.append_array(_write_string(payload.room_id)) + data.append_array(_write_int32(payload.coord.x)) + data.append_array(_write_int32(payload.coord.y)) + data.append_array(_write_int32(payload.coord.z)) + data.append_array(_write_float32(payload.bounds.position.x)) + data.append_array(_write_float32(payload.bounds.position.y)) + data.append_array(_write_float32(payload.bounds.position.z)) + data.append_array(_write_float32(payload.bounds.size.x)) + data.append_array(_write_float32(payload.bounds.size.y)) + data.append_array(_write_float32(payload.bounds.size.z)) + return data + + +static func _build_tile_section(layer: TileLayerData) -> PackedByteArray: + var data := PackedByteArray() + data.append_array(_write_string(layer.name)) + data.append_array(_write_uint16(layer.layer_id)) + data.append_array(_write_uint16(layer.width)) + data.append_array(_write_uint16(layer.height)) + data.append_array(_write_uint16(layer.depth)) + data.append_array(_write_uint16(layer.palette.size())) + + for tile_def in layer.palette: + data.append_array(_write_uint16(tile_def.id)) + data.append_array(_write_string(tile_def.asset_id)) + data.append(tile_def.solid as int) + data.append_array(_write_uint32(tile_def.flags)) + data.append(tile_def.variants) + + data.append_array(_write_uint32(layer.tiles.size())) + for tile_id in layer.tiles: + data.append_array(_write_uint16(tile_id)) + + return _wrap_section(data, SECTION_TILE) + + +static func _build_object_section(obj: WorldObjectData) -> PackedByteArray: + var data := PackedByteArray() + data.append_array(_write_string(obj.id)) + data.append_array(_write_string(obj.type)) + data.append_array(_write_string(obj.asset_id)) + data.append_array(_write_float32(obj.position.x)) + data.append_array(_write_float32(obj.position.y)) + data.append_array(_write_float32(obj.position.z)) + data.append_array(_write_float32(obj.rotation.x)) + data.append_array(_write_float32(obj.rotation.y)) + data.append_array(_write_float32(obj.rotation.z)) + data.append_array(_write_float32(obj.rotation.w)) + data.append_array(_write_float32(obj.scale.x)) + data.append_array(_write_float32(obj.scale.y)) + data.append_array(_write_float32(obj.scale.z)) + data.append(obj.dynamic as int) + + var props_json := JSON.stringify(obj.properties, " ") + data.append_array(_write_string(props_json)) + + data.append(obj.flags.violence) + data.append(obj.flags.sexual) + data.append(obj.flags.language) + data.append(obj.flags.substance) + data.append(obj.flags.gambling) + data.append(obj.flags.horror) + data.append(obj.flags.hate) + data.append(obj.flags.custom) + data.append(obj.flags.mature as int) + data.append_array(_write_uint16(obj.flags.sources.size())) + for src in obj.flags.sources: + data.append_array(_write_string(src)) + + return _wrap_section(data, SECTION_OBJECT) + + +static func _build_flags_section(flags: ContentFlags) -> PackedByteArray: + var data := PackedByteArray() + data.append(flags.violence) + data.append(flags.sexual) + data.append(flags.language) + data.append(flags.substance) + data.append(flags.gambling) + data.append(flags.horror) + data.append(flags.hate) + data.append(flags.custom) + data.append(flags.mature as int) + data.append_array(_write_uint16(flags.sources.size())) + for src in flags.sources: + data.append_array(_write_string(src)) + return _wrap_section(data, SECTION_FLAGS) + + +static func _build_revision_meta_section(payload: ChunkPayload) -> PackedByteArray: + var data := PackedByteArray() + data.append_array(_write_int64(payload.created_at)) + data.append_array(_write_int64(payload.updated_at)) + data.append_array(_write_uint32(payload.revision)) + data.append_array(_write_string(payload.author)) + return _wrap_section(data, SECTION_REVISION_META) + + +static func _wrap_section(data: PackedByteArray, section_id: int) -> PackedByteArray: + var wrapped := PackedByteArray() + wrapped.append(section_id) + wrapped.append_array(_write_uint32(data.size())) + wrapped.append_array(data) + return wrapped + + +# ─── Binary Helpers ───────────────────────────────────────────────────────── + +static func _read_uint16(data: PackedByteArray, offset: int) -> int: + return data[offset] | (data[offset + 1] << 8) + +static func _read_uint32(data: PackedByteArray, offset: int) -> int: + return (data[offset] | (data[offset + 1] << 8) | + (data[offset + 2] << 16) | (data[offset + 3] << 24)) + +static func _read_int32(data: PackedByteArray, offset: int) -> int: + var val := _read_uint32(data, offset) + if val >= 0x80000000: + return val - 0x100000000 + return val + +static func _read_int64(data: PackedByteArray, offset: int) -> int: + var lo := _read_uint32(data, offset) + var hi := _read_uint32(data, offset + 4) + return hi * 0x100000000 + lo + +static func _read_float32(data: PackedByteArray, offset: int) -> float: + var bits := _read_uint32(data, offset) + return bits as float # GDScript float is 64-bit; bits repr works for IEEE 754 + +static func _read_hex_string(data: PackedByteArray, offset: int, length: int) -> String: + var hex_chars := "0123456789abcdef" + var result := "" + for i in range(length): + var byte_val := data[offset + i] + result += hex_chars[byte_val >> 4] + result += hex_chars[byte_val & 0x0F] + return result + + +static func _write_uint16(val: int) -> PackedByteArray: + return PackedByteArray([(val & 0xFF), (val >> 8) & 0xFF]) + +static func _write_uint32(val: int) -> PackedByteArray: + return PackedByteArray([ + val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF + ]) + +static func _write_int32(val: int) -> PackedByteArray: + return _write_uint32(val) + +static func _write_int64(val: int) -> PackedByteArray: + var lo := val & 0xFFFFFFFF + var hi := (val >> 32) & 0xFFFFFFFF + return _write_uint32(lo) + _write_uint32(hi) + +static func _write_float32(val: float) -> PackedByteArray: + var bits := val as int + return _write_uint32(bits) + +static func _write_string(s: String) -> PackedByteArray: + var bytes := s.to_utf8_buffer() + return _write_uint16(bytes.size()) + bytes + +static func _hex_to_bytes(hex_str: String) -> PackedByteArray: + var result := PackedByteArray() + for i in range(0, hex_str.length(), 2): + var byte_val := int("0x" + hex_str.substr(i, 2), 16) + result.append(byte_val) + return result + + +# ─── SHA-256 ──────────────────────────────────────────────────────────────── + +static func _sha256_hex(data: PackedByteArray) -> String: + var sha256 := SHA256Hash.new() + sha256.start() + sha256.update(data) + var hash := sha256.finish() + var hex_chars := "0123456789abcdef" + var result := "" + for byte_val in hash: + result += hex_chars[(byte_val >> 4) & 0x0F] + result += hex_chars[byte_val & 0x0F] + return result From 504c92301a70ec88230720d7e06ba36e1ac5284b Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 21:57:04 +0000 Subject: [PATCH 10/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/world_object.gd | 93 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 scripts/world_object.gd diff --git a/scripts/world_object.gd b/scripts/world_object.gd new file mode 100644 index 0000000..4e7dd8e --- /dev/null +++ b/scripts/world_object.gd @@ -0,0 +1,93 @@ +class_name WorldObject +extends RefCounted +## A world object (prop, interactable, spawn point, etc.) within a chunk. +## +## This is the runtime representation used by [ChunkScene] to hold object +## metadata. It mirrors [ChunkDataDecoder.WorldObjectData] but is a standalone +## class so it can be referenced by scene nodes. + +## Stable object ID, unique within the chunk. +var id: String = "" + +## Object type: "prop", "interactable", "spawn_point", "door", etc. +var type: String = "" + +## Reference to the object's mesh/glTF asset. +var asset_id: String = "" + +## World-space position. +var position: Vector3 = Vector3.ZERO + +## World-space rotation as a quaternion (x, y, z, w). +var rotation: Vector4 = Vector4(0, 0, 0, 1) + +## World-space scale. +var scale: Vector3 = Vector3.ONE + +## Type-specific key/value properties. +var properties: Dictionary = {} + +## Whether this is a spawnable dynamic entity template. +var is_dynamic: bool = false + +## Per-object content moderation flags. +var flags: ChunkDataDecoder.ContentFlags = ChunkDataDecoder.ContentFlags.new() + +## Whether this object has been modified locally. +var is_dirty: bool = false + + +func _init( + p_id: String = "", + p_type: String = "", + p_asset_id: String = "", + p_position: Vector3 = Vector3.ZERO, + p_rotation: Vector4 = Vector4(0, 0, 0, 1), + p_scale: Vector3 = Vector3.ONE, + p_properties: Dictionary = {}, + p_dynamic: bool = false, + p_flags: ChunkDataDecoder.ContentFlags = null +) -> void: + id = p_id + type = p_type + asset_id = p_asset_id + position = p_position + rotation = p_rotation + scale = p_scale + properties = p_properties + is_dynamic = p_dynamic + if p_flags != null: + flags = p_flags + + +## Convert to a dictionary representation. +func to_dict() -> Dictionary: + return { + "id": id, + "type": type, + "asset_id": asset_id, + "position": position, + "rotation": rotation, + "scale": scale, + "properties": properties, + "dynamic": is_dynamic, + "flags": flags.to_dict(), + } + + +## Create a WorldObject from a dictionary. +static func from_dict(d: Dictionary) -> WorldObject: + var flags := ChunkDataDecoder.ContentFlags.new() + if d.has("flags"): + flags.from_dict(d["flags"]) + return WorldObject.new( + d.get("id", ""), + d.get("type", ""), + d.get("asset_id", ""), + d.get("position", Vector3.ZERO), + d.get("rotation", Vector4(0, 0, 0, 1)), + d.get("scale", Vector3.ONE), + d.get("properties", {}), + d.get("dynamic", false), + flags, + ) From d752cf528037ae668154c7ddccbbf2de90a95c89 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 21:57:57 +0000 Subject: [PATCH 11/20] Recovered orphaned WIP before switching to a new task --- scripts/world_object.gd | 80 +++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/scripts/world_object.gd b/scripts/world_object.gd index 4e7dd8e..8895fff 100644 --- a/scripts/world_object.gd +++ b/scripts/world_object.gd @@ -1,15 +1,15 @@ class_name WorldObject extends RefCounted -## A world object (prop, interactable, spawn point, etc.) within a chunk. +## A world object (static or dynamic) within a chunk. ## -## This is the runtime representation used by [ChunkScene] to hold object -## metadata. It mirrors [ChunkDataDecoder.WorldObjectData] but is a standalone -## class so it can be referenced by scene nodes. +## This is the runtime representation of a WorldObject from the chunk data +## format (see docs/architecture/world.md §3.3). It holds the object's +## identity, transform, properties, and moderation flags. ## Stable object ID, unique within the chunk. var id: String = "" -## Object type: "prop", "interactable", "spawn_point", "door", etc. +## Object type (e.g. "prop", "interactable", "spawn_point", "door"). var type: String = "" ## Reference to the object's mesh/glTF asset. @@ -27,7 +27,7 @@ var scale: Vector3 = Vector3.ONE ## Type-specific key/value properties. var properties: Dictionary = {} -## Whether this is a spawnable dynamic entity template. +## Whether this is a spawnable dynamic entity (true) or static (false). var is_dynamic: bool = false ## Per-object content moderation flags. @@ -60,34 +60,76 @@ func _init( flags = p_flags -## Convert to a dictionary representation. +## Get a property value by key. +func get_property(key: String) -> Variant: + return properties.get(key) + + +## Set a property value. +func set_property(key: String, value: Variant) -> void: + properties[key] = value + is_dirty = true + + +## Check if this object is walkable (based on properties). +func is_walkable() -> bool: + return properties.get("walkable", true) + + +## Check if this object is climbable (based on properties). +func is_climbable() -> bool: + return properties.get("climbable", false) + + +## Check if this object is interactable. +func is_interactable() -> bool: + return type == "interactable" or properties.get("interactable", false) + + +## Get the object's display name. +func get_display_name() -> String: + return properties.get("display_name", id) + + +## Serialize this object to a dictionary (for networking). func to_dict() -> Dictionary: return { "id": id, "type": type, "asset_id": asset_id, - "position": position, - "rotation": rotation, - "scale": scale, + "position": [position.x, position.y, position.z], + "rotation": [rotation.x, rotation.y, rotation.z, rotation.w], + "scale": [scale.x, scale.y, scale.z], "properties": properties, "dynamic": is_dynamic, - "flags": flags.to_dict(), } -## Create a WorldObject from a dictionary. +## Create a WorldObject from a dictionary (for networking). static func from_dict(d: Dictionary) -> WorldObject: - var flags := ChunkDataDecoder.ContentFlags.new() - if d.has("flags"): - flags.from_dict(d["flags"]) + var pos := Vector3( + d.get("position", [0, 0, 0])[0], + d.get("position", [0, 0, 0])[1], + d.get("position", [0, 0, 0])[2] + ) + var rot := Vector4( + d.get("rotation", [0, 0, 0, 1])[0], + d.get("rotation", [0, 0, 0, 1])[1], + d.get("rotation", [0, 0, 0, 1])[2], + d.get("rotation", [0, 0, 0, 1])[3] + ) + var scl := Vector3( + d.get("scale", [1, 1, 1])[0], + d.get("scale", [1, 1, 1])[1], + d.get("scale", [1, 1, 1])[2] + ) return WorldObject.new( d.get("id", ""), d.get("type", ""), d.get("asset_id", ""), - d.get("position", Vector3.ZERO), - d.get("rotation", Vector4(0, 0, 0, 1)), - d.get("scale", Vector3.ONE), + pos, + rot, + scl, d.get("properties", {}), d.get("dynamic", false), - flags, ) From 720cf6f6b9965c71a4435b5dd919df984cfde229 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 00:03:28 +0000 Subject: [PATCH 12/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/dynamic_object_node.gd | 68 ++++++++++++++++++++ scripts/static_object_node.gd | 67 ++++++++++++++++++++ scripts/tile_def.gd | 42 +++++++++++++ scripts/tile_layer.gd | 83 ++++++++++++++++++++++++ scripts/tile_layer_node.gd | 112 +++++++++++++++++++++++++++++++++ 5 files changed, 372 insertions(+) create mode 100644 scripts/dynamic_object_node.gd create mode 100644 scripts/static_object_node.gd create mode 100644 scripts/tile_def.gd create mode 100644 scripts/tile_layer.gd create mode 100644 scripts/tile_layer_node.gd diff --git a/scripts/dynamic_object_node.gd b/scripts/dynamic_object_node.gd new file mode 100644 index 0000000..f37edad --- /dev/null +++ b/scripts/dynamic_object_node.gd @@ -0,0 +1,68 @@ +class_name DynamicObjectNode +extends Node3D +## A visual representation of a dynamic (spawnable) world object template. +## +## This node is instantiated by [ChunkScene] for each dynamic [WorldObject]. +## Dynamic objects are stored as *templates* in the chunk; their runtime state +## (position, health, inventory) is managed by the entity system, not by this +## node. This node renders the template's visual representation so the object +## is visible in the world. +## +## ## Structure +## +## ``` +## DynamicObjectNode (Node3D) +## └── MeshInstance3D # the object's visual mesh +## ``` + +## The world object data this node represents. +var world_object: WorldObject = null + +## The loaded mesh instance (or placeholder). +var _mesh_instance: MeshInstance3D = null + + +func _ready() -> void: + if world_object == null: + return + _load_mesh() + + +## Load and attach the object's mesh. +func _load_mesh() -> void: + _mesh_instance = MeshInstance3D.new() + _mesh_instance.name = "mesh" + + var mesh := _load_asset_mesh(world_object.asset_id) + if mesh == null: + mesh = _make_placeholder_mesh() + + _mesh_instance.mesh = mesh + add_child(_mesh_instance) + + +## Attempt to load a mesh from the asset path. +func _load_asset_mesh(asset_id: String) -> Mesh: + if asset_id.is_empty(): + return null + var path := "res://assets/%s" % asset_id + if ResourceLoader.exists(path): + var res := load(path) + if res is Mesh: + return res + return null + + +## Create a simple placeholder sphere mesh when the real asset is unavailable. +func _make_placeholder_mesh() -> Mesh: + var mesh := SphereMesh.new() + mesh.radius = 0.5 + mesh.height = 1.0 + return mesh + + +## Get the world object's display name. +func get_display_name() -> String: + if world_object == null: + return "" + return world_object.get_display_name() diff --git a/scripts/static_object_node.gd b/scripts/static_object_node.gd new file mode 100644 index 0000000..5a4bea8 --- /dev/null +++ b/scripts/static_object_node.gd @@ -0,0 +1,67 @@ +class_name StaticObjectNode +extends Node3D +## A visual representation of a static world object within a chunk. +## +## This node is instantiated by [ChunkScene] for each static (non-dynamic) +## [WorldObject]. It renders the object's mesh (referenced by `asset_id`) at +## the object's transform. If the asset is not yet loaded, a placeholder mesh +## is shown. +## +## ## Structure +## +## ``` +## StaticObjectNode (Node3D) +## └── MeshInstance3D # the object's visual mesh +## ``` + +## The world object data this node represents. +var world_object: WorldObject = null + +## The loaded mesh instance (or placeholder). +var _mesh_instance: MeshInstance3D = null + + +func _ready() -> void: + if world_object == null: + return + _load_mesh() + + +## Load and attach the object's mesh. +func _load_mesh() -> void: + _mesh_instance = MeshInstance3D.new() + _mesh_instance.name = "mesh" + + # Try to load the asset by its id (path). Fall back to a placeholder. + var mesh := _load_asset_mesh(world_object.asset_id) + if mesh == null: + mesh = _make_placeholder_mesh() + + _mesh_instance.mesh = mesh + add_child(_mesh_instance) + + +## Attempt to load a mesh from the asset path. +func _load_asset_mesh(asset_id: String) -> Mesh: + if asset_id.is_empty(): + return null + var path := "res://assets/%s" % asset_id + if ResourceLoader.exists(path): + var res := load(path) + if res is Mesh: + return res + return null + + +## Create a simple placeholder box mesh when the real asset is unavailable. +func _make_placeholder_mesh() -> Mesh: + var mesh := BoxMesh.new() + mesh.size = Vector3(1.0, 1.0, 1.0) + return mesh + + +## Get the world object's display name. +func get_display_name() -> String: + if world_object == null: + return "" + return world_object.get_display_name() diff --git a/scripts/tile_def.gd b/scripts/tile_def.gd new file mode 100644 index 0000000..0fa7475 --- /dev/null +++ b/scripts/tile_def.gd @@ -0,0 +1,42 @@ +class_name TileDef +extends RefCounted +## A single tile definition (palette entry) within a tile layer. +## +## Maps a tile ID to its asset reference and gameplay flags. See +## docs/architecture/world.md §3.2. + +## Tile ID (index into the layer palette). 0 is reserved for empty/air. +var id: int = 0 + +## Reference to the tile/terrain asset. +var asset_id: String = "" + +## Whether this tile has collision. +var solid: bool = false + +## Bitmask flags: bit 0 = walkable, bit 1 = climbable, bit 2 = liquid. +var flags: int = 0 + +## Number of visual variants for this tile. +var variants: int = 1 + + +func _init(p_id: int = 0, p_asset_id: String = "", p_solid: bool = false, + p_flags: int = 0, p_variants: int = 1) -> void: + id = p_id + asset_id = p_asset_id + solid = p_solid + flags = p_flags + variants = p_variants + + +func is_walkable() -> bool: + return (flags & 0x01) != 0 + + +func is_climbable() -> bool: + return (flags & 0x02) != 0 + + +func is_liquid() -> bool: + return (flags & 0x04) != 0 diff --git a/scripts/tile_layer.gd b/scripts/tile_layer.gd new file mode 100644 index 0000000..c3815b9 --- /dev/null +++ b/scripts/tile_layer.gd @@ -0,0 +1,83 @@ +class_name TileLayer +extends RefCounted +## A single tile layer within a chunk. +## +## Holds the dense grid of tile IDs, the palette mapping tile IDs to tile +## definitions, and the layer metadata. This is the runtime representation of +## the `TileLayer` from the chunk data format (see docs/architecture/world.md +## §3.2). + +## Layer name (e.g. "ground", "walls", "decor"). +var name: String = "" + +## Numeric layer id, unique within the chunk. +var layer_id: int = 0 + +## Grid dimensions in tiles. +var width: int = 0 +var height: int = 0 +var depth: int = 1 + +## Palette mapping tile IDs to tile definitions (shared per layer). +var palette: Array[TileDef] = [] + +## Dense, row-major tile IDs. Length = width * height * depth. +var tiles: PackedInt32Array = PackedInt32Array() + + +func _init( + p_layer_name: String = "", + p_layer_id: int = 0, + p_width: int = 0, + p_height: int = 0, + p_depth: int = 1 +) -> void: + name = p_layer_name + layer_id = p_layer_id + width = p_width + height = p_height + depth = p_depth + + +## Total number of tiles in this layer. +func tile_count() -> int: + return width * height * depth + + +## Get the tile ID at (x, y, z). Returns 0 (empty) for out-of-bounds. +func get_tile(x: int, y: int, z: int = 0) -> int: + if x < 0 or x >= width or y < 0 or y >= height or z < 0 or z >= depth: + return 0 + var idx := (z * height + y) * width + x + if idx < tiles.size(): + return tiles[idx] + return 0 + + +## Set the tile ID at (x, y, z). No-op if out of bounds. +func set_tile(x: int, y: int, z: int, tile_id: int) -> void: + if x < 0 or x >= width or y < 0 or y >= height or z < 0 or z >= depth: + return + var idx := (z * height + y) * width + x + if idx < tiles.size(): + tiles[idx] = tile_id + + +## Get the tile definition for a tile ID, or null if not in the palette. +func get_tile_def(tile_id: int) -> TileDef: + for def in palette: + if def.id == tile_id: + return def + return null + + +## Whether the tile at (x, y, z) is solid (collision). +func is_solid(x: int, y: int, z: int = 0) -> bool: + var def := get_tile_def(get_tile(x, y, z)) + return def != null and def.solid + + +## Whether the tile at (x, y, z) is walkable. +func is_walkable(x: int, y: int, z: int = 0) -> bool: + var def := get_tile_def(get_tile(x, y, z)) + return def != null and def.is_walkable() diff --git a/scripts/tile_layer_node.gd b/scripts/tile_layer_node.gd new file mode 100644 index 0000000..0162de8 --- /dev/null +++ b/scripts/tile_layer_node.gd @@ -0,0 +1,112 @@ +class_name TileLayerNode +extends Node3D +## A visual representation of a single tile layer within a chunk. +## +## This node is instantiated by [ChunkScene] for each tile layer in a chunk. +## It renders the layer's tiles as instanced meshes (one per tile definition +## in the palette) using [MultiMeshInstance3D] for efficiency. +## +## ## Structure +## +## ``` +## TileLayerNode (Node3D) +## └── MultiMeshInstance3D # one per palette entry (tile definition) +## ``` + +## The tile layer data this node renders. +var tile_layer: TileLayer = null + +## The chunk coordinate this layer belongs to (for world positioning). +var chunk_coord: Vector3i = Vector3i.ZERO + +## Tile size in world units (each tile is 1x1x1). +const TILE_SIZE: float = 1.0 + +## Cache of MultiMeshInstance3D nodes, keyed by tile definition id. +var _mesh_instances: Dictionary = {} + + +func _ready() -> void: + if tile_layer == null: + return + _build_meshes() + + +## Build the MultiMesh instances for each tile definition in the palette. +func _build_meshes() -> void: + for tile_def in tile_layer.palette: + if tile_def.id == 0: + continue # Tile 0 is "empty/air", no mesh needed + _create_mesh_for_tile(tile_def) + + +func _create_mesh_for_tile(tile_def: TileDef) -> void: + # Count how many cells use this tile definition + var count := 0 + for tile_id in tile_layer.tiles: + if tile_id == tile_def.id: + count += 1 + + if count == 0: + return + + var multi_mesh := MultiMesh.new() + multi_mesh.transform_format = MultiMesh.TRANSFORM_3D + multi_mesh.mesh = _make_unit_cube_mesh() + multi_mesh.instance_count = count + + var instance_idx := 0 + for z in range(tile_layer.depth): + for y in range(tile_layer.height): + for x in range(tile_layer.width): + var tile_id := tile_layer.get_tile(x, y, z) + if tile_id != tile_def.id: + continue + var world_pos := _tile_to_world(x, y, z) + var xform := Transform3D(Basis(), world_pos) + multi_mesh.set_instance_transform(instance_idx, xform) + instance_idx += 1 + + var mesh_instance := MultiMeshInstance3D.new() + mesh_instance.multimesh = multi_mesh + mesh_instance.name = "tiles_%d" % tile_def.id + add_child(mesh_instance) + _mesh_instances[tile_def.id] = mesh_instance + + +## Convert a tile grid coordinate to a world-space position (center of tile). +func _tile_to_world(x: int, y: int, z: int) -> Vector3: + var origin := Vector3( + chunk_coord.x * tile_layer.width, + chunk_coord.y * tile_layer.height, + chunk_coord.z * tile_layer.depth + ) + return origin + Vector3( + x * TILE_SIZE + TILE_SIZE * 0.5, + y * TILE_SIZE + TILE_SIZE * 0.5, + z * TILE_SIZE + TILE_SIZE * 0.5 + ) + + +## Create a simple unit cube mesh for tile rendering. +func _make_unit_cube_mesh() -> Mesh: + var mesh := BoxMesh.new() + mesh.size = Vector3(TILE_SIZE, TILE_SIZE, TILE_SIZE) + return mesh + + +## Get the tile definition at a grid position, or null if empty. +func get_tile_def_at(x: int, y: int, z: int = 0) -> TileDef: + if tile_layer == null: + return null + var tile_id := tile_layer.get_tile(x, y, z) + if tile_id == 0: + return null + return tile_layer.get_tile_def(tile_id) + + +## Clear all rendered meshes. +func clear() -> void: + for child in get_children(): + child.queue_free() + _mesh_instances.clear() From 97949d6b4482ad2fdeb38fa32eafd58213fc8eec Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 00:08:24 +0000 Subject: [PATCH 13/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scenes/main.tscn | 6 ++++++ scripts/chunk_stream_manager.gd | 32 +++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 scenes/main.tscn diff --git a/scenes/main.tscn b/scenes/main.tscn new file mode 100644 index 0000000..a1be944 --- /dev/null +++ b/scenes/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://main_scene"] + +[ext_resource type="Script" path="res://scripts/chunk_stream_manager.gd" id="1_chunk_stream"] + +[node name="Main" type="Node3D"] +script = ExtResource("1_chunk_stream") diff --git a/scripts/chunk_stream_manager.gd b/scripts/chunk_stream_manager.gd index 27d04bc..f19f9b2 100644 --- a/scripts/chunk_stream_manager.gd +++ b/scripts/chunk_stream_manager.gd @@ -77,18 +77,32 @@ var _server_rpc: Object = null ## Internal: track which chunks are currently being loaded (to avoid duplicates). var _loading_in_progress: Dictionary = {} +## Internal: time accumulator for throttled streaming updates. +var _update_accumulator: float = 0.0 + +## Internal: interval between streaming updates (seconds). +const UPDATE_INTERVAL: float = 0.1 + func _ready() -> void: - _process_thread = Thread.create(_streaming_loop, Thread.PRIORITY_LOW) + # Streaming is driven from _process (main thread) so we can safely + # manipulate the scene tree. No background thread is used. + set_process(true) -func _streaming_loop() -> void: - while is_inside_tree(): - _update_streaming() - _process_load_queue() - _process_unload_queue() - streaming_updated.emit() - await get_tree().create_timer(0.1).timeout # Check every 100ms +func _process(delta: float) -> void: + if not is_active or player == null: + return + + _update_accumulator += delta + if _update_accumulator < UPDATE_INTERVAL: + return + _update_accumulator = 0.0 + + _update_streaming() + _process_load_queue() + _process_unload_queue() + streaming_updated.emit() func _update_streaming() -> void: @@ -132,7 +146,7 @@ func _compute_target_chunks() -> Dictionary: var half := streaming_radius for dx in range(-half, half + 1): for dy in range(-half, half + 1): - for dz in range(-Chunk.CHUNK_SIZE_Z, Chunk.CHUNK_SIZE_Z + 1): + for dz in range(-half, half + 1): var coord := Vector3i( _player_chunk_coord.x + dx, _player_chunk_coord.y + dy, From 342d0f69d98260ffe9504876964691f717b1201b Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 00:14:14 +0000 Subject: [PATCH 14/20] WIP checkpoint: Implement chunk streaming system in GDScript --- scripts/chunk_stream_manager.gd | 48 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/scripts/chunk_stream_manager.gd b/scripts/chunk_stream_manager.gd index f19f9b2..8c6802e 100644 --- a/scripts/chunk_stream_manager.gd +++ b/scripts/chunk_stream_manager.gd @@ -109,7 +109,7 @@ func _update_streaming() -> void: if player == null or not is_inside_tree(): return - var current_coord := Chunk.coord_for_position(player.global_position) + var current_coord: Vector3i = Chunk.coord_for_position(player.global_position) if current_coord == _player_chunk_coord: return @@ -118,7 +118,7 @@ func _update_streaming() -> void: func _rebuild_queues() -> void: - var target_set := _compute_target_chunks() + var target_set: Dictionary = _compute_target_chunks() # Determine which chunks need to be loaded var to_load: Array[Vector3i] = [] @@ -142,12 +142,12 @@ func _rebuild_queues() -> void: func _compute_target_chunks() -> Dictionary: """Compute the set of chunk coordinates that should be loaded.""" - var target := {} - var half := streaming_radius + var target: Dictionary = {} + var half: int = streaming_radius for dx in range(-half, half + 1): for dy in range(-half, half + 1): for dz in range(-half, half + 1): - var coord := Vector3i( + var coord: Vector3i = Vector3i( _player_chunk_coord.x + dx, _player_chunk_coord.y + dy, _player_chunk_coord.z + dz @@ -158,30 +158,30 @@ func _compute_target_chunks() -> Dictionary: func _compare_distance(a: Vector3i, b: Vector3i) -> bool: """Compare chunk coordinates by distance from player chunk.""" - var dist_a := _chunk_distance_squared(_player_chunk_coord, a) - var dist_b := _chunk_distance_squared(_player_chunk_coord, b) + var dist_a: int = _chunk_distance_squared(_player_chunk_coord, a) + var dist_b: int = _chunk_distance_squared(_player_chunk_coord, b) return dist_a < dist_b func _chunk_distance_squared(a: Vector3i, b: Vector3i) -> int: - var dx := a.x - b.x - var dy := a.y - b.y - var dz := a.z - b.z + var dx: int = a.x - b.x + var dy: int = a.y - b.y + var dz: int = a.z - b.z return dx * dx + dy * dy + dz * dz func _process_load_queue() -> void: - var count := 0 + var count: int = 0 while count < max_loads_per_frame and not _load_queue.is_empty(): - var coord := _load_queue.pop_front() + var coord: Vector3i = _load_queue.pop_front() _load_chunk(coord) count += 1 func _process_unload_queue() -> void: - var count := 0 + var count: int = 0 while count < max_unloads_per_frame and not _unload_queue.is_empty(): - var coord := _unload_queue.pop_front() + var coord: Vector3i = _unload_queue.pop_front() _unload_chunk(coord) count += 1 @@ -204,14 +204,14 @@ func _load_chunk(coord: Vector3i) -> void: func _on_chunk_data_received(key: String, binary_data: PackedByteArray) -> void: """Called by the networking layer when chunk data arrives from server.""" - var coord := _parse_coord_from_key(key) + var coord: Vector3i = _parse_coord_from_key(key) if coord == null: return _loading_in_progress.erase(coord) # Decode the binary chunk data - var chunk_data := ChunkDataDecoder.decode_binary(binary_data) + var chunk_data: ChunkDataDecoder.ChunkPayload = ChunkDataDecoder.decode_binary(binary_data) if chunk_data == null: chunk_load_failed.emit(coord, "Failed to decode chunk data") return @@ -228,7 +228,7 @@ func _on_chunk_load_error(coord: Vector3i, error_msg: String) -> void: func _instantiate_chunk_scene(coord: Vector3i) -> void: """Create an empty chunk scene for the given coordinate.""" - var chunk_scene := ChunkScene.new() + var chunk_scene: ChunkScene = ChunkScene.new() chunk_scene.coord = coord chunk_scene.key = Chunk.build_key("", "", coord) chunk_scene.bounds = Chunk.compute_bounds(coord) @@ -237,7 +237,7 @@ func _instantiate_chunk_scene(coord: Vector3i) -> void: func _instantiate_chunk_scene_with_data(coord: Vector3i, data: ChunkDataDecoder.ChunkPayload) -> void: """Create a chunk scene populated with server data.""" - var chunk_scene := ChunkScene.new() + var chunk_scene: ChunkScene = ChunkScene.new() chunk_scene.coord = coord chunk_scene.key = data.key chunk_scene.bounds = data.bounds @@ -246,7 +246,7 @@ func _instantiate_chunk_scene_with_data(coord: Vector3i, data: ChunkDataDecoder. # Add tiles for layer in data.tile_layers: - var tile_layer := TileLayer.new() + var tile_layer: TileLayer = TileLayer.new() tile_layer.name = layer.name tile_layer.layer_id = layer.layer_id tile_layer.width = layer.width @@ -258,7 +258,7 @@ func _instantiate_chunk_scene_with_data(coord: Vector3i, data: ChunkDataDecoder. # Add objects for obj_data in data.objects: - var world_obj := WorldObject.new() + var world_obj: WorldObject = WorldObject.new() world_obj.id = obj_data.id world_obj.type = obj_data.type world_obj.asset_id = obj_data.asset_id @@ -287,7 +287,7 @@ func _unload_chunk(coord: Vector3i) -> void: if not _loaded_chunks.has(coord): return - var chunk_scene = _loaded_chunks[coord] + var chunk_scene: Node = _loaded_chunks[coord] if is_instance_valid(chunk_scene): chunk_scene.queue_free() @@ -339,16 +339,16 @@ func force_reload_chunk(coord: Vector3i) -> void: func clear_all_chunks() -> void: """Unload all chunks immediately.""" - var coords := get_all_loaded_coords() + var coords: Array[Vector3i] = get_all_loaded_coords() for coord in coords: _unload_chunk(coord) _load_queue.clear() _unload_queue.clear() -func _parse_coord_from_key(key: String) -> Vector3i: +func _parse_coord_from_key(key: String) -> Variant: """Parse chunk coordinate from a canonical chunk key.""" - var parts := key.split(":") + var parts: Array[String] = key.split(":") if parts.size() < 5: return null return Vector3i( From 97e0804dfe23cf451c618b90313b947627dc7e2b Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 00:14:34 +0000 Subject: [PATCH 15/20] WIP checkpoint: Implement chunk streaming system in GDScript --- .godot/.gdignore | 1 + .godot/editor/filesystem_cache10 | 22 +++++ ...lding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg | 5 ++ .godot/editor/project_metadata.cfg | 11 +++ .godot/global_script_class_cache.cfg | 81 ++++++++++++++++++ .godot/uid_cache.bin | Bin 0 -> 430 bytes scripts/chunk.gd.uid | 1 + scripts/chunk_data_decoder.gd.uid | 1 + scripts/chunk_scene.gd.uid | 1 + scripts/chunk_stream_manager.gd.uid | 1 + scripts/dynamic_object_node.gd.uid | 1 + scripts/static_object_node.gd.uid | 1 + scripts/tile_def.gd.uid | 1 + scripts/tile_layer.gd.uid | 1 + scripts/tile_layer_node.gd.uid | 1 + scripts/world_object.gd.uid | 1 + 16 files changed, 130 insertions(+) create mode 100644 .godot/.gdignore create mode 100644 .godot/editor/filesystem_cache10 create mode 100644 .godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg create mode 100644 .godot/editor/project_metadata.cfg create mode 100644 .godot/global_script_class_cache.cfg create mode 100644 .godot/uid_cache.bin create mode 100644 scripts/chunk.gd.uid create mode 100644 scripts/chunk_data_decoder.gd.uid create mode 100644 scripts/chunk_scene.gd.uid create mode 100644 scripts/chunk_stream_manager.gd.uid create mode 100644 scripts/dynamic_object_node.gd.uid create mode 100644 scripts/static_object_node.gd.uid create mode 100644 scripts/tile_def.gd.uid create mode 100644 scripts/tile_layer.gd.uid create mode 100644 scripts/tile_layer_node.gd.uid create mode 100644 scripts/world_object.gd.uid diff --git a/.godot/.gdignore b/.godot/.gdignore new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.godot/.gdignore @@ -0,0 +1 @@ + diff --git a/.godot/editor/filesystem_cache10 b/.godot/editor/filesystem_cache10 new file mode 100644 index 0000000..bed9d4d --- /dev/null +++ b/.godot/editor/filesystem_cache10 @@ -0,0 +1,22 @@ +63f7b34db8d8cdea90c76aacccf841ec +::res://::1787012057 +README.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: +::res://docs/::1787011777 +::res://docs/architecture/::1787011795 +avatar.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: +content_moderation.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: +networking.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: +world.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: +::res://scenes/::1787011795 +main.tscn::PackedScene::-1::1787011795::0::1::::<><><>0<>0<><>::res://scripts/chunk_stream_manager.gd +::res://scripts/::1787011795 +chunk.gd::GDScript::7364239597441807588::1787011795::0::1::::Chunk<>Node3D<><>0<>0<><>:: +chunk_data_decoder.gd::GDScript::3922068956043581888::1787011795::0::1::::ChunkDataDecoder<>RefCounted<><>0<>0<><>:: +chunk_scene.gd::GDScript::9207602004456600331::1787011795::0::1::::ChunkScene<>Node3D<><>0<>0<><>:: +chunk_stream_manager.gd::GDScript::2366645676568508746::1787011963::0::1::::ChunkStreamManager<>Node<><>0<>0<><>:: +dynamic_object_node.gd::GDScript::8863349716576659118::1787011795::0::1::::DynamicObjectNode<>Node3D<><>0<>0<><>:: +static_object_node.gd::GDScript::5276389741929225897::1787011795::0::1::::StaticObjectNode<>Node3D<><>0<>0<><>:: +tile_def.gd::GDScript::2430528079284085109::1787011795::0::1::::TileDef<>RefCounted<><>0<>0<><>:: +tile_layer.gd::GDScript::4797906255131447844::1787011795::0::1::::TileLayer<>RefCounted<><>0<>0<><>:: +tile_layer_node.gd::GDScript::9033225196818247616::1787011795::0::1::::TileLayerNode<>Node3D<><>0<>0<><>:: +world_object.gd::GDScript::6429674491464200335::1787011795::0::1::::WorldObject<>RefCounted<><>0<>0<><>:: diff --git a/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg b/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg new file mode 100644 index 0000000..0f30ddf --- /dev/null +++ b/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg @@ -0,0 +1,5 @@ +[folding] + +node_unfolds=[NodePath("."), PackedStringArray("Visibility")] +resource_unfolds=[] +nodes_folded=[] diff --git a/.godot/editor/project_metadata.cfg b/.godot/editor/project_metadata.cfg new file mode 100644 index 0000000..e515609 --- /dev/null +++ b/.godot/editor/project_metadata.cfg @@ -0,0 +1,11 @@ +[game_view] + +select_mode=0 + +[editor_metadata] + +executable_path="/home/daddy/godot/godot" + +[recent_files] + +scenes=["res://scenes/main.tscn"] diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg new file mode 100644 index 0000000..e1f5257 --- /dev/null +++ b/.godot/global_script_class_cache.cfg @@ -0,0 +1,81 @@ +list=[{ +"base": &"Node3D", +"class": &"Chunk", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/chunk.gd" +}, { +"base": &"RefCounted", +"class": &"ChunkDataDecoder", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/chunk_data_decoder.gd" +}, { +"base": &"Node3D", +"class": &"ChunkScene", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/chunk_scene.gd" +}, { +"base": &"Node", +"class": &"ChunkStreamManager", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/chunk_stream_manager.gd" +}, { +"base": &"Node3D", +"class": &"DynamicObjectNode", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/dynamic_object_node.gd" +}, { +"base": &"Node3D", +"class": &"StaticObjectNode", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/static_object_node.gd" +}, { +"base": &"RefCounted", +"class": &"TileDef", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/tile_def.gd" +}, { +"base": &"RefCounted", +"class": &"TileLayer", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/tile_layer.gd" +}, { +"base": &"Node3D", +"class": &"TileLayerNode", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/tile_layer_node.gd" +}, { +"base": &"RefCounted", +"class": &"WorldObject", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/world_object.gd" +}] diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin new file mode 100644 index 0000000000000000000000000000000000000000..20bf5bff07b8ce51cfd5bfa1140ae5ee626e3c5a GIT binary patch literal 430 zcmd;LU|@Jsl2rGU!#GV0$Sz7Pw$j%xPAGW6wqz}bnu(h literal 0 HcmV?d00001 diff --git a/scripts/chunk.gd.uid b/scripts/chunk.gd.uid new file mode 100644 index 0000000..85995ed --- /dev/null +++ b/scripts/chunk.gd.uid @@ -0,0 +1 @@ +uid://dc6j7kqjitc6g diff --git a/scripts/chunk_data_decoder.gd.uid b/scripts/chunk_data_decoder.gd.uid new file mode 100644 index 0000000..f938701 --- /dev/null +++ b/scripts/chunk_data_decoder.gd.uid @@ -0,0 +1 @@ +uid://bv445xrd6f5tk diff --git a/scripts/chunk_scene.gd.uid b/scripts/chunk_scene.gd.uid new file mode 100644 index 0000000..8f1e1d0 --- /dev/null +++ b/scripts/chunk_scene.gd.uid @@ -0,0 +1 @@ +uid://d4gh4h5iey6xb diff --git a/scripts/chunk_stream_manager.gd.uid b/scripts/chunk_stream_manager.gd.uid new file mode 100644 index 0000000..7f35dca --- /dev/null +++ b/scripts/chunk_stream_manager.gd.uid @@ -0,0 +1 @@ +uid://8yokqwe23c23 diff --git a/scripts/dynamic_object_node.gd.uid b/scripts/dynamic_object_node.gd.uid new file mode 100644 index 0000000..b24b92a --- /dev/null +++ b/scripts/dynamic_object_node.gd.uid @@ -0,0 +1 @@ +uid://dyjqcstdnr4u1 diff --git a/scripts/static_object_node.gd.uid b/scripts/static_object_node.gd.uid new file mode 100644 index 0000000..8f99b0c --- /dev/null +++ b/scripts/static_object_node.gd.uid @@ -0,0 +1 @@ +uid://chf6mruev1kc4 diff --git a/scripts/tile_def.gd.uid b/scripts/tile_def.gd.uid new file mode 100644 index 0000000..94401fc --- /dev/null +++ b/scripts/tile_def.gd.uid @@ -0,0 +1 @@ +uid://bavmpbmthilbx diff --git a/scripts/tile_layer.gd.uid b/scripts/tile_layer.gd.uid new file mode 100644 index 0000000..34c0c4a --- /dev/null +++ b/scripts/tile_layer.gd.uid @@ -0,0 +1 @@ +uid://came2ygke3ajs diff --git a/scripts/tile_layer_node.gd.uid b/scripts/tile_layer_node.gd.uid new file mode 100644 index 0000000..e394f0a --- /dev/null +++ b/scripts/tile_layer_node.gd.uid @@ -0,0 +1 @@ +uid://d1x05tq8h43gm diff --git a/scripts/world_object.gd.uid b/scripts/world_object.gd.uid new file mode 100644 index 0000000..c65fb95 --- /dev/null +++ b/scripts/world_object.gd.uid @@ -0,0 +1 @@ +uid://cxut2ghbsvobn From 1c5c0f8c8ed0a2e778297a447515ed99bc4d496a Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 00:15:16 +0000 Subject: [PATCH 16/20] WIP checkpoint: Implement chunk streaming system in GDScript --- .godot/editor/filesystem_cache10 | 6 +++--- scripts/chunk_data_decoder.gd | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.godot/editor/filesystem_cache10 b/.godot/editor/filesystem_cache10 index bed9d4d..f3364e6 100644 --- a/.godot/editor/filesystem_cache10 +++ b/.godot/editor/filesystem_cache10 @@ -1,5 +1,5 @@ 63f7b34db8d8cdea90c76aacccf841ec -::res://::1787012057 +::res://::1787012074 README.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: ::res://docs/::1787011777 ::res://docs/architecture/::1787011795 @@ -9,9 +9,9 @@ networking.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: world.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: ::res://scenes/::1787011795 main.tscn::PackedScene::-1::1787011795::0::1::::<><><>0<>0<><>::res://scripts/chunk_stream_manager.gd -::res://scripts/::1787011795 +::res://scripts/::1787012062 chunk.gd::GDScript::7364239597441807588::1787011795::0::1::::Chunk<>Node3D<><>0<>0<><>:: -chunk_data_decoder.gd::GDScript::3922068956043581888::1787011795::0::1::::ChunkDataDecoder<>RefCounted<><>0<>0<><>:: +chunk_data_decoder.gd::GDScript::3922068956043581888::1787012104::0::1::::ChunkDataDecoder<>RefCounted<><>0<>0<><>:: chunk_scene.gd::GDScript::9207602004456600331::1787011795::0::1::::ChunkScene<>Node3D<><>0<>0<><>:: chunk_stream_manager.gd::GDScript::2366645676568508746::1787011963::0::1::::ChunkStreamManager<>Node<><>0<>0<><>:: dynamic_object_node.gd::GDScript::8863349716576659118::1787011795::0::1::::DynamicObjectNode<>Node3D<><>0<>0<><>:: diff --git a/scripts/chunk_data_decoder.gd b/scripts/chunk_data_decoder.gd index b490ca2..bfb4fed 100644 --- a/scripts/chunk_data_decoder.gd +++ b/scripts/chunk_data_decoder.gd @@ -161,13 +161,13 @@ class ChunkPayload: # ─── Binary Decoding ──────────────────────────────────────────────────────── -static const MAGIC: PackedByteArray = PackedByteArray([0x41, 0x47, 0x50, 0x43]) # "AGPC" -static const HEADER_SIZE: int = 48 -static const SECTION_META: int = 0x01 -static const SECTION_TILE: int = 0x02 -static const SECTION_OBJECT: int = 0x03 -static const SECTION_FLAGS: int = 0x04 -static const SECTION_REVISION_META: int = 0x05 +const MAGIC: PackedByteArray = PackedByteArray([0x41, 0x47, 0x50, 0x43]) # "AGPC" +const HEADER_SIZE: int = 48 +const SECTION_META: int = 0x01 +const SECTION_TILE: int = 0x02 +const SECTION_OBJECT: int = 0x03 +const SECTION_FLAGS: int = 0x04 +const SECTION_REVISION_META: int = 0x05 ## Decode a binary chunk payload into a ChunkPayload. From 929c69e5da8f39423eb2374cd8fbb02d684382f1 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 02:40:34 +0000 Subject: [PATCH 17/20] WIP checkpoint: Address review feedback: Implement chunk streaming system in GDScript --- scripts/chunk_data_decoder.gd | 43 +++++++++-------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/scripts/chunk_data_decoder.gd b/scripts/chunk_data_decoder.gd index bfb4fed..9e19863 100644 --- a/scripts/chunk_data_decoder.gd +++ b/scripts/chunk_data_decoder.gd @@ -107,27 +107,6 @@ class TileLayerData: return null -## A single tile definition (palette entry). -class TileDef: - var id: int = 0 - var asset_id: String = "" - var solid: bool = false - var flags: int = 0 - var variants: int = 1 - - func is_solid() -> bool: - return solid - - func is_walkable() -> bool: - return (flags & 0x01) != 0 - - func is_climbable() -> bool: - return (flags & 0x02) != 0 - - func is_liquid() -> bool: - return (flags & 0x04) != 0 - - ## A world object parsed from chunk data. class WorldObjectData: var id: String = "" @@ -159,9 +138,9 @@ class ChunkPayload: var checksum: String = "" -# ─── Binary Decoding ──────────────────────────────────────────────────────── +# ───────────────── Binary Decoding ───────────────── -const MAGIC: PackedByteArray = PackedByteArray([0x41, 0x47, 0x50, 0x43]) # "AGPC" +static var MAGIC := PackedByteArray([0x41, 0x47, 0x50, 0x43]) # "AGPC" const HEADER_SIZE: int = 48 const SECTION_META: int = 0x01 const SECTION_TILE: int = 0x02 @@ -274,7 +253,7 @@ static func encode_binary(payload: ChunkPayload) -> PackedByteArray: return header + payload_bytes -# ─── Section Parsers ──────────────────────────────────────────────────────── +# ───────────────── Section Parsers ───────────────── static func _parse_metadata_section(data: PackedByteArray, result: ChunkPayload) -> void: var offset := 0 @@ -496,7 +475,7 @@ static func _parse_revision_meta_section(data: PackedByteArray, result: ChunkPay result.author = data.slice(offset, offset + author_len).get_string_from_utf8() -# ─── Section Builders ─────────────────────────────────────────────────────── +# ───────────────── Section Builders ───────────────── static func _build_metadata_section(payload: ChunkPayload) -> PackedByteArray: var data := PackedByteArray() @@ -607,7 +586,7 @@ static func _wrap_section(data: PackedByteArray, section_id: int) -> PackedByteA return wrapped -# ─── Binary Helpers ───────────────────────────────────────────────────────── +# ───────────────── Binary Helpers ───────────────── static func _read_uint16(data: PackedByteArray, offset: int) -> int: return data[offset] | (data[offset + 1] << 8) @@ -668,18 +647,18 @@ static func _write_string(s: String) -> PackedByteArray: static func _hex_to_bytes(hex_str: String) -> PackedByteArray: var result := PackedByteArray() for i in range(0, hex_str.length(), 2): - var byte_val := int("0x" + hex_str.substr(i, 2), 16) + var byte_val := ("0x" + hex_str.substr(i, 2)).hex_to_int() result.append(byte_val) return result -# ─── SHA-256 ──────────────────────────────────────────────────────────────── +# ───────────────── SHA-256 ───────────────── static func _sha256_hex(data: PackedByteArray) -> String: - var sha256 := SHA256Hash.new() - sha256.start() - sha256.update(data) - var hash := sha256.finish() + var ctx := HashingContext.new() + ctx.start(HashingContext.HASH_SHA256) + ctx.update(data) + var hash := ctx.finish() var hex_chars := "0123456789abcdef" var result := "" for byte_val in hash: From 25f2dffc53df3bb8da37b02e0e36eddde4cbc315 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 02:45:44 +0000 Subject: [PATCH 18/20] WIP checkpoint: Address review feedback: Implement chunk streaming system in GDScript --- scripts/chunk_scene.gd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/chunk_scene.gd b/scripts/chunk_scene.gd index f19cf9b..ce231a4 100644 --- a/scripts/chunk_scene.gd +++ b/scripts/chunk_scene.gd @@ -11,9 +11,9 @@ extends Node3D ## ## ``` ## ChunkScene (Node3D) -## ├── TileLayer (Node3D) # one per tile layer (ground, walls, decor, ...) -## ├── StaticObject (Node3D) # props, geometry, interactables -## └── DynamicObject (Node3D) # spawnable entities (templates) +## ├─ TileLayer (Node3D) # one per tile layer (ground, walls, decor, ...) +## ├─ StaticObject (Node3D) # props, geometry, interactables +## └─ DynamicObject (Node3D) # spawnable entities (templates) ## ``` ## ## ## Lifecycle @@ -92,7 +92,7 @@ func _add_static_object(obj: WorldObject) -> void: var static_obj := StaticObjectNode.new() static_obj.world_object = obj static_obj.position = obj.position - static_obj.rotation = obj.rotation + static_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w) static_obj.scale = obj.scale static_obj.name = "static_%s" % obj.id add_child(static_obj) @@ -102,7 +102,7 @@ func _add_dynamic_object(obj: WorldObject) -> void: var dynamic_obj := DynamicObjectNode.new() dynamic_obj.world_object = obj dynamic_obj.position = obj.position - dynamic_obj.rotation = obj.rotation + dynamic_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w) dynamic_obj.scale = obj.scale dynamic_obj.name = "dynamic_%s" % obj.id add_child(dynamic_obj) From 93d83a28fc0848dbe5cc080c5e2bf8c22253bdde Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 13:11:54 +0000 Subject: [PATCH 19/20] WIP checkpoint: Address review feedback: Implement chunk streaming system in GDScript --- scripts/chunk_scene.gd | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/chunk_scene.gd b/scripts/chunk_scene.gd index ce231a4..5ac66eb 100644 --- a/scripts/chunk_scene.gd +++ b/scripts/chunk_scene.gd @@ -11,9 +11,9 @@ extends Node3D ## ## ``` ## ChunkScene (Node3D) -## ├─ TileLayer (Node3D) # one per tile layer (ground, walls, decor, ...) -## ├─ StaticObject (Node3D) # props, geometry, interactables -## └─ DynamicObject (Node3D) # spawnable entities (templates) +## ├── TileLayer (Node3D) # one per tile layer (ground, walls, decor, ...) +## ├── StaticObject (Node3D) # props, geometry, interactables +## └── DynamicObject (Node3D) # spawnable entities (templates) ## ``` ## ## ## Lifecycle @@ -164,7 +164,7 @@ func contains_position(pos: Vector3) -> bool: ## Check if a chunk coordinate is adjacent to this chunk. func is_adjacent(other_coord: Vector3i) -> bool: - var dx := abs(coord.x - other_coord.x) - var dy := abs(coord.y - other_coord.y) - var dz := abs(coord.z - other_coord.z) + var dx: int = abs(coord.x - other_coord.x) + var dy: int = abs(coord.y - other_coord.y) + var dz: int = abs(coord.z - other_coord.z) return dx <= 1 and dy <= 1 and dz <= 1 and (dx + dy + dz) > 0 From 88cd255040b2607729bb60a86d3fd74d2b9e3221 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 13:12:51 +0000 Subject: [PATCH 20/20] WIP checkpoint: Address review feedback: Implement chunk streaming system in GDScript --- .gitignore | 10 +++ .godot/.gdignore | 1 - .godot/editor/filesystem_cache10 | 22 ----- ...lding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg | 5 -- .godot/editor/project_metadata.cfg | 11 --- .godot/global_script_class_cache.cfg | 81 ------------------ .godot/uid_cache.bin | Bin 430 -> 0 bytes 7 files changed, 10 insertions(+), 120 deletions(-) create mode 100644 .gitignore delete mode 100644 .godot/.gdignore delete mode 100644 .godot/editor/filesystem_cache10 delete mode 100644 .godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg delete mode 100644 .godot/editor/project_metadata.cfg delete mode 100644 .godot/global_script_class_cache.cfg delete mode 100644 .godot/uid_cache.bin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cb0080 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Godot editor cache / generated files +.godot/ + +# Godot export artifacts +export/ +*.tmp + +# OS +.DS_Store +Thumbs.db diff --git a/.godot/.gdignore b/.godot/.gdignore deleted file mode 100644 index 8b13789..0000000 --- a/.godot/.gdignore +++ /dev/null @@ -1 +0,0 @@ - diff --git a/.godot/editor/filesystem_cache10 b/.godot/editor/filesystem_cache10 deleted file mode 100644 index f3364e6..0000000 --- a/.godot/editor/filesystem_cache10 +++ /dev/null @@ -1,22 +0,0 @@ -63f7b34db8d8cdea90c76aacccf841ec -::res://::1787012074 -README.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: -::res://docs/::1787011777 -::res://docs/architecture/::1787011795 -avatar.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: -content_moderation.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: -networking.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: -world.md::TextFile::-1::1787011777::0::1::::<><><>0<>0<><>:: -::res://scenes/::1787011795 -main.tscn::PackedScene::-1::1787011795::0::1::::<><><>0<>0<><>::res://scripts/chunk_stream_manager.gd -::res://scripts/::1787012062 -chunk.gd::GDScript::7364239597441807588::1787011795::0::1::::Chunk<>Node3D<><>0<>0<><>:: -chunk_data_decoder.gd::GDScript::3922068956043581888::1787012104::0::1::::ChunkDataDecoder<>RefCounted<><>0<>0<><>:: -chunk_scene.gd::GDScript::9207602004456600331::1787011795::0::1::::ChunkScene<>Node3D<><>0<>0<><>:: -chunk_stream_manager.gd::GDScript::2366645676568508746::1787011963::0::1::::ChunkStreamManager<>Node<><>0<>0<><>:: -dynamic_object_node.gd::GDScript::8863349716576659118::1787011795::0::1::::DynamicObjectNode<>Node3D<><>0<>0<><>:: -static_object_node.gd::GDScript::5276389741929225897::1787011795::0::1::::StaticObjectNode<>Node3D<><>0<>0<><>:: -tile_def.gd::GDScript::2430528079284085109::1787011795::0::1::::TileDef<>RefCounted<><>0<>0<><>:: -tile_layer.gd::GDScript::4797906255131447844::1787011795::0::1::::TileLayer<>RefCounted<><>0<>0<><>:: -tile_layer_node.gd::GDScript::9033225196818247616::1787011795::0::1::::TileLayerNode<>Node3D<><>0<>0<><>:: -world_object.gd::GDScript::6429674491464200335::1787011795::0::1::::WorldObject<>RefCounted<><>0<>0<><>:: diff --git a/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg b/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg deleted file mode 100644 index 0f30ddf..0000000 --- a/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[folding] - -node_unfolds=[NodePath("."), PackedStringArray("Visibility")] -resource_unfolds=[] -nodes_folded=[] diff --git a/.godot/editor/project_metadata.cfg b/.godot/editor/project_metadata.cfg deleted file mode 100644 index e515609..0000000 --- a/.godot/editor/project_metadata.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[game_view] - -select_mode=0 - -[editor_metadata] - -executable_path="/home/daddy/godot/godot" - -[recent_files] - -scenes=["res://scenes/main.tscn"] diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg deleted file mode 100644 index e1f5257..0000000 --- a/.godot/global_script_class_cache.cfg +++ /dev/null @@ -1,81 +0,0 @@ -list=[{ -"base": &"Node3D", -"class": &"Chunk", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/chunk.gd" -}, { -"base": &"RefCounted", -"class": &"ChunkDataDecoder", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/chunk_data_decoder.gd" -}, { -"base": &"Node3D", -"class": &"ChunkScene", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/chunk_scene.gd" -}, { -"base": &"Node", -"class": &"ChunkStreamManager", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/chunk_stream_manager.gd" -}, { -"base": &"Node3D", -"class": &"DynamicObjectNode", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/dynamic_object_node.gd" -}, { -"base": &"Node3D", -"class": &"StaticObjectNode", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/static_object_node.gd" -}, { -"base": &"RefCounted", -"class": &"TileDef", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/tile_def.gd" -}, { -"base": &"RefCounted", -"class": &"TileLayer", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/tile_layer.gd" -}, { -"base": &"Node3D", -"class": &"TileLayerNode", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/tile_layer_node.gd" -}, { -"base": &"RefCounted", -"class": &"WorldObject", -"icon": "", -"is_abstract": false, -"is_tool": false, -"language": &"GDScript", -"path": "res://scripts/world_object.gd" -}] diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin deleted file mode 100644 index 20bf5bff07b8ce51cfd5bfa1140ae5ee626e3c5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 430 zcmd;LU|@Jsl2rGU!#GV0$Sz7Pw$j%xPAGW6wqz}bnu(h