M2.4: the server decides online (characters, inventory, stats, death, owned graves, per-player saves); death screen with Respawn after 3 s

This commit is contained in:
Paul Bailey
2026-11-04 19:18:31 +00:00
parent 8e8f10eb96
commit 35a656e996
16 changed files with 1124 additions and 477 deletions

View File

@@ -65,6 +65,14 @@ var bed_spawn := Vector3.ZERO ## position of the bed you respawn at
func is_online() -> bool:
return not menu_mode and Net.state == "connected"
var _loading := false ## applying a save: defer support recompute
var _applying := false ## applying a save or snapshot: not news for the server's players
## Server only: changes to the island, wherever they came from (a player's
## request, a collapse, a death), for world_net.gd to send to everyone.
signal piece_added(piece: Node3D)
signal pieces_removed(gone: Array, fell: Array)
signal dropped_added(item: Node3D)
signal saved
func _ready() -> void:
_autosave_left = autosave_seconds
@@ -151,12 +159,15 @@ func air_warmth(pos: Vector3, sheltered: bool = false) -> float:
return w
## 0..1 heat from fires and a held torch.
func heat_at(pos: Vector3) -> float:
## `holder` is whose torch counts (default: the local player; on a server,
## each player's character).
func heat_at(pos: Vector3, holder: Node = null) -> float:
var h := 0.0
for n in get_tree().get_nodes_in_group("heat"):
h = maxf(h, n.heat_at(pos))
if player.hands != null:
h = maxf(h, player.hands.held_heat())
var who: Node = holder if holder != null else player
if who.get("hands") != null:
h = maxf(h, who.hands.held_heat())
return h
## Death: everything you carried goes into a grave where you fell, and you
@@ -176,16 +187,34 @@ func _on_player_died() -> void:
if st != null:
has_items = true
if has_items:
var gxf := Transform3D(Basis(Vector3.UP, player.rotation.y), pos)
if net_sync != null:
WorldNet.place("grave", gxf, player.inventory.to_data()) # the server puts it in the shared world
else:
var grave := spawn_piece(ItemDB.get_item("grave"), gxf)
grave.inventory.load_data(player.inventory.to_data())
var grave := spawn_piece(ItemDB.get_item("grave"), Transform3D(Basis(Vector3.UP, player.rotation.y), pos))
grave.inventory.load_data(player.inventory.to_data())
player.inventory.load_data(Inventory.new(player.inventory.size()).to_data())
# The death screen: you stay down for a few seconds, then Respawn.
# (Headless, with no screen, you wake straight away.)
dead_waiting = true
player.velocity = Vector3.ZERO
player.set_physics_process(false)
if hud != null:
hud.close_screens()
var bed := spawn_bed() if net_sync == null else null # (online: beds aren't shared until M2.4)
hud.show_death(player.survival.cause, _death_note(has_items, spawn_bed() != null), RESPAWN_DELAY, respawn_after_death)
else:
respawn_after_death()
const RESPAWN_DELAY := 3.0 ## seconds on the death screen before you can respawn
var dead_waiting := false ## dead, on the death screen
static func _death_note(has_items: bool, to_bed: bool) -> String:
return "%s\nYou'll wake %s." % ["Your belongings are in a grave where you fell." if has_items else "You had nothing on you.",
"at your bed" if to_bed else "at the island start"]
## Respawn (the death screen's button): at your bed, or the island start.
func respawn_after_death() -> void:
if not dead_waiting:
return
dead_waiting = false
player.set_physics_process(true)
var bed := spawn_bed()
if bed != null:
_respawn_at_bed(bed)
else:
@@ -194,7 +223,17 @@ func _on_player_died() -> void:
has_bed_spawn = false
Actions.notify.emit("Your bed is gone, so you woke at the island start")
player.survival.reset_after_death()
Actions.notify.emit("You died. Your belongings are in your grave." if has_items else "You died.")
## Online: the server says we died. Same screen; Respawn asks the server.
func died_online(text: String, has_items: bool, to_bed: bool) -> void:
dead_waiting = true
player.velocity = Vector3.ZERO
player.set_physics_process(false)
if hud != null:
hud.close_screens()
hud.show_death(text, _death_note(has_items, to_bed), RESPAWN_DELAY, func(): PlayerNet.ask("respawn"))
else:
get_tree().create_timer(RESPAWN_DELAY + 0.1).timeout.connect(func(): PlayerNet.ask("respawn"))
# --- beds and sleeping ------------------------------------------------------
@@ -217,9 +256,21 @@ func _respawn_at_bed(bed: Node3D) -> void:
player.rotation = Vector3(0.0, bed.global_basis.get_euler().y + PI, 0.0)
player.get_node("Camera3D").rotation.x = 0.0
## Online: the server says we died (or were moved) and puts us here.
func respawn_from_server(at: Vector3, yaw: float) -> void:
dead_waiting = false
player.set_physics_process(true)
if hud != null:
hud.close_screens()
hud.hide_death()
player.global_position = at
player.velocity = Vector3.ZERO
player.rotation = Vector3(0.0, yaw, 0.0)
player.get_node("Camera3D").rotation.x = 0.0
## Why you can't sleep in `bed` right now ("" = you can).
func sleep_block_reason(bed: Node3D) -> String:
if is_online():
if is_online() or server_mode:
return "Sleeping on a server isn't in yet (everyone would have to agree)"
var h: float = day_night.hours()
if h > 5.5 and h < 19.5:
@@ -334,12 +385,17 @@ func save_now() -> bool:
_autosave_left = autosave_seconds
if not Saves.has_world():
return false
if dead_waiting and not server_mode:
respawn_after_death() # never save someone lying dead
if hud != null:
hud.hide_death()
var ok: bool = Saves.save_state(Saves.current_id, collect_save(), _session_time)
if ok:
_session_time = 0.0
if server_mode:
if not ok:
printerr("[server] saving the world FAILED")
saved.emit() # (the players' characters save with it)
else:
Actions.notify.emit("Game saved" if ok else "Save FAILED")
return ok
@@ -502,6 +558,11 @@ func remove_pieces_net(gone: Array, collapsed_nids: Array) -> void:
_update_grass_cuts()
func apply_save(s: Dictionary, with_player: bool = true) -> void:
_applying = true
_apply_save(s, with_player)
_applying = false
func _apply_save(s: Dictionary, with_player: bool) -> void:
var p: Dictionary = s.get("player", {}) if with_player else {}
if p.has("pos"):
player.global_position = Vector3(p.pos[0], p.pos[1] + 0.05, p.pos[2])
@@ -596,16 +657,21 @@ func placement_valid(def: Dictionary, xf: Transform3D) -> bool:
q.exclude = [terrain.get_node("TerrainBody").get_rid(), player.get_rid()]
return get_world_3d().direct_space_state.intersect_shape(q, 1).is_empty()
func spawn_piece(def: Dictionary, xf: Transform3D) -> Node3D:
## `extra` is the piece's own state (a grave's owner, a door left open...).
func spawn_piece(def: Dictionary, xf: Transform3D, extra: Dictionary = {}) -> Node3D:
var script: Script = PIECES.get(def.place.piece, preload("res://scripts/building/piece.gd"))
var piece: StaticBody3D = script.new()
piece.setup(def)
_pieces.add_child(piece)
piece.global_transform = xf
if not extra.is_empty() and piece.has_method("load_extra"):
piece.load_extra(extra)
if piece.is_in_group("build") and not _loading:
build.recompute()
if piece.kind == "floor":
_update_grass_cuts()
if server_mode and not _applying:
piece_added.emit(piece)
return piece
## Grass doesn't grow up through floors.
@@ -636,6 +702,8 @@ func _go_online() -> void:
_spawn = Net.world_info.get("island_spawn", _spawn)
var start: Vector3 = Net.world_info.get("spawn", _spawn)
player.global_position = start + Vector3(0.0, 0.05, 0.0)
if Net.world_info.has("yaw"):
player.rotation.y = float(Net.world_info.yaw) # facing the way you were when you left
net_sync = preload("res://scripts/net/net_sync.gd").new()
net_sync.name = "NetSync"
add_child(net_sync)
@@ -645,8 +713,9 @@ func _go_online() -> void:
# The island's changes come from the server from now on.
props.regrow_enabled = false
WorldNet.join_world(self)
PlayerNet.join_world(self) # and our inventory and stats
if hud != null:
var bar := UITheme.label("Online: %s · the island is shared; your inventory isn't saved yet, and there are no animals yet" \
var bar := UITheme.label("Online: %s · no animals, hunting or fishing on servers yet" \
% Net.server_info.get("server_name", "server"), 14, UITheme.TEXT_DIM)
bar.position = Vector2(16, 40)
$HUD.add_child(bar)
@@ -654,6 +723,7 @@ func _go_online() -> void:
func _on_net_left(_reason: String) -> void:
WorldNet.leave_world()
PlayerNet.leave_world()
if is_inside_tree():
get_tree().paused = false
get_tree().auto_accept_quit = true
@@ -661,10 +731,10 @@ func _on_net_left(_reason: String) -> void:
## Leaves `count` of an item lying in the world at `xf` (an arrow stuck in a
## tree, a thrown spear, meat that didn't fit). Saved with the world.
## Online, the server puts it in the shared world (and returns null here).
## Online only the server drops things (our game never does: returns null).
func drop_item(id: String, count: int, xf: Transform3D) -> Node3D:
if net_sync != null:
WorldNet.drop(id, count, xf)
push_warning("[world] drop_item(%s) on a client: only the server drops items" % id)
return null
return _make_dropped(id, count, xf)
@@ -675,6 +745,8 @@ func _make_dropped(id: String, count: int, xf: Transform3D) -> Node3D:
pk.dropped = true
_dropped.add_child(pk)
pk.global_transform = xf
if server_mode and not _applying:
dropped_added.emit(pk)
return pk
## Casts a fishing float (Actions validated it). One at a time.
@@ -708,8 +780,12 @@ func remove_piece(piece: Node3D, refund_to: Node3D = null) -> int:
_pieces.remove_child(piece)
piece.queue_free()
if not was_build:
if server_mode:
pieces_removed.emit([piece], [])
return 0
var weak: Array = build.recompute()
if server_mode:
pieces_removed.emit([piece] + weak, weak)
for w in weak:
if refund_to != null:
Actions._give(refund_to, w.item_id, 1)