doom 0.6.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +17 -0
  3. data/README.md +65 -4
  4. data/bin/doom +138 -25
  5. data/lib/doom/benchmark.rb +282 -0
  6. data/lib/doom/game/animations.rb +9 -2
  7. data/lib/doom/game/combat.rb +377 -105
  8. data/lib/doom/game/framebuffer_blitter.rb +38 -0
  9. data/lib/doom/game/geometry.rb +68 -0
  10. data/lib/doom/game/intermission.rb +231 -0
  11. data/lib/doom/game/item_pickup.rb +108 -60
  12. data/lib/doom/game/menu.rb +347 -0
  13. data/lib/doom/game/monster_ai.rb +304 -91
  14. data/lib/doom/game/player.rb +105 -0
  15. data/lib/doom/game/player_physics.rb +384 -0
  16. data/lib/doom/game/player_state.rb +47 -64
  17. data/lib/doom/game/random.rb +82 -0
  18. data/lib/doom/game/sector_actions.rb +466 -29
  19. data/lib/doom/game/sector_effects.rb +25 -18
  20. data/lib/doom/game/snapshot.rb +584 -0
  21. data/lib/doom/game/sound_engine.rb +176 -0
  22. data/lib/doom/game/state_hash.rb +125 -0
  23. data/lib/doom/game/ticcmd.rb +61 -0
  24. data/lib/doom/game/world.rb +420 -0
  25. data/lib/doom/map/data.rb +95 -0
  26. data/lib/doom/net/client.rb +204 -0
  27. data/lib/doom/net/desync_monitor.rb +101 -0
  28. data/lib/doom/net/game_server.rb +232 -0
  29. data/lib/doom/net/lockstep.rb +170 -0
  30. data/lib/doom/net/protocol.rb +350 -0
  31. data/lib/doom/net/session.rb +282 -0
  32. data/lib/doom/net/transport.rb +110 -0
  33. data/lib/doom/platform/gosu_window.rb +820 -640
  34. data/lib/doom/platform/sdl.rb +74 -0
  35. data/lib/doom/platform/window_logic.rb +61 -0
  36. data/lib/doom/render/font.rb +80 -0
  37. data/lib/doom/render/hardware_renderer.rb +547 -0
  38. data/lib/doom/render/ray_tracing/bvh.rb +103 -0
  39. data/lib/doom/render/ray_tracing/material_state.rb +66 -0
  40. data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
  41. data/lib/doom/render/ray_tracing_renderer.rb +940 -0
  42. data/lib/doom/render/renderer.rb +684 -330
  43. data/lib/doom/render/renderer_factory.rb +50 -0
  44. data/lib/doom/render/screen_melt.rb +71 -0
  45. data/lib/doom/render/spinel_native/kernel.rb +780 -0
  46. data/lib/doom/render/spinel_native_renderer.rb +162 -0
  47. data/lib/doom/render/status_bar.rb +14 -10
  48. data/lib/doom/render/weapon_renderer.rb +15 -17
  49. data/lib/doom/render/world_mesh.rb +270 -0
  50. data/lib/doom/render/zbuffer_renderer.rb +151 -0
  51. data/lib/doom/version.rb +1 -1
  52. data/lib/doom/wad/flat.rb +1 -1
  53. data/lib/doom/wad/hud_graphics.rb +7 -3
  54. data/lib/doom/wad/palette.rb +3 -3
  55. data/lib/doom/wad/patch.rb +1 -1
  56. data/lib/doom/wad/reader.rb +59 -10
  57. data/lib/doom/wad/sound.rb +93 -0
  58. data/lib/doom/wad/sprite.rb +60 -34
  59. data/lib/doom/wad/texture.rb +5 -5
  60. data/lib/doom/wad_downloader.rb +38 -56
  61. data/lib/doom.rb +235 -12
  62. metadata +100 -7
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Net
5
+ # A multiplayer session: handshake, then lockstep play.
6
+ #
7
+ # The host coordinates only the handshake -- it hands out player ids, the
8
+ # RNG seed and the map, then tells everyone about everyone. After that
9
+ # there is no authority: every peer runs the same simulation from the same
10
+ # commands, and traffic goes peer to peer rather than through the host,
11
+ # which would add a hop of latency to every command.
12
+ #
13
+ # Everything here is driven by polling, never by blocking, so the game loop
14
+ # keeps rendering while a session is still forming.
15
+ class Session
16
+ HELLO_RESEND_SECONDS = 0.25
17
+ DEFAULT_PORT = 5029
18
+ REDUNDANCY = 8 # Commands per packet; loss is covered by resending
19
+
20
+ # How long a stall must last before it is worth telling the player about.
21
+ # Well under a second so a real problem shows up promptly, but far longer
22
+ # than the sub-tic waits that healthy play produces constantly.
23
+ STALL_WARNING_SECONDS = 0.35
24
+ attr_reader :transport, :lockstep, :monitor, :local_id, :player_ids, :seed, :map_name, :mode, :skill, :num_players
25
+
26
+ # Skill is a plain number here rather than Game::Menu::SKILL_MEDIUM: the
27
+ # network layer has no business depending on the menu.
28
+ DEFAULT_SKILL = 2
29
+
30
+ def self.host(port: DEFAULT_PORT, players: 2, map: 'E1M1', mode: :coop,
31
+ skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY)
32
+ new(role: :host, port: port, num_players: players, map_name: map, mode: mode,
33
+ skill: skill, seed: seed, delay: delay)
34
+ end
35
+
36
+ def self.connect(host:, port: DEFAULT_PORT, delay: Lockstep::DEFAULT_DELAY)
37
+ new(role: :client, remote_host: host, remote_port: port, delay: delay)
38
+ end
39
+
40
+ def initialize(role:, port: 0, num_players: 2, map_name: 'E1M1', mode: :coop,
41
+ skill: DEFAULT_SKILL, seed: nil, delay: Lockstep::DEFAULT_DELAY,
42
+ remote_host: nil, remote_port: nil, clock: -> { Time.now })
43
+ @role = role
44
+ @delay = delay
45
+ @clock = clock
46
+ @transport = Transport.new(port: role == :host ? port : 0)
47
+ @started = false
48
+ @peer_acks = Hash.new(0)
49
+ @joined = {} # player_id => [host, port]
50
+ @last_hello = nil
51
+
52
+ if host?
53
+ @local_id = 0
54
+ @num_players = num_players
55
+ @map_name = map_name
56
+ @mode = mode
57
+ @skill = skill
58
+ # The seed is part of the simulation, so it is chosen once and told
59
+ # to everyone rather than derived locally by each peer.
60
+ @seed = seed || (::Random.new_seed % 256)
61
+ start if solo?
62
+ else
63
+ @remote = @transport.add_peer(remote_host, remote_port, player_id: 0)
64
+ end
65
+ end
66
+
67
+ def host? = @role == :host
68
+ def started? = @started
69
+ def local_port = @transport.local_port
70
+ def solo? = host? && @num_players <= 1
71
+
72
+ def player_ids
73
+ @player_ids ||= (0...(@num_players || 1)).to_a
74
+ end
75
+
76
+ # Pump the network. Safe to call every frame, before and after the
77
+ # session starts.
78
+ def poll
79
+ send_hello_if_waiting
80
+ @transport.poll.each { |msg, host, port| handle(msg, host, port) }
81
+ end
82
+
83
+ # Schedule this frame's input and tell the peers about it.
84
+ def submit(cmd)
85
+ return unless @started
86
+
87
+ @lockstep.submit_local(cmd)
88
+ transmit
89
+ end
90
+
91
+ # Resend without sampling. A peer stalled on a command we already ran can
92
+ # only be freed by us sending it again, so this must keep happening even
93
+ # when there is no new input to report.
94
+ def transmit
95
+ return unless @started
96
+
97
+ @transport.peers.each_value do |peer|
98
+ pairs = @lockstep.local_since(@peer_acks[peer.player_id], REDUNDANCY)
99
+ @transport.send_to(peer, Protocol.encode_ticcmds(@local_id, pairs,
100
+ ack: @lockstep.ack_tic))
101
+ end
102
+ end
103
+
104
+ # Advance the world by whatever tics are ready, then fingerprint on
105
+ # checkpoints and tell the peers.
106
+ def run(world, limit: 10)
107
+ return 0 unless @started
108
+
109
+ ran = @lockstep.run_ready(world, limit: limit)
110
+ report_hash(world) if ran.positive?
111
+
112
+ # Track how long we have been unable to advance. Every healthy peer is
113
+ # "waiting" almost all the time: once the ready tics have run, the next
114
+ # one by definition lacks a command, or it would have run too. Only the
115
+ # duration distinguishes normal play from a peer in trouble.
116
+ if @lockstep.ready?
117
+ @stalled_since = nil
118
+ else
119
+ @stalled_since ||= @clock.call
120
+ end
121
+
122
+ ran
123
+ end
124
+
125
+ def waiting_on
126
+ @started ? @lockstep.waiting_on - [@local_id] : []
127
+ end
128
+
129
+ def stalled?
130
+ @started && !@lockstep.ready?
131
+ end
132
+
133
+ # Seconds spent unable to advance, 0 when running normally.
134
+ def stalled_seconds
135
+ return 0.0 unless @stalled_since
136
+
137
+ @clock.call - @stalled_since
138
+ end
139
+
140
+ def desyncs
141
+ @monitor&.desyncs || []
142
+ end
143
+
144
+ # Leaving before the handshake finishes is ordinary -- someone pressing
145
+ # Escape while still connecting -- and there is no id to say goodbye
146
+ # with yet, so just go.
147
+ def quit
148
+ @transport.broadcast(Protocol.encode_quit(@local_id)) if @local_id && !@transport.closed?
149
+ @transport.close
150
+ end
151
+
152
+ private
153
+
154
+ def handle(msg, host, port)
155
+ case msg[:type]
156
+ when Protocol::HELLO then on_hello(host, port)
157
+ when Protocol::SETUP then on_setup(msg)
158
+ when Protocol::PEERS then on_peers(msg)
159
+ when Protocol::TICCMD then on_ticcmds(msg)
160
+ when Protocol::HASH then on_hash(msg)
161
+ when Protocol::QUIT then on_quit(msg)
162
+ end
163
+ end
164
+
165
+ # Host side: hand the newcomer an id and, once everyone is in, tell them
166
+ # all what game they are playing and who else is here.
167
+ def on_hello(host, port)
168
+ return unless host?
169
+
170
+ existing = @transport.peer_for(host, port)
171
+ if existing
172
+ # A resent hello: their SETUP was probably lost, so send it again.
173
+ send_setup(existing) if @started
174
+ return
175
+ end
176
+
177
+ return if @joined.size + 1 >= @num_players && !@started
178
+
179
+ id = next_free_id
180
+ return unless id
181
+
182
+ peer = @transport.add_peer(host, port, player_id: id)
183
+ @joined[id] = [host, port]
184
+ start if @joined.size + 1 == @num_players
185
+ send_setup(peer) if @started
186
+ end
187
+
188
+ def next_free_id
189
+ (1...@num_players).find { |id| !@joined.key?(id) }
190
+ end
191
+
192
+ def on_setup(msg)
193
+ return if host? || @started
194
+
195
+ @local_id = msg[:player_id]
196
+ @num_players = msg[:num_players]
197
+ @seed = msg[:seed]
198
+ @map_name = msg[:map]
199
+ @mode = msg[:mode]
200
+ @skill = msg[:skill]
201
+ start
202
+ end
203
+
204
+ def on_peers(msg)
205
+ msg[:peers].each do |player_id, host, port|
206
+ next if player_id == @local_id
207
+
208
+ @transport.add_peer(host, port, player_id: player_id)
209
+ end
210
+ end
211
+
212
+ def on_ticcmds(msg)
213
+ return unless @started
214
+
215
+ @peer_acks[msg[:player_id]] = msg[:ack].to_i
216
+ @lockstep.note_ack(msg[:player_id], msg[:ack].to_i)
217
+ msg[:cmds].each { |tic, cmd| @lockstep.receive(tic, msg[:player_id], cmd) }
218
+ end
219
+
220
+ def on_hash(msg)
221
+ @monitor&.remote_report(msg[:tic], msg[:player_id], msg[:sections])
222
+ end
223
+
224
+ def on_quit(msg)
225
+ @transport.peers.delete_if { |_, peer| peer.player_id == msg[:player_id] }
226
+ end
227
+
228
+ def start
229
+ @started = true
230
+ @lockstep = Lockstep.new(local_id: @local_id, player_ids: player_ids, delay: @delay)
231
+ @monitor = DesyncMonitor.new
232
+ return unless host?
233
+
234
+ @transport.peers.each_value do |peer|
235
+ send_setup(peer)
236
+ send_peers(peer)
237
+ end
238
+ end
239
+
240
+ def send_setup(peer)
241
+ @transport.send_to(peer, Protocol.encode_setup(
242
+ player_id: peer.player_id, num_players: @num_players, seed: @seed,
243
+ map: @map_name, mode: @mode, skill: @skill
244
+ ))
245
+ end
246
+
247
+ # Everyone except the recipient, so the mesh can form without the host
248
+ # relaying play traffic.
249
+ def send_peers(peer)
250
+ entries = [[0, host_address_for(peer), @transport.local_port]]
251
+ @joined.each do |id, (host, port)|
252
+ next if id == peer.player_id
253
+
254
+ entries << [id, host, port]
255
+ end
256
+ @transport.send_to(peer, Protocol.encode_peers(entries))
257
+ end
258
+
259
+ # Tell each client to reach us on the address they already reached us on.
260
+ def host_address_for(peer)
261
+ peer.host
262
+ end
263
+
264
+ def send_hello_if_waiting
265
+ return if host? || @started
266
+
267
+ now = @clock.call
268
+ return if @last_hello && (now - @last_hello) < HELLO_RESEND_SECONDS
269
+
270
+ @last_hello = now
271
+ @transport.send_to(@remote, Protocol.encode_hello)
272
+ end
273
+
274
+ def report_hash(world)
275
+ sections = @monitor.record(world.leveltime, world)
276
+ return unless sections
277
+
278
+ @transport.broadcast(Protocol.encode_hash(world.leveltime, @local_id, sections))
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ module Doom
6
+ module Net
7
+ # Non-blocking UDP for both multiplayer stacks: the peer-to-peer lockstep
8
+ # mesh (send_to/broadcast over the peer table) and the authoritative star
9
+ # server (send_to_addr, replying straight to whoever a packet came from).
10
+ #
11
+ # UDP rather than TCP because a late ticcmd is worthless: head-of-line
12
+ # blocking would make one lost packet stall everything behind it, which is
13
+ # the opposite of what we want when the payload is already resent by the
14
+ # next packet anyway.
15
+ #
16
+ # Nothing here retries or acknowledges. Loss is handled a layer up, by
17
+ # Lockstep resending recent commands, and ordering is handled by the tic
18
+ # numbers inside the packets.
19
+ class Transport
20
+ Peer = Struct.new(:host, :port, :player_id) do
21
+ def key
22
+ "#{host}:#{port}"
23
+ end
24
+ end
25
+
26
+ MAX_DATAGRAM = 2048
27
+
28
+ attr_reader :peers
29
+
30
+ def initialize(port: 0, host: '0.0.0.0')
31
+ @socket = UDPSocket.new
32
+ @socket.bind(host, port)
33
+ @peers = {}
34
+ end
35
+
36
+ def local_port
37
+ @socket.addr[1]
38
+ end
39
+
40
+ def add_peer(host, port, player_id: nil)
41
+ peer = Peer.new(host, port, player_id)
42
+ @peers[peer.key] = peer
43
+ peer
44
+ end
45
+
46
+ def peer_for(host, port)
47
+ @peers["#{host}:#{port}"]
48
+ end
49
+
50
+ def send_to(peer, bytes)
51
+ return false if closed?
52
+
53
+ @socket.send(bytes, 0, peer.host, peer.port)
54
+ true
55
+ rescue SystemCallError, IOError
56
+ # A peer that has gone away must not take the session down with it;
57
+ # the lockstep layer already notices them by stalling.
58
+ false
59
+ end
60
+
61
+ # Send to a bare address, without registering it as a peer. The game
62
+ # server tracks its clients itself and replies to wherever a packet came
63
+ # from, so it does not want the peer table.
64
+ def send_to_addr(host, port, bytes)
65
+ return false if closed?
66
+
67
+ @socket.send(bytes, 0, host, port)
68
+ true
69
+ rescue SystemCallError, IOError
70
+ false
71
+ end
72
+
73
+ def broadcast(bytes)
74
+ @peers.each_value { |peer| send_to(peer, bytes) }
75
+ end
76
+
77
+ # Drain what has arrived, decoded. Returns [[message, host, port], ...].
78
+ # `max` bounds the work per frame so a flood cannot starve rendering.
79
+ #
80
+ # Undecodable packets are dropped silently: on an open port anyone can
81
+ # send anything, and it is not worth a log line per stray datagram.
82
+ def poll(max: 64)
83
+ return [] if closed?
84
+
85
+ messages = []
86
+ max.times do
87
+ begin
88
+ data, addr = @socket.recvfrom_nonblock(MAX_DATAGRAM)
89
+ rescue IO::WaitReadable, Errno::EWOULDBLOCK, Errno::EAGAIN
90
+ break
91
+ rescue SystemCallError, IOError
92
+ break
93
+ end
94
+
95
+ msg = Protocol.decode(data)
96
+ messages << [msg, addr[3], addr[1]] if msg
97
+ end
98
+ messages
99
+ end
100
+
101
+ def close
102
+ @socket.close unless @socket.closed?
103
+ end
104
+
105
+ def closed?
106
+ @socket.closed?
107
+ end
108
+ end
109
+ end
110
+ end