rl-item-mod 1.0.0 → 1.0.1
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 +23 -23
- 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
|
@@ -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,18 @@ 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
86
|
const cmd = `python "${pythonScriptPath}" --no-gui --target "${target.item.name}" --donor "${source.item.name}" --no-preserve-header-offsets --overwrite --donor-dir "${cookedDir}" --output-dir "${cookedDir}"`;
|
|
87
|
-
console.log(`\
|
|
87
|
+
console.log(`\nExecuting Python offset-shifter...`);
|
|
88
88
|
execSync(cmd, { stdio: 'inherit' });
|
|
89
|
-
console.log('
|
|
89
|
+
console.log('SUCCESS: Visual Swap complete! Restart your game to see your new item.');
|
|
90
90
|
}
|
|
91
91
|
catch (e) {
|
|
92
|
-
console.error(`
|
|
92
|
+
console.error(`Failed: ${e.message}`);
|
|
93
93
|
}
|
|
94
94
|
await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
|
|
95
95
|
continue;
|
|
@@ -101,22 +101,22 @@ async function runInteractiveWizard() {
|
|
|
101
101
|
const files = fs.readdirSync(cookedDir);
|
|
102
102
|
const backups = files.filter(f => f.endsWith('.bak'));
|
|
103
103
|
if (backups.length === 0) {
|
|
104
|
-
console.log('
|
|
104
|
+
console.log('No backups found.');
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
107
|
for (const bak of backups) {
|
|
108
108
|
const original = bak.replace('.bak', '');
|
|
109
109
|
const bakPath = path.join(cookedDir, bak);
|
|
110
110
|
const originalPath = path.join(cookedDir, original);
|
|
111
|
-
console.log(`
|
|
111
|
+
console.log(`Restoring ${original}...`);
|
|
112
112
|
fs.copyFileSync(bakPath, originalPath);
|
|
113
113
|
fs.unlinkSync(bakPath);
|
|
114
114
|
}
|
|
115
|
-
console.log('
|
|
115
|
+
console.log('SUCCESS: All backups restored.');
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
catch (e) {
|
|
119
|
-
console.error(`
|
|
119
|
+
console.error(`Failed: ${e.message}`);
|
|
120
120
|
}
|
|
121
121
|
await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
|
|
122
122
|
continue;
|
|
@@ -134,7 +134,7 @@ async function promptForItemAndUPK(message, cookedDir) {
|
|
|
134
134
|
}]);
|
|
135
135
|
const matches = searchAssets(searchTerm);
|
|
136
136
|
if (matches.length === 0) {
|
|
137
|
-
console.error(`
|
|
137
|
+
console.error(`No items found matching "${searchTerm}".`);
|
|
138
138
|
return null;
|
|
139
139
|
}
|
|
140
140
|
const { selectedItem } = await inquirer.prompt([{
|
|
@@ -149,14 +149,14 @@ async function promptForItemAndUPK(message, cookedDir) {
|
|
|
149
149
|
const owned = selectedItem.productId.toString();
|
|
150
150
|
const result = await resolvePackagePath(owned, cookedDir);
|
|
151
151
|
if (!result) {
|
|
152
|
-
console.error(`
|
|
152
|
+
console.error(`Could not resolve "${owned}".`);
|
|
153
153
|
return null;
|
|
154
154
|
}
|
|
155
155
|
let finalUpkPath = '';
|
|
156
156
|
if ('candidates' in result) {
|
|
157
157
|
let currentCandidates = result.candidates;
|
|
158
158
|
while (currentCandidates.length > 20) {
|
|
159
|
-
console.log(`
|
|
159
|
+
console.log(`Found ${currentCandidates.length} potential matches.`);
|
|
160
160
|
const { refine } = await inquirer.prompt([{
|
|
161
161
|
type: 'input',
|
|
162
162
|
name: 'refine',
|
|
@@ -183,27 +183,27 @@ async function promptForItemAndUPK(message, cookedDir) {
|
|
|
183
183
|
}
|
|
184
184
|
async function executePatch(upkPath, data, exportIndex) {
|
|
185
185
|
try {
|
|
186
|
-
console.log(`
|
|
186
|
+
console.log(`Patching ${upkPath}...`);
|
|
187
187
|
backupFile(upkPath);
|
|
188
188
|
const upk = new UPKFile(upkPath);
|
|
189
189
|
upk.readSummary();
|
|
190
190
|
upk.readExportMap();
|
|
191
191
|
if (exportIndex === -1) {
|
|
192
192
|
exportIndex = upk.exports.reduce((maxIdx, curr, idx, arr) => curr.serialSize > arr[maxIdx].serialSize ? idx : maxIdx, 0);
|
|
193
|
-
console.log(`
|
|
193
|
+
console.log(`Auto-selected Export[${exportIndex}] (Size: ${upk.exports[exportIndex].serialSize} bytes)`);
|
|
194
194
|
}
|
|
195
195
|
const newHex = (typeof data === 'string') ? fs.readFileSync(data) : data;
|
|
196
196
|
upk.patchExport(exportIndex, newHex);
|
|
197
|
-
console.log('
|
|
197
|
+
console.log('SUCCESS: Patch complete.');
|
|
198
198
|
}
|
|
199
199
|
catch (error) {
|
|
200
|
-
console.error('
|
|
200
|
+
console.error('CRITICAL FAILURE:', error.message);
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
function backupFile(filePath) {
|
|
204
204
|
const backupPath = `${filePath}.bak`;
|
|
205
205
|
if (!fs.existsSync(backupPath)) {
|
|
206
|
-
console.log(`
|
|
206
|
+
console.log(`Creating backup: ${path.basename(backupPath)}`);
|
|
207
207
|
fs.copyFileSync(filePath, backupPath);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
@@ -234,7 +234,7 @@ program
|
|
|
234
234
|
let upkPath = options.upk;
|
|
235
235
|
let cookedDir = options.dir;
|
|
236
236
|
if (!fs.existsSync(cookedDir)) {
|
|
237
|
-
console.error(`
|
|
237
|
+
console.error(`Error: CookedPCConsole directory not found at: ${cookedDir}`);
|
|
238
238
|
console.log('Use --dir <path> to specify the correct game directory.');
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
@@ -244,14 +244,14 @@ program
|
|
|
244
244
|
upkPath = res.path;
|
|
245
245
|
}
|
|
246
246
|
if (!upkPath) {
|
|
247
|
-
console.error(`
|
|
247
|
+
console.error(`Error: Could not resolve item "${options.owned}" in ${cookedDir}.`);
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
250
|
await executePatch(upkPath, options.target, options.export ? parseInt(options.export) : -1);
|
|
251
251
|
});
|
|
252
252
|
// Handle Ctrl+C gracefully
|
|
253
253
|
process.on('SIGINT', () => {
|
|
254
|
-
console.log('\
|
|
254
|
+
console.log('\nExiting RLItemMod.');
|
|
255
255
|
process.exit(0);
|
|
256
256
|
});
|
|
257
257
|
async function runSafeWizard() {
|
|
@@ -260,10 +260,10 @@ async function runSafeWizard() {
|
|
|
260
260
|
}
|
|
261
261
|
catch (e) {
|
|
262
262
|
if (e.name === 'ExitPromptError') {
|
|
263
|
-
console.log('\
|
|
263
|
+
console.log('\nGoodbye!');
|
|
264
264
|
}
|
|
265
265
|
else {
|
|
266
|
-
console.error('\
|
|
266
|
+
console.error('\nAn unexpected error occurred:', e.message);
|
|
267
267
|
}
|
|
268
268
|
process.exit(0);
|
|
269
269
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-item-mod",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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
|
-
}
|