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:
|
||||
|
||||
Reference in New Issue
Block a user