new format wip

This commit is contained in:
Dylan Pizzo
2026-06-10 21:47:30 -04:00
parent 07595c31ef
commit 876404d8f5
45 changed files with 10893 additions and 144129 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 # 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 # sets the working directory for any RUN, CMD, COPY command
WORKDIR /app WORKDIR /app
# Install Python and pip # # Install Python and pip
RUN apk add --update python3 py3-pip # RUN apk add --update python3 py3-pip
RUN python3 -m ensurepip # RUN python3 -m ensurepip
RUN pip3 install --no-cache --upgrade pip setuptools # RUN pip3 install --no-cache --upgrade pip setuptools
COPY ./data ./data # COPY ./data ./data
# Copies stuff to cache for install # Copies stuff to cache for install
COPY ./package.json ./package-lock.json tsconfig.json ./ COPY ./package.json ./package-lock.json tsconfig.json ./
BIN
View File
Binary file not shown.
+14 -31
View File
@@ -1,33 +1,16 @@
version: '3.9' version: "3.9"
services: services:
app: app:
container_name: picobook-app container_name: picobook-app
image: node image: node
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: base target: base
env_file: env_file: .env
.env ports:
ports: - ${PORT}:${PORT}
- ${PORT}:${PORT} profiles: ["prod"]
depends_on:
- db
profiles: ["prod"]
db: volumes:
container_name: picobook-postgres data: {}
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: {}
+9268 -8217
View File
File diff suppressed because it is too large Load Diff
+62 -60
View File
@@ -1,62 +1,64 @@
{ {
"name": "firstack", "name": "picobook",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"description": "Firstack is a template repo for a tech stack.", "description": "Picobook.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"makeuser": "npm run withenv ./scripts/makeuser.ts", "makeuser": "npm run withenv ./scripts/makeuser.ts",
"dev-docker": "docker compose --profile dev up -d", "dev-docker": "docker compose --profile dev up -d",
"dev-server": "echo \"starting server\" && npm run withenv ./src/server/index.ts", "dev-server": "echo \"starting server\" && npm run withenv ./src/server/index.ts",
"dev-watch-client": "npm run withenv ./scripts/watch.ts", "dev-watch-client": "npm run withenv ./scripts/watch.ts",
"dev-migrate": "source ./.env && pg-migrations apply --directory ./src/database/migrations", "dev-migrate": "source ./.env && pg-migrations apply --directory ./src/database/migrations",
"prod-migrate": "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-build-client": "npm run withenv ./scripts/build.ts",
"prod-docker": "docker compose --profile prod up -d", "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", "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", "withenv": "tsx ./scripts/run-with-env.ts",
"test": "echo \"Error: no test specified\" && exit 1", "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"
"add-pico": "npm run withenv ./scripts/do-release.ts" },
}, "repository": {
"repository": { "type": "git",
"type": "git", "url": "https://git.playbox.link/dylan/firstack.git"
"url": "https://git.playbox.link/dylan/firstack.git" },
}, "author": "",
"author": "", "license": "UNLICENSED",
"license": "UNLICENSED", "dependencies": {
"dependencies": { "@athingperday/react-pico-player": "^0.1.1",
"@databases/pg": "^5.4.1", "@databases/pg": "^5.4.1",
"@fastify/cookie": "^9.0.4", "@fastify/cookie": "^9.0.4",
"@fastify/secure-session": "^7.1.0", "@fastify/secure-session": "^7.1.0",
"@fastify/static": "^6.10.2", "@fastify/static": "^7.0.4",
"@fastify/websocket": "^10.0.1", "@fastify/websocket": "^10.0.1",
"@firebox/components": "^0.1.5", "@firebox/components": "^0.1.5",
"@firebox/tsutil": "^0.1.2", "@firebox/tsutil": "^0.1.2",
"@sinclair/typebox": "^0.31.5", "@octokit/core": "^7.0.6",
"dotenv": "^16.4.5", "@sinclair/typebox": "^0.31.5",
"execa": "^8.0.1", "dotenv": "^16.4.5",
"fastify": "^4.26.2", "execa": "^8.0.1",
"isomorphic-git": "^1.25.6", "fastify": "^4.26.2",
"react-pico-8": "^4.1.0", "isomorphic-git": "^1.25.6",
"react-router-dom": "^6.18.0", "octokit": "^5.0.5",
"uuid": "^9.0.1" "react-pico-8": "^4.1.0",
}, "react-router-dom": "^6.18.0",
"devDependencies": { "uuid": "^9.0.1"
"@databases/pg-migrations": "^5.0.2", },
"@emotion/css": "^11.11.2", "devDependencies": {
"@emotion/react": "^11.11.1", "@databases/pg-migrations": "^5.0.2",
"@types/node": "^20.11.30", "@emotion/css": "^11.11.2",
"@types/react": "^18.2.21", "@emotion/react": "^11.11.1",
"@types/react-dom": "^18.2.7", "@types/node": "^20.11.30",
"@types/uuid": "^9.0.7", "@types/react": "^18.2.21",
"@types/ws": "^8.5.10", "@types/react-dom": "^18.2.7",
"esbuild": "^0.19.2", "@types/uuid": "^9.0.7",
"nodemon": "^3.0.1", "@types/ws": "^8.5.10",
"react": "^18.2.0", "esbuild": "^0.19.2",
"react-dom": "^18.2.0", "nodemon": "^3.0.1",
"react-icons": "^4.10.1", "react": "^18.2.0",
"tsx": "^4.7.1", "react-dom": "^18.2.0",
"typescript": "^5.2.2" "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 { useEffect, useState } from "react";
import { DbRelease } from "../server/dbal/dbal";
import { css } from "@emotion/css"; import { css } from "@emotion/css";
// import { type Pico8Player } from "@athingperday/react-pico-player";
type Info = { type Info = {
author: string | null; author: string | null;
games: {slug: string; releases: DbRelease[]}[]; games: string[];
} };
export const AuthorPage = () => { export const AuthorPage = () => {
const {author} = useParams(); const { author } = useParams();
const [info, setInfo] = useState<Info | null>(null); const [info, setInfo] = useState<Info | null>(null);
useEffect(() => { useEffect(() => {
@@ -17,46 +17,38 @@ export const AuthorPage = () => {
let url = `/api/author?author=${author}`; let url = `/api/author?author=${author}`;
const information = await fetch(url); const information = await fetch(url);
const json = await information.json(); const json = await information.json();
console.log('json', json); console.log("json", json);
setInfo(json); setInfo(json);
} };
fetchInfo(); fetchInfo();
}, [setInfo, author]); }, [setInfo, author]);
if (!info) { if (!info) {
return ( return <div>LOADING...</div>;
<div>
LOADING...
</div>
)
} }
if (!info.author) { if (!info.author) {
return ( return <div>NOT FOUND</div>;
<div>
NOT FOUND
</div>
)
} }
return ( return (
<div className={css` <div
margin: auto; className={css`
width: max-content; margin: auto;
max-inline-size: 66ch; width: max-content;
padding: 1.5em; max-inline-size: 66ch;
display: flex; padding: 1.5em;
flex-direction: column; display: flex;
gap: 1em; flex-direction: column;
`}> gap: 1em;
`}
>
<h1>{author}</h1> <h1>{author}</h1>
{ {info.games.map((game) => (
info.games.map(game => ( <Link key={game} to={`/u/${author}/${game}`}>
<Link key={game.slug} to={`/u/${author}/${game.slug}`}> <h3>{game}</h3>
<h3>{game.releases[0].manifest.title ?? game.slug}</h3> </Link>
</Link> ))}
))
}
</div> </div>
) );
} };
+53 -72
View File
@@ -1,20 +1,18 @@
import { Link, useParams, useSearchParams } from "react-router-dom" import { Link, useParams, useSearchParams } from "react-router-dom";
import { Pico8Console, Pico8ConsoleImperatives } from "./pico8-client/Pico8Console";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
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 { Pico8Player } from "@athingperday/react-pico-player";
type Info = { type Game = {
release: DbRelease | null; carts: Parameters<typeof Pico8Player>["0"]["carts"];
versions: string[]; };
}
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",
@@ -36,68 +34,63 @@ export const GamePage = () => {
// } // }
// }) // })
// const version = searchParams.get('v'); // const version = searchParams.get('v');
const [v, setVersion] = useState<string | null>(null); const [game, setGame] = useState<Game | null>(null);
const [info, setInfo] = useState<Info | null>(null);
const version = v ?? info?.release?.version ?? info?.versions[0];
useEffect(() => { useEffect(() => {
const fetchInfo = async () => { const fetchInfo = async () => {
let url = `/api/release?author=${author}&slug=${slug}`; let url = `/api/game?author=${author}&name=${slug ?? ""}`;
if (version) {
url += `&version=${version}`;
}
const information = await fetch(url); const information = await fetch(url);
const json = await information.json(); const json = await information.json();
setInfo(json); console.log(json);
} setGame(json);
};
fetchInfo(); fetchInfo();
}, [setInfo, author, slug, version]); }, [setGame, slug]);
if (!info) { if (!game) {
return ( return <div>LOADING...</div>;
<div>
LOADING...
</div>
)
} }
if (!info.release) { if (!game.carts) {
return ( return <div>NOT FOUND</div>;
<div>
NOT FOUND
</div>
)
} }
return ( return (
<div className={css` <div
margin: auto; className={css`
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%;
margin: auto; margin: auto;
`}> width: max-content;
<div className={css` max-inline-size: 66ch;
border: 2px solid transparent; padding: 1.5em;
&:focus-within { display: flex;
border: 2px solid limegreen; flex-direction: column;
} gap: 1em;
`}> `}
<Pico8Console >
ref={picoRef} <div>
carts={info.release.carts} <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;
}
`}
>
<Pico8Player
// ref={picoRef}
carts={game.carts}
// onGpioChange={(gpio: number[]) => { // onGpioChange={(gpio: number[]) => {
// console.log("sending gpio"); // console.log("sending gpio");
// socket.sendMessage({ // socket.sendMessage({
@@ -107,22 +100,10 @@ export const GamePage = () => {
// }} // }}
/> />
</div> </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>
))
}
</select>
</div>
</div> </div>
{/* <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> */}
</div> </div>
) );
} };
+4 -4
View File
@@ -1,11 +1,11 @@
import { css } from "@emotion/css"; // import { css } from "@emotion/css";
import { Pico8Console } from "./pico8-client/Pico8Console"; // import { Pico8Console } from "./pico8-client/Pico8Console";
import testcarts from "./testcarts"; // import testcarts from "./testcarts";
import { Routing } from "./routing"; import { Routing } from "./routing";
const App = (props: {}) => { const App = (props: {}) => {
return ( return (
<Routing/> <Routing />
// <div className={css` // <div className={css`
// min-height: 100vh; // min-height: 100vh;
// `}> // `}>
-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
+9
View File
@@ -0,0 +1,9 @@
export const authors = [
{
username: "dylanpizzo",
repo: "pico-carts",
auth: {
pat: "f4a1ab7c30ecb6b68da44f5b3f231ab6+719a6bf4edff6d809d1eb6d3ba3c9eb5:a12f955e441c684193053075690f2aba161894c26ad62d6ef8e5fb71b1969cbe755333e1dc7fc90e339ccba2fae27bd82e2ebd1bb233956e09c1e0bf1b951d69a279b4731ca26f8315514524ba93c2228b02456081624db71c14fd4ba0",
},
},
];
-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 { Type } from "@sinclair/typebox";
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts"; 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 method = "GET";
const url = "/api/author"; const url = "/api/author";
const payloadT = Type.Any(); const payloadT = Type.Any();
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => { const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
const {author} = payload; const { author } = payload;
if (typeof author !== "string") { const authorData = authors.find((x) => x.username === author);
if (!authorData) {
return { return {
author: null, 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 { return {
author, author,
games, games,
} };
}; };
export default { export default {
@@ -31,4 +61,4 @@ export default {
url, url,
payloadT, payloadT,
handler, 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: [{ 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;
}
-9
View File
@@ -4,15 +4,6 @@ import fastifyStatic from "@fastify/static";
import { fastifyWebsocket } from "@fastify/websocket"; import { fastifyWebsocket } from "@fastify/websocket";
import { routeList } from "./routelist.ts"; import { routeList } from "./routelist.ts";
import { attachRoute } from "./util/routewrap.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({ const server = Fastify({
logger: true, logger: true,
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 echo from "./api/echo.ts";
import getAuthor from "./api/getAuthor.ts"; import getAuthor from "./api/getAuthor.ts";
import getRelease from "./api/getRelease.ts"; import getGame from "./api/getGame.ts";
import release from "./api/release.ts";
import room from "./api/room.ts"; import room from "./api/room.ts";
import webhook from "./api/webhook.ts"; import webhook from "./api/webhook.ts";
export const routeList = [ export const routeList = [echo, webhook, getAuthor, room, getGame];
echo,
webhook,
release,
getRelease,
getAuthor,
room,
];
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", "target": "es2017",
"module": "es2022", "module": "es2022",
"lib": ["DOM"], "lib": ["DOM"],
"moduleResolution": "node", "moduleResolution": "bundler",
"jsx": "react-jsx", "jsx": "react-jsx",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true
}, },
"ts-node": { "ts-node": {
"esm": true, "esm": true,
"experimentalSpecifierResolution": "node", "experimentalSpecifierResolution": "node"
} }
} }