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.
- checksums.yaml +4 -4
- data/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +138 -25
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +377 -105
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +231 -0
- data/lib/doom/game/item_pickup.rb +108 -60
- data/lib/doom/game/menu.rb +347 -0
- data/lib/doom/game/monster_ai.rb +304 -91
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +47 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +466 -29
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +176 -0
- data/lib/doom/game/state_hash.rb +125 -0
- data/lib/doom/game/ticcmd.rb +61 -0
- data/lib/doom/game/world.rb +420 -0
- data/lib/doom/map/data.rb +95 -0
- data/lib/doom/net/client.rb +204 -0
- data/lib/doom/net/desync_monitor.rb +101 -0
- data/lib/doom/net/game_server.rb +232 -0
- data/lib/doom/net/lockstep.rb +170 -0
- data/lib/doom/net/protocol.rb +350 -0
- data/lib/doom/net/session.rb +282 -0
- data/lib/doom/net/transport.rb +110 -0
- data/lib/doom/platform/gosu_window.rb +820 -640
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +80 -0
- data/lib/doom/render/hardware_renderer.rb +547 -0
- data/lib/doom/render/ray_tracing/bvh.rb +103 -0
- data/lib/doom/render/ray_tracing/material_state.rb +66 -0
- data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
- data/lib/doom/render/ray_tracing_renderer.rb +940 -0
- data/lib/doom/render/renderer.rb +684 -330
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +71 -0
- data/lib/doom/render/spinel_native/kernel.rb +780 -0
- data/lib/doom/render/spinel_native_renderer.rb +162 -0
- data/lib/doom/render/status_bar.rb +14 -10
- data/lib/doom/render/weapon_renderer.rb +15 -17
- data/lib/doom/render/world_mesh.rb +270 -0
- data/lib/doom/render/zbuffer_renderer.rb +151 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/flat.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +7 -3
- data/lib/doom/wad/palette.rb +3 -3
- data/lib/doom/wad/patch.rb +1 -1
- data/lib/doom/wad/reader.rb +59 -10
- data/lib/doom/wad/sound.rb +93 -0
- data/lib/doom/wad/sprite.rb +60 -34
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +235 -12
- 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
|