import { Platform, BaseZ80Platform } from "../common/baseplatform"; import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap } from "../common/emu"; import { hex } from "../common/util"; import { MasterAudio, AY38910_Audio } from "../common/audio"; const GALAXIAN_PRESETS = [ { id: 'gfxtest.c', name: 'Graphics Test' }, { id: 'shoot2.c', name: 'Solarian Game' }, ]; const GALAXIAN_KEYCODE_MAP = makeKeycodeMap([ [Keys.A, 0, 0x10], // P1 [Keys.LEFT, 0, 0x4], [Keys.RIGHT, 0, 0x8], [Keys.P2_A, 1, 0x10], // P2 [Keys.P2_LEFT, 1, 0x4], [Keys.P2_RIGHT, 1, 0x8], [Keys.SELECT, 0, 0x1], [Keys.START, 1, 0x1], [Keys.VK_2, 1, 0x2], ]); const SCRAMBLE_KEYCODE_MAP = makeKeycodeMap([ [Keys.UP, 0, -0x1], // P1 [Keys.B, 0, -0x2], // fire [Keys.VK_7, 0, -0x4], // credit [Keys.A, 0, -0x8], // bomb [Keys.RIGHT, 0, -0x10], [Keys.LEFT, 0, -0x20], [Keys.VK_6, 0, -0x40], [Keys.SELECT, 0, -0x80], [Keys.START, 1, -0x80], [Keys.VK_2, 1, -0x40], [Keys.DOWN, 2, -0x40], //[Keys.VK_UP, 2, -0x10], ]); const GalaxianVideo = function(rom:Uint8Array, vram:RAM, oram:RAM, palette:Uint32Array, options) { var gfxBase = options.gfxBase || 0x2800; this.missileWidth = options.missileWidth || 4; this.missileOffset = options.missileOffset || 0; this.showOffscreenObjects = false; this.frameCounter = 0; this.starsEnabled = 0; var stars = []; for (var i = 0; i < 256; i++) stars[i] = noise(); this.advanceFrame = function() { this.frameCounter = (this.frameCounter + 1) & 0xff; } this.drawScanline = function(pixels, sl) { if (sl < 16 && !this.showOffscreenObjects) return; // offscreen if (sl >= 240 && !this.showOffscreenObjects) return; // offscreen // draw tiles var pixofs = sl * 264; var outi = pixofs; // starting output pixel in frame buffer for (var xx = 0; xx < 32; xx++) { var xofs = xx; var scroll = oram.mem[xofs * 2]; // even entries control scroll position var attrib = oram.mem[xofs * 2 + 1]; // odd entries control the color base var sl2 = (sl + scroll) & 0xff; var vramofs = (sl2 >> 3) << 5; // offset in VRAM var yy = sl2 & 7; // y offset within tile var tile = vram.mem[vramofs + xofs]; // TODO: why undefined? var color0 = (attrib & 7) << 2; var addr = gfxBase + (tile << 3) + yy; var data1 = rom[addr]; var data2 = rom[addr + 0x800]; for (var i = 0; i < 8; i++) { var bm = 128 >> i; var color = color0 + ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); pixels[outi] = palette[color]; outi++; } } // draw sprites for (var sprnum = 7; sprnum >= 0; sprnum--) { var base = (sprnum << 2) + 0x40; var base0 = oram.mem[base]; var sy = 240 - (base0 - ((sprnum < 3) ? 1 : 0)); // the first three sprites match against y-1 var yy = (sl - sy); if (yy >= 0 && yy < 16) { var sx = oram.mem[base + 3] + 1; // +1 pixel offset from tiles if (sx == 0 && !this.showOffscreenObjects) continue; // drawn off-buffer var code = oram.mem[base + 1]; var flipx = code & 0x40; // TODO: flipx if (code & 0x80) // flipy yy = 15 - yy; code &= 0x3f; var color0 = (oram.mem[base + 2] & 7) << 2; var addr = gfxBase + (code << 5) + (yy < 8 ? yy : yy + 8); outi = pixofs + sx; //<< 1 var data1 = rom[addr]; var data2 = rom[addr + 0x800]; for (var i = 0; i < 8; i++) { var bm = 128 >> i; var color = ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); if (color) pixels[flipx ? (outi + 15 - i) : (outi + i)] = palette[color0 + color]; } var data1 = rom[addr + 8]; var data2 = rom[addr + 0x808]; for (var i = 0; i < 8; i++) { var bm = 128 >> i; var color = ((data1 & bm) ? 1 : 0) + ((data2 & bm) ? 2 : 0); if (color) pixels[flipx ? (outi + 7 - i) : (outi + i + 8)] = palette[color0 + color]; } } } // draw bullets/shells var shell = 0xff; var missile = 0xff; for (var which = 0; which < 8; which++) { var sy = oram.mem[0x60 + (which << 2) + 1]; if (((sy + sl - ((which < 3) ? 1 : 0)) & 0xff) == 0xff) { if (which != 7) shell = which; else missile = which; } } for (var i = 0; i < 2; i++) { which = i ? missile : shell; if (which != 0xff) { var sx = 255 - oram.mem[0x60 + (which << 2) + 3]; var outi = pixofs + sx - this.missileOffset; var col = which == 7 ? 0xffffff00 : 0xffffffff; for (var j = 0; j < this.missileWidth; j++) pixels[outi++] = col; } } // draw stars if (this.starsEnabled) { var starx = ((this.frameCounter + stars[sl & 0xff]) & 0xff); if ((starx + sl) & 0x10) { var outi = pixofs + starx; if ((pixels[outi] & 0xffffff) == 0) { pixels[outi] = palette[sl & 0x1f]; } } } } } const _GalaxianPlatform = function(mainElement, options) { options = options || {}; var romSize = options.romSize || 0x4000; var palBase = options.palBase || 0x3800; var keyMap = options.keyMap || GALAXIAN_KEYCODE_MAP; var cpu; var ram, vram, oram: RAM; var membus, iobus, rom, palette, outlatches; var video, audio, timer, pixels; var psg1, psg2; var inputs; var interruptEnabled = 0; var watchdog_counter; var XTAL = 18432000.0; var scanlinesPerFrame = 264; var cpuFrequency = XTAL / 6; // 3.072 MHz var hsyncFrequency = XTAL / 3 / 192 / 2; // 16 kHz var vsyncFrequency = hsyncFrequency / 132 / 2; // 60.606060 Hz var vblankDuration = 1 / vsyncFrequency * (20 / 132); // 2500 us var cpuCyclesPerLine = cpuFrequency / hsyncFrequency; var INITIAL_WATCHDOG = 8; var gfx; // = new GalaxianVideo(rom, vram, oram, palette, options); var m_protection_state = 0; var m_protection_result = 0; function scramble_protection_w(addr, data) { /* This is not fully understood; the low 4 bits of port C are inputs; the upper 4 bits are outputs. Scramble main set always writes sequences of 3 or more nibbles to the low port and expects certain results in the upper nibble afterwards. */ m_protection_state = (m_protection_state << 4) | (data & 0x0f); switch (m_protection_state & 0xfff) { /* scramble */ case 0xf09: m_protection_result = 0xff; break; case 0xa49: m_protection_result = 0xbf; break; case 0x319: m_protection_result = 0x4f; break; case 0x5c9: m_protection_result = 0x6f; break; /* scrambls */ case 0x246: m_protection_result ^= 0x80; break; case 0xb5f: m_protection_result = 0x6f; break; } } function scramble_protection_alt_r() { var bit = (m_protection_result >> 7) & 1; return (bit << 5) | ((bit ^ 1) << 7); } const bitcolors = [ 0x000021, 0x000047, 0x000097, // red 0x002100, 0x004700, 0x009700, // green 0x510000, 0xae0000 // blue ]; class GalaxianPlatform extends BaseZ80Platform implements Platform { scanline: number; poller; getPresets() { return GALAXIAN_PRESETS; } start() { ram = new RAM(0x800); vram = new RAM(0x400); oram = new RAM(0x100); rom = new Uint8Array(romSize); palette = new Uint32Array(new ArrayBuffer(32 * 4)); gfx = new GalaxianVideo(rom, vram, oram, palette, options); outlatches = new RAM(0x8); if (options.scramble) { inputs = [0xff, 0xfc, 0xf1]; membus = { read: newAddressDecoder([ [0x0000, 0x3fff, 0, function(a) { return rom ? rom[a] : null; }], [0x4000, 0x47ff, 0x7ff, function(a) { return ram.mem[a]; }], [0x4800, 0x4fff, 0x3ff, function(a) { return vram.mem[a]; }], [0x5000, 0x5fff, 0xff, function(a) { return oram.mem[a]; }], [0x7000, 0x7000, 0, function(a) { watchdog_counter = INITIAL_WATCHDOG; }], [0x7800, 0x7800, 0, function(a) { watchdog_counter = INITIAL_WATCHDOG; }], //[0x8000, 0x820f, 0, function(a) { return noise(); }], // TODO: remove [0x8100, 0x8100, 0, function(a) { return inputs[0]; }], [0x8101, 0x8101, 0, function(a) { return inputs[1]; }], [0x8102, 0x8102, 0, function(a) { return inputs[2] | scramble_protection_alt_r(); }], [0x8202, 0x8202, 0, function(a) { return m_protection_result; }], // scramble (protection) [0x9100, 0x9100, 0, function(a) { return inputs[0]; }], [0x9101, 0x9101, 0, function(a) { return inputs[1]; }], [0x9102, 0x9102, 0, function(a) { return inputs[2] | scramble_protection_alt_r(); }], [0x9212, 0x9212, 0, function(a) { return m_protection_result; }], // scramble (protection) //[0, 0xffff, 0, function(a) { console.log(hex(a)); return 0; }] ]), write: newAddressDecoder([ [0x4000, 0x47ff, 0x7ff, function(a, v) { ram.mem[a] = v; }], [0x4800, 0x4fff, 0x3ff, function(a, v) { vram.mem[a] = v; }], [0x5000, 0x5fff, 0xff, function(a, v) { oram.mem[a] = v; }], [0x6801, 0x6801, 0, function(a, v) { interruptEnabled = v & 1; /*console.log(a,v,cpu.getPC().toString(16));*/ }], [0x6802, 0x6802, 0, function(a, v) { /* TODO: coin counter */ }], [0x6803, 0x6803, 0, function(a, v) { /* TODO: backgroundColor = (v & 1) ? 0xFF000056 : 0xFF000000; */ }], [0x6804, 0x6804, 0, function(a, v) { gfx.starsEnabled = v & 1; }], [0x6808, 0x6808, 0, function(a, v) { gfx.missileWidth = v; }], // not on h/w [0x6809, 0x6809, 0, function(a, v) { gfx.missileOffset = v; }], // not on h/w [0x8202, 0x8202, 0, scramble_protection_w], //[0x8100, 0x8103, 0, function(a,v){ /* PPI 0 */ }], //[0x8200, 0x8203, 0, function(a,v){ /* PPI 1 */ }], //[0, 0xffff, 0, function(a,v) { console.log(hex(a),hex(v)); }] ]), }; } else { inputs = [0xe, 0x8, 0x0]; membus = { read: newAddressDecoder([ [0x0000, 0x3fff, 0, function(a) { return rom ? rom[a] : null; }], [0x4000, 0x47ff, 0x3ff, function(a) { return ram.mem[a]; }], [0x5000, 0x57ff, 0x3ff, function(a) { return vram.mem[a]; }], [0x5800, 0x5fff, 0xff, function(a) { return oram.mem[a]; }], [0x6000, 0x6000, 0, function(a) { return inputs[0]; }], [0x6800, 0x6800, 0, function(a) { return inputs[1]; }], [0x7000, 0x7000, 0, function(a) { return inputs[2]; }], [0x7800, 0x7800, 0, function(a) { watchdog_counter = INITIAL_WATCHDOG; }], ]), write: newAddressDecoder([ [0x4000, 0x47ff, 0x3ff, function(a, v) { ram.mem[a] = v; }], [0x5000, 0x57ff, 0x3ff, function(a, v) { vram.mem[a] = v; }], [0x5800, 0x5fff, 0xff, function(a, v) { oram.mem[a] = v; }], //[0x6004, 0x6007, 0x3, function(a,v) { }], // lfo freq //[0x6800, 0x6807, 0x7, function(a,v) { }], // sound //[0x7800, 0x7800, 0x7, function(a,v) { }], // pitch [0x6000, 0x6003, 0x3, function(a, v) { outlatches.mem[a] = v; }], [0x7001, 0x7001, 0, function(a, v) { interruptEnabled = v & 1; }], [0x7004, 0x7004, 0, function(a, v) { gfx.starsEnabled = v & 1; }], ]), isContended: function() { return false; }, }; } audio = new MasterAudio(); psg1 = new AY38910_Audio(audio); psg2 = new AY38910_Audio(audio); iobus = { read: function(addr) { return 0; }, write: function(addr, val) { if (addr & 0x1) { psg1.selectRegister(val & 0xf); }; if (addr & 0x2) { psg1.setData(val); }; if (addr & 0x4) { psg2.selectRegister(val & 0xf); }; if (addr & 0x8) { psg2.setData(val); }; } }; cpu = this.newCPU(membus, iobus); video = new RasterVideo(mainElement, 264, 264, { rotate: 90 }); video.create(); var idata = video.getFrameData(); this.poller = setKeyboardFromMap(video, inputs, keyMap); pixels = video.getFrameData(); timer = new AnimationTimer(60, this.nextFrame.bind(this)); } pollControls() { this.poller.poll(); } readAddress(a) { return (a == 0x7000 || a == 0x7800) ? null : membus.read(a); // ignore watchdog } advance(novideo: boolean) { for (var sl = 0; sl < scanlinesPerFrame; sl++) { this.scanline = sl; if (!novideo) { gfx.drawScanline(pixels, sl); } this.runCPU(cpu, cpuCyclesPerLine); } // visible area is 256x224 (before rotation) if (!novideo) { video.updateFrame(0, 0, 0, 0, gfx.showOffscreenObjects ? 264 : 256, 264); } gfx.advanceFrame(); if (watchdog_counter-- <= 0) { console.log("WATCHDOG FIRED, PC ", hex(cpu.getPC())); // TODO: alert on video this.reset(); } // NMI interrupt @ 0x66 if (interruptEnabled) { cpu.NMI(); } } getRasterScanline() { return this.scanline; } loadROM(title, data) { rom.set(padBytes(data, romSize)); for (var i = 0; i < 32; i++) { var b = rom[palBase + i]; palette[i] = 0xff000000; for (var j = 0; j < 8; j++) if (((1 << j) & b)) palette[i] += bitcolors[j]; } this.reset(); } loadState(state) { cpu.loadState(state.c); ram.mem.set(state.b); vram.mem.set(state.bv); oram.mem.set(state.bo); watchdog_counter = state.wdc; interruptEnabled = state.ie; gfx.starsEnabled = state.se; gfx.frameCounter = state.fc; inputs[0] = state.in0; inputs[1] = state.in1; inputs[2] = state.in2; } saveState() { return { c: this.getCPUState(), b: ram.mem.slice(0), bv: vram.mem.slice(0), bo: oram.mem.slice(0), fc: gfx.frameCounter, ie: interruptEnabled, se: gfx.starsEnabled, wdc: watchdog_counter, in0: inputs[0], in1: inputs[1], in2: inputs[2], }; } loadControlsState(state) { inputs[0] = state.in0; inputs[1] = state.in1; inputs[2] = state.in2; } saveControlsState() { return { in0: inputs[0], in1: inputs[1], in2: inputs[2], }; } getCPUState() { return cpu.saveState(); } isRunning() { return timer && timer.isRunning(); } pause() { timer.stop(); audio.stop(); } resume() { timer.start(); audio.start(); } reset() { cpu.reset(); watchdog_counter = INITIAL_WATCHDOG; } getMemoryMap = function() { return { main:[ {name:'Video RAM',start:0x5000,size:0x400,type:'ram'}, {name:'Sprite RAM',start:0x5800,size:0x100,type:'ram'}, {name:'I/O Registers',start:0x6000,size:0x2000,type:'io'}, ] } }; } return new GalaxianPlatform(); } const _GalaxianScramblePlatform = function(mainElement) { return _GalaxianPlatform(mainElement, { romSize: 0x5020, gfxBase: 0x4000, palBase: 0x5000, scramble: true, keyMap: SCRAMBLE_KEYCODE_MAP, missileWidth: 1, missileOffset: 6, }); } PLATFORMS['galaxian'] = _GalaxianPlatform; PLATFORMS['galaxian-scramble'] = _GalaxianScramblePlatform;