From d2b62c5d2543cf52d1130d0f3d4672a760a0594e Mon Sep 17 00:00:00 2001 From: daddy Date: Wed, 16 Sep 2026 21:31:33 +0000 Subject: [PATCH] feat: stream chunks in-game, render 3D catalog props, and ground chunk coordinates --- scripts/chunk_scene.gd | 31 ++-- scripts/game.gd | 105 +++++++++++-- scripts/server.gd | 35 ++++- scripts/static_object_node.gd | 284 ++++++++++++++++++++++++++++++++-- scripts/tile_layer_node.gd | 13 +- 5 files changed, 412 insertions(+), 56 deletions(-) diff --git a/scripts/chunk_scene.gd b/scripts/chunk_scene.gd index 5ac66eb..2a55dc1 100644 --- a/scripts/chunk_scene.gd +++ b/scripts/chunk_scene.gd @@ -54,20 +54,16 @@ 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() + # Position this chunk scene at its 3D world ground coordinates + global_position = Vector3( + float(coord.x * Chunk.CHUNK_SIZE_X), + float(coord.z * Chunk.CHUNK_SIZE_Z), + float(coord.y * Chunk.CHUNK_SIZE_Y) + ) 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) + pass ## Add a tile layer to this chunk scene. @@ -91,9 +87,10 @@ func add_object(obj: WorldObject) -> void: func _add_static_object(obj: WorldObject) -> void: var static_obj := StaticObjectNode.new() static_obj.world_object = obj - static_obj.position = obj.position - static_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w) - static_obj.scale = obj.scale + # Map chunk coordinates (X_ground, Y_ground, Z_height) to Godot 3D (X, Y_height, Z_ground) + static_obj.position = Vector3(obj.position.x, obj.position.z, obj.position.y) + static_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.z, obj.rotation.y, obj.rotation.w) + static_obj.scale = Vector3(obj.scale.x, obj.scale.z, obj.scale.y) static_obj.name = "static_%s" % obj.id add_child(static_obj) @@ -101,9 +98,9 @@ func _add_static_object(obj: WorldObject) -> void: func _add_dynamic_object(obj: WorldObject) -> void: var dynamic_obj := DynamicObjectNode.new() dynamic_obj.world_object = obj - dynamic_obj.position = obj.position - dynamic_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w) - dynamic_obj.scale = obj.scale + dynamic_obj.position = Vector3(obj.position.x, obj.position.z, obj.position.y) + dynamic_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.z, obj.rotation.y, obj.rotation.w) + dynamic_obj.scale = Vector3(obj.scale.x, obj.scale.z, obj.scale.y) dynamic_obj.name = "dynamic_%s" % obj.id add_child(dynamic_obj) diff --git a/scripts/game.gd b/scripts/game.gd index e52e102..3da1662 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -37,12 +37,65 @@ var _status_label: Label = null var _build_editor: InGameEditor = null +var _stream_manager: ChunkStreamManager = null + + func _ready() -> void: _show_build_stamp() if Session.editing: _open_editor_for_world() - elif Session.active: - _setup_networked() + else: + if Session.active: + _setup_networked() + _setup_chunk_streaming() + + +func _setup_chunk_streaming() -> void: + if Session.world_id == null or int(Session.world_id) <= 0: + return + if _stream_manager != null and is_instance_valid(_stream_manager): + return + + _stream_manager = ChunkStreamManager.new() + _stream_manager.name = "ChunkStreamManager" + _stream_manager.player = _player + _stream_manager.streaming_radius = 2 + add_child(_stream_manager) + + var chunk_source := HttpChunkSource.new() + chunk_source.name = "HttpChunkSource" + chunk_source.world_id = str(Session.world_id) + chunk_source.room_slug = "main" + chunk_source.stream_manager = _stream_manager + _stream_manager.add_child(chunk_source) + _stream_manager.set_server_rpc(chunk_source) + _stream_manager.is_active = true + + _stream_manager.chunk_loaded.connect(_on_chunk_loaded) + _stream_manager._load_chunk(Vector3i.ZERO) + + +func _on_chunk_loaded(_coord: Vector3i, chunk_scene: Node) -> void: + if chunk_scene is ChunkScene: + var cs := chunk_scene as ChunkScene + if not cs.world_objects.is_empty() or not cs.tile_layers.is_empty(): + _hide_default_test_props() + + +func _hide_default_test_props() -> void: + var props := get_node_or_null("Level/Props") + if props != null and props is Node3D: + (props as Node3D).visible = false + for child in props.get_children(): + if child is CollisionObject3D: + (child as CollisionObject3D).set_collision_layer_value(1, false) + (child as CollisionObject3D).set_collision_mask_value(1, false) + var chest := get_node_or_null("Level/ScriptedChest") + if chest != null and chest is Node3D: + (chest as Node3D).visible = false + if chest is CollisionObject3D: + (chest as CollisionObject3D).set_collision_layer_value(1, false) + (chest as CollisionObject3D).set_collision_mask_value(1, false) func _unhandled_input(event: InputEvent) -> void: @@ -89,7 +142,29 @@ func _open_build_mode() -> void: var room_slug := "main" var coord := Vector3i.ZERO - var model := ChunkEditorModel.create_empty(32, 32, world_id_str, room_slug, coord) + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _player.set_physics_process(false) + _player.set_process_unhandled_input(false) + + var save_service: ChunkSaveService = null + var model: ChunkEditorModel = null + var base_revision := 0 + + if int(world_id_str) > 0: + save_service = ChunkSaveService.new() + save_service.world_id = world_id_str + save_service.room_slug = room_slug + add_child(save_service) + + var fetch_result: Dictionary = await save_service.fetch_chunk(coord) + if fetch_result.get("ok", false) and fetch_result.get("model") != null: + model = fetch_result["model"] + base_revision = int(fetch_result.get("revision", 0)) + + if model == null: + model = ChunkEditorModel.create_empty(32, 32, world_id_str, room_slug, coord) + base_revision = 0 + var catalog := EditorObjectCatalog.new() var err := catalog.load_catalog() if err != OK: @@ -97,20 +172,12 @@ func _open_build_mode() -> void: var core := EditorCore.new(model, catalog) _build_editor = InGameEditor.new(core) - if int(world_id_str) > 0: - var save_service := ChunkSaveService.new() - save_service.world_id = world_id_str - save_service.room_slug = room_slug - add_child(save_service) - _build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, 0) + if save_service != null: + _build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, base_revision) _build_editor.closed.connect(_close_build_mode) add_child(_build_editor) - Input.mouse_mode = Input.MOUSE_MODE_VISIBLE - _player.set_physics_process(false) - _player.set_process_unhandled_input(false) - ## Shared teardown for both close paths (F2 again, or the editor's own ## Close button/`closed` signal) -- safe to call twice: `queue_free()` on an @@ -127,6 +194,14 @@ func _close_build_mode() -> void: _player.set_process_unhandled_input(true) _player._refresh_mouse_mode() + if _stream_manager == null: + _setup_chunk_streaming() + elif is_instance_valid(_stream_manager): + var source := _stream_manager.get_node_or_null("HttpChunkSource") as HttpChunkSource + if source != null and Session.world_id != null and int(Session.world_id) > 0: + source.world_id = str(Session.world_id) + _stream_manager.force_reload_chunk(Vector3i.ZERO) + ## "Edit world" entry point from the world browser ## (world_persistence_impl.md §6 D4 / §9 "Client/editor task"). Reached only @@ -198,6 +273,10 @@ func _leave_world() -> void: ## Close the ENet connection and stop it from calling us back mid-teardown. func _teardown_net() -> void: + if _stream_manager != null and is_instance_valid(_stream_manager): + _stream_manager.clear_all_chunks() + _stream_manager.queue_free() + _stream_manager = null if _net == null or not is_instance_valid(_net): return if _net.server_disconnected.is_connected(_on_server_lost): diff --git a/scripts/server.gd b/scripts/server.gd index b4dca56..059573d 100644 --- a/scripts/server.gd +++ b/scripts/server.gd @@ -263,9 +263,9 @@ func _build_chunk_collision(parent: Node3D, coord: Vector3i, data: ChunkDataDeco var body := StaticBody3D.new() body.name = "chunk_%d_%d_%d" % [coord.x, coord.y, coord.z] var chunk_origin := Vector3( - coord.x * Chunk.CHUNK_SIZE_X, - coord.y * Chunk.CHUNK_SIZE_Y, - coord.z * Chunk.CHUNK_SIZE_Z + float(coord.x * Chunk.CHUNK_SIZE_X), + float(coord.z * Chunk.CHUNK_SIZE_Z), + float(coord.y * Chunk.CHUNK_SIZE_Y) ) for layer in data.tile_layers: @@ -282,20 +282,39 @@ func _build_chunk_collision(parent: Node3D, coord: Vector3i, data: ChunkDataDeco var box := BoxShape3D.new() box.size = Vector3.ONE shape.shape = box - shape.position = chunk_origin + Vector3(x + 0.5, y + 0.5, z + 0.5) + shape.position = chunk_origin + Vector3(x + 0.5, z + 0.5, y + 0.5) body.add_child(shape) _collision_shapes_built += 1 for obj in data.objects: if obj.dynamic: continue # dynamic templates are spawnable entities, not static geometry - if bool(obj.properties.get("walkable", true)): + if obj.type == "spawn_point" or bool(obj.properties.get("walkable", false)): continue var shape := CollisionShape3D.new() var box := BoxShape3D.new() - box.size = obj.scale - shape.shape = box - shape.position = obj.position + var aid := str(obj.asset_id).to_lower() + var cat_id := str(obj.properties.get("catalog_id", "")).to_lower() + if "lamp" in aid or "lamp" in cat_id: + box.size = Vector3(0.5, 3.2, 0.5) + shape.shape = box + shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 1.6, obj.position.y) + elif "tree" in aid or "tree" in cat_id: + box.size = Vector3(0.8, 4.0, 0.8) + shape.shape = box + shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 2.0, obj.position.y) + elif "chest" in aid or "chest" in cat_id: + box.size = Vector3(1.0, 0.8, 0.6) + shape.shape = box + shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 0.4, obj.position.y) + elif "rock" in aid or "rock" in cat_id: + box.size = Vector3(1.2, 1.0, 1.2) + shape.shape = box + shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 0.5, obj.position.y) + else: + box.size = Vector3(obj.scale.x, obj.scale.z, obj.scale.y) + shape.shape = box + shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + obj.scale.y * 0.5, obj.position.y) body.add_child(shape) _collision_shapes_built += 1 diff --git a/scripts/static_object_node.gd b/scripts/static_object_node.gd index 5a4bea8..61fd835 100644 --- a/scripts/static_object_node.gd +++ b/scripts/static_object_node.gd @@ -29,16 +29,282 @@ func _ready() -> void: ## Load and attach the object's mesh. func _load_mesh() -> void: - _mesh_instance = MeshInstance3D.new() - _mesh_instance.name = "mesh" + # 1. Try to load the asset directly if it exists on disk + var asset_node := _try_load_asset(world_object.asset_id) + if asset_node != null: + add_child(asset_node) + if asset_node is MeshInstance3D: + _mesh_instance = asset_node as MeshInstance3D + return - # 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() + # 2. Build a stylized, recognizable 3D representation based on catalog ID / asset ID + var cat_id := _get_catalog_id() + _build_procedural_object(cat_id) - _mesh_instance.mesh = mesh - add_child(_mesh_instance) + +func _get_catalog_id() -> String: + if world_object == null: + return "" + if world_object.properties is Dictionary and world_object.properties.has("catalog_id"): + return str(world_object.properties["catalog_id"]).to_lower() + return world_object.asset_id.get_file().get_basename().to_lower() + + +func _try_load_asset(asset_id: String) -> Node: + 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 PackedScene: + return (res as PackedScene).instantiate() + elif res is Mesh: + var mi := MeshInstance3D.new() + mi.name = "mesh" + mi.mesh = res as Mesh + return mi + return null + + +func _build_procedural_object(cat_id: String) -> void: + var container := Node3D.new() + container.name = "visuals" + add_child(container) + + if "lamp" in cat_id: + # Lamp post: dark iron vertical pole + glowing lantern + omni light + var pole := MeshInstance3D.new() + pole.name = "pole" + var pole_mesh := CylinderMesh.new() + pole_mesh.height = 3.2 + pole_mesh.top_radius = 0.07 + pole_mesh.bottom_radius = 0.1 + var iron_mat := StandardMaterial3D.new() + iron_mat.albedo_color = Color(0.2, 0.22, 0.25) + iron_mat.metallic = 0.85 + iron_mat.roughness = 0.35 + pole_mesh.material = iron_mat + pole.mesh = pole_mesh + pole.position = Vector3(0, 1.6, 0) + container.add_child(pole) + _mesh_instance = pole + + var cap := MeshInstance3D.new() + cap.name = "cap" + var cap_mesh := CylinderMesh.new() + cap_mesh.height = 0.15 + cap_mesh.top_radius = 0.05 + cap_mesh.bottom_radius = 0.3 + cap_mesh.material = iron_mat + cap.mesh = cap_mesh + cap.position = Vector3(0, 3.2, 0) + container.add_child(cap) + + var lantern := MeshInstance3D.new() + lantern.name = "lantern" + var lantern_mesh := BoxMesh.new() + lantern_mesh.size = Vector3(0.32, 0.45, 0.32) + var glow_mat := StandardMaterial3D.new() + glow_mat.albedo_color = Color(1.0, 0.95, 0.7) + glow_mat.emission_enabled = true + glow_mat.emission = Color(1.0, 0.9, 0.4) + glow_mat.emission_energy_multiplier = 4.0 + lantern_mesh.material = glow_mat + lantern.mesh = lantern_mesh + lantern.position = Vector3(0, 2.9, 0) + container.add_child(lantern) + + var light := OmniLight3D.new() + light.name = "light" + light.light_color = Color(1.0, 0.93, 0.72) + light.light_energy = 1.6 + light.omni_range = 10.0 + light.position = Vector3(0, 2.9, 0) + container.add_child(light) + + var col := _create_static_collision() + var col_shape := CollisionShape3D.new() + var cyl := CylinderShape3D.new() + cyl.height = 3.2 + cyl.radius = 0.2 + col_shape.shape = cyl + col_shape.position = Vector3(0, 1.6, 0) + col.add_child(col_shape) + container.add_child(col) + + elif "crate" in cat_id: + var crate := MeshInstance3D.new() + crate.name = "crate" + var crate_mesh := BoxMesh.new() + crate_mesh.size = Vector3(1.0, 1.0, 1.0) + var wood_mat := StandardMaterial3D.new() + wood_mat.albedo_color = Color(0.55, 0.36, 0.18) + wood_mat.roughness = 0.8 + crate_mesh.material = wood_mat + crate.mesh = crate_mesh + crate.position = Vector3(0, 0.5, 0) + container.add_child(crate) + _mesh_instance = crate + + var col := _create_static_collision() + var col_shape := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = Vector3(1.0, 1.0, 1.0) + col_shape.shape = box + col_shape.position = Vector3(0, 0.5, 0) + col.add_child(col_shape) + container.add_child(col) + + elif "rock" in cat_id: + var rock := MeshInstance3D.new() + rock.name = "rock" + var rock_mesh := SphereMesh.new() + rock_mesh.radius = 0.65 + rock_mesh.height = 1.0 + rock_mesh.radial_segments = 8 + rock_mesh.rings = 6 + var stone_mat := StandardMaterial3D.new() + stone_mat.albedo_color = Color(0.42, 0.44, 0.46) + stone_mat.roughness = 0.9 + rock_mesh.material = stone_mat + rock.mesh = rock_mesh + rock.position = Vector3(0, 0.5, 0) + container.add_child(rock) + _mesh_instance = rock + + var col := _create_static_collision() + var col_shape := CollisionShape3D.new() + var sphere := SphereShape3D.new() + sphere.radius = 0.65 + col_shape.shape = sphere + col_shape.position = Vector3(0, 0.5, 0) + col.add_child(col_shape) + container.add_child(col) + + elif "tree" in cat_id: + var trunk := MeshInstance3D.new() + trunk.name = "trunk" + var trunk_mesh := CylinderMesh.new() + trunk_mesh.height = 1.4 + trunk_mesh.top_radius = 0.2 + trunk_mesh.bottom_radius = 0.25 + var bark_mat := StandardMaterial3D.new() + bark_mat.albedo_color = Color(0.35, 0.22, 0.12) + trunk_mesh.material = bark_mat + trunk.mesh = trunk_mesh + trunk.position = Vector3(0, 0.7, 0) + container.add_child(trunk) + + var leaves1 := MeshInstance3D.new() + leaves1.name = "leaves1" + var leaves1_mesh := CylinderMesh.new() + leaves1_mesh.height = 2.0 + leaves1_mesh.top_radius = 0.1 + leaves1_mesh.bottom_radius = 1.4 + var leaves_mat := StandardMaterial3D.new() + leaves_mat.albedo_color = Color(0.12, 0.38, 0.15) + leaves1_mesh.material = leaves_mat + leaves1.mesh = leaves1_mesh + leaves1.position = Vector3(0, 2.0, 0) + container.add_child(leaves1) + _mesh_instance = leaves1 + + var leaves2 := MeshInstance3D.new() + leaves2.name = "leaves2" + var leaves2_mesh := CylinderMesh.new() + leaves2_mesh.height = 1.6 + leaves2_mesh.top_radius = 0.0 + leaves2_mesh.bottom_radius = 0.9 + leaves2_mesh.material = leaves_mat + leaves2.mesh = leaves2_mesh + leaves2.position = Vector3(0, 3.2, 0) + container.add_child(leaves2) + + var col := _create_static_collision() + var col_shape := CollisionShape3D.new() + var cyl := CylinderShape3D.new() + cyl.height = 4.0 + cyl.radius = 0.35 + col_shape.shape = cyl + col_shape.position = Vector3(0, 2.0, 0) + col.add_child(col_shape) + container.add_child(col) + + elif "chest" in cat_id: + var chest := MeshInstance3D.new() + chest.name = "chest" + var chest_mesh := BoxMesh.new() + chest_mesh.size = Vector3(1.0, 0.8, 0.6) + var chest_mat := StandardMaterial3D.new() + chest_mat.albedo_color = Color(0.48, 0.26, 0.12) + chest_mesh.material = chest_mat + chest.mesh = chest_mesh + chest.position = Vector3(0, 0.4, 0) + container.add_child(chest) + _mesh_instance = chest + + var trim := MeshInstance3D.new() + trim.name = "trim" + var trim_mesh := BoxMesh.new() + trim_mesh.size = Vector3(0.2, 0.25, 0.64) + var gold_mat := StandardMaterial3D.new() + gold_mat.albedo_color = Color(0.9, 0.75, 0.2) + trim_mesh.material = gold_mat + trim.mesh = trim_mesh + trim.position = Vector3(0, 0.4, 0) + container.add_child(trim) + + var col := _create_static_collision() + var col_shape := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = Vector3(1.0, 0.8, 0.6) + col_shape.shape = box + col_shape.position = Vector3(0, 0.4, 0) + col.add_child(col_shape) + container.add_child(col) + + elif "spawn" in cat_id or (world_object != null and world_object.type == "spawn_point"): + var pad := MeshInstance3D.new() + pad.name = "pad" + var pad_mesh := CylinderMesh.new() + pad_mesh.height = 0.08 + pad_mesh.top_radius = 1.2 + pad_mesh.bottom_radius = 1.2 + var spawn_mat := StandardMaterial3D.new() + spawn_mat.albedo_color = Color(0.2, 0.75, 0.95) + spawn_mat.emission_enabled = true + spawn_mat.emission = Color(0.1, 0.6, 0.85) + spawn_mat.emission_energy_multiplier = 2.0 + pad_mesh.material = spawn_mat + pad.mesh = pad_mesh + pad.position = Vector3(0, 0.04, 0) + container.add_child(pad) + _mesh_instance = pad + + else: + var mi := MeshInstance3D.new() + mi.name = "mesh" + mi.mesh = _make_placeholder_mesh() + mi.position = Vector3(0, 0.5, 0) + container.add_child(mi) + _mesh_instance = mi + + var col := _create_static_collision() + var col_shape := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = Vector3(1.0, 1.0, 1.0) + col_shape.shape = box + col_shape.position = Vector3(0, 0.5, 0) + col.add_child(col_shape) + container.add_child(col) + + +func _create_static_collision() -> StaticBody3D: + var sb := StaticBody3D.new() + sb.name = "collision" + sb.set_collision_layer_value(1, true) + sb.set_collision_mask_value(1, true) + return sb ## Attempt to load a mesh from the asset path. @@ -49,7 +315,7 @@ func _load_asset_mesh(asset_id: String) -> Mesh: if ResourceLoader.exists(path): var res := load(path) if res is Mesh: - return res + return res as Mesh return null diff --git a/scripts/tile_layer_node.gd b/scripts/tile_layer_node.gd index 0162de8..5c9d2c5 100644 --- a/scripts/tile_layer_node.gd +++ b/scripts/tile_layer_node.gd @@ -74,17 +74,12 @@ func _create_mesh_for_tile(tile_def: TileDef) -> void: _mesh_instances[tile_def.id] = mesh_instance -## Convert a tile grid coordinate to a world-space position (center of tile). +## Convert a tile grid coordinate to a chunk-local 3D 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( + return Vector3( x * TILE_SIZE + TILE_SIZE * 0.5, - y * TILE_SIZE + TILE_SIZE * 0.5, - z * TILE_SIZE + TILE_SIZE * 0.5 + z * TILE_SIZE + TILE_SIZE * 0.5, + y * TILE_SIZE + TILE_SIZE * 0.5 )