forked from magcius/noclip.website
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathppf.ts
271 lines (234 loc) · 8.75 KB
/
ppf.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
import ArrayBufferSlice from "../ArrayBufferSlice.js";
import * as PLB from './plb.js';
import { align, assert } from "../util.js";
import { DataStream } from "./DataStream.js";
export function normalizeTextureName(textureName: string): string {
textureName = textureName.toLowerCase().replace(/\\/g, '/');
textureName = textureName.replace(/^c:\/psychonauts\/resource\//, '');
textureName = textureName.replace(/^workresource\//, '');
textureName = textureName.replace(/\.(tga|dds)$/, '');
return textureName;
}
export interface PPAK_Texture {
name: string;
type: TextureType;
format: TextureFormat;
width: number;
height: number;
mipData: ArrayBufferSlice[];
}
export interface PPAK {
textures: PPAK_Texture[];
mainScene: PLB.EScene | null;
}
/*
* Texture formats:
* fmt | format internalformat type | format internalformat type
* ----|--------------------------------|--------------------------------------------------------------
* 00 | 0x80E1 0x8058 0x8367 | BGRA RGBA8 UNSIGNED_INT_8_8_8_8_REV
* 01 | 0x80E1 0x8051 0x8367 | BGRA RGB8 UNSIGNED_INT_8_8_8_8_REV
* 02 | 0x1908 0x8056 0x8033 | RGBA RGBA4 UNSIGNED_SHORT_4_4_4_4
* 03 | 0x1908 0x8057 0x8034 | RGBA RGB5A1 UNSIGNED_SHORT_5_5_5_1
* 04 | 0x1907 0x8050 0x8034 | RGB RGB5 UNSIGNED_SHORT_5_5_5_1
* 05 | 0x1907 0x8050 0x8363 | RGB RGB5 UNSIGNED_SHORT_5_6_5
* 06 | 0x1906 0x803C 0x1401 | ALPHA ALPHA8 UNSIGNED_BYTE
* 07 | 0x1909 0x8040 0x1401 | LUMINANCE LUMINANCE8 UNSIGNED_BYTE
* 08 | 0x1909 0x8040 0x1401 | LUMINANCE LUMINANCE8 UNSIGNED_BYTE
* 09 | 0x83F1 0x83F1 0x1401 | DXT1 DXT1 UNSIGNED_BYTE
* 10 | 0x83F2 0x83F2 0x1401 | DXT3 DXT3 UNSIGNED_BYTE
* 11 | 0x83F3 0x83F3 0x1401 | DXT5 DXT5 UNSIGNED_BYTE
* 12 | 0x190A 0x8045 0x1401 | LUMINANCE_ALPHA LUMINANCE8_ALPHA8 UNSIGNED_BYTE
* 13 | 0x190A 0x8048 0x1403 | LUMINANCE_ALPHA LUMINANCE16_ALPHA16 UNSIGNED_SHORT
*/
export const enum TextureFormat {
B8G8R8A8,
B8G8R8X8,
R4G4B4A4,
R5G5B5A1,
R5G5B5X1,
R5G6B5,
A8,
L8,
AL8, // Why twice?
DXT1,
DXT3,
DXT5,
V8U8,
V16U16,
P8,
}
export function getTextureFormatName(fmt: TextureFormat): string {
switch (fmt) {
case TextureFormat.B8G8R8A8: return "8888";
case TextureFormat.B8G8R8X8: return "0888";
case TextureFormat.R4G4B4A4: return "4444";
case TextureFormat.R5G5B5A1: return "1555";
case TextureFormat.R5G5B5X1: return "0555";
case TextureFormat.R5G6B5: return "565";
case TextureFormat.A8: return "A8";
case TextureFormat.L8: return "L8";
case TextureFormat.AL8: return "AL8";
case TextureFormat.DXT1: return "DXT1";
case TextureFormat.DXT3: return "DXT3";
case TextureFormat.DXT5: return "DXT5";
case TextureFormat.V8U8: return "V8U8";
case TextureFormat.V16U16: return "V16U16";
case TextureFormat.P8: return "P8";
}
}
function getTextureFormatGetBytesPerPixel(fmt: TextureFormat): number {
switch (fmt) {
case TextureFormat.B8G8R8A8:
case TextureFormat.B8G8R8X8:
case TextureFormat.V16U16:
return 4;
case TextureFormat.R4G4B4A4:
case TextureFormat.R5G5B5A1:
case TextureFormat.R5G5B5X1:
case TextureFormat.R5G6B5:
case TextureFormat.V8U8:
return 2;
case TextureFormat.A8:
case TextureFormat.L8:
case TextureFormat.P8:
return 1;
default:
throw "whoops";
}
}
function calcTextureByteSize(fmt: TextureFormat, width: number, height: number): number {
switch (fmt) {
case TextureFormat.DXT1:
return (align(width, 4) * align(height, 4)) >>> 1;
case TextureFormat.DXT3:
case TextureFormat.DXT5:
return align(width, 4) * align(height, 4);
default:
return getTextureFormatGetBytesPerPixel(fmt) * width * height;
}
}
function autoDetectMipCount(width: number, height: number): number {
let count = 0;
while (width > 0 && height > 0) {
count++;
width >>= 1;
height >>= 1;
}
return count;
}
export const enum TextureType {
NORMAL = 0,
CUBEMAP = 1,
VOLUME_MAP = 2,
DEPTH_BUFFER = 3,
}
export function parsePPAKTexture(buffer: ArrayBufferSlice): PPAK_Texture {
const stream = new DataStream(buffer);
stream.offs = 0x28;
const namePtr = stream.view.getUint32(0x0C, true);
let name: string = '';
if (namePtr) {
name = stream.readStringStream_2b();
}
name = normalizeTextureName(name);
const texAnimInfoPtr = stream.view.getUint32(0x10, true);
let numFrames: number = 1;
if (texAnimInfoPtr) {
numFrames = stream.readUint32();
stream.offs += 0x18;
}
const dataId = stream.readUint32();
const format: TextureFormat = stream.readUint32();
const type: TextureType = stream.readUint32();
const flags = stream.readUint32();
const width = stream.readUint32();
const height = stream.readUint32();
let numMips = stream.readUint32();
if (numMips === 0)
numMips = autoDetectMipCount(width, height);
stream.offs += 0x10;
const mipData: ArrayBufferSlice[] = [];
let mipWidth = width, mipHeight = height;
for (let i = 0; i < numMips; i++) {
const mipByteSize = calcTextureByteSize(format, mipWidth, mipHeight);
mipData.push(stream.readSlice(mipByteSize));
if (mipWidth > 1) mipWidth >>>= 1;
if (mipHeight > 1) mipHeight >>>= 1;
}
return { name, type, format, width, height, mipData };
}
function EGameTextureManager_ReadPackFileTextures(stream: DataStream, textures: PPAK_Texture[], textureCount: number) {
for (let i = 0; i < textureCount; i++) {
const magic = stream.readString(0x04);
assert(magic === ' XT1');
const size = stream.readUint32();
textures.push(parsePPAKTexture(stream.readSlice(size)));
}
}
function EGameTextureManager_ReadPackFile(stream: DataStream): PPAK_Texture[] {
let marker = stream.readUint16();
let v9 = 0;
if (marker === 0xFDFD) {
v9 = stream.readUint16();
marker = stream.readUint16();
}
const textures: PPAK_Texture[] = [];
assert(v9 === 0x01);
while (marker === 0xFFFF) {
const language = stream.readUint16();
const size = stream.readUint32();
// English, I believe.
if (language === 0x00) {
const textureCount = stream.readUint16();
EGameTextureManager_ReadPackFileTextures(stream, textures, textureCount);
} else {
stream.offs += size;
}
marker = stream.readUint16();
}
assert(marker != 0xFFFF);
const textureCount = marker;
EGameTextureManager_ReadPackFileTextures(stream, textures, textureCount);
return textures;
}
function EScriptVM_ReadPackFile(stream: DataStream): void {
let scriptObjectCount = stream.readUint16();
let scriptFilesHaveNames = 0;
if (scriptObjectCount === 0xFCFC) {
scriptFilesHaveNames = stream.readUint16();
scriptObjectCount = stream.readUint16();
}
for (let i = 0; i < scriptObjectCount; i++) {
const name = stream.readStringStream_2b();
const scriptSize = stream.readUint32();
stream.offs += scriptSize;
}
const scriptFileCount = stream.readUint16();
for (let i = 0; i < scriptFileCount; i++) {
const filename = scriptFilesHaveNames ? stream.readStringStream_2b() : null;
const scriptSize = stream.readUint32();
stream.offs += scriptSize;
}
}
export function parse(buffer: ArrayBufferSlice, hasScene: boolean): PPAK {
const stream = new DataStream(buffer);
assert(stream.readString(0x04) === 'PPAK');
const textures = EGameTextureManager_ReadPackFile(stream);
assert(stream.readString(0x04) === 'MPAK');
const meshCount = stream.readUint16();
for (let i = 0; i < meshCount; i++) {
const name = stream.readStringStream_2b();
const v9 = stream.readUint16();
const size = stream.readUint32();
const slice = stream.readSlice(size);
// mesh.push(PLB.parse(slice, name));
}
let mainScene: PLB.EScene | null = null;
if (hasScene) {
EScriptVM_ReadPackFile(stream);
// Rest of the stream is the main level file
const mainSceneSlice = stream.buffer.slice(stream.offs);
mainScene = PLB.parse(mainSceneSlice, 'MainScene');
}
return { textures, mainScene };
}