Add dedicated server runtime

This commit is contained in:
Alexander Sellite 2026-08-10 13:56:03 -04:00
parent e204279b09
commit 74c82696fe
14 changed files with 595 additions and 47 deletions

View file

@ -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"

View file

@ -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

View file

@ -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(

View file

@ -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:

View file

@ -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

View file

@ -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",

View 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

View file

@ -0,0 +1 @@
uid://nmg477qkcbhe

View 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)

View file

@ -0,0 +1 @@
uid://ctky0gkb63nqa

View 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()

View file

@ -0,0 +1 @@
uid://cwum3r4puerll

View 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()

View file

@ -0,0 +1 @@
uid://vm5lql2w6h3e