Compare commits
1 Commits
main
...
gpio-strea
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9d085b08dd |
10
README.md
10
README.md
@ -1,6 +1,12 @@
|
|||||||
# Picobook
|
# Firstack
|
||||||
|
|
||||||
A website for hosting pico8 projects.
|
Firstack is a template repo for a tech stack. This stack includes
|
||||||
|
|
||||||
|
- [react](https://react.dev/)
|
||||||
|
- [emotion](https://emotion.sh/)
|
||||||
|
- [fastify](https://fastify.dev/)
|
||||||
|
- [postgres](https://www.postgresql.org/)
|
||||||
|
- [typescript](https://www.typescriptlang.org/)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { DbRelease } from "../server/dbal/dbal";
|
import { DbRelease } from "../server/dbal/dbal";
|
||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { useWebsocket } from "./hooks/useWebsocket";
|
import { useWebsocket } from "./hooks/useWebsocket";
|
||||||
|
import { PicoPortal } from "./components/PicoPortal";
|
||||||
|
|
||||||
type Info = {
|
type Info = {
|
||||||
release: DbRelease | null;
|
release: DbRelease | null;
|
||||||
@ -12,29 +13,35 @@ type Info = {
|
|||||||
|
|
||||||
export const GamePage = () => {
|
export const GamePage = () => {
|
||||||
const {author, slug} = useParams();
|
const {author, slug} = useParams();
|
||||||
// const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
// const room = searchParams.get('room');
|
const room = searchParams.get('room');
|
||||||
const picoRef = useRef<Pico8ConsoleImperatives>(null);
|
const picoRef = useRef<Pico8ConsoleImperatives>(null);
|
||||||
// const socket = useWebsocket({
|
const socket = useWebsocket({
|
||||||
// url: `/api/ws/room?room=${room}`,
|
url: `/api/ws/room?room=${room}&`,
|
||||||
// // url: "wss://echo.websocket.org",
|
// url: "wss://echo.websocket.org",
|
||||||
// onMessage({message}) {
|
onMessage({message}) {
|
||||||
// // const msg = message as any;
|
if (picoRef.current) {
|
||||||
// // if (msg.type === "gpio") {
|
const handle = picoRef.current.getPicoConsoleHandle();
|
||||||
// // if (picoRef.current) {
|
if (handle) {
|
||||||
// // const handle = picoRef.current.getPicoConsoleHandle();
|
handle.buttons;
|
||||||
// // if (handle) {
|
}
|
||||||
// // console.log("updating pico gpio");
|
}
|
||||||
// // (handle.gpio as any).dontSend = true;
|
// const msg = message as any;
|
||||||
// // handle.gpio.length = 0;
|
// if (msg.type === "gpio") {
|
||||||
// // handle.gpio.push(...msg.gpio);
|
// if (picoRef.current) {
|
||||||
// // (handle.gpio as any).dontSend = false;
|
// const handle = picoRef.current.getPicoConsoleHandle();
|
||||||
// // }
|
// if (handle) {
|
||||||
// // }
|
// console.log("updating pico gpio");
|
||||||
// // }
|
// (handle.gpio as any).dontSend = true;
|
||||||
// console.log('message', message);
|
// handle.gpio.length = 0;
|
||||||
// }
|
// handle.gpio.push(...msg.gpio);
|
||||||
// })
|
// (handle.gpio as any).dontSend = false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
console.log('message', message);
|
||||||
|
}
|
||||||
|
})
|
||||||
// const version = searchParams.get('v');
|
// const version = searchParams.get('v');
|
||||||
const [v, setVersion] = useState<string | null>(null);
|
const [v, setVersion] = useState<string | null>(null);
|
||||||
const [info, setInfo] = useState<Info | null>(null);
|
const [info, setInfo] = useState<Info | null>(null);
|
||||||
@ -120,6 +127,7 @@ export const GamePage = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PicoPortal />
|
||||||
{/* <div>
|
{/* <div>
|
||||||
<p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p>
|
<p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
26
src/client/components/PicoPortal.tsx
Normal file
26
src/client/components/PicoPortal.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { Pico8Console, Pico8ConsoleImperatives } from "../pico8-client/Pico8Console"
|
||||||
|
|
||||||
|
export const PicoPortal = () => {
|
||||||
|
const emptyCartData: number[] = new Array(32786).fill(0);
|
||||||
|
const cart = {name: "empty", rom: emptyCartData};
|
||||||
|
// const picoRef = useRef<Pico8ConsoleImperatives>(null);
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<Pico8Console
|
||||||
|
ref={(ref) => {
|
||||||
|
if (!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handle = ref.getPicoConsoleHandle();
|
||||||
|
if (!handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handle.buttons.subscribe((buttons) => {
|
||||||
|
console.log(buttons);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
carts={[cart]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { assertNever } from "@firebox/tsutil";
|
import { assertNever } from "@firebox/tsutil";
|
||||||
import { pngToRom } from "./pngToRom";
|
import { pngToRom } from "./pngToRom";
|
||||||
import { RenderCart, renderCart as rawRenderCart } from "./rawRenderCart";
|
import { RenderCart, renderCart as rawRenderCart } from "./rawRenderCart";
|
||||||
|
import { Watched, watch } from "../util/watch";
|
||||||
|
|
||||||
export type PicoCart = {
|
export type PicoCart = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -20,13 +21,16 @@ type PlayerButtons = {
|
|||||||
menu: boolean;
|
menu: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawHandle = ReturnType<RenderCart>;
|
||||||
|
|
||||||
export type PicoPlayerHandle = {
|
export type PicoPlayerHandle = {
|
||||||
raw: ReturnType<RenderCart>;
|
raw: RawHandle;
|
||||||
rawModule: unknown;
|
rawModule: unknown;
|
||||||
// external things
|
// external things
|
||||||
readonly canvas: HTMLCanvasElement;
|
readonly canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
// i/o
|
// i/o
|
||||||
|
buttons: Watched<NonNullable<RawHandle["pico8_buttons"]>>;
|
||||||
setButtons: (buttons: PlayerButtons[]) => void;
|
setButtons: (buttons: PlayerButtons[]) => void;
|
||||||
setMouse: (mouse: {
|
setMouse: (mouse: {
|
||||||
x: number;
|
x: number;
|
||||||
@ -177,6 +181,7 @@ export const makePicoConsole = async (props: {
|
|||||||
setMouse({x, y, leftClick, rightClick}) {
|
setMouse({x, y, leftClick, rightClick}) {
|
||||||
handle.pico8_mouse = [x, y, bitfield(leftClick, rightClick)];
|
handle.pico8_mouse = [x, y, bitfield(leftClick, rightClick)];
|
||||||
},
|
},
|
||||||
|
buttons: watch(handle.pico8_buttons!),
|
||||||
setButtons(buttons) {
|
setButtons(buttons) {
|
||||||
// TODO: pad this properly here instead of casting
|
// 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;
|
handle.pico8_buttons = buttons.map(({left, right, up, down, o, x, menu}) => bitfield(left, right, up, down, o, x, menu)) as any;
|
||||||
|
67
src/client/util/watch.ts
Normal file
67
src/client/util/watch.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const deepEqual = (a: any, b: any) => {
|
||||||
|
if (a === b) return true;
|
||||||
|
|
||||||
|
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
|
||||||
|
|
||||||
|
let keysA = Object.keys(a), keysB = Object.keys(b);
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
|
||||||
|
for (let key of keysA) {
|
||||||
|
if (!keysB.includes(key)) return false;
|
||||||
|
|
||||||
|
if (typeof a[key] === 'function' || typeof b[key] === 'function') {
|
||||||
|
if (a[key].toString() !== b[key].toString()) return false;
|
||||||
|
} else {
|
||||||
|
if (!deepEqual(a[key], b[key])) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Watched<T> = {
|
||||||
|
value: T;
|
||||||
|
subscribe: (f: (newVal: T, oldVal: T) => void) => void;
|
||||||
|
unsubscribe: (f: (newVal: T, oldVal: T) => void) => void;
|
||||||
|
locked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const watch = <T extends Record<any,any> | any[]>(target: T): Watched<T> => {
|
||||||
|
let listeners: Array<(newVal: T, oldVal: T) => void> = [];
|
||||||
|
let locked = false;
|
||||||
|
const proxy = new Proxy(target, {
|
||||||
|
get(t: any, prop) {
|
||||||
|
return t[prop as any];
|
||||||
|
},
|
||||||
|
set(t: any, prop, newValue) {
|
||||||
|
if (locked) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const prev = structuredClone(t);
|
||||||
|
t[prop as any] = newValue;
|
||||||
|
if (deepEqual(prev, t)) {
|
||||||
|
listeners.forEach(listener => listener(t, prev));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
get value() {
|
||||||
|
return target;
|
||||||
|
},
|
||||||
|
subscribe(f) {
|
||||||
|
listeners.push(f)
|
||||||
|
},
|
||||||
|
unsubscribe(f) {
|
||||||
|
listeners = listeners.filter(l => l !== f);
|
||||||
|
},
|
||||||
|
get locked() {
|
||||||
|
return locked;
|
||||||
|
},
|
||||||
|
set locked(newVal: boolean) {
|
||||||
|
locked = newVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,14 +40,6 @@
|
|||||||
footer {
|
footer {
|
||||||
max-inline-size: none;
|
max-inline-size: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
|
||||||
a:hover,
|
|
||||||
a:focus,
|
|
||||||
a:active,
|
|
||||||
a:visited {
|
|
||||||
color: lime;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user