class_name HomeWorldService extends Node3D const NetworkHomeProtocolType = preload( "res://network/network_home_protocol.gd" ) const PlayerHomeStateType = preload("res://homes/player_home_state.gd") const DecorCatalogType = preload("res://homes/decor_catalog.gd") const STARTER_MOTORCOACH_SCENE: PackedScene = preload( "res://homes/assets/starter_motorcoach.glb" ) const EXTERIOR_BODY_SIZE := Vector3(5.6, 2.5, 2.35) const EXTERIOR_DOOR_LOCAL := Vector3(-0.2, 0.85, 1.48) const EXTERIOR_SPAWN_LOCAL := Vector3(-0.2, 0.25, 2.45) const EXTERIOR_INTERACTION_DISTANCE: float = 2.7 const INTERIOR_EXIT_DISTANCE: float = 2.1 const INTERIOR_ORIGIN := Vector3(1000.0, 300.0, 1000.0) const INTERIOR_SPACING: float = 14.0 const INTERIOR_MAX_SPAWN_SLOTS: int = 64 const INTERIOR_FLOOR_SIZE := Vector3(8.0, 0.18, 4.5) const INTERIOR_WALL_HEIGHT: float = 2.7 const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0 const MIN_EXTERIOR_SEPARATION: float = 9.0 const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(3.85, 2.55) const EXTERIOR_SURFACE_INDEX_CELL_SIZE: float = 5.0 const EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: float = 0.24 const EXTERIOR_CLEARANCE_HEIGHT: float = 2.7 const WORLD_GEOMETRY_COLLISION_LAYER: int = 1 const DECOR_SELECTION_COLLISION_LAYER: int = 1 << 8 # Includes the player's 0.45 m capsule radius plus enough movement margin for # a full low-tick-rate frame before a newly solid placement can be reached. const PLAYER_DECOR_CLEARANCE_RADIUS: float = 0.8 const BASIC_CUBE_SIZE := Vector3(1.0, 0.8, 0.8) var _world: TestWorld var _spawn_service: PlayerSpawnService var _session: NetworkSession var _dedicated_runtime: bool = false var _exteriors_root: Node3D var _interiors_root: Node3D var _presentations_by_fingerprint: Dictionary[String, Dictionary] = {} var _local_space: StringName = NetworkHomeProtocolType.WORLD_SPACE var _decor_under_manipulation: Dictionary[String, bool] = {} var _pending_solid_decor: Dictionary[String, Dictionary] = {} func setup( world: TestWorld, spawn_service: PlayerSpawnService, session: NetworkSession, dedicated_runtime: bool, ) -> void: _world = world _spawn_service = spawn_service _session = session _dedicated_runtime = dedicated_runtime _ensure_roots() set_process(true) func is_world_ready() -> bool: return _world != null and _world.is_world_ready() func clear_presentations() -> void: _presentations_by_fingerprint.clear() _decor_under_manipulation.clear() _pending_solid_decor.clear() if is_instance_valid(_exteriors_root): for child: Node in _exteriors_root.get_children(): child.queue_free() if is_instance_valid(_interiors_root): for child: Node in _interiors_root.get_children(): child.queue_free() func remove_presentation(owner_fingerprint: String) -> void: var presentation: Dictionary = _presentations_by_fingerprint.get( owner_fingerprint, {} ) if presentation.is_empty(): return _clear_decor_collision_tracking(owner_fingerprint) _free_presentation_nodes(presentation) _presentations_by_fingerprint.erase(owner_fingerprint) func apply_manifest(data: Dictionary) -> bool: if ( not is_world_ready() or not NetworkHomeProtocolType.validate_manifest_broadcast(data) ): return false _ensure_roots() var fingerprint: String = str(data["owner_fingerprint"]) var prior: Dictionary = _presentations_by_fingerprint.get(fingerprint, {}) var prior_instance_ids: Dictionary[String, bool] = {} if not prior.is_empty(): var prior_home: Dictionary = prior.get("home", {}) for value: Variant in prior_home.get("placements", []): if typeof(value) == TYPE_DICTIONARY: prior_instance_ids[str( (value as Dictionary).get("instance_id", "") )] = true if not prior.is_empty(): _clear_decor_collision_tracking(fingerprint) _free_presentation_nodes(prior) var exterior_transform := NetworkHomeProtocolType.array_to_transform( data["exterior_transform"] ) var interior_index: int = int(data["interior_index"]) var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data( data["home"] ) var exterior := _build_exterior(fingerprint, exterior_transform) var interior := _build_interior(fingerprint, interior_index, home_data) var presentation: Dictionary = { "owner_peer_id": int(data["owner_peer_id"]), "fingerprint": fingerprint, "home": home_data, "exterior_transform": exterior_transform, "interior_index": interior_index, "exterior": exterior, "interior": interior, } _presentations_by_fingerprint[fingerprint] = presentation _refresh_local_visibility() if not prior.is_empty(): for value: Variant in home_data.get("placements", []): if typeof(value) != TYPE_DICTIONARY: continue var instance_id: String = str( (value as Dictionary).get("instance_id", "") ) if not instance_id.is_empty() and not prior_instance_ids.has( instance_id ): call_deferred( "_spawn_decor_poof", fingerprint, instance_id ) return true func choose_exterior_transform( owner_peer_id: int, reserved_transforms: Array[Transform3D], ) -> Transform3D: var spawn_transform: Transform3D = ( _world.get_player_spawn_transform() if _world != null else Transform3D.IDENTITY ) var triangles: Array[PackedVector3Array] = ( _world.get_spawn_surface_triangles( [&"grass_lite", &"grass", &"sand"], -0.5 ) if _world != null else [] ) if not triangles.is_empty(): var surface_index: Dictionary = _build_surface_index(triangles) var strict_result: Transform3D = _find_exterior_transform( owner_peer_id, triangles, surface_index, spawn_transform, reserved_transforms, true, true, ) if strict_result != Transform3D.IDENTITY: return strict_result var relaxed_result: Transform3D = _find_exterior_transform( owner_peer_id, triangles, surface_index, spawn_transform, reserved_transforms, false, true, ) if relaxed_result != Transform3D.IDENTITY: return relaxed_result var dry_result: Transform3D = _find_exterior_transform( owner_peer_id, triangles, surface_index, spawn_transform, reserved_transforms, false, false, ) if dry_result != Transform3D.IDENTITY: push_warning( "RV exterior used a dry fallback with obstructed clearance." ) return dry_result push_error("No dry, level RV exterior footprint was available.") var fallback: Transform3D = spawn_transform var column: int = posmod(owner_peer_id - 1, 4) var row: int = maxi(floori(float(owner_peer_id - 1) / 4.0), 0) fallback.origin += Vector3( -12.0 + float(column) * 8.0, 0.0, 8.0 + float(row) * 7.0, ) return fallback func _find_exterior_transform( owner_peer_id: int, triangles: Array[PackedVector3Array], surface_index: Dictionary, spawn_transform: Transform3D, reserved_transforms: Array[Transform3D], require_spawn_clearance: bool, require_obstacle_clearance: bool, ) -> Transform3D: var triangle_count: int = triangles.size() var start: int = posmod(owner_peer_id * 37, triangle_count) var step: int = _coprime_step(triangle_count, 29) for offset: int in triangle_count: var triangle: PackedVector3Array = triangles[ (start + offset * step) % triangle_count ] if not _is_exterior_candidate_triangle(triangle): continue var position: Vector3 = ( triangle[0] + triangle[1] + triangle[2] ) / 3.0 if ( require_spawn_clearance and position.distance_to(spawn_transform.origin) < 6.0 ): continue if _too_close_to_reserved(position, reserved_transforms): continue var yaw: float = float(posmod(owner_peer_id + offset, 4)) * PI * 0.5 var grounded_position: Vector3 = _ground_exterior_footprint( position, yaw, triangles, surface_index ) if not grounded_position.is_finite(): continue if ( require_obstacle_clearance and not _exterior_volume_is_clear(grounded_position, yaw) ): continue return Transform3D(Basis(Vector3.UP, yaw), grounded_position) return Transform3D.IDENTITY func _is_exterior_candidate_triangle(triangle: PackedVector3Array) -> bool: if triangle.size() != 3: return false var normal: Vector3 = (triangle[1] - triangle[0]).cross( triangle[2] - triangle[0] ) var area: float = normal.length() * 0.5 return ( area >= MIN_EXTERIOR_TRIANGLE_AREA and absf(normal.normalized().y) >= 0.94 ) func _ground_exterior_footprint( position: Vector3, yaw: float, triangles: Array[PackedVector3Array], surface_index: Dictionary, ) -> Vector3: var heights: Array[float] = [] for local_sample: Vector2 in _exterior_footprint_samples(): var rotated_sample: Vector3 = Vector3( local_sample.x, 0.0, local_sample.y ).rotated(Vector3.UP, yaw) var sample := Vector2( position.x + rotated_sample.x, position.z + rotated_sample.z, ) var height: float = _surface_height_at( sample, position.y, triangles, surface_index ) if not is_finite(height): return Vector3.INF heights.append(height) var minimum_height: float = heights.min() var maximum_height: float = heights.max() if maximum_height - minimum_height > EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: return Vector3.INF return Vector3(position.x, maximum_height, position.z) func _exterior_footprint_samples() -> Array[Vector2]: var samples: Array[Vector2] = [] for x_factor: float in [-1.0, -0.5, 0.0, 0.5, 1.0]: for z_factor: float in [-1.0, 0.0, 1.0]: samples.append(Vector2( EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * x_factor, EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * z_factor, )) return samples func _build_surface_index( triangles: Array[PackedVector3Array], ) -> Dictionary: var result: Dictionary = {} for triangle_index: int in triangles.size(): var triangle: PackedVector3Array = triangles[triangle_index] if triangle.size() != 3: continue var minimum_x: float = minf( triangle[0].x, minf(triangle[1].x, triangle[2].x) ) var maximum_x: float = maxf( triangle[0].x, maxf(triangle[1].x, triangle[2].x) ) var minimum_z: float = minf( triangle[0].z, minf(triangle[1].z, triangle[2].z) ) var maximum_z: float = maxf( triangle[0].z, maxf(triangle[1].z, triangle[2].z) ) var minimum_cell := Vector2i( floori(minimum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE), floori(minimum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE), ) var maximum_cell := Vector2i( floori(maximum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE), floori(maximum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE), ) for cell_x: int in range(minimum_cell.x, maximum_cell.x + 1): for cell_y: int in range(minimum_cell.y, maximum_cell.y + 1): var cell := Vector2i(cell_x, cell_y) var bucket: Array = result.get(cell, []) bucket.append(triangle_index) result[cell] = bucket return result func _surface_height_at( point: Vector2, preferred_height: float, triangles: Array[PackedVector3Array], surface_index: Dictionary, ) -> float: var cell := Vector2i( floori(point.x / EXTERIOR_SURFACE_INDEX_CELL_SIZE), floori(point.y / EXTERIOR_SURFACE_INDEX_CELL_SIZE), ) var best_height: float = INF var best_delta: float = INF for value: Variant in surface_index.get(cell, []): var triangle: PackedVector3Array = triangles[int(value)] var height: float = _triangle_height_at(point, triangle) if not is_finite(height): continue var delta: float = absf(height - preferred_height) if delta < best_delta: best_delta = delta best_height = height return best_height func _triangle_height_at( point: Vector2, triangle: PackedVector3Array, ) -> float: var a := Vector2(triangle[0].x, triangle[0].z) var b := Vector2(triangle[1].x, triangle[1].z) var c := Vector2(triangle[2].x, triangle[2].z) var denominator: float = ( (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y) ) if absf(denominator) <= 0.000001: return INF var weight_a: float = ( (b.y - c.y) * (point.x - c.x) + (c.x - b.x) * (point.y - c.y) ) / denominator var weight_b: float = ( (c.y - a.y) * (point.x - c.x) + (a.x - c.x) * (point.y - c.y) ) / denominator var weight_c: float = 1.0 - weight_a - weight_b const EDGE_EPSILON: float = 0.001 if ( weight_a < -EDGE_EPSILON or weight_b < -EDGE_EPSILON or weight_c < -EDGE_EPSILON ): return INF return ( weight_a * triangle[0].y + weight_b * triangle[1].y + weight_c * triangle[2].y ) func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool: if not is_inside_tree() or get_world_3d() == null: return true var shape := BoxShape3D.new() shape.size = Vector3( EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * 2.0, EXTERIOR_CLEARANCE_HEIGHT, EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * 2.0, ) var query := PhysicsShapeQueryParameters3D.new() query.shape = shape query.transform = Transform3D( Basis(Vector3.UP, yaw), position + Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2), ) query.collision_mask = 1 query.collide_with_areas = false query.collide_with_bodies = true return get_world_3d().direct_space_state.intersect_shape(query, 1).is_empty() func _coprime_step(value: int, preferred_step: int) -> int: var step: int = mini(preferred_step, maxi(value - 1, 1)) while step > 1 and _greatest_common_divisor(step, value) != 1: step -= 1 return step func _greatest_common_divisor(first: int, second: int) -> int: var a: int = absi(first) var b: int = absi(second) while b != 0: var remainder: int = a % b a = b b = remainder return a func get_exterior_transform(owner_fingerprint: String) -> Transform3D: var presentation: Dictionary = _presentations_by_fingerprint.get( owner_fingerprint, {} ) return presentation.get("exterior_transform", Transform3D.IDENTITY) func get_exterior_prompt_position(owner_fingerprint: String) -> Vector3: return get_exterior_transform(owner_fingerprint) * EXTERIOR_DOOR_LOCAL func get_exterior_spawn_transform(owner_fingerprint: String) -> Transform3D: var exterior_transform: Transform3D = get_exterior_transform( owner_fingerprint ) return Transform3D( exterior_transform.basis, exterior_transform * EXTERIOR_SPAWN_LOCAL, ) func get_interior_spawn_transform( owner_fingerprint: String, visitor_offset: int = 0, ) -> Transform3D: var anchor: Vector3 = get_interior_anchor(owner_fingerprint) # `visitor_offset` is allocated by the host as a compact room-local slot. # Keep it bounded as a last line of defense if a future caller accidentally # supplies an engine peer ID instead of a slot. var visitor_slot: int = posmod(visitor_offset, INTERIOR_MAX_SPAWN_SLOTS) # Build the origin as a value rather than mutating Transform3D.origin. var spawn_origin: Vector3 = anchor + Vector3( -1.0 + float(posmod(visitor_slot, 4)) * 0.65, 0.25, 0.4 + float(floori(float(visitor_slot) / 4.0)) * 0.55, ) return Transform3D(Basis.IDENTITY, spawn_origin) func get_interior_anchor(owner_fingerprint: String) -> Vector3: var presentation: Dictionary = _presentations_by_fingerprint.get( owner_fingerprint, {} ) var index: int = int(presentation.get("interior_index", 0)) return INTERIOR_ORIGIN + Vector3(float(index) * INTERIOR_SPACING, 0.0, 0.0) func is_avatar_near_exterior(avatar: Player, owner_fingerprint: String) -> bool: if avatar == null or not _presentations_by_fingerprint.has(owner_fingerprint): return false var transform: Transform3D = get_exterior_transform(owner_fingerprint) var door_position: Vector3 = transform * EXTERIOR_DOOR_LOCAL return avatar.global_position.distance_to(door_position) <= ( EXTERIOR_INTERACTION_DISTANCE ) func is_avatar_near_interior_exit( avatar: Player, owner_fingerprint: String, ) -> bool: if avatar == null: return false return avatar.global_position.distance_to( get_interior_exit_position(owner_fingerprint) ) <= INTERIOR_EXIT_DISTANCE func get_interior_exit_position(owner_fingerprint: String) -> Vector3: return get_interior_anchor(owner_fingerprint) + Vector3( -INTERIOR_FLOOR_SIZE.x * 0.5 + 0.6, 0.35, 0.0, ) func get_interior_prompt_position(owner_fingerprint: String) -> Vector3: return get_interior_exit_position(owner_fingerprint) + Vector3.UP * 1.2 func interior_local_to_global( owner_fingerprint: String, local_position: Vector3, ) -> Vector3: return get_interior_anchor(owner_fingerprint) + local_position func interior_global_to_local( owner_fingerprint: String, global_position: Vector3, ) -> Vector3: return global_position - get_interior_anchor(owner_fingerprint) func get_floor_position_from_screen( owner_fingerprint: String, camera: Camera3D, screen_position: Vector2, ) -> Vector3: if camera == null or owner_fingerprint.is_empty(): return Vector3.INF var ray_origin: Vector3 = camera.project_ray_origin(screen_position) var ray_direction: Vector3 = camera.project_ray_normal(screen_position) if absf(ray_direction.y) <= 0.0001: return Vector3.INF var floor_y: float = get_interior_anchor(owner_fingerprint).y + 0.1 var distance: float = (floor_y - ray_origin.y) / ray_direction.y if distance <= 0.0: return Vector3.INF return interior_global_to_local( owner_fingerprint, ray_origin + ray_direction * distance, ) func find_decor_instance_at_screen( owner_fingerprint: String, camera: Camera3D, screen_position: Vector2, ) -> String: if camera == null or owner_fingerprint.is_empty(): return "" var ray_origin: Vector3 = camera.project_ray_origin(screen_position) var ray_end: Vector3 = ray_origin + camera.project_ray_normal( screen_position ) * 50.0 var query := PhysicsRayQueryParameters3D.create(ray_origin, ray_end) query.collision_mask = DECOR_SELECTION_COLLISION_LAYER query.collide_with_areas = false var result: Dictionary = camera.get_world_3d().direct_space_state.intersect_ray( query ) var collider: Object = result.get("collider") if collider is Node and (collider as Node).has_meta(&"decor_instance_id"): if str((collider as Node).get_meta(&"decor_owner_fingerprint")) == ( owner_fingerprint ): return str((collider as Node).get_meta(&"decor_instance_id")) return "" func get_decor_preview_transform( owner_fingerprint: String, instance_id: String, ) -> Dictionary: var decor: Node3D = _get_decor_presentation( owner_fingerprint, instance_id ) if decor == null: return {} return { "position": Vector3(decor.position.x, 0.0, decor.position.z), "yaw": decor.rotation.y, } func set_decor_preview_transform( owner_fingerprint: String, instance_id: String, position: Vector3, yaw_radians: float, ) -> bool: var decor: Node3D = _get_decor_presentation( owner_fingerprint, instance_id ) if decor == null or not position.is_finite() or not is_finite(yaw_radians): return false decor.position = Vector3(position.x, 0.18, position.z) decor.rotation.y = yaw_radians return true func set_decor_manipulation_active( owner_fingerprint: String, instance_id: String, active: bool, ) -> bool: var decor: Node3D = _get_decor_presentation( owner_fingerprint, instance_id ) if decor == null: return false var body: StaticBody3D = decor.get_node_or_null( "BasicCubeCollision" ) as StaticBody3D if body == null: return false var key: String = _decor_collision_key( owner_fingerprint, instance_id ) if active: _decor_under_manipulation[key] = true _pending_solid_decor.erase(key) body.collision_layer = DECOR_SELECTION_COLLISION_LAYER return true _decor_under_manipulation.erase(key) _queue_decor_world_collision( owner_fingerprint, instance_id, StringName(str(decor.get_meta(&"decor_id", ""))), body, ) return true func is_decor_placement_clear_of_players( owner_fingerprint: String, decor_id: StringName, local_position: Vector3, yaw_radians: float, ) -> bool: if ( owner_fingerprint.is_empty() or not DecorCatalogType.has_product(decor_id) or not local_position.is_finite() or not is_finite(yaw_radians) ): return false if _spawn_service == null: return true var global_center: Vector3 = interior_local_to_global( owner_fingerprint, local_position ) var half_size: Vector2 = DecorCatalogType.get_footprint(decor_id) * 0.5 for peer_id: int in _spawn_service.get_peer_ids(): var avatar: Player = _spawn_service.get_avatar(peer_id) if avatar == null or not is_instance_valid(avatar): continue var offset := Vector2( avatar.global_position.x - global_center.x, avatar.global_position.z - global_center.z, ).rotated(-yaw_radians) var nearest := Vector2( clampf(offset.x, -half_size.x, half_size.x), clampf(offset.y, -half_size.y, half_size.y), ) if offset.distance_squared_to(nearest) < ( PLAYER_DECOR_CLEARANCE_RADIUS * PLAYER_DECOR_CLEARANCE_RADIUS ): return false return true func _get_decor_presentation( owner_fingerprint: String, instance_id: String, ) -> Node3D: if owner_fingerprint.is_empty() or instance_id.is_empty(): return null var presentation: Dictionary = _presentations_by_fingerprint.get( owner_fingerprint, {} ) var interior: Node3D = presentation.get("interior") if not is_instance_valid(interior): return null return interior.get_node_or_null( "Decor/%s" % instance_id ) as Node3D func find_nearby_exterior_owner(avatar: Player) -> String: if avatar == null: return "" var closest: String = "" var closest_distance: float = EXTERIOR_INTERACTION_DISTANCE for fingerprint: String in _presentations_by_fingerprint: var transform: Transform3D = get_exterior_transform(fingerprint) var distance: float = avatar.global_position.distance_to( transform * EXTERIOR_DOOR_LOCAL ) if distance <= closest_distance: closest = fingerprint closest_distance = distance return closest func set_local_space(space_id: StringName) -> void: _local_space = space_id _refresh_local_visibility() func get_manifest(owner_fingerprint: String) -> Dictionary: var presentation: Dictionary = _presentations_by_fingerprint.get( owner_fingerprint, {} ) return (presentation.get("home", {}) as Dictionary).duplicate(true) func _process(_delta: float) -> void: _refresh_pending_decor_collisions() if not _dedicated_runtime: _refresh_nearest_wall() func _queue_decor_world_collision( owner_fingerprint: String, instance_id: String, decor_id: StringName, body: StaticBody3D, ) -> void: if ( owner_fingerprint.is_empty() or instance_id.is_empty() or not DecorCatalogType.has_product(decor_id) or body == null ): return var key: String = _decor_collision_key( owner_fingerprint, instance_id ) body.collision_layer = DECOR_SELECTION_COLLISION_LAYER _pending_solid_decor[key] = { "owner_fingerprint": owner_fingerprint, "instance_id": instance_id, "decor_id": decor_id, "body": body, } func _refresh_pending_decor_collisions() -> void: if _pending_solid_decor.is_empty(): return var keys: Array[String] = [] keys.assign(_pending_solid_decor.keys()) for key: String in keys: if _decor_under_manipulation.has(key): continue var pending: Dictionary = _pending_solid_decor.get(key, {}) var body: StaticBody3D = pending.get("body") as StaticBody3D var owner_fingerprint: String = str( pending.get("owner_fingerprint", "") ) var instance_id: String = str(pending.get("instance_id", "")) var decor_id := StringName(str(pending.get("decor_id", ""))) var decor: Node3D = _get_decor_presentation( owner_fingerprint, instance_id ) if body == null or decor == null or not is_instance_valid(body): _pending_solid_decor.erase(key) continue if not is_decor_placement_clear_of_players( owner_fingerprint, decor_id, Vector3(decor.position.x, 0.0, decor.position.z), decor.rotation.y, ): continue body.collision_layer = ( WORLD_GEOMETRY_COLLISION_LAYER | DECOR_SELECTION_COLLISION_LAYER ) _pending_solid_decor.erase(key) func _clear_decor_collision_tracking(owner_fingerprint: String) -> void: var prefix: String = "%s/" % owner_fingerprint for key: String in _decor_under_manipulation.keys(): if key.begins_with(prefix): _decor_under_manipulation.erase(key) for key: String in _pending_solid_decor.keys(): if key.begins_with(prefix): _pending_solid_decor.erase(key) static func _decor_collision_key( owner_fingerprint: String, instance_id: String, ) -> String: return "%s/%s" % [owner_fingerprint, instance_id] func _ensure_roots() -> void: if not is_instance_valid(_exteriors_root): _exteriors_root = Node3D.new() _exteriors_root.name = "RVExteriors" add_child(_exteriors_root) if not is_instance_valid(_interiors_root): _interiors_root = Node3D.new() _interiors_root.name = "RVInteriors" add_child(_interiors_root) func _build_exterior( fingerprint: String, world_transform: Transform3D, ) -> Node3D: var root := Node3D.new() root.name = "RV_%s" % fingerprint.left(8) _exteriors_root.add_child(root) root.global_transform = world_transform if not _dedicated_runtime: var motorcoach: Node3D = STARTER_MOTORCOACH_SCENE.instantiate() as Node3D if motorcoach != null: motorcoach.name = "StarterMotorcoach" root.add_child(motorcoach) else: push_error("The starter motorcoach exterior could not be instantiated.") _add_box_collision( root, "Body", EXTERIOR_BODY_SIZE, Vector3(0.0, 1.35, 0.0), ) root.visible = not _dedicated_runtime return root func _build_interior( fingerprint: String, interior_index: int, home_data: Dictionary, ) -> Node3D: var root := Node3D.new() root.name = "RVInterior_%s" % fingerprint.left(8) root.position = INTERIOR_ORIGIN + Vector3( float(interior_index) * INTERIOR_SPACING, 0.0, 0.0 ) var half_width: float = INTERIOR_FLOOR_SIZE.x * 0.5 var half_depth: float = INTERIOR_FLOOR_SIZE.z * 0.5 var wall_center_y: float = INTERIOR_WALL_HEIGHT * 0.5 _interiors_root.add_child(root) _add_box( root, "Floor", INTERIOR_FLOOR_SIZE, Vector3.ZERO, Color("a6754e"), true, ) _add_wall( root, "WallNorth", Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18), Vector3(0.0, wall_center_y, -half_depth), ) _add_wall( root, "WallSouth", Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18), Vector3(0.0, wall_center_y, half_depth), ) _add_wall( root, "WallWest", Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z), Vector3(-half_width, wall_center_y, 0.0), ) _add_wall( root, "WallEast", Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z), Vector3(half_width, wall_center_y, 0.0), ) for x_position: float in [-1.75, 1.75]: _add_box( root, "WallNorthWindow%s" % ("Left" if x_position < 0.0 else "Right"), Vector3(1.65, 0.95, 0.04), Vector3(x_position, 1.55, -half_depth + 0.11), Color("153640"), false, ) _add_box( root, "WallSouthWindow%s" % ("Left" if x_position < 0.0 else "Right"), Vector3(1.65, 0.95, 0.04), Vector3(x_position, 1.55, half_depth - 0.11), Color("153640"), false, ) _add_box( root, "WallEastWindow", Vector3(0.04, 0.95, 1.55), Vector3(half_width - 0.11, 1.55, 0.0), Color("153640"), false, ) _add_box( root, "WallWestDoor", Vector3(0.04, 1.9, 1.25), Vector3(-half_width + 0.11, 1.0, 0.0), Color("2897a8"), false, ) var decor := Node3D.new() decor.name = "Decor" root.add_child(decor) _rebuild_decor(decor, home_data, fingerprint) root.visible = false if _dedicated_runtime: _set_geometry_visible(root, false) return root func _rebuild_decor( parent: Node3D, home_data: Dictionary, owner_fingerprint: String, ) -> void: for value: Variant in home_data.get("placements", []): if typeof(value) != TYPE_DICTIONARY: continue var placement: Dictionary = value var decor_id := StringName(str(placement["decor_id"])) if decor_id != DecorCatalogType.BASIC_CUBE_ID: continue var position_values: Array = placement["position"] var placement_root := Node3D.new() placement_root.name = str(placement["instance_id"]) placement_root.position = Vector3( float(position_values[0]), 0.18, float(position_values[2]) ) placement_root.rotation.y = float(placement["yaw"]) placement_root.set_meta( &"decor_instance_id", str(placement["instance_id"]) ) placement_root.set_meta( &"decor_owner_fingerprint", owner_fingerprint ) placement_root.set_meta(&"decor_id", String(decor_id)) parent.add_child(placement_root) var body := _add_box( placement_root, "BasicCube", BASIC_CUBE_SIZE, Vector3(0.0, BASIC_CUBE_SIZE.y * 0.5, 0.0), Color("b77a55"), true, ) if body != null: body.collision_layer = DECOR_SELECTION_COLLISION_LAYER body.set_meta( &"decor_instance_id", str(placement["instance_id"]) ) body.set_meta( &"decor_owner_fingerprint", owner_fingerprint ) body.set_meta(&"decor_id", String(decor_id)) _queue_decor_world_collision( owner_fingerprint, str(placement["instance_id"]), decor_id, body, ) _add_box( placement_root, "BasicCubeFront", Vector3(BASIC_CUBE_SIZE.x * 0.82, BASIC_CUBE_SIZE.y * 0.68, 0.025), Vector3( 0.0, BASIC_CUBE_SIZE.y * 0.5, -BASIC_CUBE_SIZE.z * 0.5 - 0.013, ), Color("2897a8"), false, ) func _add_wall( parent: Node3D, node_name: String, size: Vector3, position: Vector3, ) -> void: _add_box(parent, node_name, size, position, Color("d6caaa"), true) func _add_box( parent: Node3D, node_name: String, size: Vector3, position: Vector3, color: Color, with_collision: bool, invert_faces: bool = false, ) -> StaticBody3D: var mesh_instance := MeshInstance3D.new() mesh_instance.name = node_name var mesh := BoxMesh.new() mesh.size = size mesh.material = _material(color) if invert_faces: mesh.flip_faces = true mesh_instance.mesh = mesh mesh_instance.position = position parent.add_child(mesh_instance) if with_collision: return _add_box_collision(parent, node_name, size, position) return null func _add_box_collision( parent: Node3D, node_name: String, size: Vector3, position: Vector3, ) -> StaticBody3D: var body := StaticBody3D.new() body.name = "%sCollision" % node_name body.position = position var collision := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = size collision.shape = shape body.add_child(collision) parent.add_child(body) return body func _spawn_decor_poof( owner_fingerprint: String, instance_id: String, ) -> void: var presentation: Dictionary = _presentations_by_fingerprint.get( owner_fingerprint, {} ) var interior: Node3D = presentation.get("interior") if not is_instance_valid(interior): return var placement_root: Node3D = interior.get_node_or_null( "Decor/%s" % instance_id ) if placement_root == null: return var poof := CPUParticles3D.new() poof.name = "DecorPoof" poof.amount = 12 poof.lifetime = 0.45 poof.one_shot = true poof.explosiveness = 0.9 poof.direction = Vector3.UP poof.spread = 80.0 poof.initial_velocity_min = 0.7 poof.initial_velocity_max = 1.5 poof.scale_amount_min = 0.08 poof.scale_amount_max = 0.16 var particle_mesh := SphereMesh.new() particle_mesh.radius = 0.06 particle_mesh.height = 0.12 particle_mesh.material = _material(Color("d9c5a3")) poof.mesh = particle_mesh placement_root.add_child(poof) poof.emitting = true get_tree().create_timer(poof.lifetime + 0.1).timeout.connect( poof.queue_free ) func _material(color: Color) -> StandardMaterial3D: var material := StandardMaterial3D.new() material.albedo_color = color material.roughness = 1.0 material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX return material func _free_presentation_nodes(presentation: Dictionary) -> void: for key: String in ["exterior", "interior"]: var node: Node = presentation.get(key) if is_instance_valid(node): node.queue_free() func _refresh_local_visibility() -> void: var active_fingerprint: String = "" if String(_local_space).begins_with("rv:"): active_fingerprint = String(_local_space).trim_prefix("rv:") if _world != null: _world.set_local_home_interior_presentation_active( not active_fingerprint.is_empty() ) for fingerprint: String in _presentations_by_fingerprint: var presentation: Dictionary = _presentations_by_fingerprint[fingerprint] var interior: Node3D = presentation.get("interior") if is_instance_valid(interior): interior.visible = ( not _dedicated_runtime and fingerprint == active_fingerprint ) var exterior: Node3D = presentation.get("exterior") if is_instance_valid(exterior): exterior.visible = ( not _dedicated_runtime and active_fingerprint.is_empty() ) var local_player: Player = ( _spawn_service.get_local_player() if _spawn_service != null else null ) if local_player == null: return var excluded_wall_rids: Array[RID] = [] if not active_fingerprint.is_empty(): var active: Dictionary = _presentations_by_fingerprint.get( active_fingerprint, {} ) var active_interior: Node3D = active.get("interior") if is_instance_valid(active_interior): for wall_name: String in [ "WallNorth", "WallSouth", "WallWest", "WallEast", ]: var body: StaticBody3D = active_interior.get_node_or_null( "%sCollision" % wall_name ) if body != null: excluded_wall_rids.append(body.get_rid()) local_player.set_rv_interior_camera_mode( not active_fingerprint.is_empty(), excluded_wall_rids ) func _refresh_nearest_wall() -> void: if not String(_local_space).begins_with("rv:") or _spawn_service == null: return var fingerprint: String = String(_local_space).trim_prefix("rv:") var presentation: Dictionary = _presentations_by_fingerprint.get( fingerprint, {} ) var interior: Node3D = presentation.get("interior") var player: Player = _spawn_service.get_local_player() if not is_instance_valid(interior) or player == null: return var camera: Camera3D = player.get_active_gameplay_camera() if camera == null: return var local_camera: Vector3 = interior.to_local(camera.global_position) var local_view_direction: Vector3 = ( interior.global_basis.inverse() * -camera.global_basis.z ).normalized() var horizontal_view_weight: float = absf(local_view_direction.x) var depth_view_weight: float = absf(local_view_direction.z) var hidden_walls: Dictionary[String, bool] = {} var outside_horizontal_wall: bool = ( absf(local_camera.x) >= INTERIOR_FLOOR_SIZE.x * 0.5 - 0.05 ) var outside_depth_wall: bool = ( absf(local_camera.z) >= INTERIOR_FLOOR_SIZE.z * 0.5 - 0.05 ) # At a diagonal, both near walls occupy the view. Use the camera's viewing # direction to hide both instead of choosing only the dominant position # axis and leaving the adjacent wall across the screen. if ( outside_horizontal_wall and horizontal_view_weight >= depth_view_weight * 0.35 ): hidden_walls[ "WallEast" if local_camera.x >= 0.0 else "WallWest" ] = true if ( outside_depth_wall and depth_view_weight >= horizontal_view_weight * 0.35 ): hidden_walls[ "WallSouth" if local_camera.z >= 0.0 else "WallNorth" ] = true if hidden_walls.is_empty(): var fallback_wall: String = ( "WallEast" if local_camera.x >= 0.0 else "WallWest" ) if depth_view_weight > horizontal_view_weight: fallback_wall = ( "WallSouth" if local_camera.z >= 0.0 else "WallNorth" ) hidden_walls[fallback_wall] = true for wall_name: String in [ "WallNorth", "WallSouth", "WallWest", "WallEast", ]: var wall: MeshInstance3D = interior.get_node_or_null(wall_name) if wall != null: wall.visible = not hidden_walls.has(wall_name) for child: Node in interior.get_children(): if ( child is GeometryInstance3D and String(child.name).begins_with(wall_name) ): (child as GeometryInstance3D).visible = ( not hidden_walls.has(wall_name) ) func _too_close_to_reserved( position: Vector3, reserved_transforms: Array[Transform3D], ) -> bool: for transform: Transform3D in reserved_transforms: if position.distance_to(transform.origin) < MIN_EXTERIOR_SEPARATION: return true return false func _set_geometry_visible(root: Node, visible: bool) -> void: for node: Node in root.find_children("*", "GeometryInstance3D", true, false): (node as GeometryInstance3D).visible = visible