One clock for every land: going back to a land catches it up on the time away (like a night's sleep) instead of rewinding to when you left; J3 decided: a server per land with the Valley's as hub, sleep needs everyone in every land

This commit is contained in:
Paul Bailey
2026-11-06 21:57:37 +00:00
parent ad47cd53da
commit b938e045cb
4 changed files with 72 additions and 15 deletions

View File

@@ -363,8 +363,10 @@ tunnel goes **down**, inside the cliff, from the clifftop to the shore.
meat, clay jars) becomes what you carry between them. meat, clay jars) becomes what you carry between them.
- **Your bases stay** where you built them: each worldlet keeps its own - **Your bases stay** where you built them: each worldlet keeps its own
save file, so your Valley hut, chests and jars are waiting. save file, so your Valley hut, chests and jars are waiting.
- **Time:** a worldlet you're not in stands still (as a server does with - **Time:** there's one clock for every land (see the decisions below). A
nobody on), so a base you left doesn't starve or get raided. worldlet you're not in doesn't run, but catches up on the time when you
come back, as after a night's sleep; nothing roams it meanwhile, so a base
you left isn't raided.
### Building it (phases) ### Building it (phases)
| Phase | Delivers | Done when | | Phase | Delivers | Done when |
@@ -395,17 +397,45 @@ A server today runs one island. Options:
bites can make you sick, and bog, mist and the like. Others in the same bites can make you sick, and bog, mist and the like. Others in the same
spirit per biome (to design with each worldlet). spirit per biome (to design with each worldlet).
- **Multiplayer: spread out.** Each player goes where they like; the server - **Multiplayer: spread out.** Each player goes where they like; the server
runs every worldlet someone is in, and the rest stand still. Built in J3, runs every worldlet someone is in. Built in J3, after single-player travel
after single-player travel works. What that means for the code: works.
- one server process holds several worldlet scenes, each with its own - **A server per land (Paul, Nov 2026)**, rather than one process holding
clock, weather, creatures and save file; several worldlets (which would mean making every network module, the
- every player belongs to one worldlet at a time, and the server only saves and the actions handle several worlds at once):
sends them that worldlet's world, creatures, sounds and chat-free events - the Valley's server is the **hub**: the address everyone joins. It
(chat and the player list stay server-wide); starts a server for another land (the next UDP port: 24243 the Cove,
- a crossing moves a player from one worldlet to another (save their and so on) when someone first goes there, and stops it a couple of
place, load or wake the other worldlet, send them its world); minutes after its last player leaves;
- sleep: "everyone in bed" counts the players in that worldlet only; - each land server is today's server, with its own worldlet, save file,
- the Old Stag and other bosses scale with the players in their worldlet. weather and creatures; it keeps a private link to the hub on the host
machine;
- characters live in one shared folder. A crossing saves your character
first, marks you as on your way, and hands your game the land's port
and a one-time pass; your game reconnects there ("On to the Cove...").
Nothing can be lost or copied on the way: crash mid-way and you come
back where you were going. Joining the hub when your character's in
another land sends you straight on there;
- chat, the player list (with where each player is) and admin commands
go through the hub to every land;
- the tunnel belongs to the Valley's server; the Cove's asks the hub
whether it's dug through;
- from the internet, friends need those extra ports forwarded too (on a
home network, nothing to do). The Host page will list them;
- the Old Stag and other bosses scale with the players in their land.
- Phases: **J3a** the tunnel online, land servers on demand, the
crossing and the shared characters; **J3b** chat, the player list and
admin across lands; **J3c** the awkward cases (a land server crashing,
rejoining mid-crossing, the Host page's ports).
- **One clock for every land (Paul, Nov 2026).** The day and hour are the
same everywhere, so the lands never drift apart:
- sleeping until morning needs **everyone, in every land**, in bed (on a
server the hub counts them all, and every land skips to morning
together);
- a land nobody is in doesn't run, but when someone arrives it catches up
on the time since anyone was last there, as after a night's sleep:
kilns and smelters work, fires burn down, food goes off, trees and
berries grow back. (Built for single-player already: `world.catch_up()`.)
No creatures roam it meanwhile, so a base you left isn't raided.
- **Boats: one each, no passengers.** A boat belongs to whoever built it - **Boats: one each, no passengers.** A boat belongs to whoever built it
(like a bed): only the owner, and their friends under the server's locks (like a bed): only the owner, and their friends under the server's locks
rule, can sail it, and nobody else can dismantle it. It stays moored where rule, can sail it, and nobody else can dismantle it. It stays moored where
@@ -439,7 +469,7 @@ One worldlet, a dedicated server on the M5 Pro, never pausing: about
creatures, weather and pickups around them. So with *spread out*, the worst creatures, weather and pickups around them. So with *spread out*, the worst
case of 8 players in all 5 worldlets is about five of today's servers in case of 8 players in all 5 worldlets is about five of today's servers in
one process: roughly 1.4 GB and a little over half a core before players, one process: roughly 1.4 GB and a little over half a core before players,
and worldlets with nobody in them stand still and cost next to nothing. and worldlets with nobody in them aren't running and cost nothing (each starts its own server when someone goes there).
Comfortable on the home Mac; a small cloud server (2 GB) would be tight. Comfortable on the home Mac; a small cloud server (2 GB) would be tight.
### (Answered) questions ### (Answered) questions

View File

@@ -446,6 +446,7 @@ func _on_terrain_ready() -> void:
if not state.is_empty(): if not state.is_empty():
apply_save(state) apply_save(state)
print("[world] loaded '%s'" % Saves.current_id) print("[world] loaded '%s'" % Saves.current_id)
catch_up(state.get("catch_up_to", {}))
_arrive(Saves.take_arrival(Saves.current_id)) _arrive(Saves.take_arrival(Saves.current_id))
player.set_physics_process(true) player.set_physics_process(true)
set_physics_process(true) set_physics_process(true)
@@ -995,6 +996,10 @@ func travel(to: String, arrive: String) -> void:
var there: Dictionary = Saves.load_state(id, to) var there: Dictionary = Saves.load_state(id, to)
if there.is_empty(): if there.is_empty():
there = {"time": here.time} # (a new place: the same day and hour) there = {"time": here.time} # (a new place: the same day and hour)
else:
# One clock for every land: the one you're going to catches up on the
# time you were away when it loads (catch_up()).
there["catch_up_to"] = here.time
there["player"] = here.player there["player"] = here.player
Saves.save_state(id, there, 0.0, to) Saves.save_state(id, there, 0.0, to)
Saves.set_worldlet(id, to, arrive) Saves.set_worldlet(id, to, arrive)
@@ -1007,6 +1012,22 @@ func travel(to: String, arrive: String) -> void:
signal travelled(to: String) ## saved and on the way to another worldlet signal travelled(to: String) ## saved and on the way to another worldlet
## 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
## catches up on the time between, as after a night's sleep (kilns and
## smelters work, fires burn, food goes off, trees and berries grow back).
## Returns the seconds caught up.
func catch_up(to: Dictionary) -> float:
if to.is_empty():
return 0.0
var secs: float = ((int(to.get("day", day_night.day)) - day_night.day) \
+ (float(to.get("time", day_night.time)) - day_night.time)) * day_night.day_length
if secs <= 0.0:
return 0.0
skip_time(secs)
print("[world] %s caught up on %.0f s while you were away" % [worldlet_id(), secs])
return secs
## Just arrived from another worldlet: put you where you came in. ## Just arrived from another worldlet: put you where you came in.
func _arrive(at: String) -> void: func _arrive(at: String) -> void:
if at.is_empty(): if at.is_empty():

View File

@@ -238,6 +238,10 @@ func _body() -> void:
print("back up home") print("back up home")
check(w.sea_cave.open(), "the sea cave leads back up (the tunnel's dug)") check(w.sea_cave.open(), "the sea cave leads back up (the tunnel's dug)")
# A day and a half in the Cove before going home.
w.day_night.advance(w.day_night.day_length * 1.5)
var cove_day: int = w.day_night.day
var cove_time: float = w.day_night.time
_went = "" _went = ""
w.sea_cave.interact(player) w.sea_cave.interact(player)
check(_went == "valley", "into the cave: back to the Valley") check(_went == "valley", "into the cave: back to the Valley")
@@ -247,6 +251,8 @@ func _body() -> void:
player = w.player player = w.player
check(w.worldlet_id() == "valley" and w.underground_at(player.global_position) and player.global_position.distance_to(t.bottom) < 2.0, check(w.worldlet_id() == "valley" and w.underground_at(player.global_position) and player.global_position.distance_to(t.bottom) < 2.0,
"arriving at the bottom of the tunnel") "arriving at the bottom of the tunnel")
check(w.day_night.day == cove_day and absf(w.day_night.time - cove_time) < 0.01,
"one clock for every land: home is on the Cove's day and hour (day %d), not the one you left it" % w.day_night.day)
t.go_out(player) t.go_out(player)
await physics_frame await physics_frame
check(not w.underground_at(player.global_position) and player.global_position.distance_to(t.seam_out) < 1.0, "climb out at the top, on the clifftop") check(not w.underground_at(player.global_position) and player.global_position.distance_to(t.seam_out) < 1.0, "climb out at the top, on the clifftop")

View File

@@ -350,7 +350,7 @@ Low hills run down to a sheltered sandy bay, with mountains behind, a small pond
- **The pier** runs out over the bay. Fish off its end for **mackerel**, the usual catch. **Cod** are bigger but only bite where the water's at least 2.2 m deep, so cast well out. Both bite best at dawn and dusk, and grill at a campfire, salt or smoke like lake fish. - **The pier** runs out over the bay. Fish off its end for **mackerel**, the usual catch. **Cod** are bigger but only bite where the water's at least 2.2 m deep, so cast well out. Both bite best at dawn and dusk, and grill at a campfire, salt or smoke like lake fish.
- **The sea cave** at the end of the beach is the way back: `E` goes into it and up the same tunnel, arriving at the bottom. - **The sea cave** at the end of the beach is the way back: `E` goes into it and up the same tunnel, arriving at the bottom.
Each land keeps its own state. What you build in the Valley waits for you while you're away, and time stands still in a land you're not in. Each land keeps its own state, and there's one clock for them all. What you build in the Valley waits for you while you're away, and when you come back the time you were gone catches up, as after a night's sleep: kilns and smelters will have done their work, fires burned down and food gone off. Nothing roams a land while you're away, so your base won't have been raided.
## Multiplayer {#multiplayer} ## Multiplayer {#multiplayer}