Merge branch 'master' into ROMイメージ

This commit is contained in:
kayana63
2022-09-25 05:24:10 +09:00
committed by GitHub
22 changed files with 923 additions and 254 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store
notes.txt
metadata.csv
.gb
.gbc
.map

Binary file not shown.

View File

@@ -1,19 +1,25 @@
# GBNP: Game Boy Nintendo Power ROM Builder
![GBNP logo](img/gbnp.png)
This tool can build the data necessary to flash new games to a [Nintendo Power Game Boy cart](https://en.wikipedia.org/wiki/Nintendo_Power_(cartridge)). The cartridge can store up to seven roms, or a total of 896KB of game data. For flashing, I recommend the [GBxCart](https://www.gbxcart.com/).
## Features
- Original menu built in, no need to provide ROM
- Add up to seven games depending on their sizes
- Five different font options for menu entry, including a JP font similar to the original
- Load data from a previously created rom
- Built in romhack options, including an english translation patch
- Generate single rom MAP files for 1MB games
## Credits
- GB Memory Maker: https://github.com/Infinest/GB-Memory-Binary-Maker
- Without this source, this project would have been much more difficult!
- NesDev Forum Thread: http://forums.nesdev.com/viewtopic.php?f=12&t=11453
- Pixeltype Font: https://www.dafont.com/pixeltype.font
- Early GameBoy Font: https://www.dafont.com/early-gameboy.font
- Pokemon GB Font: https://www.fontspace.com/pokemon-gb-font-f9621
- Nokia Cellphone FC Font: https://www.dafont.com/nokia-cellphone.font
- Gamer Font: https://www.dafont.com/gamer-2.font
## Ideas
- Move more templating into components
- Add ability to upload arbitrary 96x8 images for menu entries
- Save settings to local storage
- A setting to change scrolling ticker text
- Other romhacks to the menu?
- NP GBメモリ (DMG-MMSA) Font
- This is an attempt at reproducing the original font used on the GB Memory cartridge. Since the font is not stored in the rom, [Lesserkuma](https://github.com/lesserkuma) has been working to recreate the font by studying cartridge dumps. The font started life as the one used in the JP Game Boy game "Kaeru no Tame ni Kane wa Naru", and has been tweaked to closly resemble the original GB Memory font! Special thanks to Lesserkuma for their hard work!
- TWL-IRAJ-1 Font
- A font with support for narrow alphanumeric characters as well as Japanese characters including most Kanji. Suitable for long game titles. Based on a font used in Pokémon Black and White, this font was also created by [Lesserkuma](https://github.com/lesserkuma).

39
data_scripts.rb Normal file
View File

@@ -0,0 +1,39 @@
# hex to byte array JS syntax string
# INPUT = "".gsub(" ", '')
# output = "["
# INPUT.chars.each_slice(2) do |pair|
# output << "\"#{pair.join.to_i(16).to_s}\","
# end
# puts "#{output[0...-1]}]"
# RLE encode byte array
# INPUT = []
# INPUT << "TERM" # terminate the algorithm cleanly
# output = "["
# current_val = INPUT.first
# current_count = 0
# INPUT.each do |val|
# if current_val != val
# if current_count == 1
# output << "#{current_val},"
# else
# output << "[#{current_val},#{current_count}],"
# end
# current_val = val
# current_count = 0
# end
# current_count += 1
# end
# puts "#{output[0...-1]}]"
# convert metadata into title -> code map
require 'csv'
map = {}
output = "TITLE_TO_CODE = {"
CSV.foreach("metadata.csv", headers: true) do |row|
next if row['ROM Size'] =~ /M/
map[row['Internal Title']&.gsub(' ', '')&.upcase] = row['Code']
end
output += map.map { |k, v| "\"#{k}\":\"#{v}\"" }.join(',')
output += '};'
puts output

BIN
font/DMG-MMSA.woff Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
font/Pokemon.woff Normal file

Binary file not shown.

BIN
font/TWL-IRAJ-1.woff Normal file

Binary file not shown.

79
games.html Normal file
View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game Boy Nintendo Power ROM Builder</title>
<link rel="shortcut icon" type="image/png" href="img/favicon.png"/>
<meta name="description" content="GBNP is a tool to create new ROMs for the Nintendo Power GB Memory cartridge.">
<meta property="og:description" content="GBNP is a tool to create new ROMs for the Nintendo Power GB Memory cartridge." />
<meta property="og:image" content="https://orangeglo.github.io/gbnp/img/gbnp.png" />
<meta property="og:url" content="https://orangeglo.github.io/gbnp/" />
<meta property="og:site_name" content="Game Boy Nintendo Power Cartridge Builder" />
<meta property="og:type" content="website" />
<link rel="stylesheet" type="text/css" href="style/style.css" />
</head>
<body>
<h1><img src="img/gbnp.png" alt="GBNP logo" title="GAME BOY Nintendo Power ROM Builder" width="250"/></h1>
<div class='text-container'>
<a href="index.html">Back to ROM Builder</a><br><br>
Here are some favorite/well-known games that fit nicely on the Nintendo Power Cart. Remember, you get 7 blocks total!
</div>
<div class="flex-row">
<div class="games-flex-item">
<h2>4 Blocks (512KB)</h2>
<section>
<ul>
<li>Donkey Kong</li>
<li>Donkey Kong Land 1, 2, & 3</li>
<li>Kirby's Dream Land 2</li>
<li>Mario Picross 2</li>
<li>Mega Man IV & V</li>
<li>Mole Mania</li>
<li>Pokemon Red/Green (JP Only)</li>
<li>Super Mario Land 2 & 3</li>
<li>Tetris DX</li>
<li>Zelda: Link's Awakening (Not DX)</li>
</ul>
</section>
</div>
<div class="games-flex-item">
<h2>2 Blocks (256KB)</h2>
<section>
<ul>
<li>Bionic Commando</li>
<li>Kid Dracula</li>
<li>Kirby's Dream Land</li>
<li>Mario's Picross</li>
<li>Mega Man I, II, & III</li>
<li>Metroid II: Return of Samus</li>
<li>Trip World</li>
</ul>
</section>
</div>
<div class="games-flex-item">
<h2>1 Block (128KB or less)</h2>
<section>
<ul>
<li>Cave Noire</li>
<li>Dr. Mario</li>
<li>F-1 Race</li>
<li>Gargoyle's Quest</li>
<li>Solar Striker</li>
<li>Super Mario Land</li>
<li>Tetris</li>
</ul>
</section>
</div>
</div>
<footer>
<a href="https://github.com/orangeglo/gbnp">view this project on github</a>
-
by orangeglo (orangeglo#6197 on Discord)
-
<a href="https://orangeglo.github.io/">more tools @ orangeglo.github.io</a>
</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
img/gbnp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 33 KiB

BIN
img/ticker_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -3,55 +3,45 @@
<head>
<meta charset="UTF-8">
<title>Game Boy Nintendo Power ROM Builder</title>
<link rel="shortcut icon" type="image/png" href="img/favicon.png"/>
<meta name="description" content="GBNP is a tool to create new ROMs for the Nintendo Power GB Memory cartridge.">
<meta property="og:description" content="GBNP is a tool to create new ROMs for the Nintendo Power GB Memory cartridge." />
<meta property="og:image" content="https://orangeglo.github.io/gbnp/img/gbnp.png" />
<meta property="og:url" content="https://orangeglo.github.io/gbnp/" />
<meta property="og:site_name" content="Game Boy Nintendo Power Cartridge Builder" />
<meta property="og:type" content="website" />
<meta name="theme-color" content="#FF8800" />
<script src="script/vue.min.js"></script>
<script id="devload" src="script/devload.js"></script>
<script src="script/patches.js"></script>
<script src="script/metadata.js"></script>
<script src="script/gbnp.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />
<link rel="stylesheet" type="text/css" href="style/style.css" />
</head>
<body>
<h1>GAME BOY Nintendo Power ROM Builder</h1>
<h1><img src="img/gbnp.png" alt="GBNP logo" title="GAME BOY Nintendo Power ROM Builder" width="250"/></h1>
<div class='text-container'>
This tool can build the data necessary to flash new games to a <a href="https://en.wikipedia.org/wiki/Nintendo_Power_(cartridge)">Nintendo Power Game Boy cart</a>. The cartridge can store up to seven roms, or a total of 896KB of game data. For flashing, I recommend the <a href="https://www.gbxcart.com/">GBxCart</a>.
This tool can build the data necessary to flash new games to a <a href="https://en.wikipedia.org/wiki/Nintendo_Power_(cartridge)">Nintendo Power Game Boy cart</a>, as well as the <a href="https://shop.insidegadgets.com/product/gameboy-1mb-128kb-sram-power-cart-multi-game-and-multi-save/">insideGadgets Power Cart</a>. The menu can have up to seven roms, or a total of 896KB of game data. You can also flash a single 1MB ROM, like <a href="https://www.littlesounddj.com">LSDJ</a>, using the generated MAP file. For flashing, I recommend the <a href="https://www.gbxcart.com/">GBxCart</a>.
<br>
<br>
Not sure what to flash? Check out <a href="games.html">this list of favorite/well-known games that fit into the menu</a>!
</div>
<noscript>
Looks like you have JS disabled. This tool needs javascript to function!
</noscript>
<div id="app">
<h2>
1. Upload Menu ROM
<span v-if="menu.ready()" style="color: green"></span>
<span v-if="!menu.ready()" style="color: red"></span>
</h2>
<h2>1. Add Game ROMs</h2>
<section>
<div v-if="!menu.present()">
<p>Menu is not loaded. Please upload GB Memory Multi Menu (NP M-MENU) ROM.</p>
</div>
<div v-if="menu.present() && !menu.valid()">
<p>Menu is not valid. Please upload GB Memory Multi Menu (NP M-MENU) ROM.</p>
</div>
<div v-if="menu.loadedFromStorage && menu.valid()">
<p>Menu loaded from storage! You're all set.</p>
</div>
<div v-if="!menu.loadedFromStorage && menu.ready()">
<p>Menu loaded and cached! You're all set.</p>
</div>
<button v-on:click="triggerAddMenuLabel" class="upload" type="button">
<label for="menuFileInput" ref="addMenuLabel" v-on:click="stopPropagation">Upload Menu</label>
</button>
<input style="display: none" id="menuFileInput" type="file" v-on:change="addMenu" accept=".gb,.gbc">
</section>
<h2>
2. Add Game ROMs
<span v-if="roms.length > 0 && !romOverflow" style="color: green"></span>
<span v-if="romOverflow"style="color: red"></span>
</h2>
<section>
<table>
<table v-if="roms.length > 0" v-on:dragover="preventDefault" v-on:drop="dropFile">
<thead>
<tr>
<th></th>
<th>
<button v-on:click="removeAllRoms" class="remove-all" :disabled="roms.length === 0" type="button" >
<label>Remove All</label>
</button>
</th>
<th>Title</th>
<th>Mapper</th>
<th>ROM</th>
@@ -65,13 +55,13 @@
<tr v-for="(rom, index) in roms" v-bind:key="index">
<td>
<button id="remove-rom" v-on:click="removeROM(index)" type="button"></button>
<button :disabled="index == 0" v-on:click="moveUp(index)" type="button"></button>
<button :disabled="index == (roms.length - 1)" v-on:click="moveDown(index)" type="button"></button>
<button :disabled="index === 0" v-on:click="moveUp(index)" type="button"></button>
<button :disabled="index === (roms.length - 1)" v-on:click="moveDown(index)" type="button"></button>
</td>
<td>{{ rom.title }}</td>
<td>{{ rom.type }}</td>
<td>{{ rom.romSizeKB() + 'k'}}</td>
<td>{{ rom.ramSizeKB() + 'k' }}</td>
<td>{{ rom.paddedRamSizeKB() + 'k' }}</td>
<td>{{ rom.paddedRomSizeKB() / 128 + ' (' + rom.paddedRomSizeKB() + 'k)'}}</td>
<td>
<input type="text" class='menu-text' :value="rom.menuText" @input="e => updateMenuText(rom, e.target.value)"/>
@@ -84,55 +74,100 @@
<tfoot>
<tr>
<td>
<button v-on:click="triggerAddRomLabel" class="upload" :disabled="!menu.ready() || romOverflow" type="button">
<label for="romFileInput" ref="addRomLabel" v-on:click="stopPropagation">Add Game</label>
<button v-on:click="triggerAddRomLabel" class="upload" :disabled="!menu.present() || romOverflow" type="button">
<label for="romFileInput" ref="addRomLabel" v-on:click="stopPropagation">Add Games</label>
</button>
<input style="display: none" id="romFileInput" type="file" v-on:change="addROM" :disabled="!menu.ready() || romOverflow" accept=".gb,.gbc" multiple>
</td>
<td></td>
<td></td>
<td>{{ processor.romTotalKB() + 'k' }}</td>
<td>{{ processor.ramUsedKB() + 'k' }}</td>
<td :class="{ overflow: romOverflow }">
<td :class="overflowClassMsg.class">
{{ processor.romUsedKB() / 128 + ' (' + processor.romUsedKB() + 'k)'}}
</td>
<td :class="{ overflow: romOverflow }">
<span v-if="romOverflow">Limit is 7 blocks</span>
<td :class="overflowClassMsg.class">
<span v-if="overflowClassMsg.msg">{{overflowClassMsg.msg}}</span>
</td>
<td></td>
</tr>
</tfoot>
</table>
<div class="drag-target" v-if="roms.length === 0" v-on:dragover="preventDefault" v-on:drop="dropFile"
v-on:dragenter="$event.target.classList.add('over')" v-on:dragleave="$event.target.classList.remove('over')"
>
<div>Drag and drop one or more ROMs to get started!</div>
<div style="color: grey">- or -</div>
<div style="padding-left: 26px;">
<button v-on:click="triggerAddRomLabel" class="upload" :disabled="!menu.present() || romOverflow" type="button">
<label for="romFileInput" ref="addRomLabel" v-on:click="stopPropagation">Add Games</label>
</button>
<input style="display: none" id="romFileInput" type="file" v-on:change="addROM" :disabled="!menu.present() || romOverflow" accept=".gb,.gbc" multiple>
<button v-on:click="triggerAddMenuLabel" class="upload" type="button">
<label for="menuFileInput" ref="addMenuLabel" v-on:click="stopPropagation">Load GBNP ROM</label>
</button>
<input style="display: none" id="menuFileInput" type="file" v-on:change="addMenu" accept=".gb,.gbc">
</div>
</div>
</section>
<h2>3. Tweak Settings </h2>
<h2>2. Tweak Settings </h2>
<section>
<div class="settings-row">
<label class="settings-label" for='cart-type'>Cartridge Type: </label>
<select id='cart-type' v-model="cartType">
<option v-bind:value="0">Nintendo GB Memory Cart</option>
<option v-bind:value="1">insideGadgets Power Cart</option>
</select>
</div>
<div class="settings-row">
<span class="settings-label">Menu Font: </span>
<input type="radio" id="font0" name="font" value="0" v-model="fontIndex">
<label for="font0" id="pixeltype" class="radio-label">Pixeltype</label>
<label for="font0" id="gameboy" class="radio-label" title="By Jimmy Campbell">Game Boy</label>
<input type="radio" id="font1" name="font" value="1" v-model="fontIndex">
<label for="font1" id="gameboy" class="radio-label">Game Boy</label>
<label for="font1" id="pokemon" class="radio-label" title="By Jackster Productions">POKéMON</label>
<input type="radio" id="font2" name="font" value="2" v-model="fontIndex">
<label for="font2" id="nokia" class="radio-label">Nokia</label>
<label for="font2" id="nokia" class="radio-label" title="By Zeh Fernando">Nokia</label>
<input type="radio" id="font3" name="font" value="3" v-model="fontIndex">
<label for="font3" id="gamer" class="radio-label">Gamer</label>
<label for="font3" id="gbmem" class="radio-label" title="By Lesserkuma">NP GBメモリ</label>
<input type="radio" id="font4" name="font" value="4" v-model="fontIndex">
<label for="font4" id="twliraj1" class="radio-label" title="By Lesserkuma">TWL-IRAJ-1</label>
</div>
<div class="settings-row">
<label class="settings-label" id="disableCGB-label" for="disableCGB-input">Disable CGB Palette: </label>
<input id="disableCGB-input" type="checkbox" v-model:checked="disableCGB"/>
The Game Boy will load the menu (and all games) in DMG palette mode.
<span class="settings-label">Ticker Text: </span>
<div style='display: inline-block'>
<input type="radio" id="original-ticker" value="0" v-model="tickerType">
<label for="original-ticker" class="radio-label">Original Japanese Ticker</label>
<input type="radio" id="custom-ticker" value="1" v-model="tickerType">
<label for="custom-ticker" class="radio-label">Custom Ticker</label>
<ticker-settings :show="tickerType == 1" :processor="processor" :font-index="fontIndex" :fonts-loaded="fontsLoaded"></ticker-settings>
</div>
</div>
<div class="settings-row">
<label class="settings-label" id="forceDMG-label" for="forceDMG-input">Force DMG Menu/Music: </label>
<input id="forceDMG-input" type="checkbox" v-model:checked="forceDMG"/>
The more minimal DMG menu and music will be used. <a href="img/menu_comparison.png">Comparison here.</a>
<span class="settings-label">ROM Hacks: </span>
<div class="flex-row">
<div class="flex-item">
<input id="englishPatch-input" type="checkbox" v-model:checked="englishPatch"/>
<label for="englishPatch-input">Translate the menu into English.</label>
<a href="img/english_patch_comparison.png">?</a>
</div>
<div class="flex-item">
<input id="forceDMG-input" type="checkbox" v-model:checked="forceDMG"/>
<label for="forceDMG-input">Use the DMG menu and music on color systems.</label>
<a href="img/menu_comparison.png">?</a>
</div>
</div>
</div>
<div class="settings-row">
@@ -141,11 +176,19 @@
</div>
</section>
<h2>4. Download Files</h2>
<h2>3. Download Files</h2>
<section>
<a class='download-link' :download="filename + '.gb'" :href="downloadEnabled ? romData : null" type="application/octet-stream" v-on:click="downloadRomFile" :class="{ disabled: !downloadEnabled }">Download GB File</a>
/
<a class='download-link' :download="filename + '.map'" :href="downloadEnabled ? mapData : null" type="application/octet-stream" v-on:click="downloadMapFile" :class="{ disabled: !downloadEnabled }">Download MAP File</a>
<a class='download-link' :download="filename + '.gb'" :href="downloadEnabled ? romData : null" type="application/octet-stream" v-on:click="downloadRomFile" :class="{ disabled: !downloadEnabled }">Download ROM File ({{filename}}.gb)</a>
<br>
<a class='download-link' v-if="mapEnabled" :download="filename + '.map'" :href="downloadEnabled ? mapData : null" type="application/octet-stream" v-on:click="downloadMapFile" :class="{ disabled: !downloadEnabled }">Download MAP File ({{filename}}.map)</a>
<p v-if="mapEnabled" class="map-note"><span></span><span>Note: If you are using <a href="https://github.com/lesserkuma/FlashGBX">FlashGBX</a>, you do not need the MAP file<span></p>
<div v-if="singleRomMapEnabled" class="single-rom-download">
<div>If you want to flash {{singleRomFilename}} without the menu, you can use this MAP file to configure the cart to work with your ROM.</div>
<a class='download-link' :download="singleRomFilename + '.map'" :href="singleRomMapData" type="application/octet-stream" v-on:click="downloadSingleRomMapFile">
Download Single Game MAP File ({{singleRomFilename}}.map)
</a>
</div>
</section>
</div>
@@ -155,6 +198,8 @@
<a href="https://github.com/orangeglo/gbnp">view this project on github</a>
-
by orangeglo (orangeglo#6197 on Discord)
-
<a href="https://orangeglo.github.io/">more tools @ orangeglo.github.io</a>
</footer>
<script src="script/app.js"></script>

View File

@@ -1,3 +1,63 @@
const parseBool = (boolString) => {
return boolString.toLowerCase() === 'true';
};
Vue.component('bitmap-preview', {
props: ['data'],
mounted: function() {
this.renderCanvas();
},
watch: {
data: function() {
this.renderCanvas();
}
},
methods: {
renderCanvas: function() {
let canvas = this.$refs.canvas;
const ctx = canvas.getContext('2d');
ctx.putImageData(new ImageData(this.data, 128, 8), 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.scale(2, 2);
ctx.drawImage(ctx.canvas, 0, 0);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
},
template: '<canvas style="vertical-align: middle" width="256" height="16" ref="canvas"></canvas>'
});
Vue.component('ticker-settings', {
props: ['processor', 'fontsLoaded', 'fontIndex', 'show'],
data: function() {
return ({
text: "Created with GBNP on " + (new Date).toISOString().slice(0, 10)
});
},
methods: {
updateBitmap: function() {
const bitmap = (new TickerText(this.text, this.fontIndex, this.$refs.canvas)).generate()
this.processor.tickerBitmap = bitmap;
}
},
watch: {
fontIndex: function() { this.updateBitmap() },
fontsLoaded: function () { this.updateBitmap() },
text: function() { this.updateBitmap() }
},
template: `
<div :class="{ hide: !show }" class="ticker-settings">
<div>
<input style="width: 450px;" type="text" v-model="text"/>
</div>
<div style="height: 17px; background-color: black">
<img src="img/ticker_logo.png" style="height: 16px; padding-top: 1px"/>
<canvas id="ticker-canvas" ref="canvas" height="16" style="vertical-align: top"></canvas>
</div>
</div>
`
});
let app = new Vue({
el: '#app',
data: {
@@ -7,72 +67,125 @@ let app = new Vue({
filename: 'GBNP',
mapData: '',
romData: '',
singleRomMapData: '',
fontIndex: 0,
disableCGB: false,
forceDMG: false
forceDMG: false,
englishPatch: false,
cartType: 0,
tickerType: 1,
fontsLoaded: false,
},
created: function() {
this.loadSettingsFromStorage();
this.processor.menu = this.menu;
this.processor.disableCGB = this.disableCGB;
this.processor.tickerText = this.tickerText;
this.processor.forceDMG = this.forceDMG;
this.processor.englishPatch = this.englishPatch;
this.processor.tickerType = this.tickerType;
if (window.location.search.substr(1).toLowerCase() == 'ig') {
this.cartType = 1;
}
window.addEventListener('error', (e) => {
alert("Whoops, some sort of exception occured! Open an issue in the GitHub repo to tell me what happened, or message me on Discord (info in footer).")
});
},
computed: {
downloadEnabled: function() {
return (this.roms.length > 0) && this.menu.present() && this.menu.valid() && !this.romOverflow;
return this.menu.present() && !this.romOverflow;
},
romOverflow: function() { return this.processor.romOverflow(); }
mapEnabled: function() {
return this.cartType == 0;
},
romOverflow: function() { return this.processor.romOverflow(); },
overflowClassMsg: function() {
if (this.processor.roms.length === 1 && this.processor.roms[0].paddedRomSizeKB() === 1024) {
return { class: 'single-rom', msg: "Game only (no menu)" };
} else if (this.romOverflow) {
return { class: 'overflow', msg: "Limit is 7 blocks" };
}
return { class: null, msg: null };
},
singleRomMapEnabled: function() {
return this.mapEnabled
&& this.processor.roms.length === 1
&& this.processor.roms[0].paddedRomSizeKB() <= 1024;
},
singleRomFilename: function() {
return this.processor.roms[0] ? this.processor.roms[0].title : '';
}
},
watch: {
fontIndex: function() {
for (let i = 0; i < this.roms.length; i++) { this.roms[i].updateBitmap(this.fontIndex); }
},
disableCGB: function() {
this.processor.disableCGB = this.disableCGB;
},
roms: function() { this.processor.roms = this.roms; },
forceDMG: function() {
this.processor.forceDMG = this.forceDMG;
}
this.writeSettingsToStorage();
},
cartType: function() {
this.processor.cartType = this.cartType;
this.writeSettingsToStorage();
},
englishPatch: function() {
this.processor.englishPatch = this.englishPatch;
this.writeSettingsToStorage();
},
tickerType: function() {
this.processor.tickerType = this.tickerType;
this.writeSettingsToStorage();
},
fontIndex: function() {
for (let i = 0; i < this.roms.length; i++) { this.roms[i].updateBitmap(this.fontIndex); }
this.writeSettingsToStorage();
},
},
methods: {
addMenu: function(e) {
let fileReader = new FileReader()
fileReader.onload = () => this.menu.setData(fileReader.result);
fileReader.onload = () => {
const parsedRoms = this.processor.parseMenuData(fileReader.result, this.fontIndex);
if (parsedRoms.length > 0) { this.roms = parsedRoms; }
}
fileReader.readAsArrayBuffer(e.target.files[0]);
e.target.value = '';
},
addROM: function(e) {
let fileReader = new FileReader()
fileReader.onload = () => this.roms.push(new ROM(fileReader.result, this.fontIndex));
for (let i = 0; i < e.target.files.length; i++){
fileReader.readAsArrayBuffer(e.target.files[i]);
}
this.processor.roms = this.roms;
e.target.value = '';
if (e) { e.target.value = ''; }
},
removeROM: function(index) {
this.roms.splice(index, 1);
this.processor.roms = this.roms;
},
removeAllRoms: function() {
this.roms = [];
},
moveUp: function(index) {
let rom = this.roms[index];
this.roms.splice(index, 1);
this.roms.splice(index - 1, 0, rom);
this.processor.roms = this.roms;
},
moveDown: function(index) {
let rom = this.roms[index];
this.roms.splice(index, 1);
this.roms.splice(index + 1, 0, rom);
this.processor.roms = this.roms;
},
downloadMapFile: function(e) {
if (this.mapData) { URL.revokeObjectURL(this.mapData) }
this.mapData = URL.createObjectURL(new Blob([this.processor.mapData()]));
this.processor.roms = this.roms; // in case they got out of sync
if (this.cartType == 0) { // regular power cart only
if (this.mapData) { URL.revokeObjectURL(this.mapData) }
this.mapData = URL.createObjectURL(new Blob([this.processor.mapData()]));
}
},
downloadSingleRomMapFile: function(e) {
this.processor.roms = this.roms; // in case they got out of sync
if (this.cartType == 0) { // regular power cart only
if (this.singleRomMapData) { URL.revokeObjectURL(this.singleRomMapData) }
this.singleRomMapData = URL.createObjectURL(new Blob([this.processor.mapData(true)]));
}
},
downloadRomFile: function(e) {
this.processor.roms = this.roms; // in case they got out of sync
if (this.romData) { URL.revokeObjectURL(this.romData) }
this.romData = URL.createObjectURL(new Blob([this.processor.romData()]));
},
@@ -82,55 +195,34 @@ let app = new Vue({
rom.updateMenuText(val, this.fontIndex);
}, 500);
},
stopPropagation: function(e) { e.stopImmediatePropagation(); },
dropFile: function(e) {
this.addROM(null, e.dataTransfer.files);
e.target.classList.remove('over')
e.preventDefault();
},
writeSettingsToStorage: function () {
window.localStorage.setItem('fontIndex', this.fontIndex);
window.localStorage.setItem('forceDMG', this.forceDMG);
window.localStorage.setItem('englishPatch', this.englishPatch);
window.localStorage.setItem('cartType', this.cartType);
window.localStorage.setItem('tickerType', this.tickerType);
},
loadSettingsFromStorage: function() {
const fontIndex = window.localStorage.getItem('fontIndex');
const forceDMG = window.localStorage.getItem('forceDMG');
const englishPatch = window.localStorage.getItem('englishPatch');
const cartType = window.localStorage.getItem('cartType');
const tickerType = window.localStorage.getItem('tickerType');
if (fontIndex) { this.fontIndex = parseInt(fontIndex); }
if (forceDMG) { this.forceDMG = parseBool(forceDMG); }
if (englishPatch) { this.englishPatch = parseBool(englishPatch); }
if (cartType) { this.cartType = parseInt(cartType); }
if (tickerType) { this.tickerType = parseInt(tickerType); }
},
triggerAddMenuLabel: function(e) { this.$refs.addMenuLabel.click(); },
triggerAddRomLabel: function(e) { this.$refs.addRomLabel.click(); }
triggerAddRomLabel: function(e) { this.$refs.addRomLabel.click(); },
stopPropagation: function(e) { e.stopImmediatePropagation(); },
preventDefault: function(e) { e.preventDefault(); },
}
});
Vue.component('bitmap-preview', {
props: ['data', 'rom', 'index'],
mounted: function() {
this.renderCanvas();
console.log(this.rom.title);
},
watch: {
data: function() {
this.renderCanvas();
}
},
methods: {
renderCanvas: function() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext('2d');
ctx.putImageData(new ImageData(this.data, 96, 8), 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.scale(2, 2);
ctx.drawImage(ctx.canvas, 0, 0);
ctx.setTransform(1, 0, 0, 1, 0, 0);
},
addImage: function(e) {
const rom = this.rom;
const imageReader = new FileReader();
imageReader.onload = function(e) {
const img = new Image();
img.addEventListener("load", function(e) {
rom.updateBitmap(null, e.target);
});
img.src = e.target.result;
};
imageReader.readAsDataURL(e.target.files[0]);
},
triggerAddImageLabel: function () { this.$refs.addImageLabel.click(); },
stopPropagation: function(e) { app.stopPropagation(e); }
},
template: `
<div>
<canvas width="192" height="16" ref="canvas"></canvas>
<button v-on:click="triggerAddImageLabel" type="button">
<label :for="'imageFileInput-' + index" ref="addImageLabel" v-on:click="stopPropagation">Upload</label>
</button>
<input style="display: none" :id="'imageFileInput-' + index" type="file" v-on:change="addImage">
</div>`
});

View File

@@ -12,21 +12,25 @@ const BITMAP_PREVIEW_BYTES = [
[0xBB, 0xBB, 0xBB, 0xFF], // light grey
[0x66, 0x66, 0x66, 0xFF], // dark grey
[0x00, 0x00, 0x00, 0xFF] // black
]
];
const FONTS = [
{ style: 'normal 16px Pixeltype', y: 7 },
{ style: 'normal 8px Gameboy', y: 7 },
{ style: 'normal 8px PokemonGB', y: 7 },
{ style: 'normal 8px Nokia', y: 7 },
{ style: 'normal 16px Gamer', y: 7 }
]
{ style: 'normal 8px DMG-MMSA', y: 7 },
{ style: 'normal 8px TWL-IRAJ-1', y: 7 },
];
const MENU_TITLE_CHECK = 'NP M-MENU';
class Menu {
constructor() {
this.data = null;
const menuData = localStorage.getItem('menuData');
if (menuData) {
this.loadedFromStorage = true;
this.data = JSON.parse(menuData).data;
console.log("Menu data loaded from storage")
} else {
this.loadMenuDataFromScript();
}
}
@@ -34,25 +38,18 @@ class Menu {
return !!this.data
}
valid() {
if (!this._valid && this.data) {
let title = String.fromCharCode(...this.data.slice(308, 317));
this._valid = (title == 'NP M-MENU');
}
return this._valid || false;
}
ready() {
return this.present() && this.valid();
}
setData(arrayBuffer) {
this._valid = null;
this.data = new Uint8Array(arrayBuffer);
if (this.valid()) {
localStorage.setItem('menuData', JSON.stringify({ data: Array.from(this.data) }));
}
localStorage.setItem('menuData', JSON.stringify({ data: Array.from(this.data) }));
console.log("Menu data loaded from script")
}
loadMenuDataFromScript() {
const head = document.getElementsByTagName('head')[0];
const menuScript = document.createElement('script');
menuScript.src = 'script/menu.js';
head.appendChild(menuScript);
}
}
@@ -61,29 +58,43 @@ class ROM {
let file = new FileSeeker(arrayBuffer);
file.seek(0x134);
this.title = String.fromCharCode(...file.read(0xF)).replace(/\0/g, '');
this.title = String.fromCharCode(...file.read(0xF)).replace(/\0/g, '').trim();
this.menuText = this.title;
file.seek(0x143);
let cgbByte = file.readByte();
const cgbByte = file.readByte();
this.cgb = cgbByte == 0x80 || cgbByte == 0xC0;
this.code = `${cgbByte == 0xC0 ? 'CGB' : 'DMG'} -??? - `; // default code
if (typeof TITLE_TO_CODE === 'object' && TITLE_TO_CODE[this.title.replace(/ /g, '')]) {
const code = TITLE_TO_CODE[this.title.replace(/ /g, '')];
this.code = `${code.slice(0, 3)} ${code.slice(3)} - `.slice(0, 12);
}
file.seek(0x147);
this.typeByte = file.readByte();
this.type = CARTRIDGE_TYPES[this.typeByte];
this.romByte = file.readByte();
this.ramByte = file.readByte();
this.updateBitmap(fontIndex);
file.rewind();
this.arrayBuffer = new ArrayBuffer(this.paddedRomSizeKB() * 1024);
let paddedFile = new FileSeeker(this.arrayBuffer);
paddedFile.writeBytes(file.read(file.size()));
this.updateBitmap(fontIndex);
if (file.size() > this.arrayBuffer.byteLength) {
if (!this.isMenu()) {
alert(`Error with ${this.title}!\nROM header size is smaller than the file size!`);
}
this.bad = true;
return;
} else {
paddedFile.writeBytes(file.read(file.size()));
}
if (!this.valid()) { alert('File is not a valid Game Boy ROM!') }
if (!this.type) { alert('Cartridge type could not be determined!') }
if (this.ramSizeKB() > 32) { alert('Game requires more than 32 KB of RAM!') }
if (!this.valid()) { alert('File is not a valid Game Boy ROM!'); this.bad = true; }
else if (!this.type) { alert(`Error with ${this.title}!\nCartridge type could not be determined!`); this.bad = true; }
}
valid() {
@@ -111,6 +122,15 @@ class ROM {
return Math.trunc(Math.pow(4, this.ramByte - 1)) * 2;
}
paddedRamSizeKB() {
if (this.typeByte === 0x06) { return 8; }
return this.ramSizeKB();
}
isMenu() {
return this.title.includes(MENU_TITLE_CHECK);
}
updateMenuText(text, fontIndex) {
this.menuText = text;
this.updateBitmap(fontIndex);
@@ -122,7 +142,7 @@ class ROM {
// const canvas = document.createElement("canvas");
const canvas = document.getElementById("scratch-canvas");
canvas.height = 8;
canvas.width = 96;
canvas.width = 128;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
@@ -162,8 +182,8 @@ class ROM {
}
let outputBuffer = []
for (let h = 0; h < 24; h+=2) {
for (let i = h; i < 192; i+=24) {
for (let h = 0; h < 32; h+=2) {
for (let i = h; i < 256; i+=32) {
let a = buffer[i]
let b = buffer[i+1]
@@ -204,11 +224,14 @@ class ROM {
}
class Processor {
constructor(roms) {
constructor(roms, tickerText) {
this.roms = roms;
this.menu = null;
this.disableCGB = false;
this.forceDMG = false;
this.tickerBitmap = [];
this.cartType = 0;
this.englishPatch = false;
this.tickerType = 1;
}
romTotalKB() {
@@ -225,7 +248,7 @@ class Processor {
ramUsedKB() {
return this.roms.reduce((total, rom) => {
return total += rom.ramSizeKB();
return total += rom.paddedRamSizeKB();
}, 0);
}
@@ -233,34 +256,37 @@ class Processor {
return (this.romUsedKB() > 896);
}
mapData() {
mapData(singleRom = false) {
const mapBuffer = new ArrayBuffer(128);
const mapFile = new FileSeeker(mapBuffer);
// write mbc type, rom size, and ram size for menu
mapFile.writeBytes([0xA8, 0, 0]);
// write mbc type, rom size, and ram size for each rom
let romOffset = 128;
let romOffset = 0;
let ramOffset = 0;
if (!singleRom) {
// write mbc type, rom size, and ram size for menu
mapFile.writeBytes([0xA8, 0, 0]);
romOffset += 128;
}
// write mbc type, rom size, and ram size for each rom
for (let i = 0; i < this.roms.length; i++) {
let rom = this.roms[i];
let bits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // 16
// set mbc bits
// set mbc bits (bits 0-2)
let tb = rom.typeByte
if (tb >= 0x01 && tb <= 0x03) {
if (tb >= 0x01 && tb <= 0x03) { // MBC1
bits[15] = 0; bits[14] = 0; bits[13] = 1;
} else if (tb >= 0x05 && tb <= 0x06) {
} else if (tb >= 0x05 && tb <= 0x06) { // MBC2
bits[15] = 0; bits[14] = 1; bits[13] = 0;
} else if (tb >= 0x0F && tb <= 0x13 ) {
} else if (tb >= 0x0F && tb <= 0x13 ) { // MBC3
bits[15] = 0; bits[14] = 1; bits[13] = 1;
} else if (tb >= 0x19 && tb <= 0x1E ) {
bits[15] = 1; bits[14] = 0; bits[13] = 0;
} else if (tb >= 0x19 && tb <= 0x1E ) { // MBC5
bits[15] = 1; bits[14] = 0; bits[13] = 1;
}
// set rom bits
// set rom bits (bits 3-5)
let rs = rom.paddedRomSizeKB();
if (rs == 64) { // 010
bits[12] = 0; bits[11] = 1; bits[10] = 0;
@@ -274,15 +300,20 @@ class Processor {
bits[12] = 1; bits[11] = 0; bits[10] = 1;
}
// set ram bits
// set ram bits (bits 7-9) (bit 9 ORd with rom offset)
let rskb = rom.ramSizeKB();
if (rom.typeByte == 0x06) { // MBC2+BATTERY 001
bits[9] = 0; bits[8] = 0; bits[7] = 1;
} else if (rom.ramSizeKB == 0) { // No RAM 000
} else if (rskb == 0) { // No RAM 000
bits[9] = 0; bits[8] = 0; bits[7] = 0;
} else if (rom.ramSizeKB == 8) { // 8KB 010
} else if (rskb == 8) { // 8KB 010
bits[9] = 0; bits[8] = 1; bits[7] = 0;
} else if (rom.ramSizeKB >= 32) { // 32KB+ 011
} else if (rskb == 32) { // 32KB 011
bits[9] = 0; bits[8] = 1; bits[7] = 1;
} else if (rskb == 64) { // 64KB 100
bits[9] = 1; bits[8] = 0; bits[7] = 0;
} else if (rskb == 128) { // 128KB 101
bits[9] = 1; bits[8] = 0; bits[7] = 1;
} else { // < 8KB 010
bits[9] = 0; bits[8] = 1; bits[7] = 0;
}
@@ -296,7 +327,8 @@ class Processor {
// ram offset
mapFile.writeByte(Math.trunc(ramOffset / 2));
ramOffset += (rom.typeByte == 0x06 || rom.ramSizeKB() < 8) ? 8 : rom.ramSizeKB();
// this is the way it was in C# source, don't really understand why everything gets at least 8
ramOffset += (rom.typeByte === 0x06 || rom.ramSizeKB() < 8) ? 8 : rom.ramSizeKB();
}
// trailer
@@ -313,12 +345,9 @@ class Processor {
// copy menu data
romFile.writeBytes(this.menu.data);
// apply cgb hack
if (this.disableCGB) {
romFile.seek(0x143);
romFile.writeByte(0);
romFile.seek(0x14D); // fix checksum
romFile.writeByte(83);
// apply iG power cart hack
if (this.cartType == 1) {
this.patchRomHeaderForIG(romFile);
}
// apply dmg menu hack
@@ -329,18 +358,44 @@ class Processor {
romFile.writeBytes([0x3C, 0xE0, 0xFE, 0x3D]);
}
// apply english patch
if (this.englishPatch && typeof ENGLISH_PATCH_DATA === 'object') {
const englishRomPatcher = new Patcher(ENGLISH_PATCH_DATA);
englishRomPatcher.apply(romFile);
}
// ticker text
if (this.tickerType == 1) {
romFile.seek(0x18040);
romFile.writeByteUntil(0x00, 0x19140); // overwrite existing data
romFile.seek(0x18040);
romFile.writeBytes(Array.from(this.tickerBitmap));
}
// timestamp / writer id
const date = (new Date).toISOString().split('.')[0].replace('T', '').replace(/-/g, '/');
const timestampId = `${date} ORANGE `; // should be 26 bytes
romFile.seek(0x1C1BF);
romFile.writeBytes(stringToCharArray(timestampId));
let romBase = 0x01;
let romFileIndex = 0x1C200;
// disable existing entries
// wipe existing entries
for (let i = 0; i < 7; i++) {
romFile.seek(romFileIndex + i * 512);
romFile.writeByte(0xFF);
romFile.writeByteUntil(0xFF, romFileIndex + (i+1) * 512);
}
// used in loop for iG power cart hack
let iGOffsets = { rom: 8, ram: 0 };
for (let i = 0; i < this.roms.length; i++) {
const rom = this.roms[i];
romFile.seek(romFileIndex);
romFile.writeByteUntil(0, romFileIndex + 512);
romFile.seek(romFileIndex);
// rom index
romFile.writeByte(i + 1);
@@ -350,23 +405,35 @@ class Processor {
romBase += Math.trunc(rom.paddedRomSizeKB() / 128);
// sram base? (this is zero in the source)
romFile.writeByte(0)
romFile.writeByte(0);
// rom size (in 128k units)
romFile.writeByte(Math.trunc(rom.paddedRomSizeKB() / 128));
romFile.writeByte(0);
// sram size in 32b units (this is zero in the source)
romFile.writeByte(0)
romFile.writeByte(0)
// sram "block BBBBBB" flags
romFile.writeByte(rom.paddedRamSizeKB() > 0 ? 1 : 0); // true if 8 used
romFile.writeByte(rom.paddedRamSizeKB() > 8 ? 1 : 0); // true if 32 used
romFile.seek(romFileIndex + 63);
// game identifier "DMG -TRA - "
romFile.writeBytes(stringToCharArray(rom.code)); // 12 characters
// Shift-JIS title (override with ascii)
romFile.seek(romFileIndex + 19);
romFile.writeByteUntil(0, 0x1C23F) // override with zeros
romFile.seek(romFileIndex + 20); // leave first byte emtpy to identify as ascii text
romFile.writeBytes(stringToCharArray(rom.title));
// title bitmap
romFile.seek(romFileIndex + 63); // 0x1C23F
romFile.writeBytes(rom.bitmapBuffer);
console.log(romFile.position)
romFileIndex += 512
// apply iG power cart hack
if (this.cartType == 1) {
this.patchBankInfoForIG(rom, romFile, iGOffsets, i);
}
}
// write the roms
@@ -377,6 +444,207 @@ class Processor {
return new Uint8Array(romBuffer);
}
patchRomHeaderForIG(romFile) {
romFile.seek(0x130E); // For CGB+
romFile.writeBytes([0xC3, 0x00, 0x1F]); // Jump to 0x1F00
romFile.seek(0x140F); // For DMG/Pocket
romFile.writeBytes([0xC3, 0x00, 0x1F]); // Jump to 0x1F00
// Set rom bank, ram enabled/disabled, 8KB/32KB locked and ram bank
romFile.seek(0x1F00);
romFile.writeBytes([0x3e, 0x01, 0xea, 0x00, 0x20, // Set 0x2000, 1
0xf0, 0xfd, 0x01, 0x00, 0x22, 0x4f, 0x0a, 0xea, 0x00, 0x40, 0x3e, 0x00, 0xea, 0x00, 0x40, // Ram bank set bit 1+. 8KB/32KB locking = bit 0
0xf0, 0xfd, 0x01, 0x00, 0x21, 0x4f, 0x0a, 0xea, 0x00, 0x00, // Ram enable/disable
0xf0, 0xfd, 0x01, 0x00, 0x20, 0x4f, 0x0a, 0xea, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // Rom bank and restart GB
// Patch out any writes to 0x0000-0x1000
romFile.seek(0x0DDF);
romFile.writeBytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
romFile.seek(0x0E22);
romFile.writeBytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
patchBankInfoForIG(rom, romFile, offsets, i) {
// Set rom bank start
romFile.seek(0x2001 + i);
if (rom.typeByte >= 1 && rom.typeByte <= 3) {// MBC1 enabled, setting bank 0 = bank 1
romFile.writeByte(offsets.rom | 0x01);
}
else {
romFile.writeByte(offsets.rom);
}
offsets.rom += Math.trunc(rom.paddedRomSizeKB() / 16);
// Set ram state
romFile.seek(0x2101 + i);
if (rom.ramByte > 0 || rom.typeByte == 0x06) { // has ram or MBC2
romFile.writeByte(1); // Ram enabled
}
else {
romFile.writeByte(0); // Ram disabled
}
// Set ram bank info
romFile.seek(0x2201 + i);
// Ram offset and if we are 8KB or 32KB locked
if (rom.ramByte == 2 || rom.typeByte == 0x06) { // 8KB or MBC2+RAM
romFile.writeByte(offsets.ram); // 8KB locked
}
else if (rom.ramByte == 3) { // 32KB
romFile.writeByte(offsets.ram | 0x01); // 32KB locked
}
else {
romFile.writeByte(0);
}
// Increment ram offset after
if (rom.ramByte == 2 || rom.typeByte == 0x06) { // 8KB or MBC2+RAM
offsets.ram += 2;
}
else if (rom.ramByte == 3) { // 32KB
offsets.ram += 4;
}
}
parseMenuData(menuBuffer, fontIndex) {
const roms = [];
const menuFile = new FileSeeker(menuBuffer);
menuFile.seek(0x134);
const title = String.fromCharCode(...menuFile.read(0xF)).replace(/\0/g, '');
if (!title.includes(MENU_TITLE_CHECK)) {
alert(`${title} does not appear to be a menu ROM!\nDid you select a regular ROM by mistake?`);
return [];
}
let romSizes = [];
for (let i = 0; i < 7; i++) {
const indexPosition = 0x1C200 + i * 512
menuFile.seek(indexPosition);
const byte = menuFile.readByte();
if (byte > 0 && byte < 8) {
menuFile.seek(indexPosition + 3)
romSizes.push(menuFile.readByte());
}
}
if (romSizes.length > 0) {
try {
menuFile.seek(0x20000)
for (let i = 0; i < romSizes.length; i++) {
const romData = menuFile.read(romSizes[i] * 128 * 1024);
const romBuffer = (new Uint8Array(romData)).buffer;
roms.push(new ROM(romBuffer, fontIndex))
}
} catch(e) {
console.log(e)
alert("Failed to parse roms from menu file!");
}
} else {
alert("No roms detected in menu file!")
}
return roms;
}
}
class TickerText {
constructor(text, fontIndex, canvas) {
this.text = text;
this.fontIndex = fontIndex;
this.canvas = canvas;
}
generate() {
let buffer = [];
const canvas = this.canvas;
let text = this.text;
// apply transforms for JP font
if (this.fontIndex == 3) {
text = text.toUpperCase();
text = textToFullWidthPunc(text);
}
const font = FONTS[this.fontIndex];
const ctx = canvas.getContext('2d');
ctx.font = font.style;
let width = Math.ceil(ctx.measureText(text).width) + 2
for (let i = 0; i < 16; i++) {
if ((width % 16) == 0) { break; }
width++
}
width = Math.min(1024, Math.max(64, width));
canvas.width = width;
ctx.font = font.style;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.fillText(text,1,5 + font.y);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
for (let i = 0; i < imageData.length; i+=16){
let byte = 0;
for (let j = 0; j < 4; j++) {
let red = imageData[i+j*4];
if (red < 210) {
byte = byte | 0b11 << (6 - j*2);
}
}
buffer.push(byte)
}
let outputBuffer = []
for (let h = 0; h < Math.trunc(canvas.width/4); h+=2) {
for (let i = h; i < canvas.width * 4; i+=Math.trunc(canvas.width/4)) {
let a = buffer[i]
let b = buffer[i+1]
let outputA =
(a & 0b10000000) | ((a & 0b00100000) << 1) | ((a & 0b00001000) << 2) | ((a & 0b00000010) << 3) |
((b & 0b10000000) >> 4) | ((b & 0b00100000) >> 3) | ((b & 0b00001000) >> 2) | ((b & 0b00000010) >> 1);
let outputB =
((a & 0b01000000) << 1) | ((a & 0b00010000) << 2) | ((a & 0b00000100) << 3) | ((a & 0b00000001) << 4) |
((b & 0b01000000) >> 3) | ((b & 0b00010000) >> 2) | ((b & 0b00000100) >> 1) | (b & 0b00000001);
outputBuffer.push(outputA, outputB);
}
}
return outputBuffer;
}
}
class Patcher {
constructor(patchData) {
this.patchData = patchData;
}
apply(file) {
const originalPosition = file.position;
this.patchData.forEach((patch) => {
file.seek(patch.address);
if (patch.rleData) {
patch.rleData.forEach((data) => {
if (Array.isArray(data)) {
for (let i = 0; i < data[1]; i++) { file.writeByte(data[0]); }
} else {
file.writeByte(data)
}
})
} else {
file.writeBytes(patch.data);
}
});
file.seek(originalPosition);
}
}
class FileSeeker {
@@ -428,3 +696,14 @@ class FileSeeker {
}
}
}
const stringToCharArray = (str) => {
return str.split('').map(c => c.charCodeAt(0));
}
const FULL_WIDTH_PUNC = {
'!': '', '?': '', '&': '', '(': '', ')': '', '~': '',
};
const textToFullWidthPunc = (text) => {
return text.split('').map((char) => FULL_WIDTH_PUNC[char] || char).join('');
};

12
script/menu.js Normal file

File diff suppressed because one or more lines are too long

1
script/metadata.js Normal file

File diff suppressed because one or more lines are too long

50
script/patches.js Normal file
View File

@@ -0,0 +1,50 @@
ENGLISH_PATCH_DATA = [
{
address: 0x03030,
rleData: [[0,16],[6,2],[7,4],[6,8],[0,2],[103,2],[109,2],[237,4],[109,4],[103,2],[0,2],[7,2],[143,2],[140,2],[141,2],[140,2],[143,2],[7,2],[0,2],[142,2],[223,2],[27,2],[159,2],[219,4],[155,2],[0,2],[99,2],[119,2],[127,2],[107,2],[99,6],[0,2],[124,2],[125,2],[97,2],[121,2],[96,2],[125,4],[0,2],[249,2],[251,2],[131,2],[251,2],[25,2],[248,2],[241,2],[0,2],[128,2],[192,6],[128,2],[0,2],[128,2],[0,26]]
},
{
address: 0x06C6C,
rleData: [[255,4],[0,12],[255,4],[0,12],[255,4],[0,12],[255,4],[0,12],[255,4],[0,12],[255,20]],
},
{
address: 0x06D5C,
rleData: [63,41,[31,2],255,4,255,4,255,60,255,12,255,60,255,60,255,60,[255,3],24,255,8,255,201,255,24,255,57,255,152,255,200,[255,3],32,255,32,255,231,255,97,255,231,255,32,255,32,[255,3],38,255,36,255,36,255,36,255,39,255,4,255,140,[255,3],8,255,8,255,249,255,8,255,201,255,8,255,24,[255,3],33,255,36,255,228,255,100,255,228,255,36,255,33,[255,2]]
},
{
address: 0x07E00,
rleData: [[255,4],[224,2],[192,3],[199,2],207,196,204,196,204,[255,4],56,120,16,48,16,51,1,227,35,103,0,32,[255,4],49,115,16,49,16,48,0,16,0,146,0,19,[255,4],224,228,192,196,0,132,0,4,0,36,32,100,[255,4],2,6,0,[4,2],252,124,252,12,28,12,30,[255,4],3,7,3,[7,2],[127,2],255,7,15]
},
{
address: 0x07EFC,
rleData: [[0,4],244,207,255,198,255,192,223,[224,2],[255,7],223,32,216,39,219,39,187,[103,2],[255,7],237,19,109,147,237,147,237,[147,2],[255,7],123,228,251,228,251,228,251,[228,2],[255,7],30,255,127,255,251,4,251,[4,2],[255,7],59,199,219,231,251,7,247,[15,2],[255,7],[231,2],[226,2],[224,2],[229,2],255,231,255,231,255,231,[255,2],[32,4],[39,2],[33,2],255,39,255,32,255,32,[255,2],[156,2],[136,2],[128,2],[148,2],255,156,255,156,255,156,[255,2],[0,14],[255,2]]
},
{
address: 0x0EC64,
rleData: [[0,8],[255,4],[0,12],[255,4],[0,12],[255,4],[0,12],[255,4],[0,12],[255,4],[0,12],[255,4]]
},
{
address: 0x0ED60,
rleData: [255,4,255,4,255,60,255,12,255,60,255,60,255,60,[255,3],24,255,8,255,201,255,24,255,57,255,152,255,200,[255,3],32,255,32,255,231,255,97,255,231,255,32,255,32,[255,3],38,255,36,255,36,255,36,255,39,255,4,255,140,[255,3],8,255,8,255,249,255,8,255,201,255,8,255,24,[255,3],33,255,36,255,228,255,100,255,228,255,36,255,33,[255,2]]
},
{
address: 0x0F9FC,
rleData: [[255,68],[252,2],[248,2],[249,6],[248,2],[252,2],[255,2],[48,2],[19,2],[240,2],[51,2],[147,2],[16,2],[48,2],[255,2],[112,2],[32,2],[39,6],[32,2],[112,2],[255,2],[124,2],[120,2],[249,6],[120,2],[124,2],[255,2],[103,2],[35,2],[33,2],[37,2],[36,2],[38,2],[103,2]]
},
{
address: 0x0FAF8,
rleData: [[0,8],[39,10],[32,4],[255,2],[51,2],[50,4],[134,2],[207,6],[255,2],[63,2],[31,6],[63,2],[255,2],[63,2],[255,98]]
},
{
address: 0x0FBE8,
rleData: [[0,8],[255,112],[39,10],[32,4],[255,2],[51,2],[50,4],[134,2],[207,6],[255,2],[63,2],[31,6],[63,2],[255,2],[63,2],[255,50]]
},
{
address: 0x0FDF8,
rleData: [[0,8],[255,4],[224,2],[192,3],[199,2],207,196,204,196,204,[255,4],56,120,16,48,16,51,1,227,35,103,0,32,[255,4],49,115,16,49,16,48,0,16,0,146,0,19,[255,4],224,228,192,196,0,132,0,4,0,36,32,100,[255,4],2,6,0,[4,2],252,124,252,12,28,12,30,[255,4],3,7,3,[7,2],[127,2],255,7,15,3,7]
},
{
address: 0x0FF00,
rleData: [244,207,255,198,255,192,223,[224,2],[255,7],223,32,216,39,219,39,187,[103,2],[255,7],237,19,109,147,237,147,237,[147,2],[255,7],123,228,251,228,251,228,251,[228,2],[255,7],30,255,127,255,251,4,251,[4,2],[255,7],59,199,219,231,251,7,247,[15,2],[255,7],[231,2],[226,2],[224,2],[229,2],255,231,255,231,255,231,[255,2],[32,4],[39,2],[33,2],255,39,255,32,255,32,[255,2],[156,2],[136,2],[128,2],[148,2],255,156,255,156,255,156,[255,2],[0,14],[255,2]]
}
];

View File

@@ -1,27 +1,34 @@
@font-face {
font-family: "Gameboy";
src: url("./font/Gameboy.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Gamer";
src: url("./font/Gamer.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Pixeltype";
src: url("./font/Pixeltype.woff") format("woff");
src: url("../font/Gameboy.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Nokia";
src: url("./font/Nokia.woff") format("woff");
src: url("../font/Nokia.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "PokemonGB";
src: url("../font/Pokemon.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "DMG-MMSA";
src: url("../font/DMG-MMSA.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "TWL-IRAJ-1";
src: url("../font/TWL-IRAJ-1.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@@ -45,19 +52,23 @@ h1, h2 {
h2 {
color: #444;
border-top: 2px dotted grey;
border-top: 1px solid grey;
display: inline-block;
border-bottom: 2px dotted grey;
border-bottom: 1px solid grey;
}
p {
margin-top: 0;
}
ul {
margin: 0;
padding-left: 10px;
}
footer {
margin-top: 40px;
font-size: 0.75em;
color: #444;
}
button {
@@ -79,12 +90,16 @@ button#remove-rom {
color: red;
}
button.remove-all{
color: red;
}
button[disabled='disabled'] {
color: grey;
}
.text-container {
width: 850px;
max-width: 850px;
margin-bottom:25px;
}
@@ -121,6 +136,10 @@ td.overflow {
background-color: red;
}
td.single-rom {
background-color: orange;
}
thead tr {
border-bottom: 1px solid #ddd;
}
@@ -136,36 +155,20 @@ tbody tr:nth-child(odd) {
.settings-label {
font-weight: bold;
margin-right: 20px;
min-width: 190px;
min-width: 160px;
display: inline-block;
vertical-align: top;
}
.settings-row {
margin-bottom: 15px;
}
.menu-hint {
font-size: 0.75em;
color: #444;
}
.tooltip {
cursor: help;
}
label.radio-label {
vertical-align: middle;
margin-right: 10px;
}
label#pixeltype {
font-family: 'Pixeltype';
font-size: 32px;
line-height: 0;
position: relative;
top: 3px;
}
label#gameboy {
font-family: 'Gameboy';
}
@@ -174,15 +177,77 @@ label#nokia {
font-family: 'Nokia';
}
label#gamer {
font-family: Gamer;
font-size: 32px;
line-height: 0;
label#pokemon {
font-family: 'PokemonGB';
position: relative;
top: -2px;
top: 3px;
}
label#gbmem {
font-family: 'DMG-MMSA';
text-transform: uppercase;
}
label#twliraj1 {
font-family: 'TWL-IRAJ-1';
}
noscript {
font-weight: bold;
color: red;
}
.drag-target {
height: 70px;
max-width: 945px;
padding: 20px;
border: 2px dashed grey;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
transition: 0.5s all;
}
.drag-target.over {
border-color: #000079;
transform: scale(1.02, 1.05);
cursor: grabbing;
}
.drag-target div {
transition: 0.5s all;
}
.drag-target.over div {
opacity: 0.5;
}
.flex-row {
display: inline-flex;
}
.flex-item {
max-width: 400px;
margin-right: 20px;
}
.games-flex-item {
min-width: 250px;
margin-right: 20px;
}
.single-rom-download {
margin-top: 1.5rem;
}
.hide {
display: none;
}
.ticker-settings {
margin-top: 0.4rem;
}
.map-note { margin-left: 0.5rem; }
.map-note span:first-child { font-size: 1.2rem; margin-right: 0.3rem; }
.map-note span:last-child { vertical-align: sub; }