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 ADDED
@@ -0,0 +1,59 @@
1
+ # RLItemMod
2
+
3
+ A powerful, user-friendly Node.js CLI tool for performing visual asset swaps in Rocket League.
4
+
5
+ ## Overview
6
+
7
+ `RLItemMod` provides an interactive terminal wizard that allows you to swap in-game items (e.g., swapping a standard boost for Alpha Reward). Under the hood, it seamlessly invokes the advanced `RLUPKTools` Python engine to accurately parse `.upk` encryption, perfectly expand Name Table string offsets, and rebuild the package architecture without causing game crashes.
8
+
9
+ ## Features
10
+
11
+ - **Interactive Wizard**: A beautiful command-line interface to search for and select your source and target items.
12
+ - **Python Interop**: Leverages a robust Python backend to handle complex LZO decompression, AES decryption, and binary offset shifting.
13
+ - **Automated Backups**: Automatically backs up original game assets before patching, with a one-click CLI restore feature.
14
+ - **Item Database**: Uses a built-in `items.json` database for fuzzy-searching and mapping in-game item names directly to their underlying UPK files.
15
+
16
+ ## Installation
17
+
18
+ ### Prerequisites
19
+
20
+ - Node.js (v18+)
21
+ - Python 3.8+ (must be available in your system PATH)
22
+
23
+ ### Global Install (Recommended)
24
+
25
+ ```bash
26
+ npm install -g rl-item-mod
27
+ ```
28
+
29
+ ### Local Development
30
+
31
+ ```bash
32
+ git clone https://github.com/bitsfdb/RLItemMod.git
33
+ cd RLItemMod
34
+ npm install
35
+ npm run build
36
+ npm link
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ Simply launch the interactive wizard from your terminal:
42
+
43
+ ```bash
44
+ rl-item-mod
45
+ ```
46
+
47
+ Or run directly via npx:
48
+
49
+ ```bash
50
+ npx rl-item-mod@latest
51
+ ```
52
+
53
+ ## Credits
54
+
55
+ Massive credits to [CrunchyRL/RLUPKTools](https://github.com/CrunchyRL/RLUPKTools) for making this repository possible. The advanced Python engineering for parsing and shifting Unreal Engine 3 UPK binaries was instrumental in making this project work safely.
56
+
57
+ ## License
58
+
59
+ MIT
package/dist/assets.js ADDED
@@ -0,0 +1,110 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import stringSimilarity from 'string-similarity';
4
+ import { fileURLToPath } from 'url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ // Load and transform items.json into ASSET_MAP
7
+ const ITEMS_JSON_PATH = path.join(__dirname, '../python/items.json');
8
+ let loadedAssets = [];
9
+ try {
10
+ if (fs.existsSync(ITEMS_JSON_PATH)) {
11
+ const rawData = JSON.parse(fs.readFileSync(ITEMS_JSON_PATH, 'utf8'));
12
+ if (rawData.Items) {
13
+ loadedAssets = rawData.Items.map((p) => ({
14
+ name: p["Product"],
15
+ productId: p["ID"],
16
+ packageName: p["AssetPackage"] ? p["AssetPackage"].replace('.upk', '') : '',
17
+ assetPath: p["AssetPath"],
18
+ slot: p["Slot"]
19
+ })).filter((a) => a.packageName !== '');
20
+ }
21
+ }
22
+ }
23
+ catch (e) {
24
+ console.warn(" Warning: Failed to load items.json. Using minimal fallback list.");
25
+ }
26
+ export const ASSET_MAP = loadedAssets.length > 0 ? loadedAssets : [
27
+ { name: "Standard Boost", packageName: "Standard_Boost_SF", productId: 63 },
28
+ { name: "Flamethrower", packageName: "Flamethrower_SF", productId: 36 },
29
+ { name: "(Alpha Reward) Gold Rush", packageName: "Boost_AlphaReward_SF", productId: 32 },
30
+ { name: "Octane", packageName: "Body_Octane_SF", productId: 23 },
31
+ ];
32
+ /**
33
+ * Performs a fuzzy/case-insensitive search for an item name or product ID.
34
+ * If mapping fails, it scans the provided directory for potential matches.
35
+ */
36
+ export async function resolvePackagePath(query, cookedDir) {
37
+ const q = query.toLowerCase();
38
+ // 1. Check Internal Map first
39
+ let match = ASSET_MAP.find(a => a.name.toLowerCase() === q || a.productId?.toString() === q);
40
+ if (match) {
41
+ const fullPath = path.join(cookedDir, `${match.packageName}.upk`);
42
+ if (fs.existsSync(fullPath)) {
43
+ return { path: fullPath, name: match.packageName };
44
+ }
45
+ }
46
+ // 2. Directory Scan + Keyword Search
47
+ if (!fs.existsSync(cookedDir)) {
48
+ console.warn(` Warning: CookedPCConsole directory not found at: ${cookedDir}`);
49
+ return null;
50
+ }
51
+ const files = fs.readdirSync(cookedDir)
52
+ .filter(f => f.endsWith('.upk'))
53
+ .filter(f => !f.toLowerCase().includes('t_sf') && !f.toLowerCase().includes('sf_t'));
54
+ const keywords = q.split(' ').filter(k => k.length > 2); // only words > 2 chars
55
+ // Find files containing ALL keywords
56
+ let keywordMatches = files.filter(f => {
57
+ const lowerF = f.toLowerCase();
58
+ return keywords.every(k => lowerF.includes(k));
59
+ });
60
+ if (keywordMatches.length === 1) {
61
+ return {
62
+ path: path.join(cookedDir, keywordMatches[0]),
63
+ name: keywordMatches[0].replace('.upk', '')
64
+ };
65
+ }
66
+ else if (keywordMatches.length > 1) {
67
+ return { candidates: keywordMatches };
68
+ }
69
+ // 3. String Similarity (Fuzzy Matching)
70
+ const matches = stringSimilarity.findBestMatch(q, files);
71
+ const topMatches = matches.ratings
72
+ .sort((a, b) => b.rating - a.rating)
73
+ .slice(0, 5)
74
+ .filter(m => m.rating > 0.3); // Threshold for sanity
75
+ if (topMatches.length > 0) {
76
+ // If the best match is very high, return it, otherwise return candidates
77
+ if (topMatches[0].rating > 0.8) {
78
+ return {
79
+ path: path.join(cookedDir, topMatches[0].target),
80
+ name: topMatches[0].target.replace('.upk', '')
81
+ };
82
+ }
83
+ return { candidates: topMatches.map(m => m.target) };
84
+ }
85
+ return null;
86
+ }
87
+ export function getClosestFiles(query, cookedDir) {
88
+ if (!fs.existsSync(cookedDir))
89
+ return [];
90
+ const files = fs.readdirSync(cookedDir)
91
+ .filter(f => f.endsWith('.upk'))
92
+ .filter(f => !f.toLowerCase().includes('t_sf') && !f.toLowerCase().includes('sf_t'));
93
+ const matches = stringSimilarity.findBestMatch(query.toLowerCase(), files);
94
+ return matches.ratings
95
+ .sort((a, b) => b.rating - a.rating)
96
+ .slice(0, 5)
97
+ .map(m => m.target);
98
+ }
99
+ export function searchAssets(term) {
100
+ const q = term.toLowerCase();
101
+ return ASSET_MAP.filter(a => {
102
+ const nameMatch = a.name.toLowerCase().includes(q);
103
+ const pkgMatch = a.packageName && a.packageName.toLowerCase().includes(q);
104
+ const idMatch = a.productId?.toString() === q;
105
+ const slotMatch = a.slot?.toLowerCase().includes(q);
106
+ const matches = nameMatch || pkgMatch || idMatch || slotMatch;
107
+ const isThumbnail = a.packageName && (a.packageName.toLowerCase().includes('t_sf') || a.packageName.toLowerCase().includes('sf_t'));
108
+ return matches && !isThumbnail;
109
+ }).slice(0, 20); // Limit to top 20 for CLI readability
110
+ }
package/dist/index.js ADDED
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { execSync } from 'child_process';
6
+ import inquirer from 'inquirer';
7
+ import { UPKFile } from './upk.js';
8
+ import { resolvePackagePath, searchAssets } from './assets.js';
9
+ import { fileURLToPath } from 'url';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const program = new Command();
13
+ program
14
+ .name('RLItemMod')
15
+ .description('Rocket League Surgical UPK Patcher')
16
+ .version('1.3.0');
17
+ const API_ENDPOINT = 'https://dank/rlapi';
18
+ const DEFAULT_COOKED_DIR = 'E:\\games\\rocketleague\\TAGame\\CookedPCConsole';
19
+ /**
20
+ * Interactive Wizard Flow
21
+ */
22
+ async function runInteractiveWizard() {
23
+ let active = true;
24
+ while (active) {
25
+ console.clear();
26
+ console.log('Welcome to RLItemMod, What would you like to do?');
27
+ console.log('-----------------------------------------------------');
28
+ const { action } = await inquirer.prompt([{
29
+ type: 'rawlist',
30
+ name: 'action',
31
+ message: 'What would you like to do?',
32
+ choices: [
33
+ { name: 'Swap Item (Visual & Offset Shifting)', value: 'swap' },
34
+ { name: 'Search Asset Database', value: 'search' },
35
+ { name: 'Restore Backups', value: 'restore' },
36
+ { name: 'Configure Game Directory', value: 'config' },
37
+ { name: 'Exit', value: 'exit' }
38
+ ]
39
+ }]);
40
+ if (action === 'exit') {
41
+ active = false;
42
+ break;
43
+ }
44
+ if (action === 'config') {
45
+ const { newDir } = await inquirer.prompt([{
46
+ type: 'input',
47
+ name: 'newDir',
48
+ message: 'Path to CookedPCConsole:',
49
+ default: global.COOKED_DIR || DEFAULT_COOKED_DIR
50
+ }]);
51
+ global.COOKED_DIR = newDir;
52
+ console.log(` Game directory updated to: ${newDir}`);
53
+ await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
54
+ continue;
55
+ }
56
+ if (action === 'search') {
57
+ const { term } = await inquirer.prompt([{
58
+ type: 'input',
59
+ name: 'term',
60
+ message: 'Enter search term (Item Name or Package):'
61
+ }]);
62
+ const results = searchAssets(term);
63
+ console.table(results);
64
+ await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
65
+ continue;
66
+ }
67
+ if (action === 'swap') {
68
+ const cookedDir = global.COOKED_DIR || DEFAULT_COOKED_DIR;
69
+ console.log('\n--- STEP 1: Target Item (The one you OWN) ---');
70
+ const target = await promptForItemAndUPK('Search for your OWNED item:', cookedDir);
71
+ if (!target) {
72
+ await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
73
+ continue;
74
+ }
75
+ console.log('\n--- STEP 2: Source Item (The one you WANT) ---');
76
+ const source = await promptForItemAndUPK('Search for the item you WANT:', cookedDir);
77
+ if (!source) {
78
+ await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
79
+ continue;
80
+ }
81
+ console.log(`\n Swapping ${target.item.name} for ${source.item.name}...`);
82
+ const targetUpk = new UPKFile(target.path);
83
+ try {
84
+ const pythonScriptPath = path.resolve(__dirname, '../python/rl_asset_swapper.py');
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!');
90
+ }
91
+ catch (e) {
92
+ console.error(` Failed: ${e.message}`);
93
+ }
94
+ await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
95
+ continue;
96
+ }
97
+ if (action === 'restore') {
98
+ const cookedDir = global.COOKED_DIR || DEFAULT_COOKED_DIR;
99
+ console.log('\n--- Restoring Backups ---');
100
+ try {
101
+ const files = fs.readdirSync(cookedDir);
102
+ const backups = files.filter(f => f.endsWith('.bak'));
103
+ if (backups.length === 0) {
104
+ console.log(' No backups found.');
105
+ }
106
+ else {
107
+ for (const bak of backups) {
108
+ const original = bak.replace('.bak', '');
109
+ const bakPath = path.join(cookedDir, bak);
110
+ const originalPath = path.join(cookedDir, original);
111
+ console.log(` Restoring ${original}...`);
112
+ fs.copyFileSync(bakPath, originalPath);
113
+ fs.unlinkSync(bakPath);
114
+ }
115
+ console.log(' SUCCESS: All backups restored!');
116
+ }
117
+ }
118
+ catch (e) {
119
+ console.error(` Failed: ${e.message}`);
120
+ }
121
+ await inquirer.prompt([{ type: 'input', name: 'pause', message: 'Press Enter to continue...' }]);
122
+ continue;
123
+ }
124
+ }
125
+ }
126
+ /**
127
+ * Helper to handle the "Search -> Refine -> Pick" flow for a UPK item
128
+ */
129
+ async function promptForItemAndUPK(message, cookedDir) {
130
+ const { searchTerm } = await inquirer.prompt([{
131
+ type: 'input',
132
+ name: 'searchTerm',
133
+ message
134
+ }]);
135
+ const matches = searchAssets(searchTerm);
136
+ if (matches.length === 0) {
137
+ console.error(` No items found matching "${searchTerm}".`);
138
+ return null;
139
+ }
140
+ const { selectedItem } = await inquirer.prompt([{
141
+ type: 'rawlist',
142
+ name: 'selectedItem',
143
+ message: 'Select the item:',
144
+ choices: matches.map(m => ({
145
+ name: `${m.name} [ID: ${m.productId}] (${m.packageName})`,
146
+ value: m
147
+ }))
148
+ }]);
149
+ const owned = selectedItem.productId.toString();
150
+ const result = await resolvePackagePath(owned, cookedDir);
151
+ if (!result) {
152
+ console.error(` Could not resolve "${owned}".`);
153
+ return null;
154
+ }
155
+ let finalUpkPath = '';
156
+ if ('candidates' in result) {
157
+ let currentCandidates = result.candidates;
158
+ while (currentCandidates.length > 20) {
159
+ console.log(` Found ${currentCandidates.length} potential matches.`);
160
+ const { refine } = await inquirer.prompt([{
161
+ type: 'input',
162
+ name: 'refine',
163
+ message: 'Too many files found! Enter a sub-keyword to narrow it down (or press Enter to see all):'
164
+ }]);
165
+ if (!refine)
166
+ break;
167
+ const filtered = currentCandidates.filter(c => c.toLowerCase().includes(refine.toLowerCase()));
168
+ if (filtered.length > 0)
169
+ currentCandidates = filtered;
170
+ }
171
+ const { selected } = await inquirer.prompt([{
172
+ type: 'rawlist',
173
+ name: 'selected',
174
+ message: `Multiple matches found. Select one (Showing top ${Math.min(currentCandidates.length, 50)}):`,
175
+ choices: currentCandidates.slice(0, 50)
176
+ }]);
177
+ finalUpkPath = path.join(cookedDir, selected);
178
+ }
179
+ else {
180
+ finalUpkPath = result.path;
181
+ }
182
+ return { path: finalUpkPath, item: selectedItem };
183
+ }
184
+ async function executePatch(upkPath, data, exportIndex) {
185
+ try {
186
+ console.log(` Patching ${upkPath}...`);
187
+ backupFile(upkPath);
188
+ const upk = new UPKFile(upkPath);
189
+ upk.readSummary();
190
+ upk.readExportMap();
191
+ if (exportIndex === -1) {
192
+ 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)`);
194
+ }
195
+ const newHex = (typeof data === 'string') ? fs.readFileSync(data) : data;
196
+ upk.patchExport(exportIndex, newHex);
197
+ console.log(' SUCCESS: Patch complete!');
198
+ }
199
+ catch (error) {
200
+ console.error(' CRITICAL FAILURE:', error.message);
201
+ }
202
+ }
203
+ function backupFile(filePath) {
204
+ const backupPath = `${filePath}.bak`;
205
+ if (!fs.existsSync(backupPath)) {
206
+ console.log(` Creating backup: ${path.basename(backupPath)}`);
207
+ fs.copyFileSync(filePath, backupPath);
208
+ }
209
+ }
210
+ // --- CLI COMMANDS ---
211
+ program
212
+ .command('list')
213
+ .description('Shows user\'s owned items from the API')
214
+ .action(async () => {
215
+ // Existing list logic...
216
+ console.log('Fetching items... (Placeholder)');
217
+ });
218
+ program
219
+ .command('search')
220
+ .argument('<term>', 'Keyword to search for')
221
+ .action((term) => {
222
+ const results = searchAssets(term);
223
+ console.table(results);
224
+ });
225
+ program
226
+ .command('swap')
227
+ .requiredOption('--owned <itemName>', 'Item name or Product ID')
228
+ .requiredOption('--target <targetHexFile>', 'Hex file path')
229
+ .option('--upk <path>', 'Explicit UPK path')
230
+ .option('--dir <path>', 'Cooked dir', DEFAULT_COOKED_DIR)
231
+ .option('--export <index>', 'Export index')
232
+ .action(async (options) => {
233
+ // Call executePatch with resolved paths...
234
+ let upkPath = options.upk;
235
+ let cookedDir = options.dir;
236
+ if (!fs.existsSync(cookedDir)) {
237
+ console.error(` Error: CookedPCConsole directory not found at: ${cookedDir}`);
238
+ console.log('Use --dir <path> to specify the correct game directory.');
239
+ return;
240
+ }
241
+ if (!upkPath) {
242
+ const res = await resolvePackagePath(options.owned, cookedDir);
243
+ if (res && 'path' in res)
244
+ upkPath = res.path;
245
+ }
246
+ if (!upkPath) {
247
+ console.error(` Error: Could not resolve item "${options.owned}" in ${cookedDir}.`);
248
+ return;
249
+ }
250
+ await executePatch(upkPath, options.target, options.export ? parseInt(options.export) : -1);
251
+ });
252
+ // Handle Ctrl+C gracefully
253
+ process.on('SIGINT', () => {
254
+ console.log('\n Exiting RLItemMod. See you next time!');
255
+ process.exit(0);
256
+ });
257
+ async function runSafeWizard() {
258
+ try {
259
+ await runInteractiveWizard();
260
+ }
261
+ catch (e) {
262
+ if (e.name === 'ExitPromptError') {
263
+ console.log('\n Goodbye!');
264
+ }
265
+ else {
266
+ console.error('\n An unexpected error occurred:', e.message);
267
+ }
268
+ process.exit(0);
269
+ }
270
+ }
271
+ // Default to wizard if no command is provided
272
+ if (process.argv.length <= 2) {
273
+ runSafeWizard();
274
+ }
275
+ else {
276
+ program.parse(process.argv);
277
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,81 @@
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
+ }