M2.7: Host Game, backups, launchd/systemd samples, hosting and Tailscale guide
This commit is contained in:
@@ -8,14 +8,14 @@ extends Node
|
||||
##
|
||||
## Anyone: /help /players /rules /friend <name> /unfriend <name> /friends
|
||||
## Admins: /kick <name> /ban <name> /unban <name> /admin <name> /save
|
||||
## /time 08:00 /weather clear|cloudy|rain|storm|snow|blizzard|auto
|
||||
## /backup /backups /time 08:00 /weather clear|cloudy|rain|storm|snow|blizzard|auto
|
||||
## /set <rule> <value> /say <message> /release-name <name>
|
||||
|
||||
signal message(text: String, kind: String) ## client: show this (kind: chat | server | reply)
|
||||
|
||||
const MAX_LEN := 200
|
||||
const GAP := 0.4 ## seconds between one player's lines
|
||||
const ADMIN_ONLY := ["kick", "ban", "unban", "admin", "save", "time", "weather", "set", "say", "release-name"]
|
||||
const ADMIN_ONLY := ["kick", "ban", "unban", "admin", "save", "backup", "backups", "time", "weather", "set", "say", "release-name"]
|
||||
|
||||
var _last: Dictionary = {} ## peer -> ticks of their last line
|
||||
var _console: Thread
|
||||
@@ -107,7 +107,7 @@ func run(line: String, peer: int) -> void:
|
||||
match cmd:
|
||||
"help":
|
||||
_reply(peer, "Commands: /players /rules /friend <name> /unfriend <name> /friends" +
|
||||
(" · admin: /kick /ban /unban /admin /save /time 08:00 /weather <state|auto> /set <rule> <value> /say /release-name" if _is_admin(peer) else ""))
|
||||
(" · admin: /kick /ban /unban /admin /save /backup /backups /time 08:00 /weather <state|auto> /set <rule> <value> /say /release-name" if _is_admin(peer) else ""))
|
||||
"players":
|
||||
var names: Array[String] = []
|
||||
for p in Net.players.values():
|
||||
@@ -174,6 +174,13 @@ func run(line: String, peer: int) -> void:
|
||||
"save":
|
||||
WorldNet.save()
|
||||
_reply(peer, "Saved")
|
||||
"backup":
|
||||
WorldNet.save()
|
||||
var b: String = WorldNet.backup("by %s" % by)
|
||||
_reply(peer, "Backed up as %s (%d kept)" % [b, WorldNet.list_backups().size()] if not b.is_empty() else "Nothing to back up yet")
|
||||
"backups":
|
||||
var all: Array = WorldNet.list_backups()
|
||||
_reply(peer, "%d backups, newest last: %s" % [all.size(), ", ".join(PackedStringArray(all.slice(-5)))] if not all.is_empty() else "No backups yet")
|
||||
"time":
|
||||
var hm := arg.split(":")
|
||||
if hm.size() < 1 or not hm[0].is_valid_int() or int(hm[0]) > 23 or (hm.size() > 1 and not hm[1].is_valid_int()):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -25,6 +25,7 @@ const DEFAULTS := {
|
||||
"difficulty": "normal",
|
||||
"autosave_minutes": 2,
|
||||
"backups_kept": 10,
|
||||
"backup_hours": 6,
|
||||
"admins": [],
|
||||
"allowlist": [],
|
||||
"banned": [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -87,8 +87,10 @@ func _ready() -> void:
|
||||
_pages["profile"] = _profile_page()
|
||||
_pages["join"] = _join_page()
|
||||
_pages["lobby"] = _lobby_page()
|
||||
_pages["host"] = _host_page()
|
||||
for p in _pages.values():
|
||||
col.add_child(p)
|
||||
Net.host_status.connect(_on_host_status)
|
||||
Net.joined.connect(_enter_server_world)
|
||||
Net.join_failed.connect(_on_join_failed)
|
||||
Net.left.connect(_on_left)
|
||||
@@ -127,7 +129,7 @@ func _maybe_capture() -> void:
|
||||
var me: RefCounted = Net.get_identity()
|
||||
if me.name.is_empty():
|
||||
me.name = "Paul" # in memory only, for the pictures
|
||||
for page in ["main", "new", "load", "profile", "join", "lobby"]:
|
||||
for page in ["main", "new", "load", "profile", "join", "host", "lobby"]:
|
||||
if page == "lobby":
|
||||
# Sample data: the lobby normally needs a live server.
|
||||
Net.server_info = {"server_name": "Paul's island", "motd": "Welcome! Be nice to the wolves.",
|
||||
@@ -147,7 +149,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not _pages["main"].visible:
|
||||
if _pages["lobby"].visible:
|
||||
Net.leave()
|
||||
elif Net.state == "connecting":
|
||||
elif Net.state == "connecting" or Net.hosting:
|
||||
Net.stop_hosting()
|
||||
Net.stop()
|
||||
_join_msg.text = ""
|
||||
_show("main")
|
||||
@@ -155,16 +158,19 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
_show("main")
|
||||
|
||||
func _show(page: String) -> void:
|
||||
# Joining needs a name: send people to their profile first.
|
||||
if page == "join" and Identity.name_problem(Net.get_identity().name) != "":
|
||||
_after_profile = "join"
|
||||
# Joining (or hosting) needs a name: send people to their profile first.
|
||||
if page in ["join", "host"] and Identity.name_problem(Net.get_identity().name) != "":
|
||||
_after_profile = page
|
||||
page = "profile"
|
||||
if page == "host":
|
||||
_fill_host()
|
||||
for k in _pages:
|
||||
_pages[k].visible = k == page
|
||||
_armed_delete = ""
|
||||
if page == "profile":
|
||||
_fill_profile()
|
||||
elif page == "join":
|
||||
_join_shown_at = Time.get_ticks_msec()
|
||||
_join_addr.text = Net.get_identity().last_server
|
||||
_join_btn.disabled = false
|
||||
_join_addr.grab_focus()
|
||||
@@ -199,6 +205,7 @@ func _main_page() -> Control:
|
||||
var lb := _button("Load World", _show.bind("load"))
|
||||
lb.disabled = worlds.is_empty()
|
||||
v.add_child(lb)
|
||||
v.add_child(_button("Host Game", _show.bind("host")))
|
||||
v.add_child(_button("Join Game", _show.bind("join")))
|
||||
var n: String = Net.get_identity().name
|
||||
v.add_child(_button("Profile: %s" % n if not n.is_empty() else "Set Up Your Profile",
|
||||
@@ -315,6 +322,119 @@ func _save_profile() -> void:
|
||||
_rebuild_main()
|
||||
_show(_after_profile)
|
||||
|
||||
var _join_shown_at := -100000
|
||||
|
||||
# --- hosting (M2.7) -------------------------------------------------------------
|
||||
|
||||
var _host_name: LineEdit
|
||||
var _host_pw: LineEdit
|
||||
var _host_port: LineEdit
|
||||
var _host_seed: LineEdit
|
||||
var _host_seed_label: Label
|
||||
var _host_preset: OptionButton
|
||||
var _host_msg: Label
|
||||
var _host_btn: Button
|
||||
const PRESET_IDS := ["co-op", "rivals", "adversarial"]
|
||||
const PRESET_NAMES := ["Co-op: no PvP, bases safe, friends share", "Friendly rivals: PvP on, bases safe",
|
||||
"Adversarial: PvP, raiding, locks, drop everything on death"]
|
||||
|
||||
func _host_page() -> Control:
|
||||
var p := UITheme.panel()
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 7)
|
||||
p.add_child(v)
|
||||
v.add_child(UITheme.label("Host a game", 24))
|
||||
v.add_child(UITheme.label("Runs a server on this computer and puts you in it. Friends join with your address.", 13, UITheme.TEXT_DIM))
|
||||
v.add_child(UITheme.label("Server name", 15, UITheme.TEXT_DIM))
|
||||
_host_name = LineEdit.new()
|
||||
_host_name.custom_minimum_size = Vector2(420, 38)
|
||||
_host_name.max_length = 40
|
||||
v.add_child(_host_name)
|
||||
v.add_child(UITheme.label("Rules", 15, UITheme.TEXT_DIM))
|
||||
_host_preset = OptionButton.new()
|
||||
for n in PRESET_NAMES:
|
||||
_host_preset.add_item(n)
|
||||
_host_preset.custom_minimum_size = Vector2(420, 36)
|
||||
v.add_child(_host_preset)
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
var c1 := VBoxContainer.new()
|
||||
c1.add_child(UITheme.label("Password (optional)", 15, UITheme.TEXT_DIM))
|
||||
_host_pw = LineEdit.new()
|
||||
_host_pw.secret = true
|
||||
_host_pw.custom_minimum_size = Vector2(200, 38)
|
||||
c1.add_child(_host_pw)
|
||||
row.add_child(c1)
|
||||
var c2 := VBoxContainer.new()
|
||||
c2.add_child(UITheme.label("Port (UDP)", 15, UITheme.TEXT_DIM))
|
||||
_host_port = LineEdit.new()
|
||||
_host_port.custom_minimum_size = Vector2(100, 38)
|
||||
c2.add_child(_host_port)
|
||||
row.add_child(c2)
|
||||
var c3 := VBoxContainer.new()
|
||||
_host_seed_label = UITheme.label("Seed", 15, UITheme.TEXT_DIM)
|
||||
c3.add_child(_host_seed_label)
|
||||
_host_seed = LineEdit.new()
|
||||
_host_seed.placeholder_text = "random"
|
||||
_host_seed.custom_minimum_size = Vector2(100, 38)
|
||||
c3.add_child(_host_seed)
|
||||
row.add_child(c3)
|
||||
v.add_child(row)
|
||||
_host_msg = UITheme.label("", 14, UITheme.TEXT_DIM)
|
||||
_host_msg.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
_host_msg.custom_minimum_size = Vector2(420, 0)
|
||||
v.add_child(_host_msg)
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", 10)
|
||||
_host_btn = _button("Start", _start_host, 140)
|
||||
h.add_child(_host_btn)
|
||||
h.add_child(_button("Back", func():
|
||||
Net.stop_hosting()
|
||||
Net.stop()
|
||||
_show("main"), 120))
|
||||
v.add_child(h)
|
||||
return p
|
||||
|
||||
func _fill_host() -> void:
|
||||
var c: RefCounted = load("res://scripts/net/server_config.gd").open(Net.host_config)
|
||||
var n: String = c.data.server_name
|
||||
_host_name.text = "%s's island" % Net.get_identity().name if n == "Emberwild server" else n
|
||||
_host_port.text = str(int(c.data.port))
|
||||
_host_pw.text = String(c.data.password)
|
||||
var preset := "co-op"
|
||||
if bool(c.data.pvp):
|
||||
preset = "adversarial" if bool(c.data.raiding) else "rivals"
|
||||
_host_preset.select(PRESET_IDS.find(preset))
|
||||
var meta_path: String = Net.host_config.get_base_dir().path_join("worlds/server-world/meta.json")
|
||||
var existing: bool = FileAccess.file_exists(meta_path)
|
||||
_host_seed.editable = not existing
|
||||
_host_seed_label.text = "Seed" if not existing else "Seed (kept)"
|
||||
_host_seed.text = "" if not existing else str(int(c.data.seed))
|
||||
_host_msg.text = "Your hosted island carries on where you left it." if existing else "A new island will be made."
|
||||
_host_msg.add_theme_color_override("font_color", UITheme.TEXT_DIM)
|
||||
_host_btn.disabled = false
|
||||
|
||||
func _start_host() -> void:
|
||||
var port_t := _host_port.text.strip_edges()
|
||||
if not port_t.is_valid_int() or int(port_t) < 1024 or int(port_t) > 65535:
|
||||
_on_host_status("The port is a number from 1024 to 65535 (24242 is the usual one)", true)
|
||||
return
|
||||
var seed_t := _host_seed.text.strip_edges()
|
||||
var seed := int(seed_t) if seed_t.is_valid_int() else (seed_t.hash() if not seed_t.is_empty() else randi() % 1000000)
|
||||
var name := _host_name.text.strip_edges()
|
||||
if name.is_empty():
|
||||
name = "%s's island" % Net.get_identity().name
|
||||
_host_btn.disabled = true
|
||||
Net.host(name, _host_pw.text, int(port_t), PRESET_IDS[maxi(_host_preset.selected, 0)], seed)
|
||||
|
||||
func _on_host_status(text: String, failed: bool) -> void:
|
||||
if not _pages["host"].visible:
|
||||
return
|
||||
_host_msg.text = text
|
||||
_host_msg.add_theme_color_override("font_color", UITheme.BAD if failed else UITheme.TEXT_DIM)
|
||||
if failed:
|
||||
_host_btn.disabled = false
|
||||
|
||||
func _join_page() -> Control:
|
||||
var p := UITheme.panel()
|
||||
var v := VBoxContainer.new()
|
||||
@@ -350,6 +470,9 @@ func _join_page() -> Control:
|
||||
return p
|
||||
|
||||
func _connect() -> void:
|
||||
# (A click or Enter carried over from the page before can't connect.)
|
||||
if Time.get_ticks_msec() - _join_shown_at < 400:
|
||||
return
|
||||
if _join_addr.text.strip_edges().is_empty():
|
||||
_join_msg.text = "Enter the server's address"
|
||||
_join_msg.add_theme_color_override("font_color", UITheme.BAD)
|
||||
@@ -360,6 +483,9 @@ func _connect() -> void:
|
||||
Net.join(_join_addr.text, _join_pw.text)
|
||||
|
||||
func _on_join_failed(reason: String) -> void:
|
||||
if _pages["host"].visible:
|
||||
_on_host_status(reason, true)
|
||||
return
|
||||
_join_btn.disabled = false
|
||||
_join_msg.text = reason
|
||||
_join_msg.add_theme_color_override("font_color", UITheme.BAD)
|
||||
|
||||
@@ -756,6 +756,9 @@ func _go_online() -> void:
|
||||
bar.position = Vector2(16, 40)
|
||||
$HUD.add_child(bar)
|
||||
Actions.notify.emit("Connected to %s" % Net.server_info.get("server_name", "the server"))
|
||||
if Net.hosting:
|
||||
var at := Net.my_addresses()
|
||||
Actions.notify.emit("You're hosting. Friends join at %s" % (", ".join(PackedStringArray(at)) if not at.is_empty() else "this computer's address"))
|
||||
|
||||
func _on_net_left(_reason: String) -> void:
|
||||
WorldNet.leave_world()
|
||||
|
||||
Reference in New Issue
Block a user