M2.7: Host Game, backups, launchd/systemd samples, hosting and Tailscale guide
This commit is contained in:
@@ -240,6 +240,8 @@ func _on_peer_disconnected(id: int) -> void:
|
||||
_s2c_player_left.rpc(id)
|
||||
PlayerNet.server_leave(id) # saves their character
|
||||
WorldNet.peer_left(id)
|
||||
if _hosted_by(String(p.id)):
|
||||
_host_left.call_deferred()
|
||||
players_changed.emit()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -255,6 +257,7 @@ func _process(delta: float) -> void:
|
||||
_send_left = SEND_INTERVAL
|
||||
_broadcast_snapshot()
|
||||
return
|
||||
_host_tick()
|
||||
if state == "connecting":
|
||||
_join_left -= delta
|
||||
if _join_left <= 0.0:
|
||||
@@ -272,6 +275,135 @@ func _tick_clock(delta: float) -> void:
|
||||
for peer in players:
|
||||
_s2c_clock.rpc_id(peer, _world.day_night.day, _world.day_night.time)
|
||||
|
||||
# --- hosting from the title screen (M2.7) ------------------------------------------
|
||||
# "Host Game" runs a dedicated server as a separate, windowless copy of the
|
||||
# game on this machine, then joins it like anyone else (so you're its first
|
||||
# player, and its admin). Its world and settings live in user://host/. Its
|
||||
# log goes to user://host/server.log. Leaving asks it to save and stop.
|
||||
|
||||
signal host_status(text: String, failed: bool)
|
||||
|
||||
var host_config := "user://host/server.json" ## (tests use their own)
|
||||
var hosting := false
|
||||
var _host_pid := 0
|
||||
var _host_io: FileAccess
|
||||
var _host_log: FileAccess
|
||||
var _host_out := ""
|
||||
var _host_port := DEFAULT_PORT
|
||||
|
||||
## Settings for a hosted game: name, password, port, a rules preset
|
||||
## (co-op / rivals / adversarial) and the seed for a new island.
|
||||
const PRESETS := {
|
||||
"co-op": {"pvp": "false", "raiding": "false", "locks": "owner_and_friends", "death_penalty": "grave"},
|
||||
"rivals": {"pvp": "true", "raiding": "false", "locks": "owner_and_friends", "death_penalty": "grave"},
|
||||
"adversarial": {"pvp": "true", "raiding": "true", "locks": "owner", "death_penalty": "drop_all"},
|
||||
}
|
||||
|
||||
func host(server_name: String, password: String, port: int, preset: String, seed: int) -> void:
|
||||
if hosting or state != "offline":
|
||||
return
|
||||
if _host_pid != 0 and OS.is_process_running(_host_pid):
|
||||
host_status.emit("The last server is still saving: try again in a moment", true)
|
||||
return
|
||||
var c: RefCounted = ServerConfig.open(host_config)
|
||||
c.set_rule("server_name", server_name)
|
||||
for k in PRESETS.get(preset, PRESETS["co-op"]):
|
||||
c.set_rule(k, PRESETS[preset][k])
|
||||
c.data.password = password
|
||||
c.data.port = port
|
||||
var fresh := not FileAccess.file_exists(host_config.get_base_dir().path_join("worlds/server-world/meta.json"))
|
||||
if fresh:
|
||||
c.data.seed = seed
|
||||
c.save()
|
||||
_host_port = port
|
||||
var args := PackedStringArray(["--headless"])
|
||||
if not OS.has_feature("template"):
|
||||
args.append_array(["--path", ProjectSettings.globalize_path("res://")]) # (running from the editor build)
|
||||
args.append_array(["--", "--server", "--host-id", String(get_identity().player_id),
|
||||
"--config", ProjectSettings.globalize_path(host_config), "--exit-with", str(OS.get_process_id())])
|
||||
var p: Dictionary = OS.execute_with_pipe(OS.get_executable_path(), args, false)
|
||||
if p.is_empty():
|
||||
host_status.emit("Couldn't start the server", true)
|
||||
return
|
||||
_host_pid = int(p.pid)
|
||||
_host_io = p.stdio
|
||||
_host_out = ""
|
||||
_host_log = FileAccess.open(host_config.get_base_dir().path_join("server.log"), FileAccess.WRITE)
|
||||
hosting = true
|
||||
host_status.emit("Starting the server%s..." % (" (building a new island)" if fresh else ""), false)
|
||||
|
||||
## Reads what the hosted server prints (so its output never fills the pipe),
|
||||
## and joins it once it's listening.
|
||||
func _host_tick() -> void:
|
||||
if _host_io == null:
|
||||
return
|
||||
var chunk := _host_io.get_buffer(8192)
|
||||
if not chunk.is_empty():
|
||||
var s := chunk.get_string_from_utf8()
|
||||
if _host_log != null:
|
||||
_host_log.store_string(s)
|
||||
_host_log.flush()
|
||||
if hosting and state == "offline":
|
||||
_host_out += s
|
||||
if _host_out.contains("listening on UDP"):
|
||||
_host_out = ""
|
||||
host_status.emit("Joining your server...", false)
|
||||
join("127.0.0.1:%d" % _host_port, String(ServerConfig.open(host_config).data.password))
|
||||
elif _host_out.contains("[server] Couldn't open UDP port") or _host_out.contains("Not starting"):
|
||||
var why := "Port %d is already in use (another server running?)" % _host_port \
|
||||
if _host_out.contains("Couldn't open") else "The server couldn't start (see user://host/server.log)"
|
||||
stop_hosting()
|
||||
host_status.emit(why, true)
|
||||
if _host_pid != 0 and not OS.is_process_running(_host_pid):
|
||||
var was_hosting := hosting
|
||||
_host_pid = 0
|
||||
# It's gone: whatever it printed last is still in the pipe.
|
||||
for _i in range(64):
|
||||
var rest := _host_io.get_buffer(8192)
|
||||
if rest.is_empty():
|
||||
break
|
||||
if _host_log != null:
|
||||
_host_log.store_string(rest.get_string_from_utf8())
|
||||
if _host_log != null:
|
||||
_host_log.flush()
|
||||
_host_io = null
|
||||
hosting = false
|
||||
if was_hosting and state == "offline":
|
||||
host_status.emit("The server stopped (see user://host/server.log)", true)
|
||||
|
||||
## Stops the hosted server: asks it to save and quit, then makes sure. Its
|
||||
## output is still read (into the log) until it's gone.
|
||||
func stop_hosting() -> void:
|
||||
if not hosting:
|
||||
return
|
||||
hosting = false
|
||||
var pid := _host_pid
|
||||
if pid != 0:
|
||||
get_tree().create_timer(6.0).timeout.connect(func():
|
||||
if OS.is_process_running(pid):
|
||||
OS.kill(pid))
|
||||
|
||||
## The addresses friends can use to reach this machine: the local network,
|
||||
## and Tailscale (100.x) if it's running.
|
||||
static func my_addresses() -> Array[String]:
|
||||
var out: Array[String] = []
|
||||
for a in IP.get_local_addresses():
|
||||
if a.contains(":") or a.begins_with("127.") or a.begins_with("169.254."):
|
||||
continue
|
||||
out.append(a)
|
||||
out.sort_custom(func(x, y): return (0 if x.begins_with("100.") else 1) < (0 if y.begins_with("100.") else 1))
|
||||
return out
|
||||
|
||||
## A hosted server (started with --host-id): when the player who hosts it
|
||||
## leaves, it saves and stops (it lives on their computer).
|
||||
func _hosted_by(id: String) -> bool:
|
||||
return is_server() and _args.has("host-id") and String(_args["host-id"]) == id
|
||||
|
||||
func _host_left() -> void:
|
||||
print("[server] the host left: saving and stopping")
|
||||
stop() # (saves the world and everyone's characters)
|
||||
get_tree().quit()
|
||||
|
||||
# --- admin (chat.gd) ---------------------------------------------------------------
|
||||
|
||||
signal rules_changed ## clients: the server's rules changed (/set)
|
||||
@@ -566,6 +698,7 @@ var _pending_password := ""
|
||||
|
||||
func leave() -> void:
|
||||
var was := state
|
||||
stop_hosting() # (our own server sees us go, and saves and stops)
|
||||
stop()
|
||||
if was == "connected":
|
||||
last_reason = "You left the server"
|
||||
@@ -659,6 +792,7 @@ func _run_queue(q: Array) -> void:
|
||||
f.call()
|
||||
|
||||
func _fail(reason: String) -> void:
|
||||
stop_hosting() # (hosting and couldn't get in: don't leave the server running)
|
||||
stop()
|
||||
last_reason = reason
|
||||
join_failed.emit(reason)
|
||||
@@ -669,6 +803,7 @@ func _on_connection_failed() -> void:
|
||||
func _on_server_disconnected() -> void:
|
||||
var was := state
|
||||
var reason := _reject_reason
|
||||
stop_hosting()
|
||||
stop()
|
||||
if was == "connecting":
|
||||
last_reason = reason if not reason.is_empty() else "The server hung up"
|
||||
|
||||
Reference in New Issue
Block a user