abse/gameboy-advance

A tscircuit package created by abse.

Version
0.0.45
License
unset
Stars
0

web/worker.js

/* Each launch owns one worker, one WASM instance and one virtual SD card. */
let game;
let started = false;
let paused = false;
let pauseStart = 0;
let pausedTime = 0;
let startTime = 0;
let buttons = 0;
const events = [];
function setButtons(value) {
  const changed = value ^ buttons;
  for (let i=0;i<12;i++) if (changed & (1<<i)) events.push(i+1 | (value & (1<<i) ? 256 : 0));
  buttons = value;
  if (game) game.buttons = buttons;
}
function virtualClock() {
  return (paused ? pauseStart : performance.now()) - startTime - pausedTime;
}
onmessage = async ({data}) => {
  if (data.type === 'save-name' && game) {
    // Clear an existing slot label, then enter the supplied name and confirm.
    const name = String(data.name).replace(/[^a-zA-Z0-9 ._-]/g,'').slice(0,20) || 'PLAY TEST';
    for (const code of [...Array(24).fill(127), ...Array.from(name,c=>c.charCodeAt(0)),13]) {
      events.push(512|256|code,512|code);
    }
    return;
  }
  if (data.type === 'buttons') return setButtons(data.value & 4095);
  if (data.type === 'pause') {
    if (paused === data.value) return;
    paused = data.value;
    if (paused) { pauseStart = performance.now(); setButtons(0); }
    else pausedTime += performance.now() - pauseStart;
    if (game) game.paused = paused;
    return;
  }
  if (data.type === 'export') {
    if (!game) return;
    // Called only between WASM yields: the engine cannot be writing concurrently.
    const files = [];
    function walk(dir) {
      for (const name of game.FS.readdir(dir)) {
        if (name === '.' || name === '..') continue;
        const path = dir + '/' + name;
        if (game.FS.isDir(game.FS.stat(path).mode)) walk(path);
        else if (!path.toLowerCase().endsWith('.wad')) files.push({path, bytes:Array.from(game.FS.readFile(path))});
      }
    }
    walk('/games');
    postMessage({type:'saves', files});
    return;
  }
  if (data.type !== 'start' || started) return;
  started = true;
  try {
    if (!['doom','star-test'].includes(data.game)) throw Error('Unknown game');
    importScripts('./' + data.game + '.js');
    game = await createGame({noInitialRun:true,
      print:text=>postMessage({type:'log',text}),
      printErr:text=>postMessage({type:'log',text}),
      onAbort:reason=>postMessage({type:'error',message:String(reason)})});
    game.FS.mkdirTree('/games/doom');
    for (const file of data.saves || []) {
      if (!/^\/games\/doom\/(?:[\w.-]+\/)*[\w.-]+$/.test(file.path) || file.path.split('/').some(p=>p==='.'||p==='..') || /\.wad$/i.test(file.path)) throw Error('Invalid save path');
      game.FS.mkdirTree(file.path.slice(0,file.path.lastIndexOf('/')));
      game.FS.writeFile(file.path,new Uint8Array(file.bytes));
    }
    game.buttons = buttons; game.events = events;
    game.clock = virtualClock; game.paused = paused;
    startTime = performance.now(); pausedTime = 0;
    if (paused) pauseStart = startTime;
    let args = [];
    if (data.game === 'doom') {
      game.FS.writeFile('/games/doom/doom.wad',new Uint8Array(data.wad));
      args = ['-iwad','/games/doom/doom.wad','-config','/games/doom/default.cfg',
        '-extraconfig','/games/doom/extra.cfg','-savedir','/games/doom','-nogui','-nosound','-mb','6'];
    }
    postMessage({type:'ready'});
    game.callMain(args);
  } catch (e) {
    postMessage({type:'error',message:e.message || String(e)});
  }
};