J3b: chat, the player list (with each player's land) and admin commands across lands; sleep needs everyone in every land, with a 'waiting for everyone else' message
This commit is contained in:
@@ -36,9 +36,28 @@ player list across lands are J3b). The rest is planned.
|
||||
- `tests/test_net_lands.gd`: Alice digs and props online, lights it,
|
||||
crosses to the Cove (bag and day kept) while Bob stays in the Valley,
|
||||
comes back up, then leaves from the Cove and rejoining sends her there.
|
||||
- Next, J3b: chat, the player list (with where each player is) and admin
|
||||
commands across lands; sleeping needs everyone in every land. J3c: a land
|
||||
server crashing, the Host page's extra ports.
|
||||
|
||||
*As built in J3b (everyone, everywhere):*
|
||||
- Chat and server messages go to every land (`Chat._all` relays through
|
||||
the hub, `Lands.relay_line`).
|
||||
- The hub keeps the **roster** (everyone on every land, with their land);
|
||||
every land and every player's game gets it (`Net.roster`,
|
||||
`Net.everyone()`): the pause menu's player list and `/players` show
|
||||
where each player is.
|
||||
- Commands typed in another land run **on the hub** (it alone keeps
|
||||
server.json) and answer back there; after one that changes server.json
|
||||
(`/set`, `/ban`, `/admin`, `/friend`...) every land reloads it.
|
||||
`/kick` and `/ban` reach the land the player's in; `/weather`, `/save`
|
||||
and `/backup` reach every land.
|
||||
- **Sleep needs everyone in every land:** land servers report their beds,
|
||||
the hub decides, and every land passes the night together. Getting into
|
||||
bed while others are up: "You're in bed: waiting for everyone else to go
|
||||
to bed (1 of 2 asleep)", and every land hears "Bob is in bed (1 of 2
|
||||
asleep)" in chat.
|
||||
- `tests/test_net_lands.gd` covers it all, Alice in the Cove and Bob in the
|
||||
Valley.
|
||||
- Next, J3c: a land server crashing (its players back to the hub), and the
|
||||
Host page's extra ports.
|
||||
|
||||
*As built in J1:*
|
||||
- `scripts/world/tunnel.gd`: the seam (an outcrop on the clifftop strip,
|
||||
|
||||
@@ -57,6 +57,11 @@ func _c2s_say(text: String) -> void:
|
||||
var who: String = Net.players[peer].name
|
||||
if text.begins_with("/"):
|
||||
print("[server] %s: %s" % [who, text])
|
||||
if not Lands.land.is_empty():
|
||||
# (Another land's server: commands run on the hub, which keeps
|
||||
# server.json, and answers back here.)
|
||||
Lands.send_hub({"t": "cmd", "line": text.substr(1), "id": String(Net.players[peer].id), "name": who, "peer": peer})
|
||||
return
|
||||
run(text.substr(1), peer)
|
||||
return
|
||||
# Plain chat only: a line every GAP seconds, so nobody can flood it.
|
||||
@@ -68,22 +73,44 @@ func _c2s_say(text: String) -> void:
|
||||
print("[chat] %s: %s" % [who, text])
|
||||
_all("%s: %s" % [who, text], "chat")
|
||||
|
||||
func _all(text: String, kind: String) -> void:
|
||||
## A line for everyone here, and (`relay`) in every other land too (J3b).
|
||||
func _all(text: String, kind: String, relay: bool = true) -> void:
|
||||
if kind == "server":
|
||||
print("[server] ", text)
|
||||
for peer in Net.players:
|
||||
_s2c_line.rpc_id(peer, text, kind)
|
||||
if relay:
|
||||
Lands.relay_line(text, kind)
|
||||
|
||||
## A command typed by someone in another land (J3b): who they are, and where
|
||||
## to send the answer. `run()` sees them as REMOTE.
|
||||
const Terrain = preload("res://scripts/world/terrain.gd")
|
||||
const REMOTE := -1
|
||||
var _caller: Dictionary = {}
|
||||
|
||||
func run_remote(line: String, caller: Dictionary) -> void:
|
||||
print("[server] %s (in the %s): /%s" % [caller.name, caller.land, line])
|
||||
_caller = caller
|
||||
run(line, REMOTE)
|
||||
_caller = {}
|
||||
|
||||
## An answer to whoever gave the command (peer 0 = the console).
|
||||
func _reply(peer: int, text: String) -> void:
|
||||
if peer == 0:
|
||||
print(text)
|
||||
elif peer == REMOTE:
|
||||
Lands.tell_land(String(_caller.land), {"t": "reply", "peer": int(_caller.peer), "text": text})
|
||||
else:
|
||||
_s2c_line.rpc_id(peer, text, "reply")
|
||||
|
||||
func _is_admin(peer: int) -> bool:
|
||||
if peer == REMOTE:
|
||||
return Net.config.is_admin(String(_caller.id))
|
||||
return peer == 0 or (Net.players.has(peer) and Net.config.is_admin(String(Net.players[peer].id)))
|
||||
|
||||
## Another land's server should know: this command changed server.json.
|
||||
const CHANGES_CONFIG := ["friend", "unfriend", "ban", "unban", "admin", "set", "release-name"]
|
||||
|
||||
## The player id a name belongs to (online or reserved), or "".
|
||||
func _id_of(n: String) -> String:
|
||||
var peer := Net.peer_by_name(n)
|
||||
@@ -102,16 +129,24 @@ func run(line: String, peer: int) -> void:
|
||||
if cmd in ADMIN_ONLY and not _is_admin(peer):
|
||||
_reply(peer, "Only admins can use /%s" % cmd)
|
||||
return
|
||||
var me: String = String(Net.players[peer].id) if Net.players.has(peer) else ""
|
||||
var by: String = String(Net.players[peer].name) if Net.players.has(peer) else "The server"
|
||||
var me: String = String(Net.players[peer].id) if Net.players.has(peer) else String(_caller.get("id", ""))
|
||||
var by: String = String(Net.players[peer].name) if Net.players.has(peer) else String(_caller.get("name", "The server"))
|
||||
_run_cmd(cmd, parts, rest, arg, peer, me, by)
|
||||
if cmd in CHANGES_CONFIG:
|
||||
Lands.tell_lands({"t": "config"})
|
||||
|
||||
func _run_cmd(cmd: String, parts: PackedStringArray, rest: String, arg: String, peer: int, me: String, by: String) -> void:
|
||||
match cmd:
|
||||
"help":
|
||||
_reply(peer, "Commands: /players /rules /friend <name> /unfriend <name> /friends" +
|
||||
(" · 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":
|
||||
# Everyone, in every land (J3b: the hub's roster), with where they are.
|
||||
var names: Array[String] = []
|
||||
for p in Net.players.values():
|
||||
names.append(String(p.name) + (" (admin)" if Net.config.is_admin(String(p.id)) else ""))
|
||||
var many := Lands.roster.any(func(e): return String(e.land) != Lands.here())
|
||||
for p in (Lands.roster if not Lands.roster.is_empty() else Lands._here_list()):
|
||||
names.append(String(p.name) + (" (admin)" if Net.config.is_admin(String(p.id)) else "")
|
||||
+ (" in the %s" % Terrain.worldlet_name(String(p.land)) if many else ""))
|
||||
_reply(peer, "%d online: %s" % [names.size(), ", ".join(names)])
|
||||
"rules":
|
||||
var c: RefCounted = Net.config
|
||||
@@ -142,10 +177,14 @@ func run(line: String, peer: int) -> void:
|
||||
_reply(peer, "Your friends: %s" % (", ".join(names) if not names.is_empty() else "nobody yet (/friend <name>)"))
|
||||
"kick":
|
||||
var kp := Net.peer_by_name(arg)
|
||||
if kp == 0:
|
||||
var away := Lands.land_of_name(arg) if kp == 0 else ""
|
||||
if kp == 0 and (away.is_empty() or away == Lands.here()):
|
||||
_reply(peer, "Nobody called '%s' is online" % arg)
|
||||
return
|
||||
Net.kick(kp, "You were kicked by %s" % by)
|
||||
if kp != 0:
|
||||
Net.kick(kp, "You were kicked by %s" % by)
|
||||
else:
|
||||
Lands.tell_land(away, {"t": "kick", "name": arg, "reason": "You were kicked by %s" % by})
|
||||
_all("%s was kicked" % arg, "server")
|
||||
"ban":
|
||||
var bid := _id_of(arg)
|
||||
@@ -156,6 +195,8 @@ func run(line: String, peer: int) -> void:
|
||||
var bp := Net.peer_by_name(arg)
|
||||
if bp != 0:
|
||||
Net.kick(bp, "You were banned by %s" % by)
|
||||
elif not Lands.land_of_name(arg).is_empty():
|
||||
Lands.tell_land(Lands.land_of_name(arg), {"t": "kick", "name": arg, "reason": "You were banned by %s" % by})
|
||||
_all("%s was banned" % arg, "server")
|
||||
"unban":
|
||||
var uid: String = Net.config.name_owner(arg)
|
||||
@@ -173,9 +214,11 @@ func run(line: String, peer: int) -> void:
|
||||
_reply(ap, "%s made you an admin (/help for the commands)" % by)
|
||||
"save":
|
||||
WorldNet.save()
|
||||
Lands.tell_lands({"t": "save"})
|
||||
_reply(peer, "Saved")
|
||||
"backup":
|
||||
WorldNet.save()
|
||||
Lands.tell_lands({"t": "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":
|
||||
@@ -198,6 +241,7 @@ func run(line: String, peer: int) -> void:
|
||||
_reply(peer, "Use /weather %s or auto" % "|".join(PackedStringArray(states)))
|
||||
return
|
||||
Net.set_weather("" if arg == "auto" else arg)
|
||||
Lands.tell_lands({"t": "weather", "state": "" if arg == "auto" else arg})
|
||||
_all("%s set the weather to %s" % [by, arg], "server")
|
||||
"set":
|
||||
if parts.size() < 3:
|
||||
|
||||
@@ -50,6 +50,9 @@ var _hub_link: Dictionary = {} ## {"conn": _hub, "buf": the partial line
|
||||
## Called once a server is listening. `args` are the command-line flags.
|
||||
func server_started(args: Dictionary) -> void:
|
||||
land = String(args.get("land", ""))
|
||||
roster = []
|
||||
if not Net.players_changed.is_connected(rebuild_roster):
|
||||
Net.players_changed.connect(rebuild_roster)
|
||||
if not land.is_empty():
|
||||
hub_port = int(args.get("hub-port", Net.DEFAULT_PORT))
|
||||
_hub = StreamPeerTCP.new()
|
||||
@@ -215,6 +218,8 @@ func _hub_tick(delta: float) -> void:
|
||||
print("[lands] the %s's server stopped" % l)
|
||||
_drain_output(s)
|
||||
servers.erase(l)
|
||||
rebuild_roster()
|
||||
PlayerNet.beds_changed()
|
||||
continue
|
||||
if s.conn != null:
|
||||
for msg in _read(s):
|
||||
@@ -253,6 +258,23 @@ func _from_land(l: String, msg: Dictionary) -> void:
|
||||
_send_land(l, {"t": "tunnel", "dug": _tunnel_dug_here()})
|
||||
"host_left":
|
||||
Net._host_left.call_deferred()
|
||||
"roster":
|
||||
servers[l].list = msg.get("list", [])
|
||||
rebuild_roster()
|
||||
"line":
|
||||
# Chat from that land: everyone here, and every other land.
|
||||
Chat._all(String(msg.text), String(msg.kind), false)
|
||||
for other in servers:
|
||||
if other != l:
|
||||
_send_land(other, msg)
|
||||
"cmd":
|
||||
Chat.run_remote(String(msg.line), {"id": String(msg.id), "name": String(msg.name), "land": l, "peer": int(msg.peer)})
|
||||
"beds":
|
||||
servers[l].beds = [int(msg.alive), int(msg.asleep)]
|
||||
# Tell the lands the running total (minus their own), so each can
|
||||
# say "N of M asleep", then see if the night passes.
|
||||
_push_bed_totals()
|
||||
PlayerNet.beds_changed()
|
||||
|
||||
## Sends `msg` to land `l`'s server.
|
||||
func _send_land(l: String, msg: Dictionary) -> void:
|
||||
@@ -302,9 +324,11 @@ func _land_tick() -> void:
|
||||
send_hub({"t": "hello", "land": land})
|
||||
send_hub({"t": "ready"})
|
||||
var n := Net.players.size()
|
||||
if n != _last_players:
|
||||
if n != _last_players or _roster_dirty:
|
||||
_last_players = n
|
||||
_roster_dirty = false
|
||||
send_hub({"t": "players", "n": n})
|
||||
send_hub({"t": "roster", "list": _here_list()})
|
||||
for msg in _read(_hub_link):
|
||||
_from_hub(msg)
|
||||
elif st == StreamPeerTCP.STATUS_ERROR or (st == StreamPeerTCP.STATUS_NONE and _said_hello):
|
||||
@@ -336,12 +360,116 @@ func _from_hub(msg: Dictionary) -> void:
|
||||
Net.push_clock()
|
||||
"tunnel":
|
||||
tunnel_dug = int(msg.dug)
|
||||
"roster":
|
||||
roster = msg.get("list", [])
|
||||
Net.push_roster()
|
||||
"line":
|
||||
Chat._all(String(msg.text), String(msg.kind), false)
|
||||
"reply":
|
||||
if Net.players.has(int(msg.peer)):
|
||||
Chat._s2c_line.rpc_id(int(msg.peer), String(msg.text), "reply")
|
||||
"kick":
|
||||
var kp: int = Net.peer_by_name(String(msg.name))
|
||||
if kp != 0:
|
||||
Net.kick(kp, String(msg.reason))
|
||||
"config":
|
||||
Net.reload_config()
|
||||
Net.broadcast_rules()
|
||||
"weather":
|
||||
Net.set_weather(String(msg.state))
|
||||
"save":
|
||||
WorldNet.save()
|
||||
"night":
|
||||
PlayerNet.pass_night_here(float(msg.secs))
|
||||
"beds_total":
|
||||
PlayerNet._hub_beds = [int(msg.alive), int(msg.asleep)]
|
||||
hub_message.emit(msg)
|
||||
|
||||
func send_hub(msg: Dictionary) -> void:
|
||||
if _hub != null and _hub.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
||||
_hub.put_data((JSON.stringify(msg) + "\n").to_utf8_buffer())
|
||||
|
||||
# --- everyone, everywhere (J3b) ------------------------------------------------------------
|
||||
|
||||
## Everyone on every land: [{"name", "id", "land", "admin"}]. The hub builds
|
||||
## it from its own players and what each land reports; every land and every
|
||||
## player's game gets a copy (Net.roster), for the player list and /players.
|
||||
var roster: Array = []
|
||||
var _roster_dirty := false
|
||||
|
||||
## The players on this server, as the roster lists them.
|
||||
func _here_list() -> Array:
|
||||
var out: Array = []
|
||||
for p in Net.players.values():
|
||||
out.append({"name": String(p.name), "id": String(p.id), "land": here(), "admin": bool(p.get("admin", false))})
|
||||
return out
|
||||
|
||||
## Hub: the roster again (its players changed, or a land's did), sent out.
|
||||
func rebuild_roster() -> void:
|
||||
if not hub:
|
||||
if not land.is_empty():
|
||||
_roster_dirty = true # (a land server: tell the hub, next tick)
|
||||
return
|
||||
var out: Array = _here_list()
|
||||
for l in servers:
|
||||
for e in servers[l].get("list", []):
|
||||
var e2: Dictionary = (e as Dictionary).duplicate()
|
||||
e2.land = l
|
||||
out.append(e2)
|
||||
roster = out
|
||||
Net.push_roster()
|
||||
for l in servers:
|
||||
_send_land(l, {"t": "roster", "list": out})
|
||||
|
||||
## The land a player called `n` is in ("" = not on anywhere).
|
||||
func land_of_name(n: String) -> String:
|
||||
for e in roster:
|
||||
if String(e.name).to_lower() == n.to_lower():
|
||||
return String(e.land)
|
||||
return ""
|
||||
|
||||
## Chat or a server message, from here to every other land.
|
||||
func relay_line(text: String, kind: String) -> void:
|
||||
if hub:
|
||||
for l in servers:
|
||||
_send_land(l, {"t": "line", "text": text, "kind": kind})
|
||||
elif not land.is_empty():
|
||||
send_hub({"t": "line", "text": text, "kind": kind})
|
||||
|
||||
## Hub: sends `msg` to every land.
|
||||
func tell_lands(msg: Dictionary) -> void:
|
||||
for l in servers:
|
||||
_send_land(l, msg)
|
||||
|
||||
## Hub: sends `msg` to land `l`.
|
||||
func tell_land(l: String, msg: Dictionary) -> void:
|
||||
_send_land(l, msg)
|
||||
|
||||
## Hub: tells each land the alive/asleep everywhere BUT that land, so a land
|
||||
## server can show the right "N of M" and know when all are abed.
|
||||
func _push_bed_totals() -> void:
|
||||
var here_alive: int = PlayerNet._alive().size()
|
||||
var here_asleep: int = PlayerNet._alive().filter(func(q): return q.in_bed).size()
|
||||
for l in servers:
|
||||
var a := here_alive
|
||||
var s := here_asleep
|
||||
for other in servers:
|
||||
if other != l:
|
||||
var b: Array = servers[other].get("beds", [0, 0])
|
||||
a += int(b[0])
|
||||
s += int(b[1])
|
||||
_send_land(l, {"t": "beds_total", "alive": a, "asleep": s})
|
||||
|
||||
## Hub: how many are alive, and how many asleep, in every other land.
|
||||
func beds_elsewhere() -> Array:
|
||||
var alive := 0
|
||||
var asleep := 0
|
||||
for l in servers:
|
||||
var b: Array = servers[l].get("beds", [0, 0])
|
||||
alive += int(b[0])
|
||||
asleep += int(b[1])
|
||||
return [alive, asleep]
|
||||
|
||||
## Land server: a pass for player `id` to go back to the hub.
|
||||
func pass_home(id: String) -> String:
|
||||
var p := Crypto.new().generate_random_bytes(16).hex_encode()
|
||||
|
||||
@@ -232,6 +232,7 @@ func stop() -> void:
|
||||
_states.clear()
|
||||
server_info.clear()
|
||||
world_info.clear()
|
||||
roster.clear()
|
||||
_out_q.clear()
|
||||
_in_q.clear()
|
||||
local_peer = 0
|
||||
@@ -514,6 +515,42 @@ func _quiet_stop() -> void:
|
||||
func is_travelling() -> bool:
|
||||
return not _travel.is_empty()
|
||||
|
||||
# --- everyone on every land (J3b, lands.gd) -------------------------------------------
|
||||
|
||||
## Client: everyone on the server, whichever land they're in: [{"name",
|
||||
## "id", "land", "admin"}] (the hub's roster). Empty on a server with only
|
||||
## one land running: then `players` is everyone.
|
||||
var roster: Array = []
|
||||
|
||||
## Server: everyone here gets the roster.
|
||||
func push_roster() -> void:
|
||||
for peer in players:
|
||||
_s2c_roster.rpc_id(peer, Lands.roster)
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_roster(list: Array) -> void:
|
||||
roster = list
|
||||
players_changed.emit()
|
||||
|
||||
## Everyone on the server to show in a list: the roster, or (one land) just
|
||||
## those here. [{"name", "admin", "land"}]
|
||||
func everyone() -> Array:
|
||||
if not roster.is_empty():
|
||||
return roster
|
||||
var out: Array = []
|
||||
for p in players.values():
|
||||
out.append({"name": String(p.name), "admin": bool(p.get("admin", false)), "land": String(world_info.get("worldlet", ""))})
|
||||
return out
|
||||
|
||||
## Server: server.json read again (the hub changed it: a /set, a ban, a
|
||||
## new admin, a friend), keeping this run's command-line overrides.
|
||||
func reload_config() -> void:
|
||||
if config == null:
|
||||
return
|
||||
var ov: Dictionary = config.overrides
|
||||
config = ServerConfig.open(config.path)
|
||||
config.overrides = ov
|
||||
|
||||
## Everyone gets the clock now (after a night's sleep, an admin's /time).
|
||||
func push_clock() -> void:
|
||||
_clock_left = CLOCK_INTERVAL
|
||||
|
||||
@@ -92,6 +92,7 @@ func server_join(peer: int, id: String, display: String) -> Vector3:
|
||||
print("[server] %s's character loaded" % display)
|
||||
if at != Vector3.INF:
|
||||
p.global_position = at
|
||||
beds_changed.call_deferred() # (one more awake: everyone's count changes)
|
||||
return at
|
||||
|
||||
func place_at(peer: int, pos: Vector3) -> void:
|
||||
@@ -212,6 +213,7 @@ func server_moved(peer: int, from: Vector3, to: Vector3, yaw: float, anim: int,
|
||||
if p.in_bed and to.distance_to(p.bed_at) > 1.5:
|
||||
p.in_bed = false
|
||||
tell(p.peer, "You got out of bed")
|
||||
beds_changed()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not Net.is_server() or proxies.is_empty():
|
||||
@@ -607,28 +609,66 @@ func go_to_bed(p: Node3D) -> void:
|
||||
p.in_bed = true
|
||||
p.bed_at = p.global_position
|
||||
if not _maybe_pass_night():
|
||||
var n: int = _alive().size()
|
||||
var asleep: int = _alive().filter(func(q): return q.in_bed).size()
|
||||
tell(p.peer, "You're in bed (%d of %d asleep). The night passes when %s." % [asleep, n,
|
||||
"everyone's in bed" if _beds_needed(n) >= n else "%d are" % _beds_needed(n)])
|
||||
for q in _alive():
|
||||
if q != p:
|
||||
tell(q.peer, "%s is in bed (%d of %d asleep)" % [p.display, asleep, n])
|
||||
# (Counting everyone, in every land: on another land's server, the
|
||||
# hub's last count, with this bed in it.)
|
||||
var counts := sleep_counts()
|
||||
if not Lands.land.is_empty():
|
||||
counts = [_alive().size() + _hub_beds[0], _alive().filter(func(q): return q.in_bed).size() + _hub_beds[1]]
|
||||
var n: int = counts[0]
|
||||
var asleep: int = counts[1]
|
||||
var need: int = _beds_needed(n)
|
||||
tell(p.peer, waiting_text(asleep, n, need))
|
||||
# Everyone else, in every land, hears it in chat (so whoever's still
|
||||
# up knows they're being waited for).
|
||||
Chat._all("%s is in bed (%d of %d asleep%s)" % [p.display, asleep, n,
|
||||
"" if need >= n else ", %d needed" % need], "server")
|
||||
|
||||
## What a player who's just got into bed is told while the others are up.
|
||||
static func waiting_text(asleep: int, n: int, need: int) -> String:
|
||||
var left := need - asleep
|
||||
var who := "everyone else" if need >= n else ("%d more" % left)
|
||||
return "You're in bed: waiting for %s to go to bed (%d of %d asleep)" % [who, asleep, n]
|
||||
|
||||
func _alive() -> Array:
|
||||
return proxies.values().filter(func(q): return not q.survival.dead)
|
||||
|
||||
## Everyone in bed (by the rule)? Then until 06:00 for all: the clock, fires,
|
||||
## regrowth and weather move on; sleepers wake rested.
|
||||
## One clock for every land (J3b): the night passes when enough of everyone,
|
||||
## in every land, is in bed. Another land's server reports its beds to the
|
||||
## hub, which decides, and tells every land to pass the night too.
|
||||
func _maybe_pass_night() -> bool:
|
||||
var w: Node3D = WorldNet.world
|
||||
var live := _alive()
|
||||
if w == null or live.is_empty():
|
||||
if w == null:
|
||||
return false
|
||||
var asleep := live.filter(func(q): return q.in_bed)
|
||||
if asleep.size() < _beds_needed(live.size()):
|
||||
if not Lands.land.is_empty():
|
||||
_report_beds()
|
||||
return false
|
||||
Lands._push_bed_totals()
|
||||
var counts := sleep_counts()
|
||||
if counts[0] == 0 or counts[1] < _beds_needed(counts[0]):
|
||||
return false
|
||||
var secs: float = fposmod(6.0 - w.day_night.hours(), 24.0) / 24.0 * w.day_night.day_length
|
||||
Lands.tell_lands({"t": "night", "secs": secs})
|
||||
pass_night_here(secs)
|
||||
return true
|
||||
|
||||
## [alive, asleep] on this server and (the hub) every other land.
|
||||
func sleep_counts() -> Array:
|
||||
var live := _alive()
|
||||
var counts := [live.size(), live.filter(func(q): return q.in_bed).size()]
|
||||
if Lands.hub:
|
||||
var e: Array = Lands.beds_elsewhere()
|
||||
counts = [counts[0] + int(e[0]), counts[1] + int(e[1])]
|
||||
return counts
|
||||
|
||||
## The night passes here: until 06:00 for everyone on this server.
|
||||
func pass_night_here(secs: float) -> void:
|
||||
var w: Node3D = WorldNet.world
|
||||
if w == null:
|
||||
return
|
||||
var live := _alive()
|
||||
var asleep := live.filter(func(q): return q.in_bed)
|
||||
w.skip_time(secs)
|
||||
for q in live:
|
||||
if q.in_bed:
|
||||
@@ -637,7 +677,20 @@ func _maybe_pass_night() -> bool:
|
||||
_s2c_slept.rpc_id(q.peer, asleep.has(q))
|
||||
Net.push_clock()
|
||||
WorldNet.save()
|
||||
return true
|
||||
if not Lands.land.is_empty():
|
||||
_report_beds()
|
||||
|
||||
## Someone got into or out of bed, or came or went: (a land server) the hub
|
||||
## hears the new count; (the hub) the night may pass.
|
||||
func beds_changed() -> void:
|
||||
if not Lands.land.is_empty():
|
||||
_report_beds()
|
||||
elif Net.is_server():
|
||||
_maybe_pass_night()
|
||||
|
||||
func _report_beds() -> void:
|
||||
var live := _alive()
|
||||
Lands.send_hub({"t": "beds", "alive": live.size(), "asleep": live.filter(func(q): return q.in_bed).size()})
|
||||
|
||||
## The chest or grave `nid`, if this player may use it right now.
|
||||
func _container(p: Node3D, nid: int) -> Node3D:
|
||||
@@ -648,6 +701,10 @@ func _container(p: Node3D, nid: int) -> Node3D:
|
||||
|
||||
const Rules = preload("res://scripts/net/rules.gd")
|
||||
|
||||
## (Land server) the hub's latest [alive, asleep] in every OTHER land, for
|
||||
## the "N of M asleep" message and the all-abed check here.
|
||||
var _hub_beds := [0, 0]
|
||||
|
||||
## Tells the player why not, if there's a reason.
|
||||
func _allowed(why: String) -> bool:
|
||||
if why.is_empty():
|
||||
|
||||
@@ -194,9 +194,18 @@ func open() -> void:
|
||||
get_tree().paused = not _online # a shared world can't be paused by one player
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
if _online:
|
||||
# Everyone on the server, in whichever land (J3b), with where they are
|
||||
# once anyone's somewhere else.
|
||||
var all: Array = Net.everyone()
|
||||
var lands := {}
|
||||
for p in all:
|
||||
lands[String(p.get("land", ""))] = true
|
||||
var names: Array[String] = []
|
||||
for p in Net.players.values():
|
||||
names.append(String(p.name) + (" (admin)" if p.get("admin", false) else ""))
|
||||
for p in all:
|
||||
var where := ""
|
||||
if lands.size() > 1 and not String(p.get("land", "")).is_empty():
|
||||
where = " · %s" % preload("res://scripts/world/terrain.gd").worldlet_name(String(p.land))
|
||||
names.append(String(p.name) + (" (admin)" if p.get("admin", false) else "") + where)
|
||||
names.sort()
|
||||
_players_label.text = "%s · %d online\n%s" % [Net.server_info.get("server_name", "Server"), names.size(), ", ".join(names)]
|
||||
|
||||
|
||||
@@ -118,6 +118,13 @@ func _body() -> void:
|
||||
check(world.all_pieces().any(func(p): return p.item_id == "torch_stand" and world.underground_at(p.global_position)),
|
||||
"a torch stand set down there, for everyone")
|
||||
|
||||
# (A hut with beds in the Valley, for Bob, later.)
|
||||
var vsite: Vector3 = world.build.debug_find_site(world._spawn + Vector3(0, 0, 14), 3.0)
|
||||
PN.ask("debug_hut", [vsite])
|
||||
await _until(func(): return _beds().size() >= 1, 8.0)
|
||||
var bob_bed: int = WN.nid(_beds()[0]) if not _beds().is_empty() else 0
|
||||
var bob_bed_at: Vector3 = _beds()[0].global_position if not _beds().is_empty() else Vector3.ZERO
|
||||
|
||||
print("out to the Cove")
|
||||
var day0: int = world.day_night.day
|
||||
var stone0: int = player.inventory.count("stone")
|
||||
@@ -138,6 +145,7 @@ func _body() -> void:
|
||||
check(world.sea_cave.open(), "the sea cave leads back up (the hub says the tunnel's through)")
|
||||
var bob := await _bob({"do": "report"})
|
||||
check(not bob.has("error") and not bob.is_empty(), "Bob carries on in the Valley, on the hub (%s)" % bob.get("error", "ok"))
|
||||
await _across_lands(bob_bed, bob_bed_at)
|
||||
|
||||
print("back up to the Valley")
|
||||
_arrived = ""
|
||||
@@ -173,8 +181,82 @@ func _body() -> void:
|
||||
await _load_world()
|
||||
check(world.worldlet_id() == "cove" and player.global_position.distance_to(spot) < 1.5,
|
||||
"where she left off (%.1f m off)" % player.global_position.distance_to(spot))
|
||||
print("an admin in the Cove kicks someone in the Valley")
|
||||
await _until(func(): return Net.roster.any(func(e): return String(e.name) == "Bob"), 4.0)
|
||||
root.get_node("Chat").say("/kick Bob")
|
||||
await _until(func(): return not Net.roster.any(func(e): return String(e.name) == "Bob"), 6.0)
|
||||
check(not Net.roster.any(func(e): return String(e.name) == "Bob"), "Bob's gone from the server (%s)" % [Net.roster.map(func(e): return e.name)])
|
||||
_finished = true
|
||||
|
||||
# --- J3b: chat, the player list, commands and sleep across lands --------------------------
|
||||
|
||||
func _across_lands(bob_bed: int, bob_bed_at: Vector3) -> void:
|
||||
var C: Node = root.get_node("Chat")
|
||||
print("chat across lands")
|
||||
await _bob({"do": "say", "text": "hello from the Valley"})
|
||||
await _until(func(): return C.heard.has("Bob: hello from the Valley"), 4.0)
|
||||
check(C.heard.has("Bob: hello from the Valley"), "Bob in the Valley says hello; Alice hears it in the Cove")
|
||||
await _sleep(1.2) # (chat's flood gap)
|
||||
C.say("hi from the Cove")
|
||||
var heard := false
|
||||
for _i in range(15):
|
||||
var h := await _bob({"do": "heard"})
|
||||
if (h.get("heard", []) as Array).has("Alice: hi from the Cove"):
|
||||
heard = true
|
||||
break
|
||||
await _sleep(0.2)
|
||||
check(heard, "and Bob hears her back")
|
||||
|
||||
print("the player list")
|
||||
await _until(func(): return Net.roster.size() >= 2, 4.0)
|
||||
var where := {}
|
||||
for e in Net.roster:
|
||||
where[String(e.name)] = String(e.land)
|
||||
check(where.get("Alice", "") == "cove" and where.get("Bob", "") == "valley", "everyone, and where they are (%s)" % [where])
|
||||
C.say("/players")
|
||||
await _until(func(): return C.heard.any(func(l): return String(l).begins_with("2 online")), 4.0)
|
||||
var line: String = ""
|
||||
for l in C.heard:
|
||||
if String(l).begins_with("2 online"):
|
||||
line = String(l)
|
||||
check(line.contains("Bob in the Valley") and line.contains("Alice (admin) in the Cove"), "/players from the Cove: %s" % line)
|
||||
C.say("/set wolf_grace_days 5")
|
||||
await _until(func(): return int(Net.server_info.get("wolf_grace_days", 0)) == 5, 4.0)
|
||||
check(int(Net.server_info.get("wolf_grace_days", 0)) == 5, "an admin's /set from the Cove: the hub keeps server.json, and the Cove's rules follow")
|
||||
|
||||
print("sleep needs everyone, in every land")
|
||||
await _bob({"do": "ask", "what": "debug_time", "args": [world.day_night.day, 21.0]})
|
||||
await _until(func(): return world.day_night.hours() > 20.5, 15.0)
|
||||
check(world.day_night.hours() > 20.5, "night in the Valley is night in the Cove (%s)" % world.day_night.clock_text())
|
||||
var day_n: int = world.day_night.day
|
||||
var site: Vector3 = world.build.debug_find_site(player.global_position + Vector3(0, 0, 14), 3.0)
|
||||
PN.ask("debug_hut", [site])
|
||||
await _until(func(): return _beds().size() >= 1, 8.0)
|
||||
check(not _beds().is_empty() and bob_bed != 0, "beds in the Cove and the Valley")
|
||||
if _beds().is_empty() or bob_bed == 0:
|
||||
return
|
||||
await _bob({"do": "tp", "to": [bob_bed_at.x, bob_bed_at.y + 0.1, bob_bed_at.z + 0.9]})
|
||||
await _bob({"do": "bed", "nid": bob_bed})
|
||||
await _bob({"do": "bed", "nid": bob_bed})
|
||||
await _until(func(): return C.heard.any(func(l): return String(l).begins_with("Bob is in bed")), 4.0)
|
||||
check(C.heard.has("Bob is in bed (1 of 2 asleep)"), "Bob gets into bed in the Valley; Alice hears it in the Cove (%s)" % [C.heard.slice(-2)])
|
||||
await _sleep(1.0)
|
||||
check(world.day_night.hours() > 20.5, "and the night goes on: Alice is still up, in another land")
|
||||
var mine: Node3D = _beds()[0]
|
||||
await _tp(mine.global_position + Vector3(0, 0.1, 0.9))
|
||||
_toasts.clear()
|
||||
A.use_bed(player, mine)
|
||||
await _sleep(0.5)
|
||||
A.use_bed(player, mine)
|
||||
await _until(func(): return world.day_night.hours() < 7.0, 5.0)
|
||||
check(world.day_night.hours() >= 5.9 and world.day_night.hours() < 7.0 and world.day_night.day == day_n + 1,
|
||||
"Alice gets into bed in the Cove, and the night passes for both (%s, day %d)" % [world.day_night.clock_text(), world.day_night.day])
|
||||
var life := await _bob({"do": "life"})
|
||||
check(float(life.get("hours", 0)) >= 5.9 and float(life.get("hours", 0)) < 7.0, "Bob wakes to morning in the Valley too (%.1f h)" % float(life.get("hours", 0)))
|
||||
|
||||
func _beds() -> Array:
|
||||
return WN.pieces.values().filter(func(p): return is_instance_valid(p) and p.item_id == "bed")
|
||||
|
||||
# --- helpers -------------------------------------------------------------------------
|
||||
|
||||
## (Re)loads the world for whichever server we're on now.
|
||||
|
||||
@@ -269,7 +269,8 @@ func _sleeping() -> void:
|
||||
await _sleep(0.5)
|
||||
A.use_bed(player, beds[0])
|
||||
await _sleep(1.0)
|
||||
check(_toasts.any(func(t): return t.begins_with("You're in bed (1 of 2")), "Alice gets into bed: 1 of 2 asleep (%s)" % [_toasts])
|
||||
check(_toasts.any(func(t): return t == "You're in bed: waiting for everyone else to go to bed (1 of 2 asleep)"),
|
||||
"Alice gets into bed: waiting for the others, 1 of 2 asleep (%s)" % [_toasts])
|
||||
check(world.day_night.hours() > 20.5, "and the night goes on, because Bob's still up")
|
||||
await _bob({"do": "bed", "nid": WN.nid(beds[1])})
|
||||
await _bob({"do": "bed", "nid": WN.nid(beds[1])})
|
||||
|
||||
@@ -372,7 +372,7 @@ A spare computer can run an always-on server with no window, set up in its `serv
|
||||
|
||||
### Playing together
|
||||
|
||||
The server decides everything, so the pause menu doesn't pause; it lists who's online and has **Disconnect**. Each player has their own bed. Only you can open your grave. Looking at someone's piece shows whose it is. More players means more animals about, and the wolf pack splits between the players it hunts, preferring those away from a fire. The night passes once enough players are in bed.
|
||||
The server decides everything, so the pause menu doesn't pause; it lists who's online, and which land each of them is in, and has **Disconnect**. Chat reaches everyone, whatever land they're in. Each player has their own bed. Only you can open your grave. Looking at someone's piece shows whose it is. More players means more animals about, and the wolf pack splits between the players it hunts, preferring those away from a fire. The night passes once enough players are in bed, counting everyone in every land: get into bed while others are up and you'll see "waiting for everyone else to go to bed", and everyone hears in chat that you're in bed.
|
||||
|
||||
### Rules and presets
|
||||
|
||||
|
||||
Reference in New Issue
Block a user