//@ts-nocheck import { EventEmitter } from 'events' import TypedEmitter from 'typed-emitter' import MinecraftData, { IndexedData } from 'minecraft-data' import blocksAtlases from 'mc-assets/dist/blocksAtlases.json' import itemsAtlases from 'mc-assets/dist/itemsAtlases.json' import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json' import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png' import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png' import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png' import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png' import christmasPack from 'mc-assets/dist/textureReplacements/christmas' import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser' import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import { isWebWorker } from '../three/documentRenderer' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import { getLoadedItemDefinitionsStore } from 'mc-assets/dist/stores' import { sanitizeWorkerEventArgs } from '../lib/workerMessageSanitize' type ResourceManagerEvents = { assetsTexturesUpdated: () => void assetsInventoryStarted: () => void assetsInventoryReady: () => void } type ItemDefinitionsStore = ReturnType let workerItemDefinitionsStore: ItemDefinitionsStore | null = null export function getItemsDefinitionsStoreForRender ( resources: LoadedResourcesTransferrable ): ItemDefinitionsStore { if (!isWebWorker) { ensureItemsDefinitionsStore(resources) return resources.itemsDefinitionsStore } if (!workerItemDefinitionsStore || typeof workerItemDefinitionsStore.get !== 'function') { workerItemDefinitionsStore = getLoadedItemDefinitionsStore(itemDefinitionsJson) } return workerItemDefinitionsStore } export function ensureItemsDefinitionsStore (resources: LoadedResourcesTransferrable): void { if (isWebWorker) return const store = resources.itemsDefinitionsStore as { get?: unknown } if (typeof store?.get === 'function') return resources.itemsDefinitionsStore = getLoadedItemDefinitionsStore( resources.sourceItemDefinitionsJson ?? itemDefinitionsJson ) } export class LoadedResourcesTransferrable { // todo transfer instead! readonly sourceItemDefinitionsJson: any = itemDefinitionsJson itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson) allReady = false // Atlas parsers itemsAtlasImage!: ImageBitmap blocksAtlasImage!: ImageBitmap blocksAtlasJson!: ItemsAtlasesOutputJson itemsAtlasJson!: ItemsAtlasesOutputJson // User data (specific to current resourcepack/version) customBlockStates?: Record customModels?: Record /** array where the index represents the custom model data value, and the element at that index is the model path to use */ customItemModelNames: Record = {} customTextures: { items?: { tileSize: number | undefined, textures: Record } blocks?: { tileSize: number | undefined, textures: Record } armor?: { tileSize: number | undefined, textures: Record } } = {} guiAtlas: { json: any, image: ImageBitmap } | null = null guiAtlasVersion = 0 itemsRenderer: ItemsRenderer | undefined worldBlockProvider?: WorldBlockProvider blockstatesModels: any = null version!: string texturesVersion!: string mcData!: IndexedData constructor(data?: any) { if (data) { const safe = { ...data } delete safe.itemsDefinitionsStore delete safe.sourceItemDefinitionsJson Object.assign(this, safe) ensureItemsDefinitionsStore(this) } if (this.version) { const globalMc = (globalThis as { loadedData?: IndexedData, mcData?: IndexedData }).loadedData ?? (globalThis as { mcData?: IndexedData }).mcData if (isWebWorker && globalMc?.entitiesByName) { this.mcData = globalMc } else { this.mcData = MinecraftData(this.version) } // this.itemsRenderer = new ItemsRenderer( // this.version, // this.blockstatesModels, // this.itemsAtlasParser, // this.blocksAtlasParser // ) } } prepareForTransfer() { const cloned: any = {} for (const key in this) { if (!Object.prototype.hasOwnProperty.call(this, key)) continue if (typeof this[key] === 'function') continue if ( key === 'itemsRenderer' || key === 'worldBlockProvider' || key === 'mcData' || key === 'itemsDefinitionsStore' || key === 'sourceItemDefinitionsJson' ) { continue } cloned[key] = this[key as keyof this] } cloned.customTextures = {} delete cloned.itemsDefinitionsStore delete cloned.sourceItemDefinitionsJson return cloned as LoadedResourcesTransferrable } } export interface ResourcesCurrentConfig { version: string texturesVersion?: string // noBlockstatesModels?: boolean noInventoryGui?: boolean includeOnlyBlocks?: string[] } export interface UpdateAssetsRequest { _?: false } export interface ResourcesManagerTransferred extends TypedEmitter { currentResources: LoadedResourcesTransferrable } export interface ResourcesManagerCommon extends TypedEmitter { currentResources: LoadedResourcesTransferrable | undefined } const STABLE_MODELS_VERSION = '1.21.4' export class ResourcesManager extends (EventEmitter as new () => TypedEmitter) { static restorerName = 'ResourcesManager' rebuildWorkerRenderers (resources: LoadedResourcesTransferrable): void { if (!isWebWorker) return if (!resources.version || !resources.blockstatesModels || !resources.blocksAtlasJson) return this.blocksAtlasParser = new AtlasParser({ latest: resources.blocksAtlasJson }, '') if (resources.itemsAtlasJson) { this.itemsAtlasParser = new AtlasParser({ latest: resources.itemsAtlasJson }, '') } else if (!this.itemsAtlasParser?.atlas?.latest) { if (!this.sourceItemsAtlases || Object.keys(this.sourceItemsAtlases).length === 0) return this.itemsAtlasParser = new AtlasParser(this.sourceItemsAtlases, '') } resources.itemsRenderer = new ItemsRenderer( resources.version, resources.blockstatesModels, this.itemsAtlasParser, this.blocksAtlasParser ) resources.worldBlockProvider = worldBlockProvider( resources.blockstatesModels, this.blocksAtlasParser.atlas, STABLE_MODELS_VERSION ) } static restoreTransferred(data: any, worker?: Worker) { const resourcesManager = new ResourcesManager() const upResources = (transferData: LoadedResourcesTransferrable) => { resourcesManager.currentResources = new LoadedResourcesTransferrable(transferData) resourcesManager.rebuildWorkerRenderers(resourcesManager.currentResources) } upResources(data.currentResources) if (worker) { worker.addEventListener('message', ({ data }) => { if (data.class === ResourcesManager.restorerName) { if (data.type === 'newResources') { console.log('[worker] got new resources') upResources(data.currentResources) } if (data.type === 'event') { resourcesManager.emit(data.eventName, ...data.args) } } }) } return resourcesManager } enrichTransferSnapshot (transfer?: LoadedResourcesTransferrable): LoadedResourcesTransferrable | undefined { if (!transfer) return transfer if (this.itemsAtlasParser?.atlas?.latest) { transfer.itemsAtlasJson = this.itemsAtlasParser.atlas.latest } if (this.blocksAtlasParser?.atlas?.latest) { transfer.blocksAtlasJson = this.blocksAtlasParser.atlas.latest } return transfer } prepareForTransfer(worker?: Worker) { if (worker) { // todo do it automatically const oldEmit = this.emit.bind(this) as any this.emit = ((eventName: keyof ResourceManagerEvents, ...args: any[]) => { oldEmit(eventName, ...args) worker.postMessage({ class: ResourcesManager.restorerName, type: 'event', eventName, args: sanitizeWorkerEventArgs(args), }) // todo handle assetsInventoryReady if (eventName === 'assetsTexturesUpdated' || eventName === 'assetsInventoryReady') { const currentResources = this.enrichTransferSnapshot( this.currentResources?.prepareForTransfer(), ) worker.postMessage({ class: ResourcesManager.restorerName, type: 'newResources', currentResources, }) } }) as any } return { __restorer: ResourcesManager.restorerName, currentResources: this.enrichTransferSnapshot(this.currentResources?.prepareForTransfer()), } } // Source data (imported, not changing) sourceBlockStatesModels: any = null readonly sourceBlocksAtlases: any = blocksAtlases readonly sourceItemsAtlases: any = itemsAtlases currentResources: LoadedResourcesTransferrable | undefined itemsAtlasParser!: AtlasParser blocksAtlasParser!: AtlasParser currentConfig: ResourcesCurrentConfig | undefined abortController = new AbortController() _promiseAssetsReadyResolvers = Promise.withResolvers() get promiseAssetsReady() { return this._promiseAssetsReadyResolvers.promise } async loadSourceData(version = this.currentConfig?.version) { if (!version) throw new Error('No version loaded') // TODO this.sourceBlockStatesModels ??= (await import('mc-assets/dist/blockStatesModels.json')).default } resetResources() { this.currentResources = new LoadedResourcesTransferrable() } async updateAssetsData(request: UpdateAssetsRequest, unstableSkipEvent = false) { if (!this.currentConfig) throw new Error('No config loaded') this._promiseAssetsReadyResolvers = Promise.withResolvers() const abortController = new AbortController() await this.loadSourceData(this.currentConfig.version) if (abortController.signal.aborted) return const resources = this.currentResources ?? new LoadedResourcesTransferrable() resources.version = this.currentConfig.version resources.texturesVersion = this.currentConfig.texturesVersion ?? resources.version // Load minecraft data resources.mcData = MinecraftData(resources.version) resources.blockstatesModels = { blockstates: {}, models: {} } // todo-low resolve version resources.blockstatesModels.blockstates.latest = { ...this.sourceBlockStatesModels.blockstates.latest, ...resources.customBlockStates } resources.blockstatesModels.models.latest = { ...this.sourceBlockStatesModels.models.latest, ...resources.customModels } console.time('recreateAtlases') await Promise.all([ this.recreateBlockAtlas(resources), this.recreateItemsAtlas(resources) ]) console.timeEnd('recreateAtlases') if (abortController.signal.aborted) return if (resources.version && resources.blockstatesModels && this.itemsAtlasParser && this.blocksAtlasParser) { resources.itemsRenderer = new ItemsRenderer( resources.version, resources.blockstatesModels, this.itemsAtlasParser, this.blocksAtlasParser ) } if (abortController.signal.aborted) return this.currentResources = resources resources.allReady = true if (!unstableSkipEvent) { // todo rework resourcepack optimization this.emit('assetsTexturesUpdated') } if (this.currentConfig.noInventoryGui) { this._promiseAssetsReadyResolvers.resolve() } else { this.emit('assetsInventoryStarted') void this.generateGuiTextures().then(() => { if (abortController.signal.aborted) return if (!unstableSkipEvent) { this.emit('assetsInventoryReady') } this._promiseAssetsReadyResolvers.resolve() }) } } async recreateBlockAtlas(resources: LoadedResourcesTransferrable = this.currentResources!) { const blockTexturesChanges = {} as Record const date = new Date() if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) { Object.assign(blockTexturesChanges, christmasPack) } const blocksAssetsParser = new AtlasParser(this.sourceBlocksAtlases, blocksAtlasLatest, blocksAtlasLegacy) const customBlockTextures = Object.keys(resources.customTextures.blocks?.textures ?? {}) console.time('createBlocksAtlas') const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas( resources.texturesVersion, (textureName) => { if (this.currentConfig!.includeOnlyBlocks && !this.currentConfig!.includeOnlyBlocks.includes(textureName)) return false const texture = resources.customTextures.blocks?.textures[textureName] return blockTexturesChanges[textureName] ?? texture }, undefined, undefined, customBlockTextures, { needHorizontalIndexes: !!this.currentConfig!.includeOnlyBlocks, } ) console.timeEnd('createBlocksAtlas') this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL()) resources.blocksAtlasImage = await createImageBitmap(blocksCanvas) resources.blocksAtlasJson = this.blocksAtlasParser.atlas.latest resources.worldBlockProvider = worldBlockProvider( resources.blockstatesModels, this.blocksAtlasParser.atlas, STABLE_MODELS_VERSION ) } async recreateItemsAtlas(resources: LoadedResourcesTransferrable = this.currentResources!) { const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy) const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {}) const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas( resources.texturesVersion, (textureName) => { const texture = resources.customTextures.items?.textures[textureName] if (!texture) return return texture }, resources.customTextures.items?.tileSize, undefined, customItemTextures ) this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL()) resources.itemsAtlasImage = await createImageBitmap(itemsCanvas) resources.itemsAtlasJson = this.itemsAtlasParser.atlas.latest } async generateGuiTextures() { // todo-low handled now in the client } async downloadDebugAtlas(isItems = false) { const resources = this.currentResources if (!resources) throw new Error('No resources loaded') const atlasParser = (isItems ? this.itemsAtlasParser : this.blocksAtlasParser)! const dataUrl = await atlasParser.createDebugImage(true) const a = document.createElement('a') a.href = dataUrl a.download = `atlas-debug-${isItems ? 'items' : 'blocks'}.png` a.click() } destroy() { this.abortController.abort() this.currentResources = undefined this.abortController = new AbortController() } }