From 25d5a3bcf46878ab516cf369d4e8493132b49f9a Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Wed, 18 Dec 2024 14:10:52 -0500 Subject: [PATCH] Optimized simple patterns with non cuboid shapes --- src/server/modules/history.ts | 5 +- src/server/modules/pattern.ts | 34 ++++---- src/server/shapes/base_shape.ts | 145 ++++++++++++++++---------------- 3 files changed, 94 insertions(+), 90 deletions(-) diff --git a/src/server/modules/history.ts b/src/server/modules/history.ts index d2992fe8a..27589e043 100644 --- a/src/server/modules/history.ts +++ b/src/server/modules/history.ts @@ -1,5 +1,5 @@ import { Vector3, Dimension, BlockPermutation, Block } from "@minecraft/server"; -import { Vector, regionVolume, regionSize, Thread, getCurrentThread } from "@notbeer-api"; +import { Vector, regionVolume, regionSize, Thread, getCurrentThread, iterateChunk } from "@notbeer-api"; import { UnloadedChunksError } from "./assert.js"; import { canPlaceBlock } from "../util.js"; import { PlayerSession } from "../sessions.js"; @@ -365,7 +365,8 @@ class BlockChangeImpl implements BlockChanges { } catch { /* pass */ } - yield ++i; + i++; + if (iterateChunk) yield i; } for (const range of this.ranges) yield* this.history.addRedoStructure(this.record, ...range); diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts index 973601ad2..faddfa452 100644 --- a/src/server/modules/pattern.ts +++ b/src/server/modules/pattern.ts @@ -1,4 +1,4 @@ -import { Vector3, BlockPermutation, Player, Dimension, BlockVolume } from "@minecraft/server"; +import { Vector3, BlockPermutation, Player, Dimension, BlockVolumeBase } from "@minecraft/server"; import { CustomArgType, commandSyntaxError, Vector, Server } from "@notbeer-api"; import { PlayerSession } from "server/sessions.js"; import { wrap } from "server/util.js"; @@ -148,22 +148,24 @@ export class Pattern implements CustomArgType { return this.block instanceof BlockPattern || (this.block instanceof ChainPattern && this.block.nodes.length == 1 && this.block.nodes[0] instanceof BlockPattern); } - fillSimpleArea(dimension: Dimension, start: Vector3, end: Vector3, mask?: Mask) { - if (!this.simpleCache) { - if (this.block instanceof BlockPattern) { - this.simpleCache = parsedBlock2BlockPermutation(this.block.block); - } else if (this.block instanceof ChainPattern) { - this.simpleCache = parsedBlock2BlockPermutation((this.block.nodes[0]).block); + fillBlocks(dimension: Dimension, volume: BlockVolumeBase, mask?: Mask) { + const filter = mask?.getSimpleBlockFilter(); + if (this.isSimple()) { + if (!this.simpleCache) { + if (this.block instanceof BlockPattern) { + this.simpleCache = parsedBlock2BlockPermutation(this.block.block); + } else if (this.block instanceof ChainPattern) { + this.simpleCache = parsedBlock2BlockPermutation((this.block.nodes[0]).block); + } } - } - return dimension.fillBlocks(new BlockVolume(start, end), this.simpleCache, { blockFilter: mask?.getSimpleBlockFilter() }).getCapacity(); - } - - getSimpleBlockFill() { - if (this.block instanceof BlockPattern) { - return parsedBlock2BlockPermutation(this.block.block); - } else if (this.block instanceof ChainPattern && this.block.nodes.length == 1 && this.block.nodes[0] instanceof BlockPattern) { - return parsedBlock2BlockPermutation(this.block.nodes[0].block); + return dimension.fillBlocks(volume, this.simpleCache, { blockFilter: filter }).getCapacity(); + } else { + let count = 0; + volume = dimension.getBlocks(volume, filter); + for (const block of volume.getBlockLocationIterator()) { + count += this.setBlock(dimension.getBlock(block)) ? 1 : 0; + } + return count; } } diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index 013bf21c5..231f74f3e 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -1,15 +1,18 @@ -import { Block, Vector3 } from "@minecraft/server"; +import { Block, BlockVolume, BlockVolumeBase, ListBlockVolume, Vector3 } from "@minecraft/server"; import { assertCanBuildWithin } from "@modules/assert.js"; import { Mask } from "@modules/mask.js"; import { Pattern } from "@modules/pattern.js"; -import { iterateChunk, regionIterateBlocks, regionIterateChunks, regionVolume, Vector } from "@notbeer-api"; +import { regionIterateBlocks, regionIterateChunks, regionVolume, Vector } from "@notbeer-api"; import { PlayerSession } from "../sessions.js"; import { getWorldHeightLimits, snap } from "../util.js"; import { JobFunction, Jobs } from "@modules/jobs.js"; enum ChunkStatus { + /** Empty part of the shape. */ EMPTY, + /** Completely filled part of the shape. */ FULL, + /** Partially filled part of the shape. */ DETAIL, } @@ -27,6 +30,8 @@ export type shapeGenVars = { [k: string]: any; }; +// const shapeCache: Record = {}; + /** * A base shape class for generating blocks in a variety of formations. */ @@ -41,6 +46,8 @@ export abstract class Shape { protected abstract customHollow: boolean; + protected shapeCacheKey: string; + private genVars: shapeGenVars; /** @@ -83,10 +90,7 @@ export abstract class Shape { * @param relLocMin relative location of the chunks minimum block corner * @param relLocMax relative location of the chunks maximum block corner * @param genVars - * @returns - * - `ChunkStatus.FULL` will use fast fill operations when simple patterns and masks are used. - * - `ChunkStatus.EMPTY` will get ignored. - * - `ChunkStatus.DETAIL` will place blocks one at a time. + * @returns ChunkStatus */ // eslint-disable-next-line @typescript-eslint/no-unused-vars protected getChunkStatus(relLocMin: Vector, relLocMax: Vector, genVars: shapeGenVars) { @@ -171,91 +175,88 @@ export abstract class Shape { max.y = Math.min(maxY, max.y); const canGenerate = max.y >= min.y; pattern = pattern.withContext(session, [min, max]); + const simplePattern = pattern.isSimple(); if (!Jobs.inContext()) assertCanBuildWithin(player, min, max); let blocksAffected = 0; - const blocksAndChunks: (Block | [Vector3, Vector3])[] = []; + const volumes: (Block[] | BlockVolumeBase)[] = []; const history = options?.recordHistory ?? true ? session.getHistory() : undefined; const record = history?.record(this.usedInBrush); + + if (!canGenerate) { + history?.commit(record); + yield Jobs.nextStep("Calculating shape..."); + yield Jobs.nextStep("Generating blocks..."); + return 0; + } + try { let count = 0; - if (canGenerate) { - this.genVars = {}; - this.prepGeneration(this.genVars, options); - - // TODO: Localize - let activeMask = mask ?? new Mask(); - const globalMask = options?.ignoreGlobalMask ?? false ? new Mask() : session.globalMask; - activeMask = (!activeMask ? globalMask : globalMask ? activeMask.intersect(globalMask) : activeMask)?.withContext(session); - const simple = pattern.isSimple() && activeMask.isSimple(); - - let progress = 0; - const volume = regionVolume(min, max); - const inShapeFunc = this.customHollow ? "inShape" : "inShapeHollow"; - yield Jobs.nextStep("Calculating shape..."); - // Collect blocks and areas that will be changed. - for (const [chunkMin, chunkMax] of regionIterateChunks(min, max)) { - yield Jobs.setProgress(progress / volume); - - const chunkStatus = this.getChunkStatus(Vector.sub(chunkMin, loc).floor(), Vector.sub(chunkMax, loc).floor(), this.genVars); - if (chunkStatus === ChunkStatus.FULL && simple) { - const volume = regionVolume(chunkMin, chunkMax); - progress += volume; - blocksAffected += volume; - const prev = blocksAndChunks[blocksAndChunks.length - 1]; - if ( - Array.isArray(prev) && - regionVolume(...prev) + volume > 32768 && - prev[1].y + 1 === chunkMin.y && - prev[0].x === chunkMin.x && - prev[1].x === chunkMax.x && - prev[0].z === chunkMin.z && - prev[1].z === chunkMax.z - ) { - // Merge chunks in the same column - prev[1].y = chunkMax.y; - } else { - blocksAndChunks.push([chunkMin, chunkMax]); - } - } else if (chunkStatus === ChunkStatus.EMPTY) { - const volume = regionVolume(chunkMin, chunkMax); - progress += volume; - } else { - for (const blockLoc of regionIterateBlocks(chunkMin, chunkMax)) { - yield Jobs.setProgress(progress / volume); - progress++; - if (this[inShapeFunc](Vector.sub(blockLoc, loc).floor(), this.genVars)) { - const block = dimension.getBlock(blockLoc) ?? (yield* Jobs.loadBlock(blockLoc)); - if (!activeMask.empty() && !activeMask.matchesBlock(block)) continue; - blocksAndChunks.push(block); + this.genVars = {}; + this.prepGeneration(this.genVars, options); + + // TODO: Localize + let activeMask = mask ?? new Mask(); + const globalMask = options?.ignoreGlobalMask ?? false ? new Mask() : session.globalMask; + activeMask = (!activeMask ? globalMask : globalMask ? activeMask.intersect(globalMask) : activeMask)?.withContext(session); + const simpleMask = activeMask.isSimple(); + + let progress = 0; + const volume = regionVolume(min, max); + const inShapeFunc = this.customHollow ? "inShape" : "inShapeHollow"; + yield Jobs.nextStep("Calculating shape..."); + // Collect blocks and areas that will be changed. + for (const [chunkMin, chunkMax] of regionIterateChunks(min, max)) { + yield Jobs.setProgress(progress / volume); + + const chunkStatus = this.getChunkStatus(Vector.sub(chunkMin, loc).floor(), Vector.sub(chunkMax, loc).floor(), this.genVars); + if (chunkStatus === ChunkStatus.FULL && simpleMask) { + const volume = regionVolume(chunkMin, chunkMax); + progress += volume; + blocksAffected += volume; + volumes.push(new BlockVolume(chunkMin, chunkMax)); + } else if (chunkStatus === ChunkStatus.EMPTY) { + const volume = regionVolume(chunkMin, chunkMax); + progress += volume; + } else { + const blocks = []; + for (const blockLoc of regionIterateBlocks(chunkMin, chunkMax)) { + yield Jobs.setProgress(progress / volume); + progress++; + if (this[inShapeFunc](Vector.sub(blockLoc, loc).floor(), this.genVars)) { + const block = dimension.getBlock(blockLoc) ?? (yield* Jobs.loadBlock(blockLoc)); + if (simpleMask || activeMask.matchesBlock(block)) { + blocks.push(block); blocksAffected++; } - yield; } + yield; } + if (blocks.length) volumes.push(simplePattern ? new ListBlockVolume(blocks) : blocks); } + } - progress = 0; - yield Jobs.nextStep("Generating blocks..."); - if (history) yield* history.addUndoStructure(record, min, max); - for (let block of blocksAndChunks) { - if (block instanceof Block) { + progress = 0; + yield Jobs.nextStep("Generating blocks..."); + if (history) yield* history.addUndoStructure(record, min, max); + const maskInSimpleFill = simpleMask ? activeMask : undefined; + for (const volume of volumes) { + yield Jobs.setProgress(progress / blocksAffected); + if (Array.isArray(volume)) { + for (let block of volume) { if (!block.isValid() && Jobs.inContext()) block = yield* Jobs.loadBlock(loc); - if (pattern.setBlock(block)) count++; - if (iterateChunk()) yield Jobs.setProgress(progress / blocksAffected); + if ((!maskInSimpleFill || maskInSimpleFill.matchesBlock(block)) && pattern.setBlock(block)) count++; progress++; - } else { - const [min, max] = block; - const volume = regionVolume(min, max); - if (Jobs.inContext()) yield* Jobs.loadArea(min, max); - count += pattern.fillSimpleArea(dimension, min, max, activeMask); - yield Jobs.setProgress(progress / blocksAffected); - progress += volume; } + } else { + if (Jobs.inContext()) yield* Jobs.loadArea(volume.getMin(), volume.getMax()); + count += pattern.fillBlocks(dimension, volume, maskInSimpleFill); + progress += volume.getCapacity(); } - if (history) yield* history.addRedoStructure(record, min, max); } + if (history) yield* history.addRedoStructure(record, min, max); + history?.commit(record); return count; } catch (e) {