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 +21 -0
- package/README.md +28 -2
- package/dist/cardata.d.ts +57 -0
- package/dist/cardata.d.ts.map +1 -0
- package/dist/cardata.js +166 -0
- package/dist/cardata.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -4
- package/src/cardata.ts +184 -0
- package/src/index.ts +13 -0
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
|
-
#
|
|
1
|
+
# ac-cardata
|
|
2
2
|
|
|
3
|
-
|
|
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"}
|
package/dist/cardata.js
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|