From 6e0173e4950cc06e90faa5a0c4e5efbe5a56dadd Mon Sep 17 00:00:00 2001 From: SIsilicon Date: Sun, 13 Oct 2024 17:20:46 -0500 Subject: [PATCH 01/26] Upgraded database system and bumped version --- mc_manifest.json | 6 +- package-lock.json | 32 +-- package.json | 6 +- src/config.ts | 2 +- .../@types/classes/databaseBuilder.d.ts | 66 +----- src/library/Minecraft.ts | 2 +- src/library/classes/databaseBuilder.ts | 211 +++++++++++++----- src/server/modules/biome_data.ts | 18 +- src/server/sessions.ts | 36 +-- src/server/tools/tool_manager.ts | 90 ++++---- src/server/util.ts | 5 +- tools/process_config.py | 4 +- tools/sync2com-mojang.py | 3 +- 13 files changed, 272 insertions(+), 209 deletions(-) diff --git a/mc_manifest.json b/mc_manifest.json index be3226666..53e06581a 100644 --- a/mc_manifest.json +++ b/mc_manifest.json @@ -9,12 +9,12 @@ "version": [ 0, 9, - 2 + 3 ], "min_engine_version": [ 1, 21, - 30 + 40 ] }, "bp_modules": [ @@ -58,7 +58,7 @@ "bp_dependencies": [ { "module_name": "@minecraft/server", - "version": "1.15.0-beta" + "version": "1.16.0-beta" }, { "module_name": "@minecraft/server-ui", diff --git a/package-lock.json b/package-lock.json index f16c169ae..8698e4a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,9 @@ "name": "worldedit", "license": "GPL-3.0-or-later", "dependencies": { - "@minecraft/server": "1.15.0-beta.1.21.30-preview.24", + "@minecraft/server": "1.16.0-beta.1.21.40-preview.25", "@minecraft/server-admin": "1.0.0-beta.1.19.80-stable", - "@minecraft/server-ui": "1.4.0-beta.1.21.30-preview.24" + "@minecraft/server-ui": "1.4.0-beta.1.21.40-preview.25" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.1.0", @@ -167,9 +167,10 @@ "integrity": "sha512-stbUtINCXbcLNRlGNVX68xRC6ZYq3k3CYmfptwrCcPBEUjVOpVkSj3H4Y0qiSYB+1rVWv7DgiP7Uf9++50Ne5g==" }, "node_modules/@minecraft/server": { - "version": "1.15.0-beta.1.21.30-preview.24", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.15.0-beta.1.21.30-preview.24.tgz", - "integrity": "sha512-ca/gcNMqddNAGBntFUM12FAP56Zen1Qsy3DrZKUge9ftuF85exdvBwtHoXwE+eLiK9QCapK97aNKkIjLnx+KFg==", + "version": "1.16.0-beta.1.21.40-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.16.0-beta.1.21.40-preview.25.tgz", + "integrity": "sha512-kLhhUbEY3H/obTArlc3tWp7bQgNAi7iy4DSIeaLfoPudg5tKYVBfqYxfdf3qa+S5HWKRYpXFK22/aMFUDow6aA==", + "license": "MIT", "dependencies": { "@minecraft/common": "^1.1.0" } @@ -180,9 +181,10 @@ "integrity": "sha512-HKyfnulfR853lsTlpzWwusDUo5S7KpOhL8w4PwObMeGK4t9ATl32dchZNZpT3N6WlgLxWpq/Z9F6s5th3cFG9A==" }, "node_modules/@minecraft/server-ui": { - "version": "1.4.0-beta.1.21.30-preview.24", - "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.30-preview.24.tgz", - "integrity": "sha512-bknRa9JcU1NgOrIakK+wbPlSwg5nf8GJPiLtRRYNfSS5dqcLhAoigWhm6p9LyBWa7HHYXzbi48IWddgYXvtv4Q==", + "version": "1.4.0-beta.1.21.40-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.40-preview.25.tgz", + "integrity": "sha512-IOTwPGQ1geoGgZE/iZ5wcDJDf2H6nfFKncEMmPrhUMuajNgpk0pev9yGGRsRK3y8OFStXzduJu3aio/JurOOqQ==", + "license": "MIT", "dependencies": { "@minecraft/common": "^1.0.0", "@minecraft/server": "^1.8.0" @@ -1896,9 +1898,9 @@ "integrity": "sha512-stbUtINCXbcLNRlGNVX68xRC6ZYq3k3CYmfptwrCcPBEUjVOpVkSj3H4Y0qiSYB+1rVWv7DgiP7Uf9++50Ne5g==" }, "@minecraft/server": { - "version": "1.15.0-beta.1.21.30-preview.24", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.15.0-beta.1.21.30-preview.24.tgz", - "integrity": "sha512-ca/gcNMqddNAGBntFUM12FAP56Zen1Qsy3DrZKUge9ftuF85exdvBwtHoXwE+eLiK9QCapK97aNKkIjLnx+KFg==", + "version": "1.16.0-beta.1.21.40-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.16.0-beta.1.21.40-preview.25.tgz", + "integrity": "sha512-kLhhUbEY3H/obTArlc3tWp7bQgNAi7iy4DSIeaLfoPudg5tKYVBfqYxfdf3qa+S5HWKRYpXFK22/aMFUDow6aA==", "requires": { "@minecraft/common": "^1.1.0" } @@ -1909,12 +1911,12 @@ "integrity": "sha512-HKyfnulfR853lsTlpzWwusDUo5S7KpOhL8w4PwObMeGK4t9ATl32dchZNZpT3N6WlgLxWpq/Z9F6s5th3cFG9A==" }, "@minecraft/server-ui": { - "version": "1.4.0-beta.1.21.30-preview.24", - "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.30-preview.24.tgz", - "integrity": "sha512-bknRa9JcU1NgOrIakK+wbPlSwg5nf8GJPiLtRRYNfSS5dqcLhAoigWhm6p9LyBWa7HHYXzbi48IWddgYXvtv4Q==", + "version": "1.4.0-beta.1.21.40-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.40-preview.25.tgz", + "integrity": "sha512-IOTwPGQ1geoGgZE/iZ5wcDJDf2H6nfFKncEMmPrhUMuajNgpk0pev9yGGRsRK3y8OFStXzduJu3aio/JurOOqQ==", "requires": { "@minecraft/common": "^1.0.0", - "@minecraft/server": "1.15.0-beta.1.21.30-preview.24" + "@minecraft/server": "1.16.0-beta.1.21.40-preview.25" } }, "@nodelib/fs.scandir": { diff --git a/package.json b/package.json index 36d5a157f..e0f93db39 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,13 @@ "typescript": "^4.7.4" }, "dependencies": { - "@minecraft/server": "1.15.0-beta.1.21.30-preview.24", + "@minecraft/server": "1.16.0-beta.1.21.40-preview.25", "@minecraft/server-admin": "1.0.0-beta.1.19.80-stable", - "@minecraft/server-ui": "1.4.0-beta.1.21.30-preview.24" + "@minecraft/server-ui": "1.4.0-beta.1.21.40-preview.25" }, "overrides": { "@minecraft/server-ui": { - "@minecraft/server": "1.15.0-beta.1.21.30-preview.24" + "@minecraft/server": "1.16.0-beta.1.21.40-preview.25" } } } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index c8732f6e4..2c9fc7a7a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -70,4 +70,4 @@ export default { }; // WorldEdit version (do not change) -export const VERSION = "0.9.2"; +export const VERSION = "0.9.3"; diff --git a/src/library/@types/classes/databaseBuilder.d.ts b/src/library/@types/classes/databaseBuilder.d.ts index 36b7c1a72..0277ba3ff 100644 --- a/src/library/@types/classes/databaseBuilder.d.ts +++ b/src/library/@types/classes/databaseBuilder.d.ts @@ -1,66 +1,20 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface Database { - /** - * Save a value or update a value in the Database under a key - * @param key The key you want to save the value as - * @param value The value you want to save - * @example Database.set('Test Key', 'Test Value'); - */ - set(key: S, value: T[S]): void; + /** Database's data. */ + data: T; - /** - * Get the value of the key - * @param key - * @returns value - * @example Database.get('Test Key'); - */ - get(key: S): T[S]; + /** Returns whether the database is a valid object that can have functions called and data read from. */ + isValid(): boolean; - /** - * Check if the key exists in the table - * @param key - * @returns Whether the key exists - * @example Database.has('Test Key'); - */ - has(key: keyof T): boolean; + /** Returns whether the database has loaded its data from its provider. */ + isLoaded(): boolean; - /** - * Delete the key from the table - * @param key - * @example Database.delete('Test Key'); - */ - delete(key: keyof T): void; - - /** - * Clear everything in the table - * @example Database.clear() - */ + /** Clears everything in the database. */ clear(): void; - /** - * Save all changes made in the database. - * @example Database.save() - */ + /** Saves all changes made in the database. */ save(): void; - /** - * Get all the keys in the database - * @returns Array of keys - * @example Database.keys(); - */ - keys(): (keyof T)[]; - - /** - * Get all the values in the database - * @returns Array of values - * @example Database.values(); - */ - values(): T[keyof T][]; - - /** - * Get all the keys and values in the database in pairs - * @returns Array of key/value pairs - * @example Database.entries(); - */ - entries(): [S, T[S]][]; + /** Deletes the database from the provider it was loaded from. */ + delete(): void; } diff --git a/src/library/Minecraft.ts b/src/library/Minecraft.ts index 9e67fa346..2396ce741 100644 --- a/src/library/Minecraft.ts +++ b/src/library/Minecraft.ts @@ -41,7 +41,7 @@ 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 { getDatabase, deleteDatabase } from "./classes/databaseBuilder.js"; +export { Databases } from "./classes/databaseBuilder.js"; export { configuration } from "./configurations.js"; class ServerBuild extends ServerBuilder { diff --git a/src/library/classes/databaseBuilder.ts b/src/library/classes/databaseBuilder.ts index 27faeb9b9..d1da7b297 100644 --- a/src/library/classes/databaseBuilder.ts +++ b/src/library/classes/databaseBuilder.ts @@ -1,96 +1,201 @@ /* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Entity, World, world } from "@minecraft/server"; -import { Server } from "./serverBuilder.js"; import { Database } from "../@types/classes/databaseBuilder"; -import { contentLog } from "@notbeer-api"; +import { contentLog, Server } from "@notbeer-api"; const objective = world.scoreboard.getObjective("GAMETEST_DB") ?? world.scoreboard.addObjective("GAMETEST_DB", ""); const databases: { [k: string]: DatabaseImpl } = {}; +const parsers: ((key: string, value: any, databaseName: string) => any)[] = []; -export function getDatabase(name: string, provider: World | Entity = world, reviver?: (key: string, value: any) => any, legacyStorage = false) { - const key = name + "//" + (provider instanceof Entity ? provider.id : "world"); - if (!databases[key]) databases[key] = new DatabaseImpl(name, provider, reviver, legacyStorage); - return databases[key] as DatabaseImpl; +function parseJSON(databaseName: string, json: string) { + return JSON.parse(json, (key, value) => { + for (const parser of parsers) value = parser(key, value, databaseName); + return value; + }); } -export function deleteDatabase(name: string, provider: World | Entity = world) { - const key = name + "//" + (provider instanceof Entity ? provider.id : "world"); - if (databases[key]) databases[key].clear(); +function getDatabaseKey(name: string, provider?: World | Entity) { + return name + "//" + (provider instanceof Entity ? provider.id : "world"); +} + +class DatabaseManager { + load(name: string, provider: World | Entity = world, legacyStorage = false) { + const key = getDatabaseKey(name, provider); + if (!databases[key]) { + databases[key] = new DatabaseImpl(name, provider, legacyStorage); + databases[key].load(); + } + return >databases[key]; + } + + delete(name: string, provider: World | Entity = world) { + const key = getDatabaseKey(name, provider); + const database = databases[key] ?? new DatabaseImpl(name, provider); + if (database.isValid()) database.delete(); + delete databases[key]; + } + + find(regexp: RegExp, provider: World | Entity = world) { + return provider.getDynamicPropertyIds().filter((name) => name.match(regexp)); + } + + getRawData(name: string, provider: World | Entity = world) { + const key = getDatabaseKey(name, provider); + const database = databases[key] ?? new DatabaseImpl(name, provider); + return database.rawData; + } - const scoreboardTable = DatabaseImpl.getScoreboardParticipant(DatabaseImpl.getScoreboardName(name, provider)); - if (scoreboardTable) objective.removeParticipant(scoreboardTable); - else provider.setDynamicProperty(name, undefined); + addParser(parser: (key: string, value: any, database: string) => any) { + parsers.push(parser); + } } +export const Databases = new DatabaseManager(); + class DatabaseImpl implements Database { - private data: T; + private _data: T = {}; + private loaded = false; + private valid = true; constructor( private name: string, private provider: World | Entity = world, - reviver?: (key: string, value: any) => any, private legacyStorage = false - ) { - const scoreboardName = DatabaseImpl.getScoreboardName(name, provider); - let table = DatabaseImpl.getScoreboardParticipant(scoreboardName); - try { - if (table) this.data = JSON.parse(JSON.parse(`"${table.displayName}"`), reviver)[1]; - else this.data = JSON.parse(provider.getDynamicProperty(name) ?? "{}", reviver); - - if (table && !legacyStorage) objective.removeParticipant(table); - } catch { - contentLog.error(`Failed to load database ${name} from ${provider instanceof Entity ? provider.nameTag ?? provider.id : "world"}`); - if (table) objective.removeParticipant(table), (table = undefined); - provider.setDynamicProperty(name, undefined); - this.data = {}; - } + ) {} + + get data() { + if (!this.valid) throw new Error(`Can't get data from invalid database "${this.name}".`); + if (!this.loaded) this.load(); + return this._data; } - set(key: S, value: T[S]): void { - this.data[key] = value; + set data(value: T) { + if (!this.valid) throw new Error(`Can't set data on invalid database "${this.name}".`); + this.loaded = true; + this._data = value; } - get(key: S): T[S] { - return this.data[key]; + + get rawData(): string | undefined { + const table = this.getScoreboardParticipant(); + let data: string | undefined; + if (table) { + data = (JSON.parse(`"${table.displayName}"`)).slice(`[\\"${this.getScoreboardName()}\\"`.length - 1, -1); + } else { + let data = this.provider.getDynamicProperty(this.name); + let page: string | undefined; + let i = 2; + while (data && (page = this.provider.getDynamicProperty(`__page${i++}__` + this.name))) data += page; + } + return data; } - has(key: keyof T): boolean { - return key in this.data; + + isLoaded() { + return this.loaded; } - delete(key: keyof T): void { - delete this.data[key]; + + isValid() { + return this.valid; } + clear(): void { - this.data = {} as T; + if (!this.valid) throw new Error(`Can't clear data from invalid database "${this.name}".`); + if (!this.loaded) this.load(); + this._data = {} as T; } + save(): void { + if (!this.valid) throw new Error(`Can't save data to invalid database "${this.name}".`); + + const table = this.getScoreboardParticipant(); if (this.legacyStorage) { - const scoreboardName = DatabaseImpl.getScoreboardName(this.name, this.provider); - const table = DatabaseImpl.getScoreboardParticipant(scoreboardName); + const scoreboardName = this.getScoreboardName(); if (table) Server.runCommand(`scoreboard players reset "${table.displayName}" GAMETEST_DB`); - Server.runCommand(`scoreboard players add ${JSON.stringify(JSON.stringify([scoreboardName, this.data]))} GAMETEST_DB 0`); - } else { - this.provider.setDynamicProperty(this.name, JSON.stringify(this.data)); + Server.runCommand(`scoreboard players add ${JSON.stringify(JSON.stringify([scoreboardName, this._data]))} GAMETEST_DB 0`); + return; + } else if (table && !this.legacyStorage) { + objective.removeParticipant(table); + } + + const data = JSON.stringify(this._data); + // Try smaller divisions of data until the right number of pages is found. + // 50 subdivions allow for a little more than 1.5 MB per database. + divisions: for (let i = 1; i <= 50; i++) { + let page: number | undefined = undefined; + const stepSize = Math.ceil(data.length / i); + for (let j = 0; j < data.length; j += stepSize) { + try { + this.provider.setDynamicProperty((page ? `__page${page}__` : "") + this.name, data.slice(j, j + stepSize)); + page = (page ?? 1) + 1; + } catch { + continue divisions; + } + } + // Remove unused pages + while (this.provider.getDynamicProperty(`__page${page}__` + this.name)) { + this.provider.setDynamicProperty(`__page${page!++}__` + this.name, undefined); + } + this.loaded = true; + return; } + + contentLog.error(`Failed to save database ${this.name} to ${this.provider instanceof Entity ? this.provider.nameTag ?? this.provider.id : "the world"}`); + contentLog.debug(contentLog.stack()); } - keys() { - return <(keyof T)[]>Object.keys(this.data); + + load() { + if (!this.valid) throw new Error(`Can't load data from invalid database "${this.name}".`); + if (this.loaded) return; + try { + this._data = parseJSON(this.name, this.rawData ?? "{}"); + } catch (err) { + contentLog.error(`Failed to load database ${this.name} from ${this.provider instanceof Entity ? this.provider.nameTag ?? this.provider.id : "the world"}`); + if (err) contentLog.debug(err); + return; + } + this.loaded = true; } - values() { - return Object.values(this.data); + + delete() { + if (!this.valid) throw new Error(`Can't delete invalid database "${this.name}".`); + const table = this.getScoreboardParticipant(); + if (table) { + objective.removeParticipant(table); + } else { + this.provider.setDynamicProperty(this.name, undefined); + let page = 2; + while (this.provider.getDynamicProperty(`__page${page}__` + this.name)) { + this.provider.setDynamicProperty(`__page${page++}__` + this.name, undefined); + } + } + this.valid = false; + Databases.delete(this.name, this.provider); } - entries() { - return <[S, T[S]][]>Object.entries(this.data); + + toJSON() { + const json: any = { __dbName__: this.name }; + if (this.provider instanceof Entity) json.__dbProvider__ = this.provider.id; + if (this.legacyStorage) json.__dbLegacy__ = true; + return json; } - static getScoreboardParticipant(scoreboardName: string) { + private getScoreboardParticipant() { for (const table of objective?.getParticipants() ?? []) { - if (table.displayName.startsWith(`[\\"${scoreboardName}\\"`)) return table; + if (table.displayName.startsWith(`[\\"${this.getScoreboardName()}\\"`)) return table; } } - static getScoreboardName(name: string, provider: World | Entity) { - name = "wedit:" + name; - if (provider instanceof Entity) name += provider.id; + private getScoreboardName() { + let name = "wedit:" + this.name; + if (this.provider instanceof Entity) name += this.provider.id; return name; } } + +Databases.addParser((_, value) => { + if (typeof value !== "object" || !("__dbName__" in value)) return value; + const provider = value.__dbProvider__ ? world.getEntity(value.__dbProvider__) : world; + const key = getDatabaseKey(value.__dbName__, provider); + if (!databases[key]) databases[key] = new DatabaseImpl(value.__dbName__, provider, value.__dbLegacy__); + return databases[key]; +}); diff --git a/src/server/modules/biome_data.ts b/src/server/modules/biome_data.ts index 9fd1c0143..f54d5a42a 100644 --- a/src/server/modules/biome_data.ts +++ b/src/server/modules/biome_data.ts @@ -1,5 +1,5 @@ import { Dimension, Vector3, world, Entity } from "@minecraft/server"; -import { commandSyntaxError, contentLog, CustomArgType, getDatabase, Vector } from "@notbeer-api"; +import { commandSyntaxError, contentLog, CustomArgType, Databases, Vector } from "@notbeer-api"; import { EventEmitter } from "library/classes/eventEmitter.js"; import { locToString, wrap } from "../util.js"; import { errorEventSym, PooledResource, readyEventSym, ResourcePool } from "./extern/resource_pools.js"; @@ -85,15 +85,15 @@ class BiomeChanges { flush() { for (const [chunk, data] of this.changes) { const tableName = `biome,${this.dimension.id},${chunk}`; - const database = getDatabase(tableName, world, undefined, true); + const database = Databases.load(tableName, world, true); let biomes: number[] = []; - if (!database.has("biomes")) { + if (!("biomes" in database)) { biomes.length = 4096; biomes = biomes.fill(-1); } else { - const palette: number[] = database.get("palette"); - biomes = (database.get("biomes") as number[]).map((idx) => (idx ? palette[idx - 1] : -1)); + const palette: number[] = database.data.palette; + biomes = (database.data.biomes as number[]).map((idx) => (idx ? palette[idx - 1] : -1)); } for (const [loc, biome] of data.entries()) { @@ -111,12 +111,8 @@ class BiomeChanges { newPalette.forEach((val, idx) => paletteMap.set(val, idx + 1)); paletteMap.set(-1, 0); - database.set( - "biomes", - biomes.map((biome) => paletteMap.get(biome)) - ); - database.set("palette", newPalette); - + database.data.biomes = biomes.map((biome) => paletteMap.get(biome)); + database.data.palette = newPalette; database.save(); } this.changes.clear(); diff --git a/src/server/sessions.ts b/src/server/sessions.ts index c98c5595d..c44ce6266 100644 --- a/src/server/sessions.ts +++ b/src/server/sessions.ts @@ -1,5 +1,5 @@ -import { Player, system } from "@minecraft/server"; -import { Server, Vector, setTickTimeout, contentLog, getDatabase, deleteDatabase } from "@notbeer-api"; +import { Player, system, world } 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"; @@ -36,9 +36,22 @@ interface gradients { [id: string]: { dither: number; patterns: Pattern[] }; } +Databases.addParser((key, value, databaseName) => { + if (databaseName === "gradients" && typeof value === "object" && value.patterns) { + try { + value.patterns = (value.patterns).map((v) => new Pattern(v)); + return value; + } catch { + contentLog.error(`Failed to load gradient ${key}`); + } + } else { + return value; + } +}); + system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => { if (id !== "wedit:reset_gradients_database" || !sourceEntity) return; - deleteDatabase("gradients", sourceEntity); + Databases.delete("gradients", sourceEntity); }); /** @@ -121,10 +134,7 @@ export class PlayerSession { this.history = new History(this); this.selection = new Selection(player); this.drawOutlines = config.drawOutlines; - this.gradients = getDatabase("gradients", player, (k, v) => { - if (k === "patterns") return (v).map((v) => new Pattern(v)); - return v; - }); + this.gradients = Databases.load("gradients", player); if (!this.getTools().length) { this.bindTool("selection_wand", config.wandItem); @@ -286,27 +296,25 @@ export class PlayerSession { } public createGradient(id: string, dither: number, patterns: Pattern[]) { - this.gradients.set(id, { dither, patterns }); + this.gradients.data[id] = { dither, patterns }; this.gradients.save(); } public getGradient(id: string) { - return this.gradients.get(id); + return this.gradients.data[id]; } public getGradientNames() { - return this.gradients.keys() as string[]; + return Object.keys(this.gradients.data); } public deleteGradient(id: string) { - this.gradients.delete(id); + delete this.gradients.data[id]; this.gradients.save(); } delete() { - for (const region of this.regions.values()) { - region.deref(); - } + for (const region of this.regions.values()) region.deref(); this.regions.clear(); this.history.delete(); this.history = null; diff --git a/src/server/tools/tool_manager.ts b/src/server/tools/tool_manager.ts index cb9709aac..625703a18 100644 --- a/src/server/tools/tool_manager.ts +++ b/src/server/tools/tool_manager.ts @@ -1,5 +1,5 @@ import { Player, ItemStack, ItemUseBeforeEvent, world, PlayerBreakBlockBeforeEvent, EntityHitBlockAfterEvent, system } from "@minecraft/server"; -import { contentLog, deleteDatabase, getDatabase, Server, sleep, Thread, Vector } from "@notbeer-api"; +import { contentLog, Databases, Server, sleep, Thread, Vector } from "@notbeer-api"; import { Tool, ToolAction } from "./base_tool.js"; import { PlayerSession, getSession, hasSession } from "../sessions.js"; import { Database } from "library/@types/classes/databaseBuilder.js"; @@ -10,13 +10,29 @@ type toolCondition = (player: Player, session: PlayerSession) => boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any type toolObject = { [key: string]: any } & Tool; +const tools = new Map(); + +Databases.addParser((k, v, databaseName) => { + if (databaseName.startsWith("tools|") && v && typeof v === "object" && "toolType" in v) { + try { + const toolClass = tools.get(v.toolType); + const tool = new toolClass(...(toolClass as toolConstruct & typeof Tool).parseJSON(v)); + tool.type = v.toolType; + return tool; + } catch (err) { + contentLog.error(`Failed to load tool from '${JSON.stringify(v)}' for '${k}': ${err}`); + } + } else { + return v; + } +}); + system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => { if (id !== "wedit:reset_tools_database" || !sourceEntity) return; - deleteDatabase(`tools|${sourceEntity.id}`); + Databases.delete(`tools|${sourceEntity.id}`); }); class ToolBuilder { - private tools = new Map(); private bindings = new Map>(); private fixedBindings = new Map(); private prevHeldTool = new Map(); @@ -74,7 +90,7 @@ class ToolBuilder { } register(toolClass: toolConstruct, name: string, item?: string | string[], condition?: toolCondition) { - this.tools.set(name, toolClass); + tools.set(name, toolClass); if (typeof item == "string") { this.fixedBindings.set(item, new toolClass()); } else if (condition && Array.isArray(item)) { @@ -89,11 +105,11 @@ class ToolBuilder { bind(toolId: string, itemId: string, playerId: string, ...args: any[]) { this.unbind(itemId, playerId); if (itemId) { - const tool = new (this.tools.get(toolId))(...args); + const tool = new (tools.get(toolId))(...args); tool.type = toolId; this.createPlayerBindingMap(playerId); - this.bindings.get(playerId).get(itemId)?.delete(); - this.bindings.get(playerId).set(itemId, tool); + this.bindings.get(playerId).data[itemId]?.delete(); + this.bindings.get(playerId).data[itemId] = tool; this.bindings.get(playerId).save(); return tool; } else { @@ -107,8 +123,8 @@ class ToolBuilder { throw "worldedit.tool.fixedBind"; } this.createPlayerBindingMap(playerId); - this.bindings.get(playerId).get(itemId)?.delete(); - this.bindings.get(playerId).delete(itemId); + this.bindings.get(playerId).data[itemId]?.delete(); + delete this.bindings.get(playerId).data[itemId]; this.bindings.get(playerId).save(); } else { throw "worldedit.tool.noItem"; @@ -117,7 +133,7 @@ class ToolBuilder { hasBinding(itemId: string, playerId: string) { if (itemId) { - return this.bindings.get(playerId)?.has(itemId) || this.fixedBindings.has(itemId); + return !!this.bindings.get(playerId)?.data[itemId] || this.fixedBindings.has(itemId); } else { return false; } @@ -125,7 +141,7 @@ class ToolBuilder { getBindingType(itemId: string, playerId: string) { if (itemId) { - const tool = this.bindings.get(playerId)?.get(itemId) || this.fixedBindings.get(itemId); + const tool = this.bindings.get(playerId)?.data[itemId] || this.fixedBindings.get(itemId); return tool?.type ?? ""; } else { return ""; @@ -134,9 +150,9 @@ class ToolBuilder { getBoundItems(playerId: string, type?: RegExp | string) { this.createPlayerBindingMap(playerId); - const tools = this.bindings.get(playerId); + const tools = this.bindings.get(playerId).data; return tools - ? Array.from(tools.entries()) + ? Array.from(Object.entries(tools)) .filter((binding) => !type || (typeof type == "string" ? binding[1].type == type : type.test(binding[1].type))) .map((binding) => binding[0] as string) : ([] as string[]); @@ -144,7 +160,7 @@ class ToolBuilder { setProperty(itemId: string, playerId: string, prop: string, value: T) { if (itemId) { - const tool: toolObject = this.bindings.get(playerId).get(itemId); + const tool: toolObject = this.bindings.get(playerId).data[itemId]; if (tool && prop in tool) { tool[prop] = value; this.bindings.get(playerId).save(); @@ -156,7 +172,7 @@ class ToolBuilder { getProperty(itemId: string, playerId: string, prop: string) { if (itemId) { - const tool: toolObject = this.bindings.get(playerId).get(itemId); + const tool: toolObject = this.bindings.get(playerId).data[itemId]; if (tool && prop in tool) { return tool[prop] as T; } @@ -166,7 +182,7 @@ class ToolBuilder { hasProperty(itemId: string, playerId: string, prop: string) { if (itemId) { - const tool: toolObject = this.bindings.get(playerId).get(itemId); + const tool: toolObject = this.bindings.get(playerId).data[itemId]; if (tool && prop in tool) { return true; } @@ -187,8 +203,8 @@ class ToolBuilder { const key = item.typeId; let tool: Tool; - if (this.bindings.get(player.id)?.has(key)) { - tool = this.bindings.get(player.id).get(key); + if (this.bindings.get(player.id)?.data[key]) { + tool = this.bindings.get(player.id).data[key]; } else if (this.fixedBindings.has(key)) { tool = this.fixedBindings.get(key); } else { @@ -209,8 +225,8 @@ class ToolBuilder { const key = item.typeId; let tool: Tool; - if (this.bindings.get(player.id)?.has(key)) { - tool = this.bindings.get(player.id).get(key); + if (this.bindings.get(player.id)?.data[key]) { + tool = this.bindings.get(player.id).data[key]; } else if (this.fixedBindings.has(key)) { tool = this.fixedBindings.get(key); } else if (this.conditionalBindings.get(key)?.condition(player, getSession(player))) { @@ -228,8 +244,8 @@ class ToolBuilder { const key = item.typeId; let tool: Tool; - if (this.bindings.get(player.id)?.has(key)) { - tool = this.bindings.get(player.id).get(key); + if (this.bindings.get(player.id)?.data[key]) { + tool = this.bindings.get(player.id).data[key]; } else if (this.fixedBindings.has(key)) { tool = this.fixedBindings.get(key); } else if (this.conditionalBindings.get(key)?.condition(player, getSession(player))) { @@ -247,8 +263,8 @@ class ToolBuilder { const key = item.typeId; let tool: Tool; - if (this.bindings.get(player.id)?.has(key)) { - tool = this.bindings.get(player.id).get(key); + if (this.bindings.get(player.id)?.data[key]) { + tool = this.bindings.get(player.id).data[key]; } else if (this.fixedBindings.has(key)) { tool = this.fixedBindings.get(key); } else if (this.conditionalBindings.get(key)?.condition(player, getSession(player))) { @@ -266,8 +282,8 @@ class ToolBuilder { const key = item.typeId; let tool: Tool; - if (this.bindings.get(player.id)?.has(key)) { - tool = this.bindings.get(player.id).get(key); + if (this.bindings.get(player.id)?.data[key]) { + tool = this.bindings.get(player.id).data[key]; } else if (this.fixedBindings.has(key)) { tool = this.fixedBindings.get(key); } else if (this.conditionalBindings.get(key)?.condition(player, getSession(player))) { @@ -280,28 +296,14 @@ class ToolBuilder { private createPlayerBindingMap(playerId: string) { if (this.bindings.has(playerId)) return; - const database = getDatabase<{ [id: string]: Tool }>(`tools|${playerId}`, world, (k, v) => { - if (v && typeof v === "object" && "toolType" in v) { - try { - const toolClass = this.tools.get(v.toolType); - const tool = new toolClass(...(toolClass as toolConstruct & typeof Tool).parseJSON(v)); - tool.type = v.toolType; - return tool; - } catch (err) { - contentLog.error(`Failed to load tool from '${JSON.stringify(v)}' for '${k}': ${err}`); - } - } else { - return v; - } - }); + const database = Databases.load<{ [id: string]: Tool }>(`tools|${playerId}`, world); this.bindings.set(playerId, database); } private stopHolding(player: Player) { - if (this.prevHeldTool.has(player)) { - this.prevHeldTool.get(player)?.process(getSession(player), ToolAction.STOP_HOLD); - this.prevHeldTool.delete(player); - } + if (!this.prevHeldTool.has(player)) return; + this.prevHeldTool.get(player)?.process(getSession(player), ToolAction.STOP_HOLD); + this.prevHeldTool.delete(player); } } export const Tools = new ToolBuilder(); diff --git a/src/server/util.ts b/src/server/util.ts index 8797ce57c..de1c26a16 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -67,10 +67,7 @@ export function blockHasNBTData(block: Block) { "minecraft:sign", "minecraft:piston", "minecraft:record_player", - "minecraft:waterContainer", - "minecraft:lavaContainer", - "minecraft:snowContainer", - "minecraft:potionContainer", + "minecraft:fluidContainer", ]; const nbt_blocks = [ "minecraft:bee_nest", diff --git a/tools/process_config.py b/tools/process_config.py index bd438e346..2d0c93764 100644 --- a/tools/process_config.py +++ b/tools/process_config.py @@ -141,7 +141,7 @@ def update(): class MyHandler(FileSystemEventHandler): def on_modified(self, ev): - if ev.src_path in [".\worldedit_settings.json", "BP\scripts\config.js"]: + if ev.src_path in ["./worldedit_settings.json", "BP/scripts/config.js"]: update() obsSettings = Observer() @@ -149,7 +149,7 @@ def on_modified(self, ev): obsSettings.start() obsConfigJS = Observer() - obsConfigJS.schedule(MyHandler(), path="BP\scripts") + obsConfigJS.schedule(MyHandler(), path="BP/scripts") obsConfigJS.start() try: diff --git a/tools/sync2com-mojang.py b/tools/sync2com-mojang.py index f8a9d7f4b..d18393000 100644 --- a/tools/sync2com-mojang.py +++ b/tools/sync2com-mojang.py @@ -124,8 +124,7 @@ def on_deleted(self, ev): sync_all() try: alert_watching() - while True: - time.sleep(1) + while True: time.sleep(1) except KeyboardInterrupt: observerBP.stop() observerRP.stop() From ad60da3a878466bebec50e70c3f240428db3337f Mon Sep 17 00:00:00 2001 From: SIsilicon Date: Sun, 13 Oct 2024 17:25:35 -0500 Subject: [PATCH 02/26] Ran formatter --- src/server/sessions.ts | 2 +- src/server/util.ts | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/server/sessions.ts b/src/server/sessions.ts index c44ce6266..36be0b081 100644 --- a/src/server/sessions.ts +++ b/src/server/sessions.ts @@ -1,4 +1,4 @@ -import { Player, system, world } from "@minecraft/server"; +import { Player, system } 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"; diff --git a/src/server/util.ts b/src/server/util.ts index de1c26a16..e0913c513 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -62,13 +62,7 @@ export function canPlaceBlock(loc: Vector3, dim: Dimension) { } export function blockHasNBTData(block: Block) { - const components: `${BlockComponentTypes}`[] = [ - "minecraft:inventory", - "minecraft:sign", - "minecraft:piston", - "minecraft:record_player", - "minecraft:fluidContainer", - ]; + const components: `${BlockComponentTypes}`[] = ["minecraft:inventory", "minecraft:sign", "minecraft:piston", "minecraft:record_player", "minecraft:fluidContainer"]; const nbt_blocks = [ "minecraft:bee_nest", "minecraft:beehive", From 72ef15484655626e9a553a47a47c664c04520f56 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Fri, 1 Nov 2024 10:56:56 -0500 Subject: [PATCH 03/26] Fixed database not getting loaded properly --- src/library/classes/databaseBuilder.ts | 2 +- src/server/tools/tool_manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/classes/databaseBuilder.ts b/src/library/classes/databaseBuilder.ts index d1da7b297..42d9ce01b 100644 --- a/src/library/classes/databaseBuilder.ts +++ b/src/library/classes/databaseBuilder.ts @@ -82,7 +82,7 @@ class DatabaseImpl implements Databas if (table) { data = (JSON.parse(`"${table.displayName}"`)).slice(`[\\"${this.getScoreboardName()}\\"`.length - 1, -1); } else { - let data = this.provider.getDynamicProperty(this.name); + data = this.provider.getDynamicProperty(this.name); let page: string | undefined; let i = 2; while (data && (page = this.provider.getDynamicProperty(`__page${i++}__` + this.name))) data += page; diff --git a/src/server/tools/tool_manager.ts b/src/server/tools/tool_manager.ts index 625703a18..9bef1fb54 100644 --- a/src/server/tools/tool_manager.ts +++ b/src/server/tools/tool_manager.ts @@ -296,7 +296,7 @@ class ToolBuilder { private createPlayerBindingMap(playerId: string) { if (this.bindings.has(playerId)) return; - const database = Databases.load<{ [id: string]: Tool }>(`tools|${playerId}`, world); + const database = Databases.load<{ [id: string]: Tool }>(`tools|${playerId}`); this.bindings.set(playerId, database); } From 988b4a4f5ec501d5ebed0785a9e3f3052e3c3992 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Fri, 1 Nov 2024 11:53:06 -0500 Subject: [PATCH 04/26] Fix error with eslint explicit any in block states --- .eslintrc.json | 1 + src/library/classes/blockBuilder.ts | 4 ++-- src/server/modules/pattern.ts | 4 ++-- src/server/modules/region_buffer.ts | 2 +- src/server/tools/cycler_tool.ts | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 3ff608b0a..fd74455e2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,6 +13,7 @@ }, "plugins": [ "prettier" ], "rules": { + "@typescript-eslint/no-explicit-any": "off", "linebreak-style": [ "error", "windows" diff --git a/src/library/classes/blockBuilder.ts b/src/library/classes/blockBuilder.ts index bd6c0019c..aebca6204 100644 --- a/src/library/classes/blockBuilder.ts +++ b/src/library/classes/blockBuilder.ts @@ -43,8 +43,8 @@ export class BlockBuilder { function* recurseStates(i: number): Generator { const state = props[--i]; for (const val of Array.from(BlockStates.get(state).validValues)) { - permutation = permutation.withState(state, val); - if (permutation.getState(state) != val) return; + permutation = permutation.withState(state, val); + if (permutation.getState(state) != val) return; if (i == 0) { yield permutation; } else { diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts index a9da0a264..b162642a3 100644 --- a/src/server/modules/pattern.ts +++ b/src/server/modules/pattern.ts @@ -391,7 +391,7 @@ class TypePattern extends PatternNode { getPermutation(block: BlockUnit): BlockPermutation { let permutation = this.permutation; Object.entries(block.permutation.getAllStates()).forEach(([state, val]) => { - if (state in this.props) permutation = permutation.withState(state, val); + if (state in this.props) permutation = permutation.withState(state, val); }); return permutation; } @@ -412,7 +412,7 @@ class StatePattern extends PatternNode { let permutation = block.permutation; const props = permutation.getAllStates(); this.states.forEach((val, state) => { - if (state in props) permutation = permutation.withState(state, val); + if (state in props) permutation = permutation.withState(state, val); }); return permutation; } diff --git a/src/server/modules/region_buffer.ts b/src/server/modules/region_buffer.ts index 04cc3eff9..b95b4a924 100644 --- a/src/server/modules/region_buffer.ts +++ b/src/server/modules/region_buffer.ts @@ -161,7 +161,7 @@ export class RegionBuffer { const withProperties = (properties: Record) => { for (const prop in properties) { - block = block.withState(prop, properties[prop]); + block = block.withState(prop, properties[prop]); } return block; }; diff --git a/src/server/tools/cycler_tool.ts b/src/server/tools/cycler_tool.ts index f7d9cb28e..220e7e0ff 100644 --- a/src/server/tools/cycler_tool.ts +++ b/src/server/tools/cycler_tool.ts @@ -32,7 +32,7 @@ class BlockCyclerTool extends Tool { const validValues = BlockStates.get(currState).validValues; const currValueIndex = validValues.indexOf(currValue); currValue = validValues[wrap(validValues.length, currValueIndex + (increment ? 1 : 0))]; - permutation = permutation.withState(currState, currValue); + permutation = permutation.withState(currState, currValue); block.setPermutation(permutation); From b5f6ec2212575d63cd1a837c025431b4fea97b48 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Fri, 1 Nov 2024 11:53:31 -0500 Subject: [PATCH 05/26] Bumped version supported to 1.21.50 --- mc_manifest.json | 6 +++--- package-lock.json | 46 +++++++++++++++++++++++++++++++--------------- package.json | 6 +++--- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/mc_manifest.json b/mc_manifest.json index 53e06581a..52350855f 100644 --- a/mc_manifest.json +++ b/mc_manifest.json @@ -9,12 +9,12 @@ "version": [ 0, 9, - 3 + 4 ], "min_engine_version": [ 1, 21, - 40 + 50 ] }, "bp_modules": [ @@ -58,7 +58,7 @@ "bp_dependencies": [ { "module_name": "@minecraft/server", - "version": "1.16.0-beta" + "version": "1.17.0-beta" }, { "module_name": "@minecraft/server-ui", diff --git a/package-lock.json b/package-lock.json index 8698e4a04..7cd629e35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,9 @@ "name": "worldedit", "license": "GPL-3.0-or-later", "dependencies": { - "@minecraft/server": "1.16.0-beta.1.21.40-preview.25", + "@minecraft/server": "1.17.0-beta.1.21.50-preview.25", "@minecraft/server-admin": "1.0.0-beta.1.19.80-stable", - "@minecraft/server-ui": "1.4.0-beta.1.21.40-preview.25" + "@minecraft/server-ui": "1.4.0-beta.1.21.50-preview.25" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.1.0", @@ -167,12 +167,15 @@ "integrity": "sha512-stbUtINCXbcLNRlGNVX68xRC6ZYq3k3CYmfptwrCcPBEUjVOpVkSj3H4Y0qiSYB+1rVWv7DgiP7Uf9++50Ne5g==" }, "node_modules/@minecraft/server": { - "version": "1.16.0-beta.1.21.40-preview.25", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.16.0-beta.1.21.40-preview.25.tgz", - "integrity": "sha512-kLhhUbEY3H/obTArlc3tWp7bQgNAi7iy4DSIeaLfoPudg5tKYVBfqYxfdf3qa+S5HWKRYpXFK22/aMFUDow6aA==", + "version": "1.17.0-beta.1.21.50-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.17.0-beta.1.21.50-preview.25.tgz", + "integrity": "sha512-VvMmkCdBeuFlfM3VNHfCw9EoVja0xXGUX2w12tSpBl2idTaFO6KWP+L1emTv2AgKd5p35Kj3ZQIl16zjoVFOLA==", "license": "MIT", "dependencies": { "@minecraft/common": "^1.1.0" + }, + "peerDependencies": { + "@minecraft/vanilla-data": ">=1.20.70" } }, "node_modules/@minecraft/server-admin": { @@ -181,15 +184,22 @@ "integrity": "sha512-HKyfnulfR853lsTlpzWwusDUo5S7KpOhL8w4PwObMeGK4t9ATl32dchZNZpT3N6WlgLxWpq/Z9F6s5th3cFG9A==" }, "node_modules/@minecraft/server-ui": { - "version": "1.4.0-beta.1.21.40-preview.25", - "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.40-preview.25.tgz", - "integrity": "sha512-IOTwPGQ1geoGgZE/iZ5wcDJDf2H6nfFKncEMmPrhUMuajNgpk0pev9yGGRsRK3y8OFStXzduJu3aio/JurOOqQ==", + "version": "1.4.0-beta.1.21.50-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.50-preview.25.tgz", + "integrity": "sha512-wO2TPInRofbZrJ6Nk+QFC+PZSWp3r5K8VZhPU0JsgS4z6YyO5Ose2xogiCUiLe9R00KNzR2PRgyZ9HjvkeRGfw==", "license": "MIT", "dependencies": { "@minecraft/common": "^1.0.0", "@minecraft/server": "^1.8.0" } }, + "node_modules/@minecraft/vanilla-data": { + "version": "1.21.43", + "resolved": "https://registry.npmjs.org/@minecraft/vanilla-data/-/vanilla-data-1.21.43.tgz", + "integrity": "sha512-FvHoZ6Q7XICUfN769h87m94E+O7d/2QYFkMpDfdLei/k8r1jrldu9kZWz/tnXQ6Ti+5hBoB9JrAL62bLdZghLQ==", + "license": "MIT", + "peer": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1898,9 +1908,9 @@ "integrity": "sha512-stbUtINCXbcLNRlGNVX68xRC6ZYq3k3CYmfptwrCcPBEUjVOpVkSj3H4Y0qiSYB+1rVWv7DgiP7Uf9++50Ne5g==" }, "@minecraft/server": { - "version": "1.16.0-beta.1.21.40-preview.25", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.16.0-beta.1.21.40-preview.25.tgz", - "integrity": "sha512-kLhhUbEY3H/obTArlc3tWp7bQgNAi7iy4DSIeaLfoPudg5tKYVBfqYxfdf3qa+S5HWKRYpXFK22/aMFUDow6aA==", + "version": "1.17.0-beta.1.21.50-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.17.0-beta.1.21.50-preview.25.tgz", + "integrity": "sha512-VvMmkCdBeuFlfM3VNHfCw9EoVja0xXGUX2w12tSpBl2idTaFO6KWP+L1emTv2AgKd5p35Kj3ZQIl16zjoVFOLA==", "requires": { "@minecraft/common": "^1.1.0" } @@ -1911,14 +1921,20 @@ "integrity": "sha512-HKyfnulfR853lsTlpzWwusDUo5S7KpOhL8w4PwObMeGK4t9ATl32dchZNZpT3N6WlgLxWpq/Z9F6s5th3cFG9A==" }, "@minecraft/server-ui": { - "version": "1.4.0-beta.1.21.40-preview.25", - "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.40-preview.25.tgz", - "integrity": "sha512-IOTwPGQ1geoGgZE/iZ5wcDJDf2H6nfFKncEMmPrhUMuajNgpk0pev9yGGRsRK3y8OFStXzduJu3aio/JurOOqQ==", + "version": "1.4.0-beta.1.21.50-preview.25", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.4.0-beta.1.21.50-preview.25.tgz", + "integrity": "sha512-wO2TPInRofbZrJ6Nk+QFC+PZSWp3r5K8VZhPU0JsgS4z6YyO5Ose2xogiCUiLe9R00KNzR2PRgyZ9HjvkeRGfw==", "requires": { "@minecraft/common": "^1.0.0", - "@minecraft/server": "1.16.0-beta.1.21.40-preview.25" + "@minecraft/server": "1.17.0-beta.1.21.50-preview.25" } }, + "@minecraft/vanilla-data": { + "version": "1.21.43", + "resolved": "https://registry.npmjs.org/@minecraft/vanilla-data/-/vanilla-data-1.21.43.tgz", + "integrity": "sha512-FvHoZ6Q7XICUfN769h87m94E+O7d/2QYFkMpDfdLei/k8r1jrldu9kZWz/tnXQ6Ti+5hBoB9JrAL62bLdZghLQ==", + "peer": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index e0f93db39..d6f64f634 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,13 @@ "typescript": "^4.7.4" }, "dependencies": { - "@minecraft/server": "1.16.0-beta.1.21.40-preview.25", + "@minecraft/server": "1.17.0-beta.1.21.50-preview.25", "@minecraft/server-admin": "1.0.0-beta.1.19.80-stable", - "@minecraft/server-ui": "1.4.0-beta.1.21.40-preview.25" + "@minecraft/server-ui": "1.4.0-beta.1.21.50-preview.25" }, "overrides": { "@minecraft/server-ui": { - "@minecraft/server": "1.16.0-beta.1.21.40-preview.25" + "@minecraft/server": "1.17.0-beta.1.21.50-preview.25" } } } \ No newline at end of file From 630ee22d1df9d2717329983c93911f8be7a55945 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Fri, 1 Nov 2024 17:52:22 -0500 Subject: [PATCH 06/26] Added matrix for transformations --- src/library/classes/structureBuilder.ts | 11 +- src/library/utils/bounds.ts | 9 +- src/library/utils/index.ts | 1 + src/library/utils/matrix.ts | 185 +++++++++++++++++++ src/library/utils/vector.ts | 68 +++---- src/server/brushes/structure_brush.ts | 3 +- src/server/commands/clipboard/paste.ts | 3 +- src/server/commands/region/rotate.ts | 2 +- src/server/commands/region/transform_func.ts | 5 +- src/server/modules/region_buffer.ts | 71 +++---- src/server/shapes/base_shape.ts | 5 +- src/server/tools/button_tools.ts | 3 +- src/server/tools/generation_tools.ts | 14 +- src/server/util.ts | 9 +- 14 files changed, 296 insertions(+), 93 deletions(-) create mode 100644 src/library/utils/matrix.ts diff --git a/src/library/classes/structureBuilder.ts b/src/library/classes/structureBuilder.ts index bc5efab8f..f1f12f588 100644 --- a/src/library/classes/structureBuilder.ts +++ b/src/library/classes/structureBuilder.ts @@ -1,4 +1,5 @@ /* eslint-disable no-empty */ +import { rotationFlipMatrix } from "server/util.js"; import { regionLoaded, regionSize, regionTransformedBounds, sleep, Vector } from "../utils/index.js"; import { Dimension, StructureMirrorAxis, StructureRotation, Vector3, world } from "@minecraft/server"; @@ -165,11 +166,12 @@ class StructureManager { if (mirror.includes("X")) dir_sc.z *= -1; if (mirror.includes("Z")) dir_sc.x *= -1; - const bounds = regionTransformedBounds(Vector.ZERO, size.sub(1).floor(), Vector.ZERO, rotation, dir_sc); + const transform = rotationFlipMatrix(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(), Vector.ZERO, rotation, dir_sc); + 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 { @@ -204,11 +206,12 @@ class StructureManager { if (flip.includes("x")) dir_sc.z *= -1; if (flip.includes("z")) dir_sc.x *= -1; - const bounds = regionTransformedBounds(Vector.ZERO, size.sub(1).floor(), Vector.ZERO, rotation, dir_sc); + const transform = rotationFlipMatrix(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(), Vector.ZERO, rotation, dir_sc); + 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)) { diff --git a/src/library/utils/bounds.ts b/src/library/utils/bounds.ts index 4c5f51580..1f81ad7dd 100644 --- a/src/library/utils/bounds.ts +++ b/src/library/utils/bounds.ts @@ -1,5 +1,5 @@ import { Dimension, Vector3 } from "@minecraft/server"; -import { Vector } from "@notbeer-api"; +import { Matrix, Vector } from "@notbeer-api"; /** * Gives the volume of a space defined by two corners. @@ -35,7 +35,8 @@ export function regionBounds(blocks: Vector3[]): [Vector3, Vector3] { return [min, max]; } -export function regionTransformedBounds(start: Vector, end: Vector, origin: Vector, rotate: Vector, flip: Vector) { +export function regionTransformedBounds(start: Vector, end: Vector, transform: Matrix) { + end = end.add(1); const corners = [ new Vector(start.x, start.y, start.z), new Vector(start.x, start.y, end.z), @@ -45,13 +46,13 @@ export function regionTransformedBounds(start: Vector, end: Vector, origin: Vect new Vector(end.x, start.y, end.z), new Vector(end.x, end.y, start.z), new Vector(end.x, end.y, end.z), - ].map((vec) => vec.sub(origin).rotateY(rotate.y).rotateX(rotate.x).rotateZ(rotate.z).mul(flip).add(origin)); + ].map((vec) => vec.transform(transform)); let [min, max] = [Vector.INF, Vector.NEG_INF]; corners.forEach((vec) => (min = min.min(vec))); corners.forEach((vec) => (max = max.max(vec))); - return [min.floor(), max.floor()] as [Vector, Vector]; + return [min.floor(), max.sub(1).ceil()] as [Vector, Vector]; } /** diff --git a/src/library/utils/index.ts b/src/library/utils/index.ts index a230950d0..f11225859 100644 --- a/src/library/utils/index.ts +++ b/src/library/utils/index.ts @@ -5,5 +5,6 @@ export * from "./debug.js"; export * from "./rawtext.js"; export * from "./scheduling.js"; export * from "./vector.js"; +export * from "./matrix.js"; export * from "./uniqueId.js"; export * from "./tickingarea.js"; diff --git a/src/library/utils/matrix.ts b/src/library/utils/matrix.ts new file mode 100644 index 000000000..663214ff4 --- /dev/null +++ b/src/library/utils/matrix.ts @@ -0,0 +1,185 @@ +import { Vector3 } from "@minecraft/server"; +import { axis } from "./vector"; + +type matrixElements = [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; + +export class Matrix { + public readonly vals: matrixElements = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + + static fromTranslation(vec: Vector3) { + return new Matrix([1, 0, 0, vec.x, 0, 1, 0, vec.y, 0, 0, 1, vec.z, 0, 0, 0, 1]); + } + + static fromRotation(degrees: number, axis: axis) { + // Based on http://www.gamedev.net/reference/articles/article1199.asp + const radians = degrees * (Math.PI / 180); + const c = Math.cos(radians); + const s = Math.sin(radians); + const t = 1 - c; + const x = axis === "x" ? 1 : 0; + const y = axis === "y" ? 1 : 0; + const z = axis === "z" ? 1 : 0; + const tx = t * x; + const ty = t * y; + + return new Matrix([tx * x + c, tx * y - s * z, tx * z + s * y, 0, tx * y + s * z, ty * y + c, ty * z - s * x, 0, tx * z - s * y, ty * z + s * x, t * z * z + c, 0, 0, 0, 0, 1]); + } + + static fromScale(vec: Vector3) { + return new Matrix([vec.x, 0, 0, 0, 0, vec.y, 0, 0, 0, 0, vec.z, 0, 0, 0, 0, 1]); + } + + constructor(values?: matrixElements) { + if (values) this.vals = [...values]; + } + + clone() { + return new Matrix(this.vals); + } + + rotate(degrees: number, axis: axis) { + return this.multiply(Matrix.fromRotation(degrees, axis)); + } + + translate(vec: Vector3) { + return this.multiply(Matrix.fromTranslation(vec)); + } + + scale(vec: Vector3) { + return this.multiply(Matrix.fromScale(vec)); + } + + transpose() { + const vals = [...this.vals]; + vals[1] = this.vals[4]; + vals[4] = this.vals[1]; + vals[2] = this.vals[8]; + vals[8] = this.vals[2]; + vals[6] = this.vals[9]; + vals[9] = this.vals[6]; + vals[3] = this.vals[12]; + vals[12] = this.vals[3]; + vals[7] = this.vals[13]; + vals[13] = this.vals[7]; + vals[11] = this.vals[14]; + vals[14] = this.vals[11]; + return new Matrix(vals); + } + + invert() { + // based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm + const result = new Matrix(); + const vals = result.vals, + n11 = vals[0], + n21 = vals[1], + n31 = vals[2], + n41 = vals[3], + n12 = vals[4], + n22 = vals[5], + n32 = vals[6], + n42 = vals[7], + n13 = vals[8], + n23 = vals[9], + n33 = vals[10], + n43 = vals[11], + n14 = vals[12], + n24 = vals[13], + n34 = vals[14], + n44 = vals[15], + t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44, + t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44, + t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44, + t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34; + + const det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14; + + if (det === 0) return new Matrix([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + const detInv = 1 / det; + + vals[0] = t11 * detInv; + vals[1] = (n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44) * detInv; + vals[2] = (n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44) * detInv; + vals[3] = (n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43) * detInv; + + vals[4] = t12 * detInv; + vals[5] = (n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44) * detInv; + vals[6] = (n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44) * detInv; + vals[7] = (n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43) * detInv; + + vals[8] = t13 * detInv; + vals[9] = (n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44) * detInv; + vals[10] = (n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44) * detInv; + vals[11] = (n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43) * detInv; + + vals[12] = t14 * detInv; + vals[13] = (n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34) * detInv; + vals[14] = (n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34) * detInv; + vals[15] = (n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33) * detInv; + + return result; + } + + multiply(mat: Matrix) { + const result = new Matrix(); + const vals = result.vals; + + const a11 = this.vals[0], + a12 = this.vals[4], + a13 = this.vals[8], + a14 = this.vals[12]; + const a21 = this.vals[1], + a22 = this.vals[5], + a23 = this.vals[9], + a24 = this.vals[13]; + const a31 = this.vals[2], + a32 = this.vals[6], + a33 = this.vals[10], + a34 = this.vals[14]; + const a41 = this.vals[3], + a42 = this.vals[7], + a43 = this.vals[11], + a44 = this.vals[15]; + + const b11 = mat.vals[0], + b12 = mat.vals[4], + b13 = mat.vals[8], + b14 = mat.vals[12]; + const b21 = mat.vals[1], + b22 = mat.vals[5], + b23 = mat.vals[9], + b24 = mat.vals[13]; + const b31 = mat.vals[2], + b32 = mat.vals[6], + b33 = mat.vals[10], + b34 = mat.vals[14]; + const b41 = mat.vals[3], + b42 = mat.vals[7], + b43 = mat.vals[11], + b44 = mat.vals[15]; + + vals[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41; + vals[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42; + vals[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43; + vals[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44; + + vals[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41; + vals[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42; + vals[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43; + vals[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44; + + vals[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41; + vals[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42; + vals[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43; + vals[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44; + + vals[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41; + vals[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42; + vals[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43; + vals[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44; + + return result; + } +} + +new Matrix(); diff --git a/src/library/utils/vector.ts b/src/library/utils/vector.ts index ac839119c..de3a6cf56 100644 --- a/src/library/utils/vector.ts +++ b/src/library/utils/vector.ts @@ -1,4 +1,5 @@ import { Vector3 } from "@minecraft/server"; +import { Matrix } from "./matrix"; type anyVec = Vector3 | [number, number, number]; @@ -150,40 +151,15 @@ export class Vector { } } - rotateX(rot: number, org: anyVec = Vector.ZERO) { - if (!rot) return this.clone(); - org = Vector.from(org); - const y = this.y - org.y; - const z = this.z - org.z; + rotate(degrees: number, axis: axis) { + if (!degrees) return this.clone(); + const radians = degrees * (Math.PI / 180); + const cos = Math.cos(radians); + const sin = Math.sin(radians); - const ang = rot * (Math.PI / 180); - const cos = Math.cos(ang); - const sin = Math.sin(ang); - return new Vector(this.x, Math.round(10000 * (y * cos - z * sin)) / 10000 + org.y, Math.round(10000 * (y * sin + z * cos)) / 10000 + org.z); - } - - rotateY(rot: number, org: anyVec = Vector.ZERO) { - if (!rot) return this.clone(); - org = Vector.from(org); - const x = this.x - org.x; - const z = this.z - org.z; - - const ang = rot * (Math.PI / 180); - const cos = Math.cos(ang); - const sin = Math.sin(ang); - return new Vector(Math.round(10000 * (x * cos - z * sin)) / 10000 + org.x, this.y, Math.round(10000 * (x * sin + z * cos)) / 10000 + org.z); - } - - rotateZ(rot: number, org: anyVec = Vector.ZERO) { - if (!rot) return this.clone(); - org = Vector.from(org); - const x = this.x - org.x; - const y = this.y - org.y; - - const ang = rot * (Math.PI / 180); - const cos = Math.cos(ang); - const sin = Math.sin(ang); - return new Vector(Math.round(10000 * (x * cos - y * sin)) / 10000 + org.x, Math.round(10000 * (x * sin + y * cos)) / 10000 + org.y, this.z); + if (axis === "x") return new Vector(this.x, Math.round(10000 * (this.y * cos - this.z * sin)) / 10000, Math.round(10000 * (this.y * sin + this.z * cos)) / 10000); + else if (axis === "y") return new Vector(Math.round(10000 * (this.x * cos - this.z * sin)) / 10000, this.y, Math.round(10000 * (this.x * sin + this.z * cos)) / 10000); + else return new Vector(Math.round(10000 * (this.x * cos - this.y * sin)) / 10000, Math.round(10000 * (this.x * sin + this.y * cos)) / 10000, this.z); } min(v: anyVec) { @@ -228,6 +204,32 @@ export class Vector { return this.x * v.x + this.y * v.y + this.z * v.z; } + transform(mat: Matrix) { + const [x, y, z] = this.vals; + const vals = mat.vals; + const w = 1 / (vals[3] * x + vals[7] * y + vals[11] * z + vals[15]); + + const result = new Vector(0, 0, 0); + result.x = (vals[0] * x + vals[4] * y + vals[8] * z + vals[12]) * w; + result.y = (vals[1] * x + vals[5] * y + vals[9] * z + vals[13]) * w; + result.z = (vals[2] * x + vals[6] * y + vals[10] * z + vals[14]) * w; + + return result; + } + + transformDirection(mat: Matrix) { + const [x, y, z] = this.vals; + const vals = mat.vals; + + const result = new Vector(0, 0, 0); + result.x = vals[0] * x + vals[4] * y + vals[8] * z; + result.y = vals[1] * x + vals[5] * y + vals[9] * z; + result.z = vals[2] * x + vals[6] * y + vals[10] * z; + result.length = this.length; + + return result; + } + print() { return `${this.x} ${this.y} ${this.z}`; } diff --git a/src/server/brushes/structure_brush.ts b/src/server/brushes/structure_brush.ts index e97c75896..cbd0cfe5c 100644 --- a/src/server/brushes/structure_brush.ts +++ b/src/server/brushes/structure_brush.ts @@ -6,6 +6,7 @@ import { Selection } from "@modules/selection.js"; import { RegionBuffer, RegionLoadOptions } from "@modules/region_buffer.js"; import { world } from "@minecraft/server"; import { importStructure } from "server/commands/structure/import.js"; +import { rotationFlipMatrix } from "server/util.js"; /** * Pastes structures on use @@ -81,7 +82,7 @@ export class StructureBrush extends Brush { options.rotation = new Vector(0, newTransform[0], 0); options.flip = newTransform[1]; this.lastTransform = newTransform; - [start, end] = regionTransformedBounds(start, end, start.lerp(end, 0.5), options.rotation, options.flip); + [start, end] = regionTransformedBounds(start, end, rotationFlipMatrix(options.rotation, options.flip, start.lerp(end, 0.5))); } yield history.addUndoStructure(record, start, end); diff --git a/src/server/commands/clipboard/paste.ts b/src/server/commands/clipboard/paste.ts index b66728547..656520d2c 100644 --- a/src/server/commands/clipboard/paste.ts +++ b/src/server/commands/clipboard/paste.ts @@ -3,6 +3,7 @@ import { Jobs } from "@modules/jobs.js"; import { PlayerUtil } from "@modules/player_util.js"; import { RawText, regionSize, regionTransformedBounds, Vector } from "@notbeer-api"; import { registerCommand } from "../register_commands.js"; +import { rotationFlipMatrix } from "server/util.js"; const registerInformation = { name: "paste", @@ -35,7 +36,7 @@ registerCommand(registerInformation, function* (session, builder, args) { const rotation = session.clipboardTransform.rotation; const flip = session.clipboardTransform.flip; - const bounds = regionTransformedBounds(Vector.ZERO.floor(), session.clipboard.getSize().offset(-1, -1, -1), Vector.ZERO, rotation, flip); + const bounds = regionTransformedBounds(Vector.ZERO.floor(), session.clipboard.getSize().offset(-1, -1, -1), rotationFlipMatrix(rotation, flip)); const size = Vector.from(regionSize(bounds[0], bounds[1])); let pasteStart: Vector; diff --git a/src/server/commands/region/rotate.ts b/src/server/commands/region/rotate.ts index 6a3dedca4..f8385eb5d 100644 --- a/src/server/commands/region/rotate.ts +++ b/src/server/commands/region/rotate.ts @@ -56,7 +56,7 @@ registerCommand(registerInformation, function* (session, builder, args) { if (!session.clipboard.isAccurate) assertValidFastArgs(); if (!args.has("o")) { - session.clipboardTransform.relative = session.clipboardTransform.relative.rotateY(args.get("rotate")); + session.clipboardTransform.relative = session.clipboardTransform.relative.rotate(args.get("rotate"), "y"); } session.clipboardTransform.rotation = session.clipboardTransform.rotation.add(rotation); blockCount = session.clipboard.getBlockCount(); diff --git a/src/server/commands/region/transform_func.ts b/src/server/commands/region/transform_func.ts index 7fef66874..c33a8ea68 100644 --- a/src/server/commands/region/transform_func.ts +++ b/src/server/commands/region/transform_func.ts @@ -6,6 +6,7 @@ import { Player } from "@minecraft/server"; import { PlayerSession } from "../../sessions.js"; import { set } from "./set.js"; import { JobFunction, Jobs } from "@modules/jobs.js"; +import { rotationFlipMatrix } from "server/util.js"; // TODO: fix the bounds sometimes not encompassing the new geometry // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -19,11 +20,11 @@ export function* transformSelection(session: PlayerSession, builder: Player, arg const dim = builder.dimension; const center = Vector.from(start).add(end).mul(0.5); - const origin = args.has("o") ? Vector.ZERO : Vector.sub(center, Vector.from(builder.location).floor()); + const origin = center.sub(args.has("o") ? Vector.ZERO : Vector.sub(center, Vector.from(builder.location).floor())); yield Jobs.nextStep("Gettings blocks..."); yield* temp.save(start, end, dim); - const [newStart, newEnd] = regionTransformedBounds(start, end, center.sub(origin), options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE); + const [newStart, newEnd] = regionTransformedBounds(start, end, rotationFlipMatrix(options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE, origin)); yield history.addUndoStructure(record, start, end, "any"); yield history.addUndoStructure(record, newStart, newEnd, "any"); diff --git a/src/server/modules/region_buffer.ts b/src/server/modules/region_buffer.ts index b95b4a924..594612526 100644 --- a/src/server/modules/region_buffer.ts +++ b/src/server/modules/region_buffer.ts @@ -2,6 +2,7 @@ import { contentLog, generateId, iterateChunk, + Matrix, regionCenter, regionIterateBlocks, regionSize, @@ -15,7 +16,7 @@ import { Vector, } from "@notbeer-api"; import { Block, BlockPermutation, Dimension, Vector3 } from "@minecraft/server"; -import { blockHasNBTData, getViewVector, locToString, stringToLoc } from "../util.js"; +import { blockHasNBTData, getViewVector, locToString, rotationFlipMatrix, stringToLoc } from "../util.js"; import { EntityCreateEvent } from "library/@types/Events.js"; import { Mask } from "./mask.js"; import { JobFunction, Jobs } from "./jobs.js"; @@ -138,7 +139,8 @@ export class RegionBuffer { public *load(loc: Vector3, dim: Dimension, options: RegionLoadOptions = {}): Generator, void> { const rotFlip: [Vector, Vector] = [options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE]; if (this.isAccurate) { - const bounds = regionTransformedBounds(Vector.ZERO, Vector.sub(this.size, [1, 1, 1]).floor(), Vector.ZERO, ...rotFlip); + const matrix = rotationFlipMatrix(...rotFlip); + const bounds = regionTransformedBounds(Vector.ZERO, Vector.sub(this.size, [1, 1, 1]).floor(), matrix); const shouldTransform = options.rotation || options.flip; let transform: (block: BlockPermutation) => BlockPermutation; @@ -167,41 +169,41 @@ export class RegionBuffer { }; if (upsideDownBit != null && openBit != null && direction != null) { - const states = (this.transformMapping(mappings.trapdoorMap, `${upsideDownBit}_${openBit}_${direction}`, ...rotFlip) as string).split("_"); + 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}`, ...rotFlip) as string).split("_"); + 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}`, ...rotFlip) as string).split("_"); + 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}`, ...rotFlip) as string).split("_"); + 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, ...rotFlip); + 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, ...rotFlip); + 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, ...rotFlip); + const state = this.transformMapping(mapping, direction, matrix); block = block.withState("direction", parseInt(state)); } else if (groundSignDir != null) { - const state = this.transformMapping(mappings.groundSignDirectionMap, groundSignDir, ...rotFlip); + 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, ...rotFlip); + 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, ...rotFlip); + 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", ...rotFlip); + 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), ...rotFlip); + const state = this.transformMapping(mappings.topSlotMap, String(topSlotBit), matrix); block = block.withState("top_slot_bit", state == "true"); } return block; @@ -213,7 +215,7 @@ export class RegionBuffer { let i = 0; for (const [key, block] of this.blocks.entries()) { let blockLoc = stringToLoc(key); - if (shouldTransform) blockLoc = Vector.from(blockLoc).rotateY(rotFlip[0].y).rotateX(rotFlip[0].x).rotateZ(rotFlip[0].z).mul(rotFlip[1]).sub(bounds[0]).floor(); + if (shouldTransform) blockLoc = Vector.from(blockLoc).transform(matrix).sub(bounds[0]).floor(); blockLoc = blockLoc.offset(loc.x, loc.y, loc.z); let oldBlock = dim.getBlock(blockLoc); @@ -237,8 +239,8 @@ export class RegionBuffer { let entityLoc = ev.entity.location; let entityFacing = Vector.from(getViewVector(ev.entity)).add(entityLoc); - entityLoc = Vector.from(entityLoc).sub(loc).rotateY(rotFlip[0].y).rotateX(rotFlip[0].x).rotateZ(rotFlip[0].z).mul(rotFlip[1]).sub(bounds[0]).add(loc); - entityFacing = Vector.from(entityFacing).sub(loc).rotateY(rotFlip[0].y).rotateX(rotFlip[0].x).rotateZ(rotFlip[0].z).mul(rotFlip[1]).sub(bounds[0]).add(loc); + entityLoc = Vector.from(entityLoc).sub(loc).transform(matrix).sub(bounds[0]).add(loc); + entityFacing = Vector.from(entityFacing).sub(loc).transform(matrix).sub(bounds[0]).add(loc); ev.entity.teleport(entityLoc, { dimension: dim, @@ -379,14 +381,13 @@ export class RegionBuffer { if (--this.refCount < 1) this.delete(); } - private transformMapping(mapping: { [key: string | number]: Vector | [number, number, number] }, state: string | number, rotate: Vector, flip: Vector): string { + private transformMapping(mapping: { [key: string | number]: Vector | [number, number, number] }, state: string | number, transform: Matrix): string { let vec = Vector.from(mapping[state]); if (!vec) { contentLog.debug(`Can't map state "${state}".`); return typeof state == "string" ? state : state.toString(); } - vec = vec.rotateY(rotate.y).rotateX(rotate.x).rotateZ(rotate.z); - vec = vec.mul(flip); + vec = vec.transformDirection(transform); let closestState: string; let closestDot = -1000; @@ -486,21 +487,21 @@ const mappings = { groundSignDirectionMap: { // ground_sign_direction 0: new Vector(0, 0, 1), - 1: new Vector(0, 0, 1).rotateY((1 / 16) * 360), - 2: new Vector(0, 0, 1).rotateY((2 / 16) * 360), - 3: new Vector(0, 0, 1).rotateY((3 / 16) * 360), - 4: new Vector(0, 0, 1).rotateY((4 / 16) * 360), - 5: new Vector(0, 0, 1).rotateY((5 / 16) * 360), - 6: new Vector(0, 0, 1).rotateY((6 / 16) * 360), - 7: new Vector(0, 0, 1).rotateY((7 / 16) * 360), - 8: new Vector(0, 0, 1).rotateY((8 / 16) * 360), - 9: new Vector(0, 0, 1).rotateY((9 / 16) * 360), - 10: new Vector(0, 0, 1).rotateY((10 / 16) * 360), - 11: new Vector(0, 0, 1).rotateY((11 / 16) * 360), - 12: new Vector(0, 0, 1).rotateY((12 / 16) * 360), - 13: new Vector(0, 0, 1).rotateY((13 / 16) * 360), - 14: new Vector(0, 0, 1).rotateY((14 / 16) * 360), - 15: new Vector(0, 0, 1).rotateY((15 / 16) * 360), + 1: new Vector(0, 0, 1).rotate((1 / 16) * 360, "y"), + 2: new Vector(0, 0, 1).rotate((2 / 16) * 360, "y"), + 3: new Vector(0, 0, 1).rotate((3 / 16) * 360, "y"), + 4: new Vector(0, 0, 1).rotate((4 / 16) * 360, "y"), + 5: new Vector(0, 0, 1).rotate((5 / 16) * 360, "y"), + 6: new Vector(0, 0, 1).rotate((6 / 16) * 360, "y"), + 7: new Vector(0, 0, 1).rotate((7 / 16) * 360, "y"), + 8: new Vector(0, 0, 1).rotate((8 / 16) * 360, "y"), + 9: new Vector(0, 0, 1).rotate((9 / 16) * 360, "y"), + 10: new Vector(0, 0, 1).rotate((10 / 16) * 360, "y"), + 11: new Vector(0, 0, 1).rotate((11 / 16) * 360, "y"), + 12: new Vector(0, 0, 1).rotate((12 / 16) * 360, "y"), + 13: new Vector(0, 0, 1).rotate((13 / 16) * 360, "y"), + 14: new Vector(0, 0, 1).rotate((14 / 16) * 360, "y"), + 15: new Vector(0, 0, 1).rotate((15 / 16) * 360, "y"), }, stairsMap: { // upside_down_bit - weirdo_direction diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index 927007365..1edd9723a 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -143,13 +143,12 @@ export abstract class Shape { } protected drawCircle(center: Vector, radius: number, axis: "x" | "y" | "z"): [string, Vector][] { - const [rotate, vec]: [typeof Vector.prototype.rotateX, Vector] = - axis === "x" ? [Vector.prototype.rotateX, new Vector(0, 1, 0)] : axis === "y" ? [Vector.prototype.rotateY, new Vector(1, 0, 0)] : [Vector.prototype.rotateZ, new Vector(0, 1, 0)]; + const vec = axis === "x" ? new Vector(0, 1, 0) : axis === "y" ? new Vector(1, 0, 0) : new Vector(0, 1, 0); const resolution = snap(Math.min(radius * 2 * Math.PI, 36), 4); const points: [string, Vector][] = []; for (let i = 0; i < resolution; i++) { - let point: Vector = rotate.call(vec, (i / resolution) * 360); + let point: Vector = vec.rotate((i / resolution) * 360, axis); point = point.mul(radius).add(center).add(0.5); points.push(["wedit:selection_draw", point]); } diff --git a/src/server/tools/button_tools.ts b/src/server/tools/button_tools.ts index 6dcdb07b0..8d7cbca58 100644 --- a/src/server/tools/button_tools.ts +++ b/src/server/tools/button_tools.ts @@ -6,6 +6,7 @@ import { Tool } from "./base_tool.js"; import { Tools } from "./tool_manager.js"; import { PlayerUtil } from "@modules/player_util.js"; import { Selection } from "@modules/selection.js"; +import { rotationFlipMatrix } from "server/util.js"; interface PreviewPaste { outlines: Map; @@ -130,7 +131,7 @@ function* previewPaste(self: PreviewPaste, player: Player, session: PlayerSessio } const rotation = session.clipboardTransform.rotation; const flip = session.clipboardTransform.flip; - const bounds = regionTransformedBounds(Vector.ZERO.floor(), session.clipboard.getSize().offset(-1, -1, -1), Vector.ZERO, rotation, flip); + const bounds = regionTransformedBounds(Vector.ZERO.floor(), session.clipboard.getSize().offset(-1, -1, -1), rotationFlipMatrix(rotation, flip)); const size = Vector.from(regionSize(bounds[0], bounds[1])); const loc = PlayerUtil.getBlockLocation(player); diff --git a/src/server/tools/generation_tools.ts b/src/server/tools/generation_tools.ts index 870aad597..c13a23bd9 100644 --- a/src/server/tools/generation_tools.ts +++ b/src/server/tools/generation_tools.ts @@ -1,6 +1,6 @@ import { Player, Vector3, system } from "@minecraft/server"; import { PlayerUtil } from "@modules/player_util"; -import { RawText, Server, Vector, regionBounds } from "@notbeer-api"; +import { RawText, Server, Vector, axis, regionBounds } from "@notbeer-api"; import { generateLine } from "server/commands/region/line"; import { PlayerSession } from "server/sessions"; import { Tool } from "./base_tool"; @@ -163,16 +163,16 @@ class DrawSphereTool extends GeneratorTool { const center = self.getFirstPos(session); const radius = Math.floor(center.sub(self.traceForPos(player)).length) + 0.5; - const axes: [typeof Vector.prototype.rotateX, Vector][] = [ - [Vector.prototype.rotateX, new Vector(0, 1, 0)], - [Vector.prototype.rotateY, new Vector(1, 0, 0)], - [Vector.prototype.rotateZ, new Vector(0, 1, 0)], + const axes: [Vector, axis][] = [ + [new Vector(0, 1, 0), "x"], + [new Vector(1, 0, 0), "y"], + [new Vector(0, 1, 0), "z"], ]; const resolution = snap(Math.min(radius * 2 * Math.PI, 36), 4); - for (const [rotateBy, vec] of axes) { + for (const [vec, axis] of axes) { for (let i = 0; i < resolution; i++) { - let point: Vector = rotateBy.call(vec, (i / resolution) * 360); + let point: Vector = vec.rotate((i / resolution) * 360, axis); point = point.mul(radius).add(center).add(0.5); trySpawnParticle(player, "wedit:selection_draw", point); } diff --git a/src/server/util.ts b/src/server/util.ts index e0913c513..467e31843 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -1,5 +1,5 @@ import { Block, Vector3, Dimension, Entity, Player, RawMessage, BlockComponentTypes } from "@minecraft/server"; -import { Server, RawText, Vector } from "@notbeer-api"; +import { Server, RawText, Vector, Matrix } from "@notbeer-api"; import config from "config.js"; /** @@ -137,3 +137,10 @@ export function arraysEqual(a: T[], b: T[], compare: (a: T, b: T) => boolean) return !compare(valA, valB); }); } + +export function rotationFlipMatrix(rotation: Vector3, flip: Vector3, origin?: Vector3) { + const matrix = new Matrix().rotate(rotation.y, "y").rotate(rotation.x, "x").rotate(rotation.z, "z").scale(flip); + if (!origin) return matrix; + const translate = Matrix.fromTranslation(origin); + return translate.multiply(matrix).multiply(matrix.invert()); +} From 40c8c709fc2384e391b10ff61f072f3163751aaa Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sun, 3 Nov 2024 18:44:39 -0500 Subject: [PATCH 07/26] Significantly improved structure transformation --- src/library/classes/structureBuilder.ts | 7 +- src/library/utils/bounds.ts | 11 ++- src/library/utils/matrix.ts | 32 +++++---- src/server/brushes/structure_brush.ts | 13 ++-- src/server/commands/brush/size.ts | 2 +- src/server/commands/clipboard/copy.ts | 4 +- src/server/commands/clipboard/paste.ts | 29 +++----- src/server/commands/region/flip.ts | 16 +++-- src/server/commands/region/rotate.ts | 7 +- src/server/commands/region/transform_func.ts | 13 ++-- src/server/commands/structure/export.ts | 4 +- src/server/commands/structure/import.ts | 2 +- src/server/modules/region_buffer.ts | 75 ++++++++++++++------ src/server/sessions.ts | 4 +- src/server/tools/button_tools.ts | 14 +--- src/server/util.ts | 9 +-- 16 files changed, 127 insertions(+), 115 deletions(-) diff --git a/src/library/classes/structureBuilder.ts b/src/library/classes/structureBuilder.ts index f1f12f588..9ea729972 100644 --- a/src/library/classes/structureBuilder.ts +++ b/src/library/classes/structureBuilder.ts @@ -1,6 +1,5 @@ /* eslint-disable no-empty */ -import { rotationFlipMatrix } from "server/util.js"; -import { regionLoaded, regionSize, regionTransformedBounds, sleep, Vector } from "../utils/index.js"; +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 } = { @@ -166,7 +165,7 @@ class StructureManager { if (mirror.includes("X")) dir_sc.z *= -1; if (mirror.includes("Z")) dir_sc.x *= -1; - const transform = rotationFlipMatrix(rotation, dir_sc); + 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; @@ -206,7 +205,7 @@ class StructureManager { if (flip.includes("x")) dir_sc.z *= -1; if (flip.includes("z")) dir_sc.x *= -1; - const transform = rotationFlipMatrix(rotation, dir_sc); + 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; diff --git a/src/library/utils/bounds.ts b/src/library/utils/bounds.ts index 1f81ad7dd..4ba54a87a 100644 --- a/src/library/utils/bounds.ts +++ b/src/library/utils/bounds.ts @@ -17,7 +17,7 @@ export function regionVolume(start: Vector3, end: Vector3) { * @param blocks The set of blocks * @return The minimum and maximum */ -export function regionBounds(blocks: Vector3[]): [Vector3, Vector3] { +export function regionBounds(blocks: Vector3[]): [Vector, Vector] { let min: Vector; let max: Vector; for (const block of blocks) { @@ -35,8 +35,9 @@ export function regionBounds(blocks: Vector3[]): [Vector3, Vector3] { return [min, max]; } -export function regionTransformedBounds(start: Vector, end: Vector, transform: Matrix) { - end = end.add(1); +export function regionTransformedBounds(start: Vector3, end: Vector3, transform: Matrix) { + start = Vector.add(start, [0.5, 0.5, 0.5]); + end = Vector.add(end, [0.5, 0.5, 0.5]); const corners = [ new Vector(start.x, start.y, start.z), new Vector(start.x, start.y, end.z), @@ -55,6 +56,10 @@ export function regionTransformedBounds(start: Vector, end: Vector, transform: M return [min.floor(), max.sub(1).ceil()] as [Vector, Vector]; } +export function regionOffset(start: Vector3, end: Vector3, offset: Vector3): [Vector, Vector] { + return [Vector.add(start, offset), Vector.add(end, offset)]; +} + /** * Gives the center of a space defined by two corners. * @param start The first location diff --git a/src/library/utils/matrix.ts b/src/library/utils/matrix.ts index 663214ff4..f396c062e 100644 --- a/src/library/utils/matrix.ts +++ b/src/library/utils/matrix.ts @@ -7,18 +7,16 @@ export class Matrix { public readonly vals: matrixElements = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; static fromTranslation(vec: Vector3) { - return new Matrix([1, 0, 0, vec.x, 0, 1, 0, vec.y, 0, 0, 1, vec.z, 0, 0, 0, 1]); + return new Matrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, vec.x, vec.y, vec.z, 1]); } - static fromRotation(degrees: number, axis: axis) { + static fromRotation(degrees: number, axis: axis | Vector3) { // Based on http://www.gamedev.net/reference/articles/article1199.asp const radians = degrees * (Math.PI / 180); - const c = Math.cos(radians); - const s = Math.sin(radians); + const c = Math.floor(10_000 * Math.cos(radians)) / 10_000; + const s = Math.floor(10_000 * Math.sin(radians)) / 10_000; const t = 1 - c; - const x = axis === "x" ? 1 : 0; - const y = axis === "y" ? 1 : 0; - const z = axis === "z" ? 1 : 0; + const { x, y, z } = typeof axis === "string" ? { x: Number(axis === "x"), y: Number(axis === "y"), z: Number(axis === "z") } : axis; const tx = t * x; const ty = t * y; @@ -29,6 +27,13 @@ export class Matrix { return new Matrix([vec.x, 0, 0, 0, 0, vec.y, 0, 0, 0, 0, vec.z, 0, 0, 0, 0, 1]); } + static fromRotationFlipOffset(rotation: Vector3, flip: Vector3, origin?: Vector3) { + const matrix = new Matrix().rotate(rotation.y, "y").rotate(rotation.x, "x").rotate(rotation.z, "z").scale(flip); + if (!origin) return matrix; + const translate = Matrix.fromTranslation(origin); + return translate.multiply(matrix).multiply(translate.invert()); + } + constructor(values?: matrixElements) { if (values) this.vals = [...values]; } @@ -38,15 +43,15 @@ export class Matrix { } rotate(degrees: number, axis: axis) { - return this.multiply(Matrix.fromRotation(degrees, axis)); + return Matrix.fromRotation(degrees, axis).multiply(this); } translate(vec: Vector3) { - return this.multiply(Matrix.fromTranslation(vec)); + return Matrix.fromTranslation(vec).multiply(this); } scale(vec: Vector3) { - return this.multiply(Matrix.fromScale(vec)); + return Matrix.fromScale(vec).multiply(this); } transpose() { @@ -68,8 +73,7 @@ export class Matrix { invert() { // based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm - const result = new Matrix(); - const vals = result.vals, + const vals = [...this.vals], n11 = vals[0], n21 = vals[1], n31 = vals[2], @@ -117,7 +121,7 @@ export class Matrix { vals[14] = (n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34) * detInv; vals[15] = (n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33) * detInv; - return result; + return new Matrix(vals); } multiply(mat: Matrix) { @@ -181,5 +185,3 @@ export class Matrix { return result; } } - -new Matrix(); diff --git a/src/server/brushes/structure_brush.ts b/src/server/brushes/structure_brush.ts index cbd0cfe5c..e232e9994 100644 --- a/src/server/brushes/structure_brush.ts +++ b/src/server/brushes/structure_brush.ts @@ -1,4 +1,4 @@ -import { Vector, regionTransformedBounds } from "@notbeer-api"; +import { Vector } from "@notbeer-api"; import { PlayerSession } from "../sessions.js"; import { brushTypes, Brush } from "./base_brush.js"; import { Mask } from "@modules/mask.js"; @@ -6,7 +6,6 @@ import { Selection } from "@modules/selection.js"; import { RegionBuffer, RegionLoadOptions } from "@modules/region_buffer.js"; import { world } from "@minecraft/server"; import { importStructure } from "server/commands/structure/import.js"; -import { rotationFlipMatrix } from "server/util.js"; /** * Pastes structures on use @@ -72,21 +71,23 @@ export class StructureBrush extends Brush { const regionSize = struct.getSize(); let start = loc.offset(-regionSize.x / 2, 1, -regionSize.z / 2).ceil(); let end = start.add(regionSize).sub(1); - const options: RegionLoadOptions = { mask: this.mask }; + const center = start.add(end.add(1)).mul(0.5); + const options: RegionLoadOptions = { offset: start.sub(center), mask: this.mask }; if (this.randomTransform) { const newTransform = this.lastTransform.slice() as typeof this.lastTransform; while (newTransform[0] == this.lastTransform[0] && newTransform[1].equals(this.lastTransform[1])) { - newTransform[0] = [0, 90, 180, 270][Math.floor(Math.random() * 4)]; + newTransform[0] = [0, 90, 180, -90][Math.floor(Math.random() * 4)]; newTransform[1] = new Vector(Math.random() > 0.5 ? 1 : -1, 1, Math.random() > 0.5 ? 1 : -1); } options.rotation = new Vector(0, newTransform[0], 0); options.flip = newTransform[1]; this.lastTransform = newTransform; - [start, end] = regionTransformedBounds(start, end, rotationFlipMatrix(options.rotation, options.flip, start.lerp(end, 0.5))); + [start, end] = struct.getBounds(center, options); + console.warn(options.rotation, options.flip); } yield history.addUndoStructure(record, start, end); - yield* struct.load(start, session.getPlayer().dimension, options); + yield* struct.load(center, session.getPlayer().dimension, options); yield history.addRedoStructure(record, start, end); history.commit(record); } catch { diff --git a/src/server/commands/brush/size.ts b/src/server/commands/brush/size.ts index dfb65b59e..82d769c70 100644 --- a/src/server/commands/brush/size.ts +++ b/src/server/commands/brush/size.ts @@ -42,7 +42,7 @@ registerCommand(registerInformation, function (session, builder, args) { size = Vector.from(session.clipboard.getSize()); blockCount = session.clipboard.getBlockCount(); - message.append("translate", "commands.wedit:size.offset").with(`${session.clipboardTransform.relative}\n`); + message.append("translate", "commands.wedit:size.offset").with(`${session.clipboardTransform.offset}\n`); } else { assertSelection(session); diff --git a/src/server/commands/clipboard/copy.ts b/src/server/commands/clipboard/copy.ts index 81309c9ce..0fbe75a84 100644 --- a/src/server/commands/clipboard/copy.ts +++ b/src/server/commands/clipboard/copy.ts @@ -50,9 +50,9 @@ export function* copy(session: PlayerSession, args: Map, buffer: Re session.clipboardTransform = { rotation: Vector.ZERO, flip: Vector.ONE, - originalLoc: Vector.add(start, end).mul(0.5), + originalLoc: start, originalDim: player.dimension.id, - relative: Vector.sub(Vector.add(start, end).mul(0.5), Vector.from(player.location).floor()), + offset: Vector.sub(start, Vector.from(player.location).floor().add(0.5)), }; buffer = session.clipboard; } diff --git a/src/server/commands/clipboard/paste.ts b/src/server/commands/clipboard/paste.ts index 656520d2c..adb397788 100644 --- a/src/server/commands/clipboard/paste.ts +++ b/src/server/commands/clipboard/paste.ts @@ -1,9 +1,8 @@ import { assertClipboard } from "@modules/assert"; import { Jobs } from "@modules/jobs.js"; -import { PlayerUtil } from "@modules/player_util.js"; -import { RawText, regionSize, regionTransformedBounds, Vector } from "@notbeer-api"; +import { RawText, Vector } from "@notbeer-api"; import { registerCommand } from "../register_commands.js"; -import { rotationFlipMatrix } from "server/util.js"; +import { RegionLoadOptions } from "@modules/region_buffer.js"; const registerInformation = { name: "paste", @@ -34,24 +33,14 @@ registerCommand(registerInformation, function* (session, builder, args) { const pasteOriginal = args.has("o"); const pasteContent = !args.has("n"); - const rotation = session.clipboardTransform.rotation; - const flip = session.clipboardTransform.flip; - const bounds = regionTransformedBounds(Vector.ZERO.floor(), session.clipboard.getSize().offset(-1, -1, -1), rotationFlipMatrix(rotation, flip)); - const size = Vector.from(regionSize(bounds[0], bounds[1])); - - let pasteStart: Vector; + let pasteFrom = Vector.from(builder.location).floor().add(0.5); + let transform: RegionLoadOptions = session.clipboardTransform; if (pasteOriginal) { - if (session.clipboardTransform.originalDim != builder.dimension.id || !session.clipboardTransform.originalLoc) { - throw "commands.wedit:paste.noOriginal"; - } - pasteStart = session.clipboardTransform.originalLoc; - } else { - const loc = PlayerUtil.getBlockLocation(builder); - pasteStart = Vector.add(loc, session.clipboardTransform.relative); + if (session.clipboardTransform.originalDim != builder.dimension.id || !session.clipboardTransform.originalLoc) throw "commands.wedit:paste.noOriginal"; + pasteFrom = session.clipboardTransform.originalLoc; + transform = {}; } - pasteStart = pasteStart.sub(size.mul(0.5).sub(1)); - const pasteEnd = pasteStart.add(Vector.sub(size, Vector.ONE)).floor(); - pasteStart = pasteStart.floor(); + const [pasteStart, pasteEnd] = session.clipboard.getBounds(pasteFrom, transform); const history = session.getHistory(); const record = history.record(); @@ -60,7 +49,7 @@ registerCommand(registerInformation, function* (session, builder, args) { if (pasteContent) { yield history.addUndoStructure(record, pasteStart, pasteEnd, "any"); yield Jobs.nextStep("Pasting blocks..."); - yield* session.clipboard.load(pasteStart, builder.dimension, { ...session.clipboardTransform, mask: args.get("m-mask") }); + yield* session.clipboard.load(pasteFrom, builder.dimension, { ...transform, mask: args.get("m-mask") }); yield history.addRedoStructure(record, pasteStart, pasteEnd, "any"); } if (setSelection) { diff --git a/src/server/commands/region/flip.ts b/src/server/commands/region/flip.ts index 3c6d0bf2a..0b689fd1d 100644 --- a/src/server/commands/region/flip.ts +++ b/src/server/commands/region/flip.ts @@ -49,13 +49,15 @@ registerCommand(registerInformation, function* (session, builder, args) { } const clipTrans = session.clipboardTransform; - if (!args.has("o")) { - if (Math.abs(dir.x)) { - clipTrans.relative.x *= -1; - } else if (Math.abs(dir.z)) { - clipTrans.relative.z *= -1; - } - } + + // TODO: Get -o flag working for clipboard flips again + // if (!args.has("o")) { + // if (Math.abs(dir.x)) { + // clipTrans.offset.x *= -1; + // } else if (Math.abs(dir.z)) { + // clipTrans.offset.z *= -1; + // } + // } clipTrans.flip = clipTrans.flip.mul(flip); blockCount = session.clipboard.getBlockCount(); diff --git a/src/server/commands/region/rotate.ts b/src/server/commands/region/rotate.ts index f8385eb5d..fbb17a7c7 100644 --- a/src/server/commands/region/rotate.ts +++ b/src/server/commands/region/rotate.ts @@ -55,9 +55,10 @@ registerCommand(registerInformation, function* (session, builder, args) { assertClipboard(session); if (!session.clipboard.isAccurate) assertValidFastArgs(); - if (!args.has("o")) { - session.clipboardTransform.relative = session.clipboardTransform.relative.rotate(args.get("rotate"), "y"); - } + // 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(); } diff --git a/src/server/commands/region/transform_func.ts b/src/server/commands/region/transform_func.ts index c33a8ea68..761caa9a1 100644 --- a/src/server/commands/region/transform_func.ts +++ b/src/server/commands/region/transform_func.ts @@ -1,14 +1,12 @@ import { assertCuboidSelection } from "@modules/assert.js"; import { Pattern } from "@modules/pattern.js"; import { RegionLoadOptions } from "@modules/region_buffer.js"; -import { regionTransformedBounds, Vector } from "@notbeer-api"; +import { Vector } from "@notbeer-api"; import { Player } from "@minecraft/server"; import { PlayerSession } from "../../sessions.js"; import { set } from "./set.js"; import { JobFunction, Jobs } from "@modules/jobs.js"; -import { rotationFlipMatrix } from "server/util.js"; -// TODO: fix the bounds sometimes not encompassing the new geometry // eslint-disable-next-line @typescript-eslint/no-explicit-any export function* transformSelection(session: PlayerSession, builder: Player, args: Map, options: RegionLoadOptions): Generator> { assertCuboidSelection(session); @@ -19,19 +17,20 @@ export function* transformSelection(session: PlayerSession, builder: Player, arg const [start, end] = session.selection.getRange(); const dim = builder.dimension; - const center = Vector.from(start).add(end).mul(0.5); - const origin = center.sub(args.has("o") ? Vector.ZERO : Vector.sub(center, Vector.from(builder.location).floor())); + const center = Vector.from(start).add(end.add(1)).mul(0.5); + const origin = args.has("o") ? center : Vector.from(builder.location).floor().add(0.5); + options = { offset: start.sub(origin), ...options }; yield Jobs.nextStep("Gettings blocks..."); yield* temp.save(start, end, dim); - const [newStart, newEnd] = regionTransformedBounds(start, end, rotationFlipMatrix(options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE, origin)); + const [newStart, newEnd] = temp.getBounds(origin, options); 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..."); - yield* temp.load(newStart, dim, options); + yield* temp.load(origin, dim, options); if (args.has("s")) { history.recordSelection(record, session); diff --git a/src/server/commands/structure/export.ts b/src/server/commands/structure/export.ts index 67c19831c..48f74f477 100644 --- a/src/server/commands/structure/export.ts +++ b/src/server/commands/structure/export.ts @@ -105,8 +105,8 @@ registerCommand(registerInformation, function* (session, builder, args) { throw "Failed to save structure"; const size = regionSize(...range); - const playerPos = PlayerUtil.getBlockLocation(builder); - const relative = Vector.sub(regionCenter(...range), playerPos); + const playerPos = PlayerUtil.getBlockLocation(builder).add(0.5); + const relative = Vector.sub(range[0], playerPos); if ( writeMetaData( diff --git a/src/server/commands/structure/import.ts b/src/server/commands/structure/import.ts index 43aefbdb9..3223299ef 100644 --- a/src/server/commands/structure/import.ts +++ b/src/server/commands/structure/import.ts @@ -64,7 +64,7 @@ registerCommand(registerInformation, function (session, builder, args) { if (session.clipboard) session.deleteRegion(session.clipboard); session.clipboard = buffer; session.clipboardTransform = { - relative: Vector.from(metadata.relative), + offset: Vector.from(metadata.relative), rotation: Vector.ZERO, flip: Vector.ONE, }; diff --git a/src/server/modules/region_buffer.ts b/src/server/modules/region_buffer.ts index 594612526..311afc885 100644 --- a/src/server/modules/region_buffer.ts +++ b/src/server/modules/region_buffer.ts @@ -16,12 +16,13 @@ import { Vector, } from "@notbeer-api"; import { Block, BlockPermutation, Dimension, Vector3 } from "@minecraft/server"; -import { blockHasNBTData, getViewVector, locToString, rotationFlipMatrix, stringToLoc } from "../util.js"; +import { blockHasNBTData, getViewVector, locToString, stringToLoc } from "../util.js"; import { EntityCreateEvent } from "library/@types/Events.js"; import { Mask } from "./mask.js"; import { JobFunction, Jobs } from "./jobs.js"; export interface RegionLoadOptions { + offset?: Vector; rotation?: Vector; flip?: Vector; mask?: Mask; @@ -137,10 +138,12 @@ export class RegionBuffer { } public *load(loc: Vector3, dim: Dimension, options: RegionLoadOptions = {}): Generator, void> { - const rotFlip: [Vector, Vector] = [options.rotation ?? Vector.ZERO, options.flip ?? Vector.ONE]; + const rotation = options.rotation ?? Vector.ZERO; + const flip = options.flip ?? Vector.ONE; + const bounds = this.getBounds(loc, options); if (this.isAccurate) { - const matrix = rotationFlipMatrix(...rotFlip); - const bounds = regionTransformedBounds(Vector.ZERO, Vector.sub(this.size, [1, 1, 1]).floor(), matrix); + const matrix = RegionBuffer.getTransformationMatrix(loc, options); + const invMatrix = matrix.invert(); const shouldTransform = options.rotation || options.flip; let transform: (block: BlockPermutation) => BlockPermutation; @@ -162,9 +165,7 @@ export class RegionBuffer { const cardinalDir = block.getState("minecraft:cardinal_direction") as string; const withProperties = (properties: Record) => { - for (const prop in properties) { - block = block.withState(prop, properties[prop]); - } + for (const prop in properties) block = block.withState(prop, properties[prop]); return block; }; @@ -212,12 +213,32 @@ export class RegionBuffer { 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]; + } + } + }; + let i = 0; - for (const [key, block] of this.blocks.entries()) { - let blockLoc = stringToLoc(key); - if (shouldTransform) blockLoc = Vector.from(blockLoc).transform(matrix).sub(bounds[0]).floor(); + for (const [blockLoc, block] of iterator()) { + if (iterateChunk()) yield Jobs.setProgress(i / totalIterationCount); + i++; + if (!block) continue; - blockLoc = blockLoc.offset(loc.x, loc.y, loc.z); let oldBlock = dim.getBlock(blockLoc); while (!oldBlock && Jobs.inContext()) { oldBlock = Jobs.loadBlock(blockLoc); @@ -228,8 +249,6 @@ export class RegionBuffer { if (block.length === 3) this.loadBlockFromStruct(block[2], blockLoc, dim); oldBlock.setPermutation(transform(block[0])); oldBlock.setWaterlogged(block[1]); - if (iterateChunk()) yield Jobs.setProgress(i / this.blocks.size); - i++; } if (this.savedEntities) { @@ -239,8 +258,8 @@ export class RegionBuffer { let entityLoc = ev.entity.location; let entityFacing = Vector.from(getViewVector(ev.entity)).add(entityLoc); - entityLoc = Vector.from(entityLoc).sub(loc).transform(matrix).sub(bounds[0]).add(loc); - entityFacing = Vector.from(entityFacing).sub(loc).transform(matrix).sub(bounds[0]).add(loc); + 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, @@ -254,16 +273,13 @@ export class RegionBuffer { Server.off("entityCreate", onEntityload); } } else { - const loadOptions: StructureLoadOptions = { - rotation: rotFlip[0].y, - flip: "none", - }; - if (options.flip?.z == -1) loadOptions.flip = "x"; - if (options.flip?.x == -1) loadOptions.flip += "z"; + 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, loc, dim, loadOptions, (min, max) => { + 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; @@ -327,6 +343,10 @@ export class RegionBuffer { return this.size; } + public getBounds(loc: Vector3, options: RegionLoadOptions = {}) { + return RegionBuffer.createBounds(loc, Vector.add(loc, this.size).sub(1), options); + } + public getBlockCount() { return this.blockCount; } @@ -381,6 +401,17 @@ 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 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 transformMapping(mapping: { [key: string | number]: Vector | [number, number, number] }, state: string | number, transform: Matrix): string { let vec = Vector.from(mapping[state]); if (!vec) { diff --git a/src/server/sessions.ts b/src/server/sessions.ts index 36be0b081..2509ba316 100644 --- a/src/server/sessions.ts +++ b/src/server/sessions.ts @@ -21,7 +21,7 @@ Server.on("playerChangeDimension", (ev) => { interface regionTransform { originalLoc?: Vector; originalDim?: string; - relative: Vector; + offset: Vector; rotation: Vector; flip: Vector; } @@ -106,7 +106,7 @@ export class PlayerSession { * The transformation properties currently on the clipboard */ public clipboardTransform: regionTransform = { - relative: Vector.ZERO, + offset: Vector.ZERO, rotation: Vector.ZERO, flip: Vector.ONE, }; diff --git a/src/server/tools/button_tools.ts b/src/server/tools/button_tools.ts index 8d7cbca58..9588c1450 100644 --- a/src/server/tools/button_tools.ts +++ b/src/server/tools/button_tools.ts @@ -4,9 +4,7 @@ import { regionSize, regionTransformedBounds, Server, Vector } from "@notbeer-ap import { PlayerSession } from "../sessions.js"; import { Tool } from "./base_tool.js"; import { Tools } from "./tool_manager.js"; -import { PlayerUtil } from "@modules/player_util.js"; import { Selection } from "@modules/selection.js"; -import { rotationFlipMatrix } from "server/util.js"; interface PreviewPaste { outlines: Map; @@ -129,17 +127,9 @@ function* previewPaste(self: PreviewPaste, player: Player, session: PlayerSessio const selection = new Selection(player); self.outlines.set(session, selection); } - const rotation = session.clipboardTransform.rotation; - const flip = session.clipboardTransform.flip; - const bounds = regionTransformedBounds(Vector.ZERO.floor(), session.clipboard.getSize().offset(-1, -1, -1), rotationFlipMatrix(rotation, flip)); - const size = Vector.from(regionSize(bounds[0], bounds[1])); - - const loc = PlayerUtil.getBlockLocation(player); - const pasteStart = Vector.add(loc, session.clipboardTransform.relative).sub(size.mul(0.5).sub(1)); - const pasteEnd = pasteStart.add(Vector.sub(size, Vector.ONE)).floor(); - + const [pasteStart, pasteEnd] = session.clipboard.getBounds(Vector.from(player.location).floor().add(0.5), session.clipboardTransform); const selection = self.outlines.get(session)!; - selection.set(0, pasteStart.floor()); + selection.set(0, pasteStart); selection.set(1, pasteEnd); selection.draw(); yield; diff --git a/src/server/util.ts b/src/server/util.ts index 467e31843..e0913c513 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -1,5 +1,5 @@ import { Block, Vector3, Dimension, Entity, Player, RawMessage, BlockComponentTypes } from "@minecraft/server"; -import { Server, RawText, Vector, Matrix } from "@notbeer-api"; +import { Server, RawText, Vector } from "@notbeer-api"; import config from "config.js"; /** @@ -137,10 +137,3 @@ export function arraysEqual(a: T[], b: T[], compare: (a: T, b: T) => boolean) return !compare(valA, valB); }); } - -export function rotationFlipMatrix(rotation: Vector3, flip: Vector3, origin?: Vector3) { - const matrix = new Matrix().rotate(rotation.y, "y").rotate(rotation.x, "x").rotate(rotation.z, "z").scale(flip); - if (!origin) return matrix; - const translate = Matrix.fromTranslation(origin); - return translate.multiply(matrix).multiply(matrix.invert()); -} From 24b531963486e369ecd6c80cc788dbe602ba047a Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sun, 3 Nov 2024 18:44:47 -0500 Subject: [PATCH 08/26] Added ;revolve command --- src/server/commands/command_list.ts | 1 + src/server/commands/region/revolve.ts | 110 ++++++++++++++++++++++++++ texts/en_US.po | 4 + 3 files changed, 115 insertions(+) create mode 100644 src/server/commands/region/revolve.ts diff --git a/src/server/commands/command_list.ts b/src/server/commands/command_list.ts index 1adc566b6..0d87f389e 100644 --- a/src/server/commands/command_list.ts +++ b/src/server/commands/command_list.ts @@ -58,6 +58,7 @@ import "./region/set.js"; import "./region/replace.js"; import "./region/move.js"; import "./region/stack.js"; +import "./region/revolve.js"; import "./region/rotate.js"; import "./region/flip.js"; import "./region/wall.js"; diff --git a/src/server/commands/region/revolve.ts b/src/server/commands/region/revolve.ts new file mode 100644 index 000000000..8d1231101 --- /dev/null +++ b/src/server/commands/region/revolve.ts @@ -0,0 +1,110 @@ +import { assertCuboidSelection } from "@modules/assert.js"; +import { Jobs } from "@modules/jobs.js"; +import { RawText, regionBounds, regionOffset, Vector } from "@notbeer-api"; +import { registerCommand } from "../register_commands.js"; +import { copy } from "../clipboard/copy.js"; +import { RegionBuffer } from "@modules/region_buffer.js"; +import { Cardinal } from "@modules/directions.js"; + +const registerInformation = { + name: "revolve", + permission: "worldedit.region.revolve", + description: "commands.wedit:revolve.description", + usage: [ + { + flag: "a", + }, + { + flag: "s", + }, + { + name: "count", + type: "int", + range: [2, null] as [number, null], + }, + { + name: "start", + type: "float", + default: 0, + }, + { + name: "end", + type: "float", + default: 360, + }, + { + name: "heightDiff", + type: "int", + default: 0, + }, + { + flag: "d", + name: "direction", + type: "Direction", + }, + { + flag: "m", + name: "mask", + type: "Mask", + }, + ], +}; + +registerCommand(registerInformation, function* (session, builder, args) { + assertCuboidSelection(session); + const amount = args.get("count"); + const dir = (args.get("d-direction"))?.getDirection(builder) ?? Vector.UP; + const [start, end] = session.selection.getRange(); + const heightDiff = args.get("heightDiff"); + const [startRotation, endRotation] = [args.get("start"), args.get("end")]; + const origin = Vector.from(builder.location).floor().add(0.5); + const offset = start.sub(origin); + const dim = builder.dimension; + + // [load position, rotation angles] + const loads: [Vector, Vector][] = []; + const points: Vector[] = []; + const [originStart, originEnd] = regionOffset(start, end, offset.mul(-1)); + for (let i = 0; i <= amount; i++) { + const t = i / amount; + const rotation = dir.mul((1 - t) * startRotation + t * endRotation); + const height = dir.mul(Math.round(t * heightDiff)); + const [loadStart, loadEnd] = RegionBuffer.createBounds(originStart, originEnd, { offset: offset.add(height), rotation }); + loads.push([origin.add(height), rotation]); + points.push(loadStart, loadEnd); + } + const revolveRegion = regionBounds(points); + + let count = 0; + const history = session.getHistory(); + const record = history.record(); + const tempRevolve = session.createRegion(true); + + yield* Jobs.run(session, loads.length + 1, function* () { + try { + yield* copy(session, args, tempRevolve); + 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(); + } + yield history.addRedoStructure(record, ...revolveRegion, "any"); + + if (args.has("s")) { + history.recordSelection(record, session); + session.selection.set(0, points[points.length - 2]); + session.selection.set(1, points[points.length - 1]); + history.recordSelection(record, session); + } + + history.commit(record); + } catch (e) { + history.cancel(record); + throw e; + } finally { + session.deleteRegion(tempRevolve); + } + }); + return RawText.translate("commands.wedit:revolve.explain").with(count); +}); diff --git a/texts/en_US.po b/texts/en_US.po index 8c6b4da48..0aca9183e 100644 --- a/texts/en_US.po +++ b/texts/en_US.po @@ -637,6 +637,10 @@ msgid "commands.wedit:removenear.description" msgstr "Remove nearby blocks" msgid "commands.wedit:replacenear.description" msgstr "Replace nearby blocks with other blocks" +msgid "commands.wedit:revolve.description" +msgstr "Make copies of the selection revolving around the player" +msgid "commands.wedit:revolve.explain" +msgstr "Created %s blocks from revolving." msgid "commands.wedit:rotate.description" msgstr "Rotate the selection" msgid "commands.wedit:rotate.explain" From 25cd1b9c547850a5cf56969c4e541e0df18a157d Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sun, 3 Nov 2024 23:54:46 -0500 Subject: [PATCH 09/26] Improved simple pattern/mask logic; Old PE blocks can once again be used in simple block patterns; Block change count more accurate with simple block patterns; Added support for simple negate masks --- src/server/modules/mask.ts | 55 ++++++++++++++++++++++++++------- src/server/modules/pattern.ts | 19 +++--------- src/server/shapes/base_shape.ts | 2 +- src/server/util.ts | 23 +++++++++++++- 4 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/server/modules/mask.ts b/src/server/modules/mask.ts index 5fca3d290..87535bef0 100644 --- a/src/server/modules/mask.ts +++ b/src/server/modules/mask.ts @@ -1,4 +1,4 @@ -import { Vector3, BlockPermutation } from "@minecraft/server"; +import { Vector3, BlockPermutation, BlockFilter } from "@minecraft/server"; import { CustomArgType, commandSyntaxError, Vector } from "@notbeer-api"; import { Token } from "./extern/tokenizr.js"; import { @@ -12,13 +12,14 @@ import { parsedBlock, blockPermutation2ParsedBlock, BlockUnit, - parsedBlock2CommandArg, + parsedBlock2BlockPermutation, } from "./block_parsing.js"; +import { iterateBlockPermutations } from "server/util.js"; export class Mask implements CustomArgType { private condition: MaskNode; private stringObj = ""; - private simpleCache: string[]; + private simpleCache: BlockFilter; constructor(mask = "") { if (mask) { @@ -109,19 +110,51 @@ export class Mask implements CustomArgType { } isSimple() { - return !this.condition || this.condition instanceof BlockMask || (this.condition instanceof ChainMask && this.condition.nodes.every((node) => node instanceof BlockMask)); + const root = this.condition; + const child = root.nodes[0]; + return ( + !root || + root instanceof BlockMask || + (root instanceof ChainMask && root.nodes.every((node) => node instanceof BlockMask)) || + (root instanceof NegateMask && (child instanceof BlockMask || (child instanceof ChainMask && child.nodes.every((node) => node instanceof BlockMask)))) + ); } - getSimpleForCommandArgs() { - if (!this.simpleCache) { - if (this.condition instanceof BlockMask) { - this.simpleCache = [parsedBlock2CommandArg(this.condition.block)]; - } else if (this.condition instanceof ChainMask) { - this.simpleCache = this.condition.nodes.map((node) => parsedBlock2CommandArg((node).block)); + getSimpleBlockFilter() { + if (this.simpleCache) return this.simpleCache; + + const addToFilter = (block: parsedBlock, types: string[], perms: BlockPermutation[]) => { + const perm = parsedBlock2BlockPermutation(block); + if (block.states != null) { + const test = Array.from(block.states.entries()); + for (const states of iterateBlockPermutations(block.id)) { + if (!test.every(([key, value]) => states[key] === value)) continue; + perms.push(BlockPermutation.resolve(block.id, states)); + } } else { - this.simpleCache = []; + types.push(perm.type.id); } + }; + + const includeTypes: string[] = []; + const excludeTypes: string[] = []; + const includePerms: BlockPermutation[] = []; + const excludePerms: BlockPermutation[] = []; + this.simpleCache = {}; + + if (this.condition instanceof BlockMask) addToFilter(this.condition.block, includeTypes, includePerms); + else if (this.condition instanceof ChainMask) this.condition.nodes.forEach((node) => addToFilter((node).block, includeTypes, includePerms)); + else if (this.condition instanceof NegateMask) { + const negated = this.condition.nodes[0]; + if (negated instanceof BlockMask) addToFilter(negated.block, excludeTypes, excludePerms); + else if (negated instanceof ChainMask) negated.nodes.forEach((node) => addToFilter((node).block, excludeTypes, excludePerms)); } + + if (includeTypes.length) this.simpleCache.includeTypes = includeTypes; + if (excludeTypes.length) this.simpleCache.excludeTypes = excludeTypes; + if (includePerms.length) this.simpleCache.includePermutations = includePerms; + if (excludePerms.length) this.simpleCache.excludePermutations = excludePerms; + return this.simpleCache; } diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts index b162642a3..46a7679dd 100644 --- a/src/server/modules/pattern.ts +++ b/src/server/modules/pattern.ts @@ -1,4 +1,4 @@ -import { Vector3, BlockPermutation, Player, Dimension } from "@minecraft/server"; +import { Vector3, BlockPermutation, Player, Dimension, BlockVolume } from "@minecraft/server"; import { CustomArgType, commandSyntaxError, Vector, Server } from "@notbeer-api"; import { PlayerSession } from "server/sessions.js"; import { wrap } from "server/util.js"; @@ -16,7 +16,6 @@ import { blockPermutation2ParsedBlock, parsedBlock2BlockPermutation, BlockUnit, - parsedBlock2CommandArg, } from "./block_parsing.js"; import { Cardinal } from "./directions.js"; import { Mask } from "./mask.js"; @@ -31,7 +30,7 @@ interface patternContext { export class Pattern implements CustomArgType { private block: PatternNode; private stringObj = ""; - private simpleCache: string; + private simpleCache: BlockPermutation; private context = {} as patternContext; @@ -148,20 +147,12 @@ export class Pattern implements CustomArgType { fillSimpleArea(dimension: Dimension, start: Vector3, end: Vector3, mask?: Mask) { if (!this.simpleCache) { if (this.block instanceof BlockPattern) { - this.simpleCache = parsedBlock2CommandArg(this.block.block); + this.simpleCache = parsedBlock2BlockPermutation(this.block.block); } else if (this.block instanceof ChainPattern) { - this.simpleCache = parsedBlock2CommandArg((this.block.nodes[0]).block); + this.simpleCache = parsedBlock2BlockPermutation((this.block.nodes[0]).block); } } - const command = `fill ${start.x} ${start.y} ${start.z} ${end.x} ${end.y} ${end.z} ${this.simpleCache}`; - const maskArgs = mask?.getSimpleForCommandArgs(); - let successCount = 0; - if (maskArgs?.length) { - maskArgs.forEach((m) => (successCount += dimension.runCommand(command + ` replace ${m}`).successCount)); - } else { - successCount += dimension.runCommand(command).successCount; - } - return !!successCount; + return dimension.fillBlocks(new BlockVolume(start, end), this.simpleCache, { blockFilter: mask?.getSimpleBlockFilter() }).getCapacity(); } getSimpleBlockFill() { diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index 1edd9723a..d90ed3b23 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -268,7 +268,7 @@ export abstract class Shape { const [min, max] = block; const volume = regionVolume(min, max); if (Jobs.inContext()) while (!Jobs.loadBlock(min)) yield sleep(1); - if (pattern.fillSimpleArea(dimension, min, max, mask)) count += volume; + count += pattern.fillSimpleArea(dimension, min, max, mask); yield Jobs.setProgress(progress / blocksAffected); progress += volume; } diff --git a/src/server/util.ts b/src/server/util.ts index e0913c513..01022dbae 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -1,4 +1,4 @@ -import { Block, Vector3, Dimension, Entity, Player, RawMessage, BlockComponentTypes } from "@minecraft/server"; +import { Block, Vector3, Dimension, Entity, Player, RawMessage, BlockComponentTypes, BlockPermutation, BlockStates } from "@minecraft/server"; import { Server, RawText, Vector } from "@notbeer-api"; import config from "config.js"; @@ -84,6 +84,27 @@ export function blockHasNBTData(block: Block) { return components.some((component) => !!block.getComponent(component)) || nbt_blocks.includes(block.typeId); } +/** + * Iterates through every possible block permutation for a specified block type. + */ +export function* iterateBlockPermutations(blockType: string) { + const perm = BlockPermutation.resolve(blockType); + const properties = Object.keys(perm.getAllStates()); + const values = properties.map((p) => BlockStates.get(p).validValues); + + function* combine(current: any[], depth: number): Generator, void> { + if (depth === values.length) { + yield Object.fromEntries(current.map((value, i) => [properties[i], value])); + return; + } + + for (let i = 0; i < values[depth].length; i++) { + yield* combine(current.concat(values[depth][i]), depth + 1); + } + } + yield* combine([], 0); +} + /** * Converts a location object to a string. * @param loc The object to convert From d62a1e8ad315c96c2bedbf33e6a77d642ba32c6f Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Mon, 4 Nov 2024 20:26:26 -0500 Subject: [PATCH 10/26] Improved structure of CustomArgTypes --- src/library/classes/commandBuilder.ts | 52 +++++++++++++------------- src/server/commands/clipboard/copy.ts | 2 +- src/server/commands/region/gmask.ts | 2 +- src/server/commands/region/hollow.ts | 2 +- src/server/commands/region/line.ts | 6 +-- src/server/commands/utilities/fill.ts | 3 +- src/server/commands/utilities/fillr.ts | 3 +- src/server/modules/biome_data.ts | 22 +++++------ src/server/modules/directions.ts | 30 +++++++-------- src/server/modules/expression.ts | 22 +++++------ src/server/modules/mask.ts | 24 ++++++------ src/server/modules/pattern.ts | 36 +++++++++--------- src/server/shapes/base_shape.ts | 2 +- src/server/tools/generation_tools.ts | 12 ++---- 14 files changed, 106 insertions(+), 112 deletions(-) diff --git a/src/library/classes/commandBuilder.ts b/src/library/classes/commandBuilder.ts index cfe69e23c..f0371bf31 100644 --- a/src/library/classes/commandBuilder.ts +++ b/src/library/classes/commandBuilder.ts @@ -9,7 +9,7 @@ import { contentLog, RawText } from "../utils/index.js"; export class CustomArgType { static parseArgs: (args: Array, argIndex: number) => argParseResult; - static clone: (argType: CustomArgType) => CustomArgType; + clone: () => CustomArgType; } export class CommandPosition implements CustomArgType { @@ -20,6 +20,29 @@ export class CommandPosition implements CustomArgType { yRelative = true; zRelative = true; + clone() { + const pos = new CommandPosition(); + pos.x = this.x; + pos.y = this.y; + pos.z = this.z; + pos.xRelative = this.xRelative; + pos.yRelative = this.yRelative; + pos.zRelative = this.zRelative; + return pos; + } + + relativeTo(player: Player, isBlockLoc = false): Vector3 { + const loc = { x: 0, y: 0, z: 0 }; + const x = this.x + (this.xRelative ? player.location.x : 0); + const y = this.y + (this.yRelative ? player.location.y : 0); + const z = this.z + (this.zRelative ? player.location.z : 0); + + loc.x = isBlockLoc ? Math.floor(x) : x; + loc.y = isBlockLoc ? Math.floor(y) : y; + loc.z = isBlockLoc ? Math.floor(z) : z; + return loc; + } + static parseArgs(args: Array, index: number, is3d = true) { const pos = new CommandPosition(); for (let i = 0; i < (is3d ? 3 : 2); i++) { @@ -57,29 +80,6 @@ export class CommandPosition implements CustomArgType { } return { result: pos, argIndex: index }; } - - static clone(original: CommandPosition) { - const pos = new CommandPosition(); - pos.x = original.x; - pos.y = original.y; - pos.z = original.z; - pos.xRelative = original.xRelative; - pos.yRelative = original.yRelative; - pos.zRelative = original.zRelative; - return pos; - } - - relativeTo(player: Player, isBlockLoc = false): Vector3 { - const loc = { x: 0, y: 0, z: 0 }; - const x = this.x + (this.xRelative ? player.location.x : 0); - const y = this.y + (this.yRelative ? player.location.y : 0); - const z = this.z + (this.zRelative ? player.location.z : 0); - - loc.x = isBlockLoc ? Math.floor(x) : x; - loc.y = isBlockLoc ? Math.floor(y) : y; - loc.z = isBlockLoc ? Math.floor(z) : z; - return loc; - } } export class CommandBuilder { @@ -443,8 +443,8 @@ export class CommandBuilder { const argDef = argDefs[defIdx]; if (!("flag" in argDef)) { if ("type" in argDef && argDef?.default != undefined && !("subName" in argDef)) { - // TODO: use clone command of customArgType here - result.set(argDef.name, argDef.default); + const def = argDef.default.clone?.() ?? argDef.default; + result.set(argDef.name, def); } else if ("subName" in argDef) { processSubCmd(i, ""); } else { diff --git a/src/server/commands/clipboard/copy.ts b/src/server/commands/clipboard/copy.ts index 0fbe75a84..2981dd2be 100644 --- a/src/server/commands/clipboard/copy.ts +++ b/src/server/commands/clipboard/copy.ts @@ -42,7 +42,7 @@ export function* copy(session: PlayerSession, args: Map, buffer: Re const usingItem = args.get("_using_item"); const includeEntities: boolean = usingItem ? session.includeEntities : args.has("e"); const includeAir: boolean = usingItem ? session.includeAir : !args.has("a"); - const mask: Mask = usingItem ? Mask.clone(session.globalMask) : args.has("m") ? args.get("m-mask") : undefined; + const mask: Mask = usingItem ? session.globalMask.clone() : args.has("m") ? args.get("m-mask") : undefined; if (!buffer) { if (session.clipboard) session.deleteRegion(session.clipboard); diff --git a/src/server/commands/region/gmask.ts b/src/server/commands/region/gmask.ts index ee966a079..e36abf8d7 100644 --- a/src/server/commands/region/gmask.ts +++ b/src/server/commands/region/gmask.ts @@ -16,7 +16,7 @@ const registerInformation = { }; registerCommand(registerInformation, function (session, builder, args) { - session.globalMask = Mask.clone(args.get("mask")); + session.globalMask = args.get("mask"); if (!args.get("mask").empty()) { return RawText.translate("commands.wedit:gmask.set"); } else { diff --git a/src/server/commands/region/hollow.ts b/src/server/commands/region/hollow.ts index f4e0d615b..3a3e28ed8 100644 --- a/src/server/commands/region/hollow.ts +++ b/src/server/commands/region/hollow.ts @@ -34,7 +34,7 @@ function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): G max.y = Math.min(maxY, max.y); const canGenerate = max.y >= min.y; - pattern.setContext(session, [min, max]); + pattern = pattern.withContext(session, [min, max]); const history = session.getHistory(); const record = history.record(); diff --git a/src/server/commands/region/line.ts b/src/server/commands/region/line.ts index 808175282..d5f52e49a 100644 --- a/src/server/commands/region/line.ts +++ b/src/server/commands/region/line.ts @@ -103,16 +103,14 @@ registerCommand(registerInformation, function* (session, builder, args) { throw "worldEdit.selectionFill.noPattern"; } - const dim = builder.dimension; - const pattern: Pattern = args.get("_using_item") ? session.globalPattern : args.get("pattern"); - let pos1: Vector3, pos2: Vector3, start: Vector3, end: Vector3; if (session.selection.mode == "cuboid") { [pos1, pos2] = session.selection.points; [start, end] = session.selection.getRange(); } - pattern.setContext(session, [start, end]); + const dim = builder.dimension; + const pattern = ((args.get("_using_item") ? session.globalPattern : args.get("pattern"))).withContext(session, [start, end]); let count: number; yield* Jobs.run(session, 1, function* () { diff --git a/src/server/commands/utilities/fill.ts b/src/server/commands/utilities/fill.ts index 206e0c42a..56a93439c 100644 --- a/src/server/commands/utilities/fill.ts +++ b/src/server/commands/utilities/fill.ts @@ -39,7 +39,6 @@ interface fillContext extends FloodFillContext { registerCommand(registerInformation, function* (session, builder, args) { const dimension = builder.dimension; const fillDir = (args.get("direction") as Cardinal).getDirection(builder); - const pattern: Pattern = args.get("pattern"); const depth: number = args.get("depth"); const startBlock = session.getPlacementPosition(); @@ -58,7 +57,7 @@ registerCommand(registerInformation, function* (session, builder, args) { if (!blocks.length) return blocks; const [min, max] = regionBounds(blocks); - pattern.setContext(session, [min, max]); + const pattern = (args.get("pattern")).withContext(session, [min, max]); const history = session.getHistory(); const record = history.record(); diff --git a/src/server/commands/utilities/fillr.ts b/src/server/commands/utilities/fillr.ts index fe9cabc71..4a41ec42f 100644 --- a/src/server/commands/utilities/fillr.ts +++ b/src/server/commands/utilities/fillr.ts @@ -35,7 +35,6 @@ const registerInformation = { registerCommand(registerInformation, function* (session, builder, args) { const dimension = builder.dimension; const fillDir = (args.get("direction") as Cardinal).getDirection(builder); - const pattern: Pattern = args.get("pattern"); const depth: number = args.get(args.get("depth") == -1 ? "radius" : "depth"); const startBlock = session.getPlacementPosition(); @@ -52,7 +51,7 @@ registerCommand(registerInformation, function* (session, builder, args) { if (!blocks.length) return blocks; const [min, max] = regionBounds(blocks); - pattern.setContext(session, [min, max]); + const pattern = (args.get("pattern")).withContext(session, [min, max]); const history = session.getHistory(); const record = history.record(); diff --git a/src/server/modules/biome_data.ts b/src/server/modules/biome_data.ts index f54d5a42a..f87c7069f 100644 --- a/src/server/modules/biome_data.ts +++ b/src/server/modules/biome_data.ts @@ -25,6 +25,17 @@ class Biome implements CustomArgType { return this.name; } + clone() { + const clone = new Biome(); + clone.id = this.id; + clone.name = this.name; + return clone; + } + + toString() { + return `[biome: ${this.name}/${this.id}]`; + } + static parseArgs(args: string[], index = 0) { const input = args[index]; const result = new Biome(); @@ -49,17 +60,6 @@ class Biome implements CustomArgType { } return { result, argIndex: index + 1 }; } - - static clone(original: Biome) { - const clone = new Biome(); - clone.id = original.id; - clone.name = original.name; - return clone; - } - - toString() { - return `[biome: ${this.name}/${this.id}]`; - } } class BiomeChanges { diff --git a/src/server/modules/directions.ts b/src/server/modules/directions.ts index 7da5167a9..cb4b068c5 100644 --- a/src/server/modules/directions.ts +++ b/src/server/modules/directions.ts @@ -50,22 +50,9 @@ export class Cardinal implements CustomArgType { }[dir]; } - static parseArgs(args: Array, index = 0) { - const dir = args[index][0].toLowerCase(); - if (!directions.includes(dir) && !dirAliases.includes(dir)) { - throw RawText.translate("commands.generic.wedit:invalidDir").with(args[index]); - /*printDebug(dir); - printDebug(dir in directions);*/ - } - - const cardinal = new Cardinal(); - cardinal.direction = dir; - return { result: cardinal, argIndex: index + 1 }; - } - - static clone(original: Cardinal) { + clone() { const cardinal = new Cardinal(); - cardinal.direction = original.direction; + cardinal.direction = this.direction; return cardinal; } @@ -111,4 +98,17 @@ export class Cardinal implements CustomArgType { getDirectionLetter() { return this.direction; } + + static parseArgs(args: Array, index = 0) { + const dir = args[index][0].toLowerCase(); + if (!directions.includes(dir) && !dirAliases.includes(dir)) { + throw RawText.translate("commands.generic.wedit:invalidDir").with(args[index]); + /*printDebug(dir); + printDebug(dir in directions);*/ + } + + const cardinal = new Cardinal(); + cardinal.direction = dir; + return { result: cardinal, argIndex: index + 1 }; + } } diff --git a/src/server/modules/expression.ts b/src/server/modules/expression.ts index ce4f4ac2c..31b235996 100644 --- a/src/server/modules/expression.ts +++ b/src/server/modules/expression.ts @@ -31,6 +31,17 @@ export class Expression implements CustomArgType { return new Function(...variables, "return " + this.root.compile()); } + clone() { + const expression = new Expression(); + expression.root = this.root; + expression.stringObj = this.stringObj; + return expression; + } + + toString() { + return `[expression: ${this.stringObj}]`; + } + static parseArgs(args: Array, index = 0) { const input = args[index]; if (!input) { @@ -151,17 +162,6 @@ export class Expression implements CustomArgType { return { result: expression, argIndex: index + 1 }; } - - static clone(original: Expression) { - const expression = new Expression(); - expression.root = original.root; - expression.stringObj = original.stringObj; - return expression; - } - - toString() { - return `[expression: ${this.stringObj}]`; - } } abstract class ExpressionNode implements AstNode { diff --git a/src/server/modules/mask.ts b/src/server/modules/mask.ts index 87535bef0..75dcb758b 100644 --- a/src/server/modules/mask.ts +++ b/src/server/modules/mask.ts @@ -111,7 +111,7 @@ export class Mask implements CustomArgType { isSimple() { const root = this.condition; - const child = root.nodes[0]; + const child = root?.nodes[0]; return ( !root || root instanceof BlockMask || @@ -158,6 +158,17 @@ export class Mask implements CustomArgType { return this.simpleCache; } + clone() { + const mask = new Mask(); + mask.condition = this.condition; + mask.stringObj = this.stringObj; + return mask; + } + + toString() { + return `[mask: ${this.stringObj}]`; + } + static parseArgs(args: Array, index = 0) { const input = args[index]; if (!input) { @@ -288,17 +299,6 @@ export class Mask implements CustomArgType { return { result: mask, argIndex: index + 1 }; } - - static clone(original: Mask) { - const mask = new Mask(); - mask.condition = original.condition; - mask.stringObj = original.stringObj; - return mask; - } - - toString() { - return `[mask: ${this.stringObj}]`; - } } abstract class MaskNode implements AstNode { diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts index 46a7679dd..45a1edaec 100644 --- a/src/server/modules/pattern.ts +++ b/src/server/modules/pattern.ts @@ -42,16 +42,18 @@ export class Pattern implements CustomArgType { } } - setContext(session: PlayerSession, range?: [Vector3, Vector3]) { - this.context.session = session; - this.context.range = [Vector.from(range[0]), Vector.from(range[1])]; - this.context.cardinal = new Cardinal(Cardinal.Dir.FORWARD); + withContext(session: PlayerSession, range: [Vector3, Vector3]) { + const pattern = this.clone(); + pattern.context.session = session; + pattern.context.range = [Vector.from(range[0]), Vector.from(range[1])]; + pattern.context.cardinal = new Cardinal(Cardinal.Dir.FORWARD); try { const item = Server.player.getHeldItem(session.getPlayer()); - this.context.hand = Server.block.itemToPermutation(item); + pattern.context.hand = Server.block.itemToPermutation(item); } catch { - this.context.hand = BlockPermutation.resolve("minecraft:air"); + pattern.context.hand = BlockPermutation.resolve("minecraft:air"); } + return pattern; } /** @@ -163,6 +165,17 @@ export class Pattern implements CustomArgType { } } + clone() { + const pattern = new Pattern(); + pattern.block = this.block; + pattern.stringObj = this.stringObj; + return pattern; + } + + toString() { + return `[pattern: ${this.stringObj}]`; + } + static parseArgs(args: Array, index = 0) { const input = args[index]; if (!input) { @@ -302,17 +315,6 @@ export class Pattern implements CustomArgType { return { result: pattern, argIndex: index + 1 }; } - - static clone(original: Pattern) { - const pattern = new Pattern(); - pattern.block = original.block; - pattern.stringObj = original.stringObj; - return pattern; - } - - toString() { - return `[pattern: ${this.stringObj}]`; - } } type NodeJSON = { type: string; nodes: NodeJSON[] }; diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index d90ed3b23..c7cdf2312 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -172,7 +172,7 @@ export abstract class Shape { min.y = Math.max(minY, min.y); max.y = Math.min(maxY, max.y); const canGenerate = max.y >= min.y; - pattern.setContext(session, [min, max]); + pattern = pattern.withContext(session, [min, max]); if (!Jobs.inContext()) assertCanBuildWithin(player, min, max); let blocksAffected = 0; diff --git a/src/server/tools/generation_tools.ts b/src/server/tools/generation_tools.ts index c13a23bd9..47cde575d 100644 --- a/src/server/tools/generation_tools.ts +++ b/src/server/tools/generation_tools.ts @@ -81,8 +81,7 @@ class DrawLineTool extends GeneratorTool { self.clearFirstPos(session); const dim = player.dimension; - const pattern = session.globalPattern; - pattern.setContext(session, [start, end]); + const pattern = session.globalPattern.withContext(session, [start, end]); const history = session.getHistory(); const record = history.record(); @@ -149,8 +148,7 @@ class DrawSphereTool extends GeneratorTool { const center = self.getFirstPos(session); const radius = Math.floor(self.traceForPos(player).sub(center).length); const sphereShape = new SphereShape(radius); - const pattern = session.globalPattern; - pattern.setContext(session, sphereShape.getRegion(center)); + const pattern = session.globalPattern.withContext(session, sphereShape.getRegion(center)); self.clearFirstPos(session); const count = yield* Jobs.run(session, 2, sphereShape.generate(center, pattern, null, session)); @@ -191,8 +189,7 @@ class DrawCylinderTool extends GeneratorTool { if (self.baseUse(player, session, loc)) return; const [shape, center] = self.getShape(player, session); - const pattern = session.globalPattern; - pattern.setContext(session, shape.getRegion(center)); + const pattern = session.globalPattern.withContext(session, shape.getRegion(center)); self.clearFirstPos(session); const count = yield* Jobs.run(session, 2, shape.generate(center, pattern, null, session)); @@ -233,8 +230,7 @@ class DrawPyramidTool extends GeneratorTool { if (self.baseUse(player, session, loc)) return; const [shape, center] = self.getShape(player, session); - const pattern = session.globalPattern; - pattern.setContext(session, shape.getRegion(center)); + const pattern = session.globalPattern.withContext(session, shape.getRegion(center)); self.clearFirstPos(session); const count = yield* Jobs.run(session, 2, shape.generate(center, pattern, null, session)); From 783558ca9676ebc7cb66a1ed85b75c93f2dded23 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Mon, 4 Nov 2024 20:40:05 -0500 Subject: [PATCH 11/26] Made masks context aware --- src/server/brushes/erosion_brush.ts | 2 +- src/server/brushes/overlay_brush.ts | 5 +++-- src/server/brushes/structure_brush.ts | 3 ++- src/server/commands/clipboard/copy.ts | 2 +- src/server/commands/clipboard/paste.ts | 2 +- src/server/commands/region/hollow.ts | 3 ++- src/server/commands/region/line.ts | 11 ++++------- src/server/commands/region/smooth_func.ts | 3 ++- src/server/commands/region/transform_func.ts | 1 + src/server/commands/selection/count.ts | 2 +- src/server/commands/selection/trim.ts | 2 +- src/server/modules/mask.ts | 13 +++++++++++++ src/server/modules/player_util.ts | 2 ++ src/server/shapes/base_shape.ts | 2 +- src/server/tools/generation_tools.ts | 5 ++--- src/server/tools/stacker_tool.ts | 5 +++-- 16 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/server/brushes/erosion_brush.ts b/src/server/brushes/erosion_brush.ts index 8ab7280f1..b3286cb27 100644 --- a/src/server/brushes/erosion_brush.ts +++ b/src/server/brushes/erosion_brush.ts @@ -79,7 +79,7 @@ export class ErosionBrush extends Brush { public *apply(loc: Vector, session: PlayerSession, mask?: Mask) { const range: [Vector, Vector] = [loc.sub(this.radius), loc.add(this.radius)]; const [minY, maxY] = getWorldHeightLimits(session.getPlayer().dimension); - const activeMask = !mask ? session.globalMask : session.globalMask ? mask.intersect(session.globalMask) : mask; + const activeMask = (!mask ? session.globalMask : session.globalMask ? mask.intersect(session.globalMask) : mask)?.withContext(session); range[0].y = Math.max(minY, range[0].y); range[1].y = Math.min(maxY, range[1].y); diff --git a/src/server/brushes/overlay_brush.ts b/src/server/brushes/overlay_brush.ts index 20a7d1b19..36bc3054c 100644 --- a/src/server/brushes/overlay_brush.ts +++ b/src/server/brushes/overlay_brush.ts @@ -56,7 +56,8 @@ export class OverlayBrush extends Brush { public *apply(hit: Vector, session: PlayerSession, mask?: Mask) { const range: [Vector, Vector] = [hit.offset(-this.radius, 1, -this.radius), hit.offset(this.radius, 1, this.radius)]; const minY = getWorldHeightLimits(session.getPlayer().dimension)[0]; - const activeMask = !mask ? session.globalMask : session.globalMask ? mask.intersect(session.globalMask) : mask; + const activeMask = (!mask ? session.globalMask : session.globalMask ? mask.intersect(session.globalMask) : mask)?.withContext(session); + const surfaceMask = this.surfaceMask.withContext(session); const isAirOrFluid = Server.block.isAirOrFluid; const r2 = Math.pow(this.radius + 0.5, 2); @@ -72,7 +73,7 @@ export class OverlayBrush extends Brush { const trace = Vector.sub(loc, [0, 1, 0]); while (trace.y >= minY) { const block = blockChanges.getBlock(trace); - if (!isAirOrFluid(block.permutation) && this.surfaceMask.matchesBlock(block)) { + if (!isAirOrFluid(block.permutation) && surfaceMask.matchesBlock(block)) { for (let i = 0; i < Math.abs(this.depth); i++) { const block = blockChanges.getBlock(trace.offset(0, this.depth > 0 ? -i : i + 1, 0)); if (!activeMask || activeMask.matchesBlock(block)) { diff --git a/src/server/brushes/structure_brush.ts b/src/server/brushes/structure_brush.ts index e232e9994..2f9763b6a 100644 --- a/src/server/brushes/structure_brush.ts +++ b/src/server/brushes/structure_brush.ts @@ -72,7 +72,8 @@ export class StructureBrush extends Brush { let start = loc.offset(-regionSize.x / 2, 1, -regionSize.z / 2).ceil(); let end = start.add(regionSize).sub(1); const center = start.add(end.add(1)).mul(0.5); - const options: RegionLoadOptions = { offset: start.sub(center), mask: this.mask }; + const mask = this.mask.withContext(session); + const options: RegionLoadOptions = { offset: start.sub(center), mask }; if (this.randomTransform) { const newTransform = this.lastTransform.slice() as typeof this.lastTransform; while (newTransform[0] == this.lastTransform[0] && newTransform[1].equals(this.lastTransform[1])) { diff --git a/src/server/commands/clipboard/copy.ts b/src/server/commands/clipboard/copy.ts index 2981dd2be..9bc3c1ac5 100644 --- a/src/server/commands/clipboard/copy.ts +++ b/src/server/commands/clipboard/copy.ts @@ -42,7 +42,7 @@ export function* copy(session: PlayerSession, args: Map, buffer: Re const usingItem = args.get("_using_item"); const includeEntities: boolean = usingItem ? session.includeEntities : args.has("e"); const includeAir: boolean = usingItem ? session.includeAir : !args.has("a"); - const mask: Mask = usingItem ? session.globalMask.clone() : args.has("m") ? args.get("m-mask") : undefined; + const mask = (usingItem ? session.globalMask.clone() : args.get("m-mask"))?.withContext(session); if (!buffer) { if (session.clipboard) session.deleteRegion(session.clipboard); diff --git a/src/server/commands/clipboard/paste.ts b/src/server/commands/clipboard/paste.ts index adb397788..677679c3f 100644 --- a/src/server/commands/clipboard/paste.ts +++ b/src/server/commands/clipboard/paste.ts @@ -49,7 +49,7 @@ registerCommand(registerInformation, function* (session, builder, args) { if (pasteContent) { 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") }); + yield* session.clipboard.load(pasteFrom, builder.dimension, { ...transform, mask: args.get("m-mask")?.withContext(session) }); yield history.addRedoStructure(record, pasteStart, pasteEnd, "any"); } if (setSelection) { diff --git a/src/server/commands/region/hollow.ts b/src/server/commands/region/hollow.ts index 3a3e28ed8..312fafdfa 100644 --- a/src/server/commands/region/hollow.ts +++ b/src/server/commands/region/hollow.ts @@ -30,6 +30,7 @@ function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): G const [min, max] = session.selection.getRange(); const dimension = session.getPlayer().dimension; const [minY, maxY] = getWorldHeightLimits(dimension); + const mask = session.globalMask.withContext(session); min.y = Math.max(minY, min.y); max.y = Math.min(maxY, max.y); const canGenerate = max.y >= min.y; @@ -110,7 +111,7 @@ function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): G yield history.addUndoStructure(record, min, max); for (const locString of locStringSet) { const block = dimension.getBlock(stringToLoc(locString)); - if (session.globalMask.matchesBlock(block) && pattern.setBlock(block)) count++; + if (mask.matchesBlock(block) && pattern.setBlock(block)) count++; if (iterateChunk()) yield Jobs.setProgress(progress / volume); progress++; } diff --git a/src/server/commands/region/line.ts b/src/server/commands/region/line.ts index d5f52e49a..6912b8391 100644 --- a/src/server/commands/region/line.ts +++ b/src/server/commands/region/line.ts @@ -96,12 +96,8 @@ export function* generateLine(p1: Vector, p2: Vector): Generator registerCommand(registerInformation, function* (session, builder, args) { assertCuboidSelection(session); - if (session.selection.mode != "cuboid") { - throw "commands.wedit:line.invalidType"; - } - if (args.get("_using_item") && session.globalPattern.empty()) { - throw "worldEdit.selectionFill.noPattern"; - } + if (session.selection.mode != "cuboid") throw "commands.wedit:line.invalidType"; + if (args.get("_using_item") && session.globalPattern.empty()) throw "worldEdit.selectionFill.noPattern"; let pos1: Vector3, pos2: Vector3, start: Vector3, end: Vector3; if (session.selection.mode == "cuboid") { @@ -111,6 +107,7 @@ registerCommand(registerInformation, function* (session, builder, args) { const dim = builder.dimension; const pattern = ((args.get("_using_item") ? session.globalPattern : args.get("pattern"))).withContext(session, [start, end]); + const mask = session.globalMask.withContext(session); let count: number; yield* Jobs.run(session, 1, function* () { @@ -126,7 +123,7 @@ registerCommand(registerInformation, function* (session, builder, args) { block = Jobs.loadBlock(point); yield sleep(1); } - if (session.globalMask.matchesBlock(block) && pattern.setBlock(block)) { + if (mask.matchesBlock(block) && pattern.setBlock(block)) { count++; } yield; diff --git a/src/server/commands/region/smooth_func.ts b/src/server/commands/region/smooth_func.ts index fec464647..79198d623 100644 --- a/src/server/commands/region/smooth_func.ts +++ b/src/server/commands/region/smooth_func.ts @@ -18,7 +18,8 @@ export function* smooth(session: PlayerSession, iter: number, shape: Shape, loc: const [minY, maxY] = getWorldHeightLimits(dim); range[0].y = Math.max(range[0].y, minY); range[1].y = Math.min(range[1].y, maxY); - mask = mask ? mask.intersect(session.globalMask) : session.globalMask; + mask = (mask ? mask.intersect(session.globalMask) : session.globalMask)?.withContext(session); + heightMask = heightMask.withContext(session); function getMap(arr: map, x: number, z: number) { return arr[x]?.[z] ?? null; diff --git a/src/server/commands/region/transform_func.ts b/src/server/commands/region/transform_func.ts index 761caa9a1..293d5488b 100644 --- a/src/server/commands/region/transform_func.ts +++ b/src/server/commands/region/transform_func.ts @@ -20,6 +20,7 @@ export function* transformSelection(session: PlayerSession, builder: Player, arg const center = Vector.from(start).add(end.add(1)).mul(0.5); const origin = args.has("o") ? center : Vector.from(builder.location).floor().add(0.5); options = { offset: start.sub(origin), ...options }; + options.mask = options.mask?.withContext(session); yield Jobs.nextStep("Gettings blocks..."); yield* temp.save(start, end, dim); diff --git a/src/server/commands/selection/count.ts b/src/server/commands/selection/count.ts index 2d9b3576c..0683bab13 100644 --- a/src/server/commands/selection/count.ts +++ b/src/server/commands/selection/count.ts @@ -18,7 +18,7 @@ const registerInformation = { registerCommand(registerInformation, function* (session, builder, args) { assertSelection(session); - const mask = args.get("mask") as Mask; + const mask = (args.get("mask")).withContext(session); const dimension = builder.dimension; const total = session.selection.getBlockCount(); diff --git a/src/server/commands/selection/trim.ts b/src/server/commands/selection/trim.ts index e0bd9c373..e24e21b04 100644 --- a/src/server/commands/selection/trim.ts +++ b/src/server/commands/selection/trim.ts @@ -18,7 +18,7 @@ const registerInformation = { registerCommand(registerInformation, function* (session, builder, args) { assertCuboidSelection(session); - const mask: Mask = args.get("mask"); + const mask = (args.get("mask")).withContext(session); const [min, max] = session.selection.getRange(); const dimension = builder.dimension; diff --git a/src/server/modules/mask.ts b/src/server/modules/mask.ts index 75dcb758b..25c774882 100644 --- a/src/server/modules/mask.ts +++ b/src/server/modules/mask.ts @@ -15,12 +15,19 @@ import { parsedBlock2BlockPermutation, } from "./block_parsing.js"; import { iterateBlockPermutations } from "server/util.js"; +import { PlayerSession } from "server/sessions.js"; + +interface maskContext { + placePosition: Vector; +} export class Mask implements CustomArgType { private condition: MaskNode; private stringObj = ""; private simpleCache: BlockFilter; + private context = {} as maskContext; + constructor(mask = "") { if (mask) { const obj = Mask.parseArgs([mask]).result; @@ -29,6 +36,12 @@ export class Mask implements CustomArgType { } } + withContext(session: PlayerSession) { + const mask = this.clone(); + mask.context.placePosition = session.getPlacementPosition(); + return mask; + } + /** * Tests if this mask matches a block * @param block diff --git a/src/server/modules/player_util.ts b/src/server/modules/player_util.ts index 599984a56..e6b6885fc 100644 --- a/src/server/modules/player_util.ts +++ b/src/server/modules/player_util.ts @@ -3,6 +3,7 @@ import { Server, contentLog, Vector } from "@notbeer-api"; import { Mask } from "./mask.js"; import config from "config.js"; import { getViewVector, getWorldHeightLimits } from "server/util.js"; +import { getSession, hasSession } from "server/sessions.js"; /** * This singleton holds utility and miscellaneous functions for players. @@ -82,6 +83,7 @@ class PlayerHandler { * @return The location of the block the ray hits or reached its range at; null otherwise */ traceForBlock(player: Player, range?: number, mask?: Mask) { + if (mask && hasSession(player.id)) mask = mask?.withContext(getSession(player)); const start = player.getHeadLocation(); const dir = getViewVector(player); const dim = player.dimension; diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index c7cdf2312..3124fdafa 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -190,7 +190,7 @@ export abstract class Shape { // TODO: Localize let activeMask = mask; const globalMask = options?.ignoreGlobalMask ?? false ? new Mask() : session.globalMask; - activeMask = !activeMask ? globalMask : globalMask ? mask.intersect(globalMask) : activeMask; + activeMask = (!activeMask ? globalMask : globalMask ? mask.intersect(globalMask) : activeMask)?.withContext(session); const simple = pattern.isSimple() && (!mask || mask.isSimple()); let progress = 0; diff --git a/src/server/tools/generation_tools.ts b/src/server/tools/generation_tools.ts index 47cde575d..b817551c3 100644 --- a/src/server/tools/generation_tools.ts +++ b/src/server/tools/generation_tools.ts @@ -82,6 +82,7 @@ class DrawLineTool extends GeneratorTool { const dim = player.dimension; const pattern = session.globalPattern.withContext(session, [start, end]); + const mask = session.globalMask.withContext(session); const history = session.getHistory(); const record = history.record(); @@ -92,9 +93,7 @@ class DrawLineTool extends GeneratorTool { count = 0; for (const point of points) { const block = dim.getBlock(point); - if (session.globalMask.matchesBlock(block) && pattern.setBlock(block)) { - count++; - } + if (mask.matchesBlock(block) && pattern.setBlock(block)) count++; yield; } diff --git a/src/server/tools/stacker_tool.ts b/src/server/tools/stacker_tool.ts index 1a3f99535..3209b883d 100644 --- a/src/server/tools/stacker_tool.ts +++ b/src/server/tools/stacker_tool.ts @@ -16,13 +16,14 @@ class StackerTool extends Tool { const dim = player.dimension; const dir = new Cardinal(Cardinal.Dir.BACK).getDirection(player); const start = loc.add(dir); - if (!self.mask.matchesBlock(dim.getBlock(start))) { + const mask = self.mask.withContext(session); + if (!mask.matchesBlock(dim.getBlock(start))) { return; } let end = loc; for (let i = 0; i < self.range; i++) { end = end.add(dir); - if (!self.mask.matchesBlock(dim.getBlock(end.add(dir)))) break; + if (!mask.matchesBlock(dim.getBlock(end.add(dir)))) break; } const history = session.getHistory(); const record = history.record(); From cd77f1cf8095f680fb8c05aa27f26de7a8307ce8 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Mon, 4 Nov 2024 21:45:39 -0500 Subject: [PATCH 12/26] Added #shadow mask --- src/library/utils/vector.ts | 22 ++++++++++--- src/server/modules/mask.ts | 65 +++++++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/library/utils/vector.ts b/src/library/utils/vector.ts index de3a6cf56..9493b61bc 100644 --- a/src/library/utils/vector.ts +++ b/src/library/utils/vector.ts @@ -1,10 +1,19 @@ -import { Vector3 } from "@minecraft/server"; +import { Direction, Vector3 } from "@minecraft/server"; import { Matrix } from "./matrix"; -type anyVec = Vector3 | [number, number, number]; +type anyVec = Vector3 | [number, number, number] | Direction; export type axis = "x" | "y" | "z"; +const DIRECTION_VECTORS: Record = { + [Direction.Up]: [0, 1, 0], + [Direction.Down]: [0, -1, 0], + [Direction.North]: [0, 0, -1], + [Direction.South]: [0, 0, 1], + [Direction.East]: [1, 0, 0], + [Direction.West]: [-1, 0, 0], +}; + export class Vector { private vals: [number, number, number] = [0, 0, 0]; @@ -28,9 +37,8 @@ export class Vector { } static from(loc: anyVec) { - if (Array.isArray(loc)) { - return new Vector(...loc); - } + if (Array.isArray(loc)) return new Vector(...loc); + else if (typeof loc === "string") return new Vector(...DIRECTION_VECTORS[loc]); return new Vector(loc.x, loc.y, loc.z); } @@ -50,6 +58,10 @@ export class Vector { return Vector.from(a).max(b); } + static equals(a: anyVec, b: anyVec) { + return Vector.from(a).equals(b); + } + constructor(x: number, y: number, z: number) { this.vals = [x, y, z]; } diff --git a/src/server/modules/mask.ts b/src/server/modules/mask.ts index 25c774882..ab095940a 100644 --- a/src/server/modules/mask.ts +++ b/src/server/modules/mask.ts @@ -1,4 +1,4 @@ -import { Vector3, BlockPermutation, BlockFilter } from "@minecraft/server"; +import { Vector3, BlockPermutation, BlockFilter, Direction } from "@minecraft/server"; import { CustomArgType, commandSyntaxError, Vector } from "@notbeer-api"; import { Token } from "./extern/tokenizr.js"; import { @@ -38,7 +38,7 @@ export class Mask implements CustomArgType { withContext(session: PlayerSession) { const mask = this.clone(); - mask.context.placePosition = session.getPlacementPosition(); + mask.context.placePosition = Vector.from(session.getPlayer().getHeadLocation()); //session.getPlacementPosition().add(0.5); return mask; } @@ -48,10 +48,8 @@ export class Mask implements CustomArgType { * @returns True if the block matches; false otherwise */ matchesBlock(block: BlockUnit) { - if (this.empty()) { - return true; - } - return this.condition.matchesBlock(block); + if (this.empty()) return true; + return this.condition.matchesBlock(block, this.context); } clear() { @@ -184,9 +182,7 @@ export class Mask implements CustomArgType { static parseArgs(args: Array, index = 0) { const input = args[index]; - if (!input) { - return { result: new Mask(), argIndex: index + 1 }; - } + if (!input) return { result: new Mask(), argIndex: index + 1 }; const tokens = tokenize(input); let token: Token; @@ -233,6 +229,8 @@ export class Mask implements CustomArgType { out.push(new ExistingMask(nodeToken())); } else if (t.value == "surface" || t.value == "exposed") { out.push(new SurfaceMask(nodeToken())); + } else if (t.value == "shadow") { + out.push(new ShadowMask(nodeToken())); } else if (t.value == "#") { const id = tokens.next(); if (id.type != "id") { @@ -321,7 +319,7 @@ abstract class MaskNode implements AstNode { constructor(public readonly token: Token) {} - abstract matchesBlock(block: BlockUnit): boolean; + abstract matchesBlock(block: BlockUnit, context: maskContext): boolean; // eslint-disable-next-line @typescript-eslint/no-empty-function postProcess() {} @@ -403,6 +401,36 @@ class ExistingMask extends MaskNode { } } +class ShadowMask extends MaskNode { + readonly prec = -1; + readonly opCount = 0; + + static readonly testFaces = [ + Vector.from(Direction.Up).mul(0.5), + Vector.from(Direction.Down).mul(0.5), + Vector.from(Direction.North).mul(0.5), + Vector.from(Direction.South).mul(0.5), + Vector.from(Direction.East).mul(0.5), + Vector.from(Direction.West).mul(0.5), + ]; + + matchesBlock(block: BlockUnit, context: maskContext) { + const start = context.placePosition; + const toBlock = Vector.sub(Vector.add(block.location, [0.5, 0.5, 0.5]), start); + for (const face of ShadowMask.testFaces) { + if (face.dot(toBlock) > 0) continue; + const target = Vector.add(block.location, face).add(0.5); + const ray = Vector.sub(target, start); + const hit = block.dimension.getBlockFromRay(start, ray, { includePassableBlocks: false, includeLiquidBlocks: false }); + if (!hit) return false; + + const hitLocation = Vector.add(hit.block, hit.faceLocation); + if (Vector.sub(hitLocation, start).length > ray.length + 0.01 || Vector.equals(hit.block, block.location)) return false; + } + return true; + } +} + class TagMask extends MaskNode { readonly prec = -1; readonly opCount = 0; @@ -439,9 +467,9 @@ class ChainMask extends MaskNode { readonly prec = 3; readonly opCount = 2; - matchesBlock(block: BlockUnit) { + matchesBlock(block: BlockUnit, context: maskContext) { for (const mask of this.nodes) { - if (mask.matchesBlock(block)) return true; + if (mask.matchesBlock(block, context)) return true; } return false; } @@ -470,9 +498,9 @@ class IntersectMask extends MaskNode { readonly prec = 1; readonly opCount = 2; - matchesBlock(block: BlockUnit) { + matchesBlock(block: BlockUnit, context: maskContext) { for (const mask of this.nodes) { - if (!mask.matchesBlock(block)) return false; + if (!mask.matchesBlock(block, context)) return false; } return true; } @@ -501,8 +529,8 @@ class NegateMask extends MaskNode { readonly prec = 2; readonly opCount = 1; - matchesBlock(block: BlockUnit) { - return !this.nodes[0].matchesBlock(block); + matchesBlock(block: BlockUnit, context: maskContext) { + return !this.nodes[0].matchesBlock(block, context); } } @@ -520,14 +548,15 @@ class OffsetMask extends MaskNode { super(token); } - matchesBlock(block: BlockUnit) { + matchesBlock(block: BlockUnit, context: maskContext) { const loc = block.location; return this.nodes[0].matchesBlock( block.dimension.getBlock({ x: loc.x + this.x, y: loc.y + this.y, z: loc.z + this.z, - }) + }), + context ); } From 1d569d32192ed4c03fb7b7b0997a5192d3426073 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Mon, 4 Nov 2024 22:42:36 -0500 Subject: [PATCH 13/26] made placePosition used by #shadow not be the player's head --- src/server/modules/mask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/modules/mask.ts b/src/server/modules/mask.ts index ab095940a..1f124705d 100644 --- a/src/server/modules/mask.ts +++ b/src/server/modules/mask.ts @@ -38,7 +38,7 @@ export class Mask implements CustomArgType { withContext(session: PlayerSession) { const mask = this.clone(); - mask.context.placePosition = Vector.from(session.getPlayer().getHeadLocation()); //session.getPlacementPosition().add(0.5); + mask.context.placePosition = session.getPlacementPosition().add(0.5); return mask; } From 2b185e901fea2efe73544d2caa64db4dd4b1af83 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Tue, 5 Nov 2024 14:54:00 -0500 Subject: [PATCH 14/26] Small improvement to vector length calculation --- src/library/utils/vector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library/utils/vector.ts b/src/library/utils/vector.ts index 9493b61bc..e92c33fd8 100644 --- a/src/library/utils/vector.ts +++ b/src/library/utils/vector.ts @@ -92,7 +92,7 @@ export class Vector { } get length() { - return Math.hypot(this.x, this.y, this.z); + return Math.hypot(...this.vals); } set length(val: number) { From 5cbca06da02e2064047d7e83a1c84634fd8f0f71 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Tue, 5 Nov 2024 21:36:41 -0500 Subject: [PATCH 15/26] Added "light" gradient shape --- src/server/modules/pattern.ts | 34 +++++++++++++++++++++++++++------- src/server/sessions.ts | 2 +- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts index 45a1edaec..cd1eaa7b4 100644 --- a/src/server/modules/pattern.ts +++ b/src/server/modules/pattern.ts @@ -24,6 +24,7 @@ interface patternContext { session: PlayerSession; hand: BlockPermutation; range: [Vector, Vector]; + placePosition: Vector; cardinal: Cardinal; } @@ -47,6 +48,7 @@ export class Pattern implements CustomArgType { pattern.context.session = session; pattern.context.range = [Vector.from(range[0]), Vector.from(range[1])]; pattern.context.cardinal = new Cardinal(Cardinal.Dir.FORWARD); + pattern.context.placePosition = session.getPlacementPosition(); try { const item = Server.player.getHeldItem(session.getPlayer()); pattern.context.hand = Server.block.itemToPermutation(item); @@ -227,12 +229,12 @@ export class Pattern implements CustomArgType { out.push(new RandStatePattern(nodeToken(), parseBlock(tokens, input, true) as string)); } else if (token.value == "$") { const t = tokens.next(); - let cardinal: Cardinal | "radial"; + let cardinal: Cardinal | "radial" | "light"; if (t.type != "id") throwTokenError(t); if (tokens.peek().value == ".") { tokens.next(); const d: string = tokens.next()?.value; - cardinal = d === "rad" ? "radial" : Cardinal.parseArgs([d]).result; + cardinal = d === "rad" ? "radial" : d === "lit" ? "light" : Cardinal.parseArgs([d]).result; } out.push(new GradientPattern(nodeToken(), t.value, cardinal)); @@ -472,16 +474,21 @@ class GradientPattern extends PatternNode { private invertCoords: boolean; private radial = false; + private radialOrigin: "center" | "placement" = "center"; private ctxCardinal: Cardinal; constructor( token: Token, public gradientId: string, - public cardinal?: Cardinal | "radial" + public cardinal?: Cardinal | "radial" | "light" ) { super(token); - if (cardinal && cardinal !== "radial") this.updateDirectionParams(cardinal); - if (cardinal === "radial") this.radial = true; + const isRadial = cardinal === "radial" || cardinal === "light"; + if (cardinal && !isRadial) this.updateDirectionParams(cardinal); + if (isRadial) { + this.radial = true; + if (cardinal === "light") this.radialOrigin = "placement"; + } } getPermutation(block: BlockUnit, context: patternContext) { @@ -490,8 +497,21 @@ class GradientPattern extends PatternNode { const patternLength = gradient.patterns.length; let index = 0; if (this.radial) { - const center = context.range[0].lerp(context.range[1], 0.5); - const maxLength = context.range[1].distanceTo(context.range[0]) / 2; + let center = context.range[0].lerp(context.range[1], 0.5); + let maxLength; + if (this.radialOrigin === "center") { + maxLength = context.range[1].distanceTo(context.range[0]) / 2; + } else { + const point = context.placePosition; + const max = context.range[1]; + const min = context.range[0]; + // Determine which corner is farthest based on the relative position of the point to the center + const farthestX = point.x < center.x ? max.x : min.x; + const farthestY = point.y < center.y ? max.y : min.y; + const farthestZ = point.z < center.z ? max.z : min.z; + maxLength = point.distanceTo(new Vector(farthestX, farthestY, farthestZ)); + center = point; + } index = Math.floor((center.distanceTo(block.location) / maxLength) * (patternLength - gradient.dither) + Math.random() * gradient.dither); } else { if (!this.cardinal && this.ctxCardinal !== context.cardinal) { diff --git a/src/server/sessions.ts b/src/server/sessions.ts index 2509ba316..5b4805f93 100644 --- a/src/server/sessions.ts +++ b/src/server/sessions.ts @@ -200,7 +200,7 @@ export class PlayerSession { } else { const point = this.selection.points[0]; if (!point) throw ""; - return point; + return point.clone(); } } From 0f870dac3c734080665a0a68034a79d43764464b Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Fri, 8 Nov 2024 22:41:51 -0500 Subject: [PATCH 16/26] Small fix to shape generator --- src/server/shapes/base_shape.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index 3124fdafa..ceff73cf2 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -102,9 +102,7 @@ export abstract class Shape { this.prepGeneration(this.genVars, options); for (const block of regionIterateBlocks(...range)) { - if (this.inShape(Vector.sub(block, loc).floor(), this.genVars)) { - yield block; - } + if (this.inShape(Vector.sub(block, loc).floor(), this.genVars)) yield block; } } @@ -163,7 +161,7 @@ export abstract class Shape { * @param session The session that's using this shape * @param options A group of options that can change how the shape is generated */ - public *generate(loc: Vector, pattern: Pattern, mask: Mask, session: PlayerSession, options?: shapeGenOptions): Generator, number> { + public *generate(loc: Vector, pattern: Pattern, mask: Mask | undefined, session: PlayerSession, options?: shapeGenOptions): Generator, number> { const [min, max] = this.getRegion(loc); const player = session.getPlayer(); const dimension = player.dimension; @@ -177,7 +175,6 @@ export abstract class Shape { if (!Jobs.inContext()) assertCanBuildWithin(player, min, max); let blocksAffected = 0; const blocksAndChunks: (Block | [Vector3, Vector3])[] = []; - mask = mask ?? new Mask(); const history = options?.recordHistory ?? true ? session.getHistory() : null; const record = history?.record(this.usedInBrush); @@ -188,10 +185,10 @@ export abstract class Shape { this.prepGeneration(this.genVars, options); // TODO: Localize - let activeMask = mask; + let activeMask = mask ?? new Mask(); const globalMask = options?.ignoreGlobalMask ?? false ? new Mask() : session.globalMask; activeMask = (!activeMask ? globalMask : globalMask ? mask.intersect(globalMask) : activeMask)?.withContext(session); - const simple = pattern.isSimple() && (!mask || mask.isSimple()); + const simple = pattern.isSimple() && activeMask.isSimple(); let progress = 0; const volume = regionVolume(min, max); @@ -268,7 +265,7 @@ export abstract class Shape { const [min, max] = block; const volume = regionVolume(min, max); if (Jobs.inContext()) while (!Jobs.loadBlock(min)) yield sleep(1); - count += pattern.fillSimpleArea(dimension, min, max, mask); + count += pattern.fillSimpleArea(dimension, min, max, activeMask); yield Jobs.setProgress(progress / blocksAffected); progress += volume; } From 47bab32d5760a0f3b9bef3cd3a4d7004e633c02b Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Thu, 21 Nov 2024 18:52:55 -0500 Subject: [PATCH 17/26] Updated region buffer / structure system --- src/library/Minecraft.ts | 3 - src/library/classes/structureBuilder.ts | 291 ----- src/library/utils/scheduling.ts | 2 +- src/library/utils/vector.ts | 12 +- src/server/brushes/structure_brush.ts | 4 +- src/server/commands/brush/size.ts | 2 +- src/server/commands/clipboard/copy.ts | 54 +- src/server/commands/clipboard/cut.ts | 19 +- src/server/commands/clipboard/paste.ts | 6 +- src/server/commands/history/redo.ts | 4 +- src/server/commands/history/undo.ts | 4 +- src/server/commands/region/flip.ts | 10 +- src/server/commands/region/hollow.ts | 6 +- src/server/commands/region/line.ts | 16 +- src/server/commands/region/move.ts | 15 +- src/server/commands/region/revolve.ts | 10 +- src/server/commands/region/rotate.ts | 12 +- src/server/commands/region/smooth_func.ts | 16 +- src/server/commands/region/stack.ts | 9 +- src/server/commands/region/transform_func.ts | 14 +- src/server/commands/selection/count.ts | 8 +- src/server/commands/selection/distr.ts | 12 +- src/server/commands/structure/export.ts | 77 +- src/server/commands/structure/import.ts | 9 +- src/server/commands/utilities/drain.ts | 16 +- src/server/commands/utilities/fill.ts | 10 +- src/server/commands/utilities/fillr.ts | 10 +- src/server/commands/utilities/fixlava.ts | 10 +- src/server/commands/utilities/fixwater.ts | 10 +- src/server/commands/utilities/snow.ts | 8 +- src/server/commands/utilities/thaw.ts | 8 +- src/server/modules/history.ts | 85 +- src/server/modules/jobs.ts | 26 +- src/server/modules/pattern.ts | 10 +- src/server/modules/region_buffer.ts | 1015 ++++++++++++------ src/server/sessions.ts | 17 +- src/server/shapes/base_shape.ts | 30 +- src/server/tools/generation_tools.ts | 4 +- src/server/tools/stacker_tool.ts | 10 +- src/server/util.ts | 2 +- 40 files changed, 891 insertions(+), 995 deletions(-) delete mode 100644 src/library/classes/structureBuilder.ts 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)}`; } /** From a1502256f5fd2f73ebb91c1387b1a68be8832b0a Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sat, 23 Nov 2024 10:38:08 -0500 Subject: [PATCH 18/26] Removed duplicate translation keys --- texts/cs_CZ.po | 6 ------ texts/de_DE.po | 6 ------ texts/el_GR.po | 6 ------ texts/en_GB.po | 6 ------ texts/en_US.po | 2 -- texts/es_ES.po | 6 ------ texts/es_MX.po | 6 ------ texts/fi_FI.po | 6 ------ texts/fr_FR.po | 6 ------ texts/id_ID.po | 6 ------ texts/it_IT.po | 6 ------ texts/ja_JP.po | 6 ------ texts/ko_KR.po | 6 ------ texts/nl_NL.po | 6 ------ texts/no_NO.po | 3 --- texts/pt_BR.po | 6 ------ texts/pt_PT.po | 6 ------ texts/ru_RU.po | 3 --- texts/tr_TR.po | 6 ------ texts/uk_UA.po | 3 --- texts/zh_CN.po | 6 ------ texts/zh_TW.po | 3 --- 22 files changed, 116 deletions(-) diff --git a/texts/cs_CZ.po b/texts/cs_CZ.po index de01e4172..6dde4c039 100644 --- a/texts/cs_CZ.po +++ b/texts/cs_CZ.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Nakreslit čáru" -msgid "item.wedit:draw_line" -msgstr "Nakreslit čáru" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/de_DE.po b/texts/de_DE.po index 189f13523..3f7d61ee3 100644 --- a/texts/de_DE.po +++ b/texts/de_DE.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Linie zeichnen" -msgid "item.wedit:draw_line" -msgstr "Linie zeichnen" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/el_GR.po b/texts/el_GR.po index 49258b150..167a365b8 100644 --- a/texts/el_GR.po +++ b/texts/el_GR.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Draw Line" -msgid "item.wedit:draw_line" -msgstr "Draw Line" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/en_GB.po b/texts/en_GB.po index 5e173f48f..073855d30 100644 --- a/texts/en_GB.po +++ b/texts/en_GB.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Draw Line" -msgid "item.wedit:draw_line" -msgstr "Draw Line" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/en_US.po b/texts/en_US.po index 0aca9183e..6aa67c1d3 100644 --- a/texts/en_US.po +++ b/texts/en_US.po @@ -68,8 +68,6 @@ msgid "item.wedit:selection_move" msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Draw Line" -msgid "item.wedit:draw_line" -msgstr "Draw Line" msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" msgid "item.wedit:draw_cylinder" diff --git a/texts/es_ES.po b/texts/es_ES.po index 3e5e8625c..137b7620c 100644 --- a/texts/es_ES.po +++ b/texts/es_ES.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Dibujar línea" -msgid "item.wedit:draw_line" -msgstr "Dibujar línea" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/es_MX.po b/texts/es_MX.po index 7f1e8154d..2593eb9db 100644 --- a/texts/es_MX.po +++ b/texts/es_MX.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Línea de dibujo" -msgid "item.wedit:draw_line" -msgstr "Línea de dibujo" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/fi_FI.po b/texts/fi_FI.po index 65faaf5b7..d443dd4f4 100644 --- a/texts/fi_FI.po +++ b/texts/fi_FI.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Draw Line" -msgid "item.wedit:draw_line" -msgstr "Draw Line" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/fr_FR.po b/texts/fr_FR.po index 13454ce6b..05c698edc 100644 --- a/texts/fr_FR.po +++ b/texts/fr_FR.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Tracer une ligne" -msgid "item.wedit:draw_line" -msgstr "Tracer une ligne" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/id_ID.po b/texts/id_ID.po index 2627f08d0..2267615e5 100644 --- a/texts/id_ID.po +++ b/texts/id_ID.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Gambar Garis" -msgid "item.wedit:draw_line" -msgstr "Gambar Garis" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/it_IT.po b/texts/it_IT.po index d2061af38..ed4987751 100644 --- a/texts/it_IT.po +++ b/texts/it_IT.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Traccia linea" -msgid "item.wedit:draw_line" -msgstr "Traccia linea" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/ja_JP.po b/texts/ja_JP.po index 976c5f589..019dc2263 100644 --- a/texts/ja_JP.po +++ b/texts/ja_JP.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "線を引く" -msgid "item.wedit:draw_line" -msgstr "線を引く" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/ko_KR.po b/texts/ko_KR.po index b67b73979..5760957df 100644 --- a/texts/ko_KR.po +++ b/texts/ko_KR.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "선 그리기" -msgid "item.wedit:draw_line" -msgstr "선 그리기" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/nl_NL.po b/texts/nl_NL.po index 35ce919b5..ba56e7001 100644 --- a/texts/nl_NL.po +++ b/texts/nl_NL.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Teken lijn" -msgid "item.wedit:draw_line" -msgstr "Teken lijn" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/no_NO.po b/texts/no_NO.po index ffda26b8b..33e958bef 100644 --- a/texts/no_NO.po +++ b/texts/no_NO.po @@ -73,9 +73,6 @@ msgstr "Flytt utvalget" msgid "item.wedit:draw_line" msgstr "Tegn Linje" -msgid "item.wedit:draw_line" -msgstr "Tegn Linje" - msgid "item.wedit:draw_sphere" msgstr "Tegn kule" diff --git a/texts/pt_BR.po b/texts/pt_BR.po index 4fe2e975a..9a0dbd374 100644 --- a/texts/pt_BR.po +++ b/texts/pt_BR.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Desenhar uma linha" -msgid "item.wedit:draw_line" -msgstr "Desenhar uma linha" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/pt_PT.po b/texts/pt_PT.po index 62dd262a6..95b2e24e9 100644 --- a/texts/pt_PT.po +++ b/texts/pt_PT.po @@ -73,9 +73,6 @@ msgstr "Move Selection" msgid "item.wedit:draw_line" msgstr "Desenhar Linha" -msgid "item.wedit:draw_line" -msgstr "Desenhar Linha" - msgid "item.wedit:draw_sphere" msgstr "Draw Sphere" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/ru_RU.po b/texts/ru_RU.po index 78a1ad88a..b6748f12b 100644 --- a/texts/ru_RU.po +++ b/texts/ru_RU.po @@ -73,9 +73,6 @@ msgstr "Переместить выделение" msgid "item.wedit:draw_line" msgstr "Нарисовать линию" -msgid "item.wedit:draw_line" -msgstr "Нарисовать линию" - msgid "item.wedit:draw_sphere" msgstr "Нарисуйте сферу" diff --git a/texts/tr_TR.po b/texts/tr_TR.po index f7127f0ef..b3629f4fc 100644 --- a/texts/tr_TR.po +++ b/texts/tr_TR.po @@ -73,9 +73,6 @@ msgstr "Hareket Seçimi" msgid "item.wedit:draw_line" msgstr "Çizgi çiz" -msgid "item.wedit:draw_line" -msgstr "Çizgi çiz" - msgid "item.wedit:draw_sphere" msgstr "Küre Çiz" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/uk_UA.po b/texts/uk_UA.po index 2d8d5c163..74a095ffc 100644 --- a/texts/uk_UA.po +++ b/texts/uk_UA.po @@ -73,9 +73,6 @@ msgstr "Виділення переміщення" msgid "item.wedit:draw_line" msgstr "Малювати лінію" -msgid "item.wedit:draw_line" -msgstr "Малювати лінію" - msgid "item.wedit:draw_sphere" msgstr "Намалювати сферу" diff --git a/texts/zh_CN.po b/texts/zh_CN.po index 0ea57d622..e05c6f748 100644 --- a/texts/zh_CN.po +++ b/texts/zh_CN.po @@ -73,9 +73,6 @@ msgstr "移动选区" msgid "item.wedit:draw_line" msgstr "填充选区对角线" -msgid "item.wedit:draw_line" -msgstr "填充选区对角线" - msgid "item.wedit:draw_sphere" msgstr "绘制球体" @@ -979,9 +976,6 @@ msgstr "Super pickaxe mode is now: area." msgid "commands.wedit:superpickaxe.description" msgstr "Toggle the super pickaxe" -msgid "commands.wedit:superpickaxe.description" -msgstr "Toggle the super pickaxe" - msgid "commands.wedit:superpickaxe.description.single" msgstr "Make the super pickaxe break a single block" diff --git a/texts/zh_TW.po b/texts/zh_TW.po index 951e30180..c25094d55 100644 --- a/texts/zh_TW.po +++ b/texts/zh_TW.po @@ -73,9 +73,6 @@ msgstr "移動選擇" msgid "item.wedit:draw_line" msgstr "畫線" -msgid "item.wedit:draw_line" -msgstr "畫線" - msgid "item.wedit:draw_sphere" msgstr "繪製球體" From 384584e5685c7bc61cecd76f464a18ffe8876eeb Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sat, 23 Nov 2024 11:24:19 -0500 Subject: [PATCH 19/26] Improved gradient command --- src/server/commands/generation/gradient.ts | 125 +++++++++++++-------- texts/en_US.po | 6 + 2 files changed, 85 insertions(+), 46 deletions(-) diff --git a/src/server/commands/generation/gradient.ts b/src/server/commands/generation/gradient.ts index ff9c08461..6eced385d 100644 --- a/src/server/commands/generation/gradient.ts +++ b/src/server/commands/generation/gradient.ts @@ -1,8 +1,7 @@ -import { RawText, regionSize } from "@notbeer-api"; +import { axis, RawText, regionSize } from "@notbeer-api"; import { registerCommand } from "../register_commands.js"; import { assertCuboidSelection } from "@modules/assert.js"; import { Pattern } from "@modules/pattern.js"; -import { Vector3 } from "@minecraft/server"; const registerInformation = { name: "gradient", @@ -10,62 +9,96 @@ const registerInformation = { description: "commands.wedit:gradient.description", usage: [ { - flag: "s", + subName: "create", + args: [ + { + flag: "s", + }, + { + flag: "f", + name: "fade", + type: "float", + range: [0, 1] as [number, number], + }, + { + name: "id", + type: "string", + }, + { + name: "patterns", + type: "Pattern...", + default: [new Pattern("stone")], + }, + ], }, { - flag: "f", - name: "fade", - type: "float", - range: [0, 1] as [number, number], + subName: "delete", + args: [ + { + name: "id", + type: "string", + }, + ], }, { - name: "id", - type: "string", - }, - { - name: "patterns", - type: "Pattern...", - default: [new Pattern("stone")], + subName: "list", }, ], }; +function truncateStringFromMiddle(str: string, length: number) { + if (str.length <= length) return str; + const halfLength = Math.floor((length - 3) / 2); + const start = str.slice(0, halfLength); + const end = str.slice(str.length - halfLength); + return start + "..." + end; +} + registerCommand(registerInformation, function (session, builder, args) { - const patterns = []; - if (args.has("s")) { - assertCuboidSelection(session); - const [min, max] = session.selection.getRange(); - const size = regionSize(min, max); - const dim = builder.dimension; - type axis = "x" | "y" | "z"; - let s: axis, t: axis, u: axis; - if (size.x > size.y && size.x > size.z) { - s = "y"; - t = "z"; - u = "x"; - } else if (size.z > size.x && size.z > size.y) { - s = "x"; - t = "y"; - u = "z"; - } else { - s = "x"; - t = "z"; - u = "y"; - } + if (args.has("create")) { + const patterns = []; + if (args.has("s")) { + assertCuboidSelection(session); + const [min, max] = session.selection.getRange(); + const size = regionSize(min, max); + const dim = builder.dimension; + let s: axis, t: axis, u: axis; + if (size.x > size.y && size.x > size.z) (s = "y"), (t = "z"), (u = "x"); + else if (size.z > size.x && size.z > size.y) (s = "x"), (t = "y"), (u = "z"); + else (s = "x"), (t = "z"), (u = "y"); - for (let i = min[u]; i <= max[u]; i++) { - const pattern = new Pattern(); - for (let j = min[s]; j <= max[s]; j++) { - for (let k = min[t]; k <= max[t]; k++) { - pattern.addBlock(dim.getBlock({ [s]: j, [t]: k, [u]: i } as unknown as Vector3).permutation); + for (let i = min[u]; i <= max[u]; i++) { + const pattern = new Pattern(); + for (let j = min[s]; j <= max[s]; j++) { + for (let k = min[t]; k <= max[t]; k++) { + pattern.addBlock(dim.getBlock({ [s]: j, [t]: k, [u]: i }).permutation); + } } + patterns.push(pattern); } - patterns.push(pattern); + } else { + patterns.push(...args.get("patterns")); } - } else { - patterns.push(...args.get("patterns")); - } - session.createGradient(args.get("id"), args.get("f-fade") ?? 1.0, patterns); - return RawText.translate("commands.wedit:gradient.create").with(args.get("id")); + session.createGradient(args.get("id"), args.get("f-fade") ?? 1.0, patterns); + return RawText.translate("commands.wedit:gradient.create").with(args.get("id")); + } else if (args.has("delete")) { + if (session.getGradientNames().includes(args.get("id"))) { + session.deleteGradient(args.get("id")); + return RawText.translate("commands.wedit:gradient.delete").with(args.get("id")); + } else { + throw RawText.translate("commands.wedit:gradient.noExist").with(args.get("id")); + } + } else if (args.has("list")) { + const gradients = session.getGradientNames(); + if (gradients.length) + return gradients + .map((id) => { + const patterns = session.getGradient(id).patterns; + const patternStr = patterns.map((p) => `"${p.toJSON()}"`).join(", "); + return `- ${id} (${truncateStringFromMiddle(patternStr, 100)})`; + }) + .join("\n"); + else "commands.wedit:gradient.none"; + } }); diff --git a/texts/en_US.po b/texts/en_US.po index 6aa67c1d3..dab1f6735 100644 --- a/texts/en_US.po +++ b/texts/en_US.po @@ -541,8 +541,14 @@ msgid "commands.wedit:gmask.set" msgstr "Global mask set." msgid "commands.wedit:gradient.create" msgstr "Gradient %s created." +msgid "commands.wedit:gradient.delete" +msgstr "Gradient %s deleted." msgid "commands.wedit:gradient.description" msgstr "Create a gradient to use in patterns." +msgid "commands.wedit:gradient.noExist" +msgstr "Gradient %s does not exist." +msgid "commands.wedit:gradient.none" +msgstr "There are no gradients to list" msgid "commands.wedit:green.description" msgstr "Turn nearby dirt into grass" msgid "commands.wedit:hcyl.description" From 34c83224e285de652b50542cb8d0ac0e390e3743 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sat, 23 Nov 2024 12:27:53 -0500 Subject: [PATCH 20/26] Fixed heightmap smoothing --- src/server/commands/region/smooth_func.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/commands/region/smooth_func.ts b/src/server/commands/region/smooth_func.ts index 3821d7eeb..1fa8df604 100644 --- a/src/server/commands/region/smooth_func.ts +++ b/src/server/commands/region/smooth_func.ts @@ -113,6 +113,7 @@ export function* smooth(session: PlayerSession, iter: number, shape: Shape, loc: let count = 0; const history = session.getHistory(); const record = history.record(); + const rangeYDiff = range[1].y - range[0].y; let warpBuffer: RegionBuffer; try { yield* history.addUndoStructure(record, range[0], range[1], "any"); @@ -127,7 +128,7 @@ export function* smooth(session: PlayerSession, iter: number, shape: Shape, loc: if (canSmooth(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); + sampleLoc.y = Math.min(Math.max(sampleLoc.y, 0), rangeYDiff); if (canSmooth(sampleLoc)) return dim.getBlock(sampleLoc.add(range[0])); } }); From d4275a43cf6c11383b4009fb3e436b5161c65fd9 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Tue, 26 Nov 2024 17:50:05 -0500 Subject: [PATCH 21/26] Fixed history record disabling in shape generator --- src/server/shapes/base_shape.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index 028db106d..013bf21c5 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -176,7 +176,7 @@ export abstract class Shape { let blocksAffected = 0; const blocksAndChunks: (Block | [Vector3, Vector3])[] = []; - const history = options?.recordHistory ?? true ? session.getHistory() : null; + const history = options?.recordHistory ?? true ? session.getHistory() : undefined; const record = history?.record(this.usedInBrush); try { let count = 0; @@ -238,7 +238,7 @@ export abstract class Shape { progress = 0; yield Jobs.nextStep("Generating blocks..."); - yield* history?.addUndoStructure(record, min, max); + if (history) yield* history.addUndoStructure(record, min, max); for (let block of blocksAndChunks) { if (block instanceof Block) { if (!block.isValid() && Jobs.inContext()) block = yield* Jobs.loadBlock(loc); @@ -254,7 +254,7 @@ export abstract class Shape { progress += volume; } } - yield* history?.addRedoStructure(record, min, max); + if (history) yield* history.addRedoStructure(record, min, max); } history?.commit(record); return count; From d679fd310ad86bd4ba4d3045434ecbc7446ced0e Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Tue, 26 Nov 2024 17:50:14 -0500 Subject: [PATCH 22/26] Fixed session deletion if undefined --- src/server/sessions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/sessions.ts b/src/server/sessions.ts index 07366cfc8..029d7f637 100644 --- a/src/server/sessions.ts +++ b/src/server/sessions.ts @@ -296,8 +296,8 @@ export class PlayerSession { } public deleteRegion(buffer: RegionBuffer) { - buffer.deref(); - this.regions.delete(buffer.id); + buffer?.deref(); + this.regions.delete(buffer?.id); } public createGradient(id: string, dither: number, patterns: Pattern[]) { From 834a238ccd703d9fde61ac5a2240b9bca0426543 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sat, 30 Nov 2024 13:42:00 -0500 Subject: [PATCH 23/26] Made it so jumpto does not fail when aiming at nothing --- src/server/commands/navigation/jumpto.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/server/commands/navigation/jumpto.ts b/src/server/commands/navigation/jumpto.ts index 442359886..3204905f2 100644 --- a/src/server/commands/navigation/jumpto.ts +++ b/src/server/commands/navigation/jumpto.ts @@ -1,6 +1,7 @@ import { PlayerUtil } from "@modules/player_util.js"; import { RawText } from "@notbeer-api"; import { getCommandFunc, registerCommand } from "../register_commands.js"; +import config from "config.js"; const registerInformation = { name: "jumpto", @@ -10,10 +11,7 @@ const registerInformation = { }; registerCommand(registerInformation, function (session, builder) { - const hit = PlayerUtil.traceForBlock(builder); - if (!hit) { - throw RawText.translate("commands.wedit:jumpto.none"); - } + const hit = PlayerUtil.traceForBlock(builder, config.traceDistance - 1); builder.teleport(hit.offset(0.5, 0, 0.5), { dimension: builder.dimension }); getCommandFunc("unstuck")(session, builder, new Map()); return RawText.translate("commands.wedit:jumpto.explain"); From cb47b508fcc9bb4e3e123729c09b2aa46fd8772b Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sun, 1 Dec 2024 22:54:47 -0500 Subject: [PATCH 24/26] Updated lang files for gradient command --- texts/en_US.po | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/texts/en_US.po b/texts/en_US.po index dab1f6735..2cdbb56fc 100644 --- a/texts/en_US.po +++ b/texts/en_US.po @@ -544,7 +544,13 @@ msgstr "Gradient %s created." msgid "commands.wedit:gradient.delete" msgstr "Gradient %s deleted." msgid "commands.wedit:gradient.description" +msgstr "Manage your gradients." +msgid "commands.wedit:gradient.description.create" msgstr "Create a gradient to use in patterns." +msgid "commands.wedit:gradient.description.delete" +msgstr "Delete a gradient." +msgid "commands.wedit:gradient.description.list" +msgstr "List created gradients." msgid "commands.wedit:gradient.noExist" msgstr "Gradient %s does not exist." msgid "commands.wedit:gradient.none" From 36c3b56bb46f649aaf9898ef58ecb33a510d52af Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sun, 1 Dec 2024 22:54:55 -0500 Subject: [PATCH 25/26] Fixed jumpto command --- src/server/commands/navigation/jumpto.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/server/commands/navigation/jumpto.ts b/src/server/commands/navigation/jumpto.ts index 3204905f2..14d2c3b2d 100644 --- a/src/server/commands/navigation/jumpto.ts +++ b/src/server/commands/navigation/jumpto.ts @@ -1,5 +1,5 @@ import { PlayerUtil } from "@modules/player_util.js"; -import { RawText } from "@notbeer-api"; +import { RawText, Vector } from "@notbeer-api"; import { getCommandFunc, registerCommand } from "../register_commands.js"; import config from "config.js"; @@ -11,8 +11,13 @@ const registerInformation = { }; registerCommand(registerInformation, function (session, builder) { - const hit = PlayerUtil.traceForBlock(builder, config.traceDistance - 1); - builder.teleport(hit.offset(0.5, 0, 0.5), { dimension: builder.dimension }); - getCommandFunc("unstuck")(session, builder, new Map()); + const hit = PlayerUtil.traceForBlock(builder); + if (hit) { + builder.teleport(hit.offset(0.5, 0, 0.5), { dimension: builder.dimension }); + getCommandFunc("unstuck")(session, builder, new Map()); + } else { + const teleportTo = Vector.add(builder.location, Vector.mul(builder.getViewDirection(), config.traceDistance)); + builder.runCommand(`tp ${teleportTo.x.toFixed(3)} ${teleportTo.y.toFixed(3)} ${teleportTo.z.toFixed(3)}`); + } return RawText.translate("commands.wedit:jumpto.explain"); }); From 2ca22ef865f3df82db4476659c1b3e4b79d7ffa4 Mon Sep 17 00:00:00 2001 From: Roujel Williams Date: Sun, 1 Dec 2024 22:56:34 -0500 Subject: [PATCH 26/26] Fixed lint error --- tools/sync2com-mojang.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/sync2com-mojang.py b/tools/sync2com-mojang.py index d18393000..f8a9d7f4b 100644 --- a/tools/sync2com-mojang.py +++ b/tools/sync2com-mojang.py @@ -124,7 +124,8 @@ def on_deleted(self, ev): sync_all() try: alert_watching() - while True: time.sleep(1) + while True: + time.sleep(1) except KeyboardInterrupt: observerBP.stop() observerRP.stop()