feat: mirrors

This commit is contained in:
tresabhi committed 2025-04-01 21:03:39 -05:00
1 parent 8087752172
commit a8dc3313cf
16 files changed
+268 -148

No files matched your search

-6
View File
@@ -1,13 +1,7 @@
syntax = "proto2"; syntax = "proto2";
import "packages/core/src/protos/tank.proto";
package blitzkit; package blitzkit;
message Tanks { message Tanks {
repeated string tanks = 1; repeated string tanks = 1;
} }
message TanksFull {
repeated Tank tanks = 1;
}
@@ -0,0 +1,9 @@
syntax = "proto2";
import "packages/core/src/protos/tank.proto";
package blitzkit;
message TanksFull {
repeated Tank tanks = 1;
}
@@ -1,7 +1,6 @@
import { fetchPB, type MessageFns } from '@blitzkit/core'; import { fetchPB, type MessageFns } from '@blitzkit/core';
import type { APIRoute } from 'astro';
export async function blobMirror(url: string) { export async function blobProxy(url: string) {
if (import.meta.env.MODE === 'development') { if (import.meta.env.MODE === 'development') {
return Response.redirect(url); return Response.redirect(url);
} else { } else {
@@ -11,16 +10,23 @@ export async function blobMirror(url: string) {
} }
} }
export function apiMirror(path: string) { export function apiBlobProxy(path: string) {
const get: APIRoute = () => blobMirror(`http://localhost:5000${path}`); return blobProxy(`http://localhost:5000${path}`);
return get;
} }
export function jsonMirror<Type>(path: string, message: MessageFns<Type>) { export function apiBlobProxyCurry(path: string) {
const get: APIRoute = async () => { return () => apiBlobProxy(path);
return new Response( }
JSON.stringify(await fetchPB(`http://localhost:5000${path}`, message)),
); export async function jsonMirror<Type>(
}; path: string,
return get; message: MessageFns<Type>,
) {
return new Response(
JSON.stringify(await fetchPB(`http://localhost:5000${path}`, message)),
);
}
export function jsonMirrorCurry<Type>(path: string, message: MessageFns<Type>) {
return () => jsonMirror(path, message);
} }
@@ -1,7 +1,21 @@
import { OpenInNewWindowIcon } from '@radix-ui/react-icons'; import { assertSecret } from '@blitzkit/core';
import { Box, Code, IconButton, Link, Table } from '@radix-ui/themes'; import {
import { useState } from 'react'; FileTextIcon,
import { Var } from '../../../core/radix/var'; OpenInNewWindowIcon,
RocketIcon,
} from '@radix-ui/react-icons';
import {
Button,
Code,
Dialog,
Flex,
IconButton,
Inset,
Link,
Separator,
Table,
} from '@radix-ui/themes';
import { BlitzKitTheme } from '../../../components/BlitzKitTheme';
import type { APIPath } from './index.astro'; import type { APIPath } from './index.astro';
interface SlugsTableProps { interface SlugsTableProps {
@@ -9,13 +23,12 @@ interface SlugsTableProps {
path: string; path: string;
} }
const MAX_UNEXPANDED = 3; interface TableRootProps {
slugs: NonNullable<APIPath['slugs']>;
export function SlugsTable({ slugs, path }: SlugsTableProps) { path: string;
if (slugs === undefined) return null; }
const [expanded, setExpanded] = useState(false);
function TableRoot({ slugs, path }: TableRootProps) {
return ( return (
<Table.Root variant="surface"> <Table.Root variant="surface">
<Table.Header> <Table.Header>
@@ -34,7 +47,6 @@ export function SlugsTable({ slugs, path }: SlugsTableProps) {
<Table.Body> <Table.Body>
{slugs.groups.map((group, groupIndex) => { {slugs.groups.map((group, groupIndex) => {
if (!expanded && groupIndex >= MAX_UNEXPANDED) return null;
let draftPath = path; let draftPath = path;
slugs.titles.forEach((title) => { slugs.titles.forEach((title) => {
@@ -60,53 +72,84 @@ export function SlugsTable({ slugs, path }: SlugsTableProps) {
</Table.Cell> </Table.Cell>
))} ))}
<Table.Cell> <Table.Cell align="right">
<Link target="_blank" href={`/api/${draftPath}`}> <Link target="_blank" href={`/api/${draftPath}`}>
<IconButton variant="surface"> <IconButton variant="solid">
<OpenInNewWindowIcon /> <RocketIcon />
</IconButton> </IconButton>
</Link> </Link>
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
); );
})} })}
<Table.Row style={{ position: 'relative' }}>
{!expanded && (
<Box
width="100%"
height="100%"
position="absolute"
top="0"
style={{
transform: 'translate(0, -100%)',
background: `linear-gradient(${Var('black-a1')}, ${Var('black-a5')})`,
}}
/>
)}
<Table.Cell />
<Link
href="#"
underline="always"
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
onClick={(event) => {
event.preventDefault();
setExpanded((state) => !state);
}}
>
{expanded
? 'Show less'
: `View all ${slugs.groups.length.toLocaleString('en')} slugs`}
</Link>
</Table.Row>
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
); );
} }
export function SlugsTable({ slugs, path }: SlugsTableProps) {
if (slugs === undefined) return null;
return (
<BlitzKitTheme>
<Dialog.Root>
<Dialog.Trigger>
<Button color="amber">
Slugs <OpenInNewWindowIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Inset>
<TableRoot slugs={slugs} path={path} />
</Inset>
</Dialog.Content>
</Dialog.Root>
</BlitzKitTheme>
);
}
interface SchemaProps {
schema: string;
name: string;
}
export function Schema({ schema, name }: SchemaProps) {
return (
<BlitzKitTheme>
<Dialog.Root>
<Dialog.Trigger>
<Button color="amber" variant="outline">
Schema <FileTextIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Flex direction="column" gap="5">
<Link
target="_blank"
href={`https://github.com/${assertSecret(
import.meta.env.PUBLIC_REPO,
)}/blob/${assertSecret(
import.meta.env.PUBLIC_BRANCH,
)}/packages/core/src/protos/${name}.proto`}
>
<Code>{name}.proto</Code> on GitHub <OpenInNewWindowIcon />
</Link>
<Separator size="4" />
<Code
color="gray"
highContrast
variant="ghost"
style={{ whiteSpace: 'pre' }}
>
{schema}
</Code>
</Flex>
</Dialog.Content>
</Dialog.Root>
</BlitzKitTheme>
);
}
@@ -1,6 +1,7 @@
--- ---
import { OpenInNewWindowIcon } from '@radix-ui/react-icons'; import { RocketIcon } from '@radix-ui/react-icons';
import { import {
Button,
Code, Code,
Flex, Flex,
Heading, Heading,
@@ -10,10 +11,11 @@ import {
type CodeProps, type CodeProps,
} from '@radix-ui/themes'; } from '@radix-ui/themes';
import type { GetStaticPaths, GetStaticPathsOptions } from 'astro'; import type { GetStaticPaths, GetStaticPathsOptions } from 'astro';
import { readFile } from 'fs/promises';
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from '../../../components/PageWrapper';
import { apiTool } from '../../../constants/tools'; import { apiTool } from '../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro'; import ToolLayout from '../../../layouts/ToolLayout.astro';
import { SlugsTable } from './_index'; import { Schema, SlugsTable } from './_index';
export { getStaticPaths } from '../index.astro'; export { getStaticPaths } from '../index.astro';
@@ -32,6 +34,10 @@ export interface APIPath {
titles: string[]; titles: string[];
groups: SlugGroup[]; groups: SlugGroup[];
}; };
schema?: {
name: string;
content: string;
};
} }
type SlugGroup = Record<string, string | number | undefined>; type SlugGroup = Record<string, string | number | undefined>;
@@ -88,9 +94,11 @@ for (const rawPath in globRaw) {
); );
const path = rawPath.replace(ROOT, '').replace('.ts', ''); const path = rawPath.replace(ROOT, '').replace('.ts', '');
let comment = ''; let comment = '';
let schema: APIPath['schema'];
const tags: Tag[] = []; const tags: Tag[] = [];
commentWithTags.split('\n').forEach((line) => {
for (const line of commentWithTags.split('\n')) {
const match = line.match(tagRegex); const match = line.match(tagRegex);
if (match) { if (match) {
@@ -103,6 +111,16 @@ for (const rawPath in globRaw) {
const firstWordMatch = descriptionRaw.match(firstWordRegex)!; const firstWordMatch = descriptionRaw.match(firstWordRegex)!;
discriminator = firstWordMatch[1]; discriminator = firstWordMatch[1];
description = firstWordMatch[2]; description = firstWordMatch[2];
} else if (tag === '@proto') {
const firstWordMatch = descriptionRaw.match(firstWordRegex)!;
const fileName = firstWordMatch[1];
const buffer = await readFile(
`../../packages/core/src/protos/${fileName}.proto`,
);
const content = buffer.toString();
schema = { name: fileName, content };
discriminator = firstWordMatch[1];
} else { } else {
description = descriptionRaw; description = descriptionRaw;
} }
@@ -115,10 +133,10 @@ for (const rawPath in globRaw) {
} else { } else {
comment += `${line}\n`; comment += `${line}\n`;
} }
}); }
comment = comment.trim(); comment = comment.trim();
apis.push({ path, content, comment, tags, slugs }); apis.push({ path, content, comment, tags, slugs, schema });
} }
apis.sort((a, b) => a.path.localeCompare(b.path)); apis.sort((a, b) => a.path.localeCompare(b.path));
@@ -131,21 +149,35 @@ apis.sort((a, b) => a.path.localeCompare(b.path));
apis.map((api, index) => ( apis.map((api, index) => (
<> <>
<Flex direction="column" gap="5"> <Flex direction="column" gap="5">
<Link <Flex align="center" justify="between">
href={api.slugs ? undefined : `/api/${api.path}`}
underline="always"
target="_blank"
>
<Heading> <Heading>
<Flex align="center" gap="2"> <Code variant="ghost">{api.path}</Code>
<Code variant="ghost">{api.path}</Code>
{!api.slugs && (
<OpenInNewWindowIcon width="1em" height="1em" />
)}
</Flex>
</Heading> </Heading>
</Link>
<Flex justify="end" gap="2">
{api.schema && (
<Schema
client:load
schema={api.schema.content}
name={api.schema.name}
/>
)}
{!api.slugs && (
<Link
href={`/api/${api.path}`}
underline="always"
target="_blank"
>
<Button color="amber">
Test <RocketIcon />
</Button>
</Link>
)}
<SlugsTable client:load path={api.path} slugs={api.slugs} />
</Flex>
</Flex>
<Text>{api.comment}</Text> <Text>{api.comment}</Text>
@@ -180,11 +212,7 @@ apis.sort((a, b) => a.path.localeCompare(b.path));
)} )}
</Flex> </Flex>
<SlugsTable path={api.path} client:load slugs={api.slugs} /> {index < apis.length - 1 && <Separator size="4" />}
{index < apis.length - 1 && (
<Separator size="4" style={{ height: 0.5 }} />
)}
</> </>
)) ))
} }
+7 -2
View File
@@ -1,5 +1,6 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import packageJSON from '../../../../../package.json'; import packageJSON from '../../../../../package.json';
import { TOOLS } from '../../constants/tools';
/** /**
* Returns information on the current build of BlitzKit like the version * Returns information on the current build of BlitzKit like the version
@@ -8,6 +9,10 @@ import packageJSON from '../../../../../package.json';
* @todo Add more meta data. * @todo Add more meta data.
* @returns JSON. * @returns JSON.
*/ */
export const GET: APIRoute = async () => { export const GET: APIRoute = async ({ request }) => {
return Response.json({ version: packageJSON.version }); return Response.json({
url: new URL(request.url).origin,
version: packageJSON.version,
tools: TOOLS.map((tool) => tool.id),
});
}; };
@@ -1,6 +1,6 @@
import { asset } from '@blitzkit/core'; import { asset } from '@blitzkit/core';
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { blobMirror } from '../../../../../core/blitzkit/blobMirror'; import { blobProxy } from '../../../../../core/blitzkit/blobMirror';
export { getStaticPaths } from '../meta.json'; export { getStaticPaths } from '../meta.json';
@@ -11,5 +11,5 @@ export { getStaticPaths } from '../meta.json';
* @returns PNG. * @returns PNG.
*/ */
export const GET: APIRoute<{}, { id: string }> = async ({ params }) => { export const GET: APIRoute<{}, { id: string }> = async ({ params }) => {
return await blobMirror(asset(`/tanks/${params.id}/icons/big.png`)); return await blobProxy(asset(`/tanks/${params.id}/icons/big.png`));
}; };
@@ -1,6 +1,6 @@
import { asset } from '@blitzkit/core'; import { asset } from '@blitzkit/core';
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { blobMirror } from '../../../../../core/blitzkit/blobMirror'; import { blobProxy } from '../../../../../core/blitzkit/blobMirror';
export { getStaticPaths } from '../meta.json'; export { getStaticPaths } from '../meta.json';
@@ -11,5 +11,5 @@ export { getStaticPaths } from '../meta.json';
* @returns PNG. * @returns PNG.
*/ */
export const GET: APIRoute<{}, { id: string }> = async ({ params }) => { export const GET: APIRoute<{}, { id: string }> = async ({ params }) => {
return await blobMirror(asset(`/tanks/${params.id}/icons/small.png`)); return await blobProxy(asset(`/tanks/${params.id}/icons/small.png`));
}; };
@@ -1,5 +1,6 @@
import { fetchTank, fetchTanks } from '@blitzkit/core'; import { fetchTanks, Tank } from '@blitzkit/core';
import type { APIRoute, GetStaticPaths } from 'astro'; import type { APIRoute, GetStaticPaths } from 'astro';
import { jsonMirror } from '../../../../core/blitzkit/blobMirror';
export const getStaticPaths = (async () => { export const getStaticPaths = (async () => {
const tanks = await fetchTanks(); const tanks = await fetchTanks();
@@ -7,15 +8,10 @@ export const getStaticPaths = (async () => {
}) satisfies GetStaticPaths; }) satisfies GetStaticPaths;
/** /**
* Returns the meta data on a tank which includes its modules, characteristics, * Identical to tanks/[id]/meta.pb but in JSON.
* ancestry, descendants, equipment, characteristics, etc. However, this does
* not include information on the tank model. Use tanks/list.json to get all
* valid ids.
* *
* @param id The tank id. * @param id The tank id.
* @returns JSON. * @returns JSON.
*/ */
export const GET: APIRoute<{}, { id: string }> = async ({ params }) => { export const GET: APIRoute<{}, { id: string }> = ({ params }) =>
const tank = await fetchTank(params.id); jsonMirror(`/tanks/${params.id}.pb`, Tank);
return Response.json(tank);
};
@@ -0,0 +1,20 @@
import { fetchTanks } from '@blitzkit/core';
import type { APIRoute, GetStaticPaths } from 'astro';
import { apiBlobProxy } from '../../../../core/blitzkit/blobMirror';
export const getStaticPaths = (async () => {
const tanks = await fetchTanks();
return tanks.tanks.map((id) => ({ params: { id } }));
}) satisfies GetStaticPaths;
/**
* Returns the meta data on a tank which includes its modules, characteristics,
* ancestry, descendants, equipment, characteristics, etc. However, this does
* not include information on the tank model. Use tanks/list.json to get all
* valid ids.
*
* @param id The tank id.
* @proto tank
*/
export const GET: APIRoute<{}, { id: string }> = ({ params }) =>
apiBlobProxy(`/tanks/${params.id}.pb`);
@@ -1,5 +1,5 @@
import { TanksFull } from '@blitzkit/core'; import { TanksFull } from '@blitzkit/core';
import { jsonMirror } from '../../../core/blitzkit/blobMirror'; import { jsonMirrorCurry } from '../../../core/blitzkit/blobMirror';
/** /**
* Identical to tanks/full.pb but in JSON. * Identical to tanks/full.pb but in JSON.
@@ -7,4 +7,4 @@ import { jsonMirror } from '../../../core/blitzkit/blobMirror';
* @warning This is a large response. Consider using tanks/[id]/meta.json for individual tanks. * @warning This is a large response. Consider using tanks/[id]/meta.json for individual tanks.
* @returns JSON. * @returns JSON.
*/ */
export const GET = jsonMirror('/tanks/full.pb', TanksFull); export const GET = jsonMirrorCurry('/tanks/full.pb', TanksFull);
@@ -1,8 +1,8 @@
import { apiMirror } from '../../../core/blitzkit/blobMirror'; import { apiBlobProxyCurry } from '../../../core/blitzkit/blobMirror';
/** /**
* Returns meta data similar to tanks/[id].json but for all tanks as an array. There is no guarantee of order. * Returns meta data similar to tanks/[id].json but for all tanks as an array. There is no guarantee of order.
* *
* @proto TanksFull * @proto tanks_full
*/ */
export const GET = apiMirror('/tanks/full.pb'); export const GET = apiBlobProxyCurry('/tanks/full.pb');
@@ -1,9 +1,9 @@
import { Tanks } from '@blitzkit/core'; import { Tanks } from '@blitzkit/core';
import { jsonMirror } from '../../../core/blitzkit/blobMirror'; import { jsonMirrorCurry } from '../../../core/blitzkit/blobMirror';
/** /**
* Identical to tanks/list.pb but in JSON. * Identical to tanks/list.pb but in JSON.
* *
* @returns JSON. * @returns JSON.
*/ */
export const GET = jsonMirror('/tanks/list.pb', Tanks); export const GET = jsonMirrorCurry('/tanks/list.pb', Tanks);
@@ -1,8 +1,8 @@
import { apiMirror } from '../../../core/blitzkit/blobMirror'; import { apiBlobProxyCurry } from '../../../core/blitzkit/blobMirror';
/** /**
* Returns a list of all tank ids. There is no guarantee of order. * Returns a list of all tank ids. There is no guarantee of order.
* *
* @returns JSON. * @proto tanks
*/ */
export const GET = apiMirror('/tanks/list.pb'); export const GET = apiBlobProxyCurry('/tanks/list.pb');
+19 -10
View File
@@ -1,5 +1,9 @@
using System.Threading.Tasks;
using AssetRipper.TextureDecoder.Rgb.Channels;
using Blitzkit;
using Google.Protobuf; using Google.Protobuf;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace BlitzKit.CLI.Functions namespace BlitzKit.CLI.Functions
{ {
@@ -9,19 +13,19 @@ namespace BlitzKit.CLI.Functions
readonly Mangler mangler = new(args); readonly Mangler mangler = new(args);
void Map<T>(string path, Func<Task<T>> func) void Map<T>(string path, Func<Task<T>> func)
where T : IMessage<T> =>
app.MapGet(path, async context => await Octet(context, await func()));
void Map<T>(string path, Func<T> func)
where T : IMessage<T> => Map(path, () => Task.FromResult(func()));
static async Task Octet<T>(HttpContext context, T message)
where T : IMessage<T> where T : IMessage<T>
{ {
app.MapGet( var bytes = message.ToByteArray();
path,
async (context) =>
{
var message = await func();
var bytes = message.ToByteArray();
context.Response.ContentType = "application/octet-stream"; context.Response.ContentType = "application/octet-stream";
await context.Response.Body.WriteAsync(bytes); await context.Response.Body.WriteAsync(bytes);
}
);
} }
public async Task Run() public async Task Run()
@@ -31,6 +35,11 @@ namespace BlitzKit.CLI.Functions
Map("/tanks/full.pb", mangler.TanksFull); Map("/tanks/full.pb", mangler.TanksFull);
Map("/tanks/list.pb", mangler.Tanks); Map("/tanks/list.pb", mangler.Tanks);
app.MapGet(
"/tanks/{id}.pb",
(HttpContext context, string id) => Octet(context, mangler.Tank(id))
);
app.Run(); app.Run();
} }
} }
+32 -22
View File
@@ -49,11 +49,21 @@ namespace BlitzKit.CLI.Functions
readonly BlitzProvider provider = new(args.Contains("--depot")); readonly BlitzProvider provider = new(args.Contains("--depot"));
Locales? locales; Locales? locales;
readonly Dictionary<string, Dictionary<string, string>> strings = []; readonly Dictionary<string, Dictionary<string, string>> strings = [];
TanksFull tanksFull = null!;
public async Task<TanksFull> TanksFull() public async Task Initialize()
{ {
await FetchStrings();
tanksFull = TanksFull();
}
public TanksFull TanksFull()
{
if (tanksFull != null)
return tanksFull;
var dirs = provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories; var dirs = provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories;
TanksFull tanksFull = new(); tanksFull = new();
foreach (var dir in dirs) foreach (var dir in dirs)
{ {
@@ -62,7 +72,7 @@ namespace BlitzKit.CLI.Functions
foreach (var tankDir in dir.Value.Directories) foreach (var tankDir in dir.Value.Directories)
{ {
var tank = MangleTank(tankDir.Value); var tank = Tank(tankDir.Value);
tanksFull.Tanks.Add(tank); tanksFull.Tanks.Add(tank);
} }
} }
@@ -72,7 +82,25 @@ namespace BlitzKit.CLI.Functions
return tanksFull; return tanksFull;
} }
Tank MangleTank(VFS tankDir) public Tanks Tanks()
{
var tanksFull = TanksFull();
Tanks tanks = new();
foreach (var tank in tanksFull.Tanks)
{
tanks.Tanks_.Add(tank.Id);
}
return tanks;
}
public Tank Tank(string id)
{
return tanksFull.Tanks.First(tank => tank.Id == id);
}
Tank Tank(VFS tankDir)
{ {
var pdaName = $"PDA_{tankDir.Name}"; var pdaName = $"PDA_{tankDir.Name}";
var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}"); var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}");
@@ -94,24 +122,6 @@ namespace BlitzKit.CLI.Functions
return tank; return tank;
} }
public async Task<Tanks> Tanks()
{
var tanksFull = await TanksFull();
Tanks tanks = new();
foreach (var tank in tanksFull.Tanks)
{
tanks.Tanks_.Add(tank.Id);
}
return tanks;
}
public async Task Initialize()
{
await FetchStrings();
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
I18n GetString(string name) I18n GetString(string name)