From b540ab781419cbaa693597bf89a2f62962d8295f Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:59:12 -0500 Subject: [PATCH] sk --- packages/website/src/hooks/useSkin.ts | 2 +- .../tanks/[id]/skins/[skin].glb.ts | 10 +- .../[...chunk_models_legendary]/.gitkeep | 0 .../[id]/_index.ts | 12 + .../[id]/skins/[skin].glb.ts | 237 ++++++++++++++++++ .../pages/[...chunk_models_tanks]/.gitkeep | 0 6 files changed, 255 insertions(+), 6 deletions(-) delete mode 100644 packages/website/src/pages/[...chunk_models_legendary]/.gitkeep create mode 100644 packages/website/src/pages/[...chunk_models_legendary]/[id]/_index.ts create mode 100644 packages/website/src/pages/[...chunk_models_legendary]/[id]/skins/[skin].glb.ts delete mode 100644 packages/website/src/pages/[...chunk_models_tanks]/.gitkeep diff --git a/packages/website/src/hooks/useSkin.ts b/packages/website/src/hooks/useSkin.ts index a18c47a44..6514d2482 100644 --- a/packages/website/src/hooks/useSkin.ts +++ b/packages/website/src/hooks/useSkin.ts @@ -27,7 +27,7 @@ async function loadSkin(path: string) { } function skinPath(id: number, skin: number) { - return alias("api", `/tanks/${id}/skins/${skin}.glb`); + return alias("models_legendary", `/${id}/skins/${skin}.glb`); } export function useSkin(id: number) { diff --git a/packages/website/src/pages/[...chunk_api]/tanks/[id]/skins/[skin].glb.ts b/packages/website/src/pages/[...chunk_api]/tanks/[id]/skins/[skin].glb.ts index bee9b21f9..d755f5062 100644 --- a/packages/website/src/pages/[...chunk_api]/tanks/[id]/skins/[skin].glb.ts +++ b/packages/website/src/pages/[...chunk_api]/tanks/[id]/skins/[skin].glb.ts @@ -16,11 +16,11 @@ import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions"; import type { APIContext } from "astro"; import type { Vector3Tuple, Vector4Tuple } from "three"; -import { SKIN_NUMBER } from "../../../../../constants/skinNumber"; -import { api } from "../../../../../core/blitzkit/api"; -import { mixStaticPaths } from "../../../../../core/blitzkit/mixStaticPaths"; -import { vfs } from "../../../../../core/blitzkit/vfs"; -import { getStaticPaths as _getStaticPaths } from "../../../_index"; +import { SKIN_NUMBER } from "../../../../constants/skinNumber"; +import { api } from "../../../../core/blitzkit/api"; +import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths"; +import { vfs } from "../../../../core/blitzkit/vfs"; +import { getStaticPaths as _getStaticPaths } from "../../../[...chunk_api]/_index"; import { extractMaterials, extractModel } from "../model.glb"; const SKIN_NUMBER_SLOTS = 5; diff --git a/packages/website/src/pages/[...chunk_models_legendary]/.gitkeep b/packages/website/src/pages/[...chunk_models_legendary]/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/website/src/pages/[...chunk_models_legendary]/[id]/_index.ts b/packages/website/src/pages/[...chunk_models_legendary]/[id]/_index.ts new file mode 100644 index 000000000..15a341f98 --- /dev/null +++ b/packages/website/src/pages/[...chunk_models_legendary]/[id]/_index.ts @@ -0,0 +1,12 @@ +import { api } from "../../../core/blitzkit/api"; +import { mixStaticPaths } from "../../../core/blitzkit/mixStaticPaths"; +import { getStaticPaths as _getStaticPaths } from "../_index"; + +export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => { + const tanks = await api.tankDefinitions(); + + return Object.values(tanks.tanks).map((tank) => ({ + params: { id: tank.id }, + props: { id: tank.id }, + })); +}); diff --git a/packages/website/src/pages/[...chunk_models_legendary]/[id]/skins/[skin].glb.ts b/packages/website/src/pages/[...chunk_models_legendary]/[id]/skins/[skin].glb.ts new file mode 100644 index 000000000..24e863e69 --- /dev/null +++ b/packages/website/src/pages/[...chunk_models_legendary]/[id]/skins/[skin].glb.ts @@ -0,0 +1,237 @@ +import { + parseHierarchies, + Sc2ReadStream, + type CamouflagesXml, + type CamouflagesYaml, + type TankParameters, +} from "@blitzkit/core"; +import { + Document, + Node, + NodeIO, + PropertyType, + Scene, +} from "@gltf-transform/core"; +import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; +import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions"; +import type { APIContext } from "astro"; +import type { Vector3Tuple, Vector4Tuple } from "three"; +import { SKIN_NUMBER } from "../../../../constants/skinNumber"; +import { api } from "../../../../core/blitzkit/api"; +import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths"; +import { vfs } from "../../../../core/blitzkit/vfs"; +import { + extractMaterials, + extractModel, +} from "../../../[...chunk_models_tanks]/[id]/model.glb"; +import { getStaticPaths as _getStaticPaths } from "../_index"; + +const SKIN_NUMBER_SLOTS = 5; +const DIGIT_COLUMNS = 5; +const DIGIT_ROWS = 2; + +export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => { + const camouflages = await api.camouflageDefinitions(); + + return Object.values(camouflages.camouflages) + .filter((camouflage) => camouflage.tank_id) + .map((camouflage) => ({ + params: { id: camouflage.tank_id!, skin: camouflage.id }, + props: { skin: camouflage.id }, + })); +}); + +const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>( + "Data/XML/item_defs/vehicles/common/camouflages.xml", +); +const camouflagesYaml = await vfs.yaml( + "Data/camouflages.yaml", +); +const customization = await vfs.yaml< + Record +>("Data/3d/Customization.yaml"); + +export async function GET({ props }: APIContext<{ skin: number }>) { + const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS); + + const items = new Map( + Object.values(customization).map(({ Name, Path }) => [ + Name, + Path.replace("~res:/3d/", "").replace(/\.sc2$/, ""), + ]), + ); + const key = Object.keys(camouflagesXml.root.camouflages).find( + (key) => camouflagesXml.root.camouflages[key].id === props.skin, + )!; + const include = camouflagesXml.root.camouflages[key].vehicleFilter.include; + const [nation, tankKey] = ( + include as { vehicle: { name: string } } + ).vehicle.name.split(":"); + const { preset, customEntities } = camouflagesYaml[key]; + const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1); + const parameters = await vfs.yaml( + `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, + ); + const basePath = parameters.resourcesPath.blitzModelPath.replace( + /\.sc2$/, + "", + ); + const sc2 = new Sc2ReadStream( + (await vfs.file(`Data/3d/${basePath}.sc2`)).buffer as ArrayBuffer, + ).sc2(); + const slots: Record< + string, + { + target: string; + position: Vector3Tuple; + rotation: Vector4Tuple; + scale: Vector3Tuple; + } + > = {}; + const states: Record = {}; + + parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => { + for (const component of components) { + if (component["comp.typename"] === "NewSlotComponent") { + for (const slot of Object.values(component.slots)) { + slots[slot.name] = { + target: hierarchy.name, + position: slot.transform.position, + rotation: slot.transform.rotation, + scale: slot.transform.scale, + }; + } + } + + if (component["comp.typename"] === "SlotComponent") { + slots[component["sc.slotName"]] = { + target: hierarchy.name, + position: component["sc.attachmentTranslation"] as Vector3Tuple, + rotation: component["sc.attachmentRotation"] as Vector4Tuple, + scale: component["sc.attachmentScale"] as Vector3Tuple, + }; + } + + if (component["comp.typename"] !== "StateSwitcherComponent") continue; + + const active = component[`ssc.state${component["ssc.activeState"]}`]; + // camos without their own variant keep the default piece, but groups + // of elements to hide stay hidden + let state: string | null = + active && !/skin\d*/.test(active) && !active.includes("hide") + ? active + : null; + + for (let index = 0; index < component["ssc.statesCount"]; index++) { + const name = component[`ssc.state${index}`]; + const match = name.match(/skin(\d*)/); + + if ( + match && + !name.includes("hide") && + Number(match[1] || 1) === skinNumber + ) { + state = name; + } + } + + states[hierarchy.name] = state; + } + + return null; + }); + + const document = new Document(); + const scene = document.createScene().setExtras({ states }); + const targets = new Map(); + + document.createBuffer(); + + if (preset) await extractMaterials(document, sc2, basePath, preset); + + for (const name in customEntities) { + const slot = slots[name]; + const path = items.get(customEntities[name].item); + + if ( + !slot || + !path || + !(await vfs.resolve(`Data/3d/${path}.sc2`)) || + !(await vfs.resolve(`Data/3d/${path}.scg`)) + ) { + continue; + } + + const part = await extractModel(vfs, path, true); + const map = mergeDocuments(document, part); + const partScene = map.get(part.getRoot().listScenes()[0]) as Scene; + const node = document + .createNode(name) + .setTranslation(slot.position) + .setRotation(slot.rotation) + .setScale(slot.scale); + let target = targets.get(slot.target); + + if (!target) { + target = document.createNode(slot.target); + targets.set(slot.target, target); + scene.addChild(target); + } + + for (const child of partScene.listChildren()) { + node.addChild(child as Node); + } + + partScene.dispose(); + target.addChild(node); + } + + applySkinNumber(document, SKIN_NUMBER); + + await document.transform( + dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.TEXTURE] }), + unpartition(), + ); + + return new Response(await nodeIO.writeBinary(document)); +} + +function applySkinNumber(document: Document, number: string) { + const start = Math.ceil((SKIN_NUMBER_SLOTS - number.length) / 2) + 1; + + for (const node of document.getRoot().listNodes()) { + const match = node.getName().match(/_number_\d+_(\d+)$/); + + if (!match) continue; + + const digit = number[Number(match[1]) - start]; + + if (digit === undefined) { + node.dispose(); + continue; + } + + const cell = (Number(digit) + 9) % 10; + const column = cell % DIGIT_COLUMNS; + const row = Math.floor(cell / DIGIT_COLUMNS); + + node.traverse((child) => { + for (const primitive of child.getMesh()?.listPrimitives() ?? []) { + const uvs = primitive.getAttribute("TEXCOORD_0")?.clone(); + + if (!uvs) continue; + + for (let index = 0; index < uvs.getCount(); index++) { + const [u, v] = uvs.getElement(index, [0, 0]) as [number, number]; + + uvs.setElement(index, [ + (column + u) / DIGIT_COLUMNS, + (row + v) / DIGIT_ROWS, + ]); + } + + primitive.setAttribute("TEXCOORD_0", uvs); + } + }); + } +} diff --git a/packages/website/src/pages/[...chunk_models_tanks]/.gitkeep b/packages/website/src/pages/[...chunk_models_tanks]/.gitkeep deleted file mode 100644 index e69de29bb..000000000