feat: expand progression and multiplayer systems
Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
This commit is contained in:
parent
1db1a5b754
commit
3b84bfe3a0
97 changed files with 7869 additions and 982 deletions
|
|
@ -21,6 +21,8 @@ const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
|||
const CAST_ORIGIN_TOLERANCE: float = 2.5
|
||||
const CAPACITY_RESPONSE_TIMEOUT: float = 5.0
|
||||
const MIN_CAST_INTERVAL: float = 0.25
|
||||
const OBSERVER_SNAPSHOT_INTERVAL: float = 0.1
|
||||
const OBSERVER_RELEVANCE_DISTANCE: float = 96.0
|
||||
|
||||
signal local_cast_accepted(attempt_id: String, target: Vector3)
|
||||
signal local_cast_rejected(message: String)
|
||||
|
|
@ -51,7 +53,13 @@ var _last_input_time: Dictionary[int, float] = {}
|
|||
var _remote_presentations: Dictionary[int, RemoteFishingPresentation] = {}
|
||||
var _pending_local_bait_by_request: Dictionary[String, StringName] = {}
|
||||
var _snapshot_accumulator: float = 0.0
|
||||
var _observer_snapshot_accumulator: float = 0.0
|
||||
var _local_input_sequence: int = 0
|
||||
var _presentation_enabled: bool = true
|
||||
var _owner_snapshot_packets_sent: int = 0
|
||||
var _owner_snapshot_states_sent: int = 0
|
||||
var _observer_snapshot_packets_sent: int = 0
|
||||
var _observer_snapshot_states_sent: int = 0
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -67,6 +75,7 @@ func setup(
|
|||
item_catalog: ItemCatalog,
|
||||
fish_catalog: FishPoolType,
|
||||
item_use: NetworkItemUseService,
|
||||
presentation_enabled: bool = true,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
|
|
@ -80,6 +89,7 @@ func setup(
|
|||
_item_catalog = item_catalog
|
||||
_fish_catalog = fish_catalog
|
||||
_item_use = item_use
|
||||
_presentation_enabled = presentation_enabled
|
||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||
|
|
@ -130,6 +140,7 @@ func request_local_cast(
|
|||
"capacity_available": bool(evidence.get("capacity_available", false)),
|
||||
"bait_id": str(bait_id),
|
||||
"lure_id": str(lure_id),
|
||||
"movement_sequence": _session.get_latest_movement_input_sequence(),
|
||||
}
|
||||
if _session.is_host():
|
||||
_handle_cast_request(_session.get_local_peer_id(), data)
|
||||
|
|
@ -213,15 +224,27 @@ func _process(delta: float) -> void:
|
|||
if now >= attempt.capacity_deadline:
|
||||
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
||||
_snapshot_accumulator += delta
|
||||
_observer_snapshot_accumulator += delta
|
||||
if _snapshot_accumulator >= NetworkFishingProtocol.SNAPSHOT_RATE:
|
||||
_snapshot_accumulator = fmod(
|
||||
_snapshot_accumulator,
|
||||
NetworkFishingProtocol.SNAPSHOT_RATE
|
||||
)
|
||||
_broadcast_snapshots()
|
||||
_broadcast_owner_snapshots()
|
||||
if _observer_snapshot_accumulator >= OBSERVER_SNAPSHOT_INTERVAL:
|
||||
_observer_snapshot_accumulator = fmod(
|
||||
_observer_snapshot_accumulator,
|
||||
OBSERVER_SNAPSHOT_INTERVAL,
|
||||
)
|
||||
_broadcast_observer_snapshots()
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"any_peer",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func submit_cast_request(data: Dictionary) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if not _session.is_host() or not _session.is_authenticated_peer(sender_id):
|
||||
|
|
@ -268,7 +291,13 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
|||
return
|
||||
var origin: Vector3 = NetworkFishingProtocol.array_to_vector3(data["origin"])
|
||||
var target: Vector3 = NetworkFishingProtocol.array_to_vector3(data["target"])
|
||||
var authoritative_origin: Vector3 = avatar.get_cast_origin_position()
|
||||
var movement_state: Dictionary = avatar.get_lag_compensated_movement_state(
|
||||
int(data.get("movement_sequence", 0))
|
||||
)
|
||||
var authoritative_origin: Vector3 = movement_state.get(
|
||||
"cast_origin",
|
||||
avatar.get_cast_origin_position(),
|
||||
)
|
||||
if origin.distance_to(authoritative_origin) > CAST_ORIGIN_TOLERANCE:
|
||||
_record_and_reject(peer_id, request_id, "Cannot fish here.")
|
||||
return
|
||||
|
|
@ -279,7 +308,10 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
|||
_fishing_spot.maximum_cast_distance,
|
||||
float(data["charge"])
|
||||
)
|
||||
var facing: Vector3 = avatar.get_facing_direction()
|
||||
var facing: Vector3 = movement_state.get(
|
||||
"facing",
|
||||
avatar.get_facing_direction(),
|
||||
)
|
||||
facing.y = 0.0
|
||||
if (
|
||||
cast_offset.length() < _fishing_spot.minimum_cast_distance - 0.25
|
||||
|
|
@ -642,7 +674,12 @@ func _handle_fishing_input(peer_id: int, data: Dictionary) -> void:
|
|||
attempt.controller.handle_primary_pressed()
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"any_peer",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func submit_cancel_request(attempt_id: String) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
var attempt: NetworkFishingAttempt = _attempts.get(sender_id)
|
||||
|
|
@ -687,16 +724,59 @@ func _on_encounter_updated(
|
|||
})
|
||||
|
||||
|
||||
func _broadcast_snapshots() -> void:
|
||||
var snapshots: Array[Dictionary] = []
|
||||
func _broadcast_owner_snapshots() -> void:
|
||||
for attempt: NetworkFishingAttempt in _attempts.values():
|
||||
var snapshot: Dictionary = attempt.get_meta("snapshot", {})
|
||||
if not snapshot.is_empty():
|
||||
snapshots.append(snapshot)
|
||||
if snapshots.is_empty():
|
||||
if snapshot.is_empty():
|
||||
continue
|
||||
var owner_peer_id: int = attempt.owner_peer_id
|
||||
if owner_peer_id == _session.get_local_peer_id():
|
||||
_apply_snapshots([snapshot])
|
||||
elif owner_peer_id > 1:
|
||||
receive_fishing_snapshots.rpc_id(owner_peer_id, [snapshot])
|
||||
_owner_snapshot_packets_sent += 1
|
||||
_owner_snapshot_states_sent += 1
|
||||
|
||||
|
||||
func _broadcast_observer_snapshots() -> void:
|
||||
if _attempts.is_empty():
|
||||
return
|
||||
_apply_snapshots(snapshots)
|
||||
receive_fishing_snapshots.rpc(snapshots)
|
||||
var observer_ids: Array[int] = _session.get_authenticated_peer_ids()
|
||||
for observer_peer_id: int in observer_ids:
|
||||
var observer_avatar: Player = _spawn_service.get_avatar(observer_peer_id)
|
||||
if observer_avatar == null:
|
||||
continue
|
||||
var summaries: Array = []
|
||||
for attempt: NetworkFishingAttempt in _attempts.values():
|
||||
if attempt.owner_peer_id == observer_peer_id:
|
||||
continue
|
||||
var owner_avatar: Player = _spawn_service.get_avatar(
|
||||
attempt.owner_peer_id
|
||||
)
|
||||
if (
|
||||
owner_avatar == null
|
||||
or observer_avatar.global_position.distance_squared_to(
|
||||
owner_avatar.global_position
|
||||
) > OBSERVER_RELEVANCE_DISTANCE * OBSERVER_RELEVANCE_DISTANCE
|
||||
):
|
||||
continue
|
||||
summaries.append([
|
||||
attempt.attempt_id,
|
||||
attempt.owner_peer_id,
|
||||
attempt.bobber_position,
|
||||
int(attempt.phase),
|
||||
])
|
||||
if summaries.is_empty():
|
||||
continue
|
||||
if observer_peer_id == _session.get_local_peer_id():
|
||||
_apply_observer_snapshots(summaries)
|
||||
elif observer_peer_id > 1:
|
||||
receive_fishing_observer_snapshots.rpc_id(
|
||||
observer_peer_id,
|
||||
summaries,
|
||||
)
|
||||
_observer_snapshot_packets_sent += 1
|
||||
_observer_snapshot_states_sent += summaries.size()
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "unreliable_ordered", 4)
|
||||
|
|
@ -704,6 +784,55 @@ func receive_fishing_snapshots(snapshots: Array) -> void:
|
|||
_apply_snapshots(snapshots)
|
||||
|
||||
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"unreliable_ordered",
|
||||
NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL,
|
||||
)
|
||||
func receive_fishing_observer_snapshots(summaries: Array) -> void:
|
||||
_apply_observer_snapshots(summaries)
|
||||
|
||||
|
||||
func _apply_observer_snapshots(summaries: Array) -> void:
|
||||
if not _presentation_enabled or summaries.size() > 128:
|
||||
return
|
||||
for value: Variant in summaries:
|
||||
if typeof(value) != TYPE_ARRAY:
|
||||
continue
|
||||
var fields: Array = value
|
||||
if (
|
||||
fields.size() != 4
|
||||
or typeof(fields[0]) != TYPE_STRING
|
||||
or str(fields[0]).is_empty()
|
||||
or str(fields[0]).length() > NetworkFishingProtocol.MAX_ID_LENGTH
|
||||
or typeof(fields[1]) != TYPE_INT
|
||||
or int(fields[1]) <= 0
|
||||
or typeof(fields[2]) != TYPE_VECTOR3
|
||||
or typeof(fields[3]) != TYPE_INT
|
||||
or not (fields[2] as Vector3).is_finite()
|
||||
or int(fields[3]) not in [
|
||||
NetworkFishingAttempt.Phase.WAITING_FOR_BITE,
|
||||
NetworkFishingAttempt.Phase.FIGHTING,
|
||||
NetworkFishingAttempt.Phase.PENDING_CAPACITY,
|
||||
]
|
||||
):
|
||||
continue
|
||||
var owner_peer_id: int = int(fields[1])
|
||||
if owner_peer_id == _session.get_local_peer_id():
|
||||
continue
|
||||
var presentation := _get_remote_presentation(owner_peer_id)
|
||||
if presentation != null:
|
||||
presentation.synchronize_active(
|
||||
str(fields[0]),
|
||||
fields[2],
|
||||
int(fields[3]) in [
|
||||
NetworkFishingAttempt.Phase.FIGHTING,
|
||||
NetworkFishingAttempt.Phase.PENDING_CAPACITY,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
func _apply_snapshots(snapshots: Array) -> void:
|
||||
var local_peer_id: int = _session.get_local_peer_id()
|
||||
for value: Variant in snapshots:
|
||||
|
|
@ -810,7 +939,12 @@ func _on_attempt_caught(peer_id: int) -> void:
|
|||
receive_capacity_probe.rpc_id(peer_id, probe)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func receive_capacity_probe(data: Dictionary) -> void:
|
||||
_handle_local_capacity_probe(data)
|
||||
|
||||
|
|
@ -844,7 +978,12 @@ func _handle_local_capacity_probe(data: Dictionary) -> void:
|
|||
)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"any_peer",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func submit_capacity_response(
|
||||
attempt_id: String,
|
||||
capacity_nonce: String,
|
||||
|
|
@ -902,7 +1041,12 @@ func _finalize_catch(attempt: NetworkFishingAttempt) -> void:
|
|||
_dispose_attempt(attempt.owner_peer_id)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func receive_target_outcome(data: Dictionary) -> void:
|
||||
_apply_target_outcome(data)
|
||||
|
||||
|
|
@ -934,7 +1078,7 @@ func _apply_target_outcome(data: Dictionary) -> void:
|
|||
)
|
||||
)
|
||||
_local_inventory.add_catch(fish_catch)
|
||||
_local_collection.mark_quality_discovered(
|
||||
_local_collection.record_catch(
|
||||
fish_id,
|
||||
fish_catch.quality,
|
||||
)
|
||||
|
|
@ -970,7 +1114,12 @@ func _acknowledge_result(result_id: String, catch_id: StringName) -> void:
|
|||
acknowledge_fishing_result.rpc_id(1, result_id, str(catch_id))
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"any_peer",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func acknowledge_fishing_result(result_id: String, catch_id: String) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if (
|
||||
|
|
@ -1027,7 +1176,12 @@ func _broadcast_public_outcome(
|
|||
receive_public_outcome.rpc(data)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func receive_public_outcome(data: Dictionary) -> void:
|
||||
_apply_public_outcome(data)
|
||||
|
||||
|
|
@ -1079,7 +1233,12 @@ func _broadcast_cast_accepted(data: Dictionary) -> void:
|
|||
receive_cast_accepted.rpc(data)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func receive_cast_accepted(data: Dictionary) -> void:
|
||||
_apply_cast_accepted(data)
|
||||
|
||||
|
|
@ -1114,10 +1273,19 @@ func _apply_cast_accepted(data: Dictionary) -> void:
|
|||
else:
|
||||
var presentation := _get_remote_presentation(peer_id)
|
||||
if presentation != null:
|
||||
presentation.show_cast(origin, target)
|
||||
presentation.show_cast(
|
||||
origin,
|
||||
target,
|
||||
str(data["attempt_id"]),
|
||||
)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func receive_bite_pending(data: Dictionary) -> void:
|
||||
_apply_bite_pending(data)
|
||||
|
||||
|
|
@ -1132,7 +1300,12 @@ func _apply_bite_pending(data: Dictionary) -> void:
|
|||
local_bite_pending.emit(str(data["attempt_id"]))
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func receive_bite_started(data: Dictionary) -> void:
|
||||
_apply_bite_started(data)
|
||||
|
||||
|
|
@ -1193,7 +1366,12 @@ func _send_cast_rejected(
|
|||
receive_cast_rejected.rpc_id(peer_id, request_id, message.left(128))
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
@rpc(
|
||||
"authority",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
NetworkFishingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func receive_cast_rejected(request_id: String, message: String) -> void:
|
||||
_pending_local_bait_by_request.erase(request_id)
|
||||
local_cast_rejected.emit(message)
|
||||
|
|
@ -1228,6 +1406,8 @@ func _resend_request_response(peer_id: int, response: Dictionary) -> void:
|
|||
func _get_remote_presentation(
|
||||
peer_id: int,
|
||||
) -> RemoteFishingPresentation:
|
||||
if not _presentation_enabled:
|
||||
return null
|
||||
var existing: RemoteFishingPresentation = _remote_presentations.get(peer_id)
|
||||
if existing != null and is_instance_valid(existing):
|
||||
return existing
|
||||
|
|
@ -1330,9 +1510,21 @@ func _clear_all() -> void:
|
|||
_last_input_time.clear()
|
||||
_pending_local_bait_by_request.clear()
|
||||
_snapshot_accumulator = 0.0
|
||||
_observer_snapshot_accumulator = 0.0
|
||||
_local_input_sequence = 0
|
||||
|
||||
|
||||
func get_network_metrics() -> Dictionary:
|
||||
return {
|
||||
"active_attempts": _attempts.size(),
|
||||
"remote_presentations": _remote_presentations.size(),
|
||||
"owner_snapshot_packets_sent": _owner_snapshot_packets_sent,
|
||||
"owner_snapshot_states_sent": _owner_snapshot_states_sent,
|
||||
"observer_snapshot_packets_sent": _observer_snapshot_packets_sent,
|
||||
"observer_snapshot_states_sent": _observer_snapshot_states_sent,
|
||||
}
|
||||
|
||||
|
||||
func _bound_result_ledger() -> void:
|
||||
while _result_ledgers.size() > MAX_LEDGER_ENTRIES_PER_PEER:
|
||||
_result_ledgers.erase(_result_ledgers.keys().front())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue