Add dedicated server runtime
This commit is contained in:
parent
e204279b09
commit
74c82696fe
14 changed files with 595 additions and 47 deletions
|
|
@ -114,6 +114,36 @@ application/architecture="universal"
|
|||
codesign/enable=false
|
||||
notarization/enable=false
|
||||
|
||||
[preset.5]
|
||||
|
||||
name="Linux Dedicated Server"
|
||||
platform="Linux"
|
||||
runnable=false
|
||||
advanced_options=false
|
||||
dedicated_server=true
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="builds/*,playtest/*,scripts/*,tests/*"
|
||||
export_path="builds/v0.6.4-alpha/server-linux-x86_64/NETfishingServer.x86_64"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.5.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=0
|
||||
binary_format/embed_pck=false
|
||||
binary_format/architecture="x86_64"
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="Android"
|
||||
|
|
|
|||
163
main/main.gd
163
main/main.gd
|
|
@ -39,6 +39,9 @@ const WorldPixelationPostprocessType = preload(
|
|||
)
|
||||
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"
|
||||
)
|
||||
|
|
@ -215,12 +218,17 @@ var _restore_data_setup_after_picker: bool = false
|
|||
var _application_initialized := false
|
||||
var _pending_existing_root_path := ""
|
||||
var _local_recovery_attempt_id: String = ""
|
||||
var _dedicated_runtime: bool = false
|
||||
|
||||
@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
|
||||
var _rain_ambience: RainAmbienceType
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_dedicated_runtime = _is_dedicated_server_runtime()
|
||||
if _dedicated_runtime:
|
||||
call_deferred("_start_dedicated_server")
|
||||
return
|
||||
DisplayServer.window_set_title("NETfishing")
|
||||
_rain_ambience = RainAmbienceType.new()
|
||||
_rain_ambience.name = "RainAmbience"
|
||||
|
|
@ -267,6 +275,83 @@ func _ready() -> void:
|
|||
_show_data_root_setup()
|
||||
|
||||
|
||||
func _is_dedicated_server_runtime() -> bool:
|
||||
if OS.has_feature("dedicated_server"):
|
||||
return true
|
||||
return "--dedicated-server" in OS.get_cmdline_user_args()
|
||||
|
||||
|
||||
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
|
||||
var manifest_path: String = data_path.path_join(
|
||||
PlayerDataRoot.MANIFEST_FILENAME
|
||||
)
|
||||
if not FileAccess.file_exists(manifest_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
|
||||
_configure_portable_stores()
|
||||
_initialize_application(true)
|
||||
_player.set_local_control(false)
|
||||
_player.visible = false
|
||||
_player.collision_layer = 0
|
||||
_player.collision_mask = 0
|
||||
_player.set_physics_process(false)
|
||||
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)
|
||||
_discovery.set_room_name(config.server_name)
|
||||
if not _network_session.set_host_open(true):
|
||||
_fail_dedicated_server("Could not open the dedicated server.")
|
||||
return
|
||||
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(
|
||||
"NETfishing dedicated server ready: %s on %s:%d (%d players, %s)"
|
||||
% [
|
||||
config.server_name,
|
||||
config.bind_address,
|
||||
config.port,
|
||||
config.max_players,
|
||||
"public" if config.public_listing else "unlisted",
|
||||
]
|
||||
)
|
||||
print(
|
||||
"Server identity: %s"
|
||||
% _network_session.get_host_identity_fingerprint()
|
||||
)
|
||||
|
||||
|
||||
func _fail_dedicated_server(message: String) -> void:
|
||||
push_error("Dedicated server startup failed: %s" % message)
|
||||
get_tree().quit(1)
|
||||
|
||||
|
||||
func _configure_portable_stores() -> void:
|
||||
_save_manager.configure_storage(
|
||||
_data_root.path_for(&"player_save"), _data_root
|
||||
|
|
@ -295,6 +380,10 @@ func _configure_portable_stores() -> void:
|
|||
|
||||
|
||||
func _initialize_after_data_root() -> void:
|
||||
_initialize_application(false)
|
||||
|
||||
|
||||
func _initialize_application(dedicated: bool) -> void:
|
||||
if _application_initialized:
|
||||
return
|
||||
_application_initialized = true
|
||||
|
|
@ -314,21 +403,23 @@ func _initialize_after_data_root() -> void:
|
|||
_known_players,
|
||||
_server_trust,
|
||||
_host_bans,
|
||||
dedicated,
|
||||
)
|
||||
_discovery.setup(_network_session)
|
||||
_world_time_visuals.setup(
|
||||
_world_time,
|
||||
_test_world.get_world_environment(),
|
||||
_test_world.get_sun(),
|
||||
_world_weather,
|
||||
_player,
|
||||
)
|
||||
if not _world_time.natural_time_advanced.is_connected(
|
||||
_on_natural_time_advanced
|
||||
):
|
||||
_world_time.natural_time_advanced.connect(
|
||||
_on_natural_time_advanced
|
||||
if not dedicated:
|
||||
_world_time_visuals.setup(
|
||||
_world_time,
|
||||
_test_world.get_world_environment(),
|
||||
_test_world.get_sun(),
|
||||
_world_weather,
|
||||
_player,
|
||||
)
|
||||
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(
|
||||
|
|
@ -357,22 +448,23 @@ func _initialize_after_data_root() -> void:
|
|||
_network_chat,
|
||||
_network_mail,
|
||||
)
|
||||
_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
|
||||
)
|
||||
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.server_lost.connect(_on_network_server_lost)
|
||||
_network_session.remote_recovery_requested.connect(
|
||||
_on_remote_recovery_requested
|
||||
)
|
||||
|
|
@ -389,13 +481,14 @@ func _initialize_after_data_root() -> void:
|
|||
_player.bag.setup(item_catalog)
|
||||
_player.hotbar.setup(_player.bag, item_catalog, _player.inventory)
|
||||
_shop_interaction = _test_world.get_fishing_shop()
|
||||
_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()
|
||||
)
|
||||
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()
|
||||
)
|
||||
_save_manager.setup(
|
||||
_player.inventory,
|
||||
_player.collection_log,
|
||||
|
|
@ -521,6 +614,8 @@ func _initialize_after_data_root() -> void:
|
|||
_world_time,
|
||||
_world_weather,
|
||||
)
|
||||
if dedicated:
|
||||
return
|
||||
_game_ui.setup(
|
||||
_player,
|
||||
_player.inventory,
|
||||
|
|
@ -1118,6 +1213,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _dedicated_runtime:
|
||||
return
|
||||
var show_shop_prompt := _can_show_shop_prompt()
|
||||
var shop_prompt_anchor := (
|
||||
_shop_interaction.get_prompt_anchor_position()
|
||||
|
|
@ -1759,6 +1856,8 @@ func _finish_quit() -> void:
|
|||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -87,6 +87,19 @@ func get_base_url() -> String:
|
|||
return _base_url
|
||||
|
||||
|
||||
func set_base_url_override(value: String) -> bool:
|
||||
var normalized: String = value.strip_edges()
|
||||
while normalized.ends_with("/"):
|
||||
normalized = normalized.left(normalized.length() - 1)
|
||||
if not normalized.is_empty() and not (
|
||||
normalized.begins_with("https://")
|
||||
or normalized.begins_with("http://")
|
||||
):
|
||||
return false
|
||||
_base_url = normalized
|
||||
return true
|
||||
|
||||
|
||||
func get_room_name() -> String:
|
||||
return _room_name
|
||||
|
||||
|
|
@ -366,7 +379,7 @@ func _valid_public_room(room: Dictionary) -> bool:
|
|||
and typeof(room.get("game_version")) == TYPE_STRING
|
||||
and str(room.get("game_version")) == expected_version
|
||||
and _valid_json_integer(room.get("port"), 1, 65535)
|
||||
and _valid_json_integer(room.get("current_players"), 1, 128)
|
||||
and _valid_json_integer(room.get("current_players"), 0, 128)
|
||||
and _valid_json_integer(room.get("max_players"), 1, 128)
|
||||
and int(room["current_players"]) <= int(room["max_players"])
|
||||
and _valid_json_integer(
|
||||
|
|
|
|||
|
|
@ -9,11 +9,17 @@ var error_message: String = ""
|
|||
var _private_key: CryptoKey
|
||||
var _prefix: String = ""
|
||||
var _allow_generation: bool = true
|
||||
var _storage_directory: String = ""
|
||||
|
||||
|
||||
func configure(prefix: String, allow_generation: bool = true) -> void:
|
||||
func configure(
|
||||
prefix: String,
|
||||
allow_generation: bool = true,
|
||||
storage_directory: String = "",
|
||||
) -> void:
|
||||
_prefix = prefix
|
||||
_allow_generation = allow_generation
|
||||
_storage_directory = storage_directory.strip_edges()
|
||||
|
||||
|
||||
func load_or_create() -> bool:
|
||||
|
|
@ -225,7 +231,12 @@ func _write_atomic(path: String, content: String) -> bool:
|
|||
|
||||
|
||||
func _path(extension: String) -> String:
|
||||
return "user://%s%s" % [_prefix, extension]
|
||||
var filename: String = "%s%s" % [_prefix, extension]
|
||||
return (
|
||||
_storage_directory.path_join(filename)
|
||||
if not _storage_directory.is_empty()
|
||||
else "user://%s" % filename
|
||||
)
|
||||
|
||||
|
||||
func identity_type() -> String:
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ var _local_appearance_snapshot: Dictionary = (
|
|||
var _moderation_disconnect_message := ""
|
||||
var _host_port: int = 0
|
||||
var _session_display_name: String = "NETfishing Room"
|
||||
var _dedicated_host: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -120,6 +121,7 @@ func setup(
|
|||
known_players: KnownPlayerStore,
|
||||
server_trust: ServerTrustStore,
|
||||
host_bans: HostBanStore,
|
||||
dedicated: bool = false,
|
||||
) -> void:
|
||||
_profile = profile
|
||||
_saved_servers = saved_servers
|
||||
|
|
@ -129,9 +131,9 @@ func setup(
|
|||
_known_players = known_players
|
||||
_server_trust = server_trust
|
||||
_host_bans = host_bans
|
||||
if _profile != null:
|
||||
if not dedicated and _profile != null:
|
||||
_profile_ready = _profile.load_or_create()
|
||||
if _player_identity != null:
|
||||
if not dedicated and _player_identity != null:
|
||||
_profile_ready = _profile_ready and _player_identity.load_or_create()
|
||||
|
||||
|
||||
|
|
@ -147,6 +149,34 @@ func start_private_host(
|
|||
or _host_identity == null
|
||||
):
|
||||
return false
|
||||
return _start_host(port, port_attempts, false)
|
||||
|
||||
|
||||
func start_dedicated_host(
|
||||
port: int = DEFAULT_PORT,
|
||||
max_players: int = DEFAULT_SESSION_MAX_PLAYERS,
|
||||
bind_address: String = "*",
|
||||
) -> bool:
|
||||
if (
|
||||
state != State.INACTIVE
|
||||
or _spawn_service == null
|
||||
or _host_identity == null
|
||||
or max_players < 1
|
||||
or max_players > 128
|
||||
or bind_address.is_empty()
|
||||
):
|
||||
return false
|
||||
session_max_players = max_players
|
||||
transport_max_clients = maxi(transport_max_clients, max_players)
|
||||
return _start_host(port, 1, true, bind_address)
|
||||
|
||||
|
||||
func _start_host(
|
||||
port: int,
|
||||
port_attempts: int,
|
||||
dedicated: bool,
|
||||
bind_address: String = "*",
|
||||
) -> bool:
|
||||
if not _host_identity.load_or_create():
|
||||
_fail(_host_identity.error_message)
|
||||
return false
|
||||
|
|
@ -154,16 +184,20 @@ func start_private_host(
|
|||
_fail("The hosting port must be from 1 to 65535.")
|
||||
return false
|
||||
_operation_generation += 1
|
||||
_set_state(State.STARTING_PRIVATE_HOST, "Starting private game...")
|
||||
_set_state(
|
||||
State.STARTING_PRIVATE_HOST,
|
||||
"Starting dedicated server..." if dedicated else "Starting private game...",
|
||||
)
|
||||
var selected_port: int = 0
|
||||
var final_port: int = mini(port + port_attempts - 1, 65535)
|
||||
for candidate_port: int in range(port, final_port + 1):
|
||||
if not _can_bind_udp_port(candidate_port):
|
||||
if not _can_bind_udp_port(candidate_port, bind_address):
|
||||
continue
|
||||
_replace_transport()
|
||||
var error: Error = _transport.start_host(
|
||||
candidate_port,
|
||||
transport_max_clients,
|
||||
bind_address,
|
||||
)
|
||||
if error == OK:
|
||||
selected_port = candidate_port
|
||||
|
|
@ -175,11 +209,28 @@ func start_private_host(
|
|||
)
|
||||
return false
|
||||
_host_port = selected_port
|
||||
_dedicated_host = dedicated
|
||||
var peer: MultiplayerPeer = _transport.get_multiplayer_peer()
|
||||
peer.refuse_new_connections = true
|
||||
multiplayer.multiplayer_peer = peer
|
||||
_session_id = Crypto.new().generate_random_bytes(16).hex_encode()
|
||||
_registry.clear()
|
||||
_spawn_service.clear_remote_players()
|
||||
if not dedicated:
|
||||
_register_player_host()
|
||||
_set_state(
|
||||
State.PRIVATE_HOST,
|
||||
(
|
||||
"Dedicated server • UDP %d"
|
||||
if dedicated else "Private game • UDP %d"
|
||||
) % selected_port,
|
||||
)
|
||||
host_openness_changed.emit(false)
|
||||
_emit_peer_count()
|
||||
return true
|
||||
|
||||
|
||||
func _register_player_host() -> void:
|
||||
_registry.add_peer(
|
||||
1,
|
||||
_profile.profile_id,
|
||||
|
|
@ -215,21 +266,20 @@ func start_private_host(
|
|||
"client_nonce": host_profile_hello["client_nonce"],
|
||||
}
|
||||
_archive_authenticated_identity(host_record)
|
||||
_spawn_service.clear_remote_players()
|
||||
_spawn_service.register_local_player(1)
|
||||
var host_avatar := _spawn_service.get_avatar(1)
|
||||
if host_avatar != null:
|
||||
host_avatar.apply_appearance_snapshot(_local_appearance_snapshot)
|
||||
_set_state(State.PRIVATE_HOST, "Private game • UDP %d" % selected_port)
|
||||
host_openness_changed.emit(false)
|
||||
_emit_peer_count()
|
||||
return true
|
||||
|
||||
|
||||
func get_host_port() -> int:
|
||||
return _host_port if is_host() else 0
|
||||
|
||||
|
||||
func is_dedicated_host() -> bool:
|
||||
return is_host() and _dedicated_host
|
||||
|
||||
|
||||
func set_session_display_name(value: String) -> void:
|
||||
var cleaned: String = value.strip_edges().left(48)
|
||||
if cleaned.is_empty():
|
||||
|
|
@ -244,9 +294,9 @@ func get_session_display_name() -> String:
|
|||
return _session_display_name
|
||||
|
||||
|
||||
static func _can_bind_udp_port(port: int) -> bool:
|
||||
static func _can_bind_udp_port(port: int, bind_address: String = "*") -> bool:
|
||||
var probe := PacketPeerUDP.new()
|
||||
var error: Error = probe.bind(port)
|
||||
var error: Error = probe.bind(port, bind_address)
|
||||
probe.close()
|
||||
return error == OK
|
||||
|
||||
|
|
@ -328,7 +378,9 @@ func set_host_open(is_open: bool) -> bool:
|
|||
_set_state(
|
||||
State.OPEN_HOST if is_open else State.PRIVATE_HOST,
|
||||
(
|
||||
"Open game • UDP %d • %d / %d players"
|
||||
"Dedicated server • UDP %d • %d / %d players"
|
||||
if _dedicated_host
|
||||
else "Open game • UDP %d • %d / %d players"
|
||||
if is_open
|
||||
else "Private game • UDP %d • %d / %d players"
|
||||
) % [_host_port, _registry.size(), session_max_players]
|
||||
|
|
@ -623,6 +675,8 @@ func _apply_display_name(peer_id: int, value: String) -> void:
|
|||
|
||||
|
||||
func get_local_peer_id() -> int:
|
||||
if is_dedicated_host():
|
||||
return 0
|
||||
return multiplayer.get_unique_id() if is_gameplay_session_active() else 0
|
||||
|
||||
|
||||
|
|
@ -1792,3 +1846,4 @@ func _teardown_peer() -> void:
|
|||
_server_identity_public_key = ""
|
||||
_session_identity_keys.clear()
|
||||
_host_port = 0
|
||||
_dedicated_host = false
|
||||
|
|
|
|||
|
|
@ -169,6 +169,13 @@ func use_existing_root(path: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func activate_process_root(path: String) -> bool:
|
||||
_load_bootstrap_identity()
|
||||
override_active = true
|
||||
mode = Mode.COMMAND_LINE_OVERRIDE
|
||||
return _activate_existing(path, "", false)
|
||||
|
||||
|
||||
func path_for(store_owner: StringName) -> String:
|
||||
var relative: String = {
|
||||
&"player_save": "player/player_save.json",
|
||||
|
|
|
|||
166
server/dedicated_server_config.gd
Normal file
166
server/dedicated_server_config.gd
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
class_name DedicatedServerConfig
|
||||
extends RefCounted
|
||||
|
||||
const DEFAULT_NAME: String = "NETfishing Dedicated Server"
|
||||
const DEFAULT_BIND_ADDRESS: String = "*"
|
||||
const DEFAULT_PORT: int = 7777
|
||||
const DEFAULT_MAX_PLAYERS: int = 8
|
||||
|
||||
var server_name: String = DEFAULT_NAME
|
||||
var bind_address: String = DEFAULT_BIND_ADDRESS
|
||||
var port: int = DEFAULT_PORT
|
||||
var max_players: int = DEFAULT_MAX_PLAYERS
|
||||
var public_listing: bool = false
|
||||
var discovery_url: String = ""
|
||||
var data_directory: String = ""
|
||||
var error_message: String = ""
|
||||
|
||||
|
||||
static func from_runtime() -> DedicatedServerConfig:
|
||||
var result := DedicatedServerConfig.new()
|
||||
result.data_directory = ProjectSettings.globalize_path(
|
||||
"user://dedicated-server-data"
|
||||
)
|
||||
result.discovery_url = str(ProjectSettings.get_setting(
|
||||
"network/discovery/base_url", ""
|
||||
)).strip_edges()
|
||||
var config_path: String = OS.get_environment(
|
||||
"NETFISHING_SERVER_CONFIG"
|
||||
).strip_edges()
|
||||
for argument: String in OS.get_cmdline_user_args():
|
||||
if argument.begins_with("--config="):
|
||||
config_path = argument.trim_prefix("--config=").strip_edges()
|
||||
if not config_path.is_empty() and not result._load_file(config_path):
|
||||
return result
|
||||
result._apply_environment()
|
||||
result._apply_arguments(OS.get_cmdline_user_args())
|
||||
result._validate()
|
||||
return result
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return error_message.is_empty()
|
||||
|
||||
|
||||
func _load_file(path: String) -> bool:
|
||||
var file := ConfigFile.new()
|
||||
var error: Error = file.load(path)
|
||||
if error != OK:
|
||||
error_message = "Could not load server configuration: %s" % path
|
||||
return false
|
||||
server_name = str(file.get_value("server", "name", server_name))
|
||||
bind_address = str(file.get_value(
|
||||
"server", "bind_address", bind_address
|
||||
))
|
||||
port = int(file.get_value("server", "port", port))
|
||||
max_players = int(file.get_value(
|
||||
"server", "max_players", max_players
|
||||
))
|
||||
public_listing = bool(file.get_value(
|
||||
"server", "public", public_listing
|
||||
))
|
||||
data_directory = str(file.get_value(
|
||||
"server", "data_directory", data_directory
|
||||
))
|
||||
discovery_url = str(file.get_value(
|
||||
"discovery", "url", discovery_url
|
||||
))
|
||||
return true
|
||||
|
||||
|
||||
func _apply_environment() -> void:
|
||||
server_name = _environment_string(
|
||||
"NETFISHING_SERVER_NAME", server_name
|
||||
)
|
||||
bind_address = _environment_string(
|
||||
"NETFISHING_SERVER_BIND", bind_address
|
||||
)
|
||||
port = _environment_int("NETFISHING_SERVER_PORT", port)
|
||||
max_players = _environment_int(
|
||||
"NETFISHING_SERVER_MAX_PLAYERS", max_players
|
||||
)
|
||||
public_listing = _environment_bool(
|
||||
"NETFISHING_SERVER_PUBLIC", public_listing
|
||||
)
|
||||
discovery_url = _environment_string(
|
||||
"NETFISHING_DISCOVERY_URL", discovery_url
|
||||
)
|
||||
data_directory = _environment_string(
|
||||
"NETFISHING_DATA_DIR", data_directory
|
||||
)
|
||||
|
||||
|
||||
func _apply_arguments(arguments: PackedStringArray) -> void:
|
||||
for argument: String in arguments:
|
||||
if argument.begins_with("--name="):
|
||||
server_name = argument.trim_prefix("--name=")
|
||||
elif argument.begins_with("--bind="):
|
||||
bind_address = argument.trim_prefix("--bind=")
|
||||
elif argument.begins_with("--port="):
|
||||
port = _parse_int(argument.trim_prefix("--port="), port)
|
||||
elif argument.begins_with("--max-players="):
|
||||
max_players = _parse_int(
|
||||
argument.trim_prefix("--max-players="), max_players
|
||||
)
|
||||
elif argument.begins_with("--data-dir="):
|
||||
data_directory = argument.trim_prefix("--data-dir=")
|
||||
elif argument.begins_with("--discovery-url="):
|
||||
discovery_url = argument.trim_prefix("--discovery-url=")
|
||||
elif argument == "--public":
|
||||
public_listing = true
|
||||
elif argument == "--private":
|
||||
public_listing = false
|
||||
|
||||
|
||||
func _validate() -> void:
|
||||
server_name = server_name.strip_edges().left(48)
|
||||
bind_address = bind_address.strip_edges()
|
||||
discovery_url = discovery_url.strip_edges().trim_suffix("/")
|
||||
data_directory = data_directory.strip_edges()
|
||||
if server_name.is_empty():
|
||||
error_message = "Server name cannot be empty."
|
||||
elif not _safe_text(server_name):
|
||||
error_message = "Server name contains unsupported characters."
|
||||
elif bind_address.is_empty():
|
||||
error_message = "Bind address cannot be empty."
|
||||
elif port < 1 or port > 65535:
|
||||
error_message = "Server port must be between 1 and 65535."
|
||||
elif max_players < 1 or max_players > 128:
|
||||
error_message = "Maximum players must be between 1 and 128."
|
||||
elif not data_directory.is_empty() and not data_directory.is_absolute_path():
|
||||
error_message = "Server data directory must be an absolute path."
|
||||
elif public_listing and not (
|
||||
discovery_url.begins_with("https://")
|
||||
or discovery_url.begins_with("http://")
|
||||
):
|
||||
error_message = "A public server requires a discovery URL."
|
||||
|
||||
|
||||
static func _safe_text(value: String) -> bool:
|
||||
for index: int in value.length():
|
||||
var codepoint: int = value.unicode_at(index)
|
||||
if codepoint < 32 or codepoint == 127:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _environment_string(name: String, fallback: String) -> String:
|
||||
return OS.get_environment(name) if OS.has_environment(name) else fallback
|
||||
|
||||
|
||||
static func _environment_int(name: String, fallback: int) -> int:
|
||||
return (
|
||||
_parse_int(OS.get_environment(name), fallback)
|
||||
if OS.has_environment(name) else fallback
|
||||
)
|
||||
|
||||
|
||||
static func _environment_bool(name: String, fallback: bool) -> bool:
|
||||
if not OS.has_environment(name):
|
||||
return fallback
|
||||
var value: String = OS.get_environment(name).strip_edges().to_lower()
|
||||
return value in ["1", "true", "yes", "on"]
|
||||
|
||||
|
||||
static func _parse_int(value: String, fallback: int) -> int:
|
||||
return int(value) if value.strip_edges().is_valid_int() else fallback
|
||||
1
server/dedicated_server_config.gd.uid
Normal file
1
server/dedicated_server_config.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://nmg477qkcbhe
|
||||
51
tests/dedicated_host_session_validation.gd
Normal file
51
tests/dedicated_host_session_validation.gd
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
extends SceneTree
|
||||
|
||||
const TEST_PORT: int = 35777
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var harness := Node.new()
|
||||
root.add_child(harness)
|
||||
var players := Node3D.new()
|
||||
harness.add_child(players)
|
||||
var spawn_service := PlayerSpawnService.new()
|
||||
harness.add_child(spawn_service)
|
||||
spawn_service.setup(players, null, Transform3D.IDENTITY)
|
||||
var host_identity := HostIdentityStore.new()
|
||||
harness.add_child(host_identity)
|
||||
var known_players := KnownPlayerStore.new()
|
||||
harness.add_child(known_players)
|
||||
var host_bans := HostBanStore.new()
|
||||
harness.add_child(host_bans)
|
||||
var session := NetworkSession.new()
|
||||
harness.add_child(session)
|
||||
await process_frame
|
||||
session.setup(
|
||||
null,
|
||||
null,
|
||||
spawn_service,
|
||||
null,
|
||||
host_identity,
|
||||
known_players,
|
||||
null,
|
||||
host_bans,
|
||||
true,
|
||||
)
|
||||
assert(session.start_dedicated_host(TEST_PORT, 5, "127.0.0.1"))
|
||||
assert(session.is_dedicated_host())
|
||||
assert(session.get_local_peer_id() == 0)
|
||||
assert(session.get_player_count() == 0)
|
||||
assert(session.get_session_max_players() == 5)
|
||||
assert(session.get_host_port() == TEST_PORT)
|
||||
assert(session.set_host_open(true))
|
||||
assert(session.is_open_host())
|
||||
session.disconnect_session("Dedicated host validation complete.")
|
||||
assert(not session.is_session_active())
|
||||
assert(not session.is_dedicated_host())
|
||||
harness.queue_free()
|
||||
print("DEDICATED_HOST_SESSION_VALIDATION_OK")
|
||||
quit(0)
|
||||
1
tests/dedicated_host_session_validation.gd.uid
Normal file
1
tests/dedicated_host_session_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ctky0gkb63nqa
|
||||
49
tests/dedicated_server_client_validation.gd
Normal file
49
tests/dedicated_server_client_validation.gd
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main/main.tscn")
|
||||
const TEST_ENDPOINT: String = "127.0.0.1:7777"
|
||||
const EXPECTED_SERVER_NAME: String = "Dedicated-Client-Test"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
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")))
|
||||
main.call("_on_title_join_game_requested", TEST_ENDPOINT)
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var joined: bool = false
|
||||
var deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < 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")):
|
||||
joined = true
|
||||
break
|
||||
assert(joined)
|
||||
assert(session.get_local_peer_id() > 1)
|
||||
assert(session.get_player_count() == 1)
|
||||
var metadata: Dictionary = session.get_last_server_metadata()
|
||||
assert(str(metadata.get("server_display_name", "")) == EXPECTED_SERVER_NAME)
|
||||
var spawn_service := main.get_node(
|
||||
"%PlayerSpawnService"
|
||||
) as PlayerSpawnService
|
||||
assert(spawn_service.get_avatar(1) == null)
|
||||
assert(spawn_service.get_avatar(session.get_local_peer_id()) != null)
|
||||
print("DEDICATED_SERVER_CLIENT_VALIDATION_OK")
|
||||
session.disconnect_session("Dedicated client validation complete.")
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
quit()
|
||||
1
tests/dedicated_server_client_validation.gd.uid
Normal file
1
tests/dedicated_server_client_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwum3r4puerll
|
||||
63
tests/dedicated_server_config_validation.gd
Normal file
63
tests/dedicated_server_config_validation.gd
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
extends SceneTree
|
||||
|
||||
const ConfigType = preload("res://server/dedicated_server_config.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var defaults := ConfigType.new()
|
||||
defaults.set("data_directory", "/tmp/netfishing-server")
|
||||
defaults.call("_validate")
|
||||
assert(defaults.is_valid())
|
||||
assert(not bool(defaults.get("public_listing")))
|
||||
|
||||
var path: String = ProjectSettings.globalize_path(
|
||||
"user://dedicated-server-validation.cfg"
|
||||
)
|
||||
var file := ConfigFile.new()
|
||||
file.set_value("server", "name", "Configured Room")
|
||||
file.set_value("server", "bind_address", "127.0.0.1")
|
||||
file.set_value("server", "port", 17777)
|
||||
file.set_value("server", "max_players", 12)
|
||||
file.set_value("server", "public", true)
|
||||
file.set_value("server", "data_directory", "/tmp/configured-server")
|
||||
file.set_value("discovery", "url", "https://discovery.netfishing.org/")
|
||||
assert(file.save(path) == OK)
|
||||
|
||||
var configured := ConfigType.new()
|
||||
assert(bool(configured.call("_load_file", path)))
|
||||
configured.call("_validate")
|
||||
assert(configured.is_valid())
|
||||
assert(str(configured.get("server_name")) == "Configured Room")
|
||||
assert(str(configured.get("bind_address")) == "127.0.0.1")
|
||||
assert(int(configured.get("port")) == 17777)
|
||||
assert(int(configured.get("max_players")) == 12)
|
||||
assert(bool(configured.get("public_listing")))
|
||||
assert(str(configured.get("discovery_url")) == "https://discovery.netfishing.org")
|
||||
assert(DirAccess.remove_absolute(path) == OK)
|
||||
|
||||
var invalid := ConfigType.new()
|
||||
invalid.set("public_listing", true)
|
||||
invalid.set("data_directory", "/tmp/invalid-server")
|
||||
invalid.call("_validate")
|
||||
assert(not invalid.is_valid())
|
||||
|
||||
var discovery := DiscoveryClient.new()
|
||||
assert(bool(discovery.call("_valid_public_room", {
|
||||
"room_id": "empty-dedicated-room",
|
||||
"room_name": "Empty Dedicated Room",
|
||||
"address": "203.0.113.10",
|
||||
"port": 7777,
|
||||
"current_players": 0,
|
||||
"max_players": 8,
|
||||
"game_version": str(ProjectSettings.get_setting(
|
||||
"application/config/version", "unknown"
|
||||
)),
|
||||
"protocol_version": NetworkProtocol.PROTOCOL_VERSION,
|
||||
})))
|
||||
discovery.free()
|
||||
print("DEDICATED_SERVER_CONFIG_VALIDATION_OK")
|
||||
quit()
|
||||
1
tests/dedicated_server_config_validation.gd.uid
Normal file
1
tests/dedicated_server_config_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://vm5lql2w6h3e
|
||||
Loading…
Add table
Add a link
Reference in a new issue