Skip to content

Commit

Permalink
Optimized simple patterns with non cuboid shapes
Browse files Browse the repository at this point in the history
  • Loading branch information
SIsilicon committed Dec 18, 2024
1 parent fbfd7b2 commit 25d5a3b
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 90 deletions.
5 changes: 3 additions & 2 deletions src/server/modules/history.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down
34 changes: 18 additions & 16 deletions src/server/modules/pattern.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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((<BlockPattern>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((<BlockPattern>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;
}
}

Expand Down
145 changes: 73 additions & 72 deletions src/server/shapes/base_shape.ts
Original file line number Diff line number Diff line change
@@ -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,
}

Expand All @@ -27,6 +30,8 @@ export type shapeGenVars = {
[k: string]: any;
};

// const shapeCache: Record<string, BlockVolumeBase[]> = {};

/**
* A base shape class for generating blocks in a variety of formations.
*/
Expand All @@ -41,6 +46,8 @@ export abstract class Shape {

protected abstract customHollow: boolean;

protected shapeCacheKey: string;

private genVars: shapeGenVars;

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 25d5a3b

Please sign in to comment.