Compare commits
4 commits
v0.6.8-alp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 65967816f5 | |||
| 5d7c3d7459 | |||
| e54ad1332b | |||
| 2045d8e288 |
36 changed files with 1141 additions and 137 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
NETfishing
|
NETfishing
|
||||||
v0.6.8-alpha
|
v0.7.1-alpha
|
||||||
Alpha 0.6.8
|
Alpha 0.7.1
|
||||||
|
|
||||||
Thank you for trying this early private playtest.
|
Thank you for trying this early private playtest.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.6.8-alpha/windows-x86_64/NETfishing.exe"
|
export_path="builds/v0.7.1-alpha/windows-x86_64/NETfishing.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -35,11 +35,11 @@ application/modify_resources=true
|
||||||
application/icon="res://art/exported/system_icons/netfishing.ico"
|
application/icon="res://art/exported/system_icons/netfishing.ico"
|
||||||
application/console_wrapper_icon=""
|
application/console_wrapper_icon=""
|
||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/file_version="0.6.8.0"
|
application/file_version="0.7.1.0"
|
||||||
application/product_version="0.6.8.0"
|
application/product_version="0.7.1.0"
|
||||||
application/company_name="Woofmeow"
|
application/company_name="Woofmeow"
|
||||||
application/product_name="NETfishing"
|
application/product_name="NETfishing"
|
||||||
application/file_description="NETfishing v0.6.8-alpha"
|
application/file_description="NETfishing v0.7.1-alpha"
|
||||||
application/copyright="Copyright © 2026 Woofmeow"
|
application/copyright="Copyright © 2026 Woofmeow"
|
||||||
application/trademarks="NETfishing and Woofmeow branding is reserved; see TRADEMARKS.md"
|
application/trademarks="NETfishing and Woofmeow branding is reserved; see TRADEMARKS.md"
|
||||||
application/export_angle=0
|
application/export_angle=0
|
||||||
|
|
@ -62,7 +62,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.6.8-alpha/linux-arm64/NETfishing.arm64"
|
export_path="builds/v0.7.1-alpha/linux-arm64/NETfishing.arm64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -92,7 +92,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.6.8-alpha/macos/NETfishing.zip"
|
export_path="builds/v0.7.1-alpha/macos/NETfishing.zip"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -108,8 +108,8 @@ custom_template/release=""
|
||||||
application/bundle_identifier="io.woofmeow.netfishing"
|
application/bundle_identifier="io.woofmeow.netfishing"
|
||||||
application/icon="res://art/exported/system_icons/netfishing_1024.png"
|
application/icon="res://art/exported/system_icons/netfishing_1024.png"
|
||||||
application/icon_interpolation=0
|
application/icon_interpolation=0
|
||||||
application/short_version="0.6.8"
|
application/short_version="0.7.1"
|
||||||
application/version="0.6.8"
|
application/version="0.7.1"
|
||||||
application/architecture="universal"
|
application/architecture="universal"
|
||||||
codesign/enable=false
|
codesign/enable=false
|
||||||
notarization/enable=false
|
notarization/enable=false
|
||||||
|
|
@ -125,7 +125,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*,tests/*"
|
exclude_filter="builds/*,scripts/*,tests/*"
|
||||||
export_path="builds/v0.6.8-alpha/server-linux-x86_64/NETfishingServer.x86_64"
|
export_path="builds/v0.7.1-alpha/server-linux-x86_64/NETfishingServer.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -155,7 +155,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.6.8-alpha/android/NETfishing.apk"
|
export_path="builds/v0.7.1-alpha/android/NETfishing.apk"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -176,8 +176,8 @@ architectures/armeabi-v7a=false
|
||||||
architectures/arm64-v8a=true
|
architectures/arm64-v8a=true
|
||||||
architectures/x86=false
|
architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=60800
|
version/code=70100
|
||||||
version/name="v0.6.8-alpha"
|
version/name="v0.7.1-alpha"
|
||||||
package/unique_name="io.woofmeow.netfishing"
|
package/unique_name="io.woofmeow.netfishing"
|
||||||
package/name="NETfishing"
|
package/name="NETfishing"
|
||||||
package/signed=true
|
package/signed=true
|
||||||
|
|
@ -214,7 +214,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.6.8-alpha/linux-x86_64/NETfishing.x86_64"
|
export_path="builds/v0.7.1-alpha/linux-x86_64/NETfishing.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,7 @@ func _on_cast_completed() -> void:
|
||||||
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
|
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
|
||||||
)
|
)
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
status_changed.emit("waiting for a bite...")
|
status_changed.emit("")
|
||||||
|
|
||||||
|
|
||||||
func _consume_active_bait() -> void:
|
func _consume_active_bait() -> void:
|
||||||
|
|
@ -1131,10 +1131,7 @@ func _on_catch_escaped() -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
_stop_fight_audio()
|
_stop_fight_audio()
|
||||||
var fish_name: String = "the fish"
|
_cleanup_attempt("", &"escape")
|
||||||
if _selected_fish != null and not _selected_fish.display_name.is_empty():
|
|
||||||
fish_name = "the %s" % _selected_fish.display_name
|
|
||||||
_cleanup_attempt("%s got away!" % fish_name, &"escape")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_outcome_completed(outcome: StringName) -> void:
|
func _on_outcome_completed(outcome: StringName) -> void:
|
||||||
|
|
@ -1597,7 +1594,7 @@ func _on_network_cast_accepted(
|
||||||
_network_input_resend_elapsed = 0.0
|
_network_input_resend_elapsed = 0.0
|
||||||
_presentation.show_withdrawal_position(_bobber_water_position)
|
_presentation.show_withdrawal_position(_bobber_water_position)
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
status_changed.emit("waiting for a bite...")
|
status_changed.emit("")
|
||||||
|
|
||||||
|
|
||||||
func _on_network_cast_rejected(message: String) -> void:
|
func _on_network_cast_rejected(message: String) -> void:
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,10 @@ func get_plan_id() -> String:
|
||||||
return _active_plan_id
|
return _active_plan_id
|
||||||
|
|
||||||
|
|
||||||
|
func has_active_board() -> bool:
|
||||||
|
return validate_board(_active_board)
|
||||||
|
|
||||||
|
|
||||||
func get_time_until_refresh_text() -> String:
|
func get_time_until_refresh_text() -> String:
|
||||||
if _active_plan_id.is_empty() or _world_time == null:
|
if _active_plan_id.is_empty() or _world_time == null:
|
||||||
return "daily jobs unavailable"
|
return "daily jobs unavailable"
|
||||||
|
|
|
||||||
18
main/main.gd
18
main/main.gd
|
|
@ -334,6 +334,7 @@ func _start_dedicated_server() -> void:
|
||||||
if not _network_session.set_host_open(true):
|
if not _network_session.set_host_open(true):
|
||||||
_fail_dedicated_server("Could not open the dedicated server.")
|
_fail_dedicated_server("Could not open the dedicated server.")
|
||||||
return
|
return
|
||||||
|
_player_jobs.begin_progression_session()
|
||||||
if config.public_listing and not _discovery.set_discoverable(true):
|
if config.public_listing and not _discovery.set_discoverable(true):
|
||||||
_network_session.disconnect_session("Discovery setup failed.")
|
_network_session.disconnect_session("Discovery setup failed.")
|
||||||
_fail_dedicated_server(_discovery.get_host_status_message())
|
_fail_dedicated_server(_discovery.get_host_status_message())
|
||||||
|
|
@ -560,7 +561,7 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_network_player_list.set_surface_drawing_service(
|
_network_player_list.set_surface_drawing_service(
|
||||||
_network_surface_drawing
|
_network_surface_drawing
|
||||||
)
|
)
|
||||||
_network_chat.setup(_network_session)
|
_network_chat.setup(_network_session, _world_time, _world_weather)
|
||||||
_network_fishing.setup(
|
_network_fishing.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
|
|
@ -1555,8 +1556,13 @@ func _on_pause_join_game_requested(endpoint: String) -> void:
|
||||||
_join_requested_from_title = false
|
_join_requested_from_title = false
|
||||||
_pending_join_endpoint = endpoint
|
_pending_join_endpoint = endpoint
|
||||||
_game_ui.get_pause_menu().close_for_title_transition()
|
_game_ui.get_pause_menu().close_for_title_transition()
|
||||||
|
var preserved_public_join: bool = (
|
||||||
|
_discovery.preserve_public_join_for_session_switch()
|
||||||
|
)
|
||||||
_network_session.disconnect_session("Connecting to another game.")
|
_network_session.disconnect_session("Connecting to another game.")
|
||||||
if not _network_session.join_direct(endpoint):
|
if not _network_session.join_direct(endpoint):
|
||||||
|
if preserved_public_join:
|
||||||
|
_discovery.cancel_pending_public_join()
|
||||||
_handle_failed_session_switch(
|
_handle_failed_session_switch(
|
||||||
"Could not begin the direct connection."
|
"Could not begin the direct connection."
|
||||||
)
|
)
|
||||||
|
|
@ -1641,14 +1647,16 @@ func _handle_failed_session_switch(message: String) -> void:
|
||||||
_show_title_music(true)
|
_show_title_music(true)
|
||||||
|
|
||||||
|
|
||||||
func _on_network_server_lost() -> void:
|
func _on_network_server_lost(message: String) -> void:
|
||||||
if not _gameplay_started:
|
if not _gameplay_started:
|
||||||
return
|
return
|
||||||
|
_join_requested_from_title = false
|
||||||
|
_join_requested_from_pause = false
|
||||||
|
_pending_join_endpoint = ""
|
||||||
_set_gameplay_active(false)
|
_set_gameplay_active(false)
|
||||||
var title_screen: TitleScreenType = _game_ui.get_title_screen()
|
var title_screen: TitleScreenType = _game_ui.get_title_screen()
|
||||||
title_screen.reopen()
|
title_screen.reopen_to_menu()
|
||||||
title_screen.open_join_game_page(_pending_join_endpoint)
|
title_screen.report_network_error(message)
|
||||||
title_screen.report_network_error("The server connection was lost.")
|
|
||||||
_show_title_music(true)
|
_show_title_music(true)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ var _join_request_in_flight: bool = false
|
||||||
var _pending_join_endpoint: String = ""
|
var _pending_join_endpoint: String = ""
|
||||||
var _pending_join_token: String = ""
|
var _pending_join_token: String = ""
|
||||||
var _pending_join_room_id: String = ""
|
var _pending_join_room_id: String = ""
|
||||||
|
var _preserve_pending_join_on_inactive: bool = false
|
||||||
var _upnp_thread: Thread
|
var _upnp_thread: Thread
|
||||||
var _upnp_mapping_in_progress: bool = false
|
var _upnp_mapping_in_progress: bool = false
|
||||||
var _upnp_operation_is_renewal: bool = false
|
var _upnp_operation_is_renewal: bool = false
|
||||||
|
|
@ -291,6 +292,17 @@ func is_public_join_preparing() -> bool:
|
||||||
return _join_request_in_flight
|
return _join_request_in_flight
|
||||||
|
|
||||||
|
|
||||||
|
func preserve_public_join_for_session_switch() -> bool:
|
||||||
|
_preserve_pending_join_on_inactive = not _pending_join_token.is_empty()
|
||||||
|
return _preserve_pending_join_on_inactive
|
||||||
|
|
||||||
|
|
||||||
|
func cancel_pending_public_join() -> void:
|
||||||
|
_preserve_pending_join_on_inactive = false
|
||||||
|
_set_public_join_state(PublicJoinState.IDLE)
|
||||||
|
_clear_pending_join()
|
||||||
|
|
||||||
|
|
||||||
func prepare_public_join(room: Dictionary) -> bool:
|
func prepare_public_join(room: Dictionary) -> bool:
|
||||||
if _join_request_in_flight:
|
if _join_request_in_flight:
|
||||||
return false
|
return false
|
||||||
|
|
@ -913,18 +925,30 @@ func _discovery_version_mismatch_message(required_version: String) -> String:
|
||||||
|
|
||||||
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
if state == NetworkSession.State.CONNECTING and not _pending_join_token.is_empty():
|
if state == NetworkSession.State.CONNECTING and not _pending_join_token.is_empty():
|
||||||
|
_preserve_pending_join_on_inactive = false
|
||||||
_set_public_join_state(PublicJoinState.CONNECTING)
|
_set_public_join_state(PublicJoinState.CONNECTING)
|
||||||
_join_probe_timer.start()
|
_join_probe_timer.start()
|
||||||
call_deferred("_send_pending_join_probe")
|
call_deferred("_send_pending_join_probe")
|
||||||
elif state in [
|
elif state in [
|
||||||
NetworkSession.State.JOINED_CLIENT,
|
NetworkSession.State.JOINED_CLIENT,
|
||||||
NetworkSession.State.SERVER_LOST,
|
NetworkSession.State.SERVER_LOST,
|
||||||
NetworkSession.State.INACTIVE,
|
|
||||||
]:
|
]:
|
||||||
|
_preserve_pending_join_on_inactive = false
|
||||||
_set_public_join_state(PublicJoinState.IDLE)
|
_set_public_join_state(PublicJoinState.IDLE)
|
||||||
_join_probe_timer.stop()
|
_join_probe_timer.stop()
|
||||||
_clear_pending_join()
|
_clear_pending_join()
|
||||||
|
elif state == NetworkSession.State.INACTIVE:
|
||||||
|
if (
|
||||||
|
_preserve_pending_join_on_inactive
|
||||||
|
and not _pending_join_token.is_empty()
|
||||||
|
):
|
||||||
|
_preserve_pending_join_on_inactive = false
|
||||||
|
else:
|
||||||
|
_set_public_join_state(PublicJoinState.IDLE)
|
||||||
|
_join_probe_timer.stop()
|
||||||
|
_clear_pending_join()
|
||||||
elif state == NetworkSession.State.CONNECTION_FAILED:
|
elif state == NetworkSession.State.CONNECTION_FAILED:
|
||||||
|
_preserve_pending_join_on_inactive = false
|
||||||
_set_public_join_state(PublicJoinState.ERROR)
|
_set_public_join_state(PublicJoinState.ERROR)
|
||||||
_join_probe_timer.stop()
|
_join_probe_timer.stop()
|
||||||
_clear_pending_join()
|
_clear_pending_join()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const BURST_COUNT: int = 3
|
||||||
const WINDOW_COUNT: int = 5
|
const WINDOW_COUNT: int = 5
|
||||||
const WINDOW_SECONDS: float = 10.0
|
const WINDOW_SECONDS: float = 10.0
|
||||||
const CALL_COOLDOWN_MILLISECONDS: int = 180
|
const CALL_COOLDOWN_MILLISECONDS: int = 180
|
||||||
|
const WORLD_COMMAND_COOLDOWN_MILLISECONDS: int = 500
|
||||||
const CALL_PITCH_VARIANTS: Array[float] = [
|
const CALL_PITCH_VARIANTS: Array[float] = [
|
||||||
0.96,
|
0.96,
|
||||||
1.03,
|
1.03,
|
||||||
|
|
@ -22,6 +23,7 @@ signal message_received(message: Dictionary)
|
||||||
signal local_message_confirmed(message: Dictionary)
|
signal local_message_confirmed(message: Dictionary)
|
||||||
signal send_rejected(message: String)
|
signal send_rejected(message: String)
|
||||||
signal history_replaced(messages: Array[Dictionary])
|
signal history_replaced(messages: Array[Dictionary])
|
||||||
|
signal world_command_finished(success: bool, message: String)
|
||||||
signal character_call_received(
|
signal character_call_received(
|
||||||
peer_id: int,
|
peer_id: int,
|
||||||
call_id: String,
|
call_id: String,
|
||||||
|
|
@ -35,13 +37,22 @@ var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||||
var _rate_times: Dictionary[int, Array] = {}
|
var _rate_times: Dictionary[int, Array] = {}
|
||||||
var _last_call_msec: Dictionary[int, int] = {}
|
var _last_call_msec: Dictionary[int, int] = {}
|
||||||
var _call_variant_indices: Dictionary[int, int] = {}
|
var _call_variant_indices: Dictionary[int, int] = {}
|
||||||
|
var _last_world_command_msec: Dictionary[int, int] = {}
|
||||||
var _sequence: int = 0
|
var _sequence: int = 0
|
||||||
var _peer_names: Dictionary[int, String] = {}
|
var _peer_names: Dictionary[int, String] = {}
|
||||||
var _relationships: PlayerRelationshipStore
|
var _relationships: PlayerRelationshipStore
|
||||||
|
var _world_time: WorldTimeService
|
||||||
|
var _world_weather: WorldWeatherService
|
||||||
|
|
||||||
|
|
||||||
func setup(session: NetworkSession) -> void:
|
func setup(
|
||||||
|
session: NetworkSession,
|
||||||
|
world_time: WorldTimeService,
|
||||||
|
world_weather: WorldWeatherService,
|
||||||
|
) -> void:
|
||||||
_session = session
|
_session = session
|
||||||
|
_world_time = world_time
|
||||||
|
_world_weather = world_weather
|
||||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
_session.state_changed.connect(_on_session_state_changed)
|
_session.state_changed.connect(_on_session_state_changed)
|
||||||
|
|
@ -242,6 +253,188 @@ func broadcast_system_message(body: String) -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func request_world_time_change(phase_name: String) -> bool:
|
||||||
|
var normalized: String = phase_name.strip_edges().to_lower()
|
||||||
|
if not _valid_time_phase(normalized) or _session == null:
|
||||||
|
return false
|
||||||
|
if _session.is_host():
|
||||||
|
return _apply_world_time_change(normalized)
|
||||||
|
if (
|
||||||
|
not _session.is_joined_client()
|
||||||
|
or not _session.is_local_operator()
|
||||||
|
or not _session.supports_server_capability(
|
||||||
|
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
submit_world_time_command.rpc_id(1, normalized)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func request_world_weather_change(weather_name: String) -> bool:
|
||||||
|
var normalized: String = _normalized_weather_name(weather_name)
|
||||||
|
if normalized.is_empty() or _session == null:
|
||||||
|
return false
|
||||||
|
if _session.is_host():
|
||||||
|
return _apply_world_weather_change(normalized)
|
||||||
|
if (
|
||||||
|
not _session.is_joined_client()
|
||||||
|
or not _session.is_local_operator()
|
||||||
|
or not _session.supports_server_capability(
|
||||||
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
submit_world_weather_command.rpc_id(1, normalized)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
func submit_world_time_command(phase_name: String) -> void:
|
||||||
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||||
|
if not _valid_world_command_sender(sender_id):
|
||||||
|
_send_world_command_result(
|
||||||
|
sender_id, false, "Only the host or an operator can change world time."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not _consume_world_command_rate(sender_id):
|
||||||
|
_send_world_command_result(sender_id, false, "Slow down.")
|
||||||
|
return
|
||||||
|
var success: bool = _apply_world_time_change(
|
||||||
|
phase_name.strip_edges().to_lower()
|
||||||
|
)
|
||||||
|
_send_world_command_result(
|
||||||
|
sender_id,
|
||||||
|
success,
|
||||||
|
"" if success else "World time could not be changed.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
func submit_world_weather_command(weather_name: String) -> void:
|
||||||
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||||
|
if not _valid_world_command_sender(sender_id):
|
||||||
|
_send_world_command_result(
|
||||||
|
sender_id,
|
||||||
|
false,
|
||||||
|
"Only the host or an operator can change world weather.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not _consume_world_command_rate(sender_id):
|
||||||
|
_send_world_command_result(sender_id, false, "Slow down.")
|
||||||
|
return
|
||||||
|
var success: bool = _apply_world_weather_change(
|
||||||
|
_normalized_weather_name(weather_name)
|
||||||
|
)
|
||||||
|
_send_world_command_result(
|
||||||
|
sender_id,
|
||||||
|
success,
|
||||||
|
"" if success else "World weather could not be changed.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
func receive_world_command_result(success: bool, message: String) -> void:
|
||||||
|
if _session == null or not _session.is_joined_client():
|
||||||
|
return
|
||||||
|
world_command_finished.emit(success, message.left(120))
|
||||||
|
|
||||||
|
|
||||||
|
func _valid_world_command_sender(peer_id: int) -> bool:
|
||||||
|
return (
|
||||||
|
_session != null
|
||||||
|
and _session.is_host()
|
||||||
|
and peer_id > 1
|
||||||
|
and _session.is_authenticated_peer(peer_id)
|
||||||
|
and _session.is_peer_operator(peer_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_world_time_change(phase_name: String) -> bool:
|
||||||
|
if _world_time == null or not _valid_time_phase(phase_name):
|
||||||
|
return false
|
||||||
|
var target_hour: float = _time_phase_hour(phase_name)
|
||||||
|
if not _world_time.set_authoritative_time(target_hour):
|
||||||
|
return false
|
||||||
|
broadcast_system_message(
|
||||||
|
"World time set to %s (%s)."
|
||||||
|
% [phase_name, _world_time.get_clock_text()]
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_world_weather_change(weather_name: String) -> bool:
|
||||||
|
if _world_weather == null:
|
||||||
|
return false
|
||||||
|
var normalized: String = _normalized_weather_name(weather_name)
|
||||||
|
if normalized.is_empty():
|
||||||
|
return false
|
||||||
|
var target_weather: WorldWeatherService.Weather = (
|
||||||
|
_weather_for_name(normalized)
|
||||||
|
)
|
||||||
|
if not _world_weather.set_authoritative_weather(target_weather):
|
||||||
|
return false
|
||||||
|
broadcast_system_message("World weather set to %s." % normalized)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _send_world_command_result(
|
||||||
|
peer_id: int,
|
||||||
|
success: bool,
|
||||||
|
message: String,
|
||||||
|
) -> void:
|
||||||
|
if peer_id > 1 and _session.is_authenticated_peer(peer_id):
|
||||||
|
receive_world_command_result.rpc_id(peer_id, success, message.left(120))
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_world_command_rate(peer_id: int) -> bool:
|
||||||
|
var now_msec: int = Time.get_ticks_msec()
|
||||||
|
var last_msec: int = _last_world_command_msec.get(
|
||||||
|
peer_id, now_msec - WORLD_COMMAND_COOLDOWN_MILLISECONDS
|
||||||
|
)
|
||||||
|
if now_msec - last_msec < WORLD_COMMAND_COOLDOWN_MILLISECONDS:
|
||||||
|
return false
|
||||||
|
_last_world_command_msec[peer_id] = now_msec
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func _valid_time_phase(phase_name: String) -> bool:
|
||||||
|
return phase_name in ["dawn", "day", "dusk", "night"]
|
||||||
|
|
||||||
|
|
||||||
|
static func _time_phase_hour(phase_name: String) -> float:
|
||||||
|
match phase_name:
|
||||||
|
"dawn":
|
||||||
|
return WorldTimeService.DAWN_START_HOUR
|
||||||
|
"day":
|
||||||
|
return WorldTimeService.DAWN_END_HOUR
|
||||||
|
"dusk":
|
||||||
|
return WorldTimeService.DUSK_START_HOUR
|
||||||
|
"night":
|
||||||
|
return WorldTimeService.DUSK_END_HOUR
|
||||||
|
return -1.0
|
||||||
|
|
||||||
|
|
||||||
|
static func _normalized_weather_name(weather_name: String) -> String:
|
||||||
|
var normalized: String = weather_name.strip_edges().to_lower()
|
||||||
|
return "clear" if normalized == "sunny" else normalized if normalized in [
|
||||||
|
"clear", "cloudy", "rainy", "foggy"
|
||||||
|
] else ""
|
||||||
|
|
||||||
|
|
||||||
|
static func _weather_for_name(
|
||||||
|
weather_name: String,
|
||||||
|
) -> WorldWeatherService.Weather:
|
||||||
|
match weather_name:
|
||||||
|
"cloudy":
|
||||||
|
return WorldWeatherService.Weather.CLOUDY
|
||||||
|
"rainy":
|
||||||
|
return WorldWeatherService.Weather.RAINY
|
||||||
|
"foggy":
|
||||||
|
return WorldWeatherService.Weather.FOGGY
|
||||||
|
return WorldWeatherService.Weather.SUNNY
|
||||||
|
|
||||||
|
|
||||||
func get_history() -> Array[Dictionary]:
|
func get_history() -> Array[Dictionary]:
|
||||||
var result: Array[Dictionary] = []
|
var result: Array[Dictionary] = []
|
||||||
for message: Dictionary in _history:
|
for message: Dictionary in _history:
|
||||||
|
|
@ -422,6 +615,7 @@ func _on_peer_removed(peer_id: int) -> void:
|
||||||
_rate_times.erase(peer_id)
|
_rate_times.erase(peer_id)
|
||||||
_last_call_msec.erase(peer_id)
|
_last_call_msec.erase(peer_id)
|
||||||
_call_variant_indices.erase(peer_id)
|
_call_variant_indices.erase(peer_id)
|
||||||
|
_last_world_command_msec.erase(peer_id)
|
||||||
if not _session.is_host():
|
if not _session.is_host():
|
||||||
return
|
return
|
||||||
var display_name: String = _peer_names.get(peer_id, "Player")
|
var display_name: String = _peer_names.get(peer_id, "Player")
|
||||||
|
|
@ -481,6 +675,7 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_rate_times.clear()
|
_rate_times.clear()
|
||||||
_last_call_msec.clear()
|
_last_call_msec.clear()
|
||||||
_call_variant_indices.clear()
|
_call_variant_indices.clear()
|
||||||
|
_last_world_command_msec.clear()
|
||||||
_peer_names.clear()
|
_peer_names.clear()
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
history_replaced.emit([])
|
history_replaced.emit([])
|
||||||
|
|
|
||||||
|
|
@ -953,7 +953,7 @@ func _on_attempt_escaped(peer_id: int) -> void:
|
||||||
if attempt == null:
|
if attempt == null:
|
||||||
return
|
return
|
||||||
attempt.phase = NetworkFishingAttempt.Phase.ESCAPED
|
attempt.phase = NetworkFishingAttempt.Phase.ESCAPED
|
||||||
_broadcast_public_outcome(attempt, &"escape", "The fish got away!")
|
_broadcast_public_outcome(attempt, &"escape", "")
|
||||||
_dispose_attempt(peer_id)
|
_dispose_attempt(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ class_name NetworkJobService
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const MAX_SESSION_ID_LENGTH: int = 96
|
const MAX_SESSION_ID_LENGTH: int = 96
|
||||||
|
const BOARD_REQUEST_INTERVAL_SECONDS: float = 1.5
|
||||||
|
|
||||||
var _session: NetworkSession
|
var _session: NetworkSession
|
||||||
var _jobs: PlayerJobService
|
var _jobs: PlayerJobService
|
||||||
var _sequence: int = 0
|
var _sequence: int = 0
|
||||||
var _last_received_sequence: int = -1
|
var _last_received_sequence: int = -1
|
||||||
|
var _board_request_accumulator: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func setup(session: NetworkSession, jobs: PlayerJobService) -> void:
|
func setup(session: NetworkSession, jobs: PlayerJobService) -> void:
|
||||||
|
|
@ -15,13 +17,36 @@ func setup(session: NetworkSession, jobs: PlayerJobService) -> void:
|
||||||
_session.state_changed.connect(_on_session_state_changed)
|
_session.state_changed.connect(_on_session_state_changed)
|
||||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||||
_jobs.board_changed.connect(_on_board_changed)
|
_jobs.board_changed.connect(_on_board_changed)
|
||||||
|
set_process(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if (
|
||||||
|
_session == null
|
||||||
|
or _jobs == null
|
||||||
|
or not _session.is_joined_client()
|
||||||
|
or not _session.supports_server_capability(
|
||||||
|
NetworkProtocol.JOBS_CAPABILITY
|
||||||
|
)
|
||||||
|
or _jobs.has_active_board()
|
||||||
|
):
|
||||||
|
_board_request_accumulator = 0.0
|
||||||
|
return
|
||||||
|
_board_request_accumulator += delta
|
||||||
|
if _board_request_accumulator < BOARD_REQUEST_INTERVAL_SECONDS:
|
||||||
|
return
|
||||||
|
_board_request_accumulator = 0.0
|
||||||
|
_request_remote_board()
|
||||||
|
|
||||||
|
|
||||||
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
|
_board_request_accumulator = 0.0
|
||||||
if state == NetworkSession.State.JOINED_CLIENT:
|
if state == NetworkSession.State.JOINED_CLIENT:
|
||||||
if not _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY):
|
if _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY):
|
||||||
|
_request_remote_board()
|
||||||
|
else:
|
||||||
_jobs.clear_remote_board()
|
_jobs.clear_remote_board()
|
||||||
elif state in [
|
elif state in [
|
||||||
NetworkSession.State.INACTIVE,
|
NetworkSession.State.INACTIVE,
|
||||||
|
|
@ -69,6 +94,33 @@ func _send_board(peer_id: int) -> void:
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func _request_remote_board() -> void:
|
||||||
|
if (
|
||||||
|
_session != null
|
||||||
|
and _session.is_joined_client()
|
||||||
|
and _session.supports_server_capability(
|
||||||
|
NetworkProtocol.JOBS_CAPABILITY
|
||||||
|
)
|
||||||
|
):
|
||||||
|
request_job_board.rpc_id(1)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||||
|
func request_job_board() -> void:
|
||||||
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||||
|
if (
|
||||||
|
_session == null
|
||||||
|
or not _session.is_host()
|
||||||
|
or sender_id <= 1
|
||||||
|
or not _session.is_authenticated_peer(sender_id)
|
||||||
|
or not _session.peer_supports_capability(
|
||||||
|
sender_id, NetworkProtocol.JOBS_CAPABILITY
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_send_board(sender_id)
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable", 0)
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
func receive_job_board(data: Dictionary) -> void:
|
func receive_job_board(data: Dictionary) -> void:
|
||||||
if (
|
if (
|
||||||
|
|
@ -86,6 +138,7 @@ func receive_job_board(data: Dictionary) -> void:
|
||||||
var board: Dictionary = data.get("board", {})
|
var board: Dictionary = data.get("board", {})
|
||||||
if _jobs.apply_remote_board(board):
|
if _jobs.apply_remote_board(board):
|
||||||
_last_received_sequence = sequence
|
_last_received_sequence = sequence
|
||||||
|
_board_request_accumulator = 0.0
|
||||||
|
|
||||||
|
|
||||||
static func validate_snapshot(value: Variant) -> bool:
|
static func validate_snapshot(value: Variant) -> bool:
|
||||||
|
|
|
||||||
94
network/network_player_animation_protocol.gd
Normal file
94
network/network_player_animation_protocol.gd
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
class_name NetworkPlayerAnimationProtocol
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const FORMAT_VERSION: int = 1
|
||||||
|
const MAX_STATE_ID_LENGTH: int = 64
|
||||||
|
const MAX_ACTION_SEQUENCE: int = 2147483647
|
||||||
|
const MAX_ACTION_ELAPSED_SECONDS: float = 86400.0
|
||||||
|
|
||||||
|
const LOCOMOTION_IDLE: StringName = &"idle"
|
||||||
|
const LOCOMOTION_WALKING: StringName = &"walking"
|
||||||
|
const LOCOMOTION_RUNNING: StringName = &"running"
|
||||||
|
|
||||||
|
|
||||||
|
static func make_state(
|
||||||
|
locomotion_id: StringName,
|
||||||
|
grounded: bool,
|
||||||
|
action_id: StringName = &"",
|
||||||
|
action_sequence: int = 0,
|
||||||
|
action_elapsed: float = 0.0,
|
||||||
|
) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"format_version": FORMAT_VERSION,
|
||||||
|
"locomotion_id": String(locomotion_id),
|
||||||
|
"grounded": grounded,
|
||||||
|
"action": make_action_state(
|
||||||
|
action_id, action_sequence, action_elapsed
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func make_action_state(
|
||||||
|
action_id: StringName = &"",
|
||||||
|
action_sequence: int = 0,
|
||||||
|
action_elapsed: float = 0.0,
|
||||||
|
) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"id": String(action_id),
|
||||||
|
"sequence": action_sequence,
|
||||||
|
"elapsed": action_elapsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_state(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var state: Dictionary = value
|
||||||
|
return (
|
||||||
|
typeof(state.get("format_version")) == TYPE_INT
|
||||||
|
and int(state["format_version"]) == FORMAT_VERSION
|
||||||
|
and valid_state_id(state.get("locomotion_id"), false)
|
||||||
|
and typeof(state.get("grounded")) == TYPE_BOOL
|
||||||
|
and validate_action_state(state.get("action"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_action_state(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var action: Dictionary = value
|
||||||
|
if (
|
||||||
|
not valid_state_id(action.get("id"), true)
|
||||||
|
or typeof(action.get("sequence")) != TYPE_INT
|
||||||
|
or typeof(action.get("elapsed")) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var sequence: int = int(action["sequence"])
|
||||||
|
var elapsed: float = float(action["elapsed"])
|
||||||
|
return (
|
||||||
|
sequence >= 0
|
||||||
|
and sequence <= MAX_ACTION_SEQUENCE
|
||||||
|
and is_finite(elapsed)
|
||||||
|
and elapsed >= 0.0
|
||||||
|
and elapsed <= MAX_ACTION_ELAPSED_SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func valid_state_id(value: Variant, allow_empty: bool) -> bool:
|
||||||
|
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
return false
|
||||||
|
var state_id: String = str(value)
|
||||||
|
if state_id.is_empty():
|
||||||
|
return allow_empty
|
||||||
|
if state_id.length() > MAX_STATE_ID_LENGTH:
|
||||||
|
return false
|
||||||
|
for index: int in state_id.length():
|
||||||
|
var codepoint: int = state_id.unicode_at(index)
|
||||||
|
var valid_character: bool = (
|
||||||
|
codepoint >= 97 and codepoint <= 122
|
||||||
|
or codepoint >= 48 and codepoint <= 57
|
||||||
|
or codepoint == 95
|
||||||
|
)
|
||||||
|
if not valid_character:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
1
network/network_player_animation_protocol.gd.uid
Normal file
1
network/network_player_animation_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cmaaep4n3p2tq
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
class_name NetworkProtocol
|
class_name NetworkProtocol
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const PROTOCOL_VERSION: int = 3
|
const PROTOCOL_VERSION: int = 4
|
||||||
const GAME_BUILD: String = "prealpha"
|
const GAME_BUILD: String = "prealpha"
|
||||||
const MAX_GAME_VERSION_LENGTH: int = 64
|
const MAX_GAME_VERSION_LENGTH: int = 64
|
||||||
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const CONNECTION_TIMEOUT_SECONDS: float = 10.0
|
||||||
const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0
|
const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0
|
||||||
const INPUT_INTERVAL: float = 1.0 / 30.0
|
const INPUT_INTERVAL: float = 1.0 / 30.0
|
||||||
const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
|
const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
|
||||||
|
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
||||||
|
|
||||||
signal state_changed(state: State)
|
signal state_changed(state: State)
|
||||||
signal status_message_changed(message: String)
|
signal status_message_changed(message: String)
|
||||||
|
|
@ -33,7 +34,7 @@ signal server_trust_required(
|
||||||
)
|
)
|
||||||
signal peer_identity_observed(peer_id: int, status: String)
|
signal peer_identity_observed(peer_id: int, status: String)
|
||||||
signal operator_status_changed(peer_id: int, is_operator: bool)
|
signal operator_status_changed(peer_id: int, is_operator: bool)
|
||||||
signal server_lost
|
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)
|
||||||
signal remote_recovery_presentation_changed(
|
signal remote_recovery_presentation_changed(
|
||||||
peer_id: int,
|
peer_id: int,
|
||||||
|
|
@ -874,7 +875,7 @@ func _on_server_disconnected() -> void:
|
||||||
)
|
)
|
||||||
_moderation_disconnect_message = ""
|
_moderation_disconnect_message = ""
|
||||||
_set_state(State.SERVER_LOST, message)
|
_set_state(State.SERVER_LOST, message)
|
||||||
server_lost.emit()
|
server_lost.emit(message)
|
||||||
connection_error.emit(message)
|
connection_error.emit(message)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1107,7 +1108,6 @@ func submit_client_identity_proof(data: Dictionary) -> void:
|
||||||
@rpc("authority", "call_remote", "reliable", 0)
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
func receive_moderation_disconnect(message: String) -> void:
|
func receive_moderation_disconnect(message: String) -> void:
|
||||||
_moderation_disconnect_message = message.left(120)
|
_moderation_disconnect_message = message.left(120)
|
||||||
connection_error.emit(_moderation_disconnect_message)
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable", 0)
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
|
|
@ -1741,9 +1741,10 @@ func _is_valid_movement_input(data: Dictionary) -> bool:
|
||||||
or typeof(data.get("sprint")) != TYPE_BOOL
|
or typeof(data.get("sprint")) != TYPE_BOOL
|
||||||
or typeof(data.get("sneak")) != TYPE_BOOL
|
or typeof(data.get("sneak")) != TYPE_BOOL
|
||||||
or typeof(data.get("slow_walk")) != TYPE_BOOL
|
or typeof(data.get("slow_walk")) != TYPE_BOOL
|
||||||
or (
|
or typeof(data.get("sitting")) != TYPE_BOOL
|
||||||
data.has("sitting")
|
or typeof(data.get("casting")) != TYPE_BOOL
|
||||||
and typeof(data.get("sitting")) != TYPE_BOOL
|
or not NetworkPlayerAnimationProtocol.validate_action_state(
|
||||||
|
data.get("animation_action")
|
||||||
)
|
)
|
||||||
or typeof(data.get("camera_yaw")) not in [TYPE_FLOAT, TYPE_INT]
|
or typeof(data.get("camera_yaw")) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
):
|
):
|
||||||
|
|
@ -1751,11 +1752,18 @@ func _is_valid_movement_input(data: Dictionary) -> bool:
|
||||||
var axis: Array = data["axis"]
|
var axis: Array = data["axis"]
|
||||||
if axis.size() != 2:
|
if axis.size() != 2:
|
||||||
return false
|
return false
|
||||||
|
if (
|
||||||
|
typeof(axis[0]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or typeof(axis[1]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
):
|
||||||
|
return false
|
||||||
var x: float = float(axis[0])
|
var x: float = float(axis[0])
|
||||||
var y: float = float(axis[1])
|
var y: float = float(axis[1])
|
||||||
var camera_yaw: float = float(data["camera_yaw"])
|
var camera_yaw: float = float(data["camera_yaw"])
|
||||||
return (
|
return (
|
||||||
is_finite(x)
|
int(data["sequence"]) > 0
|
||||||
|
and int(data["sequence"]) <= MAX_MOVEMENT_INPUT_SEQUENCE
|
||||||
|
and is_finite(x)
|
||||||
and is_finite(y)
|
and is_finite(y)
|
||||||
and is_finite(camera_yaw)
|
and is_finite(camera_yaw)
|
||||||
and absf(x) <= 1.01
|
and absf(x) <= 1.01
|
||||||
|
|
|
||||||
290
player/player.gd
290
player/player.gd
|
|
@ -68,6 +68,10 @@ const CHARACTER_FISHING_ANIMATION: StringName = &"fishing"
|
||||||
const CHARACTER_FISHING_SIT_ANIMATION: StringName = &"fishing_sit"
|
const CHARACTER_FISHING_SIT_ANIMATION: StringName = &"fishing_sit"
|
||||||
const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting"
|
const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting"
|
||||||
const CHARACTER_FIGHTING_SIT_ANIMATION: StringName = &"fighting_sit"
|
const CHARACTER_FIGHTING_SIT_ANIMATION: StringName = &"fighting_sit"
|
||||||
|
# Add future networked emote animation IDs here. The protocol accepts unknown
|
||||||
|
# safe IDs so newer clients can extend it, but Player only presents actions
|
||||||
|
# explicitly approved by this catalog.
|
||||||
|
const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = []
|
||||||
|
|
||||||
enum FishingVisualPhase {
|
enum FishingVisualPhase {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -84,6 +88,12 @@ enum PocketVisualTarget {
|
||||||
ART_KIT,
|
ART_KIT,
|
||||||
CATCH_SHOWCASE,
|
CATCH_SHOWCASE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LocomotionState {
|
||||||
|
IDLE,
|
||||||
|
WALKING,
|
||||||
|
RUNNING,
|
||||||
|
}
|
||||||
const FIGHTING_EYES_ID: String = "alligator_eyes"
|
const FIGHTING_EYES_ID: String = "alligator_eyes"
|
||||||
const BLINK_EYES_ID: String = "closed"
|
const BLINK_EYES_ID: String = "closed"
|
||||||
const BLINK_INTERVAL_SECONDS := Vector2(2.8, 7.5)
|
const BLINK_INTERVAL_SECONDS := Vector2(2.8, 7.5)
|
||||||
|
|
@ -393,8 +403,18 @@ var _last_network_input_sequence: int = 0
|
||||||
var _network_target_position: Vector3
|
var _network_target_position: Vector3
|
||||||
var _network_target_velocity: Vector3
|
var _network_target_velocity: Vector3
|
||||||
var _network_target_visual_yaw: float = 0.0
|
var _network_target_visual_yaw: float = 0.0
|
||||||
|
var _network_target_grounded: bool = false
|
||||||
|
var _network_target_locomotion_state: LocomotionState = LocomotionState.IDLE
|
||||||
|
var _network_target_animation_action_id: StringName = &""
|
||||||
|
var _network_target_animation_action_sequence: int = 0
|
||||||
|
var _network_target_animation_action_elapsed: float = 0.0
|
||||||
var _network_snapshot_ready: bool = false
|
var _network_snapshot_ready: bool = false
|
||||||
var _network_snapshot_age: float = 0.0
|
var _network_snapshot_age: float = 0.0
|
||||||
|
var _animation_action_id: StringName = &""
|
||||||
|
var _animation_action_sequence: int = 0
|
||||||
|
var _animation_action_elapsed: float = 0.0
|
||||||
|
var _presented_animation_action_id: StringName = &""
|
||||||
|
var _presented_animation_action_sequence: int = -1
|
||||||
var _character_animation_name: StringName = &""
|
var _character_animation_name: StringName = &""
|
||||||
var _sitting: bool = false
|
var _sitting: bool = false
|
||||||
var _sit_after_landing: bool = false
|
var _sit_after_landing: bool = false
|
||||||
|
|
@ -475,6 +495,72 @@ func set_controller_mapping_manager(
|
||||||
_controller_mapping_manager = mapping_manager
|
_controller_mapping_manager = mapping_manager
|
||||||
|
|
||||||
|
|
||||||
|
func begin_animation_action(
|
||||||
|
action_id: StringName,
|
||||||
|
elapsed_seconds: float = 0.0,
|
||||||
|
) -> bool:
|
||||||
|
if (
|
||||||
|
action_id.is_empty()
|
||||||
|
or not supports_network_animation_action(action_id)
|
||||||
|
or _character_animation_player == null
|
||||||
|
or not _character_animation_player.has_animation(action_id)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var next_sequence: int = (
|
||||||
|
1
|
||||||
|
if _animation_action_sequence
|
||||||
|
>= NetworkPlayerAnimationProtocol.MAX_ACTION_SEQUENCE
|
||||||
|
else _animation_action_sequence + 1
|
||||||
|
)
|
||||||
|
var action_state: Dictionary = (
|
||||||
|
NetworkPlayerAnimationProtocol.make_action_state(
|
||||||
|
action_id, next_sequence, elapsed_seconds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
|
||||||
|
return false
|
||||||
|
_apply_animation_action_state(action_state)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func end_animation_action() -> void:
|
||||||
|
if _animation_action_id.is_empty():
|
||||||
|
return
|
||||||
|
var next_sequence: int = (
|
||||||
|
1
|
||||||
|
if _animation_action_sequence
|
||||||
|
>= NetworkPlayerAnimationProtocol.MAX_ACTION_SEQUENCE
|
||||||
|
else _animation_action_sequence + 1
|
||||||
|
)
|
||||||
|
_apply_animation_action_state(
|
||||||
|
NetworkPlayerAnimationProtocol.make_action_state(
|
||||||
|
&"", next_sequence, 0.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_animation_action_state(action_state: Dictionary) -> void:
|
||||||
|
if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
|
||||||
|
return
|
||||||
|
var action_id := StringName(str(action_state["id"]))
|
||||||
|
if (
|
||||||
|
not action_id.is_empty()
|
||||||
|
and not supports_network_animation_action(action_id)
|
||||||
|
):
|
||||||
|
action_id = &""
|
||||||
|
var action_sequence: int = int(action_state["sequence"])
|
||||||
|
var action_changed: bool = (
|
||||||
|
action_id != _animation_action_id
|
||||||
|
or action_sequence != _animation_action_sequence
|
||||||
|
)
|
||||||
|
_animation_action_id = action_id
|
||||||
|
_animation_action_sequence = action_sequence
|
||||||
|
_animation_action_elapsed = float(action_state["elapsed"])
|
||||||
|
if action_changed:
|
||||||
|
_character_animation_name = &""
|
||||||
|
_update_character_animation()
|
||||||
|
|
||||||
|
|
||||||
func _initialize_fishing_rod() -> void:
|
func _initialize_fishing_rod() -> void:
|
||||||
var skeleton := get_node_or_null(
|
var skeleton := get_node_or_null(
|
||||||
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
||||||
|
|
@ -613,6 +699,11 @@ func _physics_process(delta: float) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
if not _animation_action_id.is_empty():
|
||||||
|
_animation_action_elapsed = minf(
|
||||||
|
_animation_action_elapsed + delta,
|
||||||
|
NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
|
||||||
|
)
|
||||||
_update_blink(delta)
|
_update_blink(delta)
|
||||||
_update_character_animation()
|
_update_character_animation()
|
||||||
_update_sprint_dust(delta)
|
_update_sprint_dust(delta)
|
||||||
|
|
@ -705,7 +796,7 @@ func _update_sprint_dust(delta: float) -> void:
|
||||||
if _sprint_dust == null:
|
if _sprint_dust == null:
|
||||||
return
|
return
|
||||||
var horizontal_velocity := Vector3(velocity.x, 0.0, velocity.z)
|
var horizontal_velocity := Vector3(velocity.x, 0.0, velocity.z)
|
||||||
var grounded: bool = is_on_floor()
|
var grounded: bool = _get_presented_grounded()
|
||||||
if _water_recovery_active:
|
if _water_recovery_active:
|
||||||
_sprint_dust_airborne = false
|
_sprint_dust_airborne = false
|
||||||
_sprint_dust_fall_speed = 0.0
|
_sprint_dust_fall_speed = 0.0
|
||||||
|
|
@ -730,18 +821,11 @@ func _update_sprint_dust(delta: float) -> void:
|
||||||
)
|
)
|
||||||
_sprint_dust_airborne = false
|
_sprint_dust_airborne = false
|
||||||
_sprint_dust_fall_speed = 0.0
|
_sprint_dust_fall_speed = 0.0
|
||||||
var horizontal_speed_squared: float = horizontal_velocity.length_squared()
|
|
||||||
var fastest_non_sprint_speed := maxf(
|
|
||||||
walk_speed,
|
|
||||||
maxf(sneak_speed, slow_walk_speed)
|
|
||||||
)
|
|
||||||
var running_threshold := fastest_non_sprint_speed + 0.5
|
|
||||||
var should_emit: bool = (
|
var should_emit: bool = (
|
||||||
grounded
|
grounded
|
||||||
and not _sitting
|
and not _sitting
|
||||||
and not _water_recovery_active
|
and not _water_recovery_active
|
||||||
and horizontal_speed_squared
|
and _get_presented_locomotion_state() == LocomotionState.RUNNING
|
||||||
> running_threshold * running_threshold
|
|
||||||
)
|
)
|
||||||
_sprint_dust.update_trail(
|
_sprint_dust.update_trail(
|
||||||
delta,
|
delta,
|
||||||
|
|
@ -778,18 +862,17 @@ func _get_controller_zoom_strength() -> float:
|
||||||
func _update_character_animation() -> void:
|
func _update_character_animation() -> void:
|
||||||
if _character_animation_player == null:
|
if _character_animation_player == null:
|
||||||
return
|
return
|
||||||
var horizontal_speed_squared: float = (
|
var locomotion_state: LocomotionState = (
|
||||||
velocity.x * velocity.x + velocity.z * velocity.z
|
_get_presented_locomotion_state()
|
||||||
)
|
)
|
||||||
var is_walking: bool = horizontal_speed_squared > 0.0025
|
var is_walking: bool = locomotion_state != LocomotionState.IDLE
|
||||||
var fastest_non_sprint_speed := maxf(
|
var is_running: bool = locomotion_state == LocomotionState.RUNNING
|
||||||
walk_speed,
|
var animation_action: Dictionary = _get_presented_animation_action()
|
||||||
maxf(sneak_speed, slow_walk_speed)
|
var animation_action_id := StringName(str(animation_action.get("id", "")))
|
||||||
)
|
var animation_action_available: bool = (
|
||||||
var running_threshold := fastest_non_sprint_speed + 0.5
|
not animation_action_id.is_empty()
|
||||||
var is_running := (
|
and supports_network_animation_action(animation_action_id)
|
||||||
is_walking
|
and _character_animation_player.has_animation(animation_action_id)
|
||||||
and horizontal_speed_squared > running_threshold * running_threshold
|
|
||||||
)
|
)
|
||||||
var held_show_item_visible: bool = (
|
var held_show_item_visible: bool = (
|
||||||
_held_fish_visible or _held_art_kit_visible
|
_held_fish_visible or _held_art_kit_visible
|
||||||
|
|
@ -878,6 +961,8 @@ func _update_character_animation() -> void:
|
||||||
CHARACTER_FISHING_ANIMATION,
|
CHARACTER_FISHING_ANIMATION,
|
||||||
CHARACTER_IDLE_ANIMATION,
|
CHARACTER_IDLE_ANIMATION,
|
||||||
]
|
]
|
||||||
|
elif animation_action_available:
|
||||||
|
requested_animation = [animation_action_id]
|
||||||
elif _sitting:
|
elif _sitting:
|
||||||
if held_show_item_visible:
|
if held_show_item_visible:
|
||||||
requested_animation = [
|
requested_animation = [
|
||||||
|
|
@ -934,13 +1019,45 @@ func _update_character_animation() -> void:
|
||||||
break
|
break
|
||||||
if next_animation.is_empty():
|
if next_animation.is_empty():
|
||||||
return
|
return
|
||||||
if _character_animation_name == next_animation:
|
var action_selected: bool = (
|
||||||
|
animation_action_available and next_animation == animation_action_id
|
||||||
|
)
|
||||||
|
var action_sequence: int = int(animation_action.get("sequence", 0))
|
||||||
|
var action_changed: bool = (
|
||||||
|
action_selected
|
||||||
|
and (
|
||||||
|
animation_action_id != _presented_animation_action_id
|
||||||
|
or action_sequence != _presented_animation_action_sequence
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not action_selected:
|
||||||
|
_presented_animation_action_id = &""
|
||||||
|
_presented_animation_action_sequence = -1
|
||||||
|
if _character_animation_name == next_animation and not action_changed:
|
||||||
return
|
return
|
||||||
_character_animation_player.play(next_animation)
|
_character_animation_player.play(next_animation)
|
||||||
_character_animation_name = next_animation
|
_character_animation_name = next_animation
|
||||||
|
if action_selected:
|
||||||
|
_presented_animation_action_id = animation_action_id
|
||||||
|
_presented_animation_action_sequence = action_sequence
|
||||||
|
var elapsed: float = float(animation_action.get("elapsed", 0.0))
|
||||||
|
var animation: Animation = _character_animation_player.get_animation(
|
||||||
|
next_animation
|
||||||
|
)
|
||||||
|
if elapsed > 0.0 and animation != null and animation.length > 0.0:
|
||||||
|
_character_animation_player.seek(
|
||||||
|
fposmod(elapsed, animation.length), true
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _on_character_animation_finished(animation_name: StringName) -> void:
|
func _on_character_animation_finished(animation_name: StringName) -> void:
|
||||||
|
if (
|
||||||
|
local_control_enabled
|
||||||
|
and not _animation_action_id.is_empty()
|
||||||
|
and animation_name == _animation_action_id
|
||||||
|
):
|
||||||
|
end_animation_action()
|
||||||
|
return
|
||||||
if animation_name in [
|
if animation_name in [
|
||||||
CHARACTER_POCKET_IDLE_IDLE_ANIMATION,
|
CHARACTER_POCKET_IDLE_IDLE_ANIMATION,
|
||||||
CHARACTER_POCKET_IDLE_SHOW_ANIMATION,
|
CHARACTER_POCKET_IDLE_SHOW_ANIMATION,
|
||||||
|
|
@ -1278,6 +1395,11 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
|
||||||
_network_interpolation_enabled = not authoritative_simulation
|
_network_interpolation_enabled = not authoritative_simulation
|
||||||
_network_snapshot_ready = false
|
_network_snapshot_ready = false
|
||||||
_network_snapshot_age = 0.0
|
_network_snapshot_age = 0.0
|
||||||
|
_network_target_grounded = false
|
||||||
|
_network_target_locomotion_state = LocomotionState.IDLE
|
||||||
|
_network_target_animation_action_id = &""
|
||||||
|
_network_target_animation_action_sequence = 0
|
||||||
|
_network_target_animation_action_elapsed = 0.0
|
||||||
_camera.current = false
|
_camera.current = false
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1301,6 +1423,7 @@ func capture_network_input(sequence: int) -> Dictionary:
|
||||||
"casting": (
|
"casting": (
|
||||||
_fishing_visual_phase == FishingVisualPhase.CASTING
|
_fishing_visual_phase == FishingVisualPhase.CASTING
|
||||||
),
|
),
|
||||||
|
"animation_action": _make_animation_action_state(),
|
||||||
}
|
}
|
||||||
var axis: Vector2 = Input.get_vector(
|
var axis: Vector2 = Input.get_vector(
|
||||||
"move_left",
|
"move_left",
|
||||||
|
|
@ -1318,6 +1441,7 @@ func capture_network_input(sequence: int) -> Dictionary:
|
||||||
"slow_walk": Input.is_action_pressed("slow_walk"),
|
"slow_walk": Input.is_action_pressed("slow_walk"),
|
||||||
"sitting": _sitting,
|
"sitting": _sitting,
|
||||||
"casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
|
"casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
|
||||||
|
"animation_action": _make_animation_action_state(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1335,6 +1459,7 @@ func apply_authoritative_network_input(data: Dictionary) -> void:
|
||||||
_network_sprint = bool(data.get("sprint", false))
|
_network_sprint = bool(data.get("sprint", false))
|
||||||
_network_sneak = bool(data.get("sneak", false))
|
_network_sneak = bool(data.get("sneak", false))
|
||||||
_network_slow_walk = bool(data.get("slow_walk", false))
|
_network_slow_walk = bool(data.get("slow_walk", false))
|
||||||
|
_apply_animation_action_state(data.get("animation_action", {}))
|
||||||
_apply_network_casting(bool(data.get("casting", false)))
|
_apply_network_casting(bool(data.get("casting", false)))
|
||||||
var sitting_requested: bool = bool(data.get("sitting", false))
|
var sitting_requested: bool = bool(data.get("sitting", false))
|
||||||
if sitting_requested and not is_on_floor():
|
if sitting_requested and not is_on_floor():
|
||||||
|
|
@ -1352,7 +1477,13 @@ func make_network_snapshot(peer_id: int) -> Dictionary:
|
||||||
"position": [global_position.x, global_position.y, global_position.z],
|
"position": [global_position.x, global_position.y, global_position.z],
|
||||||
"velocity": [velocity.x, velocity.y, velocity.z],
|
"velocity": [velocity.x, velocity.y, velocity.z],
|
||||||
"visual_yaw": _visuals.rotation.y,
|
"visual_yaw": _visuals.rotation.y,
|
||||||
"grounded": is_on_floor(),
|
"animation_state": NetworkPlayerAnimationProtocol.make_state(
|
||||||
|
_get_authoritative_locomotion_id(),
|
||||||
|
is_on_floor(),
|
||||||
|
_animation_action_id,
|
||||||
|
_animation_action_sequence,
|
||||||
|
_animation_action_elapsed,
|
||||||
|
),
|
||||||
"sitting": _sitting,
|
"sitting": _sitting,
|
||||||
"casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
|
"casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
|
||||||
}
|
}
|
||||||
|
|
@ -1365,6 +1496,7 @@ func push_network_snapshot(snapshot: Dictionary) -> void:
|
||||||
_network_target_position = parsed["position"]
|
_network_target_position = parsed["position"]
|
||||||
_network_target_velocity = parsed["velocity"]
|
_network_target_velocity = parsed["velocity"]
|
||||||
_network_target_visual_yaw = parsed["visual_yaw"]
|
_network_target_visual_yaw = parsed["visual_yaw"]
|
||||||
|
_apply_network_target_animation_state(parsed["animation_state"])
|
||||||
_network_snapshot_age = 0.0
|
_network_snapshot_age = 0.0
|
||||||
_apply_network_casting(bool(parsed["casting"]))
|
_apply_network_casting(bool(parsed["casting"]))
|
||||||
_set_sitting(bool(parsed["sitting"]))
|
_set_sitting(bool(parsed["sitting"]))
|
||||||
|
|
@ -1412,6 +1544,7 @@ func apply_network_teleport(snapshot: Dictionary) -> void:
|
||||||
_network_target_position = global_position
|
_network_target_position = global_position
|
||||||
_network_target_velocity = velocity
|
_network_target_velocity = velocity
|
||||||
_network_target_visual_yaw = _visuals.rotation.y
|
_network_target_visual_yaw = _visuals.rotation.y
|
||||||
|
_apply_network_target_animation_state(parsed["animation_state"])
|
||||||
_network_snapshot_age = 0.0
|
_network_snapshot_age = 0.0
|
||||||
_network_snapshot_ready = true
|
_network_snapshot_ready = true
|
||||||
|
|
||||||
|
|
@ -1437,6 +1570,12 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||||
float(network_velocity[2])
|
float(network_velocity[2])
|
||||||
)
|
)
|
||||||
var visual_yaw: float = float(snapshot.get("visual_yaw", 0.0))
|
var visual_yaw: float = float(snapshot.get("visual_yaw", 0.0))
|
||||||
|
var animation_state_value: Variant = snapshot.get("animation_state")
|
||||||
|
if not NetworkPlayerAnimationProtocol.validate_state(animation_state_value):
|
||||||
|
return {}
|
||||||
|
var animation_state: Dictionary = (
|
||||||
|
animation_state_value as Dictionary
|
||||||
|
).duplicate(true)
|
||||||
if (
|
if (
|
||||||
snapshot.has("acknowledged_input")
|
snapshot.has("acknowledged_input")
|
||||||
and (
|
and (
|
||||||
|
|
@ -1464,12 +1603,119 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||||
"position": parsed_position,
|
"position": parsed_position,
|
||||||
"velocity": parsed_velocity,
|
"velocity": parsed_velocity,
|
||||||
"visual_yaw": visual_yaw,
|
"visual_yaw": visual_yaw,
|
||||||
|
"animation_state": animation_state,
|
||||||
"acknowledged_input": acknowledged_input,
|
"acknowledged_input": acknowledged_input,
|
||||||
"sitting": sitting,
|
"sitting": sitting,
|
||||||
"casting": casting,
|
"casting": casting,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _make_animation_action_state() -> Dictionary:
|
||||||
|
return NetworkPlayerAnimationProtocol.make_action_state(
|
||||||
|
_animation_action_id,
|
||||||
|
_animation_action_sequence,
|
||||||
|
_animation_action_elapsed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_network_target_animation_state(state: Dictionary) -> void:
|
||||||
|
if not NetworkPlayerAnimationProtocol.validate_state(state):
|
||||||
|
return
|
||||||
|
_network_target_grounded = bool(state["grounded"])
|
||||||
|
_network_target_locomotion_state = _locomotion_state_from_id(
|
||||||
|
StringName(str(state["locomotion_id"]))
|
||||||
|
)
|
||||||
|
var action: Dictionary = state["action"]
|
||||||
|
_network_target_animation_action_id = StringName(str(action["id"]))
|
||||||
|
_network_target_animation_action_sequence = int(action["sequence"])
|
||||||
|
_network_target_animation_action_elapsed = float(action["elapsed"])
|
||||||
|
|
||||||
|
|
||||||
|
func _get_authoritative_locomotion_state() -> LocomotionState:
|
||||||
|
if (
|
||||||
|
_network_authoritative_simulation
|
||||||
|
and _is_movement_input_enabled()
|
||||||
|
and not _water_recovery_active
|
||||||
|
and not _sitting
|
||||||
|
and _network_axis.length_squared() > 0.0025
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
LocomotionState.RUNNING
|
||||||
|
if _network_sprint
|
||||||
|
else LocomotionState.WALKING
|
||||||
|
)
|
||||||
|
return _locomotion_state_from_velocity()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_authoritative_locomotion_id() -> StringName:
|
||||||
|
return _locomotion_id_from_state(_get_authoritative_locomotion_state())
|
||||||
|
|
||||||
|
|
||||||
|
func _get_presented_locomotion_state() -> LocomotionState:
|
||||||
|
if _network_interpolation_enabled and _network_snapshot_ready:
|
||||||
|
return _network_target_locomotion_state
|
||||||
|
return _locomotion_state_from_velocity()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_presented_animation_action() -> Dictionary:
|
||||||
|
if _network_interpolation_enabled and _network_snapshot_ready:
|
||||||
|
return NetworkPlayerAnimationProtocol.make_action_state(
|
||||||
|
_network_target_animation_action_id,
|
||||||
|
_network_target_animation_action_sequence,
|
||||||
|
minf(
|
||||||
|
_network_target_animation_action_elapsed + _network_snapshot_age,
|
||||||
|
NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return _make_animation_action_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _locomotion_id_from_state(state: LocomotionState) -> StringName:
|
||||||
|
match state:
|
||||||
|
LocomotionState.WALKING:
|
||||||
|
return NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING
|
||||||
|
LocomotionState.RUNNING:
|
||||||
|
return NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING
|
||||||
|
_:
|
||||||
|
return NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE
|
||||||
|
|
||||||
|
|
||||||
|
func _locomotion_state_from_id(state_id: StringName) -> LocomotionState:
|
||||||
|
match state_id:
|
||||||
|
NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING:
|
||||||
|
return LocomotionState.WALKING
|
||||||
|
NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING:
|
||||||
|
return LocomotionState.RUNNING
|
||||||
|
_:
|
||||||
|
return LocomotionState.IDLE
|
||||||
|
|
||||||
|
|
||||||
|
static func supports_network_animation_action(action_id: StringName) -> bool:
|
||||||
|
return action_id in NETWORK_ANIMATION_ACTION_IDS
|
||||||
|
|
||||||
|
|
||||||
|
func _locomotion_state_from_velocity() -> LocomotionState:
|
||||||
|
var horizontal_speed_squared: float = (
|
||||||
|
velocity.x * velocity.x + velocity.z * velocity.z
|
||||||
|
)
|
||||||
|
if horizontal_speed_squared <= 0.0025:
|
||||||
|
return LocomotionState.IDLE
|
||||||
|
var fastest_non_sprint_speed := maxf(
|
||||||
|
walk_speed,
|
||||||
|
maxf(sneak_speed, slow_walk_speed)
|
||||||
|
)
|
||||||
|
var running_threshold := fastest_non_sprint_speed + 0.5
|
||||||
|
if horizontal_speed_squared > running_threshold * running_threshold:
|
||||||
|
return LocomotionState.RUNNING
|
||||||
|
return LocomotionState.WALKING
|
||||||
|
|
||||||
|
|
||||||
|
func _get_presented_grounded() -> bool:
|
||||||
|
if _network_interpolation_enabled and _network_snapshot_ready:
|
||||||
|
return _network_target_grounded
|
||||||
|
return is_on_floor()
|
||||||
|
|
||||||
|
|
||||||
func _apply_network_casting(casting: bool) -> void:
|
func _apply_network_casting(casting: bool) -> void:
|
||||||
if casting:
|
if casting:
|
||||||
if _fishing_visual_phase == FishingVisualPhase.NONE:
|
if _fishing_visual_phase == FishingVisualPhase.NONE:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ config_version=5
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="NETFISHING"
|
config/name="NETFISHING"
|
||||||
config/version="0.6.8-alpha"
|
config/version="0.7.1-alpha"
|
||||||
run/main_scene="res://main/main.tscn"
|
run/main_scene="res://main/main.tscn"
|
||||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
config/icon="res://art/exported/system_icons/netfishing_256.png"
|
config/icon="res://art/exported/system_icons/netfishing_256.png"
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ set -euo pipefail
|
||||||
|
|
||||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||||
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.6.8-alpha"
|
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.7.1-alpha"
|
||||||
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
|
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
|
||||||
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
||||||
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
||||||
readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
||||||
readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/netfishing"
|
readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/netfishing"
|
||||||
readonly WINDOWS_ZIP="${BUILD_ROOT}/NETfishing-v0.6.8-alpha-windows-x86_64.zip"
|
readonly WINDOWS_ZIP="${BUILD_ROOT}/NETfishing-v0.7.1-alpha-windows-x86_64.zip"
|
||||||
readonly LINUX_ZIP="${BUILD_ROOT}/NETfishing-v0.6.8-alpha-linux-x86_64.zip"
|
readonly LINUX_ZIP="${BUILD_ROOT}/NETfishing-v0.7.1-alpha-linux-x86_64.zip"
|
||||||
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
||||||
|
|
||||||
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
|
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ readonly -a QUICK_TESTS=(
|
||||||
"tests/fishing_surface_validation.gd"
|
"tests/fishing_surface_validation.gd"
|
||||||
"tests/fur_pattern_validation.gd"
|
"tests/fur_pattern_validation.gd"
|
||||||
"tests/logbook_validation.gd"
|
"tests/logbook_validation.gd"
|
||||||
|
"tests/network_player_animation_protocol_validation.gd"
|
||||||
"tests/player_experience_validation.gd"
|
"tests/player_experience_validation.gd"
|
||||||
"tests/shoreline_ambience_validation.gd"
|
"tests/shoreline_ambience_validation.gd"
|
||||||
"tests/surface_drawing_validation.gd"
|
"tests/surface_drawing_validation.gd"
|
||||||
|
|
@ -49,6 +50,7 @@ readonly -a NETWORK_TESTS=(
|
||||||
"tests/fish_showcase_multiplayer_validation.gd"
|
"tests/fish_showcase_multiplayer_validation.gd"
|
||||||
"tests/fishing_multiplayer_validation.gd"
|
"tests/fishing_multiplayer_validation.gd"
|
||||||
"tests/job_multiplayer_validation.gd"
|
"tests/job_multiplayer_validation.gd"
|
||||||
|
"tests/movement_multiplayer_validation.gd"
|
||||||
"tests/operator_multiplayer_validation.gd"
|
"tests/operator_multiplayer_validation.gd"
|
||||||
"tests/surface_drawing_multiplayer_validation.gd"
|
"tests/surface_drawing_multiplayer_validation.gd"
|
||||||
"tests/world_time_multiplayer_validation.gd"
|
"tests/world_time_multiplayer_validation.gd"
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ func _run() -> void:
|
||||||
var invalid_state: Dictionary = valid_state.duplicate(true)
|
var invalid_state: Dictionary = valid_state.duplicate(true)
|
||||||
invalid_state["display_scale"] = 1000.0
|
invalid_state["display_scale"] = 1000.0
|
||||||
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 3)
|
assert(NetworkProtocol.PROTOCOL_VERSION == 4)
|
||||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||||
assert(
|
assert(
|
||||||
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ func _run() -> void:
|
||||||
_validate_mail_round_trip()
|
_validate_mail_round_trip()
|
||||||
_validate_collection_mastery()
|
_validate_collection_mastery()
|
||||||
_validate_version_four_migration()
|
_validate_version_four_migration()
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 3)
|
assert(NetworkProtocol.PROTOCOL_VERSION == 4)
|
||||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||||
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
|
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
|
||||||
print("Fish quality validation: PASS")
|
print("Fish quality validation: PASS")
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ func _run() -> void:
|
||||||
"%NetworkFishingService"
|
"%NetworkFishingService"
|
||||||
) as NetworkFishingService
|
) as NetworkFishingService
|
||||||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
|
var game_ui := main.get_node("UIPresentation/UIViewport/GameUI")
|
||||||
|
var fishing_status := game_ui.get_node("%StatusLabel") as Label
|
||||||
|
var fishing_panel := game_ui.get_node("%FishingPanel") as PanelContainer
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
assert(session.is_host())
|
assert(session.is_host())
|
||||||
assert(not session.is_open_host())
|
assert(not session.is_open_host())
|
||||||
|
|
@ -71,6 +74,9 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
|
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
|
||||||
assert(service.has_local_attempt())
|
assert(service.has_local_attempt())
|
||||||
|
assert(fishing_status.text.is_empty())
|
||||||
|
assert(not fishing_status.visible)
|
||||||
|
assert(not fishing_panel.visible)
|
||||||
var attempts: Dictionary = service.get("_attempts")
|
var attempts: Dictionary = service.get("_attempts")
|
||||||
var attempt: NetworkFishingAttempt = attempts.get(session.get_local_peer_id())
|
var attempt: NetworkFishingAttempt = attempts.get(session.get_local_peer_id())
|
||||||
assert(attempt != null)
|
assert(attempt != null)
|
||||||
|
|
@ -164,9 +170,12 @@ func _run() -> void:
|
||||||
<= FishQuality.BARRIER_HEALTH_MAXIMUMS[fish_catch.quality]
|
<= FishQuality.BARRIER_HEALTH_MAXIMUMS[fish_catch.quality]
|
||||||
)
|
)
|
||||||
reference_controller.queue_free()
|
reference_controller.queue_free()
|
||||||
service.call("_cancel_attempt", session.get_local_peer_id(), "")
|
service.call("_on_attempt_escaped", session.get_local_peer_id())
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(not service.has_local_attempt())
|
assert(not service.has_local_attempt())
|
||||||
|
assert(fishing_status.text.is_empty())
|
||||||
|
assert(not fishing_status.visible)
|
||||||
|
assert(not fishing_panel.visible)
|
||||||
|
|
||||||
print("Fishing authority validation: PASS")
|
print("Fishing authority validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
|
|
|
||||||
|
|
@ -147,12 +147,6 @@ func _run_host() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(remote_animation_player.current_animation == &"retract_sit")
|
assert(remote_animation_player.current_animation == &"retract_sit")
|
||||||
|
|
||||||
var completion_deadline: int = Time.get_ticks_msec() + 12000
|
|
||||||
while (
|
|
||||||
Time.get_ticks_msec() < completion_deadline
|
|
||||||
and session.get_authenticated_peer_ids().size() == 2
|
|
||||||
):
|
|
||||||
await process_frame
|
|
||||||
print("Fishing multiplayer host validation: PASS")
|
print("Fishing multiplayer host validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
@ -337,7 +331,14 @@ func _run_client() -> void:
|
||||||
while Time.get_ticks_msec() < host_observation_deadline:
|
while Time.get_ticks_msec() < host_observation_deadline:
|
||||||
await process_frame
|
await process_frame
|
||||||
print("Fishing multiplayer client validation: PASS")
|
print("Fishing multiplayer client validation: PASS")
|
||||||
session.disconnect_session("")
|
var host_completion_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < host_completion_deadline
|
||||||
|
and session.is_joined_client()
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
if session.is_joined_client():
|
||||||
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
for _frame: int in 4:
|
for _frame: int in 4:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,12 @@ func _run() -> void:
|
||||||
func _run_host() -> void:
|
func _run_host() -> void:
|
||||||
var main: Node = await _create_initialized_main()
|
var main: Node = await _create_initialized_main()
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
|
||||||
var jobs := main.get_node("%PlayerJobService") as PlayerJobService
|
var jobs := main.get_node("%PlayerJobService") as PlayerJobService
|
||||||
assert(session.start_private_host(TEST_PORT))
|
assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
|
||||||
assert(save_manager.initialize_new_game())
|
jobs.begin_progression_session()
|
||||||
main.call("_enter_gameplay")
|
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(session.set_host_open(true))
|
assert(session.set_host_open(true))
|
||||||
|
assert(session.is_dedicated_host())
|
||||||
var host_board: Dictionary = jobs.get_host_board_network_data()
|
var host_board: Dictionary = jobs.get_host_board_network_data()
|
||||||
assert(PlayerJobService.validate_board(host_board))
|
assert(PlayerJobService.validate_board(host_board))
|
||||||
|
|
||||||
|
|
@ -87,6 +86,20 @@ func _run_client() -> void:
|
||||||
assert(not jobs.get_plan_id().is_empty())
|
assert(not jobs.get_plan_id().is_empty())
|
||||||
assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
|
assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
|
||||||
assert(jobs.get_forecast().size() == JobCatalog.WEATHER_SEGMENT_COUNT)
|
assert(jobs.get_forecast().size() == JobCatalog.WEATHER_SEGMENT_COUNT)
|
||||||
|
var preserved_plan_id: String = jobs.get_plan_id()
|
||||||
|
jobs.clear_remote_board()
|
||||||
|
assert(not jobs.has_active_board())
|
||||||
|
assert(jobs.get_plan_id() == preserved_plan_id)
|
||||||
|
var retry_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < retry_deadline
|
||||||
|
and not jobs.has_active_board()
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(jobs.has_active_board())
|
||||||
|
assert(jobs.get_plan_id() == preserved_plan_id)
|
||||||
|
assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
|
||||||
|
assert(jobs.get_forecast().size() == JobCatalog.WEATHER_SEGMENT_COUNT)
|
||||||
assert(weather.get_daily_plan_id().is_empty())
|
assert(weather.get_daily_plan_id().is_empty())
|
||||||
var game_ui := main.get_node("%GameUI") as GameUI
|
var game_ui := main.get_node("%GameUI") as GameUI
|
||||||
var player_menu := game_ui.get("_player_menu") as PlayerMenu
|
var player_menu := game_ui.get("_player_menu") as PlayerMenu
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ func _run() -> void:
|
||||||
(player_menu.get_node("%NavigationCluster") as Control).get_child_count()
|
(player_menu.get_node("%NavigationCluster") as Control).get_child_count()
|
||||||
== 7
|
== 7
|
||||||
)
|
)
|
||||||
|
await _validate_unavailable_fishnet_layout()
|
||||||
|
|
||||||
var wallet_before: int = player.wallet.get_balance()
|
var wallet_before: int = player.wallet.get_balance()
|
||||||
var experience_before: int = player.experience.get_total_experience()
|
var experience_before: int = player.experience.get_total_experience()
|
||||||
|
|
@ -162,6 +163,24 @@ func _run() -> void:
|
||||||
var pause_menu := game_ui.get_pause_menu()
|
var pause_menu := game_ui.get_pause_menu()
|
||||||
var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage
|
var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage
|
||||||
assert(join_page != null)
|
assert(join_page != null)
|
||||||
|
pause_menu.show()
|
||||||
|
join_page.set_status("Connection timed out.")
|
||||||
|
join_page.show()
|
||||||
|
join_page.call(
|
||||||
|
"_on_discovery_status_changed", "2 public rooms found", false
|
||||||
|
)
|
||||||
|
var join_status := join_page.get_node("%Status") as Label
|
||||||
|
assert(join_status.text == "Could not reach the server.")
|
||||||
|
join_page.hide()
|
||||||
|
join_page.call("_set_mode", JoinGamePage.Mode.DIRECT)
|
||||||
|
join_page.call("_set_mode", JoinGamePage.Mode.DISCOVER)
|
||||||
|
join_page.show()
|
||||||
|
join_page.call(
|
||||||
|
"_on_discovery_status_changed", "2 public rooms found", false
|
||||||
|
)
|
||||||
|
assert(join_status.text == "2 public rooms found")
|
||||||
|
join_page.close_page()
|
||||||
|
pause_menu.hide()
|
||||||
join_page.call("_refresh")
|
join_page.call("_refresh")
|
||||||
var session_summary := (
|
var session_summary := (
|
||||||
join_page.get_node("%SessionSummary") as Label
|
join_page.get_node("%SessionSummary") as Label
|
||||||
|
|
@ -187,14 +206,63 @@ func _run() -> void:
|
||||||
assert(int(save_data.get("save_version", -1)) == 7)
|
assert(int(save_data.get("save_version", -1)) == 7)
|
||||||
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
||||||
|
|
||||||
session.disconnect_session("Job system validation complete.")
|
_validate_pause_session_switch(main, session)
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
for _frame: int in 4:
|
for _frame: int in 4:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
print("Job system validation: PASS")
|
print("Job system validation: PASS")
|
||||||
quit()
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_pause_session_switch(
|
||||||
|
main: Node,
|
||||||
|
session: NetworkSession,
|
||||||
|
) -> void:
|
||||||
|
var discovery := main.get_node("%DiscoveryClient") as DiscoveryClient
|
||||||
|
discovery.set("_pending_join_endpoint", "127.0.0.1:18141")
|
||||||
|
discovery.set("_pending_join_room_id", "session-switch-room")
|
||||||
|
discovery.set("_pending_join_token", "session-switch-token")
|
||||||
|
discovery.call(
|
||||||
|
"_set_public_join_state",
|
||||||
|
DiscoveryClient.PublicJoinState.CONNECTING,
|
||||||
|
)
|
||||||
|
main.call("_on_pause_join_game_requested", "127.0.0.1:18141")
|
||||||
|
assert(session.state == NetworkSession.State.CONNECTING)
|
||||||
|
assert(discovery.get("_pending_join_token") == "session-switch-token")
|
||||||
|
var join_probe_timer := discovery.get("_join_probe_timer") as Timer
|
||||||
|
assert(join_probe_timer != null and not join_probe_timer.is_stopped())
|
||||||
|
session.cancel_connection()
|
||||||
|
assert(session.state == NetworkSession.State.INACTIVE)
|
||||||
|
assert(str(discovery.get("_pending_join_token")).is_empty())
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_unavailable_fishnet_layout() -> void:
|
||||||
|
var unavailable_page := TheNetPage.new()
|
||||||
|
unavailable_page.size = Vector2(1280.0, 720.0)
|
||||||
|
root.add_child(unavailable_page)
|
||||||
|
for _frame: int in 3:
|
||||||
|
await process_frame
|
||||||
|
unavailable_page.call("_refresh_forecast", true)
|
||||||
|
await process_frame
|
||||||
|
var forecast_list := unavailable_page.get("_forecast_list") as HBoxContainer
|
||||||
|
assert(forecast_list != null)
|
||||||
|
assert(forecast_list.size.x >= 272.0)
|
||||||
|
assert(forecast_list.size.y <= 66.0)
|
||||||
|
assert(forecast_list.get_child_count() == 1)
|
||||||
|
var placeholder := forecast_list.get_child(0) as Label
|
||||||
|
assert(placeholder != null)
|
||||||
|
assert(placeholder.size.x >= 252.0)
|
||||||
|
assert(placeholder.size.y <= 66.0)
|
||||||
|
assert(placeholder.autowrap_mode == TextServer.AUTOWRAP_OFF)
|
||||||
|
var tabs := unavailable_page.get("_tabs") as HBoxContainer
|
||||||
|
assert(tabs != null)
|
||||||
|
assert(tabs.position.y + tabs.size.y < UtilityPageStyle.LAPTOP_RECT.end.y)
|
||||||
|
unavailable_page.queue_free()
|
||||||
|
for _frame: int in 2:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
func _validate_weather_guarantees(board: Dictionary) -> void:
|
func _validate_weather_guarantees(board: Dictionary) -> void:
|
||||||
var jobs: Array = board.get("jobs", [])
|
var jobs: Array = board.get("jobs", [])
|
||||||
var schedule: Array = board.get("weather_schedule", [])
|
var schedule: Array = board.get("weather_schedule", [])
|
||||||
|
|
|
||||||
163
tests/movement_multiplayer_validation.gd
Normal file
163
tests/movement_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||||
|
const TEST_PORT: int = 18141
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||||
|
if arguments.has("host"):
|
||||||
|
await _run_host()
|
||||||
|
return
|
||||||
|
if arguments.has("client"):
|
||||||
|
await _run_client()
|
||||||
|
return
|
||||||
|
push_error("Movement multiplayer validation needs host or client mode.")
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_host() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
|
assert(save_manager.initialize_new_game())
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
for _frame: int in 4:
|
||||||
|
await physics_frame
|
||||||
|
assert(session.set_host_open(true))
|
||||||
|
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||||
|
assert(remote_peer_id > 1)
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
player.configure_network_remote(true)
|
||||||
|
player.apply_authoritative_network_input(_movement_input(1, true))
|
||||||
|
await create_timer(2.5).timeout
|
||||||
|
player.apply_authoritative_network_input(_movement_input(2, false))
|
||||||
|
await create_timer(2.5).timeout
|
||||||
|
player.apply_authoritative_network_input(_movement_input(3, false, false))
|
||||||
|
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
|
and session.is_authenticated_peer(remote_peer_id)
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(not session.is_authenticated_peer(remote_peer_id))
|
||||||
|
print("Movement multiplayer host validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_client() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
main.call(
|
||||||
|
"_on_title_join_game_requested",
|
||||||
|
"127.0.0.1:%d" % TEST_PORT,
|
||||||
|
)
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < join_deadline:
|
||||||
|
await process_frame
|
||||||
|
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||||
|
main.call("_confirm_server_trust")
|
||||||
|
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||||
|
break
|
||||||
|
assert(session.is_joined_client())
|
||||||
|
var spawn_service := main.get_node(
|
||||||
|
"%PlayerSpawnService"
|
||||||
|
) as PlayerSpawnService
|
||||||
|
var host_avatar: Player = spawn_service.get_avatar(1)
|
||||||
|
assert(host_avatar != null)
|
||||||
|
var animation_player := host_avatar.get_node(
|
||||||
|
"Visuals/CharacterRig/AnimationPlayer"
|
||||||
|
) as AnimationPlayer
|
||||||
|
var sprint_dust := host_avatar.get_node("%SprintDust") as SprintDustTrail
|
||||||
|
var saw_running: bool = false
|
||||||
|
var saw_walking: bool = false
|
||||||
|
var saw_animation_advance: bool = false
|
||||||
|
var saw_dust: bool = false
|
||||||
|
var previous_animation: StringName = &""
|
||||||
|
var previous_animation_position: float = -1.0
|
||||||
|
var observation_deadline: int = Time.get_ticks_msec() + 7000
|
||||||
|
while Time.get_ticks_msec() < observation_deadline:
|
||||||
|
await process_frame
|
||||||
|
var current_animation: StringName = animation_player.current_animation
|
||||||
|
saw_running = saw_running or current_animation.begins_with("running")
|
||||||
|
saw_walking = saw_walking or current_animation.begins_with("walking")
|
||||||
|
var current_position: float = (
|
||||||
|
animation_player.current_animation_position
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
current_animation == previous_animation
|
||||||
|
and previous_animation_position >= 0.0
|
||||||
|
and absf(current_position - previous_animation_position) > 0.001
|
||||||
|
):
|
||||||
|
saw_animation_advance = true
|
||||||
|
previous_animation = current_animation
|
||||||
|
previous_animation_position = current_position
|
||||||
|
saw_dust = saw_dust or sprint_dust.get_active_puff_count() > 0
|
||||||
|
if saw_running and saw_walking and saw_animation_advance and saw_dust:
|
||||||
|
break
|
||||||
|
assert(saw_running)
|
||||||
|
assert(saw_walking)
|
||||||
|
assert(saw_animation_advance)
|
||||||
|
assert(saw_dust)
|
||||||
|
print("Movement multiplayer client validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _movement_input(
|
||||||
|
sequence: int,
|
||||||
|
sprinting: bool,
|
||||||
|
moving: bool = true,
|
||||||
|
) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"sequence": sequence,
|
||||||
|
"axis": [0.0, -1.0] if moving else [0.0, 0.0],
|
||||||
|
"camera_yaw": 0.0,
|
||||||
|
"jump": false,
|
||||||
|
"sprint": sprinting,
|
||||||
|
"sneak": false,
|
||||||
|
"slow_walk": false,
|
||||||
|
"sitting": false,
|
||||||
|
"casting": false,
|
||||||
|
"animation_action": (
|
||||||
|
NetworkPlayerAnimationProtocol.make_action_state()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _create_initialized_main() -> Node:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main: Node = MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_remote_peer(session: NetworkSession) -> int:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
for peer_id: int in session.get_authenticated_peer_ids():
|
||||||
|
if peer_id != session.get_local_peer_id():
|
||||||
|
return peer_id
|
||||||
|
return 0
|
||||||
1
tests/movement_multiplayer_validation.gd.uid
Normal file
1
tests/movement_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dlpu5lwb2pges
|
||||||
49
tests/network_player_animation_protocol_validation.gd
Normal file
49
tests/network_player_animation_protocol_validation.gd
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var idle := NetworkPlayerAnimationProtocol.make_state(
|
||||||
|
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
assert(NetworkPlayerAnimationProtocol.validate_state(idle))
|
||||||
|
var emote := NetworkPlayerAnimationProtocol.make_state(
|
||||||
|
NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING,
|
||||||
|
false,
|
||||||
|
&"wave_hello",
|
||||||
|
17,
|
||||||
|
1.25,
|
||||||
|
)
|
||||||
|
assert(NetworkPlayerAnimationProtocol.validate_state(emote))
|
||||||
|
var future_locomotion := NetworkPlayerAnimationProtocol.make_state(
|
||||||
|
&"swimming_fast",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
assert(NetworkPlayerAnimationProtocol.validate_state(future_locomotion))
|
||||||
|
var extra_future_field: Dictionary = emote.duplicate(true)
|
||||||
|
extra_future_field["future_layer"] = {"id": "umbrella"}
|
||||||
|
assert(NetworkPlayerAnimationProtocol.validate_state(extra_future_field))
|
||||||
|
|
||||||
|
var invalid_action: Dictionary = emote.duplicate(true)
|
||||||
|
invalid_action["action"] = {
|
||||||
|
"id": "../unsafe",
|
||||||
|
"sequence": 17,
|
||||||
|
"elapsed": 1.25,
|
||||||
|
}
|
||||||
|
assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_action))
|
||||||
|
var invalid_sequence: Dictionary = emote.duplicate(true)
|
||||||
|
invalid_sequence["action"] = {
|
||||||
|
"id": "wave_hello",
|
||||||
|
"sequence": -1,
|
||||||
|
"elapsed": 1.25,
|
||||||
|
}
|
||||||
|
assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_sequence))
|
||||||
|
var invalid_elapsed: Dictionary = emote.duplicate(true)
|
||||||
|
invalid_elapsed["action"] = {
|
||||||
|
"id": "wave_hello",
|
||||||
|
"sequence": 17,
|
||||||
|
"elapsed": INF,
|
||||||
|
}
|
||||||
|
assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_elapsed))
|
||||||
|
print("Network player animation protocol validation: PASS")
|
||||||
|
quit()
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cjgelp62dkykn
|
||||||
|
|
@ -88,6 +88,11 @@ func _run_host() -> void:
|
||||||
entry.revision,
|
entry.revision,
|
||||||
))
|
))
|
||||||
assert(not session.is_peer_operator(remote_peer_id))
|
assert(not session.is_peer_operator(remote_peer_id))
|
||||||
|
await create_timer(2.0).timeout
|
||||||
|
assert(session.kick_authenticated_peer(
|
||||||
|
remote_peer_id,
|
||||||
|
remote_record.identity_fingerprint,
|
||||||
|
))
|
||||||
|
|
||||||
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
while (
|
while (
|
||||||
|
|
@ -173,9 +178,25 @@ func _run_client() -> void:
|
||||||
assert(not (tabs.get_child(2) as Button).visible)
|
assert(not (tabs.get_child(2) as Button).visible)
|
||||||
assert(int(players_page.get("_current_tab")) == 0)
|
assert(int(players_page.get("_current_tab")) == 0)
|
||||||
service.request_unban.rpc_id(1, SECOND_BANNED_FINGERPRINT)
|
service.request_unban.rpc_id(1, SECOND_BANNED_FINGERPRINT)
|
||||||
await create_timer(1.0).timeout
|
var disconnect_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
|
and session.state != NetworkSession.State.SERVER_LOST
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(session.state == NetworkSession.State.SERVER_LOST)
|
||||||
|
assert(not bool(main.get("_gameplay_started")))
|
||||||
|
var game_ui := main.get_node("%GameUI") as GameUI
|
||||||
|
var title_screen := game_ui.get_title_screen()
|
||||||
|
assert(title_screen.visible)
|
||||||
|
assert(not title_screen.is_awaiting_start_input())
|
||||||
|
assert((title_screen.get_node("%Center") as Control).visible)
|
||||||
|
assert((title_screen.get_node("%ButtonCenter") as Control).visible)
|
||||||
|
assert(not (title_screen.get_node("%JoinGamePage") as Control).visible)
|
||||||
|
var feedback := title_screen.get_node("%FeedbackLabel") as RichTextLabel
|
||||||
|
assert(feedback.visible)
|
||||||
|
assert("removed by the host" in feedback.text.to_lower())
|
||||||
print("Operator multiplayer client validation: PASS")
|
print("Operator multiplayer client validation: PASS")
|
||||||
session.disconnect_session("")
|
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
for _frame: int in 4:
|
for _frame: int in 4:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ extends SceneTree
|
||||||
const MainScene = preload("res://main/main.tscn")
|
const MainScene = preload("res://main/main.tscn")
|
||||||
const TEST_PORT: int = 17983
|
const TEST_PORT: int = 17983
|
||||||
const INITIAL_HOST_TIME: float = 19.75
|
const INITIAL_HOST_TIME: float = 19.75
|
||||||
const UPDATED_HOST_TIME: float = 20.75
|
const UPDATED_HOST_TIME: float = WorldTimeService.DUSK_END_HOUR
|
||||||
const TIME_TOLERANCE_HOURS: float = 0.05
|
const TIME_TOLERANCE_HOURS: float = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -61,14 +61,31 @@ func _run_host() -> void:
|
||||||
assert(session.peer_supports_capability(
|
assert(session.peer_supports_capability(
|
||||||
remote_peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
remote_peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
))
|
))
|
||||||
|
var remote_record: PeerRegistry.PeerRecord = session.get_peer_record(
|
||||||
|
remote_peer_id
|
||||||
|
)
|
||||||
|
assert(remote_record != null and remote_record.identity_authenticated)
|
||||||
|
assert(session.set_peer_operator(
|
||||||
|
remote_peer_id,
|
||||||
|
remote_record.identity_fingerprint,
|
||||||
|
true,
|
||||||
|
))
|
||||||
assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
|
assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
|
||||||
|
|
||||||
await create_timer(1.0).timeout
|
var command_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
world_time.synchronize_time(UPDATED_HOST_TIME)
|
while (
|
||||||
|
Time.get_ticks_msec() < command_deadline
|
||||||
|
and _wrapped_time_difference(
|
||||||
|
world_time.get_time_hours(), UPDATED_HOST_TIME
|
||||||
|
) > TIME_TOLERANCE_HOURS
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
assert(is_equal_approx(
|
assert(is_equal_approx(
|
||||||
world_time.get_persistent_time_hours(), UPDATED_HOST_TIME
|
world_time.get_persistent_time_hours(), UPDATED_HOST_TIME
|
||||||
))
|
))
|
||||||
assert(chat_ui.call("_handle_chat_command", "/weather foggy"))
|
var fog_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < fog_deadline and not world_weather.is_foggy():
|
||||||
|
await process_frame
|
||||||
assert(
|
assert(
|
||||||
world_weather.get_persistent_weather()
|
world_weather.get_persistent_weather()
|
||||||
== WorldWeatherService.Weather.FOGGY
|
== WorldWeatherService.Weather.FOGGY
|
||||||
|
|
@ -153,6 +170,11 @@ func _run_client() -> void:
|
||||||
assert(is_equal_approx(clock_panel.position.y, 10.0))
|
assert(is_equal_approx(clock_panel.position.y, 10.0))
|
||||||
assert(is_equal_approx(weather_icon.position.y, 10.0))
|
assert(is_equal_approx(weather_icon.position.y, 10.0))
|
||||||
assert(clock_panel.position.y + clock_panel.size.y < chat_panel.position.y)
|
assert(clock_panel.position.y + clock_panel.size.y < chat_panel.position.y)
|
||||||
|
var operator_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < operator_deadline and not session.is_local_operator():
|
||||||
|
await process_frame
|
||||||
|
assert(session.is_local_operator())
|
||||||
|
assert(chat_ui.call("_handle_chat_command", "/time night"))
|
||||||
|
|
||||||
var update_deadline: int = Time.get_ticks_msec() + 10000
|
var update_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
while (
|
while (
|
||||||
|
|
@ -168,6 +190,8 @@ func _run_client() -> void:
|
||||||
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
||||||
assert(is_equal_approx(world_time.get_persistent_time_hours(), 15.25))
|
assert(is_equal_approx(world_time.get_persistent_time_hours(), 15.25))
|
||||||
assert(clock_label.text == world_time.get_clock_text())
|
assert(clock_label.text == world_time.get_clock_text())
|
||||||
|
await create_timer(0.6).timeout
|
||||||
|
assert(chat_ui.call("_handle_chat_command", "/weather foggy"))
|
||||||
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < fog_deadline
|
Time.get_ticks_msec() < fog_deadline
|
||||||
|
|
|
||||||
|
|
@ -456,8 +456,14 @@ func _validate_weather_presentation() -> void:
|
||||||
assert(rain.emitting)
|
assert(rain.emitting)
|
||||||
assert(rain.amount_ratio > 0.99)
|
assert(rain.amount_ratio > 0.99)
|
||||||
assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT)
|
assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT)
|
||||||
|
assert(rain.visibility_aabb == (
|
||||||
|
WorldTimeVisualControllerType.RAIN_VISIBILITY_AABB
|
||||||
|
))
|
||||||
var rain_material := rain.process_material as ParticleProcessMaterial
|
var rain_material := rain.process_material as ParticleProcessMaterial
|
||||||
assert(rain_material != null)
|
assert(rain_material != null)
|
||||||
|
assert(rain_material.emission_box_extents.is_equal_approx(
|
||||||
|
WorldTimeVisualControllerType.RAIN_EMISSION_EXTENTS
|
||||||
|
))
|
||||||
assert(is_equal_approx(
|
assert(is_equal_approx(
|
||||||
rain_material.initial_velocity_min,
|
rain_material.initial_velocity_min,
|
||||||
WorldTimeVisualControllerType.RAIN_VELOCITY_MIN,
|
WorldTimeVisualControllerType.RAIN_VELOCITY_MIN,
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ func setup(
|
||||||
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
||||||
_service.history_replaced.connect(_on_history)
|
_service.history_replaced.connect(_on_history)
|
||||||
_service.send_rejected.connect(_on_rejected)
|
_service.send_rejected.connect(_on_rejected)
|
||||||
|
_service.world_command_finished.connect(_on_world_command_finished)
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
_entry.text = _settings.current_settings.chat_draft
|
_entry.text = _settings.current_settings.chat_draft
|
||||||
_entry.caret_column = _entry.text.length()
|
_entry.caret_column = _entry.text.length()
|
||||||
|
|
@ -789,34 +790,16 @@ func _handle_time_command(parts: PackedStringArray) -> void:
|
||||||
if parts.size() != 2:
|
if parts.size() != 2:
|
||||||
_set_status("Usage: /time [dawn, day, dusk, night]")
|
_set_status("Usage: /time [dawn, day, dusk, night]")
|
||||||
return
|
return
|
||||||
if _session == null or not _session.is_host():
|
if _service == null or _world_time == null:
|
||||||
_set_status("Only the host can change world time.")
|
|
||||||
return
|
|
||||||
if _world_time == null:
|
|
||||||
_set_status("World time is unavailable.")
|
_set_status("World time is unavailable.")
|
||||||
return
|
return
|
||||||
var phase_name := String(parts[1]).to_lower()
|
var phase_name := String(parts[1]).to_lower()
|
||||||
var target_hour: float
|
if phase_name not in ["dawn", "day", "dusk", "night"]:
|
||||||
match phase_name:
|
_set_status("Usage: /time [dawn, day, dusk, night]")
|
||||||
"dawn":
|
return
|
||||||
target_hour = WorldTimeServiceType.DAWN_START_HOUR
|
if not _service.request_world_time_change(phase_name):
|
||||||
"day":
|
_set_status("Only the host or an operator can change world time.")
|
||||||
target_hour = WorldTimeServiceType.DAWN_END_HOUR
|
|
||||||
"dusk":
|
|
||||||
target_hour = WorldTimeServiceType.DUSK_START_HOUR
|
|
||||||
"night":
|
|
||||||
target_hour = WorldTimeServiceType.DUSK_END_HOUR
|
|
||||||
_:
|
|
||||||
_set_status("Usage: /time [dawn, day, dusk, night]")
|
|
||||||
return
|
|
||||||
if not _world_time.set_authoritative_time(target_hour):
|
|
||||||
_set_status("World time could not be changed.")
|
|
||||||
return
|
return
|
||||||
_set_status("")
|
|
||||||
_service.broadcast_system_message(
|
|
||||||
"World time set to %s (%s)."
|
|
||||||
% [phase_name, _world_time.get_clock_text()]
|
|
||||||
)
|
|
||||||
close_chat()
|
close_chat()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -824,37 +807,31 @@ func _handle_weather_command(parts: PackedStringArray) -> void:
|
||||||
if parts.size() != 2:
|
if parts.size() != 2:
|
||||||
_set_status("Usage: /weather [clear, cloudy, rainy, foggy]")
|
_set_status("Usage: /weather [clear, cloudy, rainy, foggy]")
|
||||||
return
|
return
|
||||||
if _session == null or not _session.is_host():
|
if _service == null or _world_weather == null:
|
||||||
_set_status("Only the host can change world weather.")
|
|
||||||
return
|
|
||||||
if _world_weather == null:
|
|
||||||
_set_status("World weather is unavailable.")
|
_set_status("World weather is unavailable.")
|
||||||
return
|
return
|
||||||
var weather_name := String(parts[1]).to_lower()
|
var weather_name := String(parts[1]).to_lower()
|
||||||
var target_weather: WorldWeatherServiceType.Weather
|
|
||||||
match weather_name:
|
match weather_name:
|
||||||
"clear", "sunny":
|
"clear", "sunny":
|
||||||
target_weather = WorldWeatherServiceType.Weather.SUNNY
|
|
||||||
weather_name = "clear"
|
weather_name = "clear"
|
||||||
"cloudy":
|
"cloudy", "rainy", "foggy":
|
||||||
target_weather = WorldWeatherServiceType.Weather.CLOUDY
|
pass
|
||||||
"rainy":
|
|
||||||
target_weather = WorldWeatherServiceType.Weather.RAINY
|
|
||||||
"foggy":
|
|
||||||
target_weather = WorldWeatherServiceType.Weather.FOGGY
|
|
||||||
_:
|
_:
|
||||||
_set_status("Usage: /weather [clear, cloudy, rainy, foggy]")
|
_set_status("Usage: /weather [clear, cloudy, rainy, foggy]")
|
||||||
return
|
return
|
||||||
if not _world_weather.set_authoritative_weather(target_weather):
|
if not _service.request_world_weather_change(weather_name):
|
||||||
_set_status("World weather could not be changed.")
|
_set_status("Only the host or an operator can change world weather.")
|
||||||
return
|
return
|
||||||
_set_status("")
|
|
||||||
_service.broadcast_system_message(
|
|
||||||
"World weather set to %s." % weather_name
|
|
||||||
)
|
|
||||||
close_chat()
|
close_chat()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_world_command_finished(success: bool, message: String) -> void:
|
||||||
|
if success:
|
||||||
|
_set_status("")
|
||||||
|
elif not message.is_empty():
|
||||||
|
_set_status(message)
|
||||||
|
|
||||||
|
|
||||||
func _on_local_message_confirmed(message: Dictionary) -> void:
|
func _on_local_message_confirmed(message: Dictionary) -> void:
|
||||||
if (
|
if (
|
||||||
not _send_pending
|
not _send_pending
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ var _discovery_refresh_timer: Timer
|
||||||
var _editing_entry_id: String = ""
|
var _editing_entry_id: String = ""
|
||||||
var _name_entry_active: bool = false
|
var _name_entry_active: bool = false
|
||||||
var _delete_armed: bool = false
|
var _delete_armed: bool = false
|
||||||
|
var _connection_error_latched: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -181,7 +182,7 @@ func open_page(preserved_endpoint: String = "") -> void:
|
||||||
_address.text = "127.0.0.1:7777"
|
_address.text = "127.0.0.1:7777"
|
||||||
show()
|
show()
|
||||||
UtilityPageStyle.animate_in(self)
|
UtilityPageStyle.animate_in(self)
|
||||||
_set_mode(_mode)
|
_set_mode(_mode, false)
|
||||||
if _mode == Mode.DIRECT:
|
if _mode == Mode.DIRECT:
|
||||||
_address.grab_focus()
|
_address.grab_focus()
|
||||||
_address.select_all()
|
_address.select_all()
|
||||||
|
|
@ -201,10 +202,13 @@ func get_endpoint_text() -> String:
|
||||||
|
|
||||||
|
|
||||||
func set_status(message: String) -> void:
|
func set_status(message: String) -> void:
|
||||||
|
_connection_error_latched = true
|
||||||
_set_status(_friendly_connection_message(message), true)
|
_set_status(_friendly_connection_message(message), true)
|
||||||
|
|
||||||
|
|
||||||
func _set_mode(mode: Mode) -> void:
|
func _set_mode(mode: Mode, clear_connection_error: bool = true) -> void:
|
||||||
|
if clear_connection_error:
|
||||||
|
_connection_error_latched = false
|
||||||
_mode = mode
|
_mode = mode
|
||||||
_selected_entry = null
|
_selected_entry = null
|
||||||
_selected_discovery_index = -1
|
_selected_discovery_index = -1
|
||||||
|
|
@ -225,6 +229,7 @@ func _set_mode(mode: Mode) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _request_join() -> void:
|
func _request_join() -> void:
|
||||||
|
_connection_error_latched = false
|
||||||
if _network_session.state in [
|
if _network_session.state in [
|
||||||
NetworkSession.State.CONNECTION_FAILED,
|
NetworkSession.State.CONNECTION_FAILED,
|
||||||
NetworkSession.State.SERVER_LOST,
|
NetworkSession.State.SERVER_LOST,
|
||||||
|
|
@ -267,6 +272,10 @@ func _on_public_join_prepared(endpoint_text: String) -> void:
|
||||||
|
|
||||||
func _on_public_join_status_changed(message: String, is_error: bool) -> void:
|
func _on_public_join_status_changed(message: String, is_error: bool) -> void:
|
||||||
if _mode == Mode.DISCOVER and is_visible_in_tree():
|
if _mode == Mode.DISCOVER and is_visible_in_tree():
|
||||||
|
if is_error:
|
||||||
|
_connection_error_latched = true
|
||||||
|
elif _connection_error_latched:
|
||||||
|
return
|
||||||
_set_status(message, is_error)
|
_set_status(message, is_error)
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
@ -708,7 +717,11 @@ func _on_discovery_rooms_updated(rooms: Array[Dictionary]) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_discovery_status_changed(message: String, is_error: bool) -> void:
|
func _on_discovery_status_changed(message: String, is_error: bool) -> void:
|
||||||
if _mode == Mode.DISCOVER and is_visible_in_tree():
|
if (
|
||||||
|
_mode == Mode.DISCOVER
|
||||||
|
and is_visible_in_tree()
|
||||||
|
and not _connection_error_latched
|
||||||
|
):
|
||||||
_set_status(message, is_error)
|
_set_status(message, is_error)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -740,6 +753,7 @@ func _friendly_connection_message(message: String) -> String:
|
||||||
return "Connection cancelled."
|
return "Connection cancelled."
|
||||||
if (
|
if (
|
||||||
"timeout" in normalized
|
"timeout" in normalized
|
||||||
|
or "timed out" in normalized
|
||||||
or "unavailable" in normalized
|
or "unavailable" in normalized
|
||||||
or "refused" in normalized
|
or "refused" in normalized
|
||||||
or "reach" in normalized
|
or "reach" in normalized
|
||||||
|
|
@ -769,6 +783,7 @@ func _clear_edit_state() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_cancel_pressed() -> void:
|
func _on_cancel_pressed() -> void:
|
||||||
|
_connection_error_latched = false
|
||||||
if _network_session != null:
|
if _network_session != null:
|
||||||
_network_session.cancel_connection()
|
_network_session.cancel_connection()
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
@ -795,11 +810,14 @@ func _on_state_changed(_state: NetworkSession.State) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_status_message_changed(message: String) -> void:
|
func _on_status_message_changed(message: String) -> void:
|
||||||
|
if _connection_error_latched:
|
||||||
|
return
|
||||||
_set_status(_friendly_connection_message(message))
|
_set_status(_friendly_connection_message(message))
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _on_connection_error(message: String) -> void:
|
func _on_connection_error(message: String) -> void:
|
||||||
|
_connection_error_latched = true
|
||||||
_set_status(_friendly_connection_message(message), true)
|
_set_status(_friendly_connection_message(message), true)
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,8 @@ func _build_laptop() -> void:
|
||||||
)
|
)
|
||||||
forecast_stack.add_child(forecast_heading)
|
forecast_stack.add_child(forecast_heading)
|
||||||
_forecast_list = HBoxContainer.new()
|
_forecast_list = HBoxContainer.new()
|
||||||
|
_forecast_list.custom_minimum_size = Vector2(272.0, 66.0)
|
||||||
|
_forecast_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_forecast_list.alignment = BoxContainer.ALIGNMENT_CENTER
|
_forecast_list.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
_forecast_list.add_theme_constant_override("separation", 6)
|
_forecast_list.add_theme_constant_override("separation", 6)
|
||||||
forecast_stack.add_child(_forecast_list)
|
forecast_stack.add_child(_forecast_list)
|
||||||
|
|
@ -501,7 +503,10 @@ func _daily_refresh_text() -> String:
|
||||||
func _add_forecast_empty(message: String) -> void:
|
func _add_forecast_empty(message: String) -> void:
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.text = message
|
label.text = message
|
||||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
label.custom_minimum_size = Vector2(252.0, 66.0)
|
||||||
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||||
label.add_theme_color_override(
|
label.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,19 @@ func reopen() -> void:
|
||||||
_start_entry_prompt_animation()
|
_start_entry_prompt_animation()
|
||||||
|
|
||||||
|
|
||||||
|
func reopen_to_menu() -> void:
|
||||||
|
reopen()
|
||||||
|
_awaiting_start_input = false
|
||||||
|
_stop_entry_prompt_animation()
|
||||||
|
_start_prompt_center.hide()
|
||||||
|
_presentation_center.show()
|
||||||
|
_button_center.show()
|
||||||
|
var bubble_field := get_node_or_null("%BubbleField") as Control
|
||||||
|
if bubble_field != null:
|
||||||
|
bubble_field.show()
|
||||||
|
_set_title_bubbles_interactive(true)
|
||||||
|
|
||||||
|
|
||||||
func open_join_game_page(endpoint: String = "") -> void:
|
func open_join_game_page(endpoint: String = "") -> void:
|
||||||
_cancel_title_entry_transition()
|
_cancel_title_entry_transition()
|
||||||
_awaiting_start_input = false
|
_awaiting_start_input = false
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
|
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
text = "v0.6.8-alpha"
|
text = "v0.7.1-alpha"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]
|
[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ const UPDATE_INTERVAL_SECONDS: float = 0.1
|
||||||
const SUN_YAW_DEGREES: float = -32.0
|
const SUN_YAW_DEGREES: float = -32.0
|
||||||
const WEATHER_TRANSITION_SECONDS: float = 10.0
|
const WEATHER_TRANSITION_SECONDS: float = 10.0
|
||||||
const RAIN_EMITTER_OFFSET := Vector3(0.0, 7.0, 0.0)
|
const RAIN_EMITTER_OFFSET := Vector3(0.0, 7.0, 0.0)
|
||||||
const RAIN_PARTICLE_AMOUNT: int = 560
|
const RAIN_EMISSION_EXTENTS := Vector3(13.0, 1.0, 13.0)
|
||||||
|
const RAIN_VISIBILITY_AABB := AABB(
|
||||||
|
Vector3(-13.5, -9.0, -13.5),
|
||||||
|
Vector3(27.0, 12.0, 27.0),
|
||||||
|
)
|
||||||
|
const RAIN_PARTICLE_AMOUNT: int = 2240
|
||||||
const RAIN_VELOCITY_MIN: float = 16.0
|
const RAIN_VELOCITY_MIN: float = 16.0
|
||||||
const RAIN_VELOCITY_MAX: float = 20.0
|
const RAIN_VELOCITY_MAX: float = 20.0
|
||||||
const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)
|
const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)
|
||||||
|
|
@ -187,14 +192,12 @@ func _prepare_rain() -> void:
|
||||||
_rain.lifetime = 1.25
|
_rain.lifetime = 1.25
|
||||||
_rain.fixed_fps = 30
|
_rain.fixed_fps = 30
|
||||||
_rain.local_coords = false
|
_rain.local_coords = false
|
||||||
_rain.visibility_aabb = AABB(
|
_rain.visibility_aabb = RAIN_VISIBILITY_AABB
|
||||||
Vector3(-7.0, -9.0, -7.0), Vector3(14.0, 12.0, 14.0)
|
|
||||||
)
|
|
||||||
var process_material := ParticleProcessMaterial.new()
|
var process_material := ParticleProcessMaterial.new()
|
||||||
process_material.emission_shape = (
|
process_material.emission_shape = (
|
||||||
ParticleProcessMaterial.EMISSION_SHAPE_BOX
|
ParticleProcessMaterial.EMISSION_SHAPE_BOX
|
||||||
)
|
)
|
||||||
process_material.emission_box_extents = Vector3(6.5, 1.0, 6.5)
|
process_material.emission_box_extents = RAIN_EMISSION_EXTENTS
|
||||||
process_material.direction = Vector3.DOWN
|
process_material.direction = Vector3.DOWN
|
||||||
process_material.spread = 5.0
|
process_material.spread = 5.0
|
||||||
process_material.initial_velocity_min = RAIN_VELOCITY_MIN
|
process_material.initial_velocity_min = RAIN_VELOCITY_MIN
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue