@quake2ts/test-utils 0.0.838 → 0.0.839

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quake2ts/test-utils",
3
- "version": "0.0.838",
3
+ "version": "0.0.839",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -55,10 +55,10 @@
55
55
  "serve-handler": "^6.1.6",
56
56
  "vitest": "^1.6.0",
57
57
  "webgpu": "^0.3.8",
58
- "@quake2ts/engine": "^0.0.838",
59
- "@quake2ts/game": "0.0.838",
60
- "@quake2ts/server": "0.0.838",
61
- "@quake2ts/shared": "0.0.838"
58
+ "@quake2ts/engine": "^0.0.839",
59
+ "@quake2ts/game": "0.0.839",
60
+ "@quake2ts/shared": "0.0.839",
61
+ "@quake2ts/server": "0.0.839"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "@quake2ts/engine": {
@@ -114,10 +114,10 @@
114
114
  "typescript": "^5.9.3",
115
115
  "vitest": "^4.0.16",
116
116
  "webgpu": "^0.3.8",
117
- "@quake2ts/engine": "^0.0.838",
118
- "@quake2ts/game": "0.0.838",
119
- "@quake2ts/shared": "0.0.838",
120
- "@quake2ts/server": "0.0.838"
117
+ "@quake2ts/engine": "^0.0.839",
118
+ "@quake2ts/server": "0.0.839",
119
+ "@quake2ts/shared": "0.0.839",
120
+ "@quake2ts/game": "0.0.839"
121
121
  },
122
122
  "dependencies": {
123
123
  "upng-js": "^2.1.0"
@@ -0,0 +1,278 @@
1
+ import { BspBrush, BspBrushSide, BspFace, BspLeaf, BspLump, BspModel, BspNode, BspPlane, BspTexInfo } from '@quake2ts/engine';
2
+
3
+ type Vec3 = [number, number, number];
4
+
5
+ const HEADER_LUMPS = 19;
6
+ const HEADER_SIZE = 4 + 4 + HEADER_LUMPS * 8;
7
+
8
+ function allocBuffer(size: number): DataView {
9
+ return new DataView(new ArrayBuffer(size));
10
+ }
11
+
12
+ function writeVec3(view: DataView, offset: number, vec: Vec3): void {
13
+ view.setFloat32(offset, vec[0], true);
14
+ view.setFloat32(offset + 4, vec[1], true);
15
+ view.setFloat32(offset + 8, vec[2], true);
16
+ }
17
+
18
+ function encodePlanes(planes: BspPlane[]): Uint8Array {
19
+ const view = allocBuffer(planes.length * 20);
20
+ planes.forEach((plane, index) => {
21
+ const base = index * 20;
22
+ writeVec3(view, base, plane.normal);
23
+ view.setFloat32(base + 12, plane.dist, true);
24
+ view.setInt32(base + 16, plane.type, true);
25
+ });
26
+ return new Uint8Array(view.buffer);
27
+ }
28
+
29
+ function encodeVertices(vertices: Vec3[]): Uint8Array {
30
+ const view = allocBuffer(vertices.length * 12);
31
+ vertices.forEach((vertex, index) => writeVec3(view, index * 12, vertex));
32
+ return new Uint8Array(view.buffer);
33
+ }
34
+
35
+ function encodeNodes(nodes: BspNode[]): Uint8Array {
36
+ const view = allocBuffer(nodes.length * 28);
37
+ nodes.forEach((node, index) => {
38
+ const base = index * 28;
39
+ view.setInt32(base, node.planeIndex, true);
40
+ view.setInt32(base + 4, node.children[0], true);
41
+ view.setInt32(base + 8, node.children[1], true);
42
+ view.setInt16(base + 12, node.mins[0], true);
43
+ view.setInt16(base + 14, node.mins[1], true);
44
+ view.setInt16(base + 16, node.mins[2], true);
45
+ view.setInt16(base + 18, node.maxs[0], true);
46
+ view.setInt16(base + 20, node.maxs[1], true);
47
+ view.setInt16(base + 22, node.maxs[2], true);
48
+ view.setUint16(base + 24, node.firstFace, true);
49
+ view.setUint16(base + 26, node.numFaces, true);
50
+ });
51
+ return new Uint8Array(view.buffer);
52
+ }
53
+
54
+ function encodeTexInfo(texInfos: BspTexInfo[]): Uint8Array {
55
+ const view = allocBuffer(texInfos.length * 76);
56
+ texInfos.forEach((tex, index) => {
57
+ const base = index * 76;
58
+ writeVec3(view, base, tex.s);
59
+ view.setFloat32(base + 12, tex.sOffset, true);
60
+ writeVec3(view, base + 16, tex.t);
61
+ view.setFloat32(base + 28, tex.tOffset, true);
62
+ view.setInt32(base + 32, tex.flags, true);
63
+ view.setInt32(base + 36, tex.value, true);
64
+ const textureBytes = new TextEncoder().encode(tex.texture);
65
+ new Uint8Array(view.buffer).set(textureBytes.slice(0, 32), base + 40);
66
+ view.setInt32(base + 72, tex.nextTexInfo, true);
67
+ });
68
+ return new Uint8Array(view.buffer);
69
+ }
70
+
71
+ function encodeFaces(faces: BspFace[]): Uint8Array {
72
+ const view = allocBuffer(faces.length * 20);
73
+ faces.forEach((face, index) => {
74
+ const base = index * 20;
75
+ view.setUint16(base, face.planeIndex, true);
76
+ view.setInt16(base + 2, face.side, true);
77
+ view.setInt32(base + 4, face.firstEdge, true);
78
+ view.setInt16(base + 8, face.numEdges, true);
79
+ view.setInt16(base + 10, face.texInfo, true);
80
+ face.styles.forEach((style, sIndex) => view.setUint8(base + 12 + sIndex, style));
81
+ view.setInt32(base + 16, face.lightOffset, true);
82
+ });
83
+ return new Uint8Array(view.buffer);
84
+ }
85
+
86
+ function encodeLeafs(leafs: BspLeaf[]): Uint8Array {
87
+ const view = allocBuffer(leafs.length * 28);
88
+ leafs.forEach((leaf, index) => {
89
+ const base = index * 28;
90
+ view.setInt32(base, leaf.contents, true);
91
+ view.setInt16(base + 4, leaf.cluster, true);
92
+ view.setInt16(base + 6, leaf.area, true);
93
+ view.setInt16(base + 8, leaf.mins[0], true);
94
+ view.setInt16(base + 10, leaf.mins[1], true);
95
+ view.setInt16(base + 12, leaf.mins[2], true);
96
+ view.setInt16(base + 14, leaf.maxs[0], true);
97
+ view.setInt16(base + 16, leaf.maxs[1], true);
98
+ view.setInt16(base + 18, leaf.maxs[2], true);
99
+ view.setUint16(base + 20, leaf.firstLeafFace, true);
100
+ view.setUint16(base + 22, leaf.numLeafFaces, true);
101
+ view.setUint16(base + 24, leaf.firstLeafBrush, true);
102
+ view.setUint16(base + 26, leaf.numLeafBrushes, true);
103
+ });
104
+ return new Uint8Array(view.buffer);
105
+ }
106
+
107
+ function encodeEdges(edges: Array<[number, number]>): Uint8Array {
108
+ const view = allocBuffer(edges.length * 4);
109
+ edges.forEach((edge, index) => {
110
+ const base = index * 4;
111
+ view.setUint16(base, edge[0], true);
112
+ view.setUint16(base + 2, edge[1], true);
113
+ });
114
+ return new Uint8Array(view.buffer);
115
+ }
116
+
117
+ function encodeModels(models: BspModel[]): Uint8Array {
118
+ const entrySize = 48;
119
+ const view = allocBuffer(models.length * entrySize);
120
+ models.forEach((model, index) => {
121
+ const base = index * entrySize;
122
+ writeVec3(view, base, model.mins);
123
+ writeVec3(view, base + 12, model.maxs);
124
+ writeVec3(view, base + 24, model.origin);
125
+ view.setInt32(base + 36, model.headNode, true);
126
+ view.setInt32(base + 40, model.firstFace, true);
127
+ view.setInt32(base + 44, model.numFaces, true);
128
+ });
129
+ return new Uint8Array(view.buffer);
130
+ }
131
+
132
+ function encodeBrushes(brushes: BspBrush[]): Uint8Array {
133
+ const view = allocBuffer(brushes.length * 12);
134
+ brushes.forEach((brush, index) => {
135
+ const base = index * 12;
136
+ view.setInt32(base, brush.firstSide, true);
137
+ view.setInt32(base + 4, brush.numSides, true);
138
+ view.setInt32(base + 8, brush.contents, true);
139
+ });
140
+ return new Uint8Array(view.buffer);
141
+ }
142
+
143
+ function encodeBrushSides(sides: BspBrushSide[]): Uint8Array {
144
+ const view = allocBuffer(sides.length * 4);
145
+ sides.forEach((side, index) => {
146
+ const base = index * 4;
147
+ view.setUint16(base, side.planeIndex, true);
148
+ view.setInt16(base + 2, side.texInfo, true);
149
+ });
150
+ return new Uint8Array(view.buffer);
151
+ }
152
+
153
+ function encodeAreas(areas: { numAreaPortals: number; firstAreaPortal: number }[]): Uint8Array {
154
+ const view = allocBuffer(areas.length * 8);
155
+ areas.forEach((area, index) => {
156
+ const base = index * 8;
157
+ view.setInt32(base, area.numAreaPortals, true);
158
+ view.setInt32(base + 4, area.firstAreaPortal, true);
159
+ });
160
+ return new Uint8Array(view.buffer);
161
+ }
162
+
163
+ function encodeAreaPortals(portals: { portalNumber: number; otherArea: number }[]): Uint8Array {
164
+ const view = allocBuffer(portals.length * 8);
165
+ portals.forEach((portal, index) => {
166
+ const base = index * 8;
167
+ view.setInt32(base, portal.portalNumber, true);
168
+ view.setInt32(base + 4, portal.otherArea, true);
169
+ });
170
+ return new Uint8Array(view.buffer);
171
+ }
172
+
173
+ function encodeVisibility(numClusters: number, pvsRows: Uint8Array[], phsRows?: Uint8Array[]): Uint8Array {
174
+ const headerBytes = 4 + numClusters * 8;
175
+ const header = allocBuffer(headerBytes);
176
+ header.setInt32(0, numClusters, true);
177
+ let cursor = headerBytes;
178
+ const payloads: Uint8Array[] = [];
179
+
180
+ for (let i = 0; i < numClusters; i += 1) {
181
+ const pvs = pvsRows[i];
182
+ const phs = phsRows?.[i] ?? pvs;
183
+ const pvsOffset = cursor - 0;
184
+ const phsOffset = cursor + pvs.byteLength - 0;
185
+ payloads.push(pvs, phs);
186
+ header.setInt32(4 + i * 8, pvsOffset, true);
187
+ header.setInt32(8 + i * 8, phsOffset, true);
188
+ cursor += pvs.byteLength + phs.byteLength;
189
+ }
190
+
191
+ const result = new Uint8Array(cursor);
192
+ result.set(new Uint8Array(header.buffer), 0);
193
+ let payloadCursor = headerBytes;
194
+ for (const payload of payloads) {
195
+ result.set(payload, payloadCursor);
196
+ payloadCursor += payload.byteLength;
197
+ }
198
+ return result;
199
+ }
200
+
201
+ export interface BspFixtureOptions {
202
+ readonly entities?: string;
203
+ readonly planes?: BspPlane[];
204
+ readonly vertices?: Vec3[];
205
+ readonly nodes?: BspNode[];
206
+ readonly texInfo?: BspTexInfo[];
207
+ readonly faces?: BspFace[];
208
+ readonly lighting?: Uint8Array;
209
+ readonly leafs?: BspLeaf[];
210
+ readonly leafFaces?: Uint16Array;
211
+ readonly leafBrushes?: Uint16Array;
212
+ readonly edges?: Array<[number, number]>;
213
+ readonly surfEdges?: Int32Array;
214
+ readonly models?: BspModel[];
215
+ readonly brushes?: BspBrush[];
216
+ readonly brushSides?: BspBrushSide[];
217
+ readonly visibility?: Uint8Array;
218
+ readonly areas?: Uint8Array;
219
+ readonly areaPortals?: Uint8Array;
220
+ }
221
+
222
+ export function buildTestBsp(options: BspFixtureOptions): ArrayBuffer {
223
+ const lumps: Record<number, Uint8Array> = {};
224
+ lumps[BspLump.Entities] = new TextEncoder().encode(options.entities ?? '{"classname" "worldspawn"}\n');
225
+ lumps[BspLump.Planes] = options.planes ? encodePlanes(options.planes) : new Uint8Array();
226
+ lumps[BspLump.Vertices] = options.vertices ? encodeVertices(options.vertices) : new Uint8Array();
227
+ lumps[BspLump.Visibility] = options.visibility ?? new Uint8Array();
228
+ lumps[BspLump.Nodes] = options.nodes ? encodeNodes(options.nodes) : new Uint8Array();
229
+ lumps[BspLump.TexInfo] = options.texInfo ? encodeTexInfo(options.texInfo) : new Uint8Array();
230
+ lumps[BspLump.Faces] = options.faces ? encodeFaces(options.faces) : new Uint8Array();
231
+ lumps[BspLump.Lighting] = options.lighting ?? new Uint8Array();
232
+ lumps[BspLump.Leafs] = options.leafs ? encodeLeafs(options.leafs) : new Uint8Array();
233
+ lumps[BspLump.LeafFaces] = options.leafFaces ? new Uint8Array(options.leafFaces.buffer) : new Uint8Array();
234
+ lumps[BspLump.LeafBrushes] = options.leafBrushes ? new Uint8Array(options.leafBrushes.buffer) : new Uint8Array();
235
+ lumps[BspLump.Edges] = options.edges ? encodeEdges(options.edges) : new Uint8Array();
236
+ lumps[BspLump.SurfEdges] = options.surfEdges ? new Uint8Array(options.surfEdges.buffer) : new Uint8Array();
237
+ lumps[BspLump.Models] = options.models ? encodeModels(options.models) : new Uint8Array();
238
+ lumps[BspLump.Brushes] = options.brushes ? encodeBrushes(options.brushes) : new Uint8Array();
239
+ lumps[BspLump.BrushSides] = options.brushSides ? encodeBrushSides(options.brushSides) : new Uint8Array();
240
+ lumps[BspLump.Areas] = options.areas ?? new Uint8Array();
241
+ lumps[BspLump.AreaPortals] = options.areaPortals ?? new Uint8Array();
242
+
243
+ let cursor = HEADER_SIZE;
244
+ const ordered: { info: { offset: number; length: number }; data: Uint8Array }[] = [];
245
+ for (let i = 0; i < HEADER_LUMPS; i += 1) {
246
+ const data = lumps[i] ?? new Uint8Array();
247
+ const info = { offset: cursor, length: data.byteLength };
248
+ ordered.push({ info, data });
249
+ cursor += data.byteLength;
250
+ }
251
+
252
+ const buffer = new ArrayBuffer(cursor);
253
+ const header = new DataView(buffer);
254
+ header.setUint8(0, 0x49); // I
255
+ header.setUint8(1, 0x42); // B
256
+ header.setUint8(2, 0x53); // S
257
+ header.setUint8(3, 0x50); // P
258
+ header.setInt32(4, 38, true);
259
+
260
+ ordered.forEach((entry, index) => {
261
+ header.setInt32(8 + index * 8, entry.info.offset, true);
262
+ header.setInt32(12 + index * 8, entry.info.length, true);
263
+ });
264
+
265
+ const body = new Uint8Array(buffer);
266
+ ordered.forEach((entry) => body.set(entry.data, entry.info.offset));
267
+ return buffer;
268
+ }
269
+
270
+ export function runLengthVisRow(values: number[]): Uint8Array {
271
+ return new Uint8Array(values);
272
+ }
273
+
274
+ export function encodedVisForClusters(numClusters: number, rows: number[][]): Uint8Array {
275
+ const rowBytes = Math.ceil(numClusters / 8);
276
+ const encodedRows = rows.map((row) => new Uint8Array(row));
277
+ return encodeVisibility(numClusters, encodedRows);
278
+ }
@@ -0,0 +1,156 @@
1
+ import { Vec3 } from '@quake2ts/shared/math/vec3';
2
+
3
+ export interface Md2FrameVertexInput {
4
+ readonly position: Vec3;
5
+ readonly normalIndex: number;
6
+ }
7
+
8
+ export interface Md2FrameInput {
9
+ readonly name: string;
10
+ readonly vertices: readonly Md2FrameVertexInput[];
11
+ readonly scale?: Vec3;
12
+ readonly translate?: Vec3;
13
+ }
14
+
15
+ export interface Md2GlCommandVertexInput {
16
+ readonly s: number;
17
+ readonly t: number;
18
+ readonly vertexIndex: number;
19
+ }
20
+
21
+ export interface Md2GlCommandInput {
22
+ readonly mode: 'strip' | 'fan';
23
+ readonly vertices: readonly Md2GlCommandVertexInput[];
24
+ }
25
+
26
+ export interface Md2BuilderOptions {
27
+ readonly skins?: readonly string[];
28
+ readonly texCoords: readonly { s: number; t: number }[];
29
+ readonly triangles: readonly { vertexIndices: [number, number, number]; texCoordIndices: [number, number, number] }[];
30
+ readonly frames: readonly Md2FrameInput[];
31
+ readonly glCommands?: readonly Md2GlCommandInput[];
32
+ readonly skinWidth?: number;
33
+ readonly skinHeight?: number;
34
+ }
35
+
36
+ function allocBuffer(size: number): DataView {
37
+ return new DataView(new ArrayBuffer(size));
38
+ }
39
+
40
+ function writeCString(view: DataView, offset: number, text: string, max: number): void {
41
+ const bytes = new TextEncoder().encode(text);
42
+ const length = Math.min(bytes.length, max - 1);
43
+ new Uint8Array(view.buffer, view.byteOffset + offset, length).set(bytes.slice(0, length));
44
+ view.setUint8(offset + length, 0);
45
+ }
46
+
47
+ function encodeFrames(frames: readonly Md2FrameInput[], numVertices: number): Uint8Array {
48
+ const frameSize = 40 + numVertices * 4;
49
+ const view = allocBuffer(frames.length * frameSize);
50
+ frames.forEach((frame, frameIndex) => {
51
+ const base = frameIndex * frameSize;
52
+ const scale: Vec3 = frame.scale ?? { x: 1, y: 1, z: 1 };
53
+ const translate: Vec3 = frame.translate ?? { x: 0, y: 0, z: 0 };
54
+ view.setFloat32(base, scale.x, true);
55
+ view.setFloat32(base + 4, scale.y, true);
56
+ view.setFloat32(base + 8, scale.z, true);
57
+ view.setFloat32(base + 12, translate.x, true);
58
+ view.setFloat32(base + 16, translate.y, true);
59
+ view.setFloat32(base + 20, translate.z, true);
60
+ writeCString(view, base + 24, frame.name, 16);
61
+
62
+ frame.vertices.forEach((vertex, index) => {
63
+ const offset = base + 40 + index * 4;
64
+ view.setUint8(offset, Math.round((vertex.position.x - translate.x) / scale.x));
65
+ view.setUint8(offset + 1, Math.round((vertex.position.y - translate.y) / scale.y));
66
+ view.setUint8(offset + 2, Math.round((vertex.position.z - translate.z) / scale.z));
67
+ view.setUint8(offset + 3, vertex.normalIndex);
68
+ });
69
+ });
70
+ return new Uint8Array(view.buffer);
71
+ }
72
+
73
+ function encodeGlCommands(commands: readonly Md2GlCommandInput[] | undefined): { data: Uint8Array; count: number } {
74
+ const bytes: number[] = [];
75
+ (commands ?? []).forEach((command) => {
76
+ const count = command.vertices.length * (command.mode === 'strip' ? 1 : -1);
77
+ bytes.push(count);
78
+ command.vertices.forEach((vertex) => {
79
+ const floatView = new DataView(new ArrayBuffer(4));
80
+ floatView.setFloat32(0, vertex.s, true);
81
+ bytes.push(floatView.getInt32(0, true));
82
+ floatView.setFloat32(0, vertex.t, true);
83
+ bytes.push(floatView.getInt32(0, true));
84
+ bytes.push(vertex.vertexIndex);
85
+ });
86
+ });
87
+ bytes.push(0);
88
+
89
+ const data = new Uint8Array(bytes.length * 4);
90
+ const view = new DataView(data.buffer);
91
+ bytes.forEach((value, index) => view.setInt32(index * 4, value, true));
92
+ return { data, count: bytes.length };
93
+ }
94
+
95
+ export function buildMd2(options: Md2BuilderOptions): ArrayBuffer {
96
+ const numVertices = options.frames[0]?.vertices.length ?? 0;
97
+ const frameSize = 40 + numVertices * 4;
98
+ const skins = options.skins ?? [];
99
+ const { data: glData, count: glCount } = encodeGlCommands(options.glCommands);
100
+
101
+ const headerSize = 68;
102
+ const skinsSize = skins.length * 64;
103
+ const texCoordSize = options.texCoords.length * 4;
104
+ const triangleSize = options.triangles.length * 12;
105
+ const frameBlockSize = options.frames.length * frameSize;
106
+ const glSize = glData.length;
107
+
108
+ const offsetSkins = headerSize;
109
+ const offsetTexCoords = offsetSkins + skinsSize;
110
+ const offsetTriangles = offsetTexCoords + texCoordSize;
111
+ const offsetFrames = offsetTriangles + triangleSize;
112
+ const offsetGlCommands = offsetFrames + frameBlockSize;
113
+ const offsetEnd = offsetGlCommands + glSize;
114
+
115
+ const view = allocBuffer(offsetEnd);
116
+ view.setInt32(0, 844121161, true); // IDP2
117
+ view.setInt32(4, 8, true);
118
+ view.setInt32(8, options.skinWidth ?? 64, true);
119
+ view.setInt32(12, options.skinHeight ?? 64, true);
120
+ view.setInt32(16, frameSize, true);
121
+ view.setInt32(20, skins.length, true);
122
+ view.setInt32(24, numVertices, true);
123
+ view.setInt32(28, options.texCoords.length, true);
124
+ view.setInt32(32, options.triangles.length, true);
125
+ view.setInt32(36, glCount, true);
126
+ view.setInt32(40, options.frames.length, true);
127
+ view.setInt32(44, offsetSkins, true);
128
+ view.setInt32(48, offsetTexCoords, true);
129
+ view.setInt32(52, offsetTriangles, true);
130
+ view.setInt32(56, offsetFrames, true);
131
+ view.setInt32(60, offsetGlCommands, true);
132
+ view.setInt32(64, offsetEnd, true);
133
+
134
+ skins.forEach((skin, index) => writeCString(view, offsetSkins + index * 64, skin, 64));
135
+
136
+ options.texCoords.forEach((coord, index) => {
137
+ const base = offsetTexCoords + index * 4;
138
+ view.setInt16(base, coord.s, true);
139
+ view.setInt16(base + 2, coord.t, true);
140
+ });
141
+
142
+ options.triangles.forEach((tri, index) => {
143
+ const base = offsetTriangles + index * 12;
144
+ view.setUint16(base, tri.vertexIndices[0], true);
145
+ view.setUint16(base + 2, tri.vertexIndices[1], true);
146
+ view.setUint16(base + 4, tri.vertexIndices[2], true);
147
+ view.setUint16(base + 6, tri.texCoordIndices[0], true);
148
+ view.setUint16(base + 8, tri.texCoordIndices[1], true);
149
+ view.setUint16(base + 10, tri.texCoordIndices[2], true);
150
+ });
151
+
152
+ new Uint8Array(view.buffer, offsetFrames, frameBlockSize).set(encodeFrames(options.frames, numVertices));
153
+ new Uint8Array(view.buffer, offsetGlCommands, glSize).set(glData);
154
+
155
+ return view.buffer as ArrayBuffer;
156
+ }
@@ -0,0 +1,149 @@
1
+ import { Vec3 } from '@quake2ts/shared';
2
+
3
+ interface Md3SurfaceSpec {
4
+ readonly name: string;
5
+ readonly triangles: readonly [number, number, number][];
6
+ readonly texCoords: readonly { s: number; t: number }[];
7
+ readonly vertices: readonly (readonly { position: Vec3; latLng: number }[])[];
8
+ readonly shaders?: readonly { name: string; index: number }[];
9
+ }
10
+
11
+ interface Md3BuildOptions {
12
+ readonly name?: string;
13
+ readonly frames: readonly { min: Vec3; max: Vec3; origin: Vec3; radius: number; name: string }[];
14
+ readonly tags?: readonly { name: string; origin: Vec3; axis: readonly [Vec3, Vec3, Vec3] }[];
15
+ readonly surfaces: readonly Md3SurfaceSpec[];
16
+ }
17
+
18
+ const HEADER_SIZE = 108;
19
+ const FRAME_SIZE = 56;
20
+ const TAG_SIZE = 112;
21
+ const SURFACE_HEADER_SIZE = 108;
22
+
23
+ function writeString(target: DataView, offset: number, text: string, length: number): void {
24
+ const encoder = new TextEncoder();
25
+ const encoded = encoder.encode(text);
26
+ const bytes = new Uint8Array(target.buffer, target.byteOffset + offset, length);
27
+ bytes.fill(0);
28
+ bytes.set(encoded.subarray(0, length));
29
+ }
30
+
31
+ function buildSurface(buffer: ArrayBuffer, offset: number, surface: Md3SurfaceSpec, numFrames: number): number {
32
+ const view = new DataView(buffer);
33
+ writeString(view, offset + 4, surface.name, 64);
34
+ view.setInt32(offset, 0x33504449, true);
35
+ view.setInt32(offset + 72, numFrames, true);
36
+ view.setInt32(offset + 76, surface.shaders?.length ?? 0, true);
37
+ view.setInt32(offset + 80, surface.vertices[0]?.length ?? 0, true);
38
+ view.setInt32(offset + 84, surface.triangles.length, true);
39
+
40
+ let cursor = SURFACE_HEADER_SIZE;
41
+ view.setInt32(offset + 88, cursor, true);
42
+ for (const tri of surface.triangles) {
43
+ view.setInt32(offset + cursor, tri[0], true);
44
+ view.setInt32(offset + cursor + 4, tri[1], true);
45
+ view.setInt32(offset + cursor + 8, tri[2], true);
46
+ cursor += 12;
47
+ }
48
+
49
+ view.setInt32(offset + 92, cursor, true);
50
+ for (const shader of surface.shaders ?? []) {
51
+ writeString(view, offset + cursor, shader.name, 64);
52
+ view.setInt32(offset + cursor + 64, shader.index, true);
53
+ cursor += 68;
54
+ }
55
+
56
+ view.setInt32(offset + 96, cursor, true);
57
+ for (const tex of surface.texCoords) {
58
+ view.setFloat32(offset + cursor, tex.s, true);
59
+ view.setFloat32(offset + cursor + 4, tex.t, true);
60
+ cursor += 8;
61
+ }
62
+
63
+ view.setInt32(offset + 100, cursor, true);
64
+ for (const frame of surface.vertices) {
65
+ for (const vertex of frame) {
66
+ view.setInt16(offset + cursor, Math.round(vertex.position.x * 64), true);
67
+ view.setInt16(offset + cursor + 2, Math.round(vertex.position.y * 64), true);
68
+ view.setInt16(offset + cursor + 4, Math.round(vertex.position.z * 64), true);
69
+ view.setUint16(offset + cursor + 6, vertex.latLng, true);
70
+ cursor += 8;
71
+ }
72
+ }
73
+
74
+ view.setInt32(offset + 104, cursor, true);
75
+ return cursor;
76
+ }
77
+
78
+ export function buildMd3(options: Md3BuildOptions): ArrayBuffer {
79
+ const numFrames = options.frames.length;
80
+ const numTags = options.tags?.length ?? 0;
81
+ const numSurfaces = options.surfaces.length;
82
+
83
+ let size = HEADER_SIZE;
84
+ size += numFrames * FRAME_SIZE;
85
+ size += numFrames * numTags * TAG_SIZE;
86
+
87
+ for (const surface of options.surfaces) {
88
+ const verts = surface.vertices[0]?.length ?? 0;
89
+ const triangles = surface.triangles.length;
90
+ const shaders = surface.shaders?.length ?? 0;
91
+ const texCoordBytes = surface.texCoords.length * 8;
92
+ const surfaceSize = SURFACE_HEADER_SIZE + triangles * 12 + shaders * 68 + texCoordBytes + verts * 8 * numFrames;
93
+ size += surfaceSize;
94
+ }
95
+
96
+ const buffer = new ArrayBuffer(size);
97
+ const view = new DataView(buffer);
98
+ writeString(view, 8, options.name ?? 'builder', 64);
99
+ view.setInt32(0, 0x33504449, true);
100
+ view.setInt32(4, 15, true);
101
+ view.setInt32(72, 0, true);
102
+ view.setInt32(76, numFrames, true);
103
+ view.setInt32(80, numTags, true);
104
+ view.setInt32(84, numSurfaces, true);
105
+ view.setInt32(88, 0, true);
106
+
107
+ let offset = HEADER_SIZE;
108
+ view.setInt32(92, offset, true);
109
+ for (const frame of options.frames) {
110
+ view.setFloat32(offset, frame.min.x, true);
111
+ view.setFloat32(offset + 4, frame.min.y, true);
112
+ view.setFloat32(offset + 8, frame.min.z, true);
113
+ view.setFloat32(offset + 12, frame.max.x, true);
114
+ view.setFloat32(offset + 16, frame.max.y, true);
115
+ view.setFloat32(offset + 20, frame.max.z, true);
116
+ view.setFloat32(offset + 24, frame.origin.x, true);
117
+ view.setFloat32(offset + 28, frame.origin.y, true);
118
+ view.setFloat32(offset + 32, frame.origin.z, true);
119
+ view.setFloat32(offset + 36, frame.radius, true);
120
+ writeString(view, offset + 40, frame.name, 16);
121
+ offset += FRAME_SIZE;
122
+ }
123
+
124
+ view.setInt32(96, offset, true);
125
+ for (let frame = 0; frame < numFrames; frame += 1) {
126
+ for (const tag of options.tags ?? []) {
127
+ writeString(view, offset, tag.name, 64);
128
+ view.setFloat32(offset + 64, tag.origin.x, true);
129
+ view.setFloat32(offset + 68, tag.origin.y, true);
130
+ view.setFloat32(offset + 72, tag.origin.z, true);
131
+ for (let axis = 0; axis < 3; axis += 1) {
132
+ const v = tag.axis[axis];
133
+ view.setFloat32(offset + 76 + axis * 12, v.x, true);
134
+ view.setFloat32(offset + 80 + axis * 12, v.y, true);
135
+ view.setFloat32(offset + 84 + axis * 12, v.z, true);
136
+ }
137
+ offset += TAG_SIZE;
138
+ }
139
+ }
140
+
141
+ view.setInt32(100, offset, true);
142
+ for (let i = 0; i < options.surfaces.length; i += 1) {
143
+ const written = buildSurface(buffer, offset, options.surfaces[i]!, numFrames);
144
+ offset += written;
145
+ }
146
+
147
+ view.setInt32(104, offset, true);
148
+ return buffer;
149
+ }
@@ -0,0 +1,50 @@
1
+ export interface PakEntrySpec {
2
+ path: string;
3
+ data: Uint8Array;
4
+ }
5
+
6
+ const HEADER_SIZE = 12;
7
+ const DIRECTORY_ENTRY_SIZE = 64;
8
+
9
+ export function buildPak(entries: PakEntrySpec[]): ArrayBuffer {
10
+ let offset = HEADER_SIZE;
11
+ const fileBuffers: Uint8Array[] = [];
12
+ const directory = new Uint8Array(entries.length * DIRECTORY_ENTRY_SIZE);
13
+ const dirView = new DataView(directory.buffer);
14
+
15
+ entries.forEach((entry, index) => {
16
+ const data = entry.data;
17
+ fileBuffers.push(data);
18
+
19
+ const nameBytes = new TextEncoder().encode(entry.path);
20
+ directory.set(nameBytes.slice(0, 56), index * DIRECTORY_ENTRY_SIZE);
21
+ dirView.setInt32(index * DIRECTORY_ENTRY_SIZE + 56, offset, true);
22
+ dirView.setInt32(index * DIRECTORY_ENTRY_SIZE + 60, data.byteLength, true);
23
+
24
+ offset += data.byteLength;
25
+ });
26
+
27
+ const directoryOffset = offset;
28
+ const directoryLength = directory.byteLength;
29
+ const totalSize = HEADER_SIZE + fileBuffers.reduce((sum, buf) => sum + buf.byteLength, 0) + directoryLength;
30
+ const buffer = new ArrayBuffer(totalSize);
31
+ const view = new DataView(buffer);
32
+ const writer = new Uint8Array(buffer);
33
+
34
+ writer.set([0x50, 0x41, 0x43, 0x4b]);
35
+ view.setInt32(4, directoryOffset, true);
36
+ view.setInt32(8, directoryLength, true);
37
+
38
+ let cursor = HEADER_SIZE;
39
+ for (const data of fileBuffers) {
40
+ writer.set(data, cursor);
41
+ cursor += data.byteLength;
42
+ }
43
+
44
+ writer.set(directory, directoryOffset);
45
+ return buffer;
46
+ }
47
+
48
+ export function textData(text: string): Uint8Array {
49
+ return new TextEncoder().encode(text);
50
+ }