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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/bin/quake +143 -0
  3. data/bin/quake-debug +83 -0
  4. data/lib/quake/bsp/face_vertices.rb +63 -0
  5. data/lib/quake/bsp/reader.rb +264 -0
  6. data/lib/quake/bsp/types.rb +30 -0
  7. data/lib/quake/bsp/vis.rb +246 -0
  8. data/lib/quake/camera.rb +99 -0
  9. data/lib/quake/debug/png_writer.rb +58 -0
  10. data/lib/quake/debug/screenshot.rb +26 -0
  11. data/lib/quake/debug/script.rb +179 -0
  12. data/lib/quake/entity.rb +116 -0
  13. data/lib/quake/game/brush_entities.rb +361 -0
  14. data/lib/quake/game/engine.rb +300 -0
  15. data/lib/quake/game/item_pickups.rb +137 -0
  16. data/lib/quake/game/player_state.rb +158 -0
  17. data/lib/quake/math/vec3.rb +35 -0
  18. data/lib/quake/mdl/reader.rb +176 -0
  19. data/lib/quake/mdl/types.rb +30 -0
  20. data/lib/quake/pak/reader.rb +57 -0
  21. data/lib/quake/pak_downloader.rb +145 -0
  22. data/lib/quake/palette.rb +32 -0
  23. data/lib/quake/physics/hull_trace.rb +193 -0
  24. data/lib/quake/physics/player.rb +357 -0
  25. data/lib/quake/renderer/gl_alias_model.rb +122 -0
  26. data/lib/quake/renderer/gl_brush_model.rb +162 -0
  27. data/lib/quake/renderer/gl_hud.rb +226 -0
  28. data/lib/quake/renderer/gl_lightmap.rb +261 -0
  29. data/lib/quake/renderer/gl_particles.rb +173 -0
  30. data/lib/quake/renderer/gl_sky.rb +166 -0
  31. data/lib/quake/renderer/gl_texture_manager.rb +54 -0
  32. data/lib/quake/renderer/gl_textured.rb +224 -0
  33. data/lib/quake/renderer/gl_viewmodel.rb +109 -0
  34. data/lib/quake/renderer/gl_water.rb +200 -0
  35. data/lib/quake/renderer/gl_wireframe.rb +36 -0
  36. data/lib/quake/sound/events.rb +58 -0
  37. data/lib/quake/sound/mixer.rb +105 -0
  38. data/lib/quake/version.rb +5 -0
  39. data/lib/quake/wad/reader.rb +69 -0
  40. data/lib/quake/window.rb +74 -0
  41. data/lib/quake.rb +19 -0
  42. metadata +140 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 76a4b7671e6182b04a0257b7ad6a229cefc9e5dc2e3c763529327e376b910f80
4
+ data.tar.gz: d855437be72bfaec56741b68bfd0497c578d21e67501c8383164f3c8ebd9abd6
5
+ SHA512:
6
+ metadata.gz: 83f646634ab2dafd538fa3ab73814218871e01f0e09c2a10108f5a487e4d70476cfba4cccd15269887ee8f9ce05720b0c65739dd76f793b959f419b4bd1ce5ea
7
+ data.tar.gz: 877663d022ddfedb05cd9c0b3b630d04dcfdef084345f0e4183ea22733d351f87968105e062567efa090ca3d147ad8df8d9c46dbfdb65f3f8a71ec60761cfa61
data/bin/quake ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ STDOUT.sync = true
5
+
6
+ require_relative "../lib/quake"
7
+ require_relative "../lib/quake/pak_downloader"
8
+ require_relative "../lib/quake/window"
9
+ require_relative "../lib/quake/camera"
10
+ require_relative "../lib/quake/renderer/gl_wireframe"
11
+ require_relative "../lib/quake/renderer/gl_texture_manager"
12
+ require_relative "../lib/quake/renderer/gl_lightmap"
13
+ require_relative "../lib/quake/renderer/gl_textured"
14
+ require_relative "../lib/quake/renderer/gl_sky"
15
+ require_relative "../lib/quake/renderer/gl_water"
16
+ require_relative "../lib/quake/renderer/gl_brush_model"
17
+ require_relative "../lib/quake/renderer/gl_alias_model"
18
+ require_relative "../lib/quake/renderer/gl_viewmodel"
19
+ require_relative "../lib/quake/renderer/gl_particles"
20
+ require_relative "../lib/quake/renderer/gl_hud"
21
+ require_relative "../lib/quake/wad/reader"
22
+ require_relative "../lib/quake/game/player_state"
23
+ require_relative "../lib/quake/game/item_pickups"
24
+ require_relative "../lib/quake/sound/mixer"
25
+ require_relative "../lib/quake/sound/events"
26
+ require_relative "../lib/quake/game/engine"
27
+
28
+ # Parse args
29
+ if ARGV.include?("-v") || ARGV.include?("--version")
30
+ puts "quake-rb v#{Quake::VERSION}"
31
+ exit 0
32
+ end
33
+
34
+ basedir = ARGV.include?("-basedir") ? ARGV[ARGV.index("-basedir") + 1] : nil
35
+ map_name = ARGV.include?("-map") ? ARGV[ARGV.index("-map") + 1] : "maps/e1m1.bsp"
36
+ noclip = ARGV.include?("--noclip")
37
+
38
+ if ARGV.include?("--yjit")
39
+ RubyVM::YJIT.enable
40
+ puts "YJIT enabled"
41
+ elsif ARGV.include?("--zjit")
42
+ RubyVM::ZJIT.enable
43
+ puts "ZJIT enabled"
44
+ end
45
+
46
+ begin
47
+ basedir = Quake::PakDownloader.ensure_pak_available(basedir)
48
+ rescue Quake::PakDownloader::DownloadError => e
49
+ warn "Error: #{e.message}"
50
+ exit 1
51
+ end
52
+
53
+ pak_path = File.join(basedir, "id1", "pak0.pak")
54
+
55
+ engine = Quake::Game::Engine.new(
56
+ pak_path: pak_path,
57
+ window_visible: true,
58
+ enable_sound: true
59
+ )
60
+ engine.load_map(map_name)
61
+ engine.player.noclip = noclip
62
+
63
+ window = engine.window
64
+ keys = {}
65
+ running = true
66
+ last_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
67
+
68
+ puts "Controls: WASD = move, Mouse = look, Space = jump, C = crouch, N = noclip, 1-8/scroll = weapons, ESC = quit"
69
+ puts "Movement mode: #{noclip ? 'noclip' : 'normal'}"
70
+
71
+ while running
72
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
+ dt = [now - last_time, 0.05].min
74
+ last_time = now
75
+
76
+ window.poll_events do |event|
77
+ case event[:type]
78
+ when SDL::QUIT
79
+ running = false
80
+ when SDL::KEYDOWN
81
+ scancode = event[:key][:keysym][:scancode]
82
+ keys[scancode] = true
83
+ running = false if scancode == SDL::SCANCODE_ESCAPE
84
+ if scancode == SDL::SCANCODE_N
85
+ engine.player.noclip = !engine.player.noclip
86
+ puts "Noclip: #{engine.player.noclip ? 'ON' : 'OFF'}"
87
+ end
88
+ if scancode == SDL::SCANCODE_L
89
+ p = engine.player.position
90
+ leaf_idx = Quake::Bsp::Vis.point_in_leaf(engine.level, p)
91
+ leaf = engine.level.leafs[leaf_idx]
92
+ contents = case leaf&.contents
93
+ when -1 then "EMPTY"
94
+ when -2 then "SOLID"
95
+ when -3 then "WATER"
96
+ when -4 then "SLIME"
97
+ when -5 then "LAVA"
98
+ else leaf&.contents.to_s
99
+ end
100
+ ts = Time.now.strftime("%H%M%S")
101
+ shot = "debug/shots/live_#{ts}.png"
102
+ engine.screenshot(shot)
103
+ puts "[log] pos=(#{p.x.round(1)}, #{p.y.round(1)}, #{p.z.round(1)}) " \
104
+ "yaw=#{engine.player.yaw.round(1)} pitch=#{engine.player.pitch.round(1)} " \
105
+ "leaf=#{leaf_idx} contents=#{contents} water_level=#{engine.player.water_level} " \
106
+ "shot=#{shot}"
107
+ end
108
+
109
+ weapon_slot = case scancode
110
+ when SDL::SCANCODE_1 then 1
111
+ when SDL::SCANCODE_2 then 2
112
+ when SDL::SCANCODE_3 then 3
113
+ when SDL::SCANCODE_4 then 4
114
+ when SDL::SCANCODE_5 then 5
115
+ when SDL::SCANCODE_6 then 6
116
+ when SDL::SCANCODE_7 then 7
117
+ when SDL::SCANCODE_8 then 8
118
+ end
119
+ if weapon_slot
120
+ engine.player_state.select_weapon(weapon_slot)
121
+ engine.viewmodel&.set_weapon(engine.player_state.current_weapon_model)
122
+ end
123
+ when SDL::KEYUP
124
+ scancode = event[:key][:keysym][:scancode]
125
+ keys[scancode] = false
126
+ when SDL::MOUSEWHEEL
127
+ if event[:wheel][:y] > 0
128
+ engine.player_state.next_weapon
129
+ elsif event[:wheel][:y] < 0
130
+ engine.player_state.prev_weapon
131
+ end
132
+ engine.viewmodel&.set_weapon(engine.player_state.current_weapon_model)
133
+ when SDL::MOUSEMOTION
134
+ engine.player.rotate(event[:motion][:xrel], event[:motion][:yrel])
135
+ end
136
+ end
137
+
138
+ engine.set_keys(keys)
139
+ engine.tick(dt)
140
+ engine.swap_buffers
141
+ end
142
+
143
+ engine.shutdown
data/bin/quake-debug ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # Headless debug runner for Quake. Executes a script that drives the
5
+ # engine without user input, captures screenshots, and dumps state.
6
+ #
7
+ # Usage:
8
+ # bin/quake-debug script.rb [options]
9
+ #
10
+ # Options:
11
+ # --visible Show the window (default: hidden)
12
+ # --width N Window width (default: 640)
13
+ # --height N Window height (default: 360)
14
+ # --no-sound Disable sound (default: disabled in debug)
15
+
16
+ require_relative "../lib/quake"
17
+ require_relative "../lib/quake/window"
18
+ require_relative "../lib/quake/camera"
19
+ require_relative "../lib/quake/renderer/gl_texture_manager"
20
+ require_relative "../lib/quake/renderer/gl_lightmap"
21
+ require_relative "../lib/quake/renderer/gl_textured"
22
+ require_relative "../lib/quake/renderer/gl_sky"
23
+ require_relative "../lib/quake/renderer/gl_water"
24
+ require_relative "../lib/quake/renderer/gl_brush_model"
25
+ require_relative "../lib/quake/renderer/gl_alias_model"
26
+ require_relative "../lib/quake/renderer/gl_viewmodel"
27
+ require_relative "../lib/quake/renderer/gl_particles"
28
+ require_relative "../lib/quake/renderer/gl_hud"
29
+ require_relative "../lib/quake/wad/reader"
30
+ require_relative "../lib/quake/game/player_state"
31
+ require_relative "../lib/quake/game/item_pickups"
32
+ require_relative "../lib/quake/sound/mixer"
33
+ require_relative "../lib/quake/sound/events"
34
+ require_relative "../lib/quake/game/engine"
35
+ require_relative "../lib/quake/debug/script"
36
+
37
+ script_path = ARGV.find { |a| !a.start_with?("--") }
38
+ unless script_path
39
+ warn "Usage: bin/quake-debug <script.rb> [--visible] [--width N] [--height N]"
40
+ exit 1
41
+ end
42
+
43
+ unless File.exist?(script_path)
44
+ warn "Script not found: #{script_path}"
45
+ exit 1
46
+ end
47
+
48
+ visible = ARGV.include?("--visible")
49
+ enable_sound = !ARGV.include?("--no-sound") && visible # sound off by default in debug
50
+
51
+ width_idx = ARGV.index("--width")
52
+ height_idx = ARGV.index("--height")
53
+ width = width_idx ? ARGV[width_idx + 1].to_i : 640
54
+ height = height_idx ? ARGV[height_idx + 1].to_i : 360
55
+
56
+ basedir = ARGV.include?("-basedir") ? ARGV[ARGV.index("-basedir") + 1] : "data"
57
+ pak_path = File.join(basedir, "id1", "pak0.pak")
58
+
59
+ puts "[quake-debug] window=#{width}x#{height} visible=#{visible} sound=#{enable_sound}"
60
+ puts "[quake-debug] pak=#{pak_path}"
61
+ puts "[quake-debug] script=#{script_path}"
62
+
63
+ engine = Quake::Game::Engine.new(
64
+ pak_path: pak_path,
65
+ window_visible: visible,
66
+ window_width: width,
67
+ window_height: height,
68
+ enable_sound: enable_sound
69
+ )
70
+
71
+ dsl = Quake::Debug::Script.new(engine)
72
+
73
+ begin
74
+ dsl.run_file(script_path)
75
+ rescue => e
76
+ warn "[quake-debug] Script failed: #{e.class}: #{e.message}"
77
+ warn e.backtrace.first(10).join("\n")
78
+ exit 1
79
+ ensure
80
+ engine.shutdown
81
+ end
82
+
83
+ puts "[quake-debug] done"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quake
4
+ module Bsp
5
+ module FaceVertices
6
+ def self.extract(level, face)
7
+ verts = Array.new(face.num_edges)
8
+ face.num_edges.times do |i|
9
+ surfedge = level.surfedges[face.first_edge + i]
10
+ if surfedge >= 0
11
+ edge = level.edges[surfedge]
12
+ verts[i] = level.vertices[edge.v0]
13
+ else
14
+ edge = level.edges[-surfedge]
15
+ verts[i] = level.vertices[edge.v1]
16
+ end
17
+ end
18
+ verts
19
+ end
20
+
21
+ # Extract vertices + texture coordinates for a face
22
+ # Returns a Surface with vertices, texcoords, and texinfo_index
23
+ def self.extract_surface(level, face)
24
+ texinfo = level.texinfo[face.texinfo_index]
25
+ texture = level.textures[texinfo.miptex_index] if texinfo
26
+
27
+ verts = Array.new(face.num_edges)
28
+ texcoords = Array.new(face.num_edges)
29
+
30
+ tex_w = (texture&.width || 64).to_f
31
+ tex_h = (texture&.height || 64).to_f
32
+
33
+ face.num_edges.times do |i|
34
+ surfedge = level.surfedges[face.first_edge + i]
35
+ if surfedge >= 0
36
+ edge = level.edges[surfedge]
37
+ v = level.vertices[edge.v0]
38
+ else
39
+ edge = level.edges[-surfedge]
40
+ v = level.vertices[edge.v1]
41
+ end
42
+
43
+ verts[i] = v
44
+
45
+ if texinfo
46
+ u = v.dot(texinfo.s_vec) + texinfo.s_offset
47
+ u /= tex_w
48
+
49
+ t = v.dot(texinfo.t_vec) + texinfo.t_offset
50
+ t /= tex_h
51
+
52
+ texcoords[i] = [u, t]
53
+ else
54
+ texcoords[i] = [0.0, 0.0]
55
+ end
56
+ end
57
+
58
+ Surface.new(vertices: verts, texcoords: texcoords,
59
+ texinfo_index: face.texinfo_index)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quake
4
+ module Bsp
5
+ class Reader
6
+ BSP_VERSION = 29
7
+
8
+ LUMP_ENTITIES = 0
9
+ LUMP_PLANES = 1
10
+ LUMP_TEXTURES = 2
11
+ LUMP_VERTICES = 3
12
+ LUMP_VISIBILITY = 4
13
+ LUMP_NODES = 5
14
+ LUMP_TEXINFO = 6
15
+ LUMP_FACES = 7
16
+ LUMP_LIGHTING = 8
17
+ LUMP_CLIPNODES = 9
18
+ LUMP_LEAFS = 10
19
+ LUMP_MARKSURFACES = 11
20
+ LUMP_EDGES = 12
21
+ LUMP_SURFEDGES = 13
22
+ LUMP_MODELS = 14
23
+
24
+ NUM_LUMPS = 15
25
+
26
+ def initialize(data)
27
+ @data = data
28
+ @lumps = []
29
+ end
30
+
31
+ def parse
32
+ parse_header
33
+
34
+ Level.new(
35
+ vertices: parse_vertices,
36
+ edges: parse_edges,
37
+ faces: parse_faces,
38
+ surfedges: parse_surfedges,
39
+ planes: parse_planes,
40
+ nodes: parse_nodes,
41
+ leafs: parse_leafs,
42
+ clipnodes: parse_clipnodes,
43
+ texinfo: parse_texinfo,
44
+ models: parse_models,
45
+ marksurfaces: parse_marksurfaces,
46
+ entities: parse_entities,
47
+ visibility: parse_visibility,
48
+ lighting: parse_lighting,
49
+ textures: parse_textures
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ def parse_header
56
+ version = @data[0, 4].unpack1("l<")
57
+ raise "Unsupported BSP version: #{version} (expected #{BSP_VERSION})" unless version == BSP_VERSION
58
+
59
+ NUM_LUMPS.times do |i|
60
+ offset = 4 + i * 8
61
+ fileofs, filelen = @data[offset, 8].unpack("l<2")
62
+ @lumps << [fileofs, filelen]
63
+ end
64
+ end
65
+
66
+ def lump_data(index)
67
+ offset, length = @lumps[index]
68
+ @data[offset, length]
69
+ end
70
+
71
+ def parse_vertices
72
+ data = lump_data(LUMP_VERTICES)
73
+ count = data.bytesize / 12
74
+ result = Array.new(count)
75
+ count.times do |i|
76
+ x, y, z = data[i * 12, 12].unpack("e3")
77
+ result[i] = Math::Vec3.new(x, y, z)
78
+ end
79
+ result
80
+ end
81
+
82
+ def parse_edges
83
+ data = lump_data(LUMP_EDGES)
84
+ count = data.bytesize / 4
85
+ result = Array.new(count)
86
+ count.times do |i|
87
+ v0, v1 = data[i * 4, 4].unpack("v2")
88
+ result[i] = Edge.new(v0:, v1:)
89
+ end
90
+ result
91
+ end
92
+
93
+ def parse_faces
94
+ data = lump_data(LUMP_FACES)
95
+ count = data.bytesize / 20
96
+ result = Array.new(count)
97
+ count.times do |i|
98
+ raw = data[i * 20, 20]
99
+ plane_idx, side, first_edge, num_edges, texinfo_idx,
100
+ s0, s1, s2, s3, light_ofs = raw.unpack("s<2l<s<2C4l<")
101
+ result[i] = Face.new(
102
+ plane_index: plane_idx, side: side,
103
+ first_edge: first_edge, num_edges: num_edges,
104
+ texinfo_index: texinfo_idx, styles: [s0, s1, s2, s3],
105
+ light_offset: light_ofs
106
+ )
107
+ end
108
+ result
109
+ end
110
+
111
+ def parse_surfedges
112
+ data = lump_data(LUMP_SURFEDGES)
113
+ data.unpack("l<*")
114
+ end
115
+
116
+ def parse_planes
117
+ data = lump_data(LUMP_PLANES)
118
+ count = data.bytesize / 20
119
+ result = Array.new(count)
120
+ count.times do |i|
121
+ nx, ny, nz, dist, type = data[i * 20, 20].unpack("e4l<")
122
+ result[i] = Plane.new(
123
+ normal: Math::Vec3.new(nx, ny, nz), dist: dist, type: type
124
+ )
125
+ end
126
+ result
127
+ end
128
+
129
+ def parse_nodes
130
+ data = lump_data(LUMP_NODES)
131
+ count = data.bytesize / 24
132
+ result = Array.new(count)
133
+ count.times do |i|
134
+ raw = data[i * 24, 24]
135
+ plane_idx, c0, c1,
136
+ minx, miny, minz, maxx, maxy, maxz,
137
+ first_face, num_faces = raw.unpack("l<s<2s<6v2")
138
+ result[i] = Node.new(
139
+ plane_index: plane_idx,
140
+ children: [c0, c1],
141
+ mins: Math::Vec3.new(minx.to_f, miny.to_f, minz.to_f),
142
+ maxs: Math::Vec3.new(maxx.to_f, maxy.to_f, maxz.to_f),
143
+ first_face: first_face, num_faces: num_faces
144
+ )
145
+ end
146
+ result
147
+ end
148
+
149
+ def parse_leafs
150
+ data = lump_data(LUMP_LEAFS)
151
+ count = data.bytesize / 28
152
+ result = Array.new(count)
153
+ count.times do |i|
154
+ raw = data[i * 28, 28]
155
+ contents, visofs,
156
+ minx, miny, minz, maxx, maxy, maxz,
157
+ first_ms, num_ms,
158
+ a0, a1, a2, a3 = raw.unpack("l<2s<6v2C4")
159
+ result[i] = Leaf.new(
160
+ contents: contents, vis_offset: visofs,
161
+ mins: Math::Vec3.new(minx.to_f, miny.to_f, minz.to_f),
162
+ maxs: Math::Vec3.new(maxx.to_f, maxy.to_f, maxz.to_f),
163
+ first_marksurface: first_ms, num_marksurfaces: num_ms,
164
+ ambient_levels: [a0, a1, a2, a3]
165
+ )
166
+ end
167
+ result
168
+ end
169
+
170
+ def parse_clipnodes
171
+ data = lump_data(LUMP_CLIPNODES)
172
+ count = data.bytesize / 8
173
+ result = Array.new(count)
174
+ count.times do |i|
175
+ plane_idx, c0, c1 = data[i * 8, 8].unpack("l<s<2")
176
+ result[i] = ClipNode.new(plane_index: plane_idx, children: [c0, c1])
177
+ end
178
+ result
179
+ end
180
+
181
+ def parse_texinfo
182
+ data = lump_data(LUMP_TEXINFO)
183
+ count = data.bytesize / 40
184
+ result = Array.new(count)
185
+ count.times do |i|
186
+ raw = data[i * 40, 40]
187
+ floats = raw[0, 32].unpack("e8")
188
+ miptex, flags = raw[32, 8].unpack("l<2")
189
+ result[i] = TexInfo.new(
190
+ s_vec: Math::Vec3.new(floats[0], floats[1], floats[2]),
191
+ s_offset: floats[3],
192
+ t_vec: Math::Vec3.new(floats[4], floats[5], floats[6]),
193
+ t_offset: floats[7],
194
+ miptex_index: miptex, flags: flags
195
+ )
196
+ end
197
+ result
198
+ end
199
+
200
+ def parse_models
201
+ data = lump_data(LUMP_MODELS)
202
+ count = data.bytesize / 64
203
+ result = Array.new(count)
204
+ count.times do |i|
205
+ raw = data[i * 64, 64]
206
+ floats = raw[0, 36].unpack("e9")
207
+ ints = raw[36, 28].unpack("l<7")
208
+ result[i] = Model.new(
209
+ mins: Math::Vec3.new(floats[0], floats[1], floats[2]),
210
+ maxs: Math::Vec3.new(floats[3], floats[4], floats[5]),
211
+ origin: Math::Vec3.new(floats[6], floats[7], floats[8]),
212
+ head_nodes: ints[0..3],
213
+ vis_leafs: ints[4],
214
+ first_face: ints[5], num_faces: ints[6]
215
+ )
216
+ end
217
+ result
218
+ end
219
+
220
+ def parse_marksurfaces
221
+ data = lump_data(LUMP_MARKSURFACES)
222
+ data.unpack("v*")
223
+ end
224
+
225
+ def parse_entities
226
+ lump_data(LUMP_ENTITIES)&.force_encoding("ASCII")&.delete("\0") || ""
227
+ end
228
+
229
+ def parse_visibility
230
+ lump_data(LUMP_VISIBILITY) || ""
231
+ end
232
+
233
+ def parse_lighting
234
+ lump_data(LUMP_LIGHTING) || ""
235
+ end
236
+
237
+ def parse_textures
238
+ data = lump_data(LUMP_TEXTURES)
239
+ return [] if data.nil? || data.bytesize < 4
240
+
241
+ num_miptex = data[0, 4].unpack1("l<")
242
+ offsets = data[4, num_miptex * 4].unpack("l<#{num_miptex}")
243
+
244
+ offsets.map do |ofs|
245
+ if ofs < 0
246
+ nil
247
+ else
248
+ raw = data[ofs, 40]
249
+ next nil if raw.nil? || raw.bytesize < 40
250
+ name = raw[0, 16].unpack1("Z16")
251
+ width, height = raw[16, 8].unpack("V2")
252
+ mip_offsets = raw[24, 16].unpack("V4")
253
+ # Extract mip level 0 pixel data (8-bit indexed)
254
+ pixel_offset = ofs + mip_offsets[0]
255
+ pixel_count = width * height
256
+ pixels = data[pixel_offset, pixel_count]
257
+ MipTex.new(name: name, width: width, height: height,
258
+ offsets: mip_offsets, pixels: pixels)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quake
4
+ module Bsp
5
+ Edge = Data.define(:v0, :v1)
6
+ Face = Data.define(:plane_index, :side, :first_edge, :num_edges,
7
+ :texinfo_index, :styles, :light_offset)
8
+ Plane = Data.define(:normal, :dist, :type)
9
+ Node = Data.define(:plane_index, :children, :mins, :maxs,
10
+ :first_face, :num_faces)
11
+ Leaf = Data.define(:contents, :vis_offset, :mins, :maxs,
12
+ :first_marksurface, :num_marksurfaces, :ambient_levels)
13
+ ClipNode = Data.define(:plane_index, :children)
14
+ TexInfo = Data.define(:s_vec, :s_offset, :t_vec, :t_offset,
15
+ :miptex_index, :flags)
16
+ Model = Data.define(:mins, :maxs, :origin, :head_nodes,
17
+ :vis_leafs, :first_face, :num_faces)
18
+ MipTex = Data.define(:name, :width, :height, :offsets, :pixels)
19
+
20
+ # Pre-computed surface data for rendering
21
+ Surface = Data.define(:vertices, :texcoords, :texinfo_index)
22
+
23
+ Level = Data.define(
24
+ :vertices, :edges, :faces, :surfedges,
25
+ :planes, :nodes, :leafs, :clipnodes,
26
+ :texinfo, :models, :marksurfaces,
27
+ :entities, :visibility, :lighting, :textures
28
+ )
29
+ end
30
+ end