New modding system

This commit is contained in:
2026-09-26 17:33:07 -03:00
parent 86d5c39eab
commit 243ff52063
25 changed files with 1089 additions and 512 deletions
+64
View File
@@ -0,0 +1,64 @@
// the texture atlas: the engine's textures and every mod's in one image, built by the client when it joins a server
// from the textures in the server's .bmod files
import { TEXTURE_SIZE } from "$/common/constants.ts";
import type { SpriteRegion } from "$/common/constants.ts";
export interface AtlasLayout {
// in pixels, a power of two
size: number;
// in sprites, by texture id. engine:missing is always at 0, 0
regions: Record<string, SpriteRegion>;
}
// engine:missing first, then every other texture sorted by id, row by row
export function atlas_layout(ids: Iterable<string>): AtlasLayout {
const sorted = [...new Set(ids)].filter((id) => id !== "engine:missing").sort((a, b) => a.localeCompare(b));
const count = sorted.length + 1;
const size = 2 ** Math.ceil(Math.log2(Math.ceil(Math.sqrt(count)) * TEXTURE_SIZE));
const per_row = size / TEXTURE_SIZE;
const regions: Record<string, SpriteRegion> = { "engine:missing": { x: 0, y: 0 } };
sorted.forEach((id, i) => {
regions[id] = { x: (i + 1) % per_row, y: Math.floor((i + 1) / per_row) };
});
return { size, regions };
}
// textures are png bytes by id. ones that don't decode show as missing
export async function build_atlas(textures: Map<string, Uint8Array>) {
const { size, regions } = atlas_layout(textures.keys());
const canvas = new OffscreenCanvas(size, size);
const ctx = canvas.getContext("2d")!;
// magenta and black checker
ctx.fillStyle = "magenta";
ctx.fillRect(0, 0, 8, 8);
ctx.fillRect(8, 8, 8, 8);
ctx.fillStyle = "black";
ctx.fillRect(8, 0, 8, 8);
ctx.fillRect(0, 8, 8, 8);
await Promise.all([...textures].map(async ([id, png]) => {
const region = regions[id];
try {
const image = await createImageBitmap(new Blob([png as Uint8Array<ArrayBuffer>], { type: "image/png" }));
ctx.drawImage(image, region.x * TEXTURE_SIZE, region.y * TEXTURE_SIZE);
image.close();
} catch {
console.warn(`Texture ${id} isn't a png that can be shown, it will show as missing`);
regions[id] = regions["engine:missing"];
}
}));
return { image: canvas.transferToImageBitmap(), regions };
}
// the engine's own textures, which come with the client instead of a mod. see build_engine_textures in build.ts
export async function engine_textures(): Promise<Map<string, Uint8Array>> {
const names: string[] = await (await fetch("/assets/textures/index.json")).json();
const textures = new Map<string, Uint8Array>();
await Promise.all(names.map(async (name) => {
const response = await fetch(`/assets/textures/${name}.png`);
textures.set(`engine:${name}`, new Uint8Array(await response.arrayBuffer()));
}));
return textures;
}