From 8367fb32158f1d04be4b4568d32ec2ef7da64ecc Mon Sep 17 00:00:00 2001 From: nickid2018 Date: Sun, 30 Jun 2024 17:54:59 +0800 Subject: [PATCH 1/5] New implementation for BSR API --- .gitignore | 2 + src/routes/renderer.ts | 493 ++++++++++++++++++++++++----------------- 2 files changed, 286 insertions(+), 209 deletions(-) diff --git a/.gitignore b/.gitignore index c6bba59..7222a96 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +.idea/ \ No newline at end of file diff --git a/src/routes/renderer.ts b/src/routes/renderer.ts index 362fc3e..e0983e7 100644 --- a/src/routes/renderer.ts +++ b/src/routes/renderer.ts @@ -2,69 +2,55 @@ import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' -import blockStateData from '../data/renderer/block_states.json' -import blockModelData from '../data/renderer/block_models.json' -import textureAtlasData from '../data/renderer/atlas.json' -import blockRenderTypeData from '../data/renderer/block_render_type.json' -import blockOcclusionShapeData from '../data/renderer/block_occlusion_shape.json' -import specialBlocksData from '../data/renderer/special.json' -import liquidComputationData from '../data/renderer/block_liquid_computation.json' +import _blockStateData from '../data/renderer/block_states.json' +import _blockModelData from '../data/renderer/block_models.json' +import _textureAtlasData from '../data/renderer/atlas.json' +import _blockRenderTypeData from '../data/renderer/block_render_type.json' +import _blockOcclusionShapeData from '../data/renderer/block_occlusion_shape.json' +import _specialBlocksData from '../data/renderer/special.json' +import _liquidComputationData from '../data/renderer/block_liquid_computation.json' + +const blockStateData = _blockStateData as unknown as Record +const blockModelData = _blockModelData as unknown as Record +const textureAtlasData = _textureAtlasData as unknown as Record +const blockRenderTypeData = _blockRenderTypeData as unknown as Record +const blockOcclusionShapeData = _blockOcclusionShapeData as unknown as Record< + string, + OcclusionFaceData +> +const specialBlocksData = _specialBlocksData as unknown as Record +const liquidComputationData = _liquidComputationData as unknown as Record< + string, + LiquidComputationData +> const app = new Hono() -const querySchema = z.record(z.string()) +// Query Schema ------------------------------------------------------------------------------------ -// Block Model Structure - -export interface BlockModel { - elements?: ModelElement[] -} - -export interface ModelElement { - from: number[] - to: number[] - rotation?: ModelRotation - shade?: boolean - faces: { - down?: ModelFace - up?: ModelFace - north?: ModelFace - south?: ModelFace - west?: ModelFace - east?: ModelFace - } -} - -export interface ModelFace { - texture: number - uv?: number[] - rotation?: number - tintindex?: number - cullface?: string -} +const BlockState = z.object({ + name: z.string(), + properties: z.record(z.string()).optional(), +}) +type BlockState = z.infer -// Block State Structure +const querySchema = z.array(BlockState) -export interface ModelRotation { - origin: number[] - axis: 'x' | 'y' | 'z' | string - angle: number - rescale?: boolean -} +// Data Definitions -------------------------------------------------------------------------------- -export interface BlockStateModelCollection { +interface BlockStateModelCollection { variants?: Record multipart?: ConditionalPart[] } -export interface ModelReference { +interface ModelReference { model: number uvlock?: boolean x?: number y?: number } -export interface ModelReferenceWithWeight { +interface ModelReferenceWithWeight { model: number uvlock?: boolean x?: number @@ -72,30 +58,20 @@ export interface ModelReferenceWithWeight { weight?: number } -export interface ConditionalPart { +interface ConditionalPart { apply: ModelReference | ModelReferenceWithWeight[] - when?: Record | AndCondition | OrCondition + when?: Record | AndCondition | OrCondition } -export interface AndCondition { - AND: (Record | AndCondition | OrCondition)[] +interface AndCondition { + AND: (Record | AndCondition | OrCondition)[] } -export interface OrCondition { - OR: (Record | AndCondition | OrCondition)[] +interface OrCondition { + OR: (Record | AndCondition | OrCondition)[] } -// Block Texture Structure - -export interface AnimatedTexture { - frames: number[] - time: number[] - interpolate?: boolean -} - -// Occlusion Face Structure - -export interface OcclusionFaceData { +interface OcclusionFaceData { down?: number[][] up?: number[][] north?: number[][] @@ -105,183 +81,282 @@ export interface OcclusionFaceData { can_occlude: boolean } -// Liquid Computation Data - -export interface LiquidComputationData { +interface LiquidComputationData { blocks_motion: boolean face_sturdy: string[] } -app.get('/', zValidator('query', querySchema), (ctx) => { - const query = ctx.req.valid('query') - - const foundBlocks: string[] = [] - const foundBlockRenderType: Record = {} - const foundBlockStates: Record = {} - const foundBlockModel: Record = {} - const foundTextureAtlas: Record = {} - const foundOcclusionShape: Record = {} - const foundSpecialBlocksData: Record = {} - const foundLiquidComputationData: Record = {} - - for (const [k, v] of Object.entries(query)) { - const modelKey = new Set() - const atlasKey = new Set() - // Split tint data - const tintDataSplitPoint = v.indexOf('!') - let tint: string | null = null - let unresolvedBlockState: string - - if (tintDataSplitPoint !== -1) { - tint = v.substring(tintDataSplitPoint + 1) - unresolvedBlockState = v.substring(0, tintDataSplitPoint - 1) - } else { - unresolvedBlockState = v - } +interface BlockModel { + elements?: ModelElement[] +} - // Sort block properties - const splitPoint = unresolvedBlockState.indexOf('[') - let block: string - let state - let nonWaterLoggedState - if (splitPoint !== -1) { - block = unresolvedBlockState.substring(0, splitPoint - 1) - const stateList = unresolvedBlockState.substring(splitPoint + 1, -2).split(',') - const unsortedStateMap: Record = {} - const stateNameList: string[] = [] - for (const state of stateList) { - const splitNamePoint = state.indexOf('=') - const stateName = state.substring(0, splitNamePoint - 1) - const stateValue = state.substring(splitNamePoint + 1) - unsortedStateMap[stateName] = stateValue - stateNameList.push(stateName) - } - stateNameList.sort() - const sortedStateList: string[] = [] - const sortedStateListNonWaterLogged: string[] = [] - for (const stateName of stateNameList) { - sortedStateList.push(stateName + '=' + unsortedStateMap[stateName]) - if (stateName === 'waterlogged') - sortedStateListNonWaterLogged.push(stateName + '=' + unsortedStateMap[stateName]) - else { - foundSpecialBlocksData['water'] = specialBlocksData['water'] - specialBlocksData.water.forEach((texture) => atlasKey.add(texture.toString())) +interface ModelElement { + from: number[] + to: number[] + rotation?: never + shade?: boolean + faces: { + down?: ModelFace + up?: ModelFace + north?: ModelFace + south?: ModelFace + west?: ModelFace + east?: ModelFace + } +} + +interface ModelFace { + texture: number + uv?: number[] + rotation?: number + tintindex?: number + cullface?: string +} + +interface AnimatedTexture { + frames: number[] + time: number[] + interpolate?: boolean +} + +// Model Selection Algorithm ----------------------------------------------------------------------- + +function conditionMatch( + condition: Record | AndCondition | OrCondition, + blockProperties: Record, +): boolean { + if ('AND' in condition) { + return (condition as AndCondition).AND.every( + (part: Record | AndCondition | OrCondition) => + conditionMatch(part, blockProperties), + ) + } else if ('OR' in condition) { + return (condition as OrCondition).OR.some( + (part: Record | AndCondition | OrCondition) => + conditionMatch(part, blockProperties), + ) + } else { + return Object.entries(condition).every(([key, value]) => { + const reversed = value.startsWith('!') + if (reversed) value = value.slice(1) + return value.split('|').includes(blockProperties[key]) !== reversed + }) + } +} + +function chooseModel( + blockState: BlockState, +): (ModelReference | ModelReferenceWithWeight[])[] | null { + const modelCollection = blockStateData[blockState.name] + + if (!modelCollection) return null + if (!modelCollection.variants && !modelCollection.multipart) return null + + const blockProperties = blockState.properties ?? {} + if (modelCollection.variants) { + for (const [key, value] of Object.entries(modelCollection.variants)) { + const stateCondition = key.split(',') + let match = true + for (const condition of stateCondition) { + const [key, value] = condition.split('=') + if (blockProperties[key] !== value) { + match = false + break } } - state = '[' + sortedStateList.join(',') + ']' - nonWaterLoggedState = '[' + sortedStateListNonWaterLogged.join(',') + ']' - } else { - if ( - unresolvedBlockState == 'seagrass' || - unresolvedBlockState == 'kelp' || - unresolvedBlockState == 'bubble_column' - ) { - foundSpecialBlocksData['water'] = specialBlocksData['water'] - specialBlocksData.water.forEach((texture) => atlasKey.add(texture.toString())) - } - block = unresolvedBlockState - state = '' - nonWaterLoggedState = '' + if (match) return [value] } + } else { + const matchingPart = modelCollection.multipart!.filter((part) => + conditionMatch(part.when ?? {}, blockProperties), + ) + if (matchingPart.length === 0) return null + return matchingPart.map((part) => part.apply) + } - const resolvedBlockState = tint !== null ? block + state + '!' + tint : block + state + return null +} - foundBlocks.push(k + '=' + resolvedBlockState) +// Response Scheme --------------------------------------------------------------------------------- - if (keyInObject(block, blockRenderTypeData)) { - foundBlockRenderType[block] = blockRenderTypeData[block] - } +interface StateData { + parts: (ModelReference | ModelReferenceWithWeight[])[] + models: number[] + textures: number[] + render_type: string + face_sturdy: string[] + blocks_motion: boolean + occlusion: boolean + occlusion_shape: Record + special_textures: number[] +} - if (keyInObject(block, specialBlocksData)) { - foundSpecialBlocksData[block] = specialBlocksData[block] - } +interface ResponseSchema { + states: { + state: BlockState + parts: (ModelReference | ModelReferenceWithWeight[])[] + render_type: string + face_sturdy: string[] + blocks_motion: boolean + occlusion: boolean + occlusion_shape: Record + special_textures: number[] + }[] + models: Record + textures: Record +} - if (keyInObject(block, blockStateData)) { - foundBlockStates[block] = blockStateData[block] - } +function stateToString(blockState: BlockState): string { + if (blockState.properties) { + return `${blockState.name}[${Object.entries(blockState.properties) + .sort(([key1], [key2]) => key1.localeCompare(key2)) + .map(([key, value]) => `${key}=${value}`) + .join(',')}]` + } else { + return blockState.name + } +} - getModelForBlock(foundBlockStates[block]).forEach((value) => modelKey.add(value)) +function stateToStringFilteredWaterLogged(blockState: BlockState): string { + const filtered = Object.entries(blockState.properties ?? {}).filter( + ([key]) => key !== 'waterlogged', + ) + if (filtered.length > 0) { + return `${blockState.name}[${filtered + .sort(([key1], [key2]) => key1.localeCompare(key2)) + .map(([key, value]) => `${key}=${value}`) + .join(',')}]` + } else { + return blockState.name + } +} - for (const key of modelKey) { - if (keyInObject(key, blockModelData)) { - foundBlockModel[key] = blockModelData[key] - const blockModel: BlockModel = blockModelData[key] - if (!blockModel.elements) continue +// Cache ------------------------------------------------------------------------------------------- + +const EMPTY_STATE_DATA: StateData = { + parts: [], + models: [], + textures: [], + render_type: 'solid', + face_sturdy: [], + blocks_motion: false, + occlusion: false, + occlusion_shape: {}, + special_textures: [], +} +const RESPONSE_CACHE = new Map() + +function findOrMakeData(blockState: BlockState): StateData { + const stateString = stateToString(blockState) + if (RESPONSE_CACHE.has(stateString)) { + return RESPONSE_CACHE.get(stateString)! + } else { + const data = makeStateData(blockState) + RESPONSE_CACHE.set(stateString, data) + return data + } +} - for (const element of blockModel.elements) { - for (const face of Object.values(element.faces)) { - atlasKey.add(face.texture.toString()) - } - } - } - } +function makeStateData(blockState: BlockState): StateData { + const stateString = stateToStringFilteredWaterLogged(blockState) + const stateName = blockState.name - for (const key of atlasKey) { - if (keyInObject(key, textureAtlasData)) { - foundTextureAtlas[key] = textureAtlasData[key] + const parts = chooseModel(blockState) + const specials = specialBlocksData[stateName] + const occlusionShape = blockOcclusionShapeData[stateString] ?? { can_occlude: false } + const liquidComputation = liquidComputationData[stateString] ?? { + blocks_motion: false, + face_sturdy: [], + } - const atlasData: AnimatedTexture | number[] = textureAtlasData[key] + if (!parts && !specials && !occlusionShape.can_occlude && !liquidComputation.blocks_motion) + return EMPTY_STATE_DATA - if (!(atlasData instanceof Array)) { - for (const frame of atlasData.frames) { - atlasKey.add(frame.toString()) - } + const models = new Set() + if (parts) { + for (const part of parts) { + if (Array.isArray(part)) { + for (const model of part) { + models.add(model.model) } + } else { + models.add(part.model) } } + } - const searchKey = block + nonWaterLoggedState - - if (keyInObject(searchKey, blockOcclusionShapeData)) { - foundOcclusionShape[k] = blockOcclusionShapeData[searchKey] + const textures = new Set() + specials?.forEach((val) => textures.add(val)) + for (const model of models) { + const modelData = blockModelData[String(model)] + if (modelData) { + for (const element of modelData.elements ?? []) { + for (const face of Object.values(element.faces)) { + textures.add(face.texture) + } + } } + } - if (keyInObject(block, liquidComputationData)) { - foundLiquidComputationData[k] = liquidComputationData[block] + const texturesParsed = new Set() + for (const texture of textures) { + const textureData = textureAtlasData[String(texture)] + if (Array.isArray(textureData)) { + texturesParsed.add(texture) + } else { + textureData.frames.forEach((val) => texturesParsed.add(val)) } } - return ctx.json({ - blockStates: foundBlockStates, - blockModel: foundBlockModel, - textureAtlas: foundTextureAtlas, - blockRenderType: foundBlockRenderType, - occlusionShape: foundOcclusionShape, - specialBlocksData: foundSpecialBlocksData, - liquidComputationData: foundLiquidComputationData, - }) -}) - -function keyInObject( - key: string | number | symbol, - obj: T, -): key is keyof typeof obj { - return key in obj + return { + parts: parts ?? [], + models: Array.from(models), + textures: Array.from(texturesParsed), + render_type: blockRenderTypeData[stateName] ?? 'solid', + face_sturdy: liquidComputation.face_sturdy, + blocks_motion: liquidComputation.blocks_motion, + occlusion: occlusionShape.can_occlude, + occlusion_shape: { + down: occlusionShape.down ?? [], + up: occlusionShape.up ?? [], + north: occlusionShape.north ?? [], + south: occlusionShape.south ?? [], + west: occlusionShape.west ?? [], + east: occlusionShape.east ?? [], + }, + special_textures: specials ?? [], + } } -function getModelForBlock(blockState?: BlockStateModelCollection) { - if (blockState === undefined) return new Set() - const modelKey = new Set() - if (blockState.multipart) { - for (const part of blockState.multipart) { - if (part.apply instanceof Array) { - for (const apply of part.apply) { - modelKey.add(apply.model.toString()) - } - } else modelKey.add(part.apply.model.toString()) - } - } - if (blockState.variants) { - for (const model of Object.values(blockState.variants)) { - if (model instanceof Array) { - for (const m of model) { - modelKey.add(m.model.toString()) - } - } else modelKey.add(model.model.toString()) - } +// Routes ------------------------------------------------------------------------------------------ + +app.post('/', zValidator('json', querySchema), (ctx) => { + const query = ctx.req.valid('json') + const response = { states: [], textures: {}, models: {} } as ResponseSchema + const collectedTextures = new Set() + const collectedModels = new Set() + + for (const blockState of query) { + const data = findOrMakeData(blockState) + response.states.push({ + state: blockState, + parts: data.parts, + render_type: data.render_type, + face_sturdy: data.face_sturdy, + blocks_motion: data.blocks_motion, + occlusion: data.occlusion, + occlusion_shape: data.occlusion_shape, + special_textures: data.special_textures, + }) + data.textures.forEach((texture) => collectedTextures.add(String(texture))) + data.special_textures.forEach((texture) => collectedTextures.add(String(texture))) + data.models.forEach((model) => collectedModels.add(String(model))) } - return modelKey -} + collectedTextures.forEach((tex) => (response.textures[tex] = textureAtlasData[tex])) + collectedModels.forEach((model) => (response.models[model] = blockModelData[model])) + + return ctx.json(response) +}) export default app From 07c31114cb54532336312211beb0fb077789cdd6 Mon Sep 17 00:00:00 2001 From: nickid2018 Date: Sun, 30 Jun 2024 19:08:11 +0800 Subject: [PATCH 2/5] Use LRU Cache for response cache --- package.json | 1 + pnpm-lock.yaml | 9 +++++++++ src/routes/renderer.ts | 16 +++++++--------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bec9953..dc50558 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dotenv": "^16.4.5", "hono": "^4.3.11", "listhen": "^1.7.2", + "lru-cache": "^10.3.0", "tsx": "^4.10.5", "zod": "^3.23.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed3a22b..a2363ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: listhen: specifier: ^1.7.2 version: 1.7.2 + lru-cache: + specifier: ^10.3.0 + version: 10.3.0 tsx: specifier: ^4.10.5 version: 4.10.5 @@ -756,6 +759,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lru-cache@10.3.0: + resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==} + engines: {node: 14 || >=16.14} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1713,6 +1720,8 @@ snapshots: lodash.merge@4.6.2: {} + lru-cache@10.3.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} diff --git a/src/routes/renderer.ts b/src/routes/renderer.ts index e0983e7..f6392d8 100644 --- a/src/routes/renderer.ts +++ b/src/routes/renderer.ts @@ -9,6 +9,7 @@ import _blockRenderTypeData from '../data/renderer/block_render_type.json' import _blockOcclusionShapeData from '../data/renderer/block_occlusion_shape.json' import _specialBlocksData from '../data/renderer/special.json' import _liquidComputationData from '../data/renderer/block_liquid_computation.json' +import { LRUCache } from 'lru-cache' const blockStateData = _blockStateData as unknown as Record const blockModelData = _blockModelData as unknown as Record @@ -244,17 +245,14 @@ const EMPTY_STATE_DATA: StateData = { occlusion_shape: {}, special_textures: [], } -const RESPONSE_CACHE = new Map() + +const RESPONSE_CACHE = new LRUCache({ + max: 2000, + memoMethod: (key, staleValue, options) => makeStateData(options.context), +}) function findOrMakeData(blockState: BlockState): StateData { - const stateString = stateToString(blockState) - if (RESPONSE_CACHE.has(stateString)) { - return RESPONSE_CACHE.get(stateString)! - } else { - const data = makeStateData(blockState) - RESPONSE_CACHE.set(stateString, data) - return data - } + return RESPONSE_CACHE.memo(stateToString(blockState), { context: blockState }) } function makeStateData(blockState: BlockState): StateData { From 28f4998f77e84e235d5d34159b16b75b4a8a843c Mon Sep 17 00:00:00 2001 From: nickid2018 Date: Mon, 1 Jul 2024 15:02:17 +0800 Subject: [PATCH 3/5] Add cache control header --- src/routes/renderer.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/routes/renderer.ts b/src/routes/renderer.ts index f6392d8..c629c37 100644 --- a/src/routes/renderer.ts +++ b/src/routes/renderer.ts @@ -208,7 +208,7 @@ interface ResponseSchema { } function stateToString(blockState: BlockState): string { - if (blockState.properties) { + if (blockState.properties && Object.keys(blockState.properties).length > 0) { return `${blockState.name}[${Object.entries(blockState.properties) .sort(([key1], [key2]) => key1.localeCompare(key2)) .map(([key, value]) => `${key}=${value}`) @@ -248,7 +248,7 @@ const EMPTY_STATE_DATA: StateData = { const RESPONSE_CACHE = new LRUCache({ max: 2000, - memoMethod: (key, staleValue, options) => makeStateData(options.context), + memoMethod: (_key, _staleValue, options) => makeStateData(options.context), }) function findOrMakeData(blockState: BlockState): StateData { @@ -302,6 +302,7 @@ function makeStateData(blockState: BlockState): StateData { if (Array.isArray(textureData)) { texturesParsed.add(texture) } else { + texturesParsed.add(texture) textureData.frames.forEach((val) => texturesParsed.add(val)) } } @@ -328,6 +329,11 @@ function makeStateData(blockState: BlockState): StateData { // Routes ------------------------------------------------------------------------------------------ +app.use(async (c, next) => { + c.res.headers.set('Cache-Control', 'public, max-age=86400, s-maxage=604800') + await next() +}) + app.post('/', zValidator('json', querySchema), (ctx) => { const query = ctx.req.valid('json') const response = { states: [], textures: {}, models: {} } as ResponseSchema From f8205c2c3f5c2ae6f7e438270c69e56fbb21d0a6 Mon Sep 17 00:00:00 2001 From: nickid2018 Date: Mon, 1 Jul 2024 15:31:28 +0800 Subject: [PATCH 4/5] Use GET Method --- src/routes/renderer.ts | 49 +++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/routes/renderer.ts b/src/routes/renderer.ts index c629c37..e9738bb 100644 --- a/src/routes/renderer.ts +++ b/src/routes/renderer.ts @@ -29,13 +29,22 @@ const app = new Hono() // Query Schema ------------------------------------------------------------------------------------ -const BlockState = z.object({ - name: z.string(), - properties: z.record(z.string()).optional(), -}) -type BlockState = z.infer +interface BlockState { + name: string + properties: Record +} -const querySchema = z.array(BlockState) +const querySchema = z.object({ + states: z + .string() + .transform((val) => + val + .split('|') + .map((state) => state.trim()) + .filter((state) => state.length > 0), + ) + .pipe(z.array(z.string()).nonempty()), +}) // Data Definitions -------------------------------------------------------------------------------- @@ -207,6 +216,25 @@ interface ResponseSchema { textures: Record } +function stringToState(blockState: string): BlockState { + const split = blockState.indexOf('[') + if (split === -1) return { name: blockState, properties: {} } + if (blockState.endsWith(']')) blockState = blockState.slice(0, -1) + const name = blockState.slice(0, split) + const properties = blockState + .slice(split + 1) + .split(',') + .reduce( + (acc, val) => { + const [key, value] = val.split('=') + acc[key] = value + return acc + }, + {} as Record, + ) + return { name, properties } +} + function stateToString(blockState: BlockState): string { if (blockState.properties && Object.keys(blockState.properties).length > 0) { return `${blockState.name}[${Object.entries(blockState.properties) @@ -334,16 +362,17 @@ app.use(async (c, next) => { await next() }) -app.post('/', zValidator('json', querySchema), (ctx) => { - const query = ctx.req.valid('json') +app.get('/', zValidator('query', querySchema), (ctx) => { + const query = ctx.req.valid('query').states const response = { states: [], textures: {}, models: {} } as ResponseSchema const collectedTextures = new Set() const collectedModels = new Set() for (const blockState of query) { - const data = findOrMakeData(blockState) + const blockStateObj = stringToState(blockState) + const data = findOrMakeData(blockStateObj) response.states.push({ - state: blockState, + state: blockStateObj, parts: data.parts, render_type: data.render_type, face_sturdy: data.face_sturdy, From 44fa93b1530fb0fa4929f1cb8dd417ef5347b28e Mon Sep 17 00:00:00 2001 From: nickid2018 Date: Mon, 1 Jul 2024 17:08:04 +0800 Subject: [PATCH 5/5] New implementation for cache --- src/routes/renderer.ts | 85 ++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/src/routes/renderer.ts b/src/routes/renderer.ts index e9738bb..4eaf60b 100644 --- a/src/routes/renderer.ts +++ b/src/routes/renderer.ts @@ -29,21 +29,15 @@ const app = new Hono() // Query Schema ------------------------------------------------------------------------------------ -interface BlockState { - name: string - properties: Record -} +const BlockState = z.object({ + name: z.string(), + properties: z.record(z.string()).optional(), +}) +type BlockState = z.infer const querySchema = z.object({ - states: z - .string() - .transform((val) => - val - .split('|') - .map((state) => state.trim()) - .filter((state) => state.length > 0), - ) - .pipe(z.array(z.string()).nonempty()), + states: z.array(BlockState), + hash: z.string(), }) // Data Definitions -------------------------------------------------------------------------------- @@ -201,7 +195,17 @@ interface StateData { special_textures: number[] } -interface ResponseSchema { +interface Response { + processed: boolean +} + +interface NotProcessedResponse extends Response { + processed: false +} + +const NOT_PROCESSED_RESPONSE: NotProcessedResponse = { processed: false } + +interface ProcessedResponse extends Response { states: { state: BlockState parts: (ModelReference | ModelReferenceWithWeight[])[] @@ -214,25 +218,7 @@ interface ResponseSchema { }[] models: Record textures: Record -} - -function stringToState(blockState: string): BlockState { - const split = blockState.indexOf('[') - if (split === -1) return { name: blockState, properties: {} } - if (blockState.endsWith(']')) blockState = blockState.slice(0, -1) - const name = blockState.slice(0, split) - const properties = blockState - .slice(split + 1) - .split(',') - .reduce( - (acc, val) => { - const [key, value] = val.split('=') - acc[key] = value - return acc - }, - {} as Record, - ) - return { name, properties } + processed: true } function stateToString(blockState: BlockState): string { @@ -274,13 +260,13 @@ const EMPTY_STATE_DATA: StateData = { special_textures: [], } -const RESPONSE_CACHE = new LRUCache({ +const BLOCK_CACHE = new LRUCache({ max: 2000, memoMethod: (_key, _staleValue, options) => makeStateData(options.context), }) function findOrMakeData(blockState: BlockState): StateData { - return RESPONSE_CACHE.memo(stateToString(blockState), { context: blockState }) + return BLOCK_CACHE.memo(stateToString(blockState), { context: blockState }) } function makeStateData(blockState: BlockState): StateData { @@ -355,24 +341,32 @@ function makeStateData(blockState: BlockState): StateData { } } +const RESOLVED_CACHE = new LRUCache({ + max: 1000, +}) + // Routes ------------------------------------------------------------------------------------------ -app.use(async (c, next) => { - c.res.headers.set('Cache-Control', 'public, max-age=86400, s-maxage=604800') - await next() +app.get('/:hash', (ctx) => { + const hash = ctx.req.param('hash') + const response = RESOLVED_CACHE.get(hash) + if (response) + return ctx.json(response, 200, { + 'Cache-Control': 'public, max-age=86400, s-maxage=604800', + }) + return ctx.json(NOT_PROCESSED_RESPONSE, 404) }) -app.get('/', zValidator('query', querySchema), (ctx) => { - const query = ctx.req.valid('query').states - const response = { states: [], textures: {}, models: {} } as ResponseSchema +app.post('/process', zValidator('json', querySchema), (ctx) => { + const query = ctx.req.valid('json') + const response = { states: [], textures: {}, models: {}, processed: true } as ProcessedResponse const collectedTextures = new Set() const collectedModels = new Set() - for (const blockState of query) { - const blockStateObj = stringToState(blockState) - const data = findOrMakeData(blockStateObj) + for (const blockState of query.states) { + const data = findOrMakeData(blockState) response.states.push({ - state: blockStateObj, + state: blockState, parts: data.parts, render_type: data.render_type, face_sturdy: data.face_sturdy, @@ -388,6 +382,7 @@ app.get('/', zValidator('query', querySchema), (ctx) => { collectedTextures.forEach((tex) => (response.textures[tex] = textureAtlasData[tex])) collectedModels.forEach((model) => (response.models[model] = blockModelData[model])) + RESOLVED_CACHE.set(query.hash, response) return ctx.json(response) })