mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-11-22 14:33:51 +00:00
galaxian: updated to use Machine interface, fixed apple2 test
This commit is contained in:
parent
d068dc0433
commit
0f284531db
@ -511,21 +511,38 @@ export class SampledAudio {
|
|||||||
|
|
||||||
import { SampledAudioSink } from "./devices";
|
import { SampledAudioSink } from "./devices";
|
||||||
|
|
||||||
|
interface TssChannel {
|
||||||
|
setBufferLength(len : number) : void;
|
||||||
|
setSampleRate(rate : number) : void;
|
||||||
|
getBuffer() : number[];
|
||||||
|
generate(numSamples : number) : void;
|
||||||
|
}
|
||||||
|
|
||||||
export class TssChannelAdapter {
|
export class TssChannelAdapter {
|
||||||
channel;
|
channels : TssChannel[];
|
||||||
audioGain = 1.0 / 8192;
|
audioGain = 1.0 / 8192;
|
||||||
constructor(channel, oversample:number, sampleRate:number) {
|
bufferLength : number;
|
||||||
this.channel = channel;
|
|
||||||
channel.setBufferLength(oversample*2);
|
constructor(chans, oversample:number, sampleRate:number) {
|
||||||
channel.setSampleRate(sampleRate);
|
this.bufferLength = oversample * 2;
|
||||||
|
this.channels = chans.generate ? [chans] : chans; // array or single channel
|
||||||
|
this.channels.forEach((c) => {
|
||||||
|
c.setBufferLength(this.bufferLength);
|
||||||
|
c.setSampleRate(sampleRate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(sink:SampledAudioSink) {
|
generate(sink:SampledAudioSink) {
|
||||||
var buf = this.channel.getBuffer();
|
var l = this.bufferLength;
|
||||||
var l = buf.length;
|
var bufs = this.channels.map((ch) => ch.getBuffer());
|
||||||
this.channel.generate(l);
|
this.channels.forEach((ch) => {
|
||||||
for (let i=0; i<l; i+=2)
|
ch.generate(l);
|
||||||
sink.feedSample(buf[i] * this.audioGain, 1);
|
});
|
||||||
//if (Math.random() < 0.001) console.log(sink);
|
for (let i=0; i<l; i+=2) {
|
||||||
|
var total = 0;
|
||||||
|
bufs.forEach((buf) => total += buf[i]);
|
||||||
|
sink.feedSample(total * this.audioGain, 1);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ interface AppleIIState extends AppleIIStateBase, AppleIIControlsState {
|
|||||||
|
|
||||||
interface SlotDevice extends Bus {
|
interface SlotDevice extends Bus {
|
||||||
readROM(address: number) : number;
|
readROM(address: number) : number;
|
||||||
|
readConst(address: number) : number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppleII extends BasicScanlineMachine {
|
export class AppleII extends BasicScanlineMachine {
|
||||||
@ -81,6 +82,9 @@ export class AppleII extends BasicScanlineMachine {
|
|||||||
default: return 0;
|
default: return 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
readConst: (a) => {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
read: (a) => { return this.noise(); },
|
read: (a) => { return this.noise(); },
|
||||||
write: (a,v) => { }
|
write: (a,v) => { }
|
||||||
};
|
};
|
||||||
@ -176,7 +180,7 @@ export class AppleII extends BasicScanlineMachine {
|
|||||||
return this.ram[address + this.bank2rdoffset];
|
return this.ram[address + this.bank2rdoffset];
|
||||||
} else if (address >= 0xc100 && address < 0xc800) {
|
} else if (address >= 0xc100 && address < 0xc800) {
|
||||||
var slot = (address >> 8) & 7;
|
var slot = (address >> 8) & 7;
|
||||||
return (this.slots[slot] && this.slots[slot].readROM(address & 0xff)) | 0;
|
return (this.slots[slot] && this.slots[slot].readConst(address & 0xff)) | 0;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -231,7 +235,8 @@ export class AppleII extends BasicScanlineMachine {
|
|||||||
return (this.slots[slot-8] && this.slots[slot-8].read(address & 0xf)) | 0;
|
return (this.slots[slot-8] && this.slots[slot-8].read(address & 0xf)) | 0;
|
||||||
}
|
}
|
||||||
} else if (address >= 0xc100 && address < 0xc800) {
|
} else if (address >= 0xc100 && address < 0xc800) {
|
||||||
return this.readConst(address);
|
var slot = (address >> 8) & 7;
|
||||||
|
return (this.slots[slot] && this.slots[slot].readROM(address & 0xff)) | 0;
|
||||||
}
|
}
|
||||||
return this.noise();
|
return this.noise();
|
||||||
}
|
}
|
||||||
@ -1091,6 +1096,7 @@ class DiskII extends DiskIIState implements SlotDevice, SavesState<DiskIIState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
readROM(address) { return DISKII_PROM[address]; }
|
readROM(address) { return DISKII_PROM[address]; }
|
||||||
|
readConst(address) { return DISKII_PROM[address]; }
|
||||||
read(address) { return this.doIO(address, 0); }
|
read(address) { return this.doIO(address, 0); }
|
||||||
write(address, value) { this.doIO(address, value); }
|
write(address, value) { this.doIO(address, value); }
|
||||||
|
|
||||||
|
406
src/machine/galaxian.ts
Normal file
406
src/machine/galaxian.ts
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
|
||||||
|
import { Z80, Z80State } from "../common/cpu/ZilogZ80";
|
||||||
|
import { BasicScanlineMachine } from "../common/devices";
|
||||||
|
import { KeyFlags, newAddressDecoder, padBytes, noise, Keys, makeKeycodeMap, newKeyboardHandler, EmuHalt } from "../common/emu";
|
||||||
|
import { TssChannelAdapter, MasterAudio, AY38910_Audio } from "../common/audio";
|
||||||
|
import { hex } from "../common/util";
|
||||||
|
|
||||||
|
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 bitcolors = [
|
||||||
|
0x000021, 0x000047, 0x000097, // red
|
||||||
|
0x002100, 0x004700, 0x009700, // green
|
||||||
|
0x510000, 0xae0000 // blue
|
||||||
|
];
|
||||||
|
|
||||||
|
const GalaxianVideo = function (rom: Uint8Array, vram: Uint8Array, oram: Uint8Array, 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) {
|
||||||
|
var pixofs = sl * 264;
|
||||||
|
// hide offscreen on left + right (b/c rotated)
|
||||||
|
if (!this.showOffscreenObjects && (sl < 16 || sl >= 240)) {
|
||||||
|
for (var i = 0; i < 264; i++)
|
||||||
|
pixels[pixofs + i] = 0xff000000;
|
||||||
|
return; // offscreen
|
||||||
|
}
|
||||||
|
// draw tiles
|
||||||
|
var outi = pixofs; // starting output pixel in frame buffer
|
||||||
|
for (var xx = 0; xx < 32; xx++) {
|
||||||
|
var xofs = xx;
|
||||||
|
var scroll = oram[xofs * 2]; // even entries control scroll position
|
||||||
|
var attrib = oram[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[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[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[base + 3] + 1; // +1 pixel offset from tiles
|
||||||
|
if (sx == 0 && !this.showOffscreenObjects) {
|
||||||
|
continue; // drawn off-buffer
|
||||||
|
}
|
||||||
|
var code = oram[base + 1];
|
||||||
|
var flipx = code & 0x40; // TODO: flipx
|
||||||
|
if (code & 0x80) // flipy
|
||||||
|
yy = 15 - yy;
|
||||||
|
code &= 0x3f;
|
||||||
|
var color0 = (oram[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[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[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 XTAL = 18432000.0;
|
||||||
|
const scanlinesPerFrame = 264;
|
||||||
|
const cpuFrequency = XTAL / 6; // 3.072 MHz
|
||||||
|
const hsyncFrequency = XTAL / 3 / 192 / 2; // 16 kHz
|
||||||
|
const vsyncFrequency = hsyncFrequency / 132 / 2; // 60.606060 Hz
|
||||||
|
const vblankDuration = 1 / vsyncFrequency * (20 / 132); // 2500 us
|
||||||
|
const cpuCyclesPerLine = cpuFrequency / hsyncFrequency;
|
||||||
|
const INITIAL_WATCHDOG = 8;
|
||||||
|
|
||||||
|
const audioOversample = 2;
|
||||||
|
const audioSampleRate = 60 * scanlinesPerFrame; // why not hsync?
|
||||||
|
|
||||||
|
export class GalaxianMachine extends BasicScanlineMachine {
|
||||||
|
|
||||||
|
options = {};
|
||||||
|
palBase = 0x3800;
|
||||||
|
keyMap = GALAXIAN_KEYCODE_MAP;
|
||||||
|
|
||||||
|
cpuFrequency = cpuFrequency;
|
||||||
|
canvasWidth = 264;
|
||||||
|
numTotalScanlines = 264;
|
||||||
|
numVisibleScanlines = 264;
|
||||||
|
defaultROMSize = 0x4000;
|
||||||
|
sampleRate = audioSampleRate * audioOversample;
|
||||||
|
cpuCyclesPerLine = cpuCyclesPerLine | 0;
|
||||||
|
rotate = 90;
|
||||||
|
|
||||||
|
cpu: Z80 = new Z80();
|
||||||
|
ram = new Uint8Array(0x800);
|
||||||
|
vram = new Uint8Array(0x400);
|
||||||
|
oram = new Uint8Array(0x100);
|
||||||
|
palette: Uint32Array;
|
||||||
|
gfx; // GalaxianVideo
|
||||||
|
audioadapter;
|
||||||
|
psg1: AY38910_Audio;
|
||||||
|
psg2: AY38910_Audio;
|
||||||
|
watchdog_counter: number = 0;
|
||||||
|
interruptEnabled: number = 0;
|
||||||
|
defaultInputs: number[] = [0xe, 0x8, 0x0];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
var audio = new MasterAudio();
|
||||||
|
this.psg1 = new AY38910_Audio(audio);
|
||||||
|
this.psg2 = new AY38910_Audio(audio);
|
||||||
|
this.audioadapter = new TssChannelAdapter([this.psg1.psg, this.psg2.psg], audioOversample, this.sampleRate);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.rom = new Uint8Array(this.defaultROMSize);
|
||||||
|
this.palette = new Uint32Array(new ArrayBuffer(32 * 4));
|
||||||
|
this.gfx = new GalaxianVideo(this.rom, this.vram, this.oram, this.palette, this.options);
|
||||||
|
this.connectCPUMemoryBus(this);
|
||||||
|
this.connectCPUIOBus(this.newIOBus());
|
||||||
|
this.inputs.set(this.defaultInputs);
|
||||||
|
this.handler = newKeyboardHandler(this.inputs, this.keyMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
read = newAddressDecoder([
|
||||||
|
[0x0000, 0x3fff, 0, (a) => { return this.rom ? this.rom[a] : null; }],
|
||||||
|
[0x4000, 0x47ff, 0x3ff, (a) => { return this.ram[a]; }],
|
||||||
|
[0x5000, 0x57ff, 0x3ff, (a) => { return this.vram[a]; }],
|
||||||
|
[0x5800, 0x5fff, 0xff, (a) => { return this.oram[a]; }],
|
||||||
|
[0x6000, 0x6000, 0, (a) => { return this.inputs[0]; }],
|
||||||
|
[0x6800, 0x6800, 0, (a) => { return this.inputs[1]; }],
|
||||||
|
[0x7000, 0x7000, 0, (a) => { return this.inputs[2]; }],
|
||||||
|
[0x7800, 0x7800, 0, (a) => { this.watchdog_counter = INITIAL_WATCHDOG; }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
readConst(a : number) {
|
||||||
|
return (a < 0x7000) ? this.read(a) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
write = newAddressDecoder([
|
||||||
|
[0x4000, 0x47ff, 0x3ff, (a, v) => { this.ram[a] = v; }],
|
||||||
|
[0x5000, 0x57ff, 0x3ff, (a, v) => { this.vram[a] = v; }],
|
||||||
|
[0x5800, 0x5fff, 0xff, (a, v) => { this.oram[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, (a, v) => { this.outlatches[a] = v; }],
|
||||||
|
[0x7001, 0x7001, 0, (a, v) => { this.interruptEnabled = v & 1; }],
|
||||||
|
[0x7004, 0x7004, 0, (a, v) => { this.gfx.starsEnabled = v & 1; }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
newIOBus() {
|
||||||
|
return {
|
||||||
|
read: (addr) => {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
write: (addr, val) => {
|
||||||
|
if (addr & 0x1) { this.psg1.selectRegister(val & 0xf); };
|
||||||
|
if (addr & 0x2) { this.psg1.setData(val); };
|
||||||
|
if (addr & 0x4) { this.psg2.selectRegister(val & 0xf); };
|
||||||
|
if (addr & 0x8) { this.psg2.setData(val); };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
super.reset();
|
||||||
|
this.psg1.reset();
|
||||||
|
this.psg2.reset();
|
||||||
|
this.watchdog_counter = INITIAL_WATCHDOG;
|
||||||
|
}
|
||||||
|
|
||||||
|
startScanline() {
|
||||||
|
this.audio && this.audioadapter && this.audioadapter.generate(this.audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawScanline() {
|
||||||
|
this.gfx.drawScanline(this.pixels, this.scanline);
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceFrame(trap) {
|
||||||
|
var steps = super.advanceFrame(trap);
|
||||||
|
|
||||||
|
// advance graphics
|
||||||
|
this.gfx.advanceFrame();
|
||||||
|
// clear bottom of screen?
|
||||||
|
if (!this.gfx.showOffscreenObjects) {
|
||||||
|
for (var i = 0; i < 264; i++)
|
||||||
|
this.pixels.fill(0xff000000, 256 + i * 264, 264 + i * 264);
|
||||||
|
}
|
||||||
|
// watchdog fired?
|
||||||
|
if (this.watchdog_counter-- <= 0) {
|
||||||
|
throw new EmuHalt("WATCHDOG FIRED");
|
||||||
|
}
|
||||||
|
// NMI interrupt @ 0x66
|
||||||
|
if (this.interruptEnabled) { this.cpu.NMI(); }
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadROM(data) {
|
||||||
|
this.rom.set(padBytes(data, this.defaultROMSize));
|
||||||
|
for (var i = 0; i < 32; i++) {
|
||||||
|
var b = this.rom[this.palBase + i];
|
||||||
|
this.palette[i] = 0xff000000;
|
||||||
|
for (var j = 0; j < 8; j++)
|
||||||
|
if (((1 << j) & b))
|
||||||
|
this.palette[i] += bitcolors[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(state) {
|
||||||
|
super.loadState(state);
|
||||||
|
this.vram.set(state.bv);
|
||||||
|
this.oram.set(state.bo);
|
||||||
|
this.watchdog_counter = state.wdc;
|
||||||
|
this.interruptEnabled = state.ie;
|
||||||
|
this.gfx.starsEnabled = state.se;
|
||||||
|
this.gfx.frameCounter = state.fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState() {
|
||||||
|
var state = super.saveState();
|
||||||
|
state['bv'] = this.vram.slice(0);
|
||||||
|
state['bo'] = this.oram.slice(0);
|
||||||
|
state['fc'] = this.gfx.frameCounter;
|
||||||
|
state['ie'] = this.interruptEnabled;
|
||||||
|
state['se'] = this.gfx.starsEnabled;
|
||||||
|
state['wdc'] = this.watchdog_counter;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GalaxianScrambleMachine extends GalaxianMachine {
|
||||||
|
|
||||||
|
defaultROMSize = 0x5020;
|
||||||
|
palBase = 0x5000;
|
||||||
|
scramble = true;
|
||||||
|
keyMap = SCRAMBLE_KEYCODE_MAP;
|
||||||
|
options = {
|
||||||
|
gfxBase: 0x4000,
|
||||||
|
missileWidth: 1,
|
||||||
|
missileOffset: 6,
|
||||||
|
};
|
||||||
|
defaultInputs = [0xff, 0xfc, 0xf1];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.init(); // TODO: why do we have to call twice?
|
||||||
|
}
|
||||||
|
|
||||||
|
read = newAddressDecoder([
|
||||||
|
[0x0000, 0x3fff, 0, (a) => { return this.rom[a]; }],
|
||||||
|
[0x4000, 0x47ff, 0x7ff, (a) => { return this.ram[a]; }],
|
||||||
|
[0x4800, 0x4fff, 0x3ff, (a) => { return this.vram[a]; }],
|
||||||
|
[0x5000, 0x5fff, 0xff, (a) => { return this.oram[a]; }],
|
||||||
|
[0x7000, 0x7000, 0, (a) => { this.watchdog_counter = INITIAL_WATCHDOG; }],
|
||||||
|
[0x7800, 0x7800, 0, (a) => { this.watchdog_counter = INITIAL_WATCHDOG; }],
|
||||||
|
//[0x8000, 0x820f, 0, function(a) { return noise(); }], // TODO: remove
|
||||||
|
[0x8100, 0x8100, 0, (a) => { return this.inputs[0]; }],
|
||||||
|
[0x8101, 0x8101, 0, (a) => { return this.inputs[1]; }],
|
||||||
|
[0x8102, 0x8102, 0, (a) => { return this.inputs[2] | this.scramble_protection_alt_r(); }],
|
||||||
|
[0x8202, 0x8202, 0, (a) => { return this.m_protection_result; }], // scramble (protection)
|
||||||
|
[0x9100, 0x9100, 0, (a) => { return this.inputs[0]; }],
|
||||||
|
[0x9101, 0x9101, 0, (a) => { return this.inputs[1]; }],
|
||||||
|
[0x9102, 0x9102, 0, (a) => { return this.inputs[2] | this.scramble_protection_alt_r(); }],
|
||||||
|
[0x9212, 0x9212, 0, (a) => { return this.m_protection_result; }], // scramble (protection)
|
||||||
|
//[0, 0xffff, 0, function(a) { console.log(hex(a)); return 0; }]
|
||||||
|
]);
|
||||||
|
write = newAddressDecoder([
|
||||||
|
[0x4000, 0x47ff, 0x7ff, (a, v) => { this.ram[a] = v; }],
|
||||||
|
[0x4800, 0x4fff, 0x3ff, (a, v) => { this.vram[a] = v; }],
|
||||||
|
[0x5000, 0x5fff, 0xff, (a, v) => { this.oram[a] = v; }],
|
||||||
|
[0x6801, 0x6801, 0, (a, v) => { this.interruptEnabled = v & 1; /*console.log(a,v,cpu.getPC().toString(16));*/ }],
|
||||||
|
[0x6802, 0x6802, 0, (a, v) => { /* TODO: coin counter */ }],
|
||||||
|
[0x6803, 0x6803, 0, (a, v) => { /* TODO: backgroundColor = (v & 1) ? 0xFF000056 : 0xFF000000; */ }],
|
||||||
|
[0x6804, 0x6804, 0, (a, v) => { this.gfx.starsEnabled = v & 1; }],
|
||||||
|
[0x6808, 0x6808, 0, (a, v) => { this.gfx.missileWidth = v; }], // not on h/w
|
||||||
|
[0x6809, 0x6809, 0, (a, v) => { this.gfx.missileOffset = v; }], // not on h/w
|
||||||
|
[0x8202, 0x8202, 0, this.scramble_protection_w.bind(this)],
|
||||||
|
//[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)); }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
m_protection_state = 0;
|
||||||
|
m_protection_result = 0;
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
this.m_protection_state = (this.m_protection_state << 4) | (data & 0x0f);
|
||||||
|
switch (this.m_protection_state & 0xfff) {
|
||||||
|
/* scramble */
|
||||||
|
case 0xf09: this.m_protection_result = 0xff; break;
|
||||||
|
case 0xa49: this.m_protection_result = 0xbf; break;
|
||||||
|
case 0x319: this.m_protection_result = 0x4f; break;
|
||||||
|
case 0x5c9: this.m_protection_result = 0x6f; break;
|
||||||
|
|
||||||
|
/* scrambls */
|
||||||
|
case 0x246: this.m_protection_result ^= 0x80; break;
|
||||||
|
case 0xb5f: this.m_protection_result = 0x6f; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scramble_protection_alt_r() {
|
||||||
|
var bit = (this.m_protection_result >> 7) & 1;
|
||||||
|
return (bit << 5) | ((bit ^ 1) << 7);
|
||||||
|
}
|
||||||
|
}
|
@ -1,442 +1,34 @@
|
|||||||
|
|
||||||
import { Platform, BaseZ80Platform } from "../common/baseplatform";
|
import { Platform } from "../common/baseplatform";
|
||||||
import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap } from "../common/emu";
|
import { PLATFORMS } from "../common/emu";
|
||||||
import { hex } from "../common/util";
|
import { GalaxianMachine, GalaxianScrambleMachine } from "../machine/galaxian";
|
||||||
import { MasterAudio, AY38910_Audio } from "../common/audio";
|
import { BaseZ80MachinePlatform } from "../common/baseplatform";
|
||||||
|
|
||||||
const GALAXIAN_PRESETS = [
|
const GALAXIAN_PRESETS = [
|
||||||
{ id: 'gfxtest.c', name: 'Graphics Test' },
|
{ id: 'gfxtest.c', name: 'Graphics Test' },
|
||||||
{ id: 'shoot2.c', name: 'Solarian Game' },
|
{ id: 'shoot2.c', name: 'Solarian Game' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const GALAXIAN_KEYCODE_MAP = makeKeycodeMap([
|
class GalaxianPlatform extends BaseZ80MachinePlatform<GalaxianMachine> implements Platform {
|
||||||
[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([
|
newMachine() { return new GalaxianMachine(); }
|
||||||
[Keys.UP, 0, -0x1], // P1
|
getPresets() { return GALAXIAN_PRESETS; }
|
||||||
[Keys.B, 0, -0x2], // fire
|
getDefaultExtension() { return ".c"; };
|
||||||
[Keys.VK_7, 0, -0x4], // credit
|
readAddress(a) { return this.machine.readConst(a); }
|
||||||
[Keys.A, 0, -0x8], // bomb
|
readVRAMAddress(a) { return (a < 0x800) ? this.machine.vram[a] : this.machine.oram[a-0x800]; }
|
||||||
[Keys.RIGHT, 0, -0x10],
|
// TODO loadBIOS(bios) { this.machine.loadBIOS(a); }
|
||||||
[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) : number {
|
|
||||||
var steps = 0;
|
|
||||||
for (var sl = 0; sl < scanlinesPerFrame; sl++) {
|
|
||||||
this.scanline = sl;
|
|
||||||
if (!novideo) {
|
|
||||||
gfx.drawScanline(pixels, sl);
|
|
||||||
}
|
|
||||||
steps += 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(); }
|
|
||||||
return steps;
|
|
||||||
}
|
|
||||||
|
|
||||||
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:[
|
getMemoryMap = function() { return { main:[
|
||||||
{name:'Video RAM',start:0x5000,size:0x400,type:'ram'},
|
{name:'Video RAM',start:0x5000,size:0x400,type:'ram'},
|
||||||
{name:'Sprite RAM',start:0x5800,size:0x100,type:'ram'},
|
{name:'Sprite RAM',start:0x5800,size:0x100,type:'ram'},
|
||||||
{name:'I/O Registers',start:0x6000,size:0x2000,type:'io'},
|
{name:'I/O Registers',start:0x6000,size:0x2000,type:'io'},
|
||||||
] } };
|
] } };
|
||||||
}
|
|
||||||
|
|
||||||
return new GalaxianPlatform();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const _GalaxianScramblePlatform = function(mainElement) {
|
class GalaxianScramblePlatform extends GalaxianPlatform implements Platform {
|
||||||
return _GalaxianPlatform(mainElement, {
|
|
||||||
romSize: 0x5020,
|
newMachine() { return new GalaxianScrambleMachine(); }
|
||||||
gfxBase: 0x4000,
|
|
||||||
palBase: 0x5000,
|
|
||||||
scramble: true,
|
|
||||||
keyMap: SCRAMBLE_KEYCODE_MAP,
|
|
||||||
missileWidth: 1,
|
|
||||||
missileOffset: 6,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PLATFORMS['galaxian'] = _GalaxianPlatform;
|
PLATFORMS['galaxian'] = GalaxianPlatform;
|
||||||
PLATFORMS['galaxian-scramble'] = _GalaxianScramblePlatform;
|
PLATFORMS['galaxian-scramble'] = GalaxianScramblePlatform;
|
||||||
|
@ -48,6 +48,7 @@ var _atari8 = require('gen/platform/atari8.js');
|
|||||||
var _atari7800 = require('gen/platform/atari7800.js');
|
var _atari7800 = require('gen/platform/atari7800.js');
|
||||||
var _coleco = require('gen/platform/coleco.js');
|
var _coleco = require('gen/platform/coleco.js');
|
||||||
var _sms = require('gen/platform/sms.js');
|
var _sms = require('gen/platform/sms.js');
|
||||||
|
var _c64 = require('gen/platform/c64.js');
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
@ -301,4 +302,14 @@ describe('Platform Replay', () => {
|
|||||||
assert.equal(0x1800, platform.saveState().maria.dll);
|
assert.equal(0x1800, platform.saveState().maria.dll);
|
||||||
assert.equal(39, platform.readAddress(0x81)); // player y pos
|
assert.equal(39, platform.readAddress(0x81)); // player y pos
|
||||||
});
|
});
|
||||||
|
/* TODO
|
||||||
|
it('Should run c64', () => {
|
||||||
|
var platform = testPlatform('c64', 'sprites.dasm.rom', 92, (platform, frameno) => {
|
||||||
|
if (frameno == 62) {
|
||||||
|
keycallback(Keys.VK_DOWN.c, Keys.VK_DOWN.c, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(39, platform.readAddress(0x81)); // player y pos
|
||||||
|
});
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user