Skip to content

Commit dbf472b

Browse files
Tim Winchestertimsama
authored andcommitted
Initial commit
Just got rid of last shell script usage, so everything is completely portable. Also switched from creating hexviews for every .raw.changes file creation in favor of just performing a line-by-line binary diff, because it's way faster.
0 parents  commit dbf472b

20 files changed

+2458
-0
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Breath of the Wild Save File Mapper
2+
###### Intro
3+
Have you hundred-percented Breath of the Wild, and are looking for a new challenge? Are you itching to create new, possibly-ridiculous ways of playing? Or are you just an _enormous_ nerd who desperately needs to know exactly how Link's current position and orientation are stored in your Breath of the Wild save? If you answered yes to any of these, Breath of the Wild Save File Mapper might be for you. And note that while some people are doing crazy things with the game involving emulators, everything crazy thing BoTWSFM does works _on your Wii U console_.
4+
5+
...OR...
6+
7+
Have you started playing Breath of the Wild, and due to an act of God, Nature or Younger Sibling, your save has been deleted, and now you've lost all motivation to get back to where you were? Well, as long as you remember the items you have and the status of your quests, you can _recreate_ your save file (or a reasonable approximation thereof) and pick up right from where you left off! I'm planning to build a UI in the future to make that much easier, but it certainly is possible to do now.
8+
9+
What BotWSFM does:
10+
- Compares different save files and outputs a raw changefile containing the differences between them. Every. Single. Bit.
11+
- Uses raw changefiles to perform binary searches by chunk or by line to identify what data changes have what in-game effects.
12+
- Exports search results to JSON effect map files that you can compile and use to your heart's content.
13+
- Apply and unapply\* effects from your effect map files to alter any v1.4 Breath of the Wild save file!
14+
- Lets you insert/modify/delete items from your inventory!
15+
- Lets you use the packaged-in effects maps to set your quest progress to ANY point in the main questline (including Trial of the Sword and The Champion's Ballad--if you purchased them).
16+
- Lets you control the completion status of all shrines and divine beasts!
17+
18+
\*_With some exceptions_
19+
20+
What BotWSFM does NOT do:
21+
- Change anything in the game while it's running.
22+
- Transfer anything to or from your Wii U. You've gotta get the save file onto your PC yourself.
23+
- Work on pre-v1.4 save files. (Well, it might work. I haven't checked, but I would be surprised if it did work.)
24+
- Work on Switch BoTW save files. (Again, it might work, but I don't have any Switch save files on my PC, so no way to check.)
25+
26+
###### Getting Started
27+
The very first thing you'll need to do is go into config.js and update `savepath` to point to the location of your save files, including the specific save slot you want to use. I happen to like slot 6, but it doesn't really matter.
28+
29+
After that, you'll want to start building raw changefiles. That might sound scary, but it's really pretty easy. For instance, if I want to figure out the savefile location that controls whether or not the Magnesis rune is unlocked, I would run `npm run build-raw-changes runes.magnesis` (which starts the script and names the raw changefile Magnesis), save the game right before getting Magnesis, hit [Enter] in my command prompt, get Magnesis, save immediately afterward, then hit [Enter] again. Viola! A raw changefile will be created. Follow this process to create changefiles for whatever save data you want to know about. Keep in mind, the fewer things you do between your saves, the easier and faster it will be to nail down where that data is stored.
30+
31+
Next, you'll want to start building finalized effects to add to your effects map. This is the fun, crazy and/or tedious part. If I want to continue nailing down the Magnesis rune, I'd run `npm run test-chunks runes.magnesis`, which will start the binary search, using the Magnesis raw changefile created in the previous step. It will temporarily back up my save file, and swap in a new one that it has created. I would then need to load up the save on my console (it won't be hard to guess which one, since the preview image will change), and answer a (y/n) prompt saying whether or not the change worked. So, after my game loaded up that save, I would check my list of runes to see if Magnesis was in it. If so, I'd answer "yes". If not, I'd answer "no". It will repeat this process several times until it has narrowed things down to some addresses that it thinks will work. Word of warning: strange things and even game crashes can happen during this process. But rest assured, the original save file is safely backed up, and will be restored at the end of the process. If my game crashes, I'd just answer "no" to the prompt for that time around. If the final save it generates works, then hooray! I found the chunk of the save file that controls whether or not the Magnesis rune is enabled.
32+
33+
Finally, unless you wanted to know about save file structure purely to satisfy your curiosity, you'll want to apply the effects you discovered and start modifying your saves. To continue with the Magnesis example, I would then create a "brand new, just got the Sheikah Slate" save file (any save file would work, but you already have Magnesis for most of it), make sure I've saved it, and then I would run `npm run apply-changes runes.magnesis`, reload the save file, and I would magically now have the Magnesis Rune, despite never having left the Shrine of Resurrection.
34+
35+
You can follow the above process to do lots of things, from giving yourself the Champion's Ballad final reward at the beginning of the game, to skipping getting the Sheikah Slate, giving yourself the Paraglider, and leaving the Great Plateau before activating any of the towers or shrines (good luck getting back up without teleports, though!). You can even fall endlessly through a cloud-filled abyss, or permanently disable all your runes if you are so inclined. The possibilities are endless!\*
36+
37+
\*_The number of possibilities is actually roughly equivalent to 2^1,027,200 - # of save file configurations that either do nothing or crash the game. Plus 2._
38+
39+
###### What's New
40+
I've gotten rid of all dependency on shell scripts, so you no longer need to be running Linux for this to work (yay!). In fact, I've switched to developing this entirely on Windows 10, using the Windows Linux Subsystem, which is actually pretty great.
41+
42+
###### Chunks vs. Singles
43+
Personally, I tend to search my raw changefiles using `test-chunks` because it's less likely to crash the game. The only difference between `test-chunks` and `test-singles` is that `test-chunks` aggregates changes to offsets that are close to each other into an atomic cluster that I call a "chunk", and will either use the whole chunk or none of it in any given test. In layman's terms, it means that I prevent any statement that says "buy sandwiches" from accidentally getting cut up and turned into "buy sand". `test-singles` is still useful if you know what you're looking for is stored in a `uint32` or less, but I typically don't touch it until `test-chunks` has failed, or has reduced things down to a chunk that I want to split apart to see if contains an offset change that can give me the effects I want by itself.
44+
45+
Some interesting things I've found using chunks and singles: there are in fact two different ways to give yourself the Paraglider, and the quest The Great Plateau does both at the end. The first is a chunk of 3 lines that give access to the Paraglider by adding it to your Key Items menu. The second is a single location where you change a 0 to a 1, which gives you the ability to use the Paraglider, but doesn't add it to your Key Items. Both of these methods remove the fog from around the Great Plateau, which allows you to reach the ground without getting pulled back onto the plateau.

apply-changes.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
var fs = require('fs');
2+
var jBinary = require('jbinary');
3+
var saveFileUtils = require('./save-file-utils.js');
4+
var CONFIG = require('./config.js');
5+
6+
const names = process.argv.slice(2) || ['unnamed'];
7+
const path = CONFIG.savepath;
8+
const saveFilename = 'game_data.sav';
9+
const saveFilepath = `${path}/${saveFilename}`;
10+
11+
jBinary.load(saveFilepath, saveFileUtils.typeSet, function (err, binary) {
12+
const writeToOffset = saveFileUtils.buildWriter('uint32', binary);
13+
14+
names.forEach((name) => {
15+
const changesFilename = name + '.changes';
16+
const changesFilepath = `${path}changes/${changesFilename}`;
17+
18+
saveFileUtils.getChangesToApply(changesFilepath).forEach((entry) => {
19+
writeToOffset(entry.offset, entry.value);
20+
});
21+
});
22+
23+
binary.saveAs(saveFilepath);
24+
});

build-hex-diff.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
var fs = require('fs');
2+
var jBinary = require('jbinary');
3+
var saveFileUtils = require('./save-file-utils.js');
4+
var CONFIG = require('./config.js');
5+
6+
// this only works because all BoTW v1.4 save files are the same length
7+
const buildHexDiff = (outputFilename, beforeFilename, afterFilename) => {
8+
const hexDiff = saveFileUtils.withBinaryFileSync(beforeFilename, (beforeBinary) => {
9+
return saveFileUtils.withBinaryFileSync(afterFilename, (afterBinary) => {
10+
const getHexLine = (offset, value) => {
11+
return '0x' + saveFileUtils.toHexString(offset) + ' ' + saveFileUtils.toHexString(value);
12+
};
13+
const nextBefore = () => {
14+
return beforeBinary.read('uint32');
15+
};
16+
const nextAfter = () => {
17+
return afterBinary.read('uint32');
18+
};
19+
20+
var minusLines = [];
21+
var plusLines = [];
22+
23+
while(beforeBinary.tell() < CONFIG.saveFileLastOffset) {
24+
const beforeOffset = beforeBinary.tell();
25+
const afterOffset = afterBinary.tell();
26+
const beforeValue = nextBefore();
27+
const afterValue = nextAfter();
28+
if (beforeValue !== afterValue) {
29+
minusLines.push('-' + getHexLine(beforeOffset, beforeValue));
30+
plusLines.push('+' + getHexLine(afterOffset, afterValue));
31+
}
32+
};
33+
34+
return minusLines.join('\n') + '\n' + plusLines.join('\n');
35+
});
36+
});
37+
38+
fs.writeFileSync(outputFilename, hexDiff);
39+
};
40+
41+
module.exports = buildHexDiff;

build-hex-view.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
var fs = require('fs');
2+
var jBinary = require('jbinary');
3+
var saveFileUtils = require('./save-file-utils.js');
4+
var CONFIG = require('./config.js');
5+
6+
const buildHexView = (filename) => {
7+
const hexView = saveFileUtils.withBinaryFileSync(filename, (binary) => {
8+
const getNextHexLine = () => {
9+
return '0x' + saveFileUtils.toHexString(binary.tell()) + ' ' + saveFileUtils.toHexString(binary.read('uint32'));
10+
};
11+
12+
var hexLines = [];
13+
14+
while(binary.tell() < CONFIG.saveFileLastOffset) {
15+
hexLines.push(getNextHexLine());
16+
};
17+
18+
return hexLines.join('\n');
19+
});
20+
21+
fs.writeFileSync(`${filename}.hexview`, hexView);
22+
};
23+
24+
module.exports = buildHexView;

build-raw-changes-from-savefiles.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
var readline = require('readline-sync');
2+
var query = require('cli-interact').getYesNo;
3+
var fs = require('fs');
4+
var buildHexDiff = require('./build-hex-diff.js');
5+
var CONFIG = require('./config.js');
6+
var folderUtils = require('./folder-utils.js');
7+
8+
const _path = process.argv[5] || CONFIG.savepath;
9+
const path = (_path.slice(-1) === '/') ? _path : _path + '/';
10+
const beforeFilepath = `${path}` + process.argv[3];
11+
const afterFilepath = `${path}` + process.argv[4];
12+
13+
const nameQuestionString = 'Name of change set: ';
14+
15+
var name = process.argv[2] || readline.question(nameQuestionString);
16+
var isSure = !!name;
17+
while (!name && !isSure) {
18+
isSure = query('Unnamed changes will likely be later overwritten. Are you sure?');
19+
if (!isSure) {
20+
name = readline.question(nameQuestionString);
21+
} else {
22+
name = 'unnamed';
23+
}
24+
}
25+
26+
const filename = `raw/${name}.raw.changes`;
27+
const filepath = path + filename;
28+
29+
folderUtils.buildFoldersIfTheyDoNotExist(filename);
30+
buildHexDiff(filepath, beforeFilepath, afterFilepath);
31+
console.log('Successfully created raw changefile!')

build-raw-changes.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
var readline = require('readline-sync');
2+
var query = require('cli-interact').getYesNo;
3+
var fs = require('fs');
4+
var buildHexDiff = require('./build-hex-diff.js');
5+
var CONFIG = require('./config.js');
6+
var folderUtils = require('./folder-utils.js');
7+
8+
const _path = process.argv[3] || CONFIG.savepath;
9+
const path = (_path.slice(-1) === '/') ? _path : _path + '/';
10+
const saveFilepath = `${path}game_data.sav`;
11+
const saveFileBackup = `${path}backup.sav`;
12+
const beforeFilepath = `${path}game_data_before.sav`;
13+
const afterFilepath = `${path}game_data_after.sav`;
14+
15+
const nameQuestionString = 'Name of change set: ';
16+
17+
var name = process.argv[2] || readline.question(nameQuestionString);
18+
var isSure = !!name;
19+
while (!name && !isSure) {
20+
isSure = query('Unnamed changes will likely be later overwritten. Are you sure?');
21+
if (!isSure) {
22+
name = readline.question(nameQuestionString);
23+
} else {
24+
name = 'unnamed';
25+
}
26+
}
27+
28+
const filename = `raw/${name}.raw.changes`;
29+
const filepath = path + filename;
30+
31+
const beforeHexViewBuilder = () => {
32+
readline.question('Please save your game immediately before the change for which you wish to build a changefile. Then press [Enter] to continue.');
33+
34+
fs.copyFileSync(saveFilepath, beforeFilepath);
35+
};
36+
37+
const afterHexViewBuilder = () => {
38+
readline.question('Please save your game immediately after the change for which you wish to build a changefile. Then press [Enter] to continue.');
39+
40+
fs.copyFileSync(saveFilepath, afterFilepath);
41+
};
42+
43+
const cleanup = () => {
44+
if (fs.existsSync(saveFileBackup)) {
45+
fs.unlinkSync(saveFileBackup);
46+
}
47+
fs.renameSync(saveFilepath, saveFileBackup);
48+
fs.renameSync(beforeFilepath, saveFilepath);
49+
fs.unlinkSync(saveFileBackup);
50+
fs.unlinkSync(afterFilepath);
51+
};
52+
53+
beforeHexViewBuilder();
54+
afterHexViewBuilder();
55+
folderUtils.buildFoldersIfTheyDoNotExist(filename);
56+
buildHexDiff(filepath, beforeFilepath, afterFilepath);
57+
console.log('Successfully created raw changefile!\nCleaning up...')
58+
cleanup();
59+
console.log('...done.')

build-recursive-searcher.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
var query = require('cli-interact').getYesNo;
2+
var saveFileUtils = require('./save-file-utils.js');
3+
4+
const BuildRecursiveSearcher = (saveFilepath, binarySync) => {
5+
return {
6+
search: (allChangesToApply, allChangesToUnapply, getChanges) => {
7+
const writeToOffset = saveFileUtils.buildWriter('uint32', binarySync);
8+
const toHexString = saveFileUtils.toHexString;
9+
10+
const recursiveSearch = (changesToApply, changesToUnapply) => {
11+
if (changesToApply.length === 1) {
12+
if (tryCombination(getChanges(changesToApply), changesToUnapply)) {
13+
return getChanges(changesToApply);
14+
} else {
15+
return [];
16+
}
17+
}
18+
19+
const half = Math.ceil(changesToApply.length / 2.0);
20+
const firstHalf = changesToApply.slice(0, half);
21+
const secondHalf = changesToApply.slice(half);
22+
23+
if (tryCombination(getChanges(firstHalf), changesToUnapply)) {
24+
return recursiveSearch(firstHalf, changesToUnapply);
25+
} else {
26+
return recursiveSearch(secondHalf, changesToUnapply);
27+
}
28+
};
29+
30+
var index = 0;
31+
32+
const tryCombination = (changesToApply, changesToUnapply) => {
33+
console.log('Trying the following entries:');
34+
changesToApply.forEach((entry) => {
35+
console.log(`0x${toHexString(entry.offset)}: ${toHexString(entry.value)}`);
36+
});
37+
38+
// apply changes to test
39+
changesToApply.forEach((entry) => {
40+
writeToOffset(entry.offset, entry.value);
41+
});
42+
43+
binarySync.saveAsSync(saveFilepath);
44+
45+
const worked = query(`Save file generated. (${++index} of ${Math.ceil(Math.log(allChangesToApply.length, 2)) + 2}) Did it work?`);
46+
47+
// revert changes for next test
48+
changesToUnapply.forEach((entry) => {
49+
writeToOffset(entry.offset, entry.value);
50+
});
51+
52+
binarySync.saveAsSync(saveFilepath);
53+
54+
return worked;
55+
};
56+
57+
return recursiveSearch(allChangesToApply, allChangesToUnapply);
58+
}
59+
};
60+
};
61+
62+
module.exports = BuildRecursiveSearcher;

config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const CONFIG = {
2+
savepath: '<Enter the path to your save file here, including the slot subfolder>',
3+
saveFileLastOffset: 0x000fac80,
4+
placeholderImagepath: './placeholder.jpg'
5+
};
6+
7+
module.exports = CONFIG;

0 commit comments

Comments
 (0)