diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccb979 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules + +game_input/* +game_output/* +mogg_input/* +mogg_output/* +tmp/* \ No newline at end of file diff --git a/MoggcryptCpp.exe b/MoggcryptCpp.exe new file mode 100644 index 0000000..13374bc Binary files /dev/null and b/MoggcryptCpp.exe differ diff --git a/README.md b/README.md index e632548..573046e 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -Fuser-Stem-Replacement-Tool +# Fuser Stem Replacement Tool + +1. Figure out which song you want to replace stems of +2. Get it's BPM +3. Prepare your own stem / song with that same BPM +4. Make sure it's **exactly** 32 bars long and optimally loops well +5. Export it as an `.ogg` file into the `mogg_input` folder +6. Copy the corresponding `.uexp` and `.uasset` file combination into the `game_input` folder +7. Run `npm start ` +8. Copy the corresponding files from `game_output` into the `pakfiles` folder you have setup with the old game files and overwrite them +9. Build the new DLC `.pak` file +10. Replace it in the `Fuser/Fuser/DLC/Songs` directory + +## Example (DK Rap => Adore You, 99 BPM, Vocals) +`npm start dk_rap adoreyou_lead_eb_99_fusion` \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..cd269c0 --- /dev/null +++ b/index.js @@ -0,0 +1,135 @@ +const fs = require('fs'); +const execFile = require('child_process').execFile; +const path = require('path'); + +const rimraf = require('rimraf'); +rimraf.sync('tmp'); + +const mkdirp = require('mkdirp'); +mkdirp.sync('tmp'); +mkdirp.sync('mogg_input'); +mkdirp.sync('mogg_output'); + +mkdirp.sync('game_input'); +mkdirp.sync('game_output'); + +const fileExists = async path => !!(await fs.promises.stat(path).catch(e => false)); +const readFile = async path => await fs.promises.readFile(path); + +async function moggify() { + const files = await fs.promises.readdir('mogg_input'); + + for(const file of files) { + console.log(file); + + const from = path.join('mogg_input', file); + const tmp = path.join('tmp', path.parse(file).name + '.mogg'); + const to = path.join('mogg_output', path.parse(file).name + '.mogg'); + + await new Promise(resolve => { + execFile('ogg2mogg.exe', [from, tmp], () => { + execFile('MoggcryptCpp.exe', [tmp, '-e', to], () => { + resolve(); + }); + }); + }) + } +} + +async function replace() { + if (process.argv.length !== 4) { + console.error('Usage: node replace.js '); + console.log('(No file extensions)'); + return; + } + + const mogg_file = process.argv[2] + '.mogg'; + const game_file = process.argv[3]; + + const mogg_exists = await fileExists(`mogg_output/${mogg_file}`); + const uexp_exists = await fileExists(`game_input/${game_file}.uexp`); + const uasset_exists = await fileExists(`game_input/${game_file}.uasset`); + + if (!mogg_exists) { + console.error(`Couldn't find the .mogg file '${mogg_file}'.`); + return; + } + + if (!uexp_exists || !uasset_exists) { + console.error(`Couldn't find a .uexp and .uasset combo for '${game_file}'.`); + return; + } + + const out_uexp = path.join('game_output', game_file + '.uexp'); + const out_uasset = path.join('game_output', game_file + '.uasset'); + + // Read .mogg + const mogg_buffer = await readFile(`mogg_output/${mogg_file}`); + console.log('Mogg', mogg_buffer); + + // Modify .uexp + const data_buffer = await readFile(`game_input/${game_file}.uexp`); + const old_uexp_length = data_buffer.length; + const start_offset = data_buffer.indexOf('MoggSampleResource') + 19; + console.log('uexp', data_buffer); + console.log('uexp length', old_uexp_length); + + // Write new MOGG Buffer length + 32 to offset start_offset + data_buffer.writeInt32LE(mogg_buffer.length + 32, start_offset); + + // Write new MOGG Buffer length to offset start_offset + 36, store old for buffering + const old_1_length = data_buffer.readInt32LE(start_offset + 36); + console.log("Old 1 length", old_1_length); + data_buffer.writeInt32LE(mogg_buffer.length, start_offset + 36); + console.log("New 1 length", data_buffer.readInt32LE(start_offset + 36)); + + // Insert MOGG at start_offset + 36 + 4, then offset to the new position + const pre_buffer_1 = data_buffer.slice(0, start_offset + 36 + 4); + const post_buffer_1 = data_buffer.slice(start_offset + 36 + 4 + old_1_length); + + // Concat buffers + const song_1_insert_buffer = Buffer.concat([pre_buffer_1, mogg_buffer, post_buffer_1]); + + // Only replace 2nd MoggSampleResource if numResourceFiles === 3 + let final_buffer = song_1_insert_buffer; + const filenameLength = final_buffer.readInt16LE(28); + const numResourceFiles = final_buffer.readInt16LE(28 + filenameLength + 12); + console.log(final_buffer.length); + if (numResourceFiles === 3) { + const new_offset = song_1_insert_buffer.indexOf('MoggSampleResource', mogg_buffer.length) + 19; + console.log("New Offset", new_offset); + console.log("InsertBufferLength", song_1_insert_buffer.length); + + // Write new MOGG Buffer length + 32 to offset new_offset + song_1_insert_buffer.writeInt32LE(mogg_buffer.length + 32, new_offset); + + // Write new MOGG Buffer length to offset new_offset + 36, store old for buffering + const old_2_length = song_1_insert_buffer.readInt32LE(new_offset + 36); + console.log("Old 2 length", old_2_length); + song_1_insert_buffer.writeInt32LE(mogg_buffer.length, new_offset + 36); + console.log("New 2 length", song_1_insert_buffer.readInt32LE(new_offset + 36)); + + // Insert MOGG at new_offset + 36 + 4, then offset to the new position + const pre_buffer_2 = song_1_insert_buffer.slice(0, new_offset + 36 + 4); + const post_buffer_2 = song_1_insert_buffer.slice(new_offset + 36 + 4 + old_2_length); + + // Concat buffers + final_buffer = Buffer.concat([pre_buffer_2, mogg_buffer, post_buffer_2]); + } + + // Finally, write + await fs.promises.writeFile(out_uexp, final_buffer); + + // Modify .uasset + const uasset_buffer = await readFile(`game_input/${game_file}.uasset`); + console.log('Uassetbuffer', uasset_buffer); + + const update_size_offset = uasset_buffer.lastIndexOf(old_uexp_length - 4); + console.log("Update size offset", update_size_offset); + uasset_buffer.writeInt32LE(final_buffer.length - 4, update_size_offset); + + // Finally, write + await fs.promises.writeFile(out_uasset, uasset_buffer); +} + +moggify().then(replace); diff --git a/ogg2mogg.exe b/ogg2mogg.exe new file mode 100644 index 0000000..ece8138 Binary files /dev/null and b/ogg2mogg.exe differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..56f2a5f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,103 @@ +{ + "name": "fusermod", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "underscore": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.0.tgz", + "integrity": "sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c09e29b --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "fusermod", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js", + "moggify": "node moggify.js", + "replace": "node replace.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2", + "underscore": "^1.12.0" + } +}