← back to kurodenkou__playTogether

Function bodies 123 total

All specs Real LLM only Function bodies
_addFn method · javascript · L219-L259 (41 LOC)
public/js/libretro-adapter.js
  _addFn(jsFunc, sig) {
    const M = this.M;

    // Path 1: standard Emscripten addFunction
    if (typeof M.addFunction === 'function') {
      return M.addFunction(jsFunc, sig);
    }

    // Path 2: direct WebAssembly.Table manipulation.
    // Emscripten exposes the function table as Module.wasmTable (older builds)
    // or Module.__indirect_function_table (Emscripten 2.x+).
    const table = M.wasmTable ?? M.__indirect_function_table;
    if (!(table instanceof WebAssembly.Table)) {
      throw new Error(
        'LibretroAdapter: cannot register callbacks — core must be compiled with ' +
        'ALLOW_TABLE_GROWTH=1 and EXPORTED_RUNTIME_METHODS=["addFunction"], or the ' +
        'WebAssembly.Table must be accessible via Module.wasmTable / ' +
        'Module.__indirect_function_table'
      );
    }

    // Try to grow the table by one slot and claim it.
    // If the table was created with a fixed maximum (grow() throws RangeError),
    // scan from index 1 for a null slot 
_registerCallbacks method · javascript · L265-L305 (41 LOC)
public/js/libretro-adapter.js
  _registerCallbacks() {
    // bool env(unsigned cmd, void *data)
    this._callbacks.env = this._addFn(
      (cmd, data) => this._onEnvironment(cmd, data) ? 1 : 0,
      'iii');

    // void video_refresh(const void *data, unsigned width, unsigned height, size_t pitch)
    this._callbacks.video = this._addFn(
      (dataPtr, width, height, pitch) => this._onVideoRefresh(dataPtr, width, height, pitch),
      'viiii');

    // void audio_sample(int16_t left, int16_t right)
    this._callbacks.audioSample = this._addFn((left, right) => {
      if (this._audioMuted) return;
      this._singleSampleBuf[0] = left;
      this._singleSampleBuf[1] = right;
      this._flushSamples(this._singleSampleBuf, 1);
    }, 'vii');

    // size_t audio_sample_batch(const int16_t *data, size_t frames)
    this._callbacks.audioBatch = this._addFn(
      (dataPtr, frames) => this._onAudioBatch(dataPtr, frames),
      'iii');

    // void input_poll(void)  — input is already snapshotted before step(); no-
_onEnvironment method · javascript · L313-L384 (72 LOC)
public/js/libretro-adapter.js
  _onEnvironment(cmd, data) {
    const M   = this.M;
    const ENV = LibretroAdapter.ENV;

    switch (cmd) {
      case ENV.SET_PIXEL_FORMAT:
        // Core declares its output pixel format before rendering begins.
        this._pixelFormat = M.HEAP32[data >> 2];
        return true;

      case ENV.GET_CAN_DUPE:
        // Signal that we accept duplicate/null frame pointers (CAN_DUPE=true).
        // This lets the core skip sending identical frames and saves CPU.
        M.HEAPU8[data] = 1;
        return true;

      case ENV.GET_SYSTEM_DIRECTORY:
      case ENV.GET_SAVE_DIRECTORY:
        // Return NULL pointer — no persistent filesystem is available in the browser.
        M.HEAP32[data >> 2] = 0;
        return true;

      case ENV.GET_VARIABLE:
        // The core is asking for a configuration variable value.
        // Return NULL to instruct the core to use its built-in default.
        M.HEAP32[(data + 4) >> 2] = 0;
        return false;

      case ENV.SET_GEOMETRY: {
  
_onVideoRefresh method · javascript · L396-L457 (62 LOC)
public/js/libretro-adapter.js
  _onVideoRefresh(dataPtr, width, height, pitch) {
    if (!dataPtr) return;  // null = duplicate frame (CAN_DUPE); nothing to do

    if (width !== this._width || height !== this._height) this._resize(width, height);

    const buf = this._frameBuffer;
    const M   = this.M;
    const PF  = LibretroAdapter.PIXEL_FORMAT;

    if (this._pixelFormat === PF.XRGB8888) {
      // Source: Uint32 per pixel, layout 0x00RRGGBB
      // Target: 0xFF000000 | (B<<16) | (G<<8) | R
      const src     = M.HEAPU32;
      const srcBase = dataPtr >> 2;
      const pitchPx = pitch >> 2;
      for (let y = 0; y < height; y++) {
        const srcRow = srcBase + y * pitchPx;
        const dstRow = y * width;
        for (let x = 0; x < width; x++) {
          const px = src[srcRow + x];  // 0x00RRGGBB
          buf[dstRow + x] = 0xFF000000
            | ((px & 0x000000FF) << 16)   // B → bits 23-16
            |  (px & 0x0000FF00)           // G → bits 15-8  (unchanged position)
            | ((px & 0x00F
_resize method · javascript · L460-L467 (8 LOC)
public/js/libretro-adapter.js
  _resize(width, height) {
    this._width  = width;
    this._height = height;
    this.canvas.width  = width;
    this.canvas.height = height;
    this._imageData   = this.ctx.createImageData(width, height);
    this._frameBuffer = new Uint32Array(this._imageData.data.buffer);
  }
_onAudioBatch method · javascript · L471-L478 (8 LOC)
public/js/libretro-adapter.js
  _onAudioBatch(dataPtr, frames) {
    if (!this._audioMuted && this._workletNode && frames > 0) {
      // this.M.HEAP16 is the signed 16-bit view; dataPtr >> 1 converts byte
      // offset to HEAP16 index.
      this._flushSamples(this.M.HEAP16, frames, dataPtr >> 1);
    }
    return frames;
  }
_flushSamples method · javascript · L486-L492 (7 LOC)
public/js/libretro-adapter.js
  _flushSamples(src, frames, offset = 0) {
    if (!this._workletNode) return;
    const f32 = new Float32Array(frames * 2);
    for (let i = 0; i < frames * 2; i++) f32[i] = src[offset + i] / 32768.0;
    if (this._audioCtx?.state === 'suspended') this._audioCtx.resume();
    this._workletNode.port.postMessage({ samples: f32 }, [f32.buffer]);
  }
About: code-quality intelligence by Repobility · https://repobility.com
_onInputState method · javascript · L496-L500 (5 LOC)
public/js/libretro-adapter.js
  _onInputState(port, device, _index, id) {
    // Only handle RETRO_DEVICE_JOYPAD (device=1) for ports 0 and 1.
    if (port >= 2 || device !== 1 || id >= 16) return 0;
    return this._inputState[port][id];
  }
_initAudio method · javascript · L508-L532 (25 LOC)
public/js/libretro-adapter.js
  async _initAudio() {
    try {
      this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      await this._audioCtx.audioWorklet.addModule('/js/snes-audio-worklet.js');
      this._workletNode = new AudioWorkletNode(
        this._audioCtx,
        'snes-audio-processor',
        { numberOfOutputs: 1, outputChannelCount: [2] }
      );
      const gain = this._audioCtx.createGain();
      gain.gain.value = 0.5;
      this._workletNode.connect(gain);
      gain.connect(this._audioCtx.destination);
      // Pre-fill with silence to stabilize the DRC buffer before the first frame.
      const silence = new Float32Array(2048 * 2);
      this._workletNode.port.postMessage({ samples: silence }, [silence.buffer]);
      if (this._audioCtx.state === 'suspended') {
        await this._audioCtx.resume().catch(() => {});
      }
    } catch (err) {
      console.error('[LibretroAdapter] AudioWorklet setup failed, audio disabled:', err);
      this._audioCtx    = null;
   
setAudioMuted method · javascript · L535-L538 (4 LOC)
public/js/libretro-adapter.js
  setAudioMuted(muted) {
    this._audioMuted = muted;
    if (!muted && this._audioCtx?.state === 'suspended') this._audioCtx.resume();
  }
stopAudio method · javascript · L540-L546 (7 LOC)
public/js/libretro-adapter.js
  stopAudio() {
    if (this._workletNode) { this._workletNode.disconnect(); this._workletNode = null; }
    if (this._audioCtx && this._audioCtx.state !== 'closed') {
      this._audioCtx.close();
      this._audioCtx = null;
    }
  }
loadROM method · javascript · L555-L606 (52 LOC)
public/js/libretro-adapter.js
  async loadROM(url) {
    const resp = await fetch(`/rom-proxy?url=${encodeURIComponent(url)}`);
    if (!resp.ok) {
      throw new Error(`ROM fetch failed: HTTP ${resp.status} — ${resp.statusText}`);
    }
    const buf = await resp.arrayBuffer();

    await this._initAudio();

    const M = this.M;

    // Copy ROM bytes onto the WASM heap so we can pass a pointer to the core.
    const romPtr = M._malloc(buf.byteLength);
    if (!romPtr) throw new Error('WASM heap exhausted allocating ROM buffer');
    M.HEAPU8.set(new Uint8Array(buf), romPtr);

    // Build retro_game_info on the heap.
    // struct { const char *path; const void *data; size_t size; const char *meta; }
    // 4 × 4 bytes = 16 bytes on 32-bit WASM.
    const infoPtr = M._malloc(16);
    if (!infoPtr) {
      M._free(romPtr);
      throw new Error('WASM heap exhausted allocating retro_game_info struct');
    }
    M.HEAP32[(infoPtr     ) >> 2] = 0;             // path = NULL (use raw bytes)
    M.HEAP32[(infoPtr + 
step method · javascript · L615-L641 (27 LOC)
public/js/libretro-adapter.js
  step(inputMap) {
    if (!this._romLoaded) return;

    const JOYPAD = LibretroAdapter.JOYPAD;
    for (let port = 0; port < 2; port++) {
      const pid   = this.playerIds[port];
      const bits  = pid ? (inputMap[pid] ?? 0) : 0;
      const state = this._inputState[port];
      state.fill(0);

      if (bits & InputBits.UP)     state[JOYPAD.UP]     = 1;
      if (bits & InputBits.DOWN)   state[JOYPAD.DOWN]   = 1;
      if (bits & InputBits.LEFT)   state[JOYPAD.LEFT]   = 1;
      if (bits & InputBits.RIGHT)  state[JOYPAD.RIGHT]  = 1;
      if (bits & InputBits.A)      state[JOYPAD.A]      = 1;
      if (bits & InputBits.B)      state[JOYPAD.B]      = 1;
      if (bits & InputBits.START)  state[JOYPAD.START]  = 1;
      if (bits & InputBits.SELECT) state[JOYPAD.SELECT] = 1;
      if (bits & InputBits.X)      state[JOYPAD.X]      = 1;
      if (bits & InputBits.Y)      state[JOYPAD.Y]      = 1;
      if (bits & InputBits.L)      state[JOYPAD.L]      = 1;
      if (bits & InputBits.R)
saveState method · javascript · L647-L660 (14 LOC)
public/js/libretro-adapter.js
  saveState() {
    if (!this._romLoaded) return null;
    const M    = this.M;
    const size = M._retro_serialize_size();
    if (!size) return null;
    const ptr = M._malloc(size);
    if (!ptr) return null;
    const ok = M._retro_serialize(ptr, size);
    if (!ok) { M._free(ptr); return null; }
    // slice() creates an independent copy — safe to free the WASM buffer immediately.
    const snap = M.HEAPU8.slice(ptr, ptr + size);
    M._free(ptr);
    return snap;
  }
loadState method · javascript · L666-L675 (10 LOC)
public/js/libretro-adapter.js
  loadState(snap) {
    if (!snap || !this._romLoaded) return;
    const M   = this.M;
    const ptr = M._malloc(snap.byteLength);
    if (!ptr) return;
    M.HEAPU8.set(snap, ptr);
    M._retro_unserialize(ptr, snap.byteLength);
    M._free(ptr);
    this._dirty = true;
  }
If a scraper extracted this row, it came from Repobility (https://repobility.com)
render method · javascript · L678-L682 (5 LOC)
public/js/libretro-adapter.js
  render() {
    if (!this._dirty || !this._imageData) return;
    this.ctx.putImageData(this._imageData, 0, 0);
    this._dirty = false;
  }
constructor method · javascript · L52-L103 (52 LOC)
public/js/nes-adapter.js
  constructor(canvas, playerIds) {
    this.canvas    = canvas;
    this.playerIds = playerIds;
    this.ctx       = canvas.getContext('2d');

    canvas.width  = NESAdapter.NES_W;
    canvas.height = NESAdapter.NES_H;

    // Off-screen pixel buffer for fast canvas writes
    this._imageData = this.ctx.createImageData(NESAdapter.NES_W, NESAdapter.NES_H);
    this._pixels    = new Uint32Array(this._imageData.data.buffer);

    this._frameBuffer = null;
    this._dirty       = false;
    this._romLoaded   = false;
    this._skipPtTile  = false; // true for CHR ROM games (tiles immutable after loadROM)
    this._btnMap      = null;

    // ── Audio ─────────────────────────────────────────────────────────────────
    // Ring buffer (power-of-2 size for fast modulo via bitwise AND).
    // 4096 samples @ 44100 Hz ≈ 93 ms of buffer — enough headroom for rollbacks
    // up to 4 frames deep without audible glitches.
    this._AUDIO_COUNT = 4096;
    this._AUDIO_MASK  = this._AUDIO_COUNT - 1
_setupScriptProcessor method · javascript · L105-L138 (34 LOC)
public/js/nes-adapter.js
  _setupScriptProcessor() {
    // ScriptProcessorNode with 512-sample buffers, 0 inputs, 2 output channels.
    // Runs on the main thread but is scheduled by the audio thread — keeps latency low.
    const sp = this._audioCtx.createScriptProcessor(512, 0, 2);

    sp.onaudioprocess = (event) => {
      const dstL = event.outputBuffer.getChannelData(0);
      const dstR = event.outputBuffer.getChannelData(1);
      const len  = dstL.length;

      const available = (this._audioWrite - this._audioRead) & this._AUDIO_MASK;
      const toRead    = Math.min(len, available);

      // Drain ring buffer
      for (let i = 0; i < toRead; i++) {
        const idx  = (this._audioRead + i) & this._AUDIO_MASK;
        dstL[i] = this._audioL[idx];
        dstR[i] = this._audioR[idx];
      }
      // Silence for any underrun (buffer was empty)
      for (let i = toRead; i < len; i++) {
        dstL[i] = 0;
        dstR[i] = 0;
      }

      this._audioRead = (this._audioRead + toRead) & this._AU
setAudioMuted method · javascript · L145-L147 (3 LOC)
public/js/nes-adapter.js
  setAudioMuted(muted) {
    this._audioMuted = muted;
  }
stopAudio method · javascript · L149-L157 (9 LOC)
public/js/nes-adapter.js
  stopAudio() {
    if (this._scriptProcessor) {
      this._scriptProcessor.disconnect();
      this._scriptProcessor = null;
    }
    if (this._audioCtx && this._audioCtx.state !== 'closed') {
      this._audioCtx.close();
    }
  }
loadROM method · javascript · L165-L184 (20 LOC)
public/js/nes-adapter.js
  async loadROM(url) {
    const resp = await fetch(`/rom-proxy?url=${encodeURIComponent(url)}`);
    if (!resp.ok) {
      throw new Error(`ROM fetch error: HTTP ${resp.status} — ${resp.statusText}`);
    }
    const buf   = await resp.arrayBuffer();
    const bytes = new Uint8Array(buf);

    // jsnes expects a binary string (char-per-byte)
    let romStr = '';
    for (let i = 0; i < bytes.length; i++) {
      romStr += String.fromCharCode(bytes[i]);
    }

    this.nes.loadROM(romStr);
    this._romLoaded  = true;
    // CHR ROM games have immutable pattern tiles — no need to save/restore ptTile
    this._skipPtTile = this.nes.rom.vromCount > 0;
    this._buildBtnMap();
  }
_buildBtnMap method · javascript · L186-L198 (13 LOC)
public/js/nes-adapter.js
  _buildBtnMap() {
    const C = jsnes.Controller;
    this._btnMap = [
      [InputBits.UP,     C.BUTTON_UP],
      [InputBits.DOWN,   C.BUTTON_DOWN],
      [InputBits.LEFT,   C.BUTTON_LEFT],
      [InputBits.RIGHT,  C.BUTTON_RIGHT],
      [InputBits.A,      C.BUTTON_A],
      [InputBits.B,      C.BUTTON_B],
      [InputBits.START,  C.BUTTON_START],
      [InputBits.SELECT, C.BUTTON_SELECT],
    ];
  }
step method · javascript · L206-L229 (24 LOC)
public/js/nes-adapter.js
  step(inputMap) {
    if (!this._romLoaded) return;

    // Lazily resume AudioContext — browser autoplay policy suspends it until
    // the user interacts. step() is called every frame; the first call after
    // a key/click event will succeed and audio starts within one frame.
    if (this._audioCtx.state === 'suspended') {
      this._audioCtx.resume();
    }

    for (let i = 0; i < 2; i++) {
      const pid   = this.playerIds[i];
      if (!pid) continue;
      const input = inputMap[pid] ?? 0;
      const ctrl  = i + 1; // jsnes controllers are 1-indexed

      for (const [bit, btn] of this._btnMap) {
        if (input & bit) this.nes.buttonDown(ctrl, btn);
        else             this.nes.buttonUp(ctrl, btn);
      }
    }

    this.nes.frame();
  }
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
saveState method · javascript · L235-L358 (124 LOC)
public/js/nes-adapter.js
  saveState() {
    if (!this._romLoaded) return null;

    const cpu  = this.nes.cpu;
    const ppu  = this.nes.ppu;
    const mmap = this.nes.mmap;

    // ── CPU ──────────────────────────────────────────────────────────────────

    // Full 6502 address space: 65536 bytes (dominant serialisation cost)
    const cpuMem = new Uint8Array(65536);
    for (let i = 0; i < 65536; i++) cpuMem[i] = cpu.mem[i];

    // CPU scalar registers — 21 fields packed into one Int32Array
    const cpuRegs = new Int32Array([
      cpu.cyclesToHalt,
      cpu.irqRequested ? 1 : 0,
      cpu.irqType,
      cpu.REG_ACC, cpu.REG_X, cpu.REG_Y, cpu.REG_SP,
      cpu.REG_PC,  cpu.REG_PC_NEW, cpu.REG_STATUS,
      cpu.F_CARRY, cpu.F_DECIMAL, cpu.F_INTERRUPT, cpu.F_INTERRUPT_NEW,
      cpu.F_OVERFLOW, cpu.F_SIGN, cpu.F_ZERO,
      cpu.F_NOTUSED, cpu.F_NOTUSED_NEW, cpu.F_BRK, cpu.F_BRK_NEW,
    ]);

    // ── PPU ──────────────────────────────────────────────────────────────────

    // VRAM — 32768 bytes (byte 
loadState method · javascript · L365-L481 (117 LOC)
public/js/nes-adapter.js
  loadState(snap) {
    if (!snap || !this._romLoaded) return;

    const cpu  = this.nes.cpu;
    const ppu  = this.nes.ppu;
    const mmap = this.nes.mmap;

    // ── CPU ──────────────────────────────────────────────────────────────────

    const { cpuMem } = snap;
    for (let i = 0; i < 65536; i++) cpu.mem[i] = cpuMem[i];

    const r = snap.cpuRegs;
    cpu.cyclesToHalt    = r[0];
    cpu.irqRequested    = r[1] !== 0;
    cpu.irqType         = r[2];
    cpu.REG_ACC         = r[3];
    cpu.REG_X           = r[4];
    cpu.REG_Y           = r[5];
    cpu.REG_SP          = r[6];
    cpu.REG_PC          = r[7];
    cpu.REG_PC_NEW      = r[8];
    cpu.REG_STATUS      = r[9];
    cpu.F_CARRY         = r[10];
    cpu.F_DECIMAL       = r[11];
    cpu.F_INTERRUPT     = r[12];
    cpu.F_INTERRUPT_NEW = r[13];
    cpu.F_OVERFLOW      = r[14];
    cpu.F_SIGN          = r[15];
    cpu.F_ZERO          = r[16];
    cpu.F_NOTUSED       = r[17];
    cpu.F_NOTUSED_NEW   = r[18];
    cpu.F_BRK     
render method · javascript · L492-L505 (14 LOC)
public/js/nes-adapter.js
  render() {
    if (!this._dirty || !this._frameBuffer) return;

    const fb     = this._frameBuffer;
    const pixels = this._pixels;
    const len    = NESAdapter.NES_W * NESAdapter.NES_H;

    for (let i = 0; i < len; i++) {
      pixels[i] = 0xFF000000 | fb[i];
    }

    this.ctx.putImageData(this._imageData, 0, 0);
    this._dirty = false;
  }
NetworkClient class · javascript · L13-L79 (67 LOC)
public/js/network.js
class NetworkClient extends EventTarget {
  constructor() {
    super();
    /** @type {WebSocket|null} */
    this.ws = null;
    this.connected = false;
    this._queue = [];   // messages queued before the socket opens
  }

  /** Open the WebSocket connection.  Resolves once the socket is ready. */
  connect() {
    return new Promise((resolve, reject) => {
      const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
      this.ws = new WebSocket(`${proto}//${location.host}`);

      this.ws.onopen = () => {
        this.connected = true;
        // Flush any messages that were sent before connection opened
        while (this._queue.length) this.ws.send(this._queue.shift());
        resolve();
      };

      this.ws.onerror = (ev) => reject(new Error('WebSocket connection failed'));

      this.ws.onmessage = (ev) => {
        let msg;
        try { msg = JSON.parse(ev.data); } catch { return; }
        // Dispatch a typed CustomEvent for specific handlers,
        // plus
constructor method · javascript · L14-L20 (7 LOC)
public/js/network.js
  constructor() {
    super();
    /** @type {WebSocket|null} */
    this.ws = null;
    this.connected = false;
    this._queue = [];   // messages queued before the socket opens
  }
connect method · javascript · L23-L53 (31 LOC)
public/js/network.js
  connect() {
    return new Promise((resolve, reject) => {
      const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
      this.ws = new WebSocket(`${proto}//${location.host}`);

      this.ws.onopen = () => {
        this.connected = true;
        // Flush any messages that were sent before connection opened
        while (this._queue.length) this.ws.send(this._queue.shift());
        resolve();
      };

      this.ws.onerror = (ev) => reject(new Error('WebSocket connection failed'));

      this.ws.onmessage = (ev) => {
        let msg;
        try { msg = JSON.parse(ev.data); } catch { return; }
        // Dispatch a typed CustomEvent for specific handlers,
        // plus a generic 'message' event for catch-all handlers.
        const typed   = new CustomEvent(msg.type,    { detail: msg });
        const generic = new CustomEvent('message',   { detail: msg });
        this.dispatchEvent(typed);
        this.dispatchEvent(generic);
      };

      this.ws.onclose = () =
on method · javascript · L61-L64 (4 LOC)
public/js/network.js
  on(type, handler) {
    this.addEventListener(type, (ev) => handler(ev.detail));
    return this;
  }
send method · javascript · L67-L74 (8 LOC)
public/js/network.js
  send(msg) {
    const raw = JSON.stringify(msg);
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(raw);
    } else {
      this._queue.push(raw);
    }
  }
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
disconnect method · javascript · L76-L78 (3 LOC)
public/js/network.js
  disconnect() {
    this.ws?.close();
  }
RollbackEngine class · javascript · L27-L265 (239 LOC)
public/js/rollback.js
class RollbackEngine {
  /**
   * @param {object} opts
   * @param {object}   opts.emulator       - emulator implementing the contract above
   * @param {string}   opts.localPlayerId  - this client's player ID
   * @param {string[]} opts.playerIds      - ordered array of all player IDs
   * @param {function} opts.readInput      - () => number  returns local input bitmask
   * @param {function} [opts.onStats]      - (stats) => void  called each frame
   */
  constructor({ emulator, localPlayerId, playerIds, readInput, onStats }) {
    this.emulator = emulator;
    this.localPlayerId = localPlayerId;
    this.playerIds = playerIds;
    this.readInput = readInput ?? (() => 0);
    this.onStats = onStats ?? (() => {});

    // ── Tuning ────────────────────────────────────────────────────────────
    /** Frames of artificial local-input delay.
     *  Gives the peer's real input time to arrive before we need to
     *  predict, reducing rollback frequency.  2 frames ≈ 33 ms headroom. */
  
constructor method · javascript · L36-L86 (51 LOC)
public/js/rollback.js
  constructor({ emulator, localPlayerId, playerIds, readInput, onStats }) {
    this.emulator = emulator;
    this.localPlayerId = localPlayerId;
    this.playerIds = playerIds;
    this.readInput = readInput ?? (() => 0);
    this.onStats = onStats ?? (() => {});

    // ── Tuning ────────────────────────────────────────────────────────────
    /** Frames of artificial local-input delay.
     *  Gives the peer's real input time to arrive before we need to
     *  predict, reducing rollback frequency.  2 frames ≈ 33 ms headroom. */
    this.INPUT_DELAY = 2;
    /** Maximum frames we are willing to roll back. */
    this.MAX_ROLLBACK = 8;
    this.TARGET_FPS = 60;
    this.FRAME_MS = 1000 / this.TARGET_FPS;

    // ── Frame counters ────────────────────────────────────────────────────
    /** Current simulation frame (next frame to be simulated). */
    this.frame = 0;
    /** Last frame where every player's input is confirmed (no prediction). */
    this.confirmedFrame = -1;

    // ──
start method · javascript · L90-L94 (5 LOC)
public/js/rollback.js
  start() {
    this._running = true;
    this._lastTime = performance.now();
    this._rafId = requestAnimationFrame(this._loop.bind(this));
  }
stop method · javascript · L96-L100 (5 LOC)
public/js/rollback.js
  stop() {
    this._running = false;
    if (this._rafId) cancelAnimationFrame(this._rafId);
    this._rafId = null;
  }
receiveRemoteInput method · javascript · L107-L130 (24 LOC)
public/js/rollback.js
  receiveRemoteInput(frame, playerId, input) {
    if (playerId === this.localPlayerId) return;

    // Check for misprediction (only if we already simulated this frame)
    if (frame < this.frame) {
      const used = this.usedInputs.get(frame);
      if (used !== undefined && used[playerId] !== input) {
        // Misprediction detected — schedule rollback to the earliest bad frame
        if (frame > this.confirmedFrame) {
          if (this._rollbackTo === null || frame < this._rollbackTo) {
            this._rollbackTo = frame;
          }
        }
      }
    }

    this._storeInput(frame, playerId, input);

    // Update last-received watermark
    const last = this.lastReceivedFrame.get(playerId) ?? -1;
    if (frame > last) this.lastReceivedFrame.set(playerId, frame);

    this._updateConfirmedFrame();
  }
_loop method · javascript · L134-L148 (15 LOC)
public/js/rollback.js
  _loop(timestamp) {
    if (!this._running) return;

    const delta = Math.min(timestamp - this._lastTime, 100); // cap spiral-of-death
    this._lastTime = timestamp;
    this._accumulator += delta;

    while (this._accumulator >= this.FRAME_MS) {
      this._tick();
      this._accumulator -= this.FRAME_MS;
    }

    this.emulator.render();
    this._rafId = requestAnimationFrame(this._loop.bind(this));
  }
_tick method · javascript · L150-L183 (34 LOC)
public/js/rollback.js
  _tick() {
    // ① Queue local input with delay and broadcast
    const localInput = this.readInput();
    const queueFrame = this.frame + this.INPUT_DELAY;
    this._storeInput(queueFrame, this.localPlayerId, localInput);
    this._sendInput(queueFrame, localInput);

    // ② Execute pending rollback
    if (this._rollbackTo !== null) {
      const rollTo = this._rollbackTo;
      this._rollbackTo = null;
      if (rollTo < this.frame && this.stateHistory.has(rollTo)) {
        this._performRollback(rollTo);
      }
    }

    // ③ Snapshot state BEFORE simulating this frame
    this.stateHistory.set(this.frame, this.emulator.saveState());

    // ④ Gather inputs (confirmed or predicted) and simulate
    const inputs = this._gatherInputs(this.frame);
    this.usedInputs.set(this.frame, inputs);
    this.emulator.step(inputs);

    // ⑤ Maintain bookkeeping
    this._updateConfirmedFrame();
    this._pruneHistory();

    this._stats.frame = this.frame;
    this._stats.confirmedFrame 
About: code-quality intelligence by Repobility · https://repobility.com
_storeInput method · javascript · L187-L190 (4 LOC)
public/js/rollback.js
  _storeInput(frame, playerId, input) {
    if (!this.confirmedInputs.has(frame)) this.confirmedInputs.set(frame, new Map());
    this.confirmedInputs.get(frame).set(playerId, input);
  }
_gatherInputs method · javascript · L192-L201 (10 LOC)
public/js/rollback.js
  _gatherInputs(frame) {
    const result = {};
    for (const pid of this.playerIds) {
      const frameMap = this.confirmedInputs.get(frame);
      result[pid] = (frameMap && frameMap.has(pid))
        ? frameMap.get(pid)
        : this._predict(pid, frame);
    }
    return result;
  }
_predict method · javascript · L204-L210 (7 LOC)
public/js/rollback.js
  _predict(playerId, forFrame) {
    for (let f = forFrame - 1; f >= Math.max(0, forFrame - this.MAX_ROLLBACK * 2); f--) {
      const m = this.confirmedInputs.get(f);
      if (m && m.has(playerId)) return m.get(playerId);
    }
    return 0;
  }
_performRollback method · javascript · L214-L232 (19 LOC)
public/js/rollback.js
  _performRollback(toFrame) {
    const depth = this.frame - toFrame;
    this.emulator.loadState(this.stateHistory.get(toFrame));

    // Suppress audio during re-simulation — only the final (authoritative) frame
    // should produce sound.  The optional-chaining (?.) keeps this compatible with
    // emulators that don't implement setAudioMuted (e.g. DemoGame/Pong).
    this.emulator.setAudioMuted?.(true);
    for (let f = toFrame; f < this.frame; f++) {
      this.stateHistory.set(f, this.emulator.saveState());
      const inputs = this._gatherInputs(f);
      this.usedInputs.set(f, inputs);
      this.emulator.step(inputs);
    }
    this.emulator.setAudioMuted?.(false);

    this._stats.rollbacks++;
    this._stats.maxRollbackDepth = Math.max(this._stats.maxRollbackDepth, depth);
  }
_updateConfirmedFrame method · javascript · L236-L245 (10 LOC)
public/js/rollback.js
  _updateConfirmedFrame() {
    // The "safe" watermark is the min of:
    //   • local:  we know inputs up through frame + INPUT_DELAY
    //   • remote: last received frame for each peer
    let minFrame = this.frame + this.INPUT_DELAY;
    for (const [, lastFrame] of this.lastReceivedFrame) {
      minFrame = Math.min(minFrame, lastFrame);
    }
    if (minFrame > this.confirmedFrame) this.confirmedFrame = minFrame;
  }
_pruneHistory method · javascript · L247-L256 (10 LOC)
public/js/rollback.js
  _pruneHistory() {
    const keepFrom = Math.max(0, this.confirmedFrame - 1);
    for (const [f] of this.stateHistory) {
      if (f < keepFrom) {
        this.stateHistory.delete(f);
        this.usedInputs.delete(f);
        this.confirmedInputs.delete(f);
      }
    }
  }
_sendInput method · javascript · L261-L264 (4 LOC)
public/js/rollback.js
  _sendInput(frame, input) {
    // Overridden by the application layer to actually transmit the input.
    // Default no-op for single-player / offline use.
  }
SNESAdapter class · javascript · L35-L261 (227 LOC)
public/js/snes-adapter.js
class SNESAdapter {
  // Display canvas dimensions: native SNES resolution (256×224).
  // The WASM screen buffer is allocated at 512×448 (SCREEN_BUF_W × SCREEN_BUF_H)
  // for hi-res / compatibility reasons, but actual game pixels occupy only the
  // top-left 256×224 region.  ctx.putImageData clips to canvas bounds, so
  // setting the canvas to 256×224 renders the correct game area automatically.
  static SNES_W = 256;
  static SNES_H = 224;
  // WASM screen-buffer dimensions — must match SCREEN_BYTES in snes9x.js.
  static SCREEN_BUF_W = 512;
  static SCREEN_BUF_H = 448;

  /**
   * @param {HTMLCanvasElement} canvas
   * @param {string[]} playerIds  ordered player IDs (index 0 → controller 1, 1 → controller 2)
   */
  constructor(canvas, playerIds) {
    this.canvas    = canvas;
    this.playerIds = playerIds;
    this.ctx       = canvas.getContext('2d');

    canvas.width  = SNESAdapter.SNES_W;
    canvas.height = SNESAdapter.SNES_H;

    // ImageData must match the WASM screen bu
If a scraper extracted this row, it came from Repobility (https://repobility.com)
constructor method · javascript · L51-L68 (18 LOC)
public/js/snes-adapter.js
  constructor(canvas, playerIds) {
    this.canvas    = canvas;
    this.playerIds = playerIds;
    this.ctx       = canvas.getContext('2d');

    canvas.width  = SNESAdapter.SNES_W;
    canvas.height = SNESAdapter.SNES_H;

    // ImageData must match the WASM screen buffer (512×448), not the display
    // canvas (256×224).  putImageData clips to the canvas, so only the
    // top-left 256×224 pixels — where the emulator places the game — are shown.
    this._imageData = this.ctx.createImageData(SNESAdapter.SCREEN_BUF_W, SNESAdapter.SCREEN_BUF_H);

    this._romLoaded   = false;
    this._audioMuted  = false;
    this._audioCtx    = null;
    this._workletNode = null;
  }
loadROM method · javascript · L76-L99 (24 LOC)
public/js/snes-adapter.js
  async loadROM(url) {
    if (typeof window.snineX === 'undefined') {
      throw new Error('snes9x not loaded — make sure snes9x.js is included before snes-adapter.js');
    }

    const resp = await fetch(`/rom-proxy?url=${encodeURIComponent(url)}`);
    if (!resp.ok) {
      throw new Error(`ROM fetch error: HTTP ${resp.status} — ${resp.statusText}`);
    }

    const buf = await resp.arrayBuffer();

    // Wait for WASM to be ready (snes9x.js initialises asynchronously)
    await this._waitForWasm();

    // Set up the AudioWorklet before starting the emulator so the sample
    // rate is known and audio is ready to receive the first frame's samples.
    await this._initAudio();

    const sampleRate = this._audioCtx?.sampleRate ?? 44100;
    window.snineX.start(buf, sampleRate);

    this._romLoaded = true;
  }
_initAudio method · javascript · L107-L142 (36 LOC)
public/js/snes-adapter.js
  async _initAudio() {
    try {
      this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      await this._audioCtx.audioWorklet.addModule('/js/snes-audio-worklet.js');
      // Explicitly request stereo output — without this, some browsers default
      // to mono (out[1] === undefined), which causes process() to crash silently.
      this._workletNode = new AudioWorkletNode(
        this._audioCtx,
        'snes-audio-processor',
        { numberOfOutputs: 1, outputChannelCount: [2] }
      );
      const gain = this._audioCtx.createGain();
      gain.gain.value = 0.5;
      this._workletNode.connect(gain);
      gain.connect(this._audioCtx.destination);
      // Pre-fill the ring buffer to the DRC target level (2048 stereo sample-pairs
      // ≈ 46 ms at 44 100 Hz) before the game loop starts.  Starting at target
      // prevents the proportional controller from applying a large initial
      // correction and ensures the first real audio frame arrives int
‹ prevpage 2 / 3next ›