Created
January 19, 2021 00:50
A heavily commented script to find Wishmaker Jirachi seeds
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This code is meant to act as documentation for calculating Wishmaker Jirachi seeds. | |
// As a result, it's not the most efficient code, but can hopefully serve as a guide for anyone interested in the mechanics. | |
// | |
// Admittedly, I wrote this fairly quickly, so it's not quite as clean as I'd like it either, and I haven't tested it too much. | |
// If you have any questions or updates, please leave them in the comments below. | |
// | |
// This code assumes some knowledge of Wishmaker, as well as a disabled RTC. | |
// ---------------------------------------- | |
// Jirachi seed logic | |
// ---------------------------------------- | |
/** | |
* Calculates the checksum of a block | |
* @param {DataView} blockView | |
*/ | |
const getBlockChecksum = (blockView) => { | |
let checksum = 0; | |
for (let i = 0; i < blockView.byteLength; i += 4) { | |
checksum += blockView.getUint32(i, true); | |
} | |
return ((checksum >>> 16) + checksum) & 0xffff; | |
}; | |
/** | |
* Calculates the checksum of the constant u32s in a block | |
* @param {DataView} blockView | |
*/ | |
const getChecksumConstant = (blockView) => { | |
let checksum = 0; | |
for (let i = 0; i < blockView.byteLength; i += 4) { | |
// Skip time bytes - we want to manipulate those | |
if (i === 0xc || i === 0x10) { | |
continue; | |
} | |
checksum += blockView.getUint32(i, true); | |
} | |
return checksum & 0xffffffff; | |
}; | |
// Tests for finding seed parameters: | |
// - Test `carry` does not give the wrong result if if `frame` is 0 | |
// - Test `carry` does not give the wrong result if `frame` and `hourLow` are 0 | |
// - Test what happens if hour = 999 (max) | |
// - Test what happens if hour = 889 (999 - 111 + 1) | |
// - Test what happens if hour = 888 (safe) | |
// - Test what happens if hour = 0 (safest) | |
// - Test 0 <= hour < 1000, 0 <= minute < 60, 0 <= second < 60, 0 <= frame < 60 | |
// - Test results are always in the future | |
/** | |
* Finds parameters that will produce a given seed | |
* @param {number} currentHour The save's current u16 hour between 0-999 | |
* @param {number} currentMinute The save's current u8 minute between 0-59 | |
* @param {number} chkConst The u32 block checksum for all constant values that won't be modified | |
* @param {number} seed The desired seed | |
*/ | |
const findParamForSeed = (currentHour, currentMinute, chkConst, seed) => { | |
// This is used later to check if a carry will occur and needs to be accounted for | |
const chkConstLow = chkConst & 0xffff; | |
const chk = (chkConst >>> 16) + chkConstLow; | |
// Our controlled parameters need to create this value | |
const params = ((seed - chk) >>> 0) & 0xffff; | |
// Split the params to calculate the variables we control | |
const paramsHigh = params >>> 8; | |
const paramsLow = params & 0xff; | |
// Calculate the max value that can be added to (hour & 0xff) | |
// 0xff - 59 - 59 = 144 | |
const maxHourLow = Math.min(999 - currentHour, 144); | |
// Check for an impossible value | |
// frame and minute have a max of 59 | |
// 59 + 59 = 111 | |
// If (hour & 0xff) cannot be manipulated to be <= 111, this is an impossible seed | |
if (paramsLow - maxHourLow > 111) { | |
return null; | |
} | |
// This will be added to the current hour later | |
let hourAddLow = Math.max(paramsLow - maxHourLow, 0); | |
let minute = Math.max(paramsLow - hourAddLow - 59, 0); | |
let frame = paramsLow - hourAddLow - minute; | |
// Set minute as low as possible for faster RNG | |
// I'd rather wait 10 minutes, 50 frames than 50 minutes, 10 frames | |
[minute, frame] = [minute, frame].sort(); | |
// This will be added to the current hour later | |
const hourAddHigh = Math.max(paramsHigh - 59, 0); | |
const second = paramsHigh - hourAddHigh; | |
// Because the u32 checksum is split into two u16s and added | |
// checksum & 0xffff + (second << 8) + frame might be greater than 0xffff | |
// The carry needs to be accounted for | |
const carry = ((second << 8) + frame + chkConstLow) >>> 16; | |
// Subtract the carry from the hour is the hour < 0 | |
const hourCarry = hourAddLow > 0 ? carry : 0; | |
// Subtract the carry from the frame if hour === 0 | |
const frameCarry = carry - hourCarry; | |
hourAddLow -= hourCarry; | |
frame -= frameCarry; | |
// Carry creates impossible scenario | |
if (frame < 0) { | |
return null; | |
} | |
// If the time is in the past, shift the hour and frame (hourLow and frame are interchangeable for the checksum) | |
if (hourAddLow === 0 && minute < currentMinute) { | |
// Can't shift - impossible time | |
if (frame <= 0) { | |
return null; | |
} | |
hourAddLow += 1; | |
frame -= 1; | |
} | |
const hour = (hourAddHigh << 8) + hourAddLow + currentHour; | |
if (hour > 999 || second > 59) { | |
return null; | |
} | |
return { | |
seed: seed.toString(16), | |
hour, | |
minute, | |
second, | |
frame, | |
}; | |
}; | |
const BLOCK_0_SIZE = 0x890; | |
/** | |
* Get block 0 of a save file | |
* @param {Uint8Array} save | |
*/ | |
const getBlock0View = (save) => { | |
// Get the index of the first save block tbat appears in the save file | |
const firstBlockIndex = save[0xff4]; | |
// There are 14 blocks total, each 0x1000 in size | |
// Find where block 0 is in the save file | |
const block0StartOffset = (14 - firstBlockIndex) * 0x1000; | |
const block0EndOffset = block0StartOffset + BLOCK_0_SIZE; | |
// Create a data view to access LE u16 and u32 values | |
return new DataView(Uint8Array.from(save.slice(block0StartOffset, block0EndOffset)).buffer); | |
}; | |
/** | |
* Find parameters for multiple seeds | |
* @param {DataView} block0View The save to calculate seeds for | |
* @param {number[]} seeds The seeds to find params for | |
*/ | |
const findParamsForSeeds = (block0View, seeds) => { | |
// A save block's checksum is made by adding all the u32s in the block together | |
// These values are part of the u32s we can modify to influence the seed | |
// The SID is part of a u32 we can modify, but we can't modify the SID by itself | |
const sid = block0View.getUint16(0xc, true); | |
const hour = block0View.getUint16(0xe, true); | |
const shiftedHour = hour << 16; | |
const minute = block0View.getUint8(0x10); | |
// These are here for documentation | |
// We don't need to account for them since their values can be reset | |
// const second = block0View.getUint8(0x11) << 8; | |
// const frame = block0View.getUint8(0x12) << 16; | |
const optionLR = block0View.getUint8(0x13) << 24; | |
// This is the current checksum of the constant values in the save file | |
const chkConst = getChecksumConstant(block0View) + sid + shiftedHour + optionLR; | |
return seeds | |
.map((seed) => findParamForSeed(hour, minute, chkConst, seed)) | |
.filter((param) => param !== null); | |
}; | |
// ---------------------------------------- | |
// Testing the logic with a save file | |
// ---------------------------------------- | |
const fs = require('fs'); | |
// Open the save file | |
const save = fs.readFileSync('./wishmaker.sav', { encoding: null }); | |
const block0View = getBlock0View(save); | |
const SHINY_SEEDS = [0x353d, 0xf500, 0xecdd, 0x9359, 0xcf37, 0x7236, 0xa030, 0x7360, 0x3d60]; | |
findParamsForSeeds(block0View, SHINY_SEEDS).forEach((param) => { | |
block0View.setUint16(0xe, param.hour, true); | |
block0View.setUint8(0x10, param.minute); | |
block0View.setUint8(0x11, param.second); | |
block0View.setUint8(0x12, param.frame); | |
block0View.setUint8(0x13, param.optionLR); | |
// Calulate the new checksum to verify the result | |
console.log(getBlockChecksum(block0View).toString(16), param); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment