rl-item-mod 1.0.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/README.md +59 -0
- package/dist/assets.js +110 -0
- package/dist/index.js +277 -0
- package/dist/scratch/ts_debug_exports.js +33 -0
- package/dist/swapper.js +81 -0
- package/dist/upk.js +376 -0
- package/package.json +40 -0
- package/python/items.json +82850 -0
- package/python/keys.txt +1049 -0
- package/python/rl_asset_swapper.py +991 -0
- package/python/rl_upk_editor.py +3859 -0
package/dist/upk.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as zlib from 'zlib';
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class DecryptionProvider {
|
|
8
|
+
static keys = [];
|
|
9
|
+
static loadKeys() {
|
|
10
|
+
if (this.keys.length > 0)
|
|
11
|
+
return;
|
|
12
|
+
const keysPath = path.join(__dirname, '../python/keys.txt');
|
|
13
|
+
if (!fs.existsSync(keysPath)) {
|
|
14
|
+
console.warn(" Warning: keys.txt not found. Encrypted files will fail.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const lines = fs.readFileSync(keysPath, 'utf8').split('\n');
|
|
18
|
+
this.keys = lines.map(l => Buffer.from(l.trim(), 'base64')).filter(b => b.length === 32);
|
|
19
|
+
}
|
|
20
|
+
static decryptECB(data, key) {
|
|
21
|
+
const decipher = crypto.createDecipheriv('aes-256-ecb', key, null);
|
|
22
|
+
decipher.setAutoPadding(false);
|
|
23
|
+
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
24
|
+
}
|
|
25
|
+
static encryptECB(data, key) {
|
|
26
|
+
const cipher = crypto.createCipheriv('aes-256-ecb', key, null);
|
|
27
|
+
cipher.setAutoPadding(false);
|
|
28
|
+
return Buffer.concat([cipher.update(data), cipher.final()]);
|
|
29
|
+
}
|
|
30
|
+
static findKey(encryptedHeader, firstPlainByte) {
|
|
31
|
+
this.loadKeys();
|
|
32
|
+
for (const key of this.keys) {
|
|
33
|
+
try {
|
|
34
|
+
const decrypted = this.decryptECB(encryptedHeader.subarray(0, 16), key);
|
|
35
|
+
// Check if the decrypted content makes sense.
|
|
36
|
+
// Usually the first entry in the name table is an FString.
|
|
37
|
+
// If it's a valid string length, it's likely the right key.
|
|
38
|
+
const len = decrypted.readInt32LE(0);
|
|
39
|
+
if (Math.abs(len) < 1000 && Math.abs(len) > 0) {
|
|
40
|
+
return key;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (e) { }
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class BinaryReader {
|
|
49
|
+
buffer;
|
|
50
|
+
pos = 0;
|
|
51
|
+
constructor(buffer) {
|
|
52
|
+
this.buffer = buffer;
|
|
53
|
+
}
|
|
54
|
+
readI32() { const v = this.buffer.readInt32LE(this.pos); this.pos += 4; return v; }
|
|
55
|
+
readU32() { const v = this.buffer.readUInt32LE(this.pos); this.pos += 4; return v; }
|
|
56
|
+
readI64() { const v = this.buffer.readBigInt64LE(this.pos); this.pos += 8; return v; }
|
|
57
|
+
readU64() { const v = this.buffer.readBigUint64LE(this.pos); this.pos += 8; return v; }
|
|
58
|
+
readFString() {
|
|
59
|
+
const len = this.readI32();
|
|
60
|
+
if (len === 0)
|
|
61
|
+
return "";
|
|
62
|
+
if (len > 0) {
|
|
63
|
+
const s = this.buffer.toString('utf8', this.pos, this.pos + len - 1);
|
|
64
|
+
this.pos += len;
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const absLen = Math.abs(len) * 2;
|
|
69
|
+
const s = this.buffer.toString('utf16le', this.pos, this.pos + absLen - 2);
|
|
70
|
+
this.pos += absLen;
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
skip(n) { this.pos += n; }
|
|
75
|
+
}
|
|
76
|
+
export class UPKFile {
|
|
77
|
+
filePath;
|
|
78
|
+
summary = null;
|
|
79
|
+
exports = [];
|
|
80
|
+
dataBuffer = null;
|
|
81
|
+
decryptionKey = null;
|
|
82
|
+
constructor(filePath) {
|
|
83
|
+
this.filePath = filePath;
|
|
84
|
+
}
|
|
85
|
+
getData() {
|
|
86
|
+
return this.dataBuffer;
|
|
87
|
+
}
|
|
88
|
+
readBytes(pos, size) {
|
|
89
|
+
if (!this.dataBuffer) {
|
|
90
|
+
const buffer = Buffer.alloc(size);
|
|
91
|
+
const fd = fs.openSync(this.filePath, 'r');
|
|
92
|
+
try {
|
|
93
|
+
fs.readSync(fd, buffer, 0, size, pos);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
fs.closeSync(fd);
|
|
97
|
+
}
|
|
98
|
+
return buffer;
|
|
99
|
+
}
|
|
100
|
+
return this.dataBuffer.subarray(pos, pos + size);
|
|
101
|
+
}
|
|
102
|
+
readSummary() {
|
|
103
|
+
const initialBuffer = this.readBytes(0, 4096);
|
|
104
|
+
const r = new BinaryReader(initialBuffer);
|
|
105
|
+
const tag = r.readU32();
|
|
106
|
+
if (tag !== 0x9E2A83C1)
|
|
107
|
+
throw new Error('Invalid UPK Tag');
|
|
108
|
+
const version = r.readU32();
|
|
109
|
+
const fileVersion = version & 0xFFFF;
|
|
110
|
+
const licenseeVersion = version >> 16;
|
|
111
|
+
const headerSize = r.readI32();
|
|
112
|
+
const folderName = r.readFString();
|
|
113
|
+
const packageFlags = r.readU32();
|
|
114
|
+
const nameCount = r.readI32();
|
|
115
|
+
const nameOffset = r.readI32();
|
|
116
|
+
const exportCount = r.readI32();
|
|
117
|
+
const exportOffset = r.readI32();
|
|
118
|
+
const importCount = r.readI32();
|
|
119
|
+
const importOffset = r.readI32();
|
|
120
|
+
const dependsOffset = r.readI32();
|
|
121
|
+
// Rocket League encrypts the metadata of all compressed packages
|
|
122
|
+
const isCompressed = (packageFlags & 0x02000000) !== 0;
|
|
123
|
+
const isEncrypted = isCompressed || (packageFlags & 0x01000000) !== 0;
|
|
124
|
+
let workingHeader = Buffer.alloc(headerSize);
|
|
125
|
+
if (isEncrypted) {
|
|
126
|
+
console.log(` Package is encrypted. Searching for key...`);
|
|
127
|
+
const encryptedHeader = Buffer.alloc(headerSize);
|
|
128
|
+
const fd = fs.openSync(this.filePath, 'r');
|
|
129
|
+
fs.readSync(fd, encryptedHeader, 0, headerSize, 0);
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
let encryptedPart = encryptedHeader.subarray(nameOffset);
|
|
132
|
+
const remainder = encryptedPart.length % 16;
|
|
133
|
+
if (remainder !== 0) {
|
|
134
|
+
const padded = Buffer.alloc(encryptedPart.length + (16 - remainder));
|
|
135
|
+
encryptedPart.copy(padded);
|
|
136
|
+
encryptedPart = padded;
|
|
137
|
+
}
|
|
138
|
+
const key = DecryptionProvider.findKey(encryptedPart, 0);
|
|
139
|
+
if (!key)
|
|
140
|
+
throw new Error("Could not find a valid decryption key for this package.");
|
|
141
|
+
console.log(` Decryption key found: ${key.toString('base64').substring(0, 8)}...`);
|
|
142
|
+
this.decryptionKey = key;
|
|
143
|
+
const decryptedPart = DecryptionProvider.decryptECB(encryptedPart, key);
|
|
144
|
+
encryptedHeader.copy(workingHeader, 0, 0, nameOffset);
|
|
145
|
+
decryptedPart.copy(workingHeader, nameOffset);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const fd = fs.openSync(this.filePath, 'r');
|
|
149
|
+
fs.readSync(fd, workingHeader, 0, headerSize, 0);
|
|
150
|
+
fs.closeSync(fd);
|
|
151
|
+
}
|
|
152
|
+
// Use workingHeader for deeper parsing
|
|
153
|
+
const hr = new BinaryReader(workingHeader);
|
|
154
|
+
hr.pos = r.pos; // Sync positions
|
|
155
|
+
hr.skip(16); // ImportExportGuidsOffset to ThumbnailTableOffset
|
|
156
|
+
hr.skip(16); // Guid
|
|
157
|
+
const genCount = hr.readI32();
|
|
158
|
+
hr.skip(genCount * 12);
|
|
159
|
+
hr.skip(8); // EngineVersion, CookerVersion
|
|
160
|
+
const compressionFlags = hr.readU32();
|
|
161
|
+
// Standard UE3 chunks
|
|
162
|
+
const standardChunks = [];
|
|
163
|
+
const standardChunkCount = hr.readI32();
|
|
164
|
+
for (let i = 0; i < standardChunkCount; i++) {
|
|
165
|
+
standardChunks.push({
|
|
166
|
+
uncompressedOffset: hr.readI32(),
|
|
167
|
+
uncompressedSize: hr.readI32(),
|
|
168
|
+
compressedOffset: hr.readI32(),
|
|
169
|
+
compressedSize: hr.readI32()
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Rocket League Metadata
|
|
173
|
+
hr.readI32(); // Unknown
|
|
174
|
+
const stringCount = hr.readI32();
|
|
175
|
+
for (let i = 0; i < stringCount; i++)
|
|
176
|
+
hr.readFString();
|
|
177
|
+
const texAllocCount = hr.readI32();
|
|
178
|
+
for (let i = 0; i < texAllocCount; i++) {
|
|
179
|
+
hr.skip(20);
|
|
180
|
+
const subCount = hr.readI32();
|
|
181
|
+
hr.skip(subCount * 4);
|
|
182
|
+
}
|
|
183
|
+
// File Compression Metadata
|
|
184
|
+
const garbageSize = hr.readI32();
|
|
185
|
+
const rlChunksOffset = hr.readI32();
|
|
186
|
+
const lastBlockSize = hr.readI32();
|
|
187
|
+
let chunks = standardChunks;
|
|
188
|
+
if (rlChunksOffset > 0) {
|
|
189
|
+
const cr = new BinaryReader(workingHeader);
|
|
190
|
+
cr.pos = nameOffset + rlChunksOffset;
|
|
191
|
+
const rlChunkCount = cr.readI32();
|
|
192
|
+
console.log(` RL Chunk Table found at 0x${cr.pos.toString(16)}. Count: ${rlChunkCount}`);
|
|
193
|
+
if (rlChunkCount > 0 && rlChunkCount < 10000) {
|
|
194
|
+
chunks = [];
|
|
195
|
+
for (let i = 0; i < rlChunkCount; i++) {
|
|
196
|
+
chunks.push({
|
|
197
|
+
uncompressedOffset: Number(cr.readI64()),
|
|
198
|
+
uncompressedSize: cr.readI32(),
|
|
199
|
+
compressedOffset: Number(cr.readI64()),
|
|
200
|
+
compressedSize: cr.readI32()
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this.summary = {
|
|
206
|
+
tag, fileVersion, licenseeVersion, headerSize, folderName,
|
|
207
|
+
packageFlags, nameCount, nameOffset, exportCount, exportOffset,
|
|
208
|
+
importCount, importOffset, dependsOffset,
|
|
209
|
+
compressionFlags, compressedChunks: chunks
|
|
210
|
+
};
|
|
211
|
+
if (isCompressed) {
|
|
212
|
+
this.decompress(workingHeader);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
this.dataBuffer = fs.readFileSync(this.filePath);
|
|
216
|
+
if (isEncrypted) {
|
|
217
|
+
workingHeader.copy(this.dataBuffer, 0, 0, headerSize);
|
|
218
|
+
this.summary.packageFlags &= ~0x01000000;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
decompress(headerBuffer) {
|
|
223
|
+
if (!this.summary)
|
|
224
|
+
return;
|
|
225
|
+
if (this.summary.compressedChunks.length === 0) {
|
|
226
|
+
console.warn(" Warning: Package marked as compressed but no chunks found.");
|
|
227
|
+
this.dataBuffer = fs.readFileSync(this.filePath);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
console.log(` Decompressing ${this.summary.compressedChunks.length} chunks...`);
|
|
231
|
+
const fd = fs.openSync(this.filePath, 'r');
|
|
232
|
+
let uncompressedBuffers = [headerBuffer];
|
|
233
|
+
for (const chunk of this.summary.compressedChunks) {
|
|
234
|
+
const chunkPayload = Buffer.alloc(chunk.compressedSize);
|
|
235
|
+
fs.readSync(fd, chunkPayload, 0, chunk.compressedSize, chunk.compressedOffset);
|
|
236
|
+
let payloadPos = 0;
|
|
237
|
+
const magic = chunkPayload.readUInt32LE(payloadPos);
|
|
238
|
+
payloadPos += 4;
|
|
239
|
+
if (magic !== 0x9E2A83C1) {
|
|
240
|
+
throw new Error(`Invalid chunk magic at offset ${chunk.compressedOffset}`);
|
|
241
|
+
}
|
|
242
|
+
const blockSize = chunkPayload.readInt32LE(payloadPos);
|
|
243
|
+
payloadPos += 4;
|
|
244
|
+
const totalCompressedSize = chunkPayload.readInt32LE(payloadPos);
|
|
245
|
+
payloadPos += 4;
|
|
246
|
+
const totalUncompressedSize = chunkPayload.readInt32LE(payloadPos);
|
|
247
|
+
payloadPos += 4;
|
|
248
|
+
const blocks = [];
|
|
249
|
+
let sumUncomp = 0;
|
|
250
|
+
while (sumUncomp < totalUncompressedSize) {
|
|
251
|
+
const compSize = chunkPayload.readInt32LE(payloadPos);
|
|
252
|
+
payloadPos += 4;
|
|
253
|
+
const uncompSize = chunkPayload.readInt32LE(payloadPos);
|
|
254
|
+
payloadPos += 4;
|
|
255
|
+
blocks.push({ compSize, uncompSize });
|
|
256
|
+
sumUncomp += uncompSize;
|
|
257
|
+
}
|
|
258
|
+
for (const block of blocks) {
|
|
259
|
+
const compressedBlock = chunkPayload.subarray(payloadPos, payloadPos + block.compSize);
|
|
260
|
+
payloadPos += block.compSize;
|
|
261
|
+
try {
|
|
262
|
+
const inflated = zlib.inflateSync(compressedBlock);
|
|
263
|
+
uncompressedBuffers.push(inflated);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
throw new Error(`Block decompression failed in chunk at ${chunk.compressedOffset}: ${e}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
fs.closeSync(fd);
|
|
271
|
+
this.dataBuffer = Buffer.concat(uncompressedBuffers);
|
|
272
|
+
this.summary.packageFlags &= ~0x02000000;
|
|
273
|
+
console.log(` Decompression complete. Final size: ${this.dataBuffer.length} bytes.`);
|
|
274
|
+
}
|
|
275
|
+
readFString(buffer, pos) {
|
|
276
|
+
const length = buffer.readInt32LE(pos);
|
|
277
|
+
if (length === 0)
|
|
278
|
+
return { str: '', bytesRead: 4 };
|
|
279
|
+
if (length > 0) {
|
|
280
|
+
const str = buffer.toString('utf8', pos + 4, pos + 4 + length - 1);
|
|
281
|
+
return { str, bytesRead: 4 + length };
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const absLen = Math.abs(length) * 2;
|
|
285
|
+
const str = buffer.toString('utf16le', pos + 4, pos + 4 + absLen - 2);
|
|
286
|
+
return { str, bytesRead: 4 + absLen };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
readExportMap() {
|
|
290
|
+
if (!this.summary)
|
|
291
|
+
return;
|
|
292
|
+
if (!this.dataBuffer) {
|
|
293
|
+
console.error(" Error: Attempted to read ExportMap before data buffer was loaded.");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const r = new BinaryReader(this.dataBuffer);
|
|
297
|
+
r.pos = this.summary.exportOffset;
|
|
298
|
+
this.exports = [];
|
|
299
|
+
console.log(` Parsing Export Map (${this.summary.exportCount} entries)...`);
|
|
300
|
+
for (let i = 0; i < this.summary.exportCount; i++) {
|
|
301
|
+
const classIndex = r.readI32();
|
|
302
|
+
const superIndex = r.readI32();
|
|
303
|
+
const outerIndex = r.readI32();
|
|
304
|
+
const objectNameIndex = r.readI32();
|
|
305
|
+
const objectNameNumber = r.readI32(); // FName instance number
|
|
306
|
+
const archetypeIndex = r.readI32();
|
|
307
|
+
const objectFlags = r.readU64();
|
|
308
|
+
const serialSize = r.readI32();
|
|
309
|
+
const serialOffset = Number(r.readI64());
|
|
310
|
+
const exportFlags = r.readI32();
|
|
311
|
+
// Rocket League extras
|
|
312
|
+
const netObjCount = r.readI32();
|
|
313
|
+
r.skip(netObjCount * 4);
|
|
314
|
+
r.skip(16); // PackageGuid
|
|
315
|
+
r.readI32(); // PackageFlags
|
|
316
|
+
this.exports.push({
|
|
317
|
+
classIndex, superIndex, outerIndex,
|
|
318
|
+
objectNameIndex, archetypeIndex,
|
|
319
|
+
objectFlags, serialSize, serialOffset, exportFlags
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
extractExport(index) {
|
|
324
|
+
if (this.exports.length === 0)
|
|
325
|
+
throw new Error('Export table is empty. Did readExportMap fail?');
|
|
326
|
+
const exp = this.exports[index];
|
|
327
|
+
if (!exp)
|
|
328
|
+
throw new Error(`Export index ${index} out of bounds (Table size: ${this.exports.length})`);
|
|
329
|
+
return this.readBytes(exp.serialOffset, exp.serialSize);
|
|
330
|
+
}
|
|
331
|
+
save() {
|
|
332
|
+
if (!this.dataBuffer || !this.summary)
|
|
333
|
+
throw new Error('File not loaded');
|
|
334
|
+
// If we have a key, we MUST re-encrypt the header before saving
|
|
335
|
+
if (this.decryptionKey) {
|
|
336
|
+
console.log(` Re-encrypting header for ${path.basename(this.filePath)}...`);
|
|
337
|
+
const headerSize = this.summary.headerSize;
|
|
338
|
+
const nameOffset = this.summary.nameOffset;
|
|
339
|
+
let plainPart = this.dataBuffer.subarray(nameOffset, headerSize);
|
|
340
|
+
const remainder = plainPart.length % 16;
|
|
341
|
+
if (remainder !== 0) {
|
|
342
|
+
const padded = Buffer.alloc(plainPart.length + (16 - remainder));
|
|
343
|
+
plainPart.copy(padded);
|
|
344
|
+
plainPart = padded;
|
|
345
|
+
}
|
|
346
|
+
const encryptedPart = DecryptionProvider.encryptECB(plainPart, this.decryptionKey);
|
|
347
|
+
const finalSaveBuffer = Buffer.from(this.dataBuffer);
|
|
348
|
+
encryptedPart.copy(finalSaveBuffer, nameOffset, 0, headerSize - nameOffset);
|
|
349
|
+
// Set the encrypted flag back
|
|
350
|
+
finalSaveBuffer.writeUInt32LE(this.summary.packageFlags | 0x01000000, this.findPackageFlagsOffset());
|
|
351
|
+
fs.writeFileSync(this.filePath, finalSaveBuffer);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
fs.writeFileSync(this.filePath, this.dataBuffer);
|
|
355
|
+
}
|
|
356
|
+
console.log(` Patched and Saved: ${path.basename(this.filePath)}`);
|
|
357
|
+
}
|
|
358
|
+
patchExport(index, newContent) {
|
|
359
|
+
const exp = this.exports[index];
|
|
360
|
+
if (!exp)
|
|
361
|
+
throw new Error('Export index out of bounds');
|
|
362
|
+
if (!this.dataBuffer || !this.summary)
|
|
363
|
+
throw new Error('File not loaded');
|
|
364
|
+
const patchSize = Math.min(newContent.length, exp.serialSize);
|
|
365
|
+
newContent.copy(this.dataBuffer, exp.serialOffset, 0, patchSize);
|
|
366
|
+
this.save();
|
|
367
|
+
}
|
|
368
|
+
findPackageFlagsOffset() {
|
|
369
|
+
// Basic summary: tag(4), version(4), headerSize(4), folderName(var)
|
|
370
|
+
// packageFlags comes after folderName
|
|
371
|
+
// folderName is an FString
|
|
372
|
+
const folderNameLen = this.dataBuffer.readInt32LE(12);
|
|
373
|
+
const bytesRead = folderNameLen === 0 ? 4 : (folderNameLen > 0 ? 4 + folderNameLen : 4 + Math.abs(folderNameLen) * 2);
|
|
374
|
+
return 12 + bytesRead;
|
|
375
|
+
}
|
|
376
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rl-item-mod",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A comprehensive CLI tool for safely applying visual asset swaps to Rocket League UPK files with full encryption and binary offset handling.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rl-item-mod": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"python"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"start": "ts-node index.ts",
|
|
17
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"rocket-league",
|
|
21
|
+
"upk",
|
|
22
|
+
"modding",
|
|
23
|
+
"asset-swapper"
|
|
24
|
+
],
|
|
25
|
+
"author": "RLItemMod",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"axios": "^1.16.0",
|
|
29
|
+
"commander": "^14.0.3",
|
|
30
|
+
"inquirer": "^13.4.2",
|
|
31
|
+
"string-similarity": "^4.0.4"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/inquirer": "^9.0.9",
|
|
35
|
+
"@types/node": "^25.6.0",
|
|
36
|
+
"@types/string-similarity": "^4.0.2",
|
|
37
|
+
"ts-node": "^10.9.2",
|
|
38
|
+
"typescript": "^6.0.3"
|
|
39
|
+
}
|
|
40
|
+
}
|