Skip to content

Instantly share code, notes, and snippets.

@alex-red
Created October 6, 2020 21:51
Show Gist options
  • Save alex-red/1221a55e79eeea9762695d0639c7493b to your computer and use it in GitHub Desktop.
Save alex-red/1221a55e79eeea9762695d0639c7493b to your computer and use it in GitHub Desktop.
wasmboy headless
// Wasmboy init
import { WasmBoy } from 'wasmboy';
import * as fs from 'fs';
import * as PNGImage from 'pngjs-image';
import * as sharp from 'sharp';
const pkmn_rom = new Uint8Array(fs.readFileSync('./src/wasmboy/pkmn.gbc'));
const pkmn_rom_save_path = './src/wasmboy/pkmn.save';
const GAMEBOY_CAMERA_WIDTH = 160;
const GAMEBOY_CAMERA_HEIGHT = 144;
const IMAGE_SAVE_PATH = './public/current.png';
const WasmBoyOptionsSchema = {
headless: true,
gameboySpeed: 100.0,
useGbcWhenOptional: true,
isAudioEnabled: false,
frameSkip: 1,
audioBatchProcessing: false,
timersBatchProcessing: false,
audioAccumulateSamples: false,
graphicsBatchProcessing: false,
graphicsDisableScanlineRendering: false,
tileRendering: true,
tileCaching: true,
gameboyFPSCap: 60,
updateGraphicsCallback: false,
updateAudioCallback: false,
saveStateCallback: false,
onReady: false,
onPlay: false,
onPause: false,
onLoadedAndStarted: false,
};
const getImageDataFromFrame = async () => {
// Get our output frame
const frameInProgressVideoOutputLocation = await WasmBoy._getWasmConstant(
'FRAME_LOCATION',
);
const frameInProgressMemory = await WasmBoy._getWasmMemorySection(
frameInProgressVideoOutputLocation,
frameInProgressVideoOutputLocation +
GAMEBOY_CAMERA_HEIGHT * GAMEBOY_CAMERA_WIDTH * 3 +
1,
);
// Going to compare pixel values from the VRAM to confirm tests
const imageDataArray = [];
const rgbColor = [];
for (let y = 0; y < GAMEBOY_CAMERA_HEIGHT; y++) {
for (let x = 0; x < GAMEBOY_CAMERA_WIDTH; x++) {
// Each color has an R G B component
const pixelStart = (y * GAMEBOY_CAMERA_WIDTH + x) * 3;
for (let color = 0; color < 3; color++) {
rgbColor[color] = frameInProgressMemory[pixelStart + color];
}
// Doing graphics using second answer on:
// https://stackoverflow.com/questions/4899799/whats-the-best-way-to-set-a-single-pixel-in-an-html5-canvas
// Image Data mapping
const imageDataIndex = (x + y * GAMEBOY_CAMERA_WIDTH) * 4;
imageDataArray[imageDataIndex] = rgbColor[0];
imageDataArray[imageDataIndex + 1] = rgbColor[1];
imageDataArray[imageDataIndex + 2] = rgbColor[2];
// Alpha, no transparency
imageDataArray[imageDataIndex + 3] = 255;
}
}
return imageDataArray;
};
// Function to create an image from output
const createImageFromFrame = (imageDataArray, outputPath) => {
return new Promise((resolve, reject) => {
// https://www.npmjs.com/package/pngjs-image
const image = PNGImage.createImage(
GAMEBOY_CAMERA_WIDTH,
GAMEBOY_CAMERA_HEIGHT,
);
// Write our pixel values
for (let i = 0; i < imageDataArray.length - 4; i = i + 4) {
// Since 4 indexes represent 1 pixels. divide i by 4
const pixelIndex = i / 4;
// Get our y value from i
const y = Math.floor(pixelIndex / GAMEBOY_CAMERA_WIDTH);
// Get our x value from i
const x = pixelIndex % GAMEBOY_CAMERA_WIDTH;
image.setAt(x, y, {
red: imageDataArray[i],
green: imageDataArray[i + 1],
blue: imageDataArray[i + 2],
alpha: imageDataArray[i + 3],
});
}
image.writeImage('tmp.png', function (err) {
if (err) {
reject(err);
}
sharp('tmp.png')
.resize(320, 288)
.toFile(outputPath)
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
});
});
};
export const getScreenshot = async () => {
const imageFrames = await getImageDataFromFrame();
const image = await createImageFromFrame(imageFrames, IMAGE_SAVE_PATH);
return image;
};
export const getSaveFile = async () => {
try {
const state = JSON.parse(
(await fs.promises.readFile(pkmn_rom_save_path)).toString(),
);
const nwIS = Uint8Array.from(state.wasmboyMemory.wasmBoyInternalState);
state.wasmboyMemory.wasmBoyInternalState = nwIS;
const nwPM = Uint8Array.from(state.wasmboyMemory.wasmBoyPaletteMemory);
state.wasmboyMemory.wasmBoyPaletteMemory = nwPM;
const ngBM = Uint8Array.from(state.wasmboyMemory.gameBoyMemory);
state.wasmboyMemory.gameBoyMemory = ngBM;
const ncR = Uint8Array.from(state.wasmboyMemory.cartridgeRam);
state.wasmboyMemory.cartridgeRam = ncR;
return state;
} catch (error) {
return null;
}
};
export const saveState = async () => {
try {
const state = await WasmBoy.saveState();
const nwIS = Array.from(state.wasmboyMemory.wasmBoyInternalState);
state.wasmboyMemory.wasmBoyInternalState = nwIS;
const nwPM = Array.from(state.wasmboyMemory.wasmBoyPaletteMemory);
state.wasmboyMemory.wasmBoyPaletteMemory = nwPM;
const ngBM = Array.from(state.wasmboyMemory.gameBoyMemory);
state.wasmboyMemory.gameBoyMemory = ngBM;
const ncR = Array.from(state.wasmboyMemory.cartridgeRam);
state.wasmboyMemory.cartridgeRam = ncR;
const saveFile = await fs.promises.writeFile(
pkmn_rom_save_path,
JSON.stringify(state),
);
return saveFile;
} catch (error) {
return null;
}
};
export const initWasmboy = async () => {
await WasmBoy.config(WasmBoyOptionsSchema);
await WasmBoy.loadROM(pkmn_rom);
const saveFile = await getSaveFile();
if (saveFile) {
await WasmBoy.loadState(saveFile);
}
WasmBoy.play();
return WasmBoy.isReady();
};
// server call
@Post('input')
async handleInput(@Query('command') command: string) {
const newState = Object.assign(
{
...WasmBoyJoypadState,
},
{
[command.toLocaleUpperCase()]: true,
},
);
// console.log(newState, WasmBoyJoypadState)
// actual input
WasmBoy.setJoypadState(newState);
await WasmBoy._runWasmExport('executeMultipleFrames', [60]);
//reset input - looks like it holds the button down otherwise
WasmBoy.setJoypadState(WasmBoyJoypadState);
const image = await getScreenshot();
await saveState();
return {
url: 'http://localhost:40404/current.png',
image,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment