M2.7: Host Game, backups, launchd/systemd samples, hosting and Tailscale guide

This commit is contained in:
Paul Bailey
2026-11-04 23:19:00 +00:00
parent 2ab79b68c6
commit ab1f417c32
14 changed files with 723 additions and 11 deletions

View File

@@ -50,6 +50,8 @@ func open_server_world(config: RefCounted) -> Node3D:
if id.is_empty():
id = "server-world"
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")))
backup("start-up") # (the state it's about to load, before anyone changes it)
_backup_left = maxf(float(config.v("backup_hours")), 0.25) * 3600.0
var w: Node3D = load("res://scenes/world.tscn").instantiate()
w.server_mode = true
w.day_length = float(config.v("day_length_minutes")) * 60.0
@@ -97,6 +99,66 @@ func save() -> void:
if world != null and is_instance_valid(world) and world.server_mode:
world.save_now() # (players too: world.saved)
# --- backups (M2.7) ----------------------------------------------------------------
# A copy of the world (meta, world and every player's file) in
# worlds/<world>/backups/<yyyy-mm-dd_hh-mm-ss>/: when the server starts (the
# state it's about to load), every backup_hours while it runs, and on
# /backup. The newest backups_kept are kept. To restore one, stop the server
# and copy its files back up two folders (docs/HOSTING.md).
var _backup_left := INF
func world_dir() -> String:
return Saves.root_dir.path_join(Saves.current_id)
## Makes a backup now. Returns its folder name ("" if there was nothing to back up).
func backup(reason: String) -> String:
var src := world_dir()
if not FileAccess.file_exists(src.path_join("world.json")):
return ""
var stamp := Time.get_datetime_string_from_system(false, true).replace(":", "-").replace(" ", "_")
var dst := src.path_join("backups").path_join(stamp)
var n := 2
while DirAccess.dir_exists_absolute(dst):
dst = src.path_join("backups").path_join("%s_%d" % [stamp, n])
n += 1
DirAccess.make_dir_recursive_absolute(dst.path_join("players"))
for f in ["meta.json", "world.json"]:
if FileAccess.file_exists(src.path_join(f)):
DirAccess.copy_absolute(src.path_join(f), dst.path_join(f))
var pd := DirAccess.open(src.path_join("players"))
if pd != null:
for f in pd.get_files():
if f.ends_with(".json"):
DirAccess.copy_absolute(src.path_join("players").path_join(f), dst.path_join("players").path_join(f))
_prune_backups()
print("[server] backup (%s): %s" % [reason, ProjectSettings.globalize_path(dst)])
return dst.get_file()
func list_backups() -> Array:
var d := DirAccess.open(world_dir().path_join("backups"))
if d == null:
return []
var out: Array = Array(d.get_directories())
out.sort()
return out
func _prune_backups() -> void:
var keep := maxi(int(Net.config.v("backups_kept")), 1)
var all := list_backups()
while all.size() > keep:
_rm_tree(world_dir().path_join("backups").path_join(String(all.pop_front())))
static func _rm_tree(d: String) -> void:
var da := DirAccess.open(d)
if da == null:
return
for sub in da.get_directories():
_rm_tree(d.path_join(sub))
for f in da.get_files():
da.remove(f)
DirAccess.remove_absolute(d)
## Something changed: save within a few seconds. Ctrl+C kills Godot without
## a chance to save, so this keeps the loss small.
func mark_changed() -> void:
@@ -139,6 +201,11 @@ func _process(delta: float) -> void:
_save_left -= delta
if _save_left <= 0.0:
save()
_backup_left -= delta
if _backup_left <= 0.0:
_backup_left = maxf(float(Net.config.v("backup_hours")), 0.25) * 3600.0
save()
backup("every %s h" % Net.config.v("backup_hours"))
_blocker_left -= delta
if _blocker_left <= 0.0:
_blocker_left = 1.0