new format wip
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
@firebox:registry = "https://nodepack.playbox.link"
|
@firebox:registry = "https://nodepack.playbox.link"
|
||||||
|
@dylanpizzo:registry = "https://dylanpizzo.dev"
|
||||||
+6
-6
@@ -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 ./
|
||||||
|
|||||||
Binary file not shown.
+14
-31
@@ -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: {}
|
|
||||||
|
|||||||
Generated
+9268
-8217
File diff suppressed because it is too large
Load Diff
+62
-60
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
// `}>
|
// `}>
|
||||||
|
|||||||
@@ -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
@@ -1,2 +0,0 @@
|
|||||||
export {makePicoConsole} from "./renderCart";
|
|
||||||
export {pngToRom} from "./pngToRom";
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
@@ -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
@@ -0,0 +1,9 @@
|
|||||||
|
export const authors = [
|
||||||
|
{
|
||||||
|
username: "dylanpizzo",
|
||||||
|
repo: "pico-carts",
|
||||||
|
auth: {
|
||||||
|
pat: "f4a1ab7c30ecb6b68da44f5b3f231ab6+719a6bf4edff6d809d1eb6d3ba3c9eb5:a12f955e441c684193053075690f2aba161894c26ad62d6ef8e5fb71b1969cbe755333e1dc7fc90e339ccba2fae27bd82e2ebd1bb233956e09c1e0bf1b951d69a279b4731ca26f8315514524ba93c2228b02456081624db71c14fd4ba0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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
@@ -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
@@ -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>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -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>;
|
|
||||||
@@ -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>;
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
+1276
-1047
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 |
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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)));
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user