@aphrody/shenron-dbxv2 0.0.0-stage → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +20 -2
- package/package.json +77 -4
- package/src/cache.ts +84 -0
- package/src/client.ts +623 -0
- package/src/db.ts +67 -0
- package/src/declarations.ts +135 -0
- package/src/env.ts +111 -0
- package/src/errors.ts +141 -0
- package/src/guard.ts +115 -0
- package/src/index.ts +15 -0
- package/src/pixel-perfect.ts +139 -0
- package/src/plugin.ts +204 -0
- package/src/preload.ts +13 -0
- package/src/proxy.ts +243 -0
- package/src/refs.ts +126 -0
- package/src/runtime.ts +130 -0
- package/src/schemas.ts +60 -0
- package/src/texture.ts +83 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime used by the modules the `dbxv2:` plugin generates. Resolves the private server from the
|
|
3
|
+
* environment on first use (so nothing about the game or the token is fixed at build time), and
|
|
4
|
+
* starts the loopback signing proxy lazily for URL imports.
|
|
5
|
+
*/
|
|
6
|
+
import { Dbxv2Client, type Dbxv2Binary } from "./client";
|
|
7
|
+
import { currentEnv, dbxv2Status, describeDisabledReason, type Dbxv2EnvInput } from "./env";
|
|
8
|
+
import { Dbxv2DisabledError } from "./errors";
|
|
9
|
+
import { createDbxv2Proxy, type Dbxv2Proxy } from "./proxy";
|
|
10
|
+
import {
|
|
11
|
+
parseRef,
|
|
12
|
+
type Dbxv2Ref,
|
|
13
|
+
type Dbxv2RefInput,
|
|
14
|
+
type Dbxv2ResourceKind,
|
|
15
|
+
type Dbxv2ResourceParams,
|
|
16
|
+
} from "./refs";
|
|
17
|
+
import type { Dbxv2ModelJson } from "./schemas";
|
|
18
|
+
|
|
19
|
+
export interface Dbxv2Runtime {
|
|
20
|
+
readonly client: Dbxv2Client;
|
|
21
|
+
/** Started on first use. */
|
|
22
|
+
proxy(): Dbxv2Proxy;
|
|
23
|
+
stop(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let current: { runtime: Dbxv2Runtime; key: string } | null = null;
|
|
27
|
+
|
|
28
|
+
/** Client + proxy built from the environment; throws {@link Dbxv2DisabledError} when disabled. */
|
|
29
|
+
export function getDbxv2Runtime(env: Dbxv2EnvInput = currentEnv()): Dbxv2Runtime {
|
|
30
|
+
const status = dbxv2Status(env);
|
|
31
|
+
if (!status.enabled) {
|
|
32
|
+
throw new Dbxv2DisabledError(
|
|
33
|
+
`DBXV2 is disabled: ${describeDisabledReason(status.reason)}. ` +
|
|
34
|
+
"Set DBXV2_SERVER_URL and DBXV2_TOKEN (see docs/products/dbxv2-plugin.md).",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const key = `${status.config.baseUrl}\n${status.config.token}`;
|
|
38
|
+
if (current?.key === key) return current.runtime;
|
|
39
|
+
current?.runtime.stop();
|
|
40
|
+
let proxy: Dbxv2Proxy | null = null;
|
|
41
|
+
const client = new Dbxv2Client(status.config);
|
|
42
|
+
const runtime: Dbxv2Runtime = {
|
|
43
|
+
client,
|
|
44
|
+
proxy: () => {
|
|
45
|
+
if (!status.config.token)
|
|
46
|
+
throw new Dbxv2DisabledError("Public clients do not create private signing proxies");
|
|
47
|
+
return (proxy ??= createDbxv2Proxy({
|
|
48
|
+
upstream: { baseUrl: status.config.baseUrl, token: status.config.token },
|
|
49
|
+
env,
|
|
50
|
+
}));
|
|
51
|
+
},
|
|
52
|
+
stop: () => {
|
|
53
|
+
proxy?.stop();
|
|
54
|
+
proxy = null;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
current = { runtime, key };
|
|
58
|
+
return runtime;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Stops the proxy and forgets the cached runtime (tests, shutdown). */
|
|
62
|
+
export function resetDbxv2Runtime(): void {
|
|
63
|
+
current?.runtime.stop();
|
|
64
|
+
current = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Dbxv2Resource {
|
|
68
|
+
readonly ref: Dbxv2Ref;
|
|
69
|
+
readonly kind: Dbxv2ResourceKind;
|
|
70
|
+
/** Short-lived signed loopback URL, safe for `<img>`, `<audio>`, `<video>` or a native loader. */
|
|
71
|
+
url(): string;
|
|
72
|
+
blob(signal?: AbortSignal): Promise<Blob>;
|
|
73
|
+
arrayBuffer(signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
74
|
+
/** `model.json` only. */
|
|
75
|
+
model(signal?: AbortSignal): Promise<Dbxv2ModelJson>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
79
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Handle on one resource; nothing is fetched until a method is called. */
|
|
83
|
+
export function resource(
|
|
84
|
+
input: Dbxv2RefInput,
|
|
85
|
+
kind: Dbxv2ResourceKind,
|
|
86
|
+
params: Dbxv2ResourceParams = {},
|
|
87
|
+
): Dbxv2Resource {
|
|
88
|
+
const ref = parseRef(input);
|
|
89
|
+
const load = async (
|
|
90
|
+
signal?: AbortSignal,
|
|
91
|
+
): Promise<Dbxv2Binary | { bytes: Uint8Array; contentType: string }> => {
|
|
92
|
+
const { client } = getDbxv2Runtime();
|
|
93
|
+
switch (kind) {
|
|
94
|
+
case "raw": {
|
|
95
|
+
const whole = await client.download(ref, { signal });
|
|
96
|
+
return { bytes: whole.bytes, contentType: "application/octet-stream" };
|
|
97
|
+
}
|
|
98
|
+
case "png":
|
|
99
|
+
return client.png(ref, { index: params.index, signal });
|
|
100
|
+
case "wav":
|
|
101
|
+
return client.wav(ref, { cue: params.cue, signal });
|
|
102
|
+
case "video":
|
|
103
|
+
return client.video(ref, { signal });
|
|
104
|
+
case "mp4":
|
|
105
|
+
return client.mp4(ref, { signal });
|
|
106
|
+
case "model.json":
|
|
107
|
+
return {
|
|
108
|
+
bytes: new TextEncoder().encode(
|
|
109
|
+
JSON.stringify(await client.modelJson(ref, { ...params, signal })),
|
|
110
|
+
),
|
|
111
|
+
contentType: "application/json",
|
|
112
|
+
};
|
|
113
|
+
case "model.glb":
|
|
114
|
+
return client.modelGlb(ref, { ...params, signal });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
ref,
|
|
119
|
+
kind,
|
|
120
|
+
url: () => getDbxv2Runtime().proxy().sign(kind, ref, params),
|
|
121
|
+
async blob(signal) {
|
|
122
|
+
const res = await load(signal);
|
|
123
|
+
return new Blob([toArrayBuffer(res.bytes)], { type: res.contentType });
|
|
124
|
+
},
|
|
125
|
+
async arrayBuffer(signal) {
|
|
126
|
+
return toArrayBuffer((await load(signal)).bytes);
|
|
127
|
+
},
|
|
128
|
+
model: (signal) => getDbxv2Runtime().client.modelJson(ref, { ...params, signal }),
|
|
129
|
+
};
|
|
130
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** zod v4 schemas of every JSON response of the private server. */
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const Dbxv2HealthSchema = z.object({ ok: z.literal(true) });
|
|
5
|
+
export type Dbxv2Health = z.infer<typeof Dbxv2HealthSchema>;
|
|
6
|
+
|
|
7
|
+
export const Dbxv2CpkInfoSchema = z.object({
|
|
8
|
+
name: z.string().regex(/^[A-Za-z0-9_]+$/),
|
|
9
|
+
size: z.number().int().nonnegative(),
|
|
10
|
+
mtime: z.string(),
|
|
11
|
+
});
|
|
12
|
+
export type Dbxv2CpkInfo = z.infer<typeof Dbxv2CpkInfoSchema>;
|
|
13
|
+
export const Dbxv2CpkListSchema = z.array(Dbxv2CpkInfoSchema);
|
|
14
|
+
|
|
15
|
+
export const Dbxv2IndexEntrySchema = z.object({
|
|
16
|
+
cpk: z.string(),
|
|
17
|
+
path: z.string(),
|
|
18
|
+
offset: z.number().int().nonnegative(),
|
|
19
|
+
size: z.number().int().nonnegative(),
|
|
20
|
+
extract_size: z.number().int().nonnegative(),
|
|
21
|
+
compressed: z.boolean(),
|
|
22
|
+
});
|
|
23
|
+
export type Dbxv2IndexEntry = z.infer<typeof Dbxv2IndexEntrySchema>;
|
|
24
|
+
|
|
25
|
+
export const Dbxv2FindResponseSchema = z.object({
|
|
26
|
+
total: z.number().int().nonnegative(),
|
|
27
|
+
offset: z.number().int().nonnegative(),
|
|
28
|
+
limit: z.number().int().positive(),
|
|
29
|
+
entries: z.array(Dbxv2IndexEntrySchema),
|
|
30
|
+
});
|
|
31
|
+
export type Dbxv2FindResponse = z.infer<typeof Dbxv2FindResponseSchema>;
|
|
32
|
+
|
|
33
|
+
export const Dbxv2ErrorBodySchema = z.object({ error: z.string() });
|
|
34
|
+
|
|
35
|
+
const Vec3 = z.tuple([z.number(), z.number(), z.number()]);
|
|
36
|
+
const Vec2 = z.tuple([z.number(), z.number()]);
|
|
37
|
+
|
|
38
|
+
/** Geometry and texture metadata of an assembled EMD (no pixels; the skeleton stays opaque). */
|
|
39
|
+
export const Dbxv2ModelPrimitiveSchema = z.looseObject({
|
|
40
|
+
name: z.string().default(""),
|
|
41
|
+
materialName: z.string().default(""),
|
|
42
|
+
positions: z.array(Vec3).default([]),
|
|
43
|
+
normals: z.array(Vec3).default([]),
|
|
44
|
+
uv: z.array(Vec2).default([]),
|
|
45
|
+
indices: z.array(z.number().int().nonnegative()).default([]),
|
|
46
|
+
textureIndex: z.number().int().nonnegative().nullish(),
|
|
47
|
+
});
|
|
48
|
+
export type Dbxv2ModelPrimitive = z.infer<typeof Dbxv2ModelPrimitiveSchema>;
|
|
49
|
+
|
|
50
|
+
export const Dbxv2ModelJsonSchema = z.looseObject({
|
|
51
|
+
name: z.string().default(""),
|
|
52
|
+
primitives: z.array(Dbxv2ModelPrimitiveSchema).default([]),
|
|
53
|
+
textures: z
|
|
54
|
+
.array(
|
|
55
|
+
z.looseObject({ width: z.number().int().positive(), height: z.number().int().positive() }),
|
|
56
|
+
)
|
|
57
|
+
.default([]),
|
|
58
|
+
skeleton: z.unknown().optional(),
|
|
59
|
+
});
|
|
60
|
+
export type Dbxv2ModelJson = z.infer<typeof Dbxv2ModelJsonSchema>;
|
package/src/texture.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed metadata of a texture served by `/png`. The server returns the decoded pixels as a
|
|
3
|
+
* lossless PNG at the native EMB dimensions; this module only reads the 33-byte PNG header (no
|
|
4
|
+
* pixel is decoded or resampled here), hashes the bytes, and refuses formats the server does not
|
|
5
|
+
* decode (GNF / PS4) instead of approximating them.
|
|
6
|
+
*/
|
|
7
|
+
import { Dbxv2Error, Dbxv2SchemaError } from "./errors";
|
|
8
|
+
|
|
9
|
+
export type Dbxv2ColorSpace = "srgb" | "linear";
|
|
10
|
+
export type Dbxv2AlphaMode = "straight" | "premultiplied";
|
|
11
|
+
|
|
12
|
+
/** Declared, not measured: the PNG carries no such flag. Override per call when a source differs. */
|
|
13
|
+
export interface Dbxv2TextureDeclaration {
|
|
14
|
+
readonly colorSpace?: Dbxv2ColorSpace;
|
|
15
|
+
readonly alpha?: Dbxv2AlphaMode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Dbxv2TextureInfo {
|
|
19
|
+
readonly format: "png";
|
|
20
|
+
/** Native dimensions, read from the PNG header (equal to the EMB entry dimensions). */
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly height: number;
|
|
23
|
+
readonly bitDepth: number;
|
|
24
|
+
/** PNG colour type (6 = RGBA, 2 = RGB, ...). */
|
|
25
|
+
readonly pngColorType: number;
|
|
26
|
+
readonly colorSpace: Dbxv2ColorSpace;
|
|
27
|
+
readonly alpha: Dbxv2AlphaMode;
|
|
28
|
+
/** Lowercase hex SHA-256 of the exact PNG bytes. */
|
|
29
|
+
readonly sha256: string;
|
|
30
|
+
readonly byteLength: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Dbxv2UnsupportedFormatError extends Dbxv2Error {
|
|
34
|
+
constructor(message: string) {
|
|
35
|
+
super("schema", message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
40
|
+
|
|
41
|
+
export function toHex(bytes: Uint8Array): string {
|
|
42
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
46
|
+
const copy = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
47
|
+
return toHex(new Uint8Array(await crypto.subtle.digest("SHA-256", copy as ArrayBuffer)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when the bytes start with the GNF (PlayStation 4) texture magic. */
|
|
51
|
+
export function isGnf(bytes: Uint8Array): boolean {
|
|
52
|
+
return bytes.length >= 4 && bytes[0] === 0x47 && bytes[1] === 0x4e && bytes[2] === 0x46;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Reads the IHDR of a PNG; throws when the bytes are not a PNG. GNF is reported explicitly. */
|
|
56
|
+
export async function describeTexture(
|
|
57
|
+
bytes: Uint8Array,
|
|
58
|
+
declaration: Dbxv2TextureDeclaration = {},
|
|
59
|
+
): Promise<Dbxv2TextureInfo> {
|
|
60
|
+
if (isGnf(bytes))
|
|
61
|
+
throw new Dbxv2UnsupportedFormatError(
|
|
62
|
+
"GNF (PS4) textures are not decoded: refusing to approximate them",
|
|
63
|
+
);
|
|
64
|
+
if (bytes.length < 33 || PNG_SIGNATURE.some((b, i) => bytes[i] !== b))
|
|
65
|
+
throw new Dbxv2SchemaError("texture is not a PNG");
|
|
66
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
67
|
+
if (view.getUint32(12) !== 0x49484452)
|
|
68
|
+
throw new Dbxv2SchemaError("PNG header chunk (IHDR) missing");
|
|
69
|
+
const width = view.getUint32(16);
|
|
70
|
+
const height = view.getUint32(20);
|
|
71
|
+
if (width === 0 || height === 0) throw new Dbxv2SchemaError("PNG has an empty dimension");
|
|
72
|
+
return {
|
|
73
|
+
format: "png",
|
|
74
|
+
width,
|
|
75
|
+
height,
|
|
76
|
+
bitDepth: bytes[24]!,
|
|
77
|
+
pngColorType: bytes[25]!,
|
|
78
|
+
colorSpace: declaration.colorSpace ?? "srgb",
|
|
79
|
+
alpha: declaration.alpha ?? "straight",
|
|
80
|
+
sha256: await sha256Hex(bytes),
|
|
81
|
+
byteLength: bytes.byteLength,
|
|
82
|
+
};
|
|
83
|
+
}
|