ac-cardata 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Misha Nasledov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,29 @@
1
- # Temporary Holding Version
1
+ # ac-cardata
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reads what an Assetto Corsa car's own data says about it: its `data/` folder,
4
+ or the same files packed into `data.acd`, and the numbers in them that place
5
+ things on the car.
6
+
7
+ ```ts
8
+ import { carDriverEyes, carWheels, readCarFile, parseIni } from "ac-cardata"
9
+
10
+ const cars = "/path/to/assettocorsa/content/cars"
11
+ await carDriverEyes(cars, "abarth500") // [0.330737, 1.19075, -0.490002]
12
+ await carWheels(cars, "abarth500") // four wheel centres, FL, FR, RL, RR
13
+
14
+ const ini = await readCarFile(`${cars}/abarth500`, "car.ini")
15
+ parseIni(ini!.toString("latin1")).get("BASIC")?.get("TOTALMASS")
16
+ ```
17
+
18
+ - **Files:** `readCarFile(carDir, name)` takes the unpacked `data/` folder
19
+ first, as the game does, and the archive otherwise. The archive's key comes
20
+ from the folder's name, so `carDir` must be named as the game names the car.
21
+ `readAcd(buffer, folder)` opens an archive you already hold.
22
+ - **Driver's eyes:** `[GRAPHICS] DRIVEREYES`, the cockpit camera, in the frame
23
+ of the car's 3D model: `[BASIC] GRAPHICS_OFFSET` does not apply. +x is the
24
+ car's left, +y up, +z forward.
25
+ - **Wheels:** from `suspensions.ini`, relative to the car's position as the
26
+ game reports it, which is its centre of gravity.
27
+
28
+ Checked against all 245 archives on one install, and the eyes against where
29
+ the steering wheel puts them on two cars' models.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What a car's own data says about it: the files in its `data/` folder, or
3
+ * packed into `data.acd`, and the few numbers in them that place things on the
4
+ * car. Read-only and in memory; nothing here writes an archive or keeps what
5
+ * it read.
6
+ */
7
+ export type Vec3 = [number, number, number];
8
+ /** Body-local wheel centers, meters: x across (the car's left), y up, z forward. FL, FR, RL, RR. */
9
+ export type WheelOffsets = [Vec3, Vec3, Vec3, Vec3];
10
+ /** The files inside a car's data archive, by name. */
11
+ export type CarFiles = Map<string, Buffer>;
12
+ /**
13
+ * Reads `data.acd`: a run of entries, each an i32 name length, the name, an
14
+ * i32 byte count, then that many i32 slots whose low byte is the file's byte
15
+ * shifted up by the next character of a key made from the car's folder name,
16
+ * the key starting over for each entry.
17
+ */
18
+ export declare function readAcd(buf: Buffer, folder: string): CarFiles;
19
+ /**
20
+ * The key for a car's archive, from its folder name: eight checksums of the
21
+ * lowercased name in signed 32-bit arithmetic, each kept to its low byte,
22
+ * joined with "-". Written from a description of the scheme two open-source
23
+ * readers share; it opens all 245 archives on the machine it was checked on.
24
+ */
25
+ export declare function acdKey(folder: string): string;
26
+ /** `[SECTION]` → key → value, values with comments and padding stripped. */
27
+ export declare function parseIni(text: string): Map<string, Map<string, string>>;
28
+ /**
29
+ * Wheel centers relative to the car's center of gravity, which is where the
30
+ * game puts a car's position: across by half the track, down by BASEY, and
31
+ * along by the wheelbase split at CG_LOCATION (the share of weight on the
32
+ * front axle). On a Nissan R32 this reproduces the wheels the game stored in
33
+ * a replay to within a centimeter.
34
+ */
35
+ export declare function wheelsFromSuspension(ini: string): WheelOffsets;
36
+ /**
37
+ * One file of a car's data, from `<carDir>/data/` when the car is unpacked and
38
+ * from its `data.acd` otherwise; undefined when it has neither or the file is
39
+ * not in them. The archive's key is the folder's name, so `carDir` has to be
40
+ * the car's own folder, named as the game names it.
41
+ */
42
+ export declare function readCarFile(carDir: string, name: string): Promise<Buffer | undefined>;
43
+ /**
44
+ * A car's wheel offsets from `<cars>/<model>`, unpacked `data/` first, then
45
+ * `data.acd`; undefined when the car has neither or they can't be read.
46
+ */
47
+ export declare function carWheels(carsDir: string, model: string): Promise<WheelOffsets | undefined>;
48
+ /**
49
+ * Where the driver's eyes are, from `car.ini`'s `[GRAPHICS] DRIVEREYES`: the
50
+ * cockpit camera's position, in the frame of the car's 3D model rather than of
51
+ * its physics, so `[BASIC] GRAPHICS_OFFSET` does not apply to it. +x is to the
52
+ * car's left, +y up, +z forward, as in the model.
53
+ */
54
+ export declare function driverEyes(carIni: string): Vec3;
55
+ /** A car's driver's eyes from `<cars>/<model>`; undefined when its data can't be read. */
56
+ export declare function carDriverEyes(carsDir: string, model: string): Promise<Vec3 | undefined>;
57
+ //# sourceMappingURL=cardata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardata.d.ts","sourceRoot":"","sources":["../src/cardata.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAE3C,oGAAoG;AACpG,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;AAEnD,sDAAsD;AACtD,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAK1C;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,CAuB7D;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoC7C;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAevE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAiB9D;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAK3F;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAGjG;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAS/C;AAED,0FAA0F;AAC1F,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CAG7F"}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * What a car's own data says about it: the files in its `data/` folder, or
3
+ * packed into `data.acd`, and the few numbers in them that place things on the
4
+ * car. Read-only and in memory; nothing here writes an archive or keeps what
5
+ * it read.
6
+ */
7
+ import { readFile } from "node:fs/promises";
8
+ import { basename, join } from "node:path";
9
+ /** Kunos's own cars start with this, then one more int, before the first entry. */
10
+ const ACD_MARKER = -1111;
11
+ /**
12
+ * Reads `data.acd`: a run of entries, each an i32 name length, the name, an
13
+ * i32 byte count, then that many i32 slots whose low byte is the file's byte
14
+ * shifted up by the next character of a key made from the car's folder name,
15
+ * the key starting over for each entry.
16
+ */
17
+ export function readAcd(buf, folder) {
18
+ const key = acdKey(folder);
19
+ const out = new Map();
20
+ let o = buf.length >= 8 && buf.readInt32LE(0) === ACD_MARKER ? 8 : 0;
21
+ while (o + 4 <= buf.length) {
22
+ const nameLen = buf.readInt32LE(o);
23
+ o += 4;
24
+ if (nameLen <= 0 || o + nameLen + 4 > buf.length)
25
+ throw new Error(`${folder}/data.acd isn't an archive this reads`);
26
+ const name = buf.subarray(o, o + nameLen).toString("latin1");
27
+ o += nameLen;
28
+ const len = buf.readInt32LE(o);
29
+ o += 4;
30
+ if (len < 0 || o + len * 4 > buf.length)
31
+ throw new Error(`${folder}/data.acd: ${name} runs past the end`);
32
+ const data = Buffer.alloc(len);
33
+ for (let i = 0; i < len; i++) {
34
+ data[i] = (buf.readUInt8(o + i * 4) - key.charCodeAt(i % key.length)) & 0xff;
35
+ }
36
+ o += len * 4;
37
+ out.set(name, data);
38
+ }
39
+ return out;
40
+ }
41
+ /**
42
+ * The key for a car's archive, from its folder name: eight checksums of the
43
+ * lowercased name in signed 32-bit arithmetic, each kept to its low byte,
44
+ * joined with "-". Written from a description of the scheme two open-source
45
+ * readers share; it opens all 245 archives on the machine it was checked on.
46
+ */
47
+ export function acdKey(folder) {
48
+ const c = [...folder.toLowerCase()].map((ch) => ch.charCodeAt(0));
49
+ const n = c.length;
50
+ const at = (i) => c[i];
51
+ const div = (a, b) => Math.trunc(a / b) | 0;
52
+ let k1 = 0;
53
+ for (let i = 0; i < n; i++)
54
+ k1 = (k1 + at(i)) | 0;
55
+ let k2 = 0;
56
+ for (let i = 0; i < n - 1; i += 2)
57
+ k2 = (Math.imul(k2, at(i)) - at(i + 1)) | 0;
58
+ let k3 = 0;
59
+ for (let i = 1; i < n - 3; i += 3) {
60
+ k3 = Math.imul(k3, at(i));
61
+ k3 = div(k3, at(i + 1) + 27);
62
+ k3 = (k3 - 27 - at(i - 1)) | 0;
63
+ }
64
+ let k4 = 0x1683;
65
+ for (let i = 1; i < n; i++)
66
+ k4 = (k4 - at(i)) | 0;
67
+ let k5 = 66;
68
+ for (let i = 1; i < n - 4; i += 4)
69
+ k5 = (Math.imul(Math.imul(k5, at(i) + 15), at(i - 1) + 15) + 22) | 0;
70
+ let k6 = 101;
71
+ for (let i = 0; i < n - 2; i += 2)
72
+ k6 = (k6 - at(i)) | 0;
73
+ let k7 = 171;
74
+ for (let i = 0; i < n - 2; i += 2)
75
+ k7 = k7 % at(i);
76
+ let k8 = 171;
77
+ for (let i = 0; i < n - 1; i++)
78
+ k8 = (div(k8, at(i)) + at(i + 1)) | 0;
79
+ return [k1, k2, k3, k4, k5, k6, k7, k8].map((k) => k & 0xff).join("-");
80
+ }
81
+ /** `[SECTION]` → key → value, values with comments and padding stripped. */
82
+ export function parseIni(text) {
83
+ const out = new Map();
84
+ let section = new Map();
85
+ for (const raw of text.split(/\r?\n/)) {
86
+ const line = raw.replace(/;.*$|\/\/.*$/, "").trim();
87
+ const head = /^\[(.+)\]$/.exec(line);
88
+ if (head) {
89
+ section = out.get(head[1]) ?? new Map();
90
+ out.set(head[1], section);
91
+ }
92
+ else {
93
+ const eq = line.indexOf("=");
94
+ if (eq > 0)
95
+ section.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
96
+ }
97
+ }
98
+ return out;
99
+ }
100
+ /**
101
+ * Wheel centers relative to the car's center of gravity, which is where the
102
+ * game puts a car's position: across by half the track, down by BASEY, and
103
+ * along by the wheelbase split at CG_LOCATION (the share of weight on the
104
+ * front axle). On a Nissan R32 this reproduces the wheels the game stored in
105
+ * a replay to within a centimeter.
106
+ */
107
+ export function wheelsFromSuspension(ini) {
108
+ const s = parseIni(ini);
109
+ const num = (section, key) => {
110
+ const v = Number(s.get(section)?.get(key));
111
+ if (!Number.isFinite(v))
112
+ throw new Error(`suspensions.ini has no usable ${section}.${key}`);
113
+ return v;
114
+ };
115
+ const wheelbase = num("BASIC", "WHEELBASE");
116
+ const cg = num("BASIC", "CG_LOCATION");
117
+ const front = { x: num("FRONT", "TRACK") / 2, y: num("FRONT", "BASEY"), z: wheelbase * (1 - cg) };
118
+ const rear = { x: num("REAR", "TRACK") / 2, y: num("REAR", "BASEY"), z: -wheelbase * cg };
119
+ return [
120
+ [front.x, front.y, front.z],
121
+ [-front.x, front.y, front.z],
122
+ [rear.x, rear.y, rear.z],
123
+ [-rear.x, rear.y, rear.z],
124
+ ];
125
+ }
126
+ /**
127
+ * One file of a car's data, from `<carDir>/data/` when the car is unpacked and
128
+ * from its `data.acd` otherwise; undefined when it has neither or the file is
129
+ * not in them. The archive's key is the folder's name, so `carDir` has to be
130
+ * the car's own folder, named as the game names it.
131
+ */
132
+ export async function readCarFile(carDir, name) {
133
+ const unpacked = await readFile(join(carDir, "data", name)).catch(() => undefined);
134
+ if (unpacked !== undefined)
135
+ return unpacked;
136
+ const acd = await readFile(join(carDir, "data.acd")).catch(() => undefined);
137
+ return acd ? readAcd(acd, basename(carDir)).get(name) : undefined;
138
+ }
139
+ /**
140
+ * A car's wheel offsets from `<cars>/<model>`, unpacked `data/` first, then
141
+ * `data.acd`; undefined when the car has neither or they can't be read.
142
+ */
143
+ export async function carWheels(carsDir, model) {
144
+ const ini = await readCarFile(join(carsDir, model), "suspensions.ini");
145
+ return ini ? wheelsFromSuspension(ini.toString("latin1")) : undefined;
146
+ }
147
+ /**
148
+ * Where the driver's eyes are, from `car.ini`'s `[GRAPHICS] DRIVEREYES`: the
149
+ * cockpit camera's position, in the frame of the car's 3D model rather than of
150
+ * its physics, so `[BASIC] GRAPHICS_OFFSET` does not apply to it. +x is to the
151
+ * car's left, +y up, +z forward, as in the model.
152
+ */
153
+ export function driverEyes(carIni) {
154
+ const raw = parseIni(carIni).get("GRAPHICS")?.get("DRIVEREYES");
155
+ const v = (raw ?? "").split(",").map((n) => Number(n.trim()));
156
+ if (v.length !== 3 || !v.every(Number.isFinite)) {
157
+ throw new Error(`car.ini has no usable GRAPHICS.DRIVEREYES${raw === undefined ? "" : `: ${JSON.stringify(raw)}`}`);
158
+ }
159
+ return [v[0], v[1], v[2]];
160
+ }
161
+ /** A car's driver's eyes from `<cars>/<model>`; undefined when its data can't be read. */
162
+ export async function carDriverEyes(carsDir, model) {
163
+ const ini = await readCarFile(join(carsDir, model), "car.ini");
164
+ return ini ? driverEyes(ini.toString("latin1")) : undefined;
165
+ }
166
+ //# sourceMappingURL=cardata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardata.js","sourceRoot":"","sources":["../src/cardata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { acdKey, type CarFiles, carDriverEyes, carWheels, driverEyes, parseIni, readAcd, readCarFile, type Vec3, type WheelOffsets, wheelsFromSuspension, } from "./cardata.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,KAAK,QAAQ,EACb,aAAa,EACb,SAAS,EACT,UAAU,EACV,QAAQ,EACR,OAAO,EACP,WAAW,EACX,KAAK,IAAI,EACT,KAAK,YAAY,EACjB,oBAAoB,GACrB,MAAM,cAAc,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { acdKey, carDriverEyes, carWheels, driverEyes, parseIni, readAcd, readCarFile, wheelsFromSuspension, } from "./cardata.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EAEN,aAAa,EACb,SAAS,EACT,UAAU,EACV,QAAQ,EACR,OAAO,EACP,WAAW,EAGX,oBAAoB,GACrB,MAAM,cAAc,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "ac-cardata",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Read an Assetto Corsa car's own data: data.acd and data/, car.ini, and the numbers that place wheels and the driver's eyes.",
5
+ "license": "MIT",
6
+ "author": "Misha Nasledov <misha@nasledov.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mishan/assetto-ts.git",
10
+ "directory": "packages/ac-cardata"
11
+ },
12
+ "homepage": "https://github.com/mishan/assetto-ts/tree/main/packages/ac-cardata#readme",
13
+ "bugs": "https://github.com/mishan/assetto-ts/issues",
14
+ "keywords": [
15
+ "assetto-corsa",
16
+ "data.acd",
17
+ "car.ini",
18
+ "sim-racing"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src"
30
+ ],
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "prepack": "npm run build"
37
+ }
38
+ }
package/src/cardata.ts ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * What a car's own data says about it: the files in its `data/` folder, or
3
+ * packed into `data.acd`, and the few numbers in them that place things on the
4
+ * car. Read-only and in memory; nothing here writes an archive or keeps what
5
+ * it read.
6
+ */
7
+
8
+ import { readFile } from "node:fs/promises"
9
+ import { basename, join } from "node:path"
10
+
11
+ export type Vec3 = [number, number, number]
12
+
13
+ /** Body-local wheel centers, meters: x across (the car's left), y up, z forward. FL, FR, RL, RR. */
14
+ export type WheelOffsets = [Vec3, Vec3, Vec3, Vec3]
15
+
16
+ /** The files inside a car's data archive, by name. */
17
+ export type CarFiles = Map<string, Buffer>
18
+
19
+ /** Kunos's own cars start with this, then one more int, before the first entry. */
20
+ const ACD_MARKER = -1111
21
+
22
+ /**
23
+ * Reads `data.acd`: a run of entries, each an i32 name length, the name, an
24
+ * i32 byte count, then that many i32 slots whose low byte is the file's byte
25
+ * shifted up by the next character of a key made from the car's folder name,
26
+ * the key starting over for each entry.
27
+ */
28
+ export function readAcd(buf: Buffer, folder: string): CarFiles {
29
+ const key = acdKey(folder)
30
+ const out: CarFiles = new Map()
31
+ let o = buf.length >= 8 && buf.readInt32LE(0) === ACD_MARKER ? 8 : 0
32
+ while (o + 4 <= buf.length) {
33
+ const nameLen = buf.readInt32LE(o)
34
+ o += 4
35
+ if (nameLen <= 0 || o + nameLen + 4 > buf.length)
36
+ throw new Error(`${folder}/data.acd isn't an archive this reads`)
37
+ const name = buf.subarray(o, o + nameLen).toString("latin1")
38
+ o += nameLen
39
+ const len = buf.readInt32LE(o)
40
+ o += 4
41
+ if (len < 0 || o + len * 4 > buf.length)
42
+ throw new Error(`${folder}/data.acd: ${name} runs past the end`)
43
+ const data = Buffer.alloc(len)
44
+ for (let i = 0; i < len; i++) {
45
+ data[i] = (buf.readUInt8(o + i * 4) - key.charCodeAt(i % key.length)) & 0xff
46
+ }
47
+ o += len * 4
48
+ out.set(name, data)
49
+ }
50
+ return out
51
+ }
52
+
53
+ /**
54
+ * The key for a car's archive, from its folder name: eight checksums of the
55
+ * lowercased name in signed 32-bit arithmetic, each kept to its low byte,
56
+ * joined with "-". Written from a description of the scheme two open-source
57
+ * readers share; it opens all 245 archives on the machine it was checked on.
58
+ */
59
+ export function acdKey(folder: string): string {
60
+ const c = [...folder.toLowerCase()].map((ch) => ch.charCodeAt(0))
61
+ const n = c.length
62
+ const at = (i: number): number => c[i]!
63
+ const div = (a: number, b: number): number => Math.trunc(a / b) | 0
64
+
65
+ let k1 = 0
66
+ for (let i = 0; i < n; i++) k1 = (k1 + at(i)) | 0
67
+
68
+ let k2 = 0
69
+ for (let i = 0; i < n - 1; i += 2) k2 = (Math.imul(k2, at(i)) - at(i + 1)) | 0
70
+
71
+ let k3 = 0
72
+ for (let i = 1; i < n - 3; i += 3) {
73
+ k3 = Math.imul(k3, at(i))
74
+ k3 = div(k3, at(i + 1) + 27)
75
+ k3 = (k3 - 27 - at(i - 1)) | 0
76
+ }
77
+
78
+ let k4 = 0x1683
79
+ for (let i = 1; i < n; i++) k4 = (k4 - at(i)) | 0
80
+
81
+ let k5 = 66
82
+ for (let i = 1; i < n - 4; i += 4)
83
+ k5 = (Math.imul(Math.imul(k5, at(i) + 15), at(i - 1) + 15) + 22) | 0
84
+
85
+ let k6 = 101
86
+ for (let i = 0; i < n - 2; i += 2) k6 = (k6 - at(i)) | 0
87
+
88
+ let k7 = 171
89
+ for (let i = 0; i < n - 2; i += 2) k7 = k7 % at(i)
90
+
91
+ let k8 = 171
92
+ for (let i = 0; i < n - 1; i++) k8 = (div(k8, at(i)) + at(i + 1)) | 0
93
+
94
+ return [k1, k2, k3, k4, k5, k6, k7, k8].map((k) => k & 0xff).join("-")
95
+ }
96
+
97
+ /** `[SECTION]` → key → value, values with comments and padding stripped. */
98
+ export function parseIni(text: string): Map<string, Map<string, string>> {
99
+ const out = new Map<string, Map<string, string>>()
100
+ let section = new Map<string, string>()
101
+ for (const raw of text.split(/\r?\n/)) {
102
+ const line = raw.replace(/;.*$|\/\/.*$/, "").trim()
103
+ const head = /^\[(.+)\]$/.exec(line)
104
+ if (head) {
105
+ section = out.get(head[1]!) ?? new Map()
106
+ out.set(head[1]!, section)
107
+ } else {
108
+ const eq = line.indexOf("=")
109
+ if (eq > 0) section.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim())
110
+ }
111
+ }
112
+ return out
113
+ }
114
+
115
+ /**
116
+ * Wheel centers relative to the car's center of gravity, which is where the
117
+ * game puts a car's position: across by half the track, down by BASEY, and
118
+ * along by the wheelbase split at CG_LOCATION (the share of weight on the
119
+ * front axle). On a Nissan R32 this reproduces the wheels the game stored in
120
+ * a replay to within a centimeter.
121
+ */
122
+ export function wheelsFromSuspension(ini: string): WheelOffsets {
123
+ const s = parseIni(ini)
124
+ const num = (section: string, key: string): number => {
125
+ const v = Number(s.get(section)?.get(key))
126
+ if (!Number.isFinite(v)) throw new Error(`suspensions.ini has no usable ${section}.${key}`)
127
+ return v
128
+ }
129
+ const wheelbase = num("BASIC", "WHEELBASE")
130
+ const cg = num("BASIC", "CG_LOCATION")
131
+ const front = { x: num("FRONT", "TRACK") / 2, y: num("FRONT", "BASEY"), z: wheelbase * (1 - cg) }
132
+ const rear = { x: num("REAR", "TRACK") / 2, y: num("REAR", "BASEY"), z: -wheelbase * cg }
133
+ return [
134
+ [front.x, front.y, front.z],
135
+ [-front.x, front.y, front.z],
136
+ [rear.x, rear.y, rear.z],
137
+ [-rear.x, rear.y, rear.z],
138
+ ]
139
+ }
140
+
141
+ /**
142
+ * One file of a car's data, from `<carDir>/data/` when the car is unpacked and
143
+ * from its `data.acd` otherwise; undefined when it has neither or the file is
144
+ * not in them. The archive's key is the folder's name, so `carDir` has to be
145
+ * the car's own folder, named as the game names it.
146
+ */
147
+ export async function readCarFile(carDir: string, name: string): Promise<Buffer | undefined> {
148
+ const unpacked = await readFile(join(carDir, "data", name)).catch(() => undefined)
149
+ if (unpacked !== undefined) return unpacked
150
+ const acd = await readFile(join(carDir, "data.acd")).catch(() => undefined)
151
+ return acd ? readAcd(acd, basename(carDir)).get(name) : undefined
152
+ }
153
+
154
+ /**
155
+ * A car's wheel offsets from `<cars>/<model>`, unpacked `data/` first, then
156
+ * `data.acd`; undefined when the car has neither or they can't be read.
157
+ */
158
+ export async function carWheels(carsDir: string, model: string): Promise<WheelOffsets | undefined> {
159
+ const ini = await readCarFile(join(carsDir, model), "suspensions.ini")
160
+ return ini ? wheelsFromSuspension(ini.toString("latin1")) : undefined
161
+ }
162
+
163
+ /**
164
+ * Where the driver's eyes are, from `car.ini`'s `[GRAPHICS] DRIVEREYES`: the
165
+ * cockpit camera's position, in the frame of the car's 3D model rather than of
166
+ * its physics, so `[BASIC] GRAPHICS_OFFSET` does not apply to it. +x is to the
167
+ * car's left, +y up, +z forward, as in the model.
168
+ */
169
+ export function driverEyes(carIni: string): Vec3 {
170
+ const raw = parseIni(carIni).get("GRAPHICS")?.get("DRIVEREYES")
171
+ const v = (raw ?? "").split(",").map((n) => Number(n.trim()))
172
+ if (v.length !== 3 || !v.every(Number.isFinite)) {
173
+ throw new Error(
174
+ `car.ini has no usable GRAPHICS.DRIVEREYES${raw === undefined ? "" : `: ${JSON.stringify(raw)}`}`,
175
+ )
176
+ }
177
+ return [v[0]!, v[1]!, v[2]!]
178
+ }
179
+
180
+ /** A car's driver's eyes from `<cars>/<model>`; undefined when its data can't be read. */
181
+ export async function carDriverEyes(carsDir: string, model: string): Promise<Vec3 | undefined> {
182
+ const ini = await readCarFile(join(carsDir, model), "car.ini")
183
+ return ini ? driverEyes(ini.toString("latin1")) : undefined
184
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export {
2
+ acdKey,
3
+ type CarFiles,
4
+ carDriverEyes,
5
+ carWheels,
6
+ driverEyes,
7
+ parseIni,
8
+ readAcd,
9
+ readCarFile,
10
+ type Vec3,
11
+ type WheelOffsets,
12
+ wheelsFromSuspension,
13
+ } from "./cardata.js"