- Net autoload: headless dedicated server (--server) on UDP 24242 via ENet, server.json preferences (flags override per run, never saved), reserved names in names.json - Identity: RSA-2048 key per install (Godot has no Ed25519), player id = key hash, challenge/response so ids can't be faked; profile with name and limited appearance (tunic, hair, beard) - Server refuses bans, allowlist misses, duplicate logins, bad or taken names, wrong passwords, old protocol versions and a full server, each with a clear reason; first player on a new server becomes admin - Title screen: Profile, Join Game, and a lobby with server info, rules and who's online - test_net_lobby: real headless server + bot processes over UDP (38 checks); --exit-with so test servers never outlive the test
107 lines
3.2 KiB
GDScript
107 lines
3.2 KiB
GDScript
extends RefCounted
|
|
## A dedicated server's preferences (`server.json`) and its reserved names
|
|
## (`names.json` alongside). Created with defaults on first run and meant to
|
|
## be edited by hand; command-line flags override single values for one run.
|
|
## See docs/MULTIPLAYER_PLAN.md §4.
|
|
|
|
const DEFAULT_PATH := "user://server/server.json"
|
|
const DEFAULTS := {
|
|
"server_name": "Valheimlike server",
|
|
"motd": "Welcome!",
|
|
"port": 24242,
|
|
"max_players": 8,
|
|
"password": "",
|
|
"world": "server-world",
|
|
"worldlet": "valley",
|
|
"seed": 1337,
|
|
"pvp": false,
|
|
"raiding": false,
|
|
"locks": "owner_and_friends",
|
|
"death_penalty": "grave",
|
|
"sleep": "all_online",
|
|
"pause_when_empty": true,
|
|
"day_length_minutes": 20,
|
|
"wolf_grace_days": 3,
|
|
"difficulty": "normal",
|
|
"autosave_minutes": 2,
|
|
"backups_kept": 10,
|
|
"admins": [],
|
|
"allowlist": [],
|
|
"banned": [],
|
|
}
|
|
## Settings sent to players when they join (never the password or the lists).
|
|
const PUBLIC := ["server_name", "motd", "max_players", "worldlet", "pvp", "raiding", "locks",
|
|
"death_penalty", "sleep", "pause_when_empty", "day_length_minutes", "wolf_grace_days", "difficulty"]
|
|
|
|
var path := DEFAULT_PATH
|
|
var data: Dictionary = {} ## what's in server.json (saved)
|
|
var overrides: Dictionary = {} ## command-line flags for this run (never saved)
|
|
## Reserved names: lower-case name -> {"name": as typed, "id": player id}.
|
|
var names: Dictionary = {}
|
|
|
|
static func open(p: String = DEFAULT_PATH) -> RefCounted:
|
|
var c = load("res://scripts/net/server_config.gd").new()
|
|
c.path = p
|
|
DirAccess.make_dir_recursive_absolute(p.get_base_dir())
|
|
var loaded: Variant = JSON.parse_string(FileAccess.get_file_as_string(p)) if FileAccess.file_exists(p) else null
|
|
c.data = DEFAULTS.duplicate(true)
|
|
if loaded is Dictionary:
|
|
for k in loaded:
|
|
c.data[k] = loaded[k]
|
|
else:
|
|
c.save() # first run: write the defaults so they can be edited
|
|
var n: Variant = JSON.parse_string(FileAccess.get_file_as_string(c.names_path())) \
|
|
if FileAccess.file_exists(c.names_path()) else null
|
|
if n is Dictionary:
|
|
c.names = n
|
|
return c
|
|
|
|
func names_path() -> String:
|
|
return path.get_base_dir().path_join("names.json")
|
|
|
|
func save() -> void:
|
|
_write(path, data)
|
|
|
|
func save_names() -> void:
|
|
_write(names_path(), names)
|
|
|
|
static func _write(p: String, d: Dictionary) -> void:
|
|
var tmp := p + ".tmp"
|
|
var f := FileAccess.open(tmp, FileAccess.WRITE)
|
|
if f == null:
|
|
push_error("[server] couldn't write %s" % p)
|
|
return
|
|
f.store_string(JSON.stringify(d, "\t"))
|
|
f.close()
|
|
DirAccess.rename_absolute(tmp, p)
|
|
|
|
## A setting's value for this run (a flag beats the file).
|
|
func v(k: String) -> Variant:
|
|
return overrides.get(k, data.get(k))
|
|
|
|
func public_info() -> Dictionary:
|
|
var out := {}
|
|
for k in PUBLIC:
|
|
out[k] = v(k)
|
|
return out
|
|
|
|
## The player id holding `n`, or "" if it's free.
|
|
func name_owner(n: String) -> String:
|
|
return String(names.get(n.to_lower(), {}).get("id", ""))
|
|
|
|
## Gives `n` to `id`, releasing any other name that id held.
|
|
func reserve_name(n: String, id: String) -> void:
|
|
for k in names.keys():
|
|
if names[k].id == id and k != n.to_lower():
|
|
names.erase(k)
|
|
names[n.to_lower()] = {"name": n, "id": id}
|
|
save_names()
|
|
|
|
func is_admin(id: String) -> bool:
|
|
return id in data.admins
|
|
|
|
func add_admin(id: String) -> void:
|
|
if not id in data.admins:
|
|
data.admins.append(id)
|
|
save()
|