quake2ts 0.0.7
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.
- package/apps/viewer/dist/browser/index.global.js +2 -0
- package/apps/viewer/dist/browser/index.global.js.map +1 -0
- package/apps/viewer/dist/cjs/index.cjs +1087 -0
- package/apps/viewer/dist/cjs/index.cjs.map +1 -0
- package/apps/viewer/dist/esm/index.js +1060 -0
- package/apps/viewer/dist/esm/index.js.map +1 -0
- package/apps/viewer/dist/tsconfig.tsbuildinfo +1 -0
- package/apps/viewer/dist/types/index.d.ts +7 -0
- package/apps/viewer/dist/types/index.d.ts.map +1 -0
- package/package.json +107 -0
- package/packages/client/dist/browser/index.global.js +2 -0
- package/packages/client/dist/browser/index.global.js.map +1 -0
- package/packages/client/dist/cjs/index.cjs +57 -0
- package/packages/client/dist/cjs/index.cjs.map +1 -0
- package/packages/client/dist/esm/index.js +32 -0
- package/packages/client/dist/esm/index.js.map +1 -0
- package/packages/client/dist/tsconfig.tsbuildinfo +1 -0
- package/packages/client/dist/types/index.d.ts +14 -0
- package/packages/client/dist/types/index.d.ts.map +1 -0
- package/packages/engine/dist/browser/index.global.js +2 -0
- package/packages/engine/dist/browser/index.global.js.map +1 -0
- package/packages/engine/dist/cjs/index.cjs +1823 -0
- package/packages/engine/dist/cjs/index.cjs.map +1 -0
- package/packages/engine/dist/esm/index.js +1764 -0
- package/packages/engine/dist/esm/index.js.map +1 -0
- package/packages/engine/dist/tsconfig.tsbuildinfo +1 -0
- package/packages/engine/dist/types/assets/browserIngestion.d.ts +7 -0
- package/packages/engine/dist/types/assets/browserIngestion.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/bsp.d.ts +157 -0
- package/packages/engine/dist/types/assets/bsp.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/cache.d.ts +17 -0
- package/packages/engine/dist/types/assets/cache.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/ingestion.d.ts +31 -0
- package/packages/engine/dist/types/assets/ingestion.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/md2.d.ts +73 -0
- package/packages/engine/dist/types/assets/md2.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/pak.d.ts +28 -0
- package/packages/engine/dist/types/assets/pak.d.ts.map +1 -0
- package/packages/engine/dist/types/assets/vfs.d.ts +23 -0
- package/packages/engine/dist/types/assets/vfs.d.ts.map +1 -0
- package/packages/engine/dist/types/configstrings.d.ts +34 -0
- package/packages/engine/dist/types/configstrings.d.ts.map +1 -0
- package/packages/engine/dist/types/cvars.d.ts +43 -0
- package/packages/engine/dist/types/cvars.d.ts.map +1 -0
- package/packages/engine/dist/types/host.d.ts +42 -0
- package/packages/engine/dist/types/host.d.ts.map +1 -0
- package/packages/engine/dist/types/index.d.ts +37 -0
- package/packages/engine/dist/types/index.d.ts.map +1 -0
- package/packages/engine/dist/types/loop.d.ts +40 -0
- package/packages/engine/dist/types/loop.d.ts.map +1 -0
- package/packages/engine/dist/types/render/bsp.d.ts +49 -0
- package/packages/engine/dist/types/render/bsp.d.ts.map +1 -0
- package/packages/engine/dist/types/render/context.d.ts +36 -0
- package/packages/engine/dist/types/render/context.d.ts.map +1 -0
- package/packages/engine/dist/types/render/resources.d.ts +56 -0
- package/packages/engine/dist/types/render/resources.d.ts.map +1 -0
- package/packages/engine/dist/types/render/shaderProgram.d.ts +18 -0
- package/packages/engine/dist/types/render/shaderProgram.d.ts.map +1 -0
- package/packages/engine/dist/types/runtime.d.ts +15 -0
- package/packages/engine/dist/types/runtime.d.ts.map +1 -0
- package/packages/game/dist/browser/index.global.js +2 -0
- package/packages/game/dist/browser/index.global.js.map +1 -0
- package/packages/game/dist/cjs/index.cjs +758 -0
- package/packages/game/dist/cjs/index.cjs.map +1 -0
- package/packages/game/dist/esm/index.js +725 -0
- package/packages/game/dist/esm/index.js.map +1 -0
- package/packages/game/dist/tsconfig.tsbuildinfo +1 -0
- package/packages/game/dist/types/entities/entity.d.ts +92 -0
- package/packages/game/dist/types/entities/entity.d.ts.map +1 -0
- package/packages/game/dist/types/entities/index.d.ts +3 -0
- package/packages/game/dist/types/entities/index.d.ts.map +1 -0
- package/packages/game/dist/types/entities/pool.d.ts +18 -0
- package/packages/game/dist/types/entities/pool.d.ts.map +1 -0
- package/packages/game/dist/types/entities/system.d.ts +18 -0
- package/packages/game/dist/types/entities/system.d.ts.map +1 -0
- package/packages/game/dist/types/entities/thinkScheduler.d.ts +8 -0
- package/packages/game/dist/types/entities/thinkScheduler.d.ts.map +1 -0
- package/packages/game/dist/types/index.d.ts +26 -0
- package/packages/game/dist/types/index.d.ts.map +1 -0
- package/packages/game/dist/types/level.d.ts +14 -0
- package/packages/game/dist/types/level.d.ts.map +1 -0
- package/packages/game/dist/types/loop.d.ts +29 -0
- package/packages/game/dist/types/loop.d.ts.map +1 -0
- package/packages/shared/dist/browser/index.global.js +2 -0
- package/packages/shared/dist/browser/index.global.js.map +1 -0
- package/packages/shared/dist/cjs/index.cjs +2793 -0
- package/packages/shared/dist/cjs/index.cjs.map +1 -0
- package/packages/shared/dist/esm/index.js +2594 -0
- package/packages/shared/dist/esm/index.js.map +1 -0
- package/packages/shared/dist/tsconfig.tsbuildinfo +1 -0
- package/packages/shared/dist/types/bsp/collision.d.ts +112 -0
- package/packages/shared/dist/types/bsp/collision.d.ts.map +1 -0
- package/packages/shared/dist/types/bsp/contents.d.ts +71 -0
- package/packages/shared/dist/types/bsp/contents.d.ts.map +1 -0
- package/packages/shared/dist/types/index.d.ts +25 -0
- package/packages/shared/dist/types/index.d.ts.map +1 -0
- package/packages/shared/dist/types/math/angles.d.ts +17 -0
- package/packages/shared/dist/types/math/angles.d.ts.map +1 -0
- package/packages/shared/dist/types/math/color.d.ts +12 -0
- package/packages/shared/dist/types/math/color.d.ts.map +1 -0
- package/packages/shared/dist/types/math/random.d.ts +48 -0
- package/packages/shared/dist/types/math/random.d.ts.map +1 -0
- package/packages/shared/dist/types/math/vec3.d.ts +76 -0
- package/packages/shared/dist/types/math/vec3.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/categorize.d.ts +36 -0
- package/packages/shared/dist/types/pmove/categorize.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/constants.d.ts +75 -0
- package/packages/shared/dist/types/pmove/constants.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/currents.d.ts +58 -0
- package/packages/shared/dist/types/pmove/currents.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/dimensions.d.ts +14 -0
- package/packages/shared/dist/types/pmove/dimensions.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/duck.d.ts +39 -0
- package/packages/shared/dist/types/pmove/duck.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/fly.d.ts +34 -0
- package/packages/shared/dist/types/pmove/fly.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/jump.d.ts +26 -0
- package/packages/shared/dist/types/pmove/jump.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/move.d.ts +58 -0
- package/packages/shared/dist/types/pmove/move.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/pmove.d.ts +36 -0
- package/packages/shared/dist/types/pmove/pmove.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/slide.d.ts +63 -0
- package/packages/shared/dist/types/pmove/slide.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/snap.d.ts +40 -0
- package/packages/shared/dist/types/pmove/snap.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/special.d.ts +39 -0
- package/packages/shared/dist/types/pmove/special.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/stuck.d.ts +21 -0
- package/packages/shared/dist/types/pmove/stuck.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/types.d.ts +48 -0
- package/packages/shared/dist/types/pmove/types.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/view.d.ts +19 -0
- package/packages/shared/dist/types/pmove/view.d.ts.map +1 -0
- package/packages/shared/dist/types/pmove/water.d.ts +21 -0
- package/packages/shared/dist/types/pmove/water.d.ts.map +1 -0
- package/packages/shared/dist/types/protocol/configstrings.d.ts +51 -0
- package/packages/shared/dist/types/protocol/configstrings.d.ts.map +1 -0
- package/packages/shared/dist/types/protocol/cvar.d.ts +15 -0
- package/packages/shared/dist/types/protocol/cvar.d.ts.map +1 -0
- package/packages/tools/dist/browser/index.global.js +2 -0
- package/packages/tools/dist/browser/index.global.js.map +1 -0
- package/packages/tools/dist/cjs/index.cjs +33 -0
- package/packages/tools/dist/cjs/index.cjs.map +1 -0
- package/packages/tools/dist/esm/index.js +8 -0
- package/packages/tools/dist/esm/index.js.map +1 -0
- package/packages/tools/dist/tsconfig.tsbuildinfo +1 -0
- package/packages/tools/dist/types/index.d.ts +7 -0
- package/packages/tools/dist/types/index.d.ts.map +1 -0
|
@@ -0,0 +1,1764 @@
|
|
|
1
|
+
// src/loop.ts
|
|
2
|
+
var DEFAULT_FIXED_DELTA_MS = 25;
|
|
3
|
+
var DEFAULT_MAX_SUBSTEPS = 5;
|
|
4
|
+
var defaultNow = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
5
|
+
var defaultScheduler = (tick) => {
|
|
6
|
+
if (typeof requestAnimationFrame === "function") {
|
|
7
|
+
requestAnimationFrame(() => tick());
|
|
8
|
+
} else {
|
|
9
|
+
setTimeout(tick, DEFAULT_FIXED_DELTA_MS);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var FixedTimestepLoop = class {
|
|
13
|
+
constructor(callbacks, options = {}) {
|
|
14
|
+
this.callbacks = callbacks;
|
|
15
|
+
this.accumulatorMs = 0;
|
|
16
|
+
this.frame = 0;
|
|
17
|
+
this.running = false;
|
|
18
|
+
this.tick = () => {
|
|
19
|
+
if (!this.running) return;
|
|
20
|
+
const nowMs = this.options.now();
|
|
21
|
+
const elapsed = this.lastTimeMs === void 0 ? 0 : nowMs - this.lastTimeMs;
|
|
22
|
+
this.lastTimeMs = nowMs;
|
|
23
|
+
this.advance(elapsed, nowMs);
|
|
24
|
+
if (this.running) {
|
|
25
|
+
this.options.schedule(this.tick);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const fixedDeltaMs = options.fixedDeltaMs ?? DEFAULT_FIXED_DELTA_MS;
|
|
29
|
+
const maxSubSteps = options.maxSubSteps ?? DEFAULT_MAX_SUBSTEPS;
|
|
30
|
+
this.options = {
|
|
31
|
+
fixedDeltaMs,
|
|
32
|
+
maxSubSteps,
|
|
33
|
+
maxDeltaMs: options.maxDeltaMs ?? fixedDeltaMs * maxSubSteps,
|
|
34
|
+
startTimeMs: options.startTimeMs,
|
|
35
|
+
now: options.now ?? defaultNow,
|
|
36
|
+
schedule: options.schedule ?? defaultScheduler
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
start() {
|
|
40
|
+
if (this.running) return;
|
|
41
|
+
this.running = true;
|
|
42
|
+
this.lastTimeMs = this.options.startTimeMs ?? this.options.now();
|
|
43
|
+
this.options.schedule(this.tick);
|
|
44
|
+
}
|
|
45
|
+
stop() {
|
|
46
|
+
this.running = false;
|
|
47
|
+
}
|
|
48
|
+
pump(elapsedMs) {
|
|
49
|
+
const nowMs = (this.lastTimeMs ?? 0) + elapsedMs;
|
|
50
|
+
this.lastTimeMs = nowMs;
|
|
51
|
+
this.advance(elapsedMs, nowMs);
|
|
52
|
+
}
|
|
53
|
+
isRunning() {
|
|
54
|
+
return this.running;
|
|
55
|
+
}
|
|
56
|
+
get frameNumber() {
|
|
57
|
+
return this.frame;
|
|
58
|
+
}
|
|
59
|
+
advance(elapsedMs, nowMs) {
|
|
60
|
+
const clampedDelta = Math.min(Math.max(elapsedMs, 0), this.options.maxDeltaMs);
|
|
61
|
+
this.accumulatorMs = Math.min(
|
|
62
|
+
this.accumulatorMs + clampedDelta,
|
|
63
|
+
this.options.fixedDeltaMs * this.options.maxSubSteps
|
|
64
|
+
);
|
|
65
|
+
let steps = 0;
|
|
66
|
+
while (this.accumulatorMs >= this.options.fixedDeltaMs && steps < this.options.maxSubSteps) {
|
|
67
|
+
this.frame += 1;
|
|
68
|
+
this.callbacks.simulate({ frame: this.frame, deltaMs: this.options.fixedDeltaMs, nowMs });
|
|
69
|
+
this.accumulatorMs -= this.options.fixedDeltaMs;
|
|
70
|
+
steps += 1;
|
|
71
|
+
}
|
|
72
|
+
const alpha = this.options.fixedDeltaMs === 0 ? 0 : this.accumulatorMs / this.options.fixedDeltaMs;
|
|
73
|
+
this.callbacks.render?.({ alpha, nowMs, accumulatorMs: this.accumulatorMs, frame: this.frame });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/host.ts
|
|
78
|
+
var EngineHost = class {
|
|
79
|
+
constructor(game, client, options = {}) {
|
|
80
|
+
this.game = game;
|
|
81
|
+
this.client = client;
|
|
82
|
+
this.started = false;
|
|
83
|
+
this.stepSimulation = (step) => {
|
|
84
|
+
this.previousFrame = this.latestFrame;
|
|
85
|
+
this.latestFrame = this.game.frame(step);
|
|
86
|
+
};
|
|
87
|
+
this.renderClient = (renderContext) => {
|
|
88
|
+
if (!this.client) return;
|
|
89
|
+
this.client.render({
|
|
90
|
+
...renderContext,
|
|
91
|
+
previous: this.previousFrame,
|
|
92
|
+
latest: this.latestFrame
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const now = options.loop?.now?.() ?? Date.now();
|
|
96
|
+
this.startTimeMs = options.startTimeMs ?? options.loop?.startTimeMs ?? now;
|
|
97
|
+
this.loop = new FixedTimestepLoop(
|
|
98
|
+
{
|
|
99
|
+
simulate: this.stepSimulation,
|
|
100
|
+
render: this.renderClient
|
|
101
|
+
},
|
|
102
|
+
{ ...options.loop, startTimeMs: this.startTimeMs }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
start() {
|
|
106
|
+
if (this.started) return;
|
|
107
|
+
this.latestFrame = this.game.init(this.startTimeMs) ?? this.latestFrame;
|
|
108
|
+
this.client?.init(this.latestFrame);
|
|
109
|
+
this.started = true;
|
|
110
|
+
this.loop.start();
|
|
111
|
+
}
|
|
112
|
+
stop() {
|
|
113
|
+
if (!this.started) return;
|
|
114
|
+
this.loop.stop();
|
|
115
|
+
this.client?.shutdown();
|
|
116
|
+
this.game.shutdown();
|
|
117
|
+
this.previousFrame = void 0;
|
|
118
|
+
this.latestFrame = void 0;
|
|
119
|
+
this.started = false;
|
|
120
|
+
}
|
|
121
|
+
pump(elapsedMs) {
|
|
122
|
+
this.loop.pump(elapsedMs);
|
|
123
|
+
}
|
|
124
|
+
getLatestFrame() {
|
|
125
|
+
return this.latestFrame;
|
|
126
|
+
}
|
|
127
|
+
isRunning() {
|
|
128
|
+
return this.loop.isRunning();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// ../shared/dist/esm/index.js
|
|
133
|
+
var DEG_TO_RAD = Math.PI / 180;
|
|
134
|
+
var DEG2RAD_FACTOR = Math.PI / 180;
|
|
135
|
+
var RAD2DEG_FACTOR = 180 / Math.PI;
|
|
136
|
+
var CONTENTS_SOLID = 1 << 0;
|
|
137
|
+
var CONTENTS_WINDOW = 1 << 1;
|
|
138
|
+
var CONTENTS_AUX = 1 << 2;
|
|
139
|
+
var CONTENTS_LAVA = 1 << 3;
|
|
140
|
+
var CONTENTS_SLIME = 1 << 4;
|
|
141
|
+
var CONTENTS_WATER = 1 << 5;
|
|
142
|
+
var CONTENTS_MIST = 1 << 6;
|
|
143
|
+
var CONTENTS_NO_WATERJUMP = 1 << 13;
|
|
144
|
+
var CONTENTS_PROJECTILECLIP = 1 << 14;
|
|
145
|
+
var CONTENTS_AREAPORTAL = 1 << 15;
|
|
146
|
+
var CONTENTS_PLAYERCLIP = 1 << 16;
|
|
147
|
+
var CONTENTS_MONSTERCLIP = 1 << 17;
|
|
148
|
+
var CONTENTS_CURRENT_0 = 1 << 18;
|
|
149
|
+
var CONTENTS_CURRENT_90 = 1 << 19;
|
|
150
|
+
var CONTENTS_CURRENT_180 = 1 << 20;
|
|
151
|
+
var CONTENTS_CURRENT_270 = 1 << 21;
|
|
152
|
+
var CONTENTS_CURRENT_UP = 1 << 22;
|
|
153
|
+
var CONTENTS_CURRENT_DOWN = 1 << 23;
|
|
154
|
+
var CONTENTS_ORIGIN = 1 << 24;
|
|
155
|
+
var CONTENTS_MONSTER = 1 << 25;
|
|
156
|
+
var CONTENTS_DEADMONSTER = 1 << 26;
|
|
157
|
+
var CONTENTS_DETAIL = 1 << 27;
|
|
158
|
+
var CONTENTS_TRANSLUCENT = 1 << 28;
|
|
159
|
+
var CONTENTS_LADDER = 1 << 29;
|
|
160
|
+
var CONTENTS_PLAYER = 1 << 30;
|
|
161
|
+
var CONTENTS_PROJECTILE = 1 << 31;
|
|
162
|
+
var SURF_NONE = 0;
|
|
163
|
+
var SURF_LIGHT = 1 << 0;
|
|
164
|
+
var SURF_SLICK = 1 << 1;
|
|
165
|
+
var SURF_SKY = 1 << 2;
|
|
166
|
+
var SURF_WARP = 1 << 3;
|
|
167
|
+
var SURF_TRANS33 = 1 << 4;
|
|
168
|
+
var SURF_TRANS66 = 1 << 5;
|
|
169
|
+
var SURF_FLOWING = 1 << 6;
|
|
170
|
+
var SURF_NODRAW = 1 << 7;
|
|
171
|
+
var SURF_ALPHATEST = 1 << 25;
|
|
172
|
+
var SURF_N64_UV = 1 << 28;
|
|
173
|
+
var SURF_N64_SCROLL_X = 1 << 29;
|
|
174
|
+
var SURF_N64_SCROLL_Y = 1 << 30;
|
|
175
|
+
var SURF_N64_SCROLL_FLIP = 1 << 31;
|
|
176
|
+
var MASK_SOLID = CONTENTS_SOLID | CONTENTS_WINDOW;
|
|
177
|
+
var MASK_PLAYERSOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW | CONTENTS_MONSTER | CONTENTS_PLAYER;
|
|
178
|
+
var MASK_DEADSOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW;
|
|
179
|
+
var MASK_MONSTERSOLID = CONTENTS_SOLID | CONTENTS_MONSTERCLIP | CONTENTS_WINDOW | CONTENTS_MONSTER | CONTENTS_PLAYER;
|
|
180
|
+
var MASK_WATER = CONTENTS_WATER | CONTENTS_LAVA | CONTENTS_SLIME;
|
|
181
|
+
var MASK_OPAQUE = CONTENTS_SOLID | CONTENTS_SLIME | CONTENTS_LAVA;
|
|
182
|
+
var MASK_SHOT = CONTENTS_SOLID | CONTENTS_MONSTER | CONTENTS_PLAYER | CONTENTS_WINDOW | CONTENTS_DEADMONSTER;
|
|
183
|
+
var MASK_CURRENT = CONTENTS_CURRENT_0 | CONTENTS_CURRENT_90 | CONTENTS_CURRENT_180 | CONTENTS_CURRENT_270 | CONTENTS_CURRENT_UP | CONTENTS_CURRENT_DOWN;
|
|
184
|
+
var MASK_BLOCK_SIGHT = CONTENTS_SOLID | CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_MONSTER | CONTENTS_PLAYER;
|
|
185
|
+
var MASK_NAV_SOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW;
|
|
186
|
+
var MASK_LADDER_NAV_SOLID = CONTENTS_SOLID | CONTENTS_WINDOW;
|
|
187
|
+
var MASK_WALK_NAV_SOLID = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WINDOW | CONTENTS_MONSTERCLIP;
|
|
188
|
+
var MASK_PROJECTILE = MASK_SHOT | CONTENTS_PROJECTILECLIP;
|
|
189
|
+
var CvarFlags = /* @__PURE__ */ ((CvarFlags2) => {
|
|
190
|
+
CvarFlags2[CvarFlags2["None"] = 0] = "None";
|
|
191
|
+
CvarFlags2[CvarFlags2["Archive"] = 1] = "Archive";
|
|
192
|
+
CvarFlags2[CvarFlags2["UserInfo"] = 2] = "UserInfo";
|
|
193
|
+
CvarFlags2[CvarFlags2["ServerInfo"] = 4] = "ServerInfo";
|
|
194
|
+
CvarFlags2[CvarFlags2["Latch"] = 8] = "Latch";
|
|
195
|
+
CvarFlags2[CvarFlags2["Cheat"] = 16] = "Cheat";
|
|
196
|
+
return CvarFlags2;
|
|
197
|
+
})(CvarFlags || {});
|
|
198
|
+
var MAX_CLIENTS = 256;
|
|
199
|
+
var MAX_LIGHTSTYLES = 256;
|
|
200
|
+
var MAX_MODELS = 8192;
|
|
201
|
+
var MAX_SOUNDS = 2048;
|
|
202
|
+
var MAX_IMAGES = 512;
|
|
203
|
+
var MAX_ITEMS = 256;
|
|
204
|
+
var MAX_GENERAL = MAX_CLIENTS * 2;
|
|
205
|
+
var MAX_SHADOW_LIGHTS = 256;
|
|
206
|
+
var MAX_WHEEL_ITEMS = 32;
|
|
207
|
+
var CS_MAX_STRING_LENGTH = 96;
|
|
208
|
+
var ConfigStringIndex = ((ConfigStringIndex2) => {
|
|
209
|
+
ConfigStringIndex2[ConfigStringIndex2["Name"] = 0] = "Name";
|
|
210
|
+
ConfigStringIndex2[ConfigStringIndex2["CdTrack"] = 1] = "CdTrack";
|
|
211
|
+
ConfigStringIndex2[ConfigStringIndex2["Sky"] = 2] = "Sky";
|
|
212
|
+
ConfigStringIndex2[ConfigStringIndex2["SkyAxis"] = 3] = "SkyAxis";
|
|
213
|
+
ConfigStringIndex2[ConfigStringIndex2["SkyRotate"] = 4] = "SkyRotate";
|
|
214
|
+
ConfigStringIndex2[ConfigStringIndex2["StatusBar"] = 5] = "StatusBar";
|
|
215
|
+
ConfigStringIndex2[ConfigStringIndex2["AirAccel"] = 59] = "AirAccel";
|
|
216
|
+
ConfigStringIndex2[ConfigStringIndex2["MaxClients"] = 60] = "MaxClients";
|
|
217
|
+
ConfigStringIndex2[ConfigStringIndex2["MapChecksum"] = 61] = "MapChecksum";
|
|
218
|
+
ConfigStringIndex2[ConfigStringIndex2["Models"] = 62] = "Models";
|
|
219
|
+
ConfigStringIndex2[ConfigStringIndex2["Sounds"] = 62 + MAX_MODELS] = "Sounds";
|
|
220
|
+
ConfigStringIndex2[ConfigStringIndex2["Images"] = ConfigStringIndex2.Sounds + MAX_SOUNDS] = "Images";
|
|
221
|
+
ConfigStringIndex2[ConfigStringIndex2["Lights"] = ConfigStringIndex2.Images + MAX_IMAGES] = "Lights";
|
|
222
|
+
ConfigStringIndex2[ConfigStringIndex2["ShadowLights"] = ConfigStringIndex2.Lights + MAX_LIGHTSTYLES] = "ShadowLights";
|
|
223
|
+
ConfigStringIndex2[ConfigStringIndex2["Items"] = ConfigStringIndex2.ShadowLights + MAX_SHADOW_LIGHTS] = "Items";
|
|
224
|
+
ConfigStringIndex2[ConfigStringIndex2["PlayerSkins"] = ConfigStringIndex2.Items + MAX_ITEMS] = "PlayerSkins";
|
|
225
|
+
ConfigStringIndex2[ConfigStringIndex2["General"] = ConfigStringIndex2.PlayerSkins + MAX_CLIENTS] = "General";
|
|
226
|
+
ConfigStringIndex2[ConfigStringIndex2["WheelWeapons"] = ConfigStringIndex2.General + MAX_GENERAL] = "WheelWeapons";
|
|
227
|
+
ConfigStringIndex2[ConfigStringIndex2["WheelAmmo"] = ConfigStringIndex2.WheelWeapons + MAX_WHEEL_ITEMS] = "WheelAmmo";
|
|
228
|
+
ConfigStringIndex2[ConfigStringIndex2["WheelPowerups"] = ConfigStringIndex2.WheelAmmo + MAX_WHEEL_ITEMS] = "WheelPowerups";
|
|
229
|
+
ConfigStringIndex2[ConfigStringIndex2["CdLoopCount"] = ConfigStringIndex2.WheelPowerups + MAX_WHEEL_ITEMS] = "CdLoopCount";
|
|
230
|
+
ConfigStringIndex2[ConfigStringIndex2["GameStyle"] = ConfigStringIndex2.CdLoopCount + 1] = "GameStyle";
|
|
231
|
+
ConfigStringIndex2[ConfigStringIndex2["MaxConfigStrings"] = ConfigStringIndex2.GameStyle + 1] = "MaxConfigStrings";
|
|
232
|
+
return ConfigStringIndex2;
|
|
233
|
+
})(ConfigStringIndex || {});
|
|
234
|
+
var MAX_CONFIGSTRINGS = ConfigStringIndex.MaxConfigStrings;
|
|
235
|
+
function configStringSize(index) {
|
|
236
|
+
if (index >= 5 && index < 59) {
|
|
237
|
+
return CS_MAX_STRING_LENGTH * (59 - index);
|
|
238
|
+
}
|
|
239
|
+
if (index >= ConfigStringIndex.General && index < ConfigStringIndex.WheelWeapons) {
|
|
240
|
+
return CS_MAX_STRING_LENGTH * (ConfigStringIndex.MaxConfigStrings - index);
|
|
241
|
+
}
|
|
242
|
+
return CS_MAX_STRING_LENGTH;
|
|
243
|
+
}
|
|
244
|
+
var WATERJUMP_CLEAR = 8 | 16 | 32 | 1024;
|
|
245
|
+
|
|
246
|
+
// src/configstrings.ts
|
|
247
|
+
function assertWithinBounds(index) {
|
|
248
|
+
if (index < 0 || index >= ConfigStringIndex.MaxConfigStrings) {
|
|
249
|
+
throw new RangeError(`Configstring index ${index} is out of range (0-${ConfigStringIndex.MaxConfigStrings - 1})`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function assertLength(index, value) {
|
|
253
|
+
const maxLength = configStringSize(index);
|
|
254
|
+
if (value.length > maxLength) {
|
|
255
|
+
throw new RangeError(
|
|
256
|
+
`Configstring ${index} exceeds maximum length (${value.length} > ${maxLength}); limit is ${CS_MAX_STRING_LENGTH} chars per slot`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
var ConfigStringRegistry = class {
|
|
261
|
+
constructor() {
|
|
262
|
+
this.values = /* @__PURE__ */ new Map();
|
|
263
|
+
this.modelCursor = ConfigStringIndex.Models;
|
|
264
|
+
this.soundCursor = ConfigStringIndex.Sounds;
|
|
265
|
+
this.imageCursor = ConfigStringIndex.Images;
|
|
266
|
+
this.lightCursor = ConfigStringIndex.Lights;
|
|
267
|
+
this.shadowLightCursor = ConfigStringIndex.ShadowLights;
|
|
268
|
+
this.itemCursor = ConfigStringIndex.Items;
|
|
269
|
+
this.playerSkinCursor = ConfigStringIndex.PlayerSkins;
|
|
270
|
+
this.generalCursor = ConfigStringIndex.General;
|
|
271
|
+
}
|
|
272
|
+
set(index, value) {
|
|
273
|
+
assertWithinBounds(index);
|
|
274
|
+
assertLength(index, value);
|
|
275
|
+
this.values.set(index, value);
|
|
276
|
+
return index;
|
|
277
|
+
}
|
|
278
|
+
get(index) {
|
|
279
|
+
return this.values.get(index);
|
|
280
|
+
}
|
|
281
|
+
getAll() {
|
|
282
|
+
const result = new Array(MAX_CONFIGSTRINGS).fill("");
|
|
283
|
+
for (const [index, value] of this.values.entries()) {
|
|
284
|
+
result[index] = value;
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
modelIndex(path) {
|
|
289
|
+
return this.register(path, ConfigStringIndex.Models, MAX_MODELS, "modelCursor");
|
|
290
|
+
}
|
|
291
|
+
soundIndex(path) {
|
|
292
|
+
return this.register(path, ConfigStringIndex.Sounds, MAX_SOUNDS, "soundCursor");
|
|
293
|
+
}
|
|
294
|
+
imageIndex(path) {
|
|
295
|
+
return this.register(path, ConfigStringIndex.Images, MAX_IMAGES, "imageCursor");
|
|
296
|
+
}
|
|
297
|
+
lightIndex(definition) {
|
|
298
|
+
return this.register(definition, ConfigStringIndex.Lights, MAX_LIGHTSTYLES, "lightCursor");
|
|
299
|
+
}
|
|
300
|
+
shadowLightIndex(definition) {
|
|
301
|
+
return this.register(definition, ConfigStringIndex.ShadowLights, MAX_SHADOW_LIGHTS, "shadowLightCursor");
|
|
302
|
+
}
|
|
303
|
+
itemIndex(name) {
|
|
304
|
+
return this.register(name, ConfigStringIndex.Items, MAX_ITEMS, "itemCursor");
|
|
305
|
+
}
|
|
306
|
+
playerSkinIndex(name) {
|
|
307
|
+
return this.register(name, ConfigStringIndex.PlayerSkins, MAX_CLIENTS, "playerSkinCursor");
|
|
308
|
+
}
|
|
309
|
+
generalIndex(value) {
|
|
310
|
+
return this.register(value, ConfigStringIndex.General, MAX_GENERAL, "generalCursor");
|
|
311
|
+
}
|
|
312
|
+
register(value, start, maxCount, cursorKey) {
|
|
313
|
+
for (let i = start; i < start + maxCount; i += 1) {
|
|
314
|
+
if (this.values.get(i) === value) {
|
|
315
|
+
return i;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const next = this[cursorKey];
|
|
319
|
+
const limit = start + maxCount;
|
|
320
|
+
if (next >= limit) {
|
|
321
|
+
throw new RangeError(`Out of configstring slots for range starting at ${start}`);
|
|
322
|
+
}
|
|
323
|
+
assertLength(next, value);
|
|
324
|
+
this.values.set(next, value);
|
|
325
|
+
this[cursorKey] = next + 1;
|
|
326
|
+
return next;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/runtime.ts
|
|
331
|
+
var EngineRuntime = class {
|
|
332
|
+
constructor(engine, host) {
|
|
333
|
+
this.engine = engine;
|
|
334
|
+
this.host = host;
|
|
335
|
+
this.started = false;
|
|
336
|
+
}
|
|
337
|
+
start() {
|
|
338
|
+
if (this.started) return;
|
|
339
|
+
this.engine.init();
|
|
340
|
+
this.host.start();
|
|
341
|
+
this.started = true;
|
|
342
|
+
}
|
|
343
|
+
stop() {
|
|
344
|
+
if (!this.started) return;
|
|
345
|
+
this.host.stop();
|
|
346
|
+
this.engine.shutdown();
|
|
347
|
+
this.started = false;
|
|
348
|
+
}
|
|
349
|
+
pump(elapsedMs) {
|
|
350
|
+
this.host.pump(elapsedMs);
|
|
351
|
+
}
|
|
352
|
+
getLatestFrame() {
|
|
353
|
+
return this.host.getLatestFrame();
|
|
354
|
+
}
|
|
355
|
+
isRunning() {
|
|
356
|
+
return this.started && this.host.isRunning();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
function createEngineRuntime(engine, game, client, options) {
|
|
360
|
+
return new EngineRuntime(engine, new EngineHost(game, client, options));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/cvars.ts
|
|
364
|
+
var Cvar = class {
|
|
365
|
+
constructor({
|
|
366
|
+
name,
|
|
367
|
+
defaultValue,
|
|
368
|
+
description,
|
|
369
|
+
flags = CvarFlags.None,
|
|
370
|
+
onChange
|
|
371
|
+
}) {
|
|
372
|
+
this.modifiedCount = 0;
|
|
373
|
+
this.name = name;
|
|
374
|
+
this.defaultValue = defaultValue;
|
|
375
|
+
this.description = description;
|
|
376
|
+
this.flags = flags;
|
|
377
|
+
this._value = defaultValue;
|
|
378
|
+
this.onChange = onChange;
|
|
379
|
+
}
|
|
380
|
+
get string() {
|
|
381
|
+
return this._value;
|
|
382
|
+
}
|
|
383
|
+
get number() {
|
|
384
|
+
return Number(this._value);
|
|
385
|
+
}
|
|
386
|
+
get integer() {
|
|
387
|
+
return Math.trunc(this.number);
|
|
388
|
+
}
|
|
389
|
+
get boolean() {
|
|
390
|
+
return Boolean(this.integer);
|
|
391
|
+
}
|
|
392
|
+
set(value) {
|
|
393
|
+
if (this.flags & CvarFlags.Latch) {
|
|
394
|
+
if (value === this._value) {
|
|
395
|
+
this.latched = void 0;
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (this.latched === value) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (this.latched !== value) {
|
|
402
|
+
this.latched = value;
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this.apply(value);
|
|
407
|
+
}
|
|
408
|
+
reset() {
|
|
409
|
+
this.latched = void 0;
|
|
410
|
+
this.apply(this.defaultValue);
|
|
411
|
+
}
|
|
412
|
+
applyLatched() {
|
|
413
|
+
if (this.latched === void 0) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
const pending = this.latched;
|
|
417
|
+
this.latched = void 0;
|
|
418
|
+
if (pending === this._value) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
this.apply(pending);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
apply(next) {
|
|
425
|
+
if (this._value === next) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const previous = this._value;
|
|
429
|
+
this._value = next;
|
|
430
|
+
this.modifiedCount += 1;
|
|
431
|
+
this.onChange?.(this, previous);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
var CvarRegistry = class {
|
|
435
|
+
constructor() {
|
|
436
|
+
this.cvars = /* @__PURE__ */ new Map();
|
|
437
|
+
}
|
|
438
|
+
register(def) {
|
|
439
|
+
const existing = this.cvars.get(def.name);
|
|
440
|
+
if (existing) {
|
|
441
|
+
return existing;
|
|
442
|
+
}
|
|
443
|
+
const cvar = new Cvar(def);
|
|
444
|
+
this.cvars.set(def.name, cvar);
|
|
445
|
+
return cvar;
|
|
446
|
+
}
|
|
447
|
+
get(name) {
|
|
448
|
+
return this.cvars.get(name);
|
|
449
|
+
}
|
|
450
|
+
setValue(name, value) {
|
|
451
|
+
const cvar = this.get(name);
|
|
452
|
+
if (!cvar) {
|
|
453
|
+
throw new Error(`Unknown cvar: ${name}`);
|
|
454
|
+
}
|
|
455
|
+
cvar.set(value);
|
|
456
|
+
return cvar;
|
|
457
|
+
}
|
|
458
|
+
resetAll() {
|
|
459
|
+
for (const cvar of this.cvars.values()) {
|
|
460
|
+
cvar.reset();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
applyLatched() {
|
|
464
|
+
let changed = false;
|
|
465
|
+
for (const cvar of this.cvars.values()) {
|
|
466
|
+
changed = cvar.applyLatched() || changed;
|
|
467
|
+
}
|
|
468
|
+
return changed;
|
|
469
|
+
}
|
|
470
|
+
list() {
|
|
471
|
+
return [...this.cvars.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/assets/pak.ts
|
|
476
|
+
var PAK_MAGIC = "PACK";
|
|
477
|
+
var HEADER_SIZE = 12;
|
|
478
|
+
var DIRECTORY_ENTRY_SIZE = 64;
|
|
479
|
+
function readCString(view, offset, maxLength) {
|
|
480
|
+
const codes = [];
|
|
481
|
+
for (let i = 0; i < maxLength; i += 1) {
|
|
482
|
+
const code = view.getUint8(offset + i);
|
|
483
|
+
if (code === 0) {
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
codes.push(code);
|
|
487
|
+
}
|
|
488
|
+
return String.fromCharCode(...codes);
|
|
489
|
+
}
|
|
490
|
+
function normalizePath(path) {
|
|
491
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "").toLowerCase();
|
|
492
|
+
}
|
|
493
|
+
function createCrcTable() {
|
|
494
|
+
const table = new Uint32Array(256);
|
|
495
|
+
for (let i = 0; i < 256; i += 1) {
|
|
496
|
+
let crc = i;
|
|
497
|
+
for (let j = 0; j < 8; j += 1) {
|
|
498
|
+
crc = (crc & 1) !== 0 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
|
|
499
|
+
}
|
|
500
|
+
table[i] = crc >>> 0;
|
|
501
|
+
}
|
|
502
|
+
return table;
|
|
503
|
+
}
|
|
504
|
+
var CRC_TABLE = createCrcTable();
|
|
505
|
+
function crc32(data) {
|
|
506
|
+
let crc = 4294967295;
|
|
507
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
508
|
+
const byte = data[i];
|
|
509
|
+
crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
510
|
+
}
|
|
511
|
+
return (crc ^ 4294967295) >>> 0;
|
|
512
|
+
}
|
|
513
|
+
var PakParseError = class extends Error {
|
|
514
|
+
};
|
|
515
|
+
var PakArchive = class _PakArchive {
|
|
516
|
+
constructor(name, buffer, entries, checksum) {
|
|
517
|
+
this.name = name;
|
|
518
|
+
this.buffer = buffer;
|
|
519
|
+
this.entries = new Map(entries.map((entry) => [entry.name, entry]));
|
|
520
|
+
this.checksum = checksum;
|
|
521
|
+
this.size = buffer.byteLength;
|
|
522
|
+
}
|
|
523
|
+
static fromArrayBuffer(name, buffer) {
|
|
524
|
+
const view = new DataView(buffer);
|
|
525
|
+
if (buffer.byteLength < HEADER_SIZE) {
|
|
526
|
+
throw new PakParseError("PAK buffer too small to contain header");
|
|
527
|
+
}
|
|
528
|
+
const magic = String.fromCharCode(
|
|
529
|
+
view.getUint8(0),
|
|
530
|
+
view.getUint8(1),
|
|
531
|
+
view.getUint8(2),
|
|
532
|
+
view.getUint8(3)
|
|
533
|
+
);
|
|
534
|
+
if (magic !== PAK_MAGIC) {
|
|
535
|
+
throw new PakParseError(`Invalid PAK header magic: ${magic}`);
|
|
536
|
+
}
|
|
537
|
+
const dirOffset = view.getInt32(4, true);
|
|
538
|
+
const dirLength = view.getInt32(8, true);
|
|
539
|
+
if (dirOffset < HEADER_SIZE) {
|
|
540
|
+
throw new PakParseError(`Invalid directory offset: ${dirOffset}`);
|
|
541
|
+
}
|
|
542
|
+
if (dirLength <= 0 || dirLength % DIRECTORY_ENTRY_SIZE !== 0) {
|
|
543
|
+
throw new PakParseError(`Invalid directory length: ${dirLength}`);
|
|
544
|
+
}
|
|
545
|
+
const dirEnd = dirOffset + dirLength;
|
|
546
|
+
if (dirEnd > buffer.byteLength) {
|
|
547
|
+
throw new PakParseError("Directory exceeds buffer length");
|
|
548
|
+
}
|
|
549
|
+
const entryCount = dirLength / DIRECTORY_ENTRY_SIZE;
|
|
550
|
+
const entries = [];
|
|
551
|
+
const dedupe = /* @__PURE__ */ new Map();
|
|
552
|
+
for (let i = 0; i < entryCount; i += 1) {
|
|
553
|
+
const offset = dirOffset + i * DIRECTORY_ENTRY_SIZE;
|
|
554
|
+
const rawName = readCString(view, offset, 56);
|
|
555
|
+
const normalized = normalizePath(rawName);
|
|
556
|
+
const fileOffset = view.getInt32(offset + 56, true);
|
|
557
|
+
const fileLength = view.getInt32(offset + 60, true);
|
|
558
|
+
if (fileOffset < 0 || fileLength < 0 || fileOffset + fileLength > buffer.byteLength) {
|
|
559
|
+
throw new PakParseError(
|
|
560
|
+
`Invalid entry bounds for ${rawName || "<unnamed>"} (offset=${fileOffset}, length=${fileLength})`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (!normalized) {
|
|
564
|
+
throw new PakParseError(`Entry ${i} has an empty name`);
|
|
565
|
+
}
|
|
566
|
+
const entry = { name: normalized, offset: fileOffset, length: fileLength };
|
|
567
|
+
dedupe.set(normalized, entry);
|
|
568
|
+
}
|
|
569
|
+
entries.push(...dedupe.values());
|
|
570
|
+
return new _PakArchive(name, buffer, entries, crc32(new Uint8Array(buffer)));
|
|
571
|
+
}
|
|
572
|
+
getEntry(path) {
|
|
573
|
+
return this.entries.get(normalizePath(path));
|
|
574
|
+
}
|
|
575
|
+
listEntries() {
|
|
576
|
+
return Array.from(this.entries.values());
|
|
577
|
+
}
|
|
578
|
+
readFile(path) {
|
|
579
|
+
const entry = this.getEntry(path);
|
|
580
|
+
if (!entry) {
|
|
581
|
+
throw new PakParseError(`File not found in PAK: ${path}`);
|
|
582
|
+
}
|
|
583
|
+
return new Uint8Array(this.buffer, entry.offset, entry.length);
|
|
584
|
+
}
|
|
585
|
+
validate() {
|
|
586
|
+
return {
|
|
587
|
+
checksum: this.checksum,
|
|
588
|
+
entries: this.listEntries()
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
function calculatePakChecksum(buffer) {
|
|
593
|
+
return crc32(new Uint8Array(buffer));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/assets/vfs.ts
|
|
597
|
+
var VirtualFileSystem = class {
|
|
598
|
+
constructor(archives = []) {
|
|
599
|
+
this.mounts = [];
|
|
600
|
+
this.files = /* @__PURE__ */ new Map();
|
|
601
|
+
archives.forEach((archive) => this.mountPak(archive));
|
|
602
|
+
}
|
|
603
|
+
mountPak(archive) {
|
|
604
|
+
this.mounts.push(archive);
|
|
605
|
+
for (const entry of archive.listEntries()) {
|
|
606
|
+
const key = normalizePath(entry.name);
|
|
607
|
+
this.files.set(key, { archive, entry });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
get mountedPaks() {
|
|
611
|
+
return [...this.mounts];
|
|
612
|
+
}
|
|
613
|
+
hasFile(path) {
|
|
614
|
+
return this.files.has(normalizePath(path));
|
|
615
|
+
}
|
|
616
|
+
stat(path) {
|
|
617
|
+
const source = this.files.get(normalizePath(path));
|
|
618
|
+
if (!source) {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
return { path: source.entry.name, size: source.entry.length, sourcePak: source.archive.name };
|
|
622
|
+
}
|
|
623
|
+
async readFile(path) {
|
|
624
|
+
const source = this.files.get(normalizePath(path));
|
|
625
|
+
if (!source) {
|
|
626
|
+
throw new Error(`File not found in VFS: ${path}`);
|
|
627
|
+
}
|
|
628
|
+
return source.archive.readFile(path);
|
|
629
|
+
}
|
|
630
|
+
list(directory = "") {
|
|
631
|
+
const dir = normalizePath(directory).replace(/\/+$|^\//g, "");
|
|
632
|
+
const files = [];
|
|
633
|
+
const directories = /* @__PURE__ */ new Set();
|
|
634
|
+
const prefix = dir ? `${dir}/` : "";
|
|
635
|
+
for (const source of this.files.values()) {
|
|
636
|
+
if (dir && !source.entry.name.startsWith(prefix)) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const relative = dir ? source.entry.name.slice(prefix.length) : source.entry.name;
|
|
640
|
+
const separatorIndex = relative.indexOf("/");
|
|
641
|
+
if (separatorIndex === -1) {
|
|
642
|
+
files.push({ path: source.entry.name, size: source.entry.length, sourcePak: source.archive.name });
|
|
643
|
+
} else {
|
|
644
|
+
directories.add(relative.slice(0, separatorIndex));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
648
|
+
return { files, directories: [...directories].sort() };
|
|
649
|
+
}
|
|
650
|
+
findByExtension(extension) {
|
|
651
|
+
const normalizedExt = extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
|
|
652
|
+
const results = [];
|
|
653
|
+
for (const source of this.files.values()) {
|
|
654
|
+
if (source.entry.name.toLowerCase().endsWith(normalizedExt)) {
|
|
655
|
+
results.push({ path: source.entry.name, size: source.entry.length, sourcePak: source.archive.name });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return results.sort((a, b) => a.path.localeCompare(b.path));
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// src/assets/ingestion.ts
|
|
663
|
+
var PakIngestionError = class extends Error {
|
|
664
|
+
constructor(file, cause) {
|
|
665
|
+
super(`Failed to ingest PAK: ${file}`);
|
|
666
|
+
this.file = file;
|
|
667
|
+
this.name = "PakIngestionError";
|
|
668
|
+
if (cause instanceof Error && cause.stack) {
|
|
669
|
+
this.stack = cause.stack;
|
|
670
|
+
}
|
|
671
|
+
this.cause = cause;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
async function readBlobWithProgress(source, onProgress) {
|
|
675
|
+
if (typeof source.arrayBuffer === "function") {
|
|
676
|
+
const buffer = await source.arrayBuffer();
|
|
677
|
+
onProgress?.({ file: "blob", loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength, state: "reading" });
|
|
678
|
+
return buffer;
|
|
679
|
+
}
|
|
680
|
+
if (typeof FileReader !== "undefined") {
|
|
681
|
+
return new Promise((resolve, reject) => {
|
|
682
|
+
const reader = new FileReader();
|
|
683
|
+
reader.onerror = () => reject(reader.error ?? new Error("Unknown FileReader error"));
|
|
684
|
+
reader.onprogress = (event) => {
|
|
685
|
+
onProgress?.({
|
|
686
|
+
file: "blob",
|
|
687
|
+
loadedBytes: event.loaded,
|
|
688
|
+
totalBytes: event.total || source.size,
|
|
689
|
+
state: "reading"
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
reader.onload = () => {
|
|
693
|
+
const result = reader.result;
|
|
694
|
+
if (result instanceof ArrayBuffer) {
|
|
695
|
+
resolve(result);
|
|
696
|
+
} else {
|
|
697
|
+
reject(new Error("Unexpected FileReader result"));
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
reader.readAsArrayBuffer(source);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (typeof Response !== "undefined") {
|
|
704
|
+
const buffer = await new Response(source).arrayBuffer();
|
|
705
|
+
onProgress?.({ file: "blob", loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength, state: "reading" });
|
|
706
|
+
return buffer;
|
|
707
|
+
}
|
|
708
|
+
if (typeof source.stream === "function") {
|
|
709
|
+
const reader = source.stream().getReader();
|
|
710
|
+
const chunks = [];
|
|
711
|
+
let loaded = 0;
|
|
712
|
+
while (true) {
|
|
713
|
+
const { done, value } = await reader.read();
|
|
714
|
+
if (done) {
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
if (!value) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const chunk = value;
|
|
721
|
+
chunks.push(chunk);
|
|
722
|
+
loaded += chunk.byteLength;
|
|
723
|
+
onProgress?.({ file: "blob", loadedBytes: loaded, totalBytes: source.size, state: "reading" });
|
|
724
|
+
}
|
|
725
|
+
const result = new Uint8Array(loaded);
|
|
726
|
+
let offset = 0;
|
|
727
|
+
for (const chunk of chunks) {
|
|
728
|
+
result.set(chunk, offset);
|
|
729
|
+
offset += chunk.byteLength;
|
|
730
|
+
}
|
|
731
|
+
return result.buffer;
|
|
732
|
+
}
|
|
733
|
+
throw new PakIngestionError("blob", new Error("Unsupported Blob type"));
|
|
734
|
+
}
|
|
735
|
+
async function toArrayBuffer(source, onProgress) {
|
|
736
|
+
if (source.data instanceof ArrayBuffer) {
|
|
737
|
+
onProgress?.({ file: source.name, loadedBytes: source.data.byteLength, totalBytes: source.data.byteLength, state: "reading" });
|
|
738
|
+
return source.data;
|
|
739
|
+
}
|
|
740
|
+
if (source.data instanceof Blob) {
|
|
741
|
+
const totalBytes = source.data.size;
|
|
742
|
+
return readBlobWithProgress(
|
|
743
|
+
source.data,
|
|
744
|
+
(progress) => onProgress?.({ ...progress, file: source.name, totalBytes })
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
const buffer = source.data.buffer.slice(source.data.byteOffset, source.data.byteOffset + source.data.byteLength);
|
|
748
|
+
onProgress?.({ file: source.name, loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength, state: "reading" });
|
|
749
|
+
return buffer;
|
|
750
|
+
}
|
|
751
|
+
async function ingestPaks(vfs, sources, onProgressOrOptions) {
|
|
752
|
+
const options = typeof onProgressOrOptions === "function" ? { onProgress: onProgressOrOptions } : onProgressOrOptions ?? {};
|
|
753
|
+
const results = [];
|
|
754
|
+
for (const source of sources) {
|
|
755
|
+
try {
|
|
756
|
+
const buffer = await toArrayBuffer(source, options.onProgress);
|
|
757
|
+
const archive = PakArchive.fromArrayBuffer(source.name, buffer);
|
|
758
|
+
vfs.mountPak(archive);
|
|
759
|
+
options.onProgress?.({ file: source.name, loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength, state: "parsed" });
|
|
760
|
+
results.push({ archive, mounted: true });
|
|
761
|
+
} catch (error) {
|
|
762
|
+
options.onProgress?.({ file: source.name, loadedBytes: 0, totalBytes: 0, state: "error" });
|
|
763
|
+
options.onError?.(source.name, error);
|
|
764
|
+
if (options.stopOnError) {
|
|
765
|
+
throw new PakIngestionError(source.name, error);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return results;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/assets/cache.ts
|
|
773
|
+
var LruCache = class {
|
|
774
|
+
constructor(capacity) {
|
|
775
|
+
this.capacity = capacity;
|
|
776
|
+
this.map = /* @__PURE__ */ new Map();
|
|
777
|
+
if (capacity <= 0) {
|
|
778
|
+
throw new RangeError("LRU cache capacity must be greater than zero");
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
get size() {
|
|
782
|
+
return this.map.size;
|
|
783
|
+
}
|
|
784
|
+
has(key) {
|
|
785
|
+
return this.map.has(key);
|
|
786
|
+
}
|
|
787
|
+
get(key) {
|
|
788
|
+
const value = this.map.get(key);
|
|
789
|
+
if (value === void 0) {
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
this.map.delete(key);
|
|
793
|
+
this.map.set(key, value);
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
set(key, value) {
|
|
797
|
+
if (this.map.has(key)) {
|
|
798
|
+
this.map.delete(key);
|
|
799
|
+
}
|
|
800
|
+
this.map.set(key, value);
|
|
801
|
+
if (this.map.size > this.capacity) {
|
|
802
|
+
const oldestKey = this.map.keys().next();
|
|
803
|
+
if (!oldestKey.done) {
|
|
804
|
+
this.map.delete(oldestKey.value);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
delete(key) {
|
|
809
|
+
return this.map.delete(key);
|
|
810
|
+
}
|
|
811
|
+
clear() {
|
|
812
|
+
this.map.clear();
|
|
813
|
+
}
|
|
814
|
+
entries() {
|
|
815
|
+
return Array.from(this.map.entries()).reverse().map(([key, value]) => ({ key, value }));
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
// src/assets/browserIngestion.ts
|
|
820
|
+
function toArray(files) {
|
|
821
|
+
return Array.isArray(files) ? files : Array.from(files);
|
|
822
|
+
}
|
|
823
|
+
function filesToPakSources(files) {
|
|
824
|
+
return toArray(files).map((file) => ({ name: file.name, data: file }));
|
|
825
|
+
}
|
|
826
|
+
async function ingestPakFiles(vfs, files, options) {
|
|
827
|
+
const sources = filesToPakSources(files);
|
|
828
|
+
return ingestPaks(vfs, sources, options ?? {});
|
|
829
|
+
}
|
|
830
|
+
function wireDropTarget(element, handler) {
|
|
831
|
+
const onDragOver = (event) => {
|
|
832
|
+
event.preventDefault();
|
|
833
|
+
event.dataTransfer?.dropEffect && (event.dataTransfer.dropEffect = "copy");
|
|
834
|
+
};
|
|
835
|
+
const onDrop = (event) => {
|
|
836
|
+
event.preventDefault();
|
|
837
|
+
const droppedFiles = event.dataTransfer?.files;
|
|
838
|
+
if (droppedFiles && droppedFiles.length > 0) {
|
|
839
|
+
handler(Array.from(droppedFiles));
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
element.addEventListener("dragover", onDragOver);
|
|
843
|
+
element.addEventListener("drop", onDrop);
|
|
844
|
+
return () => {
|
|
845
|
+
element.removeEventListener("dragover", onDragOver);
|
|
846
|
+
element.removeEventListener("drop", onDrop);
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function wireFileInput(input, handler) {
|
|
850
|
+
const onChange = (event) => {
|
|
851
|
+
const target = event.target;
|
|
852
|
+
if (!target || !target.files || target.files.length === 0) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
handler(target.files);
|
|
856
|
+
target.value = "";
|
|
857
|
+
};
|
|
858
|
+
input.addEventListener("change", onChange);
|
|
859
|
+
return () => input.removeEventListener("change", onChange);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/assets/md2.ts
|
|
863
|
+
var MD2_MAGIC = 844121161;
|
|
864
|
+
var MD2_VERSION = 8;
|
|
865
|
+
var HEADER_SIZE2 = 17 * 4;
|
|
866
|
+
var MD2_NORMALS = [
|
|
867
|
+
{ x: -0.525731, y: 0, z: 0.850651 },
|
|
868
|
+
{ x: -0.442863, y: 0.238856, z: 0.864188 },
|
|
869
|
+
{ x: -0.295242, y: 0, z: 0.955423 },
|
|
870
|
+
{ x: -0.309017, y: 0.5, z: 0.809017 },
|
|
871
|
+
{ x: -0.16246, y: 0.262866, z: 0.951056 },
|
|
872
|
+
{ x: 0, y: 0, z: 1 },
|
|
873
|
+
{ x: 0, y: 0.850651, z: 0.525731 },
|
|
874
|
+
{ x: -0.147621, y: 0.716567, z: 0.681718 },
|
|
875
|
+
{ x: 0.147621, y: 0.716567, z: 0.681718 },
|
|
876
|
+
{ x: 0, y: 0.525731, z: 0.850651 },
|
|
877
|
+
{ x: 0.309017, y: 0.5, z: 0.809017 },
|
|
878
|
+
{ x: 0.525731, y: 0, z: 0.850651 },
|
|
879
|
+
{ x: 0.295242, y: 0, z: 0.955423 },
|
|
880
|
+
{ x: 0.442863, y: 0.238856, z: 0.864188 },
|
|
881
|
+
{ x: 0.16246, y: 0.262866, z: 0.951056 },
|
|
882
|
+
{ x: -0.681718, y: 0.147621, z: 0.716567 },
|
|
883
|
+
{ x: -0.809017, y: 0.309017, z: 0.5 },
|
|
884
|
+
{ x: -0.587785, y: 0.425325, z: 0.688191 },
|
|
885
|
+
{ x: -0.850651, y: 0.525731, z: 0 },
|
|
886
|
+
{ x: -0.864188, y: 0.442863, z: 0.238856 },
|
|
887
|
+
{ x: -0.716567, y: 0.681718, z: 0.147621 },
|
|
888
|
+
{ x: -0.688191, y: 0.587785, z: 0.425325 },
|
|
889
|
+
{ x: -0.5, y: 0.809017, z: 0.309017 },
|
|
890
|
+
{ x: -0.238856, y: 0.864188, z: 0.442863 },
|
|
891
|
+
{ x: -0.425325, y: 0.688191, z: 0.587785 },
|
|
892
|
+
{ x: -0.716567, y: 0.681718, z: -0.147621 },
|
|
893
|
+
{ x: -0.5, y: 0.809017, z: -0.309017 },
|
|
894
|
+
{ x: -0.525731, y: 0.850651, z: 0 },
|
|
895
|
+
{ x: 0, y: 0.850651, z: -0.525731 },
|
|
896
|
+
{ x: -0.238856, y: 0.864188, z: -0.442863 },
|
|
897
|
+
{ x: 0, y: 0.955423, z: -0.295242 },
|
|
898
|
+
{ x: -0.262866, y: 0.951056, z: -0.16246 },
|
|
899
|
+
{ x: 0, y: 1, z: 0 },
|
|
900
|
+
{ x: 0, y: 0.955423, z: 0.295242 },
|
|
901
|
+
{ x: -0.262866, y: 0.951056, z: 0.16246 },
|
|
902
|
+
{ x: 0.238856, y: 0.864188, z: 0.442863 },
|
|
903
|
+
{ x: 0.262866, y: 0.951056, z: 0.16246 },
|
|
904
|
+
{ x: 0.5, y: 0.809017, z: 0.309017 },
|
|
905
|
+
{ x: 0.238856, y: 0.864188, z: -0.442863 },
|
|
906
|
+
{ x: 0.262866, y: 0.951056, z: -0.16246 },
|
|
907
|
+
{ x: 0.5, y: 0.809017, z: -0.309017 },
|
|
908
|
+
{ x: 0.850651, y: 0.525731, z: 0 },
|
|
909
|
+
{ x: 0.716567, y: 0.681718, z: 0.147621 },
|
|
910
|
+
{ x: 0.716567, y: 0.681718, z: -0.147621 },
|
|
911
|
+
{ x: 0.525731, y: 0.850651, z: 0 },
|
|
912
|
+
{ x: 0.425325, y: 0.688191, z: 0.587785 },
|
|
913
|
+
{ x: 0.864188, y: 0.442863, z: 0.238856 },
|
|
914
|
+
{ x: 0.688191, y: 0.587785, z: 0.425325 },
|
|
915
|
+
{ x: 0.809017, y: 0.309017, z: 0.5 },
|
|
916
|
+
{ x: 0.681718, y: 0.147621, z: 0.716567 },
|
|
917
|
+
{ x: 0.587785, y: 0.425325, z: 0.688191 },
|
|
918
|
+
{ x: 0.955423, y: 0.295242, z: 0 },
|
|
919
|
+
{ x: 1, y: 0, z: 0 },
|
|
920
|
+
{ x: 0.951056, y: 0.16246, z: 0.262866 },
|
|
921
|
+
{ x: 0.850651, y: -0.525731, z: 0 },
|
|
922
|
+
{ x: 0.955423, y: -0.295242, z: 0 },
|
|
923
|
+
{ x: 0.864188, y: -0.442863, z: 0.238856 },
|
|
924
|
+
{ x: 0.951056, y: -0.16246, z: 0.262866 },
|
|
925
|
+
{ x: 0.809017, y: -0.309017, z: 0.5 },
|
|
926
|
+
{ x: 0.681718, y: -0.147621, z: 0.716567 },
|
|
927
|
+
{ x: 0.850651, y: 0, z: 0.525731 },
|
|
928
|
+
{ x: 0.864188, y: 0.442863, z: -0.238856 },
|
|
929
|
+
{ x: 0.809017, y: 0.309017, z: -0.5 },
|
|
930
|
+
{ x: 0.951056, y: 0.16246, z: -0.262866 },
|
|
931
|
+
{ x: 0.525731, y: 0, z: -0.850651 },
|
|
932
|
+
{ x: 0.681718, y: 0.147621, z: -0.716567 },
|
|
933
|
+
{ x: 0.681718, y: -0.147621, z: -0.716567 },
|
|
934
|
+
{ x: 0.850651, y: 0, z: -0.525731 },
|
|
935
|
+
{ x: 0.809017, y: -0.309017, z: -0.5 },
|
|
936
|
+
{ x: 0.864188, y: -0.442863, z: -0.238856 },
|
|
937
|
+
{ x: 0.951056, y: -0.16246, z: -0.262866 },
|
|
938
|
+
{ x: 0.147621, y: 0.716567, z: -0.681718 },
|
|
939
|
+
{ x: 0.309017, y: 0.5, z: -0.809017 },
|
|
940
|
+
{ x: 0.425325, y: 0.688191, z: -0.587785 },
|
|
941
|
+
{ x: 0.442863, y: 0.238856, z: -0.864188 },
|
|
942
|
+
{ x: 0.587785, y: 0.425325, z: -0.688191 },
|
|
943
|
+
{ x: 0.688191, y: 0.587785, z: -0.425325 },
|
|
944
|
+
{ x: -0.147621, y: 0.716567, z: -0.681718 },
|
|
945
|
+
{ x: -0.309017, y: 0.5, z: -0.809017 },
|
|
946
|
+
{ x: 0, y: 0.525731, z: -0.850651 },
|
|
947
|
+
{ x: -0.525731, y: 0, z: -0.850651 },
|
|
948
|
+
{ x: -0.442863, y: 0.238856, z: -0.864188 },
|
|
949
|
+
{ x: -0.295242, y: 0, z: -0.955423 },
|
|
950
|
+
{ x: -0.16246, y: 0.262866, z: -0.951056 },
|
|
951
|
+
{ x: 0, y: 0, z: -1 },
|
|
952
|
+
{ x: 0.295242, y: 0, z: -0.955423 },
|
|
953
|
+
{ x: 0.16246, y: 0.262866, z: -0.951056 },
|
|
954
|
+
{ x: -0.442863, y: -0.238856, z: -0.864188 },
|
|
955
|
+
{ x: -0.309017, y: -0.5, z: -0.809017 },
|
|
956
|
+
{ x: -0.16246, y: -0.262866, z: -0.951056 },
|
|
957
|
+
{ x: 0, y: -0.850651, z: -0.525731 },
|
|
958
|
+
{ x: -0.147621, y: -0.716567, z: -0.681718 },
|
|
959
|
+
{ x: 0.147621, y: -0.716567, z: -0.681718 },
|
|
960
|
+
{ x: 0, y: -0.525731, z: -0.850651 },
|
|
961
|
+
{ x: 0.309017, y: -0.5, z: -0.809017 },
|
|
962
|
+
{ x: 0.442863, y: -0.238856, z: -0.864188 },
|
|
963
|
+
{ x: 0.16246, y: -0.262866, z: -0.951056 },
|
|
964
|
+
{ x: 0.238856, y: -0.864188, z: -0.442863 },
|
|
965
|
+
{ x: 0.5, y: -0.809017, z: -0.309017 },
|
|
966
|
+
{ x: 0.425325, y: -0.688191, z: -0.587785 },
|
|
967
|
+
{ x: 0.716567, y: -0.681718, z: -0.147621 },
|
|
968
|
+
{ x: 0.688191, y: -0.587785, z: -0.425325 },
|
|
969
|
+
{ x: 0.587785, y: -0.425325, z: -0.688191 },
|
|
970
|
+
{ x: 0, y: -0.955423, z: -0.295242 },
|
|
971
|
+
{ x: 0, y: -1, z: 0 },
|
|
972
|
+
{ x: 0.262866, y: -0.951056, z: -0.16246 },
|
|
973
|
+
{ x: 0, y: -0.850651, z: 0.525731 },
|
|
974
|
+
{ x: 0, y: -0.955423, z: 0.295242 },
|
|
975
|
+
{ x: 0.238856, y: -0.864188, z: 0.442863 },
|
|
976
|
+
{ x: 0.262866, y: -0.951056, z: 0.16246 },
|
|
977
|
+
{ x: 0.5, y: -0.809017, z: 0.309017 },
|
|
978
|
+
{ x: 0.716567, y: -0.681718, z: 0.147621 },
|
|
979
|
+
{ x: 0.525731, y: -0.850651, z: 0 },
|
|
980
|
+
{ x: -0.238856, y: -0.864188, z: -0.442863 },
|
|
981
|
+
{ x: -0.5, y: -0.809017, z: -0.309017 },
|
|
982
|
+
{ x: -0.262866, y: -0.951056, z: -0.16246 },
|
|
983
|
+
{ x: -0.850651, y: -0.525731, z: 0 },
|
|
984
|
+
{ x: -0.716567, y: -0.681718, z: -0.147621 },
|
|
985
|
+
{ x: -0.716567, y: -0.681718, z: 0.147621 },
|
|
986
|
+
{ x: -0.525731, y: -0.850651, z: 0 },
|
|
987
|
+
{ x: -0.5, y: -0.809017, z: 0.309017 },
|
|
988
|
+
{ x: -0.238856, y: -0.864188, z: 0.442863 },
|
|
989
|
+
{ x: -0.262866, y: -0.951056, z: 0.16246 },
|
|
990
|
+
{ x: -0.864188, y: -0.442863, z: 0.238856 },
|
|
991
|
+
{ x: -0.809017, y: -0.309017, z: 0.5 },
|
|
992
|
+
{ x: -0.688191, y: -0.587785, z: 0.425325 },
|
|
993
|
+
{ x: -0.681718, y: -0.147621, z: 0.716567 },
|
|
994
|
+
{ x: -0.442863, y: -0.238856, z: 0.864188 },
|
|
995
|
+
{ x: -0.587785, y: -0.425325, z: 0.688191 },
|
|
996
|
+
{ x: -0.309017, y: -0.5, z: 0.809017 },
|
|
997
|
+
{ x: -0.147621, y: -0.716567, z: 0.681718 },
|
|
998
|
+
{ x: -0.425325, y: -0.688191, z: 0.587785 },
|
|
999
|
+
{ x: -0.16246, y: -0.262866, z: 0.951056 },
|
|
1000
|
+
{ x: 0.442863, y: -0.238856, z: 0.864188 },
|
|
1001
|
+
{ x: 0.16246, y: -0.262866, z: 0.951056 },
|
|
1002
|
+
{ x: 0.309017, y: -0.5, z: 0.809017 },
|
|
1003
|
+
{ x: 0.147621, y: -0.716567, z: 0.681718 },
|
|
1004
|
+
{ x: 0, y: -0.525731, z: 0.850651 },
|
|
1005
|
+
{ x: 0.425325, y: -0.688191, z: 0.587785 },
|
|
1006
|
+
{ x: 0.587785, y: -0.425325, z: 0.688191 },
|
|
1007
|
+
{ x: 0.688191, y: -0.587785, z: 0.425325 },
|
|
1008
|
+
{ x: -0.955423, y: 0.295242, z: 0 },
|
|
1009
|
+
{ x: -0.951056, y: 0.16246, z: 0.262866 },
|
|
1010
|
+
{ x: -1, y: 0, z: 0 },
|
|
1011
|
+
{ x: -0.850651, y: 0, z: 0.525731 },
|
|
1012
|
+
{ x: -0.955423, y: -0.295242, z: 0 },
|
|
1013
|
+
{ x: -0.951056, y: -0.16246, z: 0.262866 },
|
|
1014
|
+
{ x: -0.864188, y: 0.442863, z: -0.238856 },
|
|
1015
|
+
{ x: -0.951056, y: 0.16246, z: -0.262866 },
|
|
1016
|
+
{ x: -0.809017, y: 0.309017, z: -0.5 },
|
|
1017
|
+
{ x: -0.864188, y: -0.442863, z: -0.238856 },
|
|
1018
|
+
{ x: -0.951056, y: -0.16246, z: -0.262866 },
|
|
1019
|
+
{ x: -0.809017, y: -0.309017, z: -0.5 },
|
|
1020
|
+
{ x: -0.681718, y: 0.147621, z: -0.716567 },
|
|
1021
|
+
{ x: -0.681718, y: -0.147621, z: -0.716567 },
|
|
1022
|
+
{ x: -0.850651, y: 0, z: -0.525731 },
|
|
1023
|
+
{ x: -0.688191, y: 0.587785, z: -0.425325 },
|
|
1024
|
+
{ x: -0.587785, y: 0.425325, z: -0.688191 },
|
|
1025
|
+
{ x: -0.425325, y: 0.688191, z: -0.587785 },
|
|
1026
|
+
{ x: -0.425325, y: -0.688191, z: -0.587785 },
|
|
1027
|
+
{ x: -0.587785, y: -0.425325, z: -0.688191 },
|
|
1028
|
+
{ x: -0.688191, y: -0.587785, z: -0.425325 }
|
|
1029
|
+
];
|
|
1030
|
+
var Md2ParseError = class extends Error {
|
|
1031
|
+
};
|
|
1032
|
+
var Md2Loader = class {
|
|
1033
|
+
constructor(vfs) {
|
|
1034
|
+
this.vfs = vfs;
|
|
1035
|
+
}
|
|
1036
|
+
async load(path) {
|
|
1037
|
+
const bytes = await this.vfs.readFile(path);
|
|
1038
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
1039
|
+
copy.set(bytes);
|
|
1040
|
+
return parseMd2(copy.buffer);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
function readCString2(view, offset, maxLength) {
|
|
1044
|
+
const chars = [];
|
|
1045
|
+
for (let i = 0; i < maxLength; i += 1) {
|
|
1046
|
+
const code = view.getUint8(offset + i);
|
|
1047
|
+
if (code === 0) break;
|
|
1048
|
+
chars.push(code);
|
|
1049
|
+
}
|
|
1050
|
+
return String.fromCharCode(...chars);
|
|
1051
|
+
}
|
|
1052
|
+
function validateSection(buffer, offset, length, label) {
|
|
1053
|
+
if (length === 0) return;
|
|
1054
|
+
if (offset < HEADER_SIZE2 || offset + length > buffer.byteLength) {
|
|
1055
|
+
throw new Md2ParseError(`${label} section is out of bounds`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function parseHeader(buffer) {
|
|
1059
|
+
if (buffer.byteLength < HEADER_SIZE2) {
|
|
1060
|
+
throw new Md2ParseError("MD2 buffer too small to contain header");
|
|
1061
|
+
}
|
|
1062
|
+
const view = new DataView(buffer);
|
|
1063
|
+
const ident = view.getInt32(0, true);
|
|
1064
|
+
const version = view.getInt32(4, true);
|
|
1065
|
+
if (ident !== MD2_MAGIC) {
|
|
1066
|
+
throw new Md2ParseError(`Invalid MD2 ident: ${ident}`);
|
|
1067
|
+
}
|
|
1068
|
+
if (version !== MD2_VERSION) {
|
|
1069
|
+
throw new Md2ParseError(`Unsupported MD2 version: ${version}`);
|
|
1070
|
+
}
|
|
1071
|
+
const header = {
|
|
1072
|
+
ident,
|
|
1073
|
+
version,
|
|
1074
|
+
skinWidth: view.getInt32(8, true),
|
|
1075
|
+
skinHeight: view.getInt32(12, true),
|
|
1076
|
+
frameSize: view.getInt32(16, true),
|
|
1077
|
+
numSkins: view.getInt32(20, true),
|
|
1078
|
+
numVertices: view.getInt32(24, true),
|
|
1079
|
+
numTexCoords: view.getInt32(28, true),
|
|
1080
|
+
numTriangles: view.getInt32(32, true),
|
|
1081
|
+
numGlCommands: view.getInt32(36, true),
|
|
1082
|
+
numFrames: view.getInt32(40, true),
|
|
1083
|
+
offsetSkins: view.getInt32(44, true),
|
|
1084
|
+
offsetTexCoords: view.getInt32(48, true),
|
|
1085
|
+
offsetTriangles: view.getInt32(52, true),
|
|
1086
|
+
offsetFrames: view.getInt32(56, true),
|
|
1087
|
+
offsetGlCommands: view.getInt32(60, true),
|
|
1088
|
+
offsetEnd: view.getInt32(64, true)
|
|
1089
|
+
};
|
|
1090
|
+
const expectedFrameSize = 40 + header.numVertices * 4;
|
|
1091
|
+
if (header.frameSize !== expectedFrameSize) {
|
|
1092
|
+
throw new Md2ParseError(`Unexpected frame size ${header.frameSize}, expected ${expectedFrameSize}`);
|
|
1093
|
+
}
|
|
1094
|
+
if (header.offsetEnd > buffer.byteLength) {
|
|
1095
|
+
throw new Md2ParseError("MD2 offset_end exceeds buffer length");
|
|
1096
|
+
}
|
|
1097
|
+
return header;
|
|
1098
|
+
}
|
|
1099
|
+
function parseSkins(buffer, header) {
|
|
1100
|
+
const size = header.numSkins * 64;
|
|
1101
|
+
validateSection(buffer, header.offsetSkins, size, "skins");
|
|
1102
|
+
const view = new DataView(buffer, header.offsetSkins, size);
|
|
1103
|
+
const skins = [];
|
|
1104
|
+
for (let i = 0; i < header.numSkins; i += 1) {
|
|
1105
|
+
skins.push({ name: readCString2(view, i * 64, 64) });
|
|
1106
|
+
}
|
|
1107
|
+
return skins;
|
|
1108
|
+
}
|
|
1109
|
+
function parseTexCoords(buffer, header) {
|
|
1110
|
+
const size = header.numTexCoords * 4;
|
|
1111
|
+
validateSection(buffer, header.offsetTexCoords, size, "texcoords");
|
|
1112
|
+
const view = new DataView(buffer, header.offsetTexCoords, size);
|
|
1113
|
+
const texCoords = [];
|
|
1114
|
+
for (let i = 0; i < header.numTexCoords; i += 1) {
|
|
1115
|
+
const base = i * 4;
|
|
1116
|
+
texCoords.push({ s: view.getInt16(base, true), t: view.getInt16(base + 2, true) });
|
|
1117
|
+
}
|
|
1118
|
+
return texCoords;
|
|
1119
|
+
}
|
|
1120
|
+
function parseTriangles(buffer, header) {
|
|
1121
|
+
const size = header.numTriangles * 12;
|
|
1122
|
+
validateSection(buffer, header.offsetTriangles, size, "triangles");
|
|
1123
|
+
const view = new DataView(buffer, header.offsetTriangles, size);
|
|
1124
|
+
const triangles = [];
|
|
1125
|
+
for (let i = 0; i < header.numTriangles; i += 1) {
|
|
1126
|
+
const base = i * 12;
|
|
1127
|
+
const vertexIndices = [
|
|
1128
|
+
view.getUint16(base, true),
|
|
1129
|
+
view.getUint16(base + 2, true),
|
|
1130
|
+
view.getUint16(base + 4, true)
|
|
1131
|
+
];
|
|
1132
|
+
const texCoordIndices = [
|
|
1133
|
+
view.getUint16(base + 6, true),
|
|
1134
|
+
view.getUint16(base + 8, true),
|
|
1135
|
+
view.getUint16(base + 10, true)
|
|
1136
|
+
];
|
|
1137
|
+
if (vertexIndices.some((v) => v >= header.numVertices) || texCoordIndices.some((t) => t >= header.numTexCoords)) {
|
|
1138
|
+
throw new Md2ParseError("Triangle references out of range vertex or texcoord");
|
|
1139
|
+
}
|
|
1140
|
+
triangles.push({ vertexIndices, texCoordIndices });
|
|
1141
|
+
}
|
|
1142
|
+
return triangles;
|
|
1143
|
+
}
|
|
1144
|
+
function parseFrames(buffer, header) {
|
|
1145
|
+
const size = header.numFrames * header.frameSize;
|
|
1146
|
+
validateSection(buffer, header.offsetFrames, size, "frames");
|
|
1147
|
+
const frames = [];
|
|
1148
|
+
for (let i = 0; i < header.numFrames; i += 1) {
|
|
1149
|
+
const base = header.offsetFrames + i * header.frameSize;
|
|
1150
|
+
const view = new DataView(buffer, base, header.frameSize);
|
|
1151
|
+
const scale = { x: view.getFloat32(0, true), y: view.getFloat32(4, true), z: view.getFloat32(8, true) };
|
|
1152
|
+
const translate = {
|
|
1153
|
+
x: view.getFloat32(12, true),
|
|
1154
|
+
y: view.getFloat32(16, true),
|
|
1155
|
+
z: view.getFloat32(20, true)
|
|
1156
|
+
};
|
|
1157
|
+
const name = readCString2(view, 24, 16);
|
|
1158
|
+
const vertices = [];
|
|
1159
|
+
for (let v = 0; v < header.numVertices; v += 1) {
|
|
1160
|
+
const offset = 40 + v * 4;
|
|
1161
|
+
const position = {
|
|
1162
|
+
x: view.getUint8(offset) * scale.x + translate.x,
|
|
1163
|
+
y: view.getUint8(offset + 1) * scale.y + translate.y,
|
|
1164
|
+
z: view.getUint8(offset + 2) * scale.z + translate.z
|
|
1165
|
+
};
|
|
1166
|
+
const normalIndex = view.getUint8(offset + 3);
|
|
1167
|
+
const normal = MD2_NORMALS[normalIndex];
|
|
1168
|
+
if (!normal) {
|
|
1169
|
+
throw new Md2ParseError(`Invalid normal index ${normalIndex} in frame ${name}`);
|
|
1170
|
+
}
|
|
1171
|
+
vertices.push({ position, normalIndex, normal });
|
|
1172
|
+
}
|
|
1173
|
+
frames.push({ name, vertices });
|
|
1174
|
+
}
|
|
1175
|
+
return frames;
|
|
1176
|
+
}
|
|
1177
|
+
function parseGlCommands(buffer, header) {
|
|
1178
|
+
const size = header.numGlCommands * 4;
|
|
1179
|
+
validateSection(buffer, header.offsetGlCommands, size, "gl commands");
|
|
1180
|
+
if (size === 0) {
|
|
1181
|
+
return [];
|
|
1182
|
+
}
|
|
1183
|
+
const view = new DataView(buffer, header.offsetGlCommands, size);
|
|
1184
|
+
const commands = [];
|
|
1185
|
+
let cursor = 0;
|
|
1186
|
+
while (true) {
|
|
1187
|
+
if (cursor + 4 > size) {
|
|
1188
|
+
throw new Md2ParseError("GL command list ended unexpectedly");
|
|
1189
|
+
}
|
|
1190
|
+
const count = view.getInt32(cursor, true);
|
|
1191
|
+
cursor += 4;
|
|
1192
|
+
if (count === 0) break;
|
|
1193
|
+
const vertexCount = Math.abs(count);
|
|
1194
|
+
const vertices = [];
|
|
1195
|
+
const bytesNeeded = vertexCount * 12;
|
|
1196
|
+
if (cursor + bytesNeeded > size) {
|
|
1197
|
+
throw new Md2ParseError("GL command vertex block exceeds buffer");
|
|
1198
|
+
}
|
|
1199
|
+
for (let i = 0; i < vertexCount; i += 1) {
|
|
1200
|
+
const s = view.getFloat32(cursor, true);
|
|
1201
|
+
const t = view.getFloat32(cursor + 4, true);
|
|
1202
|
+
const vertexIndex = view.getInt32(cursor + 8, true);
|
|
1203
|
+
cursor += 12;
|
|
1204
|
+
if (vertexIndex < 0 || vertexIndex >= header.numVertices) {
|
|
1205
|
+
throw new Md2ParseError("GL command references invalid vertex index");
|
|
1206
|
+
}
|
|
1207
|
+
vertices.push({ s, t, vertexIndex });
|
|
1208
|
+
}
|
|
1209
|
+
commands.push({ mode: count > 0 ? "strip" : "fan", vertices });
|
|
1210
|
+
}
|
|
1211
|
+
if (cursor !== size) {
|
|
1212
|
+
throw new Md2ParseError("GL command list did not consume expected data");
|
|
1213
|
+
}
|
|
1214
|
+
return commands;
|
|
1215
|
+
}
|
|
1216
|
+
function parseMd2(buffer) {
|
|
1217
|
+
const header = parseHeader(buffer);
|
|
1218
|
+
const skins = parseSkins(buffer, header);
|
|
1219
|
+
const texCoords = parseTexCoords(buffer, header);
|
|
1220
|
+
const triangles = parseTriangles(buffer, header);
|
|
1221
|
+
const frames = parseFrames(buffer, header);
|
|
1222
|
+
const glCommands = parseGlCommands(buffer, header);
|
|
1223
|
+
return { header, skins, texCoords, triangles, frames, glCommands };
|
|
1224
|
+
}
|
|
1225
|
+
function groupMd2Animations(frames) {
|
|
1226
|
+
const animations = [];
|
|
1227
|
+
let index = 0;
|
|
1228
|
+
while (index < frames.length) {
|
|
1229
|
+
const name = frames[index].name;
|
|
1230
|
+
const base = name.replace(/\d+$/, "") || name;
|
|
1231
|
+
let end = index;
|
|
1232
|
+
while (end + 1 < frames.length) {
|
|
1233
|
+
const nextBase = frames[end + 1].name.replace(/\d+$/, "") || frames[end + 1].name;
|
|
1234
|
+
if (nextBase !== base) break;
|
|
1235
|
+
end += 1;
|
|
1236
|
+
}
|
|
1237
|
+
animations.push({ name: base, firstFrame: index, lastFrame: end });
|
|
1238
|
+
index = end + 1;
|
|
1239
|
+
}
|
|
1240
|
+
return animations;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/render/context.ts
|
|
1244
|
+
function configureDefaultGLState(gl) {
|
|
1245
|
+
gl.enable(gl.DEPTH_TEST);
|
|
1246
|
+
gl.depthFunc(gl.LEQUAL);
|
|
1247
|
+
gl.enable(gl.CULL_FACE);
|
|
1248
|
+
gl.cullFace(gl.BACK);
|
|
1249
|
+
gl.enable(gl.BLEND);
|
|
1250
|
+
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1251
|
+
}
|
|
1252
|
+
function queryExtensions(gl, required, optional, collector) {
|
|
1253
|
+
for (const name of required) {
|
|
1254
|
+
const ext = gl.getExtension(name);
|
|
1255
|
+
if (!ext) {
|
|
1256
|
+
throw new Error(`Missing required WebGL extension: ${name}`);
|
|
1257
|
+
}
|
|
1258
|
+
collector.set(name, ext);
|
|
1259
|
+
}
|
|
1260
|
+
for (const name of optional) {
|
|
1261
|
+
const ext = gl.getExtension(name);
|
|
1262
|
+
if (ext) {
|
|
1263
|
+
collector.set(name, ext);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
function createWebGLContext(canvas, options = {}) {
|
|
1268
|
+
const { contextAttributes, requiredExtensions = [], optionalExtensions = [] } = options;
|
|
1269
|
+
const gl = canvas.getContext("webgl2", contextAttributes ?? { antialias: true });
|
|
1270
|
+
if (!gl) {
|
|
1271
|
+
throw new Error("WebGL2 not supported or failed to initialize");
|
|
1272
|
+
}
|
|
1273
|
+
configureDefaultGLState(gl);
|
|
1274
|
+
const extensions = /* @__PURE__ */ new Map();
|
|
1275
|
+
queryExtensions(gl, requiredExtensions, optionalExtensions, extensions);
|
|
1276
|
+
let lost = false;
|
|
1277
|
+
const lostCallbacks = /* @__PURE__ */ new Set();
|
|
1278
|
+
const restoreCallbacks = /* @__PURE__ */ new Set();
|
|
1279
|
+
const lostListener = (event) => {
|
|
1280
|
+
lost = true;
|
|
1281
|
+
event.preventDefault();
|
|
1282
|
+
for (const callback of lostCallbacks) {
|
|
1283
|
+
callback();
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
const restoreListener = () => {
|
|
1287
|
+
lost = false;
|
|
1288
|
+
for (const callback of restoreCallbacks) {
|
|
1289
|
+
callback();
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
canvas.addEventListener("webglcontextlost", lostListener);
|
|
1293
|
+
canvas.addEventListener("webglcontextrestored", restoreListener);
|
|
1294
|
+
return {
|
|
1295
|
+
gl,
|
|
1296
|
+
extensions,
|
|
1297
|
+
isLost: () => lost,
|
|
1298
|
+
onLost(callback) {
|
|
1299
|
+
lostCallbacks.add(callback);
|
|
1300
|
+
return () => lostCallbacks.delete(callback);
|
|
1301
|
+
},
|
|
1302
|
+
onRestored(callback) {
|
|
1303
|
+
restoreCallbacks.add(callback);
|
|
1304
|
+
return () => restoreCallbacks.delete(callback);
|
|
1305
|
+
},
|
|
1306
|
+
dispose() {
|
|
1307
|
+
canvas.removeEventListener("webglcontextlost", lostListener);
|
|
1308
|
+
canvas.removeEventListener("webglcontextrestored", restoreListener);
|
|
1309
|
+
lostCallbacks.clear();
|
|
1310
|
+
restoreCallbacks.clear();
|
|
1311
|
+
extensions.clear();
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// src/render/shaderProgram.ts
|
|
1317
|
+
function compileShader(gl, type, source) {
|
|
1318
|
+
const shader = gl.createShader(type);
|
|
1319
|
+
if (!shader) {
|
|
1320
|
+
throw new Error("Failed to allocate shader");
|
|
1321
|
+
}
|
|
1322
|
+
gl.shaderSource(shader, source);
|
|
1323
|
+
gl.compileShader(shader);
|
|
1324
|
+
const ok = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
|
1325
|
+
if (!ok) {
|
|
1326
|
+
const log = gl.getShaderInfoLog(shader) ?? "Unknown shader compile failure";
|
|
1327
|
+
gl.deleteShader(shader);
|
|
1328
|
+
throw new Error(log);
|
|
1329
|
+
}
|
|
1330
|
+
return shader;
|
|
1331
|
+
}
|
|
1332
|
+
function linkProgram(gl, vertexShader, fragmentShader, attributeLocations) {
|
|
1333
|
+
const program = gl.createProgram();
|
|
1334
|
+
if (!program) {
|
|
1335
|
+
throw new Error("Failed to allocate shader program");
|
|
1336
|
+
}
|
|
1337
|
+
gl.attachShader(program, vertexShader);
|
|
1338
|
+
gl.attachShader(program, fragmentShader);
|
|
1339
|
+
if (attributeLocations) {
|
|
1340
|
+
for (const [name, location] of Object.entries(attributeLocations)) {
|
|
1341
|
+
gl.bindAttribLocation(program, location, name);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
gl.linkProgram(program);
|
|
1345
|
+
const ok = gl.getProgramParameter(program, gl.LINK_STATUS);
|
|
1346
|
+
if (!ok) {
|
|
1347
|
+
const log = gl.getProgramInfoLog(program) ?? "Unknown shader link failure";
|
|
1348
|
+
gl.deleteProgram(program);
|
|
1349
|
+
throw new Error(log);
|
|
1350
|
+
}
|
|
1351
|
+
return program;
|
|
1352
|
+
}
|
|
1353
|
+
var ShaderProgram = class _ShaderProgram {
|
|
1354
|
+
constructor(gl, program) {
|
|
1355
|
+
this.uniformLocations = /* @__PURE__ */ new Map();
|
|
1356
|
+
this.attributeLocations = /* @__PURE__ */ new Map();
|
|
1357
|
+
this.gl = gl;
|
|
1358
|
+
this.program = program;
|
|
1359
|
+
}
|
|
1360
|
+
static create(gl, sources, attributeLocations) {
|
|
1361
|
+
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, sources.vertex);
|
|
1362
|
+
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, sources.fragment);
|
|
1363
|
+
try {
|
|
1364
|
+
const program = linkProgram(gl, vertexShader, fragmentShader, attributeLocations);
|
|
1365
|
+
return new _ShaderProgram(gl, program);
|
|
1366
|
+
} finally {
|
|
1367
|
+
gl.deleteShader(vertexShader);
|
|
1368
|
+
gl.deleteShader(fragmentShader);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
use() {
|
|
1372
|
+
this.gl.useProgram(this.program);
|
|
1373
|
+
}
|
|
1374
|
+
getUniformLocation(name) {
|
|
1375
|
+
if (!this.uniformLocations.has(name)) {
|
|
1376
|
+
const location = this.gl.getUniformLocation(this.program, name);
|
|
1377
|
+
this.uniformLocations.set(name, location);
|
|
1378
|
+
}
|
|
1379
|
+
return this.uniformLocations.get(name) ?? null;
|
|
1380
|
+
}
|
|
1381
|
+
getAttributeLocation(name) {
|
|
1382
|
+
if (!this.attributeLocations.has(name)) {
|
|
1383
|
+
const location = this.gl.getAttribLocation(this.program, name);
|
|
1384
|
+
this.attributeLocations.set(name, location);
|
|
1385
|
+
}
|
|
1386
|
+
return this.attributeLocations.get(name) ?? -1;
|
|
1387
|
+
}
|
|
1388
|
+
dispose() {
|
|
1389
|
+
this.gl.deleteProgram(this.program);
|
|
1390
|
+
this.uniformLocations.clear();
|
|
1391
|
+
this.attributeLocations.clear();
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
function createProgramFromSources(gl, sources, attributeLocations) {
|
|
1395
|
+
return ShaderProgram.create(gl, sources, attributeLocations);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/render/resources.ts
|
|
1399
|
+
var VertexBuffer = class {
|
|
1400
|
+
constructor(gl, usage = gl.STATIC_DRAW, target) {
|
|
1401
|
+
this.gl = gl;
|
|
1402
|
+
this.target = target ?? gl.ARRAY_BUFFER;
|
|
1403
|
+
const buffer = gl.createBuffer();
|
|
1404
|
+
if (!buffer) {
|
|
1405
|
+
throw new Error("Failed to allocate buffer");
|
|
1406
|
+
}
|
|
1407
|
+
this.buffer = buffer;
|
|
1408
|
+
gl.bindBuffer(this.target, this.buffer);
|
|
1409
|
+
gl.bufferData(this.target, 0, usage);
|
|
1410
|
+
}
|
|
1411
|
+
bind() {
|
|
1412
|
+
this.gl.bindBuffer(this.target, this.buffer);
|
|
1413
|
+
}
|
|
1414
|
+
upload(data, usage = this.gl.STATIC_DRAW) {
|
|
1415
|
+
this.bind();
|
|
1416
|
+
this.gl.bufferData(this.target, data, usage);
|
|
1417
|
+
}
|
|
1418
|
+
update(data, offset = 0) {
|
|
1419
|
+
this.bind();
|
|
1420
|
+
this.gl.bufferSubData(this.target, offset, data);
|
|
1421
|
+
}
|
|
1422
|
+
dispose() {
|
|
1423
|
+
this.gl.deleteBuffer(this.buffer);
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
var IndexBuffer = class extends VertexBuffer {
|
|
1427
|
+
constructor(gl, usage = gl.STATIC_DRAW) {
|
|
1428
|
+
super(gl, usage, gl.ELEMENT_ARRAY_BUFFER);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
var VertexArray = class {
|
|
1432
|
+
constructor(gl) {
|
|
1433
|
+
this.gl = gl;
|
|
1434
|
+
const vao = gl.createVertexArray();
|
|
1435
|
+
if (!vao) {
|
|
1436
|
+
throw new Error("Failed to allocate vertex array object");
|
|
1437
|
+
}
|
|
1438
|
+
this.vao = vao;
|
|
1439
|
+
}
|
|
1440
|
+
bind() {
|
|
1441
|
+
this.gl.bindVertexArray(this.vao);
|
|
1442
|
+
}
|
|
1443
|
+
configureAttributes(layouts, buffer) {
|
|
1444
|
+
this.bind();
|
|
1445
|
+
if (buffer) {
|
|
1446
|
+
buffer.bind();
|
|
1447
|
+
}
|
|
1448
|
+
for (const layout of layouts) {
|
|
1449
|
+
this.gl.enableVertexAttribArray(layout.index);
|
|
1450
|
+
this.gl.vertexAttribPointer(
|
|
1451
|
+
layout.index,
|
|
1452
|
+
layout.size,
|
|
1453
|
+
layout.type,
|
|
1454
|
+
layout.normalized ?? false,
|
|
1455
|
+
layout.stride ?? 0,
|
|
1456
|
+
layout.offset ?? 0
|
|
1457
|
+
);
|
|
1458
|
+
if (layout.divisor !== void 0) {
|
|
1459
|
+
this.gl.vertexAttribDivisor(layout.index, layout.divisor);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
dispose() {
|
|
1464
|
+
this.gl.deleteVertexArray(this.vao);
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
var Texture2D = class {
|
|
1468
|
+
constructor(gl, target = gl.TEXTURE_2D) {
|
|
1469
|
+
this.gl = gl;
|
|
1470
|
+
this.target = target;
|
|
1471
|
+
const texture = gl.createTexture();
|
|
1472
|
+
if (!texture) {
|
|
1473
|
+
throw new Error("Failed to allocate texture");
|
|
1474
|
+
}
|
|
1475
|
+
this.texture = texture;
|
|
1476
|
+
}
|
|
1477
|
+
bind(unit = 0) {
|
|
1478
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
|
|
1479
|
+
this.gl.bindTexture(this.target, this.texture);
|
|
1480
|
+
}
|
|
1481
|
+
setParameters(params) {
|
|
1482
|
+
this.bind();
|
|
1483
|
+
if (params.wrapS !== void 0) {
|
|
1484
|
+
this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_S, params.wrapS);
|
|
1485
|
+
}
|
|
1486
|
+
if (params.wrapT !== void 0) {
|
|
1487
|
+
this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_T, params.wrapT);
|
|
1488
|
+
}
|
|
1489
|
+
if (params.minFilter !== void 0) {
|
|
1490
|
+
this.gl.texParameteri(this.target, this.gl.TEXTURE_MIN_FILTER, params.minFilter);
|
|
1491
|
+
}
|
|
1492
|
+
if (params.magFilter !== void 0) {
|
|
1493
|
+
this.gl.texParameteri(this.target, this.gl.TEXTURE_MAG_FILTER, params.magFilter);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
uploadImage(level, internalFormat, width, height, border, format, type, data) {
|
|
1497
|
+
this.bind();
|
|
1498
|
+
this.gl.texImage2D(this.target, level, internalFormat, width, height, border, format, type, data);
|
|
1499
|
+
}
|
|
1500
|
+
dispose() {
|
|
1501
|
+
this.gl.deleteTexture(this.texture);
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
var Framebuffer = class {
|
|
1505
|
+
constructor(gl) {
|
|
1506
|
+
this.gl = gl;
|
|
1507
|
+
const framebuffer = gl.createFramebuffer();
|
|
1508
|
+
if (!framebuffer) {
|
|
1509
|
+
throw new Error("Failed to allocate framebuffer");
|
|
1510
|
+
}
|
|
1511
|
+
this.framebuffer = framebuffer;
|
|
1512
|
+
}
|
|
1513
|
+
bind(target = this.gl.FRAMEBUFFER) {
|
|
1514
|
+
this.gl.bindFramebuffer(target, this.framebuffer);
|
|
1515
|
+
}
|
|
1516
|
+
attachTexture2D(attachment, texture, level = 0) {
|
|
1517
|
+
this.bind();
|
|
1518
|
+
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, attachment, texture.target, texture.texture, level);
|
|
1519
|
+
}
|
|
1520
|
+
dispose() {
|
|
1521
|
+
this.gl.deleteFramebuffer(this.framebuffer);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
// src/render/bsp.ts
|
|
1526
|
+
var FLOAT_BYTES = 4;
|
|
1527
|
+
var STRIDE = 7 * FLOAT_BYTES;
|
|
1528
|
+
var BSP_VERTEX_LAYOUT = [
|
|
1529
|
+
// Position
|
|
1530
|
+
{ index: 0, size: 3, type: 5126, stride: STRIDE, offset: 0 },
|
|
1531
|
+
// Diffuse UV
|
|
1532
|
+
{ index: 1, size: 2, type: 5126, stride: STRIDE, offset: 3 * FLOAT_BYTES },
|
|
1533
|
+
// Lightmap UV
|
|
1534
|
+
{ index: 2, size: 2, type: 5126, stride: STRIDE, offset: 5 * FLOAT_BYTES }
|
|
1535
|
+
];
|
|
1536
|
+
function createAtlasBuilder(size, padding) {
|
|
1537
|
+
const channelCount = 4;
|
|
1538
|
+
return {
|
|
1539
|
+
width: size,
|
|
1540
|
+
height: size,
|
|
1541
|
+
padding,
|
|
1542
|
+
data: new Uint8Array(size * size * channelCount),
|
|
1543
|
+
cursorX: 0,
|
|
1544
|
+
cursorY: 0,
|
|
1545
|
+
rowHeight: 0
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function detectChannels(lightmap) {
|
|
1549
|
+
const pixels = lightmap.width * lightmap.height;
|
|
1550
|
+
if (pixels === 0) {
|
|
1551
|
+
throw new Error("Invalid lightmap with zero area");
|
|
1552
|
+
}
|
|
1553
|
+
const channels = lightmap.samples.byteLength / pixels;
|
|
1554
|
+
if (!Number.isInteger(channels) || channels < 3 || channels > 4) {
|
|
1555
|
+
throw new Error("Unsupported lightmap channel count");
|
|
1556
|
+
}
|
|
1557
|
+
return channels;
|
|
1558
|
+
}
|
|
1559
|
+
function writeLightmapIntoAtlas(atlas, placement, lightmap) {
|
|
1560
|
+
const channels = detectChannels(lightmap);
|
|
1561
|
+
const stride = atlas.width * 4;
|
|
1562
|
+
const startX = placement.x + atlas.padding;
|
|
1563
|
+
const startY = placement.y + atlas.padding;
|
|
1564
|
+
let srcIndex = 0;
|
|
1565
|
+
for (let y = 0; y < lightmap.height; y++) {
|
|
1566
|
+
const destRow = (startY + y) * stride + startX * 4;
|
|
1567
|
+
for (let x = 0; x < lightmap.width; x++) {
|
|
1568
|
+
const dest = destRow + x * 4;
|
|
1569
|
+
atlas.data[dest] = lightmap.samples[srcIndex];
|
|
1570
|
+
atlas.data[dest + 1] = lightmap.samples[srcIndex + 1];
|
|
1571
|
+
atlas.data[dest + 2] = lightmap.samples[srcIndex + 2];
|
|
1572
|
+
atlas.data[dest + 3] = channels === 4 ? lightmap.samples[srcIndex + 3] : 255;
|
|
1573
|
+
srcIndex += channels;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function placeLightmap(builders, lightmap, options) {
|
|
1578
|
+
const paddedWidth = lightmap.width + options.lightmapPadding * 2;
|
|
1579
|
+
const paddedHeight = lightmap.height + options.lightmapPadding * 2;
|
|
1580
|
+
if (paddedWidth > options.atlasSize || paddedHeight > options.atlasSize) {
|
|
1581
|
+
throw new Error("Lightmap too large for atlas");
|
|
1582
|
+
}
|
|
1583
|
+
for (const atlas2 of builders) {
|
|
1584
|
+
if (atlas2.cursorX + paddedWidth > atlas2.width) {
|
|
1585
|
+
atlas2.cursorX = 0;
|
|
1586
|
+
atlas2.cursorY += atlas2.rowHeight + options.lightmapPadding;
|
|
1587
|
+
atlas2.rowHeight = 0;
|
|
1588
|
+
}
|
|
1589
|
+
if (atlas2.cursorY + paddedHeight > atlas2.height) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const placement2 = {
|
|
1593
|
+
atlasIndex: builders.indexOf(atlas2),
|
|
1594
|
+
x: atlas2.cursorX,
|
|
1595
|
+
y: atlas2.cursorY,
|
|
1596
|
+
width: lightmap.width,
|
|
1597
|
+
height: lightmap.height
|
|
1598
|
+
};
|
|
1599
|
+
atlas2.cursorX += paddedWidth + options.lightmapPadding;
|
|
1600
|
+
atlas2.rowHeight = Math.max(atlas2.rowHeight, paddedHeight);
|
|
1601
|
+
return { placement: placement2, atlas: atlas2 };
|
|
1602
|
+
}
|
|
1603
|
+
const atlas = createAtlasBuilder(options.atlasSize, options.lightmapPadding);
|
|
1604
|
+
builders.push(atlas);
|
|
1605
|
+
const placement = { atlasIndex: builders.length - 1, x: 0, y: 0, width: lightmap.width, height: lightmap.height };
|
|
1606
|
+
atlas.cursorX = paddedWidth + options.lightmapPadding;
|
|
1607
|
+
atlas.rowHeight = paddedHeight;
|
|
1608
|
+
return { placement, atlas };
|
|
1609
|
+
}
|
|
1610
|
+
function ensureFloat32(array) {
|
|
1611
|
+
return array instanceof Float32Array ? array : new Float32Array(array);
|
|
1612
|
+
}
|
|
1613
|
+
function ensureIndexArray(indices, vertexCount) {
|
|
1614
|
+
if (!indices) {
|
|
1615
|
+
const generated = new Uint16Array(vertexCount);
|
|
1616
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
1617
|
+
generated[i] = i;
|
|
1618
|
+
}
|
|
1619
|
+
return generated;
|
|
1620
|
+
}
|
|
1621
|
+
const converted = indices instanceof Uint16Array ? indices : new Uint16Array(indices);
|
|
1622
|
+
return converted;
|
|
1623
|
+
}
|
|
1624
|
+
function remapLightmapCoords(coords, placement) {
|
|
1625
|
+
const remapped = new Float32Array(coords.length);
|
|
1626
|
+
for (let i = 0; i < coords.length; i += 2) {
|
|
1627
|
+
remapped[i] = placement.offset[0] + coords[i] * placement.scale[0];
|
|
1628
|
+
remapped[i + 1] = placement.offset[1] + coords[i + 1] * placement.scale[1];
|
|
1629
|
+
}
|
|
1630
|
+
return remapped;
|
|
1631
|
+
}
|
|
1632
|
+
function buildVertexData(surface, placement) {
|
|
1633
|
+
const vertices = ensureFloat32(surface.vertices);
|
|
1634
|
+
const texCoords = ensureFloat32(surface.textureCoords);
|
|
1635
|
+
const lightmapCoords = placement ? remapLightmapCoords(ensureFloat32(surface.lightmapCoords ?? surface.textureCoords), placement) : ensureFloat32(surface.lightmapCoords ?? new Float32Array(texCoords.length));
|
|
1636
|
+
const vertexCount = vertices.length / 3;
|
|
1637
|
+
if (texCoords.length / 2 !== vertexCount) {
|
|
1638
|
+
throw new Error("Texture coordinates count mismatch");
|
|
1639
|
+
}
|
|
1640
|
+
if (lightmapCoords.length / 2 !== vertexCount) {
|
|
1641
|
+
throw new Error("Lightmap coordinates count mismatch");
|
|
1642
|
+
}
|
|
1643
|
+
const interleaved = new Float32Array(vertexCount * 7);
|
|
1644
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
1645
|
+
const v = i * 3;
|
|
1646
|
+
const t = i * 2;
|
|
1647
|
+
const o = i * 7;
|
|
1648
|
+
interleaved[o] = vertices[v];
|
|
1649
|
+
interleaved[o + 1] = vertices[v + 1];
|
|
1650
|
+
interleaved[o + 2] = vertices[v + 2];
|
|
1651
|
+
interleaved[o + 3] = texCoords[t];
|
|
1652
|
+
interleaved[o + 4] = texCoords[t + 1];
|
|
1653
|
+
interleaved[o + 5] = lightmapCoords[t];
|
|
1654
|
+
interleaved[o + 6] = lightmapCoords[t + 1];
|
|
1655
|
+
}
|
|
1656
|
+
return interleaved;
|
|
1657
|
+
}
|
|
1658
|
+
function buildBspGeometry(gl, surfaces, options = {}) {
|
|
1659
|
+
const resolved = {
|
|
1660
|
+
atlasSize: options.atlasSize ?? 1024,
|
|
1661
|
+
lightmapPadding: options.lightmapPadding ?? 1
|
|
1662
|
+
};
|
|
1663
|
+
const atlasBuilders = [];
|
|
1664
|
+
const placements = /* @__PURE__ */ new Map();
|
|
1665
|
+
surfaces.forEach((surface, index) => {
|
|
1666
|
+
if (!surface.lightmap) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const { placement, atlas } = placeLightmap(atlasBuilders, surface.lightmap, resolved);
|
|
1670
|
+
writeLightmapIntoAtlas(atlas, placement, surface.lightmap);
|
|
1671
|
+
placements.set(index, {
|
|
1672
|
+
atlasIndex: placement.atlasIndex,
|
|
1673
|
+
offset: [
|
|
1674
|
+
(placement.x + resolved.lightmapPadding) / resolved.atlasSize,
|
|
1675
|
+
(placement.y + resolved.lightmapPadding) / resolved.atlasSize
|
|
1676
|
+
],
|
|
1677
|
+
scale: [placement.width / resolved.atlasSize, placement.height / resolved.atlasSize]
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1680
|
+
const lightmaps = atlasBuilders.map((builder) => {
|
|
1681
|
+
const texture = new Texture2D(gl);
|
|
1682
|
+
texture.setParameters({
|
|
1683
|
+
wrapS: gl.CLAMP_TO_EDGE,
|
|
1684
|
+
wrapT: gl.CLAMP_TO_EDGE,
|
|
1685
|
+
minFilter: gl.LINEAR,
|
|
1686
|
+
magFilter: gl.LINEAR
|
|
1687
|
+
});
|
|
1688
|
+
texture.uploadImage(0, gl.RGBA, builder.width, builder.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, builder.data);
|
|
1689
|
+
return { texture, width: builder.width, height: builder.height, pixels: builder.data };
|
|
1690
|
+
});
|
|
1691
|
+
const results = surfaces.map((surface, index) => {
|
|
1692
|
+
const placement = placements.get(index);
|
|
1693
|
+
const vertexData = buildVertexData(surface, placement);
|
|
1694
|
+
const indexData = ensureIndexArray(surface.indices, vertexData.length / 7);
|
|
1695
|
+
const vertexBuffer = new VertexBuffer(gl, gl.STATIC_DRAW, gl.ARRAY_BUFFER);
|
|
1696
|
+
vertexBuffer.upload(vertexData);
|
|
1697
|
+
const indexBuffer = new IndexBuffer(gl, gl.STATIC_DRAW);
|
|
1698
|
+
indexBuffer.upload(indexData);
|
|
1699
|
+
const vao = new VertexArray(gl);
|
|
1700
|
+
vao.configureAttributes(BSP_VERTEX_LAYOUT, vertexBuffer);
|
|
1701
|
+
return {
|
|
1702
|
+
vao,
|
|
1703
|
+
vertexBuffer,
|
|
1704
|
+
indexBuffer,
|
|
1705
|
+
indexCount: indexData.length,
|
|
1706
|
+
texture: surface.texture,
|
|
1707
|
+
surfaceFlags: surface.surfaceFlags ?? SURF_NONE,
|
|
1708
|
+
lightmap: placement,
|
|
1709
|
+
vertexData,
|
|
1710
|
+
indexData
|
|
1711
|
+
};
|
|
1712
|
+
});
|
|
1713
|
+
return { surfaces: results, lightmaps };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/index.ts
|
|
1717
|
+
function createEngine(imports) {
|
|
1718
|
+
return {
|
|
1719
|
+
init() {
|
|
1720
|
+
void imports.trace({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 });
|
|
1721
|
+
},
|
|
1722
|
+
shutdown() {
|
|
1723
|
+
},
|
|
1724
|
+
createMainLoop(callbacks, options) {
|
|
1725
|
+
return new FixedTimestepLoop(callbacks, options);
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
export {
|
|
1730
|
+
BSP_VERTEX_LAYOUT,
|
|
1731
|
+
ConfigStringRegistry,
|
|
1732
|
+
Cvar,
|
|
1733
|
+
CvarRegistry,
|
|
1734
|
+
EngineHost,
|
|
1735
|
+
EngineRuntime,
|
|
1736
|
+
FixedTimestepLoop,
|
|
1737
|
+
Framebuffer,
|
|
1738
|
+
IndexBuffer,
|
|
1739
|
+
LruCache,
|
|
1740
|
+
Md2Loader,
|
|
1741
|
+
Md2ParseError,
|
|
1742
|
+
PakArchive,
|
|
1743
|
+
PakIngestionError,
|
|
1744
|
+
PakParseError,
|
|
1745
|
+
ShaderProgram,
|
|
1746
|
+
Texture2D,
|
|
1747
|
+
VertexArray,
|
|
1748
|
+
VertexBuffer,
|
|
1749
|
+
VirtualFileSystem,
|
|
1750
|
+
buildBspGeometry,
|
|
1751
|
+
calculatePakChecksum,
|
|
1752
|
+
createEngine,
|
|
1753
|
+
createEngineRuntime,
|
|
1754
|
+
createProgramFromSources,
|
|
1755
|
+
createWebGLContext,
|
|
1756
|
+
filesToPakSources,
|
|
1757
|
+
groupMd2Animations,
|
|
1758
|
+
ingestPakFiles,
|
|
1759
|
+
ingestPaks,
|
|
1760
|
+
parseMd2,
|
|
1761
|
+
wireDropTarget,
|
|
1762
|
+
wireFileInput
|
|
1763
|
+
};
|
|
1764
|
+
//# sourceMappingURL=index.js.map
|