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.
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 +134 -24
  5. data/lib/doom/benchmark.rb +282 -0
  6. data/lib/doom/game/animations.rb +9 -2
  7. data/lib/doom/game/combat.rb +239 -153
  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 +22 -39
  11. data/lib/doom/game/item_pickup.rb +87 -75
  12. data/lib/doom/game/menu.rb +90 -85
  13. data/lib/doom/game/monster_ai.rb +150 -136
  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 +38 -64
  17. data/lib/doom/game/random.rb +82 -0
  18. data/lib/doom/game/sector_actions.rb +169 -93
  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 +33 -58
  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 +604 -832
  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 +5 -3
  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 +466 -440
  43. data/lib/doom/render/renderer_factory.rb +50 -0
  44. data/lib/doom/render/screen_melt.rb +7 -7
  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 +9 -10
  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 +12 -15
  57. data/lib/doom/wad/sound.rb +14 -6
  58. data/lib/doom/wad/sprite.rb +36 -35
  59. data/lib/doom/wad/texture.rb +5 -5
  60. data/lib/doom/wad_downloader.rb +38 -56
  61. data/lib/doom.rb +197 -16
  62. 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', # Zombieman
22
- 9 => 'DSSGTSIT', # Shotgun Guy
21
+ 3004 => 'DSPOSIT1', # Zombieman
22
+ 9 => 'DSSGTSIT', # Shotgun Guy
23
23
  3001 => 'DSBGSIT1', # Imp
24
24
  3002 => 'DSSGTSIT', # Demon
25
- 58 => 'DSSGTSIT', # Spectre
26
- 3003 => 'DSBRSSIT', # Baron
27
- 69 => 'DSBRSSIT', # Hell Knight
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 => 'DSPOSIT2', # Heavy Weapon Dude
31
- 16 => 'DSCYBSIT', # Cyberdemon
32
- 7 => 'DSSPISIT', # Spider Mastermind
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', # Zombieman
38
- 9 => 'DSSGTDTH', # Shotgun Guy
37
+ 3004 => 'DSPODTH1', # Zombieman
38
+ 9 => 'DSSGTDTH', # Shotgun Guy
39
39
  3001 => 'DSBGDTH1', # Imp
40
40
  3002 => 'DSSGTDTH', # Demon
41
- 58 => 'DSSGTDTH', # Spectre
42
- 3003 => 'DSBRSDTH', # Baron
43
- 69 => 'DSBRSDTH', # Hell Knight
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 => 'DSPODTH2', # Heavy Weapon Dude
47
- 16 => 'DSCYBDTH', # Cyberdemon
48
- 7 => 'DSSPIDTH', # Spider Mastermind
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', # Zombieman
54
- 9 => 'DSPOPAIN', # Shotgun Guy
53
+ 3004 => 'DSPOPAIN', # Zombieman
54
+ 9 => 'DSPOPAIN', # Shotgun Guy
55
55
  3001 => 'DSDMPAIN', # Imp
56
56
  3002 => 'DSDMPAIN', # Demon
57
- 58 => 'DSDMPAIN', # Spectre
58
- 3003 => 'DSDMPAIN', # Baron
59
- 69 => 'DSDMPAIN', # Hell Knight
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 => 'DSPOPAIN', # Heavy Weapon Dude
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', # Zombieman: pistol
68
- 9 => 'DSSHOTGN', # Shotgun Guy: shotgun
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 => '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
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 = {} # Throttle rapid repeats
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
- return if @last_played[name] && (now - @last_played[name]) < throttle
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