fix: harden recovery flow and clean diagnostics

This commit is contained in:
Alexander Sellite 2026-09-01 07:44:01 -04:00
parent 0158b0215f
commit ff78710303
64 changed files with 1163 additions and 418 deletions

View file

@ -18,4 +18,4 @@ static func get_unit_value(item: ItemDataType) -> int:
if not is_sellable(item):
return -1
var purchase_price := FishingShopStockType.get_price(item.item_id)
return purchase_price / 2 if purchase_price >= 0 else -1
return int(float(purchase_price) / 2.0) if purchase_price >= 0 else -1

View file

@ -262,10 +262,10 @@ func _cast_marker_ray(from: Vector3, to: Vector3) -> Dictionary:
func _apply_marker_surface(hit: Dictionary, facing: Vector3) -> bool:
if hit.is_empty():
return false
var position: Variant = hit.get("position")
var surface_position: Variant = hit.get("position")
var normal_value: Variant = hit.get("normal")
if (
typeof(position) != TYPE_VECTOR3
typeof(surface_position) != TYPE_VECTOR3
or typeof(normal_value) != TYPE_VECTOR3
):
return false
@ -274,7 +274,7 @@ func _apply_marker_surface(hit: Dictionary, facing: Vector3) -> bool:
return false
surface_normal = surface_normal.normalized()
_marker_position = (
(position as Vector3) + surface_normal * marker_surface_offset
(surface_position as Vector3) + surface_normal * marker_surface_offset
)
_marker.global_position = _marker_position
_marker.global_basis = marker_basis_for_surface(surface_normal, facing)

View file

@ -23,7 +23,7 @@ var _despawning: bool = false
func configure(
configured_entity_id: String,
configured_data: GatherableDataType,
position: Vector3,
network_position: Vector3,
yaw: float,
) -> void:
entity_id = configured_entity_id
@ -31,7 +31,7 @@ func configure(
type_id = data.type_id if data != null else StringName()
# Establish the authoritative transform before constructing transient visuals.
# Effects emitted before this point would begin at the default world origin.
apply_network_state(position, yaw, true)
apply_network_state(network_position, yaw, true)
if data != null and data.is_stationary_hotspot():
_ensure_water_spurt_visual()
_water_spurt_elapsed = (
@ -57,16 +57,16 @@ func configure(
func apply_network_state(
position: Vector3,
network_position: Vector3,
yaw: float,
immediate: bool = false,
) -> void:
if _despawning or not position.is_finite() or not is_finite(yaw):
if _despawning or not network_position.is_finite() or not is_finite(yaw):
return
_target_position = position
_target_position = network_position
_target_yaw = yaw
if immediate or not _has_state:
global_position = position
global_position = network_position
rotation.y = yaw
_has_state = true

View file

@ -253,19 +253,19 @@ func _find_exterior_transform(
]
if not _is_exterior_candidate_triangle(triangle):
continue
var position: Vector3 = (
var candidate_position: Vector3 = (
triangle[0] + triangle[1] + triangle[2]
) / 3.0
if (
require_spawn_clearance
and position.distance_to(spawn_transform.origin) < 9.0
and candidate_position.distance_to(spawn_transform.origin) < 9.0
):
continue
if _too_close_to_reserved(position, reserved_transforms):
if _too_close_to_reserved(candidate_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
candidate_position, yaw, triangles, surface_index
)
if not grounded_position.is_finite():
continue
@ -299,7 +299,7 @@ func _is_exterior_candidate_triangle(triangle: PackedVector3Array) -> bool:
func _ground_exterior_footprint(
position: Vector3,
candidate_position: Vector3,
yaw: float,
triangles: Array[PackedVector3Array],
surface_index: Dictionary,
@ -310,11 +310,11 @@ func _ground_exterior_footprint(
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,
candidate_position.x + rotated_sample.x,
candidate_position.z + rotated_sample.z,
)
var height: float = _surface_height_at(
sample, position.y, triangles, surface_index
sample, candidate_position.y, triangles, surface_index
)
if not is_finite(height):
return Vector3.INF
@ -323,7 +323,9 @@ func _ground_exterior_footprint(
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)
return Vector3(
candidate_position.x, maximum_height, candidate_position.z
)
func _exterior_footprint_samples() -> Array[Vector2]:
@ -434,7 +436,9 @@ func _triangle_height_at(
)
func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool:
func _exterior_volume_is_clear(
candidate_position: Vector3, yaw: float
) -> bool:
if not is_inside_tree() or get_world_3d() == null:
return true
var shape := BoxShape3D.new()
@ -447,7 +451,8 @@ func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool:
query.shape = shape
query.transform = Transform3D(
Basis(Vector3.UP, yaw),
position + Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2),
candidate_position
+ Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2),
)
query.collision_mask = 1
query.collide_with_areas = false
@ -563,9 +568,11 @@ func get_interior_anchor(owner_fingerprint: String) -> Vector3:
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 exterior_transform: Transform3D = get_exterior_transform(
owner_fingerprint
)
var door_position: Vector3 = (
transform * _exterior_door_local(owner_fingerprint)
exterior_transform * _exterior_door_local(owner_fingerprint)
)
return avatar.global_position.distance_to(door_position) <= (
EXTERIOR_INTERACTION_DISTANCE
@ -639,9 +646,9 @@ func interior_local_to_global(
func interior_global_to_local(
owner_fingerprint: String,
global_position: Vector3,
world_position: Vector3,
) -> Vector3:
return global_position - get_interior_anchor(owner_fingerprint)
return world_position - get_interior_anchor(owner_fingerprint)
func get_floor_position_from_screen(
@ -709,15 +716,19 @@ func get_decor_preview_transform(
func set_decor_preview_transform(
owner_fingerprint: String,
instance_id: String,
position: Vector3,
decor_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):
if (
decor == null
or not decor_position.is_finite()
or not is_finite(yaw_radians)
):
return false
decor.position = Vector3(position.x, 0.18, position.z)
decor.position = Vector3(decor_position.x, 0.18, decor_position.z)
decor.rotation.y = yaw_radians
return true
@ -817,9 +828,11 @@ func find_nearby_exterior_owner(avatar: Player) -> String:
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 exterior_transform: Transform3D = get_exterior_transform(
fingerprint
)
var distance: float = avatar.global_position.distance_to(
transform * _exterior_door_local(fingerprint)
exterior_transform * _exterior_door_local(fingerprint)
)
if distance <= closest_distance:
closest = fingerprint
@ -1261,16 +1274,16 @@ func _add_wall(
parent: Node3D,
node_name: String,
size: Vector3,
position: Vector3,
local_position: Vector3,
) -> void:
_add_box(parent, node_name, size, position, Color("d6caaa"), true)
_add_box(parent, node_name, size, local_position, Color("d6caaa"), true)
func _add_box(
parent: Node3D,
node_name: String,
size: Vector3,
position: Vector3,
local_position: Vector3,
color: Color,
with_collision: bool,
invert_faces: bool = false,
@ -1283,10 +1296,10 @@ func _add_box(
if invert_faces:
mesh.flip_faces = true
mesh_instance.mesh = mesh
mesh_instance.position = position
mesh_instance.position = local_position
parent.add_child(mesh_instance)
if with_collision:
return _add_box_collision(parent, node_name, size, position)
return _add_box_collision(parent, node_name, size, local_position)
return null
@ -1294,11 +1307,11 @@ func _add_box_collision(
parent: Node3D,
node_name: String,
size: Vector3,
position: Vector3,
local_position: Vector3,
) -> StaticBody3D:
var body := StaticBody3D.new()
body.name = "%sCollision" % node_name
body.position = position
body.position = local_position
var collision := CollisionShape3D.new()
var shape := BoxShape3D.new()
shape.size = size
@ -1490,15 +1503,18 @@ func _refresh_nearest_wall() -> void:
func _too_close_to_reserved(
position: Vector3,
candidate_position: Vector3,
reserved_transforms: Array[Transform3D],
) -> bool:
for transform: Transform3D in reserved_transforms:
if position.distance_to(transform.origin) < MIN_EXTERIOR_SEPARATION:
for reserved_transform: Transform3D in reserved_transforms:
if (
candidate_position.distance_to(reserved_transform.origin)
< MIN_EXTERIOR_SEPARATION
):
return true
return false
func _set_geometry_visible(root: Node, visible: bool) -> void:
func _set_geometry_visible(root: Node, should_show: bool) -> void:
for node: Node in root.find_children("*", "GeometryInstance3D", true, false):
(node as GeometryInstance3D).visible = visible
(node as GeometryInstance3D).visible = should_show

View file

@ -369,7 +369,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
return false
_tier = int(sanitized["tier"])
_exterior_id = exterior_id_for_tier(_tier)
_privacy = int(sanitized["privacy"])
_privacy = int(sanitized["privacy"]) as Privacy
_owned_decor.clear()
for key: Variant in (sanitized["owned_decor"] as Dictionary):
_owned_decor[StringName(str(key))] = int(

View file

@ -1541,6 +1541,12 @@ func _configure_popup_dialog(
func _input(event: InputEvent) -> void:
# The exit walk and its authoritative relocation own gameplay input until the
# return fade completes. This also keeps a late cancel/pause press from
# manipulating a menu while the world space is changing.
if _home_exit_transition_active:
get_viewport().set_input_as_handled()
return
# Do not let controller aliases close or manipulate the world/menu behind
# the modal on-screen keyboard. The keyboard consumes the event itself.
if _game_ui.is_controller_text_entry_open():
@ -1671,6 +1677,9 @@ func _is_pause_open_request(event: InputEvent) -> bool:
func _unhandled_input(event: InputEvent) -> void:
if _home_exit_transition_active:
get_viewport().set_input_as_handled()
return
if (
_gameplay_started
and event.is_action_pressed("fish_primary")
@ -1858,6 +1867,12 @@ func _reset_pixelation() -> void:
func _set_gameplay_active(active: bool) -> void:
var was_gameplay_started: bool = _gameplay_started
_gameplay_started = active
# Session loss, a world switch, or a return to title can interrupt the home
# handoff before its fade callback runs. Do not carry its input suppressors
# or black screen into the next gameplay session.
if not active:
_cancel_home_entry_transition()
_cancel_home_exit_transition(true)
_shoreline_ambience.set_active(active)
_rain_ambience.set_active(active)
if active and not was_gameplay_started:
@ -2499,6 +2514,7 @@ func _on_local_respawn_completed(entry_position: Vector3) -> void:
func _on_remote_recovery_requested(
peer_id: int,
entry_position: Vector3,
recovery_input_sequence: int,
) -> void:
if not _network_session.is_host():
return
@ -2509,6 +2525,9 @@ func _on_remote_recovery_requested(
peer_id,
"Fishing attempt ended."
)
avatar.neutralize_authoritative_input_for_relocation(
recovery_input_sequence
)
var target_position := _test_world.get_water_recovery_position(
entry_position
)
@ -3039,6 +3058,12 @@ func _begin_home_exit_transition(owner_fingerprint: String) -> void:
_refresh_home_door_prompt({})
_player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, true)
_player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, true)
# A same-frame interact can otherwise open home storage after the threshold
# was crossed. The exit suppressor is already active, so restoring storage's
# base snapshot cannot return effective control before the transition ends.
_game_ui.get_player_storage().close_storage()
_game_ui.set_rv_storage_button_visible(false)
_game_ui.set_gameplay_input_locked(true)
var exit_direction: Vector3 = _home_world.get_interior_exit_direction(
owner_fingerprint
)
@ -3126,6 +3151,11 @@ func _cancel_home_exit_transition(reset_fade: bool) -> void:
_home_exit_transition_active = false
_home_exit_waiting_for_network = false
_home_exit_start_position = Vector3.ZERO
if is_instance_valid(_game_ui):
_game_ui.set_gameplay_input_locked(false)
_game_ui.set_rv_storage_button_visible(
_can_show_rv_storage_button()
)
func _on_rv_storage_requested() -> void:
@ -3247,6 +3277,7 @@ func _refresh_home_door_prompt(prompt: Dictionary) -> void:
func _can_show_rv_storage_button() -> bool:
return (
_gameplay_started
and not _home_exit_transition_active
and not _network_home.get_local_home_owner_fingerprint().is_empty()
and _network_home.can_local_decorate()
and not _water_recovery.is_recovery_active()
@ -3257,6 +3288,7 @@ func _can_show_rv_storage_button() -> bool:
func _get_home_prompt() -> Dictionary:
if (
not _gameplay_started
or _home_exit_transition_active
or _game_ui.get_player_storage().visible
or _game_ui.get_fishing_shop().visible
or _game_ui.get_decor_shop().visible

View file

@ -1042,9 +1042,11 @@ func _valid_json_integer(value: Variant, minimum: int, maximum: int) -> bool:
)
func _make_social_request(name: String, callback: Callable) -> HTTPRequest:
func _make_social_request(
request_name: String, callback: Callable
) -> HTTPRequest:
var request := HTTPRequest.new()
request.name = name
request.name = request_name
request.timeout = REQUEST_TIMEOUT_SECONDS
add_child(request)
request.request_completed.connect(callback)

View file

@ -340,11 +340,12 @@ func _handle_request(peer_id: int, data: Dictionary) -> void:
message["request_id"] = request_id
message["sender_fingerprint"] = data["sender_fingerprint"]
message["sender_signature"] = data["sender_signature"]
ledger[request_id] = (
message.duplicate(true)
if _should_store_host_history()
else true
)
# `true` is the intentional no-history replay sentinel. It records that the
# request was handled without retaining a message payload for replay.
var ledger_entry: Variant = true
if _should_store_host_history():
ledger_entry = message.duplicate(true)
ledger[request_id] = ledger_entry
while ledger.size() > 64:
ledger.erase(ledger.keys().front())
_request_ledgers[peer_id] = ledger

View file

@ -29,6 +29,11 @@ const ANIMATION_REFRESH_INTERVAL: float = 1.0
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
const SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE: float = 12.0
# A reliable teleport travels on a different channel from ordinary movement
# audits. Keep the acceptance radius below Player's hard-reconciliation
# distance so an old audit can never pull a just-relocated local player back
# into the place they were recovered from.
const AUTHORITATIVE_TELEPORT_SNAPSHOT_GUARD_DISTANCE: float = 2.0
const MOVEMENT_FLAG_JUMP: int = 1 << 0
const MOVEMENT_FLAG_SPRINT: int = 1 << 1
@ -77,7 +82,11 @@ signal server_trust_required(
signal peer_identity_observed(peer_id: int, status: String)
signal operator_status_changed(peer_id: int, is_operator: bool)
signal server_lost(message: String)
signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
signal remote_recovery_requested(
peer_id: int,
entry_position: Vector3,
recovery_input_sequence: int,
)
signal remote_recovery_presentation_changed(
peer_id: int,
active: bool,
@ -128,6 +137,9 @@ var _last_local_snapshot_received_msec: int = 0
var _local_space_transition_guard_active: bool = false
var _local_space_transition_guard_position: Vector3 = Vector3.ZERO
var _local_space_transition_minimum_ack: int = 0
var _local_authoritative_teleport_guard_active: bool = false
var _local_authoritative_teleport_guard_position: Vector3 = Vector3.ZERO
var _local_authoritative_teleport_minimum_ack: int = 0
var _animation_refresh_accumulator: float = 0.0
var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {}
var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {}
@ -601,20 +613,20 @@ func get_last_server_metadata() -> Dictionary:
}
func set_host_world_seed(seed: int) -> bool:
return set_host_world(WorldLayout.GENERATED, seed)
func set_host_world_seed(world_seed: int) -> bool:
return set_host_world(WorldLayout.GENERATED, world_seed)
func set_host_world(world_layout: StringName, seed: int) -> bool:
func set_host_world(world_layout: StringName, world_seed: int) -> bool:
if (
state != State.INACTIVE
or not WorldLayout.is_valid(world_layout)
or seed <= 0
or seed > NetworkProtocol.MAX_WORLD_SEED
or world_seed <= 0
or world_seed > NetworkProtocol.MAX_WORLD_SEED
):
return false
_host_world_layout = world_layout
_host_world_seed = seed
_host_world_seed = world_seed
return true
@ -2088,10 +2100,10 @@ func _maybe_send_local_input() -> void:
if avatar == null:
return
var state_hash: int = avatar.get_network_input_state_hash()
var state_changed: bool = state_hash != _last_input_state_hash
var input_state_changed: bool = state_hash != _last_input_state_hash
if (
avatar.has_active_network_input()
or state_changed
or input_state_changed
or _idle_input_accumulator >= IDLE_INPUT_INTERVAL
):
_send_local_input()
@ -2125,8 +2137,8 @@ func _maybe_send_local_animation_action() -> void:
var avatar: Player = _spawn_service.get_avatar(peer_id)
if avatar == null:
return
var state: Dictionary = avatar.make_network_animation_state()
var action: Dictionary = state.get("action", {})
var animation_state: Dictionary = avatar.make_network_animation_state()
var action: Dictionary = animation_state.get("action", {})
if not NetworkPlayerAnimationProtocol.validate_action_state(action):
return
var signature: Array = [
@ -2182,11 +2194,15 @@ func submit_movement_animation_action(encoded: Array) -> void:
if not is_host() or not _registry.has_peer(sender_id):
return
var avatar: Player = _spawn_service.get_avatar(sender_id)
var state: Dictionary = _decode_movement_animation_action(encoded)
if avatar == null or state.is_empty():
var animation_state: Dictionary = _decode_movement_animation_action(encoded)
if avatar == null or animation_state.is_empty():
return
avatar.apply_authoritative_network_animation_action(state["action"])
avatar.apply_authoritative_network_sitting_state(bool(state["sitting"]))
avatar.apply_authoritative_network_animation_action(
animation_state["action"]
)
avatar.apply_authoritative_network_sitting_state(
bool(animation_state["sitting"])
)
static func _encode_movement_input(data: Dictionary) -> Array:
@ -2386,18 +2402,20 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
var avatar: Player = _spawn_service.get_avatar(subject_id)
if avatar == null:
continue
var state: Dictionary = avatar.make_network_animation_state()
var animation_state: Dictionary = avatar.make_network_animation_state()
var previous: Dictionary = _last_animation_state_by_peer.get(
subject_id, {}
)
if (
not refresh_all
and _movement_animation_signature(state)
and _movement_animation_signature(animation_state)
== _movement_animation_signature(previous)
):
continue
_last_animation_state_by_peer[subject_id] = state.duplicate(true)
var encoded: Array = _encode_movement_animation(subject_id, state)
_last_animation_state_by_peer[subject_id] = animation_state.duplicate(true)
var encoded: Array = _encode_movement_animation(
subject_id, animation_state
)
if not encoded.is_empty():
updates_by_subject[subject_id] = encoded
if updates_by_subject.is_empty():
@ -2416,13 +2434,13 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
_movement_animation_states_sent += updates.size()
static func _movement_animation_signature(state: Dictionary) -> Array:
if not NetworkPlayerAnimationProtocol.validate_state(state):
static func _movement_animation_signature(animation_state: Dictionary) -> Array:
if not NetworkPlayerAnimationProtocol.validate_state(animation_state):
return []
var action: Dictionary = state["action"]
var action: Dictionary = animation_state["action"]
return [
str(state["locomotion_id"]),
bool(state["grounded"]),
str(animation_state["locomotion_id"]),
bool(animation_state["grounded"]),
str(action["id"]),
int(action["sequence"]),
bool(action.get("paused", false)),
@ -2505,15 +2523,15 @@ static func _decode_movement_snapshot(value: Variant) -> Dictionary:
static func _encode_movement_animation(
peer_id: int,
state: Dictionary,
animation_state: Dictionary,
) -> Array:
if not NetworkPlayerAnimationProtocol.validate_state(state):
if not NetworkPlayerAnimationProtocol.validate_state(animation_state):
return []
var action: Dictionary = state["action"]
var action: Dictionary = animation_state["action"]
return [
peer_id,
str(state["locomotion_id"]),
bool(state["grounded"]),
str(animation_state["locomotion_id"]),
bool(animation_state["grounded"]),
str(action["id"]),
int(action["sequence"]),
float(action["elapsed"]),
@ -2536,7 +2554,7 @@ static func _decode_movement_animation(value: Variant) -> Dictionary:
or typeof(fields[6]) != TYPE_BOOL
):
return {}
var state: Dictionary = NetworkPlayerAnimationProtocol.make_state(
var animation_state: Dictionary = NetworkPlayerAnimationProtocol.make_state(
StringName(str(fields[1])),
bool(fields[2]),
StringName(str(fields[3])),
@ -2546,10 +2564,10 @@ static func _decode_movement_animation(value: Variant) -> Dictionary:
)
if (
int(fields[0]) <= 0
or not NetworkPlayerAnimationProtocol.validate_state(state)
or not NetworkPlayerAnimationProtocol.validate_state(animation_state)
):
return {}
return {"peer_id": int(fields[0]), "state": state}
return {"peer_id": int(fields[0]), "state": animation_state}
static func _encode_movement_animation_action(
@ -2610,7 +2628,10 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
if avatar == null:
continue
if peer_id == local_peer_id:
if _reject_stale_space_transition_snapshot(snapshot):
if (
_reject_stale_space_transition_snapshot(snapshot)
or _reject_stale_authoritative_teleport_snapshot(snapshot)
):
continue
_discard_acknowledged_movement_inputs(
int(snapshot.get("acknowledged_input", 0))
@ -2656,6 +2677,37 @@ func _reject_stale_space_transition_snapshot(snapshot: Dictionary) -> bool:
return false
func _reject_stale_authoritative_teleport_snapshot(
snapshot: Dictionary,
) -> bool:
if not _local_authoritative_teleport_guard_active:
return false
var position_values: Array = snapshot.get("position", [])
if position_values.size() != 3:
return true
var position := Vector3(
float(position_values[0]),
float(position_values[1]),
float(position_values[2]),
)
var acknowledged_input: int = int(
snapshot.get("acknowledged_input", 0)
)
if acknowledged_input < _local_authoritative_teleport_minimum_ack:
return true
# Movement input and reliable recovery RPCs travel on different channels.
# A higher input acknowledgement alone therefore does not prove that this
# snapshot was authored after the teleport; a held pre-recovery packet can
# arrive at the host late. Only a snapshot that is still near the teleported
# position is allowed to release the barrier.
if position.distance_to(_local_authoritative_teleport_guard_position) > (
AUTHORITATIVE_TELEPORT_SNAPSHOT_GUARD_DISTANCE
):
return true
_local_authoritative_teleport_guard_active = false
return false
func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void:
while (
not _pending_movement_inputs.is_empty()
@ -2823,11 +2875,16 @@ func request_safe_respawn(entry_position: Vector3) -> void:
if not entry_position.is_finite():
return
if is_host():
remote_recovery_requested.emit(1, entry_position)
remote_recovery_requested.emit(1, entry_position, _input_sequence)
elif state == State.JOINED_CLIENT:
submit_safe_respawn_request.rpc_id(
1,
[entry_position.x, entry_position.y, entry_position.z]
[
entry_position.x,
entry_position.y,
entry_position.z,
_input_sequence,
]
)
@ -2837,9 +2894,18 @@ func submit_safe_respawn_request(position_data: Array) -> void:
if (
not is_host()
or not _registry.has_peer(sender_id)
or position_data.size() != 3
or position_data.size() not in [3, 4]
):
return
var recovery_input_sequence: int = -1
if position_data.size() == 4:
if (
typeof(position_data[3]) != TYPE_INT
or int(position_data[3]) < 0
or int(position_data[3]) > MAX_MOVEMENT_INPUT_SEQUENCE
):
return
recovery_input_sequence = int(position_data[3])
var entry_position := Vector3(
float(position_data[0]),
float(position_data[1]),
@ -2847,7 +2913,11 @@ func submit_safe_respawn_request(position_data: Array) -> void:
)
if not entry_position.is_finite():
return
remote_recovery_requested.emit(sender_id, entry_position)
remote_recovery_requested.emit(
sender_id,
entry_position,
recovery_input_sequence,
)
@rpc("authority", "call_remote", "reliable", 0)
@ -2856,8 +2926,38 @@ func receive_authoritative_teleport(snapshot: Dictionary) -> void:
return
var peer_id: int = int(snapshot.get("peer_id", 0))
var avatar: Player = _spawn_service.get_avatar(peer_id)
if avatar != null:
avatar.apply_network_teleport(snapshot)
if avatar == null or not avatar.apply_network_teleport(snapshot):
return
if peer_id != multiplayer.get_unique_id():
return
_begin_local_authoritative_teleport_barrier(
avatar,
int(snapshot.get("acknowledged_input", 0)),
)
func _begin_local_authoritative_teleport_barrier(
avatar: Player,
acknowledged_input: int,
) -> void:
if avatar == null or not avatar.global_position.is_finite():
return
# A recovery teleport is a movement discontinuity, just like a home-space
# transition. Drop pre-teleport client prediction and request an immediate
# neutral input so the first post-teleport audit is unambiguous.
_pending_movement_inputs.clear()
_last_local_snapshot_received_msec = 0
_last_input_state_hash = 0
_idle_input_accumulator = IDLE_INPUT_INTERVAL
avatar.reset_local_prediction_after_authoritative_teleport()
_local_authoritative_teleport_guard_active = true
_local_authoritative_teleport_guard_position = avatar.global_position
_local_authoritative_teleport_minimum_ack = maxi(acknowledged_input, 0)
# Water recovery has already suppressed local movement, so this sends a
# neutral post-relocation input on the next server tick instead of waiting
# for the regular input cadence.
if avatar.is_water_recovery_active():
submit_neutral_local_movement()
func _expire_pending_authentication(now: float) -> void:
@ -2987,6 +3087,9 @@ func _teardown_peer() -> void:
_local_space_transition_guard_active = false
_local_space_transition_guard_position = Vector3.ZERO
_local_space_transition_minimum_ack = 0
_local_authoritative_teleport_guard_active = false
_local_authoritative_teleport_guard_position = Vector3.ZERO
_local_authoritative_teleport_minimum_ack = 0
_animation_refresh_accumulator = 0.0
_last_animation_state_by_peer.clear()
_pending_animation_state_by_peer.clear()

View file

@ -79,10 +79,10 @@ func get_impact_world_position() -> Vector3:
func _sync_spring_activity() -> void:
if _spring == null:
return
var is_visible := _catching_net.is_visible_in_tree()
if is_visible and not _spring_was_visible:
var net_is_visible: bool = _catching_net.is_visible_in_tree()
if net_is_visible and not _spring_was_visible:
_spring.active = true
_spring.reset()
elif not is_visible:
elif not net_is_visible:
_spring.active = false
_spring_was_visible = is_visible
_spring_was_visible = net_is_visible

View file

@ -531,6 +531,7 @@ var _network_sprint: bool = false
var _network_sneak: bool = false
var _network_slow_walk: bool = false
var _last_network_input_sequence: int = 0
var _authoritative_relocation_input_quarantine: bool = false
var _network_input_age: float = 0.0
var _network_input_stale: bool = false
var _network_input_stale_timeout_seconds: float = (
@ -1383,7 +1384,7 @@ func _update_character_animation() -> void:
var is_sneaking_pose: bool = (
locomotion_state == LocomotionState.SNEAKING
)
var is_moving_horizontally: bool = (
var is_presented_moving_horizontally: bool = (
_is_presented_moving_horizontally()
)
var animation_action: Dictionary = _get_presented_animation_action()
@ -1500,7 +1501,7 @@ func _update_character_animation() -> void:
&"idle_loop_sit",
]
elif is_sneaking_pose:
if is_moving_horizontally:
if is_presented_moving_horizontally:
requested_animation = [
CHARACTER_SNEAKING_ANIMATION,
CHARACTER_WALKING_ANIMATION,
@ -1787,6 +1788,11 @@ func apply_network_sitting_state(should_sit: bool) -> void:
func apply_authoritative_network_sitting_state(should_sit: bool) -> void:
# Reliable animation updates use a separate channel from movement input.
# Never let a delayed seated state re-arm motion while a recovery relocation
# is still waiting for its explicit post-relocation neutral input.
if _water_recovery_active or _authoritative_relocation_input_quarantine:
return
if should_sit and (
not _can_begin_sitting() or _network_jump_pending
):
@ -2149,6 +2155,7 @@ func reset_network_movement_state() -> void:
_network_sprint = false
_network_sneak = false
_network_slow_walk = false
_authoritative_relocation_input_quarantine = false
_network_input_age = 0.0
_network_input_stale = false
_network_input_stale_timeout_seconds = NETWORK_INPUT_STALE_TIMEOUT_SECONDS
@ -2161,6 +2168,53 @@ func reset_network_movement_state() -> void:
_local_prediction_largest_error = 0.0
func neutralize_authoritative_input_for_relocation(
recovery_input_sequence: int = -1,
) -> void:
# A host-side relocation must not inherit a held movement input from the
# previous position. The client stamps its reliable recovery request with
# the most recent input sequence it created; every lower sequence was made
# before that request, even if it arrives on the movement channel later.
# Keep that watermark in the acknowledgement and accept movement only after
# an explicit neutral packet beyond it.
var acknowledged_input: int = maxi(
_last_network_input_sequence,
maxi(recovery_input_sequence, 0),
)
reset_network_movement_state()
_last_network_input_sequence = acknowledged_input
_authoritative_relocation_input_quarantine = true
_clear_authoritative_network_motion_intent()
velocity = Vector3.ZERO
func _clear_authoritative_network_motion_intent() -> void:
_network_axis = Vector2.ZERO
_network_jump_pending = false
_network_jump_intent_active = false
_network_sprint = false
_network_sneak = false
_network_slow_walk = false
_sit_after_landing = false
_set_sitting(false)
_apply_network_casting(false)
func _is_neutral_authoritative_network_input(data: Dictionary) -> bool:
var axis: Array = data.get("axis", [])
if axis.size() != 2:
return false
# Sprint/sneak/slow-walk modifiers alone cannot move the avatar. Letting a
# held modifier through preserves a responsive recovery while still requiring
# zero movement, jump, sitting, and fishing state at the relocation barrier.
return (
Vector2(float(axis[0]), float(axis[1])).length_squared() <= 0.0025
and not bool(data.get("jump", false))
and not bool(data.get("sitting", false))
and not bool(data.get("casting", false))
)
func apply_network_space_transition(
target_transform: Transform3D,
align_local_camera: bool,
@ -2315,8 +2369,17 @@ func apply_authoritative_network_input(
NETWORK_INPUT_STALE_TIMEOUT_SECONDS,
NETWORK_INPUT_STALE_TIMEOUT_MAX_SECONDS,
)
_network_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
_network_camera_yaw = float(data.get("camera_yaw", 0.0))
if _water_recovery_active or _authoritative_relocation_input_quarantine:
var clears_relocation_quarantine: bool = (
_authoritative_relocation_input_quarantine
and _is_neutral_authoritative_network_input(data)
)
_clear_authoritative_network_motion_intent()
if clears_relocation_quarantine:
_authoritative_relocation_input_quarantine = false
return
_network_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
var jump_intent_active: bool = bool(data.get("jump", false))
if jump_intent_active and not _network_jump_intent_active:
_network_jump_pending = true
@ -2365,6 +2428,8 @@ func apply_network_animation_state(state: Dictionary) -> void:
func apply_authoritative_network_animation_action(
action_state: Dictionary,
) -> void:
if _water_recovery_active or _authoritative_relocation_input_quarantine:
return
if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
return
var incoming_sequence: int = int(action_state["sequence"])
@ -2438,6 +2503,13 @@ func apply_local_prediction_correction(
var parsed: Dictionary = _parse_network_snapshot(snapshot)
if parsed.is_empty():
return
# Water recovery owns the local transform until its reliable respawn
# teleport arrives. Ordinary movement audits may have been authored before
# the recovery presentation reached the host, so ignore every one of their
# fields while recovery is active.
if _water_recovery_active:
_reset_local_prediction_error()
return
var acknowledged_input: int = parsed["acknowledged_input"]
var sitting_intent_acknowledged: bool = (
_sitting_intent_pending
@ -2717,10 +2789,10 @@ static func resolve_local_prediction_transit_seconds(
)
func apply_network_teleport(snapshot: Dictionary) -> void:
func apply_network_teleport(snapshot: Dictionary) -> bool:
var parsed: Dictionary = _parse_network_snapshot(snapshot)
if parsed.is_empty():
return
return false
global_position = parsed["position"]
velocity = parsed["velocity"]
_visuals.rotation.y = parsed["visual_yaw"]
@ -2733,6 +2805,13 @@ func apply_network_teleport(snapshot: Dictionary) -> void:
_apply_network_target_animation_state(parsed["animation_state"])
_network_snapshot_age = 0.0
_network_snapshot_ready = true
return true
func reset_local_prediction_after_authoritative_teleport() -> void:
_clear_local_network_jump_intent()
_reset_local_prediction_error()
_clear_local_reconciliation_offsets()
func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
@ -3070,16 +3149,18 @@ func is_movement_enabled() -> bool:
return _movement_enabled
func set_local_input_suppressed(owner: StringName, suppressed: bool) -> void:
if owner.is_empty():
func set_local_input_suppressed(
suppressor_id: StringName, suppressed: bool
) -> void:
if suppressor_id.is_empty():
return
if suppressed:
_local_input_suppressors[owner] = true
_local_input_suppressors[suppressor_id] = true
velocity.x = 0.0
velocity.z = 0.0
_set_camera_dragging(false)
else:
_local_input_suppressors.erase(owner)
_local_input_suppressors.erase(suppressor_id)
func _is_movement_input_enabled() -> bool:
@ -3101,16 +3182,16 @@ func set_camera_input_enabled(enabled: bool) -> void:
func set_camera_input_suppressed(
owner: StringName,
suppressor_id: StringName,
suppressed: bool,
) -> void:
if owner.is_empty():
if suppressor_id.is_empty():
return
if suppressed:
_camera_input_suppressors[owner] = true
_camera_input_suppressors[suppressor_id] = true
_set_camera_dragging(false)
else:
_camera_input_suppressors.erase(owner)
_camera_input_suppressors.erase(suppressor_id)
func set_camera_active(active: bool) -> void:
@ -3198,6 +3279,8 @@ func is_camera_input_enabled() -> bool:
func set_water_recovery_active(active: bool) -> void:
_water_recovery_active = active
if active:
_clear_authoritative_network_motion_intent()
velocity = Vector3.ZERO
@ -3374,8 +3457,8 @@ func set_active_item_is_rod(
if _showcase_rod_state_stored:
_showcase_rod_visibility = active_is_rod
return
var visibility_changed: bool = _fishing_rod.visible != active_is_rod
if not animate_transition or not visibility_changed:
var rod_visibility_changed: bool = _fishing_rod.visible != active_is_rod
if not animate_transition or not rod_visibility_changed:
_fishing_rod.visible = active_is_rod
return
if _has_held_show_item() or _showcase_animation_active:

View file

@ -156,24 +156,24 @@ func _initialize_pool() -> void:
func _spawn_puff(source_position: Vector3, direction: Vector3) -> void:
var lateral := Vector3(-direction.z, 0.0, direction.x)
var position := (
var puff_position := (
source_position
- direction * PUFF_BACK_OFFSET
+ lateral * PUFF_SIDE_OFFSET * _foot_side
+ Vector3.UP * PUFF_HEIGHT
)
_spawn_puff_at(position)
_spawn_puff_at(puff_position)
_foot_side *= -1.0
func _spawn_puff_at(
position: Vector3,
puff_position: Vector3,
size_multiplier: float = 1.0,
) -> void:
var variation: float = _stable_variation(_spawn_serial)
var second_variation: float = _stable_variation(_spawn_serial + 37)
var index: int = _next_puff
_puff_positions[index] = position
_puff_positions[index] = puff_position
_puff_ages[index] = 0.0
_puff_widths[index] = (
lerpf(0.34, 0.43, variation) * size_multiplier

View file

@ -56,8 +56,8 @@ func _initialize() -> void:
quit(2)
return
for root: String in roots:
var normalized_root := root
for root_path: String in roots:
var normalized_root := root_path
if not normalized_root.ends_with("/"):
normalized_root += "/"
if not normalized_root.begins_with("res://") and not normalized_root.is_absolute_path():

View file

@ -101,7 +101,7 @@ func copy() -> PlayerSettings:
func normalize_implicit_presentation_layout() -> bool:
if presentation_layout_customized:
return false
var changed: bool = (
var layout_changed: bool = (
chat_dock_right != DEFAULT_CHAT_DOCK_RIGHT
or chat_mobile_mode != DEFAULT_CHAT_MOBILE_MODE
or paint_dock_right != DEFAULT_PAINT_DOCK_RIGHT
@ -109,7 +109,7 @@ func normalize_implicit_presentation_layout() -> bool:
chat_dock_right = DEFAULT_CHAT_DOCK_RIGHT
chat_mobile_mode = DEFAULT_CHAT_MOBILE_MODE
paint_dock_right = DEFAULT_PAINT_DOCK_RIGHT
return changed
return layout_changed
static func _is_valid_audio_volume(value: float) -> bool:

View file

@ -44,18 +44,20 @@ func _init() -> void:
assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
assert(_has_joypad_button(&"open_chat", JOY_BUTTON_BACK))
assert(_has_joypad_button(&"focus_gameplay", JOY_BUTTON_LEFT_SHOULDER))
assert(
GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS == JOY_AXIS_TRIGGER_RIGHT
)
assert(
var virtual_mouse_trigger_axis: int = GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS
var virtual_mouse_shared_trigger_axis: int = (
GameUIType.VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS
== JOY_AXIS_TRIGGER_LEFT
)
assert(
var virtual_mouse_secondary_click_axis: int = (
GameUIType.VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS
== JOY_AXIS_TRIGGER_LEFT
)
assert(PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS == JOY_AXIS_TRIGGER_LEFT)
var controller_zoom_trigger_axis: int = (
PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS
)
assert(virtual_mouse_trigger_axis == JOY_AXIS_TRIGGER_RIGHT)
assert(virtual_mouse_shared_trigger_axis == JOY_AXIS_TRIGGER_LEFT)
assert(virtual_mouse_secondary_click_axis == JOY_AXIS_TRIGGER_LEFT)
assert(controller_zoom_trigger_axis == JOY_AXIS_TRIGGER_LEFT)
assert(is_zero_approx(
GameUIType.normalized_trigger_strength(-1.0, -1.0)
))

View file

@ -112,7 +112,9 @@ func _validate_mesh_weights(mesh_instance: MeshInstance3D) -> int:
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
assert(not weights.is_empty())
assert(not vertices.is_empty())
var components_per_vertex := weights.size() / vertices.size()
var components_per_vertex: int = int(
float(weights.size()) / float(vertices.size())
)
assert(components_per_vertex == 4 or components_per_vertex == 8)
for offset in range(0, weights.size(), components_per_vertex):
var total := 0.0

View file

@ -367,13 +367,13 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
if str(binding.get("kind", "")) == "button":
var button := InputEventJoypadButton.new()
button.device = manager.get_active_device_id()
button.button_index = int(binding.get("button", -1))
button.button_index = int(binding.get("button", -1)) as JoyButton
button.pressed = true
manager.controller_input_observed.emit(button)
else:
var motion := InputEventJoypadMotion.new()
motion.device = manager.get_active_device_id()
motion.axis = int(binding.get("axis", -1))
motion.axis = int(binding.get("axis", -1)) as JoyAxis
motion.axis_value = float(binding.get("direction", -1.0))
manager.controller_input_observed.emit(motion)
if panel._auto_map_active:

View file

@ -2015,7 +2015,7 @@ func _assert_neighbor(
% [
origin.name,
property,
actual.name if actual != null else "nothing",
str(actual.name) if actual != null else "nothing",
expected.name,
],
)

View file

@ -21,10 +21,16 @@ func _run() -> void:
assert(not bool(defaults.get("public_listing")))
assert(not bool(defaults.get("chat_logging")))
assert(str(defaults.get("chat_log_path")).is_empty())
assert(NetworkChatService.BURST_COUNT == 3)
assert(NetworkChatService.WINDOW_COUNT == 5)
assert(is_equal_approx(NetworkChatService.WINDOW_SECONDS, 10.0))
assert(NetworkChatService.CALL_COOLDOWN_MILLISECONDS == 90)
var chat_burst_count: int = NetworkChatService.BURST_COUNT
var chat_window_count: int = NetworkChatService.WINDOW_COUNT
var chat_window_seconds: float = NetworkChatService.WINDOW_SECONDS
var chat_call_cooldown_milliseconds: int = (
NetworkChatService.CALL_COOLDOWN_MILLISECONDS
)
assert(chat_burst_count == 3)
assert(chat_window_count == 5)
assert(is_equal_approx(chat_window_seconds, 10.0))
assert(chat_call_cooldown_milliseconds == 90)
var path: String = ProjectSettings.globalize_path(
"user://dedicated-server-validation.cfg"
@ -156,14 +162,17 @@ func _run() -> void:
str(discovery.call("_default_room_name", "River"))
== "River's Server"
)
assert(
var upnp_renew_interval_seconds: float = (
DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
< float(DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS)
)
assert(
var upnp_mapping_duration_seconds: float = float(
DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS
)
var upnp_retry_interval_seconds: float = (
DiscoveryClient.UPNP_RETRY_INTERVAL_SECONDS
< DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
)
assert(upnp_renew_interval_seconds < upnp_mapping_duration_seconds)
assert(upnp_retry_interval_seconds < upnp_renew_interval_seconds)
var discovery_settings_path: String = ProjectSettings.globalize_path(
DiscoveryClient.SETTINGS_PATH
)

View file

@ -76,7 +76,7 @@ func _run() -> void:
assert(session.set_host_open(false))
await _test_host_shop_purchase(main, player, shop_service)
await _test_host_rod_purchase(player, shop_service)
_test_host_rod_purchase(player, shop_service)
await _test_host_art_shop_purchase(main, player, shop_service)
await _test_fishing_shop_sale_ui(
main, player, catalog, sale_service, reservations

View file

@ -34,20 +34,20 @@ func _run() -> void:
var root_controls: Array[Control] = (
FileDialogControllerNavigationType.interactive_controls(root_scope)
)
var directory_list: ItemList
var parent_button: Button
var create_folder_button: Button
var path_edit: LineEdit
var drive_button: MenuButton
var refresh_button: Button
var favorite_button: Button
var hidden_button: Button
var grid_button: Button
var list_button: Button
var filter_button: Button
var sort_button: MenuButton
var cancel_button: Button
var select_button: Button
var directory_list: ItemList = null
var parent_button: Button = null
var create_folder_button: Button = null
var path_edit: LineEdit = null
var drive_button: MenuButton = null
var refresh_button: Button = null
var favorite_button: Button = null
var hidden_button: Button = null
var grid_button: Button = null
var list_button: Button = null
var filter_button: Button = null
var sort_button: MenuButton = null
var cancel_button: Button = null
var select_button: Button = null
for control: Control in root_controls:
if (
control is ItemList

View file

@ -200,15 +200,17 @@ func _run() -> void:
var invalid_state: Dictionary = valid_state.duplicate(true)
invalid_state["display_scale"] = 1000.0
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
var enet_channel_count: int = NetworkProtocol.ENET_CHANNEL_COUNT
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
var showcase_capability: StringName = NetworkFishShowcaseProtocol.CAPABILITY
assert(protocol_version == 12)
assert(enet_channel_count == 18)
assert(
NetworkProtocol.FISH_QUALITY_CAPABILITY
== "fish_quality_v1"
fish_quality_capability == "fish_quality_v1"
)
assert(
NetworkFishShowcaseProtocol.CAPABILITY
== &"fish_showcase_v1"
showcase_capability == &"fish_showcase_v1"
)
print("Fish hotbar showcase validation: PASS")

View file

@ -26,18 +26,28 @@ func _run() -> void:
_validate_collection_mastery()
_validate_version_four_migration()
_validate_fishing_protocol_v2()
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11)
assert(
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
var enet_channel_count: int = NetworkProtocol.ENET_CHANNEL_COUNT
var movement_animation_channel: int = (
NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL
)
var movement_reconciliation_capability: String = (
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY
== "movement_reconciliation_v2"
)
var fishing_replication_capability: String = (
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
)
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
assert(protocol_version == 12)
assert(enet_channel_count == 18)
assert(movement_animation_channel == 11)
assert(
movement_reconciliation_capability == "movement_reconciliation_v2"
)
assert(
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
== "fishing_replication_v2"
fishing_replication_capability == "fishing_replication_v2"
)
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
assert(fish_quality_capability == "fish_quality_v1")
print("Fish quality validation: PASS")
quit()
@ -71,11 +81,17 @@ func _validate_fishing_protocol_v2() -> void:
NetworkFishingProtocol.validate_cast_request(invalid_sequence)
== "Fishing request values are outside allowed limits."
)
assert(NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL == 10)
assert(NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL == 15)
assert(
var observer_snapshot_channel: int = (
NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL
)
var world_spawn_snapshot_channel: int = (
NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL
!= NetworkFishingProtocol.SNAPSHOT_CHANNEL
)
var fishing_snapshot_channel: int = NetworkFishingProtocol.SNAPSHOT_CHANNEL
assert(observer_snapshot_channel == 10)
assert(world_spawn_snapshot_channel == 15)
assert(
world_spawn_snapshot_channel != fishing_snapshot_channel
)
@ -240,10 +256,14 @@ func _validate_barrier_challenge_curve() -> void:
func _validate_fight_pacing_and_reel_upgrades() -> void:
assert(is_equal_approx(CatchController.CHASE_SPEED, 0.07))
assert(is_equal_approx(CatchController.CHASE_START_DELAY, 1.5))
assert(is_equal_approx(CatchController.CHASE_START_OFFSET, 0.04))
assert(is_equal_approx(Player.BASE_REEL_SPEED, 0.16))
var chase_speed: float = CatchController.CHASE_SPEED
var chase_start_delay: float = CatchController.CHASE_START_DELAY
var chase_start_offset: float = CatchController.CHASE_START_OFFSET
var base_reel_speed: float = Player.BASE_REEL_SPEED
assert(is_equal_approx(chase_speed, 0.07))
assert(is_equal_approx(chase_start_delay, 1.5))
assert(is_equal_approx(chase_start_offset, 0.04))
assert(is_equal_approx(base_reel_speed, 0.16))
var upgrades := PlayerFishingUpgrades.new()
assert(is_equal_approx(upgrades.get_reel_speed_multiplier(), 1.0))

View file

@ -277,6 +277,28 @@ func _run() -> void:
water_recovery.call("_finish_recovery")
assert(player.is_movement_enabled())
# Session/world teardown can interrupt the fade before its normal completion.
# Recovery must still release FishingSpot's external input lock without
# restoring movement that gameplay shutdown already disabled.
water_recovery.call(
"_on_recovery_requested",
player,
player.global_position.y,
)
assert(water_recovery.is_recovery_active())
var screen_fade := game_ui.get_screen_fade() as ScreenFade
assert(screen_fade != null)
screen_fade.fade_to_black(int(water_recovery.get("_generation")))
water_recovery.set_recovery_enabled(false)
assert(not water_recovery.is_recovery_active())
assert(not player.is_water_recovery_active())
assert(not bool(fishing_spot.get("_external_input_blocked")))
assert(not player.is_movement_enabled())
assert(not screen_fade.visible)
water_recovery.set_recovery_enabled(true)
player.set_movement_enabled(true)
player.set_camera_input_enabled(true)
print("Fishing authority validation: PASS")
session.disconnect_session("")
main.queue_free()

View file

@ -271,7 +271,6 @@ func _run_client() -> void:
assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0)
for _frame: int in 2:
await physics_frame
var expected_position: Vector3 = player.global_position
var expected_direction: Vector3 = player.get_facing_direction()
player.set_casting_visual()

View file

@ -59,11 +59,11 @@ func _run() -> void:
wall.queue_free()
await process_frame
var floor := _make_surface(
var floor_surface := _make_surface(
Vector3(10.0, 0.2, 10.0),
Vector3(0.0, -0.1, -1.7),
)
root.add_child(floor)
root.add_child(floor_surface)
await physics_frame
controller.call("_update_marker_target")
assert(bool(controller.get("_marker_has_surface")))
@ -80,7 +80,7 @@ func _run() -> void:
)
)
floor.queue_free()
floor_surface.queue_free()
controller.queue_free()
player.queue_free()
await process_frame

View file

@ -18,6 +18,12 @@ const Gatherables: GatherableCatalog = preload(
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const FOLIAGE_WIND_SHADER: Shader = preload(
"res://world/materials/foliage_wind.gdshader"
)
const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = (
&"foliage_wind_source_materials"
)
const FIRST_SEED := 13001
const SECOND_SEED := 13002
const RIVER_FALLBACK_SEED := 13012
@ -130,8 +136,8 @@ func _validate_generated_region(
var placements := generator.placement_keys()
var expected_chunk_count := generator.grid_size.x * generator.grid_size.y
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
var center_index := center.y * generator.grid_size.x + center.x
assert(generator.grid_size == Vector2i(20, 20))
@ -341,7 +347,7 @@ func _validate_generated_region(
):
var outlet_coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
int(float(index) / float(generator.grid_size.x)),
)
outlet_coordinates.append(outlet_coordinate)
assert(
@ -366,7 +372,7 @@ func _validate_generated_region(
for index: int in placements.size():
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
int(float(index) / float(generator.grid_size.x)),
)
if placements[index].begins_with("chunk_0001@"):
assert(generator._distance_from_map_boundary(coordinate) <= 1)
@ -587,8 +593,8 @@ func _validate_generated_region(
Vector2i.ZERO,
)
var spawn_coordinate := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
var spawn_distance := (
absi(coordinate.x - spawn_coordinate.x)
@ -1146,8 +1152,8 @@ func _validate_biome_catalog(
for biome_id: StringName in EXPECTED_BIOME_IDS:
assert(counts.get(biome_id, 0) > 0)
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
assert(region.get_biome_at(center) == &"biome_plains")
for child: Node in generator.get_generated_chunks_root().get_children():
@ -1504,12 +1510,27 @@ func _has_material_variant_override(
return false
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
var source_materials: Dictionary = mesh_instance.get_meta(
FOLIAGE_WIND_SOURCE_MATERIALS_META,
{},
)
for surface_index: int in mesh_instance.mesh.get_surface_count():
var override := mesh_instance.get_surface_override_material(
surface_index
)
if override != null and override in variants:
return true
var wind_material := override as ShaderMaterial
if (
wind_material != null
and wind_material.shader == FOLIAGE_WIND_SHADER
):
var source_material: Material = source_materials.get(
surface_index,
null,
) as Material
if source_material != null and source_material in variants:
return true
for child: Node in root_node.get_children():
if _has_material_variant_override(child, variants):
return true
@ -1647,7 +1668,7 @@ func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:
return result
func _validate_projected_terrain_materials(root: Node) -> void:
func _validate_projected_terrain_materials(projected_root: Node) -> void:
var expected_sizes := {
"grass_lite": 1.75,
"sand": 2.6,
@ -1658,17 +1679,21 @@ func _validate_projected_terrain_materials(root: Node) -> void:
"sand": 0,
"dirt": 0,
}
_validate_projected_material_node(root, expected_sizes, surface_counts)
_validate_projected_material_node(
projected_root,
expected_sizes,
surface_counts,
)
for material_name: String in expected_sizes:
assert(surface_counts[material_name] > 0)
func _validate_projected_material_node(
root: Node,
node: Node,
expected_sizes: Dictionary,
surface_counts: Dictionary,
) -> void:
var mesh_instance := root as MeshInstance3D
var mesh_instance := node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
for surface_index: int in mesh_instance.mesh.get_surface_count():
var material := mesh_instance.get_active_material(surface_index)
@ -1689,7 +1714,7 @@ func _validate_projected_material_node(
)
)
surface_counts[material.resource_name] += 1
for child: Node in root.get_children():
for child: Node in node.get_children():
_validate_projected_material_node(
child,
expected_sizes,
@ -1708,8 +1733,10 @@ func _validate_pond_collision(
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
var triangle_count := (
indices.size() / 3 if not indices.is_empty() else vertices.size() / 3
var triangle_count: int = (
int(float(indices.size()) / 3.0)
if not indices.is_empty()
else int(float(vertices.size()) / 3.0)
)
for triangle_index: int in triangle_count:
var offset := triangle_index * 3

View file

@ -678,7 +678,7 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
player_menu.call("_show_section_immediate", sections[index])
await process_frame
await process_frame
await _save_capture(suffixes[index])
_save_capture(suffixes[index])
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
var confirmation_message := player_menu.get_node(
@ -693,7 +693,7 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
sale_confirmation.visible = true
await process_frame
await process_frame
await _save_capture("sale-confirmation")
_save_capture("sale-confirmation")
sale_confirmation.visible = false

View file

@ -38,16 +38,18 @@ func _run() -> void:
KeyboardMouseMappingManagerType.ROLE_INTERACT
) == "e"
)
assert(
var zoom_in_label: String = str(
KeyboardMouseMappingManagerType.ROLE_LABELS[
KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_IN
] == "hotbar select (+SHIFT to zoom)"
]
)
assert(
var zoom_out_label: String = str(
KeyboardMouseMappingManagerType.ROLE_LABELS[
KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_OUT
] == "hotbar select (+SHIFT to zoom)"
]
)
assert(zoom_in_label == "hotbar select (+SHIFT to zoom)")
assert(zoom_out_label == "hotbar select (+SHIFT to zoom)")
assert(
str(defaults[
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)

View file

@ -293,25 +293,25 @@ func _wait_for_avatar_sitting(
var deadline: int = Time.get_ticks_msec() + 10000
while Time.get_ticks_msec() < deadline:
await process_frame
var avatar: Player = spawn_service.get_avatar(peer_id)
if avatar != null and avatar.is_sitting() == expected:
var observed_avatar: Player = spawn_service.get_avatar(peer_id)
if observed_avatar != null and observed_avatar.is_sitting() == expected:
return true
var avatar: Player = spawn_service.get_avatar(peer_id)
if avatar != null:
var timed_out_avatar: Player = spawn_service.get_avatar(peer_id)
if timed_out_avatar != null:
print(
"sitting wait timed out: ",
{
"peer_id": peer_id,
"expected": expected,
"sitting": avatar.is_sitting(),
"sit_after_landing": bool(avatar.get("_sit_after_landing")),
"on_floor": avatar.is_on_floor(),
"position": avatar.global_position,
"velocity": avatar.velocity,
"authoritative": bool(avatar.get(
"sitting": timed_out_avatar.is_sitting(),
"sit_after_landing": bool(timed_out_avatar.get("_sit_after_landing")),
"on_floor": timed_out_avatar.is_on_floor(),
"position": timed_out_avatar.global_position,
"velocity": timed_out_avatar.velocity,
"authoritative": bool(timed_out_avatar.get(
"_network_authoritative_simulation"
)),
"last_input": int(avatar.get(
"last_input": int(timed_out_avatar.get(
"_last_network_input_sequence"
)),
},

View file

@ -217,11 +217,11 @@ func _validate_new_game_music_transition(main: Node) -> void:
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
var root := main.get_node(
var fresh_water_root := main.get_node(
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
) as Node3D
assert(root != null and root.get_child_count() > 0)
return root.get_child(0).get_node("VisualWater") as MeshInstance3D
assert(fresh_water_root != null and fresh_water_root.get_child_count() > 0)
return fresh_water_root.get_child(0).get_node("VisualWater") as MeshInstance3D
func _validate_ocean_fishing_coverage(

View file

@ -545,7 +545,7 @@ func _validate_page() -> void:
== "flathead\ncatfish"
)
var shared_material: Material
var shared_material: Material = null
var silhouette_count: int = 0
var target_silhouette_area: float = (
LogbookPage.CATALOG_PORTRAIT_SIZE.x
@ -558,9 +558,9 @@ func _validate_page() -> void:
]:
page.call("_select_category", category)
await create_timer(0.25).timeout
var entries: Dictionary = page.get("_entry_buttons")
var category_entries: Dictionary = page.get("_entry_buttons")
assert(
entries.size()
category_entries.size()
== (108 if category == LogbookCatalog.Category.FRESH_WATER else 202)
)
for fish: FishDataType in LogbookCatalog.ordered_species(
@ -571,7 +571,7 @@ func _validate_page() -> void:
var unknown_key := StringName(
"unknown_%d" % LogbookCatalog.catalog_number(fish)
)
var entry := entries.get(unknown_key) as Button
var entry := category_entries.get(unknown_key) as Button
assert(entry != null)
assert(entry.text.is_empty())
assert(entry.tooltip_text.is_empty())

View file

@ -34,6 +34,7 @@ func _validate_latency_smoothing() -> void:
avatar.set_physics_process(false)
_validate_compact_input_encoding()
_validate_compact_snapshot_encoding()
_validate_authoritative_teleport_snapshot_guard()
_validate_compact_animation_encoding()
_validate_animation_action_ordering(avatar)
_validate_transit_estimation()
@ -72,8 +73,12 @@ func _validate_compact_snapshot_encoding() -> void:
Vector3(4.5, 0.0, 0.0),
1,
)
assert(NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE == 8)
assert(NetworkSession.OWNER_SNAPSHOT_DIVISOR == 3)
var movement_snapshot_batch_size: int = (
NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE
)
var owner_snapshot_divisor: int = NetworkSession.OWNER_SNAPSHOT_DIVISOR
assert(movement_snapshot_batch_size == 8)
assert(owner_snapshot_divisor == 3)
var encoded_snapshots: Array = []
for _peer: int in NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE:
encoded_snapshots.append(
@ -100,6 +105,50 @@ func _validate_compact_snapshot_encoding() -> void:
)
func _validate_authoritative_teleport_snapshot_guard() -> void:
var session := NetworkSession.new()
var destination := Vector3(24.0, 0.0, -8.0)
session.set("_local_authoritative_teleport_guard_active", true)
session.set("_local_authoritative_teleport_guard_position", destination)
session.set("_local_authoritative_teleport_minimum_ack", 48)
# A channel-two audit from before the reliable relocation must not move the
# local player back to its old position.
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 48),
)))
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination, Vector3.ZERO, 47),
)))
# A matching audit from the new location clears the barrier. A higher
# acknowledgement at the old position is not sufficient: movement input and
# the reliable teleport travelled on different channels, so it can still be
# a held packet from before recovery.
assert(not bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination + Vector3.RIGHT, Vector3.ZERO, 48),
)))
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
session.set("_local_authoritative_teleport_guard_active", true)
session.set("_local_authoritative_teleport_guard_position", destination)
session.set("_local_authoritative_teleport_minimum_ack", 48)
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 49),
)))
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
assert(not bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination, Vector3.ZERO, 49),
)))
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
session.free()
func _validate_compact_animation_encoding() -> void:
var paused_state := NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
@ -315,16 +364,16 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
# Reconciliation only applies after both simulations report a grounded body.
# Give the unit avatar a real floor so this exercises the production path.
var floor := StaticBody3D.new()
floor.collision_layer = 1
floor.collision_mask = 0
floor.position = Vector3(0.0, -0.1, 0.0)
var floor_body := StaticBody3D.new()
floor_body.collision_layer = 1
floor_body.collision_mask = 0
floor_body.position = Vector3(0.0, -0.1, 0.0)
var floor_shape := CollisionShape3D.new()
var floor_box := BoxShape3D.new()
floor_box.size = Vector3(100.0, 0.2, 100.0)
floor_shape.shape = floor_box
floor.add_child(floor_shape)
root.add_child(floor)
floor_body.add_child(floor_shape)
root.add_child(floor_body)
await physics_frame
avatar.set_local_control(true)
avatar.global_position = Vector3(0.8, 0.0, 0.0)
@ -434,6 +483,24 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
int(avatar.get("_local_prediction_hard_corrections")) == 1
)
# Recovery owns all local state until its reliable teleport arrives. A
# delayed audit must neither pull the body back nor acknowledge an old jump.
avatar.global_position = Vector3(10.0, 0.0, 0.0)
avatar.call("_queue_local_network_jump_intent")
avatar.capture_network_input(20)
avatar.set_water_recovery_active(true)
avatar.apply_local_prediction_correction(
drift_snapshot,
20,
1.0 / 30.0,
0.0,
0.1,
)
assert(avatar.global_position == Vector3(10.0, 0.0, 0.0))
assert(bool(avatar.get("_local_network_jump_intent_pending")))
avatar.set_water_recovery_active(false)
avatar.call("_clear_local_network_jump_intent")
# Soft reconciliation must respect the same terrain collision as ordinary
# player movement instead of phasing the capsule through a solid obstacle.
var blocker := StaticBody3D.new()
@ -470,7 +537,7 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
== blocked_camera_position
)
blocker.queue_free()
floor.queue_free()
floor_body.queue_free()
await physics_frame
@ -567,6 +634,40 @@ func _validate_stale_input_expiry(avatar: Player) -> void:
assert(not bool(avatar.get("_network_input_stale")))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
# The reliable recovery request carries the last client-created sequence.
# A host uses it as a hard watermark, so every delayed input from before the
# request is discarded even if it arrives after the teleport RPC.
avatar.neutralize_authoritative_input_for_relocation(44)
assert(int(avatar.get("_last_network_input_sequence")) == 44)
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(not bool(avatar.get("_network_sprint")))
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(41, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
assert(int(avatar.get("_last_network_input_sequence")) == 44)
# A non-neutral post-watermark packet cannot restart motion. The first
# neutral packet past the watermark releases the barrier; the following
# input is the first one allowed to move the recovered avatar.
avatar.apply_authoritative_network_input(_movement_input(45, false, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(46, false, false))
assert(not bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(47, false, true))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
# Recovery itself also consumes incoming movement rather than remembering a
# held axis which could resume on the exact frame recovery presentation ends.
avatar.set_water_recovery_active(true)
avatar.apply_authoritative_network_input(_movement_input(48, false, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
avatar.set_water_recovery_active(false)
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
avatar.apply_authoritative_network_input(_movement_input(49, false, true))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
func _network_snapshot(
position: Vector3,

View file

@ -307,13 +307,13 @@ func _validate_native_keyboard() -> void:
await process_frame
edit.grab_focus()
assert(keyboard.is_open())
var focus_exit_count: int = 0
var focus_exit_state: Dictionary = {"count": 0}
edit.focus_exited.connect(func() -> void:
focus_exit_count += 1
focus_exit_state["count"] = int(focus_exit_state["count"]) + 1
)
assert(keyboard.request_for_control(edit))
assert(edit.has_focus())
assert(focus_exit_count == 0)
assert(int(focus_exit_state["count"]) == 0)
assert(not edit.virtual_keyboard_enabled)
assert(not edit.virtual_keyboard_show_on_focus)
assert(keyboard.shown_controls == [edit])

View file

@ -19,7 +19,7 @@ func _run() -> void:
assert(not vertices.is_empty())
assert(vertices.size() % 2 == 0)
assert(indices.size() == vertices.size() * 3)
var point_count := vertices.size() / 2
var point_count: int = int(float(vertices.size()) / 2.0)
var land_edge := PackedVector2Array()
var water_edge := PackedVector2Array()
for index: int in point_count:

View file

@ -40,13 +40,16 @@ func _validate_palette() -> void:
func _validate_protocol_bounds() -> void:
assert(SurfaceDrawingProtocol.GRID_WIDTH == 16)
assert(SurfaceDrawingProtocol.GRID_HEIGHT == 16)
assert(SurfaceDrawingProtocol.GRID_SIZES == [16, 32, 64, 128])
assert(is_equal_approx(SurfaceDrawingProtocol.CELL_SIZE, 0.075))
var grid_width: int = SurfaceDrawingProtocol.GRID_WIDTH
var grid_height: int = SurfaceDrawingProtocol.GRID_HEIGHT
var grid_sizes: Variant = SurfaceDrawingProtocol.GRID_SIZES
var cell_size: float = SurfaceDrawingProtocol.CELL_SIZE
assert(grid_width == 16)
assert(grid_height == 16)
assert(grid_sizes == [16, 32, 64, 128])
assert(is_equal_approx(cell_size, 0.075))
assert(is_equal_approx(
float(SurfaceDrawingProtocol.GRID_WIDTH)
* SurfaceDrawingProtocol.CELL_SIZE,
float(grid_width) * cell_size,
1.2,
))
var canvas_request: Dictionary = {
@ -157,14 +160,14 @@ func _validate_grid_snapping() -> void:
"creator_fingerprint": FINGERPRINT_A,
"cells": [],
}
var snapped: Dictionary = SurfaceDrawingPlacement.resolve(
var snap_result: Dictionary = SurfaceDrawingPlacement.resolve(
Vector3(1.08, 0.02, 0.08),
Vector3.UP,
Vector3.RIGHT,
[anchor],
)
assert(bool(snapped["snapped"]))
var snapped_origin: Vector3 = snapped["origin"]
assert(bool(snap_result["snapped"]))
var snapped_origin: Vector3 = snap_result["origin"]
assert(snapped_origin.is_equal_approx(Vector3(1.2, 0.0, 0.0)))
var unsnapped: Dictionary = SurfaceDrawingPlacement.resolve(
Vector3(0.6, 0.0, 0.0),

View file

@ -1878,7 +1878,7 @@ func _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
for index: int in generator._placements.size():
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
int(float(index) / float(generator.grid_size.x)),
)
var current: TerrainChunkVariant = generator._placements[index]
if coordinate.x > 0:
@ -1918,8 +1918,8 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
"All walkable generated terrain must remain connected to spawn.",
)
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
var safe_spawn_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -1943,7 +1943,7 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
var placement := generator._placements[index]
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
int(float(index) / float(generator.grid_size.x)),
)
if "coast" in placement.definition.tags:
_check(

View file

@ -19,15 +19,15 @@ func _initialize() -> void:
func _run() -> void:
var root := Node.new()
get_root().add_child(root)
var scene_root := Node.new()
get_root().add_child(scene_root)
var bag := PlayerBag.new()
var catches := FishInventory.new()
var capacity := PlayerCoolerCapacity.new()
var layout := PlayerInventoryLayout.new()
var hotbar := PlayerHotbar.new()
for node: Node in [bag, catches, capacity, layout, hotbar]:
root.add_child(node)
scene_root.add_child(node)
bag.setup(ItemCatalogResource)
layout.setup(bag, catches, ItemCatalogResource, capacity)
bag.set_inventory_layout(layout)
@ -109,7 +109,7 @@ func _run() -> void:
assert(layout.get_storage_count() == 1)
assert(layout.get_storage_capacity() == 9)
var inventory_grid := GeneralInventoryGrid.new()
root.add_child(inventory_grid)
scene_root.add_child(inventory_grid)
inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
inventory_grid.setup(
layout,
@ -143,7 +143,7 @@ func _run() -> void:
)
staged_slot.set_staged(false)
var quality_slot := GeneralInventorySlot.new()
root.add_child(quality_slot)
scene_root.add_child(quality_slot)
quality_slot.set_presentation_size(Vector2(78.0, 78.0))
quality_slot.configure(
0,
@ -181,7 +181,7 @@ func _run() -> void:
distinct_quality_colors[quality_color] = true
assert(distinct_quality_colors.size() == FishQuality.TIER_COUNT)
var sale_tray_slot := ShopSaleTraySlot.new()
root.add_child(sale_tray_slot)
scene_root.add_child(sale_tray_slot)
sale_tray_slot.configure(
"catch:%s" % fish_catch.catch_id,
fish_catch.fish.display_texture,
@ -207,7 +207,7 @@ func _run() -> void:
expected_tray_quality_color.a = 0.96
assert(sale_tray_style.bg_color == expected_tray_quality_color)
var wallet := PlayerWallet.new()
root.add_child(wallet)
scene_root.add_child(wallet)
assert(wallet.restore_balance(15000))
assert(layout.get_next_backpack_cost() == 1500)
assert(layout.purchase_backpack(wallet))
@ -223,7 +223,7 @@ func _run() -> void:
assert(inventory_grid.get_slots().size() == 36)
assert(layout.get_next_backpack_cost() == -1)
var storage_grid := GeneralInventoryGrid.new()
root.add_child(storage_grid)
scene_root.add_child(storage_grid)
storage_grid.setup(
layout,
bag,
@ -240,7 +240,7 @@ func _run() -> void:
== "locked storage slot 10"
)
var storage_wallet := PlayerWallet.new()
root.add_child(storage_wallet)
scene_root.add_child(storage_wallet)
assert(storage_wallet.restore_balance(9500))
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
assert(capacity.purchase(storage_wallet))
@ -266,8 +266,8 @@ func _run() -> void:
var network_sale := NetworkSaleService.new()
var session := NetworkSession.new()
root.add_child(session)
root.add_child(network_sale)
scene_root.add_child(session)
scene_root.add_child(network_sale)
network_sale.set("_session", session)
network_sale.set("_item_catalog", ItemCatalogResource)
network_sale.set("_fish_catalog", FishCatalogResource)
@ -284,5 +284,5 @@ func _run() -> void:
assert((result.get("items", []) as Array).size() == 1)
print("Unified inventory validation: PASS")
root.free()
scene_root.free()
quit()

View file

@ -68,9 +68,9 @@ func _validate_setup_page() -> void:
page.call("_set_world_layout", WorldLayout.STARTER_ISLAND)
assert(not (page.get_node("%SeedSection") as Control).visible)
var request: Dictionary = {}
page.start_requested.connect(func(layout: StringName, seed: int) -> void:
page.start_requested.connect(func(layout: StringName, world_seed: int) -> void:
request["layout"] = layout
request["seed"] = seed
request["seed"] = world_seed
)
page.call("_request_start")
assert(request.get("layout") == WorldLayout.STARTER_ISLAND)

View file

@ -20,12 +20,21 @@ func _initialize() -> void:
func _run() -> void:
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
assert(
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
var world_spawn_capability: String = (
NetworkWorldSpawnProtocol.CAPABILITY
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
)
assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
var protocol_world_spawn_capability: String = (
NetworkProtocol.WORLD_SPAWN_CAPABILITY
)
var snapshot_entities_per_envelope: int = (
NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE
)
assert(protocol_version == 12)
assert(
world_spawn_capability == protocol_world_spawn_capability
)
assert(snapshot_entities_per_envelope <= 4)
_validate_catalog_statuses()
await _validate_billboard_presentation()
_validate_envelopes()

View file

@ -42,9 +42,15 @@ func _run() -> void:
func _validate_clock_boundaries_and_duration() -> void:
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 86400.0)
var real_seconds_per_cycle: float = (
WorldTimeServiceType.REAL_SECONDS_PER_CYCLE
)
var hours_per_real_second: float = (
WorldTimeServiceType.HOURS_PER_REAL_SECOND
)
assert(real_seconds_per_cycle == 86400.0)
assert(is_equal_approx(
WorldTimeServiceType.HOURS_PER_REAL_SECOND,
hours_per_real_second,
1.0 / 3600.0,
))
assert(is_equal_approx(

View file

@ -382,11 +382,13 @@ func _validate_weather_presentation() -> void:
"res://world/environment/local_storm_cloud.gdshader"
)
assert("cull_disabled" in cloud_shader.code)
assert(LocalStormCloudLayer.WRAP_RADIUS >= 315.0)
assert(is_equal_approx(LocalStormCloudLayer.MAXIMUM_OPACITY, 1.0))
var cloud_wrap_radius: float = LocalStormCloudLayer.WRAP_RADIUS
var cloud_maximum_opacity: float = LocalStormCloudLayer.MAXIMUM_OPACITY
var cloud_distance_fade_end: float = LocalStormCloudLayer.DISTANCE_FADE_END
assert(cloud_wrap_radius >= 315.0)
assert(is_equal_approx(cloud_maximum_opacity, 1.0))
assert(
LocalStormCloudLayer.DISTANCE_FADE_END
< LocalStormCloudLayer.WRAP_RADIUS
cloud_distance_fade_end < cloud_wrap_radius
)
assert(is_zero_approx(float(storm_clouds.call("get_storm_amount"))))
visuals.apply_weather_immediately(
@ -524,10 +526,18 @@ func _validate_weather_presentation() -> void:
assert(rain.emitting)
assert(rain.amount_ratio > 0.99)
assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT)
var rain_emitter_offset: Vector3 = (
WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET
)
var rain_supported_canopy_height: float = (
WorldTimeVisualControllerType.RAIN_SUPPORTED_CANOPY_HEIGHT
)
var rain_canopy_clearance: float = (
WorldTimeVisualControllerType.RAIN_CANOPY_CLEARANCE
)
assert(
WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET.y
>= WorldTimeVisualControllerType.RAIN_SUPPORTED_CANOPY_HEIGHT
+ WorldTimeVisualControllerType.RAIN_CANOPY_CLEARANCE
rain_emitter_offset.y
>= rain_supported_canopy_height + rain_canopy_clearance
)
assert(is_equal_approx(
rain.lifetime,

View file

@ -1463,8 +1463,8 @@ func is_mobile_mode() -> bool:
return _mobile_mode
func set_hud_hidden(hidden: bool) -> void:
_hud_hidden = hidden
func set_hud_hidden(is_hidden: bool) -> void:
_hud_hidden = is_hidden
_refresh_visibility()

View file

@ -53,7 +53,9 @@ static func make_icon_texture(
Image.FORMAT_RGBA8,
)
canvas.fill(Color.TRANSPARENT)
var inset := Vector2i.ONE * ((canvas_size - icon_size) / 2)
var inset := Vector2i.ONE * floori(
float(canvas_size - icon_size) / 2.0
)
canvas.blend_rect(
image,
Rect2i(Vector2i.ZERO, Vector2i(icon_size, icon_size)),

View file

@ -93,10 +93,16 @@ static func uniform_footprint_size(
maximum_size.x / visible_size.x,
maximum_size.y / visible_size.y,
)
var scale: float = minf(area_scale, fit_scale)
var footprint_scale: float = minf(area_scale, fit_scale)
return Vector2(
minf(maximum_size.x, maxf(1.0, roundf(visible_size.x * scale))),
minf(maximum_size.y, maxf(1.0, roundf(visible_size.y * scale))),
minf(
maximum_size.x,
maxf(1.0, roundf(visible_size.x * footprint_scale)),
),
minf(
maximum_size.y,
maxf(1.0, roundf(visible_size.y * footprint_scale)),
),
)

View file

@ -91,14 +91,14 @@ func _initialize_motion() -> void:
_set_visual_y(_target_y())
func _on_toggled(is_pressed: bool) -> void:
if _selected and not is_pressed:
func _on_toggled(is_button_pressed: bool) -> void:
if _selected and not is_button_pressed:
# A selected organizer tab is a page marker, not a collapsible toggle.
# Restore without another signal before any lower-state frame is drawn.
set_pressed_no_signal(true)
refresh_state(false)
return
_selected = is_pressed
_selected = is_button_pressed
refresh_state()

View file

@ -65,14 +65,14 @@ func _apply_style() -> void:
func configure(
key: String,
icon: Texture2D,
icon_texture: Texture2D,
label: String,
quantity: int = 1,
quality: int = -1,
) -> void:
entry_key = key
_quality_tier = quality
_icon.texture = icon
_icon.texture = icon_texture
_quantity.text = "×%d" % quantity if quantity > 1 else ""
tooltip_text = "%s · select to remove" % label
accessibility_name = tooltip_text

View file

@ -565,7 +565,7 @@ func _popup_item_height(popup: PopupMenu, index: int) -> float:
popup.get_theme_font_size(&"font_size")
)
var icon: Texture2D = popup.get_item_icon(index)
var icon_height: float = icon.get_height() if icon != null else 0.0
var icon_height: float = float(icon.get_height()) if icon != null else 0.0
return maxf(font_height, icon_height) + vertical_separation

View file

@ -69,7 +69,9 @@ const CURRENCY_ICON: Texture2D = preload(
signal menu_visibility_changed(is_open: bool)
signal menu_exit_started
@warning_ignore("unused_signal")
signal sell_fish_requested
@warning_ignore("unused_signal")
signal shop_cooler_return_requested
signal shop_cooler_confirmation_cancel_requested
@ -395,9 +397,11 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
_controller_zone = ControllerZone.CONTENT
if section_index == int(_shop_section):
if section_index != ShopSection.SELL_FISH:
var showing_upgrades := section_index == ShopSection.UPGRADES
_upgrades_content.visible = showing_upgrades
_supplies_content.visible = not showing_upgrades
var showing_current_upgrades: bool = (
section_index == ShopSection.UPGRADES
)
_upgrades_content.visible = showing_current_upgrades
_supplies_content.visible = not showing_current_upgrades
_update_shop_tab_selection()
if focus_content:
_focus_shop_section()
@ -470,12 +474,12 @@ func _apply_shop_controller_zone_focus_modes() -> void:
func _set_descendant_button_focus_mode(
root_control: Control,
focus_mode: Control.FocusMode,
requested_focus_mode: Control.FocusMode,
) -> void:
for child: Node in root_control.find_children("*", "BaseButton", true, false):
var button := child as BaseButton
if button != null:
button.focus_mode = focus_mode
button.focus_mode = requested_focus_mode
func _update_shop_tab_selection() -> void:
@ -704,14 +708,14 @@ func deactivate_shop_cooler_page() -> void:
func _set_shop_panel_background_pointer_blocking(blocking: bool) -> void:
var mouse_filter := (
var panel_mouse_filter := (
Control.MOUSE_FILTER_STOP
if blocking
else Control.MOUSE_FILTER_IGNORE
)
_shop_panel.mouse_filter = mouse_filter
_shop_panel_margin.mouse_filter = mouse_filter
_shop_panel_layout.mouse_filter = mouse_filter
_shop_panel.mouse_filter = panel_mouse_filter
_shop_panel_margin.mouse_filter = panel_mouse_filter
_shop_panel_layout.mouse_filter = panel_mouse_filter
func set_shop_cooler_modal_open(is_open: bool) -> void:

View file

@ -204,6 +204,7 @@ var _hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _player_menu_open: bool = false
var _gameplay_ui_enabled: bool = false
var _gameplay_input_locked: bool = false
var _gameplay_hud_hidden: bool = false
var _fishing_spot: FishingSpotType
var _system_menu_open: bool = false
@ -631,6 +632,9 @@ func _prioritize_surface_drawing_pointer_input() -> void:
func _input(event: InputEvent) -> void:
if _gameplay_input_locked:
get_viewport().set_input_as_handled()
return
if _social_prompt_open and event.is_action_pressed("ui_cancel"):
_decline_social_prompt()
get_viewport().set_input_as_handled()
@ -681,6 +685,7 @@ func _input(event: InputEvent) -> void:
return
var can_open: bool = (
_gameplay_ui_enabled
and not _gameplay_input_locked
and not _system_menu_open
and not _player_menu_open
and not _shop_open
@ -719,6 +724,7 @@ func _input(event: InputEvent) -> void:
func _reconcile_radial_menu_input() -> void:
var blocked: bool = (
not _gameplay_ui_enabled
or _gameplay_input_locked
or _system_menu_open
or _player_menu_open
or _shop_open
@ -769,6 +775,7 @@ func _close_quick_radial_menu() -> void:
func _can_use_character_call() -> bool:
return (
_gameplay_ui_enabled
and not _gameplay_input_locked
and not _system_menu_open
and not _player_menu_open
and not _shop_open
@ -840,6 +847,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
button_event == null
or not button_event.pressed
or not _gameplay_ui_enabled
or _gameplay_input_locked
or _system_menu_open
or _player_menu_open
or _shop_open
@ -1051,6 +1059,7 @@ func _mapped_virtual_mouse_stick() -> Vector2:
func _can_start_virtual_mouse() -> bool:
return (
_gameplay_ui_enabled
and not _gameplay_input_locked
and not _showcase_active
and not _system_menu_open
and not _player_menu_open
@ -1430,6 +1439,7 @@ func _can_surface_drawing_be_active() -> bool:
return (
_surface_drawing_hotbar_selected
and _gameplay_ui_enabled
and not _gameplay_input_locked
and not _system_menu_open
and not _player_menu_open
and not _shop_open
@ -1838,12 +1848,38 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
_hotbar_ui.set_gameplay_input_enabled(false)
else:
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(true)
_hotbar_ui.set_gameplay_input_enabled(
not _gameplay_input_locked
)
_refresh_fishing_panel_visibility()
call_deferred("_start_next_experience_animation")
_refresh_surface_drawing_activation()
func set_gameplay_input_locked(locked: bool) -> void:
if _gameplay_input_locked == locked:
return
_gameplay_input_locked = locked
if locked:
_end_virtual_mouse()
_close_radial_menus()
_hotbar_ui.set_drag_enabled(false)
if _surface_drawing != null:
_surface_drawing.deactivate()
_refresh_rv_storage_button_visibility()
_refresh_hotbar_visibility()
_refresh_chat_availability()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled
and not _gameplay_input_locked
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
)
_refresh_surface_drawing_activation()
func set_gameplay_hud_hidden(hidden: bool) -> void:
_gameplay_hud_hidden = hidden
if _quick_radial_menu != null:
@ -1862,6 +1898,7 @@ func is_gameplay_hud_hidden() -> bool:
func _can_toggle_gameplay_hud() -> bool:
return (
_gameplay_ui_enabled
and not _gameplay_input_locked
and not _system_menu_open
and not _player_menu_open
and not _shop_open
@ -1903,6 +1940,7 @@ func set_system_menu_open(is_open: bool) -> void:
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled
and not _gameplay_input_locked
and not is_open
and not _player_menu_open
and not _shop_open
@ -1995,6 +2033,7 @@ func _refresh_rv_storage_button_visibility() -> void:
_rv_storage_button.visible = (
_rv_storage_button_requested_visible
and _gameplay_ui_enabled
and not _gameplay_input_locked
and not _gameplay_hud_hidden
and not _system_menu_open
and not _player_menu_open
@ -2799,7 +2838,9 @@ func _on_player_menu_visibility_changed(is_open: bool) -> void:
_hotbar_ui.set_player_menu_context(false)
_refresh_chat_availability()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled and not is_open
_gameplay_ui_enabled
and not _gameplay_input_locked
and not is_open
)
if not is_open:
_hotbar_ui.set_drag_enabled(false)
@ -2844,6 +2885,7 @@ func _on_shop_visibility_changed(is_open: bool) -> void:
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled
and not _gameplay_input_locked
and not is_open
and not _system_menu_open
and not _player_menu_open
@ -2865,6 +2907,7 @@ func _on_storage_visibility_changed(is_open: bool) -> void:
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled
and not _gameplay_input_locked
and not is_open
and not _system_menu_open
and not _player_menu_open
@ -2977,6 +3020,7 @@ func _on_hotbar_presentation_transition_finished(
) -> void:
_hotbar_ui.set_drag_enabled(
presentation_visible
and not _gameplay_input_locked
and _player_menu_open
and _player_menu_hotbar_visible
and not _system_menu_open
@ -2986,6 +3030,7 @@ func _on_hotbar_presentation_transition_finished(
func _refresh_hotbar_visibility() -> void:
_hotbar_ui.set_presentation_visible(
_gameplay_ui_enabled
and not _gameplay_input_locked
and (
not _gameplay_hud_hidden
or (_player_menu_open and _player_menu_hotbar_visible)
@ -3027,6 +3072,7 @@ func _refresh_chat_availability() -> void:
return
var chat_available := (
_gameplay_ui_enabled
and not _gameplay_input_locked
and not _system_menu_open
and not _player_menu_open
and not _shop_open

View file

@ -100,7 +100,11 @@ func popup_file_dialog(dialog: FileDialog) -> void:
# clamp guarantees the embedded window cannot escape the physical screen.
dialog.size = available_size
if dialog.get_viewport().gui_embed_subwindows:
dialog.position = (host_size - available_size) / 2
var centered_position := Vector2i(
floori(float(host_size.x - available_size.x) / 2.0),
floori(float(host_size.y - available_size.y) / 2.0),
)
dialog.position = centered_position
_finalize_compact_file_dialog.call_deferred(
dialog, host_size, available_size
)

View file

@ -329,9 +329,9 @@ func _configure_compose_controller_navigation() -> void:
and _attachment_amount.is_visible_in_tree()
)
var amount_left: Control = _amount_minus if amount_visible else _compose_cancel
var amount_middle: Control = (
_attachment_amount if amount_visible else _compose_cancel
)
var amount_middle: Control = _compose_cancel
if amount_visible:
amount_middle = _attachment_amount
var amount_right: Control = _amount_plus if amount_visible else _send_button
_set_compose_neighbors(
_greeting, _greeting, _recipient, _greeting, _body

View file

@ -394,7 +394,10 @@ func _set_mode(mode: Mode, clear_connection_error: bool = true) -> void:
_discovery_refresh_timer.stop()
if not is_visible_in_tree():
return
_defer_focus_control(_address if mode == Mode.DIRECT else _server_list)
var initial_focus: Control = _server_list
if mode == Mode.DIRECT:
initial_focus = _address
_defer_focus_control(initial_focus)
func _default_mode_status(mode: Mode) -> String:
@ -688,9 +691,10 @@ func _restore_entry_selection_and_focus(
_selected_entry = null
_server_list.deselect_all()
_refresh()
_defer_focus_control(
_server_list if selected else _current_mode_button()
)
var restored_focus: Control = _current_mode_button()
if selected:
restored_focus = _server_list
_defer_focus_control(restored_focus)
func _current_mode_button() -> Button:
@ -1203,10 +1207,10 @@ func _recover_controller_focus(
) -> void:
if not is_visible_in_tree():
return
var owner: Control = get_viewport().gui_get_focus_owner()
if owner != null and owner in controls:
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner != null and focus_owner in controls:
return
if owner != null and _delete_confirmation.is_ancestor_of(owner):
if focus_owner != null and _delete_confirmation.is_ancestor_of(focus_owner):
return
if fallback != null:
_defer_focus_control(fallback)
@ -1539,7 +1543,9 @@ func _on_back_pressed() -> void:
if _name_entry_active:
_clear_edit_state()
_refresh()
var content: Control = _address if _mode == Mode.DIRECT else _server_list
var content: Control = _server_list
if _mode == Mode.DIRECT:
content = _address
_defer_focus_control(content)
return
if (

View file

@ -167,8 +167,8 @@ func _on_seed_submitted(_text: String) -> void:
func _request_start() -> void:
var seed: int = get_selected_world_seed()
if _world_layout == WorldLayoutType.GENERATED and seed == 0:
var world_seed: int = get_selected_world_seed()
if _world_layout == WorldLayoutType.GENERATED and world_seed == 0:
_status.text = (
"enter some text or a whole number from 1 to %d."
% SaveManagerType.MAX_WORLD_SEED
@ -176,10 +176,10 @@ func _request_start() -> void:
_seed_edit.grab_focus()
_seed_edit.select_all()
return
if seed == 0:
seed = SaveManagerType.roll_world_seed()
if world_seed == 0:
world_seed = SaveManagerType.roll_world_seed()
_start_button.disabled = true
start_requested.emit(_world_layout, seed)
start_requested.emit(_world_layout, world_seed)
func _refresh_presentation() -> void:
@ -226,33 +226,43 @@ func _refresh_start_state() -> void:
func _configure_controller_focus() -> void:
var generated: bool = _world_layout == WorldLayoutType.GENERATED
var custom: bool = generated and _seed_mode == SeedMode.CUSTOM
var generated_button_bottom: Control = _start_button
var starter_button_bottom: Control = _back_button
var random_seed_button_bottom: Control = _start_button
var custom_seed_button_bottom: Control = _back_button
if generated:
generated_button_bottom = _random_seed_button
starter_button_bottom = _custom_seed_button
if custom:
random_seed_button_bottom = _seed_edit
custom_seed_button_bottom = _seed_edit
_set_neighbors(
_generated_button,
_generated_button,
_starter_button,
_generated_button,
_random_seed_button if generated else _start_button,
generated_button_bottom,
)
_set_neighbors(
_starter_button,
_generated_button,
_starter_button,
_starter_button,
_custom_seed_button if generated else _back_button,
starter_button_bottom,
)
_set_neighbors(
_random_seed_button,
_random_seed_button,
_custom_seed_button,
_generated_button,
_seed_edit if custom else _start_button,
random_seed_button_bottom,
)
_set_neighbors(
_custom_seed_button,
_random_seed_button,
_custom_seed_button,
_starter_button,
_seed_edit if custom else _back_button,
custom_seed_button_bottom,
)
_set_neighbors(
_seed_edit,
@ -261,13 +271,11 @@ func _configure_controller_focus() -> void:
_custom_seed_button,
_start_button,
)
var action_top: Control = (
_seed_edit
if custom
else _random_seed_button
if generated
else _generated_button
)
var action_top: Control = _generated_button
if generated:
action_top = _random_seed_button
if custom:
action_top = _seed_edit
_set_neighbors(
_start_button,
_start_button,

View file

@ -710,7 +710,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
if not _is_inventory_section(_current_section):
return _handle_active_page_controller_input(event)
var button_event := event as InputEventJoypadButton
var accept_event: bool = (
var accept_button_event: bool = (
button_event != null
and _event_matches_controller_role(
event,
@ -719,7 +719,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
)
)
var accept_pressed: bool = (
accept_event
accept_button_event
and button_event.pressed
)
var cancel_pressed: bool = (
@ -777,7 +777,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
if accept_pressed:
_confirm_controller_storage_placement()
return true
if accept_event:
if accept_button_event:
return true
if (
event.is_action_pressed("ui_down")
@ -810,7 +810,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
):
_cancel_active_item_move()
return _try_enter_notepad_controller_ownership()
if _current_section == Section.TACKLE_BOX and accept_event:
if _current_section == Section.TACKLE_BOX and accept_button_event:
# Bait and lures are unlock collections, not movable inventory slots.
# Controller Y owns their contextual equip/notepad action; A is
# intentionally inert and consumed so it cannot activate the Button.
@ -831,7 +831,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
if _current_section == Section.COOLER:
_enter_inventory_sort_zone()
return true
if accept_event:
if accept_button_event:
if button_event.pressed:
_controller_accept_held = true
_controller_accept_hold_elapsed = 0.0
@ -986,11 +986,14 @@ func _controller_focus_is_on_last_inventory_row() -> bool:
var slot := focus_owner as GeneralInventorySlotType
if slot == null:
return false
return (
slot.slot_index / BAG_STORAGE_COLUMNS
>= (_inventory_layout.get_inventory_capacity() - 1)
/ BAG_STORAGE_COLUMNS
var focused_row: int = int(
float(slot.slot_index) / float(BAG_STORAGE_COLUMNS)
)
var final_row: int = int(
float(_inventory_layout.get_inventory_capacity() - 1)
/ float(BAG_STORAGE_COLUMNS)
)
return focused_row >= final_row
return false
@ -1139,10 +1142,13 @@ func _controller_storage_focus_is_on_last_row() -> bool:
var slot := get_viewport().gui_get_focus_owner() as BagStorageSlotType
if slot == null:
return false
return (
slot.storage_slot_index / BAG_STORAGE_COLUMNS
>= (_bag_slot_nodes.size() - 1) / BAG_STORAGE_COLUMNS
var focused_row: int = int(
float(slot.storage_slot_index) / float(BAG_STORAGE_COLUMNS)
)
var final_row: int = int(
float(_bag_slot_nodes.size() - 1) / float(BAG_STORAGE_COLUMNS)
)
return focused_row >= final_row
func _find_controller_hotbar_assignment(
@ -4575,11 +4581,6 @@ func _refresh_economy_summary() -> void:
if not is_node_ready():
return
var balance: int = _wallet.get_balance() if _wallet != null else 0
var held_total: int = (
_inventory.get_total_sale_value()
if _inventory != null
else 0
)
_notepad_wallet_value.set_amount(balance)
_notepad_capacity_value.text = "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0,

View file

@ -1501,10 +1501,10 @@ func _resize_feature_preview_image(image: Image, max_dimension: int) -> void:
var largest_dimension := maxi(source_size.x, source_size.y)
if largest_dimension <= max_dimension:
return
var scale := float(max_dimension) / float(largest_dimension)
var resize_scale := float(max_dimension) / float(largest_dimension)
image.resize(
maxi(roundi(float(source_size.x) * scale), 1),
maxi(roundi(float(source_size.y) * scale), 1),
maxi(roundi(float(source_size.x) * resize_scale), 1),
maxi(roundi(float(source_size.y) * resize_scale), 1),
Image.INTERPOLATE_NEAREST,
)

View file

@ -18,8 +18,8 @@ const SECTOR_COUNT: int = 3
var _hud_hidden: bool = false
func set_hud_hidden(hidden: bool) -> void:
_hud_hidden = hidden
func set_hud_hidden(is_hidden: bool) -> void:
_hud_hidden = is_hidden
_refresh_labels()

View file

@ -509,21 +509,21 @@ func _export_file_selected(path: String) -> void:
func _request_create() -> void:
if _create_button.disabled:
return
var seed: int = _selected_world_seed()
if _world_layout == WorldLayoutType.GENERATED and seed == 0:
var world_seed: int = _selected_world_seed()
if _world_layout == WorldLayoutType.GENERATED and world_seed == 0:
_status.text = "enter text or a whole number from 1 to %d." % SaveManagerType.MAX_WORLD_SEED
_seed_edit.grab_focus()
_seed_edit.select_all()
return
if seed == 0:
seed = SaveManagerType.roll_world_seed()
if world_seed == 0:
world_seed = SaveManagerType.roll_world_seed()
_busy = true
_status.text = "creating save slot..."
_refresh_action_state()
create_requested.emit(
PlayerSaveSlotCatalog.normalized_name(_new_slot_name.text),
_world_layout,
seed,
world_seed,
_duplicate_source_slot_id,
)
@ -656,13 +656,27 @@ func _configure_controller_focus() -> void:
_new_slot_tab.focus_neighbor_bottom = _new_slot_tab.get_path_to(_new_slot_name)
var generated: bool = _world_layout == WorldLayoutType.GENERATED
var custom: bool = generated and _seed_mode == SeedMode.CUSTOM
var generated_button_bottom: Control = _create_button
var starter_button_bottom: Control = _back_button
var random_seed_button_bottom: Control = _create_button
var custom_seed_button_bottom: Control = _back_button
if generated:
generated_button_bottom = _random_seed_button
starter_button_bottom = _custom_seed_button
if custom:
random_seed_button_bottom = _seed_edit
custom_seed_button_bottom = _seed_edit
_set_neighbors(_new_slot_name, _new_slot_name, _new_slot_name, _new_slot_tab, _generated_button)
_set_neighbors(_generated_button, _generated_button, _starter_button, _new_slot_name, _random_seed_button if generated else _create_button)
_set_neighbors(_starter_button, _generated_button, _starter_button, _new_slot_name, _custom_seed_button if generated else _back_button)
_set_neighbors(_random_seed_button, _random_seed_button, _custom_seed_button, _generated_button, _seed_edit if custom else _create_button)
_set_neighbors(_custom_seed_button, _random_seed_button, _custom_seed_button, _starter_button, _seed_edit if custom else _back_button)
_set_neighbors(_generated_button, _generated_button, _starter_button, _new_slot_name, generated_button_bottom)
_set_neighbors(_starter_button, _generated_button, _starter_button, _new_slot_name, starter_button_bottom)
_set_neighbors(_random_seed_button, _random_seed_button, _custom_seed_button, _generated_button, random_seed_button_bottom)
_set_neighbors(_custom_seed_button, _random_seed_button, _custom_seed_button, _starter_button, custom_seed_button_bottom)
_set_neighbors(_seed_edit, _seed_edit, _seed_edit, _custom_seed_button, _create_button)
var action_top: Control = _seed_edit if custom else _random_seed_button if generated else _generated_button
var action_top: Control = _generated_button
if generated:
action_top = _random_seed_button
if custom:
action_top = _seed_edit
_set_neighbors(_create_button, _create_button, _back_button, action_top, _create_button)
_set_neighbors(_back_button, _create_button, _back_button, action_top, _back_button)

View file

@ -57,6 +57,7 @@ const INTRO_MENU_REVEAL_DURATION: float = 0.30
const INTRO_LOGO_CENTER_Y_RATIO: float = 0.43
signal new_game_requested(world_layout: StringName, world_seed: int)
@warning_ignore("unused_signal")
signal continue_game_requested
signal slot_play_requested(slot_id: String)
signal new_slot_requested(
@ -89,7 +90,6 @@ enum ConfirmationAction {
@onready var _title_presentation_scale_root: Control = (
%TitlePresentationScaleRoot
)
@onready var _background: ColorRect = %Background
@onready var _title_logo: TextureRect = %TitleLogo
@onready var _playtest_label: Label = %PlaytestLabel
@onready var _presentation_center: CenterContainer = %Center
@ -1081,7 +1081,6 @@ func _on_confirmation_accepted() -> void:
or _confirmation_action == ConfirmationAction.NONE
):
return
var action: ConfirmationAction = _confirmation_action
_confirmation_page.lock_interaction()
_action_in_progress = true
if not _save_manager.delete_progression_save():

View file

@ -1,3 +1,5 @@
# Generated terrain initialization is runtime-only, not editor-time tooling.
@warning_ignore("missing_tool")
class_name GeneratedWorldRegion
extends WorldRegion
@ -18,6 +20,9 @@ const RVUpgradeInteractionType = preload(
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const FOLIAGE_WIND_SHADER: Shader = preload(
"res://world/materials/foliage_wind.gdshader"
)
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
const SALT_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water.tres"
@ -74,10 +79,27 @@ const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
&"grass_detail",
&"sand_tree",
]
const FOLIAGE_MATERIAL_NAMES: Array[StringName] = [
&"leaf",
&"leaf_light",
&"leaf_dark",
&"pine",
&"leaf_variant_light",
&"leaf_variant_mid",
&"leaf_variant_dark",
]
# Retain the exact generated material override while foliage wind is active so
# profile changes restore both the selected palette and authored surface state.
const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = (
&"foliage_wind_source_materials"
)
@export var initial_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
@export var prop_catalog: TerrainPropCatalog
@export var biome_catalog: TerrainBiomeCatalog
@export_group("Foliage Wind")
@export_range(0.0, 0.4, 0.005) var foliage_wind_strength: float = 0.36
@export_range(0.0, 4.0, 0.05) var foliage_wind_speed: float = 0.9
@onready var _generator: TerrainChunkGenerator = %TerrainChunkGenerator
@onready var _player_spawn: Marker3D = %PlayerSpawn
@ -114,6 +136,7 @@ var _placed_group_coordinates: Dictionary[StringName, Array] = {}
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
var _home_accessible_elevations_by_coordinate: Dictionary[Vector2i, Array] = {}
var _foliage_wind_enabled: bool = false
func _ready() -> void:
@ -122,18 +145,19 @@ func _ready() -> void:
_validate_biome_catalog()
_configure_static_water()
_build_shoreline_reference()
_set_foliage_wind_enabled(not _light_performance_profile)
func generate_world(seed: int) -> bool:
if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED:
func generate_world(world_seed: int) -> bool:
if world_seed <= 0 or world_seed > PlayerSaveManager.MAX_WORLD_SEED:
return false
if (
seed == _current_seed
world_seed == _current_seed
and _generator.get_generated_chunks_root() != null
):
return true
_current_seed = seed
_generator.generation_seed = seed
_current_seed = world_seed
_generator.generation_seed = world_seed
return _generator.generate()
@ -191,6 +215,7 @@ func get_biome_at(coordinate: Vector2i) -> StringName:
func set_light_performance_profile(enabled: bool) -> void:
_light_performance_profile = enabled
_set_foliage_wind_enabled(not enabled)
_apply_water_materials()
@ -224,8 +249,8 @@ func get_spawn_surface_triangles(
return triangles
func is_home_exterior_position_accessible(position: Vector3) -> bool:
var local_position := _generator.to_local(position)
func is_home_exterior_position_accessible(world_position: Vector3) -> bool:
var local_position := _generator.to_local(world_position)
if local_position.y <= HOME_GROUND_HEIGHT_TOLERANCE:
return true
var coordinate := _generator.coordinate_for_local_position(local_position)
@ -310,12 +335,12 @@ func _on_generation_completed(summary: Dictionary) -> void:
group_counts[group_key] = 0
_placed_group_coordinates[group_key] = []
for record: Dictionary in records:
var position: Vector3 = record.get("position", Vector3.ZERO)
var record_position: Vector3 = record.get("position", Vector3.ZERO)
var tags: PackedStringArray = record.get(
"tags", PackedStringArray()
)
if "spawn" in tags:
spawn_position = position
spawn_position = record_position
continue
var coordinate: Vector2i = record.get(
"coordinate",
@ -418,6 +443,7 @@ func _on_generation_completed(summary: Dictionary) -> void:
# generated world is assembled. Mark the final decoration tree once every
# prop and material variant is in place so rain canopy occlusion persists.
_mark_precipitation_occluders()
_set_foliage_wind_enabled(_foliage_wind_enabled)
world_generated.emit(_current_seed, summary)
@ -520,6 +546,127 @@ func _mark_precipitation_occluders() -> void:
PrecipitationOcclusionType.mark_tree_meshes(prop)
func _set_foliage_wind_enabled(enabled: bool) -> void:
_foliage_wind_enabled = enabled
for prop: Node in _decorations.get_children():
_apply_foliage_wind_to_node(prop, enabled)
func _apply_foliage_wind_to_node(root_node: Node, enabled: bool) -> void:
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null:
if enabled:
_apply_foliage_wind_to_mesh(mesh_instance)
else:
_remove_foliage_wind_from_mesh(mesh_instance)
for child: Node in root_node.get_children():
_apply_foliage_wind_to_node(child, enabled)
func _remove_foliage_wind_from_mesh(mesh_instance: MeshInstance3D) -> void:
if mesh_instance.mesh == null:
return
var source_materials: Dictionary = mesh_instance.get_meta(
FOLIAGE_WIND_SOURCE_MATERIALS_META,
{},
)
for surface_index: int in mesh_instance.mesh.get_surface_count():
var override_material: Material = (
mesh_instance.get_surface_override_material(surface_index)
)
var shader_material := override_material as ShaderMaterial
if (
shader_material != null
and shader_material.shader == FOLIAGE_WIND_SHADER
):
var source_material: Material = source_materials.get(
surface_index,
null,
) as Material
mesh_instance.set_surface_override_material(
surface_index,
source_material,
)
source_materials.erase(surface_index)
if source_materials.is_empty():
mesh_instance.remove_meta(FOLIAGE_WIND_SOURCE_MATERIALS_META)
else:
mesh_instance.set_meta(
FOLIAGE_WIND_SOURCE_MATERIALS_META,
source_materials,
)
func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void:
if mesh_instance.mesh == null:
return
var source_materials: Dictionary = mesh_instance.get_meta(
FOLIAGE_WIND_SOURCE_MATERIALS_META,
{},
)
var bounds: AABB = mesh_instance.get_aabb()
var mesh_global_scale: Vector3 = mesh_instance.global_basis.get_scale().abs()
var horizontal_scale: float = maxf(
(mesh_global_scale.x + mesh_global_scale.z) * 0.5,
0.001,
)
var local_strength: float = foliage_wind_strength / horizontal_scale
var phase: float = fposmod(
mesh_instance.global_position.x * 0.73
+ mesh_instance.global_position.z * 0.41,
TAU,
)
for surface_index: int in mesh_instance.mesh.get_surface_count():
var source_material: Material = mesh_instance.get_active_material(
surface_index
)
if not source_material is StandardMaterial3D:
continue
if not FOLIAGE_MATERIAL_NAMES.has(
StringName(source_material.resource_name)
):
continue
if not source_materials.has(surface_index):
source_materials[surface_index] = (
mesh_instance.get_surface_override_material(surface_index)
)
var source_standard := source_material as StandardMaterial3D
var wind_material := ShaderMaterial.new()
wind_material.shader = FOLIAGE_WIND_SHADER
wind_material.set_shader_parameter(
"albedo_color",
source_standard.albedo_color,
)
wind_material.set_shader_parameter(
"material_roughness",
source_standard.roughness,
)
wind_material.set_shader_parameter(
"material_metallic",
source_standard.metallic,
)
wind_material.set_shader_parameter("local_min_y", bounds.position.y)
wind_material.set_shader_parameter(
"local_height",
maxf(bounds.size.y, 0.001),
)
wind_material.set_shader_parameter(
"local_wind_strength",
local_strength,
)
wind_material.set_shader_parameter("wind_speed", foliage_wind_speed)
wind_material.set_shader_parameter("wind_phase", phase)
mesh_instance.set_surface_override_material(
surface_index,
wind_material,
)
if not source_materials.is_empty():
mesh_instance.set_meta(
FOLIAGE_WIND_SOURCE_MATERIALS_META,
source_materials,
)
func _assign_biomes(
records: Array[Dictionary],
summary: Dictionary,
@ -1056,12 +1203,12 @@ func _prop_yaw(
)
func _nearest_ocean_direction(position: Vector3) -> Vector2:
func _nearest_ocean_direction(world_position: Vector3) -> Vector2:
var half_extents := get_playable_half_extents()
var distance_x := half_extents.x - absf(position.x)
var distance_z := half_extents.y - absf(position.z)
var direction_x := Vector2.RIGHT if position.x >= 0.0 else Vector2.LEFT
var direction_z := Vector2.DOWN if position.z >= 0.0 else Vector2.UP
var distance_x := half_extents.x - absf(world_position.x)
var distance_z := half_extents.y - absf(world_position.z)
var direction_x := Vector2.RIGHT if world_position.x >= 0.0 else Vector2.LEFT
var direction_z := Vector2.DOWN if world_position.z >= 0.0 else Vector2.UP
if absf(distance_x - distance_z) <= _generator.catalog.chunk_size * 0.35:
return (direction_x + direction_z).normalized()
return direction_x if distance_x < distance_z else direction_z
@ -1193,8 +1340,8 @@ func _spawn_distance_eligible_definitions(
) -> Array[TerrainPropDefinition]:
var result: Array[TerrainPropDefinition] = []
var center := Vector2i(
_generator.grid_size.x / 2,
_generator.grid_size.y / 2,
int(float(_generator.grid_size.x) / 2.0),
int(float(_generator.grid_size.y) / 2.0),
)
var distance := (
absi(coordinate.x - center.x)
@ -1233,7 +1380,7 @@ func _record_prop_group_coordinate(
func _has_preferred_prop_near(
definition: TerrainPropDefinition,
position: Vector3,
world_position: Vector3,
) -> bool:
for index: int in _placed_prop_positions.size():
if (
@ -1242,7 +1389,7 @@ func _has_preferred_prop_near(
):
continue
var other := _placed_prop_positions[index]
if Vector2(position.x - other.x, position.z - other.z).length() <= (
if Vector2(world_position.x - other.x, world_position.z - other.z).length() <= (
definition.preferred_nearby_radius
):
return true
@ -1323,12 +1470,12 @@ func _biome_group_key(
return StringName("%s:%s" % [biome_id, group])
func _has_prop_clearance(position: Vector3, radius: float) -> bool:
func _has_prop_clearance(world_position: Vector3, radius: float) -> bool:
for index: int in _placed_prop_positions.size():
var other := _placed_prop_positions[index]
var distance := Vector2(
position.x - other.x,
position.z - other.z,
world_position.x - other.x,
world_position.z - other.z,
).length()
if distance < radius + _placed_prop_clearance_radii[index]:
return false
@ -1584,12 +1731,12 @@ func _safe_recovery_surface_position(candidate: Vector3) -> Variant:
func _water_recovery_surface_triangles_near(
position: Vector3,
world_position: Vector3,
) -> Array[PackedVector3Array]:
var result: Array[PackedVector3Array] = []
if _generator.catalog == null or _generator.catalog.chunk_size <= 0.0:
return result
var local_position := _generator.to_local(position)
var local_position := _generator.to_local(world_position)
var half_grid := Vector2(
float(_generator.grid_size.x - 1) * 0.5,
float(_generator.grid_size.y - 1) * 0.5,
@ -1662,12 +1809,12 @@ func _surface_triangles_for_coordinate(
func _surface_height_at(
position: Vector3,
world_position: Vector3,
triangles: Array[PackedVector3Array],
) -> float:
var highest := -INF
var segment_start := position + Vector3.UP * 100.0
var segment_end := position + Vector3.DOWN * 100.0
var segment_start := world_position + Vector3.UP * 100.0
var segment_end := world_position + Vector3.DOWN * 100.0
for triangle: PackedVector3Array in triangles:
if triangle.size() != 3:
continue
@ -1684,13 +1831,13 @@ func _surface_height_at(
func _surface_supports_prop_footprint(
position: Vector3,
world_position: Vector3,
surface_height: float,
radius: float,
triangles: Array[PackedVector3Array],
) -> bool:
for direction: Vector2 in PROP_SURFACE_SAMPLE_DIRECTIONS:
var sample_position := position + Vector3(
var sample_position := world_position + Vector3(
direction.x * radius,
0.0,
direction.y * radius,

View file

@ -282,11 +282,13 @@ func _resolved_layout_validation_error(
if counts.get(required_id, 0) <= 0:
return "required chunk %s is missing." % required_id
if force_center_chunk_id != &"":
@warning_ignore("integer_division")
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
var center_index := center.y * grid_size.x + center.x
if layout[center_index].definition.stable_id != force_center_chunk_id:
return "the forced center chunk is missing from the center cell."
for index: int in layout.size():
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var current := layout[index]
if not _variant_respects_ocean_boundary(current, coordinate):
@ -956,6 +958,7 @@ func _candidate_has_domain_support(
index: int,
domains: Dictionary,
) -> bool:
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var possible_required_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -1015,6 +1018,7 @@ func _placed_neighbor_requirements_have_domain_support(
var placement := _placements[index]
if placement == null or placement.definition.minimum_required_neighbors <= 0:
continue
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var possible_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -1205,6 +1209,7 @@ func _candidate_dynamic_cell_rules_are_satisfied(
) -> bool:
if not _definition_has_capacity(candidate.definition):
return false
@warning_ignore("integer_division")
var center_index := (grid_size.y / 2) * grid_size.x + grid_size.x / 2
if (
index != center_index
@ -1288,12 +1293,15 @@ func _candidate_can_occupy_cell(
candidate: TerrainChunkVariant,
index: int,
) -> bool:
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
@warning_ignore("integer_division")
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
if (
_cell_requires_reserved_grass(index)
and candidate.definition.stable_id != elevated_cliff_base_chunk_id
and not (
coordinate == Vector2i(grid_size.x / 2, grid_size.y / 2)
coordinate == center
and force_center_chunk_id != &""
and candidate.definition.stable_id == force_center_chunk_id
and "grass" in candidate.definition.tags
@ -1301,7 +1309,6 @@ func _candidate_can_occupy_cell(
)
):
return false
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
if (
coordinate == center
and force_center_chunk_id != &""
@ -1353,6 +1360,7 @@ func _prepare_elevated_feature_region() -> bool:
"Elevated cliff feature requires at least an 11x11 terrain grid."
)
return false
@warning_ignore("integer_division")
var forced_center := Vector2i(grid_size.x / 2, grid_size.y / 2)
var candidates: Array[Vector2i] = []
for row: int in range(
@ -1668,6 +1676,7 @@ func _lake_feature_candidates(
var result: Array[Dictionary] = []
if footprint_size.x < 2 or footprint_size.y < 2:
return result
@warning_ignore("integer_division")
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
for row: int in range(
LAKE_FEATURE_CLEARANCE + 1,
@ -1961,6 +1970,7 @@ func _river_feature_candidate_is_valid(
or outlet_coordinates.size() != 2
):
return false
@warning_ignore("integer_division")
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
var source_lookup: Dictionary[Vector2i, bool] = {}
var source_exit_lookup: Dictionary[Vector2i, bool] = {}
@ -2527,8 +2537,10 @@ func _build_grid_solver_caches() -> void:
_neighbor_indices.resize(_placements.size())
_static_cell_candidate_masks.resize(_placements.size())
_static_cell_candidate_masks.fill(0)
@warning_ignore("integer_division")
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
for index: int in _placements.size():
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var neighbors := PackedInt32Array([-1, -1, -1, -1])
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -2596,6 +2608,7 @@ func _apply_grass_sand_smoothing() -> bool:
return false
var candidates: Array[Dictionary] = []
@warning_ignore("integer_division")
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
for index: int in _placements.size():
var placement := _placements[index]
@ -2604,6 +2617,7 @@ func _apply_grass_sand_smoothing() -> bool:
smoothing_sand_chunk_id,
]:
continue
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
if (
_coordinate_is_on_boundary(coordinate)
@ -2646,6 +2660,7 @@ func _apply_grass_sand_smoothing() -> bool:
if replacement_count >= maximum_smoothing_placements:
break
var index := int(candidate["index"])
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var diagonal := candidate["variant"] as TerrainChunkVariant
if (
@ -3260,6 +3275,7 @@ func _primary_elevated_top_coordinate() -> Vector2i:
if _coordinate_has_primary_elevated_top(_elevated_feature_center):
return _elevated_feature_center
for index: int in _placements.size():
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
if not _coordinate_has_primary_elevated_top(coordinate):
continue
@ -3635,11 +3651,11 @@ func _weighted_candidate_order(
candidate.definition,
):
weight *= long_repeat_weight_multiplier
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
if (
candidate.definition.prefers_map_boundary
and _coordinate_is_on_boundary(
Vector2i(index % grid_size.x, index / grid_size.x)
)
and _coordinate_is_on_boundary(coordinate)
):
weight *= boundary_preference_multiplier
remaining.append(candidate)
@ -3669,6 +3685,7 @@ func _adjacent_definition_repeat_count(
index: int,
definition: TerrainChunkDefinition,
) -> int:
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var count := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -3690,6 +3707,7 @@ func _preferred_neighbor_count(
index: int,
definition: TerrainChunkDefinition,
) -> int:
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var count := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -3711,6 +3729,7 @@ func _long_definition_run_count(
index: int,
definition: TerrainChunkDefinition,
) -> int:
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var count := 0
for axis: Vector2i in [Vector2i.RIGHT, Vector2i.DOWN]:
@ -3762,6 +3781,7 @@ func _neighbor_requirement_validation_error(
var placement := layout[index]
if placement == null or placement.definition.minimum_required_neighbors <= 0:
continue
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var matching_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -3809,6 +3829,7 @@ func _walkable_connectivity_validation_error(
return ""
var start_index := walkable_indices[0]
if force_center_chunk_id != &"":
@warning_ignore("integer_division")
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
var center_index := center.y * grid_size.x + center.x
if (
@ -3822,6 +3843,7 @@ func _walkable_connectivity_validation_error(
while pending_cursor < pending.size():
var index := pending[pending_cursor]
pending_cursor += 1
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var current := layout[index]
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -3892,6 +3914,7 @@ func _least_repeated_equivalent_rotation(
definition: TerrainChunkDefinition,
rotations: PackedInt32Array,
) -> int:
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var best_score := 3
var best_rotations := PackedInt32Array()
@ -3944,6 +3967,7 @@ func _build_solution_root() -> Node3D:
var variant := _placements[index]
if variant == null:
continue
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var chunk_root := _instantiate_chunk_root(variant.definition)
if chunk_root == null:
@ -4325,6 +4349,7 @@ func _count_adjacent_repeat_edges() -> int:
var current := _placements[index]
if current == null:
continue
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
if coordinate.x > 0:
var west := _placements[index - 1]
@ -4383,6 +4408,7 @@ func placement_records() -> Array[Dictionary]:
var variant: TerrainChunkVariant = _placements[index]
if variant == null:
continue
@warning_ignore("integer_division")
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
result.append({
"coordinate": coordinate,
@ -4414,7 +4440,7 @@ func chunk_position(coordinate: Vector2i) -> Vector3:
)
func coordinate_for_local_position(position: Vector3) -> Vector2i:
func coordinate_for_local_position(local_position: Vector3) -> Vector2i:
if catalog == null or catalog.chunk_size <= 0.0:
return Vector2i(-1, -1)
var half_grid := Vector2(
@ -4422,8 +4448,8 @@ func coordinate_for_local_position(position: Vector3) -> Vector2i:
float(grid_size.y - 1) * 0.5,
)
return Vector2i(
roundi(position.x / catalog.chunk_size + half_grid.x),
roundi(position.z / catalog.chunk_size + half_grid.y),
roundi(local_position.x / catalog.chunk_size + half_grid.x),
roundi(local_position.z / catalog.chunk_size + half_grid.y),
)

View file

@ -196,36 +196,36 @@ func get_saltwater_surface_height() -> float:
)
func is_home_exterior_position_accessible(position: Vector3) -> bool:
func is_home_exterior_position_accessible(world_position: Vector3) -> bool:
return (
_active_region != null
and _active_region.is_home_exterior_position_accessible(position)
and _active_region.is_home_exterior_position_accessible(world_position)
)
func activate_world(layout: StringName, seed: int) -> bool:
func activate_world(layout: StringName, world_seed: int) -> bool:
if (
not WorldLayoutType.is_valid(layout)
or seed <= 0
or seed > PlayerSaveManager.MAX_WORLD_SEED
or world_seed <= 0
or world_seed > PlayerSaveManager.MAX_WORLD_SEED
):
return false
if _active_region == null or _active_region.region_id != layout:
if not _replace_active_region(layout, seed):
if not _replace_active_region(layout, world_seed):
return false
_world_layout = layout
_world_seed = seed
_world_seed = world_seed
if layout == WorldLayoutType.GENERATED:
if not _active_region.has_method("generate_world"):
return false
if not bool(_active_region.call("generate_world", seed)):
if not bool(_active_region.call("generate_world", world_seed)):
return false
_configure_world_coverage()
return true
func generate_world(seed: int) -> bool:
return activate_world(WorldLayoutType.GENERATED, seed)
func generate_world(world_seed: int) -> bool:
return activate_world(WorldLayoutType.GENERATED, world_seed)
func get_world_layout() -> StringName:
@ -293,7 +293,7 @@ func _find_active_region() -> WorldRegion:
return null
func _replace_active_region(layout: StringName, seed: int) -> bool:
func _replace_active_region(layout: StringName, world_seed: int) -> bool:
var region_scene: PackedScene = (
GENERATED_REGION_SCENE
if layout == WorldLayoutType.GENERATED
@ -303,7 +303,7 @@ func _replace_active_region(layout: StringName, seed: int) -> bool:
if replacement == null:
return false
if layout == WorldLayoutType.GENERATED:
replacement.set("initial_seed", seed)
replacement.set("initial_seed", world_seed)
if _active_region != null:
_regions_root.remove_child(_active_region)
_active_region.free()
@ -345,8 +345,8 @@ func _configure_world_coverage() -> void:
coverage_shape.size = Vector3(half.x * 2.0, 4.0, half.y * 2.0)
func _set_bound(body: Node3D, position: Vector3, size: Vector3) -> void:
body.position = position
func _set_bound(body: Node3D, world_position: Vector3, size: Vector3) -> void:
body.position = world_position
var collision := body.get_node("Shape") as CollisionShape3D
var shape := collision.shape as BoxShape3D
if shape != null:

View file

@ -78,6 +78,11 @@ func update_world_context(
safe_points: Array[SafeRespawnPoint],
recovery_position_resolver: Callable = Callable(),
) -> void:
# A world change replaces the trigger set and recovery resolver. Never let
# an in-flight recovery retain its old-world input lock or fade callback.
# The caller may be changing worlds while gameplay remains active, so this
# variant restores the control state that recovery itself captured.
_abort_active_recovery(true)
for water_trigger: PlayerWaterTrigger in _water_triggers:
if (
water_trigger != null
@ -117,7 +122,15 @@ func _process(delta: float) -> void:
func set_recovery_enabled(enabled: bool) -> void:
if _recovery_enabled == enabled:
return
_recovery_enabled = enabled
if not enabled:
# Gameplay/session teardown can happen while the screen is fading. The
# normal finish callback would then never be allowed to leave the fishing
# input gate, which makes the next session appear unable to fish. Preserve
# the caller's disabled controls while releasing recovery-owned state.
_abort_active_recovery(false)
func is_recovery_active() -> bool:
@ -125,15 +138,7 @@ func is_recovery_active() -> bool:
func _exit_tree() -> void:
_generation += 1
if _screen_fade != null and is_instance_valid(_screen_fade):
_screen_fade.reset_immediately()
if _player != null and is_instance_valid(_player):
_player.restore_gameplay_orientation_after_recovery()
_player.set_water_recovery_active(false)
_player.set_movement_enabled(_prior_movement_enabled)
_player.set_camera_input_enabled(_prior_camera_input_enabled)
state = RecoveryState.IDLE
_abort_active_recovery(true)
func _on_recovery_requested(
@ -225,3 +230,26 @@ func _finish_recovery() -> void:
state = RecoveryState.IDLE
_bob_elapsed = 0.0
recovery_finished.emit()
func _abort_active_recovery(restore_controls: bool) -> void:
if state == RecoveryState.IDLE:
return
# Invalidate every in-flight ScreenFade completion before returning control.
# This makes cancellation safe whether recovery was bobbing, fading out, or
# fading in.
_generation += 1
if _screen_fade != null and is_instance_valid(_screen_fade):
_screen_fade.reset_immediately()
if _player != null and is_instance_valid(_player):
_player.restore_gameplay_orientation_after_recovery()
_player.set_water_recovery_active(false)
_player.velocity = Vector3.ZERO
if restore_controls:
_player.set_movement_enabled(_prior_movement_enabled)
_player.set_camera_input_enabled(_prior_camera_input_enabled)
if _fishing_spot != null and is_instance_valid(_fishing_spot):
_fishing_spot.end_water_recovery()
state = RecoveryState.IDLE
_bob_elapsed = 0.0
recovery_finished.emit()

View file

@ -381,8 +381,8 @@ func _set_remote_datetime(time_hours: float, date_id: String) -> void:
"year": int(date_id.substr(0, 4)),
"month": int(date_id.substr(5, 2)),
"day": int(date_id.substr(8, 2)),
"hour": total_seconds / 3600,
"minute": (total_seconds / 60) % 60,
"hour": int(float(total_seconds) / 3600.0),
"minute": int(float(total_seconds) / 60.0) % 60,
"second": total_seconds % 60,
}
_set_calendar_date_id(date_id)
@ -432,11 +432,14 @@ static func _is_leap_year(year: int) -> bool:
static func _weekday_for_date(year: int, month: int, day: int) -> int:
var adjusted_year: int = year - 1 if month < 3 else year
var leap_years: int = int(float(adjusted_year) / 4.0)
var skipped_centuries: int = int(float(adjusted_year) / 100.0)
var restored_quadricentennials: int = int(float(adjusted_year) / 400.0)
return posmod(
adjusted_year
+ adjusted_year / 4
- adjusted_year / 100
+ adjusted_year / 400
+ leap_years
- skipped_centuries
+ restored_quadricentennials
+ WEEKDAY_MONTH_OFFSETS[month - 1]
+ day,
7,