Class: Doom::Net::Session
- Inherits:
-
Object
- Object
- Doom::Net::Session
- Defined in:
- lib/doom/net/session.rb
Overview
A multiplayer session: handshake, then lockstep play.
The host coordinates only the handshake -- it hands out player ids, the RNG seed and the map, then tells everyone about everyone. After that there is no authority: every peer runs the same simulation from the same commands, and traffic goes peer to peer rather than through the host, which would add a hop of latency to every command.
Everything here is driven by polling, never by blocking, so the game loop keeps rendering while a session is still forming.
Constant Summary collapse
- HELLO_RESEND_SECONDS =
0.25- DEFAULT_PORT =
5029- REDUNDANCY =
Commands per packet; loss is covered by resending
8- STALL_WARNING_SECONDS =
How long a stall must last before it is worth telling the player about. Well under a second so a real problem shows up promptly, but far longer than the sub-tic waits that healthy play produces constantly.
0.35- DEFAULT_SKILL =
Skill is a plain number here rather than Game::Menu::SKILL_MEDIUM: the network layer has no business depending on the menu.
2
Instance Attribute Summary collapse
-
#local_id ⇒ Object
readonly
Returns the value of attribute local_id.
-
#lockstep ⇒ Object
readonly
Returns the value of attribute lockstep.
-
#map_name ⇒ Object
readonly
Returns the value of attribute map_name.
-
#mode ⇒ Object
readonly
Returns the value of attribute mode.
-
#monitor ⇒ Object
readonly
Returns the value of attribute monitor.
-
#num_players ⇒ Object
readonly
Returns the value of attribute num_players.
-
#player_ids ⇒ Object
readonly
Returns the value of attribute player_ids.
-
#seed ⇒ Object
readonly
Returns the value of attribute seed.
-
#skill ⇒ Object
readonly
Returns the value of attribute skill.
-
#transport ⇒ Object
readonly
Returns the value of attribute transport.
Class Method Summary collapse
- .connect(host:, port: DEFAULT_PORT, delay: Lockstep::DEFAULT_DELAY) ⇒ Object
- .host(port: DEFAULT_PORT, players: 2, map: 'E1M1', mode: :coop, skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY) ⇒ Object
Instance Method Summary collapse
- #desyncs ⇒ Object
- #host? ⇒ Boolean
-
#initialize(role:, port: 0, num_players: 2, map_name: 'E1M1', mode: :coop, skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY, remote_host: nil, remote_port: nil, clock: -> { Time.now }) ⇒ Session
constructor
A new instance of Session.
- #local_port ⇒ Object
-
#poll ⇒ Object
Pump the network.
-
#quit ⇒ Object
Leaving before the handshake finishes is ordinary -- someone pressing Escape while still connecting -- and there is no id to say goodbye with yet, so just go.
-
#run(world, limit: 10) ⇒ Object
Advance the world by whatever tics are ready, then fingerprint on checkpoints and tell the peers.
- #solo? ⇒ Boolean
- #stalled? ⇒ Boolean
-
#stalled_seconds ⇒ Object
Seconds spent unable to advance, 0 when running normally.
- #started? ⇒ Boolean
-
#submit(cmd) ⇒ Object
Schedule this frame's input and tell the peers about it.
-
#transmit ⇒ Object
Resend without sampling.
- #waiting_on ⇒ Object
Constructor Details
#initialize(role:, port: 0, num_players: 2, map_name: 'E1M1', mode: :coop, skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY, remote_host: nil, remote_port: nil, clock: -> { Time.now }) ⇒ Session
Returns a new instance of Session.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/doom/net/session.rb', line 40 def initialize(role:, port: 0, num_players: 2, map_name: 'E1M1', mode: :coop, skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY, remote_host: nil, remote_port: nil, clock: -> { Time.now }) @role = role @delay = delay @clock = clock @transport = Transport.new(port: role == :host ? port : 0) @started = false @peer_acks = Hash.new(0) @joined = {} # player_id => [host, port] @last_hello = nil if host? @local_id = 0 @num_players = num_players @map_name = map_name @mode = mode @skill = skill # The seed is part of the simulation, so it is chosen once and told # to everyone rather than derived locally by each peer. @seed = seed || (::Random.new_seed % 256) start if solo? else @remote = @transport.add_peer(remote_host, remote_port, player_id: 0) end end |
Instance Attribute Details
#local_id ⇒ Object (readonly)
Returns the value of attribute local_id.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def local_id @local_id end |
#lockstep ⇒ Object (readonly)
Returns the value of attribute lockstep.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def lockstep @lockstep end |
#map_name ⇒ Object (readonly)
Returns the value of attribute map_name.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def map_name @map_name end |
#mode ⇒ Object (readonly)
Returns the value of attribute mode.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def mode @mode end |
#monitor ⇒ Object (readonly)
Returns the value of attribute monitor.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def monitor @monitor end |
#num_players ⇒ Object (readonly)
Returns the value of attribute num_players.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def num_players @num_players end |
#player_ids ⇒ Object (readonly)
Returns the value of attribute player_ids.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def player_ids @player_ids end |
#seed ⇒ Object (readonly)
Returns the value of attribute seed.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def seed @seed end |
#skill ⇒ Object (readonly)
Returns the value of attribute skill.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def skill @skill end |
#transport ⇒ Object (readonly)
Returns the value of attribute transport.
24 25 26 |
# File 'lib/doom/net/session.rb', line 24 def transport @transport end |
Class Method Details
.connect(host:, port: DEFAULT_PORT, delay: Lockstep::DEFAULT_DELAY) ⇒ Object
36 37 38 |
# File 'lib/doom/net/session.rb', line 36 def self.connect(host:, port: DEFAULT_PORT, delay: Lockstep::DEFAULT_DELAY) new(role: :client, remote_host: host, remote_port: port, delay: delay) end |
.host(port: DEFAULT_PORT, players: 2, map: 'E1M1', mode: :coop, skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY) ⇒ Object
30 31 32 33 34 |
# File 'lib/doom/net/session.rb', line 30 def self.host(port: DEFAULT_PORT, players: 2, map: 'E1M1', mode: :coop, skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY) new(role: :host, port: port, num_players: players, map_name: map, mode: mode, skill: skill, seed: seed, delay: delay) end |
Instance Method Details
#desyncs ⇒ Object
140 141 142 |
# File 'lib/doom/net/session.rb', line 140 def desyncs @monitor&.desyncs || [] end |
#host? ⇒ Boolean
67 |
# File 'lib/doom/net/session.rb', line 67 def host? = @role == :host |
#local_port ⇒ Object
69 |
# File 'lib/doom/net/session.rb', line 69 def local_port = @transport.local_port |
#poll ⇒ Object
Pump the network. Safe to call every frame, before and after the session starts.
78 79 80 81 |
# File 'lib/doom/net/session.rb', line 78 def poll send_hello_if_waiting @transport.poll.each { |msg, host, port| handle(msg, host, port) } end |
#quit ⇒ Object
Leaving before the handshake finishes is ordinary -- someone pressing Escape while still connecting -- and there is no id to say goodbye with yet, so just go.
147 148 149 150 |
# File 'lib/doom/net/session.rb', line 147 def quit @transport.broadcast(Protocol.encode_quit(@local_id)) if @local_id && !@transport.closed? @transport.close end |
#run(world, limit: 10) ⇒ Object
Advance the world by whatever tics are ready, then fingerprint on checkpoints and tell the peers.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/doom/net/session.rb', line 106 def run(world, limit: 10) return 0 unless @started ran = @lockstep.run_ready(world, limit: limit) report_hash(world) if ran.positive? # Track how long we have been unable to advance. Every healthy peer is # "waiting" almost all the time: once the ready tics have run, the next # one by definition lacks a command, or it would have run too. Only the # duration distinguishes normal play from a peer in trouble. if @lockstep.ready? @stalled_since = nil else @stalled_since ||= @clock.call end ran end |
#solo? ⇒ Boolean
70 |
# File 'lib/doom/net/session.rb', line 70 def solo? = host? && @num_players <= 1 |
#stalled? ⇒ Boolean
129 130 131 |
# File 'lib/doom/net/session.rb', line 129 def stalled? @started && !@lockstep.ready? end |
#stalled_seconds ⇒ Object
Seconds spent unable to advance, 0 when running normally.
134 135 136 137 138 |
# File 'lib/doom/net/session.rb', line 134 def stalled_seconds return 0.0 unless @stalled_since @clock.call - @stalled_since end |
#started? ⇒ Boolean
68 |
# File 'lib/doom/net/session.rb', line 68 def started? = @started |
#submit(cmd) ⇒ Object
Schedule this frame's input and tell the peers about it.
84 85 86 87 88 89 |
# File 'lib/doom/net/session.rb', line 84 def submit(cmd) return unless @started @lockstep.submit_local(cmd) transmit end |
#transmit ⇒ Object
Resend without sampling. A peer stalled on a command we already ran can only be freed by us sending it again, so this must keep happening even when there is no new input to report.
94 95 96 97 98 99 100 101 102 |
# File 'lib/doom/net/session.rb', line 94 def transmit return unless @started @transport.peers.each_value do |peer| pairs = @lockstep.local_since(@peer_acks[peer.player_id], REDUNDANCY) @transport.send_to(peer, Protocol.encode_ticcmds(@local_id, pairs, ack: @lockstep.ack_tic)) end end |
#waiting_on ⇒ Object
125 126 127 |
# File 'lib/doom/net/session.rb', line 125 def waiting_on @started ? @lockstep.waiting_on - [@local_id] : [] end |