Files
Cinderwild/scripts/net/server_config.gd

213 lines
6.5 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": "Cinderwild 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",
"rot": "normal",
"autosave_minutes": 2,
"backups_kept": 10,
"backup_hours": 6,
"check_updates": true,
"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", "rot"]
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"],
"rot": ["normal", "slow", "off"],
"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, [])