diff --git a/src/app/components/generator/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx index b5e7da81..6707c4c6 100644 --- a/src/app/components/generator/PreviewPanel.tsx +++ b/src/app/components/generator/PreviewPanel.tsx @@ -4,7 +4,7 @@ import { useState } from 'preact/hooks' import { useModel } from '../../hooks/index.js' import type { VersionId } from '../../services/index.js' import { checkVersion } from '../../services/index.js' -import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, StructureSetPreview } from '../previews/index.js' +import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, FeaturePreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, StructureSetPreview } from '../previews/index.js' export const HasPreview = ['loot_table', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'model'] @@ -50,6 +50,11 @@ export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) { return } + if (id === 'worldgen/configured_feature' && checkVersion(version, '1.18')) { + console.log('FEATURE') + return + } + if (id === 'worldgen/structure_set' && checkVersion(version, '1.19')) { return } diff --git a/src/app/components/generator/SchemaGenerator.tsx b/src/app/components/generator/SchemaGenerator.tsx index 9fe4b4fe..01087a72 100644 --- a/src/app/components/generator/SchemaGenerator.tsx +++ b/src/app/components/generator/SchemaGenerator.tsx @@ -10,7 +10,7 @@ import { DRAFT_PROJECT, useLocale, useProject, useVersion } from '../../contexts import { AsyncCancel, useActiveTimeout, useAsync, useModel, useSearchParam } from '../../hooks/index.js' import { getOutput } from '../../schema/transformOutput.js' import type { VersionId } from '../../services/index.js' -import { checkVersion, fetchPreset, getBlockStates, getCollections, getModel, getSnippet, shareSnippet } from '../../services/index.js' +import { fetchPreset, getBlockStates, getCollections, getModel, getSnippet, shareSnippet } from '../../services/index.js' import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js' export const SHARE_KEY = 'share' @@ -265,7 +265,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { const [copyActive, copySuccess] = useActiveTimeout() const [previewShown, setPreviewShown] = useState(Store.getPreviewPanelOpen() ?? window.innerWidth > 800) - const hasPreview = HasPreview.includes(gen.id) && !(gen.id === 'worldgen/configured_feature' && checkVersion(version, '1.18')) + const hasPreview = HasPreview.includes(gen.id) if (previewShown && !hasPreview) setPreviewShown(false) let actionsShown = 2 if (hasPreview) actionsShown += 1 diff --git a/src/app/components/previews/Decorator.ts b/src/app/components/previews/Decorator.ts index 23a26de4..bce45e3d 100644 --- a/src/app/components/previews/Decorator.ts +++ b/src/app/components/previews/Decorator.ts @@ -1,9 +1,10 @@ import { DataModel } from '@mcschema/core' import type { BlockPos, ChunkPos, PerlinNoise, Random } from 'deepslate/worldgen' import type { Color } from '../../Utils.js' -import { clamp, isObject, stringToColor } from '../../Utils.js' +import { clamp, stringToColor } from '../../Utils.js' import type { VersionId } from '../../services/index.js' import { checkVersion } from '../../services/index.js' +import { normalizeId, sampleHeight, sampleInt } from './WorldgenUtils.jsx' export type Placement = [BlockPos, number] @@ -54,90 +55,10 @@ export function decorateChunk(pos: ChunkPos, state: any, ctx: PlacementContext): }) } -function normalize(id: string) { - return id.startsWith('minecraft:') ? id.slice(10) : id -} - function decorateY(pos: BlockPos, y: number): BlockPos[] { return [[ pos[0], y, pos[2] ]] } -export function sampleInt(value: any, ctx: PlacementContext): number { - if (typeof value === 'number') { - return value - } else if (value.base) { - 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 + 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 clamp(sampleInt(value.value.source, ctx), value.value.min_inclusive, value.value.max_inclusive) - case 'clamped_normal': - const normal = value.value.mean + ctx.nextGaussian() * value.value.deviation - return Math.floor(clamp(value.value.min_inclusive, value.value.max_inclusive, normal)) - case 'weighted_list': - const totalWeight = (value.distribution as any[]).reduce((sum, e) => sum + e.weight, 0) - let i = ctx.nextInt(totalWeight) - for (const e of value.distribution) { - i -= e.weight - if (i < 0) return sampleInt(e.data, ctx) - } - return 0 - } - return 1 - } -} - -function resolveAnchor(anchor: any, _ctx: PlacementContext): number { - if (!isObject(anchor)) return 0 - if (anchor.absolute !== undefined) return anchor.absolute - if (anchor.above_bottom !== undefined) return anchor.above_bottom - if (anchor.below_top !== undefined) return 256 - anchor.below_top - return 0 -} - -function sampleHeight(height: any, ctx: PlacementContext): number { - if (!isObject(height)) throw new Error('Invalid height provider') - if (typeof height.type !== 'string') { - return resolveAnchor(height, ctx) - } - switch (normalize(height.type)) { - case 'constant': return resolveAnchor(height.value, ctx) - case 'uniform': { - const min = resolveAnchor(height.min_inclusive, ctx) - const max = resolveAnchor(height.max_inclusive, ctx) - return min + ctx.nextInt(max - min + 1) - } - case 'biased_to_bottom': { - const min = resolveAnchor(height.min_inclusive, ctx) - const max = resolveAnchor(height.max_inclusive, ctx) - const n = ctx.nextInt(max - min - (height.inner ?? 1) + 1) - return min + ctx.nextInt(n + (height.inner ?? 1)) - } - case 'very_biased_to_bottom': { - const min = resolveAnchor(height.min_inclusive, ctx) - const max = resolveAnchor(height.max_inclusive, ctx) - const inner = height.inner ?? 1 - const n1 = min + inner + ctx.nextInt(max - min - inner + 1) - const n2 = min + ctx.nextInt(n1 - min) - return min + ctx.nextInt(n2 - min + inner) - } - case 'trapezoid': { - const min = resolveAnchor(height.min_inclusive, ctx) - const max = resolveAnchor(height.max_inclusive, ctx) - const plateau = height.plateau ?? 0 - if (plateau >= max - min) { - return min + ctx.nextInt(max - min + 1) - } - const n1 = (max - min - plateau) / 2 - const n2 = (max - min) - n1 - return min + ctx.nextInt(n2 + 1) + ctx.nextInt(n1 + 1) - } - default: throw new Error(`Invalid height provider ${height.type}`) - } -} - // 1.17 and before function useFeature(s: string, ctx: PlacementContext) { const i = ctx.features.indexOf(s) @@ -151,7 +72,7 @@ function getPlacements(pos: BlockPos, feature: any, ctx: PlacementContext): void ctx.placements.push([pos, useFeature(feature, ctx)]) return } - const type = normalize(feature?.type ?? 'no_op') + const type = normalizeId(feature?.type ?? 'no_op') const featureFn = Features[type] if (featureFn) { featureFn(feature.config, pos, ctx) @@ -161,7 +82,7 @@ function getPlacements(pos: BlockPos, feature: any, ctx: PlacementContext): void } function getPositions(pos: BlockPos, decorator: any, ctx: PlacementContext): BlockPos[] { - const type = normalize(decorator?.type ?? 'nope') + const type = normalizeId(decorator?.type ?? 'nope') const decoratorFn = Decorators[type] if (!decoratorFn) { return [pos] @@ -357,10 +278,10 @@ const Decorators: { function modifyPlacement(pos: BlockPos, placement: any[], ctx: PlacementContext) { let positions = [pos] for (const modifier of placement) { - const modifierFn = PlacementModifiers[normalize(modifier?.type ?? 'nope')] + const modifierFn = PlacementModifiers[normalizeId(modifier?.type ?? 'nope')] if (!modifierFn) continue positions = positions.flatMap(pos => - PlacementModifiers[normalize(modifier.type)](modifier, pos, ctx) + PlacementModifiers[normalizeId(modifier.type)](modifier, pos, ctx) ) } for (const pos of positions) { diff --git a/src/app/components/previews/DecoratorPreview.tsx b/src/app/components/previews/DecoratorPreview.tsx index 9b038e0d..208d6e78 100644 --- a/src/app/components/previews/DecoratorPreview.tsx +++ b/src/app/components/previews/DecoratorPreview.tsx @@ -1,13 +1,14 @@ import { BlockPos, ChunkPos, LegacyRandom, PerlinNoise } from 'deepslate' import type { mat3 } from 'gl-matrix' import { useCallback, useMemo, useRef, useState } from 'preact/hooks' -import { useLocale } from '../../contexts/index.js' import { computeIfAbsent, iterateWorld2D, randomSeed } from '../../Utils.js' +import { useLocale } from '../../contexts/index.js' import { Btn } from '../index.js' import type { PlacedFeature, PlacementContext } from './Decorator.js' import { decorateChunk } from './Decorator.js' -import type { PreviewProps } from './index.js' import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx' +import { nextGaussian } from './WorldgenUtils.jsx' +import type { PreviewProps } from './index.js' export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => { const { locale } = useLocale() @@ -25,7 +26,7 @@ export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => { version: version, nextFloat: () => random.nextFloat(), nextInt: (max: number) => random.nextInt(max), - nextGaussian: () => Math.sqrt(-2 * Math.log(1 - random.nextFloat())) * Math.cos(2 * Math.PI * random.nextFloat()), + nextGaussian: nextGaussian(random), } return { context, diff --git a/src/app/components/previews/Feature.ts b/src/app/components/previews/Feature.ts new file mode 100644 index 00000000..48a074c2 --- /dev/null +++ b/src/app/components/previews/Feature.ts @@ -0,0 +1,80 @@ +import type { Random } from 'deepslate' +import { BlockPos, BlockState } from 'deepslate' +import type { VersionId } from '../../services/index.js' +import { sampleBlockState, sampleInt } from './WorldgenUtils.jsx' + +export type FeatureContext = { + version: VersionId, + random: Random, + place: (pos: BlockPos, block: string | BlockState) => void, + nextFloat(): number, + nextInt(max: number): number, + nextGaussian(): number, +} + +export function placeFeature(data: any, ctx: FeatureContext) { + const type = data.type.replace(/^minecraft:/, '') + Features[type]?.(data.config, ctx) +} + +const Features: { + [key: string]: (config: any, ctx: FeatureContext) => void, +} = { + bamboo: (config, ctx) => { + const n = ctx.nextInt(12) + 5 + if (ctx.nextFloat() < config?.probability ?? 0) { + const s = ctx.nextInt(4) + 1 + for (let x = -s; x <= s; x += 1) { + for (let z = -s; z <= s; z += 1) { + if (x * x + z * z <= s * s) { + ctx.place([x, -1, z], new BlockState('podzol', { snowy: 'false' })) + } + } + } + } + for (let i = 0; i < n; i += 1) { + ctx.place([0, i, 0], new BlockState('bamboo', { age: '1', leaves: 'none', stage: '0' })) + } + ctx.place([0, n, 0], new BlockState('bamboo', { age: '1', leaves: 'large', stage: '1'})) + ctx.place([0, n-1, 0], new BlockState('bamboo', { age: '1', leaves: 'large', stage: '0'})) + ctx.place([0, n-2, 0], new BlockState('bamboo', { age: '1', leaves: 'small', stage: '0'})) + }, + tree: (config, ctx) => { + const trunk = config.trunk_placer + const trunkPlacerType = trunk.type.replace(/^minecraft:/, '') + const treeHeight = trunk.base_height + ctx.nextInt(trunk.height_rand_a + 1) + ctx.nextInt(trunk.height_rand_b + 1) + + function placeLog(pos: BlockPos) { + ctx.place(pos, sampleBlockState(config.trunk_provider, ctx)) + } + + const horizontalDirs = [[-1, 0], [0, 1], [1, 0], [0, -1]] as const + const startPos = BlockPos.ZERO // TODO: roots + switch (trunkPlacerType) { + case 'upwards_branching_trunk_placer': { + const branchProbability = trunk.place_branch_per_log_probability + const extraBranchLength = trunk.extra_branch_length + const extraBranchSteps = trunk.extra_branch_steps + for (let i = 0; i < treeHeight; i += 1) { + const y = startPos[1] + i + placeLog(BlockPos.create(startPos[0], y, startPos[2])) + if (i < treeHeight - 1 && ctx.nextFloat() < branchProbability) { + const dir = horizontalDirs[ctx.nextInt(4)] + const branchLength = Math.max(0, sampleInt(extraBranchLength, ctx) - sampleInt(extraBranchLength, ctx) - 1) + let branchSteps = sampleInt(extraBranchSteps, ctx) + let x = startPos[0] + let z = startPos[1] + for (let j = branchLength; length < treeHeight && branchSteps > 0; j += 1) { + if (j >= 1) { + x += dir[0] + z += dir[1] + placeLog([x, y + j, z]) + } + branchSteps -= 1 + } + } + } + } + } + }, +} diff --git a/src/app/components/previews/FeaturePreview.tsx b/src/app/components/previews/FeaturePreview.tsx new file mode 100644 index 00000000..db6b2b46 --- /dev/null +++ b/src/app/components/previews/FeaturePreview.tsx @@ -0,0 +1,79 @@ +import { DataModel } from '@mcschema/core' +import { LegacyRandom, Structure, StructureRenderer } from 'deepslate' +import { BlockPos } from 'deepslate-1.18.2' +import type { mat4 } from 'gl-matrix' +import { useCallback, useMemo, useRef, useState } from 'preact/hooks' +import { randomSeed } from '../../Utils.js' +import { useLocale } from '../../contexts/index.js' +import { useAsync } from '../../hooks/useAsync.js' +import { AsyncCancel } from '../../hooks/useAsyncFn.js' +import { getResources } from '../../services/Resources.js' +import { Btn } from '../Btn.jsx' +import type { FeatureContext } from './Feature.js' +import { placeFeature } from './Feature.js' +import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx' +import { nextGaussian } from './WorldgenUtils.jsx' +import type { PreviewProps } from './index.js' + +const MAX_SIZE = 45 + +export const FeaturePreview = ({ data, version, shown }: PreviewProps) => { + const { locale } = useLocale() + const [seed, setSeed] = useState(randomSeed()) + const serializedData = JSON.stringify(data) + + const { value: resources } = useAsync(async () => { + if (!shown) return AsyncCancel + return await getResources(version) + }, [shown, version, serializedData]) + + const { structure } = useMemo(() => { + const structure = new Structure([MAX_SIZE, MAX_SIZE, MAX_SIZE]) + const random = new LegacyRandom(seed) + const placeOffset = Math.floor((MAX_SIZE - 1) / 2) + const context: FeatureContext = { + version: version, + random, + place: (pos, block) => { + const structurePos = BlockPos.offset(pos, placeOffset, placeOffset, placeOffset) + if (structurePos.some((v, i) => v < 0 || v >= structure.getSize()[i])) return + const name = typeof block === 'string' ? block : block.getName() + const properties = typeof block === 'string' ? undefined : block.getProperties() + structure.addBlock(structurePos, name, properties) + }, + nextFloat: () => random.nextFloat(), + nextInt: (max: number) => random.nextInt(max), + nextGaussian: nextGaussian(random), + } + placeFeature(DataModel.unwrapLists(data), context) + return { structure } + }, [serializedData, version, seed]) + + const renderer = useRef(undefined) + + const onSetup = useCallback((canvas: HTMLCanvasElement) => { + if (renderer.current) { + renderer.current.setStructure(structure) + return + } + if (!resources || !shown) return + const gl = canvas.getContext('webgl') + if (!gl) return + renderer.current = new StructureRenderer(gl, structure, resources, { useInvisibleBlockBuffer: false }) + }, [resources, shown, structure]) + const onResize = useCallback((width: number, height: number) => { + renderer.current?.setViewport(0, 0, width, height) + }, [resources]) + const onDraw = useCallback((transform: mat4) => { + renderer.current?.drawStructure(transform) + }, []) + + return <> +
+ setSeed(randomSeed())} /> +
+
+ +
+ +} diff --git a/src/app/components/previews/WorldgenUtils.tsx b/src/app/components/previews/WorldgenUtils.tsx new file mode 100644 index 00000000..03f21c63 --- /dev/null +++ b/src/app/components/previews/WorldgenUtils.tsx @@ -0,0 +1,106 @@ +import type { Random } from 'deepslate' +import { BlockState } from 'deepslate' +import { clamp, isObject } from '../../Utils.js' +import type { VersionId } from '../../services/index.js' + +export type WorldgenUtilsContext = { + random: Random, + version: VersionId, + nextFloat(): number, + nextInt(max: number): number, + nextGaussian(): number, +} + +export function nextGaussian(random: Random) { + return () => Math.sqrt(-2 * Math.log(1 - random.nextFloat())) * Math.cos(2 * Math.PI * random.nextFloat()) +} + +export function normalizeId(id: string) { + return id.startsWith('minecraft:') ? id.slice(10) : id +} + +export function sampleInt(value: any, ctx: WorldgenUtilsContext): number { + if (typeof value === 'number') { + return value + } else if (value.base) { + return value.base ?? 1 + ctx.nextInt(1 + (value.spread ?? 0)) + } else { + switch (normalizeId(value.type)) { + case 'constant': return value.value + 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 clamp(sampleInt(value.value.source, ctx), value.value.min_inclusive, value.value.max_inclusive) + case 'clamped_normal': + const normal = value.value.mean + ctx.nextGaussian() * value.value.deviation + return Math.floor(clamp(value.value.min_inclusive, value.value.max_inclusive, normal)) + case 'weighted_list': + const totalWeight = (value.distribution as any[]).reduce((sum, e) => sum + e.weight, 0) + let i = ctx.nextInt(totalWeight) + for (const e of value.distribution) { + i -= e.weight + if (i < 0) return sampleInt(e.data, ctx) + } + return 0 + } + return 1 + } +} + +export function resolveAnchor(anchor: any, _ctx: WorldgenUtilsContext): number { + if (!isObject(anchor)) return 0 + if (anchor.absolute !== undefined) return anchor.absolute + if (anchor.above_bottom !== undefined) return anchor.above_bottom + if (anchor.below_top !== undefined) return 256 - anchor.below_top + return 0 +} + +export function sampleHeight(height: any, ctx: WorldgenUtilsContext): number { + if (!isObject(height)) throw new Error('Invalid height provider') + if (typeof height.type !== 'string') { + return resolveAnchor(height, ctx) + } + switch (normalizeId(height.type)) { + case 'constant': return resolveAnchor(height.value, ctx) + case 'uniform': { + const min = resolveAnchor(height.min_inclusive, ctx) + const max = resolveAnchor(height.max_inclusive, ctx) + return min + ctx.nextInt(max - min + 1) + } + case 'biased_to_bottom': { + const min = resolveAnchor(height.min_inclusive, ctx) + const max = resolveAnchor(height.max_inclusive, ctx) + const n = ctx.nextInt(max - min - (height.inner ?? 1) + 1) + return min + ctx.nextInt(n + (height.inner ?? 1)) + } + case 'very_biased_to_bottom': { + const min = resolveAnchor(height.min_inclusive, ctx) + const max = resolveAnchor(height.max_inclusive, ctx) + const inner = height.inner ?? 1 + const n1 = min + inner + ctx.nextInt(max - min - inner + 1) + const n2 = min + ctx.nextInt(n1 - min) + return min + ctx.nextInt(n2 - min + inner) + } + case 'trapezoid': { + const min = resolveAnchor(height.min_inclusive, ctx) + const max = resolveAnchor(height.max_inclusive, ctx) + const plateau = height.plateau ?? 0 + if (plateau >= max - min) { + return min + ctx.nextInt(max - min + 1) + } + const n1 = (max - min - plateau) / 2 + const n2 = (max - min) - n1 + return min + ctx.nextInt(n2 + 1) + ctx.nextInt(n1 + 1) + } + default: throw new Error(`Invalid height provider ${height.type}`) + } +} + +export function sampleBlockState(provider: any, _ctx: WorldgenUtilsContext): BlockState { + const type = provider.type.replace(/^minecraft:/, '') + switch (type) { + case 'simple_state_provider': { + return BlockState.fromJson(provider.state) + } + } + return BlockState.AIR +} diff --git a/src/app/components/previews/index.ts b/src/app/components/previews/index.ts index e33b5cd4..b0b98fa3 100644 --- a/src/app/components/previews/index.ts +++ b/src/app/components/previews/index.ts @@ -5,6 +5,7 @@ export * from './BiomeSourcePreview.js' export * from './BlockStatePreview.jsx' export * from './DecoratorPreview.js' export * from './DensityFunctionPreview.js' +export * from './FeaturePreview.jsx' export * from './LootTablePreview.jsx' export * from './ModelPreview.jsx' export * from './NoisePreview.js'