rl-item-mod 1.0.0 → 1.0.2
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/dist/index.js +42 -26
- package/package.json +3 -3
- package/dist/scratch/ts_debug_exports.js +0 -33
- package/dist/swapper.js +0 -81
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import { UPKFile } from './upk.js';
|
|
8
8
|
import { resolvePackagePath, searchAssets } from './assets.js';
|
|
@@ -24,7 +24,7 @@ async function runInteractiveWizard() {
|
|
|
24
24
|
while (active) {
|
|
25
25
|
console.clear();
|
|
26
26
|
console.log('Welcome to RLItemMod, What would you like to do?');
|
|
27
|
-
console.log('
|
|
27
|
+
console.log('-------------------------------------------------');
|
|
28
28
|
const { action } = await inquirer.prompt([{
|
|
29
29
|
type: 'rawlist',
|
|
30
30
|
name: 'action',
|
|
@@ -49,7 +49,7 @@ async function runInteractiveWizard() {
|
|
|
49
49
|
default: global.COOKED_DIR || DEFAULT_COOKED_DIR
|
|
50
50
|
}]);
|
|
51
51
|
global.COOKED_DIR = newDir;
|
|
52
|
-
console.log(`
|
|
52
|
+
console.log(`Game directory updated to: ${newDir}`);
|
|
53
53
|
await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
@@ -78,18 +78,34 @@ async function runInteractiveWizard() {
|
|
|
78
78
|
await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
|
|
79
79
|
continue;
|
|
80
80
|
}
|
|
81
|
-
console.log(`\
|
|
81
|
+
console.log(`\nSwapping ${target.item.name} for ${source.item.name}...`);
|
|
82
82
|
const targetUpk = new UPKFile(target.path);
|
|
83
83
|
try {
|
|
84
84
|
const pythonScriptPath = path.resolve(__dirname, '../python/rl_asset_swapper.py');
|
|
85
85
|
backupFile(target.path);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
console.log(`\nExecuting Python offset-shifter...`);
|
|
87
|
+
const result = spawnSync('python', [
|
|
88
|
+
pythonScriptPath,
|
|
89
|
+
'--no-gui',
|
|
90
|
+
'--target', target.item.name,
|
|
91
|
+
'--donor', source.item.name,
|
|
92
|
+
'--no-preserve-header-offsets',
|
|
93
|
+
'--overwrite',
|
|
94
|
+
'--donor-dir', cookedDir,
|
|
95
|
+
'--output-dir', cookedDir
|
|
96
|
+
], { stdio: ['inherit', 'inherit', 'pipe'], encoding: 'utf8' });
|
|
97
|
+
if (result.stderr && result.stderr.trim()) {
|
|
98
|
+
console.error('\n--- Python Error Output ---');
|
|
99
|
+
console.error(result.stderr.trim());
|
|
100
|
+
console.error('---------------------------');
|
|
101
|
+
}
|
|
102
|
+
if (result.status !== 0) {
|
|
103
|
+
throw new Error(`Python script exited with code ${result.status}`);
|
|
104
|
+
}
|
|
105
|
+
console.log('SUCCESS: Visual Swap complete! Restart your game to see your new item.');
|
|
90
106
|
}
|
|
91
107
|
catch (e) {
|
|
92
|
-
console.error(`
|
|
108
|
+
console.error(`Failed: ${e.message}`);
|
|
93
109
|
}
|
|
94
110
|
await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
|
|
95
111
|
continue;
|
|
@@ -101,22 +117,22 @@ async function runInteractiveWizard() {
|
|
|
101
117
|
const files = fs.readdirSync(cookedDir);
|
|
102
118
|
const backups = files.filter(f => f.endsWith('.bak'));
|
|
103
119
|
if (backups.length === 0) {
|
|
104
|
-
console.log('
|
|
120
|
+
console.log('No backups found.');
|
|
105
121
|
}
|
|
106
122
|
else {
|
|
107
123
|
for (const bak of backups) {
|
|
108
124
|
const original = bak.replace('.bak', '');
|
|
109
125
|
const bakPath = path.join(cookedDir, bak);
|
|
110
126
|
const originalPath = path.join(cookedDir, original);
|
|
111
|
-
console.log(`
|
|
127
|
+
console.log(`Restoring ${original}...`);
|
|
112
128
|
fs.copyFileSync(bakPath, originalPath);
|
|
113
129
|
fs.unlinkSync(bakPath);
|
|
114
130
|
}
|
|
115
|
-
console.log('
|
|
131
|
+
console.log('SUCCESS: All backups restored.');
|
|
116
132
|
}
|
|
117
133
|
}
|
|
118
134
|
catch (e) {
|
|
119
|
-
console.error(`
|
|
135
|
+
console.error(`Failed: ${e.message}`);
|
|
120
136
|
}
|
|
121
137
|
await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
|
|
122
138
|
continue;
|
|
@@ -134,7 +150,7 @@ async function promptForItemAndUPK(message, cookedDir) {
|
|
|
134
150
|
}]);
|
|
135
151
|
const matches = searchAssets(searchTerm);
|
|
136
152
|
if (matches.length === 0) {
|
|
137
|
-
console.error(`
|
|
153
|
+
console.error(`No items found matching "${searchTerm}".`);
|
|
138
154
|
return null;
|
|
139
155
|
}
|
|
140
156
|
const { selectedItem } = await inquirer.prompt([{
|
|
@@ -149,14 +165,14 @@ async function promptForItemAndUPK(message, cookedDir) {
|
|
|
149
165
|
const owned = selectedItem.productId.toString();
|
|
150
166
|
const result = await resolvePackagePath(owned, cookedDir);
|
|
151
167
|
if (!result) {
|
|
152
|
-
console.error(`
|
|
168
|
+
console.error(`Could not resolve "${owned}".`);
|
|
153
169
|
return null;
|
|
154
170
|
}
|
|
155
171
|
let finalUpkPath = '';
|
|
156
172
|
if ('candidates' in result) {
|
|
157
173
|
let currentCandidates = result.candidates;
|
|
158
174
|
while (currentCandidates.length > 20) {
|
|
159
|
-
console.log(`
|
|
175
|
+
console.log(`Found ${currentCandidates.length} potential matches.`);
|
|
160
176
|
const { refine } = await inquirer.prompt([{
|
|
161
177
|
type: 'input',
|
|
162
178
|
name: 'refine',
|
|
@@ -183,27 +199,27 @@ async function promptForItemAndUPK(message, cookedDir) {
|
|
|
183
199
|
}
|
|
184
200
|
async function executePatch(upkPath, data, exportIndex) {
|
|
185
201
|
try {
|
|
186
|
-
console.log(`
|
|
202
|
+
console.log(`Patching ${upkPath}...`);
|
|
187
203
|
backupFile(upkPath);
|
|
188
204
|
const upk = new UPKFile(upkPath);
|
|
189
205
|
upk.readSummary();
|
|
190
206
|
upk.readExportMap();
|
|
191
207
|
if (exportIndex === -1) {
|
|
192
208
|
exportIndex = upk.exports.reduce((maxIdx, curr, idx, arr) => curr.serialSize > arr[maxIdx].serialSize ? idx : maxIdx, 0);
|
|
193
|
-
console.log(`
|
|
209
|
+
console.log(`Auto-selected Export[${exportIndex}] (Size: ${upk.exports[exportIndex].serialSize} bytes)`);
|
|
194
210
|
}
|
|
195
211
|
const newHex = (typeof data === 'string') ? fs.readFileSync(data) : data;
|
|
196
212
|
upk.patchExport(exportIndex, newHex);
|
|
197
|
-
console.log('
|
|
213
|
+
console.log('SUCCESS: Patch complete.');
|
|
198
214
|
}
|
|
199
215
|
catch (error) {
|
|
200
|
-
console.error('
|
|
216
|
+
console.error('CRITICAL FAILURE:', error.message);
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
219
|
function backupFile(filePath) {
|
|
204
220
|
const backupPath = `${filePath}.bak`;
|
|
205
221
|
if (!fs.existsSync(backupPath)) {
|
|
206
|
-
console.log(`
|
|
222
|
+
console.log(`Creating backup: ${path.basename(backupPath)}`);
|
|
207
223
|
fs.copyFileSync(filePath, backupPath);
|
|
208
224
|
}
|
|
209
225
|
}
|
|
@@ -234,7 +250,7 @@ program
|
|
|
234
250
|
let upkPath = options.upk;
|
|
235
251
|
let cookedDir = options.dir;
|
|
236
252
|
if (!fs.existsSync(cookedDir)) {
|
|
237
|
-
console.error(`
|
|
253
|
+
console.error(`Error: CookedPCConsole directory not found at: ${cookedDir}`);
|
|
238
254
|
console.log('Use --dir <path> to specify the correct game directory.');
|
|
239
255
|
return;
|
|
240
256
|
}
|
|
@@ -244,14 +260,14 @@ program
|
|
|
244
260
|
upkPath = res.path;
|
|
245
261
|
}
|
|
246
262
|
if (!upkPath) {
|
|
247
|
-
console.error(`
|
|
263
|
+
console.error(`Error: Could not resolve item "${options.owned}" in ${cookedDir}.`);
|
|
248
264
|
return;
|
|
249
265
|
}
|
|
250
266
|
await executePatch(upkPath, options.target, options.export ? parseInt(options.export) : -1);
|
|
251
267
|
});
|
|
252
268
|
// Handle Ctrl+C gracefully
|
|
253
269
|
process.on('SIGINT', () => {
|
|
254
|
-
console.log('\
|
|
270
|
+
console.log('\nExiting RLItemMod.');
|
|
255
271
|
process.exit(0);
|
|
256
272
|
});
|
|
257
273
|
async function runSafeWizard() {
|
|
@@ -260,10 +276,10 @@ async function runSafeWizard() {
|
|
|
260
276
|
}
|
|
261
277
|
catch (e) {
|
|
262
278
|
if (e.name === 'ExitPromptError') {
|
|
263
|
-
console.log('\
|
|
279
|
+
console.log('\nGoodbye!');
|
|
264
280
|
}
|
|
265
281
|
else {
|
|
266
|
-
console.error('\
|
|
282
|
+
console.error('\nAn unexpected error occurred:', e.message);
|
|
267
283
|
}
|
|
268
284
|
process.exit(0);
|
|
269
285
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-item-mod",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"rl-item-mod": "
|
|
8
|
+
"rl-item-mod": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"start": "ts-node index.ts",
|
|
16
|
+
"start": "ts-node src/index.ts",
|
|
17
17
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { UPKFile } from '../dist/upk.js';
|
|
2
|
-
const upk = new UPKFile('E:\\games\\rocketleague\\TAGame\\CookedPCConsole\\explosion_badaboom_SF.upk');
|
|
3
|
-
try {
|
|
4
|
-
upk.readSummary();
|
|
5
|
-
const r = upk.dataBuffer ? new (require('../dist/upk.js').BinaryReader)(upk.dataBuffer) : null;
|
|
6
|
-
if (r) {
|
|
7
|
-
r.pos = upk.summary.exportOffset;
|
|
8
|
-
for (let i = 0; i < upk.summary.exportCount; i++) {
|
|
9
|
-
const classIndex = r.readI32();
|
|
10
|
-
const superIndex = r.readI32();
|
|
11
|
-
const outerIndex = r.readI32();
|
|
12
|
-
const objectNameIndex = r.readI32();
|
|
13
|
-
const objectNameNumber = r.readI32();
|
|
14
|
-
const archetypeIndex = r.readI32();
|
|
15
|
-
const objectFlags = r.readU64();
|
|
16
|
-
const serialSize = r.readI32();
|
|
17
|
-
const serialOffset = r.readI32();
|
|
18
|
-
const exportFlags = r.readI32();
|
|
19
|
-
const netObjCount = r.readI32();
|
|
20
|
-
console.log(`Export ${i}: NetObjCount = ${netObjCount}, pos before skip: ${r.pos}`);
|
|
21
|
-
if (netObjCount < 0 || netObjCount > 1000) {
|
|
22
|
-
console.log(`Abnormal netObjCount: ${netObjCount} at export ${i}`);
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
r.skip(netObjCount * 4);
|
|
26
|
-
r.skip(16); // PackageGuid
|
|
27
|
-
r.readI32(); // PackageFlags
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch (e) {
|
|
32
|
-
console.error(e);
|
|
33
|
-
}
|
package/dist/swapper.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Implements the "Fixed Rename" logic from RLUPKTools.
|
|
3
|
-
* This renames a name table entry IN-PLACE by padding with nulls.
|
|
4
|
-
* This ensures the header size and all subsequent offsets remain identical.
|
|
5
|
-
*/
|
|
6
|
-
export class UPKSwapper {
|
|
7
|
-
upk;
|
|
8
|
-
constructor(upk) {
|
|
9
|
-
this.upk = upk;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Renames an existing name entry to a new name.
|
|
13
|
-
* The new name must be shorter than or equal to the original name's space.
|
|
14
|
-
*/
|
|
15
|
-
fixedRename(oldName, newName) {
|
|
16
|
-
if (!this.upk.summary || !this.upk.getData())
|
|
17
|
-
return false;
|
|
18
|
-
const data = this.upk.getData();
|
|
19
|
-
const nameOffset = this.upk.summary.nameOffset;
|
|
20
|
-
let pos = nameOffset;
|
|
21
|
-
for (let i = 0; i < this.upk.summary.nameCount; i++) {
|
|
22
|
-
const entryStart = pos;
|
|
23
|
-
const length = data.readInt32LE(pos);
|
|
24
|
-
pos += 4;
|
|
25
|
-
let currentName;
|
|
26
|
-
let totalBytes;
|
|
27
|
-
if (length > 0) {
|
|
28
|
-
// ANSI
|
|
29
|
-
currentName = data.toString('utf8', pos, pos + length - 1);
|
|
30
|
-
totalBytes = length;
|
|
31
|
-
pos += length;
|
|
32
|
-
}
|
|
33
|
-
else if (length < 0) {
|
|
34
|
-
// UTF-16
|
|
35
|
-
const absLen = Math.abs(length) * 2;
|
|
36
|
-
currentName = data.toString('utf16le', pos, pos + absLen - 2);
|
|
37
|
-
totalBytes = absLen;
|
|
38
|
-
pos += absLen;
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
pos += 8; // Skip flags
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
// Skip ObjectFlags (8 bytes)
|
|
45
|
-
pos += 8;
|
|
46
|
-
if (currentName === oldName) {
|
|
47
|
-
console.log(` Found name entry "${oldName}" at index ${i}. Renaming to "${newName}"...`);
|
|
48
|
-
// Prepare the new string
|
|
49
|
-
const newBuf = Buffer.alloc(totalBytes, 0);
|
|
50
|
-
if (length > 0) {
|
|
51
|
-
newBuf.write(newName, 'utf8');
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
newBuf.write(newName, 'utf16le');
|
|
55
|
-
}
|
|
56
|
-
// Write it back
|
|
57
|
-
newBuf.copy(data, entryStart + 4);
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Swaps all references of donorName to targetName in the package.
|
|
65
|
-
* This is useful for "tricking" the engine into loading one asset instead of another.
|
|
66
|
-
*/
|
|
67
|
-
swapAssetReferences(targetAssetName, donorAssetName) {
|
|
68
|
-
console.log(` Swapping references: ${targetAssetName} <-> ${donorAssetName}`);
|
|
69
|
-
// We actually want to rename the TARGET name to something else,
|
|
70
|
-
// and the DONOR name (which we might have imported) to the TARGET name.
|
|
71
|
-
// But for a simple swap where we just patch the file, we can just rename
|
|
72
|
-
// the internal package name.
|
|
73
|
-
const success = this.fixedRename(targetAssetName, donorAssetName);
|
|
74
|
-
if (success) {
|
|
75
|
-
console.log(` Successfully swapped ${targetAssetName} for ${donorAssetName} references.`);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
console.warn(` Failed to find name entry for ${targetAssetName}.`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|