mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-24 23:56:51 +00:00
Improve biome map (#246)
* Start biome map rewrite * Remove climate layers and add end biome source * Update to use RandomState * Make biome map work for past versions * Remove old biome map code * Implement multi noise presets and fix caching * Update deepslate * Fix biome hover * Fix #190 biome map diagonal lines Also increases performance by making better use of the biome cache * Add proper zoom limit
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -256,8 +256,8 @@ export function deepEqual(a: any, b: any) {
|
||||
}
|
||||
|
||||
export class BiMap<A, B> {
|
||||
private readonly forward: Map<A, B>
|
||||
private readonly backward: Map<B, A>
|
||||
public readonly forward: Map<A, B>
|
||||
public readonly backward: Map<B, A>
|
||||
|
||||
constructor() {
|
||||
this.forward = new Map()
|
||||
@@ -285,6 +285,16 @@ export class BiMap<A, B> {
|
||||
}
|
||||
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<string> {
|
||||
}))
|
||||
return await writer.close()
|
||||
}
|
||||
|
||||
export function computeIfAbsent<K, V>(map: Map<K, V>, 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<K, V>(map: Map<K, V>, key: K, getter: (key: K) => Promise<V>): Promise<V> {
|
||||
const existing = map.get(key)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
const value = await getter(key)
|
||||
map.set(key, value)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -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<typeof LAYERS[number]>(['biomes']))
|
||||
const { biomeColors } = useStore()
|
||||
const offset = useRef<[number, number]>([0, 0])
|
||||
const res = useRef(1)
|
||||
const refineTimeout = useRef<number>()
|
||||
|
||||
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 <>
|
||||
<div class="controls preview-controls">
|
||||
{focused && <Btn label={focused.biome as string} class="no-pointer" />}
|
||||
{type === 'multi_noise' &&
|
||||
<BtnMenu icon="stack" tooltip={locale('configure_layers')}>
|
||||
{LAYERS.map(name => {
|
||||
const enabled = layers.has(name)
|
||||
return <Btn label={locale(`layer.${name}`)}
|
||||
active={enabled}
|
||||
tooltip={enabled ? locale('enabled') : locale('disabled')}
|
||||
onClick={(e) => {
|
||||
setLayers(new Set([name]))
|
||||
e.stopPropagation()
|
||||
}} />
|
||||
})}
|
||||
</BtnMenu>}
|
||||
{(type === 'multi_noise' || type === 'checkerboard') && <>
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => changeScale(scale * 1.5)} />
|
||||
<Btn icon="plus" tooltip={locale('zoom_in')}
|
||||
onClick={() => changeScale(scale / 1.5)} />
|
||||
</>}
|
||||
{type === 'multi_noise' &&
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => changeScale(scale * 2)} />
|
||||
<Btn icon="plus" tooltip={locale(Math.round(scale) <= 1 ? 'zoom_in_limit' : 'zoom_in')}
|
||||
disabled={Math.round(scale) <= 1}
|
||||
onClick={() => changeScale(scale / 2)} />
|
||||
{(type === 'multi_noise' || type === 'the_end') &&
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => newSeed(model)} />}
|
||||
onClick={() => setSeed(randomSeed())} />}
|
||||
</div>
|
||||
{focused?.temperature !== undefined && <div class="controls secondary-controls">
|
||||
<Btn class="no-pointer" label={Object.entries(focused)
|
||||
@@ -108,43 +88,3 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps
|
||||
<canvas ref={canvas} width="200" height="200"></canvas>
|
||||
</>
|
||||
}
|
||||
|
||||
function calculateState(data: any, octaves: Record<string, NoiseParameters>) {
|
||||
return JSON.stringify([data, octaves])
|
||||
}
|
||||
|
||||
export function getOctaves(obj: any): Record<string, NoiseParameters> {
|
||||
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] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, Triple>
|
||||
type BiomeSourceOptions = {
|
||||
octaves: Record<string, NoiseParameters>,
|
||||
biomeColors: BiomeColors,
|
||||
offset: [number, number],
|
||||
scale: number,
|
||||
res: number,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
layers: Set<keyof typeof LAYERS | 'biomes'>,
|
||||
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<CachedBiomeSource> {
|
||||
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<string, number>()
|
||||
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<string, Triple> = {
|
||||
'minecraft:badlands': [217,69,21],
|
||||
'minecraft:badlands_plateau': [202,140,101],
|
||||
@@ -267,9 +106,9 @@ export const VanillaColors: Record<string, Triple> = {
|
||||
'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<string, Triple> = {
|
||||
'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<string, Triple> = {
|
||||
'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<string, Triple> = {
|
||||
'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
|
||||
}
|
||||
|
||||
@@ -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<string, Record<string, unknown>>
|
||||
|
||||
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<void> | undefined
|
||||
private readonly deepslateCache = new Map<VersionId, typeof deepslate19>()
|
||||
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<string, string> = new Map()
|
||||
private readonly presetCache: Map<string, unknown> = 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<deepslate19.BiomeSource> {
|
||||
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<string, number>()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<string, [number, number, number]> = {
|
||||
'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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user