feat: dev tools

This commit is contained in:
TrèsAbhi committed 2025-06-12 17:48:43 -05:00
1 parent 74b821ac13
commit edce64c1bd
24 files changed
+698 -93

No files matched your search

-1
View File
@@ -34,7 +34,6 @@ target
*.sln.docstates
*.userprefs
mono_crash.*
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
+1
View File
@@ -69,6 +69,7 @@
"DIDS",
"donotremove",
"drei",
"dsapp",
"dvpd",
"dvpl",
"DVPLCRC",
BIN
View File
Binary file not shown.
+20 -1
View File
@@ -1,14 +1,17 @@
import { CatalogItem } from '@protos/blitz_items';
import { Any } from '@protos/google/protobuf/any';
import { lowerFirst } from 'lodash-es';
import { compareUint8Arrays } from '../../../scripts/src/core/github/compareUint8Arrays';
import { MessageFns } from '../protos';
export class CatalogItemAccessor {
public id: string;
components: Record<string, Any> = {};
count: number;
constructor(item: CatalogItem) {
constructor(private item: CatalogItem) {
this.id = item.catalog_id;
this.count = item.components.length;
for (const component of item.components) {
if (
@@ -77,4 +80,20 @@ export class CatalogItemAccessor {
encode() {
return CatalogItem.encode(this.pack()).finish();
}
equals(other: CatalogItemAccessor) {
return (
this.count === other.count &&
this.item.components.every(
(component) =>
component.value === undefined ||
(component.key in other.components &&
(component.value.component === undefined ||
compareUint8Arrays(
other.components[component.key].value,
component.value.component.value,
))),
)
);
}
}
+10
View File
@@ -118,4 +118,14 @@ export class MetadataAccessor {
return filtered;
}
toArray() {
return Object.values(this.items);
}
toSortedArray() {
return this.toArray().sort((a, b) =>
a.catalog_id.localeCompare(b.catalog_id),
);
}
}
+3
View File
@@ -22,6 +22,7 @@
"@react-three/fiber": "^8.17.10",
"@types/ffi-napi": "^4.0.10",
"@types/lodash-es": "^4.17.12",
"@types/pako": "^2.0.3",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@types/ref-napi": "^3.0.12",
@@ -33,6 +34,8 @@
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"minimatch": "^10.0.1",
"minizlib": "^3.0.2",
"pako": "^2.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"ref-napi": "^3.0.3",
@@ -1,13 +1,11 @@
import { Flex, type FlexProps, Theme } from '@radix-ui/themes';
import type { ComponentProps } from 'react';
import type { RadixSize } from '../stores/embedState';
import { BlitzKitTheme } from './BlitzKitTheme';
type PageWrapperProps = FlexProps & {
color?: ComponentProps<typeof Theme>['accentColor'];
containerProps?: ComponentProps<typeof Theme>;
noFlex1?: boolean;
padding?: RadixSize;
noMinHeight?: boolean;
};
@@ -18,7 +16,6 @@ export function PageWrapper({
noFlex1 = false,
containerProps,
noMinHeight = false,
padding = '4',
...props
}: PageWrapperProps) {
return (
@@ -35,8 +32,9 @@ export function PageWrapper({
>
<Flex
direction="column"
gap="4"
p={padding}
gap="8"
px="4"
py="8"
width="100%"
maxWidth="80rem"
style={{
@@ -1,6 +1,12 @@
import { fetchMetadata } from '@blitzkit/closed';
import { BlitzkitServerAPI, MetadataAccessor } from '@blitzkit/core';
import {
assertSecret,
BlitzkitServerAPI,
MetadataAccessor,
} from '@blitzkit/core';
const metadata = await fetchMetadata();
const metadata = await fetchMetadata(
assertSecret(import.meta.env.AUTO_PROXY_ORIGIN),
);
const accessor = new MetadataAccessor(metadata);
export const api = new BlitzkitServerAPI(accessor);
@@ -8,6 +8,8 @@ export const ffi = Library(FFI_PATH, {
debug_echo: [types.CString, [types.CString]],
mount_game: ['void', [types.CString]],
debug_get_file_count: [types.int, []],
get_bindings_url_production: [types.CString, []],
get_bindings_url_dev: [types.CString, []],
});
ffi.mount_game(assertSecret(import.meta.env.GAME_MOUNT_POINT));
@@ -1,14 +1,49 @@
import { Heading } from '@radix-ui/themes';
import { FFIEcho } from '../../components/Internal/FFIEcho';
import { FFIFileCount } from '../../components/Internal/FFIFileCount';
import { Link, Table } from '@radix-ui/themes';
import { PageWrapper } from '../../components/PageWrapper';
export function Page() {
return (
<PageWrapper color="amber">
<Heading>BlitzKit FFI</Heading>
<FFIEcho />
<FFIFileCount />
<PageWrapper align="center">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Path</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Memo</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.RowHeaderCell>
<Link href="/internal/debug">Debug</Link>
</Table.RowHeaderCell>
<Table.Cell>
Some debug information to check if everything is up
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.RowHeaderCell>
<Link href="/internal/bindings">Bindings</Link>
</Table.RowHeaderCell>
<Table.Cell>List of all active bindings</Table.Cell>
</Table.Row>
<Table.Row>
<Table.RowHeaderCell>
<Link href="/internal/discovery">Discovery</Link>
</Table.RowHeaderCell>
<Table.Cell>List of all active discovery lists</Table.Cell>
</Table.Row>
<Table.Row>
<Table.RowHeaderCell>
<Link href="/internal/differ">Differ</Link>
</Table.RowHeaderCell>
<Table.Cell>Differentiate two branches</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
</PageWrapper>
);
}
@@ -0,0 +1,89 @@
import { Badge, Flex, Heading, Link, Separator, Text } from '@radix-ui/themes';
import { PageWrapper } from 'packages/website-ue/src/components/PageWrapper';
import { ffi } from 'packages/website-ue/src/core/blitzkit/ffi';
export interface Bindings {
bindings: Binding[];
forbidden: string[];
}
interface Binding {
name: string;
versions: string[];
discovery: string;
env: string;
show_server_selection: boolean;
}
export async function Page() {
return (
<PageWrapper>
<Bindings url={JSON.parse(ffi.get_bindings_url_dev()!)} dev />
<Separator size="4" />
<Bindings url={JSON.parse(ffi.get_bindings_url_production()!)} />
</PageWrapper>
);
}
async function Bindings({ url, dev }: { url: string; dev?: boolean }) {
const data = await fetch(url).then(
(response) => response.json() as Promise<Bindings>,
);
return (
<>
<Text>
Bindings URL{' '}
<Link href={url} target="_blank">
{url}
</Link>
</Text>
{data.bindings.map((binding) => (
<Flex direction="column" gap="2" key={binding.name}>
<Heading>
<Flex align="center" gap="2">
{binding.env}
{!dev && (
<Badge variant="solid" color="blue">
Production
</Badge>
)}
{dev && (
<Badge variant="solid" color="orange">
Dev
</Badge>
)}
{binding.show_server_selection && (
<Badge color="green">Public</Badge>
)}
{!binding.show_server_selection && (
<Badge color="red">Private</Badge>
)}
</Flex>
</Heading>
<Text>{binding.name}</Text>
<Flex gap="2">
<Text>Discovery:</Text>
<Link href={binding.discovery} target="_blank">
{binding.discovery}
</Link>
</Flex>
<Flex gap="2">
<Text>Versions:</Text>
{binding.versions.map((version) => (
<Badge variant="outline" color="gray" key={version}>
{version}
</Badge>
))}
</Flex>
</Flex>
))}
</>
);
}
@@ -0,0 +1,8 @@
---
import InternalBaseLayout from 'packages/website-ue/src/layouts/InternalBaseLayout.astro';
import { Page } from './_index';
---
<InternalBaseLayout>
<Page />
</InternalBaseLayout>
@@ -0,0 +1,14 @@
import { Heading } from '@radix-ui/themes';
import { FFIEcho } from '../../../components/Internal/FFIEcho';
import { FFIFileCount } from '../../../components/Internal/FFIFileCount';
import { PageWrapper } from '../../../components/PageWrapper';
export function Page() {
return (
<PageWrapper color="amber">
<Heading>BlitzKit FFI</Heading>
<FFIEcho />
<FFIFileCount />
</PageWrapper>
);
}
@@ -0,0 +1,8 @@
---
import InternalBaseLayout from 'packages/website-ue/src/layouts/InternalBaseLayout.astro';
import { Page } from './_index';
---
<InternalBaseLayout>
<Page />
</InternalBaseLayout>
@@ -0,0 +1,289 @@
import type { MessageFns } from '@protos/blitz_static_profile_avatar_component';
import type { Any } from '@protos/google/protobuf/any';
import * as protos from '@protos/index';
import { CommitIcon, ReaderIcon } from '@radix-ui/react-icons';
import {
Badge,
Box,
Code,
Flex,
IconButton,
Select,
Spinner,
Text,
} from '@radix-ui/themes';
import { MetadataAccessor } from 'packages/core/src';
import type { CatalogItemAccessor } from 'packages/core/src/blitz/catalogItemAccessor';
import { PageWrapper } from 'packages/website-ue/src/components/PageWrapper';
import { Var } from 'packages/website-ue/src/core/radix/var';
import { useState } from 'react';
import { fetchMetadata } from 'submodules/blitzkit-closed/src';
import type { DiscoveryServer } from '../discovery/_index';
export function Page({ servers }: { servers: DiscoveryServer[] }) {
const [serverA, setSeverA] = useState<DiscoveryServer | undefined>(undefined);
const [serverB, setSeverB] = useState<DiscoveryServer | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [diff, setDiff] = useState<CatalogDiffEntry[] | null>(null);
return (
<PageWrapper justify="center">
<Flex justify="center" align="center" gap="2">
<Select.Root
size="3"
defaultValue="undefined"
onValueChange={(value) => {
setSeverA(servers.find((server) => server.name === value));
}}
>
<Select.Trigger style={{ flex: 1, maxWidth: '16rem' }} />
<Select.Content>
<Select.Item value="undefined">Select base server</Select.Item>
{servers.map((server) => (
<Select.Item key={server.name} value={server.name}>
{server.name}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<IconButton
size="3"
disabled={!serverA || !serverB}
onClick={async () => {
setLoading(true);
const diff = await diffServers(serverA!.addr, serverB!.addr);
setLoading(false);
setDiff(diff);
}}
>
{loading ? <Spinner /> : <CommitIcon />}
</IconButton>
<Select.Root
size="3"
defaultValue="undefined"
onValueChange={(value) => {
setSeverB(servers.find((server) => server.name === value));
}}
>
<Select.Trigger style={{ flex: 1, maxWidth: '16rem' }} />
<Select.Content>
<Select.Item value="undefined">Select base server</Select.Item>
{servers.map((server) => (
<Select.Item key={server.name} value={server.name}>
{server.name}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</Flex>
{diff !== null && (
<DiffDisplay
diff={diff.filter(
(entry) => entry.type !== CatalogItemDiffType.Unmodified,
)}
/>
)}
</PageWrapper>
);
}
function DiffDisplay({ diff }: { diff: CatalogDiffEntry[] }) {
return (
<Flex direction="column" gap="4">
{diff.length === 0 && (
<Text align="center" color="gray">
Branches are identical
</Text>
)}
{diff.map((entry) => (
<Flex
direction="column"
gap="2"
style={{
backgroundColor: Var('gray-3'),
boxShadow: Var('shadow-3'),
borderRadius: Var('radius-3'),
overflow: 'hidden',
}}
>
<Box p="3">
<Text>
{diffEmojis[entry.type]} {entry.name}
</Text>
</Box>
<Flex gap="1">
{(entry.type === CatalogItemDiffType.Removed ||
entry.type === CatalogItemDiffType.Unmodified) && (
<ItemDisplay item={entry.item} />
)}
{entry.type === CatalogItemDiffType.Modified && (
<ItemDisplay item={entry.a} />
)}
{(entry.type === CatalogItemDiffType.Added ||
entry.type === CatalogItemDiffType.Unmodified) && (
<ItemDisplay item={entry.item} />
)}
{entry.type === CatalogItemDiffType.Modified && (
<ItemDisplay item={entry.b} />
)}
</Flex>
</Flex>
))}
</Flex>
);
}
function ItemDisplay({ item }: { item: CatalogItemAccessor }) {
const entries = Object.entries(item.components);
protos;
return (
<Flex
direction="column"
gap="4"
flexGrow="1"
p="3"
style={{
backgroundColor: Var('gray-2'),
}}
>
{entries.length === 0 && <Text color="gray">No components</Text>}
{entries.map(([key, value]) => (
<ComponentDisplay key={key} name={key} value={value} />
))}
</Flex>
);
}
function ComponentDisplay({ name, value }: { name: string; value: Any }) {
const [code, setCode] = useState<string | null>(null);
return (
<Flex direction="column" gap="1">
<Flex gap="2" align="center">
<IconButton
size="1"
variant="ghost"
onClick={() => {
const messageName = value.type_url
.slice(2)
.split('.')
.slice(1)
.join('.');
const json = (
protos[messageName as keyof typeof protos] as MessageFns<any>
).decode(value.value);
setCode(JSON.stringify(json, null, 2));
}}
>
<ReaderIcon />
</IconButton>
<Code color="gray" highContrast variant="ghost">
{name}
</Code>
<Badge>{value.value.length.toLocaleString()}B</Badge>
</Flex>
{code !== null && (
<Code
ml="4"
size="1"
color="gray"
highContrast
style={{ whiteSpace: 'pre' }}
>
{code}
</Code>
)}
</Flex>
);
}
enum CatalogItemDiffType {
Unmodified,
Modified,
Added,
Removed,
}
type CatalogDiffEntry = { name: string } & (
| {
type: CatalogItemDiffType.Modified;
a: CatalogItemAccessor;
b: CatalogItemAccessor;
}
| {
type: Exclude<CatalogItemDiffType, CatalogItemDiffType.Modified>;
item: CatalogItemAccessor;
}
);
const diffEmojis: Record<CatalogItemDiffType, string> = {
[CatalogItemDiffType.Unmodified]: '🩶',
[CatalogItemDiffType.Modified]: '🟡',
[CatalogItemDiffType.Added]: '🟢',
[CatalogItemDiffType.Removed]: '🔴',
};
async function diffServers(urlA: string, urlB: string) {
const [metadataA, metadataB] = await Promise.all([
fetchMetadata(urlA),
fetchMetadata(urlB),
]).then((members) =>
members.map((metadata) => new MetadataAccessor(metadata)),
);
const itemNameSet = new Set<string>();
for (const item in metadataA.items) itemNameSet.add(item);
for (const item in metadataB.items) itemNameSet.add(item);
const itemNames = [...itemNameSet].sort();
const diff: CatalogDiffEntry[] = [];
for (const name of itemNames) {
if (name in metadataA.items) {
if (name in metadataB.items) {
const a = metadataA.get(name);
const b = metadataB.get(name);
if (a.equals(b)) {
diff.push({ name, type: CatalogItemDiffType.Unmodified, item: a });
} else {
diff.push({
name,
type: CatalogItemDiffType.Modified,
a: a,
b: b,
});
}
} else {
diff.push({
name,
type: CatalogItemDiffType.Removed,
item: metadataA.get(name),
});
}
} else if (name in metadataB.items) {
diff.push({
name,
type: CatalogItemDiffType.Added,
item: metadataB.get(name),
});
}
}
return diff;
}
@@ -0,0 +1,40 @@
---
import { ffi } from '../../../core/blitzkit/ffi';
import InternalBaseLayout from '../../../layouts/InternalBaseLayout.astro';
import { Bindings } from '../bindings/_index';
import { Discovery, DiscoveryServer } from '../discovery/_index';
import { Page } from './_index';
const serversMap = new Map<string, DiscoveryServer>();
await Promise.all(
[ffi.get_bindings_url_dev()!, ffi.get_bindings_url_production()!]
.map((raw) => JSON.parse(raw) as string)
.map(async (url) => {
const { bindings } = await fetch(url).then(
(response) => response.json() as Promise<Bindings>,
);
await Promise.all(
bindings.map(async (binding) => {
const discovery = await fetch(binding.discovery).then(
(response) => response.json() as Promise<Discovery>,
);
Object.values(discovery.servers).map((server) => {
if (serversMap.has(server.name)) return;
serversMap.set(server.name, server);
});
}),
);
}),
);
const servers = [...serversMap.values()].sort((a, b) =>
a.name.localeCompare(b.name),
);
---
<InternalBaseLayout>
<Page {servers} client:load />
</InternalBaseLayout>
@@ -0,0 +1,99 @@
import { Flex, Heading, Link, Text } from '@radix-ui/themes';
import { PageWrapper } from 'packages/website-ue/src/components/PageWrapper';
import { ffi } from 'packages/website-ue/src/core/blitzkit/ffi';
import type { Bindings } from '../bindings/_index';
export interface Discovery {
version: string;
clusters: string[];
default_server: string;
servers: Record<string, DiscoveryServer>;
icmp_servers: ICMPServers;
clusters_versions: Record<string, Empty[]>;
countries_by_clusters: CountriesByClusters;
extensions: Extensions;
}
interface Empty {
version: string;
server: string;
}
type CountriesByClusters = Record<string, { countries: string[] }>;
interface Extensions {
endpoints: Record<string, Endpoint>;
}
interface Endpoint {
playability_config_url: string;
fullscreen_notifications_url: string;
mgg_frontier_url: string;
mgg_pay_url: string;
}
interface ICMPServers {
default: Record<string, string[]>;
}
export interface DiscoveryServer {
name: string;
addr: string;
addr_dsapp?: string;
}
const servers: DiscoveryServer[] = [];
async function populate(url: string) {
const { bindings } = await fetch(url).then(
(response) => response.json() as Promise<Bindings>,
);
await Promise.all(
bindings.map(async (binding) => {
const discovery = await fetch(binding.discovery).then(
(response) => response.json() as Promise<Discovery>,
);
Object.values(discovery.servers).map((server) => {
const doesInclude = servers.some((s) => s.addr === server.addr);
if (!doesInclude) servers.push(server);
});
}),
);
}
await Promise.all([
populate(JSON.parse(ffi.get_bindings_url_dev()!)),
populate(JSON.parse(ffi.get_bindings_url_production()!)),
]);
servers.sort((a, b) => a.name.localeCompare(b.name));
export function Page() {
return (
<PageWrapper>
{servers.map((server) => (
<Flex direction="column" key={server.addr}>
<Heading>{server.name}</Heading>
<Text>
Address:{' '}
<Link href={server.addr} target="_blank">
{server.addr}
</Link>
</Text>
{server.addr_dsapp && (
<Text>
DSApp:{' '}
<Link href={server.addr_dsapp} target="_blank">
{server.addr_dsapp}
</Link>
</Text>
)}
</Flex>
))}
</PageWrapper>
);
}
@@ -0,0 +1,8 @@
---
import InternalBaseLayout from 'packages/website-ue/src/layouts/InternalBaseLayout.astro';
import { Page } from './_index';
---
<InternalBaseLayout>
<Page />
</InternalBaseLayout>
+3 -1
View File
@@ -7,8 +7,10 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../submodules/CUE4Parse/CUE4Parse/CUE4Parse.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ini-parser" Version="2.5.2" />
</ItemGroup>
</Project>
+14 -1
View File
@@ -2,7 +2,6 @@
using System.Runtime.InteropServices;
using BlitzKit.FFI.Models;
using BlitzKit.FFI.Utils;
using CUE4Parse.Compression;
namespace BlitzKit.FFI;
@@ -32,4 +31,18 @@ public static class FFI
{
return provider.Files.Count;
}
[UnmanagedCallersOnly(EntryPoint = "get_bindings_url_production")]
public static IntPtr GetBindingsURLProduction()
{
var url = provider.GetBindingsURLProduction();
return Marshal.StringToHGlobalAnsi(url);
}
[UnmanagedCallersOnly(EntryPoint = "get_bindings_url_dev")]
public static IntPtr GetBindingsURLDev()
{
var url = provider.GetBindingsURLDev();
return Marshal.StringToHGlobalAnsi(url);
}
}
+6 -16
View File
@@ -1,6 +1,6 @@
using System;
using System.IO;
using System.Text;
using BlitzKit.FFI.Utils;
using CUE4Parse.FileProvider;
using CUE4Parse.UE4.Versions;
@@ -18,24 +18,14 @@ namespace BlitzKit.FFI.Models
{
Initialize();
Mount();
Console.WriteLine(DiscoveryURL());
}
public string DiscoveryURL()
{
if (TryGetGameFile("Blitz/Config/DefaultEngine.ini", out var defaultEngine))
{
INIAccessor accessor = new(Encoding.UTF8.GetString(defaultEngine.Read()));
public INIAccessor GetDefaultEngine() => new(this["Blitz/Config/DefaultEngine.ini"]);
var url = (string)accessor.Get("DiscoveryURLDedicProduction");
public string GetBindingsURLProduction() =>
GetDefaultEngine()["/Script/MintSDK.MintSDKSettingsObject"]["DiscoveryBindingsURLProduction"];
return url;
}
else
{
throw new FileNotFoundException("Could not find DefaultEngine.ini");
}
}
public string GetBindingsURLDev() =>
GetDefaultEngine()["/Script/MintSDK.MintSDKSettingsObject"]["DiscoveryBindingsURLDev"];
}
}
-58
View File
@@ -1,58 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace BlitzKit.FFI.Models
{
public class INIAccessor
{
private Dictionary<string, object> data = [];
public INIAccessor(string content)
{
var lines = content.Split(['\n', '\r'], System.StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var trimmed = line.Trim();
if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith(';'))
continue;
var prefix = trimmed[0];
string key,
value;
if (prefix == '+' || prefix == '-')
{
var x = trimmed.IndexOf('=');
key = trimmed[1..x];
value = trimmed[(x + 1)..];
if (!data.TryGetValue(key, out var existing))
{
data[key] = existing = new List<string>();
}
var list = (List<string>)existing;
if (prefix == '+')
list.Add(value);
else
list.Remove(value);
}
else
{
var x = trimmed.IndexOf('=');
key = trimmed[..x];
value = trimmed[(x + 1)..];
data[key] = value;
}
}
}
public bool TryGet(string key, out object value) => data.TryGetValue(key, out value);
public object Get(string key) => data[key];
}
}
+30
View File
@@ -0,0 +1,30 @@
using System.Text;
using CUE4Parse.FileProvider.Objects;
using IniParser.Model;
using IniParser.Model.Configuration;
using IniParser.Parser;
namespace BlitzKit.FFI.Utils
{
public class INIAccessor
{
private static readonly IniParserConfiguration configuration = new()
{
AllowDuplicateSections = true,
AllowDuplicateKeys = true,
ConcatenateDuplicateKeys = true,
};
private readonly IniData data;
public INIAccessor(GameFile file)
{
var bytes = file.Read();
var str = Encoding.UTF8.GetString(bytes);
IniDataParser parser = new(configuration);
data = parser.Parse(str);
}
public KeyDataCollection this[string sectionName] => data[sectionName];
}
}