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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Platform
|
|
5
|
+
# SDL2 keyboard grab via Gosu's bundled SDL -- prevents OS key interception.
|
|
6
|
+
module SDLKeyboardGrab
|
|
7
|
+
def self.setup
|
|
8
|
+
require 'fiddle'
|
|
9
|
+
gosu_spec = Gem.loaded_specs['gosu']
|
|
10
|
+
lib_ext = RbConfig::CONFIG['DLEXT'] || 'so'
|
|
11
|
+
bundle = File.join(gosu_spec.full_gem_path, 'lib', "gosu.#{lib_ext}")
|
|
12
|
+
@lib = Fiddle.dlopen(bundle)
|
|
13
|
+
@shared_window = Fiddle::Function.new(
|
|
14
|
+
@lib['_ZN4Gosu13shared_windowEv'], [], Fiddle::TYPE_VOIDP
|
|
15
|
+
)
|
|
16
|
+
@set_kb_grab = Fiddle::Function.new(
|
|
17
|
+
@lib['SDL_SetWindowKeyboardGrab'],
|
|
18
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_VOID
|
|
19
|
+
)
|
|
20
|
+
@ready = true
|
|
21
|
+
rescue StandardError, LoadError => e
|
|
22
|
+
warn "SDLKeyboardGrab.setup failed: #{e.class}: #{e.message}"
|
|
23
|
+
@ready = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.grab!
|
|
27
|
+
return unless @ready
|
|
28
|
+
|
|
29
|
+
@set_kb_grab.call(@shared_window.call, 1)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.release!
|
|
33
|
+
return unless @ready
|
|
34
|
+
|
|
35
|
+
@set_kb_grab.call(@shared_window.call, 0)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Ask SDL for the display's refresh rate.
|
|
40
|
+
#
|
|
41
|
+
# Timing our own buffer swaps cannot answer this: when the engine renders
|
|
42
|
+
# slower than the display refreshes -- which is the normal case here --
|
|
43
|
+
# the swap cadence measures our render time, not the monitor. SDL knows
|
|
44
|
+
# the real number, and Gosu bundles SDL, so we read it directly the same
|
|
45
|
+
# way SDLKeyboardGrab reaches for SDL_SetWindowKeyboardGrab.
|
|
46
|
+
module SDLDisplayMode
|
|
47
|
+
# SDL_DisplayMode { Uint32 format; int w; int h; int refresh_rate; void *driverdata; }
|
|
48
|
+
REFRESH_RATE_OFFSET = 12
|
|
49
|
+
STRUCT_SIZE = 32
|
|
50
|
+
|
|
51
|
+
def self.refresh_rate
|
|
52
|
+
require 'fiddle'
|
|
53
|
+
gosu_spec = Gem.loaded_specs['gosu']
|
|
54
|
+
lib_ext = RbConfig::CONFIG['DLEXT'] || 'so'
|
|
55
|
+
lib = Fiddle.dlopen(File.join(gosu_spec.full_gem_path, 'lib', "gosu.#{lib_ext}"))
|
|
56
|
+
|
|
57
|
+
get_mode = Fiddle::Function.new(
|
|
58
|
+
lib['SDL_GetCurrentDisplayMode'],
|
|
59
|
+
[Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
buf = Fiddle::Pointer.malloc(STRUCT_SIZE)
|
|
63
|
+
return nil unless get_mode.call(0, buf).zero?
|
|
64
|
+
|
|
65
|
+
hz = buf[REFRESH_RATE_OFFSET, 4].unpack1('l')
|
|
66
|
+
# SDL reports 0 when the rate is unspecified (some virtual displays).
|
|
67
|
+
hz.positive? ? hz : nil
|
|
68
|
+
rescue StandardError, LoadError => e
|
|
69
|
+
warn "SDLDisplayMode.refresh_rate failed: #{e.class}: #{e.message}"
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Platform
|
|
5
|
+
# Pure helpers extracted from GosuWindow so they can be tested without a
|
|
6
|
+
# display. GosuWindow subclasses Gosu::Window, which cannot be loaded (let
|
|
7
|
+
# alone instantiated) under RSpec, so any decision logic worth testing lives
|
|
8
|
+
# here as plain functions and GosuWindow just delegates to them.
|
|
9
|
+
module WindowLogic
|
|
10
|
+
# DOOM thing flags: bit 0 = skill 1-2 (baby/easy), bit 1 = skill 3
|
|
11
|
+
# (medium), bit 2 = skill 4-5 (hard/nightmare), bit 4 = multiplayer-only.
|
|
12
|
+
SKILL_FLAG_EASY = 0x0001
|
|
13
|
+
SKILL_FLAG_MEDIUM = 0x0002
|
|
14
|
+
SKILL_FLAG_HARD = 0x0004
|
|
15
|
+
MULTIPLAYER_ONLY_FLAG = 0x0010
|
|
16
|
+
|
|
17
|
+
# The flag bit a thing must carry to appear at the given skill.
|
|
18
|
+
# skill uses Game::Menu::SKILL_* values (0-4).
|
|
19
|
+
def self.skill_flag_bit(skill)
|
|
20
|
+
case skill
|
|
21
|
+
when Game::Menu::SKILL_BABY, Game::Menu::SKILL_EASY then SKILL_FLAG_EASY
|
|
22
|
+
when Game::Menu::SKILL_MEDIUM then SKILL_FLAG_MEDIUM
|
|
23
|
+
when Game::Menu::SKILL_HARD, Game::Menu::SKILL_NIGHTMARE then SKILL_FLAG_HARD
|
|
24
|
+
else SKILL_FLAG_EASY | SKILL_FLAG_MEDIUM | SKILL_FLAG_HARD
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Which things this skill hides. A thing is hidden if it is
|
|
29
|
+
# multiplayer-only, or if it does not carry the flag bit for this skill.
|
|
30
|
+
# Returns { index => true } (matching the world's skill_hidden shape).
|
|
31
|
+
def self.skill_hidden(skill, things)
|
|
32
|
+
flag_bit = skill_flag_bit(skill)
|
|
33
|
+
hidden = {}
|
|
34
|
+
things.each_with_index do |thing, idx|
|
|
35
|
+
hidden[idx] = true if (thing.flags & MULTIPLAYER_ONLY_FLAG) != 0 || (thing.flags & flag_bit) == 0
|
|
36
|
+
end
|
|
37
|
+
hidden
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Frame-presentation cadence. Rendering runs every loop iteration, but we
|
|
41
|
+
# only blit/swap once a refresh interval (minus slack, so we aim slightly
|
|
42
|
+
# before vblank) has elapsed since the last present.
|
|
43
|
+
def self.present_due?(now_ms, last_present_ms, interval_ms, slack)
|
|
44
|
+
(now_ms - last_present_ms) >= (interval_ms * slack)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Net status overlay lines. A lockstep stall under the threshold is normal
|
|
48
|
+
# -- every peer briefly waits for the next command between tics -- so it
|
|
49
|
+
# produces nothing; only a stall past the threshold, a session that has
|
|
50
|
+
# not started yet, or a desync is worth interrupting the player for.
|
|
51
|
+
def self.net_status_lines(started:, host:, waiting:, stalled_seconds:,
|
|
52
|
+
stall_threshold:, desync: nil)
|
|
53
|
+
lines = []
|
|
54
|
+
lines << (host ? 'WAITING FOR PLAYERS' : 'CONNECTING...') unless started
|
|
55
|
+
lines << "WAITING FOR PLAYER #{waiting.join(', ')}" if waiting.any? && stalled_seconds > stall_threshold
|
|
56
|
+
lines << "DESYNC AT TIC #{desync.tic} (#{Array(desync.sections).join(', ')})" if desync
|
|
57
|
+
lines
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Render
|
|
5
|
+
# DOOM's built-in font loaded from STCFN patches (ASCII 33-121).
|
|
6
|
+
# Uppercase only -- lowercase is auto-uppercased.
|
|
7
|
+
class Font
|
|
8
|
+
SPACE_WIDTH = 4
|
|
9
|
+
|
|
10
|
+
def initialize(_wad, hud_graphics)
|
|
11
|
+
@chars = {}
|
|
12
|
+
(33..121).each do |ascii|
|
|
13
|
+
name = 'STCFN%03d' % ascii
|
|
14
|
+
sprite = hud_graphics.send(:load_graphic, name)
|
|
15
|
+
@chars[ascii] = sprite if sprite
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Draw text into framebuffer at (x, y). Returns width drawn.
|
|
20
|
+
def draw_text(framebuffer, text, x, y, screen_width: 320, screen_height: 240)
|
|
21
|
+
cursor_x = x
|
|
22
|
+
text.upcase.each_char do |char|
|
|
23
|
+
if char == ' '
|
|
24
|
+
cursor_x += SPACE_WIDTH
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sprite = @chars[char.ord]
|
|
29
|
+
next unless sprite
|
|
30
|
+
|
|
31
|
+
draw_char(framebuffer, sprite, cursor_x, y, screen_width, screen_height)
|
|
32
|
+
cursor_x += sprite.width
|
|
33
|
+
end
|
|
34
|
+
cursor_x - x
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Measure text width without drawing
|
|
38
|
+
def text_width(text)
|
|
39
|
+
width = 0
|
|
40
|
+
text.upcase.each_char do |char|
|
|
41
|
+
if char == ' '
|
|
42
|
+
width += SPACE_WIDTH
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
sprite = @chars[char.ord]
|
|
46
|
+
width += sprite.width if sprite
|
|
47
|
+
end
|
|
48
|
+
width
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Draw text centered horizontally
|
|
52
|
+
def draw_centered(framebuffer, text, y, screen_width: 320, screen_height: 240)
|
|
53
|
+
w = text_width(text)
|
|
54
|
+
x = (screen_width - w) / 2
|
|
55
|
+
draw_text(framebuffer, text, x, y, screen_width: screen_width, screen_height: screen_height)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def draw_char(framebuffer, sprite, x, y, screen_width, screen_height)
|
|
61
|
+
sprite.width.times do |col_x|
|
|
62
|
+
sx = x + col_x
|
|
63
|
+
next if sx < 0 || sx >= screen_width
|
|
64
|
+
|
|
65
|
+
col = sprite.column_pixels(col_x)
|
|
66
|
+
next unless col
|
|
67
|
+
|
|
68
|
+
col.each_with_index do |color, col_y|
|
|
69
|
+
next unless color
|
|
70
|
+
|
|
71
|
+
sy = y + col_y
|
|
72
|
+
next if sy < 0 || sy >= screen_height
|
|
73
|
+
|
|
74
|
+
framebuffer[(sy * screen_width) + sx] = color
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|