Compare commits

4 Commits

Author SHA1 Message Date
Dylan Pizzo 19e0ae4605 downloadable carts 2026-06-11 18:39:38 -04:00
Dylan Pizzo 6313364ba4 patching for smoother experience 2026-06-11 16:52:15 -04:00
Dylan Pizzo 064528b178 try to fix websockets 2026-06-11 14:32:55 -04:00
Dylan Pizzo f0e522b039 remove some logs 2026-06-11 14:24:07 -04:00
7 changed files with 149 additions and 103 deletions
+52 -1
View File
@@ -15,6 +15,22 @@ type Game = {
carts: Parameters<typeof Pico8Player>["0"]["carts"]; carts: Parameters<typeof Pico8Player>["0"]["carts"];
}; };
const getPatch = (before: number[], after: number[]) => {
const diff: Record<number, number> = {};
for (const i in after) {
if (after[i] !== before[i]) {
diff[i] = after[i];
}
}
return diff;
};
const applyPatch = (before: number[], patch: Record<number, number>) => {
for (const i in patch) {
before[i] = patch[i];
}
};
export const GamePage = () => { export const GamePage = () => {
const { author, slug } = useParams(); const { author, slug } = useParams();
// const [text, setText] = useState(""); // const [text, setText] = useState("");
@@ -46,6 +62,16 @@ export const GamePage = () => {
} }
} }
} }
if (msg.gpioPatch) {
if (picoRef.current) {
const handle = picoRef.current;
if (handle) {
// console.log("updating pico gpio");
applyPatch(handle.gpio, msg.gpioPatch);
setPrevGpio([...handle.gpio]);
}
}
}
}, },
}); });
const [game, setGame] = useState<Game | null>(null); const [game, setGame] = useState<Game | null>(null);
@@ -70,7 +96,9 @@ export const GamePage = () => {
if (JSON.stringify(handle.gpio) !== JSON.stringify(prevGpio)) { if (JSON.stringify(handle.gpio) !== JSON.stringify(prevGpio)) {
if (prevGpio) { if (prevGpio) {
setPrevGpio([...handle.gpio]); setPrevGpio([...handle.gpio]);
socket.sendMessage({ gpio: handle.gpio }); socket.sendMessage({
gpioPatch: getPatch(prevGpio, handle.gpio),
});
} else { } else {
socket.sendMessage({ getGpio: true }); socket.sendMessage({ getGpio: true });
setPrevGpio([...handle.gpio]); setPrevGpio([...handle.gpio]);
@@ -127,6 +155,29 @@ export const GamePage = () => {
<Pico8Player consoleRef={picoRef} carts={game.carts} /> <Pico8Player consoleRef={picoRef} carts={game.carts} />
</div> </div>
</div> </div>
<div
className={css`
display: flex;
flex-wrap: wrap;
`}
>
{game.carts.map((cart) =>
"src" in cart ? (
<div
key={cart.name}
className={css`
display: flex;
flex-direction: column;
`}
>
<img src={cart.src} />
<a href={cart.src} download={`${cart.name}.p8.png`}>
Download
</a>
</div>
) : null,
)}
</div>
{/* <div> {/* <div>
<input onChange={(x) => setText(x.target.value)} /> <input onChange={(x) => setText(x.target.value)} />
<button <button
+1 -2
View File
@@ -20,7 +20,7 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
games: [], games: [],
}; };
} }
console.log("author", authorData); // console.log("author", authorData);
const pat = decrypt({ const pat = decrypt({
password: process.env.ENCRYPTION_PASSWORD!, password: process.env.ENCRYPTION_PASSWORD!,
@@ -47,7 +47,6 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
.filter((x) => x.endsWith(".p8.png")) .filter((x) => x.endsWith(".p8.png"))
.map((x) => x.slice(0, -".p8.png".length)), .map((x) => x.slice(0, -".p8.png".length)),
); );
console.log("hi");
} }
return { return {
+2 -2
View File
@@ -17,7 +17,7 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
if (!authorData) { if (!authorData) {
return null; return null;
} }
console.log("author", authorData); // console.log("author", authorData);
const pat = decrypt({ const pat = decrypt({
password: process.env.ENCRYPTION_PASSWORD!, password: process.env.ENCRYPTION_PASSWORD!,
@@ -36,7 +36,7 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
}) })
).data; ).data;
console.log(gameData); // console.log(gameData);
if (Array.isArray(gameData)) { if (Array.isArray(gameData)) {
return null; return null;
-19
View File
@@ -1,19 +0,0 @@
import { Type } from "@sinclair/typebox";
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.js";
const method = "POST";
const url = "/api/webhook-gh";
const payloadT = Type.Any();
const handler = ({payload}: FirRouteInput<typeof payloadT>) => {
console.log(payload);
return {};
};
export default {
method,
url,
payloadT,
handler,
} as const satisfies FirRouteOptions<typeof payloadT>;
+1 -1
View File
@@ -9,7 +9,7 @@ const server = Fastify({
logger: true, logger: true,
}); });
server.register(fastifyWebsocket); server.register<any>(fastifyWebsocket, { server: server.server });
server.register(fastifyStatic, { server.register(fastifyStatic, {
root: new URL("public", import.meta.url).toString().slice("file://".length), root: new URL("public", import.meta.url).toString().slice("file://".length),
+1 -2
View File
@@ -2,8 +2,7 @@ import echo from "./api/echo.ts";
import getAuthor from "./api/getAuthor.ts"; import getAuthor from "./api/getAuthor.ts";
import getGame from "./api/getGame.ts"; import getGame from "./api/getGame.ts";
import room from "./api/room.ts"; import room from "./api/room.ts";
import webhook from "./api/webhook.ts";
export const routeList = [echo, webhook, getAuthor, room, getGame]; export const routeList = [echo, getAuthor, room, getGame];
export type RouteList = typeof routeList; export type RouteList = typeof routeList;
+92 -76
View File
@@ -1,6 +1,6 @@
import { Static, TSchema } from "@sinclair/typebox"; import { Static, TSchema } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value"; import { Value } from "@sinclair/typebox/value";
import { FastifyInstance, FastifyRequest, HTTPMethods } from "fastify" import { FastifyInstance, FastifyRequest, HTTPMethods } from "fastify";
import { RouteOptions } from "fastify/types/route.js"; import { RouteOptions } from "fastify/types/route.js";
import { type WebSocket } from "@fastify/websocket"; import { type WebSocket } from "@fastify/websocket";
@@ -9,113 +9,129 @@ type WebsocketConnection = Parameters<Defined<RouteOptions["wsHandler"]>>[0];
type URLString = string; type URLString = string;
export type FirRouteInput<TPayloadSchema extends TSchema> = { export type FirRouteInput<TPayloadSchema extends TSchema> = {
payload: Static<TPayloadSchema>, payload: Static<TPayloadSchema>;
}
export type FirWebsocketInput<TPayloadSchema extends TSchema> = {
socket: WebsocketConnection,
req: FastifyRequest,
payload: Static<TPayloadSchema>,
}
export type FirWebsocketHandler<TIn extends TSchema = TSchema> = {
onMessage?(input: FirWebsocketInput<TIn>): void,
onOpen?(input: {socket: WebSocket, req: FastifyRequest}): void,
onClose?(input: {socket: WebSocket, req: FastifyRequest}): void,
onError?(input: {socket: WebSocket, req: FastifyRequest, error: unknown}): void,
}; };
export type FirRouteOptions<TIn extends TSchema = TSchema, TOut extends TSchema = TSchema> = { export type FirWebsocketInput<TPayloadSchema extends TSchema> = {
method: HTTPMethods, socket: WebsocketConnection;
url: URLString, req: FastifyRequest;
payloadT: TIn, payload: Static<TPayloadSchema>;
responseT?: TOut, };
} & ({
handler: (input: FirRouteInput<TIn>) => Static<TOut> | Promise<Static<TOut>>, export type FirWebsocketHandler<TIn extends TSchema = TSchema> = {
} | { onMessage?(input: FirWebsocketInput<TIn>): void;
websocket: FirWebsocketHandler<TIn>, onOpen?(input: { socket: WebSocket; req: FastifyRequest }): void;
}) onClose?(input: { socket: WebSocket; req: FastifyRequest }): void;
onError?(input: {
socket: WebSocket;
req: FastifyRequest;
error: unknown;
}): void;
};
export type FirRouteOptions<
TIn extends TSchema = TSchema,
TOut extends TSchema = TSchema,
> = {
method: HTTPMethods;
url: URLString;
payloadT: TIn;
responseT?: TOut;
} & (
| {
handler: (
input: FirRouteInput<TIn>,
) => Static<TOut> | Promise<Static<TOut>>;
}
| {
websocket: FirWebsocketHandler<TIn>;
}
);
type Defined<T> = T extends undefined ? never : T; type Defined<T> = T extends undefined ? never : T;
export const attachRoute = <TIn extends TSchema, TOut extends TSchema>(server: FastifyInstance, routeOptions: FirRouteOptions<TIn, TOut>) => { export const attachRoute = <TIn extends TSchema, TOut extends TSchema>(
const { server: FastifyInstance,
method, routeOptions: FirRouteOptions<TIn, TOut>,
url, ) => {
payloadT, const { method, url, payloadT } = routeOptions;
} = routeOptions;
if ("websocket" in routeOptions) { if ("websocket" in routeOptions) {
console.log('SETTING UP WS'); console.log("SETTING UP WS");
const {websocket} = routeOptions; const { websocket } = routeOptions;
server.register(async function(fastify: FastifyInstance) { server.register(async function (fastify: FastifyInstance) {
fastify.get('/api/ws/room', { websocket: true }, (socket: WebSocket, req: FastifyRequest) => { fastify.get(
websocket.onOpen && websocket.onOpen({socket, req}); "/api/ws/room",
socket.on('message', (message: any) => { { websocket: true },
const payload = JSON.parse(message.toString()); (socket: WebSocket, req: FastifyRequest) => {
if (Value.Check(payloadT, payload)) { websocket.onOpen && websocket.onOpen({ socket, req });
websocket.onMessage && websocket.onMessage({socket, payload, req}); socket.on("message", (message: any) => {
} else { const payload = JSON.parse(message.toString());
throw new Error("Payload wrong shape."); if (Value.Check(payloadT, payload)) {
} websocket.onMessage &&
}); websocket.onMessage({ socket, payload, req });
socket.on('close', () => { } else {
websocket.onClose && websocket.onClose({socket, req}); throw new Error("Payload wrong shape.");
}); }
socket.on('error', (error: any) => { });
websocket.onError && websocket.onError({socket, error, req}); socket.on("close", () => {
}); websocket.onClose && websocket.onClose({ socket, req });
}) });
socket.on("error", (error: any) => {
websocket.onError &&
websocket.onError({ socket, error, req });
});
},
);
}); });
return; return;
// const {websocket} = routeOptions; // const {websocket} = routeOptions;
// const augmentedWsHandler = (conn: Parameters<Defined<RouteOptions["wsHandler"]>>[0]) => { // const augmentedWsHandler = (conn: Parameters<Defined<RouteOptions["wsHandler"]>>[0]) => {
// console.log('HELLO'); // console.log('HELLO');
// conn.on("message", (message) => { // conn.on("message", (message) => {
// const payload = JSON.parse(message.toString()); // const payload = JSON.parse(message.toString());
// if (Value.Check(payloadT, payload)) { // if (Value.Check(payloadT, payload)) {
// websocket({socket: conn, payload}); // websocket({socket: conn, payload});
// } else { // } else {
// throw new Error("Payload wrong shape."); // throw new Error("Payload wrong shape.");
// } // }
// }); // });
// } // }
// return { // return {
// method: 'GET', // WebSocket upgrades are GET requests // method: 'GET', // WebSocket upgrades are GET requests
// url, // url,
// // websocket: true, // // websocket: true,
// wsHandler: augmentedWsHandler, // wsHandler: augmentedWsHandler,
// handler: (...args) => { // handler: (...args) => {
// console.log('socket!'); // console.log('socket!');
// const socket = args[0].socket.on("message", () => { // const socket = args[0].socket.on("message", () => {
// console.log("connected!"); // console.log("connected!");
// }) // })
// }, // },
// // handler: (request, reply) => { // // handler: (request, reply) => {
// // reply.code(405).send({ message: 'Method Not Allowed' }); // Handle non-WebSocket requests // // reply.code(405).send({ message: 'Method Not Allowed' }); // Handle non-WebSocket requests
// // } // // }
// } // }
} }
const {handler} = routeOptions; const { handler } = routeOptions;
const augmentedHandler = (request: Parameters<RouteOptions["handler"]>[0]) => { const augmentedHandler = (
const { request: Parameters<RouteOptions["handler"]>[0],
body, ) => {
query, const { body, query } = request;
} = request;
const payload = body ?? query; const payload = body ?? query;
if (Value.Check(payloadT, payload)) { if (Value.Check(payloadT, payload)) {
return handler({payload}); return handler({ payload });
} else { } else {
throw new Error("Payload wrong shape."); throw new Error("Payload wrong shape.");
} }
} };
server.route({ server.route({
method, method,
url, url,
handler: augmentedHandler, handler: augmentedHandler,
}); });
} };