doom 0.8.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 +134 -24
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +239 -153
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +22 -39
- data/lib/doom/game/item_pickup.rb +87 -75
- data/lib/doom/game/menu.rb +90 -85
- data/lib/doom/game/monster_ai.rb +150 -136
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +38 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +169 -93
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +33 -58
- 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 +604 -832
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +5 -3
- 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 +466 -440
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +7 -7
- 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 +9 -10
- 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 +12 -15
- data/lib/doom/wad/sound.rb +14 -6
- data/lib/doom/wad/sprite.rb +36 -35
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +197 -16
- metadata +94 -7
|
@@ -13,78 +13,77 @@ module Doom
|
|
|
13
13
|
PlayerState::WEAPON_ROCKET => 'DSRLAUNC',
|
|
14
14
|
PlayerState::WEAPON_PLASMA => 'DSPLASMA',
|
|
15
15
|
PlayerState::WEAPON_BFG => 'DSBFG',
|
|
16
|
-
PlayerState::WEAPON_CHAINSAW => 'DSSAWIDL'
|
|
16
|
+
PlayerState::WEAPON_CHAINSAW => 'DSSAWIDL'
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
19
19
|
# Monster see/activation sounds (from mobjinfo seesound)
|
|
20
20
|
MONSTER_SEE = {
|
|
21
|
-
3004 => 'DSPOSIT1',
|
|
22
|
-
9
|
|
21
|
+
3004 => 'DSPOSIT1', # Zombieman
|
|
22
|
+
9 => 'DSSGTSIT', # Shotgun Guy
|
|
23
23
|
3001 => 'DSBGSIT1', # Imp
|
|
24
24
|
3002 => 'DSSGTSIT', # Demon
|
|
25
|
-
58
|
|
26
|
-
3003 => 'DSBRSSIT',
|
|
27
|
-
69
|
|
25
|
+
58 => 'DSSGTSIT', # Spectre
|
|
26
|
+
3003 => 'DSBRSSIT', # Baron
|
|
27
|
+
69 => 'DSBRSSIT', # Hell Knight
|
|
28
28
|
3005 => 'DSCACSIT', # Cacodemon
|
|
29
29
|
3006 => 'DSSKLATK', # Lost Soul
|
|
30
|
-
65
|
|
31
|
-
16
|
|
32
|
-
7
|
|
30
|
+
65 => 'DSPOSIT2', # Heavy Weapon Dude
|
|
31
|
+
16 => 'DSCYBSIT', # Cyberdemon
|
|
32
|
+
7 => 'DSSPISIT' # Spider Mastermind
|
|
33
33
|
}.freeze
|
|
34
34
|
|
|
35
35
|
# Monster death sounds (from mobjinfo deathsound)
|
|
36
36
|
MONSTER_DEATH = {
|
|
37
|
-
3004 => 'DSPODTH1',
|
|
38
|
-
9
|
|
37
|
+
3004 => 'DSPODTH1', # Zombieman
|
|
38
|
+
9 => 'DSSGTDTH', # Shotgun Guy
|
|
39
39
|
3001 => 'DSBGDTH1', # Imp
|
|
40
40
|
3002 => 'DSSGTDTH', # Demon
|
|
41
|
-
58
|
|
42
|
-
3003 => 'DSBRSDTH',
|
|
43
|
-
69
|
|
41
|
+
58 => 'DSSGTDTH', # Spectre
|
|
42
|
+
3003 => 'DSBRSDTH', # Baron
|
|
43
|
+
69 => 'DSBRSDTH', # Hell Knight
|
|
44
44
|
3005 => 'DSCACDTH', # Cacodemon
|
|
45
45
|
3006 => 'DSFIRXPL', # Lost Soul
|
|
46
|
-
65
|
|
47
|
-
16
|
|
48
|
-
7
|
|
46
|
+
65 => 'DSPODTH2', # Heavy Weapon Dude
|
|
47
|
+
16 => 'DSCYBDTH', # Cyberdemon
|
|
48
|
+
7 => 'DSSPIDTH' # Spider Mastermind
|
|
49
49
|
}.freeze
|
|
50
50
|
|
|
51
51
|
# Monster pain sounds (from mobjinfo painsound)
|
|
52
52
|
MONSTER_PAIN = {
|
|
53
|
-
3004 => 'DSPOPAIN',
|
|
54
|
-
9
|
|
53
|
+
3004 => 'DSPOPAIN', # Zombieman
|
|
54
|
+
9 => 'DSPOPAIN', # Shotgun Guy
|
|
55
55
|
3001 => 'DSDMPAIN', # Imp
|
|
56
56
|
3002 => 'DSDMPAIN', # Demon
|
|
57
|
-
58
|
|
58
|
-
3003 => 'DSDMPAIN',
|
|
59
|
-
69
|
|
57
|
+
58 => 'DSDMPAIN', # Spectre
|
|
58
|
+
3003 => 'DSDMPAIN', # Baron
|
|
59
|
+
69 => 'DSDMPAIN', # Hell Knight
|
|
60
60
|
3005 => 'DSDMPAIN', # Cacodemon
|
|
61
61
|
3006 => 'DSDMPAIN', # Lost Soul
|
|
62
|
-
65
|
|
62
|
+
65 => 'DSPOPAIN' # Heavy Weapon Dude
|
|
63
63
|
}.freeze
|
|
64
64
|
|
|
65
65
|
# Monster attack sounds (from mobjinfo attacksound)
|
|
66
66
|
MONSTER_ATTACK = {
|
|
67
|
-
3004 => 'DSPISTOL',
|
|
68
|
-
9
|
|
67
|
+
3004 => 'DSPISTOL', # Zombieman: pistol
|
|
68
|
+
9 => 'DSSHOTGN', # Shotgun Guy: shotgun
|
|
69
69
|
3001 => 'DSFIRSHT', # Imp: fireball launch
|
|
70
70
|
3002 => 'DSSGTATK', # Demon: bite
|
|
71
|
-
58
|
|
72
|
-
3003 => 'DSFIRSHT',
|
|
73
|
-
69
|
|
74
|
-
3005 => 'DSFIRSHT',
|
|
75
|
-
65
|
|
71
|
+
58 => 'DSSGTATK', # Spectre: bite
|
|
72
|
+
3003 => 'DSFIRSHT', # Baron: fireball
|
|
73
|
+
69 => 'DSFIRSHT', # Hell Knight: fireball
|
|
74
|
+
3005 => 'DSFIRSHT', # Cacodemon: fireball
|
|
75
|
+
65 => 'DSSHOTGN' # Heavy Weapon Dude: chaingun burst
|
|
76
76
|
}.freeze
|
|
77
77
|
|
|
78
78
|
def initialize(sound_manager)
|
|
79
79
|
@sounds = sound_manager
|
|
80
|
-
@last_played = {}
|
|
80
|
+
@last_played = {} # Throttle rapid repeats
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def play(name, volume: 1.0, throttle: 0)
|
|
84
84
|
now = Time.now.to_f
|
|
85
|
-
if throttle > 0
|
|
86
|
-
|
|
87
|
-
end
|
|
85
|
+
return if (throttle > 0) && @last_played[name] && (now - @last_played[name]) < throttle
|
|
86
|
+
|
|
88
87
|
sample = @sounds[name]
|
|
89
88
|
sample&.play(volume)
|
|
90
89
|
@last_played[name] = now
|
|
@@ -109,14 +108,6 @@ module Doom
|
|
|
109
108
|
play(sound, throttle: 0.05) if sound
|
|
110
109
|
end
|
|
111
110
|
|
|
112
|
-
def shotgun_cock
|
|
113
|
-
play('DSSGCOCK', throttle: 0.3)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def chainsaw_hit
|
|
117
|
-
play('DSSAWFUL', throttle: 0.1)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
111
|
# --- Player events ---
|
|
121
112
|
def player_pain
|
|
122
113
|
play('DSPLPAIN', throttle: 0.3)
|
|
@@ -134,10 +125,6 @@ module Doom
|
|
|
134
125
|
play('DSWPNUP')
|
|
135
126
|
end
|
|
136
127
|
|
|
137
|
-
def oof
|
|
138
|
-
play('DSOOF', throttle: 0.3)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
128
|
def noway
|
|
142
129
|
play('DSNOWAY', throttle: 0.3)
|
|
143
130
|
end
|
|
@@ -151,10 +138,6 @@ module Doom
|
|
|
151
138
|
play('DSDORCLS', throttle: 0.2)
|
|
152
139
|
end
|
|
153
140
|
|
|
154
|
-
def switch_activate
|
|
155
|
-
play('DSSWTCHN')
|
|
156
|
-
end
|
|
157
|
-
|
|
158
141
|
def platform_start
|
|
159
142
|
play('DSPSTART', throttle: 0.2)
|
|
160
143
|
end
|
|
@@ -188,14 +171,6 @@ module Doom
|
|
|
188
171
|
def explosion
|
|
189
172
|
play('DSBAREXP')
|
|
190
173
|
end
|
|
191
|
-
|
|
192
|
-
def rocket_explode
|
|
193
|
-
play('DSRXPLOD')
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def fireball_hit
|
|
197
|
-
play('DSFIRXPL', throttle: 0.1)
|
|
198
|
-
end
|
|
199
174
|
end
|
|
200
175
|
end
|
|
201
176
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zlib'
|
|
4
|
+
|
|
5
|
+
module Doom
|
|
6
|
+
module Game
|
|
7
|
+
# A fingerprint of the simulation, for catching lockstep desync.
|
|
8
|
+
#
|
|
9
|
+
# Peers exchange this every so often. If two peers disagree at the same
|
|
10
|
+
# tic their worlds have diverged, and we want to say so loudly at the tic
|
|
11
|
+
# it happened rather than let the games drift silently apart.
|
|
12
|
+
#
|
|
13
|
+
# Deliberately NOT built on Object#hash: Ruby randomises string and array
|
|
14
|
+
# hashing per process, so two peers would disagree on identical state and
|
|
15
|
+
# every session would look like a desync. Zlib.crc32 over an explicitly
|
|
16
|
+
# packed byte string is stable across processes and machines.
|
|
17
|
+
#
|
|
18
|
+
# Floats are packed as raw IEEE754 ('E', little-endian), not rounded. In
|
|
19
|
+
# lockstep a one-ULP difference is a real divergence -- it compounds -- so
|
|
20
|
+
# the check must see it.
|
|
21
|
+
module StateHash
|
|
22
|
+
# Sections are hashed separately as well as folded together. When a
|
|
23
|
+
# desync fires, the section that differs says where to look.
|
|
24
|
+
SECTIONS = %i[rng players monsters combat sectors].freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def of(world)
|
|
29
|
+
fold(sections(world))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sections(world)
|
|
33
|
+
{
|
|
34
|
+
rng: crc([world.leveltime, world.random.index].pack('q<q<')),
|
|
35
|
+
players: crc(pack_players(world.players)),
|
|
36
|
+
monsters: crc(pack_monsters(world.monster_ai)),
|
|
37
|
+
combat: crc(pack_combat(world.combat)),
|
|
38
|
+
sectors: crc(pack_sectors(world))
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fold(sections)
|
|
43
|
+
crc(SECTIONS.map { |name| sections[name] }.pack('L<*'))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def crc(bytes)
|
|
47
|
+
Zlib.crc32(bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pack_players(players)
|
|
51
|
+
players.flat_map do |p|
|
|
52
|
+
s = p.state
|
|
53
|
+
[
|
|
54
|
+
p.id, s.health, s.armor, s.weapon, (s.dead ? 1 : 0),
|
|
55
|
+
s.ammo_bullets, s.ammo_shells, s.ammo_rockets, s.ammo_cells,
|
|
56
|
+
s.keys.count { |_, v| v },
|
|
57
|
+
# Frags are simulation state: peers that disagree on the score
|
|
58
|
+
# disagree on who won, and that must show up as a desync.
|
|
59
|
+
p.frags
|
|
60
|
+
].pack('q<*') +
|
|
61
|
+
[p.x, p.y, p.z, p.angle, p.momx, p.momy].pack('E*')
|
|
62
|
+
end.join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pack_monsters(monster_ai)
|
|
66
|
+
return '' unless monster_ai
|
|
67
|
+
|
|
68
|
+
things = monster_ai.instance_variable_get(:@map).things
|
|
69
|
+
monster_ai.monsters.flat_map do |m|
|
|
70
|
+
thing = things[m.thing_idx]
|
|
71
|
+
[
|
|
72
|
+
m.thing_idx, m.movedir, m.movecount, (m.active ? 1 : 0),
|
|
73
|
+
(m.attacking ? 1 : 0), m.target&.id || -1,
|
|
74
|
+
# The map thing's integer position, which hitscan traces against,
|
|
75
|
+
# lags mon.x/mon.y and is distinct sim state -- a desync in it makes
|
|
76
|
+
# bullets miss where a monster appears to be.
|
|
77
|
+
thing.x, thing.y
|
|
78
|
+
].pack('q<*') + [m.x, m.y].pack('E*')
|
|
79
|
+
end.join
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def pack_combat(combat)
|
|
83
|
+
return '' unless combat
|
|
84
|
+
|
|
85
|
+
hp = combat.instance_variable_get(:@monster_hp) || {}
|
|
86
|
+
parts = [
|
|
87
|
+
hp.sort.flatten.pack('q<*'),
|
|
88
|
+
combat.dead_things.keys.sort.pack('q<*')
|
|
89
|
+
]
|
|
90
|
+
# z and dz are as much sim state as x/y -- a projectile diverging only in
|
|
91
|
+
# height still diverges. spawn_tic pins its identity, and the damage
|
|
92
|
+
# multiplier it carries changes the damage it will deal on impact.
|
|
93
|
+
parts << combat.projectiles.flat_map do |pr|
|
|
94
|
+
[pr.x, pr.y, pr.z, pr.dx, pr.dy, pr.dz || 0.0, pr.damage_multiplier || 1.0]
|
|
95
|
+
end.pack('E*')
|
|
96
|
+
parts << combat.projectiles.map(&:spawn_tic).pack('q<*')
|
|
97
|
+
parts.join
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Sector heights as well as lights. Doors and lifts move floor and
|
|
101
|
+
# ceiling heights, and until now the fingerprint watched only the light
|
|
102
|
+
# level -- so a door desyncing between peers went unnoticed. Active door
|
|
103
|
+
# and lift animations are folded in too, so a divergence is caught while
|
|
104
|
+
# the sector is still moving rather than only once it settles.
|
|
105
|
+
def pack_sectors(world)
|
|
106
|
+
sectors = world.map.sectors
|
|
107
|
+
heights = sectors.flat_map { |s| [s.light_level, s.floor_height, s.ceiling_height] }
|
|
108
|
+
|
|
109
|
+
actions = world.sector_actions
|
|
110
|
+
doors = actions.instance_variable_get(:@active_doors) || {}
|
|
111
|
+
lifts = actions.instance_variable_get(:@active_lifts) || {}
|
|
112
|
+
sorted_lifts = lifts.sort_by { |idx, _| idx }
|
|
113
|
+
moving = doors.sort_by { |idx, _| idx }.flat_map { |idx, d| [idx, d[:state], d[:wait_tics]] } +
|
|
114
|
+
sorted_lifts.flat_map { |idx, l| [idx, l[:wait_tics]] }
|
|
115
|
+
# A lift's :state is a symbol (going down / waiting / coming up), not an
|
|
116
|
+
# integer like a door's, so it is folded in as bytes rather than packed
|
|
117
|
+
# with the integer run -- otherwise a lift changing phase between peers
|
|
118
|
+
# would go unnoticed the way a moving door once did.
|
|
119
|
+
lift_states = sorted_lifts.map { |_, l| l[:state].to_s }.join("\0")
|
|
120
|
+
|
|
121
|
+
heights.pack('q<*') + moving.pack('q<*') + lift_states
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# One tic's worth of player intent, as in DOOM's ticcmd_t.
|
|
6
|
+
#
|
|
7
|
+
# This is the only thing that travels between peers in lockstep
|
|
8
|
+
# multiplayer: never positions, never health. Each peer feeds identical
|
|
9
|
+
# ticcmds to an identical simulation and arrives at an identical world, so
|
|
10
|
+
# the wire format stays tiny and cheating by position injection is
|
|
11
|
+
# impossible by construction.
|
|
12
|
+
#
|
|
13
|
+
# forwardmove -1.0..1.0 (negative = backward)
|
|
14
|
+
# sidemove -1.0..1.0 (positive = strafe right)
|
|
15
|
+
# angleturn degrees to turn this tic (positive = left, as DOOM)
|
|
16
|
+
# buttons bitmask, see constants below
|
|
17
|
+
# Subclassed rather than built with a Struct.new block: constants assigned
|
|
18
|
+
# inside a block land in the enclosing lexical scope (Doom::Game), not on
|
|
19
|
+
# the struct class, so Ticcmd::BTN_FIRE would not resolve.
|
|
20
|
+
class Ticcmd < Struct.new(:forwardmove, :sidemove, :angleturn, :buttons)
|
|
21
|
+
BTN_FIRE = 1
|
|
22
|
+
BTN_USE = 2
|
|
23
|
+
PACKED_SIZE = 7
|
|
24
|
+
|
|
25
|
+
def self.none
|
|
26
|
+
new(0.0, 0.0, 0.0, 0)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def fire?
|
|
30
|
+
(buttons & BTN_FIRE) != 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def use?
|
|
34
|
+
(buttons & BTN_USE) != 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def moving?
|
|
38
|
+
forwardmove != 0.0 || sidemove != 0.0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Fixed 7 bytes on the wire ('s>s>s>C', == PACKED_SIZE): three signed
|
|
42
|
+
# shorts plus a byte of buttons, no padding. Movement is quantised to
|
|
43
|
+
# 1/1000 and the turn to 1/100 of a degree -- both far finer than any input
|
|
44
|
+
# device resolves, and identical on every peer, which matters more than the
|
|
45
|
+
# precision itself.
|
|
46
|
+
def pack
|
|
47
|
+
[
|
|
48
|
+
(forwardmove * 1000).round.clamp(-32_768, 32_767),
|
|
49
|
+
(sidemove * 1000).round.clamp(-32_768, 32_767),
|
|
50
|
+
(angleturn * 100).round.clamp(-32_768, 32_767),
|
|
51
|
+
buttons & 0xff
|
|
52
|
+
].pack('s>s>s>C')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.unpack(bytes)
|
|
56
|
+
f, s, a, b = bytes.unpack('s>s>s>C')
|
|
57
|
+
new(f / 1000.0, s / 1000.0, a / 100.0, b)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|