Add host-authoritative multiplayer fishing
This commit is contained in:
parent
b0e2e84ba4
commit
d3e3bf4053
17 changed files with 1672 additions and 23 deletions
|
|
@ -135,6 +135,53 @@ static func from_save_dict(
|
|||
return fish_catch if fish_catch.is_valid() else null
|
||||
|
||||
|
||||
static func from_network_dict(
|
||||
data: Dictionary,
|
||||
resolved_fish: FishDataType,
|
||||
) -> FishCatch:
|
||||
if resolved_fish == null:
|
||||
return null
|
||||
for required_key: String in [
|
||||
"catch_id",
|
||||
"fish_id",
|
||||
"weight_lb",
|
||||
"display_scale",
|
||||
"sale_value",
|
||||
]:
|
||||
if not data.has(required_key):
|
||||
return null
|
||||
var loaded_id := StringName(str(data.get("catch_id", "")))
|
||||
var loaded_fish_id := StringName(str(data.get("fish_id", "")))
|
||||
var loaded_weight: float = _read_safe_float(
|
||||
data.get("weight_lb"), 0.0, MAX_SAFE_WEIGHT_LB
|
||||
)
|
||||
var loaded_scale: float = _read_safe_float(
|
||||
data.get("display_scale"), 0.0, MAX_SAFE_DISPLAY_SCALE
|
||||
)
|
||||
var loaded_value: int = _read_safe_integer(
|
||||
data.get("sale_value"), -1, MAX_SAFE_SALE_VALUE
|
||||
)
|
||||
if (
|
||||
loaded_id.is_empty()
|
||||
or loaded_id.length() > 160
|
||||
or loaded_fish_id != resolved_fish.id
|
||||
or loaded_weight <= 0.0
|
||||
or loaded_scale <= 0.0
|
||||
or loaded_value < 0
|
||||
):
|
||||
return null
|
||||
var fish_catch := FishCatch.new()
|
||||
fish_catch.fish = resolved_fish
|
||||
fish_catch.fish_id = loaded_fish_id
|
||||
fish_catch.catch_id = loaded_id
|
||||
fish_catch.catch_sequence = 0
|
||||
fish_catch.weight_lb = loaded_weight
|
||||
fish_catch.display_scale = loaded_scale
|
||||
fish_catch.sale_value = loaded_value
|
||||
fish_catch.is_favorited = false
|
||||
return fish_catch if fish_catch.is_valid() else null
|
||||
|
||||
|
||||
static func _read_safe_integer(
|
||||
value: Variant,
|
||||
invalid_value: int,
|
||||
|
|
|
|||
|
|
@ -122,6 +122,21 @@ func start_encounter(
|
|||
_emit_encounter_update()
|
||||
|
||||
|
||||
func start_authoritative_encounter(
|
||||
profile: CatchDifficultyProfileType,
|
||||
reel_speed: float,
|
||||
click_power: int,
|
||||
seed: int,
|
||||
) -> void:
|
||||
var previous_test_mode: bool = use_deterministic_test_seed
|
||||
var previous_seed: int = deterministic_test_seed
|
||||
use_deterministic_test_seed = true
|
||||
deterministic_test_seed = seed
|
||||
start_encounter(profile, reel_speed, click_power)
|
||||
use_deterministic_test_seed = previous_test_mode
|
||||
deterministic_test_seed = previous_seed
|
||||
|
||||
|
||||
func set_reel_input(held: bool) -> void:
|
||||
_reel_input_held = held
|
||||
if not held:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ const FishableWaterRegionType = preload(
|
|||
"res://world/fishable_water_region.gd"
|
||||
)
|
||||
const NetworkSessionType = preload("res://network/network_session.gd")
|
||||
const NetworkFishingServiceType = preload(
|
||||
"res://network/network_fishing_service.gd"
|
||||
)
|
||||
|
||||
signal status_changed(status: String)
|
||||
signal catch_display_changed(
|
||||
|
|
@ -106,6 +109,7 @@ var _fishing_upgrades: PlayerFishingUpgradesType
|
|||
var _item_effects: PlayerItemEffectsType
|
||||
var _cooler_capacity: PlayerCoolerCapacityType
|
||||
var _network_session: NetworkSessionType
|
||||
var _network_fishing: NetworkFishingServiceType
|
||||
var _active_player: PlayerType
|
||||
var _state_time_remaining: float = 0.0
|
||||
var _cast_charge: float = 0.0
|
||||
|
|
@ -129,6 +133,8 @@ var _pending_catch: FishCatchType
|
|||
var _showcase_ready: bool = false
|
||||
var _put_away_press_armed: bool = false
|
||||
var _showcase_restore_generation: int = 0
|
||||
var _network_auto_click_accumulator: float = 0.0
|
||||
var _network_active_barrier_index: int = -1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -150,6 +156,7 @@ func setup(
|
|||
item_effects: PlayerItemEffectsType,
|
||||
cooler_capacity: PlayerCoolerCapacityType,
|
||||
network_session: NetworkSessionType = null,
|
||||
network_fishing: NetworkFishingServiceType = null,
|
||||
) -> void:
|
||||
_local_player = local_player
|
||||
_local_inventory = local_inventory
|
||||
|
|
@ -161,6 +168,26 @@ func setup(
|
|||
_item_effects = item_effects
|
||||
_cooler_capacity = cooler_capacity
|
||||
_network_session = network_session
|
||||
_network_fishing = network_fishing
|
||||
if _network_fishing != null:
|
||||
_network_fishing.local_cast_accepted.connect(
|
||||
_on_network_cast_accepted
|
||||
)
|
||||
_network_fishing.local_cast_rejected.connect(
|
||||
_on_network_cast_rejected
|
||||
)
|
||||
_network_fishing.local_bite_started.connect(
|
||||
_on_network_bite_started
|
||||
)
|
||||
_network_fishing.local_snapshot_received.connect(
|
||||
_on_network_fishing_snapshot
|
||||
)
|
||||
_network_fishing.local_catch_received.connect(
|
||||
_on_network_catch_received
|
||||
)
|
||||
_network_fishing.local_attempt_ended.connect(
|
||||
_on_network_attempt_ended
|
||||
)
|
||||
if not _local_inventory.catches_changed.is_connected(
|
||||
_on_cooler_availability_changed
|
||||
):
|
||||
|
|
@ -350,14 +377,18 @@ func _process(delta: float) -> void:
|
|||
if not Input.is_action_pressed("fish_primary"):
|
||||
_confirm_cast()
|
||||
FishingState.WAITING_FOR_BITE:
|
||||
_update_waiting_for_bite(delta)
|
||||
if _network_fishing == null:
|
||||
_update_waiting_for_bite(delta)
|
||||
FishingState.FIGHTING:
|
||||
_catch_controller.set_effective_stats(
|
||||
_get_effective_reel_speed(),
|
||||
_get_effective_barrier_damage()
|
||||
)
|
||||
if not Input.is_action_pressed("fish_primary"):
|
||||
_catch_controller.set_reel_input(false)
|
||||
if _network_fishing == null:
|
||||
_catch_controller.set_effective_stats(
|
||||
_get_effective_reel_speed(),
|
||||
_get_effective_barrier_damage()
|
||||
)
|
||||
if not Input.is_action_pressed("fish_primary"):
|
||||
_catch_controller.set_reel_input(false)
|
||||
else:
|
||||
_update_network_auto_click(delta)
|
||||
FishingState.COOLDOWN:
|
||||
_state_time_remaining -= delta
|
||||
if _state_time_remaining <= 0.0:
|
||||
|
|
@ -375,14 +406,6 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
return
|
||||
if not event.is_action("fish_primary"):
|
||||
return
|
||||
if not _can_use_shared_gameplay():
|
||||
if event.is_pressed():
|
||||
status_changed.emit(
|
||||
"Fishing in joined games is coming in the next multiplayer phase."
|
||||
)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
if event.is_pressed():
|
||||
match state:
|
||||
FishingState.READY:
|
||||
|
|
@ -414,11 +437,16 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
_begin_aiming(_local_player)
|
||||
FishingState.WAITING_FOR_BITE:
|
||||
_withdrawal_input_held = true
|
||||
if _network_fishing != null:
|
||||
_network_fishing.submit_local_input(true, true)
|
||||
_presentation.set_line_mode(
|
||||
FishingPresentationType.LineMode.TAUT
|
||||
)
|
||||
FishingState.FIGHTING:
|
||||
_catch_controller.handle_primary_pressed()
|
||||
if _network_fishing != null:
|
||||
_network_fishing.submit_local_input(true, true)
|
||||
else:
|
||||
_catch_controller.handle_primary_pressed()
|
||||
_presentation.set_line_mode(
|
||||
FishingPresentationType.LineMode.TAUT
|
||||
)
|
||||
|
|
@ -435,10 +463,15 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
elif state == FishingState.WAITING_FOR_BITE:
|
||||
_withdrawal_input_held = false
|
||||
if _network_fishing != null:
|
||||
_network_fishing.submit_local_input(false, false)
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif state == FishingState.FIGHTING:
|
||||
_catch_controller.set_reel_input(false)
|
||||
if _network_fishing != null:
|
||||
_network_fishing.submit_local_input(false, false)
|
||||
else:
|
||||
_catch_controller.set_reel_input(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
|
|
@ -558,6 +591,15 @@ func _confirm_cast() -> void:
|
|||
func _on_cast_completed() -> void:
|
||||
if state != FishingState.CASTING:
|
||||
return
|
||||
if _network_fishing != null:
|
||||
_network_fishing.request_local_cast(
|
||||
_cast_origin_position,
|
||||
_cast_target,
|
||||
_cast_charge,
|
||||
_build_network_evidence()
|
||||
)
|
||||
status_changed.emit("checking the water...")
|
||||
return
|
||||
|
||||
_selected_water_region = get_fishable_water_region(_cast_target)
|
||||
_cast_landing_is_fishable = _selected_water_region != null
|
||||
|
|
@ -593,6 +635,8 @@ func _on_cast_completed() -> void:
|
|||
)
|
||||
_withdrawal_progress = 0.0
|
||||
_withdrawal_input_held = false
|
||||
_network_auto_click_accumulator = 0.0
|
||||
_network_active_barrier_index = -1
|
||||
_bobber_water_position = _cast_target
|
||||
_withdrawal_endpoint = Vector3(
|
||||
_cast_origin_position.x + _cast_direction.x * withdrawal_cancel_distance,
|
||||
|
|
@ -948,6 +992,9 @@ func _return_to_ready() -> void:
|
|||
|
||||
|
||||
func _cancel_attempt() -> void:
|
||||
if _network_fishing != null and _network_fishing.has_local_attempt():
|
||||
_network_fishing.cancel_local_attempt("Fishing cancelled.")
|
||||
return
|
||||
_cleanup_attempt("fishing cancelled.", &"cancel")
|
||||
|
||||
|
||||
|
|
@ -1045,6 +1092,169 @@ func _build_fishing_context(
|
|||
return context
|
||||
|
||||
|
||||
func build_network_context(
|
||||
region: FishableWaterRegionType,
|
||||
) -> FishingContextType:
|
||||
return _build_fishing_context(region)
|
||||
|
||||
|
||||
func _build_network_evidence() -> Dictionary:
|
||||
var rarity_multipliers: Array[float] = []
|
||||
for rarity: int in range(FishDataType.Rarity.size()):
|
||||
rarity_multipliers.append(
|
||||
_item_effects.get_rarity_weight_multiplier(rarity)
|
||||
if _item_effects != null
|
||||
else 1.0
|
||||
)
|
||||
var discovered_ids: Array[String] = []
|
||||
if _local_collection_log != null:
|
||||
for fish_id: StringName in _local_collection_log.get_discovered_ids():
|
||||
discovered_ids.append(str(fish_id))
|
||||
var item: ItemDataType = _get_active_item()
|
||||
return {
|
||||
"rod_id": str(item.item_id) if item != null else "",
|
||||
"reel_speed": _get_effective_reel_speed(),
|
||||
"barrier_damage": _get_effective_barrier_damage(),
|
||||
"bite_multiplier": (
|
||||
_item_effects.get_bite_time_multiplier()
|
||||
if _item_effects != null
|
||||
else 1.0
|
||||
),
|
||||
"rarity_multipliers": rarity_multipliers,
|
||||
"discovered_fish_ids": discovered_ids,
|
||||
"capacity_available": not _is_cooler_full(),
|
||||
}
|
||||
|
||||
|
||||
func _on_network_cast_accepted(
|
||||
_attempt_id: String,
|
||||
target: Vector3,
|
||||
) -> void:
|
||||
if state != FishingState.CASTING:
|
||||
return
|
||||
_cast_target = target
|
||||
_bobber_water_position = target
|
||||
state = FishingState.WAITING_FOR_BITE
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||
status_changed.emit("waiting for a bite...")
|
||||
|
||||
|
||||
func _on_network_cast_rejected(message: String) -> void:
|
||||
if state not in [FishingState.CASTING, FishingState.AIMING_CAST]:
|
||||
return
|
||||
_cleanup_attempt(message, &"invalid")
|
||||
|
||||
|
||||
func _on_network_bite_started(_attempt_id: String) -> void:
|
||||
if state != FishingState.WAITING_FOR_BITE:
|
||||
return
|
||||
state = FishingState.FIGHTING
|
||||
_withdrawal_input_held = false
|
||||
_network_auto_click_accumulator = 0.0
|
||||
_network_active_barrier_index = -1
|
||||
_fight_start_position = _bobber_water_position
|
||||
bite_activated.emit()
|
||||
status_changed.emit("fish on!")
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||
_presentation.begin_reeling()
|
||||
_presentation.show_bite()
|
||||
_network_fishing.submit_local_input(
|
||||
Input.is_action_pressed("fish_primary"),
|
||||
false
|
||||
)
|
||||
|
||||
|
||||
func _on_network_fishing_snapshot(snapshot: Dictionary) -> void:
|
||||
if state not in [
|
||||
FishingState.WAITING_FOR_BITE,
|
||||
FishingState.FIGHTING,
|
||||
]:
|
||||
return
|
||||
var positions := PackedFloat32Array(snapshot.get("barrier_positions", []))
|
||||
var health := PackedInt32Array(snapshot.get("barrier_health", []))
|
||||
var maximum_health := PackedInt32Array(
|
||||
snapshot.get("barrier_max_health", [])
|
||||
)
|
||||
var progress: float = float(snapshot.get("progress", 0.0))
|
||||
var chase_progress: float = float(snapshot.get("chase_progress", 0.0))
|
||||
if state == FishingState.FIGHTING:
|
||||
_network_active_barrier_index = int(
|
||||
snapshot.get("active_barrier_index", -1)
|
||||
)
|
||||
catch_display_changed.emit(
|
||||
progress,
|
||||
chase_progress,
|
||||
positions,
|
||||
health,
|
||||
maximum_health,
|
||||
int(snapshot.get("active_barrier_index", -1)),
|
||||
bool(snapshot.get("visible", false))
|
||||
)
|
||||
var bobber_data: Array = snapshot.get("bobber_position", [])
|
||||
if bobber_data.size() == 3:
|
||||
_bobber_water_position = Vector3(
|
||||
float(bobber_data[0]),
|
||||
float(bobber_data[1]),
|
||||
float(bobber_data[2])
|
||||
)
|
||||
if state == FishingState.WAITING_FOR_BITE:
|
||||
_presentation.show_withdrawal_position(_bobber_water_position)
|
||||
else:
|
||||
_presentation.show_reel_position(
|
||||
_bobber_water_position,
|
||||
Input.is_action_pressed("fish_primary")
|
||||
)
|
||||
|
||||
|
||||
func _update_network_auto_click(delta: float) -> void:
|
||||
if (
|
||||
not _catch_controller.auto_click_enabled
|
||||
or not Input.is_action_pressed("fish_primary")
|
||||
or _network_active_barrier_index < 0
|
||||
):
|
||||
_network_auto_click_accumulator = 0.0
|
||||
return
|
||||
_network_auto_click_accumulator += delta
|
||||
var interval: float = maxf(
|
||||
_catch_controller.auto_click_interval,
|
||||
0.05
|
||||
)
|
||||
while _network_auto_click_accumulator >= interval:
|
||||
_network_auto_click_accumulator -= interval
|
||||
_network_fishing.submit_local_input(true, true)
|
||||
|
||||
|
||||
func _on_network_catch_received(fish_catch: FishCatchType) -> void:
|
||||
if state != FishingState.FIGHTING or fish_catch == null:
|
||||
return
|
||||
_pending_catch = fish_catch
|
||||
state = FishingState.SHOWING_CATCH
|
||||
_showcase_ready = false
|
||||
_put_away_press_armed = false
|
||||
catch_display_changed.emit(
|
||||
0.0, 0.0, PackedFloat32Array(), PackedInt32Array(),
|
||||
PackedInt32Array(), -1, false
|
||||
)
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||
_presentation.play_outcome(&"catch")
|
||||
|
||||
|
||||
func _on_network_attempt_ended(
|
||||
outcome: StringName,
|
||||
message: String,
|
||||
) -> void:
|
||||
if state not in [
|
||||
FishingState.CASTING,
|
||||
FishingState.WAITING_FOR_BITE,
|
||||
FishingState.FIGHTING,
|
||||
]:
|
||||
return
|
||||
_cleanup_attempt(
|
||||
message if not message.is_empty() else "Fishing attempt ended.",
|
||||
&"escape" if outcome == &"escape" else &"cancel"
|
||||
)
|
||||
|
||||
|
||||
func find_last_fishable_position(from: Vector3, to: Vector3) -> Vector3:
|
||||
if from.is_equal_approx(to):
|
||||
return from
|
||||
|
|
|
|||
91
fishing/remote_fishing_presentation.gd
Normal file
91
fishing/remote_fishing_presentation.gd
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
class_name RemoteFishingPresentation
|
||||
extends Node3D
|
||||
|
||||
var _owner: Player
|
||||
var _bobber: MeshInstance3D
|
||||
var _line: MeshInstance3D
|
||||
var _line_mesh := ImmediateMesh.new()
|
||||
var _target: Vector3
|
||||
var _active: bool = false
|
||||
|
||||
|
||||
func setup(owner: Player) -> void:
|
||||
_owner = owner
|
||||
_bobber = MeshInstance3D.new()
|
||||
var bobber_mesh := SphereMesh.new()
|
||||
bobber_mesh.radius = 0.12
|
||||
bobber_mesh.height = 0.24
|
||||
_bobber.mesh = bobber_mesh
|
||||
var bobber_material := StandardMaterial3D.new()
|
||||
bobber_material.albedo_color = Color(0.92, 0.12, 0.08, 1.0)
|
||||
bobber_material.roughness = 0.45
|
||||
_bobber.material_override = bobber_material
|
||||
add_child(_bobber)
|
||||
_line = MeshInstance3D.new()
|
||||
_line.mesh = _line_mesh
|
||||
var line_material := StandardMaterial3D.new()
|
||||
line_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
line_material.albedo_color = Color(0.9, 0.9, 0.82, 1.0)
|
||||
_line.material_override = line_material
|
||||
add_child(_line)
|
||||
cleanup()
|
||||
|
||||
|
||||
func show_cast(target: Vector3) -> void:
|
||||
if _owner == null or not target.is_finite():
|
||||
return
|
||||
_active = true
|
||||
_target = target
|
||||
_bobber.global_position = target
|
||||
_bobber.visible = true
|
||||
_line.visible = true
|
||||
_owner.set_active_item_is_rod(true)
|
||||
_redraw_line()
|
||||
|
||||
|
||||
func update_bobber(position: Vector3) -> void:
|
||||
if not _active or not position.is_finite():
|
||||
return
|
||||
_target = position
|
||||
_bobber.global_position = position
|
||||
_redraw_line()
|
||||
|
||||
|
||||
func show_bite() -> void:
|
||||
if not _active:
|
||||
return
|
||||
var tween: Tween = create_tween()
|
||||
tween.tween_property(_bobber, "scale", Vector3.ONE * 0.7, 0.08)
|
||||
tween.tween_property(_bobber, "scale", Vector3.ONE, 0.12)
|
||||
|
||||
|
||||
func cleanup() -> void:
|
||||
_active = false
|
||||
if _bobber != null:
|
||||
_bobber.visible = false
|
||||
_bobber.scale = Vector3.ONE
|
||||
if _line != null:
|
||||
_line.visible = false
|
||||
_line_mesh.clear_surfaces()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _active:
|
||||
_redraw_line()
|
||||
|
||||
|
||||
func _redraw_line() -> void:
|
||||
if _owner == null or not is_instance_valid(_owner):
|
||||
cleanup()
|
||||
return
|
||||
var tip: Marker3D = _owner.get_fishing_rod_tip()
|
||||
if tip == null:
|
||||
return
|
||||
_line_mesh.clear_surfaces()
|
||||
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)
|
||||
_line_mesh.surface_add_vertex(to_local(tip.global_position))
|
||||
var midpoint: Vector3 = tip.global_position.lerp(_target, 0.5)
|
||||
midpoint.y -= 0.12
|
||||
_line_mesh.surface_add_vertex(to_local(midpoint))
|
||||
_line_mesh.surface_add_vertex(to_local(_target))
|
||||
_line_mesh.surface_end()
|
||||
1
fishing/remote_fishing_presentation.gd.uid
Normal file
1
fishing/remote_fishing_presentation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://db3t8mrcaw062
|
||||
24
main/main.gd
24
main/main.gd
|
|
@ -43,6 +43,9 @@ const SavedServerStoreType = preload(
|
|||
const PlayerSpawnServiceType = preload(
|
||||
"res://network/player_spawn_service.gd"
|
||||
)
|
||||
const NetworkFishingServiceType = preload(
|
||||
"res://network/network_fishing_service.gd"
|
||||
)
|
||||
|
||||
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||
|
||||
|
|
@ -77,6 +80,9 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
|||
@onready var _player_spawn_service: PlayerSpawnServiceType = (
|
||||
%PlayerSpawnService
|
||||
)
|
||||
@onready var _network_fishing: NetworkFishingServiceType = (
|
||||
%NetworkFishingService
|
||||
)
|
||||
@onready var _players_root: Node3D = $Players
|
||||
|
||||
var _gameplay_started: bool = false
|
||||
|
|
@ -136,6 +142,17 @@ func _ready() -> void:
|
|||
_player.cooler_capacity
|
||||
)
|
||||
_save_manager.set_autosave_enabled(false)
|
||||
_network_fishing.setup(
|
||||
_network_session,
|
||||
_player_spawn_service,
|
||||
_fishing_spot,
|
||||
_player.inventory,
|
||||
_player.collection_log,
|
||||
_player.cooler_capacity,
|
||||
_save_manager,
|
||||
item_catalog,
|
||||
fish_catalog
|
||||
)
|
||||
_fishing_spot.setup(
|
||||
_player,
|
||||
_player.inventory,
|
||||
|
|
@ -146,7 +163,8 @@ func _ready() -> void:
|
|||
_player.fishing_upgrades,
|
||||
_player.item_effects,
|
||||
_player.cooler_capacity,
|
||||
_network_session
|
||||
_network_session,
|
||||
_network_fishing
|
||||
)
|
||||
_game_ui.setup(
|
||||
_player,
|
||||
|
|
@ -562,6 +580,10 @@ func _on_remote_recovery_requested(
|
|||
var avatar: PlayerType = _player_spawn_service.get_avatar(peer_id)
|
||||
if avatar == null:
|
||||
return
|
||||
_network_fishing.cancel_peer_attempt(
|
||||
peer_id,
|
||||
"Fishing attempt ended."
|
||||
)
|
||||
var target_position: Vector3 = _test_world.get_player_spawn_transform().origin
|
||||
var nearest_distance: float = INF
|
||||
for point: SafeRespawnPoint in _test_world.get_safe_respawn_points():
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=21 format=3]
|
||||
[gd_scene load_steps=22 format=3]
|
||||
|
||||
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
|
||||
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
[ext_resource type="Script" path="res://network/network_profile_preferences.gd" id="18_network_profile"]
|
||||
[ext_resource type="Script" path="res://network/saved_server_store.gd" id="19_saved_servers"]
|
||||
[ext_resource type="Script" path="res://network/player_spawn_service.gd" id="20_spawn_service"]
|
||||
[ext_resource type="Script" path="res://network/network_fishing_service.gd" id="21_network_fishing"]
|
||||
|
||||
[node name="Main" type="Node3D"]
|
||||
script = ExtResource("3_main")
|
||||
|
|
@ -44,6 +45,10 @@ script = ExtResource("19_saved_servers")
|
|||
unique_name_in_owner = true
|
||||
script = ExtResource("20_spawn_service")
|
||||
|
||||
[node name="NetworkFishingService" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("21_network_fishing")
|
||||
|
||||
[node name="TestWorld" parent="." instance=ExtResource("1_world")]
|
||||
|
||||
[node name="Players" type="Node3D" parent="."]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func start_host(
|
|||
var enet_peer := ENetMultiplayerPeer.new()
|
||||
if not bind_address.is_empty() and bind_address != "*":
|
||||
enet_peer.set_bind_ip(bind_address)
|
||||
var error: Error = enet_peer.create_server(port, max_clients, 3)
|
||||
var error: Error = enet_peer.create_server(port, max_clients, 5)
|
||||
if error != OK:
|
||||
transport_error.emit("Unable to host UDP port %d." % port)
|
||||
return error
|
||||
|
|
@ -27,7 +27,7 @@ func connect_to_route(route: ConnectionRoute) -> Error:
|
|||
return ERR_INVALID_PARAMETER
|
||||
var endpoint: ConnectionEndpoint = route.direct_endpoint
|
||||
var enet_peer := ENetMultiplayerPeer.new()
|
||||
var error: Error = enet_peer.create_client(endpoint.host, endpoint.port, 3)
|
||||
var error: Error = enet_peer.create_client(endpoint.host, endpoint.port, 5)
|
||||
if error != OK:
|
||||
transport_error.emit(
|
||||
"Unable to connect to %s." % endpoint.normalized_display
|
||||
|
|
|
|||
38
network/network_fishing_attempt.gd
Normal file
38
network/network_fishing_attempt.gd
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
class_name NetworkFishingAttempt
|
||||
extends RefCounted
|
||||
|
||||
enum Phase {
|
||||
CASTING,
|
||||
WAITING_FOR_BITE,
|
||||
FIGHTING,
|
||||
PENDING_CAPACITY,
|
||||
CAUGHT,
|
||||
ESCAPED,
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
var owner_peer_id: int = 0
|
||||
var request_id: String = ""
|
||||
var attempt_id: String = ""
|
||||
var result_id: String = ""
|
||||
var session_id: String = ""
|
||||
var phase: Phase = Phase.CASTING
|
||||
var origin: Vector3
|
||||
var target: Vector3
|
||||
var bobber_position: Vector3
|
||||
var fish_id: StringName
|
||||
var encounter_seed: int = 0
|
||||
var bite_time_remaining: float = 0.0
|
||||
var withdrawal_progress: float = 0.0
|
||||
var reel_speed: float = 0.0
|
||||
var barrier_damage: int = 1
|
||||
var input_held: bool = false
|
||||
var last_input_sequence: int = 0
|
||||
var catch_payload: Dictionary = {}
|
||||
var capacity_nonce: String = ""
|
||||
var capacity_deadline: float = 0.0
|
||||
var controller: CatchController
|
||||
|
||||
|
||||
func is_terminal() -> bool:
|
||||
return phase in [Phase.CAUGHT, Phase.ESCAPED, Phase.CANCELLED]
|
||||
1
network/network_fishing_attempt.gd.uid
Normal file
1
network/network_fishing_attempt.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c4c00yp1546af
|
||||
142
network/network_fishing_protocol.gd
Normal file
142
network/network_fishing_protocol.gd
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
class_name NetworkFishingProtocol
|
||||
extends RefCounted
|
||||
|
||||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_EVIDENCE_FISH_IDS: int = 256
|
||||
const MAX_CAST_DISTANCE: float = 30.0
|
||||
const MAX_REEL_SPEED: float = 10.0
|
||||
const MAX_BARRIER_DAMAGE: int = 100
|
||||
const INPUT_CHANNEL: int = 3
|
||||
const SNAPSHOT_CHANNEL: int = 4
|
||||
const INPUT_RATE: float = 1.0 / 25.0
|
||||
const SNAPSHOT_RATE: float = 1.0 / 15.0
|
||||
|
||||
enum Outcome {
|
||||
CATCH,
|
||||
ESCAPE,
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
|
||||
static func validate_cast_request(data: Variant) -> String:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return "Malformed fishing request."
|
||||
var payload: Dictionary = data
|
||||
for key: String in [
|
||||
"request_id",
|
||||
"session_id",
|
||||
"origin",
|
||||
"target",
|
||||
"charge",
|
||||
"rod_id",
|
||||
"reel_speed",
|
||||
"barrier_damage",
|
||||
"bite_multiplier",
|
||||
"rarity_multipliers",
|
||||
"discovered_fish_ids",
|
||||
"capacity_available",
|
||||
]:
|
||||
if not payload.has(key):
|
||||
return "Malformed fishing request."
|
||||
if (
|
||||
typeof(payload["request_id"]) != TYPE_STRING
|
||||
or typeof(payload["session_id"]) != TYPE_STRING
|
||||
or typeof(payload["origin"]) != TYPE_ARRAY
|
||||
or typeof(payload["target"]) != TYPE_ARRAY
|
||||
or typeof(payload["charge"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(payload["rod_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or typeof(payload["reel_speed"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(payload["barrier_damage"]) != TYPE_INT
|
||||
or typeof(payload["bite_multiplier"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or typeof(payload["rarity_multipliers"]) != TYPE_ARRAY
|
||||
or typeof(payload["discovered_fish_ids"]) != TYPE_ARRAY
|
||||
or typeof(payload["capacity_available"]) != TYPE_BOOL
|
||||
):
|
||||
return "Malformed fishing request."
|
||||
var request_id: String = payload["request_id"]
|
||||
var session_id: String = payload["session_id"]
|
||||
var origin: Array = payload["origin"]
|
||||
var target: Array = payload["target"]
|
||||
var charge: float = float(payload["charge"])
|
||||
var reel_speed: float = float(payload["reel_speed"])
|
||||
var barrier_damage: int = payload["barrier_damage"]
|
||||
var bite_multiplier: float = float(payload["bite_multiplier"])
|
||||
var rarity: Array = payload["rarity_multipliers"]
|
||||
var discovered: Array = payload["discovered_fish_ids"]
|
||||
if (
|
||||
request_id.is_empty()
|
||||
or request_id.length() > MAX_ID_LENGTH
|
||||
or session_id.is_empty()
|
||||
or session_id.length() > MAX_ID_LENGTH
|
||||
or origin.size() != 3
|
||||
or target.size() != 3
|
||||
or not _array_is_finite_vector3(origin)
|
||||
or not _array_is_finite_vector3(target)
|
||||
or not is_finite(charge)
|
||||
or charge < 0.0
|
||||
or charge > 1.01
|
||||
or not is_finite(reel_speed)
|
||||
or reel_speed <= 0.0
|
||||
or reel_speed > MAX_REEL_SPEED
|
||||
or barrier_damage < 1
|
||||
or barrier_damage > MAX_BARRIER_DAMAGE
|
||||
or not is_finite(bite_multiplier)
|
||||
or bite_multiplier < 0.1
|
||||
or bite_multiplier > 10.0
|
||||
or rarity.size() > 16
|
||||
or discovered.size() > MAX_EVIDENCE_FISH_IDS
|
||||
or str(payload["rod_id"]).is_empty()
|
||||
or str(payload["rod_id"]).length() > 96
|
||||
):
|
||||
return "Fishing request values are outside allowed limits."
|
||||
for value: Variant in rarity:
|
||||
if (
|
||||
typeof(value) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or not is_finite(float(value))
|
||||
or float(value) < 0.0
|
||||
or float(value) > 10.0
|
||||
):
|
||||
return "Fishing rarity evidence is invalid."
|
||||
for value: Variant in discovered:
|
||||
if (
|
||||
typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
or str(value).is_empty()
|
||||
or str(value).length() > 96
|
||||
):
|
||||
return "Fishing discovery evidence is invalid."
|
||||
return ""
|
||||
|
||||
|
||||
static func validate_input(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var payload: Dictionary = data
|
||||
return (
|
||||
typeof(payload.get("attempt_id")) == TYPE_STRING
|
||||
and not str(payload["attempt_id"]).is_empty()
|
||||
and str(payload["attempt_id"]).length() <= MAX_ID_LENGTH
|
||||
and typeof(payload.get("sequence")) == TYPE_INT
|
||||
and int(payload["sequence"]) > 0
|
||||
and typeof(payload.get("held")) == TYPE_BOOL
|
||||
and typeof(payload.get("pressed")) == TYPE_BOOL
|
||||
)
|
||||
|
||||
|
||||
static func vector3_to_array(value: Vector3) -> Array[float]:
|
||||
return [value.x, value.y, value.z]
|
||||
|
||||
|
||||
static func array_to_vector3(value: Array) -> Vector3:
|
||||
if value.size() != 3:
|
||||
return Vector3.INF
|
||||
return Vector3(float(value[0]), float(value[1]), float(value[2]))
|
||||
|
||||
|
||||
static func _array_is_finite_vector3(value: Array) -> bool:
|
||||
for component: Variant in value:
|
||||
if (
|
||||
typeof(component) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or not is_finite(float(component))
|
||||
):
|
||||
return false
|
||||
return true
|
||||
1
network/network_fishing_protocol.gd.uid
Normal file
1
network/network_fishing_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cbihol2i6e2iw
|
||||
1048
network/network_fishing_service.gd
Normal file
1048
network/network_fishing_service.gd
Normal file
File diff suppressed because it is too large
Load diff
1
network/network_fishing_service.gd.uid
Normal file
1
network/network_fishing_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b4v56htdn1y2l
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
class_name NetworkProtocol
|
||||
extends RefCounted
|
||||
|
||||
const PROTOCOL_VERSION: int = 1
|
||||
const PROTOCOL_VERSION: int = 2
|
||||
const GAME_BUILD: String = "prealpha"
|
||||
const MAX_DISPLAY_NAME_LENGTH: int = 48
|
||||
const MAX_PROFILE_ID_LENGTH: int = 96
|
||||
|
|
@ -99,7 +99,10 @@ static func make_server_hello(
|
|||
"server_display_name": "NETFISHING",
|
||||
"player_count": player_count,
|
||||
"max_players": max_players,
|
||||
"capability_flags": PackedStringArray(["movement_v1"]),
|
||||
"capability_flags": PackedStringArray([
|
||||
"movement_v1",
|
||||
"fishing_v1",
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,26 @@ func can_use_host_gameplay() -> bool:
|
|||
return is_host()
|
||||
|
||||
|
||||
func is_gameplay_session_active() -> bool:
|
||||
return state in [State.PRIVATE_HOST, State.OPEN_HOST, State.JOINED_CLIENT]
|
||||
|
||||
|
||||
func is_authenticated_peer(peer_id: int) -> bool:
|
||||
return _registry.has_peer(peer_id)
|
||||
|
||||
|
||||
func get_session_id() -> String:
|
||||
return _session_id
|
||||
|
||||
|
||||
func get_operation_generation() -> int:
|
||||
return _operation_generation
|
||||
|
||||
|
||||
func get_local_peer_id() -> int:
|
||||
return multiplayer.get_unique_id() if is_gameplay_session_active() else 0
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var now: float = Time.get_ticks_msec() / 1000.0
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -506,6 +506,10 @@ func set_water_recovery_active(active: bool) -> void:
|
|||
velocity = Vector3.ZERO
|
||||
|
||||
|
||||
func is_water_recovery_active() -> bool:
|
||||
return _water_recovery_active
|
||||
|
||||
|
||||
func prepare_for_water_recovery() -> void:
|
||||
_restore_gameplay_presentation_for_recovery()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue