| function GameBoyAdvance() { |
| this.LOG_ERROR = 1; |
| this.LOG_WARN = 2; |
| this.LOG_STUB = 4; |
| this.LOG_INFO = 8; |
| this.LOG_DEBUG = 16; |
|
|
| this.SYS_ID = 'com.endrift.gbajs'; |
|
|
| this.logLevel = this.LOG_ERROR | this.LOG_WARN; |
|
|
| this.rom = null; |
|
|
| this.cpu = new ARMCore(); |
| this.mmu = new GameBoyAdvanceMMU() |
| this.irq = new GameBoyAdvanceInterruptHandler(); |
| this.io = new GameBoyAdvanceIO(); |
| this.audio = new GameBoyAdvanceAudio(); |
| this.video = new GameBoyAdvanceVideo(); |
| this.keypad = new GameBoyAdvanceKeypad(); |
| this.sio = new GameBoyAdvanceSIO(); |
|
|
| |
| this.cpu.mmu = this.mmu; |
| this.cpu.irq = this.irq; |
|
|
| this.mmu.cpu = this.cpu; |
| this.mmu.core = this; |
|
|
| this.irq.cpu = this.cpu; |
| this.irq.io = this.io; |
| this.irq.audio = this.audio; |
| this.irq.video = this.video; |
| this.irq.core = this; |
|
|
| this.io.cpu = this.cpu; |
| this.io.audio = this.audio; |
| this.io.video = this.video; |
| this.io.keypad = this.keypad; |
| this.io.sio = this.sio; |
| this.io.core = this; |
|
|
| this.audio.cpu = this.cpu; |
| this.audio.core = this; |
|
|
| this.video.cpu = this.cpu; |
| this.video.core = this; |
|
|
| this.keypad.core = this; |
|
|
| this.sio.core = this; |
|
|
| this.keypad.registerHandlers(); |
| this.doStep = this.waitFrame; |
| this.paused = false; |
|
|
| this.seenFrame = false; |
| this.seenSave = false; |
| this.lastVblank = 0; |
|
|
| this.queue = null; |
| this.reportFPS = null; |
| this.throttle = 16; |
|
|
| var self = this; |
| window.queueFrame = function (f) { |
| self.queue = window.setTimeout(f, self.throttle); |
| }; |
|
|
| window.URL = window.URL || window.webkitURL; |
|
|
| this.video.vblankCallback = function() { |
| self.seenFrame = true; |
| }; |
| }; |
|
|
| GameBoyAdvance.prototype.setCanvas = function(canvas) { |
| var self = this; |
| if (canvas.offsetWidth != 240 || canvas.offsetHeight != 160) { |
| this.indirectCanvas = document.createElement("canvas"); |
| this.indirectCanvas.setAttribute("height", "160"); |
| this.indirectCanvas.setAttribute("width", "240"); |
| this.targetCanvas = canvas; |
| this.setCanvasDirect(this.indirectCanvas); |
| var targetContext = canvas.getContext('2d'); |
| this.video.drawCallback = function() { |
| targetContext.drawImage(self.indirectCanvas, 0, 0, canvas.offsetWidth, canvas.offsetHeight); |
| } |
| } else { |
| this.setCanvasDirect(canvas); |
| var self = this; |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.setCanvasDirect = function(canvas) { |
| this.context = canvas.getContext('2d'); |
| this.video.setBacking(this.context); |
| }; |
|
|
| GameBoyAdvance.prototype.setBios = function(bios, real) { |
| this.mmu.loadBios(bios, real); |
| }; |
|
|
| GameBoyAdvance.prototype.setRom = function(rom) { |
| this.reset(); |
|
|
| this.rom = this.mmu.loadRom(rom, true); |
| if (!this.rom) { |
| return false; |
| } |
| this.retrieveSavedata(); |
| return true; |
| }; |
|
|
| GameBoyAdvance.prototype.hasRom = function() { |
| return !!this.rom; |
| }; |
|
|
| GameBoyAdvance.prototype.loadRomFromFile = function(romFile, callback) { |
| var reader = new FileReader(); |
| var self = this; |
| reader.onload = function(e) { |
| var result = self.setRom(e.target.result); |
| if (callback) { |
| callback(result); |
| } |
| } |
| reader.readAsArrayBuffer(romFile); |
| }; |
|
|
| GameBoyAdvance.prototype.reset = function() { |
| this.audio.pause(true); |
|
|
| this.mmu.clear(); |
| this.io.clear(); |
| this.audio.clear(); |
| this.video.clear(); |
| this.sio.clear(); |
|
|
| this.mmu.mmap(this.mmu.REGION_IO, this.io); |
| this.mmu.mmap(this.mmu.REGION_PALETTE_RAM, this.video.renderPath.palette); |
| this.mmu.mmap(this.mmu.REGION_VRAM, this.video.renderPath.vram); |
| this.mmu.mmap(this.mmu.REGION_OAM, this.video.renderPath.oam); |
|
|
| this.cpu.resetCPU(0); |
| }; |
|
|
| GameBoyAdvance.prototype.step = function() { |
| while (this.doStep()) { |
| this.cpu.step(); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.waitFrame = function() { |
| var seen = this.seenFrame; |
| this.seenFrame = false; |
| return !seen; |
| }; |
|
|
| GameBoyAdvance.prototype.pause = function() { |
| this.paused = true; |
| this.audio.pause(true); |
| if (this.queue) { |
| clearTimeout(this.queue); |
| this.queue = null; |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.advanceFrame = function() { |
| this.step(); |
| if (this.seenSave) { |
| if (!this.mmu.saveNeedsFlush()) { |
| this.storeSavedata(); |
| this.seenSave = false; |
| } else { |
| this.mmu.flushSave(); |
| } |
| } else if (this.mmu.saveNeedsFlush()) { |
| this.seenSave = true; |
| this.mmu.flushSave(); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.runStable = function() { |
| if (this.interval) { |
| return; |
| } |
| var self = this; |
| var timer = 0; |
| var frames = 0; |
| var runFunc; |
| var start = Date.now(); |
| this.paused = false; |
| this.audio.pause(false); |
|
|
| if (this.reportFPS) { |
| runFunc = function() { |
| try { |
| timer += Date.now() - start; |
| if (self.paused) { |
| return; |
| } else { |
| queueFrame(runFunc); |
| } |
| start = Date.now(); |
| self.advanceFrame(); |
| ++frames; |
| if (frames == 60) { |
| self.reportFPS((frames * 1000) / timer); |
| frames = 0; |
| timer = 0; |
| } |
| } catch(exception) { |
| self.ERROR(exception); |
| if (exception.stack) { |
| self.logStackTrace(exception.stack.split('\n')); |
| } |
| throw exception; |
| } |
| }; |
| } else { |
| runFunc = function() { |
| try { |
| if (self.paused) { |
| return; |
| } else { |
| queueFrame(runFunc); |
| } |
| self.advanceFrame(); |
| } catch(exception) { |
| self.ERROR(exception); |
| if (exception.stack) { |
| self.logStackTrace(exception.stack.split('\n')); |
| } |
| throw exception; |
| } |
| }; |
| } |
| queueFrame(runFunc); |
| }; |
|
|
| GameBoyAdvance.prototype.setSavedata = function(data) { |
| this.mmu.loadSavedata(data); |
| }; |
|
|
| GameBoyAdvance.prototype.loadSavedataFromFile = function(saveFile) { |
| var reader = new FileReader(); |
| var self = this; |
| reader.onload = function(e) { self.setSavedata(e.target.result); } |
| reader.readAsArrayBuffer(saveFile); |
| }; |
|
|
| GameBoyAdvance.prototype.decodeSavedata = function(string) { |
| this.setSavedata(this.decodeBase64(string)); |
| }; |
|
|
| GameBoyAdvance.prototype.decodeBase64 = function(string) { |
| var length = (string.length * 3 / 4); |
| if (string[string.length - 2] == '=') { |
| length -= 2; |
| } else if (string[string.length - 1] == '=') { |
| length -= 1; |
| } |
| var buffer = new ArrayBuffer(length); |
| var view = new Uint8Array(buffer); |
| var bits = string.match(/..../g); |
| for (var i = 0; i + 2 < length; i += 3) { |
| var s = atob(bits.shift()); |
| view[i] = s.charCodeAt(0); |
| view[i + 1] = s.charCodeAt(1); |
| view[i + 2] = s.charCodeAt(2); |
| } |
| if (i < length) { |
| var s = atob(bits.shift()); |
| view[i++] = s.charCodeAt(0); |
| if (s.length > 1) { |
| view[i++] = s.charCodeAt(1); |
| } |
| } |
|
|
| return buffer; |
| }; |
|
|
| GameBoyAdvance.prototype.encodeBase64 = function(view) { |
| var data = []; |
| var b; |
| var wordstring = []; |
| var triplet; |
| for (var i = 0; i < view.byteLength; ++i) { |
| b = view.getUint8(i, true); |
| wordstring.push(String.fromCharCode(b)); |
| while (wordstring.length >= 3) { |
| triplet = wordstring.splice(0, 3); |
| data.push(btoa(triplet.join(''))); |
| } |
| }; |
| if (wordstring.length) { |
| data.push(btoa(wordstring.join(''))); |
| } |
| return data.join(''); |
| }; |
|
|
| GameBoyAdvance.prototype.downloadSavedata = function() { |
| var sram = this.mmu.save; |
| if (!sram) { |
| this.WARN("No save data available"); |
| return null; |
| } |
| if (window.URL) { |
| var url = window.URL.createObjectURL(new Blob([sram.buffer], { type: 'application/octet-stream' })); |
| window.open(url); |
| } else { |
| var data = this.encodeBase64(sram.view); |
| window.open('data:application/octet-stream;base64,' + data, this.rom.code + '.sav'); |
| } |
| }; |
|
|
|
|
| GameBoyAdvance.prototype.storeSavedata = function() { |
| var sram = this.mmu.save; |
| try { |
| var storage = window.localStorage; |
| storage[this.SYS_ID + '.' + this.mmu.cart.code] = this.encodeBase64(sram.view); |
| } catch (e) { |
| this.WARN('Could not store savedata! ' + e); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.retrieveSavedata = function() { |
| try { |
| var storage = window.localStorage; |
| var data = storage[this.SYS_ID + '.' + this.mmu.cart.code]; |
| if (data) { |
| this.decodeSavedata(data); |
| return true; |
| } |
| } catch (e) { |
| this.WARN('Could not retrieve savedata! ' + e); |
| } |
| return false; |
| }; |
|
|
| GameBoyAdvance.prototype.freeze = function() { |
| return { |
| 'cpu': this.cpu.freeze(), |
| 'mmu': this.mmu.freeze(), |
| 'irq': this.irq.freeze(), |
| 'io': this.io.freeze(), |
| 'audio': this.audio.freeze(), |
| 'video': this.video.freeze() |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.defrost = function(frost) { |
| this.cpu.defrost(frost.cpu); |
| this.mmu.defrost(frost.mmu); |
| this.audio.defrost(frost.audio); |
| this.video.defrost(frost.video); |
| this.irq.defrost(frost.irq); |
| this.io.defrost(frost.io); |
| }; |
|
|
| GameBoyAdvance.prototype.log = function(level, message) {}; |
|
|
| GameBoyAdvance.prototype.setLogger = function(logger) { |
| this.log = logger; |
| }; |
|
|
| GameBoyAdvance.prototype.logStackTrace = function(stack) { |
| var overflow = stack.length - 32; |
| this.ERROR('Stack trace follows:'); |
| if (overflow > 0) { |
| this.log(-1, '> (Too many frames)'); |
| } |
| for (var i = Math.max(overflow, 0); i < stack.length; ++i) { |
| this.log(-1, '> ' + stack[i]); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.ERROR = function(error) { |
| if (this.logLevel & this.LOG_ERROR) { |
| this.log(this.LOG_ERROR, error); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.WARN = function(warn) { |
| if (this.logLevel & this.LOG_WARN) { |
| this.log(this.LOG_WARN, warn); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.STUB = function(func) { |
| if (this.logLevel & this.LOG_STUB) { |
| this.log(this.LOG_STUB, func); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.INFO = function(info) { |
| if (this.logLevel & this.LOG_INFO) { |
| this.log(this.LOG_INFO, info); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.DEBUG = function(info) { |
| if (this.logLevel & this.LOG_DEBUG) { |
| this.log(this.LOG_DEBUG, info); |
| } |
| }; |
|
|
| GameBoyAdvance.prototype.ASSERT_UNREACHED = function(err) { |
| throw new Error("Should be unreached: " + err); |
| }; |
|
|
| GameBoyAdvance.prototype.ASSERT = function(test, err) { |
| if (!test) { |
| throw new Error("Assertion failed: " + err); |
| } |
| }; |
|
|