quake-rb 0.1.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 +7 -0
- data/bin/quake +143 -0
- data/bin/quake-debug +83 -0
- data/lib/quake/bsp/face_vertices.rb +63 -0
- data/lib/quake/bsp/reader.rb +264 -0
- data/lib/quake/bsp/types.rb +30 -0
- data/lib/quake/bsp/vis.rb +246 -0
- data/lib/quake/camera.rb +99 -0
- data/lib/quake/debug/png_writer.rb +58 -0
- data/lib/quake/debug/screenshot.rb +26 -0
- data/lib/quake/debug/script.rb +179 -0
- data/lib/quake/entity.rb +116 -0
- data/lib/quake/game/brush_entities.rb +361 -0
- data/lib/quake/game/engine.rb +300 -0
- data/lib/quake/game/item_pickups.rb +137 -0
- data/lib/quake/game/player_state.rb +158 -0
- data/lib/quake/math/vec3.rb +35 -0
- data/lib/quake/mdl/reader.rb +176 -0
- data/lib/quake/mdl/types.rb +30 -0
- data/lib/quake/pak/reader.rb +57 -0
- data/lib/quake/pak_downloader.rb +145 -0
- data/lib/quake/palette.rb +32 -0
- data/lib/quake/physics/hull_trace.rb +193 -0
- data/lib/quake/physics/player.rb +357 -0
- data/lib/quake/renderer/gl_alias_model.rb +122 -0
- data/lib/quake/renderer/gl_brush_model.rb +162 -0
- data/lib/quake/renderer/gl_hud.rb +226 -0
- data/lib/quake/renderer/gl_lightmap.rb +261 -0
- data/lib/quake/renderer/gl_particles.rb +173 -0
- data/lib/quake/renderer/gl_sky.rb +166 -0
- data/lib/quake/renderer/gl_texture_manager.rb +54 -0
- data/lib/quake/renderer/gl_textured.rb +224 -0
- data/lib/quake/renderer/gl_viewmodel.rb +109 -0
- data/lib/quake/renderer/gl_water.rb +200 -0
- data/lib/quake/renderer/gl_wireframe.rb +36 -0
- data/lib/quake/sound/events.rb +58 -0
- data/lib/quake/sound/mixer.rb +105 -0
- data/lib/quake/version.rb +5 -0
- data/lib/quake/wad/reader.rb +69 -0
- data/lib/quake/window.rb +74 -0
- data/lib/quake.rb +19 -0
- metadata +140 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opengl"
|
|
4
|
+
|
|
5
|
+
module Quake
|
|
6
|
+
module Renderer
|
|
7
|
+
# Renders turbulent water/lava/slime surfaces with sine-wave warping.
|
|
8
|
+
# Quake liquid textures have names starting with '*'.
|
|
9
|
+
class GLWater
|
|
10
|
+
TURBSCALE = 256.0 / (2.0 * ::Math::PI) # ~40.74
|
|
11
|
+
|
|
12
|
+
SUBDIVIDE_SIZE = 64.0
|
|
13
|
+
|
|
14
|
+
def initialize(level, texture_manager)
|
|
15
|
+
@level = level
|
|
16
|
+
@texture_manager = texture_manager
|
|
17
|
+
@water_surfaces = []
|
|
18
|
+
@time = 0.0
|
|
19
|
+
|
|
20
|
+
# Precompute 256-entry sine table matching Quake's turbsin
|
|
21
|
+
@turbsin = Array.new(256) do |i|
|
|
22
|
+
(::Math.sin(i * 2.0 * ::Math::PI / 256.0) * 8.0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
precompute_water_surfaces
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update(dt)
|
|
29
|
+
@time += dt
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Render water surfaces with turbulent sine-wave warp.
|
|
33
|
+
# alpha: 1.0 = fully opaque (GLQuake default r_wateralpha).
|
|
34
|
+
# Values < 1.0 enable semi-transparent water.
|
|
35
|
+
def render(alpha: 1.0)
|
|
36
|
+
return if @water_surfaces.empty?
|
|
37
|
+
|
|
38
|
+
GL.Enable(GL::TEXTURE_2D)
|
|
39
|
+
# Quake BSP stores liquid surfaces as front/back face pairs on the
|
|
40
|
+
# same plane so the surface is visible from above and below. Without
|
|
41
|
+
# culling both copies rasterize at the same depth, z-fight, and (with
|
|
42
|
+
# alpha < 1) double-blend, producing flicker. Match TyrQuake:
|
|
43
|
+
# glCullFace(GL_FRONT) with default CCW front face.
|
|
44
|
+
GL.Enable(GL::CULL_FACE)
|
|
45
|
+
GL.CullFace(GL::FRONT)
|
|
46
|
+
|
|
47
|
+
if alpha < 1.0
|
|
48
|
+
GL.Enable(GL::BLEND)
|
|
49
|
+
GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA)
|
|
50
|
+
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)
|
|
51
|
+
GL.Color4f(1.0, 1.0, 1.0, alpha)
|
|
52
|
+
else
|
|
53
|
+
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::REPLACE)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@water_surfaces.each do |ws|
|
|
57
|
+
@texture_manager.bind(ws[:miptex_index])
|
|
58
|
+
|
|
59
|
+
ws[:faces].each do |face_data|
|
|
60
|
+
GL.Begin(GL::TRIANGLE_FAN)
|
|
61
|
+
face_data[:verts].each_with_index do |v, i|
|
|
62
|
+
os, ot = face_data[:texcoords][i]
|
|
63
|
+
|
|
64
|
+
# Cross-modulated sine warp
|
|
65
|
+
s = os + @turbsin[((ot * 0.125 + @time) * TURBSCALE).to_i & 255]
|
|
66
|
+
s /= 64.0
|
|
67
|
+
|
|
68
|
+
t = ot + @turbsin[((os * 0.125 + @time) * TURBSCALE).to_i & 255]
|
|
69
|
+
t /= 64.0
|
|
70
|
+
|
|
71
|
+
GL.TexCoord2f(s, t)
|
|
72
|
+
GL.Vertex3f(v.x, v.y, v.z)
|
|
73
|
+
end
|
|
74
|
+
GL.End
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
GL.Disable(GL::CULL_FACE)
|
|
79
|
+
|
|
80
|
+
if alpha < 1.0
|
|
81
|
+
GL.Disable(GL::BLEND)
|
|
82
|
+
GL.Color4f(1.0, 1.0, 1.0, 1.0)
|
|
83
|
+
end
|
|
84
|
+
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def precompute_water_surfaces
|
|
90
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
|
91
|
+
|
|
92
|
+
@level.faces.each do |face|
|
|
93
|
+
texinfo = @level.texinfo[face.texinfo_index]
|
|
94
|
+
next if texinfo.nil?
|
|
95
|
+
|
|
96
|
+
tex = @level.textures[texinfo.miptex_index]
|
|
97
|
+
next if tex.nil?
|
|
98
|
+
next unless tex.name.start_with?("*")
|
|
99
|
+
|
|
100
|
+
# Extract vertices
|
|
101
|
+
face_verts = []
|
|
102
|
+
face.num_edges.times do |i|
|
|
103
|
+
surfedge = @level.surfedges[face.first_edge + i]
|
|
104
|
+
if surfedge >= 0
|
|
105
|
+
edge = @level.edges[surfedge]
|
|
106
|
+
face_verts << @level.vertices[edge.v0]
|
|
107
|
+
else
|
|
108
|
+
edge = @level.edges[-surfedge]
|
|
109
|
+
face_verts << @level.vertices[edge.v1]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Subdivide into smaller polygons for smoother warp effect
|
|
114
|
+
subdivided = subdivide_polygon(face_verts)
|
|
115
|
+
|
|
116
|
+
subdivided.each do |poly_verts|
|
|
117
|
+
texcoords = poly_verts.map do |v|
|
|
118
|
+
# Raw dot product only, no offset (matches TyrQuake SubdividePolygon)
|
|
119
|
+
s = v.dot(texinfo.s_vec)
|
|
120
|
+
t = v.dot(texinfo.t_vec)
|
|
121
|
+
[s, t]
|
|
122
|
+
end
|
|
123
|
+
grouped[texinfo.miptex_index] << { verts: poly_verts, texcoords: texcoords }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
grouped.each do |miptex_index, faces|
|
|
128
|
+
@water_surfaces << { miptex_index: miptex_index, faces: faces }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
puts "Found #{@water_surfaces.sum { |ws| ws[:faces].size }} water surfaces"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Subdivide polygon along axial 64-unit boundaries for smoother warp.
|
|
135
|
+
# Matches TyrQuake's SubdividePolygon.
|
|
136
|
+
def subdivide_polygon(verts)
|
|
137
|
+
polys = [verts]
|
|
138
|
+
|
|
139
|
+
3.times do |axis|
|
|
140
|
+
next_polys = []
|
|
141
|
+
polys.each do |poly|
|
|
142
|
+
split_polygon_axis(poly, axis, next_polys)
|
|
143
|
+
end
|
|
144
|
+
polys = next_polys
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
polys
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def split_polygon_axis(verts, axis, result)
|
|
151
|
+
# Find bounds on this axis
|
|
152
|
+
mins = Float::INFINITY
|
|
153
|
+
maxs = -Float::INFINITY
|
|
154
|
+
verts.each do |v|
|
|
155
|
+
val = v.to_a[axis]
|
|
156
|
+
mins = val if val < mins
|
|
157
|
+
maxs = val if val > maxs
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# If small enough, no split needed
|
|
161
|
+
if maxs - mins <= SUBDIVIDE_SIZE
|
|
162
|
+
result << verts
|
|
163
|
+
return
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Find split plane (first 64-unit boundary)
|
|
167
|
+
dist = ((mins + SUBDIVIDE_SIZE) / SUBDIVIDE_SIZE).floor * SUBDIVIDE_SIZE
|
|
168
|
+
# Ensure we're actually splitting
|
|
169
|
+
dist = mins + SUBDIVIDE_SIZE if dist <= mins
|
|
170
|
+
|
|
171
|
+
front = []
|
|
172
|
+
back = []
|
|
173
|
+
|
|
174
|
+
verts.each_with_index do |v, i|
|
|
175
|
+
v_next = verts[(i + 1) % verts.size]
|
|
176
|
+
d1 = v.to_a[axis] - dist
|
|
177
|
+
d2 = v_next.to_a[axis] - dist
|
|
178
|
+
|
|
179
|
+
front << v if d1 >= 0
|
|
180
|
+
back << v if d1 < 0
|
|
181
|
+
|
|
182
|
+
# Edge crosses the split plane
|
|
183
|
+
if (d1 >= 0) != (d2 >= 0)
|
|
184
|
+
frac = d1 / (d1 - d2)
|
|
185
|
+
mid = Quake::Math::Vec3.new(
|
|
186
|
+
v.x + frac * (v_next.x - v.x),
|
|
187
|
+
v.y + frac * (v_next.y - v.y),
|
|
188
|
+
v.z + frac * (v_next.z - v.z)
|
|
189
|
+
)
|
|
190
|
+
front << mid
|
|
191
|
+
back << mid
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
split_polygon_axis(front, axis, result) if front.size >= 3
|
|
196
|
+
split_polygon_axis(back, axis, result) if back.size >= 3
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opengl"
|
|
4
|
+
|
|
5
|
+
module Quake
|
|
6
|
+
module Renderer
|
|
7
|
+
class GLWireframe
|
|
8
|
+
def initialize(level)
|
|
9
|
+
@level = level
|
|
10
|
+
@face_polygons = precompute_polygons
|
|
11
|
+
puts "Precomputed #{@face_polygons.size} face polygons"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render(camera, aspect)
|
|
15
|
+
GL.Clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT)
|
|
16
|
+
|
|
17
|
+
camera.apply_projection_gl(aspect)
|
|
18
|
+
camera.apply_gl
|
|
19
|
+
|
|
20
|
+
GL.Color3f(0.0, 1.0, 0.0)
|
|
21
|
+
|
|
22
|
+
@face_polygons.each do |verts|
|
|
23
|
+
GL.Begin(GL::LINE_LOOP)
|
|
24
|
+
verts.each { |v| GL.Vertex3f(v.x, v.y, v.z) }
|
|
25
|
+
GL.End
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def precompute_polygons
|
|
32
|
+
@level.faces.map { |face| Bsp::FaceVertices.extract(@level, face) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quake
|
|
4
|
+
module Sound
|
|
5
|
+
# Maps game events to sound file paths and plays them.
|
|
6
|
+
class Events
|
|
7
|
+
# Item pickup sounds
|
|
8
|
+
PICKUP_SOUNDS = {
|
|
9
|
+
health: "items/health1.wav",
|
|
10
|
+
armor: "items/armor1.wav",
|
|
11
|
+
ammo: "weapons/lock4.wav",
|
|
12
|
+
weapon: "weapons/pkup.wav",
|
|
13
|
+
powerup: "items/protect.wav"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# Door/platform sounds
|
|
17
|
+
DOOR_SOUNDS = {
|
|
18
|
+
open: "doors/medtry.wav",
|
|
19
|
+
close: "doors/medclose.wav"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
PLAT_SOUNDS = {
|
|
23
|
+
move: "plats/medplat1.wav",
|
|
24
|
+
stop: "plats/medplat2.wav"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Player sounds
|
|
28
|
+
PLAYER_SOUNDS = {
|
|
29
|
+
jump: "player/plyrjmp8.wav",
|
|
30
|
+
land: "player/land.wav",
|
|
31
|
+
water_enter: "player/inh2o.wav",
|
|
32
|
+
water_exit: "player/inlava.wav"
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def initialize(mixer)
|
|
36
|
+
@mixer = mixer
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def on_pickup(event)
|
|
40
|
+
return unless @mixer&.loaded?
|
|
41
|
+
sound = PICKUP_SOUNDS[event[:type]]
|
|
42
|
+
@mixer.play(sound) if sound
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def on_door(action)
|
|
46
|
+
return unless @mixer&.loaded?
|
|
47
|
+
sound = DOOR_SOUNDS[action]
|
|
48
|
+
@mixer.play(sound) if sound
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_player(action)
|
|
52
|
+
return unless @mixer&.loaded?
|
|
53
|
+
sound = PLAYER_SOUNDS[action]
|
|
54
|
+
@mixer.play(sound) if sound
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module Quake
|
|
6
|
+
module Sound
|
|
7
|
+
# Low-level FFI bindings to SDL2_mixer for WAV playback.
|
|
8
|
+
module MixerLib
|
|
9
|
+
extend FFI::Library
|
|
10
|
+
ffi_lib "/opt/homebrew/lib/libSDL2_mixer.dylib"
|
|
11
|
+
|
|
12
|
+
MIX_DEFAULT_FORMAT = 0x8010 # AUDIO_S16LSB
|
|
13
|
+
|
|
14
|
+
attach_function :Mix_OpenAudio, [:int, :uint16, :int, :int], :int
|
|
15
|
+
attach_function :Mix_CloseAudio, [], :void
|
|
16
|
+
attach_function :Mix_AllocateChannels, [:int], :int
|
|
17
|
+
attach_function :Mix_LoadWAV_RW, [:pointer, :int], :pointer
|
|
18
|
+
attach_function :Mix_FreeChunk, [:pointer], :void
|
|
19
|
+
attach_function :Mix_PlayChannelTimed, [:int, :pointer, :int, :int], :int
|
|
20
|
+
attach_function :Mix_Volume, [:int, :int], :int
|
|
21
|
+
attach_function :Mix_HaltChannel, [:int], :int
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# High-level sound manager: loads WAVs from PAK, plays them by name.
|
|
25
|
+
class Mixer
|
|
26
|
+
MAX_CHANNELS = 16
|
|
27
|
+
|
|
28
|
+
def initialize(pak)
|
|
29
|
+
@pak = pak
|
|
30
|
+
@chunks = {} # sound_path -> FFI pointer
|
|
31
|
+
@open = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def open
|
|
35
|
+
result = MixerLib.Mix_OpenAudio(22050, MixerLib::MIX_DEFAULT_FORMAT, 2, 1024)
|
|
36
|
+
if result < 0
|
|
37
|
+
puts "Warning: Could not open audio"
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
MixerLib.Mix_AllocateChannels(MAX_CHANNELS)
|
|
41
|
+
@open = true
|
|
42
|
+
puts "Audio initialized (#{MAX_CHANNELS} channels)"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def close
|
|
46
|
+
@chunks.each_value { |chunk| MixerLib.Mix_FreeChunk(chunk) }
|
|
47
|
+
@chunks.clear
|
|
48
|
+
MixerLib.Mix_CloseAudio if @open
|
|
49
|
+
@open = false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Play a sound from the PAK. path is relative (e.g. "items/health1.wav").
|
|
53
|
+
# Returns channel number or -1 on failure.
|
|
54
|
+
def play(path, volume: 128, loop_count: 0)
|
|
55
|
+
return -1 unless @open
|
|
56
|
+
|
|
57
|
+
full_path = path.start_with?("sound/") ? path : "sound/#{path}"
|
|
58
|
+
chunk = load_chunk(full_path)
|
|
59
|
+
return -1 unless chunk
|
|
60
|
+
|
|
61
|
+
MixerLib.Mix_Volume(-1, volume)
|
|
62
|
+
MixerLib.Mix_PlayChannelTimed(-1, chunk, loop_count, -1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def loaded?
|
|
66
|
+
@open
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def load_chunk(path)
|
|
72
|
+
return @chunks[path] if @chunks.key?(path)
|
|
73
|
+
|
|
74
|
+
data = @pak.read(path)
|
|
75
|
+
unless data
|
|
76
|
+
@chunks[path] = nil
|
|
77
|
+
return nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Create SDL_RWops from memory and load as WAV
|
|
81
|
+
rw = sdl_rw_from_mem(data)
|
|
82
|
+
chunk = MixerLib.Mix_LoadWAV_RW(rw, 1) # 1 = free RW after load
|
|
83
|
+
if chunk.null?
|
|
84
|
+
@chunks[path] = nil
|
|
85
|
+
return nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@chunks[path] = chunk
|
|
89
|
+
chunk
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def sdl_rw_from_mem(data)
|
|
93
|
+
# Use SDL's RWFromMem
|
|
94
|
+
ptr = FFI::MemoryPointer.new(:uint8, data.bytesize)
|
|
95
|
+
ptr.put_bytes(0, data)
|
|
96
|
+
|
|
97
|
+
# Keep reference to prevent GC
|
|
98
|
+
@mem_refs ||= []
|
|
99
|
+
@mem_refs << ptr
|
|
100
|
+
|
|
101
|
+
SDL.RWFromMem(ptr, data.bytesize)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quake
|
|
4
|
+
module Wad
|
|
5
|
+
# Reads WAD2 archives (Quake's texture/picture format).
|
|
6
|
+
# Used primarily for gfx.wad which contains HUD graphics.
|
|
7
|
+
class Reader
|
|
8
|
+
MAGIC = "WAD2"
|
|
9
|
+
|
|
10
|
+
Entry = Data.define(:name, :offset, :size, :type)
|
|
11
|
+
|
|
12
|
+
# QPic: width(4) + height(4) + indexed pixels
|
|
13
|
+
QPic = Data.define(:width, :height, :pixels)
|
|
14
|
+
|
|
15
|
+
def initialize(data)
|
|
16
|
+
@data = data
|
|
17
|
+
@entries = {}
|
|
18
|
+
parse_directory
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list
|
|
22
|
+
@entries.keys
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read(name)
|
|
26
|
+
entry = @entries[name.downcase]
|
|
27
|
+
return nil unless entry
|
|
28
|
+
@data[entry.offset, entry.size]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Read a QPic (status bar graphic): 4-byte width, 4-byte height, pixels
|
|
32
|
+
def read_qpic(name)
|
|
33
|
+
entry = @entries[name.downcase]
|
|
34
|
+
return nil unless entry
|
|
35
|
+
|
|
36
|
+
raw = @data[entry.offset, entry.size]
|
|
37
|
+
return nil unless raw && raw.bytesize >= 8
|
|
38
|
+
|
|
39
|
+
width, height = raw[0, 8].unpack("VV")
|
|
40
|
+
pixels = raw[8, width * height]
|
|
41
|
+
QPic.new(width: width, height: height, pixels: pixels)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def parse_directory
|
|
47
|
+
return if @data.bytesize < 12
|
|
48
|
+
|
|
49
|
+
magic = @data[0, 4]
|
|
50
|
+
return unless magic == MAGIC
|
|
51
|
+
|
|
52
|
+
numlumps, infotableofs = @data[4, 8].unpack("VV")
|
|
53
|
+
|
|
54
|
+
numlumps.times do |i|
|
|
55
|
+
offset = infotableofs + i * 32
|
|
56
|
+
break if offset + 32 > @data.bytesize
|
|
57
|
+
|
|
58
|
+
filepos, disksize, _size, type = @data[offset, 16].unpack("VVVC")
|
|
59
|
+
_compression = @data[offset + 13].unpack1("C")
|
|
60
|
+
name = @data[offset + 16, 16].unpack1("Z16").downcase
|
|
61
|
+
|
|
62
|
+
@entries[name] = Entry.new(
|
|
63
|
+
name: name, offset: filepos, size: disksize, type: type
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/quake/window.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sdl2"
|
|
4
|
+
require "opengl"
|
|
5
|
+
require "glu"
|
|
6
|
+
|
|
7
|
+
module Quake
|
|
8
|
+
class Window
|
|
9
|
+
DEFAULT_WIDTH = 1280
|
|
10
|
+
DEFAULT_HEIGHT = 720
|
|
11
|
+
TITLE = "RubyQuake"
|
|
12
|
+
|
|
13
|
+
attr_reader :width, :height
|
|
14
|
+
|
|
15
|
+
def initialize(width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, visible: true)
|
|
16
|
+
@width = width
|
|
17
|
+
@height = height
|
|
18
|
+
@visible = visible
|
|
19
|
+
|
|
20
|
+
SDL.load_lib("/opt/homebrew/lib/libSDL2.dylib")
|
|
21
|
+
SDL.Init(SDL::INIT_VIDEO)
|
|
22
|
+
|
|
23
|
+
SDL.GL_SetAttribute(SDL::GL_CONTEXT_MAJOR_VERSION, 2)
|
|
24
|
+
SDL.GL_SetAttribute(SDL::GL_CONTEXT_MINOR_VERSION, 1)
|
|
25
|
+
SDL.GL_SetAttribute(SDL::GL_DOUBLEBUFFER, 1)
|
|
26
|
+
SDL.GL_SetAttribute(SDL::GL_DEPTH_SIZE, 24)
|
|
27
|
+
|
|
28
|
+
window_flags = SDL::WINDOW_OPENGL
|
|
29
|
+
window_flags |= visible ? SDL::WINDOW_SHOWN : SDL::WINDOW_HIDDEN
|
|
30
|
+
|
|
31
|
+
@window = SDL.CreateWindow(
|
|
32
|
+
TITLE,
|
|
33
|
+
SDL::WINDOWPOS_CENTERED_MASK,
|
|
34
|
+
SDL::WINDOWPOS_CENTERED_MASK,
|
|
35
|
+
width, height,
|
|
36
|
+
window_flags
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@context = SDL.GL_CreateContext(@window)
|
|
40
|
+
SDL.GL_MakeCurrent(@window, @context)
|
|
41
|
+
SDL.GL_SetSwapInterval(visible ? 1 : 0) # vsync only for visible
|
|
42
|
+
|
|
43
|
+
GL.load_lib
|
|
44
|
+
GLU.load_lib
|
|
45
|
+
|
|
46
|
+
GL.Viewport(0, 0, width, height)
|
|
47
|
+
GL.Enable(GL::DEPTH_TEST)
|
|
48
|
+
GL.ClearColor(0.0, 0.0, 0.0, 1.0)
|
|
49
|
+
|
|
50
|
+
SDL.SetRelativeMouseMode(SDL::TRUE) if visible
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def swap
|
|
54
|
+
SDL.GL_SwapWindow(@window)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def poll_events
|
|
58
|
+
@event ||= SDL::Event.new
|
|
59
|
+
while SDL.PollEvent(@event.to_ptr) != 0
|
|
60
|
+
yield @event
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def aspect_ratio
|
|
65
|
+
@width.to_f / @height
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def close
|
|
69
|
+
SDL.GL_DeleteContext(@context)
|
|
70
|
+
SDL.DestroyWindow(@window)
|
|
71
|
+
SDL.Quit
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/quake.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quake
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
require_relative "quake/version"
|
|
7
|
+
require_relative "quake/math/vec3"
|
|
8
|
+
require_relative "quake/pak/reader"
|
|
9
|
+
require_relative "quake/bsp/types"
|
|
10
|
+
require_relative "quake/bsp/reader"
|
|
11
|
+
require_relative "quake/bsp/face_vertices"
|
|
12
|
+
require_relative "quake/bsp/vis"
|
|
13
|
+
require_relative "quake/palette"
|
|
14
|
+
require_relative "quake/entity"
|
|
15
|
+
require_relative "quake/mdl/types"
|
|
16
|
+
require_relative "quake/mdl/reader"
|
|
17
|
+
require_relative "quake/physics/hull_trace"
|
|
18
|
+
require_relative "quake/physics/player"
|
|
19
|
+
require_relative "quake/game/brush_entities"
|
metadata
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: quake-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chris Hasinski
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: opengl-bindings2
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sdl2-bindings
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
description: A port of the Quake (1996) engine to Ruby. Reads original BSP/MDL/PAK
|
|
69
|
+
assets and renders them via OpenGL + SDL2.
|
|
70
|
+
email:
|
|
71
|
+
- krzysztof.hasinski@gmail.com
|
|
72
|
+
executables:
|
|
73
|
+
- quake
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- bin/quake
|
|
78
|
+
- bin/quake-debug
|
|
79
|
+
- lib/quake.rb
|
|
80
|
+
- lib/quake/bsp/face_vertices.rb
|
|
81
|
+
- lib/quake/bsp/reader.rb
|
|
82
|
+
- lib/quake/bsp/types.rb
|
|
83
|
+
- lib/quake/bsp/vis.rb
|
|
84
|
+
- lib/quake/camera.rb
|
|
85
|
+
- lib/quake/debug/png_writer.rb
|
|
86
|
+
- lib/quake/debug/screenshot.rb
|
|
87
|
+
- lib/quake/debug/script.rb
|
|
88
|
+
- lib/quake/entity.rb
|
|
89
|
+
- lib/quake/game/brush_entities.rb
|
|
90
|
+
- lib/quake/game/engine.rb
|
|
91
|
+
- lib/quake/game/item_pickups.rb
|
|
92
|
+
- lib/quake/game/player_state.rb
|
|
93
|
+
- lib/quake/math/vec3.rb
|
|
94
|
+
- lib/quake/mdl/reader.rb
|
|
95
|
+
- lib/quake/mdl/types.rb
|
|
96
|
+
- lib/quake/pak/reader.rb
|
|
97
|
+
- lib/quake/pak_downloader.rb
|
|
98
|
+
- lib/quake/palette.rb
|
|
99
|
+
- lib/quake/physics/hull_trace.rb
|
|
100
|
+
- lib/quake/physics/player.rb
|
|
101
|
+
- lib/quake/renderer/gl_alias_model.rb
|
|
102
|
+
- lib/quake/renderer/gl_brush_model.rb
|
|
103
|
+
- lib/quake/renderer/gl_hud.rb
|
|
104
|
+
- lib/quake/renderer/gl_lightmap.rb
|
|
105
|
+
- lib/quake/renderer/gl_particles.rb
|
|
106
|
+
- lib/quake/renderer/gl_sky.rb
|
|
107
|
+
- lib/quake/renderer/gl_texture_manager.rb
|
|
108
|
+
- lib/quake/renderer/gl_textured.rb
|
|
109
|
+
- lib/quake/renderer/gl_viewmodel.rb
|
|
110
|
+
- lib/quake/renderer/gl_water.rb
|
|
111
|
+
- lib/quake/renderer/gl_wireframe.rb
|
|
112
|
+
- lib/quake/sound/events.rb
|
|
113
|
+
- lib/quake/sound/mixer.rb
|
|
114
|
+
- lib/quake/version.rb
|
|
115
|
+
- lib/quake/wad/reader.rb
|
|
116
|
+
- lib/quake/window.rb
|
|
117
|
+
homepage: https://github.com/khasinski/quake
|
|
118
|
+
licenses:
|
|
119
|
+
- GPL-2.0-only
|
|
120
|
+
metadata:
|
|
121
|
+
source_code_uri: https://github.com/khasinski/quake
|
|
122
|
+
bug_tracker_uri: https://github.com/khasinski/quake/issues
|
|
123
|
+
rdoc_options: []
|
|
124
|
+
require_paths:
|
|
125
|
+
- lib
|
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '3.1'
|
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '0'
|
|
136
|
+
requirements: []
|
|
137
|
+
rubygems_version: 4.0.3
|
|
138
|
+
specification_version: 4
|
|
139
|
+
summary: Quake engine port in Ruby
|
|
140
|
+
test_files: []
|