diff --git a/package-lock.json b/package-lock.json index 54ac4d4e..17a48b49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", - "deepslate": "^0.11.1", + "deepslate": "^0.12.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", @@ -1934,9 +1934,9 @@ "dev": true }, "node_modules/deepslate": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.11.1.tgz", - "integrity": "sha512-1m4TFkzHcTdAH00+S/oIzfhaeUomQokf66FIz8rvtakBcF/YraRnu8h4ic6tBMlnWI9pOb4Wuc2rtbEw8pgbNQ==", + "version": "0.12.0-beta.1", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.12.0-beta.1.tgz", + "integrity": "sha512-gxYokRPgnQ7Hrb8k4iJPA23gNBC8VIWXJVNDuiWiZLYaFhZec3HkkZZXqn+Ba/z3gIDinpatCdiQKP/8gJyZzA==", "dependencies": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", @@ -6627,9 +6627,9 @@ "dev": true }, "deepslate": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.11.1.tgz", - "integrity": "sha512-1m4TFkzHcTdAH00+S/oIzfhaeUomQokf66FIz8rvtakBcF/YraRnu8h4ic6tBMlnWI9pOb4Wuc2rtbEw8pgbNQ==", + "version": "0.12.0-beta.1", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.12.0-beta.1.tgz", + "integrity": "sha512-gxYokRPgnQ7Hrb8k4iJPA23gNBC8VIWXJVNDuiWiZLYaFhZec3HkkZZXqn+Ba/z3gIDinpatCdiQKP/8gJyZzA==", "requires": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", diff --git a/package.json b/package.json index 75b58e36..134d5221 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", - "deepslate": "^0.11.1", + "deepslate": "^0.12.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", diff --git a/src/app/Utils.ts b/src/app/Utils.ts index c075b210..2e8a8bd8 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -256,8 +256,8 @@ export function deepEqual(a: any, b: any) { } export class BiMap { - private readonly forward: Map - private readonly backward: Map + public readonly forward: Map + public readonly backward: Map constructor() { this.forward = new Map() @@ -285,6 +285,16 @@ export class BiMap { } return b } + + public computeIfAbsent(key: A, value: () => B) { + const b = this.forward.get(key) + if (b === undefined) { + const newValue = value() + this.set(key, newValue) + return newValue + } + return b + } } export async function readZip(file: File): Promise<[string, string][]> { @@ -307,3 +317,23 @@ export async function writeZip(entries: [string, string][]): Promise { })) return await writer.close() } + +export function computeIfAbsent(map: Map, key: K, getter: (key: K) => V): V { + const existing = map.get(key) + if (existing) { + return existing + } + const value = getter(key) + map.set(key, value) + return value +} + +export async function computeIfAbsentAsync(map: Map, key: K, getter: (key: K) => Promise): Promise { + const existing = map.get(key) + if (existing) { + return existing + } + const value = await getter(key) + map.set(key, value) + return value +} diff --git a/src/app/components/previews/BiomeSourcePreview.tsx b/src/app/components/previews/BiomeSourcePreview.tsx index 6535b959..7be03ce3 100644 --- a/src/app/components/previews/BiomeSourcePreview.tsx +++ b/src/app/components/previews/BiomeSourcePreview.tsx @@ -1,32 +1,25 @@ -import { Path } from '@mcschema/core' -import type { NoiseParameters } from 'deepslate/worldgen' -import { useEffect, useMemo, useRef, useState } from 'preact/hooks' -import { useLocale, useStore } from '../../contexts/index.js' +import { DataModel, Path } from '@mcschema/core' +import { useEffect, useRef, useState } from 'preact/hooks' +import { useLocale, useProject, useStore } from '../../contexts/index.js' import { useCanvas } from '../../hooks/index.js' import { biomeMap, getBiome } from '../../previews/index.js' -import { newSeed, randomSeed } from '../../Utils.js' -import { Btn, BtnMenu } from '../index.js' +import { randomSeed } from '../../Utils.js' +import { Btn } from '../index.js' import type { PreviewProps } from './index.js' -const LAYERS = ['biomes', 'temperature', 'humidity', 'continentalness', 'erosion', 'weirdness'] as const - export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps) => { const { locale } = useLocale() - const [configuredSeed] = useState(randomSeed()) + const { project } = useProject() + const [seed, setSeed] = useState(randomSeed()) const [scale, setScale] = useState(2) const [focused, setFocused] = useState<{[k: string]: number | string} | undefined>(undefined) - const [layers, setLayers] = useState(new Set(['biomes'])) const { biomeColors } = useStore() const offset = useRef<[number, number]>([0, 0]) const res = useRef(1) const refineTimeout = useRef() - const seed = BigInt(model.get(new Path(['generator', 'seed'])) ?? configuredSeed) - const octaves = useMemo(() => { - if (!shown) return undefined - return getOctaves(model.get(new Path(['generator', 'settings']))) - }, [shown]) - const state = shown ? calculateState(data, octaves!) : '' + const settings = DataModel.unwrapLists(model.get(new Path(['generator', 'settings']))) + const state = JSON.stringify([data, settings]) const type: string = data.type?.replace(/^minecraft:/, '') const { canvas, redraw } = useCanvas({ @@ -34,7 +27,7 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps return [200 / res.current, 200 / res.current] }, async draw(img) { - const options = { octaves: octaves!, biomeColors, layers, offset: offset.current, scale, seed, res: res.current, version } + const options = { settings, biomeColors, offset: offset.current, scale, seed, res: res.current, version, project } await biomeMap(data, img, options) if (res.current === 4) { clearTimeout(refineTimeout.current) @@ -52,23 +45,24 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps redraw() }, async onHover(x, y) { - const options = { octaves: octaves!, biomeColors, layers, offset: offset.current, scale, seed: configuredSeed, res: 1, version } + const options = { settings, biomeColors, offset: offset.current, scale, seed: seed, res: 1, version, project } const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options) setFocused(biome) }, onLeave() { setFocused(undefined) }, - }, [version, state, scale, configuredSeed, layers, biomeColors]) + }, [version, state, scale, seed, biomeColors, project]) useEffect(() => { if (shown) { res.current = type === 'multi_noise' ? 4 : 1 redraw() } - }, [version, state, scale, configuredSeed, layers, shown, biomeColors]) + }, [version, state, scale, seed, shown, biomeColors, project]) const changeScale = (newScale: number) => { + newScale = Math.max(1, Math.round(newScale)) offset.current[0] = offset.current[0] * scale / newScale offset.current[1] = offset.current[1] * scale / newScale setScale(newScale) @@ -77,28 +71,14 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps return <>
{focused && } - {type === 'multi_noise' && - - {LAYERS.map(name => { - const enabled = layers.has(name) - return { - setLayers(new Set([name])) - e.stopPropagation() - }} /> - })} - } - {(type === 'multi_noise' || type === 'checkerboard') && <> - changeScale(scale * 1.5)} /> - changeScale(scale / 1.5)} /> - } - {type === 'multi_noise' && + changeScale(scale * 2)} /> + changeScale(scale / 2)} /> + {(type === 'multi_noise' || type === 'the_end') && newSeed(model)} />} + onClick={() => setSeed(randomSeed())} />}
{focused?.temperature !== undefined &&
} - -function calculateState(data: any, octaves: Record) { - return JSON.stringify([data, octaves]) -} - -export function getOctaves(obj: any): Record { - if (typeof obj !== 'string') { - obj = obj.legacy_random_source ? 'minecraft:nether' : 'minecraft:overworld' - } - switch (obj.replace(/^minecraft:/, '')) { - case 'overworld': - case 'amplified': - return { - temperature: { firstOctave: -9, amplitudes: [1.5, 0, 1, 0, 0, 0] }, - humidity: { firstOctave: -7, amplitudes: [1, 1, 0, 0, 0, 0] }, - continentalness: { firstOctave: -9, amplitudes: [1, 1, 2, 2, 2, 1, 1, 1, 1] }, - erosion: { firstOctave: -9, amplitudes: [1, 1, 0, 1, 1] }, - weirdness: { firstOctave: -7, amplitudes: [1, 2, 1, 0, 0, 0] }, - shift: { firstOctave: -3, amplitudes: [1, 1, 1, 0] }, - } - case 'large_biomes': - return { - temperature: { firstOctave: -12, amplitudes: [1.5, 0, 1, 0, 0, 0] }, - humidity: { firstOctave: -10, amplitudes: [1, 1, 0, 0, 0, 0] }, - continentalness: { firstOctave: -11, amplitudes: [1, 1, 2, 2, 2, 1, 1, 1, 1] }, - erosion: { firstOctave: -11, amplitudes: [1, 1, 0, 1, 1] }, - weirdness: { firstOctave: -7, amplitudes: [1, 2, 1, 0, 0, 0] }, - shift: { firstOctave: -3, amplitudes: [1, 1, 1, 0] }, - } - default: - return { - temperature: { firstOctave: -7, amplitudes: [1, 1] }, - humidity: { firstOctave: -7, amplitudes: [1, 1] }, - continentalness: { firstOctave: -7, amplitudes: [1, 1] }, - erosion: { firstOctave: -7, amplitudes: [1, 1] }, - weirdness: { firstOctave: -7, amplitudes: [1, 1] }, - shift: { firstOctave: 0, amplitudes: [0] }, - } - } -} diff --git a/src/app/previews/BiomeSource.ts b/src/app/previews/BiomeSource.ts index 6d4016f2..eb577aa6 100644 --- a/src/app/previews/BiomeSource.ts +++ b/src/app/previews/BiomeSource.ts @@ -1,234 +1,79 @@ import { DataModel } from '@mcschema/core' -import { biome_parameters, climate_noise, climate_sampler, default as init, multi_noise } from 'deepslate-rs' -// @ts-expect-error -import wasm from 'deepslate-rs/deepslate_rs_bg.wasm?url' -import type { NoiseParameters } from 'deepslate/worldgen' -import { FixedBiome, Identifier, LegacyRandom, NormalNoise } from 'deepslate/worldgen' +import type { Project } from '../contexts/Project.jsx' import type { VersionId } from '../services/index.js' -import { checkVersion, fetchPreset } from '../services/index.js' -import { BiMap, clamp, deepClone, deepEqual, square, stringToColor } from '../Utils.js' - -let ready = false -async function loadWasm() { - if (ready) return - await init(wasm) - ready = true - console.debug(`Loaded deepslate-rs from "${wasm}"`) -} - -const LAYERS = { - temperature: [-1, 1], - humidity: [-1, 1], - continentalness: [-1.1, 1], - erosion: [-1, 1], - weirdness: [-1, 1], - offset: [-1, 1], - factor: [0, 12], - jaggedness: [0, 1], -} +import { stringToColor } from '../Utils.js' +import { DEEPSLATE } from './Deepslate.js' +import { getProjectData } from './NoiseSettings.js' type Triple = [number, number, number] type BiomeColors = Record type BiomeSourceOptions = { - octaves: Record, biomeColors: BiomeColors, offset: [number, number], scale: number, res: number, seed: bigint, version: VersionId, - layers: Set, + settings: unknown, + project: Project, } -interface CachedBiomeSource { - getBiome(x: number, y: number, z: number): Identifier - getBiomes?(xFrom: number, xTo: number, xStep: number, yFrom: number, yTo: number, yStep: number, zFrom: number, zTo: number, zStep: number): Identifier[] - getClimate?(x: number, y: number, z: number): {[k: string]: number} - getClimates?(xFrom: number, xTo: number, xStep: number, yFrom: number, yTo: number, yStep: number, zFrom: number, zTo: number, zStep: number): {[k: string]: number}[] -} - -let cacheState: any -let biomeSourceCache: CachedBiomeSource - export async function biomeMap(state: any, img: ImageData, options: BiomeSourceOptions) { - const { biomeSource } = await getCached(state, options) + await DEEPSLATE.loadVersion(options.version, getProjectData(options.project)) + await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(options.settings), DataModel.unwrapLists(state), options.seed) - const data = img.data - const ox = -Math.round(options.offset[0]) - 100 + options.res / 2 - const oz = -Math.round(options.offset[1]) - 100 + options.res / 2 - const row = img.width * 4 / options.res - const col = 4 / options.res + const quartStep = Math.max(1, Math.round(options.scale)) + const quartWidth = 200 * quartStep - const xRange: Triple = [ox * options.scale, (200 + ox) * options.scale, options.res * options.scale] - const zRange: Triple = [oz * options.scale, (200 + oz) * options.scale, options.res * options.scale] + const centerX = Math.round(-options.offset[0] * options.scale) + const centerZ = Math.round(-options.offset[1] * options.scale) - const biomes = !options.layers.has('biomes') ? undefined : biomeSource.getBiomes?.(...xRange, 64, 65, 1, ...zRange) - const layers = [...options.layers].filter(l => l !== 'biomes') as (keyof typeof LAYERS)[] - const noise = layers.length === 0 ? undefined : biomeSource.getClimates?.(...xRange, 64, 65, 1, ...zRange) + const minX = Math.floor(centerX - quartWidth / 2) + const minZ = Math.floor(centerZ - quartWidth / 2) + const maxX = minX + quartWidth + const maxZ = minZ + quartWidth - for (let x = 0; x < 200; x += options.res) { - for (let z = 0; z < 200; z += options.res) { - const i = z * row + x * col - const j = (x / options.res) * 200 / options.res + z / options.res - const worldX = (x + ox) * options.scale - const worldZ = (z + oz) * options.scale - let color: Triple = [50, 50, 50] - if (options.layers.has('biomes')) { - const biome = biomes?.[j] ?? biomeSource.getBiome(worldX, 64, worldZ) - color = getBiomeColor(biome.toString(), options.biomeColors) - } else if (noise && layers[0]) { - const value = noise[j][layers[0]] - const [min, max] = LAYERS[layers[0]] - const brightness = (value - min) / (max - min) * 256 - color = [brightness, brightness, brightness] - } - data[i] = color[0] - data[i + 1] = color[1] - data[i + 2] = color[2] - data[i + 3] = 255 + const { palette, data, width, height } = DEEPSLATE.fillBiomes(minX * 4, maxX * 4, minZ * 4, maxZ * 4, quartStep * options.res) + + let x = 0 + let z = 0 + for (let i = 0; i < data.length; i += 1) { + const biome = palette.get(data[i]) + const color = getBiomeColor(biome ?? '', options.biomeColors) + const j = z * width + x + img.data[j * 4] = color[0] + img.data[j * 4 + 1] = color[1] + img.data[j * 4 + 2] = color[2] + img.data[j * 4 + 3] = 255 + + z += 1 + if (z >= height) { + z = 0 + x += 1 } } } export async function getBiome(state: any, x: number, z: number, options: BiomeSourceOptions): Promise<{[k: string]: number | string} | undefined> { - const { biomeSource } = await getCached(state, options) + await DEEPSLATE.loadVersion(options.version, getProjectData(options.project)) + await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(options.settings), DataModel.unwrapLists( state), options.seed) + + const quartStep = Math.max(1, Math.round(options.scale)) + + const centerX = Math.round(-options.offset[0] * options.scale) + const centerZ = Math.round(-options.offset[1] * options.scale) + + const xx = Math.floor(centerX + ((x - 100) * quartStep)) + const zz = Math.floor(centerZ + ((z - 100) * quartStep)) + + const { palette, data } = DEEPSLATE.fillBiomes(xx * 4, xx * 4 + 4, zz * 4, zz * 4 + 4) + const biome = palette.get(data[0])! - const [xx, zz] = toWorld([x, z], options) return { - biome: biomeSource.getBiome(xx, 64, zz).toString(), - ...biomeSource.getClimate?.(xx, 64, zz), + biome, } } -async function getCached(state: any, options: BiomeSourceOptions): Promise<{ biomeSource: CachedBiomeSource}> { - const newState = [state, options.octaves, `${options.seed}`, options.version] - if (!deepEqual(newState, cacheState)) { - cacheState = deepClone(newState) - - biomeSourceCache = await getBiomeSource(state, options) - } - return { - biomeSource: biomeSourceCache, - } -} - -async function getBiomeSource(state: any, options: BiomeSourceOptions): Promise { - switch (state?.type?.replace(/^minecraft:/, '')) { - case 'fixed': - return new FixedBiome(Identifier.parse(state.biome as string)) - - case 'checkerboard': - const shift = (state.scale ?? 2) + 2 - const numBiomes = state.biomes?.length ?? 0 - return { - getBiome(x: number, _y: number, z: number) { - const i = (((x >> shift) + (z >> shift)) % numBiomes + numBiomes) % numBiomes - return Identifier.parse(state.biomes?.[i].node as string) - }, - } - - case 'multi_noise': - switch(state.preset?.replace(/^minecraft:/, '')) { - case 'nether': - state = checkVersion(options.version, '1.18') ? NetherPreset18 : NetherPreset - break - case 'overworld': - state = checkVersion(options.version, '1.18') ? await OverworldPreset18() : state - break - } - state = DataModel.unwrapLists(state) - if (checkVersion(options.version, '1.18')) { - await loadWasm() - const BiomeIds = new BiMap() - const param = (p: number | number[]) => { - return typeof p === 'number' ? [p, p] : p - } - const [t0, t1, h0, h1, c0, c1, e0, e1, w0, w1, d0, d1, o, b] = [[], [], [], [], [], [], [], [], [], [], [], [], [], []] as number[][] - for (const i of state.biomes) { - const { temperature, humidity, continentalness, erosion, weirdness, depth, offset } = i.parameters - t0.push(param(temperature)[0]) - t1.push(param(temperature)[1]) - h0.push(param(humidity)[0]) - h1.push(param(humidity)[1]) - c0.push(param(continentalness)[0]) - c1.push(param(continentalness)[1]) - e0.push(param(erosion)[0]) - e1.push(param(erosion)[1]) - w0.push(param(weirdness)[0]) - w1.push(param(weirdness)[1]) - d0.push(param(depth)[0]) - d1.push(param(depth)[1]) - o.push(offset) - b.push(BiomeIds.getOrPut(i.biome, Math.floor(Math.random() * 2147483647))) - } - const parameters = biome_parameters(new Float64Array(t0), new Float64Array(t1), new Float64Array(h0), new Float64Array(h1), new Float64Array(c0), new Float64Array(c1), new Float64Array(e0), new Float64Array(e1), new Float64Array(w0), new Float64Array(w1), new Float64Array(d0), new Float64Array(d1), new Float64Array(o), new Int32Array(b)) - const sampler = climate_sampler(options.seed, options.octaves.temperature.firstOctave, new Float64Array(options.octaves.temperature.amplitudes), options.octaves.humidity.firstOctave, new Float64Array(options.octaves.humidity.amplitudes), options.octaves.continentalness.firstOctave, new Float64Array(options.octaves.continentalness.amplitudes), options.octaves.erosion.firstOctave, new Float64Array(options.octaves.erosion.amplitudes), options.octaves.weirdness.firstOctave, new Float64Array(options.octaves.weirdness.amplitudes), options.octaves.shift.firstOctave, new Float64Array(options.octaves.shift.amplitudes)) - return { - getBiome(x, y, z) { - const ids = multi_noise(parameters, sampler, x, x + 1, 1, y, y + 1, 1, z, z + 1, 1) - return Identifier.parse(BiomeIds.getA(ids[0]) ?? 'unknown') - }, - getBiomes(xFrom, xTo, xStep, yFrom, yTo, yStep, zFrom, zTo, zStep) { - const ids = multi_noise(parameters, sampler, xFrom, xTo, xStep, yFrom, yTo, yStep, zFrom, zTo, zStep) - return [...ids].map(id => Identifier.parse(BiomeIds.getA(id) ?? 'unknown')) - }, - getClimate(x, y, z) { - const climate = climate_noise(sampler, x, x + 1, 1, y, y + 1, 1, z, z + 1, 1) - const [t, h, c, e, w] = climate.slice(0, 5) - return { - temperature: t, - humidity: h, - continentalness: c, - erosion: e, - weirdness: w, - } - }, - getClimates(xFrom, xTo, xStep, yFrom, yTo, yStep, zFrom, zTo, zStep) { - const climate = climate_noise(sampler, xFrom, xTo, xStep, yFrom, yTo, yStep, zFrom, zTo, zStep) - const result = [] - for (let i = 0; i < climate.length; i += 7) { - const [t, h, c, e, w] = climate.slice(i, i + 5) - result.push({ - temperature: t, - humidity: h, - continentalness: c, - erosion: e, - weirdness: w, - }) - } - return result - }, - } - } else { - const noise = ['altitude', 'temperature', 'humidity', 'weirdness'] - .map((id, i) => { - const config = state[`${id}_noise`] - config.firstOctave = clamp(config.firstOctave ?? -7, -100, -1) - return new NormalNoise(new LegacyRandom(options.seed + BigInt(i)), config) - }) - if (!Array.isArray(state.biomes) || state.biomes.length === 0) { - return new FixedBiome(Identifier.create('unknown')) - } - return { - getBiome(x: number, _y: number, z: number): Identifier { - const n = noise.map(n => n.sample(x, z, 0)) - let minDist = Infinity - let minBiome = '' - for (const { biome, parameters: p } of state.biomes) { - const dist = square(p.altitude - n[0]) + square(p.temperature - n[1]) + square(p.humidity - n[2]) + square(p.weirdness - n[3]) + square(p.offset) - if (dist < minDist) { - minDist = dist - minBiome = biome - } - } - return Identifier.parse(minBiome) - }, - } - } - } - throw new Error('Unknown biome source') -} - function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple { if (!biome) { return [128, 128, 128] @@ -240,12 +85,6 @@ function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple { return color } -function toWorld([x, z]: [number, number], options: BiomeSourceOptions) { - const xx = (x - options.offset[0] - 100 + options.res / 2) * options.scale - const zz = (z - options.offset[1] - 100 + options.res / 2) * options.scale - return [xx, zz] -} - export const VanillaColors: Record = { 'minecraft:badlands': [217,69,21], 'minecraft:badlands_plateau': [202,140,101], @@ -267,9 +106,9 @@ export const VanillaColors: Record = { 'minecraft:desert': [250,148,24], 'minecraft:desert_hills': [210,95,18], 'minecraft:desert_lakes': [255,188,64], - 'minecraft:end_barrens': [128,128,255], - 'minecraft:end_highlands': [128,128,255], - 'minecraft:end_midlands': [128,128,255], + 'minecraft:end_barrens': [39,30,61], + 'minecraft:end_highlands': [232,244,178], + 'minecraft:end_midlands': [194,187,136], 'minecraft:eroded_badlands': [255,109,61], 'minecraft:flower_forest': [45,142,73], 'minecraft:forest': [5,102,33], @@ -310,7 +149,7 @@ export const VanillaColors: Record = { 'minecraft:shattered_savanna': [229,218,135], 'minecraft:windswept_savanna': [229,218,135], 'minecraft:shattered_savanna_plateau': [207,197,140], - 'minecraft:small_end_islands': [128,128,255], + 'minecraft:small_end_islands': [16,12,28], 'minecraft:snowy_beach': [250,240,192], 'minecraft:snowy_mountains': [160,160,160], 'minecraft:snowy_taiga': [49,85,74], @@ -330,7 +169,7 @@ export const VanillaColors: Record = { 'minecraft:tall_birch_forest': [88,156,108], 'minecraft:old_growth_birch_forest': [88,156,108], 'minecraft:tall_birch_hills': [71,135,90], - 'minecraft:the_end': [128,128,255], + 'minecraft:the_end': [59,39,84], 'minecraft:the_void': [0,0,0], 'minecraft:warm_ocean': [0,0,172], 'minecraft:warped_forest': [73,144,123], @@ -350,12 +189,3 @@ export const VanillaColors: Record = { 'minecraft:lush_caves': [112, 255, 79], 'minecraft:dripstone_caves': [140, 124, 0], } - -const NetherPreset = {type:'minecraft:multi_noise',seed:0,altitude_noise:{firstOctave:-7,amplitudes:[1,1]},temperature_noise:{firstOctave:-7,amplitudes:[1,1]},humidity_noise:{firstOctave:-7,amplitudes:[1,1]},weirdness_noise:{firstOctave:-7,amplitudes:[1,1]},biomes:[{biome:'minecraft:nether_wastes',parameters:{altitude:0,temperature:0,humidity:0,weirdness:0,offset:0}},{biome:'minecraft:soul_sand_valley',parameters:{altitude:0,temperature:0,humidity:-0.5,weirdness:0,offset:0}},{biome:'minecraft:crimson_forest',parameters:{altitude:0,temperature:0.4,humidity:0,weirdness:0,offset:0}},{biome:'minecraft:warped_forest',parameters:{altitude:0,temperature:0,humidity:0.5,weirdness:0,offset:0.375}},{biome:'minecraft:basalt_deltas',parameters:{altitude:0,temperature:-0.5,humidity:0,weirdness:0,offset:0.175}}]} - -const NetherPreset18 = {type:'minecraft:multi_noise',biomes:[{biome:'minecraft:nether_wastes',parameters:{temperature:0,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:soul_sand_valley',parameters:{temperature:0,humidity:-0.5,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:crimson_forest',parameters:{temperature:0.4,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0}},{biome:'minecraft:warped_forest',parameters:{temperature:0,humidity:0.5,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0.375}},{biome:'minecraft:basalt_deltas',parameters:{temperature:-0.5,humidity:0,continentalness:0,erosion:0,depth:0,weirdness:0,offset:0.175}}]} - -async function OverworldPreset18() { - const overworld = await fetchPreset('1.18', 'dimension', 'overworld') - return overworld.generator.biome_source -} diff --git a/src/app/previews/Deepslate.ts b/src/app/previews/Deepslate.ts index db1e610f..07fbece7 100644 --- a/src/app/previews/Deepslate.ts +++ b/src/app/previews/Deepslate.ts @@ -1,14 +1,14 @@ -import { DataModel } from '@mcschema/core' import * as deepslate19 from 'deepslate/worldgen' import type { VersionId } from '../services/index.js' -import { checkVersion, fetchAllPresets } from '../services/index.js' -import { deepClone, deepEqual } from '../Utils.js' +import { checkVersion, fetchAllPresets, fetchPreset } from '../services/index.js' +import { BiMap, clamp, computeIfAbsentAsync, deepClone, deepEqual, isObject, square } from '../Utils.js' export type ProjectData = Record> const DYNAMIC_REGISTRIES = new Set([ 'minecraft:worldgen/noise', 'minecraft:worldgen/density_function', + 'minecraft:worldgen/noise_settings', ]) export class Deepslate { @@ -17,12 +17,18 @@ export class Deepslate { private loadingVersion: VersionId | undefined private loadingPromise: Promise | undefined private readonly deepslateCache = new Map() + private readonly Y = 64 private readonly Z = 0 + private readonly DEBUG = false private cacheState: unknown private settingsCache: NoiseSettings | undefined private generatorCache: ChunkGenerator | undefined + private biomeSourceCache: BiomeSource | undefined + private randomStateCache: deepslate19.RandomState | undefined private chunksCache: Chunk[] = [] + private biomeCache: Map = new Map() + private readonly presetCache: Map = new Map() public async loadVersion(version: VersionId, project?: ProjectData) { if (this.loadedVersion === version) { @@ -58,14 +64,14 @@ export class Deepslate { } })) } else if (checkVersion(version, '1.18.2')) { - const REGISTRIES: [string, keyof typeof deepslate19.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) + await Promise.all([...DYNAMIC_REGISTRIES].map(async (id) => { + const entries = await fetchAllPresets(version, id.replace(/^minecraft:/, '')) for (const [key, value] of entries.entries()) { - this.d.WorldgenRegistries[name].register(this.d.Identifier.parse(key), parser.fromJson(value), true) + if (id === 'minecraft:worldgen/noise') { + this.d.WorldgenRegistries.NOISE.register(this.d.Identifier.parse(key), this.d.NoiseParameters.fromJson(value), true) + } else if (id === 'minecraft:worldgen/density_function') { + this.d.WorldgenRegistries.DENSITY_FUNCTION.register(this.d.Identifier.parse(key), this.d.DensityFunction.fromJson(value), true) + } } })) } @@ -89,22 +95,121 @@ export class Deepslate { } } - public loadChunkGenerator(settings: unknown, seed: bigint, biome = 'unknown') { - if (!this.loadedVersion) { - throw new Error('No deepslate version loaded') - } - const newCacheState = [settings, `${seed}`, biome] + public async loadChunkGenerator(settings: unknown, biomeState: unknown, seed: bigint) { + const newCacheState = [settings, `${seed}`, biomeState] if (!deepEqual(this.cacheState, newCacheState)) { - const biomeSource = new this.d.FixedBiome(checkVersion(this.loadedVersion, '1.18.2') ? this.d.Identifier.parse(biome) : biome as any) - const noiseSettings = this.d.NoiseGeneratorSettings.fromJson(DataModel.unwrapLists(settings)) - const chunkGenerator = new this.d.NoiseChunkGenerator(seed, biomeSource, noiseSettings) + const noiseSettings = this.createNoiseSettings(settings) + const biomeSource = await this.createBiomeSource(noiseSettings, biomeState, seed) + const chunkGenerator = this.isVersion('1.19') + ? new this.d.NoiseChunkGenerator(biomeSource, noiseSettings) + : new (this.d.NoiseChunkGenerator as any)(seed, biomeSource, noiseSettings) this.settingsCache = noiseSettings.noise this.generatorCache = chunkGenerator + if (this.isVersion('1.19')) { + this.randomStateCache = new this.d.RandomState(noiseSettings, seed) + } else { + this.randomStateCache = undefined + } + this.biomeSourceCache = { + getBiome: (x, y, z) => biomeSource.getBiome(x, y, z, undefined!), + } this.chunksCache = [] + this.biomeCache = new Map() this.cacheState = deepClone(newCacheState) } } + private async createBiomeSource(noiseSettings: deepslate19.NoiseGeneratorSettings, biomeState: unknown, seed: bigint): Promise { + if (this.loadedVersion && isObject(biomeState) && typeof biomeState.preset === 'string') { + const version = this.loadedVersion + const preset = biomeState.preset.replace(/^minecraft:/, '') + const biomes = await computeIfAbsentAsync(this.presetCache, `${version}-${preset}`, async () => { + const dimension = await fetchPreset(version, 'dimension', preset === 'overworld' ? 'overworld' : 'the_nether') + return dimension.generator.biome_source.biomes + }) + biomeState = { type: biomeState.type, biomes } + } + if (this.isVersion('1.19')) { + return this.d.BiomeSource.fromJson(biomeState) + } else { + const root = isObject(biomeState) ? biomeState : {} + const type = typeof root.type === 'string' ? root.type.replace(/^minecraft:/, '') : undefined + switch (type) { + case 'fixed': + return new (this.d as any).FixedBiome(this.isVersion('1.18.2') ? this.d.Identifier.parse(root.biome as string) : root.biome as any) + case 'checkerboard': + const shift = (root.scale ?? 2) + 2 + const numBiomes = root.biomes?.length ?? 0 + return { getBiome: (x: number, _y: number, z: number) => { + const i = (((x >> shift) + (z >> shift)) % numBiomes + numBiomes) % numBiomes + const biome = root.biomes?.[i] + return this.isVersion('1.18.2') ? this.d.Identifier.parse(biome) : biome + } } + case 'multi_noise': + if (this.isVersion('1.18')) { + const parameters = new this.d.Climate.Parameters(root.biomes.map((b: any) => { + const biome = this.isVersion('1.18.2') ? this.d.Identifier.parse(b.biome) : b.biome + return [this.d.Climate.ParamPoint.fromJson(b.parameters), () => biome] + })) + const multiNoise = new (this.d as any).MultiNoise(parameters) + let sampler: any + if (this.isVersion('1.18.2')) { + const router = this.d.NoiseRouter.create({ + temperature: new this.d.DensityFunction.Noise(0.25, 0, (this.d as any).Noises.TEMPERATURE), + vegetation: new this.d.DensityFunction.Noise(0.25, 0, (this.d as any).Noises.VEGETATION), + continents: new this.d.DensityFunction.Noise(0.25, 0, (this.d as any).Noises.CONTINENTALNESS), + erosion: new this.d.DensityFunction.Noise(0.25, 0, (this.d as any).Noises.EROSION), + ridges: new this.d.DensityFunction.Noise(0.25, 0, (this.d as any).Noises.RIDGE), + }) + sampler = this.d.Climate.Sampler.fromRouter((this.d.NoiseRouter as any).withSettings(router, noiseSettings, seed)) + } else { + const noiseSampler = new (this.d as any).NoiseSampler(this.d.NoiseSettings.fromJson(null), true, seed, true) + sampler = (x: number, y: number, z: number) => noiseSampler.sample(x, y, z) + } + return { getBiome: (x: number, y: number, z: number) => { + return multiNoise.getBiome(x, y, z, sampler) + } } + } else { + const noise = ['altitude', 'temperature', 'humidity', 'weirdness'] + .map((id, i) => { + const config = root[`${id}_noise`] + config.firstOctave = clamp(config.firstOctave ?? -7, -100, -1) + return new this.d.NormalNoise(new this.d.LegacyRandom(seed + BigInt(i)), config) + }) + if (!Array.isArray(root.biomes) || root.biomes.length === 0) { + return { getBiome: () => this.d.Identifier.create('unknown') } + } + return { getBiome: (x: number, _y: number, z: number) => { + const n = noise.map(n => n.sample(x, z, 0)) + let minDist = Infinity + let minBiome = 'unknown' + for (const { biome, parameters: p } of root.biomes) { + const dist = square(p.altitude - n[0]) + square(p.temperature - n[1]) + square(p.humidity - n[2]) + square(p.weirdness - n[3]) + square(p.offset) + if (dist < minDist) { + minDist = dist + minBiome = biome + } + } + return minBiome as unknown as deepslate19.Identifier + } } + } + default: throw new Error(`Unsupported biome source ${type}`) + } + } + } + + private createNoiseSettings(settings: unknown): deepslate19.NoiseGeneratorSettings { + if (typeof settings === 'string') { + if (this.isVersion('1.19')) { + return this.d.WorldgenRegistries.NOISE_SETTINGS.getOrThrow(this.d.Identifier.parse(settings)) + } else { + return this.d.NoiseGeneratorSettings.fromJson(undefined) + } + } else { + return this.d.NoiseGeneratorSettings.fromJson(settings) + } + } + public generateChunks(minX: number, width: number, biome = 'unknown') { minX = Math.floor(minX) if (!this.settingsCache) { @@ -114,7 +219,7 @@ export class Deepslate { const height = this.settingsCache.height return [...Array(Math.ceil(width / 16) + 1)].map((_, i) => { - const x = (minX >> 4) + i + const x: number = (minX >> 4) + i const cached = this.chunksCache.find(c => c.pos[0] === x) if (cached) { return cached @@ -123,28 +228,110 @@ export class Deepslate { if (!this.generatorCache) { throw new Error('Tried to generate chunks before generator is loaded') } - this.generatorCache.fill(chunk, true) - this.generatorCache.buildSurface(chunk, biome) + if (checkVersion(this.loadedVersion!, '1.19')) { + if (!this.randomStateCache) { + throw new Error('Tried to generate chunks before random state is loaded') + } + this.generatorCache.fill(this.randomStateCache, chunk, true) + this.generatorCache.buildSurface(this.randomStateCache, chunk, biome) + } else { + (this.generatorCache as any).fill(chunk, true); + (this.generatorCache as any).buildSurface(chunk, biome) + } this.chunksCache.push(chunk) return chunk }) } + public fillBiomes(minX: number, maxX: number, minZ: number, maxZ: number, step = 1) { + if (!this.generatorCache || !this.settingsCache) { + throw new Error('Tried to fill biomes before generator is loaded') + } + const quartY = (this.Y - this.settingsCache.minY) >> 2 + const minQuartX = minX >> 2 + const maxQuartX = maxX >> 2 + const minQuartZ = minZ >> 2 + const maxQuartZ = maxZ >> 2 + const countX = Math.floor((maxQuartX - minQuartX) / step) + const countZ = Math.floor((maxQuartZ - minQuartZ) / step) + + const biomeIds = new BiMap() + const data = new Int8Array(countX * countZ) + let biomeId = 0 + let i = 0 + + for (let x = minQuartX; x < maxQuartX; x += step) { + for (let z = minQuartZ; z < maxQuartZ; z += step) { + const posKey = `${x}:${z}` + let biome = this.biomeCache.get(posKey) + if (!biome) { + if (this.DEBUG) { + biome = this.computeDebugBiome(x, z) + } else if (this.isVersion('1.19')) { + if (!this.randomStateCache) { + throw new Error('Tried to compute biomes before random state is loaded') + } + biome = this.generatorCache.computeBiome(this.randomStateCache, x, quartY, z).toString() + } else { + if(!this.biomeSourceCache) { + throw new Error('Tried to compute biomes before biome source is loaded') + } + biome = this.biomeSourceCache.getBiome(x, quartY, z).toString() + } + this.biomeCache.set(posKey, biome) + } + data[i++] = biomeIds.computeIfAbsent(biome, () => biomeId++) + } + } + + return { + palette: biomeIds.backward, + data, + width: countX, + height: countZ, + } + } + + private computeDebugBiome(x: number, z: number) { + if (x > 0) { + return z > 0 ? 'minecraft:plains' : 'minecraft:forest' + } else { + return z > 0 ? 'minecraft:badlands' : 'minecraft:desert' + } + } + 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)) + if (this.isVersion('1.19')) { + const settings = this.d.NoiseGeneratorSettings.create({ + noise: { + minY: -64, + height: 384, + xzSize: 1, + ySize: 2, + }, + noiseRouter: this.d.NoiseRouter.create({ + finalDensity: this.d.DensityFunction.fromJson(state), + }), + }) + this.settingsCache = settings.noise + const randomState = new this.d.RandomState(settings, seed) + return randomState.router.finalDensity + } else { + 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 as any).Visitor(random, settings)) + } } public getNoiseSettings(): NoiseSettings { @@ -160,19 +347,33 @@ export class Deepslate { 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, this.Z)) } + + private isVersion(min?: VersionId, max?: VersionId) { + if (!this.loadedVersion) { + throw new Error('No deepslate version loaded') + } + return checkVersion(this.loadedVersion, min, max) + } } +export const DEEPSLATE = new Deepslate() + interface NoiseSettings { - minY: number, - height: number, + minY: number + height: number } interface ChunkGenerator { - fill(chunk: Chunk, onlyFirstZ?: boolean): void - buildSurface(chunk: Chunk, biome: string): void + fill(randomState: deepslate19.RandomState, chunk: Chunk, onlyFirstZ?: boolean): void + buildSurface(randomState: deepslate19.RandomState, chunk: Chunk, biome: string): void + computeBiome(randomState: deepslate19.RandomState, quartX: number, quartY: number, quartZ: number): deepslate19.Identifier } interface Chunk { - readonly pos: deepslate19.ChunkPos; - getBlockState(pos: deepslate19.BlockPos): deepslate19.BlockState; + readonly pos: deepslate19.ChunkPos + getBlockState(pos: deepslate19.BlockPos): deepslate19.BlockState +} + +interface BiomeSource { + getBiome(x: number, y: number, z: number): deepslate19.Identifier } diff --git a/src/app/previews/NoiseSettings.ts b/src/app/previews/NoiseSettings.ts index 82404dd4..306689f9 100644 --- a/src/app/previews/NoiseSettings.ts +++ b/src/app/previews/NoiseSettings.ts @@ -1,8 +1,9 @@ +import { DataModel } from '@mcschema/core' import { BlockState, clampedMap, DensityFunction } from 'deepslate/worldgen' import type { Project } from '../contexts/Project.jsx' import type { VersionId } from '../services/index.js' import { checkVersion } from '../services/index.js' -import { Deepslate } from './Deepslate.js' +import { DEEPSLATE } from './Deepslate.js' import { NoiseChunkGenerator as OldNoiseChunkGenerator } from './noise/NoiseChunkGenerator.js' export type NoiseSettingsOptions = { @@ -35,13 +36,12 @@ const colors: Record = { 'minecraft:end_stone': [200, 200, 140], } -const DEEPSLATE = new Deepslate() - export async function noiseSettings(state: any, img: ImageData, options: NoiseSettingsOptions) { if (checkVersion(options.version, '1.18')) { await DEEPSLATE.loadVersion(options.version, getProjectData(options.project)) - DEEPSLATE.loadChunkGenerator(state, options.seed, options.biome) - DEEPSLATE.generateChunks(-options.offset, options.width, options.biome) + const biomeSource = { type: 'fixed', biome: options.biome } + await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(state), biomeSource, options.seed) + DEEPSLATE.generateChunks(-options.offset, options.width) const noise = DEEPSLATE.getNoiseSettings() const data = img.data @@ -82,7 +82,7 @@ export function getNoiseBlock(x: number, y: number) { export async function densityFunction(state: any, img: ImageData, options: NoiseSettingsOptions) { await DEEPSLATE.loadVersion(options.version, getProjectData(options.project)) - const fn = DEEPSLATE.loadDensityFunction(state, options.seed) + const fn = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(state), options.seed) const noise = DEEPSLATE.getNoiseSettings() const arr = Array(options.width * noise.height) @@ -108,7 +108,7 @@ export async function densityFunction(state: any, img: ImageData, options: Noise } } -function getProjectData(project: Project) { +export function getProjectData(project: Project) { return Object.fromEntries(['worldgen/noise', 'worldgen/density_function'].map(type => { const resources = Object.fromEntries( project.files.filter(file => file.type === type) diff --git a/src/locales/en.json b/src/locales/en.json index 6813526e..6fc57e98 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -219,5 +219,6 @@ "worldgen/world_preset": "World Preset", "worldgen/flat_level_generator_preset": "Flat World Preset", "zoom_in": "Zoom in", + "zoom_in_limit": "Cannot zoom in further\n1 pixel = 4 blocks", "zoom_out": "Zoom out" }