152 lines
6.2 KiB
GDScript
152 lines
6.2 KiB
GDScript
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, "valley")
|
|
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)))
|
|
check(String(Net.world_info.get("worldlet", "")) == "valley", "in the worldlet picked (%s)" % Net.world_info.get("worldlet", "-"))
|
|
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, "cove")
|
|
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(String(load("res://scripts/net/server_config.gd").open(DIR + "/server.json").data.worldlet) == "valley",
|
|
"and the same worldlet (the Valley, not the new one picked)")
|
|
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)
|