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
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
|