Files
picobook/src/client/pico8-client/renderCart.ts

202 lines
5.9 KiB
TypeScript
Raw Normal View History

2024-03-29 22:23:01 -07:00
import { assertNever } from "@firebox/tsutil";
2024-03-29 20:23:14 -07:00
import { pngToRom } from "./pngToRom";
2024-03-31 12:13:27 -07:00
import { RenderCart, renderCart as rawRenderCart } from "./rawRenderCart";
2024-03-29 02:02:40 -07:00
2024-03-31 12:13:27 -07:00
export type PicoCart = {
2024-03-29 02:02:40 -07:00
name: string;
2024-03-29 22:23:01 -07:00
src: string;
} | {
name: string;
rom: number[];
2024-03-29 02:02:40 -07:00
}
type PlayerButtons = {
left: boolean;
right: boolean;
up: boolean;
down: boolean;
o: boolean;
x: boolean;
menu: boolean;
}
2024-03-31 12:13:27 -07:00
export type PicoPlayerHandle = {
raw: ReturnType<RenderCart>;
rawModule: unknown;
2024-03-29 02:02:40 -07:00
// external things
readonly canvas: HTMLCanvasElement;
// i/o
setButtons: (buttons: PlayerButtons[]) => void;
setMouse: (mouse: {
x: number;
y: number;
leftClick: boolean;
rightClick: boolean;
}) => void;
setGamepadCount: (count: number) => void;
2024-04-04 08:58:39 -07:00
gpio: (
number[]
// & {subscribe: (f: (gpio: number[]) => void) => void}
); // read + write (should be 256-tuple)
2024-03-29 02:02:40 -07:00
2024-03-31 12:13:27 -07:00
// state
2024-03-29 02:02:40 -07:00
readonly state: {
2024-03-31 12:13:27 -07:00
frameNumber: number;
isPaused: boolean;
hasFocus: boolean;
requestPointerLock: boolean;
requirePageNavigateConfirmation: boolean;
showDpad: boolean;
shutdownRequested: boolean;
soundVolume: number;
2024-03-29 02:02:40 -07:00
};
// misc?
setTouchDetected: (touchDetected: boolean) => void;
dropCart: (cart: PicoCart) => void;
2024-03-29 20:23:14 -07:00
// Module
toggleSound: () => void;
toggleControlMenu: () => void;
togglePaused: () => void;
// TODO: rename these two better (what do they do??)
modDragOver: () => void;
modDragStop: () => void;
2024-03-29 02:02:40 -07:00
}
const bitfield = (...args: boolean[]): number => {
if (!args.length) {
return 0;
}
return (args[0]?1:0)+2*bitfield(...args.slice(1));
}
2024-03-29 22:23:01 -07:00
const getRom = async (cart: PicoCart) => {
if ("src" in cart) {
return await pngToRom(cart.src);
} else if ("rom" in cart) {
return cart.rom;
2024-03-29 20:23:14 -07:00
}
2024-03-29 22:23:01 -07:00
assertNever(cart);
}
2024-03-29 20:23:14 -07:00
2024-03-29 22:23:01 -07:00
export const makePicoConsole = async (props: {
canvas?: HTMLCanvasElement;
2024-03-31 21:25:54 -07:00
codoTextarea?: HTMLTextAreaElement;
2024-03-29 22:23:01 -07:00
audioContext?: AudioContext;
carts: PicoCart[];
}): Promise<PicoPlayerHandle> => {
2024-03-31 21:25:54 -07:00
const {carts, canvas = document.createElement("canvas"), codoTextarea = document.createElement("textarea"), audioContext = new AudioContext()} = props;
2024-03-29 22:23:01 -07:00
canvas.style.imageRendering = "pixelated";
2024-03-31 21:25:54 -07:00
codoTextarea.style.display="none";
codoTextarea.style.position="fixed";
codoTextarea.style.left="-9999px";
codoTextarea.style.height="0px";
codoTextarea.style.overflow="hidden";
2024-04-01 00:22:52 -07:00
const Module = {canvas, keyboardListeningElement: canvas};
2024-03-29 22:23:01 -07:00
const cartsDatas = await Promise.all(carts.map(cart => getRom(cart)));
2024-03-29 20:23:14 -07:00
const handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext);
2024-03-29 02:02:40 -07:00
handle.pico8_state = {};
handle.pico8_buttons = [0,0,0,0,0,0,0,0];
handle.pico8_mouse = [0,0,0];
2024-04-04 08:58:39 -07:00
handle.pico8_gpio = [
2024-03-29 02:02:40 -07:00
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
];
2024-04-04 08:58:39 -07:00
// let gpioChanged = (gpio: number[]) => {};
// const gpioInner = [
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
// ];
// handle.pico8_gpio = new Proxy(gpioInner, {
// get(target, prop) {
// return target[prop as any];
// },
// set(target, prop, newValue) {
// const t = target as any;
// if (t.setting) {
// return false;
// }
// const prev = [...target];
// target[prop as any] = newValue;
// const next = [...target];
// if (!t.dontSend && prev.some((p, i) => p !== next[i])) {
// gpioChanged(target);
// }
// return true;
// }
// });
// (handle as any).pico8_gpio.subscribe = (f: (gpio: number[]) => void) => {
// gpioChanged = f;
// }
2024-03-29 02:02:40 -07:00
handle.pico8_gamepads = {count: 0};
return {
2024-03-31 12:13:27 -07:00
raw: handle,
rawModule: Module,
2024-03-29 02:02:40 -07:00
canvas,
state: {
frameNumber: handle.pico8_state.frame_number!,
isPaused: !!handle.pico8_state.is_paused!,
hasFocus: !!handle.pico8_state.has_focus!,
requestPointerLock: !!handle.pico8_state.request_pointer_lock!,
requirePageNavigateConfirmation: !!handle.pico8_state.require_page_navigate_confirmation!,
showDpad: !!handle.pico8_state.show_dpad!,
shutdownRequested: !!handle.pico8_state.shutdown_requested!,
soundVolume: handle.pico8_state.sound_volume!,
},
2024-04-03 22:53:11 -07:00
gpio: handle.pico8_gpio as PicoPlayerHandle["gpio"],
2024-03-29 02:02:40 -07:00
setMouse({x, y, leftClick, rightClick}) {
handle.pico8_mouse = [x, y, bitfield(leftClick, rightClick)];
},
setButtons(buttons) {
// TODO: pad this properly here instead of casting
handle.pico8_buttons = buttons.map(({left, right, up, down, o, x, menu}) => bitfield(left, right, up, down, o, x, menu)) as any;
},
setGamepadCount(count) {
handle.pico8_gamepads = {count};
},
setTouchDetected(touchDetected) {
handle.p8_touch_detected = touchDetected ? 1 : 0;
},
dropCart(cart) {
handle.p8_dropped_cart_name = cart.name;
// TODO: make sure this is a dataURL first, and if not, load it and then pass it in
2024-03-29 22:23:01 -07:00
// handle.p8_dropped_cart = cart.src;
2024-03-30 15:07:49 -07:00
// handle.codo_command = 9;
2024-03-29 20:23:14 -07:00
},
modDragOver: (Module as any).pico8DragOver,
modDragStop: (Module as any).pico8DragStop,
togglePaused: (Module as any).pico8TogglePaused,
toggleSound: (Module as any).pico8ToggleSound,
toggleControlMenu: (Module as any).pico8ToggleControlMenu,
2024-03-29 02:02:40 -07:00
}
}