J3a: the journey on a server. A server per land (Lands: the hub starts land servers on demand over a private link), the cliff tunnel online, crossings with one-time passes, shared characters that remember their land, one clock from the hub; test_net_lands

This commit is contained in:
Paul Bailey
2026-11-06 23:14:11 +00:00
parent 6544862885
commit 85bbed249b
17 changed files with 1050 additions and 28 deletions

View File

@@ -51,6 +51,11 @@ func place(w: Node3D) -> bool:
## Whether the Valley's tunnel has been dug right down to here.
func open() -> bool:
var full: int = preload("res://scripts/world/tunnel.gd").STRETCHES
if not Lands.land.is_empty():
return Lands.tunnel_dug >= full # (a land server: the hub says how far it's dug)
if world.is_online():
return int(Net.world_info.get("tunnel_dug", 0)) >= full # (as the server said when we came in)
if not Saves.has_world():
return false
var s: Dictionary = Saves.load_state(Saves.current_id, to)

View File

@@ -88,7 +88,7 @@ func _ready() -> void:
elif saves != null and saves.has_world():
noise_seed = saves.world_seed()
# (worlds from before worldlets have none: they're the Valley)
worldlet_id = String(saves.read_meta(saves.current_id).get("worldlet", "valley"))
worldlet_id = String(saves.worldlet_of(saves.current_id)) # (a land server's own land, or the world's)
var why := use_worldlet(worldlet_id)
if not why.is_empty():
push_error("[terrain] %s: using the Valley" % why)

View File

@@ -143,8 +143,20 @@ func dig_refusal(stage: int) -> String:
return "It's loose: prop the last stretch first ([E] at its mouth, %d wood)" % PROP_WOOD
return ""
## The face called `uid` (what a player's game asks the server to hit).
func face_by_uid(uid: String) -> Node3D:
return _face if _face != null and is_instance_valid(_face) and String(_face.uid) == uid and not _face.depleted else null
## On a server, everyone playing (and the other lands) hears how the tunnel
## stands after each change.
func _tell_everyone() -> void:
if Net.is_server():
WorldNet.tunnel_state(to_data())
func face_hit(f: Node) -> void:
face_hp = f.hp
if not f.depleted:
_tell_everyone()
func face_broken(f: Node) -> void:
face_hp = FACE_HP
@@ -158,9 +170,14 @@ func face_broken(f: Node) -> void:
else:
Actions.notify.emit("The rock gives. Prop this stretch before you dig on")
_rebuild.call_deferred()
_tell_everyone()
## [E] at the mouth of stretch `k`: sets a timber prop there.
## [E] at the mouth of stretch `k`: sets a timber prop there. (Online, your
## game asks the server, which sets it and tells everyone.)
func set_prop(player: Node, k: int) -> bool:
if world.is_online():
PlayerNet.ask("tunnel_prop", [k])
return true
if k < 1 or k > dug or props[k]:
return false
if player.inventory.count("wood") < PROP_WOOD:
@@ -170,14 +187,22 @@ func set_prop(player: Node, k: int) -> bool:
props[k] = true
Actions.notify.emit("Propped. The next face is safe to dig")
_rebuild.call_deferred()
_tell_everyone()
return true
# --- going in and out ----------------------------------------------------------
## (Online, your game asks the server, which moves you: player_net.gd.)
func go_in(player: Node3D) -> void:
if world.is_online() and player == world.player:
PlayerNet.ask("tunnel_go", ["in"])
return
_move(player, top + Vector3(0, 0.1, 0), Vector3(1, 0, 0))
func go_out(player: Node3D) -> void:
if world.is_online() and player == world.player:
PlayerNet.ask("tunnel_go", ["out"])
return
_move(player, seam_out + Vector3(0, 0.2, 0), -Vector3(seam_at.x, 0.0, seam_at.z).normalized())
func _move(player: Node3D, at: Vector3, facing: Vector3) -> void:
@@ -195,6 +220,7 @@ func to_data() -> Dictionary:
return {"opened": opened, "dug": dug, "face_hp": face_hp, "props": p}
func load_data(d: Dictionary) -> void:
var was := [opened, dug, props.duplicate()]
opened = bool(d.get("opened", false))
dug = clampi(int(d.get("dug", 0)), 0, STRETCHES)
face_hp = float(d.get("face_hp", FACE_HP))
@@ -202,6 +228,11 @@ func load_data(d: Dictionary) -> void:
for k in d.get("props", []):
if int(k) >= 1 and int(k) <= STRETCHES:
props[int(k)] = true
# (Online, a blow on the face only changes how much is left of it: no
# need to rebuild the rock for that.)
if was == [opened, dug, props] and _face != null and is_instance_valid(_face):
_face.hp = clampf(face_hp, 0.5, _face.max_hp)
return
_rebuild()
# --- building it ---------------------------------------------------------------

View File

@@ -366,8 +366,9 @@ func _on_terrain_ready() -> void:
else:
pr.free()
# The journey's ways on (J1): the Valley's cliff tunnel and the Cove's sea
# cave. Single-player for now (J3 brings travel online).
if not server_mode and not menu_mode and not is_online():
# cave. On a server too (J3): the server decides the digging and the
# crossings, and every player's game has the same rock to see and walk.
if not menu_mode:
var tn := preload("res://scripts/world/tunnel.gd").new()
tn.name = "Tunnel"
add_child(tn)
@@ -698,6 +699,7 @@ func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, drop
"bear": bears.save_data() if bears != null else _saved_bear,
"barrow": barrow.save_data() if barrow != null else {},
"stones": stones.save_data() if stones != null else {},
"tunnel": tunnel.to_data() if tunnel != null else {},
}
## The live pieces (placed and not being removed).
@@ -986,7 +988,15 @@ func underground_at(p: Vector3) -> bool:
## at `arrive` ("sea_cave", "tunnel_bottom"). Each worldlet keeps its own
## state file (Saves.state_path), so what you built here waits for you.
func travel(to: String, arrive: String) -> void:
if traveling or not Saves.has_world() or is_online() or server_mode:
if server_mode:
return # (on a server, the crossing is the player's game asking: player_net.gd "cross")
if is_online():
# The server checks, saves your character and sends you on (J3).
if not traveling and Time.get_ticks_msec() > _cross_asked + 3000:
_cross_asked = Time.get_ticks_msec()
PlayerNet.ask("cross", ["tunnel" if arrive == "sea_cave" else "sea_cave"])
return
if traveling or not Saves.has_world():
return
traveling = true
var id := Saves.current_id
@@ -1011,6 +1021,25 @@ func travel(to: String, arrive: String) -> void:
get_tree().change_scene_to_file.call_deferred(scene_file_path if not scene_file_path.is_empty() else "res://scenes/world.tscn")
signal travelled(to: String) ## saved and on the way to another worldlet
var _cross_asked := -100000 ## (online: when we last asked the server to cross)
## Online (J3): the server's sending us on to another land's server.
func _on_net_travelling(to: String) -> void:
traveling = true
player.set_physics_process(false)
if net_sync != null:
net_sync.set_process(false) # (nothing more to tell the land we're leaving,
net_sync.set_physics_process(false) # nor the next one about this one)
if hud != null:
hud.toast("On to the %s..." % Terrain.worldlet_name(to))
hud.fade(func(): pass)
## Online: in at the other land: the world reloads as that land (its terrain
## comes from what that server says, Net.world_info).
func _on_net_arrived(_to: String) -> void:
travelled.emit(_to)
if get_tree().current_scene == self:
get_tree().change_scene_to_file.call_deferred(scene_file_path if not scene_file_path.is_empty() else "res://scenes/world.tscn")
## Back in a land you've been to before: its clock comes forward to `to`
## ({day, time}, the clock where you've come from), and everything in it
@@ -1029,20 +1058,30 @@ func catch_up(to: Dictionary) -> float:
return secs
## Just arrived from another worldlet: put you where you came in.
## Where the way in called `at` puts you ("sea_cave", "tunnel_bottom"), or
## INF if this land has no such way in. (A server places crossing players
## here too: player_net.gd.)
func arrival_point(at: String) -> Vector3:
match at:
"sea_cave":
if sea_cave != null:
return sea_cave.front
"tunnel_bottom":
if tunnel != null and tunnel.dug >= tunnel.STRETCHES:
return tunnel.bottom + Vector3(0.8, 0.0, 0.0)
return Vector3.INF
func _arrive(at: String) -> void:
if at.is_empty():
return
var pos := Vector3.INF
var pos := arrival_point(at)
var facing := Vector3.FORWARD
match at:
"sea_cave":
if sea_cave != null:
pos = sea_cave.front
facing = (sea_cave.front - sea_cave.global_position).normalized()
"tunnel_bottom":
if tunnel != null and tunnel.dug >= tunnel.STRETCHES:
pos = tunnel.bottom + Vector3(0.8, 0.0, 0.0)
facing = Vector3(1, 0, 0)
facing = Vector3(1, 0, 0)
if pos == Vector3.INF:
push_warning("[world] nowhere to arrive at '%s': the start instead" % at)
_respawn()
@@ -1228,6 +1267,8 @@ func _go_online() -> void:
load_online_map() # (your map of this server, from your own computer)
Net.left.connect(func(_why = ""): save_online_map(), CONNECT_ONE_SHOT)
Net.left.connect(_on_net_left)
Net.travelling.connect(_on_net_travelling)
Net.arrived.connect(_on_net_arrived)
get_tree().auto_accept_quit = false # leave the server cleanly on window close
# The island's changes come from the server from now on.
props.regrow_enabled = false