mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: dev tools
This commit is contained in:
1 parent
74b821ac13
commit
edce64c1bd
24 files changed
+698
-93
No files matched your search
@@ -34,7 +34,6 @@ target
|
||||
*.sln.docstates
|
||||
*.userprefs
|
||||
mono_crash.*
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
|
||||
Vendored
+1
@@ -69,6 +69,7 @@
|
||||
"DIDS",
|
||||
"donotremove",
|
||||
"drei",
|
||||
"dsapp",
|
||||
"dvpd",
|
||||
"dvpl",
|
||||
"DVPLCRC",
|
||||
|
||||
@@ -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,
|
||||
))),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"];
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
Submodule submodules/blitzkit-closed updated: 89dc337d27...69df3b07a2.
Reference in new issue
Block a user