straywild/main/main.gd
Voyager 4575830857 Improve nearby water recovery and camera prompts
Recover players on generated worlds to the nearest safe grass or sand surface. Use the active gameplay camera when positioning shop and storage prompts.
2026-08-29 17:43:11 -04:00

2732 lines
82 KiB
GDScript

extends Node3D
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const GameUIType = preload("res://ui/game_ui.gd")
const PlayerType = preload("res://player/player.gd")
const TestWorldType = preload("res://world/test_world.gd")
const WorldLayoutType = preload("res://world/world_layout.gd")
const WaterRecoveryControllerType = preload(
"res://world/water_recovery_controller.gd"
)
const PlayerSaveManagerType = preload(
"res://save/player_save_manager.gd"
)
const PlayerSaveSlotCatalogType = preload(
"res://save/player_save_slot_catalog.gd"
)
const PlayerSettingsManagerType = preload(
"res://settings/player_settings_manager.gd"
)
const PlayerSettingsType = preload("res://settings/player_settings.gd")
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const KeyboardMouseMappingManagerType = preload(
"res://settings/keyboard_mouse_mapping_manager.gd"
)
const TitleScreenType = preload("res://ui/title_screen.gd")
const PauseMenuType = preload("res://ui/pause_menu.gd")
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
const ItemDataType = preload("res://items/item_data.gd")
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
const FishingShopType = preload("res://ui/fishing_shop.gd")
const FishingShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
const PlayerStorageInteractionType = preload(
"res://world/player_storage_interaction.gd"
)
const UIPixelationPresenterType = preload(
"res://ui/ui_pixelation_presenter.gd"
)
const PixelationResetOverlayType = preload(
"res://ui/pixelation_reset_overlay.gd"
)
const WorldPixelationPostprocessType = preload(
"res://main/world_pixelation_postprocess.gd"
)
const RuntimePerformanceProfileType = preload(
"res://main/runtime_performance_profile.gd"
)
const NetworkSessionType = preload("res://network/network_session.gd")
const DiscoveryClientType = preload("res://network/discovery_client.gd")
const DedicatedServerConfigType = preload(
"res://server/dedicated_server_config.gd"
)
const NetworkProfilePreferencesType = preload(
"res://network/network_profile_preferences.gd"
)
const SavedServerStoreType = preload(
"res://network/saved_server_store.gd"
)
const PlayerSpawnServiceType = preload(
"res://network/player_spawn_service.gd"
)
const NetworkFishingServiceType = preload(
"res://network/network_fishing_service.gd"
)
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const NetworkSaleServiceType = preload(
"res://network/network_sale_service.gd"
)
const NetworkShopServiceType = preload(
"res://network/network_shop_service.gd"
)
const NetworkItemUseServiceType = preload(
"res://network/network_item_use_service.gd"
)
const NetworkFishShowcaseServiceType = preload(
"res://network/network_fish_showcase_service.gd"
)
const NetworkSurfaceDrawingServiceType = preload(
"res://network/network_surface_drawing_service.gd"
)
const NetworkChatServiceType = preload(
"res://network/network_chat_service.gd"
)
const NetworkMailServiceType = preload(
"res://network/network_mail_service.gd"
)
const PlayerAssetReservationServiceType = preload(
"res://progression/player_asset_reservation_service.gd"
)
const PlayerAppearanceStoreType = preload(
"res://progression/player_appearance_store.gd"
)
const NetworkProfileServiceType = preload(
"res://network/network_profile_service.gd"
)
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldTimeVisualControllerType = preload(
"res://world/world_time_visual_controller.gd"
)
const NetworkWorldTimeServiceType = preload(
"res://network/network_world_time_service.gd"
)
const WorldWeatherServiceType = preload("res://world/world_weather_service.gd")
const NetworkWorldWeatherServiceType = preload(
"res://network/network_world_weather_service.gd"
)
const RainAmbienceType = preload("res://world/rain_ambience.gd")
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
const NetworkJobServiceType = preload("res://network/network_job_service.gd")
const GatherableCatalogType = preload(
"res://gathering/gatherable_catalog.gd"
)
const GatheringControllerType = preload(
"res://gathering/gathering_controller.gd"
)
const NetworkWorldSpawnServiceType = preload(
"res://network/network_world_spawn_service.gd"
)
const TITLE_MUSIC_SILENCE_DB: float = -80.0
const TITLE_MUSIC_PATH: String = (
"res://audio/music/title/as_in_four_wolves.ogg"
)
const NEW_GAME_MUSIC_PATH: String = "res://audio/music/world/tulips.wav"
const DUSK_MUSIC_PATH: String = "res://audio/music/world/craft.mp3"
const TIME_CROSSING_EPSILON_HOURS: float = 0.000001
const PLAYER_MENU_PATTERN_SCALE: float = 0.85
const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0)
const SHOP_PATTERN_SCALE: float = 1.75
const DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND: int = 10
const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
@export var fish_catalog: FishPoolType
@export var pelican_buyer_profile: FishBuyerProfileType
@export var main_shop_buyer_profile: FishBuyerProfileType
@export var item_catalog: ItemCatalogType
@export var gatherable_catalog: GatherableCatalogType
@export_category("Title Music")
@export_range(-40.0, 0.0, 0.5) var title_music_volume_db: float = -6.0
@export_range(0.0, 10.0, 0.05) var title_music_fade_out_seconds: float = 5.0
@export_range(-40.0, 0.0, 0.5) var new_game_music_volume_db: float = -6.0
@onready var _test_world: TestWorldType = $TestWorld
@onready var _player: PlayerType = %Player
@onready var _fishing_spot: FishingSpotType = %FishingSpot
@onready var _game_ui: GameUIType = %GameUI
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
@onready var _controller_mapping_manager: ControllerMappingManagerType = (
%ControllerMappingManager
)
@onready var _keyboard_mouse_mapping_manager: KeyboardMouseMappingManagerType = (
%KeyboardMouseMappingManager
)
@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController
@onready var _title_music: AudioStreamPlayer = %TitleMusic
@onready var _new_game_music: AudioStreamPlayer = %NewGameMusic
@onready var _dusk_music: AudioStreamPlayer = %DuskMusic
@onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation
@onready var _pixelation_reset: PixelationResetOverlayType = (
%PixelationResetOverlay
)
@onready var _world_pixelation: WorldPixelationPostprocessType = (
%WorldPixelationPostprocess
)
@onready var _network_session: NetworkSessionType = %NetworkSession
@onready var _discovery: DiscoveryClientType = %DiscoveryClient
@onready var _world_time: WorldTimeServiceType = %WorldTimeService
@onready var _world_time_visuals: WorldTimeVisualControllerType = (
%WorldTimeVisualController
)
@onready var _network_world_time: NetworkWorldTimeServiceType = (
%NetworkWorldTimeService
)
@onready var _world_weather: WorldWeatherServiceType = %WorldWeatherService
@onready var _network_world_weather: NetworkWorldWeatherServiceType = (
%NetworkWorldWeatherService
)
@onready var _player_jobs: PlayerJobServiceType = %PlayerJobService
@onready var _network_jobs: NetworkJobServiceType = %NetworkJobService
@onready var _network_world_spawns: NetworkWorldSpawnServiceType = (
%NetworkWorldSpawnService
)
@onready var _gathering_controller: GatheringControllerType = (
%GatheringController
)
@onready var _data_root: PlayerDataRoot = %PlayerDataRoot
@onready var _identity_backups: IdentityBackupService = %IdentityBackupService
@onready var _network_profile: NetworkProfilePreferencesType = (
%NetworkProfilePreferences
)
@onready var _player_identity: PlayerIdentityStore = %PlayerIdentityStore
@onready var _host_identity: HostIdentityStore = %HostIdentityStore
@onready var _known_players: KnownPlayerStore = %KnownPlayerStore
@onready var _server_trust: ServerTrustStore = %ServerTrustStore
@onready var _relationships: PlayerRelationshipStore = %PlayerRelationshipStore
@onready var _host_bans: HostBanStore = %HostBanStore
@onready var _saved_servers: SavedServerStoreType = %SavedServerStore
@onready var _player_spawn_service: PlayerSpawnServiceType = (
%PlayerSpawnService
)
@onready var _network_fishing: NetworkFishingServiceType = (
%NetworkFishingService
)
@onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService
@onready var _network_shop: NetworkShopServiceType = %NetworkShopService
@onready var _network_item_use: NetworkItemUseServiceType = (
%NetworkItemUseService
)
@onready var _network_fish_showcase: NetworkFishShowcaseServiceType = (
%NetworkFishShowcaseService
)
@onready var _network_surface_drawing: NetworkSurfaceDrawingServiceType = (
%NetworkSurfaceDrawingService
)
@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
@onready var _network_mail: NetworkMailServiceType = %NetworkMailService
@onready var _network_player_list: NetworkPlayerListService = %NetworkPlayerListService
@onready var _asset_reservations: PlayerAssetReservationServiceType = (
%PlayerAssetReservationService
)
@onready var _appearance_store: PlayerAppearanceStoreType = %PlayerAppearanceStore
@onready var _network_profile_service: NetworkProfileServiceType = (
%NetworkProfileService
)
@onready var _players_root: Node3D = $Players
@onready var _surface_drawings_root: Node3D = $SurfaceDrawings
@onready var _world_gatherables_root: Node3D = $WorldGatherables
@onready var _title_background: ColorRect = %TitleBackground
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
@onready var _shop_backdrop: ColorRect = %ShopBackdrop
var _gameplay_started: bool = false
var _save_slots := PlayerSaveSlotCatalogType.new()
var _shop_interaction: FishingShopInteractionType
var _storage_interaction: PlayerStorageInteractionType
var _title_music_tween: Tween
var _title_music_transition_generation: int = 0
var _title_music_requested: bool = false
var _dusk_music_played_for_natural_day: bool = false
var _quit_in_progress: bool = false
var _join_requested_from_title: bool = false
var _join_requested_from_pause: bool = false
var _player_menu_backdrop_tween: Tween
var _shop_backdrop_tween: Tween
var _pending_join_endpoint: String = ""
var _social_prompt_queue: Array[Dictionary] = []
var _active_social_prompt: Dictionary = {}
var _pending_social_invite_join: bool = false
var _pending_social_invite_gameplay: bool = false
var _server_trust_dialog: ConfirmationDialog
var _pending_trust_changed: bool = false
var _identity_notice_dialog: AcceptDialog
var _data_setup_dialog: ConfirmationDialog
var _data_setup_choose_button: Button
var _data_setup_current_button: Button
var _data_folder_dialog: FileDialog
var _data_folder_picker_generation: int = 0
var _restore_data_setup_after_picker: bool = false
var _application_initialized := false
var _performance_profile: RuntimePerformanceProfileType
var _pending_existing_root_path := ""
var _local_recovery_attempt_id: String = ""
var _dedicated_runtime: bool = false
var _dedicated_metrics_accumulator: float = 0.0
@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
var _rain_ambience: RainAmbienceType
func _ready() -> void:
_data_root.prepare_legacy_user_data()
_performance_profile = RuntimePerformanceProfileType.from_environment()
_dedicated_runtime = _is_dedicated_server_runtime()
if _dedicated_runtime:
Engine.max_fps = 30
Engine.physics_ticks_per_second = 30
_disable_dedicated_presentation_tree()
call_deferred("_start_dedicated_server")
return
_world_pixelation.set_light_performance_profile(
_performance_profile.is_light()
)
_test_world.set_light_performance_profile(
_performance_profile.is_light()
)
_configure_presentation_performance_profile(
_performance_profile.is_light()
)
DisplayServer.window_set_title("straywild")
_rain_ambience = RainAmbienceType.new()
_rain_ambience.name = "RainAmbience"
add_child(_rain_ambience)
_rain_ambience.configure(_world_weather)
_configure_audio_performance_profile(_performance_profile.is_light())
_shoreline_ambience.configure(
_player,
_test_world.get_saltwater_shoreline_mesh(),
)
get_window().size_changed.connect(_resize_native_overlays)
_resize_native_overlays()
if not _settings_manager.settings_changed.is_connected(
_apply_runtime_settings
):
_settings_manager.settings_changed.connect(_apply_runtime_settings)
_settings_manager.load_settings()
_interface_fonts.enforce_standard_font()
_interface_fonts.set_controller_text_entry_request(
Callable(_game_ui, "request_controller_text_entry_for"),
Callable(_game_ui, "is_controller_text_entry_open"),
)
if _data_root.resolve():
_configure_portable_stores()
_initialize_after_data_root()
return
_show_data_root_setup()
func _configure_presentation_performance_profile(light_profile: bool) -> void:
_game_ui.set_light_performance_profile(light_profile)
var title_background_material := (
_title_background.material as ShaderMaterial
)
if title_background_material != null:
title_background_material.set_shader_parameter(
"animation_enabled",
not light_profile,
)
var menu_pattern_material := (
_player_menu_backdrop.material as ShaderMaterial
)
if menu_pattern_material != null:
menu_pattern_material.set_shader_parameter(
"animation_enabled",
not light_profile,
)
menu_pattern_material.set_shader_parameter(
"display_scale",
PLAYER_MENU_PATTERN_SCALE,
)
menu_pattern_material.set_shader_parameter(
"scroll_velocity_pixels",
(
Vector2.ZERO
if light_profile
else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
),
)
var shop_pattern_material := _shop_backdrop.material as ShaderMaterial
if shop_pattern_material != null:
shop_pattern_material.set_shader_parameter(
"animation_enabled",
not light_profile,
)
shop_pattern_material.set_shader_parameter(
"display_scale",
SHOP_PATTERN_SCALE,
)
shop_pattern_material.set_shader_parameter(
"scroll_velocity_pixels",
(
Vector2.ZERO
if light_profile
else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
),
)
func _configure_audio_performance_profile(light_profile: bool) -> void:
_title_music.stream = _load_optional_audio_stream(TITLE_MUSIC_PATH)
_new_game_music.stream = _load_optional_audio_stream(NEW_GAME_MUSIC_PATH)
_dusk_music.stream = _load_optional_audio_stream(DUSK_MUSIC_PATH)
if light_profile:
_shoreline_ambience.set_audio_enabled(false)
_rain_ambience.set_audio_enabled(false)
return
_shoreline_ambience.set_audio_enabled(true)
_rain_ambience.set_audio_enabled(true)
func _load_optional_audio_stream(path: String) -> AudioStream:
if not ResourceLoader.exists(path, "AudioStream"):
return null
return load(path) as AudioStream
func _is_dedicated_server_runtime() -> bool:
if OS.has_feature("dedicated_server"):
return true
return "--dedicated-server" in OS.get_cmdline_user_args()
func _disable_dedicated_presentation_tree() -> void:
for path: NodePath in [
NodePath("TitleBackgroundLayer"),
NodePath("PlayerMenuBackdrop"),
NodePath("ShopBackdrop"),
NodePath("WorldPixelationPostprocess"),
NodePath("UIPresentation"),
NodePath("PixelationResetOverlay"),
NodePath("WorldTimeVisualController"),
NodePath("GatheringController"),
NodePath("FishingSpot"),
NodePath("WaterRecovery"),
NodePath("ControllerMappingManager"),
NodePath("KeyboardMouseMappingManager"),
]:
var presentation_node: Node = get_node_or_null(path)
if presentation_node == null:
continue
presentation_node.process_mode = Node.PROCESS_MODE_DISABLED
if presentation_node is CanvasItem:
(presentation_node as CanvasItem).visible = false
elif presentation_node is CanvasLayer:
(presentation_node as CanvasLayer).visible = false
for audio_path: NodePath in [
NodePath("TitleMusic"),
NodePath("NewGameMusic"),
NodePath("DuskMusic"),
NodePath("ShorelineAmbience"),
]:
var audio_node: Node = get_node_or_null(audio_path)
if audio_node == null:
continue
if audio_node.has_method("stop"):
audio_node.call("stop")
audio_node.process_mode = Node.PROCESS_MODE_DISABLED
func _print_dedicated_metrics() -> void:
var metrics: Dictionary = {
"players": _network_session.get_player_count(),
"fps": Engine.get_frames_per_second(),
"frame_process_ms": Performance.get_monitor(
Performance.TIME_PROCESS
) * 1000.0,
"frame_physics_ms": Performance.get_monitor(
Performance.TIME_PHYSICS_PROCESS
) * 1000.0,
"memory_mib": Performance.get_monitor(
Performance.MEMORY_STATIC
) / (1024.0 * 1024.0),
"nodes": int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)),
"physics_active_objects": int(Performance.get_monitor(
Performance.PHYSICS_3D_ACTIVE_OBJECTS
)),
"physics_collision_pairs": int(Performance.get_monitor(
Performance.PHYSICS_3D_COLLISION_PAIRS
)),
"movement": _network_session.get_movement_metrics(),
}
if _network_fishing.has_method("get_network_metrics"):
metrics["fishing"] = _network_fishing.call("get_network_metrics")
if _network_world_spawns.has_method("get_network_metrics"):
metrics["gatherables"] = _network_world_spawns.call(
"get_network_metrics"
)
print("straywild server metrics: %s" % JSON.stringify(metrics))
func _start_dedicated_server() -> void:
var config: DedicatedServerConfigType = DedicatedServerConfigType.from_runtime()
if not config.is_valid():
_fail_dedicated_server(config.error_message)
return
var data_path: String = config.data_directory
if not _data_root.has_data_manifest(data_path):
var created: Dictionary = _data_root.create_unbound_root(data_path)
if not bool(created.get("ok", false)):
_fail_dedicated_server(str(created.get(
"message", "Could not create the server data directory."
)))
return
if not _data_root.activate_process_root(data_path):
_fail_dedicated_server(_data_root.error_message)
return
var identity_directory: String = data_path.path_join("server")
if DirAccess.make_dir_recursive_absolute(identity_directory) != OK:
_fail_dedicated_server("Could not create the server identity directory.")
return
_host_identity.configure("host_identity", true, identity_directory)
if not _discovery.set_base_url_override(config.discovery_url):
_fail_dedicated_server("The discovery URL is invalid.")
return
if not _network_session.configure_dedicated_operators(
config.operator_fingerprints
):
_fail_dedicated_server("The server operator list is invalid.")
return
if not _network_chat.configure_dedicated_history(
config.chat_logging,
config.chat_log_path,
):
_fail_dedicated_server(
"The dedicated-server chat privacy configuration is invalid."
)
return
if not _apply_world(
WorldLayoutType.GENERATED,
config.world_seed,
false,
):
_fail_dedicated_server("The configured world seed could not be generated.")
return
_test_world.set_dedicated_simulation(true)
if not _network_session.set_host_world(
WorldLayoutType.GENERATED,
config.world_seed,
):
_fail_dedicated_server("The configured world seed is invalid.")
return
_configure_portable_stores()
if not _discovery.configure_dedicated_runtime(config.server_name):
_fail_dedicated_server("The server room name is invalid.")
return
_initialize_application(true)
_player.set_local_control(false)
_player.set_network_simulation_only(true)
_player.visible = false
_player.collision_layer = 0
_player.collision_mask = 0
_player.set_physics_process(false)
if not _network_session.peer_count_changed.is_connected(
_on_dedicated_peer_count_changed
):
_network_session.peer_count_changed.connect(
_on_dedicated_peer_count_changed
)
if not _network_session.start_dedicated_host(
config.port,
config.max_players,
config.bind_address,
):
_fail_dedicated_server("Could not start the dedicated server.")
return
_network_session.set_session_display_name(config.server_name)
if not _network_session.set_host_open(true):
_fail_dedicated_server("Could not open the dedicated server.")
return
_on_dedicated_peer_count_changed(
_network_session.get_player_count(),
config.max_players,
)
_player_jobs.begin_progression_session()
if config.public_listing and not _discovery.set_discoverable(true):
_network_session.disconnect_session("Discovery setup failed.")
_fail_dedicated_server(_discovery.get_host_status_message())
return
print(
"straywild dedicated server ready: %s on %s:%d (%d players, %s, timezone %s)"
% [
config.server_name,
config.bind_address,
config.port,
config.max_players,
"public" if config.public_listing else "unlisted",
str(Time.get_time_zone_from_system().get("name", "local")),
]
)
print(
"Server identity: %s"
% _network_session.get_host_identity_fingerprint()
)
func _on_dedicated_peer_count_changed(
player_count: int,
_max_players: int,
) -> void:
if not _dedicated_runtime:
return
Engine.physics_ticks_per_second = (
DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND
if player_count > 0
else DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND
)
func _fail_dedicated_server(message: String) -> void:
push_error("Dedicated server startup failed: %s" % message)
get_tree().quit(1)
func _configure_portable_stores() -> void:
if _dedicated_runtime:
_save_manager.configure_storage(
_data_root.path_for(&"player_save"), _data_root
)
elif not _save_slots.configure(_data_root, _save_manager):
push_error(
"Save-slot catalog setup failed: %s"
% _save_slots.get_error_message()
)
_network_profile.configure_storage(
_data_root.path_for(&"network_profile"), _data_root
)
_appearance_store.configure_storage(
_data_root.path_for(&"player_appearance"), _data_root
)
_saved_servers.configure_storage(
_data_root.path_for(&"saved_servers"), _data_root
)
_known_players.configure_storage(
_data_root.path_for(&"known_players"), _data_root
)
_relationships.configure_storage(
_data_root.path_for(&"player_relationships"), _data_root
)
_server_trust.configure_storage(
_data_root.path_for(&"server_trust"), _data_root
)
_host_bans.configure_storage(
_data_root.path_for(&"host_bans"), _data_root
)
func _initialize_after_data_root() -> void:
_initialize_application(false)
func _initialize_application(dedicated: bool) -> void:
if _application_initialized:
return
_application_initialized = true
_player.global_transform = _test_world.get_player_spawn_transform()
_player.velocity = Vector3.ZERO
_player_spawn_service.setup(
_players_root,
_player,
_test_world.get_player_spawn_transform(),
dedicated,
)
_network_session.setup(
_network_profile,
_saved_servers,
_player_spawn_service,
_player_identity,
_host_identity,
_known_players,
_server_trust,
_host_bans,
dedicated,
)
_discovery.setup(_network_session, _relationships)
if not dedicated:
_world_time_visuals.setup(
_world_time,
_test_world.get_world_environment(),
_test_world.get_sun(),
_world_weather,
_player,
Callable(_player, "get_active_gameplay_camera"),
_performance_profile.is_light(),
)
if not _world_time.natural_time_advanced.is_connected(
_on_natural_time_advanced
):
_world_time.natural_time_advanced.connect(
_on_natural_time_advanced
)
_network_world_time.setup(_network_session, _world_time)
_network_world_weather.setup(_network_session, _world_weather)
_player_jobs.setup(
_player.wallet,
_player.experience,
_player.collection_log,
fish_catalog,
_world_time,
_world_weather,
_network_session,
)
_network_jobs.setup(_network_session, _player_jobs)
_identity_backups.setup(_data_root, _player_identity, _host_identity)
_network_profile_service.setup(
_network_session,
_network_profile,
_appearance_store,
_player_spawn_service,
)
_network_player_list.setup(
_network_session,
_relationships,
_host_bans,
_known_players,
_player_spawn_service,
_network_chat,
_network_mail,
)
if not dedicated:
_network_session.set_local_appearance_snapshot(
_appearance_store.get_snapshot()
)
_network_session.join_authenticated.connect(
_on_network_join_authenticated
)
_network_session.connection_error.connect(
_on_network_connection_error
)
_network_session.server_trust_required.connect(
_on_server_trust_required
)
_network_session.server_lost.connect(_on_network_server_lost)
_network_session.peer_identity_observed.connect(
_on_peer_identity_observed
)
_network_session.remote_recovery_requested.connect(
_on_remote_recovery_requested
)
_network_session.remote_recovery_presentation_changed.connect(
_on_remote_recovery_presentation_changed
)
_player.fish_sale_service.setup(
_player.inventory,
_player.wallet,
_asset_reservations
)
_player.bag.setup(item_catalog)
_player.inventory_layout.setup(
_player.bag,
_player.inventory,
item_catalog,
_player.cooler_capacity,
)
_player.bag.set_inventory_layout(_player.inventory_layout)
_player.inventory.set_inventory_layout(_player.inventory_layout)
_player.hotbar.setup(
_player.bag,
item_catalog,
_player.inventory,
_player.inventory_layout,
)
_shop_interaction = _test_world.get_fishing_shop()
_storage_interaction = _test_world.get_player_storage()
if not dedicated:
_shop_interaction.setup_local_player(_player)
_shop_interaction.local_player_range_changed.connect(
_on_shop_range_changed
)
_game_ui.set_shop_npc_player_in_range(
_shop_interaction.is_local_player_in_range()
)
_storage_interaction.setup_local_player(_player)
_storage_interaction.local_player_range_changed.connect(
_on_storage_range_changed
)
_save_manager.setup(
_player.inventory,
_player.collection_log,
_player.wallet,
fish_catalog,
_player.bag,
_player.hotbar,
_player.inventory_layout,
item_catalog,
_player.fishing_upgrades,
_player.cooler_capacity,
_player.art_unlocks,
_player.experience,
_world_time,
_world_weather,
_player_jobs,
_player,
)
_player_jobs.set_save_manager(_save_manager)
_save_manager.set_autosave_enabled(false)
_asset_reservations.setup(
_player.wallet, _player.inventory, _player.bag, item_catalog
)
_player.inventory.set_reservation_service(_asset_reservations)
_network_mail.setup(
_network_session,
_asset_reservations,
_player.wallet,
_player.inventory,
_player.bag,
_player.collection_log,
_player.cooler_capacity,
item_catalog,
fish_catalog,
_save_manager
)
_network_item_use.setup(
_network_session,
_player_spawn_service,
item_catalog,
_player.bag,
_player.item_effects,
_save_manager,
_asset_reservations
)
_network_world_spawns.setup(
_network_session,
_player_spawn_service,
_test_world,
_world_gatherables_root,
gatherable_catalog,
fish_catalog,
_player.inventory,
_player.collection_log,
_player.cooler_capacity,
_player.experience,
_save_manager,
_network_item_use,
_world_time,
not dedicated,
)
_gathering_controller.setup(
_player,
_player.bag,
_player.hotbar,
_fishing_spot,
_network_world_spawns,
)
_network_world_spawns.local_capture_received.connect(
_fishing_spot.present_external_catch
)
_gathering_controller.status_changed.connect(
_fishing_spot.report_external_status
)
_network_fish_showcase.setup(
_network_session,
_player_spawn_service,
fish_catalog,
_player.inventory,
_player.hotbar,
)
_network_surface_drawing.setup(
_network_session,
_player_spawn_service,
_relationships,
_player,
_surface_drawings_root,
_player.bag,
_player.hotbar,
_player.art_unlocks,
_data_root,
)
_network_player_list.set_surface_drawing_service(
_network_surface_drawing
)
_network_chat.setup(_network_session)
_network_fishing.setup(
_network_session,
_player_spawn_service,
_fishing_spot,
_player.inventory,
_player.bag,
_player.collection_log,
_player.cooler_capacity,
_player.experience,
_save_manager,
item_catalog,
fish_catalog,
_network_item_use,
not dedicated,
)
var sale_buyers: Array[FishBuyerProfileType] = [
pelican_buyer_profile,
main_shop_buyer_profile,
]
_network_sale.setup(
_network_session,
_player_spawn_service,
_network_fishing,
_shop_interaction,
_player.inventory,
_player.bag,
item_catalog,
_player.wallet,
_player.fish_sale_service,
_save_manager,
fish_catalog,
sale_buyers,
_asset_reservations,
_player.inventory_layout,
)
_player_jobs.bind_authoritative_services(
_network_fishing,
_network_sale,
_network_world_spawns,
)
_network_shop.setup(
_network_session,
_player_spawn_service,
_network_fishing,
_shop_interaction,
_player.wallet,
_player.bag,
item_catalog,
_player.fishing_upgrades,
_player.cooler_capacity,
_player.art_unlocks,
_save_manager,
_asset_reservations,
_player.inventory_layout,
)
_fishing_spot.setup(
_player,
_player.inventory,
_player.collection_log,
_player.experience,
_player.bag,
_player.hotbar,
item_catalog,
_player.fishing_upgrades,
_player.item_effects,
_network_item_use,
_player.cooler_capacity,
_network_session,
_network_fishing,
_world_time,
_world_weather,
)
if dedicated:
return
_game_ui.setup(
_player,
_player.inventory,
_player.collection_log,
_player.experience,
_player.wallet,
_player.fish_sale_service,
pelican_buyer_profile,
fish_catalog,
_fishing_spot,
_player.bag,
_player.hotbar,
_player.inventory_layout,
item_catalog,
main_shop_buyer_profile,
_player.fishing_upgrades,
_shop_interaction,
_storage_interaction,
_player.item_effects,
_player.cooler_capacity,
_network_session,
_network_sale,
_network_shop,
_network_chat,
_network_profile,
_player_spawn_service,
_network_mail,
_asset_reservations,
_network_profile_service,
_network_player_list,
_discovery,
_settings_manager,
_network_surface_drawing,
_player.art_unlocks,
_world_time,
_world_weather,
_player_jobs,
_test_world.get_world_environment(),
_test_world.get_sun(),
)
_game_ui.setup_data_and_identity(
_data_root,
_identity_backups,
_player_identity,
_host_identity,
_network_session,
_interface_fonts,
)
_game_ui.data_root_changed.connect(_on_data_root_changed)
_game_ui.setup_controller_mapping(_controller_mapping_manager)
_ui_pixelation.setup_controller_mapping(_controller_mapping_manager)
_game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_mapping_manager)
_network_player_list.friend_request_received.connect(
_on_friend_request_received
)
_discovery.friend_invite_received.connect(_on_friend_invite_received)
_discovery.public_join_prepared.connect(
_on_social_public_join_prepared
)
_discovery.public_join_status_changed.connect(
_on_social_public_join_status_changed
)
_game_ui.social_prompt_accepted.connect(_on_social_prompt_accepted)
_game_ui.social_prompt_declined.connect(_on_social_prompt_declined)
_data_root.conflict_detected.connect(_on_portable_conflict)
if (
PortableFileGuard.has_syncthing_conflict(
_data_root.root_path.path_join("player")
)
or PortableFileGuard.has_syncthing_conflict(
_data_root.root_path.path_join("social")
)
):
call_deferred(
"_on_data_root_status",
"Syncthing conflict copies were found. Review the data folder.",
)
_water_recovery.setup(
_player,
_fishing_spot,
_game_ui,
_game_ui.get_screen_fade(),
_test_world.get_player_water_triggers(),
_test_world.get_safe_respawn_points(),
Callable(_test_world, "get_water_recovery_position"),
)
_ui_pixelation.effective_pixel_size_changed.connect(
_on_effective_ui_pixel_size_changed
)
_game_ui.pixelation_settings_visibility_changed.connect(
_pixelation_reset.set_settings_open
)
_game_ui.crisp_reset_focus_requested.connect(
_pixelation_reset.focus_reset_button
)
_game_ui.interactive_pointer_ui_changed.connect(
_ui_pixelation.set_interactive_ui_open
)
_game_ui.passive_pointer_ui_changed.connect(
_ui_pixelation.set_passive_pointer_ui_enabled
)
_game_ui.player_menu_backdrop_visibility_changed.connect(
_set_player_menu_backdrop_visible
)
_game_ui.shop_backdrop_visibility_changed.connect(
_set_shop_backdrop_visible
)
_pixelation_reset.return_to_settings_requested.connect(
_game_ui.focus_open_settings_back_button
)
_pixelation_reset.reset_requested.connect(_reset_pixelation)
_apply_runtime_settings(_settings_manager.current_settings)
_set_gameplay_active(false)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
title_screen.slot_play_requested.connect(_on_slot_play_requested)
title_screen.new_slot_requested.connect(_on_new_slot_requested)
title_screen.quit_requested.connect(_on_quit_requested)
title_screen.setup(
_save_manager,
_save_slots,
_settings_manager,
_network_session,
_saved_servers,
_server_trust,
_discovery,
_data_root,
_interface_fonts,
)
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
pause_menu.setup(
_player,
_save_manager,
_settings_manager,
_fishing_spot,
_network_session,
_saved_servers,
_server_trust,
_discovery,
)
title_screen.join_game_requested.connect(_on_title_join_game_requested)
pause_menu.join_game_requested.connect(_on_pause_join_game_requested)
pause_menu.return_to_title_requested.connect(
_on_return_to_title_requested
)
pause_menu.reset_progress_requested.connect(
_on_reset_progress_requested
)
pause_menu.quit_requested.connect(_on_quit_requested)
pause_menu.menu_visibility_changed.connect(
_game_ui.set_system_menu_open
)
_player.hotbar.selected_assignment_changed.connect(
_on_active_hotbar_item_changed
)
_player.bag.contents_changed.connect(_refresh_active_hotbar_item)
_fishing_spot.ready_for_equipment_refresh.connect(
_refresh_active_hotbar_item
)
_water_recovery.recovery_starting.connect(
_on_water_recovery_starting
)
_water_recovery.recovery_finished.connect(
_on_water_recovery_finished
)
_water_recovery.local_respawn_completed.connect(
_on_local_respawn_completed
)
_refresh_active_hotbar_item()
_show_title_music(true)
func _show_data_root_setup() -> void:
if _data_setup_dialog == null:
_data_setup_dialog = ConfirmationDialog.new()
_data_setup_dialog.title = "straywild player data"
_data_setup_dialog.ok_button_text = "Use This Folder"
_data_setup_dialog.cancel_button_text = "Quit"
_data_setup_dialog.confirmed.connect(_use_default_data_root)
_data_setup_dialog.canceled.connect(get_tree().quit)
_data_setup_choose_button = _data_setup_dialog.add_button(
"Choose Another Folder", false, "choose"
)
_data_setup_current_button = _data_setup_dialog.add_button(
"Keep Current Location", false, "current"
)
_data_setup_dialog.custom_action.connect(_on_data_setup_action)
_interface_fonts.apply_utility_theme(_data_setup_dialog)
add_child(_data_setup_dialog)
var default_path: String = _data_root.default_visible_path()
var legacy: bool = PortableDataMigration.legacy_files_present()
_data_setup_dialog.dialog_text = (
("Move your existing straywild data to an easy-to-find folder."
if legacy else "Choose where straywild stores your player data.")
+ "\n\n%s\n\nYou can choose a Syncthing folder."
% (default_path if not default_path.is_empty() else "Choose a folder")
)
_data_setup_dialog.ok_button_text = (
(
"Move to Documents/straywild"
if legacy else "Use This Folder"
)
if not default_path.is_empty()
else "Keep Current Location"
)
_data_setup_current_button.visible = not default_path.is_empty()
if not _data_root.error_message.is_empty():
_data_setup_dialog.dialog_text = (
"Your straywild data folder is unavailable.\n\n%s"
% _data_root.error_message
)
_data_setup_dialog.popup_centered(Vector2i(640, 360))
_focus_data_setup_actions.call_deferred()
func _focus_data_setup_actions() -> void:
if _data_setup_dialog == null or not _data_setup_dialog.visible:
return
var actions: Array[Button] = [
_data_setup_dialog.get_ok_button(),
_data_setup_choose_button,
]
if _data_setup_current_button.visible:
actions.append(_data_setup_current_button)
actions.append(_data_setup_dialog.get_cancel_button())
for index: int in actions.size():
var action: Button = actions[index]
var previous: Button = actions[maxi(index - 1, 0)]
var next: Button = actions[mini(index + 1, actions.size() - 1)]
action.focus_mode = Control.FOCUS_ALL
action.focus_neighbor_left = action.get_path_to(previous)
action.focus_neighbor_right = action.get_path_to(next)
action.focus_neighbor_top = action.get_path_to(action)
action.focus_neighbor_bottom = action.get_path_to(action)
_data_setup_dialog.get_ok_button().grab_focus()
func _use_default_data_root() -> void:
var path: String = _data_root.default_visible_path()
if path.is_empty():
_activate_selected_data_path(
ProjectSettings.globalize_path(
PlayerDataRoot.APP_DATA_PORTABLE_PATH
),
true,
)
return
_activate_selected_data_path(path)
func _on_data_setup_action(action: StringName) -> void:
if action == &"choose":
_show_folder_picker()
elif action == &"current":
_activate_selected_data_path(
ProjectSettings.globalize_path(PlayerDataRoot.APP_DATA_PORTABLE_PATH), true
)
func _show_folder_picker() -> void:
if _data_folder_dialog == null:
_data_folder_dialog = FileDialog.new()
_data_folder_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
_data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM
_data_folder_dialog.use_native_dialog = false
_data_folder_dialog.dir_selected.connect(_activate_selected_data_path)
_data_folder_dialog.canceled.connect(_on_data_folder_picker_canceled)
_interface_fonts.apply_utility_theme(_data_folder_dialog)
add_child(_data_folder_dialog)
if _data_folder_dialog.visible:
return
_data_folder_picker_generation += 1
var generation: int = _data_folder_picker_generation
_restore_data_setup_after_picker = (
_data_setup_dialog != null and _data_setup_dialog.visible
)
if _data_setup_dialog != null:
_data_setup_dialog.hide()
_open_folder_picker_after_modal.call_deferred(generation)
func _open_folder_picker_after_modal(generation: int) -> void:
await get_tree().process_frame
if (
generation != _data_folder_picker_generation
or _data_folder_dialog == null
or _data_folder_dialog.visible
):
return
_data_folder_dialog.current_dir = (
_data_root.default_visible_path().get_base_dir()
if not _data_root.default_visible_path().is_empty()
else OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
)
_interface_fonts.popup_file_dialog(_data_folder_dialog)
func _on_data_folder_picker_canceled() -> void:
_data_folder_picker_generation += 1
var generation: int = _data_folder_picker_generation
var should_restore_setup: bool = _restore_data_setup_after_picker
_restore_data_setup_after_picker = false
if _data_folder_dialog != null:
_data_folder_dialog.hide()
if should_restore_setup:
_restore_data_setup_after_picker_after_modal.call_deferred(generation)
func _restore_data_setup_after_picker_after_modal(generation: int) -> void:
await get_tree().process_frame
if generation != _data_folder_picker_generation:
return
_show_data_root_setup()
func _activate_selected_data_path(path: String, app_data: bool = false) -> void:
_restore_data_setup_after_picker = false
var ok: bool = false
if PortableDataMigration.legacy_files_present():
var result: Dictionary = (
PortableDataMigration.adopt_legacy_app_data(_data_root)
if app_data
else PortableDataMigration.migrate_legacy_to(_data_root, path)
)
ok = bool(result.get("ok", false))
if not ok:
if bool(result.get("requires_existing_root_decision", false)):
_show_existing_root_choice(path)
return
_show_data_error(str(result.get("message", "Migration failed.")))
return
else:
ok = _data_root.select_new_root(path, app_data)
if not ok:
_show_data_error(_data_root.error_message)
return
_data_setup_dialog.hide()
_configure_portable_stores()
_initialize_after_data_root()
func _show_existing_root_choice(path: String) -> void:
_pending_existing_root_path = path
var dialog: ConfirmationDialog = ConfirmationDialog.new()
dialog.title = "Existing straywild data"
dialog.ok_button_text = "Use Data Already in Selected Folder"
dialog.cancel_button_text = "Cancel"
dialog.dialog_text = (
"This folder already contains straywild player data.\n\n"
+ "Choose which complete data set to use. Data is never merged automatically."
)
dialog.add_button(
"Replace with This Device's Data", false, "replace"
)
dialog.confirmed.connect(func() -> void:
if _data_root.use_existing_root(_pending_existing_root_path):
dialog.queue_free()
_data_setup_dialog.hide()
_configure_portable_stores()
_initialize_after_data_root()
else:
_show_data_error(_data_root.error_message)
)
dialog.custom_action.connect(func(action: StringName) -> void:
if action != &"replace":
return
var result: Dictionary = PortableDataMigration.replace_existing_with_legacy(
_data_root, _pending_existing_root_path
)
if bool(result.get("ok", false)):
dialog.queue_free()
_data_setup_dialog.hide()
_configure_portable_stores()
_initialize_after_data_root()
else:
_show_data_error(str(result.get("message", "Migration failed.")))
)
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog)
dialog.popup_centered(Vector2i(680, 360))
_configure_popup_dialog.call_deferred(
dialog, dialog.get_cancel_button()
)
func _show_data_error(message: String) -> void:
if _data_setup_dialog != null:
_data_setup_dialog.dialog_text = message
_data_setup_dialog.popup_centered(Vector2i(640, 300))
_focus_data_setup_actions.call_deferred()
func _on_portable_conflict(message: String, _path: String) -> void:
var dialog: AcceptDialog = AcceptDialog.new()
_interface_fonts.apply_utility_theme(dialog)
dialog.title = "Player data conflict"
dialog.dialog_text = (
message
+ "\n\nDo not play the same profile on two devices at the same time."
+ "\n\nThe data folder can be opened from Settings > Data."
)
dialog.confirmed.connect(dialog.queue_free)
_game_ui.add_child(dialog)
dialog.popup_centered(Vector2i(560, 300))
_configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
func _on_data_root_status(message: String) -> void:
if "Syncthing conflict" not in message:
return
var dialog: AcceptDialog = AcceptDialog.new()
_interface_fonts.apply_utility_theme(dialog)
dialog.title = "Synced data needs review"
dialog.dialog_text = (
message + "\n\nThe data folder can be opened from Settings > Data."
)
dialog.confirmed.connect(dialog.queue_free)
_game_ui.add_child(dialog)
dialog.popup_centered(Vector2i(560, 260))
_configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
func _on_data_root_changed() -> void:
_save_manager.set_autosave_enabled(false)
_save_manager.cancel_pending_autosave()
_configure_portable_stores()
if not _network_profile_service.reload_persisted_profile():
push_warning("The selected data folder's player profile could not be loaded.")
_network_session.set_local_appearance_snapshot(
_appearance_store.get_snapshot()
)
_game_ui.get_title_screen().refresh_after_data_root_change()
func _on_server_trust_required(
endpoint: String,
expected_fingerprint: String,
received_fingerprint: String,
is_changed: bool,
) -> void:
if _server_trust_dialog == null:
_server_trust_dialog = ConfirmationDialog.new()
_server_trust_dialog.title = "Server identity"
_server_trust_dialog.ok_button_text = "Trust & Connect"
_server_trust_dialog.cancel_button_text = "Cancel"
_server_trust_dialog.confirmed.connect(_confirm_server_trust)
_server_trust_dialog.canceled.connect(
_network_session.resolve_server_trust.bind(false)
)
_game_ui.add_child(_server_trust_dialog)
_pending_trust_changed = is_changed
if is_changed:
_server_trust_dialog.dialog_text = (
"Server identity changed.\n\n"
+ "This may mean the server was reinstalled, moved, or is being "
+ "impersonated.\n\nExpected:\n%s\n\nReceived:\n%s"
% [
NetworkIdentityCrypto.format_fingerprint(expected_fingerprint),
NetworkIdentityCrypto.format_fingerprint(received_fingerprint),
]
)
_server_trust_dialog.ok_button_text = "Trust New Identity"
else:
_server_trust_dialog.dialog_text = (
"First time connecting to this server.\n\n%s\n\nHost identity:\n%s"
% [
endpoint,
NetworkIdentityCrypto.format_fingerprint(received_fingerprint),
]
)
_server_trust_dialog.ok_button_text = "Trust & Connect"
_server_trust_dialog.popup_centered(Vector2i(560, 360))
_configure_popup_dialog.call_deferred(
_server_trust_dialog,
_server_trust_dialog.get_cancel_button(),
)
func _confirm_server_trust() -> void:
if _pending_trust_changed:
_pending_trust_changed = false
_server_trust_dialog.dialog_text = (
"Replace the saved server identity pin?\n\n"
+ "Only continue if you expected this server identity to change."
)
_server_trust_dialog.ok_button_text = "Replace Pin"
_server_trust_dialog.popup_centered(Vector2i(520, 260))
_configure_popup_dialog.call_deferred(
_server_trust_dialog,
_server_trust_dialog.get_cancel_button(),
)
return
if _server_trust_dialog != null:
_server_trust_dialog.hide()
_network_session.resolve_server_trust(true)
func _on_peer_identity_observed(_peer_id: int, status: String) -> void:
if status != "New identity using a familiar name":
return
if _identity_notice_dialog == null:
_identity_notice_dialog = AcceptDialog.new()
_identity_notice_dialog.title = "Player identity"
_game_ui.add_child(_identity_notice_dialog)
_identity_notice_dialog.dialog_text = (
"New identity using a familiar name.\n\n"
+ "This may be a different player using the same display name."
)
_identity_notice_dialog.popup_centered(Vector2i(480, 240))
_configure_popup_dialog.call_deferred(
_identity_notice_dialog,
_identity_notice_dialog.get_ok_button(),
)
func _configure_popup_dialog(
dialog: Window,
preferred_control: Control = null,
) -> void:
if dialog != null and is_instance_valid(dialog) and dialog.visible:
FileDialogControllerNavigation.configure_scope(
dialog, preferred_control
)
func _input(event: InputEvent) -> void:
# Do not let controller aliases close or manipulate the world/menu behind
# the modal on-screen keyboard. The keyboard consumes the event itself.
if _game_ui.is_controller_text_entry_open():
return
if _handle_data_root_controller_input(event):
get_viewport().set_input_as_handled()
return
if _game_ui.is_input_mapping_capturing():
return
if (
not (
event.is_action_pressed("ui_cancel")
or event.is_action_pressed("open_system_menu")
)
or (event is InputEventKey and event.echo)
):
return
var pause_open_requested: bool = _is_pause_open_request(event)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
if title_screen.visible or not _gameplay_started:
return
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
var fishing_shop: FishingShopType = _game_ui.get_fishing_shop()
if _game_ui.get_player_storage().consume_escape():
get_viewport().set_input_as_handled()
return
if fishing_shop.consume_escape():
get_viewport().set_input_as_handled()
return
if pause_menu.handle_escape():
get_viewport().set_input_as_handled()
return
if _game_ui.consume_player_menu_escape():
get_viewport().set_input_as_handled()
return
if _fishing_spot.handle_escape_action():
get_viewport().set_input_as_handled()
return
if (
pause_open_requested
and not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_system_menu()
):
_game_ui.close_player_menu_for_game_menu()
pause_menu.open_menu()
get_viewport().set_input_as_handled()
func _handle_data_root_controller_input(event: InputEvent) -> bool:
if _game_ui.is_controller_text_entry_open():
return false
var button_event := event as InputEventJoypadButton
if button_event == null or not button_event.pressed:
return false
var setup_visible: bool = (
_data_setup_dialog != null and _data_setup_dialog.visible
)
var picker_visible: bool = (
_data_folder_dialog != null and _data_folder_dialog.visible
)
if not setup_visible and not picker_visible:
return false
var picker_scope: Window = (
FileDialogControllerNavigation.active_scope(_data_folder_dialog)
if picker_visible
else null
)
var use_mapping: bool = _controller_mapping_manager != null
var accept_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event,
ControllerMappingManagerType.ROLE_A,
)
if use_mapping
else (
button_event.button_index == JOY_BUTTON_A
or event.is_action_pressed("ui_accept")
)
)
var cancel_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event,
ControllerMappingManagerType.ROLE_B,
)
if use_mapping
else (
button_event.button_index == JOY_BUTTON_B
or event.is_action_pressed("ui_cancel")
)
)
if cancel_pressed:
if picker_visible:
if picker_scope != null and picker_scope != _data_folder_dialog:
picker_scope.hide()
else:
_on_data_folder_picker_canceled()
else:
_data_setup_dialog.get_cancel_button().pressed.emit()
return true
if not accept_pressed:
return false
var focused: Control = (
picker_scope.gui_get_focus_owner()
if picker_visible
else _data_setup_dialog.gui_get_focus_owner()
)
if (
(focused is LineEdit or focused is TextEdit)
and _game_ui.request_controller_text_entry_for(focused)
):
return true
# Buttons are activated once through Godot's native ui_accept path. This
# handler only owns modal back behavior and controller text entry.
return false
func _is_pause_open_request(event: InputEvent) -> bool:
return (
event.is_action_pressed("open_system_menu")
or event is InputEventKey
)
func _unhandled_input(event: InputEvent) -> void:
if (
not _gameplay_started
or not event.is_action_pressed("interact")
or (event is InputEventKey and event.echo)
):
return
if (
_storage_interaction != null
and _storage_interaction.is_local_player_in_range()
and _fishing_spot.can_open_fishing_shop()
and _game_ui.get_player_storage().open_storage()
):
_game_ui.set_storage_prompt_visible(false)
get_viewport().set_input_as_handled()
return
if (
_shop_interaction != null
and _shop_interaction.is_local_player_in_range()
and _fishing_spot.can_open_fishing_shop()
and _game_ui.get_fishing_shop().open_shop()
):
_game_ui.set_shop_prompt_visible(false)
get_viewport().set_input_as_handled()
func _process(delta: float) -> void:
if _dedicated_runtime:
_dedicated_metrics_accumulator += delta
if _dedicated_metrics_accumulator >= 60.0:
_dedicated_metrics_accumulator = fmod(
_dedicated_metrics_accumulator,
60.0,
)
_print_dedicated_metrics()
return
var show_shop_prompt := _can_show_shop_prompt()
var shop_prompt_anchor := (
_shop_interaction.get_prompt_anchor_position()
if _shop_interaction != null
else Vector3.ZERO
)
_game_ui.set_shop_prompt_visible(
show_shop_prompt,
shop_prompt_anchor,
)
var show_storage_prompt := _can_show_storage_prompt()
var storage_prompt_anchor := (
_storage_interaction.get_prompt_anchor_position()
if _storage_interaction != null
else Vector3.ZERO
)
_game_ui.set_storage_prompt_visible(
show_storage_prompt,
storage_prompt_anchor,
)
func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
if settings == null:
return
var requested_window_mode: DisplayServer.WindowMode = (
DisplayServer.WINDOW_MODE_FULLSCREEN
if settings.fullscreen_enabled
else DisplayServer.WINDOW_MODE_WINDOWED
)
if DisplayServer.window_get_mode() != requested_window_mode:
DisplayServer.window_set_mode(requested_window_mode)
_apply_world_pixelation(settings.world_pixel_size)
_ui_pixelation.set_pixel_size(settings.ui_pixel_size)
_ui_pixelation.set_on_screen_keyboard_enabled(
settings.on_screen_keyboard_enabled
)
_game_ui.get_title_screen().set_world_pixelation(
settings.world_pixel_size
)
_player.apply_camera_settings(
settings.mouse_camera_sensitivity,
settings.controller_camera_sensitivity,
settings.invert_camera_y,
settings.swap_hotbar_camera_scroll,
)
_fishing_spot.configure_accessibility_auto_click(
settings.auto_click_enabled,
settings.auto_click_interval
)
_game_ui.set_edge_docks(
settings.chat_dock_right,
settings.paint_dock_right,
settings.chat_mobile_mode,
)
func _apply_world_pixelation(pixel_size: int) -> void:
var root_viewport: Viewport = get_viewport()
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
root_viewport.scaling_3d_scale = (
_performance_profile.get_world_render_scale()
)
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
root_viewport.use_taa = false
_world_pixelation.set_pixel_size(pixel_size)
func _on_effective_ui_pixel_size_changed(
_requested_pixel_size: int,
effective_pixel_size: int,
) -> void:
_game_ui.set_effective_ui_pixel_size(effective_pixel_size)
func _reset_pixelation() -> void:
if _settings_manager.apply_pixelation(
PlayerSettingsType.DEFAULT_WORLD_PIXEL_SIZE,
PlayerSettingsType.DEFAULT_UI_PIXEL_SIZE
):
_game_ui.refresh_open_settings_panel()
func _set_gameplay_active(active: bool) -> void:
var was_gameplay_started: bool = _gameplay_started
_gameplay_started = active
_shoreline_ambience.set_active(active)
_rain_ambience.set_active(active)
if active and not was_gameplay_started:
var current_hour: float = _world_time.get_time_hours()
_dusk_music_played_for_natural_day = (
current_hour >= WorldTimeServiceType.DUSK_START_HOUR
or current_hour < WorldTimeServiceType.DAWN_START_HOUR
)
elif not active:
_dusk_music.stop()
_title_background.visible = not active
_world_pixelation.set_gameplay_active(active)
_ui_pixelation.set_gameplay_active(active)
if not active:
_player.item_effects.reset_all()
_player.set_movement_enabled(active)
_player.set_camera_input_enabled(active)
_player.set_camera_active(active)
_fishing_spot.set_gameplay_input_enabled(active)
_gathering_controller.set_gameplay_input_enabled(active)
_water_recovery.set_recovery_enabled(active)
_game_ui.set_gameplay_ui_enabled(active)
_save_manager.set_autosave_enabled(active)
if active:
_player_jobs.begin_progression_session()
_refresh_active_hotbar_item()
else:
_player_jobs.end_progression_session()
_set_player_menu_backdrop_visible(false)
_set_shop_backdrop_visible(false)
func _on_natural_time_advanced(advanced_hours: float) -> void:
if not _gameplay_started or advanced_hours <= 0.0:
return
var current_hour: float = _world_time.get_time_hours()
var previous_hour: float = fposmod(
current_hour - advanced_hours,
WorldTimeServiceType.HOURS_PER_DAY,
)
if _natural_interval_crosses_hour(
previous_hour,
advanced_hours,
WorldTimeServiceType.DAWN_START_HOUR,
):
_dusk_music_played_for_natural_day = false
if (
not _dusk_music_played_for_natural_day
and _natural_interval_crosses_hour(
previous_hour,
advanced_hours,
WorldTimeServiceType.DUSK_START_HOUR,
)
):
_dusk_music_played_for_natural_day = true
if (
_dusk_music.stream != null
and not _new_game_music.playing
):
_dusk_music.play(0.0)
static func _natural_interval_crosses_hour(
previous_hour: float,
advanced_hours: float,
target_hour: float,
) -> bool:
if advanced_hours >= WorldTimeServiceType.HOURS_PER_DAY:
return true
var distance_to_target: float = fposmod(
target_hour - previous_hour,
WorldTimeServiceType.HOURS_PER_DAY,
)
return (
distance_to_target > 0.0
and distance_to_target
<= advanced_hours + TIME_CROSSING_EPSILON_HOURS
)
func _set_player_menu_backdrop_visible(requested_visible: bool) -> void:
if _player_menu_backdrop_tween != null:
_player_menu_backdrop_tween.kill()
_player_menu_backdrop_tween = null
var should_show: bool = requested_visible and _gameplay_started
if should_show:
var was_visible: bool = _player_menu_backdrop.visible
_player_menu_backdrop.visible = true
if not was_visible:
_player_menu_backdrop.modulate.a = 0.0
_player_menu_backdrop_tween = create_tween()
_player_menu_backdrop_tween.tween_property(
_player_menu_backdrop,
"modulate:a",
1.0,
UIMotion.PLAYER_MENU_ENTER_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
return
if not _player_menu_backdrop.visible:
return
_player_menu_backdrop_tween = create_tween()
_player_menu_backdrop_tween.tween_property(
_player_menu_backdrop,
"modulate:a",
0.0,
UIMotion.PLAYER_MENU_EXIT_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
_player_menu_backdrop_tween.finished.connect(
func() -> void:
_player_menu_backdrop.visible = false
_player_menu_backdrop.modulate.a = 1.0
_player_menu_backdrop_tween = null
)
func _set_shop_backdrop_visible(requested_visible: bool) -> void:
if _shop_backdrop_tween != null:
_shop_backdrop_tween.kill()
_shop_backdrop_tween = null
var should_show: bool = requested_visible and _gameplay_started
if should_show:
var was_visible: bool = _shop_backdrop.visible
_shop_backdrop.visible = true
if not was_visible:
_shop_backdrop.modulate.a = 0.0
_shop_backdrop_tween = create_tween()
_shop_backdrop_tween.tween_property(
_shop_backdrop,
"modulate:a",
1.0,
UIMotion.PLAYER_MENU_ENTER_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
return
if not _shop_backdrop.visible:
return
_shop_backdrop_tween = create_tween()
_shop_backdrop_tween.tween_property(
_shop_backdrop,
"modulate:a",
0.0,
UIMotion.PLAYER_MENU_EXIT_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
_shop_backdrop_tween.finished.connect(
func() -> void:
_shop_backdrop.visible = false
_shop_backdrop.modulate.a = 1.0
_shop_backdrop_tween = null
)
func _resize_native_overlays() -> void:
if not is_node_ready():
return
_player_menu_backdrop.position = Vector2.ZERO
_player_menu_backdrop.size = Vector2(get_window().size)
_shop_backdrop.position = Vector2.ZERO
_shop_backdrop.size = Vector2(get_window().size)
func _on_new_game_requested(
world_layout: StringName,
world_seed: int,
) -> void:
if _gameplay_started or _quit_in_progress:
return
_start_new_game_music()
if not _apply_world(world_layout, world_seed, true):
_show_title_music(true)
_game_ui.get_title_screen().report_network_error(
"Could not prepare that world. Try another seed or the starter island."
)
return
if not _prepare_host_world(world_layout, world_seed):
_show_title_music(true)
_game_ui.get_title_screen().report_network_error(
"Could not prepare that world for hosting."
)
return
if not _prepare_private_host():
_show_title_music(true)
return
if (
not _save_manager.delete_progression_save()
or not _save_manager.initialize_new_game(world_seed, world_layout)
):
_network_session.disconnect_session("New Game setup failed.")
_show_title_music(true)
_game_ui.get_title_screen().report_network_error(
"Could not initialize local progression. Existing data was preserved where possible."
)
return
_enter_gameplay()
func _on_continue_game_requested() -> void:
if _gameplay_started or _quit_in_progress:
return
if not _save_manager.load_player_data():
_game_ui.get_title_screen().report_network_error(
"Failed to load save. The original was preserved."
)
return
var world_layout: StringName = _save_manager.get_world_layout()
var world_seed: int = _save_manager.get_world_seed()
if (
not _apply_world(world_layout, world_seed, true)
or not _prepare_host_world(world_layout, world_seed)
):
_game_ui.get_title_screen().report_network_error(
"The saved world could not be loaded. The save was preserved."
)
return
if not _prepare_private_host():
return
_enter_gameplay()
func _on_slot_play_requested(slot_id: String) -> void:
if _gameplay_started or _quit_in_progress:
return
if not _save_slots.activate_slot(slot_id):
_game_ui.get_title_screen().report_network_error(
"The selected save slot could not be opened."
)
return
if not _save_manager.load_player_data():
_game_ui.get_title_screen().report_network_error(
"Failed to load that save slot. The original was preserved."
)
return
var world_layout: StringName = _save_manager.get_world_layout()
var world_seed: int = _save_manager.get_world_seed()
if (
not _apply_world(world_layout, world_seed, true)
or not _prepare_host_world(world_layout, world_seed)
):
_game_ui.get_title_screen().report_network_error(
"The saved world could not be loaded. The save was preserved."
)
return
if not _prepare_private_host():
return
_save_slots.mark_played(slot_id)
_enter_gameplay()
func _on_new_slot_requested(
display_name: String,
world_layout: StringName,
world_seed: int,
duplicate_source_slot_id: String,
) -> void:
if _gameplay_started or _quit_in_progress:
return
var created: Dictionary = (
_save_slots.duplicate_slot(
duplicate_source_slot_id,
display_name,
world_layout,
world_seed,
)
if not duplicate_source_slot_id.is_empty()
else _save_slots.create_empty_slot(display_name)
)
if not bool(created.get("ok", false)):
_game_ui.get_title_screen().report_network_error(
str(created.get("message", "The save slot could not be created."))
)
return
var slot_id: String = str(created.get("slot_id", ""))
if not _save_slots.activate_slot(slot_id):
_game_ui.get_title_screen().report_network_error(
"The new save slot could not be selected."
)
return
if not duplicate_source_slot_id.is_empty():
_on_slot_play_requested(slot_id)
return
_start_new_game_music()
if not _apply_world(world_layout, world_seed, true):
_show_title_music(true)
_game_ui.get_title_screen().report_network_error(
"Could not prepare that world. Try another seed or the starter island."
)
return
if not _prepare_host_world(world_layout, world_seed):
_show_title_music(true)
_game_ui.get_title_screen().report_network_error(
"Could not prepare that world for hosting."
)
return
if not _prepare_private_host():
_show_title_music(true)
return
if (
not _save_manager.delete_progression_save()
or not _save_manager.initialize_new_game(world_seed, world_layout)
):
_network_session.disconnect_session("New Game setup failed.")
_show_title_music(true)
_game_ui.get_title_screen().report_network_error(
"Could not initialize local progression."
)
return
_save_slots.mark_played(slot_id)
_enter_gameplay()
func _prepare_private_host() -> bool:
_join_requested_from_title = false
_join_requested_from_pause = false
if _network_session.state in [
NetworkSessionType.State.CONNECTION_FAILED,
NetworkSessionType.State.SERVER_LOST,
]:
_network_session.reset_failure()
if not _network_session.start_private_host(
NetworkSessionType.DEFAULT_PORT,
NetworkSessionType.DEFAULT_PRIVATE_HOST_PORT_ATTEMPTS,
):
_game_ui.get_title_screen().report_network_error(
"Could not start the private multiplayer session."
)
return false
return true
func _prepare_host_world(
world_layout: StringName,
world_seed: int,
) -> bool:
if _network_session.state in [
NetworkSessionType.State.CONNECTION_FAILED,
NetworkSessionType.State.SERVER_LOST,
]:
_network_session.reset_failure()
return _network_session.set_host_world(world_layout, world_seed)
func _enter_gameplay() -> void:
_fade_out_title_music()
_game_ui.get_title_screen().hide()
_set_gameplay_active(true)
func _on_return_to_title_requested() -> void:
if _quit_in_progress:
return
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
if not _save_manager.save_world_time_checkpoint():
pause_menu.report_network_error(
"Could not save progression before returning to title."
)
return
pause_menu.close_for_title_transition()
_network_session.disconnect_session("Returned to title.")
_set_gameplay_active(false)
_game_ui.get_title_screen().reopen()
_show_title_music(true)
func _on_title_join_game_requested(endpoint: String) -> void:
if _quit_in_progress or _gameplay_started:
return
var active_slot: Dictionary = _save_slots.ensure_active_slot()
if not bool(active_slot.get("ok", false)):
_game_ui.get_title_screen().report_network_error(
str(active_slot.get("message", "Local progression is unavailable."))
)
return
if _network_session.state != NetworkSessionType.State.INACTIVE:
_network_session.disconnect_session("Preparing direct connection.")
_join_requested_from_title = true
_join_requested_from_pause = false
_pending_join_endpoint = endpoint
if not _network_session.join_direct(endpoint):
_join_requested_from_title = false
func _on_pause_join_game_requested(endpoint: String) -> void:
if _quit_in_progress or not _gameplay_started:
return
if not _save_manager.save_world_time_checkpoint():
_game_ui.get_pause_menu().report_network_error(
"Could not save progression before leaving this session."
)
return
_join_requested_from_pause = true
_join_requested_from_title = false
_pending_join_endpoint = endpoint
_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.")
if not _network_session.join_direct(endpoint):
if preserved_public_join:
_discovery.cancel_pending_public_join()
_handle_failed_session_switch(
"Could not begin the direct connection."
)
func _on_friend_request_received(
request_id: String,
fingerprint: String,
display_name: String,
) -> void:
_queue_social_prompt({
"kind": "friend_request",
"request_id": request_id,
"fingerprint": fingerprint,
"display_name": display_name,
})
func _on_friend_invite_received(
fingerprint: String,
display_name: String,
room: Dictionary,
) -> void:
if (
not _relationships.is_friend(fingerprint)
or _relationships.is_blocked(fingerprint)
):
return
_queue_social_prompt({
"kind": "friend_invite",
"fingerprint": fingerprint,
"display_name": display_name,
"room": room.duplicate(true),
})
func _queue_social_prompt(details: Dictionary) -> void:
_social_prompt_queue.append(details.duplicate(true))
_show_next_social_prompt()
func _show_next_social_prompt() -> void:
if (
not _active_social_prompt.is_empty()
or _game_ui.is_social_prompt_open()
or _social_prompt_queue.is_empty()
):
return
_active_social_prompt = _social_prompt_queue.pop_front()
var kind := str(_active_social_prompt.get("kind", ""))
var display_name := str(
_active_social_prompt.get("display_name", "Player")
)
var shown: bool = false
if kind == "friend_request":
shown = _game_ui.show_social_prompt(
"%s wants to add you as a friend.\n" % display_name
+ "Friendships are saved on this device.",
"accept",
"decline",
)
elif kind == "friend_invite":
var room: Dictionary = _active_social_prompt.get("room", {})
var room_name := str(room.get("room_name", "their room"))
shown = _game_ui.show_social_prompt(
"%s invited you to join %s." % [display_name, room_name],
"join",
"not now",
)
if not shown:
_active_social_prompt.clear()
call_deferred("_show_next_social_prompt")
func _on_social_prompt_accepted() -> void:
var details: Dictionary = _active_social_prompt.duplicate(true)
_active_social_prompt.clear()
match str(details.get("kind", "")):
"friend_request":
_network_player_list.respond_friend_request(
str(details.get("request_id", "")), true
)
"friend_invite":
var room: Dictionary = details.get("room", {})
_pending_social_invite_join = true
_pending_social_invite_gameplay = _gameplay_started
if room.is_empty() or not _discovery.prepare_public_join(room):
_pending_social_invite_join = false
_report_social_join_error(
"This person needs to be online to do this."
)
call_deferred("_show_next_social_prompt")
func _on_social_prompt_declined() -> void:
var details: Dictionary = _active_social_prompt.duplicate(true)
_active_social_prompt.clear()
if str(details.get("kind", "")) == "friend_request":
_network_player_list.respond_friend_request(
str(details.get("request_id", "")), false
)
call_deferred("_show_next_social_prompt")
func _on_social_public_join_prepared(endpoint: String) -> void:
if not _pending_social_invite_join:
return
var from_gameplay := _pending_social_invite_gameplay
_pending_social_invite_join = false
_pending_social_invite_gameplay = false
if from_gameplay:
_on_pause_join_game_requested(endpoint)
else:
_on_title_join_game_requested(endpoint)
func _on_social_public_join_status_changed(
message: String,
is_error: bool,
) -> void:
if not _pending_social_invite_join or not is_error:
return
_pending_social_invite_join = false
_pending_social_invite_gameplay = false
_report_social_join_error(message)
func _report_social_join_error(message: String) -> void:
if _gameplay_started:
_game_ui.get_pause_menu().report_network_error(message)
else:
_game_ui.get_title_screen().report_network_error(message)
func _on_network_join_authenticated() -> void:
var connected_endpoint: ConnectionEndpoint = (
_network_session.get_current_endpoint()
)
var server_metadata: Dictionary = (
_network_session.get_last_server_metadata()
)
if connected_endpoint != null:
_saved_servers.record_successful_connection(
connected_endpoint,
int(server_metadata.get("max_players", 0)),
str(server_metadata.get("server_display_name", "")),
int(server_metadata.get("protocol_version", 0)),
int(server_metadata.get("player_count", 0)),
)
if _join_requested_from_title:
var inspection = _save_manager.inspect_save()
var progression_ready: bool = (
_save_manager.load_player_data()
if inspection.can_continue()
else _save_manager.initialize_new_game()
)
if not progression_ready:
_network_session.disconnect_session(
"Local progression could not be prepared."
)
_game_ui.get_title_screen().report_network_error(
"Your local progression could not be prepared."
)
_join_requested_from_title = false
return
_save_slots.mark_played(_save_slots.get_active_slot_id())
if not _apply_joined_world(server_metadata):
return
_fade_out_title_music()
_game_ui.get_title_screen().hide()
_set_gameplay_active(true)
elif _join_requested_from_pause:
if not _apply_joined_world(server_metadata):
return
_set_gameplay_active(true)
_join_requested_from_title = false
_join_requested_from_pause = false
_pending_join_endpoint = ""
func _on_network_connection_error(message: String) -> void:
var failed_endpoint: ConnectionEndpoint = (
_network_session.get_current_endpoint()
)
if failed_endpoint != null:
_saved_servers.record_connection_failure(
failed_endpoint,
_connection_result_code(message),
)
if _join_requested_from_title:
_game_ui.get_title_screen().report_network_error(message)
elif _join_requested_from_pause:
_handle_failed_session_switch(message)
func _connection_result_code(message: String) -> String:
var lowered: String = message.to_lower()
if lowered.contains("protocol"):
return "PROTOCOL_MISMATCH"
if lowered.contains("full"):
return "SERVER_FULL"
if lowered.contains("cancel"):
return "CANCELLED"
if lowered.contains("timed out"):
return "TIMEOUT"
return "UNAVAILABLE"
func _handle_failed_session_switch(message: String) -> void:
_join_requested_from_pause = false
_set_gameplay_active(false)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
title_screen.reopen()
title_screen.open_join_game_page(_pending_join_endpoint)
title_screen.report_network_error(message)
_show_title_music(true)
func _on_network_server_lost(message: String) -> void:
if not _gameplay_started:
return
_join_requested_from_title = false
_join_requested_from_pause = false
_pending_join_endpoint = ""
_set_gameplay_active(false)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
title_screen.reopen_to_menu()
title_screen.report_network_error(message)
_show_title_music(true)
func _on_local_respawn_completed(entry_position: Vector3) -> void:
if _network_session.is_host():
_network_session.publish_authoritative_teleport(
_player.get_network_peer_id()
)
elif _network_session.is_joined_client():
_network_session.request_safe_respawn(entry_position)
func _on_remote_recovery_requested(
peer_id: int,
entry_position: Vector3,
) -> void:
if not _network_session.is_host():
return
var avatar: PlayerType = _player_spawn_service.get_avatar(peer_id)
if avatar == null:
return
_network_fishing.cancel_peer_attempt(
peer_id,
"Fishing attempt ended."
)
var target_position := _test_world.get_water_recovery_position(
entry_position
)
target_position.y += _water_recovery.respawn_height_offset
avatar.global_position = target_position
avatar.velocity = Vector3.ZERO
_network_session.publish_authoritative_teleport(peer_id)
func _on_reset_progress_requested() -> void:
if _quit_in_progress:
return
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
if not _save_manager.delete_progression_save():
pause_menu.report_reset_failure()
return
var world_seed: int = PlayerSaveManager.roll_world_seed()
if not _save_manager.initialize_new_game(
world_seed,
WorldLayoutType.GENERATED,
):
pause_menu.report_reset_failure()
return
_network_session.disconnect_session("Progress reset.")
if not _apply_world(WorldLayoutType.GENERATED, world_seed, true):
pause_menu.report_reset_failure()
return
pause_menu.close_for_title_transition()
_set_gameplay_active(false)
_game_ui.get_title_screen().reopen()
_show_title_music(true)
func _apply_joined_world(server_metadata: Dictionary) -> bool:
var world_layout: StringName = WorldLayoutType.normalized(
server_metadata.get(
"world_layout",
String(WorldLayoutType.GENERATED),
),
&"",
)
var world_seed: int = int(server_metadata.get("world_seed", 0))
if (
not world_layout.is_empty()
and _apply_world(world_layout, world_seed, true)
):
return true
_network_session.disconnect_session("World generation failed.")
_join_requested_from_title = false
_join_requested_from_pause = false
_set_gameplay_active(false)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
title_screen.reopen_to_menu()
title_screen.report_network_error(
"The host's world could not be built locally."
)
_show_title_music(true)
return false
func _apply_world(
world_layout: StringName,
world_seed: int,
reposition_player: bool,
) -> bool:
if not _test_world.activate_world(world_layout, world_seed):
return false
var spawn_transform: Transform3D = _test_world.get_player_spawn_transform()
_player_spawn_service.set_spawn_transform(spawn_transform)
if reposition_player:
_player.global_transform = spawn_transform
_player.velocity = Vector3.ZERO
if _application_initialized:
_refresh_active_world_bindings()
_network_world_spawns.refresh_world_context()
if _application_initialized and not _dedicated_runtime:
_water_recovery.update_world_context(
spawn_transform,
_test_world.get_player_water_triggers(),
_test_world.get_safe_respawn_points(),
Callable(_test_world, "get_water_recovery_position"),
)
_shoreline_ambience.configure(
_player,
_test_world.get_saltwater_shoreline_mesh(),
)
return true
func _refresh_active_world_bindings() -> void:
if is_instance_valid(_shop_interaction):
if _shop_interaction.local_player_range_changed.is_connected(
_on_shop_range_changed
):
_shop_interaction.local_player_range_changed.disconnect(
_on_shop_range_changed
)
if is_instance_valid(_storage_interaction):
if _storage_interaction.local_player_range_changed.is_connected(
_on_storage_range_changed
):
_storage_interaction.local_player_range_changed.disconnect(
_on_storage_range_changed
)
_shop_interaction = _test_world.get_fishing_shop()
_storage_interaction = _test_world.get_player_storage()
_network_sale.set_shop_interaction(_shop_interaction)
_network_shop.set_shop_interaction(_shop_interaction)
if _dedicated_runtime:
return
if _shop_interaction != null:
_shop_interaction.setup_local_player(_player)
_shop_interaction.local_player_range_changed.connect(
_on_shop_range_changed
)
if _storage_interaction != null:
_storage_interaction.setup_local_player(_player)
_storage_interaction.local_player_range_changed.connect(
_on_storage_range_changed
)
_game_ui.set_world_interactions(
_shop_interaction,
_storage_interaction,
)
_on_shop_range_changed(
_shop_interaction != null
and _shop_interaction.is_local_player_in_range()
)
_on_storage_range_changed(
_storage_interaction != null
and _storage_interaction.is_local_player_in_range()
)
func _on_water_recovery_starting() -> void:
_local_recovery_attempt_id = (
"recovery:%s"
% Crypto.new().generate_random_bytes(16).hex_encode()
)
_network_session.request_recovery_presentation(
true, _local_recovery_attempt_id
)
_game_ui.close_player_menu_for_water_recovery()
_game_ui.get_pause_menu().close_for_water_recovery()
_game_ui.get_fishing_shop().close_for_water_recovery()
_game_ui.get_player_storage().close_for_water_recovery()
func _on_water_recovery_finished() -> void:
if _local_recovery_attempt_id.is_empty():
return
_network_session.request_recovery_presentation(
false, _local_recovery_attempt_id
)
_local_recovery_attempt_id = ""
func _on_remote_recovery_presentation_changed(
peer_id: int,
active: bool,
_attempt_id: String,
) -> void:
var avatar: PlayerType = _player_spawn_service.get_avatar(peer_id)
if avatar == null or avatar == _player:
return
avatar.set_water_recovery_active(active)
avatar.set_remote_recovery_presentation(active)
func _on_active_hotbar_item_changed(
_slot_index: int,
kind: int,
identity: StringName,
) -> void:
if _fishing_spot.state != FishingSpotType.FishingState.READY:
return
var item_id: StringName = (
identity
if kind == PlayerHotbarType.AssignmentKind.ITEM
else StringName()
)
var item = item_catalog.get_item_by_id(item_id)
var active_is_rod: bool = (
item != null
and item.is_available()
and item.category == ItemDataType.Category.ROD
and _player.bag.owns_item(item_id)
)
var active_is_art_kit: bool = (
item_id == ArtShopStockType.ART_KIT_ITEM_ID
and item != null
and item.is_available()
and _player.bag.owns_item(item_id)
)
var active_is_catching_net: bool = (
item_id == FishingShopStockType.CRAB_NET_ID
and item != null
and item.is_available()
and _player.bag.owns_item(item_id)
)
var active_is_shovel: bool = (
item_id == FishingShopStockType.STANDARD_SHOVEL_ID
and item != null
and item.is_available()
and _player.bag.owns_item(item_id)
)
_player.set_active_fishing_rod(
item as FishingRodDataType if active_is_rod else null,
true,
)
_player.set_active_art_kit(
item.icon if item != null else null,
active_is_art_kit,
)
_player.set_active_catching_net(active_is_catching_net)
_player.set_active_shovel(active_is_shovel)
_game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
item != null and _player.bag.owns_item(item_id)
))
_fishing_spot.refresh_active_item_status()
func _refresh_active_hotbar_item() -> void:
var kind: int = (
_player.hotbar.get_selected_assignment_kind()
)
var identity: StringName = StringName()
if kind == PlayerHotbarType.AssignmentKind.ITEM:
identity = _player.hotbar.get_selected_item_id()
elif kind == PlayerHotbarType.AssignmentKind.FISH:
identity = _player.hotbar.get_selected_fish_catch_id()
_on_active_hotbar_item_changed(
_player.hotbar.get_selected_slot(),
kind,
identity,
)
func _on_quit_requested() -> void:
if _quit_in_progress:
return
_quit_in_progress = true
_settings_manager.save_if_dirty()
if _gameplay_started:
_save_manager.save_world_time_checkpoint()
_network_session.disconnect_session("Application closing.")
_title_music_requested = false
_replace_title_music_transition()
_finish_quit()
func _show_title_music(restart_from_beginning: bool = false) -> void:
_new_game_music.stop()
_title_music_requested = true
_replace_title_music_transition()
if _title_music.stream == null:
return
if restart_from_beginning:
_title_music.stop()
_title_music.volume_db = title_music_volume_db
if restart_from_beginning or not _title_music.playing:
_title_music.play(0.0)
func _start_new_game_music() -> void:
if _new_game_music.stream == null:
return
_title_music_requested = false
_replace_title_music_transition()
_title_music.stop()
_title_music.volume_db = title_music_volume_db
_dusk_music.stop()
_new_game_music.stop()
_new_game_music.volume_db = new_game_music_volume_db
_new_game_music.play(0.0)
func _fade_out_title_music(on_complete: Callable = Callable()) -> void:
_title_music_requested = false
var generation: int = _replace_title_music_transition()
if not _title_music.playing:
_title_music.volume_db = title_music_volume_db
if on_complete.is_valid():
on_complete.call()
return
if title_music_fade_out_seconds <= 0.0:
_complete_title_music_fade_out(generation, on_complete)
return
_title_music_tween = create_tween()
_title_music_tween.set_trans(Tween.TRANS_CUBIC)
_title_music_tween.set_ease(Tween.EASE_IN)
_title_music_tween.tween_property(
_title_music,
"volume_db",
TITLE_MUSIC_SILENCE_DB,
title_music_fade_out_seconds
)
_title_music_tween.finished.connect(
_complete_title_music_fade_out.bind(generation, on_complete),
CONNECT_ONE_SHOT
)
func _replace_title_music_transition() -> int:
_title_music_transition_generation += 1
if _title_music_tween != null:
_title_music_tween.kill()
_title_music_tween = null
return _title_music_transition_generation
func _complete_title_music_fade_out(
generation: int,
on_complete: Callable,
) -> void:
if (
generation != _title_music_transition_generation
or _title_music_requested
):
return
_title_music_tween = null
_title_music.stop()
_title_music.volume_db = title_music_volume_db
if on_complete.is_valid():
on_complete.call()
func _finish_quit() -> void:
_title_music.queue_free()
await get_tree().process_frame
get_tree().quit()
func _exit_tree() -> void:
if _dedicated_runtime and _network_session != null:
_network_session.disconnect_session("Dedicated server stopping.")
if _title_music_tween != null:
_title_music_tween.kill()
_title_music_tween = null
if is_instance_valid(_title_music):
_title_music.stop()
_title_music.stream = null
if is_instance_valid(_new_game_music):
_new_game_music.stop()
_new_game_music.stream = null
if is_instance_valid(_dusk_music):
_dusk_music.stop()
_dusk_music.stream = null
func _on_shop_range_changed(in_range: bool) -> void:
_game_ui.set_shop_npc_player_in_range(in_range)
if not in_range:
_game_ui.get_fishing_shop().close_for_range_exit()
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
func _on_storage_range_changed(in_range: bool) -> void:
if not in_range:
_game_ui.get_player_storage().close_for_range_exit()
_game_ui.set_storage_prompt_visible(_can_show_storage_prompt())
func _can_show_shop_prompt() -> bool:
return (
_gameplay_started
and _shop_interaction != null
and _shop_interaction.is_local_player_in_range()
and not _game_ui.get_fishing_shop().visible
and not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_fishing_shop()
)
func _can_show_storage_prompt() -> bool:
return (
_gameplay_started
and _storage_interaction != null
and _storage_interaction.is_local_player_in_range()
and not _game_ui.get_player_storage().visible
and not _game_ui.get_fishing_shop().visible
and not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_fishing_shop()
)