Class: Doom::Net::Session

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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_idObject (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

#lockstepObject (readonly)

Returns the value of attribute lockstep.



24
25
26
# File 'lib/doom/net/session.rb', line 24

def lockstep
  @lockstep
end

#map_nameObject (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

#modeObject (readonly)

Returns the value of attribute mode.



24
25
26
# File 'lib/doom/net/session.rb', line 24

def mode
  @mode
end

#monitorObject (readonly)

Returns the value of attribute monitor.



24
25
26
# File 'lib/doom/net/session.rb', line 24

def monitor
  @monitor
end

#num_playersObject (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_idsObject (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

#seedObject (readonly)

Returns the value of attribute seed.



24
25
26
# File 'lib/doom/net/session.rb', line 24

def seed
  @seed
end

#skillObject (readonly)

Returns the value of attribute skill.



24
25
26
# File 'lib/doom/net/session.rb', line 24

def skill
  @skill
end

#transportObject (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

#desyncsObject



140
141
142
# File 'lib/doom/net/session.rb', line 140

def desyncs
  @monitor&.desyncs || []
end

#host?Boolean

Returns:

  • (Boolean)


67
# File 'lib/doom/net/session.rb', line 67

def host? = @role == :host

#local_portObject



69
# File 'lib/doom/net/session.rb', line 69

def local_port = @transport.local_port

#pollObject

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

#quitObject

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

Returns:

  • (Boolean)


70
# File 'lib/doom/net/session.rb', line 70

def solo? = host? && @num_players <= 1

#stalled?Boolean

Returns:

  • (Boolean)


129
130
131
# File 'lib/doom/net/session.rb', line 129

def stalled?
  @started && !@lockstep.ready?
end

#stalled_secondsObject

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

Returns:

  • (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

#transmitObject

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_onObject



125
126
127
# File 'lib/doom/net/session.rb', line 125

def waiting_on
  @started ? @lockstep.waiting_on - [@local_id] : []
end