diff --git a/BP/items/buttons/draw_cylinder.item.json b/BP/items/buttons/draw_cylinder.item.json new file mode 100644 index 000000000..0a9bc86c3 --- /dev/null +++ b/BP/items/buttons/draw_cylinder.item.json @@ -0,0 +1,16 @@ +{ + "format_version": "1.20.10", + "minecraft:item": { + "description": { + "identifier": "wedit:draw_cylinder", + "category": "commands" + }, + "components": { + "minecraft:icon": { + "texture": "draw_cylinder" + }, + "minecraft:max_stack_size": 1, + "minecraft:throwable": {} + } + } +} \ No newline at end of file diff --git a/BP/items/buttons/draw_pyramid.item.json b/BP/items/buttons/draw_pyramid.item.json new file mode 100644 index 000000000..96656980e --- /dev/null +++ b/BP/items/buttons/draw_pyramid.item.json @@ -0,0 +1,16 @@ +{ + "format_version": "1.20.10", + "minecraft:item": { + "description": { + "identifier": "wedit:draw_pyramid", + "category": "commands" + }, + "components": { + "minecraft:icon": { + "texture": "draw_pyramid" + }, + "minecraft:max_stack_size": 1, + "minecraft:throwable": {} + } + } +} \ No newline at end of file diff --git a/BP/items/buttons/draw_sphere.item.json b/BP/items/buttons/draw_sphere.item.json new file mode 100644 index 000000000..b7284d4bc --- /dev/null +++ b/BP/items/buttons/draw_sphere.item.json @@ -0,0 +1,16 @@ +{ + "format_version": "1.20.10", + "minecraft:item": { + "description": { + "identifier": "wedit:draw_sphere", + "category": "commands" + }, + "components": { + "minecraft:icon": { + "texture": "draw_sphere" + }, + "minecraft:max_stack_size": 1, + "minecraft:throwable": {} + } + } +} \ No newline at end of file diff --git a/BP/items/buttons/selection_hollow.item.json b/BP/items/buttons/selection_hollow.item.json new file mode 100644 index 000000000..e65f84927 --- /dev/null +++ b/BP/items/buttons/selection_hollow.item.json @@ -0,0 +1,16 @@ +{ + "format_version": "1.20.10", + "minecraft:item": { + "description": { + "identifier": "wedit:selection_hollow", + "category": "commands" + }, + "components": { + "minecraft:icon": { + "texture": "selection_hollow" + }, + "minecraft:max_stack_size": 1, + "minecraft:throwable": {} + } + } +} \ No newline at end of file diff --git a/RP/textures/item_texture.json b/RP/textures/item_texture.json index c04a978e9..642e69af0 100644 --- a/RP/textures/item_texture.json +++ b/RP/textures/item_texture.json @@ -50,6 +50,15 @@ "draw_line": { "textures": "textures/items/draw_line" }, + "draw_sphere": { + "textures": "textures/items/draw_sphere" + }, + "draw_cylinder": { + "textures": "textures/items/draw_cylinder" + }, + "draw_pyramid": { + "textures": "textures/items/draw_pyramid" + }, "config": { "textures": "textures/ui/gear" }, diff --git a/RP/textures/items/draw_cylinder.png b/RP/textures/items/draw_cylinder.png new file mode 100644 index 000000000..d221f178a Binary files /dev/null and b/RP/textures/items/draw_cylinder.png differ diff --git a/RP/textures/items/draw_pyramid.png b/RP/textures/items/draw_pyramid.png new file mode 100644 index 000000000..8043cccc2 Binary files /dev/null and b/RP/textures/items/draw_pyramid.png differ diff --git a/RP/textures/items/draw_sphere.png b/RP/textures/items/draw_sphere.png new file mode 100644 index 000000000..edf73c147 Binary files /dev/null and b/RP/textures/items/draw_sphere.png differ diff --git a/mc_manifest.json b/mc_manifest.json index a52a6819b..987c0ab6f 100644 --- a/mc_manifest.json +++ b/mc_manifest.json @@ -4,12 +4,13 @@ "bp_name": "WorldEdit Bedrock Edition", "rp_name": "WorldEdit Bedrock Edition [RES]", - "description": "pack.description", + "description": "pack.description", - "bp_uuid": "3cdb2ddf-662e-4f8f-a0a1-1293b91ccb2f", + "bp_uuid": "3cdb2ddf-662e-4f8f-a0a1-1293b91ccb2f", "rp_uuid": "e304a4eb-f0a0-4979-ac17-7b0f46a555c8", - "version": [0, 8, 4], - "min_engine_version": [ 1, 20, 50 ] + + "version": [0, 9, 0, 1], + "min_engine_version": [ 1, 20, 60 ] }, "bp_modules": [ { @@ -38,7 +39,7 @@ "bp_dependencies": [ { "module_name": "@minecraft/server", - "version": "1.8.0-beta" + "version": "1.9.0-beta" }, { "module_name": "@minecraft/server-ui", diff --git a/package-lock.json b/package-lock.json index f609441b0..d2e8f9554 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.8.0-beta.1.20.50-preview.23", + "@minecraft/server": "^1.9.0-beta.1.20.60-preview.22", "@minecraft/server-admin": "^1.0.0-beta.1.19.80-stable", - "@minecraft/server-ui": "^1.2.0-beta.1.20.50-preview.23" + "@minecraft/server-ui": "^1.2.0-beta.1.20.60-preview.22" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.33.0", @@ -69,16 +69,16 @@ "dev": true }, "node_modules/@minecraft/common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.0.0.tgz", - "integrity": "sha512-7WObMvUWaS2zBVBZwc150Kd/apLUYa5tx2fvkLe3JoviEKm7QjZNLghZV9sV/STXmd9k3HBKTKvRyGdNaTIVBA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.1.0.tgz", + "integrity": "sha512-stbUtINCXbcLNRlGNVX68xRC6ZYq3k3CYmfptwrCcPBEUjVOpVkSj3H4Y0qiSYB+1rVWv7DgiP7Uf9++50Ne5g==" }, "node_modules/@minecraft/server": { - "version": "1.8.0-beta.1.20.50-preview.23", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.8.0-beta.1.20.50-preview.23.tgz", - "integrity": "sha512-yiKskrKYtsPAYjmhtFbsweU5qKAe4ZxdFAvma6e1YK55IWDoce4HSd0gpdVp6+5FRJktLtauLNQ2k6iti+m2fw==", + "version": "1.9.0-beta.1.20.60-preview.22", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.9.0-beta.1.20.60-preview.22.tgz", + "integrity": "sha512-QXbO+uBxE5Bou7UCXLBc66FzATbFELUQ2fcy5pnTW0PjqQ5AQjn87wKLr1DLN2c2kj+JA+yullLIxeqORMe5Jg==", "dependencies": { - "@minecraft/common": "^1.1.0-rc.1.20.50-preview.23" + "@minecraft/common": "^1.1.0" } }, "node_modules/@minecraft/server-admin": { @@ -87,18 +87,21 @@ "integrity": "sha512-HKyfnulfR853lsTlpzWwusDUo5S7KpOhL8w4PwObMeGK4t9ATl32dchZNZpT3N6WlgLxWpq/Z9F6s5th3cFG9A==" }, "node_modules/@minecraft/server-ui": { - "version": "1.2.0-beta.1.20.50-preview.23", - "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.2.0-beta.1.20.50-preview.23.tgz", - "integrity": "sha512-RmqHHXzgjVtgB/RfJBQ/UQWTDMTzvu9ldCdO5dmoy/apjgZ29vHvout2kv2lcpxXS8qy27Z0pZZRx+I1mMOxEA==", + "version": "1.2.0-beta.1.20.60-preview.22", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.2.0-beta.1.20.60-preview.22.tgz", + "integrity": "sha512-fpp0ysdw4LOXQANbItw9NxHgrX8stCRUwZgHrysHMy12p6cQOzH9DVNB72HeaihB5yBHtdqL7ErK8KlYHeWHtw==", "dependencies": { "@minecraft/common": "^1.0.0", - "@minecraft/server": "^1.8.0-beta.1.20.50-preview.23" + "@minecraft/server": "^1.8.0-rc.1.20.60-preview.22" } }, - "node_modules/@minecraft/server/node_modules/@minecraft/common": { - "version": "1.1.0-rc.1.20.50-preview.24", - "resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.1.0-rc.1.20.50-preview.24.tgz", - "integrity": "sha512-de50nNqwdvdBlBValwnQNJQ/BCZ9O6naJrPVjDASHhWmG6niRgHIECgchc0tPA+OuB/Xrc/93L/Tlv19IKFmGA==" + "node_modules/@minecraft/server-ui/node_modules/@minecraft/server": { + "version": "1.8.0-rc.1.20.60-preview.22", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.8.0-rc.1.20.60-preview.22.tgz", + "integrity": "sha512-u+bTE3Dw3AOdXLYA5n+86/ZvsOw5HmXOSimN2wQmYrqsHqjGny/fsMLB5Ykcj/iDgnfbH4OG53sqTO56v6YQ8g==", + "dependencies": { + "@minecraft/common": "^1.1.0" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1651,23 +1654,16 @@ "dev": true }, "@minecraft/common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.0.0.tgz", - "integrity": "sha512-7WObMvUWaS2zBVBZwc150Kd/apLUYa5tx2fvkLe3JoviEKm7QjZNLghZV9sV/STXmd9k3HBKTKvRyGdNaTIVBA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.1.0.tgz", + "integrity": "sha512-stbUtINCXbcLNRlGNVX68xRC6ZYq3k3CYmfptwrCcPBEUjVOpVkSj3H4Y0qiSYB+1rVWv7DgiP7Uf9++50Ne5g==" }, "@minecraft/server": { - "version": "1.8.0-beta.1.20.50-preview.23", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.8.0-beta.1.20.50-preview.23.tgz", - "integrity": "sha512-yiKskrKYtsPAYjmhtFbsweU5qKAe4ZxdFAvma6e1YK55IWDoce4HSd0gpdVp6+5FRJktLtauLNQ2k6iti+m2fw==", + "version": "1.9.0-beta.1.20.60-preview.22", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.9.0-beta.1.20.60-preview.22.tgz", + "integrity": "sha512-QXbO+uBxE5Bou7UCXLBc66FzATbFELUQ2fcy5pnTW0PjqQ5AQjn87wKLr1DLN2c2kj+JA+yullLIxeqORMe5Jg==", "requires": { - "@minecraft/common": "^1.1.0-rc.1.20.50-preview.23" - }, - "dependencies": { - "@minecraft/common": { - "version": "1.1.0-rc.1.20.50-preview.24", - "resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.1.0-rc.1.20.50-preview.24.tgz", - "integrity": "sha512-de50nNqwdvdBlBValwnQNJQ/BCZ9O6naJrPVjDASHhWmG6niRgHIECgchc0tPA+OuB/Xrc/93L/Tlv19IKFmGA==" - } + "@minecraft/common": "^1.1.0" } }, "@minecraft/server-admin": { @@ -1676,12 +1672,22 @@ "integrity": "sha512-HKyfnulfR853lsTlpzWwusDUo5S7KpOhL8w4PwObMeGK4t9ATl32dchZNZpT3N6WlgLxWpq/Z9F6s5th3cFG9A==" }, "@minecraft/server-ui": { - "version": "1.2.0-beta.1.20.50-preview.23", - "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.2.0-beta.1.20.50-preview.23.tgz", - "integrity": "sha512-RmqHHXzgjVtgB/RfJBQ/UQWTDMTzvu9ldCdO5dmoy/apjgZ29vHvout2kv2lcpxXS8qy27Z0pZZRx+I1mMOxEA==", + "version": "1.2.0-beta.1.20.60-preview.22", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-1.2.0-beta.1.20.60-preview.22.tgz", + "integrity": "sha512-fpp0ysdw4LOXQANbItw9NxHgrX8stCRUwZgHrysHMy12p6cQOzH9DVNB72HeaihB5yBHtdqL7ErK8KlYHeWHtw==", "requires": { "@minecraft/common": "^1.0.0", - "@minecraft/server": "^1.8.0-beta.1.20.50-preview.23" + "@minecraft/server": "^1.8.0-rc.1.20.60-preview.22" + }, + "dependencies": { + "@minecraft/server": { + "version": "1.8.0-rc.1.20.60-preview.22", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-1.8.0-rc.1.20.60-preview.22.tgz", + "integrity": "sha512-u+bTE3Dw3AOdXLYA5n+86/ZvsOw5HmXOSimN2wQmYrqsHqjGny/fsMLB5Ykcj/iDgnfbH4OG53sqTO56v6YQ8g==", + "requires": { + "@minecraft/common": "^1.1.0" + } + } } }, "@nodelib/fs.scandir": { diff --git a/package.json b/package.json index c0662757a..848d07c5d 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "typescript": "^4.7.4" }, "dependencies": { - "@minecraft/server": "^1.8.0-beta.1.20.50-preview.23", + "@minecraft/server": "^1.9.0-beta.1.20.60-preview.22", "@minecraft/server-admin": "^1.0.0-beta.1.19.80-stable", - "@minecraft/server-ui": "^1.2.0-beta.1.20.50-preview.23" + "@minecraft/server-ui": "^1.2.0-beta.1.20.60-preview.22" } } diff --git a/src/library/@types/Events.d.ts b/src/library/@types/Events.d.ts index 049f555ee..2792a14e4 100644 --- a/src/library/@types/Events.d.ts +++ b/src/library/@types/Events.d.ts @@ -1,7 +1,6 @@ import { ChatSendBeforeEvent, ExplosionBeforeEvent, - PistonActivateBeforeEvent, BlockExplodeAfterEvent, ItemUseBeforeEvent, ItemUseOnBeforeEvent, @@ -23,7 +22,6 @@ import { registerInformation } from "./classes/CommandBuilder"; export interface EventList { beforeMessage: [ChatSendBeforeEvent], beforeExplosion: [ExplosionBeforeEvent], - beforePistonActivate: [PistonActivateBeforeEvent], blockExplode: [BlockExplodeAfterEvent], messageCreate: [ChatSendAfterEvent], itemUseBefore: [ItemUseBeforeEvent], diff --git a/src/library/@types/classes/uiFormBuilder.d.ts b/src/library/@types/classes/uiFormBuilder.d.ts index 10df7132b..22cd36dd2 100644 --- a/src/library/@types/classes/uiFormBuilder.d.ts +++ b/src/library/@types/classes/uiFormBuilder.d.ts @@ -49,7 +49,7 @@ interface BaseForm { /** The title of the UI form */ title: DynamicElem, /** Action to perform when the user exits or cancels the form */ - cancel: UIAction + cancel?: UIAction } /** A form with a message and one or two options */ diff --git a/src/library/Minecraft.ts b/src/library/Minecraft.ts index 2056112c1..bd37bdd27 100644 --- a/src/library/Minecraft.ts +++ b/src/library/Minecraft.ts @@ -80,10 +80,6 @@ class ServerBuild extends ServerBuilder { * Emit to 'beforeExplosion' event listener */ beforeEvents.explosion.subscribe(data => this.emit("beforeExplosion", data)); - /** - * Emit to 'beforePistonActivate' event listener - */ - beforeEvents.pistonActivate.subscribe(data => this.emit("beforePistonActivate", data)); /** * Emit to 'blockExplode' event listener */ @@ -95,8 +91,7 @@ class ServerBuild extends ServerBuilder { /** * Emit to 'pistonActivate' event listener */ - beforeEvents.pistonActivate.subscribe(data => this.emit("pistonActivate", data)); - + afterEvents.pistonActivate.subscribe(data => this.emit("pistonActivate", data)); /** * Emit to 'itemUse' event listener */ diff --git a/src/library/classes/playerBuilder.ts b/src/library/classes/playerBuilder.ts index b47396fba..51ecca29a 100644 --- a/src/library/classes/playerBuilder.ts +++ b/src/library/classes/playerBuilder.ts @@ -65,6 +65,14 @@ export class PlayerBuilder { getInventory(player: Player) { return (player.getComponent("minecraft:inventory") as Minecraft.EntityInventoryComponent).container; } + /** + * Get the player's equipment component + * @param {Player} [player] Player of interest + * @returns {Minecraft.EntityEquippableComponent} + */ + getEquipment(player: Player) { + return (player.getComponent("minecraft:equippable") as Minecraft.EntityEquippableComponent); + } /** * Get the amount on a specific items player(s) has * @param {Player} [player] Player you are searching diff --git a/src/library/classes/uiFormBuilder.ts b/src/library/classes/uiFormBuilder.ts index 4d748bb57..c232fb1d7 100644 --- a/src/library/classes/uiFormBuilder.ts +++ b/src/library/classes/uiFormBuilder.ts @@ -6,7 +6,7 @@ import { setTickTimeout, contentLog } from "@notbeer-api"; abstract class UIForm { private readonly form: Form; - protected readonly cancelAction: UIAction; + protected readonly cancelAction?: UIAction; constructor(form: Form) { this.form = form; @@ -26,7 +26,7 @@ abstract class UIForm { setTickTimeout(() => this.enter(player, ctx)); } else { ctx.goto(null); - this.cancelAction(ctx, player); + this.cancelAction?.(ctx, player); } return true; } @@ -247,7 +247,7 @@ class UIFormBuilder { return true; } const ctx = new MenuContext(player); - Object.entries(data).forEach(e => ctx.setData(e[0] as keyof T, e[1] as typeof data[keyof T])); + Object.entries(data ?? {}).forEach(e => ctx.setData(e[0] as keyof T, e[1] as typeof data[keyof T])); ctx.goto(name); return false; } diff --git a/src/library/utils/vector.ts b/src/library/utils/vector.ts index 55163fdd6..1f5346767 100644 --- a/src/library/utils/vector.ts +++ b/src/library/utils/vector.ts @@ -51,7 +51,7 @@ export class Vector { } get length() { - return Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z); + return Math.hypot(this.x, this.y, this.z); } set length(val: number) { diff --git a/src/server/commands/region/hollow.ts b/src/server/commands/region/hollow.ts index 6188e776e..985313d3f 100644 --- a/src/server/commands/region/hollow.ts +++ b/src/server/commands/region/hollow.ts @@ -115,11 +115,8 @@ function* hollow(session: PlayerSession, pattern: Pattern, thickness: number): G registerCommand(registerInformation, function* (session, builder, args) { assertSelection(session); assertCanBuildWithin(builder, ...session.selection.getRange()); - if (args.get("_using_item") && session.globalPattern.empty()) { - throw RawText.translate("worldEdit.selectionFill.noPattern"); - } - const pattern: Pattern = args.get("_using_item") ? session.globalPattern : args.get("pattern"); + const pattern: Pattern = args.get("pattern"); const thickness = args.get("thickness") as number; const job = Jobs.startJob(session, 3, session.selection.getRange()); diff --git a/src/server/commands/region/line.ts b/src/server/commands/region/line.ts index 122556b3e..eddbe7c26 100644 --- a/src/server/commands/region/line.ts +++ b/src/server/commands/region/line.ts @@ -16,7 +16,7 @@ const registerInformation = { ] }; -function* bresenham3d(p1: Vector, p2: Vector): Generator { +export function* generateLine(p1: Vector, p2: Vector): Generator { const pointList: Vector[] = []; pointList.push(p1.clone()); const d = p2.sub(p1).abs(); @@ -118,7 +118,7 @@ registerCommand(registerInformation, function* (session, builder, args) { const record = history.record(); let count: number; try { - const points = (yield* bresenham3d(Vector.from(pos1), Vector.from(pos2))).map(p => p.floor()); + const points = (yield* generateLine(Vector.from(pos1), Vector.from(pos2))).map(p => p.floor()); history.addUndoStructure(record, start, end); count = 0; for (const point of points) { diff --git a/src/server/index.ts b/src/server/index.ts index f626a351a..65dc479c5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -13,7 +13,7 @@ import { PlayerUtil } from "@modules/player_util.js"; import config from "config.js"; import "./commands/command_list.js"; -import "./tools/register_tools.js"; +import "./tools/tool_list.js"; import "./ui/index.js"; Server.setMaxListeners(256); diff --git a/src/server/modules/biome_data.ts b/src/server/modules/biome_data.ts index cfef58c8f..51970fa64 100644 --- a/src/server/modules/biome_data.ts +++ b/src/server/modules/biome_data.ts @@ -171,10 +171,10 @@ class BiomeDetector extends EventEmitter implements PooledResource { this.emit(errorEventSym); reject(err); } finally { - world.afterEvents.dataDrivenEntityTriggerEvent.unsubscribe(onEvent); + world.afterEvents.dataDrivenEntityTrigger.unsubscribe(onEvent); } }; - world.afterEvents.dataDrivenEntityTriggerEvent.subscribe(onEvent, { + world.afterEvents.dataDrivenEntityTrigger.subscribe(onEvent, { entities: [this.entity], eventTypes: ["wedit:biome_update"] }); diff --git a/src/server/modules/selection.ts b/src/server/modules/selection.ts index ce194a63f..d98df5cff 100644 --- a/src/server/modules/selection.ts +++ b/src/server/modules/selection.ts @@ -4,7 +4,7 @@ import { Shape, shapeGenOptions } from "../shapes/base_shape.js"; import { SphereShape } from "../shapes/sphere.js"; import { CuboidShape } from "../shapes/cuboid.js"; import { CylinderShape } from "../shapes/cylinder.js"; -import { arraysEqual, getWorldHeightLimits, snap } from "../util.js"; +import { arraysEqual, getWorldHeightLimits } from "../util.js"; import config from "config.js"; // TODO: Add other selection modes @@ -22,7 +22,7 @@ export class Selection { private pointsLastDraw: Vector[] = []; private player: Player; - private drawPoints: Vector[] = []; + private drawParticles: [string, Vector][] = []; private lastDraw = 0; constructor(player: Player) { @@ -160,16 +160,19 @@ export class Selection { if (!this._visible) return; if (system.currentTick > this.lastDraw + drawFrequency) { if (this._mode != this.modeLastDraw || !arraysEqual(this._points, this.pointsLastDraw, (a, b) => a.equals(b))) { - this.updatePoints(); + this.drawParticles.length = 0; + if (this.isValid()) { + const [shape, loc] = this.getShape(); + this.drawParticles.push(...shape.getOutline(loc)); + } this.modeLastDraw = this._mode; this.pointsLastDraw = this.points; } try { - const dimension = this.player.dimension; - for (const point of this.drawPoints) { + for (const [id, loc] of this.drawParticles) { try { - dimension.spawnParticle("wedit:selection_draw", point, new MolangVariableMap()); + this.player.spawnParticle(id, loc); } catch { /* pass */ } } } catch { /* pass */ } @@ -207,91 +210,4 @@ export class Selection { public set visible(value: boolean) { this._visible = value; } - - private updatePoints() { - this.drawPoints.length = 0; - if (!this.isValid()) return; - - if (this.isCuboid()) { - const min = Vector.min(this._points[0], this._points[1]).add(Vector.ZERO); - const max = Vector.max(this._points[0], this._points[1]).add(Vector.ONE); - - const corners = [ - new Vector(min.x, min.y, min.z), - new Vector(max.x, min.y, min.z), - new Vector(min.x, max.y, min.z), - new Vector(max.x, max.y, min.z), - new Vector(min.x, min.y, max.z), - new Vector(max.x, min.y, max.z), - new Vector(min.x, max.y, max.z), - new Vector(max.x, max.y, max.z) - ]; - - const edgeData: [number, number][]= [ - [0, 1], [2, 3], [4, 5], [6, 7], - [0, 2], [1, 3], [4, 6], [5, 7], - [0, 4], [1, 5], [2, 6], [3, 7] - ]; - const edgePoints: Vector[] = []; - for (const edge of edgeData) { - const [a, b] = [corners[edge[0]], corners[edge[1]]]; - const resolution = Math.min(Math.floor(b.sub(a).length), 16); - for (let i = 1; i < resolution; i++) { - const t = i / resolution; - edgePoints.push(a.lerp(b, t)); - } - } - this.drawPoints = corners.concat(edgePoints); - } else if (this._mode == "sphere") { - 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 loc = this._points[0]; - const radius = Vector.sub(this._points[1], loc).length + 0.5; - const resolution = snap(Math.min(radius * 2*Math.PI, 36), 4); - - for (const [rotateBy, vec] of axes) { - for (let i = 0; i < resolution; i++) { - let point: Vector = rotateBy.call(vec, i / resolution * 360); - point = point.mul(radius).add(loc).add(0.5); - this.drawPoints.push(point); - } - } - } else if (this._mode == "cylinder") { - const offset = new Vector(0.5, 0, 0.5); - const [pointA, pointB] = [this._points[0], this._points[1]]; - - const diff = Vector.sub(pointB, pointA); - const radius = diff.mul([1, 0, 1]).length + 0.5; - const height = Math.abs(diff.y) + 1; - - const resolution = snap(Math.min(radius * 2*Math.PI, 36), 4); - const vec = new Vector(1, 0, 0); - - for (let i = 0; i < resolution; i++) { - let point = vec.rotateY(i / resolution * 360); - point = point.mul(radius).add(pointA).add(offset); - this.drawPoints.push(point); - this.drawPoints.push(point.add([0, height, 0])); - } - - const corners = [ - new Vector(1, 0, 0), new Vector(-1, 0, 0), - new Vector(0, 0, 1), new Vector(0, 0, -1) - ]; - for (const corner of corners) { - const [a, b] = [ - corner.mul(radius).add(pointA).add(offset), - corner.mul(radius).add(pointA).add(offset).add([0, height, 0]) - ]; - const resolution = Math.min(Math.floor(b.sub(a).length), 16); - for (let i = 1; i < resolution; i++) { - const t = i / resolution; - this.drawPoints.push(a.lerp(b, t)); - } - } - } - } } \ No newline at end of file diff --git a/src/server/shapes/base_shape.ts b/src/server/shapes/base_shape.ts index 4d6b84a66..5f1fbc2b6 100644 --- a/src/server/shapes/base_shape.ts +++ b/src/server/shapes/base_shape.ts @@ -4,7 +4,7 @@ import { Mask } from "@modules/mask.js"; import { Pattern } from "@modules/pattern.js"; import { contentLog, iterateChunk, regionIterateBlocks, regionVolume, Vector } from "@notbeer-api"; import { PlayerSession } from "../sessions.js"; -import { getWorldHeightLimits } from "../util.js"; +import { getWorldHeightLimits, snap } from "../util.js"; export type shapeGenOptions = { hollow?: boolean, @@ -62,6 +62,11 @@ export abstract class Shape { * @return True if a block should be generated; false otherwise */ protected abstract inShape(relLoc: Vector, genVars: shapeGenVars): boolean; + + /** + * Generates a list of particles that when displayed, shows the shape. + */ + public abstract getOutline(loc: Vector): [string, Vector][]; /** * Returns blocks that are in the shape. @@ -91,6 +96,35 @@ export abstract class Shape { } } + protected drawShape(vertices: Vector[], edges: [number, number][]): [string, Vector][] { + const edgePoints: Vector[] = []; + for (const edge of edges) { + const [a, b] = [vertices[edge[0]], vertices[edge[1]]]; + const resolution = Math.min(Math.floor(b.sub(a).length), 16); + for (let i = 1; i < resolution; i++) { + const t = i / resolution; + edgePoints.push(a.lerp(b, t)); + } + } + return vertices.concat(edgePoints).map((v => ["wedit:selection_draw", v])); + } + + 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 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); + point = point.mul(radius).add(center).add(0.5); + points.push(["wedit:selection_draw", point]); + } + return points; + } + /** * Generates a block formation at a certain location. * @param loc The location the shape will be generated at diff --git a/src/server/shapes/cuboid.ts b/src/server/shapes/cuboid.ts index fb502f92c..b186e2bef 100644 --- a/src/server/shapes/cuboid.ts +++ b/src/server/shapes/cuboid.ts @@ -21,6 +21,28 @@ export class CuboidShape extends Shape { public getYRange() { return <[number, number]>[0, this.size[1] - 1]; } + + public getOutline(loc: Vector) { + const min = loc; + const max = loc.add(this.size); + + const vertices = [ + new Vector(min.x, min.y, min.z), + new Vector(max.x, min.y, min.z), + new Vector(min.x, max.y, min.z), + new Vector(max.x, max.y, min.z), + new Vector(min.x, min.y, max.z), + new Vector(max.x, min.y, max.z), + new Vector(min.x, max.y, max.z), + new Vector(max.x, max.y, max.z) + ]; + const edges: [number, number][]= [ + [0, 1], [2, 3], [4, 5], [6, 7], + [0, 2], [1, 3], [4, 6], [5, 7], + [0, 4], [1, 5], [2, 6], [3, 7] + ]; + return this.drawShape(vertices, edges); + } protected prepGeneration(genVars: shapeGenVars, options?: shapeGenOptions) { genVars.isHollow = options?.hollow ?? false; diff --git a/src/server/shapes/cylinder.ts b/src/server/shapes/cylinder.ts index 797aa3e18..a43f60a81 100644 --- a/src/server/shapes/cylinder.ts +++ b/src/server/shapes/cylinder.ts @@ -30,6 +30,25 @@ export class CylinderShape extends Shape { return (lX*lX + lZ*lZ > 1.0) ? null : <[number, number]>[-this.height/2, this.height-1-this.height/2]; } + public getOutline(loc: Vector) { + // TODO: Support oblique cylinders + loc = loc.offset(0, -this.height/2, 0).ceil(); + const locWithOffset = loc.offset(0.5, 0, 0.5); + const maxRadius = Math.max(...this.radii) + 0.5; + const vertices = [ + locWithOffset.add([-maxRadius, 0, 0]), locWithOffset.add([-maxRadius, this.height, 0]), + locWithOffset.add([maxRadius, 0, 0]), locWithOffset.add([maxRadius, this.height, 0]), + locWithOffset.add([0, 0, -maxRadius]), locWithOffset.add([0, this.height, -maxRadius]), + locWithOffset.add([0, 0, maxRadius]), locWithOffset.add([0, this.height, maxRadius]), + ] + const edges: [number, number][] = [[0, 1], [2, 3], [4, 5], [6, 7]]; + return [ + ...this.drawCircle(loc.sub([0, 0.5, 0]), maxRadius, "y"), + ...this.drawCircle(loc.sub([0, 0.5, 0]).add([0, this.height, 0]), maxRadius, "y"), + ...this.drawShape(vertices, edges) + ]; + } + protected prepGeneration(genVars: shapeGenVars, options?: shapeGenOptions) { genVars.isHollow = options?.hollow ?? false; genVars.radiiOff = this.radii.map(v => v + 0.5); diff --git a/src/server/shapes/expression.ts b/src/server/shapes/expression.ts index a037fb3f5..865416eb0 100644 --- a/src/server/shapes/expression.ts +++ b/src/server/shapes/expression.ts @@ -26,6 +26,28 @@ export class ExpressionShape extends Shape { return [null, null] as [number, number]; } + public getOutline(loc: Vector) { + const min = loc; + const max = loc.add(this.size); + + const vertices = [ + new Vector(min.x, min.y, min.z), + new Vector(max.x, min.y, min.z), + new Vector(min.x, max.y, min.z), + new Vector(max.x, max.y, min.z), + new Vector(min.x, min.y, max.z), + new Vector(max.x, min.y, max.z), + new Vector(min.x, max.y, max.z), + new Vector(max.x, max.y, max.z) + ]; + const edges: [number, number][]= [ + [0, 1], [2, 3], [4, 5], [6, 7], + [0, 2], [1, 3], [4, 6], [5, 7], + [0, 4], [1, 5], [2, 6], [3, 7] + ]; + return this.drawShape(vertices, edges); + } + protected prepGeneration(genVars: shapeGenVars, options?: shapeGenOptions) { genVars.hollow = options.hollow; genVars.neighbourOffsets = [[0, 1, 0], [0, -1, 0], [1, 0, 0], [-1, 0, 0], [0, 0, 1], [0, 0, -1]]; diff --git a/src/server/shapes/pyramid.ts b/src/server/shapes/pyramid.ts index 0cd02f920..6fa243b2c 100644 --- a/src/server/shapes/pyramid.ts +++ b/src/server/shapes/pyramid.ts @@ -22,6 +22,21 @@ export class PyramidShape extends Shape { throw new Error("getYRange not implemented!"); return null; } + + public getOutline(loc: Vector) { + const vertices = [ + loc.add([-this.size + 1, 0, -this.size + 1]), + loc.add([-this.size + 1, 0, this.size]), + loc.add([this.size, 0, -this.size + 1]), + loc.add([this.size, 0, this.size]), + loc.add([0.5, this.size, 0.5]), + ]; + const edges: [number, number][]= [ + [0, 1], [1, 3], [2, 0], [3, 2], + [0,4], [1, 4], [2, 4], [3, 4], + ]; + return this.drawShape(vertices, edges); + } protected prepGeneration(genVars: shapeGenVars, options?: shapeGenOptions) { genVars.isHollow = options?.hollow ?? false; diff --git a/src/server/shapes/sphere.ts b/src/server/shapes/sphere.ts index 0f01cf357..1c7dcf06d 100644 --- a/src/server/shapes/sphere.ts +++ b/src/server/shapes/sphere.ts @@ -25,6 +25,12 @@ export class SphereShape extends Shape { return null; } + public getOutline(loc: Vector) { + // TODO: Support oblique spheres + const maxRadius = Math.max(...this.radii) + 0.5; + return [...this.drawCircle(loc, maxRadius, "x"), ...this.drawCircle(loc, maxRadius, "y"), ...this.drawCircle(loc, maxRadius, "z")]; + } + protected prepGeneration(genVars: shapeGenVars, options?: shapeGenOptions) { genVars.isHollow = options?.hollow ?? false; genVars.radiiOff = this.radii.map(v => v + 0.5); diff --git a/src/server/tools/base_tool.ts b/src/server/tools/base_tool.ts index a4f12a1c2..50c29f886 100644 --- a/src/server/tools/base_tool.ts +++ b/src/server/tools/base_tool.ts @@ -1,14 +1,16 @@ import { Player, system } from "@minecraft/server"; import { PlayerSession } from "../sessions.js"; import { Server, Thread } from "@notbeer-api"; -import { print, printerr } from "../util.js"; +import { printerr } from "../util.js"; import { RawText, Vector } from "@notbeer-api"; export enum ToolAction { USE = "use", USE_ON = "useOn", BREAK = "break", - HIT = "hit" + HIT = "hit", + DROP = "drop", + STOP_HOLD = "stopHold" } /** @@ -31,10 +33,18 @@ export abstract class Tool { * The function that's called when the tool has hit a block. */ readonly hit: (self: Tool, player: Player, session: PlayerSession, loc: Vector) => void | Generator; + /** + * The function that's called when the tool is dropped. + */ + readonly drop: (self: Tool, player: Player, session: PlayerSession) => void | Generator; /** * The function that's called every tick the tool is held. */ readonly tick: (self: Tool, player: Player, session: PlayerSession, tick: number) => Generator; + /** + * The function that's called when the tool stops being held. + */ + readonly stopHold: (self: Tool, player: Player, session: PlayerSession) => void | Generator; /** * The permission required for the tool to be used. */ @@ -50,16 +60,12 @@ export abstract class Tool { */ type: string; - private currentPlayer: Player; - log(message: string | RawText) { - print(message, this.currentPlayer, true); - } - private useOnTick = 0; private lastUse = system.currentTick; - process(session: PlayerSession, tick: number, action: ToolAction, loc?: Vector): boolean { + process(session: PlayerSession, action: ToolAction, loc?: Vector): boolean { const player = session.getPlayer(); + const tick = system.currentTick; if (!this[action]) return false; @@ -72,7 +78,6 @@ export abstract class Tool { }; new Thread().start(function* (self: Tool, player: Player, session: PlayerSession, action: ToolAction, loc: Vector) { - self.currentPlayer = player; session.usingItem = true; try { if (!Server.player.hasPermission(player, self.permission)) { @@ -97,7 +102,6 @@ export abstract class Tool { } finally { session.usingItem = false; } - self.currentPlayer = null; }, this, player, session, action, loc); return true; } diff --git a/src/server/tools/button_tools.ts b/src/server/tools/button_tools.ts index af8d2c361..5f63dfc36 100644 --- a/src/server/tools/button_tools.ts +++ b/src/server/tools/button_tools.ts @@ -1,13 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Vector3, Player, world } from "@minecraft/server"; -import { contentLog, regionSize, regionTransformedBounds, Server, Vector } from "@notbeer-api"; +import { Player } from "@minecraft/server"; +import { regionSize, regionTransformedBounds, Server, Vector } from "@notbeer-api"; import { PlayerSession } from "../sessions.js"; import { Tool } from "./base_tool.js"; import { Tools } from "./tool_manager.js"; -import { RawText } from "@notbeer-api"; import { PlayerUtil } from "@modules/player_util.js"; import { Selection } from "@modules/selection.js"; -import { print } from "server/util.js"; abstract class CommandButton extends Tool { abstract readonly command: string | string[]; @@ -129,37 +127,6 @@ class SpawnGlassTool extends Tool { } Tools.register(SpawnGlassTool, "spawn_glass", "wedit:spawn_glass"); -class SelectionFillTool extends Tool { - permission = "worldedit.region.replace"; - - use = function (self: Tool, player: Player, session: PlayerSession) { - if (session.globalMask.empty()) { - Server.command.callCommand(player, "set", ["air"]); - } else { - Server.command.callCommand(player, "replace", ["air", "air"]); - } - }; -} -Tools.register(SelectionFillTool, "selection_fill", "wedit:selection_fill"); - -class SelectionWallTool extends CommandButton { - permission = "worldedit.region.walls"; - command = ["walls", "air"]; -} -Tools.register(SelectionWallTool, "selection_wall", "wedit:selection_wall"); - -class SelectionOutlineTool extends CommandButton { - permission = "worldedit.region.faces"; - command = ["faces", "air"]; -} -Tools.register(SelectionOutlineTool, "selection_outline", "wedit:selection_outline"); - -class DrawLineTool extends CommandButton { - permission = "worldedit.region.line"; - command = ["line", "air"]; -} -Tools.register(DrawLineTool, "draw_line", "wedit:draw_line"); - class ConfigTool extends Tool { use = function (self: Tool, player: Player, session: PlayerSession) { session.enterSettings(); diff --git a/src/server/tools/generation_tools.ts b/src/server/tools/generation_tools.ts new file mode 100644 index 000000000..102b530ef --- /dev/null +++ b/src/server/tools/generation_tools.ts @@ -0,0 +1,272 @@ +import { Dimension, Player, Vector3, system } from "@minecraft/server"; +import { PlayerUtil } from "@modules/player_util"; +import { RawText, Server, Vector, regionBounds } from "@notbeer-api"; +import { generateLine } from "server/commands/region/line"; +import { PlayerSession } from "server/sessions"; +import { Tool } from "./base_tool"; +import { Tools } from "./tool_manager"; +import { print, snap } from "server/util"; +import { Jobs } from "@modules/jobs"; +import { SphereShape } from "server/shapes/sphere"; +import { CylinderShape } from "server/shapes/cylinder"; +import { PyramidShape } from "server/shapes/pyramid"; +import { Shape } from "server/shapes/base_shape"; + +function trySpawnParticle(player: Player, type: string, location: Vector3) { + try { + player.spawnParticle(type, location); + } catch {} +} + +abstract class GeneratorTool extends Tool { + protected posStart = new Map(); // [location, dimension type] + + protected baseUse(player: Player, session: PlayerSession, loc?: Vector) { + if (player.isSneaking) { + Server.uiForms.show("$selectGenMode", player); + return true; + } + + if (session.globalPattern.empty()) throw "worldEdit.selectionFill.noPattern"; + if (!this.posStart.has(session)) { + if (loc) this.posStart.set(session, [loc, player.dimension.id]); + return true; + } + return false; + }; + + protected baseTick(player: Player, session: PlayerSession) { + if (system.currentTick % 5 !== 0 || !this.posStart.has(session) || !session.drawOutlines || this.posStart.get(session)[1] !== player.dimension.id) { + return true; + } + + if (this.posStart.get(session)[1] !== player.dimension.id) { + this.posStart.delete(session); + return true; + } + return false; + } + + protected traceForPos(player: Player) { + return PlayerUtil.traceForBlock(player, 8); + } + + protected getFirstPos(session: PlayerSession) { + return this.posStart.get(session)[0]; + } + + protected clearFirstPos(session: PlayerSession) { + return this.posStart.delete(session); + } + + stopHold = function (self: GeneratorTool, _: Player, session: PlayerSession) { + self.posStart.delete(session); + }; + + drop = function (self: GeneratorTool, _: Player, session: PlayerSession) { + self.posStart.delete(session); + }; +} + +class DrawLineTool extends GeneratorTool { + permission = "worldedit.region.line"; + + commonUse = function* (self: DrawLineTool, player: Player, session: PlayerSession, loc?: Vector) { + if (self.baseUse(player, session, loc)) return; + + const pos1 = self.getFirstPos(session); + const pos2 = self.traceForPos(player); + const [start, end] = regionBounds([pos1, pos2]); + self.clearFirstPos(session); + + const dim = player.dimension; + const pattern = session.globalPattern; + pattern.setContext(session, [start, end]); + + const history = session.getHistory(); + const record = history.record(); + let count: number; + try { + const points = (yield* generateLine(pos1, pos2)).map((p) => p.floor()); + history.addUndoStructure(record, start, end); + count = 0; + for (const point of points) { + const block = dim.getBlock(point); + if (session.globalMask.matchesBlock(block) && pattern.setBlock(block)) { + count++; + } + yield; + } + + history.recordSelection(record, session); + history.addRedoStructure(record, start, end); + history.commit(record); + } catch (e) { + history.cancel(record); + throw e; + } + + print(RawText.translate("commands.blocks.wedit:created").with(`${count}`), player, true); + }; + + tick = function* (self: DrawLineTool, player: Player, session: PlayerSession) { + if (self.baseTick(player, session)) return; + + let lineStart = self.posStart.get(session)[0]; + const lineEnd = self.traceForPos(player); + const length = lineEnd.sub(lineStart).length; + if (length > 32) { + lineStart = lineEnd + .add(lineStart.sub(lineEnd).normalized().mul(32)) + .floor(); + } + + const genLine = generateLine(lineStart, lineEnd); + let val: IteratorResult; + while (!val?.done) val = genLine.next(); + val.value.forEach((p) => { + trySpawnParticle(player, "wedit:selection_draw", p); + trySpawnParticle(player, "wedit:selection_draw", Vector.add(p, [1, 0, 0])); + trySpawnParticle(player, "wedit:selection_draw", Vector.add(p, [0, 1, 0])); + trySpawnParticle(player, "wedit:selection_draw", Vector.add(p, [1, 1, 0])); + trySpawnParticle(player, "wedit:selection_draw", Vector.add(p, [0, 0, 1])); + trySpawnParticle(player, "wedit:selection_draw", Vector.add(p, [1, 0, 1])); + trySpawnParticle(player, "wedit:selection_draw", Vector.add(p, [0, 1, 1])); + trySpawnParticle(player, "wedit:selection_draw", Vector.add(p, [1, 1, 1])); + }); + }; + + useOn = this.commonUse; + use = this.commonUse; +} +Tools.register(DrawLineTool, "draw_line", "wedit:draw_line"); + +class DrawSphereTool extends GeneratorTool { + permission = "worldedit.generation.sphere"; + + commonUse = function* (self: DrawSphereTool, player: Player, session: PlayerSession, loc?: Vector) { + if (self.baseUse(player, session, loc)) return; + + 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)); + self.clearFirstPos(session); + + const job = Jobs.startJob(session, 2, sphereShape.getRegion(center)); + const count = yield* Jobs.perform(job, sphereShape.generate(center, pattern, null, session)); + Jobs.finishJob(job); + + print(RawText.translate("commands.blocks.wedit:created").with(`${count}`), player, true); + }; + + tick = function* (self: DrawSphereTool, player: Player, session: PlayerSession) { + if (self.baseTick(player, session)) return; + + 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 resolution = snap(Math.min(radius * 2*Math.PI, 36), 4); + + for (const [rotateBy, vec] of axes) { + for (let i = 0; i < resolution; i++) { + let point: Vector = rotateBy.call(vec, i / resolution * 360); + point = point.mul(radius).add(center).add(0.5); + trySpawnParticle(player, "wedit:selection_draw", point); + } + } + }; + + useOn = this.commonUse; + use = this.commonUse; +} +Tools.register(DrawSphereTool, "draw_sphere", "wedit:draw_sphere"); + +class DrawCylinderTool extends GeneratorTool { + permission = "worldedit.generation.cyl"; + + commonUse = function* (self: DrawCylinderTool, player: Player, session: PlayerSession, loc?: Vector) { + if (self.baseUse(player, session, loc)) return; + + const [shape, center] = self.getShape(player, session); + const pattern = session.globalPattern; + pattern.setContext(session, shape.getRegion(center)); + self.clearFirstPos(session); + + const job = Jobs.startJob(session, 2, shape.getRegion(center)); + const count = yield* Jobs.perform(job, shape.generate(center, pattern, null, session)); + Jobs.finishJob(job); + + print(RawText.translate("commands.blocks.wedit:created").with(`${count}`), player, true); + }; + + tick = function* (self: DrawCylinderTool, player: Player, session: PlayerSession) { + if (self.baseTick(player, session)) return; + + const [shape, loc] = self.getShape(player, session); + for (const particle of shape.getOutline(loc)) { + trySpawnParticle(player, ...particle); + } + }; + + getShape(player: Player, session: PlayerSession): [CylinderShape, Vector] { + const center = this.getFirstPos(session).clone(); + const pos2 = this.traceForPos(player); + const radius = Math.floor(pos2.sub(center).mul([1, 0, 1]).length); + let height = pos2.y - center.y + 1; + if (height < 1) { + center.y += height; + height = -height + 1; + } + return [new CylinderShape(height, radius), center]; + } + + useOn = this.commonUse; + use = this.commonUse; +} +Tools.register(DrawCylinderTool, "draw_cylinder", "wedit:draw_cylinder"); + +class DrawPyramidTool extends GeneratorTool { + permission = "worldedit.generation.pyramid"; + + commonUse = function* (self: DrawPyramidTool, player: Player, session: PlayerSession, loc?: Vector) { + if (self.baseUse(player, session, loc)) return; + + const [shape, center] = self.getShape(player, session); + const pattern = session.globalPattern; + pattern.setContext(session, shape.getRegion(center)); + self.clearFirstPos(session); + + const job = Jobs.startJob(session, 2, shape.getRegion(center)); + const count = yield* Jobs.perform(job, shape.generate(center, pattern, null, session)); + Jobs.finishJob(job); + + print(RawText.translate("commands.blocks.wedit:created").with(`${count}`), player, true); + }; + + tick = function* (self: DrawPyramidTool, player: Player, session: PlayerSession) { + if (self.baseTick(player, session)) return; + + const [shape, loc] = self.getShape(player, session); + for (const particle of shape.getOutline(loc)) { + trySpawnParticle(player, ...particle); + } + }; + + getShape(player: Player, session: PlayerSession): [PyramidShape, Vector] { + const center = this.getFirstPos(session).clone(); + const pos2 = this.traceForPos(player); + const size = Math.max(...pos2.sub(center).toArray().map((v, i) => i !== 1 ? Math.abs(v) : v)) + 1; + return [new PyramidShape(size), center]; + } + + useOn = this.commonUse; + use = this.commonUse; +} +Tools.register(DrawPyramidTool, "draw_pyramid", "wedit:draw_pyramid"); diff --git a/src/server/tools/picker_tool.ts b/src/server/tools/picker_tool.ts index e106e8d63..56370c0a7 100644 --- a/src/server/tools/picker_tool.ts +++ b/src/server/tools/picker_tool.ts @@ -3,6 +3,7 @@ import { BlockPermutation, Vector3, Player } from "@minecraft/server"; import { PlayerSession } from "../sessions.js"; import { Tool } from "./base_tool.js"; import { Tools } from "./tool_manager.js"; +import { print } from "server/util.js"; class PatternPickerTool extends Tool { useOn = function (self: Tool, player: Player, session: PlayerSession, loc: Vector3) { @@ -22,9 +23,7 @@ class PatternPickerTool extends Tool { if (blockName.startsWith("minecraft:")) { blockName = blockName.slice("minecraft:".length); } - self.log(RawText.translate("worldedit.patternPicker." + (addedToPattern ? "add" : "set")) - .append("text", blockName) - ); + print(RawText.translate("worldedit.patternPicker." + (addedToPattern ? "add" : "set")).append("text", blockName), player, true); }; use = function (self: Tool, player: Player, session: PlayerSession) { let addedToPattern = true; @@ -33,9 +32,7 @@ class PatternPickerTool extends Tool { addedToPattern = false; } session.globalPattern.addBlock(BlockPermutation.resolve("minecraft:air")); - self.log(RawText.translate("worldedit.patternPicker." + (addedToPattern ? "add" : "set")) - .append("text", "air") - ); + print(RawText.translate("worldedit.patternPicker." + (addedToPattern ? "add" : "set")).append("text", "air"), player, true); }; } Tools.register(PatternPickerTool, "pattern_picker", "wedit:pattern_picker"); @@ -59,9 +56,7 @@ class MaskPickerTool extends Tool { if (blockName.startsWith("minecraft:")) { blockName = blockName.slice("minecraft:".length); } - self.log(RawText.translate("worldedit.maskPicker." + (addedToPattern ? "add" : "set")) - .append("text", blockName) - ); + print(RawText.translate("worldedit.maskPicker." + (addedToPattern ? "add" : "set")).append("text", blockName), player, true); }; use = function (self: Tool, player: Player, session: PlayerSession) { let addedToPattern = true; @@ -70,9 +65,7 @@ class MaskPickerTool extends Tool { addedToPattern = false; } session.globalMask.addBlock(BlockPermutation.resolve("minecraft:air")); - self.log(RawText.translate("worldedit.maskPicker." + (addedToPattern ? "add" : "set")) - .append("text", "air") - ); + print(RawText.translate("worldedit.maskPicker." + (addedToPattern ? "add" : "set")).append("text", "air"), player, true); }; } Tools.register(MaskPickerTool, "mask_picker", "wedit:mask_picker"); diff --git a/src/server/tools/region_tools.ts b/src/server/tools/region_tools.ts new file mode 100644 index 000000000..712fa2032 --- /dev/null +++ b/src/server/tools/region_tools.ts @@ -0,0 +1,61 @@ +import { Player } from "@minecraft/server"; +import { Server } from "@notbeer-api"; +import { PlayerSession } from "server/sessions"; +import { Tool } from "./base_tool"; +import { Tools } from "./tool_manager"; + +class SelectionFillTool extends Tool { + permission = "worldedit.region.replace"; + + use = function (_: Tool, player: Player, session: PlayerSession) { + if (player.isSneaking) { + Server.uiForms.show("$selectRegionMode", player); + } else { + if (session.globalMask.empty()) { + Server.command.callCommand(player, "set", ["air"]); + } else { + Server.command.callCommand(player, "replace", ["air", "air"]); + } + } + }; +} +Tools.register(SelectionFillTool, "selection_fill", "wedit:selection_fill"); + +class SelectionWallTool extends Tool { + permission = "worldedit.region.walls"; + + use = function (_t: Tool, player: Player, _s: PlayerSession) { + if (player.isSneaking) { + Server.uiForms.show("$selectRegionMode", player); + } else { + Server.command.callCommand(player, "walls", ["air"]); + } + }; +} +Tools.register(SelectionWallTool, "selection_wall", "wedit:selection_wall"); + +class SelectionOutlineTool extends Tool { + permission = "worldedit.region.faces"; + + use = function (_t: Tool, player: Player, _s: PlayerSession) { + if (player.isSneaking) { + Server.uiForms.show("$selectRegionMode", player); + } else { + Server.command.callCommand(player, "faces", ["air"]); + } + }; +} +Tools.register(SelectionOutlineTool, "selection_outline", "wedit:selection_outline"); + +class SelectionHollowTool extends Tool { + permission = "worldedit.region.hollow"; + + use = function (_t: Tool, player: Player, _s: PlayerSession) { + if (player.isSneaking) { + Server.uiForms.show("$selectRegionMode", player); + } else { + Server.command.callCommand(player, "hollow", ["1"]); + } + }; +} +Tools.register(SelectionHollowTool, "selection_hollow", "wedit:selection_hollow"); diff --git a/src/server/tools/selection_tools.ts b/src/server/tools/selection_tools.ts index ab060e207..a4f5507c3 100644 --- a/src/server/tools/selection_tools.ts +++ b/src/server/tools/selection_tools.ts @@ -12,6 +12,9 @@ class SelectionTool extends Tool { break = function (self: Tool, player: Player, session: PlayerSession, loc: Vector3) { Server.command.callCommand(player, "pos1", [`${loc.x}`, `${loc.y}`, `${loc.z}`]); }; + drop = function (self: Tool, player: Player, session: PlayerSession) { + Server.command.callCommand(player, "desel"); + }; } Tools.register(SelectionTool, "selection_wand"); @@ -23,5 +26,8 @@ class FarSelectionTool extends Tool { break = function (self: Tool, player: Player) { Server.command.callCommand(player, "hpos1"); }; + drop = function (self: Tool, player: Player, session: PlayerSession) { + Server.command.callCommand(player, "desel"); + }; } Tools.register(FarSelectionTool, "far_selection_wand"); \ No newline at end of file diff --git a/src/server/tools/register_tools.ts b/src/server/tools/tool_list.ts similarity index 82% rename from src/server/tools/register_tools.ts rename to src/server/tools/tool_list.ts index e70869dc1..3af153502 100644 --- a/src/server/tools/register_tools.ts +++ b/src/server/tools/tool_list.ts @@ -3,8 +3,10 @@ import "./selection_tools.js"; import "./stacker_tool.js"; import "./navigation_tool.js"; import "./command_tool.js"; +import "./generation_tools.js"; import "./replacer_tool.js"; import "./cycler_tool.js"; import "./button_tools.js"; import "./brush_tools.js"; +import "./region_tools.js"; import "./superpickaxe_tool.js"; diff --git a/src/server/tools/tool_manager.ts b/src/server/tools/tool_manager.ts index b029fb247..aab507d84 100644 --- a/src/server/tools/tool_manager.ts +++ b/src/server/tools/tool_manager.ts @@ -1,4 +1,4 @@ -import { Player, ItemStack, ItemUseBeforeEvent, world, PlayerBreakBlockBeforeEvent, EntityHitBlockAfterEvent } from "@minecraft/server"; +import { Player, ItemStack, ItemUseBeforeEvent, world, PlayerBreakBlockBeforeEvent, EntityHitBlockAfterEvent, system } from "@minecraft/server"; import { contentLog, Server, sleep, Thread, Vector, Database } from "@notbeer-api"; import { Tool, ToolAction } from "./base_tool.js"; import { PlayerSession, getSession, hasSession } from "../sessions.js"; @@ -14,10 +14,9 @@ class ToolBuilder { private bindings = new Map>(); private databases = new Map(); private fixedBindings = new Map(); + private prevHeldTool = new Map(); private conditionalBindings = new Map(); - private disabled: string[] = []; - private currentTick = 0; constructor() { Server.on("itemUseBefore", ev => { @@ -30,6 +29,13 @@ class ToolBuilder { this.onItemUse(ev.itemStack, ev.source as Player, ev, Vector.from(ev.block)); }); + Server.on("entityCreate", ({ entity }) => { + if (!entity.hasComponent("minecraft:item")) return; + + const player = entity.dimension.getPlayers({ closest: 1, location: entity.location, maxDistance: 2 })[0]; + if (player) this.onItemDrop(entity.getComponent("item").itemStack, player); + }); + Server.on("blockBreak", ev => { if (!ev.itemStack) return; this.onBlockBreak(ev.itemStack, ev.player, ev, Vector.from(ev.block)); @@ -42,17 +48,16 @@ class ToolBuilder { this.onBlockHit(item, ev.damagingEntity as Player, ev, Vector.from(ev.hitBlock)); }); - Server.on("tick", ev => { - this.currentTick = ev.currentTick; - }); - new Thread().start(function* (self: ToolBuilder) { while (true) { for (const player of world.getPlayers()) { try { const item = Server.player.getHeldItem(player); - if (!item) continue; - yield* self.onItemTick(item, player, self.currentTick); + if (item) { + yield* self.onItemTick(item, player, system.currentTick); + } else { + self.stopHolding(player); + } } catch (err) { contentLog.error(err); } @@ -180,9 +185,7 @@ class ToolBuilder { } private *onItemTick(item: ItemStack, player: Player, tick: number) { - if (this.disabled.includes(player.id) || !hasSession(player.id)) { - return; - } + if (this.disabled.includes(player.id) || !hasSession(player.id)) return this.stopHolding(player); const key = item.typeId; let tool: Tool; @@ -191,7 +194,12 @@ class ToolBuilder { } else if (this.fixedBindings.has(key)) { tool = this.fixedBindings.get(key); } else { - return; + return this.stopHolding(player); + } + + if (this.prevHeldTool.get(player) !== tool) { + this.stopHolding(player); + this.prevHeldTool.set(player, tool); } const gen = tool.tick?.(tool, player, getSession(player), tick); @@ -199,9 +207,7 @@ class ToolBuilder { } private onItemUse(item: ItemStack, player: Player, ev: ItemUseBeforeEvent, loc?: Vector) { - if (this.disabled.includes(player.id) || !hasSession(player.id)) { - return; - } + if (this.disabled.includes(player.id) || !hasSession(player.id)) return; const key = item.typeId; let tool: Tool; @@ -214,16 +220,13 @@ class ToolBuilder { } else { return; } - - if (tool.process(getSession(player), this.currentTick, loc ? ToolAction.USE_ON : ToolAction.USE, loc)) { + if (tool.process(getSession(player), loc ? ToolAction.USE_ON : ToolAction.USE, loc)) { ev.cancel = true; } } private onBlockBreak(item: ItemStack, player: Player, ev: PlayerBreakBlockBeforeEvent, loc: Vector) { - if (this.disabled.includes(player.id)) { - return; - } + if (this.disabled.includes(player.id)) return; const key = item.typeId; let tool: Tool; @@ -236,14 +239,30 @@ class ToolBuilder { } else { return; } - - if (tool.process(getSession(player), this.currentTick, ToolAction.BREAK, loc)) { + if (tool.process(getSession(player), ToolAction.BREAK, loc)) { ev.cancel = true; } } private onBlockHit(item: ItemStack, player: Player, ev: EntityHitBlockAfterEvent, loc: Vector) { - if (this.disabled.includes(player.id)) { + if (this.disabled.includes(player.id)) return; + + const key = item.typeId; + let tool: Tool; + if (this.bindings.get(player.id)?.has(key)) { + tool = this.bindings.get(player.id).get(key); + } else if (this.fixedBindings.has(key)) { + tool = this.fixedBindings.get(key); + } else if (this.conditionalBindings.get(key)?.condition(player, getSession(player))) { + tool = this.conditionalBindings.get(key).tool; + } else { + return; + } + tool.process(getSession(player), ToolAction.DROP); + } + + private onItemDrop(item: ItemStack, player: Player) { + if (this.disabled.includes(player.id) || !hasSession(player.id)) { return; } @@ -258,8 +277,7 @@ class ToolBuilder { } else { return; } - - tool.process(getSession(player), this.currentTick, ToolAction.HIT, loc); + tool.process(getSession(player), ToolAction.DROP); } private createPlayerBindingMap(playerId: string) { @@ -280,5 +298,12 @@ class ToolBuilder { } } } + + private stopHolding(player: Player) { + if (this.prevHeldTool.has(player)) { + this.prevHeldTool.get(player)?.process(getSession(player), ToolAction.STOP_HOLD); + this.prevHeldTool.delete(player); + } + } } export const Tools = new ToolBuilder(); \ No newline at end of file diff --git a/src/server/ui/index.ts b/src/server/ui/index.ts index 3d7214266..5e7f9d90e 100644 --- a/src/server/ui/index.ts +++ b/src/server/ui/index.ts @@ -1,2 +1,4 @@ import "./hotbar_menus.js"; -import "./config_menu.js"; \ No newline at end of file +import "./config_menu.js"; +import "./select_region_tool.js"; +import "./select_gen_tool.js"; \ No newline at end of file diff --git a/src/server/ui/select_gen_tool.ts b/src/server/ui/select_gen_tool.ts new file mode 100644 index 000000000..78fb2bd83 --- /dev/null +++ b/src/server/ui/select_gen_tool.ts @@ -0,0 +1,36 @@ +import { EquipmentSlot, ItemStack } from "@minecraft/server"; +import { Server } from "@notbeer-api"; + +Server.uiForms.register("$selectGenMode", { + title: "%worldedit.genMode.selectOp", + buttons: [ + { + text: "%worldedit.genMode.line", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:draw_line")); + }, + icon: "textures/items/draw_line" + }, + { + text: "%worldedit.genMode.sphere", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:draw_sphere")); + }, + icon: "textures/items/draw_sphere" + }, + { + text: "%worldedit.genMode.cylinder", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:draw_cylinder")); + }, + icon: "textures/items/draw_cylinder" + }, + { + text: "%worldedit.genMode.pyramid", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:draw_pyramid")); + }, + icon: "textures/items/draw_pyramid" + } + ] +}); \ No newline at end of file diff --git a/src/server/ui/select_region_tool.ts b/src/server/ui/select_region_tool.ts new file mode 100644 index 000000000..824f94565 --- /dev/null +++ b/src/server/ui/select_region_tool.ts @@ -0,0 +1,36 @@ +import { EquipmentSlot, ItemStack } from "@minecraft/server"; +import { Server } from "@notbeer-api"; + +Server.uiForms.register("$selectRegionMode", { + title: "%worldedit.regionMode.selectOp", + buttons: [ + { + text: "%worldedit.regionMode.fill", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:selection_fill")); + }, + icon: "textures/items/selection_fill" + }, + { + text: "%worldedit.regionMode.outline", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:selection_outline")); + }, + icon: "textures/items/selection_outline" + }, + { + text: "%worldedit.regionMode.wall", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:selection_wall")); + }, + icon: "textures/items/selection_wall" + }, + { + text: "%worldedit.regionMode.hollow", + action: (_, player) => { + Server.player.getEquipment(player).setEquipment(EquipmentSlot.Mainhand, new ItemStack("wedit:selection_hollow")); + }, + icon: "textures/items/selection_hollow" + }, + ] +}); \ No newline at end of file diff --git a/texts/en_US.po b/texts/en_US.po index 37542095e..7cc253107 100644 --- a/texts/en_US.po +++ b/texts/en_US.po @@ -60,8 +60,18 @@ msgid "item.wedit:selection_wall" msgstr "Wall Selection" msgid "item.wedit:selection_outline" msgstr "Outline Selection" +msgid "item.wedit:selection_hollow" +msgstr "Hollow 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" +msgstr "Draw Cylinder" +msgid "item.wedit:draw_pyramid" +msgstr "Draw Pyramid" # WORLDEDIT TOOLS AND CONFIG @@ -111,6 +121,28 @@ msgstr "Sphere" msgid "worldedit.selectionMode.cylinder" msgstr "Cylinder" +msgid "worldedit.regionMode.selectOp" +msgstr "Select Region Operation" +msgid "worldedit.regionMode.fill" +msgstr "Fill Mode" +msgid "worldedit.regionMode.outline" +msgstr "Outline Mode" +msgid "worldedit.regionMode.wall" +msgstr "Wall Mode" +msgid "worldedit.regionMode.hollow" +msgstr "Hollow Mode" + +msgid "worldedit.genMode.selectOp" +msgstr "Select Shape to Generate" +msgid "worldedit.genMode.line" +msgstr "Line" +msgid "worldedit.genMode.sphere" +msgstr "Sphere" +msgid "worldedit.genMode.cylinder" +msgstr "Cylinder" +msgid "worldedit.genMode.pyramid" +msgstr "Pyramid" + msgid "worldedit.config.tool.noProps" msgstr "No Tool Properties" msgid "worldedit.config.tool.noProps.detail"