├── .gitignore ├── README.md ├── css └── main.css ├── imgs ├── battletiles.PNG ├── fallback.png ├── tiles.png └── tiles0.png ├── index.html ├── js ├── background.js ├── main.js └── sav_parser.js └── utlities ├── css └── hex.css ├── hexViewer.html └── js └── hexViewer.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/* 3 | notes/* 4 | js/lib/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readme for Pokémon Save File Reader # 2 | 3 | by Lyndon Armitage 4 | 5 | 6 | This is a HTML5 implementation of a Pokémon Red, Blue and Yellow save game viewer (and possibly editor in the future). 7 | Made for fun, and educational purposes only. 8 | 9 | You can see it in action here: 10 | [lyndon.codes/assets/html5-pokemon-save-reader/](https://lyndon.codes/assets/html5-pokemon-save-reader/) 11 | 12 | There is a more maintained fork of this project by [Daniell Mesquita](https://github.com/DaniellMesquita) that can be seen here: 13 | [github.com/Universav/UniversavPoke](https://github.com/Universav/UniversavPoke) 14 | 15 | ## Current Version: ## 16 | 17 | Currently in the very early stages of development. 18 | Can currently open a .sav file and read the following: 19 | * Trainer Name 20 | * Rival Name 21 | * Trainer ID 22 | * Time Played 23 | * Items in Pocket 24 | * items in PC 25 | * Money 26 | * Checksum 27 | * Current Box selected 28 | * Pokédex Owned and Seen stats 29 | * Basic Location support (x, y, and map id) 30 | 31 | ## Other: ## 32 | 33 | [This page](http://bulbapedia.bulbagarden.net/wiki/Save_data_structure_in_Generation_I) is useful for The structure of Pokémon Red/Blue & Possibly Yellow save files. Although not all of it's data is accurate. 34 | 35 | I have also created a small Hex Viewer using the HTML5 File API and Storage API that let's you view a files contents and write notes. 36 | Note that this may crash your browser for files with a lot of bytes (i.e. bigger files). I have only tested it with .sav files which are 32kb large. 37 | 38 | ### Copyright: ### 39 | 40 | Pokémon © 2002-2015 Pokémon. © 1995-2015 Nintendo/Creatures Inc./GAME FREAK inc. TM, ® and Pokémon character names are trademarks of Nintendo. 41 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'VT323', cursive; 3 | } 4 | 5 | #container { 6 | position: relative; 7 | width: 800px; 8 | min-height: 600px; 9 | margin: 0 auto; 10 | border: #000000 solid 1px; 11 | background: url("../imgs/fallback.png") repeat; 12 | } 13 | 14 | .btn { 15 | width: 200px; 16 | height: 100px; 17 | border: #000000 outset 2px; 18 | text-align: center; 19 | line-height: 100px; 20 | border-radius: 20px; 21 | cursor: pointer; 22 | } 23 | 24 | .btn:active { 25 | border: #000000 inset 2px; 26 | } 27 | 28 | #inputLabel { 29 | margin: 10px auto; 30 | background: red; 31 | } 32 | 33 | 34 | #saveButton { 35 | background: blue; 36 | } 37 | 38 | #outputSection { 39 | width: 600px; 40 | min-height: 200px; 41 | border: #000000 groove 2px; 42 | border-radius: 20px; 43 | background: #ffffff; 44 | margin: 10px auto; 45 | padding: 10px; 46 | } 47 | 48 | #error { 49 | text-align: center; 50 | color: red; 51 | font-weight: bold; 52 | } 53 | 54 | h1 { 55 | text-align: center; 56 | font-size: 25px; 57 | } 58 | 59 | h2 { 60 | text-align: center; 61 | font-size: 20px; 62 | } 63 | 64 | #footer { 65 | text-align: center; 66 | font-size: 12px; 67 | } -------------------------------------------------------------------------------- /imgs/battletiles.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LyndonArmitage/HTML5PokemonSaveReader/c0b30d1b963cfdddb2708f38d6f6a53b56f140b2/imgs/battletiles.PNG -------------------------------------------------------------------------------- /imgs/fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LyndonArmitage/HTML5PokemonSaveReader/c0b30d1b963cfdddb2708f38d6f6a53b56f140b2/imgs/fallback.png -------------------------------------------------------------------------------- /imgs/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LyndonArmitage/HTML5PokemonSaveReader/c0b30d1b963cfdddb2708f38d6f6a53b56f140b2/imgs/tiles.png -------------------------------------------------------------------------------- /imgs/tiles0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LyndonArmitage/HTML5PokemonSaveReader/c0b30d1b963cfdddb2708f38d6f6a53b56f140b2/imgs/tiles0.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML5 Pokémon Save File Viewer 5 | 6 | 7 | 8 | 9 | 10 |

Pokémon Save File Viewer

11 |
12 |
13 |
Select Save File
14 | 15 | 16 |
17 |
18 |

Select a Save File to begin.

19 |
20 |
21 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /js/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | Background Generator 3 | 4 | This will generate a good looking background for the Save File Editor using a few sprites and a canvas 5 | **/ 6 | 7 | /** 8 | * Returns image data to be used as the background to an element 9 | * @param width 10 | * @param height 11 | */ 12 | function setBackground(element ,width, height) { 13 | 14 | function isSupported(){ 15 | var elem = document.createElement('canvas'); 16 | return !!(elem.getContext && elem.getContext('2d')); 17 | } 18 | 19 | // Returns a random integer between min and max 20 | // Using Math.round() will give you a non-uniform distribution! 21 | function getRandomInt(min, max) { 22 | return Math.floor(Math.random() * (max - min + 1)) + min; 23 | } 24 | 25 | function prob(value) { 26 | return getRandomInt(0, 100) < value; 27 | } 28 | 29 | function create2DArray(rows) { 30 | var arr = []; 31 | for (var i=0;i 0 && map[x-1][y] == ALIVE) { 49 | // left 50 | live ++; 51 | } 52 | if(y < mapHeight-1 && map[x][y+1] == ALIVE) { 53 | // up 54 | live ++; 55 | } 56 | if(y > 0 && map[x][y-1] == ALIVE) { 57 | // down 58 | live ++; 59 | } 60 | if(y < mapHeight-1 && x < mapWidth-1 && map[x+1][y+1] == ALIVE) { 61 | // top right 62 | live ++; 63 | } 64 | if(y > 0 && x < mapWidth-1 && map[x+1][y-1] == ALIVE) { 65 | // bottom right 66 | live ++; 67 | } 68 | if(y < mapHeight-1 && x > 0 && map[x-1][y+1] == ALIVE) { 69 | // top left 70 | live ++; 71 | } 72 | if(y > 0 && x > 0 && map[x-1][y-1] == ALIVE) { 73 | // bottom left 74 | live ++; 75 | } 76 | return live; 77 | } 78 | 79 | // set everything to random 80 | for(var x = 0; x < mapWidth; x ++) { 81 | for(var y = 0; y < mapHeight; y ++) { 82 | if(prob(10)) { 83 | map[x][y] = ALIVE; 84 | } 85 | else { 86 | map[x][y] = DEAD; 87 | } 88 | } 89 | } 90 | 91 | var iterations = 10; 92 | while(iterations > 0) { 93 | var newMap = create2DArray(mapWidth); 94 | for(var x = 0; x < mapWidth; x ++) { 95 | for(var y = 0; y < mapHeight; y ++) { 96 | var cell = map[x][y]; 97 | if(cell == ALIVE) { 98 | if(getLiveNeighbours(x, y) < 2) { 99 | newMap[x][y] = DEAD; 100 | } 101 | else { 102 | newMap[x][y] = ALIVE; 103 | } 104 | } 105 | else { 106 | if(getLiveNeighbours(x, y) > 2) { 107 | newMap[x][y] = ALIVE; 108 | } 109 | else { 110 | newMap[x][y] = DEAD; 111 | } 112 | } 113 | } 114 | } 115 | map = newMap; 116 | iterations --; 117 | } 118 | 119 | 120 | return map; 121 | } 122 | 123 | function drawBackground(canvas, tileMap) { 124 | var ctx = canvas.getContext("2d"); 125 | var tileSize = 8; 126 | 127 | // run the map generator 128 | var map = generateMap(canvas.width / tileSize, canvas.height / tileSize); 129 | 130 | for(var x = 0; x < canvas.width / tileSize; x ++) { 131 | for(var y = 0; y < canvas.height / tileSize; y ++) { 132 | if(map[x][y] == true) { 133 | ctx.drawImage(tileMap,16, 104, tileSize, tileSize, x*tileSize, y*tileSize, tileSize, tileSize); 134 | } 135 | else { 136 | ctx.drawImage(tileMap,104, 64, tileSize, tileSize, x*tileSize, y*tileSize, tileSize, tileSize); 137 | } 138 | } 139 | } 140 | 141 | 142 | 143 | 144 | } 145 | 146 | if(isSupported()) { 147 | var canvas = document.createElement("canvas"); 148 | canvas.width = width; 149 | canvas.height = height; 150 | var tileMap = new Image(); 151 | tileMap.onload = function() { 152 | drawBackground(canvas, this); 153 | element.style.background = "url(" + canvas.toDataURL() + ") repeat"; 154 | } 155 | tileMap.src = "imgs/tiles.png"; 156 | } 157 | else { 158 | element.style.background = "url('../imgs/fallback.png') repeat;"; 159 | } 160 | 161 | 162 | 163 | } 164 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | loadEvent(); 4 | }); 5 | 6 | function updateBG() { 7 | var $container = $("#container"); 8 | setBackground($container[0], $container.width(), $container.height()); 9 | } 10 | 11 | function loadEvent() { 12 | var $container = $("#container"); 13 | updateBG(); 14 | if(!supportsFileAPI()) { 15 | $container.html("
Your browser does not support HTML5's File API. Please update to a better browser.
"); 16 | } 17 | else { 18 | // We support HTML5 File API so let's get cooking. 19 | var $fileIn = $("#fileInput"); 20 | $fileIn.bind("change", loadFile); 21 | $("#inputLabel").bind("click", function() { 22 | $fileIn.click(); 23 | }) 24 | } 25 | } 26 | 27 | /** 28 | * Function that returns true if the user's web browser supports the HTML5 File API 29 | * @returns {boolean} 30 | */ 31 | function supportsFileAPI() { 32 | return window.File && window.FileReader && window.FileList && window.Blob; 33 | } 34 | 35 | /** 36 | * Ends with method for checking if a string ends with a bit of text 37 | * @param suffix 38 | * @returns {boolean} 39 | */ 40 | String.prototype.endsWith = function(suffix) { 41 | return this.indexOf(suffix, this.length - suffix.length) !== -1; 42 | }; 43 | 44 | function error(msg) { 45 | $("#outputSection").html("
"+msg+"
"); 46 | } 47 | 48 | function readFile(savefile) { 49 | var reader = new FileReader(); 50 | 51 | reader.onload = (function(theFile) { 52 | return function(e) { 53 | $("#outputSection").html("

"+ theFile.name +" loaded

"); 54 | var results = parseSav(e.target.result); 55 | if(results.checksum === 0) { 56 | error("Invalid save file provided. Is it a Generation I save file?"); 57 | } 58 | else { 59 | console.log(results); 60 | var resultsContents = ""; 132 | $("#outputSection").append(resultsContents); 133 | 134 | } 135 | updateBG(); 136 | }; 137 | })(savefile); 138 | reader.readAsBinaryString(savefile); 139 | } 140 | 141 | function loadFile(evt) { 142 | evt = evt.originalEvent || evt; 143 | var savefile = evt.target.files[0]; 144 | // save files are 32kb large and end in .sav 145 | if(savefile != null && savefile.size >= 32768 && savefile.name.endsWith(".sav")) { 146 | readFile(savefile); 147 | } 148 | else { 149 | error("Invalid save file provided. File size or extension is wrong."); 150 | } 151 | } -------------------------------------------------------------------------------- /js/sav_parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | JavaScript Based Pokemon Save File viewer by Lyndon Armitage 2013 4 | --- 5 | This will make use of HTML5's File API so will not work in older browsers. 6 | 7 | */ 8 | 9 | /** 10 | * Save Data Parser.
11 | * Will Take in a binary string and return an object representing the save data gleamed from it.
12 | * Currently only supports Generation 1 save files (Yellow, Red and Blue) 13 | * @param data 14 | * @returns {{trainerName: *, rivalName: *, trainerID: *, timePlayed: *, pocketItemList: *, PCItemList: *, checksum: *}} 15 | */ 16 | function parseSav(data) { 17 | // lets test getting the trainer name data 18 | // Currently just output the hex, it works so next step is to translate into text 19 | function getTrainerName() { 20 | var offset = 0x2598; // trainer name offset 21 | var size = 8; 22 | return getTextString(offset, size); 23 | } 24 | 25 | function getRivalName() { 26 | var offset = 0x25F6; // rival name offset 27 | var size = 8; 28 | return getTextString(offset, size); 29 | } 30 | 31 | function getTrainerID() { 32 | var offset = 0x2605; 33 | var size = 2; 34 | return hex2int(offset, size); 35 | } 36 | 37 | function getTimePlayed() { 38 | var offset = 0x2CED; 39 | return { 40 | hours : hex2int(offset, 1), 41 | minutes : hex2int(offset+2, 1), 42 | seconds : hex2int(offset+3, 1), 43 | frames : hex2int(offset+4, 1) 44 | }; 45 | } 46 | 47 | function getPocketItemList() { 48 | // Can hold 20 items making the size 42 because: Capacity * 2 + 2 = 42 49 | return getItemList(0x25C9, 20); 50 | } 51 | 52 | function getPCItemList() { 53 | // Can hold 50 items making the size 102 because: Capacity * 2 + 2 = 102 54 | // Offset seems to be 0x27E6 not 0x27E7 as said on bulbapedia 55 | return getItemList(0x27E6, 50); 56 | } 57 | 58 | function getChecksum() { 59 | return hex2int(0x3523, 1); 60 | } 61 | 62 | function getMoney() { 63 | // makes use of packed BCD 64 | // Represents how much money the character has. 65 | // The figure is a 6-digit number, 2 digits per byte, encoded as binary-coded decimal, 66 | // where each digit is allocated a full 4 bits. 67 | var offset = 0x25F3; 68 | var size = 3; // 3 bytes because each digit comes from a nibble 69 | var out = ""; 70 | var shouldAdd = false; 71 | for(var i = 0; i < size; i ++) { 72 | var byteVal = data.charCodeAt(offset+i); 73 | var digit1 = byteVal >> 4; 74 | var digit2 = byteVal & 0xF; 75 | // Check if we should add 0s (for middle of number) 76 | if(shouldAdd || digit1 > 0) { 77 | out += digit1; 78 | shouldAdd = true; 79 | } 80 | if(shouldAdd || digit2 > 0) { 81 | out += digit2; 82 | shouldAdd = true; 83 | } 84 | } 85 | return out; 86 | } 87 | 88 | function getCurrentPCBox() { 89 | return lowNibble(hex2int(0x284C, 1)) + 1; 90 | } 91 | 92 | function getSeenList() { 93 | return getPokedexList(0x25B6); 94 | } 95 | 96 | function getOwnedList() { 97 | return getPokedexList(0x25A3); 98 | } 99 | 100 | function getPlayerPosition() { 101 | var obj = { 102 | x : hex2int(0x260E, 1), 103 | y : hex2int(0x260D, 1), 104 | mapNum : hex2int(0x260A, 1) 105 | }; 106 | return obj; 107 | } 108 | 109 | function getPartyList() { 110 | return getPokemonList(0x2F2C, 6, true); 111 | } 112 | 113 | function getBoxList(boxNumber) { 114 | var boxMap = { 115 | 1 : 0x4000, 116 | 2 : 0x4462, 117 | 3 : 0x48C4, 118 | 4 : 0x4D26, 119 | 5 : 0x5188, 120 | 6 : 0x55EA, 121 | 7 : 0x6000, 122 | 8 : 0x6462, 123 | 9 : 0x68C4, 124 | 10 : 0x6D26, 125 | 11 : 0x7188, 126 | 12 : 0x75EA 127 | }; 128 | var offset = boxMap[boxNumber]; 129 | console.log("! " + offset.toString(16)); 130 | return getPokemonList(offset, 20, false); 131 | } 132 | 133 | function getCurrentBoxList() { 134 | return getPokemonList(0x30C0, 20, false); 135 | } 136 | 137 | function getPokemonList(offset, capacity, isParty) { 138 | var size = isParty ? 44 : 33; 139 | var totalSize = capacity * (size + 23) + 2; 140 | var count = hex2int(offset, 1); 141 | 142 | var list = { 143 | count : hex2int(offset, 1), 144 | species : [], 145 | pokemon : [], 146 | OTNames : [], 147 | names : [] 148 | }; 149 | 150 | // we will use count here as it tells us the amount actually present so we don't need to look for 0xFF 151 | for(var i = 0; i < count; i ++) { 152 | list.species[i] = getSpeciesFromIndex(hex2int(offset + 1 + i, 1)); 153 | list.pokemon[i] = new Pokemon((offset + 2 + capacity) + (i * size), isParty) 154 | list.OTNames[i] = getTextString( ((offset + 2 + capacity) + ((capacity-1) * size)) + size + (i*11) , 10); 155 | list.names[i] = getTextString( (((offset + 2 + capacity) + ((capacity-1) * size)) + size + ((capacity-1)*11)) + ((i+1)*11) , 10); 156 | } 157 | 158 | return list; 159 | } 160 | 161 | /** 162 | * Returns a binary string where a 1 represents that the Pokemon with that index is present. 163 | * @param offset 164 | * @returns {string} 165 | */ 166 | function getPokedexList(offset) { 167 | var length = 19; // 19 bytes long = 152 possible bits 168 | var binaryString = ""; 169 | for(var i = length; i > 0; i --) { 170 | var str = data.charCodeAt(offset + i).toString(2); 171 | var padding = ""; 172 | if(str.length < 8) { 173 | var dif = 8 - str.length; 174 | while(dif > 0) { 175 | padding += "0"; 176 | dif --; 177 | } 178 | } 179 | binaryString += str + padding; 180 | } 181 | return binaryString.split("").reverse().join(""); 182 | } 183 | 184 | function getItemList(offset, maxSize) { 185 | 186 | // Two extra bytes: 187 | // The byte at offset 0x00 is the total Count of different items in the list 188 | // The byte following the last item entry, according to Count, must always be a terminator, which is byte value 0xFF. 189 | 190 | var obj = { 191 | count : hex2int(offset, 1), 192 | items : [] 193 | }; 194 | 195 | offset ++; 196 | // Each entry is 2 bytes large, first byte is the Count of that item, second is it's Index 197 | for(var i = 0; i < obj.count && i < maxSize; i ++) { 198 | var itemIndex = hex2int(offset+(i*2), 1); 199 | var itemCount = hex2int(offset+(i*2)+1, 1); 200 | var name = getItemNameFromHexIndex(data.charCodeAt(offset+(i*2))); 201 | obj.items.push({ 202 | count : itemCount, 203 | index : itemIndex, 204 | name : name 205 | }); 206 | } 207 | 208 | return obj; 209 | } 210 | 211 | function getTextString(offset, size) { 212 | var output = ""; 213 | for(var i = 0; i < size; i ++) { 214 | var code = data.charCodeAt(offset + i); 215 | if(code == 0x50) break; // terminate string 216 | output += getChar(code); 217 | } 218 | return output; 219 | } 220 | 221 | function getChar(hex) { 222 | var charMap = { 223 | 0x50 : "\0", 0x7F : " ", 224 | 225 | 0x80 : "A", 0x81 : "B", 0x82 : "C", 0x83 : "D", 0x84 : "E", 226 | 0x85 : "F", 0x86 : "G", 0x87 : "H", 0x88 : "I", 0x89 : "J", 227 | 0x8A : "K", 0x8B : "L", 0x8C : "M", 0x8D : "N", 0x8E : "O", 228 | 0x8F : "P", 0x90 : "Q", 0x91 : "R", 0x92 : "S", 0x93 : "T", 229 | 0x94 : "U", 0x95 : "V", 0x96 : "W", 0x97 : "X", 0x98 : "Y", 230 | 0x99 : "Z", 0x9A : "(", 0x9B : ")", 0x9C : ":", 0x9D : ";", 231 | 0x9E : "[", 0x9F : "]", 232 | 233 | 0xA0 : "a", 0xA1 : "b", 0xA2 : "c", 0xA3 : "d", 0xA4 : "e", 234 | 0xA5 : "f", 0xA6 : "g", 0xA7 : "h", 0xA8 : "i", 0xA9 : "j", 235 | 0xAA : "k", 0xAB : "l", 0xAC : "m", 0xAD : "n", 0xAE : "o", 236 | 0xAF : "p", 0xB0 : "q", 0xB1 : "r", 0xB2 : "s", 0xB3 : "t", 237 | 0xB4 : "u", 0xB5 : "v", 0xB6 : "w", 0xB7 : "x", 0xB8 : "y", 238 | 0xB9 : "z", 239 | 240 | 0xE1 : "PK", 0xE2 : "MN", 0xE3 : "-", 241 | 0xE6 : "?", 0xE7 : "!", 0xE8 : ".", 242 | 243 | 0xF1 : "*", 244 | 0xF3 : "/", 0xF4 : ",", 245 | 246 | 0xF6 : "0", 0xF7 : "1", 0xF8 : "2", 0xF9 : "3", 0xFA : "4", 247 | 0xFB : "5", 0xFC : "6", 0xFD : "7", 0xFE : "8", 0xFF : "9" 248 | }; 249 | return charMap[hex]; 250 | } 251 | 252 | function getItemNameFromHexIndex(hex) { 253 | var itemMap = { 254 | 0x00 : "Nothing", 0x01 : "Master Ball", 0x02 : "Ultra Ball", 0x03 : "Great Ball", 0x04 : "Poké Ball", 255 | 0x05 : "Town Map", 0x06 : "Bicycle", 0x07 : "?????", 0x08 : "Safari Ball", 0x09 : "Pokédex", 256 | 0x0A : "Moon Stone", 0x0B : "Antidote", 0x0C : "Burn Heal", 0x0D : "Ice Heal", 0x0E : "Awakening", 257 | 0x0F : "Parlyz Heal", 0x10 : "Full Restore", 0x11 : "Max Potion", 0x12 : "Hyper Potion", 0x13 : "Super Potion", 258 | 0x14 : "Potion", 0x15 : "BoulderBadge", 0x16 : "CascadeBadge", 0x17 : "ThunderBadge", 0x18 : "RainbowBadge", 259 | 0x19 : "SoulBadge", 0x1A : "MarshBadge", 0x1B : "VolcanoBadge", 0x1C : "EarthBadge", 0x1D : "Escape Rope", 260 | 0x1E : "Repel", 0x1F : "Old Amber", 0x20 : "Fire Stone", 0x21 : "Thunderstone", 0x22 : "Water Stone", 261 | 0x23 : "HP Up", 0x24 : "Protein", 0x25 : "Iron", 0x26 : "Carbos", 0x27 : "Calcium", 262 | 0x28 : "Rare Candy", 0x29 : "Dome Fossil", 0x2A : "Helix Fossil", 0x2B : "Secret Key", 0x2C : "?????", 263 | 0x2D : "Bike Voucher", 0x2E : "X Accuracy", 0x2F : "Leaf Stone", 0x30 : "Card Key", 0x31 : "Nugget", 264 | 0x32 : "PP Up", 0x33 : "Poké Doll", 0x34 : "Full Heal", 0x35 : "Revive", 0x36 : "Max Revive", 265 | 0x37 : "Guard Spec.", 0x38 : "Super Repel", 0x39 : "Max Repel", 0x3A : "Dire Hit", 0x3B : "Coin", 266 | 0x3C : "Fresh Water", 0x3D : "Soda Pop", 0x3E : "Lemonade", 0x3F : "S.S. Ticket", 0x40 : "Gold Teeth", 267 | 0x41 : "X Attack", 0x42 : "X Defend", 0x43 : "X Speed", 0x44 : "X Special", 0x45 : "Coin Case", 268 | 0x46 : "Oak's Parcel", 0x47 : "Itemfinder", 0x48 : "Silph Scope", 0x49 : "Poké Flute", 0x4A : "Lift Key", 269 | 0x4B : "Exp. All", 0x4C : "Old Rod", 0x4D : "Good Rod", 0x4E : "Super Rod", 0x4F : "PP Up", 270 | 0x50 : "Ether", 0x51 : "Max Ether", 0x52 : "Elixir", 0x53 : "Max Elixir" 271 | }; 272 | // Add all 5 of the HMs 273 | for(var i = 0; i < 5; i ++) { 274 | itemMap[0xC4+i] = "HM0" + (1+i); 275 | } 276 | // Add all 55 og the TMs 277 | for(i = 0; i < 55; i ++) { 278 | var num = (1+i); 279 | if(num < 10) num = "0" + num; 280 | itemMap[0xC9+i] = "TM" + num; 281 | } 282 | return itemMap[hex]; 283 | } 284 | 285 | /** 286 | * Returns how friendly Pikachu is to you 287 | * @returns {*} 288 | */ 289 | function getPikachuFriendship() { 290 | return hex2int(0x271C, 1); 291 | } 292 | 293 | function hex2int(offset, size) { 294 | var val = ""; 295 | for(var i = 0; i < size; i ++) { 296 | var d = data.charCodeAt(offset + i).toString(16); 297 | if(d.length < 2) d = "0" + d; // append leading 0, should not break anything from previous version 298 | val += d; 299 | } 300 | return parseInt(val, 16); 301 | } 302 | 303 | function lowNibble(val) { 304 | return val & 0x0F; 305 | } 306 | 307 | function highNibble(val) { 308 | return (val >> 4) & 0x0F; 309 | } 310 | 311 | function getPokemonType(index) { 312 | var types = { 313 | 0x00: "Normal", 314 | 0x01: "Fighting", 315 | 0x02: "Flying", 316 | 0x03: "Poison", 317 | 0x04: "Ground", 318 | 0x05: "Rock", 319 | 0x07: "Bug", 320 | 0x08: "Ghost", 321 | 0x14: "Fire", 322 | 0x15: "Water", 323 | 0x16: "Grass", 324 | 0x17: "Electric", 325 | 0x18: "Psychic", 326 | 0x19: "Ice", 327 | 0x1A: "Dragon" 328 | }; 329 | return types[index]; 330 | } 331 | 332 | function getSpeciesFromIndex(index) { 333 | var species = { 334 | 0x01: " Rhydon", 0x02: "Kangaskhan", 0x03: "Nidoran♂", 0x04: "Clefairy", 0x05: "Spearow", 335 | 0x06: "Voltorb", 0x07: "Nidoking", 0x08: "Slowbro", 0x09: "Ivysaur", 0x0A: "Exeggutor", 336 | 0x0B: "Lickitung", 0x0C: "Exeggcute", 0x0D: "Grimer", 0x0E: "Gengar", 0x0F: "Nidoran♀", 337 | 0x10: "Nidoqueen", 0x11: "Cubone", 0x12: "Rhyhorn", 0x13: "Lapras", 0x14: "Arcanine", 338 | 0x15: "Mew", 0x16: "Gyarados", 0x17: "Shellder", 0x18: "Tentacool", 0x19: "Gastly", 339 | 0x1A: "Scyther", 0x1B: "Staryu", 0x1C: "Blastoise", 0x1D: "Pinsir", 0x1E: "Tangela", 340 | 0x1F: "Missingno.", 0x20: "Missingno.", 0x21: "Growlithe", 0x22: "Onix", 0x23: "Fearow", 341 | 0x24: "Pidgey", 0x25: "Slowpoke", 0x26: "Kadabra", 0x27: "Graveler", 0x28: "Chansey", 342 | 0x29: "Machoke", 0x2A: "Mr. Mime", 0x2B: "Hitmonlee", 0x2C: "Hitmonchan", 0x2D: "Arbok", 343 | 0x2E: "Parasect", 0x2F: "Psyduck", 0x30: "Drowzee", 0x31: "Golem", 0x32: "Missingno.", 344 | 0x33: "Magmar", 0x34: "Missingno.", 0x35: "Electabuzz", 0x36: "Magneton", 0x37: "Koffing", 345 | 0x38: "Missingno.", 0x39: "Mankey", 0x3A: "Seel", 0x3B: "Diglett", 0x3C: "Tauros", 346 | 0x3D: "Missingno.", 347 | 0x3E: "Missingno.", 348 | 0x3F: "Missingno.", 349 | 0x40: "Farfetch'd", 350 | 0x41: "Venonat", 351 | 0x42: "Dragonite", 352 | 0x43: "Missingno.", 353 | 0x44: "Missingno.", 354 | 0x45: "Missingno.", 355 | 0x46: "Doduo", 356 | 0x47: "Poliwag", 357 | 0x48: "Jynx", 358 | 0x49: "Moltres", 359 | 0x4A: "Articuno", 360 | 0x4B: "Zapdos", 361 | 0x4C: "Ditto", 362 | 0x4D: "Meowth", 363 | 0x4E: "Krabby", 364 | 0x4F: "Missingno.", 365 | 0x50: "Missingno.", 366 | 0x51: "Missingno.", 367 | 0x52: "Vulpix", 368 | 0x53: "Ninetales", 369 | 0x54: "Pikachu", 370 | 0x55: "Raichu", 371 | 0x56: "Missingno.", 372 | 0x57: "Missingno.", 373 | 0x58: "Dratini", 374 | 0x59: "Dragonair", 375 | 0x5A: "Kabuto", 376 | 0x5B: "Kabutops", 377 | 0x5C: "Horsea", 378 | 0x5D: "Seadra", 379 | 0x5E: "Missingno.", 380 | 0x5F: "Missingno.", 381 | 0x60: "Sandshrew", 382 | 0x61: "Sandslash", 383 | 0x62: "Omanyte", 384 | 0x63: "Omastar", 385 | 0x64: "Jigglypuff", 386 | 0x65: "Wigglytuff", 387 | 0x66: "Eevee", 388 | 0x67: "Flareon", 389 | 0x68: "Jolteon", 390 | 0x69: "Vaporeon", 391 | 0x6A: "Machop", 392 | 0x6B: "Zubat", 393 | 0x6C: "Ekans", 394 | 0x6D: "Paras", 395 | 0x6E: "Poliwhirl", 396 | 0x6F: "Poliwrath", 397 | 0x70: "Weedle", 398 | 0x71: "Kakuna", 399 | 0x72: "Beedrill", 400 | 0x73: "Missingno.", 401 | 0x74: "Dodrio", 402 | 0x75: "Primeape", 403 | 0x76: "Dugtrio", 404 | 0x77: "Venomoth", 405 | 0x78: "Dewgong", 406 | 0x79: "Missingno.", 407 | 0x7A: "Missingno.", 408 | 0x7B: "Caterpie", 409 | 0x7C: "Metapod", 410 | 0x7D: "Butterfree", 411 | 0x7E: "Machamp", 412 | 0x7F: "Missingno.", 413 | 0x80: "Golduck", 414 | 0x81: "Hypno", 415 | 0x82: "Golbat", 416 | 0x83: "Mewtwo", 417 | 0x84: "Snorlax", 418 | 0x85: "Magikarp", 419 | 0x86: "Missingno.", 420 | 0x87: "Missingno.", 421 | 0x88: "Muk", 422 | 0x89: "Missingno.", 423 | 0x8A: "Kingler", 424 | 0x8B: "Cloyster", 425 | 0x8C: "Missingno.", 426 | 0x8D: "Electrode", 427 | 0x8E: "Clefable", 428 | 0x8F: "Weezing", 429 | 0x90: "Persian", 430 | 0x91: "Marowak", 431 | 0x92: "Missingno.", 432 | 0x93: "Haunter", 433 | 0x94: "Abra", 434 | 0x95: "Alakazam", 435 | 0x96: "Pidgeotto", 436 | 0x97: "Pidgeot", 437 | 0x98: "Starmie", 438 | 0x99: "Bulbasaur", 439 | 0x9A: "Venusaur", 440 | 0x9B: "Tentacruel", 441 | 0x9C: "Missingno.", 442 | 0x9D: "Goldeen", 443 | 0x9E: "Seaking", 444 | 0x9F: "Missingno.", 445 | 0xA0: "Missingno.", 446 | 0xA1: "Missingno.", 447 | 0xA2: "Missingno.", 448 | 0xA3: "Ponyta", 449 | 0xA4: "Rapidash", 450 | 0xA5: "Rattata", 451 | 0xA6: "Raticate", 452 | 0xA7: "Nidorino", 453 | 0xA8: "Nidorina", 454 | 0xA9: "Geodude", 455 | 0xAA: "Porygon", 456 | 0xAB: "Aerodactyl", 457 | 0xAC: "Missingno.", 458 | 0xAD: "Magnemite", 459 | 0xAE: "Missingno.", 460 | 0xAF: "Missingno.", 461 | 0xB0: "Charmander", 462 | 0xB1: "Squirtle", 463 | 0xB2: "Charmeleon", 464 | 0xB3: "Wartortle", 465 | 0xB4: "Charizard", 466 | 0xB5: "Missingno.", 467 | 0xB6: "Missingno.", 468 | 0xB7: "Missingno.", 469 | 0xB8: "Missingno.", 470 | 0xB9: "Oddish", 471 | 0xBA: "Gloom", 472 | 0xBB: "Vileplume", 473 | 0xBC: "Bellsprout", 474 | 0xBD: "Weepinbell", 475 | 0xBE: "Victreebel" 476 | }; 477 | 478 | return species[index]; 479 | } 480 | 481 | /** 482 | * Constructor for new Pokemon 483 | * @param startOffset 484 | * @param isPartyMember 485 | * @constructor 486 | */ 487 | function Pokemon(startOffset, isPartyMember) { 488 | this.index = hex2int(startOffset, 1); 489 | this.species = getSpeciesFromIndex(this.index); // derived from index 490 | this.currentHp = hex2int(startOffset + 0x01, 2); 491 | this.level = hex2int(startOffset + 0x03, 1); 492 | this.status = hex2int(startOffset + 0x04, 1); 493 | this.type1Index = hex2int(startOffset + 0x05, 1); 494 | this.type2Index = hex2int(startOffset + 0x06, 1); 495 | this.type1 = getPokemonType(this.type1Index); 496 | this.type2 = getPokemonType(this.type2Index); 497 | this.catchRate = hex2int(startOffset + 0x07, 1); 498 | this.move1Index = hex2int(startOffset + 0x08, 1); 499 | this.move2Index = hex2int(startOffset + 0x09, 1); 500 | this.move3Index = hex2int(startOffset + 0x0A, 1); 501 | this.move4Index = hex2int(startOffset + 0x0B, 1); 502 | this.ownerID = hex2int(startOffset + 0x0C, 2); 503 | this.exp = hex2int(startOffset + 0x0E, 3); 504 | this.hpEV = hex2int(startOffset + 0x11, 2); 505 | this.attackEV = hex2int(startOffset + 0x13, 2); 506 | this.defenseEV = hex2int(startOffset + 0x15, 2); 507 | this.speedEV = hex2int(startOffset + 0x17, 2); 508 | this.specialEV = hex2int(startOffset + 0x19, 2); 509 | this.IV = hex2int(startOffset + 0x1B, 2); 510 | this.move1PP = hex2int(startOffset + 0x1D, 1); 511 | this.move2PP = hex2int(startOffset + 0x1E, 1); 512 | this.move3PP = hex2int(startOffset + 0x1F, 1); 513 | this.move4PP = hex2int(startOffset + 0x20, 1); 514 | if(isPartyMember) { 515 | this.partyLevel = hex2int(startOffset + 0x21, 1); 516 | this.partyMaxHp = hex2int(startOffset + 0x22, 2); 517 | this.partyAttack = hex2int(startOffset + 0x24, 2); 518 | this.partyDefense = hex2int(startOffset + 0x26, 2); 519 | this.partySpeed = hex2int(startOffset + 0x28, 2); 520 | this.partySpecial = hex2int(startOffset + 0x2A, 2); 521 | this.isParty = true; 522 | } 523 | else { 524 | this.isParty = false; 525 | } 526 | } 527 | 528 | // return an object containing the data in an easy to manipulate format 529 | return { 530 | trainerName: getTrainerName(), 531 | rivalName: getRivalName(), 532 | trainerID: getTrainerID(), 533 | timePlayed : getTimePlayed(), 534 | pocketItemList : getPocketItemList(), 535 | PCItemList : getPCItemList(), 536 | checksum : getChecksum(), 537 | money : getMoney(), 538 | currentPCBox : getCurrentPCBox(), 539 | seenList : getSeenList(), 540 | ownedList : getOwnedList(), 541 | playerPosition : getPlayerPosition(), 542 | pikachuFriendship: getPikachuFriendship(), 543 | partyList : getPartyList(), 544 | currentBoxList : getCurrentBoxList() 545 | }; 546 | } -------------------------------------------------------------------------------- /utlities/css/hex.css: -------------------------------------------------------------------------------- 1 | 2 | h1 { 3 | text-align: center; 4 | } 5 | 6 | #container { 7 | width: 800px; 8 | min-height: 100px; 9 | border: #000000 solid 1px; 10 | margin: 0 auto; 11 | } 12 | 13 | #container h2 { 14 | text-align: center; 15 | } 16 | 17 | .hex abbr { 18 | border-bottom-width: 0; 19 | } 20 | 21 | 22 | #inputSection { 23 | border: #000000 solid 1px; 24 | width : 300px; 25 | background: #ffffff; 26 | } 27 | 28 | @media (min-width: 1450px) { 29 | #inputSection { 30 | position: fixed; 31 | right : 1px; 32 | } 33 | } 34 | 35 | @media (max-width: 1450px) { 36 | #inputSection { 37 | margin: 0 auto; 38 | } 39 | } 40 | 41 | 42 | #inputSection h3 { 43 | margin-bottom: 0; 44 | margin-top: 0; 45 | text-align: center; 46 | } 47 | 48 | #inputSection label, #inputSection .label { 49 | font-weight: bold; 50 | } 51 | 52 | #noteBox { 53 | width: 90%; 54 | height: 100px; 55 | } 56 | 57 | .noted { 58 | background: green; 59 | } 60 | 61 | #gotoResult { 62 | color: red; 63 | font-weight: bold; 64 | margin-left: 10px; 65 | } 66 | 67 | #selectedHex { 68 | background: yellow; 69 | } 70 | 71 | #dataTable { 72 | text-align: center; 73 | margin: 0 auto; 74 | border: #000000 solid 1px; 75 | border-collapse:collapse; 76 | } 77 | 78 | #dataTable td { 79 | border: 1px solid #000000; 80 | } 81 | -------------------------------------------------------------------------------- /utlities/hexViewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File Hex Viewer 5 | 6 | 7 | 8 | 9 |

Basic Hex Viewer

10 | 11 |
12 |

Options

13 |
14 |
15 |
16 |
Notes:
17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |

Please select a file to begin

25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /utlities/js/hexViewer.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | var $fileIn = $("#fileInput"); 4 | var $gotoBox = $("#gotoBox"); 5 | if(supportsFileAPI()) { 6 | $fileIn.bind("change", loadFile); 7 | $gotoBox.bind("keypress", searchTable); 8 | if(supportsStorage()) { 9 | $("#saveNotes").bind("click", saveNotes); 10 | $("#noteBox").bind("blur", setNote); 11 | $("#noteBox").bind("keypress", function(e) { 12 | if(e.keyCode === 13) { 13 | setNote(e); 14 | } 15 | }); 16 | $("#clearNotes").bind("click", clearNotes); 17 | $(window).on('beforeunload', saveNotes); // save when they close or navigate away 18 | } 19 | else { 20 | $("#notesSection").remove(); 21 | } 22 | } 23 | else { 24 | $("#container").html( 25 | "

Your browser does not support the HTML5 File API

" + 26 | "
This application makes use of the HTML5 File API to read local files.
" + 27 | "Please update your browser
" 28 | ); 29 | $("#inputSection").remove(); 30 | } 31 | 32 | 33 | }); 34 | 35 | /** 36 | * Function that returns true if the user's web browser supports the HTML5 File API 37 | * @returns {boolean} 38 | */ 39 | function supportsFileAPI() { 40 | return window.File && window.FileReader && window.FileList && window.Blob; 41 | } 42 | 43 | /** 44 | * Function to check if Storage API is supported 45 | * @returns {boolean} 46 | */ 47 | function supportsStorage() { 48 | return window.localStorage != null && window.sessionStorage != null; 49 | } 50 | 51 | function loadFile(evt) { 52 | evt = evt.originalEvent || evt; 53 | var file = evt.target.files[0]; 54 | if(file != null) { 55 | openFile(file); 56 | } 57 | } 58 | 59 | function openFile(file) { 60 | var reader = new FileReader(); 61 | reader.onload = (function(theFile) { 62 | return function(e) { 63 | var container = document.getElementById("container"); 64 | container.innerHTML = ""; 65 | createDataTable(e.target.result, "Contents of " + theFile.name, container); 66 | if(supportsStorage()) { 67 | loadNotes(theFile.name); 68 | changeNote(null); 69 | } 70 | }; 71 | })(file); 72 | reader.readAsBinaryString(file); 73 | } 74 | 75 | function int2HexString(i) { 76 | var str = i.toString(16); 77 | if(str.length < 2) { 78 | str = "0" + str; 79 | } 80 | return str.toUpperCase(); 81 | } 82 | 83 | function createDataTable(data, heading, container) { 84 | var headingEl = document.createElement("h2"); 85 | headingEl.id = "dataHeading"; 86 | headingEl.appendChild(document.createTextNode(heading)) 87 | container.appendChild(headingEl); 88 | 89 | var tableEl = document.createElement("table"); 90 | tableEl.id = "dataTable"; 91 | var topRow = document.createElement("tr"); 92 | var topLeftTh = document.createElement("th"); 93 | tableEl.appendChild(topRow); 94 | topRow.appendChild(topLeftTh); 95 | var columns = 16; 96 | for(var i = 0; i < columns; i ++) { 97 | var th = document.createElement("th"); 98 | th.innerHTML = int2HexString(i); 99 | topRow.appendChild(th); 100 | } 101 | 102 | function getHex(offset) { 103 | return int2HexString(data.charCodeAt(offset)); 104 | } 105 | 106 | var count = 0; 107 | while(count < data.length) { 108 | var tr = document.createElement("tr"); 109 | var th = document.createElement("th"); 110 | th.innerHTML = int2HexString(count); 111 | tr.appendChild(th); 112 | for(var i = 0;i < columns; i ++) { 113 | var td = document.createElement("td"); 114 | td.className = "hex"; 115 | td.setAttribute("pos", int2HexString(count)); 116 | td.innerHTML = "" + getHex(count) + ""; 117 | tr.appendChild(td); 118 | count ++; 119 | if(count >= data.length) break; 120 | } 121 | tableEl.appendChild(tr); 122 | } 123 | 124 | container.appendChild(tableEl); 125 | 126 | $(".hex").bind("click", function() { 127 | var $abbr = $(this).find("abbr"); 128 | selectHex($abbr[0]); 129 | changeNote($abbr[0]); 130 | }) 131 | } 132 | 133 | function selectHex(element) { 134 | var previous = document.getElementById("selectedHex"); 135 | if(previous != null) { 136 | previous.id = ""; 137 | } 138 | if(element != null) { 139 | element.id = "selectedHex"; 140 | var gotoBox = document.getElementById("gotoBox"); 141 | gotoBox.value = element.getAttribute("name"); 142 | document.getElementById("gotoResult").innerHTML = "Found"; 143 | } 144 | setDebugDiv(element); 145 | } 146 | 147 | function setDebugDiv(element) { 148 | var $debugDiv = $("#debugData"); 149 | function hexString2ascii(hex) { 150 | var str = ''; 151 | for (var i = 0; i < hex.length; i += 2) 152 | str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); 153 | return str; 154 | } 155 | function hexString2int(hex) { 156 | var str = ''; 157 | for (var i = 0; i < hex.length; i += 2) 158 | str += parseInt(hex.substr(i, 2), 16); 159 | return str; 160 | } 161 | function hexString2binary(hex) { 162 | return parseInt(hex, 16).toString(2); 163 | } 164 | if(element != null) { 165 | $debugDiv.html( 166 | "As Text: "+ hexString2ascii(element.innerHTML) +"
" + 167 | "As Integer: "+ hexString2int(element.innerHTML) +"
" + 168 | "As Binary: "+ hexString2binary(element.innerHTML) +"
" 169 | ); 170 | } 171 | else { 172 | $debugDiv.html(""); 173 | } 174 | } 175 | 176 | function searchTable(event) { 177 | if(event.keyCode === 13) { 178 | var previous = document.getElementById("selectedHex"); 179 | if(previous != null) { 180 | previous.id = ""; 181 | } 182 | var elements = document.getElementsByName(event.target.value); 183 | if(elements.length > 0) { 184 | elements[0].scrollIntoView(true); 185 | elements[0].id = "selectedHex"; 186 | changeNote(elements[0]); 187 | document.getElementById("gotoResult").innerHTML = "Found"; 188 | setDebugDiv(elements[0]); 189 | } 190 | else { 191 | document.getElementById("gotoResult").innerHTML = "Not Found"; 192 | setDebugDiv(null); 193 | } 194 | } 195 | } 196 | 197 | function changeNote(element) { 198 | if(element != null) { 199 | $("#noteBox").val(element.getAttribute("note")); 200 | } 201 | else { 202 | $("#noteBox").val(""); 203 | } 204 | } 205 | 206 | function setNote() { 207 | var fileInput = document.getElementById("fileInput"); 208 | if(fileInput.files.length > 0) { 209 | var txt = document.getElementById("noteBox").value; 210 | var gotoBox = document.getElementById("gotoBox"); 211 | var elementName = gotoBox.value; 212 | var $abbr = $(".hex abbr[name="+ elementName +"]"); 213 | if(txt != null && txt.length > 0) { 214 | $abbr.attr("note", txt); 215 | $abbr.addClass("noted"); 216 | } 217 | else { 218 | $abbr.removeAttr("note"); 219 | $abbr.removeClass("noted"); 220 | } 221 | } 222 | } 223 | 224 | function saveNotes() { 225 | var fileInput = document.getElementById("fileInput"); 226 | if(fileInput.files.length > 0) { 227 | var filename = fileInput.files[0].name; 228 | var $abbrs = $(".hex abbr.noted"); 229 | if($abbrs.size() > 0) { 230 | var notes = {}; 231 | $abbrs.each(function() { 232 | var $abbr = $(this); 233 | if($abbr.attr("note") != null) { 234 | notes[$abbr.attr("name")] = $abbr.attr("note"); 235 | } 236 | }); 237 | console.log("Notes saved"); 238 | localStorage.setItem(filename, JSON.stringify(notes)); 239 | } 240 | else { 241 | localStorage.removeItem(filename); 242 | } 243 | 244 | } 245 | } 246 | 247 | function loadNotes(filename) { 248 | var notes = JSON.parse(localStorage.getItem(filename)); 249 | if(notes != null) { 250 | for(var key in notes) { 251 | var $abbr = $(".hex abbr[name="+ key +"]"); 252 | $abbr.attr("note", notes[key]); 253 | $abbr.addClass("noted"); 254 | } 255 | console.log("Notes loaded"); 256 | } 257 | } 258 | 259 | function clearNotes() { 260 | var fileInput = document.getElementById("fileInput"); 261 | if(fileInput.files.length > 0) { 262 | var filename = fileInput.files[0].name; 263 | if(confirm("Continuing will delete all notes for " +filename) == false) { 264 | return; 265 | } 266 | localStorage.removeItem(filename); 267 | $(".hex abbr.noted").removeAttr("note"); 268 | $(".hex abbr.noted").removeClass("noted"); 269 | changeNote(null); 270 | } 271 | } --------------------------------------------------------------------------------