diff --git a/src/library/Minecraft.ts b/src/library/Minecraft.ts index 2396ce741..23ada8b59 100644 --- a/src/library/Minecraft.ts +++ b/src/library/Minecraft.ts @@ -33,14 +33,12 @@ export * from "./utils/index.js"; import { Player as PlayerBuilder } from "./classes/playerBuilder.js"; import { Command } from "./classes/commandBuilder.js"; -import { Structure } from "./classes/structureBuilder.js"; import { ServerBuilder } from "./classes/serverBuilder.js"; import { UIForms } from "./classes/uiFormBuilder.js"; import { Block } from "./classes/blockBuilder.js"; export { CustomArgType, CommandPosition } from "./classes/commandBuilder.js"; export { commandSyntaxError, registerInformation as CommandInfo } from "./@types/classes/CommandBuilder"; -export { StructureSaveOptions, StructureLoadOptions } from "./classes/structureBuilder.js"; export { Databases } from "./classes/databaseBuilder.js"; export { configuration } from "./configurations.js"; @@ -49,7 +47,6 @@ class ServerBuild extends ServerBuilder { public player = PlayerBuilder; public command = Command; public uiForms = UIForms; - public structure = Structure; constructor() { super(); diff --git a/src/library/classes/structureBuilder.ts b/src/library/classes/structureBuilder.ts deleted file mode 100644 index 9ea729972..000000000 --- a/src/library/classes/structureBuilder.ts +++ /dev/null @@ -1,291 +0,0 @@ -/* eslint-disable no-empty */ -import { Matrix, regionLoaded, regionSize, regionTransformedBounds, sleep, Vector } from "../utils/index.js"; -import { Dimension, StructureMirrorAxis, StructureRotation, Vector3, world } from "@minecraft/server"; - -const ROT2STRUCT: { [key: number]: StructureRotation } = { - 0: StructureRotation.None, - 90: StructureRotation.Rotate90, - 180: StructureRotation.Rotate180, - 270: StructureRotation.Rotate270, -}; - -const FLIP2STRUCT = { - none: StructureMirrorAxis.None, - x: StructureMirrorAxis.X, - z: StructureMirrorAxis.Z, - xz: StructureMirrorAxis.XZ, -}; - -interface SubStructure { - name: string; - start: Vector; - end: Vector; -} - -interface StructureMeta { - subRegions?: SubStructure[]; - size: Vector; -} - -export interface StructureSaveOptions { - includeEntities?: boolean; - includeBlocks?: boolean; - saveToDisk?: boolean; -} - -export interface StructureLoadOptions { - rotation?: number; - flip?: "none" | "x" | "z" | "xz"; - importedSize?: Vector; -} - -class StructureManager { - private readonly MAX_SIZE: Vector = new Vector(64, 256, 64); - - private readonly structures = new Map(); - - save(name: string, start: Vector3, end: Vector3, dim: Dimension, options: StructureSaveOptions = {}) { - const min = Vector.min(start, end); - const max = Vector.max(start, end); - const size = Vector.from(regionSize(start, end)); - const saveOptions = { - includeEntities: options.includeEntities ?? false, - includeBlocks: options.includeBlocks ?? true, - }; - const saveToDisk = options.saveToDisk ?? false; - - if (this.beyondMaxSize(size)) { - let error = false; - const subStructs = this.getSubStructs(start, end); - const saved = []; - for (const sub of subStructs) { - try { - world.structureManager.delete(name + sub.name); - const struct = world.structureManager.createFromWorld(name + sub.name, dim, min.add(sub.start), min.add(sub.end), saveOptions); - saved.push(struct); - if (saveToDisk) struct.saveToWorld(); - } catch { - error = true; - break; - } - } - - if (error) { - saved.forEach((struct) => world.structureManager.delete(struct)); - return true; - } else { - this.structures.set(name, { subRegions: subStructs, size: size }); - return false; - } - } else { - try { - world.structureManager.delete(name); - const struct = world.structureManager.createFromWorld(name, dim, min, max, saveOptions); - if (saveToDisk) struct.saveToWorld(); - this.structures.set(name, { size }); - return false; - } catch { - return true; - } - } - } - - async saveWhileLoadingChunks(name: string, start: Vector3, end: Vector3, dim: Dimension, options: StructureSaveOptions = {}, loadArea: (min: Vector3, max: Vector3) => boolean) { - const min = Vector.min(start, end); - const max = Vector.max(start, end); - const size = Vector.from(regionSize(start, end)); - const saveOptions = { - includeEntities: options.includeEntities ?? false, - includeBlocks: options.includeBlocks ?? true, - }; - const saveToDisk = options.saveToDisk ?? false; - - if (this.beyondMaxSize(size)) { - let error = false; - const saved = []; - const subStructs = this.getSubStructs(start, end); - subs: for (const sub of subStructs) { - const subStart = min.add(sub.start); - const subEnd = min.add(sub.end); - const subName = name + sub.name; - world.structureManager.delete(subName); - // eslint-disable-next-line no-constant-condition - while (true) { - try { - const struct = world.structureManager.createFromWorld(subName, dim, min.add(sub.start), min.add(sub.end), saveOptions); - saved.push(struct); - if (saveToDisk) struct.saveToWorld(); - break; - } catch (err) { - if (loadArea(subStart, subEnd)) { - error = true; - break subs; - } - await sleep(1); - } - } - } - - if (error) { - saved.forEach((struct) => world.structureManager.delete(struct)); - return true; - } else { - this.structures.set(name, { subRegions: subStructs, size: size }); - return false; - } - } else { - world.structureManager.delete(name); - // eslint-disable-next-line no-constant-condition - while (true) { - try { - const struct = world.structureManager.createFromWorld(name, dim, min, max, saveOptions); - if (saveToDisk) struct.saveToWorld(); - this.structures.set(name, { size }); - return false; - } catch (err) { - if (loadArea(min, max)) return true; - await sleep(1); - } - } - } - } - - load(name: string, location: Vector3, dim: Dimension, options: StructureLoadOptions = {}) { - const loadPos = Vector.from(location); - let rot = options.rotation ?? 0; - rot = rot >= 0 ? rot % 360 : ((rot % 360) + 360) % 360; - const mirror = FLIP2STRUCT[options.flip ?? "none"]; - const loadOptions = { rotation: ROT2STRUCT[rot], mirror }; - - const struct = this.structures.get(name); - if (struct?.subRegions || this.beyondMaxSize(options.importedSize ?? Vector.ZERO)) { - const size = options.importedSize ?? struct.size; - const rotation = new Vector(0, options.rotation ?? 0, 0); - const dir_sc = Vector.ONE; - if (mirror.includes("X")) dir_sc.z *= -1; - if (mirror.includes("Z")) dir_sc.x *= -1; - - const transform = Matrix.fromRotationFlipOffset(rotation, dir_sc); - const bounds = regionTransformedBounds(Vector.ZERO, size.sub(1).floor(), transform); - let error = false; - const subStructs = options.importedSize ? this.getSubStructs(location, Vector.add(location, options.importedSize).floor()) : struct.subRegions; - for (const sub of subStructs) { - const subBounds = regionTransformedBounds(sub.start.floor(), sub.end.floor(), transform); - try { - world.structureManager.place(name + sub.name, dim, Vector.sub(subBounds[0], bounds[0]).add(loadPos), loadOptions); - } catch { - error = true; - break; - } - } - return error; - } else { - try { - world.structureManager.place(name, dim, loadPos, loadOptions); - return false; - } catch { - return true; - } - } - } - - async loadWhileLoadingChunks(name: string, location: Vector3, dim: Dimension, options: StructureLoadOptions = {}, loadArea: (min: Vector3, max: Vector3) => boolean) { - const loadPos = Vector.from(location); - let rot = options.rotation ?? 0; - rot = rot >= 0 ? rot % 360 : ((rot % 360) + 360) % 360; - const mirror = FLIP2STRUCT[options.flip ?? "none"]; - const loadOptions = { rotation: ROT2STRUCT[rot], mirror }; - - const struct = this.structures.get(name); - if (struct?.subRegions || this.beyondMaxSize(options.importedSize ?? Vector.ZERO)) { - const size = options.importedSize ?? struct.size; - const rotation = new Vector(0, options.rotation ?? 0, 0); - const flip = options.flip ?? "none"; - const dir_sc = Vector.ONE; - if (flip.includes("x")) dir_sc.z *= -1; - if (flip.includes("z")) dir_sc.x *= -1; - - const transform = Matrix.fromRotationFlipOffset(rotation, dir_sc); - const bounds = regionTransformedBounds(Vector.ZERO, size.sub(1).floor(), transform); - let error = false; - const subStructs = options.importedSize ? this.getSubStructs(location, Vector.add(location, options.importedSize).floor()) : struct.subRegions; - sub: for (const sub of subStructs) { - const subBounds = regionTransformedBounds(sub.start.floor(), sub.end.floor(), transform); - const subStart = Vector.sub(subBounds[0], bounds[0]).add(loadPos); - const subEnd = Vector.sub(subBounds[1], bounds[0]).add(loadPos); - while (!regionLoaded(subStart, subEnd, dim)) { - if (loadArea(subStart, subEnd)) { - error = true; - break sub; - } - await sleep(1); - } - try { - world.structureManager.place(name + sub.name, dim, subStart, loadOptions); - } catch { - error = true; - break; - } - } - return error; - } else { - while (!regionLoaded(loadPos, loadPos.add(struct.size).sub(1), dim)) { - if (loadArea(loadPos, loadPos.add(struct.size).sub(1))) return true; - await sleep(1); - } - try { - world.structureManager.place(name, dim, loadPos, loadOptions); - return false; - } catch { - return true; - } - } - } - - has(name: string) { - return this.structures.has(name); - } - - delete(name: string) { - const struct = this.structures.get(name); - if (struct) { - if (struct.subRegions) { - for (const sub of struct.subRegions) world.structureManager.delete(`${name}${sub.name}`); - } else { - world.structureManager.delete(name); - } - this.structures.delete(name); - return false; - } - return true; - } - - getSize(name: string) { - return this.structures.get(name).size.floor(); - } - - private beyondMaxSize(size: Vector3) { - return size.x > this.MAX_SIZE.x || size.y > this.MAX_SIZE.y || size.z > this.MAX_SIZE.z; - } - - private getSubStructs(start: Vector3, end: Vector3) { - const size = regionSize(start, end); - const subStructs: SubStructure[] = []; - for (let z = 0; z < size.z; z += this.MAX_SIZE.z) - for (let y = 0; y < size.y; y += this.MAX_SIZE.y) - for (let x = 0; x < size.x; x += this.MAX_SIZE.x) { - const subStart = new Vector(x, y, z); - const subEnd = Vector.min(subStart.add(this.MAX_SIZE).sub(1), size.add([-1, -1, -1])); - const subName = `_${x / this.MAX_SIZE.x}_${y / this.MAX_SIZE.y}_${z / this.MAX_SIZE.z}`; - - subStructs.push({ - name: subName, - start: subStart, - end: subEnd, - }); - } - return subStructs; - } -} - -export const Structure = new StructureManager(); diff --git a/src/library/utils/scheduling.ts b/src/library/utils/scheduling.ts index 4551720f8..37c707e62 100644 --- a/src/library/utils/scheduling.ts +++ b/src/library/utils/scheduling.ts @@ -67,7 +67,7 @@ system.runInterval(() => { }); function sleep(ticks: number) { - return new Promise((resolve) => setTickTimeout(resolve, ticks)); + return new Promise((resolve) => setTickTimeout(resolve, ticks)); } function shutdownTimers() { diff --git a/src/library/utils/vector.ts b/src/library/utils/vector.ts index e92c33fd8..64b847cf1 100644 --- a/src/library/utils/vector.ts +++ b/src/library/utils/vector.ts @@ -42,14 +42,22 @@ export class Vector { return new Vector(loc.x, loc.y, loc.z); } - static add(a: anyVec, b: anyVec) { + static add(a: anyVec, b: anyVec | number) { return Vector.from(a).add(b); } - static sub(a: anyVec, b: anyVec) { + static sub(a: anyVec, b: anyVec | number) { return Vector.from(a).sub(b); } + static mul(a: anyVec, b: anyVec | number) { + return Vector.from(a).mul(b); + } + + static div(a: anyVec, b: anyVec | number) { + return Vector.from(a).div(b); + } + static min(a: anyVec, b: anyVec) { return Vector.from(a).min(b); } diff --git a/src/server/brushes/structure_brush.ts b/src/server/brushes/structure_brush.ts index 2f9763b6a..e6f5dd512 100644 --- a/src/server/brushes/structure_brush.ts +++ b/src/server/brushes/structure_brush.ts @@ -87,9 +87,9 @@ export class StructureBrush extends Brush { console.warn(options.rotation, options.flip); } - yield history.addUndoStructure(record, start, end); + yield* history.addUndoStructure(record, start, end); yield* struct.load(center, session.getPlayer().dimension, options); - yield history.addRedoStructure(record, start, end); + yield* history.addRedoStructure(record, start, end); history.commit(record); } catch { history.cancel(record); diff --git a/src/server/commands/brush/size.ts b/src/server/commands/brush/size.ts index 82d769c70..ccd558ab2 100644 --- a/src/server/commands/brush/size.ts +++ b/src/server/commands/brush/size.ts @@ -40,7 +40,7 @@ registerCommand(registerInformation, function (session, builder, args) { assertClipboard(session); size = Vector.from(session.clipboard.getSize()); - blockCount = session.clipboard.getBlockCount(); + blockCount = session.clipboard.getVolume(); message.append("translate", "commands.wedit:size.offset").with(`${session.clipboardTransform.offset}\n`); } else { diff --git a/src/server/commands/clipboard/copy.ts b/src/server/commands/clipboard/copy.ts index 9bc3c1ac5..e0af7e9ee 100644 --- a/src/server/commands/clipboard/copy.ts +++ b/src/server/commands/clipboard/copy.ts @@ -27,16 +27,15 @@ const registerInformation = { }; /** - * Copies a region into a buffer (session's clipboard by default). When performed in a job, takes 1 step to execute. + * Copies a region into a buffer. When performed in a job, takes 1 step to execute. * @param session The session whose player is running this command * @param args The arguments that change how the copying will happen - * @param buffer An optional buffer to place the copy in. Leaving it blank copies to the clipboard instead + * @param toClipboard Whether the created buffer is set to the session's clipboard. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function* copy(session: PlayerSession, args: Map, buffer: RegionBuffer = null): Generator, boolean> { +export function* copy(session: PlayerSession, args: Map, toClipboard: boolean): Generator, RegionBuffer> { assertCuboidSelection(session); const player = session.getPlayer(); - const dimension = player.dimension; const [start, end] = session.selection.getRange(); const usingItem = args.get("_using_item"); @@ -44,9 +43,23 @@ export function* copy(session: PlayerSession, args: Map, buffer: Re const includeAir: boolean = usingItem ? session.includeAir : !args.has("a"); const mask = (usingItem ? session.globalMask.clone() : args.get("m-mask"))?.withContext(session); - if (!buffer) { + const airBlock = BlockPermutation.resolve("minecraft:air"); + const filter = mask || !includeAir; + + yield Jobs.nextStep("Copying blocks..."); + const blocks = (block: Block) => { + const isAir = block.isAir; + const willBeAir = isAir || (mask ? !mask.matchesBlock(block) : false); + if (includeAir && mask && !isAir && willBeAir) return airBlock; + else if (!includeAir && willBeAir) return false; + return true; + }; + + const buffer = yield* session.createRegion(start, end, { includeEntities, modifier: filter ? blocks : undefined }); + if (!buffer) return undefined; + + if (toClipboard) { if (session.clipboard) session.deleteRegion(session.clipboard); - session.clipboard = session.createRegion(true); session.clipboardTransform = { rotation: Vector.ZERO, flip: Vector.ONE, @@ -54,37 +67,16 @@ export function* copy(session: PlayerSession, args: Map, buffer: Re originalDim: player.dimension.id, offset: Vector.sub(start, Vector.from(player.location).floor().add(0.5)), }; - buffer = session.clipboard; + session.clipboard = buffer; } - let error = false; - - if (buffer.isAccurate) { - const airBlock = BlockPermutation.resolve("minecraft:air"); - const filter = mask || !includeAir; - - yield Jobs.nextStep("Copying blocks..."); - const blocks = (block: Block) => { - const isAir = block.isAir; - const willBeAir = isAir || (mask ? !mask.matchesBlock(block) : false); - if (includeAir && mask && !isAir && willBeAir) { - return airBlock; - } else if (!includeAir && willBeAir) { - return false; - } - return true; - }; - error = yield* buffer.save(start, end, dimension, { includeEntities }, filter ? blocks : "all"); - } else { - error = yield* buffer.save(start, end, dimension, { includeEntities }); - } - return error; + return buffer; } registerCommand(registerInformation, function* (session, builder, args) { assertCuboidSelection(session); - if (yield* Jobs.run(session, 1, copy(session, args))) { + if (!(yield* Jobs.run(session, 1, copy(session, args, true)))) { throw RawText.translate("commands.generic.wedit:commandFail"); } - return RawText.translate("commands.wedit:copy.explain").with(`${session.clipboard.getBlockCount()}`); + return RawText.translate("commands.wedit:copy.explain").with(`${session.clipboard.getVolume()}`); }); diff --git a/src/server/commands/clipboard/cut.ts b/src/server/commands/clipboard/cut.ts index dd628c7bd..26de60afe 100644 --- a/src/server/commands/clipboard/cut.ts +++ b/src/server/commands/clipboard/cut.ts @@ -35,21 +35,22 @@ const registerInformation = { }; /** - * Cuts a region into a buffer (session's clipboard by default). When performed in a job, takes 3 steps to execute. + * Cuts a region into a buffer. When performed in a job, takes 3 steps to execute. * @param session The session whose player is running this command * @param args The arguments that change how the cutting will happen * @param fill The pattern to fill after cutting the region out - * @param buffer An optional buffer to place the cut in. Leaving it blank cuts to the clipboard instead + * @param toClipboard Whether the created buffer is set to the session's clipboard. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function* cut(session: PlayerSession, args: Map, fill: Pattern = new Pattern("air"), buffer: RegionBuffer = null): Generator, boolean> { +export function* cut(session: PlayerSession, args: Map, fill: Pattern = new Pattern("air"), toClipboard: boolean): Generator, RegionBuffer> { const usingItem = args.get("_using_item"); const dim = session.getPlayer().dimension; const mask: Mask = usingItem ? session.globalMask : args.has("m") ? args.get("m-mask") : undefined; const includeEntities: boolean = usingItem ? session.includeEntities : args.has("e"); const [start, end] = session.selection.getRange(); - if (yield* copy(session, args, buffer)) return true; + let buffer: RegionBuffer; + if (!(buffer = yield* copy(session, args, toClipboard))) return undefined; yield* set(session, fill, mask, false); if (includeEntities) { @@ -64,6 +65,8 @@ export function* cut(session: PlayerSession, args: Map, fill: Patte Server.runCommand("execute @e[name=wedit:marked_for_deletion] ~~~ tp @s ~ -512 ~", dim); Server.runCommand("kill @e[name=wedit:marked_for_deletion]", dim); } + + return buffer; } registerCommand(registerInformation, function* (session, builder, args) { @@ -75,16 +78,16 @@ registerCommand(registerInformation, function* (session, builder, args) { yield* Jobs.run(session, 3, function* () { try { history.recordSelection(record, session); - yield history.addUndoStructure(record, start, end, "any"); - if (yield* cut(session, args, args.get("fill"))) { + yield* history.addUndoStructure(record, start, end, "any"); + if (!(yield* cut(session, args, args.get("fill"), true))) { throw RawText.translate("commands.generic.wedit:commandFail"); } - yield history.addRedoStructure(record, start, end, "any"); + yield* history.addRedoStructure(record, start, end, "any"); history.commit(record); } catch (e) { history.cancel(record); throw e; } }); - return RawText.translate("commands.wedit:cut.explain").with(`${session.clipboard.getBlockCount()}`); + return RawText.translate("commands.wedit:cut.explain").with(`${session.clipboard.getVolume()}`); }); diff --git a/src/server/commands/clipboard/paste.ts b/src/server/commands/clipboard/paste.ts index 677679c3f..c148409c8 100644 --- a/src/server/commands/clipboard/paste.ts +++ b/src/server/commands/clipboard/paste.ts @@ -47,10 +47,10 @@ registerCommand(registerInformation, function* (session, builder, args) { yield* Jobs.run(session, 1, function* () { try { if (pasteContent) { - yield history.addUndoStructure(record, pasteStart, pasteEnd, "any"); + yield* history.addUndoStructure(record, pasteStart, pasteEnd, "any"); yield Jobs.nextStep("Pasting blocks..."); yield* session.clipboard.load(pasteFrom, builder.dimension, { ...transform, mask: args.get("m-mask")?.withContext(session) }); - yield history.addRedoStructure(record, pasteStart, pasteEnd, "any"); + yield* history.addRedoStructure(record, pasteStart, pasteEnd, "any"); } if (setSelection) { history.recordSelection(record, session); @@ -66,7 +66,7 @@ registerCommand(registerInformation, function* (session, builder, args) { } }); if (pasteContent) { - return RawText.translate("commands.wedit:paste.explain").with(`${session.clipboard.getBlockCount()}`); + return RawText.translate("commands.wedit:paste.explain").with(`${session.clipboard.getVolume()}`); } return ""; }); diff --git a/src/server/commands/history/redo.ts b/src/server/commands/history/redo.ts index b9914f40c..25fe601e4 100644 --- a/src/server/commands/history/redo.ts +++ b/src/server/commands/history/redo.ts @@ -24,9 +24,7 @@ registerCommand(registerInformation, function* (session, builder, args) { yield* Jobs.run(session, 1, function* () { const times = args.get("times") as number; for (i = 0; i < times; i++) { - if (yield history.redo(session)) { - break; - } + if (yield* history.redo(session)) break; } }); return RawText.translate(i == 0 ? "commands.wedit:redo.none" : "commands.wedit:redo.explain").with(`${i}`); diff --git a/src/server/commands/history/undo.ts b/src/server/commands/history/undo.ts index ba77f981a..2313375ff 100644 --- a/src/server/commands/history/undo.ts +++ b/src/server/commands/history/undo.ts @@ -24,9 +24,7 @@ registerCommand(registerInformation, function* (session, builder, args) { yield* Jobs.run(session, 1, function* () { const times = args.get("times") as number; for (i = 0; i < times; i++) { - if (yield history.undo(session)) { - break; - } + if (yield* history.undo(session)) break; } }); return RawText.translate(i == 0 ? "commands.wedit:undo.none" : "commands.wedit:undo.explain").with(`${i}`); diff --git a/src/server/commands/region/flip.ts b/src/server/commands/region/flip.ts index 0b689fd1d..3a136c8bb 100644 --- a/src/server/commands/region/flip.ts +++ b/src/server/commands/region/flip.ts @@ -4,7 +4,6 @@ import { Cardinal } from "@modules/directions.js"; import { RawText, Vector } from "@notbeer-api"; import { transformSelection } from "./transform_func.js"; import { Jobs } from "@modules/jobs.js"; -import config from "config.js"; const registerInformation = { name: "flip", @@ -37,17 +36,10 @@ registerCommand(registerInformation, function* (session, builder, args) { let blockCount = 0; if (args.has("w")) { - if (dir.y != 0 && (config.performanceMode || session.performanceMode)) { - throw "commands.wedit:flip.notLateral"; - } yield* Jobs.run(session, 4, transformSelection(session, builder, args, { flip })); blockCount = session.selection.getBlockCount(); } else { assertClipboard(session); - if (dir.y != 0 && !session.clipboard.isAccurate) { - throw "commands.wedit:flip.notLateral"; - } - const clipTrans = session.clipboardTransform; // TODO: Get -o flag working for clipboard flips again @@ -60,7 +52,7 @@ registerCommand(registerInformation, function* (session, builder, args) { // } clipTrans.flip = clipTrans.flip.mul(flip); - blockCount = session.clipboard.getBlockCount(); + blockCount = session.clipboard.getVolume(); } return RawText.translate("commands.wedit:flip.explain").with(blockCount); diff --git a/src/server/commands/region/hollow.ts b/src/server/commands/region/hollow.ts index 312fafdfa..0d94c9a6f 100644 --- a/src/server/commands/region/hollow.ts +++ b/src/server/commands/region/hollow.ts @@ -26,7 +26,7 @@ const registerInformation = { ], }; -function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): Generator, number> { +function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): Generator, number> { const [min, max] = session.selection.getRange(); const dimension = session.getPlayer().dimension; const [minY, maxY] = getWorldHeightLimits(dimension); @@ -108,7 +108,7 @@ function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): G progress = 0; volume = locStringSet.size; yield Jobs.nextStep("Generating blocks..."); - yield history.addUndoStructure(record, min, max); + yield* history.addUndoStructure(record, min, max); for (const locString of locStringSet) { const block = dimension.getBlock(stringToLoc(locString)); if (mask.matchesBlock(block) && pattern.setBlock(block)) count++; @@ -116,7 +116,7 @@ function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): G progress++; } history.recordSelection(record, session); - yield history.addRedoStructure(record, min, max); + yield* history.addRedoStructure(record, min, max); } history.commit(record); diff --git a/src/server/commands/region/line.ts b/src/server/commands/region/line.ts index 6912b8391..8f73b3104 100644 --- a/src/server/commands/region/line.ts +++ b/src/server/commands/region/line.ts @@ -1,7 +1,7 @@ import { Vector3 } from "@minecraft/server"; import { assertCuboidSelection } from "@modules/assert.js"; import { Pattern } from "@modules/pattern.js"; -import { RawText, Vector, sleep } from "@notbeer-api"; +import { RawText, Vector } from "@notbeer-api"; import { registerCommand } from "../register_commands.js"; import { Jobs } from "@modules/jobs.js"; @@ -115,21 +115,15 @@ registerCommand(registerInformation, function* (session, builder, args) { const record = history.record(); try { const points = (yield* generateLine(Vector.from(pos1), Vector.from(pos2))).map((p) => p.floor()); - yield history.addUndoStructure(record, start, end); + yield* history.addUndoStructure(record, start, end); count = 0; for (const point of points) { - let block = dim.getBlock(point); - while (!block) { - block = Jobs.loadBlock(point); - yield sleep(1); - } - if (mask.matchesBlock(block) && pattern.setBlock(block)) { - count++; - } + const block = dim.getBlock(point) ?? (yield* Jobs.loadBlock(point)); + if (mask.matchesBlock(block) && pattern.setBlock(block)) count++; yield; } history.recordSelection(record, session); - yield history.addRedoStructure(record, start, end); + yield* history.addRedoStructure(record, start, end); history.commit(record); } catch (e) { history.cancel(record); diff --git a/src/server/commands/region/move.ts b/src/server/commands/region/move.ts index 6152ef562..2cc492aea 100644 --- a/src/server/commands/region/move.ts +++ b/src/server/commands/region/move.ts @@ -5,6 +5,7 @@ import { Pattern } from "@modules/pattern.js"; import { RawText } from "@notbeer-api"; import { Jobs } from "@modules/jobs.js"; import { cut } from "../clipboard/cut.js"; +import { RegionBuffer } from "@modules/region_buffer.js"; const registerInformation = { name: "move", @@ -55,22 +56,22 @@ registerCommand(registerInformation, function* (session, builder, args) { const history = session.getHistory(); const record = history.record(); - const temp = session.createRegion(true); let count: number; yield* Jobs.run(session, 4, function* () { + let temp: RegionBuffer; try { - yield history.addUndoStructure(record, start, end, "any"); - yield history.addUndoStructure(record, movedStart, movedEnd, "any"); - if (yield* cut(session, args, args.get("replace"), temp)) { + yield* history.addUndoStructure(record, start, end, "any"); + yield* history.addUndoStructure(record, movedStart, movedEnd, "any"); + if (!(temp = yield* cut(session, args, args.get("replace"), false))) { throw RawText.translate("commands.generic.wedit:commandFail"); } - count = temp.getBlockCount(); + count = temp.getVolume(); yield Jobs.nextStep("Pasting blocks..."); yield* temp.load(movedStart, dim); - yield history.addRedoStructure(record, start, end, "any"); - yield history.addRedoStructure(record, movedStart, movedEnd, "any"); + yield* history.addRedoStructure(record, start, end, "any"); + yield* history.addRedoStructure(record, movedStart, movedEnd, "any"); if (args.has("s")) { history.recordSelection(record, session); diff --git a/src/server/commands/region/revolve.ts b/src/server/commands/region/revolve.ts index 8d1231101..be998fa4e 100644 --- a/src/server/commands/region/revolve.ts +++ b/src/server/commands/region/revolve.ts @@ -78,18 +78,18 @@ registerCommand(registerInformation, function* (session, builder, args) { let count = 0; const history = session.getHistory(); const record = history.record(); - const tempRevolve = session.createRegion(true); yield* Jobs.run(session, loads.length + 1, function* () { + let tempRevolve: RegionBuffer; try { - yield* copy(session, args, tempRevolve); - yield history.addUndoStructure(record, ...revolveRegion, "any"); + tempRevolve = yield* copy(session, args, false); + yield* history.addUndoStructure(record, ...revolveRegion, "any"); for (const [loadPosition, rotation] of loads) { yield Jobs.nextStep("Pasting blocks..."); yield* tempRevolve.load(loadPosition, dim, { rotation, offset }); - count += tempRevolve.getBlockCount(); + count += tempRevolve.getVolume(); } - yield history.addRedoStructure(record, ...revolveRegion, "any"); + yield* history.addRedoStructure(record, ...revolveRegion, "any"); if (args.has("s")) { history.recordSelection(record, session); diff --git a/src/server/commands/region/rotate.ts b/src/server/commands/region/rotate.ts index fbb17a7c7..05aba974b 100644 --- a/src/server/commands/region/rotate.ts +++ b/src/server/commands/region/rotate.ts @@ -3,7 +3,6 @@ import { RawText, Vector } from "@notbeer-api"; import { assertClipboard } from "@modules/assert.js"; import { transformSelection } from "./transform_func.js"; import { Jobs } from "@modules/jobs.js"; -import config from "config.js"; const registerInformation = { name: "rotate", @@ -39,28 +38,19 @@ const registerInformation = { registerCommand(registerInformation, function* (session, builder, args) { let blockCount = 0; const rotation = new Vector(args.get("rotateX"), args.get("rotate"), args.get("rotateZ")); - function assertValidFastArgs() { - if ((Math.abs(rotation.y) / 90) % 1 != 0) { - throw RawText.translate("commands.wedit:rotate.notNinety").with(args.get("rotate")); - } else if (rotation.x || rotation.z) { - throw RawText.translate("commands.wedit:rotate.yOnly"); - } - } if (args.has("w")) { - if (config.performanceMode || session.performanceMode) assertValidFastArgs(); yield* Jobs.run(session, 4, transformSelection(session, builder, args, { rotation })); blockCount = session.selection.getBlockCount(); } else { assertClipboard(session); - if (!session.clipboard.isAccurate) assertValidFastArgs(); // TODO: Get -o flag working for clipboard rotations again // if (!args.has("o")) { // session.clipboardTransform.offset = session.clipboardTransform.offset.rotate(args.get("rotate"), "y"); // } session.clipboardTransform.rotation = session.clipboardTransform.rotation.add(rotation); - blockCount = session.clipboard.getBlockCount(); + blockCount = session.clipboard.getVolume(); } return RawText.translate("commands.wedit:rotate.explain").with(blockCount); diff --git a/src/server/commands/region/smooth_func.ts b/src/server/commands/region/smooth_func.ts index 79198d623..3821d7eeb 100644 --- a/src/server/commands/region/smooth_func.ts +++ b/src/server/commands/region/smooth_func.ts @@ -113,12 +113,12 @@ export function* smooth(session: PlayerSession, iter: number, shape: Shape, loc: let count = 0; const history = session.getHistory(); const record = history.record(); - const warpBuffer = new RegionBuffer(true); + let warpBuffer: RegionBuffer; try { - yield history.addUndoStructure(record, range[0], range[1], "any"); + yield* history.addUndoStructure(record, range[0], range[1], "any"); yield Jobs.nextStep("Calculating blocks..."); - yield* warpBuffer.create(range[0], range[1], (loc) => { + warpBuffer = yield* RegionBuffer.create(range[0], range[1], (loc) => { const canSmooth = (loc: Vector3) => { const global = Vector.add(loc, range[0]); return dim.getBlock(global).isAir || mask.matchesBlock(dim.getBlock(global)); @@ -128,23 +128,21 @@ export function* smooth(session: PlayerSession, iter: number, shape: Shape, loc: const heightDiff = getMap(map, loc.x, loc.z) - getMap(base, loc.x, loc.z); const sampleLoc = Vector.add(loc, [0, -heightDiff, 0]).round(); sampleLoc.y = Math.min(Math.max(sampleLoc.y, 0), warpBuffer.getSize().y - 1); - if (canSmooth(sampleLoc)) { - return dim.getBlock(sampleLoc.add(range[0])); - } + if (canSmooth(sampleLoc)) return dim.getBlock(sampleLoc.add(range[0])); } }); yield Jobs.nextStep("Placing blocks..."); yield* warpBuffer.load(range[0], dim); - count = warpBuffer.getBlockCount(); + count = warpBuffer.getVolume(); - yield history.addRedoStructure(record, range[0], range[1], "any"); + yield* history.addRedoStructure(record, range[0], range[1], "any"); history.commit(record); } catch (e) { history.cancel(record); throw e; } finally { - warpBuffer.deref(); + warpBuffer?.deref(); } return count; diff --git a/src/server/commands/region/stack.ts b/src/server/commands/region/stack.ts index 79156b2d9..aa9251aed 100644 --- a/src/server/commands/region/stack.ts +++ b/src/server/commands/region/stack.ts @@ -5,6 +5,7 @@ import { RawText, regionBounds, regionSize, regionVolume, Vector } from "@notbee import { registerCommand } from "../register_commands.js"; import { copy } from "../clipboard/copy.js"; import { Vector3 } from "@minecraft/server"; +import { RegionBuffer } from "@modules/region_buffer.js"; const registerInformation = { name: "stack", @@ -69,17 +70,17 @@ registerCommand(registerInformation, function* (session, builder, args) { const history = session.getHistory(); const record = history.record(); - const tempStack = session.createRegion(true); yield* Jobs.run(session, loads.length + 1, function* () { + let tempStack: RegionBuffer; try { - yield* copy(session, args, tempStack); - yield history.addUndoStructure(record, ...stackRegion, "any"); + tempStack = yield* copy(session, args, false); + yield* history.addUndoStructure(record, ...stackRegion, "any"); for (const load of loads) { yield Jobs.nextStep("Pasting blocks..."); yield* tempStack.load(load[0], dim); count += regionVolume(load[0], load[1]); } - yield history.addRedoStructure(record, ...stackRegion, "any"); + yield* history.addRedoStructure(record, ...stackRegion, "any"); if (args.has("s")) { history.recordSelection(record, session); diff --git a/src/server/commands/region/transform_func.ts b/src/server/commands/region/transform_func.ts index 293d5488b..7989cf699 100644 --- a/src/server/commands/region/transform_func.ts +++ b/src/server/commands/region/transform_func.ts @@ -1,6 +1,6 @@ import { assertCuboidSelection } from "@modules/assert.js"; import { Pattern } from "@modules/pattern.js"; -import { RegionLoadOptions } from "@modules/region_buffer.js"; +import { RegionBuffer, RegionLoadOptions } from "@modules/region_buffer.js"; import { Vector } from "@notbeer-api"; import { Player } from "@minecraft/server"; import { PlayerSession } from "../../sessions.js"; @@ -12,7 +12,7 @@ export function* transformSelection(session: PlayerSession, builder: Player, arg assertCuboidSelection(session); const history = session.getHistory(); const record = history.record(); - const temp = session.createRegion(true); + let temp: RegionBuffer; try { const [start, end] = session.selection.getRange(); const dim = builder.dimension; @@ -22,12 +22,12 @@ export function* transformSelection(session: PlayerSession, builder: Player, arg options = { offset: start.sub(origin), ...options }; options.mask = options.mask?.withContext(session); yield Jobs.nextStep("Gettings blocks..."); - yield* temp.save(start, end, dim); + temp = yield* session.createRegion(start, end); const [newStart, newEnd] = temp.getBounds(origin, options); - yield history.addUndoStructure(record, start, end, "any"); - yield history.addUndoStructure(record, newStart, newEnd, "any"); + yield* history.addUndoStructure(record, start, end, "any"); + yield* history.addUndoStructure(record, newStart, newEnd, "any"); yield* set(session, new Pattern("air"), null, false); yield Jobs.nextStep("Transforming blocks..."); @@ -40,8 +40,8 @@ export function* transformSelection(session: PlayerSession, builder: Player, arg history.recordSelection(record, session); } - yield history.addRedoStructure(record, newStart, newEnd, "any"); - yield history.addRedoStructure(record, start, end, "any"); + yield* history.addRedoStructure(record, newStart, newEnd, "any"); + yield* history.addRedoStructure(record, start, end, "any"); history.commit(record); } catch (e) { history.cancel(record); diff --git a/src/server/commands/selection/count.ts b/src/server/commands/selection/count.ts index 0683bab13..90d392cf5 100644 --- a/src/server/commands/selection/count.ts +++ b/src/server/commands/selection/count.ts @@ -1,7 +1,7 @@ import { assertSelection } from "@modules/assert.js"; import { Jobs } from "@modules/jobs.js"; import { Mask } from "@modules/mask.js"; -import { RawText, sleep } from "@notbeer-api"; +import { RawText } from "@notbeer-api"; import { registerCommand } from "../register_commands.js"; const registerInformation = { @@ -27,11 +27,7 @@ registerCommand(registerInformation, function* (session, builder, args) { let count = 0; yield Jobs.nextStep("Counting blocks..."); for (const loc of session.selection.getBlocks()) { - let block = dimension.getBlock(loc); - while (!block) { - block = Jobs.loadBlock(loc); - yield sleep(1); - } + const block = dimension.getBlock(loc) ?? (yield* Jobs.loadBlock(loc)); count += mask.matchesBlock(block) ? 1 : 0; yield Jobs.setProgress(++i / total); } diff --git a/src/server/commands/selection/distr.ts b/src/server/commands/selection/distr.ts index bb6997071..b6d7a25f8 100644 --- a/src/server/commands/selection/distr.ts +++ b/src/server/commands/selection/distr.ts @@ -1,6 +1,6 @@ import { assertClipboard, assertSelection } from "@modules/assert.js"; import { Jobs } from "@modules/jobs.js"; -import { RawText, sleep } from "@notbeer-api"; +import { RawText } from "@notbeer-api"; import { BlockPermutation } from "@minecraft/server"; import { registerCommand } from "../register_commands.js"; @@ -38,11 +38,11 @@ registerCommand(registerInformation, function* (session, builder, args) { if (args.has("c")) { assertClipboard(session); - total = session.clipboard.getBlockCount(); + total = session.clipboard.getVolume(); const clipboard = session.clipboard; for (const block of clipboard.getBlocks()) { - processBlock(block[0]); + processBlock(block.permutation); yield Jobs.setProgress(++i / total); } } else { @@ -52,11 +52,7 @@ registerCommand(registerInformation, function* (session, builder, args) { yield Jobs.nextStep("Analysing blocks..."); for (const loc of session.selection.getBlocks()) { - let block = dimension.getBlock(loc); - while (!block) { - block = Jobs.loadBlock(loc); - yield sleep(1); - } + const block = dimension.getBlock(loc) ?? (yield* Jobs.loadBlock(loc)); processBlock(block.permutation); yield Jobs.setProgress(++i / total); } diff --git a/src/server/commands/structure/export.ts b/src/server/commands/structure/export.ts index 48f74f477..28a516905 100644 --- a/src/server/commands/structure/export.ts +++ b/src/server/commands/structure/export.ts @@ -1,9 +1,10 @@ import { assertCanBuildWithin, assertCuboidSelection } from "@modules/assert.js"; import { PlayerUtil } from "@modules/player_util.js"; -import { RawText, regionCenter, regionIterateChunks, regionSize, Server, sleep, Vector } from "@notbeer-api"; -import { BlockPermutation, BlockVolume, Player, world } from "@minecraft/server"; +import { RawText, regionIterateChunks, regionLoaded, regionSize, Server, Vector } from "@notbeer-api"; +import { BlockPermutation, BlockVolume, Player, StructureSaveMode, world } from "@minecraft/server"; import { registerCommand } from "../register_commands.js"; import { Jobs } from "@modules/jobs.js"; +import { RegionBuffer } from "@modules/region_buffer.js"; const registerInformation = { name: "export", @@ -23,12 +24,8 @@ const registerInformation = { ], }; -let tempID = 0; - function writeMetaData(name: string, data: string, player: Player) { - if (!name.includes(":")) { - name = "mystructure:" + name; - } + if (!name.includes(":")) name = "mystructure:" + name; const dimension = player.dimension; let blockLoc = PlayerUtil.getBlockLocation(player); @@ -38,13 +35,14 @@ function writeMetaData(name: string, data: string, player: Player) { const entity = dimension.spawnEntity("wedit:struct_meta", blockLoc); entity.nameTag = data; - const error = Server.structure.save(name, blockLoc, blockLoc, dimension, { - saveToDisk: true, + console.warn("saving", name); + const structure = world.structureManager.createFromWorld(name, dimension, blockLoc, blockLoc, { includeBlocks: false, includeEntities: true, + saveMode: StructureSaveMode.World, }); entity.triggerEvent("wedit:despawn"); - return error; + return structure; } const users: Player[] = []; @@ -61,9 +59,9 @@ registerCommand(registerInformation, function* (session, builder, args) { } const [namespace, struct] = struct_name.split(":") as [string, string]; - const tempStruct = `wedit:temp_export${tempID++}`; + let tempStruct: RegionBuffer; yield* Jobs.run(session, 1, function* () { - if (excludeAir) Server.structure.save(tempStruct, ...range, dimension); + if (excludeAir) tempStruct = yield* RegionBuffer.createFromWorld(...range, dimension); try { world.scoreboard.getObjective("wedit:exports") ?? world.scoreboard.addObjective("wedit:exports", ""); @@ -75,7 +73,7 @@ registerCommand(registerInformation, function* (session, builder, args) { const size = Vector.sub(range[1], range[0]).add(1); const structVoid = BlockPermutation.resolve("minecraft:structure_void"); for (const [subStart, subEnd] of regionIterateChunks(...range)) { - while (!Jobs.loadBlock(regionCenter(subStart, subEnd))) yield sleep(1); + if (!regionLoaded(subStart, subEnd, dimension)) yield* Jobs.loadArea(subStart, subEnd); dimension.fillBlocks(new BlockVolume(subStart.floor(), subEnd.floor()), structVoid, { blockFilter: { includeTypes: ["air"] } }); const subSize = subEnd.sub(subStart).add(1); count += subSize.x * subSize.y * subSize.z; @@ -83,56 +81,37 @@ registerCommand(registerInformation, function* (session, builder, args) { } } - const jobCtx = Jobs.getContext(); if ( - yield Server.structure.saveWhileLoadingChunks( - namespace + ":weditstructexport_" + struct, - ...range, - dimension, - { - saveToDisk: true, - includeEntities: args.has("e"), - }, - (min, max) => { - if (Jobs.isContextValid(jobCtx)) { - Jobs.loadBlock(regionCenter(min, max)); - return false; - } - return true; - } - ) + !(yield* RegionBuffer.createFromWorld(...range, dimension, { + saveAs: namespace + ":weditstructexport_" + struct, + includeEntities: args.has("e"), + })) ) throw "Failed to save structure"; const size = regionSize(...range); const playerPos = PlayerUtil.getBlockLocation(builder).add(0.5); const relative = Vector.sub(range[0], playerPos); + const data = { + size: { x: size.x, y: size.y, z: size.z }, + relative: { x: relative.x, y: relative.y, z: relative.z }, + exporter: builder.name, + }; - if ( - writeMetaData( - namespace + ":weditstructmeta_" + struct, - JSON.stringify({ - size: { x: size.x, y: size.y, z: size.z }, - relative: { x: relative.x, y: relative.y, z: relative.z }, - exporter: builder.name, - }), - builder - ) - ) - throw "Failed to save metadata"; - if (writeMetaData("weditstructref_" + struct, struct_name, builder)) throw "Failed to save reference data"; + if (!writeMetaData(namespace + ":weditstructmeta_" + struct, JSON.stringify(data), builder)) throw "Failed to save metadata"; + if (!writeMetaData("weditstructref_" + struct, struct_name, builder)) throw "Failed to save reference data"; } catch (e) { const [namespace, name] = struct_name.split(":") as [string, string]; - Server.structure.delete(namespace + ":weditstructexport_" + name); - Server.structure.delete(namespace + ":weditstructmeta_" + name); - Server.structure.delete("weditstructref_" + name); + world.structureManager.delete(namespace + ":weditstructexport_" + name); + world.structureManager.delete(namespace + ":weditstructmeta_" + name); + world.structureManager.delete("weditstructref_" + name); Server.runCommand(`scoreboard players reset ${struct_name} wedit:exports`); console.error(e); throw "commands.generic.wedit:commandFail"; } finally { - if (excludeAir) { - Server.structure.load(tempStruct, range[0], dimension); - Server.structure.delete(tempStruct); + if (tempStruct) { + tempStruct.load(range[0], dimension); + tempStruct.deref(); } } }); diff --git a/src/server/commands/structure/import.ts b/src/server/commands/structure/import.ts index 3223299ef..e5dfa31cb 100644 --- a/src/server/commands/structure/import.ts +++ b/src/server/commands/structure/import.ts @@ -1,7 +1,7 @@ import { PlayerUtil } from "@modules/player_util.js"; import { RegionBuffer } from "@modules/region_buffer.js"; -import { RawText, Server, Vector } from "@notbeer-api"; -import { Player } from "@minecraft/server"; +import { RawText, Vector } from "@notbeer-api"; +import { Player, world } from "@minecraft/server"; import { registerCommand } from "../register_commands.js"; const registerInformation = { @@ -27,7 +27,7 @@ function readMetaData(name: string, player: Player) { const entity = dimension.spawnEntity("wedit:struct_meta", blockLoc); entity.nameTag = "__placeholder__"; - Server.structure.load(name, blockLoc, player.dimension); + world.structureManager.place(name, player.dimension, blockLoc); let data: string; const imported = dimension.getEntitiesAtBlockLocation(blockLoc).find((entity) => entity.typeId == "wedit:struct_meta" && entity.nameTag != "__placeholder__"); if (imported) { @@ -52,8 +52,7 @@ export function importStructure(name: string, player: Player) { throw "commands.generic.wedit:commandFail"; } - const buffer = new RegionBuffer(false); - buffer.import(namespace + ":weditstructexport_" + struct, Vector.from(metadata.size).floor()); + const buffer = RegionBuffer.get(namespace + ":weditstructexport_" + struct); return { buffer, metadata }; } diff --git a/src/server/commands/utilities/drain.ts b/src/server/commands/utilities/drain.ts index 82ab6e0ab..fd6d5ca8e 100644 --- a/src/server/commands/utilities/drain.ts +++ b/src/server/commands/utilities/drain.ts @@ -1,5 +1,5 @@ import { Jobs } from "@modules/jobs.js"; -import { RawText, regionBounds, sleep, Vector } from "@notbeer-api"; +import { RawText, regionBounds, Vector } from "@notbeer-api"; import { BlockPermutation } from "@minecraft/server"; import { registerCommand } from "../register_commands.js"; import { floodFill } from "./floodfill_func.js"; @@ -77,19 +77,15 @@ registerCommand(registerInformation, function* (session, builder, args) { const record = history.record(); const air = BlockPermutation.resolve("minecraft:air"); try { - yield history.addUndoStructure(record, min, max, blocks); + yield* history.addUndoStructure(record, min, max, blocks); let i = 0; for (const loc of blocks) { - let block = dimension.getBlock(loc); - while (!(block || (block = Jobs.loadBlock(loc)))) yield sleep(1); - if (drainWaterLogged && !block.typeId.match(fluidMatch)) { - block.setWaterlogged(false); - } else { - block.setPermutation(air); - } + const block = dimension.getBlock(loc) ?? (yield* Jobs.loadBlock(loc)); + if (drainWaterLogged && !block.typeId.match(fluidMatch)) block.setWaterlogged(false); + else block.setPermutation(air); yield Jobs.setProgress(i++ / blocks.length); } - yield history.addRedoStructure(record, min, max, blocks); + yield* history.addRedoStructure(record, min, max, blocks); history.commit(record); } catch (err) { history.cancel(record); diff --git a/src/server/commands/utilities/fill.ts b/src/server/commands/utilities/fill.ts index 56a93439c..632b5ecb9 100644 --- a/src/server/commands/utilities/fill.ts +++ b/src/server/commands/utilities/fill.ts @@ -1,7 +1,7 @@ import { Cardinal } from "@modules/directions.js"; import { Jobs } from "@modules/jobs.js"; import { Pattern } from "@modules/pattern.js"; -import { RawText, regionBounds, sleep } from "@notbeer-api"; +import { RawText, regionBounds } from "@notbeer-api"; import { registerCommand } from "../register_commands.js"; import { floodFill, FloodFillContext } from "./floodfill_func.js"; @@ -62,15 +62,13 @@ registerCommand(registerInformation, function* (session, builder, args) { const history = session.getHistory(); const record = history.record(); try { - yield history.addUndoStructure(record, min, max, blocks); + yield* history.addUndoStructure(record, min, max, blocks); let i = 0; for (const loc of blocks) { - let block = dimension.getBlock(loc); - while (!(block || (block = Jobs.loadBlock(loc)))) yield sleep(1); - pattern.setBlock(block); + pattern.setBlock(dimension.getBlock(loc) ?? (yield* Jobs.loadBlock(loc))); yield Jobs.setProgress(i++ / blocks.length); } - yield history.addRedoStructure(record, min, max, blocks); + yield* history.addRedoStructure(record, min, max, blocks); history.commit(record); } catch (err) { history.cancel(record); diff --git a/src/server/commands/utilities/fillr.ts b/src/server/commands/utilities/fillr.ts index 4a41ec42f..48996c919 100644 --- a/src/server/commands/utilities/fillr.ts +++ b/src/server/commands/utilities/fillr.ts @@ -1,7 +1,7 @@ import { Cardinal } from "@modules/directions.js"; import { Jobs } from "@modules/jobs.js"; import { Pattern } from "@modules/pattern.js"; -import { RawText, regionBounds, sleep } from "@notbeer-api"; +import { RawText, regionBounds } from "@notbeer-api"; import { registerCommand } from "../register_commands.js"; import { floodFill } from "./floodfill_func.js"; @@ -56,15 +56,13 @@ registerCommand(registerInformation, function* (session, builder, args) { const history = session.getHistory(); const record = history.record(); try { - yield history.addUndoStructure(record, min, max, blocks); + yield* history.addUndoStructure(record, min, max, blocks); let i = 0; for (const loc of blocks) { - let block = dimension.getBlock(loc); - while (!(block || (block = Jobs.loadBlock(loc)))) yield sleep(1); - pattern.setBlock(block); + pattern.setBlock(dimension.getBlock(loc) ?? (yield* Jobs.loadBlock(loc))); yield Jobs.setProgress(i++ / blocks.length); } - yield history.addRedoStructure(record, min, max, blocks); + yield* history.addRedoStructure(record, min, max, blocks); history.commit(record); } catch (err) { history.cancel(record); diff --git a/src/server/commands/utilities/fixlava.ts b/src/server/commands/utilities/fixlava.ts index 11f95aa39..5e5cf4ae4 100644 --- a/src/server/commands/utilities/fixlava.ts +++ b/src/server/commands/utilities/fixlava.ts @@ -1,5 +1,5 @@ import { Jobs } from "@modules/jobs.js"; -import { RawText, regionBounds, sleep, Vector } from "@notbeer-api"; +import { RawText, regionBounds, Vector } from "@notbeer-api"; import { BlockPermutation } from "@minecraft/server"; import { registerCommand } from "../register_commands.js"; import { fluidLookPositions, lavaMatch } from "./drain.js"; @@ -50,15 +50,13 @@ registerCommand(registerInformation, function* (session, builder, args) { const record = history.record(); const lava = BlockPermutation.resolve("minecraft:lava"); try { - yield history.addUndoStructure(record, min, max, blocks); + yield* history.addUndoStructure(record, min, max, blocks); let i = 0; for (const loc of blocks) { - let block = dimension.getBlock(loc); - while (!(block || (block = Jobs.loadBlock(loc)))) yield sleep(1); - block.setPermutation(lava); + dimension.getBlock(loc) ?? (yield* Jobs.loadBlock(loc)).setPermutation(lava); yield Jobs.setProgress(i++ / blocks.length); } - yield history.addRedoStructure(record, min, max, blocks); + yield* history.addRedoStructure(record, min, max, blocks); history.commit(record); } catch (err) { history.cancel(record); diff --git a/src/server/commands/utilities/fixwater.ts b/src/server/commands/utilities/fixwater.ts index b9ca4065b..70e8c61ab 100644 --- a/src/server/commands/utilities/fixwater.ts +++ b/src/server/commands/utilities/fixwater.ts @@ -1,5 +1,5 @@ import { Jobs } from "@modules/jobs.js"; -import { RawText, regionBounds, sleep, Vector } from "@notbeer-api"; +import { RawText, regionBounds, Vector } from "@notbeer-api"; import { BlockPermutation } from "@minecraft/server"; import { registerCommand } from "../register_commands.js"; import { fluidLookPositions, waterMatch } from "./drain.js"; @@ -50,15 +50,13 @@ registerCommand(registerInformation, function* (session, builder, args) { const record = history.record(); const water = BlockPermutation.resolve("minecraft:water"); try { - yield history.addUndoStructure(record, min, max, blocks); + yield* history.addUndoStructure(record, min, max, blocks); let i = 0; for (const loc of blocks) { - let block = dimension.getBlock(loc); - while (!(block || (block = Jobs.loadBlock(loc)))) yield sleep(1); - block.setPermutation(water); + dimension.getBlock(loc) ?? (yield* Jobs.loadBlock(loc)).setPermutation(water); yield Jobs.setProgress(i++ / blocks.length); } - yield history.addRedoStructure(record, min, max, blocks); + yield* history.addRedoStructure(record, min, max, blocks); history.commit(record); } catch (err) { history.cancel(record); diff --git a/src/server/commands/utilities/snow.ts b/src/server/commands/utilities/snow.ts index f88076496..7eb70b693 100644 --- a/src/server/commands/utilities/snow.ts +++ b/src/server/commands/utilities/snow.ts @@ -1,5 +1,5 @@ import { Jobs } from "@modules/jobs.js"; -import { RawText, Vector, sleep } from "@notbeer-api"; +import { RawText, Vector } from "@notbeer-api"; import { Block, Vector3, BlockPermutation } from "@minecraft/server"; import { getWorldHeightLimits } from "../../util.js"; import { CylinderShape } from "../../shapes/cylinder.js"; @@ -112,12 +112,12 @@ registerCommand(registerInformation, function* (session, builder, args) { const record = history.record(); try { - yield history.addUndoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); + yield* history.addUndoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); const snowLayer = BlockPermutation.resolve("minecraft:snow_layer"); const ice = BlockPermutation.resolve("minecraft:ice"); for (let block of blocks) { const loc = block.location; - while (!(block?.isValid() || (block = Jobs.loadBlock(loc)))) yield sleep(1); + if (!block?.isValid()) block = yield* Jobs.loadBlock(loc); if (block.typeId.match(waterMatch)) { block.setPermutation(ice); @@ -140,7 +140,7 @@ registerCommand(registerInformation, function* (session, builder, args) { yield Jobs.setProgress(i++ / blocks.length); yield; } - yield history.addRedoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); + yield* history.addRedoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); history.commit(record); } catch (err) { history.cancel(record); diff --git a/src/server/commands/utilities/thaw.ts b/src/server/commands/utilities/thaw.ts index 2a056272a..d4765c2da 100644 --- a/src/server/commands/utilities/thaw.ts +++ b/src/server/commands/utilities/thaw.ts @@ -1,5 +1,5 @@ import { Jobs } from "@modules/jobs.js"; -import { RawText, Vector, sleep } from "@notbeer-api"; +import { RawText, Vector } from "@notbeer-api"; import { Block, Vector3, BlockPermutation } from "@minecraft/server"; import { getWorldHeightLimits } from "../../util.js"; import { CylinderShape } from "../../shapes/cylinder.js"; @@ -89,12 +89,12 @@ registerCommand(registerInformation, function* (session, builder, args) { const history = session.getHistory(); const record = history.record(); try { - yield history.addUndoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); + yield* history.addUndoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); const air = BlockPermutation.resolve("minecraft:air"); const water = BlockPermutation.resolve("minecraft:water"); for (let block of blocks) { const loc = block.location; - while (!(block?.isValid() || (block = Jobs.loadBlock(loc)))) yield sleep(1); + if (!block?.isValid()) block = yield* Jobs.loadBlock(loc); if (block.typeId == "minecraft:ice") { block.setPermutation(water); @@ -105,7 +105,7 @@ registerCommand(registerInformation, function* (session, builder, args) { } yield Jobs.setProgress(i++ / blocks.length); } - yield history.addRedoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); + yield* history.addRedoStructure(record, affectedBlockRange[0], affectedBlockRange[1], blockLocs); history.commit(record); } catch (err) { history.cancel(record); diff --git a/src/server/modules/history.ts b/src/server/modules/history.ts index 5aeb6ca5e..6308efad6 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, Server, regionSize, regionCenter, Thread, getCurrentThread } from "@notbeer-api"; +import { Vector, regionVolume, regionSize, Thread, getCurrentThread } from "@notbeer-api"; import { UnloadedChunksError } from "./assert.js"; import { canPlaceBlock } from "../util.js"; import { PlayerSession } from "../sessions.js"; @@ -7,9 +7,10 @@ import { selectMode } from "./selection.js"; import { BlockUnit } from "./block_parsing.js"; import config from "config.js"; import { Jobs } from "./jobs.js"; +import { RegionBuffer } from "./region_buffer.js"; type historyEntry = { - name: string; + buffer: RegionBuffer; dimension: Dimension; location: Vector3; size: Vector3; @@ -33,7 +34,6 @@ type historyPoint = { const air = BlockPermutation.resolve("minecraft:air"); -let historyId = 0; let historyPointId = 0; export class History { @@ -95,39 +95,37 @@ export class History { const point = this.historyPoints.get(historyPoint); this.historyPoints.delete(historyPoint); - for (const struct of point.undo) Server.structure.delete(struct.name); - for (const struct of point.redo) Server.structure.delete(struct.name); + for (const struct of point.undo) struct.buffer.deref(); + for (const struct of point.redo) struct.buffer.deref(); } collectBlockChanges(historyPoint: number) { return this.historyPoints.get(historyPoint)?.blockChange; } - async addUndoStructure(historyPoint: number, start: Vector3, end: Vector3, blocks: Vector3[] | "any" = "any") { + *addUndoStructure(historyPoint: number, start: Vector3, end: Vector3, blocks: Vector3[] | "any" = "any") { // contentLog.debug("adding undo structure"); const point = this.historyPoints.get(historyPoint); point.blocksChanged += blocks == "any" ? regionVolume(start, end) : blocks.length; // We test the change limit here, - if (point.blocksChanged > this.session.changeLimit) { - throw "commands.generic.wedit:blockLimit"; - } + if (point.blocksChanged > this.session.changeLimit) throw "commands.generic.wedit:blockLimit"; - const structName = await this.processRegion(historyPoint, start, end, blocks); + const buffer = yield* this.processRegion(historyPoint, start, end, blocks); point.undo.push({ - name: structName, + buffer, dimension: this.session.getPlayer().dimension, location: Vector.min(start, end).floor(), size: regionSize(start, end), }); } - async addRedoStructure(historyPoint: number, start: Vector3, end: Vector3, blocks: Vector3[] | "any" = "any") { + *addRedoStructure(historyPoint: number, start: Vector3, end: Vector3, blocks: Vector3[] | "any" = "any") { const point = this.historyPoints.get(historyPoint); this.assertRecording(); - const structName = await this.processRegion(historyPoint, start, end, blocks); + const buffer = yield* this.processRegion(historyPoint, start, end, blocks); point.redo.push({ - name: structName, + buffer, dimension: this.session.getPlayer().dimension, location: Vector.min(start, end).floor(), size: regionSize(start, end), @@ -154,23 +152,14 @@ export class History { } } - async undo(session: PlayerSession) { + *undo(session: PlayerSession) { this.assertNotRecording(); - if (this.historyIdx <= -1) { - return true; - } + if (this.historyIdx <= -1) return true; const player = this.session.getPlayer(); const dim = player.dimension; - const jobCtx = Jobs.getContext(); for (const region of this.undoStructures[this.historyIdx]) { - await Server.structure.loadWhileLoadingChunks(region.name, region.location, dim, {}, (min, max) => { - if (Jobs.isContextValid(jobCtx)) { - Jobs.loadBlock(regionCenter(min, max), jobCtx); - return false; - } - return true; - }); + yield* region.buffer.load(region.location, dim); } let selection: selectionEntry; @@ -190,24 +179,15 @@ export class History { return false; } - async redo(session: PlayerSession) { + *redo(session: PlayerSession) { this.assertNotRecording(); - if (this.historyIdx >= this.redoStructures.length - 1) { - return true; - } + if (this.historyIdx >= this.redoStructures.length - 1) return true; const player = this.session.getPlayer(); const dim = player.dimension; - const jobCtx = Jobs.getContext(); this.historyIdx++; for (const region of this.redoStructures[this.historyIdx]) { - await Server.structure.loadWhileLoadingChunks(region.name, region.location, dim, {}, (min, max) => { - if (Jobs.isContextValid(jobCtx)) { - Jobs.loadBlock(regionCenter(min, max), jobCtx); - return false; - } - return true; - }); + yield* region.buffer.load(region.location, dim); } let selection: selectionEntry; @@ -255,22 +235,18 @@ export class History { private deleteHistoryRegions(index: number) { try { - for (const struct of this.undoStructures[index]) { - Server.structure.delete(struct.name); - } - for (const struct of this.redoStructures[index]) { - Server.structure.delete(struct.name); - } + for (const struct of this.undoStructures[index]) struct.buffer.deref(); + for (const struct of this.redoStructures[index]) struct.buffer.deref(); } catch { /* pass */ } } // eslint-disable-next-line @typescript-eslint/no-unused-vars - private async processRegion(historyPoint: number, start: Vector3, end: Vector3, blocks: Vector3[] | "any") { - let structName: string; + private *processRegion(historyPoint: number, start: Vector3, end: Vector3, blocks: Vector3[] | "any") { const player = this.session.getPlayer(); const dim = player.dimension; + let buffer: RegionBuffer; try { const jobCtx = Jobs.getContext(); @@ -278,24 +254,13 @@ export class History { throw new UnloadedChunksError("worldedit.error.saveHistory"); } - structName = "wedit:history_" + (historyId++).toString(16); - if ( - await Server.structure.saveWhileLoadingChunks(structName, start, end, dim, {}, (min, max) => { - if (Jobs.isContextValid(jobCtx)) { - Jobs.loadBlock(regionCenter(min, max), jobCtx); - return false; - } - return true; - }) - ) { - this.cancel(historyPoint); - throw new UnloadedChunksError("worldedit.error.saveHistory"); - } + buffer = yield* RegionBuffer.createFromWorld(start, end, dim); + if (!buffer) throw new UnloadedChunksError("worldedit.error.saveHistory"); } catch (err) { this.cancel(historyPoint); throw err; } - return structName; + return buffer!; } private assertRecording() { diff --git a/src/server/modules/jobs.ts b/src/server/modules/jobs.ts index bc715b513..efd23ac3b 100644 --- a/src/server/modules/jobs.ts +++ b/src/server/modules/jobs.ts @@ -1,4 +1,4 @@ -import { Server, RawText, removeTickingArea, setTickingAreaCircle, Thread, getCurrentThread } from "@notbeer-api"; +import { Server, RawText, removeTickingArea, setTickingAreaCircle, Thread, getCurrentThread, regionCenter, sleep } from "@notbeer-api"; import { Player, Dimension, Vector3, Block } from "@minecraft/server"; import { PlayerSession, getSession } from "server/sessions"; import { UnloadedChunksError } from "./assert"; @@ -88,20 +88,26 @@ class JobHandler { if (this.current) return { jobFunc: "setProgress", data: percent }; } - public loadBlock(loc: Vector3, ctx?: JobContext): Block | undefined { - const job = this.jobs.get(ctx ?? this.current); - const block = job?.dimension.getBlock(loc); - if ((ctx || !block) && job) { + public *loadBlock(loc: Vector3, ctx: JobContext = this.current): Generator, Block | undefined> { + const job = this.jobs.get(ctx); + while (true) { + if (!Jobs.isContextValid(ctx)) return undefined; + const block = job.dimension.getBlock(loc); + if (block) return block; + if (job.tickingAreaSlot === undefined) { if (!job.tickingAreaRequestTime) job.tickingAreaRequestTime = Date.now(); - return; + yield sleep(1); + continue; } - if (!setTickingAreaCircle(loc, 4, job.dimension, "wedit:ticking_area_" + job.tickingAreaSlot)) { - throw new UnloadedChunksError("worldedit.error.tickArea"); - } + if (setTickingAreaCircle(loc, 4, job.dimension, "wedit:ticking_area_" + job.tickingAreaSlot)) yield sleep(1); + else throw new UnloadedChunksError("worldedit.error.tickArea"); } - return block; + } + + public *loadArea(start: Vector3, end: Vector3, ctx?: JobContext) { + return !!(yield* this.loadBlock(regionCenter(start, end), ctx)); } public inContext(): boolean { diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts index cd1eaa7b4..973601ad2 100644 --- a/src/server/modules/pattern.ts +++ b/src/server/modules/pattern.ts @@ -444,12 +444,10 @@ class ClipboardPattern extends PatternNode { getPermutation(block: BlockUnit, context: patternContext) { const clipboard = context.session?.clipboard; - if (clipboard?.isAccurate) { - const size = clipboard.getSize(); - const offset = Vector.sub(block.location, this.offset); - const sampledLoc = new Vector(wrap(size.x, offset.x), wrap(size.y, offset.y), wrap(size.z, offset.z)); - return clipboard.getBlock(sampledLoc); - } + const size = clipboard.getSize(); + const offset = Vector.sub(block.location, this.offset); + const sampledLoc = new Vector(wrap(size.x, offset.x), wrap(size.y, offset.y), wrap(size.z, offset.z)); + return clipboard.getBlock(sampledLoc).permutation; } } diff --git a/src/server/modules/region_buffer.ts b/src/server/modules/region_buffer.ts index 311afc885..e3d53374c 100644 --- a/src/server/modules/region_buffer.ts +++ b/src/server/modules/region_buffer.ts @@ -1,26 +1,16 @@ -import { - contentLog, - generateId, - iterateChunk, - Matrix, - regionCenter, - regionIterateBlocks, - regionSize, - regionTransformedBounds, - regionVolume, - Server, - sleep, - StructureLoadOptions, - StructureSaveOptions, - Thread, - Vector, -} from "@notbeer-api"; -import { Block, BlockPermutation, Dimension, Vector3 } from "@minecraft/server"; -import { blockHasNBTData, getViewVector, locToString, stringToLoc } from "../util.js"; -import { EntityCreateEvent } from "library/@types/Events.js"; +import { axis, contentLog, generateId, iterateChunk, Matrix, regionIterateBlocks, regionSize, regionTransformedBounds, regionVolume, Thread, Vector } from "@notbeer-api"; +import { Block, BlockPermutation, BlockType, Dimension, Structure, StructureMirrorAxis, StructureRotation, StructureSaveMode, Vector3, VectorXZ, world } from "@minecraft/server"; +import { blockHasNBTData, locToString, stringToLoc } from "../util.js"; import { Mask } from "./mask.js"; import { JobFunction, Jobs } from "./jobs.js"; +export interface RegionSaveOptions { + saveAs?: string; + includeEntities?: boolean; + recordBlocksWithData?: boolean; + modifier?: (block: Block) => boolean | BlockPermutation; +} + export interface RegionLoadOptions { offset?: Vector; rotation?: Vector; @@ -28,315 +18,459 @@ export interface RegionLoadOptions { mask?: Mask; } -type blockData = [BlockPermutation, boolean] | [BlockPermutation, boolean, string]; -type blockList = Vector3[] | ((loc: Block) => boolean | BlockPermutation) | "all"; +export interface RegionBlock { + /** + * @remarks + * Returns the buffer that the block is within. + */ + readonly buffer: RegionBuffer; + /** + * @remarks + * Returns true if this block is an air block (i.e., empty + * space). + */ + readonly isAir: boolean; + /** + * @remarks + * Returns true if this block is a liquid block - (e.g., a + * water block and a lava block are liquid, while an air block + * and a stone block are not. Water logged blocks are not + * liquid blocks). + */ + readonly isLiquid: boolean; + /** + * @beta + * @remarks + * Returns or sets whether this block has a liquid on it. + */ + readonly isWaterlogged: boolean; + /** + * @remarks + * Coordinates of the specified block. + */ + readonly location: Vector3; + /** + * @remarks + * Additional block configuration data that describes the + * block. + */ + readonly permutation: BlockPermutation | undefined; + /** + * @remarks + * Structure representation of the block. Only exists if + * the block contains extra data like items and text. + */ + readonly nbtStructure: Structure | undefined; + /** + * @remarks + * Gets the type of block. + */ + readonly type: BlockType; + /** + * @remarks + * Identifier of the type of block for this block. Warning: + * Vanilla block names can be changed in future releases, try + * using 'Block.matches' instead for block comparison. + */ + readonly typeId: string; + /** + * @remarks + * X coordinate of the block. + */ + readonly x: number; + /** + * @remarks + * Y coordinate of the block. + */ + readonly y: number; + /** + * @remarks + * Z coordinate of the block. + */ + readonly z: number; + /** + * @remarks + * Returns the {@link RegionBlock} above this block (positive in the + * Y direction). + * + * @param steps + * Number of steps above to step before returning. + */ + above(steps?: number): RegionBlock | undefined; + /** + * @remarks + * Returns the {@link RegionBlock} below this block (negative in the + * Y direction). + * + * @param steps + * Number of steps below to step before returning. + */ + below(steps?: number): RegionBlock | undefined; + /** + * @remarks + * Returns the {@link Vector3} of the center of this block on + * the X and Z axis. + */ + bottomCenter(): Vector3; + /** + * @remarks + * Returns the {@link Vector3} of the center of this block on + * the X, Y, and Z axis. + */ + center(): Vector3; + /** + * @remarks + * Returns the {@link RegionBlock} to the east of this block + * (positive in the X direction). + * + * @param steps + * Number of steps to the east to step before returning. + */ + east(steps?: number): RegionBlock | undefined; + /** + * @remarks + * Returns a set of tags for a block. + * + * @returns + * The list of tags that the block has. + */ + getTags(): string[]; + /** + * @remarks + * Checks to see if the permutation of this block has a + * specific tag. + * + * @param tag + * Tag to check for. + * @returns + * Returns `true` if the permutation of this block has the tag, + * else `false`. + */ + hasTag(tag: string): boolean; + /** + * @remarks + * Tests whether this block matches a specific criteria. + * + * @param blockName + * Block type identifier to match this API against. + * @param states + * Optional set of block states to test this block against. + * @returns + * Returns true if the block matches the specified criteria. + */ + matches(blockName: string, states?: Record): boolean; + /** + * @remarks + * Returns the {@link RegionBlock} to the north of this block + * (negative in the Z direction). + * + * @param steps + * Number of steps to the north to step before returning. + */ + north(steps?: number): RegionBlock | undefined; + /** + * @remarks + * Returns a block at an offset relative vector to this block. + * + * @param offset + * The offset vector. For example, an offset of 0, 1, 0 will + * return the block above the current block. + * @returns + * Block at the specified offset, or undefined if that block + * could not be retrieved (for example, the block and its + * relative chunk is not loaded yet.) + */ + offset(offset: Vector3): RegionBlock | undefined; + /** + * @remarks + * Sets the block in the dimension to the state of the + * permutation. + * + * This function can't be called in read-only mode. + * + * @param permutation + * Permutation that contains a set of property states for the + * Block. + */ + setPermutation(permutation: BlockPermutation): void; + /** + * @remarks + * Sets the type of block. + * + * This function can't be called in read-only mode. + * + * @param blockType + * Identifier of the type of block to apply - for example, + * minecraft:powered_repeater. + */ + setType(blockType: BlockType | string): void; + /** + * @remarks + * Returns the {@link RegionBlock} to the south of this block + * (positive in the Z direction). + * + * @param steps + * Number of steps to the south to step before returning. + */ + south(steps?: number): RegionBlock | undefined; + /** + * @remarks + * Returns the {@link RegionBlock} to the west of this block + * (negative in the X direction). + * + * @param steps + * Number of steps to the west to step before returning. + */ + west(steps?: number): RegionBlock | undefined; +} -interface transformContext { - blockData: blockData; - sampleBlock: (loc: Vector) => blockData; +interface SubStructure { + name: string; + structure: Structure; + start: Vector; + end: Vector; } export class RegionBuffer { - readonly isAccurate: boolean; - readonly id: string; + private static readonly MAX_SIZE: Vector = new Vector(64, 256, 64); - private size = Vector.ZERO; - private blocks = new Map(); - private blockCount = 0; - private subId = 0; - private savedEntities = false; - private imported = ""; + public readonly id: string; + public readonly getBlock: (loc: Vector3) => RegionBlock | undefined; + private structure: Structure | undefined; + private readonly structures: Record = {}; + private readonly extraBlockData: Record = {}; + + private size = Vector.ZERO; + private volume = 0; private refCount = 1; - constructor(isAccurate = false) { - this.isAccurate = isAccurate; - this.id = "wedit:buffer_" + generateId(); - contentLog.debug("creating structure", this.id); + static *create(start: Vector3, end: Vector3, func: (loc: Vector3) => Block | BlockPermutation | undefined): Generator, RegionBuffer> { + const min = Vector.min(start, end); + const size = Vector.from(regionSize(start, end)); + + const buffer = yield* this.saveStructs(undefined, start, end, (name, start, end) => world.structureManager.createEmpty(name, regionSize(start, end))); + if (!buffer) return undefined; + + const volume = regionVolume(start, end); + buffer.volume = volume; + buffer.size = size; + + let i = 0; + for (const loc of regionIterateBlocks(start, end)) { + const localLoc = Vector.sub(loc, min); + const block = func(localLoc); + + if (block) buffer.getBlock(localLoc).setPermutation(block instanceof BlockPermutation ? block : block.permutation); + if (block instanceof Block && blockRecordable(block)) { + const locString = locToString(localLoc); + const name = buffer.id + "_block" + locString; + world.structureManager.delete(name); + buffer.extraBlockData[locString] = world.structureManager.createFromWorld(name, block.dimension, loc, loc, { includeEntities: false }); + } + + if (iterateChunk()) yield Jobs.setProgress(i / volume); + i++; + } + return buffer; } - public *save(start: Vector3, end: Vector3, dim: Dimension, options: StructureSaveOptions = {}, blocks: blockList = "all"): Generator, boolean> { - if (this.isAccurate) { - const min = Vector.min(start, end); - const iterate = (block: Block) => { - const relLoc = Vector.sub(block, min).floor(); - if (blockHasNBTData(block)) { - const id = this.id + "_" + this.subId++; - this.saveBlockAsStruct(id, block, dim); - this.blocks.set(locToString(relLoc), [block.permutation, block.isWaterlogged, id]); - } else { - this.blocks.set(locToString(relLoc), [block.permutation, block.isWaterlogged]); - } - }; + static *createFromWorld(start: Vector3, end: Vector3, dim: Dimension, options: RegionSaveOptions = {}): Generator, RegionBuffer> { + const min = Vector.min(start, end); + const size = Vector.from(regionSize(start, end)); + const saveOptions = { includeEntities: options.includeEntities ?? false, saveMode: StructureSaveMode[options.saveAs ? "World" : "Memory"] }; - let count = 0; - const isFilter = typeof blocks == "function"; - if (blocks == "all" || isFilter) { - const volume = regionVolume(start, end); - let i = 0; - for (const loc of regionIterateBlocks(start, end)) { - let block = dim.getBlock(loc); - while (!block && Jobs.inContext()) { - block = Jobs.loadBlock(loc); - yield sleep(1); - } + const buffer = yield* this.saveStructs(options.saveAs, start, end, (name, start, end) => world.structureManager.createFromWorld(name, dim, start, end, saveOptions)); + if (!buffer) return undefined; + buffer.volume = regionVolume(start, end); + buffer.size = size; - if (!isFilter) { - iterate(block); - count++; - } else { - const filtered = blocks(block); - if (typeof filtered != "boolean") { - const relLoc = Vector.sub(block, min).floor(); - this.blocks.set(locToString(relLoc), [filtered, false]); - count++; - } else if (filtered) { - iterate(block); - count++; - } - } - if (iterateChunk()) yield Jobs.setProgress(i / volume); - i++; - } - } else if (Array.isArray(blocks)) { - for (let i = 0; i < blocks.length; i++) { - let block = dim.getBlock(blocks[i]); - while (!block && Jobs.inContext()) { - block = Jobs.loadBlock(blocks[i]); - yield sleep(1); + if (options.recordBlocksWithData || options.modifier) { + let i = 0; + const volume = regionVolume(start, end); + const modifier = options.modifier ?? (() => true); + for (const loc of regionIterateBlocks(start, end)) { + const block = dim.getBlock(loc) ?? (yield* Jobs.loadBlock(loc)); + const modResult = modifier(block); + const localLoc = Vector.sub(loc, min); + // Explicitly compare it to "true" since it could succeed with a block permutation + if (modResult === true) { + if (options.recordBlocksWithData && blockRecordable(block)) { + const locString = locToString(localLoc); + const name = buffer.id + "_block" + locString; + world.structureManager.delete(name); + buffer.extraBlockData[locString] = world.structureManager.createFromWorld(name, dim, loc, loc, { includeEntities: false }); } - iterate(block); - if (iterateChunk()) yield Jobs.setProgress(i / blocks.length); + } else { + buffer.getBlock(localLoc).setPermutation(!modResult ? undefined : modResult); } - count = blocks.length; + + if (iterateChunk()) yield Jobs.setProgress(i / volume); + i++; } - this.blockCount = count; - if (options.includeEntities) { - Server.structure.save(this.id, start, end, dim, { - includeBlocks: false, - includeEntities: true, - }); + } + + return buffer; + } + + static get(name: string): RegionBuffer | undefined { + let structure: Structure; + if ((structure = world.structureManager.get(name))) { + const buffer = new RegionBuffer(name, false); + buffer.structure = structure; + buffer.volume = structure.size.x * structure.size.y * structure.size.z; + buffer.size = Vector.from(structure.size); + return buffer; + } else if ((structure = world.structureManager.get(name + "_" + locToString(Vector.ZERO)))) { + const maxIdx = Vector.ZERO; + for (const axis of ["x", "y", "z"]) { + while ((structure = world.structureManager.get(name + "_" + locToString(maxIdx)))) maxIdx[axis]++; + maxIdx[axis]--; } - } else { - const jobCtx = Jobs.getContext(); - if ( - yield Server.structure.saveWhileLoadingChunks(this.id, start, end, dim, options, (min, max) => { - if (Jobs.isContextValid(jobCtx)) { - Jobs.loadBlock(regionCenter(min, max), jobCtx); - return false; - } - return true; - }) - ) - return true; - this.blockCount = regionVolume(start, end); + const size = maxIdx.mul(this.MAX_SIZE).add(structure.size); + const buffer = new RegionBuffer(name, true); + Array.from(Object.entries(this.getSubStructs(name, size))).forEach(([key, sub]) => (buffer.structures[key] = sub.structure)); + buffer.volume = size.x * size.y * size.z; + buffer.size = size; + return buffer; } - this.imported = ""; - this.savedEntities = options.includeEntities; - this.size = regionSize(start, end); - return false; + } + + private constructor(id: string | undefined, multipleStructures: boolean) { + contentLog.debug("creating structure", this.id); + this.id = id ?? "wedit:buffer_" + generateId(); + if (multipleStructures) this.getBlock = this.getBlockMulti; + else this.getBlock = this.getBlockSingle; } public *load(loc: Vector3, dim: Dimension, options: RegionLoadOptions = {}): Generator, void> { const rotation = options.rotation ?? Vector.ZERO; const flip = options.flip ?? Vector.ONE; const bounds = this.getBounds(loc, options); - if (this.isAccurate) { - const matrix = RegionBuffer.getTransformationMatrix(loc, options); - const invMatrix = matrix.invert(); - const shouldTransform = options.rotation || options.flip; - - let transform: (block: BlockPermutation) => BlockPermutation; - if (shouldTransform) { - transform = (block) => { - const blockName = block.type.id; - const attachment = block.getState("attachment") as string; - const direction = block.getState("direction") as number; - const doorHingeBit = block.getState("door_hinge_bit") as boolean; - const facingDir = block.getState("facing_direction") as number; - const groundSignDir = block.getState("ground_sign_direction") as number; - const openBit = block.getState("open_bit") as boolean; - const pillarAxis = block.getState("pillar_axis") as string; - const topSlotBit = block.getState("top_slot_bit") as boolean; - const upsideDownBit = block.getState("upside_down_bit") as boolean; - const weirdoDir = block.getState("weirdo_direction") as number; - const torchFacingDir = block.getState("torch_facing_direction") as string; - const leverDir = block.getState("lever_direction") as string; - const cardinalDir = block.getState("minecraft:cardinal_direction") as string; - - const withProperties = (properties: Record) => { - for (const prop in properties) block = block.withState(prop, properties[prop]); - return block; - }; - if (upsideDownBit != null && openBit != null && direction != null) { - const states = (this.transformMapping(mappings.trapdoorMap, `${upsideDownBit}_${openBit}_${direction}`, matrix) as string).split("_"); - block = withProperties({ upside_down_bit: states[0] == "true", open_bit: states[1] == "true", direction: parseInt(states[2]) }); - } else if (weirdoDir != null && upsideDownBit != null) { - const states = (this.transformMapping(mappings.stairsMap, `${upsideDownBit}_${weirdoDir}`, matrix) as string).split("_"); - block = withProperties({ upside_down_bit: states[0] == "true", weirdo_direction: parseInt(states[1]) }); - } else if (doorHingeBit != null && direction != null) { - const states = (this.transformMapping(mappings.doorMap, `${doorHingeBit}_${direction}`, matrix) as string).split("_"); - block = withProperties({ door_hinge_bit: states[0] == "true", direction: parseInt(states[1]) }); - } else if (attachment != null && direction != null) { - const states = (this.transformMapping(mappings.bellMap, `${attachment}_${direction}`, matrix) as string).split("_"); - block = withProperties({ attachment: states[0], direction: parseInt(states[1]) }); - } else if (cardinalDir != null) { - const state = this.transformMapping(mappings.cardinalDirectionMap, cardinalDir, matrix); - block = block.withState("minecraft:cardinal_direction", state); - } else if (facingDir != null) { - const state = this.transformMapping(mappings.facingDirectionMap, facingDir, matrix); - block = block.withState("facing_direction", parseInt(state)); - } else if (direction != null) { - const mapping = blockName.includes("powered_repeater") || blockName.includes("powered_comparator") ? mappings.redstoneMap : mappings.directionMap; - const state = this.transformMapping(mapping, direction, matrix); - block = block.withState("direction", parseInt(state)); - } else if (groundSignDir != null) { - const state = this.transformMapping(mappings.groundSignDirectionMap, groundSignDir, matrix); - block = block.withState("ground_sign_direction", parseInt(state)); - } else if (torchFacingDir != null) { - const state = this.transformMapping(mappings.torchMap, torchFacingDir, matrix); - block = block.withState("torch_facing_direction", state); - } else if (leverDir != null) { - const state = this.transformMapping(mappings.leverMap, leverDir, matrix); - block = block.withState("lever_direction", state.replace("0", "")); - } else if (pillarAxis != null) { - const state = this.transformMapping(mappings.pillarAxisMap, pillarAxis + "_0", matrix); - block = block.withState("pillar_axis", state[0]); - } else if (topSlotBit != null) { - const state = this.transformMapping(mappings.topSlotMap, String(topSlotBit), matrix); - block = block.withState("top_slot_bit", state == "true"); - } + const matrix = RegionBuffer.getTransformationMatrix(loc, options); + const invMatrix = matrix.invert(); + const shouldTransform = options.rotation || options.flip; + + let transform: (block: BlockPermutation) => BlockPermutation; + if (shouldTransform) { + transform = (block) => { + const blockName = block.type.id; + const attachment = block.getState("attachment") as string; + const direction = block.getState("direction") as number; + const doorHingeBit = block.getState("door_hinge_bit") as boolean; + const facingDir = block.getState("facing_direction") as number; + const groundSignDir = block.getState("ground_sign_direction") as number; + const openBit = block.getState("open_bit") as boolean; + const pillarAxis = block.getState("pillar_axis") as string; + const topSlotBit = block.getState("top_slot_bit") as boolean; + const upsideDownBit = block.getState("upside_down_bit") as boolean; + const weirdoDir = block.getState("weirdo_direction") as number; + const torchFacingDir = block.getState("torch_facing_direction") as string; + const leverDir = block.getState("lever_direction") as string; + const cardinalDir = block.getState("minecraft:cardinal_direction") as string; + + const withProperties = (properties: Record) => { + for (const prop in properties) block = block.withState(prop, properties[prop]); return block; }; - } else { - transform = (block) => block; - } - const blocks = this.blocks; - let totalIterationCount = 0; - const iterator = function* (): Generator<[Vector3, blockData | undefined]> { - if (rotation.x % 90 || rotation.y % 90 || rotation.z % 90) { - totalIterationCount = regionVolume(...bounds); - for (const blockLoc of regionIterateBlocks(...bounds)) { - const sample = Vector.from(blockLoc).add(0.5).transform(invMatrix).floor(); - const block = blocks.get(locToString(sample)); - yield [blockLoc, block]; - } - } else { - totalIterationCount = blocks.size; - for (const [key, block] of blocks.entries()) { - let blockLoc = stringToLoc(key); - blockLoc = (shouldTransform ? blockLoc.add(0.5).transform(matrix) : blockLoc.add(loc)).floor(); - yield [blockLoc, block]; - } + if (upsideDownBit != null && openBit != null && direction != null) { + const states = (this.transformMapping(mappings.trapdoorMap, `${upsideDownBit}_${openBit}_${direction}`, matrix) as string).split("_"); + block = withProperties({ upside_down_bit: states[0] == "true", open_bit: states[1] == "true", direction: parseInt(states[2]) }); + } else if (weirdoDir != null && upsideDownBit != null) { + const states = (this.transformMapping(mappings.stairsMap, `${upsideDownBit}_${weirdoDir}`, matrix) as string).split("_"); + block = withProperties({ upside_down_bit: states[0] == "true", weirdo_direction: parseInt(states[1]) }); + } else if (doorHingeBit != null && direction != null) { + const states = (this.transformMapping(mappings.doorMap, `${doorHingeBit}_${direction}`, matrix) as string).split("_"); + block = withProperties({ door_hinge_bit: states[0] == "true", direction: parseInt(states[1]) }); + } else if (attachment != null && direction != null) { + const states = (this.transformMapping(mappings.bellMap, `${attachment}_${direction}`, matrix) as string).split("_"); + block = withProperties({ attachment: states[0], direction: parseInt(states[1]) }); + } else if (cardinalDir != null) { + const state = this.transformMapping(mappings.cardinalDirectionMap, cardinalDir, matrix); + block = block.withState("minecraft:cardinal_direction", state); + } else if (facingDir != null) { + const state = this.transformMapping(mappings.facingDirectionMap, facingDir, matrix); + block = block.withState("facing_direction", parseInt(state)); + } else if (direction != null) { + const mapping = blockName.includes("powered_repeater") || blockName.includes("powered_comparator") ? mappings.redstoneMap : mappings.directionMap; + const state = this.transformMapping(mapping, direction, matrix); + block = block.withState("direction", parseInt(state)); + } else if (groundSignDir != null) { + const state = this.transformMapping(mappings.groundSignDirectionMap, groundSignDir, matrix); + block = block.withState("ground_sign_direction", parseInt(state)); + } else if (torchFacingDir != null) { + const state = this.transformMapping(mappings.torchMap, torchFacingDir, matrix); + block = block.withState("torch_facing_direction", state); + } else if (leverDir != null) { + const state = this.transformMapping(mappings.leverMap, leverDir, matrix); + block = block.withState("lever_direction", state.replace("0", "")); + } else if (pillarAxis != null) { + const state = this.transformMapping(mappings.pillarAxisMap, pillarAxis + "_0", matrix); + block = block.withState("pillar_axis", state[0]); + } else if (topSlotBit != null) { + const state = this.transformMapping(mappings.topSlotMap, String(topSlotBit), matrix); + block = block.withState("top_slot_bit", state == "true"); } + return block; }; + } else { + transform = (block) => block; + } + if ((Math.abs(rotation.y) / 90) % 1 != 0 || rotation.x || rotation.z || flip.y != 1 || options.mask) { let i = 0; - for (const [blockLoc, block] of iterator()) { + const totalIterationCount = regionVolume(...bounds); + for (const blockLoc of regionIterateBlocks(...bounds)) { + const sample = Vector.from(blockLoc).add(0.5).transform(invMatrix).floor(); + const block = this.getBlock(sample); + if (iterateChunk()) yield Jobs.setProgress(i / totalIterationCount); i++; - if (!block) continue; + if (!block?.permutation) continue; let oldBlock = dim.getBlock(blockLoc); - while (!oldBlock && Jobs.inContext()) { - oldBlock = Jobs.loadBlock(blockLoc); - yield sleep(1); - } + if (!oldBlock && Jobs.inContext()) oldBlock = yield* Jobs.loadBlock(blockLoc); if (options.mask && !options.mask.matchesBlock(oldBlock)) continue; - if (block.length === 3) this.loadBlockFromStruct(block[2], blockLoc, dim); - oldBlock.setPermutation(transform(block[0])); - oldBlock.setWaterlogged(block[1]); + if (block.nbtStructure) world.structureManager.place(block.nbtStructure, dim, blockLoc); + oldBlock.setPermutation(transform(block.permutation)); } - if (this.savedEntities) { - const onEntityload = (ev: EntityCreateEvent) => { - if (shouldTransform) { - // FIXME: Not properly aligned - let entityLoc = ev.entity.location; - let entityFacing = Vector.from(getViewVector(ev.entity)).add(entityLoc); - - entityLoc = Vector.from(entityLoc).sub(loc).transform(matrix).add(loc); - entityFacing = Vector.from(entityFacing).sub(loc).transform(matrix).add(loc); - - ev.entity.teleport(entityLoc, { - dimension: dim, - facingLocation: entityFacing, - }); - } - }; + const volumeQuery = { location: loc, volume: Vector.sub(this.size, [1, 1, 1]) }; + const oldEntities = dim.getEntities(volumeQuery); + yield* this.loadStructs(loc, dim, { includeBlocks: false }); - Server.on("entityCreate", onEntityload); - Server.structure.load(this.id, loc, dim); - Server.off("entityCreate", onEntityload); + if (shouldTransform) { + dim.getEntities(volumeQuery) + .filter((entity) => !oldEntities.some((old) => old.id === entity.id)) + .forEach((entity) => { + let location = entity.location; + let facingLocation = Vector.add(entity.getViewDirection(), location); + location = Vector.from(location).sub(loc).transform(matrix).add(loc); + facingLocation = Vector.from(facingLocation).sub(loc).transform(matrix).add(loc); + entity.teleport(location, { dimension: dim, facingLocation }); + }); } } else { - const loadOptions: StructureLoadOptions = { rotation: rotation.y, flip: "none" }; - if (flip.z == -1) loadOptions.flip = "x"; - if (flip.x == -1) loadOptions.flip += "z"; - if ((loadOptions.flip as string) == "nonez") loadOptions.flip = "z"; - if (this.imported) loadOptions.importedSize = Vector.from(this.size); - const jobCtx = Jobs.getContext(); - yield Server.structure.loadWhileLoadingChunks(this.imported || this.id, bounds[0], dim, loadOptions, (min, max) => { - if (Jobs.isContextValid(jobCtx)) { - Jobs.loadBlock(regionCenter(min, max), jobCtx); - return false; - } - return true; - }); - } - } + yield* this.loadStructs(bounds[0], dim, { rotation: rotation.y, flip }); - /** - * @param func - * @returns - */ - public *warp(func: (loc: Vector3, ctx: transformContext) => blockData): Generator { - if (!this.isAccurate) return; - - const region: [Vector, Vector] = [Vector.ZERO.floor(), this.size.sub(-1)]; - const output = new Map(); - const volume = regionVolume(...region); - const sampleBlock = (loc: Vector) => this.blocks.get(locToString(loc)); - - let i = 0; - for (const coord of regionIterateBlocks(...region)) { - const block = func(coord, { - blockData: this.blocks.get(locToString(coord)), - sampleBlock, - }); - if (block) output.set(locToString(coord), block); - yield ++i / volume; - } - - this.blocks = output; - this.blockCount = this.blocks.size; - } - - public *create(start: Vector3, end: Vector3, func: (loc: Vector3) => Block | BlockPermutation): Generator { - if (!this.isAccurate || !this.size.equals(Vector.ZERO)) return; + let i = 0; + const totalIterationCount = Object.keys(this.extraBlockData).length; + for (const key in this.extraBlockData) { + let blockLoc = stringToLoc(key); + blockLoc = (shouldTransform ? Vector.add(blockLoc, 0.5).transform(matrix) : Vector.add(blockLoc, loc)).floor(); - this.size = regionSize(start, end); - const region: [Vector, Vector] = [Vector.ZERO.floor(), this.size.offset(-1, -1, -1)]; - const volume = regionVolume(...region); + if (iterateChunk()) yield Jobs.setProgress(i / totalIterationCount); + i++; - let i = 0; - for (const coord of regionIterateBlocks(...region)) { - const block = func(coord); - if (block) { - if (block instanceof Block && blockHasNBTData(block)) { - const id = this.id + "_" + this.subId++; - this.saveBlockAsStruct(id, block.location, block.dimension); - this.blocks.set(locToString(coord), [block.permutation, block.isWaterlogged, id]); - } else { - this.blocks.set(locToString(coord), block instanceof Block ? [block.permutation, block.isWaterlogged] : [block, false]); - } + let oldBlock = dim.getBlock(blockLoc); + if (!oldBlock && Jobs.inContext()) oldBlock = yield* Jobs.loadBlock(blockLoc); + world.structureManager.place(this.extraBlockData[key], dim, blockLoc); + oldBlock.setPermutation(transform(oldBlock.permutation)); } - yield Jobs.setProgress(++i / volume); } - this.blockCount = this.blocks.size; } public getSize() { @@ -347,50 +481,12 @@ export class RegionBuffer { return RegionBuffer.createBounds(loc, Vector.add(loc, this.size).sub(1), options); } - public getBlockCount() { - return this.blockCount; - } - - public getBlock(loc: Vector) { - if (!this.isAccurate) return null; - const block = this.blocks.get(locToString(loc)); - if (block) return block[0]; - } - - public getBlocks() { - return Array.from(this.blocks.values()); - } - - public setBlock(loc: Vector3, block: Block | BlockPermutation, options?: StructureSaveOptions & { loc?: Vector; dim?: Dimension }) { - let error: boolean; - const key = locToString(loc); - - if (this.blocks.has(key) && Array.isArray(this.blocks.get(key))) { - this.deleteBlockStruct((this.blocks.get(key) as [BlockPermutation, boolean, string])[2]); - } - - if (block instanceof BlockPermutation) { - if (options?.includeEntities) { - const id = this.id + "_" + this.subId++; - error = Server.structure.save(id, options.loc, options.loc, options.dim, options); - this.blocks.set(key, [block, false, id]); - } else { - this.blocks.set(key, [block, false]); - } - } else { - const id = this.id + "_" + this.subId++; - error = Server.structure.save(id, block.location, block.location, block.dimension, options); - this.blocks.set(key, [block.permutation, block.isWaterlogged, id]); - } - this.size = Vector.max(this.size, Vector.from(loc).add(1)).floor(); - this.blockCount = this.blocks.size; - return error ?? false; + public getVolume() { + return this.volume; } - public import(structure: string, size: Vector3) { - this.imported = structure; - this.size = Vector.from(size); - this.blockCount = size.x * size.y * size.z; + public *getBlocks() { + for (const loc of regionIterateBlocks(Vector.ZERO, Vector.sub(this.size, [1, 1, 1]))) yield this.getBlock(loc); } public ref() { @@ -401,15 +497,66 @@ export class RegionBuffer { if (--this.refCount < 1) this.delete(); } - public static createBounds(start: Vector3, end: Vector3, options: RegionLoadOptions = {}) { - return regionTransformedBounds(Vector.ZERO, Vector.sub(end, start).floor(), RegionBuffer.getTransformationMatrix(start, options)); + private getBlockSingle(loc: Vector3) { + if (loc.x < 0 || loc.x >= this.size.x || loc.y < 0 || loc.y >= this.size.y || loc.z < 0 || loc.z >= this.size.z) return undefined; + return new RegionBlockImpl(this, this.extraBlockData, loc, this.structure, loc); } - private static getTransformationMatrix(loc: Vector3, options: RegionLoadOptions = {}) { - const offset = Matrix.fromTranslation(options.offset ?? Vector.ZERO); - return Matrix.fromRotationFlipOffset(options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE) - .multiply(offset) - .translate(loc); + private getBlockMulti(loc: Vector3) { + if (loc.x < 0 || loc.x >= this.size.x || loc.y < 0 || loc.y >= this.size.y || loc.z < 0 || loc.z >= this.size.z) return undefined; + const offset = { x: loc.x / RegionBuffer.MAX_SIZE.x, y: loc.y / RegionBuffer.MAX_SIZE.y, z: loc.z / RegionBuffer.MAX_SIZE.z }; + const structure = this.structures[locToString(offset)]; + return new RegionBlockImpl(this, this.extraBlockData, loc, structure, Vector.sub(loc, Vector.mul(offset, RegionBuffer.MAX_SIZE))); + } + + private *loadStructs(loc: Vector3, dim: Dimension, options: { rotation?: number; flip?: VectorXZ; includeBlocks?: boolean } = {}) { + const loadPos = Vector.from(loc); + const rotation = new Vector(0, options.rotation ?? 0, 0); + const mirror = new Vector(Math.sign(options.flip?.x ?? 1), 1, Math.sign(options.flip?.z ?? 1)); + const loadOptions = { + rotation: { + 0: StructureRotation.None, + 1: StructureRotation.Rotate90, + 2: StructureRotation.Rotate180, + 3: StructureRotation.Rotate270, + }[((rotation.y ?? 0) / 90) % 4], + mirror: { + "1 1": StructureMirrorAxis.None, + "-1 1": StructureMirrorAxis.X, + "1 -1": StructureMirrorAxis.Z, + "-1 -1": StructureMirrorAxis.XZ, + }[`${mirror.x} ${mirror.z}`], + includeBlocks: options.includeBlocks ?? true, + }; + + if (!this.structure) { + const size = this.size; + const transform = Matrix.fromRotationFlipOffset(rotation, mirror); + const bounds = regionTransformedBounds(Vector.ZERO, size.sub(1).floor(), transform); + let error = false; + for (const [key, structure] of Object.entries(this.structures)) { + const offset = stringToLoc(key).mul(RegionBuffer.MAX_SIZE); + const subBounds = regionTransformedBounds(offset, offset.add(structure.size).sub(1), transform); + const subStart = Vector.sub(subBounds[0], bounds[0]).add(loadPos); + const subEnd = Vector.sub(subBounds[1], bounds[0]).add(loadPos); + yield* Jobs.loadArea(subStart, subEnd); + try { + world.structureManager.place(structure, dim, subStart, loadOptions); + } catch { + error = true; + break; + } + } + return error; + } else { + yield* Jobs.loadArea(loc, Vector.add(loc, this.size).sub(1)); + try { + world.structureManager.place(this.structure, dim, loadPos.floor(), loadOptions); + return false; + } catch { + return true; + } + } } private transformMapping(mapping: { [key: string | number]: Vector | [number, number, number] }, state: string | number, transform: Matrix): string { @@ -433,42 +580,194 @@ export class RegionBuffer { return closestState; } - private saveBlockAsStruct(id: string, loc: Vector3, dim: Dimension) { - const locStr = `${loc.x} ${loc.y} ${loc.z}`; - return Server.runCommand(`structure save ${id} ${locStr} ${locStr} false memory`, dim); + private delete() { + const thread = new Thread(); + thread.start(function* (self: RegionBuffer) { + for (const structure of Object.values(self.extraBlockData)) world.structureManager.delete(structure), yield; + for (const structure of Object.values(self.structures)) world.structureManager.delete(structure), yield; + if (self.structure) world.structureManager.delete(self.structure); + self.size = Vector.ZERO; + self.volume = 0; + contentLog.debug("deleted structure", self.id); + }, this); } - private loadBlockFromStruct(id: string, loc: Vector3, dim: Dimension) { - const locStr = `${loc.x} ${loc.y} ${loc.z}`; - return Server.runCommand(`structure load ${id} ${locStr}`, dim); + public static createBounds(start: Vector3, end: Vector3, options: RegionLoadOptions = {}) { + return regionTransformedBounds(Vector.ZERO, Vector.sub(end, start).floor(), RegionBuffer.getTransformationMatrix(start, options)); } - private deleteBlockStruct(id: string) { - Server.queueCommand(`structure delete ${id}`); + private static getTransformationMatrix(loc: Vector3, options: RegionLoadOptions = {}) { + const offset = Matrix.fromTranslation(options.offset ?? Vector.ZERO); + return Matrix.fromRotationFlipOffset(options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE) + .multiply(offset) + .translate(loc); } - private delete() { - const thread = new Thread(); - thread.start(function* (self: RegionBuffer) { - if (self.isAccurate) { - const promises = []; - for (const block of self.blocks.values()) { - if (block.length === 3) { - promises.push(self.deleteBlockStruct(block[2])); - yield; - } - } - if (promises.length) { - yield Promise.all(promises); + private static *saveStructs(name: string | undefined, start: Vector3, end: Vector3, createFunc: (name: string, start: Vector3, end: Vector3) => Structure) { + const min = Vector.min(start, end); + const size = regionSize(start, end); + if (RegionBuffer.beyondMaxSize(size)) { + let error = false; + const buffer = new RegionBuffer(name, true); + for (const [key, sub] of Object.entries(this.getSubStructs(buffer.id, size))) { + const subStart = min.add(sub.start); + const subEnd = min.add(sub.end); + yield* Jobs.loadArea(subStart, subEnd); + try { + world.structureManager.delete(sub.name); + sub.structure = createFunc(sub.name, min.add(sub.start), min.add(sub.end)); + buffer.structures[key] = sub.structure; + } catch { + error = true; + break; } - self.blocks.clear(); } - self.size = Vector.ZERO; - self.blockCount = 0; - yield Server.structure.delete(self.id); - contentLog.debug("deleted structure", self.id); - }, this); + if (error) { + Object.values(buffer.structures).forEach((struct) => world.structureManager.delete(struct)); + return; + } else { + return buffer; + } + } else { + const buffer = new RegionBuffer(name, false); + yield* Jobs.loadArea(start, end); + try { + world.structureManager.delete(buffer.id); + buffer.structure = createFunc(buffer.id, start, end); + return buffer; + } catch { + return; + } + } + } + + private static beyondMaxSize(size: Vector3) { + return size.x > RegionBuffer.MAX_SIZE.x || size.y > RegionBuffer.MAX_SIZE.y || size.z > RegionBuffer.MAX_SIZE.z; + } + + private static getSubStructs(name: string, size: Vector) { + const subStructs: Record = {}; + for (let z = 0; z < size.z; z += this.MAX_SIZE.z) + for (let y = 0; y < size.y; y += this.MAX_SIZE.y) + for (let x = 0; x < size.x; x += this.MAX_SIZE.x) { + const subStart = new Vector(x, y, z); + const subEnd = Vector.min(subStart.add(this.MAX_SIZE).sub(1), size.sub(1)); + const locString = `${x / this.MAX_SIZE.x}_${y / this.MAX_SIZE.y}_${z / this.MAX_SIZE.z}`; + + subStructs[locString] = { + structure: world.structureManager.get(name + "_" + locString), + name: name + "_" + locString, + start: subStart, + end: subEnd, + }; + } + return subStructs; + } +} + +class RegionBlockImpl implements RegionBlock { + private static AIR = "minecraft:air"; + private static LIQUIDS = ["minecraft:water", "minecraft:flowing_water", "minecraft:lava", "minecraft:flowing_lava"]; + + readonly buffer: RegionBuffer; + readonly x: number; + readonly y: number; + readonly z: number; + + private readonly bufferStructure: Structure; + private readonly bufferStructureLocation: Vector3; + private readonly bufferBlockNBT: Record; + + constructor(buffer: RegionBuffer, extraBlockData: Record, location: Vector3, structure: Structure, inStructureLocation: Vector3) { + this.buffer = buffer; + this.x = Math.floor(location.x); + this.y = Math.floor(location.y); + this.z = Math.floor(location.z); + this.bufferBlockNBT = extraBlockData; + this.bufferStructure = structure; + this.bufferStructureLocation = inStructureLocation; + } + + get permutation(): BlockPermutation | undefined { + return this.bufferStructure.getBlockPermutation(this.bufferStructureLocation); + } + get location(): Vector3 { + return { x: this.x, y: this.y, z: this.z }; + } + get nbtStructure(): Structure | undefined { + return this.bufferBlockNBT[locToString(this.location)]; } + get type(): BlockType { + return this.permutation.type; + } + get typeId(): string { + return this.permutation.type.id; + } + + get isAir(): boolean { + return this.permutation.matches(RegionBlockImpl.AIR); + } + get isLiquid(): boolean { + return RegionBlockImpl.LIQUIDS.includes(this.permutation.type.id); + } + get isWaterlogged(): boolean { + return this.bufferStructure.getIsWaterlogged(this.bufferStructureLocation); + } + + above(steps?: number): RegionBlock | undefined { + return this.buffer.getBlock({ x: this.x, y: this.y + (steps ?? 1), z: this.z }); + } + below(steps?: number): RegionBlock | undefined { + return this.buffer.getBlock({ x: this.x, y: this.y - (steps ?? 1), z: this.z }); + } + north(steps?: number): RegionBlock | undefined { + return this.buffer.getBlock({ x: this.x, y: this.y, z: this.z - (steps ?? 1) }); + } + south(steps?: number): RegionBlock | undefined { + return this.buffer.getBlock({ x: this.x, y: this.y, z: this.z + (steps ?? 1) }); + } + east(steps?: number): RegionBlock | undefined { + return this.buffer.getBlock({ x: this.x + (steps ?? 1), y: this.y, z: this.z }); + } + west(steps?: number): RegionBlock | undefined { + return this.buffer.getBlock({ x: this.x - (steps ?? 1), y: this.y, z: this.z }); + } + offset(offset: Vector3): RegionBlock | undefined { + return this.buffer.getBlock({ x: this.x + offset.x, y: this.y + offset.y, z: this.z + offset.z }); + } + + bottomCenter(): Vector3 { + return { x: this.x + 0.5, y: this.y, z: this.z + 0.5 }; + } + center(): Vector3 { + return { x: this.x + 0.5, y: this.y + 0.5, z: this.z + 0.5 }; + } + + getTags(): string[] { + return this.permutation.getTags(); + } + hasTag(tag: string): boolean { + return this.permutation.hasTag(tag); + } + + matches(blockName: string, states?: Record): boolean { + return this.permutation.matches(blockName, states); + } + setPermutation(permutation: BlockPermutation): void { + let key: string; + if (permutation?.type.id !== this.permutation?.type.id && (key = locToString(this.location)) in this.bufferBlockNBT) { + world.structureManager.delete(this.bufferBlockNBT[key]); + delete this.bufferBlockNBT[key]; + } + this.bufferStructure.setBlockPermutation(this.bufferStructureLocation, permutation); + } + setType(blockType: BlockType | string): void { + this.setPermutation(BlockPermutation.resolve(typeof blockType === "string" ? blockType : blockType.id)); + } +} + +function blockRecordable(block: Block) { + return blockHasNBTData(block) || /* Until Mojang fixes trapdoor rotation... */ block.typeId.match(/^minecraft:.*trapdoor$/); } const mappings = { diff --git a/src/server/sessions.ts b/src/server/sessions.ts index 5b4805f93..07366cfc8 100644 --- a/src/server/sessions.ts +++ b/src/server/sessions.ts @@ -1,11 +1,11 @@ -import { Player, system } from "@minecraft/server"; +import { Player, system, Vector3 } from "@minecraft/server"; import { Server, Vector, setTickTimeout, contentLog, Databases } from "@notbeer-api"; import { Tools } from "./tools/tool_manager.js"; import { History } from "@modules/history.js"; import { Mask } from "@modules/mask.js"; import { Pattern } from "@modules/pattern.js"; import { PlayerUtil } from "@modules/player_util.js"; -import { RegionBuffer } from "@modules/region_buffer.js"; +import { RegionBuffer, RegionSaveOptions } from "@modules/region_buffer.js"; import { Selection, selectMode } from "@modules/selection.js"; import { ConfigContext } from "./ui/types.js"; import config from "config.js"; @@ -284,10 +284,15 @@ export class PlayerSession { // this.settingsHotbar = new SettingsHotbar(this); } - public createRegion(isAccurate: boolean) { - const buffer = new RegionBuffer(isAccurate && !config.performanceMode && !this.performanceMode); - this.regions.set(buffer.id, buffer); - return buffer; + public *createRegion(start: Vector3, end: Vector3, options: RegionSaveOptions = {}) { + const buffer = yield* RegionBuffer.createFromWorld(start, end, this.player.dimension, { + ...options, + recordBlocksWithData: (options.recordBlocksWithData ?? true) && !config.performanceMode && !this.performanceMode, + }); + if (buffer) { + this.regions.set(buffer.id, buffer); + return buffer; + } } public deleteRegion(buffer: RegionBuffer) { diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index ceff73cf2..028db106d 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -2,7 +2,7 @@ import { Block, 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, sleep, Vector } from "@notbeer-api"; +import { iterateChunk, 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"; @@ -187,7 +187,7 @@ export abstract class Shape { // TODO: Localize let activeMask = mask ?? new Mask(); const globalMask = options?.ignoreGlobalMask ?? false ? new Mask() : session.globalMask; - activeMask = (!activeMask ? globalMask : globalMask ? mask.intersect(globalMask) : activeMask)?.withContext(session); + activeMask = (!activeMask ? globalMask : globalMask ? activeMask.intersect(globalMask) : activeMask)?.withContext(session); const simple = pattern.isSimple() && activeMask.isSimple(); let progress = 0; @@ -226,15 +226,7 @@ export abstract class Shape { yield Jobs.setProgress(progress / volume); progress++; if (this[inShapeFunc](Vector.sub(blockLoc, loc).floor(), this.genVars)) { - let block; - do { - if (Jobs.inContext()) { - block = Jobs.loadBlock(blockLoc); - if (!block) yield sleep(1); - } else { - block = dimension.getBlock(blockLoc); - } - } while (!block && Jobs.inContext()); + const block = dimension.getBlock(blockLoc) ?? (yield* Jobs.loadBlock(blockLoc)); if (!activeMask.empty() && !activeMask.matchesBlock(block)) continue; blocksAndChunks.push(block); blocksAffected++; @@ -246,31 +238,23 @@ export abstract class Shape { progress = 0; yield Jobs.nextStep("Generating blocks..."); - yield history?.addUndoStructure(record, min, max); + yield* history?.addUndoStructure(record, min, max); for (let block of blocksAndChunks) { if (block instanceof Block) { - if (!block.isValid() && Jobs.inContext()) { - const loc = block.location; - block = undefined; - do { - block = Jobs.loadBlock(loc); - if (!block) yield sleep(1); - } while (!block); - } - + if (!block.isValid() && Jobs.inContext()) block = yield* Jobs.loadBlock(loc); if (pattern.setBlock(block)) count++; if (iterateChunk()) yield Jobs.setProgress(progress / blocksAffected); progress++; } else { const [min, max] = block; const volume = regionVolume(min, max); - if (Jobs.inContext()) while (!Jobs.loadBlock(min)) yield sleep(1); + if (Jobs.inContext()) yield* Jobs.loadArea(min, max); count += pattern.fillSimpleArea(dimension, min, max, activeMask); yield Jobs.setProgress(progress / blocksAffected); progress += volume; } } - yield history?.addRedoStructure(record, min, max); + yield* history?.addRedoStructure(record, min, max); } history?.commit(record); return count; diff --git a/src/server/tools/generation_tools.ts b/src/server/tools/generation_tools.ts index b817551c3..d51eb296c 100644 --- a/src/server/tools/generation_tools.ts +++ b/src/server/tools/generation_tools.ts @@ -89,7 +89,7 @@ class DrawLineTool extends GeneratorTool { let count: number; try { const points = (yield* generateLine(pos1, pos2)).map((p) => p.floor()); - yield history.addUndoStructure(record, start, end); + yield* history.addUndoStructure(record, start, end); count = 0; for (const point of points) { const block = dim.getBlock(point); @@ -98,7 +98,7 @@ class DrawLineTool extends GeneratorTool { } history.recordSelection(record, session); - yield history.addRedoStructure(record, start, end); + yield* history.addRedoStructure(record, start, end); history.commit(record); } catch (e) { history.cancel(record); diff --git a/src/server/tools/stacker_tool.ts b/src/server/tools/stacker_tool.ts index 3209b883d..29dbde376 100644 --- a/src/server/tools/stacker_tool.ts +++ b/src/server/tools/stacker_tool.ts @@ -27,19 +27,19 @@ class StackerTool extends Tool { } const history = session.getHistory(); const record = history.record(); - const tempStack = new RegionBuffer(true); + let tempStack: RegionBuffer; try { - yield history.addUndoStructure(record, start, end, "any"); + yield* history.addUndoStructure(record, start, end, "any"); - yield* tempStack.save(loc, loc, dim); + tempStack = yield* RegionBuffer.createFromWorld(loc, loc, dim); for (const pos of regionIterateBlocks(start, end)) yield* tempStack.load(pos, dim); - yield history.addRedoStructure(record, start, end, "any"); + yield* history.addRedoStructure(record, start, end, "any"); history.commit(record); } catch (e) { history.cancel(record); throw e; } finally { - tempStack.deref(); + tempStack?.deref(); } }; diff --git a/src/server/util.ts b/src/server/util.ts index 01022dbae..b0d601fb4 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -120,7 +120,7 @@ export function printLocation(loc: Vector3, pretty = true) { * Converts loc to a string */ export function locToString(loc: Vector3) { - return `${loc.x}_${loc.y}_${loc.z}`; + return `${Math.floor(loc.x)}_${Math.floor(loc.y)}_${Math.floor(loc.z)}`; } /**