M2.7: Host Game, backups, launchd/systemd samples, hosting and Tailscale guide
This commit is contained in:
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
||||
echo "Importing the project first (fresh copy)..."
|
||||
godot --headless --path . --import >/dev/null 2>&1
|
||||
fi
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_den_sites test_fishing_diet test_net_lobby test_net_world test_net_shared test_net_life test_net_rules test_terrain_edges test_boundary; do
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_den_sites test_fishing_diet test_net_lobby test_net_world test_net_shared test_net_life test_net_rules test_host test_terrain_edges test_boundary; do
|
||||
extra=""
|
||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||
# A suite that waits on a world that failed to load would hang forever,
|
||||
|
||||
148
tests/test_host.gd
Normal file
148
tests/test_host.gd
Normal file
@@ -0,0 +1,148 @@
|
||||
extends SceneTree
|
||||
## M2.7: hosting from the title screen, and backups. "Host Game" starts a
|
||||
## server as a windowless copy of the game, joins it (you're its admin),
|
||||
## and leaving makes it save and stop. Hosting again carries on the same
|
||||
## island. The server backs the world up at start-up and on /backup, and
|
||||
## keeps only backups_kept of them. Also: Tailscale/LAN addresses are found.
|
||||
## Run: godot --headless --path . --script res://tests/test_host.gd
|
||||
|
||||
const DIR := "user://test_net/host"
|
||||
const PORT := 24351
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var Net: Node
|
||||
var WN: Node
|
||||
var C: Node
|
||||
var PN: Node
|
||||
|
||||
func check(cond: bool, what: String) -> void:
|
||||
print((" ok " if cond else " FAIL ") + what)
|
||||
if not cond:
|
||||
_fails += 1
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
func _run() -> void:
|
||||
Net = root.get_node("Net")
|
||||
WN = root.get_node("WorldNet")
|
||||
C = root.get_node("Chat")
|
||||
PN = root.get_node("PlayerNet")
|
||||
_rm_tree(DIR)
|
||||
Net.host_config = DIR + "/server.json"
|
||||
var me = load("res://scripts/net/identity.gd").open("user://test_net/a")
|
||||
me.name = "Alice"
|
||||
Net.identity = me
|
||||
await _body()
|
||||
Net.leave()
|
||||
await _sleep(1.0)
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
_rm_tree(DIR)
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
func _body() -> void:
|
||||
var statuses: Array[String] = []
|
||||
Net.host_status.connect(func(t, _f): statuses.append(t))
|
||||
print("hosting")
|
||||
Net.host("Alice's island", "", PORT, "rivals", 4242)
|
||||
check(Net.hosting, "Host Game starts a server (%s)" % ", ".join(statuses))
|
||||
await _until(func(): return Net.state == "connected", 40.0)
|
||||
check(Net.state == "connected", "and joins it (%s)" % " -> ".join(statuses))
|
||||
if Net.state != "connected":
|
||||
return
|
||||
check(String(Net.server_info.get("server_name", "")) == "Alice's island" and bool(Net.server_info.get("pvp", false)),
|
||||
"with the name and the rules picked (friendly rivals: PvP on)")
|
||||
check(bool(Net.players.get(Net.local_peer, {}).get("admin", false)), "and Alice is its admin")
|
||||
check(int(Net.world_info.get("seed", 0)) == 4242, "a new island from the seed given (%d)" % int(Net.world_info.get("seed", 0)))
|
||||
var addrs: Array[String] = Net.my_addresses()
|
||||
check(not addrs.is_empty(), "the addresses friends can use: %s" % ", ".join(addrs))
|
||||
var log_path := ProjectSettings.globalize_path(DIR + "/server.log")
|
||||
check(FileAccess.get_file_as_string(DIR + "/server.log").contains("listening on UDP %d" % PORT), "the server's log goes to %s" % log_path)
|
||||
|
||||
print("backups")
|
||||
check(_backups().is_empty(), "nothing to back up yet on a brand-new island")
|
||||
# Load a world so there's something to save, change it, then /backup.
|
||||
var world: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
for _i in range(8):
|
||||
await physics_frame
|
||||
world.player.set_physics_process(false)
|
||||
PN.ask("debug_give", ["stone", 3]) # (not allowed: no --debug-cheats on a hosted server)
|
||||
await _sleep(0.5)
|
||||
check(world.player.inventory.count("stone") == 0, "a hosted server has no test cheats")
|
||||
C.say("/backup")
|
||||
await _until(func(): return _backups().size() == 1, 5.0)
|
||||
check(_backups().size() == 1, "/backup makes one (%s)" % [_backups()])
|
||||
var b0: String = _backups()[0] if not _backups().is_empty() else ""
|
||||
var bdir := DIR + "/worlds/server-world/backups/" + b0
|
||||
check(FileAccess.file_exists(bdir + "/world.json") and FileAccess.file_exists(bdir + "/meta.json"),
|
||||
"with the world in it")
|
||||
check(DirAccess.get_files_at(bdir + "/players").size() >= 1, "and the players' characters")
|
||||
C.say("/set backups_kept 2")
|
||||
await _sleep(0.4)
|
||||
check(C.heard.any(func(l): return String(l).begins_with("Can't set 'backups_kept'")), "(backups_kept is set in server.json, not in game)")
|
||||
for _i in range(3):
|
||||
await _sleep(1.1) # (one-second timestamps)
|
||||
C.say("/backup")
|
||||
await _sleep(1.0)
|
||||
check(_backups().size() == 4, "four backups now (%d)" % _backups().size())
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
|
||||
print("leaving")
|
||||
Net.leave()
|
||||
await _until(func(): return not _server_running(), 8.0)
|
||||
check(not _server_running(), "leaving stops the server")
|
||||
# (Its last words are read from the pipe once the process has gone.)
|
||||
await _until(func(): return FileAccess.get_file_as_string(DIR + "/server.log").contains("the host left"), 3.0)
|
||||
check(FileAccess.get_file_as_string(DIR + "/server.log").contains("the host left: saving and stopping"),
|
||||
"after saving (it was asked to, not killed)")
|
||||
|
||||
print("hosting again")
|
||||
var c = load("res://scripts/net/server_config.gd").open(DIR + "/server.json")
|
||||
c.data.backups_kept = 3
|
||||
c.save()
|
||||
statuses.clear()
|
||||
Net.host("Alice's island", "", PORT, "co-op", 999)
|
||||
await _until(func(): return Net.state == "connected", 40.0)
|
||||
check(Net.state == "connected", "Host Game again: joined (%s)" % " -> ".join(statuses))
|
||||
check(int(Net.world_info.get("seed", 0)) == 4242, "the same island (seed %d, not the new one typed)" % int(Net.world_info.get("seed", 0)))
|
||||
check(not bool(Net.server_info.get("pvp", true)), "with the rules picked this time (co-op: PvP off)")
|
||||
check(_backups().size() == 3, "start-up made a backup, and only backups_kept (3) are kept (%d)" % _backups().size())
|
||||
_finished = true
|
||||
|
||||
## Whether anything is still listening on the hosted server's port.
|
||||
func _server_running() -> bool:
|
||||
var out: Array = []
|
||||
OS.execute("lsof", ["-nP", "-iUDP:%d" % PORT], out)
|
||||
return String(out[0] if not out.is_empty() else "").contains("UDP")
|
||||
|
||||
func _backups() -> Array:
|
||||
var d := DirAccess.open(DIR + "/worlds/server-world/backups")
|
||||
if d == null:
|
||||
return []
|
||||
var a: Array = Array(d.get_directories())
|
||||
a.sort()
|
||||
return a
|
||||
|
||||
func _until(cond: Callable, limit: float) -> bool:
|
||||
var t := 0.0
|
||||
while not cond.call() and t < limit:
|
||||
await _sleep(0.1)
|
||||
t += 0.1
|
||||
return cond.call()
|
||||
|
||||
func _sleep(s: float) -> void:
|
||||
await create_timer(s).timeout
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user