web/shell -> shell/

This commit is contained in:
kichikuou
2017-03-25 13:31:02 +09:00
parent 51b5280d61
commit d829fa85d5
12 changed files with 1 additions and 1 deletions
+264
View File
@@ -0,0 +1,264 @@
/// <reference path="util.ts" />
/// <reference path="volume.ts" />
namespace xsystem35 {
declare var webkitAudioContext: any;
export class AudioManager {
private context: AudioContext;
private masterGain: GainNode;
private slots: PCMSound[];
private buffers: AudioBuffer[];
private isSafari: boolean;
constructor(volumeControl: VolumeControl) {
if (typeof (AudioContext) !== 'undefined') {
this.context = new AudioContext();
} else if (typeof (webkitAudioContext) !== 'undefined') {
this.context = new webkitAudioContext();
this.isSafari = true;
this.removeUserGestureRestriction();
}
this.masterGain = this.context.createGain();
this.masterGain.connect(this.context.destination);
this.slots = [];
this.buffers = [];
volumeControl.addEventListener(this.onVolumeChanged.bind(this));
this.masterGain.gain.value = volumeControl.volume();
}
private removeUserGestureRestriction() {
let hanlder = () => {
let src = this.context.createBufferSource();
src.buffer = this.context.createBuffer(1, 1, 22050);
src.connect(this.context.destination);
src.start();
console.log('AudioContext unlocked');
window.removeEventListener('touchend', hanlder);
};
window.addEventListener('touchend', hanlder);
}
private load(no: number): Promise<AudioBuffer> {
let buf = this.getWave(no);
if (!buf)
return Promise.reject('Failed to open wave ' + no);
let decoded: Promise<AudioBuffer>;
if (this.isSafari) {
decoded = new Promise((resolve, reject) => {
this.context.decodeAudioData(buf, resolve, reject);
});
} else {
decoded = this.context.decodeAudioData(buf);
}
return decoded.then((audioBuf) => {
this.buffers[no] = audioBuf;
return audioBuf;
});
}
private getWave(no: number): ArrayBuffer {
let dfile = _ald_getdata(2 /* DRIFILE_WAVE */, no - 1);
if (!dfile)
return null;
let ptr = Module.getValue(dfile + 8, '*');
let size = Module.getValue(dfile, 'i32');
let buf = Module.HEAPU8.buffer.slice(ptr, ptr + size);
_ald_freedata(dfile);
return buf;
}
pcm_load(slot: number, no: number) {
EmterpreterAsync.handle((resume) => {
this.pcm_stop(slot);
if (this.buffers[no]) {
this.slots[slot] = new PCMSoundSimple(this.masterGain, this.buffers[no]);
return resume();
}
this.load(no).then((audioBuf) => {
this.slots[slot] = new PCMSoundSimple(this.masterGain, audioBuf);
resume();
});
});
}
pcm_load_mixlr(slot: number, noL: number, noR: number) {
EmterpreterAsync.handle((resume) => {
this.pcm_stop(slot);
if (this.buffers[noL] && this.buffers[noR]) {
this.slots[slot] = new PCMSoundMixLR(this.masterGain, this.buffers[noL], this.buffers[noR]);
return resume();
}
let ps = [];
if (!this.buffers[noL]) ps.push(this.load(noL));
if (!this.buffers[noR]) ps.push(this.load(noR));
Promise.all(ps).then(() => {
this.slots[slot] = new PCMSoundMixLR(this.masterGain, this.buffers[noL], this.buffers[noR]);
resume();
});
});
}
pcm_start(slot: number, loop: number): number {
if (this.slots[slot]) {
this.slots[slot].start(loop);
return 1;
}
return 0;
}
pcm_stop(slot: number): number {
if (!this.slots[slot])
return 0;
this.slots[slot].stop();
this.slots[slot] = null;
return 1;
}
pcm_fadeout(slot: number, msec: number): number {
if (!this.slots[slot])
return 0;
this.slots[slot].fadeout(msec);
return 1;
}
pcm_getpos(slot: number): number {
if (!this.slots[slot])
return 0;
return this.slots[slot].getPosition() * 1000;
}
pcm_setvol(slot: number, vol: number): number {
if (!this.slots[slot])
return 0;
this.slots[slot].setGain(vol / 100);
return 1;
}
pcm_getwavelen(slot: number): number {
if (!this.slots[slot])
return 0;
return this.slots[slot].duration * 1000;
}
pcm_isplaying(slot: number): number {
if (!this.slots[slot])
return 0;
return this.slots[slot].isPlaying() ? 1 : 0;
}
private onVolumeChanged(evt: CustomEvent) {
this.masterGain.gain.value = evt.detail;
}
}
abstract class PCMSound {
protected context: AudioContext;
protected gain: GainNode;
protected startTime: number;
constructor(protected dst: AudioNode) {
this.context = dst.context;
this.gain = this.context.createGain();
this.gain.connect(dst);
}
abstract start(loop: number): void;
abstract stop(): void;
setGain(gain: number) {
this.gain.gain.value = gain;
}
fadeout(msec: number) {
this.gain.gain.linearRampToValueAtTime(0, this.context.currentTime + msec / 1000);
}
getPosition(): number {
if (!this.startTime)
return 0;
return this.context.currentTime - this.startTime;
}
isPlaying(): boolean {
return !!this.startTime;
}
abstract get duration(): number;
}
class PCMSoundSimple extends PCMSound {
private node: AudioBufferSourceNode;
constructor(dst: AudioNode, buf: AudioBuffer) {
super(dst);
this.node = this.context.createBufferSource();
this.node.buffer = buf;
this.node.connect(this.gain);
this.node.onended = this.onended.bind(this);
}
start(loop: number) {
if (loop === 0)
this.node.loop = true;
else if (loop !== 1)
console.warn('Unsupported PCM loop count ' + loop);
this.node.start();
this.startTime = this.context.currentTime;
}
stop() {
if (this.startTime) {
this.node.stop();
this.startTime = null;
}
}
get duration() {
return this.node.buffer.duration;
}
private onended() {
this.startTime = null;
}
}
class PCMSoundMixLR extends PCMSound {
private lsrc: AudioBufferSourceNode;
private rsrc: AudioBufferSourceNode;
private endCount = 0;
constructor(dst: AudioNode, lbuf: AudioBuffer, rbuf: AudioBuffer) {
super(dst);
this.lsrc = this.context.createBufferSource();
this.rsrc = this.context.createBufferSource();
this.lsrc.buffer = lbuf;
this.rsrc.buffer = rbuf;
let merger = this.context.createChannelMerger(2);
merger.connect(this.gain);
this.lsrc.connect(merger, 0, 0);
this.rsrc.connect(merger, 0, 1);
this.lsrc.onended = this.rsrc.onended = this.onended.bind(this);
}
start(loop: number) {
if (loop !== 1)
console.warn('PCMSoundMixLR: loop is not supported ' + loop);
this.lsrc.start();
this.rsrc.start();
this.startTime = this.context.currentTime;
}
stop() {
if (this.startTime) {
this.lsrc.stop();
this.rsrc.stop();
this.startTime = null;
}
}
get duration() {
return Math.max(this.lsrc.buffer.duration, this.rsrc.buffer.duration);
}
private onended() {
this.endCount++;
if (this.endCount === 2)
this.startTime = null;
}
}
}
+65
View File
@@ -0,0 +1,65 @@
/// <reference path="util.ts" />
/// <reference path="volume.ts" />
namespace xsystem35 {
export class CDPlayer {
private audio = <HTMLAudioElement>$('audio');
private blobs: Blob[];
private currentTrack: number;
waiting: boolean;
constructor(private imageLoader: ImageLoader, volumeControl: VolumeControl) {
this.blobs = [];
volumeControl.addEventListener(this.onVolumeChanged.bind(this));
this.audio.volume = volumeControl.volume();
this.waiting = false;
this.removeUserGestureRestriction();
}
play(track: number, loop: number) {
this.currentTrack = track;
this.waiting = true;
if (this.blobs[track]) {
this.startPlayback(this.blobs[track], loop);
return;
}
this.imageLoader.getCDDA(track, (blob) => {
this.blobs[track] = blob;
this.startPlayback(blob, loop);
});
}
stop() {
this.audio.pause();
this.currentTrack = null;
}
getPosition(): number {
if (!this.currentTrack)
return 0;
let time = Math.round(this.audio.currentTime * 75);
return this.currentTrack | time << 8;
}
private startPlayback(blob: Blob, loop: number) {
this.audio.setAttribute('src', URL.createObjectURL(blob));
this.audio.loop = (loop !== 0);
this.audio.load();
this.audio.play();
this.waiting = false;
}
private onVolumeChanged(evt: CustomEvent) {
this.audio.volume = evt.detail;
}
private removeUserGestureRestriction() {
let hanlder = () => {
this.audio.load();
console.log('CDDA unlocked');
window.removeEventListener('touchend', hanlder);
};
window.addEventListener('touchend', hanlder);
}
}
}
+303
View File
@@ -0,0 +1,303 @@
/// <reference path="text-decoder.ts" />
class ISO9660FileSystem {
static async create(sectorReader: CDImageReader): Promise<ISO9660FileSystem> {
let pvd = new PVD(await sectorReader.readSector(0x10));
return new ISO9660FileSystem(sectorReader, pvd);
}
private constructor(private sectorReader: CDImageReader, private pvd: PVD) {
if (this.pvd.type !== 1)
throw('PVD not found');
}
volumeLabel(): string {
return this.pvd.volumeLabel();
}
rootDir(): DirEnt {
return this.pvd.rootDirEnt();
}
async getDirEnt(name: string, parent: DirEnt): Promise<DirEnt> {
name = name.toLowerCase();
for (let e of await this.readDir(parent)) {
if (e.name.toLowerCase() === name)
return e;
}
return null;
}
async readDir(dirent: DirEnt): Promise<DirEnt[]> {
let sector = dirent.sector;
let position = 0;
let length = dirent.size;
let entries: DirEnt[] = [];
let buf: ArrayBuffer;
while (position < length) {
if (position === 0)
buf = await this.sectorReader.readSector(sector);
let child = new DirEnt(buf, position);
if (child.length === 0) {
// Padded end of sector
position = 2048;
} else {
entries.push(child);
position += child.length;
}
if (position > 2048)
throw('dirent across sector boundary');
if (position === 2048) {
sector++;
position = 0;
length -= 2048;
}
}
return entries;
}
readFile(dirent: DirEnt): Promise<Uint8Array[]> {
return this.sectorReader.readSequentialSectors(dirent.sector, dirent.size);
}
}
class PVD {
private view: DataView;
constructor(private buf: ArrayBuffer) {
this.view = new DataView(buf);
}
get type(): number {
return this.view.getUint8(0);
}
volumeLabel(): string {
let decoder = new TextDecoder('shift_jis');
return decoder.decode(new DataView(this.buf, 40, 32)).trim();
}
rootDirEnt(): DirEnt {
return new DirEnt(this.buf, 156);
}
}
class DirEnt {
private view: DataView;
constructor(private buf: ArrayBuffer, private offset: number) {
this.view = new DataView(buf, offset);
}
get length(): number {
return this.view.getUint8(0);
}
get sector(): number {
return this.view.getUint32(2, true);
}
get size(): number {
return this.view.getUint32(10, true);
}
get isDirectory(): boolean {
return (this.view.getUint8(25) & 2) !== 0;
}
get name(): string {
let len = this.view.getUint8(32);
let decoder = new TextDecoder('shift_jis');
return decoder.decode(new DataView(this.buf, this.offset + 33, len)).split(';')[0];
}
}
interface CDImageReader {
readSector(sector: number): Promise<ArrayBuffer>;
readSequentialSectors(startSector: number, length: number): Promise<Uint8Array[]>;
maxTrack(): number;
extractTrack(track: number): Promise<Blob>;
}
function readAsArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => { resolve(reader.result); };
reader.onerror = () => { reject(reader.error); };
reader.readAsArrayBuffer(blob);
});
}
function readAsText(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => { resolve(reader.result); };
reader.onerror = () => { reject(reader.error); };
reader.readAsText(blob);
});
}
class ImageReaderBase {
constructor(public image: File) {}
async readSequential(startOffset: number,
bytesToRead: number,
blockSize: number,
sectorSize: number,
sectorOffset: number): Promise<Uint8Array[]> {
let sectors = Math.ceil(bytesToRead / sectorSize);
let blob = this.image.slice(startOffset, startOffset + sectors * blockSize);
let buf = await readAsArrayBuffer(blob);
let bufs: Uint8Array[] = [];
for (let i = 0; i < sectors; i++) {
bufs.push(new Uint8Array(buf, i * blockSize + sectorOffset, Math.min(bytesToRead, sectorSize)));
bytesToRead -= sectorSize;
}
return bufs;
}
}
class ImgCueReader extends ImageReaderBase implements CDImageReader {
private tracks: Array<{ type: string; index: string[]; }>;
static async create(img: File, cue: File): Promise<ImgCueReader> {
let reader = new ImgCueReader(img);
await reader.parseCue(cue);
return reader;
}
private constructor(img: File) {
super(img);
}
readSector(sector: number): Promise<ArrayBuffer> {
let start = sector * 2352 + 16;
let end = start + 2048;
return readAsArrayBuffer(this.image.slice(start, end));
}
readSequentialSectors(startSector: number, length: number): Promise<Uint8Array[]> {
return this.readSequential(startSector * 2352, length, 2352, 2048, 16);
}
private async parseCue(cueFile: File) {
let lines = (await readAsText(cueFile)).split('\n');
this.tracks = [];
let currentTrack: number = null;
for (let line of lines) {
let fields = line.trim().split(/\s+/);
switch (fields[0]) {
case 'TRACK':
currentTrack = Number(fields[1]);
this.tracks[currentTrack] = {type: fields[2], index: []};
break;
case 'INDEX':
if (currentTrack)
this.tracks[currentTrack].index[Number(fields[1])] = fields[2];
break;
default:
; // Do nothing
}
}
}
maxTrack(): number {
return this.tracks.length - 1;
}
async extractTrack(track: number): Promise<Blob> {
if (!this.tracks[track] || this.tracks[track].type !== 'AUDIO')
return;
let start = this.indexToSector(this.tracks[track].index[1]) * 2352;
let end: number;
if (this.tracks[track + 1]) {
let index = this.tracks[track + 1].index[0] || this.tracks[track + 1].index[1];
end = this.indexToSector(index) * 2352;
} else {
end = this.image.size;
}
let size = end - start;
return new Blob([createWaveHeader(size), this.image.slice(start, start + size)], {type : 'audio/wav'});
}
private indexToSector(index: string): number {
let msf = index.split(':').map(Number);
return msf[0] * 60 * 75 + msf[1] * 75 + msf[2];
}
}
enum MdsTrackMode { Audio = 0xa9, Mode1 = 0xaa };
class MdfMdsReader extends ImageReaderBase implements CDImageReader {
private tracks: Array<{ mode: number; sectorSize: number; offset: number; sectors: number; }>;
static async create(mdf: File, mds: File): Promise<MdfMdsReader> {
let reader = new MdfMdsReader(mdf);
await reader.parseMds(mds);
return reader;
}
private constructor(mdf: File) {
super(mdf);
}
private async parseMds(mdsFile: File) {
let buf = await readAsArrayBuffer(mdsFile);
let signature = new TextDecoder().decode(new DataView(buf, 0, 16));
if (signature !== 'MEDIA DESCRIPTOR')
throw mdsFile.name + ': not a mds file';
let header = new DataView(buf, 0, 0x70);
let entries = header.getUint8(0x62);
this.tracks = [];
for (let i = 0; i < entries; i++) {
let trackData = new DataView(buf, 0x70 + i * 0x50, 0x50);
let extraData = new DataView(buf, 0x70 + entries * 0x50 + i * 8, 8);
let mode = trackData.getUint8(0x00);
let track = trackData.getUint8(0x04);
let sectorSize = trackData.getUint16(0x10, true);
let offset = trackData.getUint32(0x28, true); // >4GB offset is not supported.
let sectors = extraData.getUint32(0x4, true);
if (track < 100)
this.tracks[track] = {mode, sectorSize, offset, sectors};
}
if (this.tracks[1].mode !== MdsTrackMode.Mode1)
throw 'track 1 is not mode1';
}
readSector(sector: number): Promise<ArrayBuffer> {
let start = sector * this.tracks[1].sectorSize + 16;
let end = start + 2048;
return readAsArrayBuffer(this.image.slice(start, end));
}
readSequentialSectors(startSector: number, length: number): Promise<Uint8Array[]> {
let track = this.tracks[1];
return this.readSequential(track.offset + startSector * track.sectorSize, length, track.sectorSize, 2048, 16);
}
maxTrack(): number {
return this.tracks.length - 1;
}
async extractTrack(track: number): Promise<Blob> {
if (!this.tracks[track] || this.tracks[track].mode !== MdsTrackMode.Audio)
return;
let size = this.tracks[track].sectors * 2352;
let chunks = await this.readSequential(this.tracks[track].offset, size, this.tracks[track].sectorSize, 2352, 0);
return new Blob([<any>createWaveHeader(size)].concat(chunks), {type : 'audio/wav'});
}
}
function createWaveHeader(size: number): ArrayBuffer {
let buf = new ArrayBuffer(44);
let view = new DataView(buf);
view.setUint32(0, 0x52494646, false); // 'RIFF'
view.setUint32(4, size + 36, true); // filesize - 8
view.setUint32(8, 0x57415645, false); // 'WAVE'
view.setUint32(12, 0x666D7420, false); // 'fmt '
view.setUint32(16, 16, true); // size of fmt chunk
view.setUint16(20, 1, true); // PCM format
view.setUint16(22, 2, true); // stereo
view.setUint32(24, 44100, true); // sampling rate
view.setUint32(28, 176400, true); // bytes/sec
view.setUint16(32, 4, true); // block size
view.setUint16(34, 16, true); // bit/sample
view.setUint32(36, 0x64617461, false); // 'data'
view.setUint32(40, size, true); // data size
return buf;
}
+142
View File
@@ -0,0 +1,142 @@
/// <reference path="util.ts" />
/// <reference path="cdimage.ts" />
namespace xsystem35 {
export class ImageLoader {
private imgFile: File;
private cueFile: File;
private imageReader: CDImageReader;
constructor(private runMain: () => void) {
$('#fileselect').addEventListener('change', this.handleFileSelect.bind(this), false);
document.body.ondragover = this.handleDragOver.bind(this);
document.body.ondrop = this.handleDrop.bind(this);
}
async getCDDA(track: number, callback: (wav: Blob) => void) {
let blob = await this.imageReader.extractTrack(track);
callback(blob);
}
private handleFileSelect(evt: Event) {
let input = <HTMLInputElement>evt.target;
let files = input.files;
for (let i = 0; i < files.length; i++)
this.setFile(files[i]);
input.value = '';
}
private handleDragOver(evt: DragEvent) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
}
private handleDrop(evt: DragEvent) {
evt.stopPropagation();
evt.preventDefault();
let files = evt.dataTransfer.files;
for (let i = 0; i < files.length; i++)
this.setFile(files[i]);
}
private async setFile(file: File) {
let name = file.name.toLowerCase();
if (name.endsWith('.img') || name.endsWith('.mdf')) {
this.imgFile = file;
$('#imgReady').classList.remove('notready');
$('#imgReady').textContent = file.name;
} else if (name.endsWith('.cue') || name.endsWith('.mds')) {
this.cueFile = file;
$('#cueReady').classList.remove('notready');
$('#cueReady').textContent = file.name;
}
if (this.imgFile && this.cueFile) {
if (this.cueFile.name.endsWith('.cue'))
this.imageReader = await ImgCueReader.create(this.imgFile, this.cueFile);
else
this.imageReader = await MdfMdsReader.create(this.imgFile, this.cueFile);
await this.installAndRun();
}
}
private async extractFile(isofs: ISO9660FileSystem, entry: DirEnt): Promise<ArrayBuffer> {
let buffer = new ArrayBuffer(entry.size);
let uint8 = new Uint8Array(buffer);
let ptr = 0;
for (let buf of await isofs.readFile(entry)) {
uint8.set(buf, ptr);
ptr += buf.byteLength;
}
if (ptr !== entry.size)
throw ('expected ' + entry.size + ' bytes, but read ' + ptr + 'bytes');
return buffer;
}
private async installAndRun() {
let isofs = await ISO9660FileSystem.create(this.imageReader);
// this.walk(isofs, isofs.rootDir(), '/');
let gamedata = await isofs.getDirEnt('gamedata', isofs.rootDir());
if (!gamedata) {
this.setError('インストールできません。GAMEDATAフォルダが見つかりません。');
return;
}
let grGenerator = new GameResourceGenerator();
for (let e of await isofs.readDir(gamedata)) {
if (e.name.toLowerCase().endsWith('.ald')) {
let data = await this.extractFile(isofs, e);
// Store contents in the emscripten heap, so that it can be mmap-ed without copying
let ptr = Module.getMemory(data.byteLength);
Module.HEAPU8.set(new Uint8Array(data), ptr);
FS.writeFile(e.name, Module.HEAPU8.subarray(ptr, ptr + data.byteLength),
{ encoding: 'binary', canOwn: true });
grGenerator.addFile(e.name);
}
}
FS.writeFile('xsystem35.gr', grGenerator.generate());
FS.writeFile('.xsys35rc', xsystem35.xsys35rc);
this.runMain();
}
private setError(msg: string) {
console.log(msg);
}
// For debug
private async walk(isofs: ISO9660FileSystem, dir: DirEnt, dirname: string) {
for (let e of await isofs.readDir(dir)) {
if (e.name !== '\0' && e.name !== '\x01') {
console.log(dirname + e.name);
if (e.isDirectory)
this.walk(isofs, e, dirname + e.name + '/');
}
}
}
}
class GameResourceGenerator {
static resourceType: { [ch: string]: string } =
{ s: 'Scenario', g: 'Graphics', w: 'Wave', d: 'Data', r: 'Resource', m: 'Midi' };
private basename: string;
private lines: string[] = [];
addFile(name: string) {
let type = name.charAt(name.length - 6).toLowerCase();
let id = name.charAt(name.length - 5);
this.basename = name.slice(0, -6);
this.lines.push(GameResourceGenerator.resourceType[type] + id.toUpperCase() + ' ' + name);
}
generate(): string {
for (let i = 0; i < 26; i++) {
let id = String.fromCharCode(65 + i);
this.lines.push('Save' + id + ' save/' + this.basename + 's' + id.toLowerCase() + '.asd');
}
return this.lines.join('\n') + '\n';
}
isEmpty(): boolean {
return this.lines.length === 0;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "xsystem35-shell",
"version": "1.0.0",
"description": "",
"dependencies": {
"@types/emscripten": "0.0.29",
"@types/text-encoding": "0.0.30"
}
}
+114
View File
@@ -0,0 +1,114 @@
/// <reference path="util.ts" />
/// <reference path="loader.ts" />
/// <reference path="zoom.ts" />
/// <reference path="volume.ts" />
/// <reference path="cdda.ts" />
/// <reference path="audio.ts" />
namespace xsystem35 {
const Font = { url: 'fonts/MTLc3m.ttf', fname: 'MTLc3m.ttf'};
export const xsys35rc = [
'font_device: ttf',
'ttfont_mincho: ' + Font.fname,
'ttfont_gothic: ' + Font.fname, '',
].join('\n');
export let cdPlayer: CDPlayer;
export let audio: AudioManager;
class System35Shell {
private imageLoader: ImageLoader;
status: HTMLElement = document.getElementById('status');
private zoom: ZoomManager;
private volumeControl: VolumeControl;
private antialiasCheckbox: HTMLInputElement;
constructor() {
this.imageLoader = new ImageLoader(this.run.bind(this));
this.setStatus('Downloading...');
window.onerror = () => {
this.setStatus('Exception thrown, see JavaScript console');
this.setStatus = (text: string) => {
if (text) Module.printErr('[post-exception status] ' + text);
};
};
// Initialize the Module object
Module.TOTAL_MEMORY = 96 * 1024 * 1024;
Module.print = Module.printErr = console.log.bind(console);
Module.canvas = document.getElementById('canvas');
Module.noInitialRun = true;
Module.setStatus = this.setStatus.bind(this);
Module.preRun = [
function loadFont() {
FS.createPreloadedFile('/', Font.fname, Font.url, true, false);
},
function prepareSaveDir() {
FS.mkdir('/save');
FS.mount(IDBFS, {}, '/save');
Module.addRunDependency('syncfs');
FS.syncfs(true, (err) => { Module.removeRunDependency('syncfs'); });
},
];
this.volumeControl = new VolumeControl();
xsystem35.cdPlayer = new CDPlayer(this.imageLoader, this.volumeControl);
this.zoom = new ZoomManager();
this.antialiasCheckbox = <HTMLInputElement>$('#antialias');
this.antialiasCheckbox.addEventListener('change', this.antialiasChanged.bind(this));
this.antialiasCheckbox.checked = localStorage.getItem('antialias') !== 'false';
xsystem35.audio = new AudioManager(this.volumeControl);
}
run() {
$('#loader').hidden = true;
$('#xsystem35').hidden = false;
setTimeout(() => {
Module.callMain();
this.antialiasChanged();
}, 0);
this.addRightClickEmulation();
}
setStatus(text: string) {
console.log(text);
this.status.innerHTML = text;
}
windowSizeChanged() {
this.zoom.handleZoom();
}
private fsyncTimer: number;
syncfs(fname: string) {
window.clearTimeout(this.fsyncTimer);
this.fsyncTimer = window.setTimeout(() => {
FS.syncfs(false, (err) => {
if (err)
console.log('FS.syncfs error: ', err);
});
}, 100);
}
private antialiasChanged() {
localStorage.setItem('antialias', String(this.antialiasCheckbox.checked));
_ags_setAntialiasedStringMode(this.antialiasCheckbox.checked ? 1 : 0);
}
private addRightClickEmulation() {
let emulatingRightClick = false;
document.body.addEventListener('touchstart', (e) => {
if (e.target !== document.body)
return;
_sdl_rightButton(1);
emulatingRightClick = true;
});
document.body.addEventListener('touchend', (e) => {
if (!emulatingRightClick)
return;
_sdl_rightButton(0);
emulatingRightClick = false;
});
}
}
export let shell = new System35Shell();
}
+18
View File
@@ -0,0 +1,18 @@
class FallbackTextDecoder {
constructor(utfLabel?: string) {}
decode(buffer: DataView): string {
let out = '';
for (let i = 0; i < buffer.byteLength; i++) {
let c = buffer.getUint8(i);
if (c < 128)
out += String.fromCharCode(c);
else
out += '%' + c.toString(16);
}
return out;
}
}
if (!(<any>window).TextDecoder)
(<any>window).TextDecoder = <TextEncoding.TextDecoderStatic>FallbackTextDecoder;
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"outFile": "../docs/shell.js",
"noImplicitAny": true,
"sourceMap": false
},
"files": [
"shell.ts"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"extends": "tslint:recommended",
"rules": {
"curly": false,
"interface-name": false,
"max-classes-per-file": false,
"member-access": false,
"member-ordering": false,
"no-bitwise": false,
"no-empty": false,
"no-namespace": false,
"no-reference": false,
"only-arrow-functions": ["allow-declarations", "allow-named-functions"],
"prefer-for-of": false,
"quotemark": [true, "single", "avoid-escape"],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"no-console": false
}
}
+28
View File
@@ -0,0 +1,28 @@
let $: (selector: string) => HTMLElement = document.querySelector.bind(document);
// xsystem35 exported functions
declare function _musfade_setvolval_all(vol: number): void;
declare function _ags_setAntialiasedStringMode(on: number): void;
declare function _sdl_rightButton(down: number): void;
declare function _ald_getdata(type: number, no: number): number;
declare function _ald_freedata(data: number): void;
declare namespace Module {
let noInitialRun: boolean;
// Undocumented methods / attributes
let canvas: HTMLElement;
function setStatus(status: string): void;
function monitorRunDependencies(left: number): void;
function callMain(): void;
function getMemory(size: number): number;
}
declare namespace FS {
function writeFile(path: string, data: ArrayBufferView | string,
opts?: {encoding?: string; flags?: string; canOwn?: boolean}): void;
}
declare namespace EmterpreterAsync {
function handle(asyncOp: (resume: () => void) => void): void;
}
+66
View File
@@ -0,0 +1,66 @@
/// <reference path="util.ts" />
namespace xsystem35 {
export class VolumeControl {
private vol: number; // 0.0 - 1.0
private muted: boolean;
private elem: HTMLElement;
private icon: HTMLElement;
private slider: HTMLInputElement;
constructor() {
this.vol = Number(localStorage.getItem('volume') || 1);
this.muted = false;
this.elem = $('#volume-control');
this.icon = $('#volume-control-icon');
this.slider = <HTMLInputElement>$('#volume-control-slider');
this.slider.value = String(Math.round(this.vol * 100));
this.icon.addEventListener('click', this.onIconClicked.bind(this));
this.slider.addEventListener('input', this.onSliderValueChanged.bind(this));
this.slider.addEventListener('change', this.onSliderValueSettled.bind(this));
}
volume(): number {
return this.muted ? 0 : parseInt(this.slider.value, 10) / 100;
}
addEventListener(handler: (evt: CustomEvent) => any) {
this.elem.addEventListener('volumechange', handler);
}
private onIconClicked(e: Event) {
this.muted = !this.muted;
if (this.muted) {
this.icon.classList.remove('fa-volume-up');
this.icon.classList.add('fa-volume-off');
this.slider.value = '0';
} else {
this.icon.classList.remove('fa-volume-off');
this.icon.classList.add('fa-volume-up');
this.slider.value = String(Math.round(this.vol * 100));
}
this.dispatchEvent();
}
private onSliderValueChanged(e: Event) {
this.vol = parseInt(this.slider.value, 10) / 100;
if (this.vol > 0 && this.muted) {
this.muted = false;
this.icon.classList.remove('fa-volume-off');
this.icon.classList.add('fa-volume-up');
}
this.dispatchEvent();
}
private onSliderValueSettled(e: Event) {
localStorage.setItem('volume', this.vol + '');
}
private dispatchEvent() {
let event = new CustomEvent('volumechange', { detail: this.volume() });
this.elem.dispatchEvent(event);
}
}
}
+44
View File
@@ -0,0 +1,44 @@
/// <reference path="util.ts" />
namespace xsystem35 {
export class ZoomManager {
private canvas: HTMLCanvasElement;
private zoomSelect: HTMLInputElement;
private smoothingCheckbox: HTMLInputElement;
constructor() {
this.canvas = <HTMLCanvasElement>$('#canvas');
this.zoomSelect = <HTMLInputElement>$('#zoom');
this.zoomSelect.addEventListener('change', this.handleZoom.bind(this));
this.zoomSelect.value = localStorage.getItem('zoom') || '1';
this.smoothingCheckbox = <HTMLInputElement>$('#smoothing');
this.smoothingCheckbox.addEventListener('change', this.handleSmoothing.bind(this));
if (localStorage.getItem('smoothing') === 'false') {
this.smoothingCheckbox.checked = false;
this.handleSmoothing();
}
}
handleZoom() {
let value = this.zoomSelect.value;
localStorage.setItem('zoom', value);
let contentsStyle = $('.contents').style;
if (value === 'fit') {
contentsStyle.maxWidth = 'none';
contentsStyle.width = this.canvas.style.width = '100%';
} else {
let ratio = Number(value);
contentsStyle.maxWidth = 'none';
contentsStyle.width = this.canvas.style.width = this.canvas.width * ratio + 'px';
}
}
private handleSmoothing() {
localStorage.setItem('smoothing', String(this.smoothingCheckbox.checked));
if (this.smoothingCheckbox.checked)
this.canvas.classList.remove('pixelated');
else
this.canvas.classList.add('pixelated');
}
}
}