M2.3: the shared world (trees, pickups, pieces, chests, fires, doors, graves, clock on the server)
This commit is contained in:
@@ -32,7 +32,7 @@ signal clock(day: int, time: float) ## the server's time of day
|
||||
const Identity = preload("res://scripts/net/identity.gd")
|
||||
const ServerConfig = preload("res://scripts/net/server_config.gd")
|
||||
|
||||
const PROTOCOL := 2
|
||||
const PROTOCOL := 3 ## 3: the shared world (M2.3)
|
||||
const DEFAULT_PORT := 24242
|
||||
const HANDSHAKE_TIMEOUT := 10.0
|
||||
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join
|
||||
@@ -62,12 +62,11 @@ var debug_latency_ms := 0.0
|
||||
var debug_jitter_ms := 0.0
|
||||
|
||||
# Server state.
|
||||
var _terrain: Node3D ## the island, regenerated from the seed, for movement checks
|
||||
var _world: Node3D ## the shared island (world.gd in server mode)
|
||||
var _terrain: Node3D ## its terrain, for movement checks
|
||||
var _spawn := Vector3.ZERO
|
||||
var _states: Dictionary = {} ## peer -> {"pos", "yaw", "pitch", "anim", "held", "t", "corr_t"}
|
||||
var server_time := 0.0 ## seconds since the server started
|
||||
var _clock_day := 1
|
||||
var _clock_time := 8.0 / 24.0
|
||||
var _send_left := 0.0
|
||||
var _clock_left := 0.0
|
||||
# Client delay queues (debug latency): [release_ms, callable].
|
||||
@@ -180,20 +179,20 @@ func start_server(config_path: String, overrides: Dictionary = {}) -> String:
|
||||
if overrides.has(flag):
|
||||
var v: Variant = overrides[flag]
|
||||
config.overrides[map[flag]] = int(v) if map[flag] in ["port", "max_players", "seed"] else String(v)
|
||||
# The island, from the seed: the same terrain every client generates.
|
||||
_terrain = load("res://scripts/world/terrain.gd").new()
|
||||
_terrain.name = "ServerTerrain"
|
||||
_terrain.noise_seed = int(config.v("seed"))
|
||||
add_child(_terrain)
|
||||
# The shared island (world_net.gd): the same terrain and scenery every
|
||||
# client generates from the seed, plus everything players changed.
|
||||
_world = WorldNet.open_server_world(config)
|
||||
_terrain = _world.terrain
|
||||
_spawn = _terrain.find_spawn(Vector2.ZERO)
|
||||
server_time = 0.0
|
||||
_clock_day = 1
|
||||
_clock_time = 8.0 / 24.0
|
||||
_peer = ENetMultiplayerPeer.new()
|
||||
var port := int(config.v("port"))
|
||||
# A few spare slots so a full server can still say "server is full".
|
||||
var err := _peer.create_server(port, int(config.v("max_players")) + 4)
|
||||
if err != OK:
|
||||
WorldNet.close_server_world()
|
||||
_world = null
|
||||
_terrain = null
|
||||
return "Couldn't open UDP port %d (error %d). Is another server already running on it?" % [port, err]
|
||||
multiplayer.multiplayer_peer = _peer
|
||||
multiplayer.server_relay = false # clients only ever talk to the server
|
||||
@@ -218,9 +217,10 @@ func stop() -> void:
|
||||
_out_q.clear()
|
||||
_in_q.clear()
|
||||
local_peer = 0
|
||||
if _terrain != null:
|
||||
_terrain.queue_free()
|
||||
_terrain = null
|
||||
if _world != null:
|
||||
WorldNet.close_server_world() # saves it first
|
||||
_world = null
|
||||
_terrain = null
|
||||
if state != "offline":
|
||||
_set_state("offline")
|
||||
|
||||
@@ -238,6 +238,7 @@ func _on_peer_disconnected(id: int) -> void:
|
||||
players.erase(id)
|
||||
print("[server] %s left (%d online)" % [p.name, players.size()])
|
||||
_s2c_player_left.rpc(id)
|
||||
WorldNet.peer_left(id)
|
||||
players_changed.emit()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -260,20 +261,19 @@ func _process(delta: float) -> void:
|
||||
_run_queue(_out_q)
|
||||
_run_queue(_in_q)
|
||||
|
||||
## The world clock runs while anyone's on (or always, if the server's set
|
||||
## not to pause when empty).
|
||||
## The world's clock (it runs while anyone's on, or always if the server's
|
||||
## set not to pause when empty: world_net.gd pauses the world). Players get
|
||||
## it every few seconds.
|
||||
func _tick_clock(delta: float) -> void:
|
||||
if players.is_empty() and bool(config.v("pause_when_empty")):
|
||||
return
|
||||
_clock_time += delta / (float(config.v("day_length_minutes")) * 60.0)
|
||||
while _clock_time >= 1.0:
|
||||
_clock_time -= 1.0
|
||||
_clock_day += 1
|
||||
_clock_left -= delta
|
||||
if _clock_left <= 0.0:
|
||||
if _clock_left <= 0.0 and not players.is_empty():
|
||||
_clock_left = CLOCK_INTERVAL
|
||||
for peer in players:
|
||||
_s2c_clock.rpc_id(peer, _clock_day, _clock_time)
|
||||
_s2c_clock.rpc_id(peer, _world.day_night.day, _world.day_night.time)
|
||||
|
||||
## Where a player is, as far as the server knows.
|
||||
func player_pos(peer: int) -> Vector3:
|
||||
return _states[peer].pos if _states.has(peer) else Vector3.INF
|
||||
|
||||
func _broadcast_snapshot() -> void:
|
||||
if _states.is_empty():
|
||||
@@ -343,8 +343,36 @@ func move_problem(from: Vector3, to: Vector3, dt: float, teleported: bool) -> St
|
||||
return "outside the island"
|
||||
if to.y < _terrain.sample_height(to.x, to.z) - 1.5:
|
||||
return "underground"
|
||||
if not teleported and _through_wall(from, to):
|
||||
return "through a wall"
|
||||
return ""
|
||||
|
||||
## Whether the straight line between two positions crosses a wall, a closed
|
||||
## door or a gable (at waist height). A genuine crossing hits the wall from
|
||||
## both sides; someone a wall was built on top of can still step out of it.
|
||||
func _through_wall(from: Vector3, to: Vector3) -> bool:
|
||||
if _world == null or from.distance_to(to) < 0.05:
|
||||
return false
|
||||
var space: PhysicsDirectSpaceState3D = _world.get_world_3d().direct_space_state
|
||||
var up := Vector3(0, 0.9, 0)
|
||||
return _wall_hit(space, from + up, to + up) and _wall_hit(space, to + up, from + up)
|
||||
|
||||
func _wall_hit(space: PhysicsDirectSpaceState3D, a: Vector3, b: Vector3) -> bool:
|
||||
var ex: Array[RID] = []
|
||||
for _i in range(4):
|
||||
var rq := PhysicsRayQueryParameters3D.create(a, b, 4, ex) # 4: building pieces
|
||||
var hit := space.intersect_ray(rq)
|
||||
if hit.is_empty():
|
||||
return false
|
||||
var c: Object = hit.collider
|
||||
var piece: Node = c as Node
|
||||
while piece != null and piece.get("kind") == null:
|
||||
piece = piece.get_parent()
|
||||
if piece != null and String(piece.kind) in ["wall", "door", "gable"]:
|
||||
return true
|
||||
ex.append(hit.rid) # a floor or roof edge: look past it
|
||||
return false
|
||||
|
||||
func _reject(peer: int, reason: String) -> void:
|
||||
_pending.erase(peer)
|
||||
print("[server] rejected peer %d: %s" % [peer, reason])
|
||||
@@ -400,8 +428,9 @@ func _c2s_proof(sig: PackedByteArray) -> void:
|
||||
"stamp": server_time, "off": INF}
|
||||
print("[server] %s joined (id %s%s, %d/%d online)" % [name, id, ", admin" if p.admin else "",
|
||||
players.size(), int(config.v("max_players"))])
|
||||
var world := {"seed": int(config.v("seed")), "worldlet": String(config.v("worldlet")), "spawn": start,
|
||||
"island_spawn": _spawn, "day": _clock_day, "time": _clock_time, "server_time": server_time}
|
||||
# (The world's own seed: an existing world keeps the seed it was made with.)
|
||||
var world := {"seed": int(_terrain.noise_seed), "worldlet": String(config.v("worldlet")), "spawn": start,
|
||||
"island_spawn": _spawn, "day": _world.day_night.day, "time": _world.day_night.time, "server_time": server_time}
|
||||
_s2c_welcome.rpc_id(peer, {"you": peer, "server": config.public_info(), "players": players, "world": world})
|
||||
for other in players:
|
||||
if other != peer:
|
||||
|
||||
626
scripts/net/world_net.gd
Normal file
626
scripts/net/world_net.gd
Normal file
@@ -0,0 +1,626 @@
|
||||
extends Node
|
||||
## Autoload "WorldNet": the shared world (M2.3, docs/MULTIPLAYER_PLAN.md §5).
|
||||
##
|
||||
## A dedicated server runs the real world scene headless, with no player of its
|
||||
## own (world.gd `server_mode`). It owns everything in the island that changes:
|
||||
## trees and rocks (damage, felling, regrowth), pickups, berry bushes, dropped
|
||||
## items, building pieces, chests and graves, fires, doors and the clock. The
|
||||
## weather follows from the seed and the clock, so it needs no syncing.
|
||||
##
|
||||
## Clients ask; the server checks, applies and tells everyone:
|
||||
## client -> server _c2s_hit uid server -> all _s2c_node uid hp
|
||||
## client -> server _c2s_place item server -> all _s2c_piece_added {...}
|
||||
## ...and so on for each kind of change (the _c2s_* / _s2c_* pairs below).
|
||||
## When a change earns the asker something (wood from a tree, a pickup, a
|
||||
## dismantled wall back), the server sends it to them with _s2c_give.
|
||||
##
|
||||
## Inventories, stats and crafting stay on each player's own game until M2.4,
|
||||
## so the server trusts what a client says it puts into the world (a placed
|
||||
## wall, a chest deposit) but decides everything it takes out.
|
||||
##
|
||||
## On joining, a client's world asks for a snapshot (_c2s_world) once its
|
||||
## scenery is up; from then on it gets every change, in order.
|
||||
|
||||
const REACH := 3.2 + 2.5 ## Actions._in_reach, plus slack for latency
|
||||
const SLACK := 2.0
|
||||
const MAX_HIT := 12.0 ## no tool hits harder than this
|
||||
const FIRE_SYNC := 10.0 ## seconds between fire fuel updates
|
||||
const SAVE_AFTER := 10.0 ## seconds after a change before saving
|
||||
const FUELS := ["wood", "stick", "fiber"]
|
||||
|
||||
var world: Node3D ## server: its world; client: ours while online
|
||||
var pieces: Dictionary = {} ## net id -> piece (both sides)
|
||||
var dropped: Dictionary = {} ## net id -> dropped pickup (both sides)
|
||||
var _next_nid := 1
|
||||
var _ready_peers: Dictionary = {} ## server: peers that have their snapshot
|
||||
var _fire_left := FIRE_SYNC
|
||||
var _blocker_left := 0.0
|
||||
var _save_left := INF ## counting down to a save after a change
|
||||
var _chest_cb := Callable() ## client: waiting for a chest click's result
|
||||
var chest_busy := false
|
||||
|
||||
## Counters the tests read.
|
||||
var received := 0 ## client: world events applied
|
||||
|
||||
func is_server() -> bool:
|
||||
return Net.is_server() and world != null
|
||||
|
||||
func is_client() -> bool:
|
||||
return Net.state == "connected" and world != null and is_instance_valid(world)
|
||||
|
||||
# --- server: the world -----------------------------------------------------------
|
||||
|
||||
## Loads (or creates) the server's world from its config and adds it to the
|
||||
## tree. Saves live next to server.json, in worlds/<world>/.
|
||||
func open_server_world(config: RefCounted) -> Node3D:
|
||||
Saves.root_dir = config.path.get_base_dir().path_join("worlds")
|
||||
var id := String(config.v("world")).validate_filename()
|
||||
if id.is_empty():
|
||||
id = "server-world"
|
||||
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")))
|
||||
var w: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
w.server_mode = true
|
||||
w.day_length = float(config.v("day_length_minutes")) * 60.0
|
||||
w.autosave_seconds = maxf(float(config.v("autosave_minutes")), 0.5) * 60.0
|
||||
get_tree().root.add_child(w)
|
||||
world = w
|
||||
pieces.clear()
|
||||
dropped.clear()
|
||||
_ready_peers.clear()
|
||||
_next_nid = 1
|
||||
for p in w.all_pieces():
|
||||
if p.has_meta("nid"):
|
||||
_next_nid = maxi(_next_nid, int(p.get_meta("nid")) + 1)
|
||||
for p in w.all_pieces():
|
||||
if not p.has_meta("nid"):
|
||||
p.set_meta("nid", _new_nid())
|
||||
pieces[int(p.get_meta("nid"))] = p
|
||||
for d in w.all_dropped():
|
||||
if not d.has_meta("nid"):
|
||||
d.set_meta("nid", _new_nid())
|
||||
dropped[int(d.get_meta("nid"))] = d
|
||||
_next_nid = maxi(_next_nid, _max_nid() + 1)
|
||||
w.props.regrown.connect(_on_regrown)
|
||||
var args: Dictionary = Net.parse_args(OS.get_cmdline_user_args())
|
||||
if args.has("debug-regrow"):
|
||||
w.props.regrow_scale = float(args["debug-regrow"]) # (tests: things grow back in seconds)
|
||||
print("[server] world '%s' (seed %d): %d pieces, %d dropped items. Saves: %s" % [Saves.current_id,
|
||||
w.terrain.noise_seed, pieces.size(), dropped.size(), ProjectSettings.globalize_path(Saves.root_dir)])
|
||||
return w
|
||||
|
||||
func close_server_world() -> void:
|
||||
if world == null:
|
||||
return
|
||||
if Net.is_server() and is_instance_valid(world):
|
||||
save()
|
||||
if is_instance_valid(world):
|
||||
world.queue_free()
|
||||
world = null
|
||||
pieces.clear()
|
||||
dropped.clear()
|
||||
_ready_peers.clear()
|
||||
|
||||
func save() -> void:
|
||||
_save_left = INF
|
||||
if world != null and is_instance_valid(world) and world.server_mode:
|
||||
world.save_now()
|
||||
|
||||
func _new_nid() -> int:
|
||||
_next_nid += 1
|
||||
return _next_nid - 1
|
||||
|
||||
func _max_nid() -> int:
|
||||
var m := 0
|
||||
for k in pieces:
|
||||
m = maxi(m, k)
|
||||
for k in dropped:
|
||||
m = maxi(m, k)
|
||||
return m
|
||||
|
||||
## A player left: they no longer get events. Save while it's fresh.
|
||||
func peer_left(peer: int) -> void:
|
||||
_ready_peers.erase(peer)
|
||||
save()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
# Pause the island while nobody's on (fires, regrowth, the clock).
|
||||
var paused: bool = Net.players.is_empty() and bool(Net.config.v("pause_when_empty"))
|
||||
world.process_mode = Node.PROCESS_MODE_DISABLED if paused else Node.PROCESS_MODE_INHERIT
|
||||
if paused:
|
||||
return
|
||||
_blocker_left -= delta
|
||||
if _blocker_left <= 0.0:
|
||||
_blocker_left = 1.0
|
||||
var at: Array = []
|
||||
for peer in Net.players:
|
||||
at.append(Net.player_pos(peer))
|
||||
world.props.blockers = at # nothing regrows on top of a player
|
||||
_save_left -= delta
|
||||
if _save_left <= 0.0:
|
||||
_save_left = INF
|
||||
save()
|
||||
_fire_left -= delta
|
||||
if _fire_left <= 0.0:
|
||||
_fire_left = FIRE_SYNC
|
||||
var fires: Array = []
|
||||
for nid in pieces:
|
||||
var p: Node3D = pieces[nid]
|
||||
if is_instance_valid(p) and p.get("fuel") != null:
|
||||
fires.append([nid, p.fuel])
|
||||
if not fires.is_empty():
|
||||
_all("_s2c_fires", [fires])
|
||||
|
||||
## Sends to every player who has their snapshot. Any change to the island
|
||||
## also means saving soon: Ctrl+C kills Godot without a chance to save, so
|
||||
## this keeps the loss to a few seconds.
|
||||
func _all(method: String, args: Array) -> void:
|
||||
if method != "_s2c_fires" and _save_left == INF:
|
||||
_save_left = SAVE_AFTER
|
||||
for peer in _ready_peers:
|
||||
callv("rpc_id", [peer, method] + args)
|
||||
|
||||
func _give_to(peer: int, items: Dictionary) -> void:
|
||||
var clean := {}
|
||||
for id in items:
|
||||
if int(items[id]) > 0:
|
||||
clean[id] = int(items[id])
|
||||
if not clean.is_empty():
|
||||
_s2c_give.rpc_id(peer, clean)
|
||||
|
||||
func _tell(peer: int, text: String) -> void:
|
||||
_s2c_notify.rpc_id(peer, text)
|
||||
|
||||
## The asker, if they're a player in the world (else 0).
|
||||
func _asker() -> int:
|
||||
var peer := multiplayer.get_remote_sender_id()
|
||||
if not is_server() or not _ready_peers.has(peer):
|
||||
return 0
|
||||
return peer
|
||||
|
||||
func _near(peer: int, at: Vector3, r: float) -> bool:
|
||||
return Net.player_pos(peer).distance_to(at) <= r + SLACK
|
||||
|
||||
func _piece(nid: int) -> Node3D:
|
||||
var p: Node3D = pieces.get(nid)
|
||||
if p == null or not is_instance_valid(p) or p.is_queued_for_deletion():
|
||||
pieces.erase(nid)
|
||||
return null
|
||||
return p
|
||||
|
||||
func _entry(p: Node3D) -> Dictionary:
|
||||
return world.piece_entry(p)
|
||||
|
||||
static func _clean_stack(s: Variant) -> Variant:
|
||||
if not s is Dictionary:
|
||||
return null
|
||||
var id := String(s.get("id", ""))
|
||||
var n := int(s.get("count", 0))
|
||||
if ItemDB.get_item(id).is_empty() or n <= 0:
|
||||
return null
|
||||
return {"id": id, "count": mini(n, ItemDB.stack_size(id))}
|
||||
|
||||
static func _empty(inv: Inventory) -> bool:
|
||||
for s in inv.slots:
|
||||
if s != null:
|
||||
return false
|
||||
return true
|
||||
|
||||
# --- client -> server -------------------------------------------------------------
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_world() -> void:
|
||||
var peer := multiplayer.get_remote_sender_id()
|
||||
if not is_server() or not Net.players.has(peer):
|
||||
return
|
||||
_ready_peers[peer] = true
|
||||
_s2c_world.rpc_id(peer, world.collect_shared())
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_hit(uid: String, damage: float, point: Vector3) -> void:
|
||||
var peer := _asker()
|
||||
var n: Node3D = world.props.resource_nodes.get(uid) if peer != 0 else null
|
||||
if n == null or not is_instance_valid(n) or n.depleted:
|
||||
return
|
||||
if not _near(peer, point, 3.2 + 1.0) or point.distance_to(n.global_position) > 6.0:
|
||||
return
|
||||
var from: Vector3 = Net.player_pos(peer)
|
||||
var drops: Dictionary = n.hit(clampf(damage, 0.0, MAX_HIT), point, from)
|
||||
_give_to(peer, drops)
|
||||
_all("_s2c_node", [uid, n.hp, point, from])
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_take(key: String) -> void:
|
||||
var peer := _asker()
|
||||
if peer == 0:
|
||||
return
|
||||
var pk: Node3D = _find_pickup(key)
|
||||
if pk == null or pk.taken or not _near(peer, pk.global_position, REACH):
|
||||
return
|
||||
var got := {pk.item_id: pk.amount}
|
||||
if key.begins_with("#"):
|
||||
dropped.erase(int(key.substr(1)))
|
||||
pk.collected()
|
||||
_give_to(peer, got)
|
||||
_all("_s2c_taken", [key])
|
||||
|
||||
func _find_pickup(key: String) -> Node3D:
|
||||
var pk: Node3D
|
||||
if key.begins_with("#"):
|
||||
pk = dropped.get(int(key.substr(1)))
|
||||
else:
|
||||
pk = world.props.pickups.get(key) if world != null else null
|
||||
return pk if pk != null and is_instance_valid(pk) else null
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_forage(uid: String) -> void:
|
||||
var peer := _asker()
|
||||
var bush: Node3D = world.props.forage.get(uid) if peer != 0 else null
|
||||
if bush == null or not bush.ripe() or not _near(peer, bush.global_position, REACH):
|
||||
return
|
||||
_give_to(peer, bush.harvest())
|
||||
_all("_s2c_forage", [uid, bush.regrow_left])
|
||||
|
||||
## Places `item` at `xf`. The client has already taken it out of its
|
||||
## inventory: if it can't go here, it's given back. A grave carries what the
|
||||
## player was holding when they died.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_place(item: String, xf: Transform3D, contents: Array) -> void:
|
||||
var peer := _asker()
|
||||
if peer == 0:
|
||||
return
|
||||
var def: Dictionary = ItemDB.get_item(item)
|
||||
if def.is_empty() or not def.has("place"):
|
||||
return
|
||||
# (A grave can be further off: drowning puts it on the nearest shore.)
|
||||
var ok := xf.origin.is_finite() and _near(peer, xf.origin, 40.0 if item == "grave" else 8.0)
|
||||
if item == "grave":
|
||||
ok = ok and not contents.is_empty()
|
||||
else:
|
||||
ok = ok and world.placement_valid(def, xf)
|
||||
if not ok:
|
||||
if item != "grave":
|
||||
_give_to(peer, {item: 1})
|
||||
_tell(peer, "Can't place that here")
|
||||
return
|
||||
var p: Node3D = world.spawn_piece(def, xf.orthonormalized())
|
||||
if item == "grave" and p.get("inventory") != null:
|
||||
var clean: Array = []
|
||||
for s in contents.slice(0, p.inventory.size()):
|
||||
clean.append(_clean_stack(s))
|
||||
p.inventory.load_data(clean)
|
||||
p.set_meta("nid", _new_nid())
|
||||
pieces[int(p.get_meta("nid"))] = p
|
||||
_all("_s2c_piece_added", [_entry(p)])
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_dismantle(nid: int) -> void:
|
||||
var peer := _asker()
|
||||
var p: Node3D = _piece(nid) if peer != 0 else null
|
||||
if p == null or not _near(peer, p.global_position, 8.0) or p.item_id == "grave":
|
||||
return
|
||||
var why: String = p.dismantle_block_reason()
|
||||
if not why.is_empty():
|
||||
_tell(peer, why)
|
||||
return
|
||||
# Whatever loses its support collapses: those come back to you too.
|
||||
var items := {}
|
||||
for k in pieces:
|
||||
if _piece(k) != null:
|
||||
items[k] = pieces[k].item_id
|
||||
world.remove_piece(p, null)
|
||||
var gone: Array = []
|
||||
var fell: Array = []
|
||||
var refund := {}
|
||||
for k in items:
|
||||
if _piece(k) == null:
|
||||
gone.append(k)
|
||||
refund[items[k]] = refund.get(items[k], 0) + 1
|
||||
if k != nid:
|
||||
fell.append(k)
|
||||
_all("_s2c_pieces_removed", [gone, fell])
|
||||
_give_to(peer, refund)
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_door(nid: int) -> void:
|
||||
var peer := _asker()
|
||||
var p: Node3D = _piece(nid) if peer != 0 else null
|
||||
if p == null or p.get("open") == null or not _near(peer, p.global_position, REACH + 1.0):
|
||||
return
|
||||
p.set_open(not p.open)
|
||||
_all("_s2c_piece_state", [nid, p.save_extra()])
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_fuel(nid: int, item: String) -> void:
|
||||
var peer := _asker()
|
||||
if peer == 0 or not item in FUELS:
|
||||
return
|
||||
var p := _piece(nid)
|
||||
if p == null or p.get("fuel") == null or not _near(peer, p.global_position, REACH) \
|
||||
or p.fuel >= p.MAX_FUEL - 1.0:
|
||||
_give_to(peer, {item: 1})
|
||||
return
|
||||
p.add_fuel(float(ItemDB.get_item(item).get("fuel", 10)))
|
||||
_all("_s2c_piece_state", [nid, p.save_extra()])
|
||||
|
||||
## A click on a chest slot while holding `cursor`: the same as clicking in
|
||||
## your own inventory (pick up, put down, swap). The new cursor comes back.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_chest_click(nid: int, slot: int, cursor: Variant, right: bool) -> void:
|
||||
var peer := _asker()
|
||||
if peer == 0:
|
||||
return
|
||||
var c: Variant = _clean_stack(cursor)
|
||||
var p := _piece(nid)
|
||||
if p == null or p.get("inventory") == null or slot < 0 or slot >= p.inventory.size() \
|
||||
or not _near(peer, p.global_position, REACH + 1.0):
|
||||
_s2c_cursor.rpc_id(peer, c) # nothing happened: keep what you were holding
|
||||
return
|
||||
var res: Variant = p.inventory.click(slot, c, right)
|
||||
_s2c_cursor.rpc_id(peer, res)
|
||||
_chest_changed(nid, p)
|
||||
|
||||
## Shift-click from your inventory: as much of `stack` as fits goes in, the
|
||||
## rest comes back.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_chest_put(nid: int, stack: Variant) -> void:
|
||||
var peer := _asker()
|
||||
var s: Variant = _clean_stack(stack)
|
||||
if peer == 0 or s == null:
|
||||
return
|
||||
var p := _piece(nid)
|
||||
if p == null or p.get("inventory") == null or not _near(peer, p.global_position, REACH + 1.0):
|
||||
_give_to(peer, {s.id: s.count})
|
||||
return
|
||||
var left: int = p.inventory.add(s.id, s.count)
|
||||
_give_to(peer, {s.id: left})
|
||||
_chest_changed(nid, p)
|
||||
|
||||
## Shift-click on a chest slot: up to `room` of it comes to you.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_chest_take(nid: int, slot: int, room: int) -> void:
|
||||
var peer := _asker()
|
||||
var p: Node3D = _piece(nid) if peer != 0 else null
|
||||
if p == null or p.get("inventory") == null or slot < 0 or slot >= p.inventory.size() or room <= 0 \
|
||||
or not _near(peer, p.global_position, REACH + 1.0):
|
||||
return
|
||||
var s: Variant = p.inventory.get_slot(slot)
|
||||
if s == null:
|
||||
return
|
||||
var id: String = s.id
|
||||
var n: int = p.inventory.take_from(slot, room)
|
||||
_give_to(peer, {id: n})
|
||||
_chest_changed(nid, p)
|
||||
|
||||
func _chest_changed(nid: int, p: Node3D) -> void:
|
||||
_all("_s2c_chest", [nid, p.inventory.to_data()])
|
||||
# An emptied grave crumbles.
|
||||
if p.item_id == "grave" and _empty(p.inventory):
|
||||
world.remove_piece(p, null)
|
||||
pieces.erase(nid)
|
||||
_all("_s2c_pieces_removed", [[nid], []])
|
||||
|
||||
## Something the player put down in the world: an arrow that landed, a fish
|
||||
## that didn't fit. Anyone can pick it up.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_drop(item: String, count: int, xf: Transform3D) -> void:
|
||||
var peer := _asker()
|
||||
if peer == 0 or ItemDB.get_item(item).is_empty() or count <= 0 or count > 999 \
|
||||
or not xf.origin.is_finite() or not _near(peer, xf.origin, 60.0):
|
||||
return # (60 m: an arrow can land a long way off)
|
||||
var d: Node3D = world.drop_item(item, count, xf.orthonormalized())
|
||||
d.set_meta("nid", _new_nid())
|
||||
dropped[int(d.get_meta("nid"))] = d
|
||||
_all("_s2c_dropped", [world.dropped_entry(d)])
|
||||
|
||||
func _on_regrown(uid: String) -> void:
|
||||
_all("_s2c_regrown", [uid])
|
||||
|
||||
# --- client: asking ----------------------------------------------------------------
|
||||
|
||||
## Our world is up: ask for the shared state.
|
||||
func join_world(w: Node3D) -> void:
|
||||
world = w
|
||||
pieces.clear()
|
||||
dropped.clear()
|
||||
chest_busy = false
|
||||
_chest_cb = Callable()
|
||||
received = 0
|
||||
_c2s_world.rpc_id(1)
|
||||
|
||||
func leave_world() -> void:
|
||||
if not Net.is_server():
|
||||
world = null
|
||||
pieces.clear()
|
||||
dropped.clear()
|
||||
|
||||
func hit(node: Node3D, damage: float, point: Vector3) -> void:
|
||||
_c2s_hit.rpc_id(1, String(node.uid), damage, point)
|
||||
|
||||
## The key the server knows a pickup by: its scenery id, or "#<net id>".
|
||||
func pickup_key(pk: Node3D) -> String:
|
||||
return "#%d" % int(pk.get_meta("nid")) if pk.has_meta("nid") else String(pk.uid)
|
||||
|
||||
func take(pk: Node3D) -> void:
|
||||
_c2s_take.rpc_id(1, pickup_key(pk))
|
||||
|
||||
func forage(bush: Node3D) -> void:
|
||||
_c2s_forage.rpc_id(1, String(bush.uid))
|
||||
|
||||
func place(item: String, xf: Transform3D, contents: Array = []) -> void:
|
||||
_c2s_place.rpc_id(1, item, xf, contents)
|
||||
|
||||
static func nid(p: Node3D) -> int:
|
||||
return int(p.get_meta("nid", 0))
|
||||
|
||||
func dismantle(p: Node3D) -> void:
|
||||
_c2s_dismantle.rpc_id(1, nid(p))
|
||||
|
||||
func door(p: Node3D) -> void:
|
||||
_c2s_door.rpc_id(1, nid(p))
|
||||
|
||||
func fuel(p: Node3D, item: String) -> void:
|
||||
_c2s_fuel.rpc_id(1, nid(p), item)
|
||||
|
||||
## `done(cursor)` gets what you're holding afterwards.
|
||||
func chest_click(p: Node3D, slot: int, cursor: Variant, right: bool, done: Callable) -> void:
|
||||
chest_busy = true
|
||||
_chest_cb = done
|
||||
_c2s_chest_click.rpc_id(1, nid(p), slot, cursor, right)
|
||||
|
||||
func chest_put(p: Node3D, stack: Dictionary) -> void:
|
||||
_c2s_chest_put.rpc_id(1, nid(p), stack)
|
||||
|
||||
func chest_take(p: Node3D, slot: int, room: int) -> void:
|
||||
_c2s_chest_take.rpc_id(1, nid(p), slot, room)
|
||||
|
||||
func drop(item: String, count: int, xf: Transform3D) -> void:
|
||||
_c2s_drop.rpc_id(1, item, count, xf)
|
||||
|
||||
# --- server -> client ------------------------------------------------------------
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_world(state: Dictionary) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
world.apply_shared(state)
|
||||
pieces.clear()
|
||||
dropped.clear()
|
||||
for p in world.all_pieces():
|
||||
if p.has_meta("nid"):
|
||||
pieces[int(p.get_meta("nid"))] = p
|
||||
for d in world.all_dropped():
|
||||
if d.has_meta("nid"):
|
||||
dropped[int(d.get_meta("nid"))] = d
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_node(uid: String, hp: float, point: Vector3, from: Vector3) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var n: Node3D = world.props.resource_nodes.get(uid)
|
||||
if n != null and is_instance_valid(n):
|
||||
n.apply_remote(hp, point, from)
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_regrown(uid: String) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var n: Node3D = world.props.pickups.get(uid, world.props.resource_nodes.get(uid))
|
||||
if n != null and is_instance_valid(n):
|
||||
world.props.regrow.erase(uid)
|
||||
n.regrow()
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_taken(key: String) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var pk := _find_pickup(key)
|
||||
if key.begins_with("#"):
|
||||
dropped.erase(int(key.substr(1)))
|
||||
if pk != null:
|
||||
pk.collected()
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_forage(uid: String, left: float) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var bush: Node3D = world.props.forage.get(uid)
|
||||
if bush != null:
|
||||
bush.restore(left)
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_piece_added(e: Dictionary) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var p: Node3D = world.add_piece_entry(e)
|
||||
if p != null:
|
||||
pieces[int(e.nid)] = p
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_pieces_removed(nids: Array, fell: Array) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var gone: Array = []
|
||||
for k in nids:
|
||||
var p: Node3D = pieces.get(int(k))
|
||||
pieces.erase(int(k))
|
||||
if p != null and is_instance_valid(p):
|
||||
gone.append(p)
|
||||
var collapsed: Array = []
|
||||
for k in fell:
|
||||
collapsed.append(int(k))
|
||||
world.remove_pieces_net(gone, collapsed)
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_piece_state(nid_: int, extra: Dictionary) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var p: Node3D = pieces.get(nid_)
|
||||
if p == null or not is_instance_valid(p):
|
||||
return
|
||||
if extra.has("open") and p.has_method("set_open"):
|
||||
p.set_open(bool(extra.open))
|
||||
elif p.has_method("load_extra"):
|
||||
p.load_extra(extra)
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_fires(fires: Array) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
for f in fires:
|
||||
var p: Node3D = pieces.get(int(f[0]))
|
||||
if p != null and is_instance_valid(p) and p.has_method("load_extra"):
|
||||
p.load_extra({"fuel": float(f[1])})
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_chest(nid_: int, data: Array) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var p: Node3D = pieces.get(nid_)
|
||||
if p != null and is_instance_valid(p) and p.get("inventory") != null:
|
||||
p.inventory.load_data(data)
|
||||
received += 1
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_dropped(e: Dictionary) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
var d: Node3D = world.add_dropped_entry(e)
|
||||
if d != null:
|
||||
dropped[int(e.nid)] = d
|
||||
received += 1
|
||||
|
||||
## Items the server decided we earned. Anything that doesn't fit goes back
|
||||
## on the ground at our feet (for anyone to pick up).
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_give(items: Dictionary) -> void:
|
||||
if not is_client():
|
||||
return
|
||||
for id in items:
|
||||
var n := int(items[id])
|
||||
var got: int = Actions._give(world.player, String(id), n)
|
||||
if got < n:
|
||||
drop(String(id), n - got, Transform3D(Basis(), world.player.global_position + Vector3(0, 0.1, 0)))
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_cursor(stack: Variant) -> void:
|
||||
chest_busy = false
|
||||
var cb := _chest_cb
|
||||
_chest_cb = Callable()
|
||||
if cb.is_valid():
|
||||
cb.call(stack)
|
||||
elif stack != null and is_client():
|
||||
_s2c_give({stack.id: stack.count})
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_notify(text: String) -> void:
|
||||
Actions.notify.emit(text)
|
||||
Reference in New Issue
Block a user