210 lines
6.4 KiB
GDScript
210 lines
6.4 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": "Emberwild 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,
|
|
"backup_hours": 6,
|
|
"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
|
|
c.load_friends()
|
|
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()
|
|
|
|
func ban(id: String) -> void:
|
|
if not id in data.banned:
|
|
data.banned.append(id)
|
|
save()
|
|
|
|
func unban(id: String) -> bool:
|
|
if not id in data.banned:
|
|
return false
|
|
data.banned.erase(id)
|
|
save()
|
|
return true
|
|
|
|
## A reserved name back up for grabs (the player who had it keeps their
|
|
## character, and picks another name next time they join).
|
|
func release_name(n: String) -> bool:
|
|
if not names.has(n.to_lower()):
|
|
return false
|
|
names.erase(n.to_lower())
|
|
save_names()
|
|
return true
|
|
|
|
## The name a player id goes by here ("" if none).
|
|
func name_of(id: String) -> String:
|
|
for k in names:
|
|
if names[k].id == id:
|
|
return String(names[k].name)
|
|
return ""
|
|
|
|
# --- rules that can change while the server runs (/set) --------------------------
|
|
|
|
## What each can be set to. The others in DEFAULTS are fixed at start-up.
|
|
const SETTABLE := {
|
|
"pvp": ["false", "true"],
|
|
"raiding": ["false", "true"],
|
|
"locks": ["off", "owner", "owner_and_friends"],
|
|
"death_penalty": ["grave", "keep", "drop_all"],
|
|
"sleep": ["all_online", "half", "anyone", "off"],
|
|
"pause_when_empty": ["false", "true"],
|
|
"difficulty": ["easy", "normal", "hard"],
|
|
"wolf_grace_days": "0-10",
|
|
"motd": "text",
|
|
"server_name": "text",
|
|
}
|
|
|
|
## Changes a rule and saves server.json. Returns "" or what's wrong.
|
|
func set_rule(k: String, value: String) -> String:
|
|
if not SETTABLE.has(k):
|
|
return "Can't set '%s'. These can be set: %s" % [k, ", ".join(PackedStringArray(SETTABLE.keys()))]
|
|
var allowed: Variant = SETTABLE[k]
|
|
var val: Variant
|
|
if allowed is Array:
|
|
if not value in allowed:
|
|
return "%s can be: %s" % [k, " / ".join(PackedStringArray(allowed))]
|
|
val = (value == "true") if value in ["true", "false"] else value
|
|
elif allowed == "0-10":
|
|
if not value.is_valid_int() or int(value) < 0 or int(value) > 10:
|
|
return "%s is a number from 0 to 10" % k
|
|
val = int(value)
|
|
else:
|
|
if value.strip_edges().is_empty() or value.length() > 80:
|
|
return "%s needs some text (up to 80 characters)" % k
|
|
val = value.strip_edges()
|
|
data[k] = val
|
|
overrides.erase(k) # (a flag no longer wins over what the admin just set)
|
|
save()
|
|
return ""
|
|
|
|
# --- friends (docs/MULTIPLAYER_PLAN.md §4) ----------------------------------------
|
|
# Per player: whose doors and chests they share. Stored as owner id -> [ids],
|
|
# in friends.json next to server.json.
|
|
|
|
var friends: Dictionary = {}
|
|
|
|
func friends_path() -> String:
|
|
return path.get_base_dir().path_join("friends.json")
|
|
|
|
func load_friends() -> void:
|
|
var f: Variant = JSON.parse_string(FileAccess.get_file_as_string(friends_path())) \
|
|
if FileAccess.file_exists(friends_path()) else null
|
|
friends = f if f is Dictionary else {}
|
|
|
|
## `owner` lets `friend` use their things.
|
|
func add_friend(owner: String, friend: String) -> void:
|
|
var l: Array = friends.get(owner, [])
|
|
if not friend in l:
|
|
l.append(friend)
|
|
friends[owner] = l
|
|
_write(friends_path(), friends)
|
|
|
|
func remove_friend(owner: String, friend: String) -> bool:
|
|
var l: Array = friends.get(owner, [])
|
|
if not friend in l:
|
|
return false
|
|
l.erase(friend)
|
|
friends[owner] = l
|
|
_write(friends_path(), friends)
|
|
return true
|
|
|
|
func is_friend(owner: String, friend: String) -> bool:
|
|
return friend in friends.get(owner, [])
|