From 4e51d41c54918ed5e0506a6e4253f9d938319176 Mon Sep 17 00:00:00 2001 From: Misode Date: Sun, 8 May 2022 03:18:07 +0200 Subject: [PATCH] Refactor: create deepslate abstraction --- package-lock.json | 23 +++++ package.json | 1 + src/app/hooks/useCanvas.ts | 5 +- src/app/previews/Deepslate.ts | 129 ++++++++++++++++++++++++ src/app/previews/NoiseSettings.ts | 159 ++++-------------------------- 5 files changed, 175 insertions(+), 142 deletions(-) create mode 100644 src/app/previews/Deepslate.ts diff --git a/package-lock.json b/package-lock.json index ba64ede3..f269fa09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "comment-json": "^4.1.1", "deepslate": "^0.11.0-beta.1", "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", + "deepslate-1.18.2": "npm:deepslate@^0.9.0-beta.13", "deepslate-rs": "^0.1.6", "highlight.js": "^11.5.1", "howler": "^2.2.3", @@ -1788,6 +1789,18 @@ "pako": "^2.0.3" } }, + "node_modules/deepslate-1.18.2": { + "name": "deepslate", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.9.0.tgz", + "integrity": "sha512-juHNoXVu7+Gejw0xk4AwIt99FgIe/nkxTqJTv3bTlcfXINcF3VNlg1l6MnSiVTUFnuFHZl81woE9lVMAMp53Aw==", + "license": "MIT", + "dependencies": { + "gl-matrix": "^3.3.0", + "md5": "^2.3.0", + "pako": "^2.0.3" + } + }, "node_modules/deepslate-rs": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/deepslate-rs/-/deepslate-rs-0.1.6.tgz", @@ -6441,6 +6454,16 @@ "pako": "^2.0.3" } }, + "deepslate-1.18.2": { + "version": "npm:deepslate@0.9.0", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.9.0.tgz", + "integrity": "sha512-juHNoXVu7+Gejw0xk4AwIt99FgIe/nkxTqJTv3bTlcfXINcF3VNlg1l6MnSiVTUFnuFHZl81woE9lVMAMp53Aw==", + "requires": { + "gl-matrix": "^3.3.0", + "md5": "^2.3.0", + "pako": "^2.0.3" + } + }, "deepslate-rs": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/deepslate-rs/-/deepslate-rs-0.1.6.tgz", diff --git a/package.json b/package.json index b5fcba51..614ff1f4 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "comment-json": "^4.1.1", "deepslate": "^0.11.0-beta.1", "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", + "deepslate-1.18.2": "npm:deepslate@^0.9.0-beta.13", "deepslate-rs": "^0.1.6", "highlight.js": "^11.5.1", "howler": "^2.2.3", diff --git a/src/app/hooks/useCanvas.ts b/src/app/hooks/useCanvas.ts index fd1d0f10..24d2a7d9 100644 --- a/src/app/hooks/useCanvas.ts +++ b/src/app/hooks/useCanvas.ts @@ -36,8 +36,9 @@ export function useCanvas({ size, draw, onDrag, onHover, onLeave }: { if (!(dx === 0 && dy === 0)) { dragPending.current = [dragPending.current[0] + dx, dragPending.current[1] + dy] if (!dragBusy.current) { - if (!dragRequest.current) return - cancelAnimationFrame(dragRequest.current) + if (dragRequest.current) { + cancelAnimationFrame(dragRequest.current) + } dragRequest.current = requestAnimationFrame(async () => { if (!canvas.current) return dragBusy.current = true diff --git a/src/app/previews/Deepslate.ts b/src/app/previews/Deepslate.ts new file mode 100644 index 00000000..58e7393b --- /dev/null +++ b/src/app/previews/Deepslate.ts @@ -0,0 +1,129 @@ +import { DataModel } from '@mcschema/core' +import * as deepslate19 from 'deepslate/worldgen' +import type { VersionId } from '../services' +import { checkVersion, fetchAllPresets } from '../services' +import { deepClone, deepEqual } from '../Utils' + +export class Deepslate { + private d = deepslate19 + private loadedVersion: VersionId | undefined + private readonly registriesLoaded = new Set() + private static readonly Z = 0 + + private cacheState: unknown + private settingsCache: NoiseSettings | undefined + private generatorCache: ChunkGenerator | undefined + private chunksCache: Chunk[] = [] + + public async loadVersion(version: VersionId) { + console.debug('Load deepslate version', this.loadedVersion, '->', version) + if (checkVersion(version, '1.19')) { + this.d = deepslate19 + } else if (checkVersion(version, '1.18.2')) { + this.d = await import('deepslate-1.18.2') as any + } else { + this.d = await import('deepslate-1.18') as any + } + + if (this.d.WorldgenRegistries) { + if (!this.registriesLoaded.has(version)) { + const REGISTRIES: [string, keyof typeof this.d.WorldgenRegistries, { fromJson(obj: unknown): any}][] = [ + ['worldgen/noise', 'NOISE', this.d.NoiseParameters], + ['worldgen/density_function', 'DENSITY_FUNCTION', this.d.DensityFunction], + ] + await Promise.all(REGISTRIES.map(async ([id, name, parser]) => { + const entries = await fetchAllPresets(version, id) + const registry = new this.d.Registry(this.d.Identifier.create(id)) + for (const [key, value] of entries.entries()) { + registry.register(this.d.Identifier.parse(key), parser.fromJson(value)) + } + this.d.WorldgenRegistries[name].assign(registry as any) + })) + this.registriesLoaded.add(version) + } + } + console.debug('Finished loading deepslate version', this.loadedVersion, '->', version) + this.loadedVersion = version + } + + public loadChunkGenerator(settings: unknown, seed: bigint, biome = 'unknown') { + const newCacheState = [settings, `${seed}`, biome] + if (!deepEqual(this.cacheState, newCacheState)) { + const biomeSource = new this.d.FixedBiome(this.d.Identifier.parse(biome)) + const noiseSettings = this.d.NoiseGeneratorSettings.fromJson(DataModel.unwrapLists(settings)) + const chunkGenerator = new this.d.NoiseChunkGenerator(seed, biomeSource, noiseSettings) + this.settingsCache = noiseSettings.noise + this.generatorCache = chunkGenerator + this.chunksCache = [] + this.cacheState = deepClone(newCacheState) + } + } + + public generateChunks(minX: number, width: number, biome = 'unknown') { + if (!this.settingsCache) { + throw new Error('Tried to generate chunks before settings are loaded') + } + const minY = this.settingsCache.minY + const height = this.settingsCache.height + + return [...Array(Math.ceil(width / 16) + 1)].map((_, i) => { + const x = (minX >> 4) + i + const cached = this.chunksCache.find(c => c.pos[0] === x) + if (cached) { + return cached + } + const chunk = new this.d.Chunk(minY, height, this.d.ChunkPos.create(x, Deepslate.Z >> 4)) + if (!this.generatorCache) { + throw new Error('Tried to generate chunks before generator is loaded') + } + this.generatorCache.fill(chunk, true) + this.generatorCache.buildSurface(chunk, biome) + this.chunksCache.push(chunk) + return chunk + }) + } + + public loadDensityFunction(state: unknown, seed: bigint) { + const random = this.d.XoroshiroRandom.create(seed).forkPositional() + const settings = this.d.NoiseSettings.fromJson({ + min_y: -64, + height: 384, + size_horizontal: 1, + size_vertical: 2, + sampling: { xz_scale: 1, y_scale: 1, xz_factor: 80, y_factor: 160 }, + bottom_slide: { target: 0.1171875, size: 3, offset: 0 }, + top_slide: { target: -0.078125, size: 2, offset: 8 }, + terrain_shaper: { offset: 0.044, factor: 4, jaggedness: 0 }, + }) + this.settingsCache = settings + const originalFn = this.d.DensityFunction.fromJson(state) + return originalFn.mapAll(new this.d.NoiseRouter.Visitor(random, settings)) + } + + public getNoiseSettings(): NoiseSettings { + if (!this.settingsCache) { + throw new Error('Tried to access noise settings when they are not loaded') + } + return this.settingsCache + } + + public getBlockState(x: number, y: number) { + const chunk = this.chunksCache.find(c => this.d.ChunkPos.minBlockX(c.pos) <= x && this.d.ChunkPos.maxBlockX(c.pos) >= x) + return chunk?.getBlockState(this.d.BlockPos.create(x, y, Deepslate.Z)) + } +} + +interface NoiseSettings { + minY: number, + height: number, +} + +interface ChunkGenerator { + fill(chunk: Chunk, onlyFirstZ?: boolean): void + buildSurface(chunk: Chunk, biome: string): void +} + +interface Chunk { + readonly pos: deepslate19.ChunkPos; + getBlockState(pos: deepslate19.BlockPos): deepslate19.BlockState; +} diff --git a/src/app/previews/NoiseSettings.ts b/src/app/previews/NoiseSettings.ts index 14d8ad20..4b0045e6 100644 --- a/src/app/previews/NoiseSettings.ts +++ b/src/app/previews/NoiseSettings.ts @@ -1,9 +1,7 @@ -import { DataModel } from '@mcschema/core' -import type { BlockState } from 'deepslate/worldgen' -import { BlockPos, Chunk, ChunkPos, clampedMap, DensityFunction, FixedBiome, Identifier, NoiseChunkGenerator, NoiseGeneratorSettings, NoiseParameters, NoiseRouter, NoiseSettings, Registry, WorldgenRegistries, XoroshiroRandom } from 'deepslate/worldgen' +import { BlockState, clampedMap, DensityFunction } from 'deepslate/worldgen' import type { VersionId } from '../services' -import { checkVersion, fetchAllPresets } from '../services' -import { deepClone, deepEqual } from '../Utils' +import { checkVersion } from '../services' +import { Deepslate } from './Deepslate' import { NoiseChunkGenerator as OldNoiseChunkGenerator } from './noise/NoiseChunkGenerator' export type NoiseSettingsOptions = { @@ -16,8 +14,6 @@ export type NoiseSettingsOptions = { version: VersionId, } -const Z = 0 - const colors: Record = { 'minecraft:air': [150, 160, 170], 'minecraft:water': [20, 80, 170], @@ -37,27 +33,21 @@ const colors: Record = { 'minecraft:end_stone': [200, 200, 140], } -let cacheState: any -let generatorCache: NoiseChunkGenerator -let chunkCache: Chunk[] = [] -const registryCache = new Map>>() +const DEEPSLATE = new Deepslate() export async function noiseSettings(state: any, img: ImageData, options: NoiseSettingsOptions) { if (checkVersion(options.version, '1.18')) { - if (checkVersion(options.version, '1.18.2')) { - await initRegistries(options.version) - } - const { settings, generator } = await getCached(state, options) - - const slice = new LevelSlice(-options.offset, options.width, settings.noise.minY, settings.noise.height) - slice.generate(generator, options.biome) + await DEEPSLATE.loadVersion(options.version) + DEEPSLATE.loadChunkGenerator(state, options.seed, options.biome) + DEEPSLATE.generateChunks(-options.offset, options.width, options.biome) + const noise = DEEPSLATE.getNoiseSettings() const data = img.data for (let x = 0; x < options.width; x += 1) { - for (let y = 0; y < settings.noise.height; y += 1) { - const i = x * 4 + (settings.noise.height-y-1) * 4 * img.width - const state = slice.getBlockState([x - options.offset, y + settings.noise.minY, Z]) + for (let y = 0; y < noise.height; y += 1) { + const i = x * 4 + (noise.height-y-1) * 4 * img.width + const state = DEEPSLATE.getBlockState(x - options.offset, y + noise.minY) ?? BlockState.AIR const color = colors[state.getName().toString()] ?? [0, 0, 0] data[i] = color[0] data[i + 1] = color[1] @@ -86,22 +76,20 @@ export async function noiseSettings(state: any, img: ImageData, options: NoiseSe } export function getNoiseBlock(x: number, y: number) { - const chunk = chunkCache.find(c => ChunkPos.minBlockX(c.pos) <= x && ChunkPos.maxBlockX(c.pos) >= x) - if (!chunk) { - return undefined - } - return chunk.getBlockState(BlockPos.create(x, y, Z)) + return DEEPSLATE.getBlockState(x, y) } export async function densityFunction(state: any, img: ImageData, options: NoiseSettingsOptions) { - const { fn, settings } = await createDensityFunction(state, options) + await DEEPSLATE.loadVersion(options.version) + const fn = DEEPSLATE.loadDensityFunction(state, options.seed) + const noise = DEEPSLATE.getNoiseSettings() - const arr = Array(options.width * settings.height) + const arr = Array(options.width * noise.height) let min = Infinity let max = -Infinity for (let x = 0; x < options.width; x += 1) { - for (let y = 0; y < settings.height; y += 1) { - const i = x + (settings.height-y-1) * options.width + for (let y = 0; y < noise.height; y += 1) { + const i = x + (noise.height-y-1) * options.width const density = fn.compute(DensityFunction.context(x - options.offset, y, 0)) min = Math.min(min, density) max = Math.max(max, density) @@ -110,7 +98,7 @@ export async function densityFunction(state: any, img: ImageData, options: Noise } const data = img.data - for (let i = 0; i < options.width * settings.height; i += 1) { + for (let i = 0; i < options.width * noise.height; i += 1) { const color = Math.floor(clampedMap(arr[i], min, max, 0, 256)) data[4 * i] = color data[4 * i + 1] = color @@ -119,75 +107,6 @@ export async function densityFunction(state: any, img: ImageData, options: Noise } } -async function createDensityFunction(state: any, options: NoiseSettingsOptions) { - await initRegistries(options.version) - - const random = XoroshiroRandom.create(options.seed).forkPositional() - const settings = NoiseSettings.fromJson({ - min_y: -64, - height: 384, - size_horizontal: 1, - size_vertical: 2, - sampling: { xz_scale: 1, y_scale: 1, xz_factor: 80, y_factor: 160 }, - bottom_slide: { target: 0.1171875, size: 3, offset: 0 }, - top_slide: { target: -0.078125, size: 2, offset: 8 }, - terrain_shaper: { offset: 0.044, factor: 4, jaggedness: 0 }, - }) - const originalFn = DensityFunction.fromJson(state) - const fn = originalFn.mapAll(new NoiseRouter.Visitor(random, settings)) - - return { - fn, - settings, - } -} - -const Registries: [string, { fromJson(obj: unknown): any}][] = [ - ['worldgen/noise', NoiseParameters], - ['worldgen/density_function', DensityFunction], -] - -async function initRegistries(version: VersionId) { - const rootRegistries = registryCache.get(version) ?? new Registry(new Identifier('misode', 'temp')) - if (!registryCache.has(version)) { - await Promise.all(Registries.map(([id, c]) => fetchRegistry(version, rootRegistries, id, c))) - registryCache.set(version, rootRegistries) - } - WorldgenRegistries.DENSITY_FUNCTION.clear().assign(rootRegistries.getOrThrow(Identifier.create('worldgen/density_function'))) - WorldgenRegistries.NOISE.clear().assign(rootRegistries.getOrThrow(Identifier.create('worldgen/noise'))) -} - -async function fetchRegistry(version: VersionId, root: Registry>, id: string, clazz: T) { - const entries = await fetchAllPresets(version, id) - const registry = new Registry(Identifier.create(id)) - for (const [key, value] of entries.entries()) { - registry.register(Identifier.parse(key), clazz.fromJson(value)) - } - root.register(registry.key, registry) -} - -async function getCached(state: unknown, options: NoiseSettingsOptions) { - const settings = NoiseGeneratorSettings.fromJson(DataModel.unwrapLists(state)) - - const newState = [state, `${options.seed}`, options.biome] - if (!deepEqual(newState, cacheState)) { - cacheState = deepClone(newState) - chunkCache = [] - if (checkVersion(options.version, '1.18.2')) { - const biomeSource = new FixedBiome(Identifier.create('unknown')) - generatorCache = new NoiseChunkGenerator(options.seed, biomeSource, settings) - } else { - const deepslate18 = await import('deepslate-1.18') - const biomeSource = new deepslate18.FixedBiome('unknown') - generatorCache = new deepslate18.NoiseChunkGenerator(options.seed, biomeSource, settings as any) as any - } - } - return { - settings, - generator: generatorCache, - } -} - function getColor(noise: number[], y: number): number { if (noise[y] > 0) { return 0 @@ -197,43 +116,3 @@ function getColor(noise: number[], y: number): number { } return 255 } - -class LevelSlice { - private readonly chunks: Chunk[] - private readonly done: boolean[] - - constructor( - private readonly minX: number, - width: number, - minY: number, - height: number, - ) { - this.done = [] - this.chunks = [...Array(Math.ceil(width / 16) + 1)] - .map((_, i) => { - const x = (minX >> 4) + i - const cached = chunkCache.find(c => c.pos[0] === x) - if (cached) { - this.done[i] = true - return cached - } - return new Chunk(minY, height, ChunkPos.create(x, Z >> 4)) - }) - } - - public generate(generator: NoiseChunkGenerator, forcedBiome?: string) { - this.chunks.forEach((chunk, i) => { - if (!this.done[i]) { - generator.fill(chunk, true) - generator.buildSurface(chunk, forcedBiome) - this.done[i] = true - chunkCache.push(chunk) - } - }) - } - - public getBlockState(pos: BlockPos): BlockState { - const chunkIndex = (pos[0] >> 4) - (this.minX >> 4) - return this.chunks[chunkIndex].getBlockState(pos) - } -}