diff --git a/package-lock.json b/package-lock.json index 0361fe2e..0a80c06e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,10 @@ "@mcschema/java-1.15": "^0.2.0", "@mcschema/java-1.16": "^0.6.3", "@mcschema/java-1.17": "^0.2.23", - "@mcschema/locales": "^0.1.21", - "rfdc": "^1.3.0", - "seedrandom": "^3.0.5" + "@mcschema/java-1.18": "^0.1.1", + "@mcschema/locales": "^0.1.23", + "deepslate": "^0.9.0-beta.2", + "rfdc": "^1.3.0" }, "devDependencies": { "@preact/preset-vite": "^2.1.0", @@ -342,10 +343,18 @@ "@mcschema/core": "^0.12.4" } }, + "node_modules/@mcschema/java-1.18": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mcschema/java-1.18/-/java-1.18-0.1.1.tgz", + "integrity": "sha512-qyt3okv3837xyCYt7dxEpodDmn1sIh19OVvlxLIxr3AtCgm1h3c/HjOHan/X6RBdU7BWJqg4uTWO2II43VvrQA==", + "dependencies": { + "@mcschema/core": "^0.12.4" + } + }, "node_modules/@mcschema/locales": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@mcschema/locales/-/locales-0.1.21.tgz", - "integrity": "sha512-o3fLWz5RkuIzG86fIMELp2e+gB71KNb3I4gLRnDZoFeOim4BseF6Lg2lG2P+H0tuxlX7OWeZA6aiJdM+k0YKjg==" + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@mcschema/locales/-/locales-0.1.23.tgz", + "integrity": "sha512-D6577AtukJHU2THNbiYHKw/6KSpz+c0ooYt+5H7KQr3VyGRcpi2q7kl/WS8FYwR4ZoxEXaIFXVFMstG/SLGIJA==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -929,6 +938,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "node_modules/deepslate": { + "version": "0.9.0-beta.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.9.0-beta.2.tgz", + "integrity": "sha512-qJTzAfngaYXGXS5nXiHUgLbw+6/GtSXOuz+bDVb+EWYHUfekYw5qxpIQdKEKNVGobqchE4mAKJ84KAeWKW5wjw==", + "dependencies": { + "gl-matrix": "^3.3.0", + "pako": "^2.0.3" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1476,6 +1494,11 @@ "node": ">=6.9.0" } }, + "node_modules/gl-matrix": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz", + "integrity": "sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA==" + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -1883,6 +1906,11 @@ "node": ">= 0.8.0" } }, + "node_modules/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2141,11 +2169,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "node_modules/seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" - }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -2795,10 +2818,18 @@ "@mcschema/core": "^0.12.4" } }, + "@mcschema/java-1.18": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mcschema/java-1.18/-/java-1.18-0.1.1.tgz", + "integrity": "sha512-qyt3okv3837xyCYt7dxEpodDmn1sIh19OVvlxLIxr3AtCgm1h3c/HjOHan/X6RBdU7BWJqg4uTWO2II43VvrQA==", + "requires": { + "@mcschema/core": "^0.12.4" + } + }, "@mcschema/locales": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@mcschema/locales/-/locales-0.1.21.tgz", - "integrity": "sha512-o3fLWz5RkuIzG86fIMELp2e+gB71KNb3I4gLRnDZoFeOim4BseF6Lg2lG2P+H0tuxlX7OWeZA6aiJdM+k0YKjg==" + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@mcschema/locales/-/locales-0.1.23.tgz", + "integrity": "sha512-D6577AtukJHU2THNbiYHKw/6KSpz+c0ooYt+5H7KQr3VyGRcpi2q7kl/WS8FYwR4ZoxEXaIFXVFMstG/SLGIJA==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -3207,6 +3238,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepslate": { + "version": "0.9.0-beta.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.9.0-beta.2.tgz", + "integrity": "sha512-qJTzAfngaYXGXS5nXiHUgLbw+6/GtSXOuz+bDVb+EWYHUfekYw5qxpIQdKEKNVGobqchE4mAKJ84KAeWKW5wjw==", + "requires": { + "gl-matrix": "^3.3.0", + "pako": "^2.0.3" + } + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3620,6 +3660,11 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, + "gl-matrix": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz", + "integrity": "sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA==" + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -3936,6 +3981,11 @@ "word-wrap": "^1.2.3" } }, + "pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4095,11 +4145,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", diff --git a/package.json b/package.json index c790309d..2e8ba692 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,10 @@ "@mcschema/java-1.15": "^0.2.0", "@mcschema/java-1.16": "^0.6.3", "@mcschema/java-1.17": "^0.2.23", - "@mcschema/locales": "^0.1.21", - "rfdc": "^1.3.0", - "seedrandom": "^3.0.5" + "@mcschema/java-1.18": "^0.1.1", + "@mcschema/locales": "^0.1.23", + "deepslate": "^0.9.0-beta.2", + "rfdc": "^1.3.0" }, "devDependencies": { "@preact/preset-vite": "^2.1.0", diff --git a/src/app/DataFetcher.ts b/src/app/DataFetcher.ts index d8a7d3d0..7ed7a209 100644 --- a/src/app/DataFetcher.ts +++ b/src/app/DataFetcher.ts @@ -155,10 +155,11 @@ async function fetchDynamicRegistries(version: Version, target: CollectionRegist } export async function fetchPreset(version: VersionId, registry: string, id: string) { - console.debug(`[fetchPreset] ${id} ${registry} ${id}`) + console.debug(`[fetchPreset] ${registry} ${id}`) const versionData = config.versions.find(v => v.id === version)! try { - const res = await fetch(`${vanillaDatapackUrl}/${versionData.refs.vanilla_datapack_data}/data/minecraft/${registry}/${id}.json`) + const url = `${vanillaDatapackUrl}/${versionData.refs.vanilla_datapack_data}/data/minecraft/${registry}/${id}.json` + const res = await fetch(url) return await res.json() } catch (e) { console.warn(`Error occurred while fetching ${registry} preset ${id}:`, message(e)) diff --git a/src/app/Main.tsx b/src/app/Main.tsx index 85845ca9..9a038517 100644 --- a/src/app/Main.tsx +++ b/src/app/Main.tsx @@ -2,6 +2,7 @@ import { render } from 'preact' import type { RouterOnChangeArgs } from 'preact-router' import { Router } from 'preact-router' import { useEffect, useState } from 'preact/hooks' +import config from '../config.json' import '../styles/global.css' import '../styles/nodes.css' import { Analytics } from './Analytics' @@ -12,6 +13,8 @@ import type { VersionId } from './Schemas' import { Store } from './Store' import { cleanUrl } from './Utils' +const VERSIONS_IN_TITLE = 3 + function Main() { const [lang, setLanguage] = useState('en') const changeLanguage = async (language: string) => { @@ -51,7 +54,9 @@ function Main() { } const [title, setTitle] = useState(locale(lang, 'title.home')) - const changeTitle = (title: string, versions = ['1.15', '1.16', '1.17']) => { + const changeTitle = (title: string, versions?: VersionId[]) => { + versions ??= config.versions.map(v => v.id as VersionId) + versions.splice(0, versions.length - VERSIONS_IN_TITLE) document.title = `${title} Minecraft ${versions.join(', ')}` setTitle(title) } diff --git a/src/app/Schemas.ts b/src/app/Schemas.ts index 51988f21..e0705531 100644 --- a/src/app/Schemas.ts +++ b/src/app/Schemas.ts @@ -3,11 +3,12 @@ import { DataModel } from '@mcschema/core' import * as java15 from '@mcschema/java-1.15' import * as java16 from '@mcschema/java-1.16' import * as java17 from '@mcschema/java-1.17' +import * as java18 from '@mcschema/java-1.18' import config from '../config.json' import { fetchData } from './DataFetcher' import { message } from './Utils' -export const VersionIds = ['1.15', '1.16', '1.17'] as const +export const VersionIds = ['1.15', '1.16', '1.17', '1.18'] as const export type VersionId = typeof VersionIds[number] export type BlockStateRegistry = { @@ -43,6 +44,7 @@ const versionGetter: { 1.15: java15, 1.16: java16, 1.17: java17, + 1.18: java18, } async function getVersion(id: VersionId): Promise { diff --git a/src/app/Store.ts b/src/app/Store.ts index 7bb85294..12794bc1 100644 --- a/src/app/Store.ts +++ b/src/app/Store.ts @@ -39,7 +39,7 @@ export namespace Store { if (version) localStorage.setItem(ID_VERSION, version) } - export function setIndent(indent: string) { + export function setIndent(indent: string | undefined) { if (indent) localStorage.setItem(ID_INDENT, indent) } } diff --git a/src/app/Utils.ts b/src/app/Utils.ts index 61fb1978..aa12a858 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -1,3 +1,6 @@ +import type { DataModel } from '@mcschema/core' +import { Path } from '@mcschema/core' +import rfdc from 'rfdc' import config from '../config.json' export function isPromise(obj: any): obj is Promise { @@ -12,6 +15,16 @@ export function hexId(length = 12) { return Array.from(arr, dec2hex).join('') } +export function randomSeed() { + return BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) +} + +export function newSeed(model: DataModel) { + const seed = Math.floor(Math.random() * (4294967296)) - 2147483648 + model.set(new Path(['generator', 'seed']), seed, true) + model.set(new Path(['generator', 'biome_source', 'seed']), seed) +} + export function htmlEncode(str: string) { return str.replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/') @@ -38,6 +51,10 @@ export function stringToColor(str: string): [number, number, number] { return [h % 256, (h >> 8) % 256, (h >> 16) % 256] } +export function square(a: number) { + return a * a +} + export function clamp(a: number, b: number, c: number) { return Math.max(a, Math.min(b, c)) } @@ -72,3 +89,42 @@ export function message(e: unknown): string { if (e instanceof Error) return e.message return `${e}` } + +export const deepClone = rfdc() + +/** + * MIT License + * + * Copyright (c) 2017 Evgeny Poberezkin + * + * https://github.com/epoberezkin/fast-deep-equal/blob/master/LICENSE + */ +export function deepEqual(a: any, b: any) { + if (a === b) return true + + if (a && b && typeof a == 'object' && typeof b == 'object') { + if (a.constructor !== b.constructor) return false + let length, i + if (Array.isArray(a)) { + length = a.length + if (length != b.length) return false + for (i = length; i-- !== 0;) { + if (!deepEqual(a[i], b[i])) return false + } + return true + } + if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf() + if (a.toString !== Object.prototype.toString) return a.toString() === b.toString() + const keys = Object.keys(a) + length = keys.length + if (length !== Object.keys(b).length) return false + for (i = length; i-- !== 0;) + if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false + for (i = length; i-- !== 0;) { + const key = keys[i] + if (!deepEqual(a[key], b[key])) return false + } + return true + } + return a !== a && b !== b +} diff --git a/src/app/components/BtnInput.tsx b/src/app/components/BtnInput.tsx index 4e28007d..d2341998 100644 --- a/src/app/components/BtnInput.tsx +++ b/src/app/components/BtnInput.tsx @@ -5,17 +5,14 @@ type BtnInputProps = { icon?: keyof typeof Octicon, label?: string, large?: boolean, - type?: 'number' | 'text', doSelect?: number, value?: string, onChange?: (value: string) => unknown, } -export function BtnInput({ icon, label, large, type, doSelect, value, onChange }: BtnInputProps) { +export function BtnInput({ icon, label, large, doSelect, value, onChange }: BtnInputProps) { const onInput = onChange === undefined ? () => {} : (e: any) => { const value = (e.target as HTMLInputElement).value - if (type !== 'number' || (!value.endsWith('.') && !isNaN(Number(value)))) { - onChange?.(value) - } + onChange?.(value) } const ref = useRef(null) @@ -28,6 +25,6 @@ export function BtnInput({ icon, label, large, type, doSelect, value, onChange } return
e.stopPropagation()}> {icon && Octicon[icon]} {label && {label}} - +
} diff --git a/src/app/components/Octicon.tsx b/src/app/components/Octicon.tsx index eb506f57..6df56ae5 100644 --- a/src/app/components/Octicon.tsx +++ b/src/app/components/Octicon.tsx @@ -2,6 +2,7 @@ export const Octicon = { archive: , arrow_left: , arrow_right: , + check: , chevron_down: , chevron_right: , chevron_up: , diff --git a/src/app/components/PreviewPanel.tsx b/src/app/components/PreviewPanel.tsx index bd593f10..b343d43c 100644 --- a/src/app/components/PreviewPanel.tsx +++ b/src/app/components/PreviewPanel.tsx @@ -28,7 +28,7 @@ export function PreviewPanel({ lang, model, version, id, shown }: PreviewPanelPr } if (id === 'worldgen/noise_settings' && model) { - const data = model.get(new Path(['noise'])) + const data = model.get(new Path([])) if (data) return } diff --git a/src/app/components/SourcePanel.tsx b/src/app/components/SourcePanel.tsx index af826f32..3a92a104 100644 --- a/src/app/components/SourcePanel.tsx +++ b/src/app/components/SourcePanel.tsx @@ -1,6 +1,6 @@ import type { DataModel } from '@mcschema/core' import { ModelPath } from '@mcschema/core' -import { useEffect, useRef, useState } from 'preact/hooks' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { Btn, BtnMenu } from '.' import { useModel } from '../hooks' import { locale } from '../Locales' @@ -9,6 +9,8 @@ import type { BlockStateRegistry } from '../Schemas' import { Store } from '../Store' import { message } from '../Utils' +const OUTPUT_CHARS_LIMIT = 10000 + const INDENT: Record = { '2_spaces': 2, '4_spaces': 4, @@ -23,22 +25,31 @@ type SourcePanelProps = { doCopy?: number, doDownload?: number, doImport?: number, + copySuccess: () => unknown, onError: (message: string) => unknown, } -export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload, doImport, onError }: SourcePanelProps) { +export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) { const loc = locale.bind(null, lang) const [indent, setIndent] = useState(Store.getIndent()) const source = useRef(null) const download = useRef(null) const retransform = useRef() + const getOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => { + const data = model.schema.hook(transformOutput, new ModelPath(model), model.data, { blockStates }) + return JSON.stringify(data, null, INDENT[indent]) + '\n' + }, []) + useEffect(() => { retransform.current = () => { if (!model || !blockStates) return try { - const props = { blockStates: blockStates ?? {} } - const data = model.schema.hook(transformOutput, new ModelPath(model), model.data, props) - source.current.value = JSON.stringify(data, null, INDENT[indent]) + '\n' + const output = getOutput(model, blockStates) + if (output.length >= OUTPUT_CHARS_LIMIT) { + source.current.value = output.slice(0, OUTPUT_CHARS_LIMIT) + `\n\nOutput is too large to display (+${OUTPUT_CHARS_LIMIT} chars)\nExport to view complete output\n\n` + } else { + source.current.value = output + } } catch (e) { onError(`Error getting JSON output: ${message(e)}`) console.error(e) @@ -68,9 +79,10 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload } useEffect(() => { - if (doCopy && source.current) { - source.current.select() - document.execCommand('copy') + if (doCopy && model && blockStates) { + navigator.clipboard.writeText(getOutput(model, blockStates)).then(() => { + copySuccess() + }) } }, [doCopy]) diff --git a/src/app/components/previews/BiomeSourcePreview.tsx b/src/app/components/previews/BiomeSourcePreview.tsx index eab95ad9..53869a19 100644 --- a/src/app/components/previews/BiomeSourcePreview.tsx +++ b/src/app/components/previews/BiomeSourcePreview.tsx @@ -1,75 +1,67 @@ -import type { DataModel } from '@mcschema/core' +import { Path } from '@mcschema/core' +import type { NoiseOctaves } from 'deepslate' +import { NoiseGeneratorSettings } from 'deepslate' import { useEffect, useRef, useState } from 'preact/hooks' +import type { PreviewProps } from '.' import { Btn } from '..' -import { useOnDrag, useOnHover } from '../../hooks' -import { biomeSource, getBiome } from '../../previews' -import { hexId } from '../../Utils' +import { useCanvas } from '../../hooks' +import { biomeMap, getBiome } from '../../previews' +import { newSeed } from '../../Utils' -type BiomeSourceProps = { - lang: string, - model: DataModel, - data: any, - shown: boolean, -} -export const BiomeSourcePreview = ({ data, shown }: BiomeSourceProps) => { +export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps) => { const [scale, setScale] = useState(2) - const [seed, setSeed] = useState(hexId()) const [focused, setFocused] = useState(undefined) + const offset = useRef<[number, number]>([0, 0]) + const res = useRef(1) + const refineTimeout = useRef(undefined) + + const seed = BigInt(model.get(new Path(['generator', 'seed']))) + const octaves = getOctaves(model.get(new Path(['generator', 'settings']))) + const state = calculateState(data, octaves) const type: string = data.type?.replace(/^minecraft:/, '') - const canvas = useRef(null) - const offset = useRef<[number, number]>([0, 0]) - const redrawTimeout = useRef(undefined) - const redraw = useRef() - const refocus = useRef() - - useEffect(() => { - redraw.current = (res = 4) => { - if (type !== 'multi_noise') res = 1 - const ctx = canvas.current.getContext('2d')! - canvas.current.width = 200 / res - canvas.current.height = 200 / res - const img = ctx.createImageData(canvas.current.width, canvas.current.height) - biomeSource(data, img, { biomeColors: {}, offset: offset.current, scale, seed, res }) - ctx.putImageData(img, 0, 0) - if (res !== 1) { - clearTimeout(redrawTimeout.current) - redrawTimeout.current = setTimeout(() => redraw.current(1), 150) as any + const { canvas, redraw } = useCanvas({ + size() { + return [200 / res.current, 200 / res.current] + }, + async draw(img) { + const options = { octaves, biomeColors: {}, offset: offset.current, scale, seed, res: res.current, version } + await biomeMap(data, img, options) + if (res.current === 4) { + clearTimeout(refineTimeout.current) + refineTimeout.current = setTimeout(() => { + res.current = 1 + redraw() + }, 150) } - } - refocus.current = (x: number, y: number) => { - const x2 = x * 200 / canvas.current.clientWidth - const y2 = y * 200 / canvas.current.clientHeight - const biome = getBiome(data, x2, y2, { biomeColors: {}, offset: offset.current, scale, seed, res: 1 }) + }, + async onDrag(dx, dy) { + offset.current[0] = offset.current[0] + dx * 200 + offset.current[1] = offset.current[1] + dy * 200 + clearTimeout(refineTimeout.current) + res.current = type === 'multi_noise' ? 4 : 1 + redraw() + }, + async onHover(x, y) { + const options = { octaves, biomeColors: {}, offset: offset.current, scale, seed, res: 1, version } + const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options) setFocused(biome) - } - }) - - useOnDrag(canvas.current, (dx, dy) => { - const x = dx * 200 / canvas.current.clientWidth - const y = dy * 200 / canvas.current.clientHeight - offset.current = [offset.current[0] + x, offset.current[1] + y] - redraw.current() - }) - - useOnHover(canvas.current, (x, y) => { - if (x === undefined || y === undefined) { + }, + onLeave() { setFocused(undefined) - } else { - refocus.current(x, y) - } - }) + }, + }, [state, scale, seed]) - const state = JSON.stringify(data) useEffect(() => { if (shown) { - redraw.current() + res.current = type === 'multi_noise' ? 4 : 1 + redraw() } }, [state, scale, seed, shown]) const changeScale = (newScale: number) => { - offset.current[0] *= scale / newScale - offset.current[1] *= scale / newScale + offset.current[0] = offset.current[0] * scale / newScale + offset.current[1] = offset.current[1] * scale / newScale setScale(newScale) } @@ -81,8 +73,49 @@ export const BiomeSourcePreview = ({ data, shown }: BiomeSourceProps) => { changeScale(scale / 1.5)} /> } {type === 'multi_noise' && - setSeed(hexId())} />} + newSeed(model)} />} } + +function calculateState(data: any, octaves: NoiseOctaves) { + return JSON.stringify([data, octaves]) +} + +function getOctaves(obj: any): NoiseOctaves { + if (typeof obj === 'string') { + 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 'end': + case 'floating_islands': + return { + temperature: { firstOctave: 0, amplitudes: [0] }, + humidity: { firstOctave: 0, amplitudes: [0] }, + continentalness: { firstOctave: 0, amplitudes: [0] }, + erosion: { firstOctave: 0, amplitudes: [0] }, + weirdness: { firstOctave: 0, amplitudes: [0] }, + shift: { firstOctave: 0, amplitudes: [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] }, + } + } + } + return NoiseGeneratorSettings.fromJson(obj).octaves +} diff --git a/src/app/components/previews/DecoratorPreview.tsx b/src/app/components/previews/DecoratorPreview.tsx index 81cf71dd..8cc25a66 100644 --- a/src/app/components/previews/DecoratorPreview.tsx +++ b/src/app/components/previews/DecoratorPreview.tsx @@ -1,39 +1,27 @@ -import type { DataModel } from '@mcschema/core' -import { useEffect, useRef, useState } from 'preact/hooks' +import { useEffect, useState } from 'preact/hooks' +import type { PreviewProps } from '.' import { Btn } from '..' +import { useCanvas } from '../../hooks' import { decorator } from '../../previews' -import type { VersionId } from '../../Schemas' -import { hexId } from '../../Utils' +import { randomSeed } from '../../Utils' -type DecoratorProps = { - lang: string, - model: DataModel, - data: any, - version: VersionId, - shown: boolean, -} -export const DecoratorPreview = ({ data, version, shown }: DecoratorProps) => { +export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => { const [scale, setScale] = useState(4) - const [seed, setSeed] = useState(hexId()) + const [seed, setSeed] = useState(randomSeed()) - const canvas = useRef(null) - const redraw = useRef() - - useEffect(() => { - redraw.current = () => { - const ctx = canvas.current.getContext('2d')! - canvas.current.width = scale * 16 - canvas.current.height = scale * 16 - const img = ctx.createImageData(canvas.current.width, canvas.current.height) + const { canvas, redraw } = useCanvas({ + size() { + return [scale * 16, scale * 16] + }, + async draw(img) { decorator(data, img, { seed, version, size: [scale * 16, 128, scale * 16] }) - ctx.putImageData(img, 0, 0) - } + }, }) const state = JSON.stringify(data) useEffect(() => { if (shown) { - setTimeout(() => redraw.current()) + redraw() } }, [state, scale, seed, shown]) @@ -41,7 +29,7 @@ export const DecoratorPreview = ({ data, version, shown }: DecoratorProps) => {
setScale(Math.min(16, scale + 1))} /> setScale(Math.max(1, scale - 1))} /> - setSeed(hexId())} /> + setSeed(randomSeed())} />
diff --git a/src/app/components/previews/NoiseSettingsPreview.tsx b/src/app/components/previews/NoiseSettingsPreview.tsx index e5da2c67..722edf61 100644 --- a/src/app/components/previews/NoiseSettingsPreview.tsx +++ b/src/app/components/previews/NoiseSettingsPreview.tsx @@ -1,60 +1,71 @@ -import type { DataModel } from '@mcschema/core' import { useEffect, useRef, useState } from 'preact/hooks' +import type { PreviewProps } from '.' import { Btn, BtnInput, BtnMenu } from '..' -import { useOnDrag } from '../../hooks' +import { useCanvas } from '../../hooks' import { locale } from '../../Locales' import { noiseSettings } from '../../previews' -import { hexId } from '../../Utils' +import { checkVersion } from '../../Schemas' +import { randomSeed } from '../../Utils' -type NoiseSettingsProps = { - lang: string, - model: DataModel, - data: any, - shown: boolean, -} -export const NoiseSettingsPreview = ({ lang, data, shown }: NoiseSettingsProps) => { +export const NoiseSettingsPreview = ({ lang, data, shown, version }: PreviewProps) => { const loc = locale.bind(null, lang) - const [seed, setSeed] = useState(hexId()) - const [biomeDepth, setBiomeDepth] = useState(0.1) - const [biomeScale, setBiomeScale] = useState(0.2) - - const canvas = useRef(null) - const offset = useRef(0) - const redraw = useRef() + const [seed, setSeed] = useState(randomSeed()) + const [biomeFactor, setBiomeFactor] = useState(0.2) + const [biomeOffset, setBiomeOffset] = useState(0.1) + const [biomePeaks, setBiomePeaks] = useState(0) + const [focused, setFocused] = useState(undefined) + const offset = useRef(0) + const state = JSON.stringify([data, biomeFactor, biomeOffset, biomePeaks]) + const hasPeaks = checkVersion(version, '1.18') useEffect(() => { - redraw.current = () => { - const ctx = canvas.current.getContext('2d')! - const size = data.height - canvas.current.width = size - canvas.current.height = size - const img = ctx.createImageData(canvas.current.width, canvas.current.height) - noiseSettings(data, img, { biomeDepth, biomeScale, offset: offset.current, width: size, seed }) - ctx.putImageData(img, 0, 0) - } - }) + setBiomeFactor(hasPeaks ? 600 : 0.2) + setBiomeOffset(hasPeaks ? 0.05 : 0.1) + }, [hasPeaks]) - useOnDrag(canvas.current, (dx) => { - const x = dx * canvas.current.width / canvas.current.clientWidth - offset.current = offset.current + x - redraw.current() - }) + const size = data?.noise?.height ?? 256 + const { canvas, redraw } = useCanvas({ + size() { + return [size, size] + }, + async draw(img) { + const options = { biomeOffset, biomeFactor, biomePeaks, offset: offset.current, width: img.width, seed, version } + noiseSettings(data, img, options) + }, + async onDrag(dx) { + offset.current += dx * size + redraw() + }, + async onHover(_, y) { + const worldY = size - Math.max(1, Math.ceil(y * size)) + (data?.noise?.min_y ?? 0) + setFocused(`${worldY}`) + }, + onLeave() { + setFocused(undefined) + }, + }, [state, seed]) - const state = JSON.stringify(data) useEffect(() => { if (shown) { - redraw.current() + redraw() } - }, [state, biomeDepth, biomeScale, seed, shown]) + }, [state, seed, shown]) return <>
+ {focused && } - setBiomeDepth(Number(v))} /> - setBiomeScale(Number(v))} /> + {hasPeaks ? <> + setBiomeFactor(Number(v))} /> + setBiomeOffset(Number(v))} /> + setBiomePeaks(Number(v))} /> + : <> + setBiomeFactor(Number(v))} /> + setBiomeOffset(Number(v))} /> + } - setSeed(hexId())} /> + setSeed(randomSeed())} />
- + } diff --git a/src/app/components/previews/index.ts b/src/app/components/previews/index.ts index 9703f631..15e6975d 100644 --- a/src/app/components/previews/index.ts +++ b/src/app/components/previews/index.ts @@ -1,3 +1,14 @@ +import type { DataModel } from '@mcschema/core' +import type { VersionId } from '../../Schemas' + export * from './BiomeSourcePreview' export * from './DecoratorPreview' export * from './NoiseSettingsPreview' + +export type PreviewProps = { + lang: string, + model: DataModel, + data: any, + shown: boolean, + version: VersionId, +} diff --git a/src/app/hooks/index.ts b/src/app/hooks/index.ts index b94b6ade..1378cf60 100644 --- a/src/app/hooks/index.ts +++ b/src/app/hooks/index.ts @@ -1,4 +1,3 @@ +export * from './useCanvas' export * from './useFocus' export * from './useModel' -export * from './useOnDrag' -export * from './useOnHover' diff --git a/src/app/hooks/useCanvas.ts b/src/app/hooks/useCanvas.ts new file mode 100644 index 00000000..f44965dc --- /dev/null +++ b/src/app/hooks/useCanvas.ts @@ -0,0 +1,89 @@ +import type { Inputs } from 'preact/hooks' +import { useEffect, useRef } from 'preact/hooks' + +type Vec2 = [number, number] + +export function useCanvas({ size, draw, onDrag, onHover, onLeave }: { + size: () => Vec2, + draw: (img: ImageData) => Promise, + onDrag?: (dx: number, dy: number) => Promise, + onHover?: (x: number, y: number) => unknown, + onLeave?: () => unknown, +}, inputs?: Inputs) { + const canvas = useRef(null) + + const dragStart = useRef() + const dragRequest = useRef() + const dragPending = useRef([0, 0]) + const dragBusy = useRef(false) + + useEffect(() => { + const onMouseDown = (e: MouseEvent) => { + dragStart.current = [e.offsetX, e.offsetY] + } + const onMouseMove = (e: MouseEvent) => { + if (dragStart.current === undefined) { + const x = e.offsetX / canvas.current.clientWidth + const y = e.offsetY / canvas.current.clientHeight + onHover?.(x, y) + return + } + if (!onDrag) return + const dx = e.offsetX - dragStart.current[0] + const dy = e.offsetY - dragStart.current[1] + if (!(dx === 0 && dy === 0)) { + dragPending.current = [dragPending.current[0] + dx, dragPending.current[1] + dy] + if (!dragBusy.current) { + cancelAnimationFrame(dragRequest.current) + dragRequest.current = requestAnimationFrame(async () => { + dragBusy.current = true + const dx = dragPending.current[0] / canvas.current.clientWidth + const dy = dragPending.current[1] / canvas.current.clientHeight + dragPending.current = [0, 0] + await onDrag?.(dx, dy) + dragBusy.current = false + }) + } + } + dragStart.current = [e.offsetX, e.offsetY] + } + const onMouseUp = () => { + dragStart.current = undefined + } + const onMouseLeave = () => { + onLeave?.() + } + + canvas.current.addEventListener('mousedown', onMouseDown) + canvas.current.addEventListener('mousemove', onMouseMove) + canvas.current.addEventListener('mouseleave', onMouseLeave) + document.body.addEventListener('mouseup', onMouseUp) + + return () => { + canvas.current.removeEventListener('mousedown', onMouseDown) + canvas.current.removeEventListener('mousemove', onMouseMove) + canvas.current.removeEventListener('mouseleave', onMouseLeave) + document.body.removeEventListener('mouseup', onMouseUp) + } + }, [...inputs ?? [], canvas.current]) + + const redraw = useRef<() => Promise>() + const redrawCount = useRef(0) + redraw.current = async () => { + const ctx = canvas.current.getContext('2d')! + const s = size() + canvas.current.width = s[0] + canvas.current.height = s[1] + const img = ctx.getImageData(0, 0, s[0], s[1]) + const ownCount = redrawCount.current += 1 + await draw(img) + if (ownCount === redrawCount.current) { + ctx.putImageData(img, 0, 0) + } + } + + return { + canvas, + redraw: redraw.current, + } +} diff --git a/src/app/hooks/useOnDrag.ts b/src/app/hooks/useOnDrag.ts deleted file mode 100644 index 35a92efb..00000000 --- a/src/app/hooks/useOnDrag.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useRef } from 'preact/hooks' - -export function useOnDrag(element: HTMLElement, drag: (dx: number, dy: number) => unknown) { - if (!element) return - - const request = useRef() - const dragStart = useRef<[number, number] | undefined>() - const pending = useRef<[number, number]>([0, 0]) - - useEffect(() => { - const onMouseDown = (e: MouseEvent) => { - dragStart.current = [e.offsetX, e.offsetY] - } - const onMouseMove = (e: MouseEvent) => { - if (dragStart.current === undefined) return - const dx = e.offsetX - dragStart.current[0] - const dy = e.offsetY - dragStart.current[1] - if (!(dx === 0 && dy === 0)) { - cancelAnimationFrame(request.current) - pending.current = [pending.current[0] + dx, pending.current[1] + dy] - request.current = requestAnimationFrame(() => { - drag(...pending.current) - pending.current = [0, 0] - }) - } - dragStart.current = [e.offsetX, e.offsetY] - } - const onMouseUp = (_e: MouseEvent) => { - dragStart.current = undefined - } - - element.addEventListener('mousedown', onMouseDown) - element.addEventListener('mousemove', onMouseMove) - document.body.addEventListener('mouseup', onMouseUp) - return () => { - element.removeEventListener('mousedown', onMouseDown) - element.removeEventListener('mousemove', onMouseMove) - document.body.removeEventListener('mouseup', onMouseUp) - } - }, [element]) -} diff --git a/src/app/hooks/useOnHover.ts b/src/app/hooks/useOnHover.ts deleted file mode 100644 index 44170b94..00000000 --- a/src/app/hooks/useOnHover.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect } from 'preact/hooks' - -export function useOnHover(element: HTMLElement, hover: (x: number | undefined, y: number | undefined) => unknown) { - if (!element) return - - const onMouseMove = (e: MouseEvent) => { - hover(e.offsetX, e.offsetY) - } - const onMouseLeave = () => { - hover(undefined, undefined) - } - - useEffect(() => { - element.addEventListener('mousemove', onMouseMove) - element.addEventListener('mouseleave', onMouseLeave) - return () => { - element.removeEventListener('mousemove', onMouseMove) - element.removeEventListener('mouseleave', onMouseLeave) - } - }, [element]) -} diff --git a/src/app/pages/Generator.tsx b/src/app/pages/Generator.tsx index 19549f4c..b7db25ae 100644 --- a/src/app/pages/Generator.tsx +++ b/src/app/pages/Generator.tsx @@ -1,6 +1,7 @@ import type { DataModel } from '@mcschema/core' +import { Path } from '@mcschema/core' import { getCurrentUrl } from 'preact-router' -import { useEffect, useErrorBoundary, useState } from 'preact/hooks' +import { useEffect, useErrorBoundary, useRef, useState } from 'preact/hooks' import config from '../../config.json' import { Analytics } from '../Analytics' import { Ad, Btn, BtnInput, BtnMenu, ErrorPanel, HasPreview, Octicon, PreviewPanel, SourcePanel, Tree } from '../components' @@ -13,7 +14,7 @@ import { getGenerator } from '../Utils' type GeneratorProps = { lang: string, - changeTitle: (title: string, versions?: string[]) => unknown, + changeTitle: (title: string, versions?: VersionId[]) => unknown, version: VersionId, onChangeVersion: (version: VersionId) => unknown, default?: true, @@ -110,6 +111,13 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener const loadPreset = (id: string) => { Analytics.generatorEvent('load-preset', id) fetchPreset(version, gen.path ?? gen.id, id).then(preset => { + const seed = model?.get(new Path(['generator', 'seed'])) + if (preset?.generator?.seed !== undefined && seed !== undefined) { + preset.generator.seed = seed + if (preset.generator.biome_source?.seed !== undefined) { + preset.generator.biome_source.seed = seed + } + } model?.reset(preset, false) }) } @@ -140,6 +148,16 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener setImport(0) } + const [copyActive, setCopyActive] = useState(false) + const copyTimeout = useRef(undefined) + const copySuccess = () => { + setCopyActive(true) + if (copyTimeout.current !== undefined) clearTimeout(copyTimeout.current) + copyTimeout.current = setTimeout(() => { + setCopyActive(false) + }, 2000) + } + const [previewShown, setPreviewShown] = useState(false) const hasPreview = HasPreview.includes(gen.id) if (previewShown && !hasPreview) setPreviewShown(false) @@ -185,8 +203,8 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
{Octicon.download}
-
- {Octicon.clippy} +
+ {copyActive ? Octicon.check : Octicon.clippy}
{sourceShown ? Octicon.chevron_right : Octicon.code} @@ -196,7 +214,7 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
- +
} diff --git a/src/app/previews/BiomeSource.ts b/src/app/previews/BiomeSource.ts index 610596b5..05a6add2 100644 --- a/src/app/previews/BiomeSource.ts +++ b/src/app/previews/BiomeSource.ts @@ -1,62 +1,39 @@ -import { stringToColor } from '../Utils' -import { NormalNoise } from './noise/NormalNoise' +import type { BiomeSource, Climate, NoiseOctaves } from 'deepslate' +import { FixedBiome, MultiNoise, NoiseGeneratorSettings, NoiseSampler, NormalNoise, Random } from 'deepslate' +import { fetchPreset } from '../DataFetcher' +import type { VersionId } from '../Schemas' +import { deepClone, deepEqual, square, stringToColor } from '../Utils' -type BiomeColors =Record +type BiomeColors = Record type BiomeSourceOptions = { + octaves: NoiseOctaves, biomeColors: BiomeColors, offset: [number, number], scale: number, res: number, - seed: string, + seed: bigint, + version: VersionId, } -const NoiseMaps = ['altitude', 'temperature', 'humidity', 'weirdness'] +let cacheState: any +let biomeSourceCache: BiomeSource +let climateSamplerCache: Climate.Sampler -export function biomeSource(state: any, img: ImageData, options: BiomeSourceOptions) { - switch (state?.type?.replace(/^minecraft:/, '')) { - case 'multi_noise': return multiNoise(state, img, options) - case 'fixed': return fixed(state, img, options) - case 'checkerboard': return checkerboard(state, img, options) - } -} - -function fixed(state: any, img: ImageData, options: BiomeSourceOptions) { - const data = img.data - const color = getBiomeColor(state.biome, options.biomeColors) - const row = img.width * 4 / options.res - const col = 4 / options.res - for (let x = 0; x < 200; x += options.res) { - for (let y = 0; y < 200; y += options.res) { - const i = y * row + x * col - data[i] = color[0] - data[i + 1] = color[1] - data[i + 2] = color[2] - data[i + 3] = 255 - } - } -} - -function checkerboard(state: any, img: ImageData, options: BiomeSourceOptions) { - const biomeColorCache: BiomeColors = {} - state.biomes?.forEach((b: string) => { - biomeColorCache[b] = getBiomeColor(b, options.biomeColors) - }) +export async function biomeMap(state: any, img: ImageData, options: BiomeSourceOptions) { + const { biomeSource, climateSampler } = await getCached(state, options) const data = img.data const ox = -options.offset[0] - 100 + options.res / 2 - const oy = -options.offset[1] - 100 + options.res / 2 + const oz = -options.offset[1] - 100 + options.res / 2 const row = img.width * 4 / options.res const col = 4 / options.res - const shift = (state.scale ?? 2) + 2 - const numBiomes = state.biomes?.length ?? 0 for (let x = 0; x < 200; x += options.res) { - for (let y = 0; y < 200; y += options.res) { - const i = y * row + x * col - const xx = (x + ox) * options.scale - const yy = (y + oy) * options.scale - const j = (((xx >> shift) + (yy >> shift)) % numBiomes + numBiomes) % numBiomes - const b = state.biomes?.[j] - const color = biomeColorCache[b] ?? [128, 128, 128] + for (let z = 0; z < 200; z += options.res) { + const i = z * row + x * col + const worldX = (x + ox) * options.scale + const worldZ = (z + oz) * options.scale + const b = biomeSource.getBiome(worldX, 64, worldZ, climateSampler) + const color = getBiomeColor(b, options.biomeColors) data[i] = color[0] data[i + 1] = color[1] data[i + 2] = color[2] @@ -65,61 +42,86 @@ function checkerboard(state: any, img: ImageData, options: BiomeSourceOptions) { } } -function multiNoise(state: any, img: ImageData, options: BiomeSourceOptions) { - if (state.preset?.replace(/^minecraft:/, '') === 'nether') { - state = NetherPreset - } +export async function getBiome(state: any, x: number, z: number, options: BiomeSourceOptions): Promise { + const { biomeSource, climateSampler } = await getCached(state, options) - const noise = NoiseMaps.map((id, i) => { - const config = state[`${id}_noise`] - return new NormalNoise(options.seed + i, config.firstOctave, config.amplitudes) - }) - - const biomeColorCache: BiomeColors = {} - state.biomes.forEach((b: any) => { - biomeColorCache[b.biome] = getBiomeColor(b.biome, options.biomeColors) - }) - - const data = img.data - const ox = -options.offset[0] - 100 + options.res / 2 - const oy = -options.offset[1] - 100 + options.res / 2 - const row = img.width * 4 / options.res - const col = 4 / options.res - for (let x = 0; x < 200; x += options.res) { - for (let y = 0; y < 200; y += options.res) { - const i = y * row + x * col - const xx = (x + ox) * options.scale - const yy = (y + oy) * options.scale - const b = closestBiome(noise, state.biomes, xx, yy) - const color = biomeColorCache[b] ?? [128, 128, 128] - data[i] = color[0] - data[i + 1] = color[1] - data[i + 2] = color[2] - data[i + 3] = 255 - } - } + const [xx, zz] = toWorld([x, z], options) + return biomeSource.getBiome(xx, 64, zz, climateSampler) } -export function getBiome(state: any, x: number, y: number, options: BiomeSourceOptions): string | undefined { - const [xx, yy] = toWorld([x, y], options) +async function getCached(state: any, options: BiomeSourceOptions): Promise<{ biomeSource: BiomeSource, climateSampler: Climate.Sampler }> { + const newState = [state, options.octaves, `${options.seed}`, options.version] + if (!deepEqual(newState, cacheState)) { + cacheState = deepClone(newState) + + biomeSourceCache = await getBiomeSource(state, options) + + const settings = NoiseGeneratorSettings.fromJson({ octaves: options.octaves }) + const noiseSampler = new NoiseSampler(4, 4, 32, biomeSourceCache, settings.noise, options.octaves, options.seed) + climateSamplerCache = noiseSampler.getClimate.bind(noiseSampler) + } + return { + biomeSource: biomeSourceCache, + climateSampler: climateSamplerCache, + } +} + +async function getBiomeSource(state: any, options: BiomeSourceOptions): Promise { switch (state?.type?.replace(/^minecraft:/, '')) { - case 'multi_noise': - const noise = NoiseMaps.map((id, i) => { - const config = state[`${id}_noise`] - return new NormalNoise(options.seed + i, config.firstOctave, config.amplitudes) - }) - return closestBiome(noise, state.biomes, xx, yy) - case 'fixed': return state.biome + case 'fixed': + return new FixedBiome(state.biome as string) + case 'checkerboard': const shift = (state.scale ?? 2) + 2 const numBiomes = state.biomes?.length ?? 0 - const j = (((xx >> shift) + (yy >> shift)) % numBiomes + numBiomes) % numBiomes - return state.biomes?.[j] + return { + getBiome(x: number, _y: number, z: number) { + const i = (((x >> shift) + (z >> shift)) % numBiomes + numBiomes) % numBiomes + return (state.biomes?.[i] as string) + }, + } + + case 'multi_noise': + switch(state.preset?.replace(/^minecraft:/, '')) { + case 'nether': + state = options.version === '1.18' ? NetherPreset18 : NetherPreset + break + case 'overworld': + state = options.version === '1.18' ? await OverworldPreset18() : state + break + } + if (options.version === '1.18') { + return MultiNoise.fromJson(state) + } else { + const noise = ['altitude', 'temperature', 'humidity', 'weirdness'] + .map((id, i) => { + const config = state[`${id}_noise`] + return new NormalNoise(new Random(options.seed + BigInt(i)), config) + }) + if (!Array.isArray(state.biomes) || state.biomes.length === 0) { + return new FixedBiome('unknown') + } + return { + getBiome(x: number, _y: number, z: number): string { + 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 minBiome + }, + } + } } - return undefined + throw new Error('Unknown biome source') } -export function getBiomeColor(biome: string, biomeColors: BiomeColors) { +function getBiomeColor(biome: string, biomeColors: BiomeColors) { if (!biome) { return [128, 128, 128] } @@ -130,29 +132,10 @@ export function getBiomeColor(biome: string, biomeColors: BiomeColors) { return color } -function toWorld([x, y]: [number, number], options: BiomeSourceOptions) { +function toWorld([x, z]: [number, number], options: BiomeSourceOptions) { const xx = (x - options.offset[0] - 100 + options.res / 2) * options.scale - const yy = (y - options.offset[1] - 100 + options.res / 2) * options.scale - return [xx, yy] -} - -function closestBiome(noise: NormalNoise[], biomes: any[], x: number, y: number): string { - if (!Array.isArray(biomes) || biomes.length === 0) return '' - const n = noise.map(n => n.getValue(x, y, 0)) - let minDist = Infinity - let minBiome = '' - for (const b of biomes) { - const dist = fitness(b.parameters, {altitude: n[0], temperature: n[1], humidity: n[2], weirdness: n[3], offset: 0}) - if (dist < minDist) { - minDist = dist - minBiome = b.biome - } - } - return minBiome -} - -function fitness(a: any, b: any) { - return (a.altitude - b.altitude) * (a.altitude - b.altitude) + (a.temperature - b.temperature) * (a.temperature - b.temperature) + (a.humidity - b.humidity) * (a.humidity - b.humidity) + (a.weirdness - b.weirdness) * (a.weirdness - b.weirdness) + (a.offset - b.offset) * (a.offset - b.offset) + const zz = (z - options.offset[1] - 100 + options.res / 2) * options.scale + return [xx, zz] } const VanillaColors: Record = { @@ -235,6 +218,21 @@ const VanillaColors: Record = { 'minecraft:wooded_badlands_plateau': [176,151,101], 'minecraft:wooded_hills': [34,85,28], 'minecraft:wooded_mountains': [80,112,80], + 'minecraft:snowy_slopes': [140, 195, 222], + 'minecraft:lofty_peaks': [196, 168, 193], + 'minecraft:snowcapped_peaks': [200, 198, 200], + 'minecraft:stony_peaks': [82, 92, 103], + 'minecraft:grove': [150, 150, 189], + 'minecraft:meadow': [169, 197, 80], + '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/Decorator.ts b/src/app/previews/Decorator.ts index dbe624ba..9f6e81cd 100644 --- a/src/app/previews/Decorator.ts +++ b/src/app/previews/Decorator.ts @@ -1,7 +1,6 @@ -import seedrandom from 'seedrandom' +import { PerlinNoise, Random } from 'deepslate' import type { VersionId } from '../Schemas' import { clamp, stringToColor } from '../Utils' -import { PerlinNoise } from './noise/PerlinNoise' type BlockPos = [number, number, number] type Placement = [BlockPos, number] @@ -9,10 +8,13 @@ type Placement = [BlockPos, number] type PlacementContext = { placements: Placement[], features: string[], - random: seedrandom.prng, + random: Random, biomeInfoNoise: PerlinNoise, seaLevel: number, version: VersionId, + nextFloat(): number, + nextInt(max: number): number, + sampleInt(provider: any): number, } const terrain = [50, 50, 51, 51, 52, 52, 53, 54, 56, 57, 57, 58, 58, 59, 60, 60, 60, 59, 59, 59, 60, 61, 61, 62, 63, 63, 64, 64, 64, 65, 65, 66, 66, 65, 65, 66, 66, 67, 67, 67, 68, 69, 71, 73, 74, 76, 79, 80, 81, 81, 82, 83, 83, 82, 82, 81, 81, 80, 80, 80, 81, 81, 82, 82] @@ -28,18 +30,21 @@ const featureColors = [ export type DecoratorOptions = { size: [number, number, number], - seed: string, + seed: bigint, version: VersionId, } export function decorator(state: any, img: ImageData, options: DecoratorOptions) { - const random = seedrandom(options.seed) + const random = new Random(options.seed) const ctx: PlacementContext = { placements: [], features: [], random, - biomeInfoNoise: new PerlinNoise(options.seed + 'frwynup', 0, [1]), + biomeInfoNoise: new PerlinNoise(random.fork(), 0, [1]), seaLevel: 63, version: options.version, + nextFloat: () => random.nextFloat(), + nextInt: (max: number) => random.nextInt(max), + sampleInt(value) { return sampleInt(value, this) }, } for (let x = 0; x < options.size[0] / 16; x += 1) { @@ -80,20 +85,16 @@ function decorateY(pos: BlockPos, y: number): BlockPos[] { return [[ pos[0], y, pos[2] ]] } -function nextInt(max: number, ctx: PlacementContext): number { - return Math.floor(ctx.random() * max) -} - function sampleInt(value: any, ctx: PlacementContext): number { if (typeof value === 'number') { return value } else if (value.base) { - return value.base ?? 1 + nextInt(1 + (value.spread ?? 0), ctx) + return value.base ?? 1 + ctx.nextInt(1 + (value.spread ?? 0)) } else { switch (normalize(value.type)) { case 'constant': return value.value - case 'uniform': return value.value.min_inclusive + nextInt(value.value.max_inclusive - value.value.min_inclusive + 1, ctx) - case 'biased_to_bottom': return value.value.min_inclusive + nextInt(nextInt(value.value.max_inclusive - value.value.min_inclusive + 1, ctx) + 1, ctx) + case 'uniform': return value.value.min_inclusive + ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1) + case 'biased_to_bottom': return value.value.min_inclusive + ctx.nextInt(ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1) + 1) case 'clamped': return Math.max(value.value.min_inclusive, Math.min(value.value.max_inclusive, sampleInt(value.value.source, ctx))) } return 1 @@ -138,12 +139,12 @@ const Features: { positions.forEach(p => getPlacements(p, config?.feature, ctx)) }, random_boolean_selector: (config, pos, ctx) => { - const feature = ctx.random() < 0.5 ? config?.feature_true : config?.feature_false + const feature = ctx.nextFloat() < 0.5 ? config?.feature_true : config?.feature_false getPlacements(pos, feature, ctx) }, random_selector: (config, pos, ctx) => { for (const f of config?.features ?? []) { - if (ctx.random() < (f?.chance ?? 0)) { + if (ctx.nextFloat() < (f?.chance ?? 0)) { getPlacements(pos, f.feature, ctx) return } @@ -151,7 +152,7 @@ const Features: { getPlacements(pos, config?.default, ctx) }, simple_random_selector: (config, pos, ctx) => { - const feature = config?.features?.[nextInt(config?.features?.length ?? 0, ctx)] + const feature = config?.features?.[ctx.nextInt(config?.features?.length ?? 0)] getPlacements(pos, feature, ctx) }, } @@ -160,42 +161,42 @@ const Decorators: { [key: string]: (config: any, pos: BlockPos, ctx: PlacementContext) => BlockPos[], } = { chance: (config, pos, ctx) => { - return ctx.random() < 1 / (config?.chance ?? 1) ? [pos] : [] + return ctx.nextFloat() < 1 / (config?.chance ?? 1) ? [pos] : [] }, count: (config, pos, ctx) => { - return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos) + return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos) }, count_extra: (config, pos, ctx) => { let count = config?.count ?? 1 - if (ctx.random() < config.extra_chance ?? 0){ + if (ctx.nextFloat() < config.extra_chance ?? 0){ count += config.extra_count ?? 0 } return new Array(count).fill(pos) }, count_multilayer: (config, pos, ctx) => { - return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos) + return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos) .map(p => [ - p[0] + nextInt(16, ctx), + p[0] + ctx.nextInt(16), p[1], - p[2] + nextInt(16, ctx), + p[2] + ctx.nextInt(16), ]) }, count_noise: (config, pos, ctx) => { - const noise = ctx.biomeInfoNoise.getValue(pos[0] / 200, 0, pos[2] / 200) + const noise = ctx.biomeInfoNoise.sample(pos[0] / 200, 0, pos[2] / 200) const count = noise < config.noise_level ? config.below_noise : config.above_noise return new Array(count).fill(pos) }, count_noise_biased: (config, pos, ctx) => { const factor = Math.max(1, config.noise_factor) - const noise = ctx.biomeInfoNoise.getValue(pos[0] / factor, 0, pos[2] / factor) + const noise = ctx.biomeInfoNoise.sample(pos[0] / factor, 0, pos[2] / factor) const count = Math.max(0, Math.ceil((noise + (config.noise_offset ?? 0)) * config.noise_to_count_ratio)) return new Array(count).fill(pos) }, dark_oak_tree: (_config, pos, ctx) => { return [...new Array(16)].map((_, i) => { - const x = Math.floor(i / 4) * 4 + 1 + nextInt(3, ctx) + pos[0] + const x = Math.floor(i / 4) * 4 + 1 + ctx.nextInt(3) + pos[0] const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, x)]) - const z = Math.floor(i % 4) * 4 + 1 + nextInt(3, ctx) + pos[2] + const z = Math.floor(i % 4) * 4 + 1 + ctx.nextInt(3) + pos[2] return [x, y, z] }) }, @@ -205,31 +206,31 @@ const Decorators: { }) }, depth_average: (config, pos, ctx) => { - const y = nextInt(config?.spread ?? 0, ctx) + nextInt(config?.spread ?? 0, ctx) - (config.spread ?? 0) + (config?.baseline ?? 0) + const y = ctx.nextInt(config?.spread ?? 0) + ctx.nextInt(config?.spread ?? 0) - (config.spread ?? 0) + (config?.baseline ?? 0) return decorateY(pos, y) }, emerald_ore: (_config, pos, ctx) => { - const count = 3 + nextInt(6, ctx) + const count = 3 + ctx.nextInt(6) return [...new Array(count)].map(() => [ - pos[0] + nextInt(16, ctx), - 4 + nextInt(28, ctx), - pos[2] + nextInt(16, ctx), + pos[0] + ctx.nextInt(16), + 4 + ctx.nextInt(28), + pos[2] + ctx.nextInt(16), ]) }, fire: (config, pos, ctx) => { - const count = 1 + nextInt(nextInt(sampleInt(config?.count, ctx), ctx), ctx) + const count = 1 + ctx.nextInt(ctx.nextInt(ctx.sampleInt(config?.count))) return [...new Array(count)].map(() => [ - pos[0] + nextInt(16, ctx), - nextInt(128, ctx), - pos[2] + nextInt(16, ctx), + pos[0] + ctx.nextInt(16), + ctx.nextInt(128), + pos[2] + ctx.nextInt(16), ]) }, glowstone: (config, pos, ctx) => { - const count = nextInt(1 + nextInt(sampleInt(config?.count, ctx), ctx), ctx) + const count = ctx.nextInt(1 + ctx.nextInt(ctx.sampleInt(config?.count))) return [...new Array(count)].map(() => [ - pos[0] + nextInt(16, ctx), - nextInt(128, ctx), - pos[2] + nextInt(16, ctx), + pos[0] + ctx.nextInt(16), + ctx.nextInt(128), + pos[2] + ctx.nextInt(16), ]) }, heightmap: (_config, pos, ctx) => { @@ -238,7 +239,7 @@ const Decorators: { }, heightmap_spread_double: (_config, pos, ctx) => { const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, pos[0])]) - return decorateY(pos, nextInt(y * 2, ctx)) + return decorateY(pos, ctx.nextInt(y * 2)) }, heightmap_world_surface: (_config, pos, ctx) => { const y = Math.max(ctx.seaLevel, terrain[clamp(0, 63, pos[0])]) @@ -246,17 +247,17 @@ const Decorators: { }, iceberg: (_config, pos, ctx) => { return [[ - pos[0] + 4 + nextInt(8, ctx), + pos[0] + 4 + ctx.nextInt(8), pos[1], - pos[2] + 4 + nextInt(8, ctx), + pos[2] + 4 + ctx.nextInt(8), ]] }, lava_lake: (config, pos, ctx) => { - if (nextInt((config.chance ?? 1) / 10, ctx) === 0) { - const y = nextInt(nextInt(256 - 8, ctx) + 8, ctx) - if (y < ctx.seaLevel || nextInt((config?.chance ?? 1) / 8, ctx) == 0) { - const x = nextInt(16, ctx) + pos[0] - const z = nextInt(16, ctx) + pos[2] + if (ctx.nextInt((config.chance ?? 1) / 10) === 0) { + const y = ctx.nextInt(ctx.nextInt(256 - 8) + 8) + if (y < ctx.seaLevel || ctx.nextInt((config?.chance ?? 1) / 8) == 0) { + const x = ctx.nextInt(16) + pos[0] + const z = ctx.nextInt(16) + pos[2] return [[x, y, z]] } } @@ -266,19 +267,19 @@ const Decorators: { return [pos] }, range: (config, pos, ctx) => { - const y = nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0) + const y = ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0) return decorateY(pos, y) }, range_biased: (config, pos, ctx) => { - const y = nextInt(nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx) + const y = ctx.nextInt(ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) return decorateY(pos, y) }, range_very_biased: (config, pos, ctx) => { - const y = nextInt(nextInt(nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx) + (config?.bottom_offset ?? 0), ctx) + const y = ctx.nextInt(ctx.nextInt(ctx.nextInt((config?.maximum ?? 1) - (config?.top_offset ?? 0)) + (config?.bottom_offset ?? 0)) + (config?.bottom_offset ?? 0)) return decorateY(pos, y) }, spread_32_above: (_config, pos, ctx) => { - const y = nextInt(pos[1] + 32, ctx) + const y = ctx.nextInt(pos[1] + 32) return decorateY(pos, y) }, top_solid_heightmap: (_config, pos) => { @@ -286,22 +287,28 @@ const Decorators: { return decorateY(pos, y) }, magma: (_config, pos, ctx) => { - const y = nextInt(pos[1] + 32, ctx) + const y = ctx.nextInt(pos[1] + 32) return decorateY(pos, y) }, square: (_config, pos, ctx) => { return [[ - pos[0] + nextInt(16, ctx), + pos[0] + ctx.nextInt(16), pos[1], - pos[2] + nextInt(16, ctx), + pos[2] + ctx.nextInt(16), ]] }, + surface_relative_threshold: (config, pos) => { + const height = terrain[clamp(0, 63, pos[0])] + const min = height + (config?.min_inclusive ?? -Infinity) + const max = height + (config?.max_inclusive ?? Infinity) + return (pos[1] < min || pos[1] > max) ? [pos] : [] + }, water_lake: (config, pos, ctx) => { - if (nextInt(config.chance ?? 1, ctx) === 0) { + if (ctx.nextInt(config.chance ?? 1) === 0) { return [[ - pos[0] + nextInt(16, ctx), - nextInt(256, ctx), - pos[2] + nextInt(16, ctx), + pos[0] + ctx.nextInt(16), + ctx.nextInt(256), + pos[2] + ctx.nextInt(16), ]] } return [] diff --git a/src/app/previews/NoiseSettings.ts b/src/app/previews/NoiseSettings.ts index 54379b84..a876f208 100644 --- a/src/app/previews/NoiseSettings.ts +++ b/src/app/previews/NoiseSettings.ts @@ -1,21 +1,64 @@ -import { NoiseChunkGenerator } from './noise/NoiseChunkGenerator' +import type { BlockPos, BlockState } from 'deepslate' +import { Chunk, ChunkPos, FixedBiome, NoiseChunkGenerator, NoiseGeneratorSettings } from 'deepslate' +import type { VersionId } from '../Schemas' +import { checkVersion } from '../Schemas' +import { deepClone, deepEqual } from '../Utils' +import { NoiseChunkGenerator as OldNoiseChunkGenerator } from './noise/NoiseChunkGenerator' export type NoiseSettingsOptions = { - biomeScale: number, - biomeDepth: number, + biomeFactor: number, + biomeOffset: number, + biomePeaks: number, offset: number, width: number, - seed: string, + seed: bigint, + version: VersionId, } +const Z = 0 + +const colors: Record = { + 'minecraft:air': [150, 160, 170], + 'minecraft:water': [20, 80, 170], + 'minecraft:lava': [200, 100, 0], + 'minecraft:stone': [50, 50, 50], + 'minecraft:netherrack': [100, 40, 40], + 'minecraft:end_stone': [200, 200, 140], +} + +let cacheState: any +let generatorCache: NoiseChunkGenerator +let chunkCache: Chunk[] = [] + export function noiseSettings(state: any, img: ImageData, options: NoiseSettingsOptions) { - const generator = new NoiseChunkGenerator(options.seed) - generator.reset(state, options.biomeDepth, options.biomeScale, options.offset, 200) + if (checkVersion(options.version, '1.18')) { + const { settings, generator } = getCached(state, options) + + const slice = new LevelSlice(-options.offset, options.width, settings.noise.minY, settings.noise.height) + slice.fill(generator) + + 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, Z]) + const color = colors[state.getName()] ?? [0, 0, 0] + data[i] = color[0] + data[i + 1] = color[1] + data[i + 2] = color[2] + data[i + 3] = 255 + } + } + return + } + + const generator = new OldNoiseChunkGenerator(options.seed) + generator.reset(state.noise, options.biomeOffset, options.biomeFactor, options.offset, 200) const data = img.data const row = img.width * 4 for (let x = 0; x < options.width; x += 1) { const noise = generator.iterateNoiseColumn(x - options.offset).reverse() - for (let y = 0; y < state.height; y += 1) { + for (let y = 0; y < state.noise.height; y += 1) { const i = y * row + x * 4 const color = getColor(noise, y) data[i] = color @@ -26,6 +69,26 @@ export function noiseSettings(state: any, img: ImageData, options: NoiseSettings } } +function getCached(state: unknown, options: NoiseSettingsOptions) { + const settings = NoiseGeneratorSettings.fromJson(state) + // Temporary fix for slides + settings.noise.bottomSlide.target *= 128 + settings.noise.topSlide.target *= 128 + const shape = { factor: options.biomeFactor, offset: options.biomeOffset, peaks: options.biomePeaks, nearWater: false } + + const newState = [state, shape, `${options.seed}`] + if (!deepEqual(newState, cacheState)) { + cacheState = deepClone(newState) + chunkCache = [] + const biomeSource = new FixedBiome('unknown') + generatorCache = new NoiseChunkGenerator(options.seed, biomeSource, settings, shape) + } + return { + settings, + generator: generatorCache, + } +} + function getColor(noise: number[], y: number): number { if (noise[y] > 0) { return 0 @@ -35,3 +98,42 @@ function getColor(noise: number[], y: number): number { } return 255 } + +class LevelSlice { + private readonly chunks: Chunk[] + private readonly filled: boolean[] + + constructor( + private readonly minX: number, + width: number, + minY: number, + height: number, + ) { + this.filled = [] + 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.filled[i] = true + return cached + } + return new Chunk(minY, height, ChunkPos.create(x, Z >> 4)) + }) + } + + public fill(generator: NoiseChunkGenerator) { + this.chunks.forEach((chunk, i) => { + if (!this.filled[i]) { + generator.fill(chunk) + this.filled[i] = true + chunkCache.push(chunk) + } + }) + } + + public getBlockState(pos: BlockPos): BlockState { + const chunkIndex = (pos[0] >> 4) - (this.minX >> 4) + return this.chunks[chunkIndex].getBlockState(pos) + } +} diff --git a/src/app/previews/noise/ImprovedNoise.ts b/src/app/previews/noise/ImprovedNoise.ts deleted file mode 100644 index 7bedf3ba..00000000 --- a/src/app/previews/noise/ImprovedNoise.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type seedrandom from 'seedrandom' -import { lerp3, smoothstep } from '../../Utils' - -export class ImprovedNoise { - private static readonly GRADIENT = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1], [1, 1, 0], [0, -1, 1], [-1, 1, 0], [0, -1, -1]] - private readonly p: number[] - public readonly xo: number - public readonly yo: number - public readonly zo: number - - constructor(random: seedrandom.prng) { - this.xo = random() * 256 - this.yo = random() * 256 - this.zo = random() * 256 - this.p = Array(256) - - for (let i = 0; i < 256; i += 1) { - this.p[i] = i - } - for (let i = 0; i < 256; i += 1) { - const n = random.int32() % (256 - i) - const b = this.p[i] - this.p[i] = this.p[i + n] - this.p[i + n] = b - } - } - - public noise(x: number, y: number, z: number, a: number, b: number) { - const x2 = x + this.xo - const y2 = y + this.yo - const z2 = z + this.zo - const x3 = Math.floor(x2) - const y3 = Math.floor(y2) - const z3 = Math.floor(z2) - const x4 = x2 - x3 - const y4 = y2 - y3 - const z4 = z2 - z3 - const x5 = smoothstep(x4) - const y5 = smoothstep(y4) - const z5 = smoothstep(z4) - - let y6 = 0 - if (a !== 0) { - y6 = Math.floor(Math.min(b, y4) / a) * a - } - - return this.sampleAndLerp(x3, y3, z3, x4, y4 - y6, z4, x5, y5, z5) - } - - private gradDot(a: number, b: number, c: number, d: number) { - const grad = ImprovedNoise.GRADIENT[a & 15] - return grad[0] * b + grad[1] * c + grad[2] * d - } - - private P(i: number) { - return this.p[i & 255] & 255 - } - - public sampleAndLerp(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) { - const j = this.P(a) + b - const k = this.P(j) + c - const l = this.P(j + 1) + c - const m = this.P(a + 1) + b - const n = this.P(m) + c - const o = this.P(m + 1) + c - - const p = this.gradDot(this.P(k), d, e, f) - const q = this.gradDot(this.P(n), d - 1, e, f) - const r = this.gradDot(this.P(l), d, e - 1, f) - const s = this.gradDot(this.P(o), d - 1, e - 1, f) - - const t = this.gradDot(this.P(k + 1), d, e, f - 1) - const u = this.gradDot(this.P(n + 1), d - 1, e, f - 1) - const v = this.gradDot(this.P(l + 1), d, e - 1, f - 1) - const w = this.gradDot(this.P(o + 1), d - 1, e - 1, f - 1) - - return lerp3(g, h, i, p, q, r, s, t, u, v, w) - } -} diff --git a/src/app/previews/noise/NoiseChunkGenerator.ts b/src/app/previews/noise/NoiseChunkGenerator.ts index abf8f85c..054687fd 100644 --- a/src/app/previews/noise/NoiseChunkGenerator.ts +++ b/src/app/previews/noise/NoiseChunkGenerator.ts @@ -1,5 +1,5 @@ +import { PerlinNoise, Random } from 'deepslate' import { clampedLerp, lerp2 } from '../../Utils' -import { PerlinNoise } from './PerlinNoise' export class NoiseChunkGenerator { private readonly minLimitPerlinNoise: PerlinNoise @@ -17,11 +17,12 @@ export class NoiseChunkGenerator { private noiseColumnCache: (number[] | null)[] = [] private xOffset: number = 0 - constructor(seed: string) { - this.minLimitPerlinNoise = PerlinNoise.fromRange(seed + 'djfqnqd', -15, 0) - this.maxLimitPerlinNoise = PerlinNoise.fromRange(seed + 'gowdnqs', -15, 0) - this.mainPerlinNoise = PerlinNoise.fromRange(seed + 'afiwmco', -7, 0) - this.depthNoise = PerlinNoise.fromRange(seed + 'qphnmeo', -15, 0) + constructor(seed: bigint) { + const random = new Random(seed) + this.minLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + this.maxLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + this.mainPerlinNoise = new PerlinNoise(random, -7, [1, 1, 1, 1, 1, 1, 1, 1]) + this.depthNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) } public reset(settings: any, depth: number, scale: number, xOffset: number, width: number) { @@ -71,7 +72,7 @@ export class NoiseChunkGenerator { const randomDensity = this.settings.random_density_offset ? this.getRandomDensity(x) : 0 for (let y = 0; y <= this.chunkCountY; y += 1) { - let noise = this.sampleAndClampNoise(x, y, this.mainPerlinNoise.getOctaveNoise(0).zo, xzScale, yScale, xzFactor, yFactor) + let noise = this.sampleAndClampNoise(x, y, this.mainPerlinNoise.getOctaveNoise(0)!.zo, xzScale, yScale, xzFactor, yFactor) const yOffset = 1 - y * 2 / this.chunkCountY + randomDensity const density = yOffset * this.settings.density_factor + this.settings.density_offset const falloff = (density + this.biomeDepth) * this.biomeScale @@ -100,7 +101,7 @@ export class NoiseChunkGenerator { } private getRandomDensity(x: number): number { - const noise = this.depthNoise.getValue(x * 200, 10, this.depthNoise.getOctaveNoise(0).zo, 1, 0, true) + const noise = this.depthNoise.sample(x * 200, 10, this.depthNoise.getOctaveNoise(0)!.zo, 1, 0, true) const a = (noise < 0) ? -noise * 0.3 : noise const b = a * 24.575625 - 2 return (b < 0) ? b * 0.009486607142857142 : Math.min(b, 1) * 0.006640625 @@ -120,18 +121,18 @@ export class NoiseChunkGenerator { const minLimitNoise = this.minLimitPerlinNoise.getOctaveNoise(i) if (minLimitNoise) { - a += minLimitNoise.noise(x2, y2, z2, e, y * e) / d + a += minLimitNoise.sample(x2, y2, z2, e, y * e) / d } const maxLimitNoise = this.maxLimitPerlinNoise.getOctaveNoise(i) if (maxLimitNoise) { - b += maxLimitNoise.noise(x2, y2, z2, e, y * e) / d + b += maxLimitNoise.sample(x2, y2, z2, e, y * e) / d } if (i < 8) { const mainNoise = this.mainPerlinNoise.getOctaveNoise(i) if (mainNoise) { - c += mainNoise.noise( + c += mainNoise.sample( PerlinNoise.wrap(x * xzFactor * d), PerlinNoise.wrap(y * yFactor * d), PerlinNoise.wrap(z * xzFactor * d), diff --git a/src/app/previews/noise/NormalNoise.ts b/src/app/previews/noise/NormalNoise.ts deleted file mode 100644 index 1f370a10..00000000 --- a/src/app/previews/noise/NormalNoise.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PerlinNoise } from './PerlinNoise' - -export class NormalNoise { - private readonly valueFactor: number - private readonly first: PerlinNoise - private readonly second: PerlinNoise - - constructor(seed: string, firstOctave: number, amplitudes: number[]) { - this.first = new PerlinNoise(seed, firstOctave, amplitudes) - this.second = new PerlinNoise(seed + 'a', firstOctave, amplitudes) - - let min = +Infinity - let max = -Infinity - for (let i = 0; i < amplitudes.length; i += 1) { - if (amplitudes[i] !== 0) { - min = Math.min(min, i) - max = Math.max(max, i) - } - } - - const expectedDeviation = 0.1 * (1 + 1 / (max - min + 1)) - this.valueFactor = (1/6) / expectedDeviation - } - - getValue(x: number, y: number, z: number) { - const x2 = x * 1.0181268882175227 - const y2 = y * 1.0181268882175227 - const z2 = z * 1.0181268882175227 - return (this.first.getValue(x, y, z) + this.second.getValue(x2, y2, z2)) * this.valueFactor - } -} diff --git a/src/app/previews/noise/PerlinNoise.ts b/src/app/previews/noise/PerlinNoise.ts deleted file mode 100644 index a43511ab..00000000 --- a/src/app/previews/noise/PerlinNoise.ts +++ /dev/null @@ -1,54 +0,0 @@ -import seedrandom from 'seedrandom' -import { ImprovedNoise } from './ImprovedNoise' - -export class PerlinNoise { - private readonly noiseLevels: ImprovedNoise[] - private readonly amplitudes: number[] - private readonly lowestFreqValueFactor: number - private readonly lowestFreqInputFactor: number - - constructor(seed: string, firstOctave: number, amplitudes: number[]) { - this.amplitudes = amplitudes - - this.noiseLevels = Array(this.amplitudes.length) - for (let i = 0; i < this.amplitudes.length; i += 1) { - this.noiseLevels[i] = new ImprovedNoise(seedrandom(seed)) - } - - this.lowestFreqInputFactor = Math.pow(2, firstOctave) - this.lowestFreqValueFactor = Math.pow(2, (amplitudes.length - 1)) / (Math.pow(2, amplitudes.length) - 1) - } - - public static fromRange(seed: string, min: number, max: number) { - return new PerlinNoise(seed, min, Array(max - min + 1).fill(1)) - } - - public getValue(x: number, y: number, z: number, a = 0, b = 0, fixY = false) { - let value = 0 - let inputF = this.lowestFreqInputFactor - let valueF = this.lowestFreqValueFactor - for (let i = 0; i < this.noiseLevels.length; i += 1) { - const noise = this.noiseLevels[i] - if (noise) { - value += this.amplitudes[i] * noise.noise( - PerlinNoise.wrap(x * inputF), - fixY ? -noise.yo : PerlinNoise.wrap(y * inputF), - PerlinNoise.wrap(z * inputF), - a * inputF, - b * inputF - ) * valueF - } - inputF *= 2 - valueF /= 2 - } - return value - } - - public getOctaveNoise(i: number) { - return this.noiseLevels[this.noiseLevels.length - 1 - i] - } - - public static wrap(value: number) { - return value - Math.floor(value / 3.3554432E7 + 0.5) * 3.3554432E7 - } -} diff --git a/src/app/schema/Mounter.ts b/src/app/schema/Mounter.ts deleted file mode 100644 index de82cad0..00000000 --- a/src/app/schema/Mounter.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { hexId } from '../Utils' - -export class Mounter { - private registry: { [id: string]: (el: Element) => void } = {} - - register(callback: (el: Element) => void): string { - const id = hexId() - this.registry[id] = callback - return id - } - - on(type: string, callback: (el: Element) => void): string { - return this.register(el => { - el.addEventListener(type, evt => { - callback(el) - evt.stopPropagation() - }) - }) - } - - onChange(callback: (el: Element) => void): string { - return this.on('change', callback) - } - - onClick(callback: (el: Element) => void): string { - return this.on('click', callback) - } - - mounted(el: Element): void { - el.querySelectorAll('[data-id]').forEach(el => { - const id = el.getAttribute('data-id')! - this.registry[id]?.(el) - }) - this.registry = {} - } -} diff --git a/src/app/schema/renderHtml.tsx b/src/app/schema/renderHtml.tsx index d36459af..8f4c8ce3 100644 --- a/src/app/schema/renderHtml.tsx +++ b/src/app/schema/renderHtml.tsx @@ -6,13 +6,17 @@ import { Octicon } from '../components/Octicon' import { useFocus } from '../hooks' import { locale } from '../Locales' import type { BlockStateRegistry } from '../Schemas' -import { hexId } from '../Utils' +import { hexId, newSeed } from '../Utils' -const selectRegistries = ['loot_table.type', 'loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'carver.type', 'feature.type', 'decorator.type', 'feature.tree.minimum_size.type', 'block_state_provider.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'structure_feature.type', 'surface_builder.type', 'processor.processor_type', 'rule_test.predicate_type', 'pos_rule_test.predicate_type', 'template_element.element_type', 'block_placer.type'] +const LIST_LIMIT = 20 +const LIST_LIMIT_SHOWN = 5 + +const selectRegistries = ['loot_table.type', 'loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'dimension.generator.biome_source.preset', 'carver.type', 'feature.type', 'decorator.type', 'feature.tree.minimum_size.type', 'block_state_provider.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'structure_feature.type', 'surface_builder.type', 'processor.processor_type', 'rule_test.predicate_type', 'pos_rule_test.predicate_type', 'template_element.element_type', 'block_placer.type'] const hiddenFields = ['number_provider.type', 'score_provider.type', 'nbt_provider.type', 'int_provider.type', 'float_provider.type', 'height_provider.type'] const flattenedFields = ['feature.config', 'decorator.config', 'int_provider.value', 'float_provider.value', 'block_state_provider.simple_state_provider.state', 'block_state_provider.rotated_block_provider.state', 'block_state_provider.weighted_state_provider.entries.entry.data', 'rule_test.block_state', 'structure_feature.config', 'surface_builder.config', 'template_pool.elements.entry.element'] -const inlineFields = ['loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'feature.type', 'decorator.type', 'block_state_provider.type', 'feature.tree.minimum_size.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'block_placer.type', 'rule_test.predicate_type', 'processor.processor_type', 'template_element.element_type', 'nbt_operation.op', 'number_provider.value', 'score_provider.name', 'score_provider.target', 'nbt_provider.source', 'nbt_provider.target'] +const inlineFields = ['loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'feature.type', 'decorator.type', 'block_state_provider.type', 'feature.tree.minimum_size.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'block_placer.type', 'rule_test.predicate_type', 'processor.processor_type', 'template_element.element_type', 'nbt_operation.op', 'number_provider.value', 'score_provider.name', 'score_provider.target', 'nbt_provider.source', 'nbt_provider.target', 'generator_biome.biome'] const nbtFields = ['function.set_nbt.tag', 'advancement.display.icon.nbt', 'text_component_object.nbt', 'entity.nbt', 'block.nbt', 'item.nbt'] +const fixedLists = ['generator_biome.parameters.temperature', 'generator_biome.parameters.humidity', 'generator_biome.parameters.continentalness', 'generator_biome.parameters.erosion', 'generator_biome.parameters.depth', 'generator_biome.parameters.weirdness', 'feature.end_spike.crystal_beam_target', 'feature.end_gateway.exit'] /** * Secondary model used to remember the keys of a map @@ -50,7 +54,6 @@ export const renderHtml: RenderHook = { const choiceContextPath = config?.choiceContext ? new Path([], [config.choiceContext]) : config?.context ? new Path([], [config.context]) : path const set = (value: string) => { const c = choices.find(c => c.type === value) ?? choice - console.log(c) path.model.set(path, c.change ? c.change(value) : c.node.default()) } const inject = + {path.equals(new Path(['generator', 'seed'])) && } + } function StringSuffix({ path, getValues, config, node, value, lang, states }: NodeProps) { diff --git a/src/config.json b/src/config.json index 58f184da..d45efe12 100644 --- a/src/config.json +++ b/src/config.json @@ -63,6 +63,14 @@ }, { "id": "1.17", + "refs": { + "mcdata_master": "1.17.1", + "vanilla_datapack_data": "1.17.1-data", + "vanilla_datapack_summary": "1.17.1-summary" + } + }, + { + "id": "1.18", "refs": { "mcdata_master": "master", "vanilla_datapack_data": "data", @@ -76,7 +84,8 @@ "id": "loot_table", "url": "loot-table", "path": "loot_tables", - "schema": "loot_table" + "schema": "loot_table", + "maxVersion": "1.17" }, { "id": "predicate", diff --git a/src/locales/en.json b/src/locales/en.json index c7a1a73b..14259201 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -38,6 +38,9 @@ "preview": "Visualize", "preview.scale": "Scale", "preview.depth": "Depth", + "preview.factor": "Factor", + "preview.offset": "Offset", + "preview.peaks": "Peaks", "preview.width": "Width", "source_placeholder": "Paste JSON content here", "undo": "Undo", diff --git a/src/styles/global.css b/src/styles/global.css index aeb3bb72..f0106366 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -7,8 +7,8 @@ --text-1: #ffffff; --text-2: #dcdcdc; --text-3: #c3c3c3; - --accent-blue: #50baf9; - --accent-red: #f1453f; + --accent-primary: #50baf9; + --accent-success: #3eb84f; --nav: #91908f; --nav-hover: #b4b3b0; --nav-faded: #4d4c4c; @@ -27,8 +27,8 @@ --text-1: #000000; --text-2: #505050; --text-3: #6a6a6a; - --accent-blue: #088cdb; - --accent-red: #cc312c; + --accent-primary: #088cdb; + --accent-success: #1a7f37; --nav: #343a40; --nav-hover: #565d64; --nav-faded: #9fa2a7; @@ -48,8 +48,8 @@ --text-1: #000000; --text-2: #505050; --text-3: #6a6a6a; - --accent-blue: #088cdb; - --accent-red: #cc312c; + --accent-primary: #088cdb; + --accent-success: #1a7f37; --nav: #343a40; --nav-hover: #565d64; --nav-faded: #9fa2a7; @@ -331,8 +331,8 @@ main.has-preview { } .btn.active { - color: var(--accent-blue); - fill: var(--accent-blue); + color: var(--accent-primary); + fill: var(--accent-primary); } .btn:not(.btn-input):hover { @@ -462,7 +462,11 @@ main.has-preview { } .popup-action.action-preview { - fill: var(--accent-blue); + fill: var(--accent-primary); +} + +.popup-action.action-copy.active { + fill: var(--accent-success); } .error { @@ -648,7 +652,7 @@ hr { } .ea-content strong { - color: var(--accent-blue) !important; + color: var(--accent-primary) !important; } .ea-callout { diff --git a/src/styles/nodes.css b/src/styles/nodes.css index ea7598ef..7f8e1d1c 100644 --- a/src/styles/nodes.css +++ b/src/styles/nodes.css @@ -3,6 +3,7 @@ --node-background-label: #1b1b1b; --node-background-input: #272727; --node-text: #dadada; + --node-text-dimmed: #b4b4b4; --node-selected: #ad9715; --node-selected-border: #8d7a0d; --node-add: #487c13; @@ -29,6 +30,7 @@ --node-background-label: #e4e4e4; --node-background-input: #ffffff; --node-text: #000000; + --node-text-dimmed: #2c2c2c; --node-selected: #f0e65e; --node-selected-border: #b9a327; --node-add: #9bd464; @@ -56,6 +58,7 @@ --node-background-label: #e4e4e4; --node-background-input: #ffffff; --node-text: #000000; + --node-text-dimmed: #2c2c2c; --node-selected: #f0e65e; --node-selected-border: #b9a327; --node-add: #9bd464; @@ -144,7 +147,8 @@ .node-error ~ select:last-child, .node-error ~ input:last-child, -.node-error ~ input[list]:nth-last-child(2) { +.node-error ~ input[list]:nth-last-child(2), +.node-error + .fixed-list ~ input { border-color: var(--node-remove) !important; } @@ -339,6 +343,11 @@ span.menu-item { color: var(--node-popup-text-dimmed); } +.node-message { + color: var(--node-text-dimmed); + margin: 6px 0; +} + /* Node body and list entry */ .node { @@ -421,8 +430,13 @@ span.menu-item { width: 25px; } +.fixed-list { + display: none; +} + .number-node input, -.range-node input { +.range-node input, +.fixed-list ~ input { width: 100px; }