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 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 { execSync } from 'child_process';
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(` Game directory updated to: ${newDir}`);
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(`\n Swapping ${target.item.name} for ${source.item.name}...`);
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
- 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(`\n Executing advanced Python offset-shifter...`);
88
- execSync(cmd, { stdio: 'inherit' });
89
- console.log(' SUCCESS: Visual Swap complete! please restart your game and view your new item!');
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(` Failed: ${e.message}`);
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(' No backups found.');
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(` Restoring ${original}...`);
127
+ console.log(`Restoring ${original}...`);
112
128
  fs.copyFileSync(bakPath, originalPath);
113
129
  fs.unlinkSync(bakPath);
114
130
  }
115
- console.log(' SUCCESS: All backups restored!');
131
+ console.log('SUCCESS: All backups restored.');
116
132
  }
117
133
  }
118
134
  catch (e) {
119
- console.error(` Failed: ${e.message}`);
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(` No items found matching "${searchTerm}".`);
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(` Could not resolve "${owned}".`);
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(` Found ${currentCandidates.length} potential matches.`);
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(` Patching ${upkPath}...`);
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(` Auto-selected Export[${exportIndex}] (Size: ${upk.exports[exportIndex].serialSize} bytes)`);
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(' SUCCESS: Patch complete!');
213
+ console.log('SUCCESS: Patch complete.');
198
214
  }
199
215
  catch (error) {
200
- console.error(' CRITICAL FAILURE:', error.message);
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(` Creating backup: ${path.basename(backupPath)}`);
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(` Error: CookedPCConsole directory not found at: ${cookedDir}`);
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(` Error: Could not resolve item "${options.owned}" in ${cookedDir}.`);
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('\n Exiting RLItemMod. See you next time!');
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('\n Goodbye!');
279
+ console.log('\nGoodbye!');
264
280
  }
265
281
  else {
266
- console.error('\n An unexpected error occurred:', e.message);
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.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": "./dist/index.js"
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
- }