Class: Doom::Net::GameServer

Inherits:
Object
  • Object
show all
Defined in:
lib/doom/net/game_server.rb

Overview

The authoritative game server for large, chaotic sessions.

Peer-to-peer lockstep does not survive a conference: with mobile clients behind CGNAT they cannot reach each other, and lockstep makes everyone wait for the slowest, so the first person to close their laptop freezes the match. This server fixes both. Clients speak only to it (outbound UDP, so NAT is a non-issue), and it never waits: on every tic it takes whatever input has arrived, fills the gaps with neutral commands, runs the tic, and broadcasts the finalized result. A player whose phone dies simply stops acting -- nobody else is held up.

It is still deterministic underneath: it holds the one canonical world, every client runs the identical command stream, and joins and leaves are tic events (a deathmatch spawn draws the shared RNG, so it must happen on the same tic everywhere). Because it holds the canonical world, it can hand a late joiner a snapshot of the exact current state instead of making them replay the match from the start.

Nothing here blocks. poll drains the socket, advance runs whatever tics are due against an injected clock; a host loops them.

Defined Under Namespace

Classes: Member

Constant Summary collapse

TICRATE =
35
TIC_SECONDS =
1.0 / TICRATE
FRAME_REDUNDANCY =

Recent frames repeated per packet against loss

6
CLIENT_TIMEOUT =

Drop a client silent this long

5.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(world:, transport:, max_players: 16, clock: -> { Time.now }, client_timeout: CLIENT_TIMEOUT) ⇒ GameServer

Returns a new instance of GameServer.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/doom/net/game_server.rb', line 39

def initialize(world:, transport:, max_players: 16, clock: -> { Time.now },
               client_timeout: CLIENT_TIMEOUT)
  @world = world
  @transport = transport
  @max_players = max_players
  @clock = clock
  @client_timeout = client_timeout
  @tic = world.leveltime
  @start_tic = world.leveltime
  @start_time = @clock.call

  @clients = {}                 # addr key => Client
  @by_id = {}                   # player_id => Client
  @inputs = Hash.new { |h, k| h[k] = {} } # tic => { player_id => Ticcmd }
  @pending_joins = []           # player ids to add on the next tic
  @pending_leaves = []          # player ids to remove on the next tic
  @frame_log = []               # recent finalized frames, for redundancy
end

Instance Attribute Details

#clientsObject (readonly)

Returns the value of attribute clients.



37
38
39
# File 'lib/doom/net/game_server.rb', line 37

def clients
  @clients
end

#ticObject (readonly)

Returns the value of attribute tic.



37
38
39
# File 'lib/doom/net/game_server.rb', line 37

def tic
  @tic
end

#transportObject (readonly)

Returns the value of attribute transport.



37
38
39
# File 'lib/doom/net/game_server.rb', line 37

def transport
  @transport
end

#worldObject (readonly)

Returns the value of attribute world.



37
38
39
# File 'lib/doom/net/game_server.rb', line 37

def world
  @world
end

Instance Method Details

#advance(now = @clock.call, limit: 8) ⇒ Object

Run every tic whose deadline has passed, capping the catch-up so one long stall cannot make a single call run thousands of tics. Returns how many ran.



80
81
82
83
84
85
86
87
88
# File 'lib/doom/net/game_server.rb', line 80

def advance(now = @clock.call, limit: 8)
  target = @start_tic + ((now - @start_time) * TICRATE).floor
  ran = 0
  while @tic < target && ran < limit
    step_one_tic
    ran += 1
  end
  ran
end

#player_countObject



90
# File 'lib/doom/net/game_server.rb', line 90

def player_count = @world.players.size

#pollObject

Drain and dispatch whatever has arrived. Safe every loop iteration.



59
60
61
# File 'lib/doom/net/game_server.rb', line 59

def poll
  @transport.poll.each { |msg, host, port| handle(msg, host, port) }
end

#reap(now = @clock.call) ⇒ Object

Drop clients that have gone silent, so their stale bodies stop being simulated and stop holding a player slot. Called from the host loop. A leave is a tic event like a join, applied on the next tic.



66
67
68
69
70
71
72
73
74
75
# File 'lib/doom/net/game_server.rb', line 66

def reap(now = @clock.call)
  @clients.values.each do |c|
    next unless c.joined
    next if now - c.last_seen < @client_timeout

    @clients.delete(c.key)
    @by_id.delete(c.player_id)
    @pending_leaves << c.player_id
  end
end