diff --git a/.github/workflows/build-bot.yml b/.github/workflows/build-bot.yml index 14c9cb343..501cd703f 100644 --- a/.github/workflows/build-bot.yml +++ b/.github/workflows/build-bot.yml @@ -28,7 +28,7 @@ jobs: run: bun run build:bot - name: Archive build - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: bot path: dist/bot diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 623200286..1c739206c 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -46,14 +46,14 @@ jobs: - uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GH_TOKEN }} - - uses: actions/configure-pages@v5 + - uses: actions/configure-pages@v6 - run: bun run setup - name: Build website run: bun run build:website - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: dist/website @@ -69,4 +69,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/docs/changelogs/1.30.3.md b/docs/changelogs/1.30.3.md new file mode 100644 index 000000000..69137dcac --- /dev/null +++ b/docs/changelogs/1.30.3.md @@ -0,0 +1,18 @@ +# BlitzKit 1.30.3 + +Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions this update. + +## Features + +- [GonnaHetzMe] Added tooltips to the module selector in Tankopedia + +## Fixes + +- Added aggressive re-rendering for the Guess mini-game's score readout to make inspect-element harder +- Removed tank visual and collision model caching +- Guesser once again breaks streaks if not all tiers are selected +- BlitzKit now properly unloads tank models from memory when visually hidden in Guesser +- [GonnaHetzMe] Tanks with complex transparency like the Herdbreaker once again handle opacity correctly +- [GonnaHetzMe] Pieces of tank models that have transparency now handle culling properly (they're visible from both sides) +- [GonnaHetzMe] Tanks with "cardboard" textures have been fixed, thanks to better PVR file parsing +- [GonnaHetzMe] Tank icons are now scaled correctly so small tanks are now properly zoomed out in contrast to bigger ones diff --git a/package.json b/package.json index ed634eb22..3fa646324 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkit", - "version": "1.30.2", + "version": "1.30.3", "license": "BlitzKit License", "private": true, "scripts": { diff --git a/packages/core/src/streams/pvr.ts b/packages/core/src/streams/pvr.ts index 979b9c396..8703f5cd7 100644 --- a/packages/core/src/streams/pvr.ts +++ b/packages/core/src/streams/pvr.ts @@ -1,5 +1,5 @@ -import { clamp, isEqual, times } from 'lodash-es'; -import { ReadStream } from './buffer'; +import { clamp, isEqual, times } from "lodash-es"; +import { ReadStream } from "./buffer"; export enum PvrFlags { NoFlag = 0, @@ -96,7 +96,7 @@ export class PvrReadStream extends ReadStream { const header = this.header(); this.metadata(); - if (typeof header.pixelFormat === 'number') { + if (typeof header.pixelFormat === "number") { switch (header.pixelFormat) { default: throw new TypeError( @@ -142,12 +142,12 @@ export class PvrReadStream extends ReadStream { times(pixelCount, (index) => { const buffer = this.consumeUint8Array(2); - const bufferIndex = index * 4; + const bufferIndex = index * 3; const r = ((buffer[1] & 0b11111000) >>> 3) / 31; const g = - ((buffer[1] & 0b111) << 3) | - (((buffer[0] & 0b11100000) >>> 5) / 31); - const b = buffer[0] & 0b11111; + (((buffer[1] & 0b111) << 3) | ((buffer[0] & 0b11100000) >>> 5)) / + 63; + const b = (buffer[0] & 0b11111) / 31; data[bufferIndex] = Math.round(clamp(r * 255, 0, 255)); data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255)); @@ -186,7 +186,7 @@ export class PvrReadStream extends ReadStream { metadataSize: this.uint32(), }; - if (header.version !== 'PVR\x03') throw new TypeError('Endian mismatch'); + if (header.version !== "PVR\x03") throw new TypeError("Endian mismatch"); return header; } @@ -205,19 +205,19 @@ export class PvrReadStream extends ReadStream { resolveBitRatePixelFormat( pixelFormat: Exclude, number>, ) { - if (pixelFormat.order === 'rgba') { + if (pixelFormat.order === "rgba") { if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) { return ResolvedBitRatePixelFormat.R4G4B4A4; } else throw new TypeError( - `Unhandled rgba bit rate ${pixelFormat.bitRates.join(', ')}`, + `Unhandled rgba bit rate ${pixelFormat.bitRates.join(", ")}`, ); - } else if (pixelFormat.order === 'rgb\0') { + } else if (pixelFormat.order === "rgb\0") { if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) { return ResolvedBitRatePixelFormat.R5G6B5A0; } else throw new TypeError( - `Unhandled rgb bit rate ${pixelFormat.bitRates.join(', ')}`, + `Unhandled rgb bit rate ${pixelFormat.bitRates.join(", ")}`, ); } else { throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`); diff --git a/packages/core/src/streams/sc2.ts b/packages/core/src/streams/sc2.ts index eb5ebccd7..8b4837bab 100644 --- a/packages/core/src/streams/sc2.ts +++ b/packages/core/src/streams/sc2.ts @@ -15,6 +15,9 @@ type DataNode = { qualityGroup?: string; fxName?: string; textures?: Textures; + customCullMode?: number; + enabledPresets?: EnabledPresets; + properties?: Properties; } & ( | ({ configCount: number; @@ -24,7 +27,7 @@ type DataNode = { } ); -interface ConfigArchive { +export interface ConfigArchive { configName: string; fxName: string; properties: Properties; diff --git a/packages/scripts/src/buildAssets/tankIcons.ts b/packages/scripts/src/buildAssets/tankIcons.ts index 3a0fbfc22..fb74388d9 100644 --- a/packages/scripts/src/buildAssets/tankIcons.ts +++ b/packages/scripts/src/buildAssets/tankIcons.ts @@ -1,6 +1,7 @@ import { NATION_IDS } from "@blitzkit/core"; import sharp from "sharp"; import type { Vector3Tuple } from "three"; +import { parsePackedSpriteRect } from "../core/blitz/parsePackedSpriteRect"; import { AssetUploader } from "../core/github/assetUploader"; import { vfs } from "./constants"; import type { VehicleDefinitionList } from "./definitions"; @@ -37,6 +38,45 @@ export interface TankParameters { }; } +async function extractPackedTankIcon(packedBuffer: Buffer, context: string) { + const texture = sharp(packedBuffer); + const spriteRect = parsePackedSpriteRect(packedBuffer); + + if (!spriteRect) { + throw new Error(`Missing RIFF sprite bounds for ${context}`); + } + + const [left, top, width, height] = spriteRect; + const bounds = [left, top, width, height]; + + if (!bounds.every(Number.isInteger)) { + throw new Error( + `Invalid RIFF sprite bounds for ${context}: ${bounds.join(" ")}`, + ); + } + + const metadata = await texture.metadata(); + + if (metadata.width === undefined || metadata.height === undefined) { + throw new Error(`Failed to read image dimensions for ${context}`); + } + + if ( + left < 0 || + top < 0 || + width <= 0 || + height <= 0 || + left + width > metadata.width || + top + height > metadata.height + ) { + throw new Error( + `Out-of-bounds RIFF sprite for ${context}: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`, + ); + } + + return await texture.extract({ left, top, width, height }).toBuffer(); +} + export async function tankIcons() { console.log("Building tank icons..."); @@ -67,12 +107,14 @@ export async function tankIcons() { const bigPath = `Data/${parameters.resourcesPath.bigIconPath .replace(/~res:\//, "") .replace(/\..+/, "")}.packed.webp`; - const big = await sharp(await vfs.file(bigPath)) - .trim() - .toBuffer(); - const small = await sharp(await vfs.file(smallPath)) - .trim() - .toBuffer(); + const big = await extractPackedTankIcon( + await vfs.file(bigPath), + `tank ${tankKey} big`, + ); + const small = await extractPackedTankIcon( + await vfs.file(smallPath), + `tank ${tankKey} small`, + ); if (big) { await uploader.add({ diff --git a/packages/scripts/src/core/blitz/extractModel/index.ts b/packages/scripts/src/core/blitz/extractModel/index.ts index 846dd80c9..3139a8608 100644 --- a/packages/scripts/src/core/blitz/extractModel/index.ts +++ b/packages/scripts/src/core/blitz/extractModel/index.ts @@ -1,5 +1,6 @@ import { bufferToBigInt, + ConfigArchive, Hierarchy, Sc2ReadStream, ScgReadStream, @@ -75,6 +76,42 @@ export async function extractModel(vfs: AbstractVFS, path: string) { .setImage(baseColor), ); + let defaultConfigArchive: ConfigArchive | undefined = undefined; + + if (node.configCount) { + const names = times( + node.configCount, + (index) => node[`configArchive_${index}`], + ); + + defaultConfigArchive = names.find( + (archive) => archive.configName === "Default", + ); + defaultConfigArchive ??= node.configArchive_0; + } + + const customCullMode = + node.customCullMode ?? defaultConfigArchive?.customCullMode; + + if (customCullMode === 0) { + material.setDoubleSided(true); + } else if (customCullMode !== undefined) { + material.setDoubleSided(false); + } + + if ( + node.enabledPresets?.AlphaTest && + node.properties?.alphatestThreshold + ) { + const view = new DataView(node.properties.alphatestThreshold); + const alphaCutoff = + view.byteLength < 4 + ? 0.5 + : view.getFloat32(view.byteLength - 4, true); + + material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff); + } + if (node.configCount) { for ( let configIndex = 0; @@ -85,14 +122,17 @@ export async function extractModel(vfs: AbstractVFS, path: string) { if ( archive.configName !== "Default" || - !archive.enabledPresets?.AlphaTest + !archive.enabledPresets?.AlphaTest || + !archive.properties?.alphatestThreshold ) { continue; } const view = new DataView(archive.properties.alphatestThreshold); - const _ = view.getUint8(0); // only god knows what these flags are - const alphaCutoff = view.getFloat32(1, true); + const alphaCutoff = + view.byteLength < 4 + ? 0.5 + : view.getFloat32(view.byteLength - 4, true); material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff); } @@ -171,8 +211,8 @@ export async function extractModel(vfs: AbstractVFS, path: string) { case "TransformComponent": { const localTranslation = component["tc.localTranslation"]; - // The game resets top-level node translation to [0, 0, 0] on load. - // Child nodes must keep their authored local offsets. + // The game resets top-level node translation to [0, 0, 0] on load. + // Child nodes must keep their authored local offsets. node.setTranslation( parent instanceof Scene ? [0, 0, 0] : localTranslation, ); @@ -284,4 +324,4 @@ export async function extractModel(vfs: AbstractVFS, path: string) { await document.transform(prune({ keepAttributes: true }), dedup()); return document; -} \ No newline at end of file +} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx index 937c216d3..a290cf245 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx @@ -18,6 +18,7 @@ import { Heading, IconButton, Text, + Tooltip, } from "@radix-ui/themes"; import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions"; import { useLocale } from "../../../../hooks/useLocale"; @@ -55,12 +56,14 @@ function ModuleButton({ selected, unlock, top, + tooltip, onClick, }: { unlock: Unlock; tier: number; selected: boolean; top: boolean; + tooltip?: string; onClick: () => void; }) { const isTank = unlock.type === ModuleType.MODULE_TYPE_VEHICLE; @@ -134,7 +137,7 @@ function ModuleButton({ ); - return isTank ? ( + const node = isTank ? ( {node}; } const tankDefinitions = await awaitableTankDefinitions; @@ -167,7 +174,7 @@ export function Modules() { const topGun = topTurret.guns.at(-1)!; const topEngine = tank.engines.at(-1)!; const topTrack = tank.tracks.at(-1)!; - const { strings } = useLocale(); + const { strings, unwrap } = useLocale(); function setByUnlock(unlock: Unlock) { Duel.mutate((draft) => { @@ -249,6 +256,8 @@ export function Modules() { if (module === undefined) return null; + const tooltip = unwrap(module.name!); + return (