straywild/homes/home_world_service.gd

1209 lines
35 KiB
GDScript

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