Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configured feature visualizer #449

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/app/components/generator/PreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -50,6 +50,11 @@ export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) {
return <DecoratorPreview {...{ model, version, shown, data }} />
}

if (id === 'worldgen/configured_feature' && checkVersion(version, '1.18')) {
console.log('FEATURE')
return <FeaturePreview {...{ model, version, shown, data }} />
}

if (id === 'worldgen/structure_set' && checkVersion(version, '1.19')) {
return <StructureSetPreview {...{ model, version, shown, data }} />
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/generator/SchemaGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
91 changes: 6 additions & 85 deletions src/app/components/previews/Decorator.ts
Original file line number Diff line number Diff line change
@@ -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]

Expand Down Expand Up @@ -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<number>((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)
Expand All @@ -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)
Expand All @@ -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]
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 4 additions & 3 deletions src/app/components/previews/DecoratorPreview.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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,
Expand Down
80 changes: 80 additions & 0 deletions src/app/components/previews/Feature.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
},
}
79 changes: 79 additions & 0 deletions src/app/components/previews/FeaturePreview.tsx
Original file line number Diff line number Diff line change
@@ -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<StructureRenderer | undefined>(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 <>
<div class="controls preview-controls">
<Btn icon="sync" tooltip={locale('generate_new_seed')} onClick={() => setSeed(randomSeed())} />
</div>
<div class="full-preview">
<InteractiveCanvas3D onSetup={onSetup} onDraw={onDraw} onResize={onResize} startDistance={10} startPosition={[MAX_SIZE/2, MAX_SIZE/2, MAX_SIZE/2]} startYRotation={2.6} />
</div>
</>
}
Loading