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", "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, [])