Compare commits

11 Commits

Author SHA1 Message Date
Dylan Pizzo daed4245fd remove debug code 2 2026-06-11 13:43:13 -04:00
Dylan Pizzo afea1ce949 remove debug code 2026-06-11 13:42:54 -04:00
Dylan Pizzo 6ad0ffe1c5 gpio! 2026-06-11 13:41:52 -04:00
Dylan Pizzo e51df30a08 add author list to homepage 2026-06-11 10:01:31 -04:00
Dylan Pizzo 03e13075da add loupizzo as an author 2026-06-11 09:35:09 -04:00
Dylan Pizzo e76e4eb34c remove unneeded deps 2026-06-10 22:01:38 -04:00
Dylan Pizzo cb14976221 fix prod-start 2026-06-10 21:58:39 -04:00
Dylan Pizzo e6fede22f6 fix games 2026-06-10 21:57:26 -04:00
Dylan Pizzo 876404d8f5 new format wip 2026-06-10 21:47:30 -04:00
Dylan Pizzo 07595c31ef prettier i guess 2026-01-14 12:44:20 -08:00
Dylan Pizzo db2007e4b0 Update the readme 2025-02-19 19:54:07 -08:00
48 changed files with 11036 additions and 144216 deletions
+2 -1
View File
@@ -1 +1,2 @@
@firebox:registry = "https://nodepack.playbox.link"
@firebox:registry = "https://nodepack.playbox.link"
@dylanpizzo:registry = "https://dylanpizzo.dev"
+1 -1
View File
@@ -1 +1 @@
18
20
+6 -6
View File
@@ -1,15 +1,15 @@
# Installs Node image
FROM node:18-alpine as base
FROM node:20-alpine as base
# sets the working directory for any RUN, CMD, COPY command
WORKDIR /app
# Install Python and pip
RUN apk add --update python3 py3-pip
RUN python3 -m ensurepip
RUN pip3 install --no-cache --upgrade pip setuptools
# # Install Python and pip
# RUN apk add --update python3 py3-pip
# RUN python3 -m ensurepip
# RUN pip3 install --no-cache --upgrade pip setuptools
COPY ./data ./data
# COPY ./data ./data
# Copies stuff to cache for install
COPY ./package.json ./package-lock.json tsconfig.json ./
+2 -8
View File
@@ -1,12 +1,6 @@
# Firstack
# Picobook
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/)
A website for hosting pico8 projects.
## Dependencies
BIN
View File
Binary file not shown.
+14 -31
View File
@@ -1,33 +1,16 @@
version: '3.9'
version: "3.9"
services:
app:
container_name: picobook-app
image: node
build:
context: .
dockerfile: Dockerfile
target: base
env_file:
.env
ports:
- ${PORT}:${PORT}
depends_on:
- db
profiles: ["prod"]
app:
container_name: picobook-app
image: node
build:
context: .
dockerfile: Dockerfile
target: base
env_file: .env
ports:
- ${PORT}:${PORT}
profiles: ["prod"]
db:
container_name: picobook-postgres
image: postgres
env_file:
.env
ports:
- '5432:${DB_PORT}'
volumes:
- data:/data/db
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
profiles: ["dev", "prod"]
volumes:
data: {}
volumes:
data: {}
+9268 -8217
View File
File diff suppressed because it is too large Load Diff
+60 -60
View File
@@ -1,62 +1,62 @@
{
"name": "firstack",
"version": "1.0.0",
"type": "module",
"description": "Firstack is a template repo for a tech stack.",
"main": "index.js",
"scripts": {
"makeuser": "npm run withenv ./scripts/makeuser.ts",
"dev-docker": "docker compose --profile dev up -d",
"dev-server": "echo \"starting server\" && npm run withenv ./src/server/index.ts",
"dev-watch-client": "npm run withenv ./scripts/watch.ts",
"dev-migrate": "source ./.env && pg-migrations apply --directory ./src/database/migrations",
"prod-migrate": "pg-migrations apply --directory ./src/database/migrations",
"prod-build-client": "npm run withenv ./scripts/build.ts",
"prod-docker": "docker compose --profile prod up -d",
"prod-start": "echo \"building frontend\" && npm run prod-build-client && echo \"${DATABASE_URL}\" && echo \"running migrations\" && npm run prod-migrate && echo \"starting server\" && npm run withenv ./src/server/index.ts",
"withenv": "tsx ./scripts/run-with-env.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"build-p8client": "bun build src/client/pico8-client/veryRawRenderCart.js --outdir src/client/pico8-client/build",
"add-pico": "npm run withenv ./scripts/do-release.ts"
},
"repository": {
"type": "git",
"url": "https://git.playbox.link/dylan/firstack.git"
},
"author": "",
"license": "UNLICENSED",
"dependencies": {
"@databases/pg": "^5.4.1",
"@fastify/cookie": "^9.0.4",
"@fastify/secure-session": "^7.1.0",
"@fastify/static": "^6.10.2",
"@fastify/websocket": "^10.0.1",
"@firebox/components": "^0.1.5",
"@firebox/tsutil": "^0.1.2",
"@sinclair/typebox": "^0.31.5",
"dotenv": "^16.4.5",
"execa": "^8.0.1",
"fastify": "^4.26.2",
"isomorphic-git": "^1.25.6",
"react-pico-8": "^4.1.0",
"react-router-dom": "^6.18.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@databases/pg-migrations": "^5.0.2",
"@emotion/css": "^11.11.2",
"@emotion/react": "^11.11.1",
"@types/node": "^20.11.30",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"esbuild": "^0.19.2",
"nodemon": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.10.1",
"tsx": "^4.7.1",
"typescript": "^5.2.2"
}
"name": "picobook",
"version": "1.0.0",
"type": "module",
"description": "Picobook.",
"main": "index.js",
"scripts": {
"makeuser": "npm run withenv ./scripts/makeuser.ts",
"dev-docker": "docker compose --profile dev up -d",
"dev-server": "echo \"starting server\" && npm run withenv ./src/server/index.ts",
"dev-watch-client": "npm run withenv ./scripts/watch.ts",
"dev-migrate": "source ./.env && pg-migrations apply --directory ./src/database/migrations",
"prod-migrate": "pg-migrations apply --directory ./src/database/migrations",
"prod-build-client": "npm run withenv ./scripts/build.ts",
"prod-docker": "docker compose --profile prod up -d",
"prod-start": "echo \"building frontend\" && npm run prod-build-client && echo \"starting server\" && npm run withenv ./src/server/index.ts",
"withenv": "tsx ./scripts/run-with-env.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"add-pico": "npm run withenv ./scripts/do-release.ts"
},
"repository": {
"type": "git",
"url": "https://git.playbox.link/dylan/firstack.git"
},
"author": "",
"license": "UNLICENSED",
"dependencies": {
"@athingperday/react-pico-player": "^0.1.1",
"@databases/pg": "^5.4.1",
"@fastify/cookie": "^9.0.4",
"@fastify/secure-session": "^7.1.0",
"@fastify/static": "^7.0.4",
"@fastify/websocket": "^10.0.1",
"@octokit/core": "^7.0.6",
"@sinclair/typebox": "^0.31.5",
"dotenv": "^16.4.5",
"execa": "^8.0.1",
"fastify": "^4.26.2",
"isomorphic-git": "^1.25.6",
"octokit": "^5.0.5",
"react-pico-8": "^4.1.0",
"react-router-dom": "^6.18.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@databases/pg-migrations": "^5.0.2",
"@emotion/css": "^11.11.2",
"@emotion/react": "^11.11.1",
"@types/node": "^20.11.30",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"esbuild": "^0.19.2",
"nodemon": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.10.1",
"tsx": "^4.7.1",
"typescript": "^5.2.2"
}
}
+28 -36
View File
@@ -1,15 +1,15 @@
import { Link, useParams } from "react-router-dom"
import { Link, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { DbRelease } from "../server/dbal/dbal";
import { css } from "@emotion/css";
// import { type Pico8Player } from "@athingperday/react-pico-player";
type Info = {
author: string | null;
games: {slug: string; releases: DbRelease[]}[];
}
author: string | null;
games: string[];
};
export const AuthorPage = () => {
const {author} = useParams();
const { author } = useParams();
const [info, setInfo] = useState<Info | null>(null);
useEffect(() => {
@@ -17,46 +17,38 @@ export const AuthorPage = () => {
let url = `/api/author?author=${author}`;
const information = await fetch(url);
const json = await information.json();
console.log('json', json);
console.log("json", json);
setInfo(json);
}
};
fetchInfo();
}, [setInfo, author]);
if (!info) {
return (
<div>
LOADING...
</div>
)
return <div>LOADING...</div>;
}
if (!info.author) {
return (
<div>
NOT FOUND
</div>
)
return <div>NOT FOUND</div>;
}
return (
<div className={css`
margin: auto;
width: max-content;
max-inline-size: 66ch;
padding: 1.5em;
display: flex;
flex-direction: column;
gap: 1em;
`}>
<div
className={css`
margin: auto;
width: max-content;
max-inline-size: 66ch;
padding: 1.5em;
display: flex;
flex-direction: column;
gap: 1em;
`}
>
<h1>{author}</h1>
{
info.games.map(game => (
<Link key={game.slug} to={`/u/${author}/${game.slug}`}>
<h3>{game.releases[0].manifest.title ?? game.slug}</h3>
</Link>
))
}
{info.games.map((game) => (
<Link key={game} to={`/u/${author}/${game}`}>
<h3>{game}</h3>
</Link>
))}
</div>
)
}
);
};
+120 -103
View File
@@ -1,128 +1,145 @@
import { Link, useParams, useSearchParams } from "react-router-dom"
import { Pico8Console, Pico8ConsoleImperatives } from "./pico8-client/Pico8Console";
import { useEffect, useRef, useState } from "react";
import { DbRelease } from "../server/dbal/dbal";
import { Link, useParams, useSearchParams } from "react-router-dom";
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { css } from "@emotion/css";
import { useWebsocket } from "./hooks/useWebsocket";
import { Pico8Player } from "@athingperday/react-pico-player";
type Info = {
release: DbRelease | null;
versions: string[];
}
type PicoPlayerHandle =
NonNullable<
Parameters<typeof Pico8Player>["0"]["consoleRef"]
> extends MutableRefObject<infer T>
? NonNullable<T>
: never;
type Game = {
carts: Parameters<typeof Pico8Player>["0"]["carts"];
};
export const GamePage = () => {
const {author, slug} = useParams();
// const [searchParams, setSearchParams] = useSearchParams();
// const room = searchParams.get('room');
const picoRef = useRef<Pico8ConsoleImperatives>(null);
// const socket = useWebsocket({
// url: `/api/ws/room?room=${room}`,
// // url: "wss://echo.websocket.org",
// onMessage({message}) {
// // const msg = message as any;
// // if (msg.type === "gpio") {
// // if (picoRef.current) {
// // const handle = picoRef.current.getPicoConsoleHandle();
// // if (handle) {
// // console.log("updating pico gpio");
// // (handle.gpio as any).dontSend = true;
// // 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 [v, setVersion] = useState<string | null>(null);
const [info, setInfo] = useState<Info | null>(null);
const version = v ?? info?.release?.version ?? info?.versions[0];
const { author, slug } = useParams();
// const [text, setText] = useState("");
const [prevGpio, setPrevGpio] = useState<number[] | null>(null);
const [searchParams, setSearchParams] = useSearchParams();
const room = searchParams.get("room");
const picoRef = useRef<PicoPlayerHandle>(null);
const socket = useWebsocket({
url: `/api/ws/room?room=${room}`,
onMessage({ message }) {
// console.log("message", message);
const msg = message as any;
if (msg.getGpio) {
if (picoRef.current) {
const handle = picoRef.current;
if (handle) {
socket.sendMessage({ gpio: handle.gpio });
}
}
}
if (msg.gpio) {
if (picoRef.current) {
const handle = picoRef.current;
if (handle) {
// console.log("updating pico gpio");
handle.gpio.length = 0;
handle.gpio.push(...msg.gpio);
setPrevGpio([...handle.gpio]);
}
}
}
},
});
const [game, setGame] = useState<Game | null>(null);
useEffect(() => {
const fetchInfo = async () => {
let url = `/api/release?author=${author}&slug=${slug}`;
if (version) {
url += `&version=${version}`;
}
let url = `/api/game?author=${author}&name=${slug ?? ""}`;
const information = await fetch(url);
const json = await information.json();
setInfo(json);
}
console.log(json);
setGame(json);
};
fetchInfo();
}, [setInfo, author, slug, version]);
}, [setGame, slug]);
if (!info) {
return (
<div>
LOADING...
</div>
)
useEffect(() => {
const interval = setInterval(() => {
const handle = picoRef.current;
if (!handle) {
return;
}
if (JSON.stringify(handle.gpio) !== JSON.stringify(prevGpio)) {
if (prevGpio) {
setPrevGpio([...handle.gpio]);
socket.sendMessage({ gpio: handle.gpio });
} else {
socket.sendMessage({ getGpio: true });
setPrevGpio([...handle.gpio]);
}
}
}, 1000 / 60);
return () => {
clearInterval(interval);
};
});
if (!game) {
return <div>LOADING...</div>;
}
if (!info.release) {
return (
<div>
NOT FOUND
</div>
)
if (!game.carts) {
return <div>NOT FOUND</div>;
}
return (
<div className={css`
margin: auto;
width: max-content;
max-inline-size: 66ch;
padding: 1.5em;
display: flex;
flex-direction: column;
gap: 1em;
`}>
<div>
<h1>{info.release.manifest.title ?? slug!.split("-").map(word => word[0].toUpperCase()+word.slice(1)).join(" ")}</h1>
<h2>by <Link to={`/u/${info.release.author}`}>{info.release.author}</Link></h2>
</div>
<div className={css`
width: 512px;
max-width: 100%;
<div
className={css`
margin: auto;
`}>
<div className={css`
border: 2px solid transparent;
&:focus-within {
border: 2px solid limegreen;
}
`}>
<Pico8Console
ref={picoRef}
carts={info.release.carts}
// onGpioChange={(gpio: number[]) => {
// console.log("sending gpio");
// socket.sendMessage({
// type: "gpio",
// gpio,
// });
// }}
/>
</div>
<div className={css`
display: flex;
justify-content: end;
`}>
Version: <select defaultValue={info.release.version} onChange={(ev) => setVersion(ev.target.value)}>
{
[...info.versions].reverse().map(v => (
<option key={v} value={v}>{v}</option>
))
width: max-content;
max-inline-size: 66ch;
padding: 1.5em;
display: flex;
flex-direction: column;
gap: 1em;
`}
>
<div>
<h1>{slug}</h1>
<h2>
by <Link to={`/u/${author}`}>{author}</Link>
</h2>
</div>
<div
className={css`
width: 512px;
max-width: 100%;
margin: auto;
`}
>
<div
className={css`
border: 2px solid transparent;
&:focus-within {
border: 2px solid limegreen;
}
</select>
`}
>
<Pico8Player consoleRef={picoRef} carts={game.carts} />
</div>
</div>
{/* <div>
<input onChange={(x) => setText(x.target.value)} />
<button
onClick={() => {
socket.sendMessage({ text });
}}
>
Send
</button>
</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>
</div> */}
</div>
)
}
);
};
+14 -2
View File
@@ -1,3 +1,15 @@
import { Link } from "react-router-dom";
import { authors } from "../data/authors";
export const HomePage = () => {
return <div>Welcome to Picobook!</div>
}
return (
<div>
<h2>Welcome to Picobook!</h2>
{authors.map((author) => (
<Link key={author.username} to={`/u/${author.username}`}>
<h3>{author.username}</h3>
</Link>
))}
</div>
);
};
+4 -4
View File
@@ -1,11 +1,11 @@
import { css } from "@emotion/css";
import { Pico8Console } from "./pico8-client/Pico8Console";
import testcarts from "./testcarts";
// import { css } from "@emotion/css";
// import { Pico8Console } from "./pico8-client/Pico8Console";
// import testcarts from "./testcarts";
import { Routing } from "./routing";
const App = (props: {}) => {
return (
<Routing/>
<Routing />
// <div className={css`
// min-height: 100vh;
// `}>
+40 -34
View File
@@ -1,46 +1,52 @@
import { useCallback, useEffect, useRef, useState } from "react";
export const useWebsocket = (props: {url: string; onMessage: (stuff: {socket: WebSocket; message: unknown}) => void}) => {
const {url, onMessage} = props;
const onMessageRef = useRef(onMessage);
const ws = useRef<WebSocket | null>(null);
onMessageRef.current = onMessage;
export const useWebsocket = (props: {
url: string;
onMessage: (stuff: { socket: WebSocket; message: unknown }) => void;
}) => {
const { url, onMessage } = props;
const onMessageRef = useRef(onMessage);
const ws = useRef<WebSocket | null>(null);
onMessageRef.current = onMessage;
useEffect(() => {
const webSocket = new WebSocket(url);
useEffect(() => {
const webSocket = new WebSocket(url);
webSocket.addEventListener("open", () => {
console.log("WebSocket is open now.");
});
webSocket.addEventListener("open", () => {
console.log("WebSocket is open now.");
});
webSocket.addEventListener("message", (event: any) => {
onMessageRef.current({socket: webSocket, message: JSON.parse(event.data)});
});
webSocket.addEventListener("message", (event: any) => {
onMessageRef.current({
socket: webSocket,
message: JSON.parse(event.data),
});
});
webSocket.addEventListener("error", (event) => {
console.log("WebSocket error: ", event);
});
console.log("WebSocket error: ", event);
});
webSocket.addEventListener("close", () => {
console.log("WebSocket is closed now.");
ws.current = null;
});
webSocket.addEventListener("close", () => {
console.log("WebSocket is closed now.");
ws.current = null;
});
ws.current = webSocket;
ws.current = webSocket;
return () => {
webSocket.close();
};
return () => {
webSocket.close();
};
}, [url]);
}, [url]);
const sendMessage = useCallback((message: unknown) => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(message));
// console.log("sending", message);
} else {
console.error("WebSocket is not open. Message not sent.");
}
}, []);
const sendMessage = useCallback((message: unknown) => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(message));
} else {
console.error("WebSocket is not open. Message not sent.");
}
}, []);
return {sendMessage};
};
return { sendMessage };
};
-100
View File
@@ -1,100 +0,0 @@
import { css } from "@emotion/css";
import { PicoCart, PicoPlayerHandle, makePicoConsole } from "./renderCart";
import { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
export type Pico8ConsoleImperatives = {
getPicoConsoleHandle(): PicoPlayerHandle | null;
}
export type Pico8ConsoleProps = {
carts: PicoCart[],
// onGpioChange?(gpio: number[]): void,
}
// const noop = () => {};
export const Pico8Console = forwardRef((props: Pico8ConsoleProps, forwardedRef: ForwardedRef<Pico8ConsoleImperatives>) => {
const {
carts,
// onGpioChange = noop
} = props;
const [playing, setPlaying] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [handle, setHandle] = useState<PicoPlayerHandle | null>(null);
const attachConsole = useCallback(async () => {
const picoConsole = await makePicoConsole({
carts,
});
picoConsole.canvas.tabIndex=0;
if (ref.current) {
ref.current.appendChild(picoConsole.canvas);
// Set the width and height because pico8 adds them as properties on chrome-based browsers
picoConsole.canvas.style.width = "";
picoConsole.canvas.style.height = "";
picoConsole.canvas.focus();
}
setHandle(picoConsole);
// picoConsole.gpio.subscribe(onGpioChange);
picoConsole.canvas.addEventListener('keydown',(event) => {
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)) {
event.preventDefault();
}
}, {passive: false});
picoConsole.canvas.addEventListener('click', () => {
picoConsole.canvas.focus();
})
}, [carts]);
useImperativeHandle(forwardedRef, () => ({
getPicoConsoleHandle() {
return handle;
}
}), [handle]);
useEffect(() => {
if (playing) {
attachConsole();
return () => {
if (ref.current) {
ref.current.innerHTML = "";
}
}
}
}, [playing, attachConsole]);
if (!playing) {
return (
<div
ref={ref}
className={css`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
background-color: black;
color: white;
cursor: pointer;
`}
tabIndex={0}
onClick={() => {setPlaying(true)}}
>
Play!
</div>
)
}
return (
<div ref={ref} className={css`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
& > canvas {
width: 100%;
height: 100%;
}
`}></div>
);
});
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
export {makePicoConsole} from "./renderCart";
export {pngToRom} from "./pngToRom";
-57
View File
@@ -1,57 +0,0 @@
// TODO: something is broken for the new mygame.p8.png
const imageDataToRom = (imageData: ImageData) => {
const width = imageData.width;
const height = imageData.height;
const data = []; // For raw cart data
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
const r = imageData.data[index];
const g = imageData.data[index + 1];
const b = imageData.data[index + 2];
const a = imageData.data[index + 3];
// Extracting and encoding the data from pixel components
const byte = ((b & 3) << 0) | ((g & 3) << 2) | ((r & 3) << 4) | ((a & 3) << 6);
data.push(byte);
}
}
// At this point, `data` contains the raw bytes extracted from the image
// Here, you would decode this data into a format representing the cart
// This could involve uncompressing code, reading sprite data, etc.
// The specifics depend on your cart format and how data was originally encoded
// Returning raw data for demonstration; you'll need to adapt this part
return data.slice(0, 32768);
// return data.slice(0, 32768);
}
const pngGetImageData = (src: string) => {
return new Promise<ImageData>((resolve, reject) => {
try {
const img = document.createElement('img');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
resolve(imageData);
};
img.onerror = () => {
reject(Error("BAD IMAGE"));
}
img.src = src;
} catch (err) {
reject(err);
}
})
}
export const pngToRom = async (src: string) => {
return imageDataToRom(await pngGetImageData(src));
}
-30
View File
@@ -1,30 +0,0 @@
// @ts-ignore
import "./build/veryRawRenderCart.js";
export type PicoBool = 0 | 1;
export type RenderCart = (Module: {canvas: HTMLCanvasElement, codo_textarea?: HTMLTextAreaElement}, cartNames: string[], cartDatas: number[][], audioContext: AudioContext) => {
p8_touch_detected?: PicoBool;
p8_dropped_cart?: string;
p8_dropped_cart_name?: string;
pico8_state?: Partial<{
frame_number: number;
has_focus: PicoBool;
is_paused: PicoBool;
request_pointer_lock: PicoBool;
require_page_navigate_confirmation: PicoBool;
show_dpad: PicoBool;
shutdown_requested: PicoBool;
sound_volume: number;
}>;
pico8_buttons?: [number, number, number, number, number, number, number, number];
pico8_gamepads?: {count: number};
pico8_gpio?: number[]; // should be 128 length
pico8_audio_context?: AudioContext;
pico8_mouse?: [number, number, number];
codo_command?: number;
}
const typedRenderCart = (window as any).P8 as RenderCart;
export {typedRenderCart as renderCart}
-202
View File
@@ -1,202 +0,0 @@
import { assertNever } from "@firebox/tsutil";
import { pngToRom } from "./pngToRom";
import { RenderCart, renderCart as rawRenderCart } from "./rawRenderCart";
export type PicoCart = {
name: string;
src: string;
} | {
name: string;
rom: number[];
}
type PlayerButtons = {
left: boolean;
right: boolean;
up: boolean;
down: boolean;
o: boolean;
x: boolean;
menu: boolean;
}
export type PicoPlayerHandle = {
raw: ReturnType<RenderCart>;
rawModule: unknown;
// 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;
gpio: (
number[]
// & {subscribe: (f: (gpio: number[]) => void) => void}
); // read + write (should be 256-tuple)
// state
readonly state: {
frameNumber: number;
isPaused: boolean;
hasFocus: boolean;
requestPointerLock: boolean;
requirePageNavigateConfirmation: boolean;
showDpad: boolean;
shutdownRequested: boolean;
soundVolume: number;
};
// misc?
setTouchDetected: (touchDetected: boolean) => void;
dropCart: (cart: PicoCart) => void;
// Module
toggleSound: () => void;
toggleControlMenu: () => void;
togglePaused: () => void;
// TODO: rename these two better (what do they do??)
modDragOver: () => void;
modDragStop: () => void;
}
const bitfield = (...args: boolean[]): number => {
if (!args.length) {
return 0;
}
return (args[0]?1:0)+2*bitfield(...args.slice(1));
}
const getRom = async (cart: PicoCart) => {
if ("src" in cart) {
return await pngToRom(cart.src);
} else if ("rom" in cart) {
return cart.rom;
}
assertNever(cart);
}
export const makePicoConsole = async (props: {
canvas?: HTMLCanvasElement;
codoTextarea?: HTMLTextAreaElement;
audioContext?: AudioContext;
carts: PicoCart[];
}): Promise<PicoPlayerHandle> => {
const {carts, canvas = document.createElement("canvas"), codoTextarea = document.createElement("textarea"), audioContext = new AudioContext()} = props;
canvas.style.imageRendering = "pixelated";
codoTextarea.style.display="none";
codoTextarea.style.position="fixed";
codoTextarea.style.left="-9999px";
codoTextarea.style.height="0px";
codoTextarea.style.overflow="hidden";
const Module = {canvas, keyboardListeningElement: canvas};
const cartsDatas = await Promise.all(carts.map(cart => getRom(cart)));
const handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext);
handle.pico8_state = {};
handle.pico8_buttons = [0,0,0,0,0,0,0,0];
handle.pico8_mouse = [0,0,0];
handle.pico8_gpio = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
];
// 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;
// }
handle.pico8_gamepads = {count: 0};
return {
raw: handle,
rawModule: Module,
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!,
},
gpio: handle.pico8_gpio as PicoPlayerHandle["gpio"],
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
// handle.p8_dropped_cart = cart.src;
// handle.codo_command = 9;
},
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,
}
}
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
export const authors = [
{
username: "dylanpizzo",
repo: "pico-carts",
auth: {
pat: "f4a1ab7c30ecb6b68da44f5b3f231ab6+719a6bf4edff6d809d1eb6d3ba3c9eb5:a12f955e441c684193053075690f2aba161894c26ad62d6ef8e5fb71b1969cbe755333e1dc7fc90e339ccba2fae27bd82e2ebd1bb233956e09c1e0bf1b951d69a279b4731ca26f8315514524ba93c2228b02456081624db71c14fd4ba0",
},
},
{
username: "loupizzo",
repo: "pico-carts",
auth: {
pat: "4827f847fdc737e5480748b4d5269bae+be1b031d650bfad15bbfef146d736c5e:3ac3c214ae4236bf89a317ba6b731b781e6e8dfa22c304adfe1ba6328cc497bc24a97f3389ce1a439c6956f2dfea900f4fba559ebe8ecb91b573bc897656c08d7f60372b69f3ee7310ae70eb75ee7bff9ce35e7c3c1b088e86e3e458a7",
},
},
];
-28
View File
@@ -1,28 +0,0 @@
import createConnectionPool, {ConnectionPool, ConnectionPoolConfig, sql} from '@databases/pg';
export {sql};
const portString = process.env["DB_PORT"];
const portNumber = portString ? parseInt(portString) : undefined;
const clientConfig: ConnectionPoolConfig = {
host: process.env["DB_HOST"],
user: process.env["DB_USER"],
database: process.env["DB_NAME"],
password: process.env["DB_PASSWORD"],
port: portNumber,
};
// @ts-ignore
const db: ConnectionPool = createConnectionPool({
connectionString: false,
...clientConfig
});
process.once('SIGTERM', () => {
db.dispose().catch((ex) => {
console.error(ex);
});
});
export {db};
@@ -1,5 +0,0 @@
CREATE TABLE users (
id text,
name text,
password text
)
@@ -1,6 +0,0 @@
CREATE TABLE repos (
id text,
repo_fullname text, -- e.g. "username/reponame"
repo_hosttype text, -- "github", "gitea", "gitlab", ...
user_id text
)
@@ -1,7 +0,0 @@
CREATE TABLE releases (
id text,
repo_id text,
release_number integer,
cart_png_base64 text,
created_at time
)
@@ -1,14 +0,0 @@
DROP TABLE repos;
DROP TABLE releases;
CREATE TABLE releases (
id text,
repo text,
author text,
slug text,
version text,
carts json,
manifest json,
created_at time
);
@@ -1,2 +0,0 @@
ALTER TABLE releases DROP COLUMN created_at;
ALTER TABLE releases ADD created_at timestamp;
+13 -6
View File
@@ -1,11 +1,18 @@
import type { RouteList } from "./routelist.ts"
// import type { RouteList } from "./routelist.ts";
type RouteUrl = RouteList[number]["url"];
// type RouteUrl = RouteList[number]["url"];
type HttpMethod = RouteList[number]["method"];
// type HttpMethod = RouteList[number]["method"];
type Route<M extends HttpMethod, U extends RouteUrl> = Extract<RouteList[number], {url: U, method: M}>;
// type Route<M extends HttpMethod, U extends RouteUrl> = Extract<
// RouteList[number],
// { url: U; method: M }
// >;
export type RoutePayload<M extends HttpMethod, U extends RouteUrl> = Parameters<Route<M, U>["handler"]>[0]["payload"];
// export type RoutePayload<M extends HttpMethod, U extends RouteUrl> = Parameters<
// Route<M, U>["handler"]
// >[0]["payload"];
export type RouteResponse<M extends HttpMethod, U extends RouteUrl> = Awaited<ReturnType<Route<M, U>["handler"]>>;
// export type RouteResponse<M extends HttpMethod, U extends RouteUrl> = Awaited<
// ReturnType<Route<M, U>["handler"]>
// >;
+40 -10
View File
@@ -1,29 +1,59 @@
import { Type } from "@sinclair/typebox";
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
import { getAuthorGames, getReleases } from "../dbal/dbal.ts";
import { authors } from "../../data/authors.ts";
import { Octokit } from "octokit";
import { decrypt } from "../util/crypt.ts";
const method = "GET";
const url = "/api/author";
const payloadT = Type.Any();
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => {
const {author} = payload;
const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
const { author } = payload;
if (typeof author !== "string") {
const authorData = authors.find((x) => x.username === author);
if (!authorData) {
return {
author: null,
releases: [],
games: [],
};
}
console.log("author", author);
console.log("author", authorData);
const pat = decrypt({
password: process.env.ENCRYPTION_PASSWORD!,
cyphertext: authorData.auth.pat,
});
const octokit = new Octokit({
auth: pat,
});
const gameStuff = await octokit.rest.repos.getContent({
owner: authorData.username,
repo: authorData.repo,
path: ".picobook",
});
// TODO: get the games.
const games: string[] = [];
if (Array.isArray(gameStuff.data)) {
games.push(
...gameStuff.data
.map((x) => x.name)
.filter((x) => x.endsWith(".p8.png"))
.map((x) => x.slice(0, -".p8.png".length)),
);
console.log("hi");
}
const games = await getAuthorGames({author});
return {
author,
games,
}
};
};
export default {
@@ -31,4 +61,4 @@ export default {
url,
payloadT,
handler,
} as const satisfies FirRouteOptions<typeof payloadT>;
} as const satisfies FirRouteOptions<typeof payloadT>;
+58
View File
@@ -0,0 +1,58 @@
import { Type } from "@sinclair/typebox";
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
import { authors } from "../../data/authors.ts";
import { Octokit } from "octokit";
import { decrypt } from "../util/crypt.ts";
const method = "GET";
const url = "/api/game";
const payloadT = Type.Any();
const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
const { author, name } = payload;
const authorData = authors.find((x) => x.username === author);
if (!authorData) {
return null;
}
console.log("author", authorData);
const pat = decrypt({
password: process.env.ENCRYPTION_PASSWORD!,
cyphertext: authorData.auth.pat,
});
const octokit = new Octokit({
auth: pat,
});
const gameData = (
await octokit.rest.repos.getContent({
owner: authorData.username,
repo: authorData.repo,
path: `.picobook/${name}.p8.png`,
})
).data;
console.log(gameData);
if (Array.isArray(gameData)) {
return null;
}
if (gameData.type !== "file") {
return null;
}
return {
carts: [{ name, src: `data:image/png;base64,${gameData.content}` }],
};
};
export default {
method,
url,
payloadT,
handler,
} as const satisfies FirRouteOptions<typeof payloadT>;
-42
View File
@@ -1,42 +0,0 @@
import { Type } from "@sinclair/typebox";
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
import { getRelease, getReleases } from "../dbal/dbal.ts";
const method = "GET";
const url = "/api/release";
const payloadT = Type.Any();
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => {
const {author, slug, version} = payload;
if (typeof author !== "string") {
return {
release: null,
versions: [],
};
}
if (typeof slug !== "string") {
return {
release: null,
versions: [],
};
}
const release = await getRelease({author, slug, version});
const releases = await getReleases({author, slug});
const versions = releases.map(r => r.version);
return {
release,
versions,
}
};
export default {
method,
url,
payloadT,
handler,
} as const satisfies FirRouteOptions<typeof payloadT>;
-59
View File
@@ -1,59 +0,0 @@
import { Type } from "@sinclair/typebox";
import { FirRouteInput, FirRouteOptions } from "../util/routewrap";
import {git} from "../util/git.ts";
import { randomUUID } from "crypto";
import path from "path";
import {fileURLToPath} from 'url';
import { getCarts } from "../util/carts.ts";
import { getRelease, insertRelease } from "../dbal/dbal.ts";
import { ManifestType } from "../types.ts";
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const reposPath = path.resolve(__dirname, "..", "..", "..", "repos");
const method = "POST";
const url = "/api/release";
const payloadT = Type.Any();
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => {
const {manifest, token} = payload;
if (!ManifestType.Check(manifest)) {
return false;
}
const release = await getRelease({author: manifest.author, slug: manifest.id, version: manifest.version});
if (release) {
return false;
}
const uuid = randomUUID();
const repoPath = path.join(reposPath, uuid);
await git.clone({
from: manifest.repo,
to: repoPath,
auth: token,
});
const carts = await getCarts(repoPath, manifest.carts);
await insertRelease({
manifest,
carts,
});
console.log({
manifest,
carts,
});
return true;
};
export default {
method,
url,
payloadT,
handler,
} as const satisfies FirRouteOptions<typeof payloadT>;
-122
View File
@@ -1,122 +0,0 @@
// Database Access Layer stuff goes here
import { v4 as uuidv4 } from 'uuid';
import { db, sql } from "../../database/db";
import { PicobookManifest } from '../types';
export type DbRelease = {
id: string;
slug: string;
repo: string;
version: string;
carts: {name: string; rom: number[]}[];
author: string;
manifest: PicobookManifest;
}
export type DbReleaseInternal = {
id: string;
slug: string;
repo: string;
version: string;
carts: {carts: {name: string; rom: number[]}[]};
author: string;
manifest: PicobookManifest;
}
const compareVersions = (a: string, b: string) => {
const [a1, a2] = a.split(".").map(x => Number(x));
const [b1, b2] = b.split(".").map(x => Number(x));
if (a1 !== b1) {
return a1 - b1;
} else {
return a2 - b2;
}
}
const compareByVersion = (a: DbRelease, b: DbRelease) => compareVersions(a.version, b.version);
export const getReleases = async (where: {
author: string;
slug?: string;
version?: string;
}): Promise<DbRelease[]> => {
const {author, slug, version} = where;
let rows: DbReleaseInternal[];
if (!slug) {
rows = await db.query(sql`
SELECT * from releases
WHERE
author = ${author}
`);
} else if (!version) {
rows = await db.query(sql`
SELECT * from releases
WHERE
slug = ${slug} AND
author = ${author}
`);
} else {
rows = await db.query(sql`
SELECT * from releases
WHERE
slug = ${slug} AND
author = ${author} AND
version = ${version}
`);
}
return rows.map(row => ({...row, carts: row.carts.carts}));
}
export const getRelease = async (where: {
author: string;
slug: string;
version?: string;
}) => {
const {version} = where;
const releases = await getReleases(where);
if (version) {
if (releases.length === 1) {
return releases[0];
} else {
return null;
}
} else {
if (releases.length < 1) {
return null;
}
releases.sort(compareByVersion);
return releases[releases.length-1];
}
}
export const getAuthorGames = async (where: {
author: string;
}) => {
const releases = await getReleases(where);
const games = releases.reduce((accum, curr) => {
const found = accum.find(r => r.slug === curr.slug);
if (found) {
found.releases.push(curr);
} else {
accum.push({slug: curr.slug, releases: [curr]});
}
return accum;
}, [] as {slug: string; releases: DbRelease[]}[]);
games.forEach(game => {
game.releases.sort(compareByVersion).reverse();
});
return games;
}
export const insertRelease = async (props: {manifest: PicobookManifest, carts: {name: string; rom: number[]}[]}) => {
const {manifest, carts} = props;
const {id: slug, author, repo, version} = manifest;
const id = uuidv4();
const now = new Date();
await db.query(sql`
INSERT INTO releases (id, slug, repo, version, author, carts, manifest, created_at)
VALUES (${id}, ${slug}, ${repo}, ${version}, ${author}, ${{carts}}, ${manifest}, ${now})
`);
return id;
}
+15 -21
View File
@@ -1,37 +1,28 @@
// Import the framework and instantiate it
import Fastify from 'fastify'
import fastifyStatic from '@fastify/static'
import {fastifyWebsocket} from '@fastify/websocket';
import Fastify from "fastify";
import fastifyStatic from "@fastify/static";
import { fastifyWebsocket } from "@fastify/websocket";
import { routeList } from "./routelist.ts";
import { attachRoute } from "./util/routewrap.ts";
import { git } from './util/git.ts';
import path from "path";
import {fileURLToPath} from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
await git.clone({
from: "https://github.com/thisismypassport/shrinko8",
to: path.resolve(__dirname, "shrinko8"),
});
const server = Fastify({
logger: true
logger: true,
});
server.register(fastifyWebsocket);
server.register(fastifyStatic, {
root: new URL('public', import.meta.url).toString().slice("file://".length),
prefix: '/',
root: new URL("public", import.meta.url).toString().slice("file://".length),
prefix: "/",
});
routeList.forEach(firRoute => {
routeList.forEach((firRoute) => {
attachRoute(server, firRoute);
});
server.setNotFoundHandler((req, res) => {
if (!req.url.startsWith("/api")) {
res.sendFile('index.html');
res.sendFile("index.html");
}
});
@@ -40,8 +31,11 @@ try {
// Note: host needs to be 0.0.0.0 rather than omitted or localhost, otherwise
// it always returns an empty reply when used inside docker...
// See: https://github.com/fastify/fastify/issues/935
await server.listen({ port: parseInt(process.env["PORT"] ?? "3000"), host: "0.0.0.0" })
await server.listen({
port: parseInt(process.env["PORT"] ?? "3000"),
host: "0.0.0.0",
});
} catch (err) {
server.log.error(err)
process.exit(1)
}
server.log.error(err);
process.exit(1);
}
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

-57
View File
@@ -1,57 +0,0 @@
<html>
<head>
<title>PICO-8 Cartridge</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
.cart-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.cart-container > canvas {
width: 512px;
max-width: calc(100% - 2px);
border: 1px solid white;
box-sizing: border-box;
}
</style>
</head>
<body style="padding:0px; margin:0px; background-color:#222; color:#ccc">
<div id="container" class="cart-container"></div>
<button id="start-button">Click</button>
<script type="module">
import {makePicoConsole, pngToRom} from "./dist/index.js";
import mygame from "./mygamefull.js";
// const arrayToHex = (a) => {
// const h = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];
// return a.map(n => h[Math.floor(n/16)]+h[n%16]).join("");
// };
const hexToArray = (hex) => {
const h = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];
const a = [];
for (let i = 0; i < hex.length; i+=2) {
a.push(16*h.indexOf(hex[i])+h.indexOf(hex[i+1]));
}
return a;
};
mygame.carts[0] = ({name: mygame.carts[0].name, rom: hexToArray(mygame.carts[0].hex)});
// const mainRom = mygame.carts[0].rom;
// const pngRom = await pngToRom("mygame.p8.png");
// pngRom.forEach((v,i) => {
// if (v !== mainRom[i]) {
// console.log(i, Math.floor(i/160), i%160, v, mainRom[i]);
// }
// })
// console.log(arrayToHex(await pngToRom("secondcart.p8.png")));
async function start() {
const console1 = await makePicoConsole({carts: mygame.carts});
console.log(console1);
document.getElementById("container").appendChild(console1.canvas);
}
document.getElementById("start-button").addEventListener("click", start);
</script>
</body>
</html>
+3 -11
View File
@@ -1,17 +1,9 @@
import echo from "./api/echo.ts";
import getAuthor from "./api/getAuthor.ts";
import getRelease from "./api/getRelease.ts";
import release from "./api/release.ts";
import getGame from "./api/getGame.ts";
import room from "./api/room.ts";
import webhook from "./api/webhook.ts";
export const routeList = [
echo,
webhook,
release,
getRelease,
getAuthor,
room,
];
export const routeList = [echo, webhook, getAuthor, room, getGame];
export type RouteList = typeof routeList;
export type RouteList = typeof routeList;
-44
View File
@@ -1,44 +0,0 @@
import { randomUUID } from "crypto";
import { shrinko8 } from "./shrinko8";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const outputDirPath = path.resolve(__dirname, "..", "..", "..", "output");
const getRom = async (inputFile: string) => {
const uuid = randomUUID();
const dir = path.join(outputDirPath, uuid);
fs.mkdirSync(dir, {recursive: true});
const outputFile = path.join(dir, "output.js");
await shrinko8({
inputFile,
outputFile,
});
const js = await fs.promises.readFile(outputFile, "utf8");
await fs.promises.rm(dir, {recursive: true, force: true});
const match = js.match(/\b_cartdat\s*=\s*(\[.*?\])/s);
if (!match) {
console.log("BEGIN js contents --------------------------------------------");
console.log(js);
console.log("END js contents ----------------------------------------------");
throw Error("Could not properly parse js file to find _cartdat");
}
return JSON.parse(match[1]) as number[]
}
const getCart = async (baseDir: string, inputFile: string) => {
return {
name: inputFile,
rom: await getRom(path.join(baseDir, inputFile)),
}
}
export const getCarts = async (baseDir: string, inputFiles: string[]) => {
return await Promise.all(inputFiles.map(inputFile => getCart(baseDir, inputFile)));
}
+51
View File
@@ -0,0 +1,51 @@
import crypto from "node:crypto";
// Key derivation from password
const algorithm = "aes-256-gcm";
// TODO: make salt vary by use case;
const salt = "saltysalt";
export const encrypt = (props: { password: string; text: string }) => {
const { password, text } = props;
const key = new Uint8Array(crypto.scryptSync(password, salt, 32)); // 32 bytes key
const iv = new Uint8Array(crypto.randomBytes(16)); // Initialization Vector
// Encryption
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag().toString("hex");
const fullEncrypted =
authTag + "+" + Buffer.from(iv).toString("hex") + ":" + encrypted; // Store IV with encrypted data
// console.log({ iv, authTag });
return fullEncrypted;
};
export const decrypt = (props: { password: string; cyphertext: string }) => {
const { password, cyphertext } = props;
const key = new Uint8Array(crypto.scryptSync(password, salt, 32)); // 32 bytes key
// Decryption
const [pre, encryptedHex] = cyphertext.split(":");
const [authTag, ivHex] = pre.split("+");
const ivFromStorage = new Uint8Array(Buffer.from(ivHex, "hex"));
// console.log({ ivFromStorage, authTag });
const decipher = crypto.createDecipheriv(algorithm, key, ivFromStorage);
decipher.setAuthTag(new Uint8Array(Buffer.from(authTag, "hex")));
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
};
// console.log(process.env.ENCRYPTION_PASSWORD);
// const x = encrypt({
// password: process.env.ENCRYPTION_PASSWORD!,
// text: "",
// });
// const y = decrypt({
// password: process.env.ENCRYPTION_PASSWORD!,
// cyphertext: "",
// });
// console.log(x);
// console.log(y);
-27
View File
@@ -1,27 +0,0 @@
import fs from "fs";
import isogit from "isomorphic-git";
import http from "isomorphic-git/http/node";
const clone = async (options: {
from: string;
to: string;
auth?: string;
}) => {
fs.mkdirSync(options.to, {recursive: true});
await isogit.clone({
fs,
http,
onAuth() {
return {
username: 'x-access-token',
password: options.auth,
}
},
dir: options.to,
url: options.from,
});
}
export const git = {
clone,
}
-38
View File
@@ -1,38 +0,0 @@
import fs from "fs";
import path from "path";
import {fileURLToPath} from 'url';
import {execFile} from "child_process";
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const picoDirPath = path.resolve(__dirname, "..", "..", "..", "pico8");
const picoBinPath = path.resolve(picoDirPath, "pico8_dyn");
const cmd = (cmd: string, args: string[], options = {}) => {
return new Promise((resolve, reject) => {
execFile(cmd, args, options, (error, stdout, stderr) => {
if (error) {
reject({error, stderr});
} else {
resolve({stdout});
}
})
});
}
const execPico = async (args: string[]) => {
return await cmd(picoBinPath, args);
}
export const pico8 = {
async export(filesIn: string[], fileOut: string) {
try {
// console.log((await cmd("ls", ["-la", "/app/pico8"]) as any).stdout)
return await execPico([...filesIn, "-export", fileOut]);
} catch (err) {
console.log("CAUGHT ERROR", err);
}
}
}
// const result = await pico8.export(["/home/dylan/.lexaloffle/pico-8/carts/my-pico-project/mygame.p8","/home/dylan/.lexaloffle/pico-8/carts/my-pico-project/secondcart.p8"], "/home/dylan/repos/picobook/sample2.js");
// console.log(result);
-19
View File
@@ -1,19 +0,0 @@
import { execa } from "execa";
import path from "path";
import {fileURLToPath} from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const shrinko8DirPath = path.resolve(__dirname, "../shrinko8");
const shrinko8Path = path.resolve(shrinko8DirPath, "shrinko8.py");
const pico8DatPath = path.resolve(__dirname, "../../../data/pico8.dat");
export const shrinko8 = async (props: {
inputFile: string;
outputFile: string;
options?: string[];
}) => {
const {inputFile, outputFile, options = []} = props;
return await execa("python3", [shrinko8Path, inputFile, outputFile, "--pico8-dat", pico8DatPath, ...options])
}
+5 -5
View File
@@ -3,16 +3,16 @@
"target": "es2017",
"module": "es2022",
"lib": ["DOM"],
"moduleResolution": "node",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"noEmit": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"strict": true
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node",
}
}
"experimentalSpecifierResolution": "node"
}
}