From 37eb01ec37b81f827fb675c32ff254e767a53f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Thu, 20 Mar 2025 22:07:26 +0100 Subject: [PATCH] World edit commands for Cubyz (#1141) * Add basic worldedit commands * Fix style issues * Fix style issues and command names * Fix style issues * Store worldedit command data in User * Fix blueprint memory leak * Add loading from Zon * Use Block instead of u32 * Add binary storage format * Add binary blueprint loading * Fix formatting in copy.zig * Use BinaryWriter for writing * Use ReaderWriter for reading * Add delete command * Update src/blueprint.zig * Apply review suggestions * Fix formatting issues * Update src/blueprint.zig * Fix formatting issues * Fix compilation issue * make pos1 and pos2 null initially and also show the selection on the client * fix issue * Fix formatting issues * Add deselect command * Update src/blueprint.zig * Add clone to Blueprint * Convert to manual serialization * Apply review suggestions * Use Array3D * Apply suggestions from code review Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Apply review suggestions * Reorder functions * Rename * Apply review suggestions * Apply review suggestions * Fix outlines * Remove append * Apply review suggestions * Update src/blueprint.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Replace index with dash * No green it is * Update src/server/command/worldedit/pos2.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Update src/server/command/worldedit/pos2.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Update src/server/command/worldedit/pos1.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Apply review suggestions * Abstract file io to struct * Revert "Abstract file io to struct" This reverts commit f0bbe50aad0887d562069cb9ce18085f3de6e4cb. * Add openBlueprintsDir function * Apply review suggestions * Apply review suggestions * Update src/server/command/worldedit/blueprint.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Apply review suggestions --------- Co-authored-by: OneAvargeCoder193 Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> --- src/blueprint.zig | 244 +++++++++++++++++++++ src/game.zig | 4 + src/main.zig | 1 + src/network.zig | 44 +++- src/renderer.zig | 11 + src/server/command/_command.zig | 1 + src/server/command/_list.zig | 7 + src/server/command/worldedit/blueprint.zig | 187 ++++++++++++++++ src/server/command/worldedit/copy.zig | 41 ++++ src/server/command/worldedit/deselect.zig | 20 ++ src/server/command/worldedit/paste.zig | 29 +++ src/server/command/worldedit/pos1.zig | 22 ++ src/server/command/worldedit/pos2.zig | 22 ++ src/server/server.zig | 15 ++ src/utils.zig | 6 + 15 files changed, 652 insertions(+), 2 deletions(-) create mode 100644 src/blueprint.zig create mode 100644 src/server/command/worldedit/blueprint.zig create mode 100644 src/server/command/worldedit/copy.zig create mode 100644 src/server/command/worldedit/deselect.zig create mode 100644 src/server/command/worldedit/paste.zig create mode 100644 src/server/command/worldedit/pos1.zig create mode 100644 src/server/command/worldedit/pos2.zig diff --git a/src/blueprint.zig b/src/blueprint.zig new file mode 100644 index 00000000..25daff04 --- /dev/null +++ b/src/blueprint.zig @@ -0,0 +1,244 @@ +const std = @import("std"); + +const main = @import("main.zig"); +const Compression = main.utils.Compression; +const ZonElement = @import("zon.zig").ZonElement; +const vec = main.vec; +const Vec3i = vec.Vec3i; + +const Array3D = main.utils.Array3D; +const Block = main.blocks.Block; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const User = main.server.User; + +const GameIdToBlueprintIdMapType = std.AutoHashMap(u16, u16); +const BlockIdSizeType = u32; +const BlockStorageType = u32; + +const BinaryWriter = main.utils.BinaryWriter; +const BinaryReader = main.utils.BinaryReader; + +pub const blueprintVersion = 0; + +pub const BlueprintCompression = enum(u16) { + deflate, +}; + +pub const Blueprint = struct { + blocks: Array3D(Block), + + pub fn init(allocator: NeverFailingAllocator) Blueprint { + return .{.blocks = .init(allocator, 0, 0, 0)}; + } + pub fn deinit(self: Blueprint, allocator: NeverFailingAllocator) void { + self.blocks.deinit(allocator); + } + pub fn clone(self: *Blueprint, allocator: NeverFailingAllocator) Blueprint { + return .{.blocks = self.blocks.clone(allocator)}; + } + const CaptureResult = union(enum) { + success: Blueprint, + failure: struct {pos: Vec3i, message: []const u8}, + }; + + pub fn capture(allocator: NeverFailingAllocator, pos1: Vec3i, pos2: Vec3i) CaptureResult { + const startX = @min(pos1[0], pos2[0]); + const startY = @min(pos1[1], pos2[1]); + const startZ = @min(pos1[2], pos2[2]); + + const endX = @max(pos1[0], pos2[0]); + const endY = @max(pos1[1], pos2[1]); + const endZ = @max(pos1[2], pos2[2]); + + const sizeX: u32 = @intCast(endX - startX + 1); + const sizeY: u32 = @intCast(endY - startY + 1); + const sizeZ: u32 = @intCast(endZ - startZ + 1); + + const self = Blueprint{.blocks = .init(allocator, sizeX, sizeY, sizeZ)}; + + for(0..sizeX) |x| { + const worldX = startX +% @as(i32, @intCast(x)); + + for(0..sizeY) |y| { + const worldY = startY +% @as(i32, @intCast(y)); + + for(0..sizeZ) |z| { + const worldZ = startZ +% @as(i32, @intCast(z)); + + const maybeBlock = main.server.world.?.getBlock(worldX, worldY, worldZ); + if(maybeBlock) |block| { + self.blocks.set(x, y, z, block); + } else { + return .{.failure = .{.pos = .{worldX, worldY, worldZ}, .message = "Chunk containing block not loaded."}}; + } + } + } + } + return .{.success = self}; + } + pub fn paste(self: Blueprint, pos: Vec3i) void { + const startX = pos[0]; + const startY = pos[1]; + const startZ = pos[2]; + + for(0..self.blocks.width) |x| { + const worldX = startX +% @as(i32, @intCast(x)); + + for(0..self.blocks.depth) |y| { + const worldY = startY +% @as(i32, @intCast(y)); + + for(0..self.blocks.height) |z| { + const worldZ = startZ +% @as(i32, @intCast(z)); + + const block = self.blocks.get(x, y, z); + _ = main.server.world.?.updateBlock(worldX, worldY, worldZ, block); + } + } + } + } + pub fn load(allocator: NeverFailingAllocator, inputBuffer: []u8) !Blueprint { + var compressedReader = BinaryReader.init(inputBuffer, .big); + const version = try compressedReader.readInt(u16); + + if(version > blueprintVersion) { + std.log.err("Blueprint version {d} is not supported. Current version is {d}.", .{version, blueprintVersion}); + return error.UnsupportedVersion; + } + const compression = try compressedReader.readEnum(BlueprintCompression); + const blockPaletteSizeBytes = try compressedReader.readInt(u32); + const paletteBlockCount = try compressedReader.readInt(u16); + const width = try compressedReader.readInt(u16); + const depth = try compressedReader.readInt(u16); + const height = try compressedReader.readInt(u16); + + const self = Blueprint{.blocks = .init(allocator, width, depth, height)}; + + const decompressedData = try self.decompressBuffer(compressedReader.remaining, blockPaletteSizeBytes, compression); + defer main.stackAllocator.free(decompressedData); + var decompressedReader = BinaryReader.init(decompressedData, .big); + + const palette = try loadBlockPalette(main.stackAllocator, paletteBlockCount, &decompressedReader); + defer main.stackAllocator.free(palette); + + const blueprintIdToGameIdMap = makeBlueprintIdToGameIdMap(main.stackAllocator, palette); + defer main.stackAllocator.free(blueprintIdToGameIdMap); + + for(self.blocks.mem) |*block| { + const blueprintBlockRaw = try decompressedReader.readInt(BlockStorageType); + + const blueprintBlock = Block.fromInt(blueprintBlockRaw); + const gameBlockId = blueprintIdToGameIdMap[blueprintBlock.typ]; + + block.* = .{.typ = gameBlockId, .data = blueprintBlock.data}; + } + return self; + } + pub fn store(self: Blueprint, allocator: NeverFailingAllocator) []u8 { + var gameIdToBlueprintId = self.makeGameIdToBlueprintIdMap(main.stackAllocator); + defer gameIdToBlueprintId.deinit(); + std.debug.assert(gameIdToBlueprintId.count() != 0); + + var uncompressedWriter = BinaryWriter.init(main.stackAllocator, .big); + defer uncompressedWriter.deinit(); + + const blockPaletteSizeBytes = storeBlockPalette(gameIdToBlueprintId, &uncompressedWriter); + + for(self.blocks.mem) |block| { + const blueprintBlock: BlockStorageType = Block.toInt(.{.typ = gameIdToBlueprintId.get(block.typ).?, .data = block.data}); + uncompressedWriter.writeInt(BlockStorageType, blueprintBlock); + } + + const compressed = self.compressOutputBuffer(main.stackAllocator, uncompressedWriter.data.items); + defer main.stackAllocator.free(compressed.data); + + var outputWriter = BinaryWriter.initCapacity(allocator, .big, @sizeOf(i16) + @sizeOf(BlueprintCompression) + @sizeOf(u32) + @sizeOf(u16)*4 + compressed.data.len); + + outputWriter.writeInt(u16, blueprintVersion); + outputWriter.writeEnum(BlueprintCompression, compressed.mode); + outputWriter.writeInt(u32, @intCast(blockPaletteSizeBytes)); + outputWriter.writeInt(u16, @intCast(gameIdToBlueprintId.count())); + outputWriter.writeInt(u16, @intCast(self.blocks.width)); + outputWriter.writeInt(u16, @intCast(self.blocks.depth)); + outputWriter.writeInt(u16, @intCast(self.blocks.height)); + + outputWriter.writeSlice(compressed.data); + + return outputWriter.data.toOwnedSlice(); + } + fn makeBlueprintIdToGameIdMap(allocator: NeverFailingAllocator, palette: [][]const u8) []u16 { + var blueprintIdToGameIdMap = allocator.alloc(u16, palette.len); + + for(palette, 0..) |blockName, blueprintBlockId| { + const gameBlockId = main.blocks.parseBlock(blockName).typ; + blueprintIdToGameIdMap[blueprintBlockId] = gameBlockId; + } + return blueprintIdToGameIdMap; + } + fn makeGameIdToBlueprintIdMap(self: Blueprint, allocator: NeverFailingAllocator) GameIdToBlueprintIdMapType { + var gameIdToBlueprintId: GameIdToBlueprintIdMapType = .init(allocator.allocator); + + for(self.blocks.mem) |block| { + const result = gameIdToBlueprintId.getOrPut(block.typ) catch unreachable; + if(!result.found_existing) { + result.value_ptr.* = @intCast(gameIdToBlueprintId.count() - 1); + } + } + + return gameIdToBlueprintId; + } + fn loadBlockPalette(allocator: NeverFailingAllocator, paletteBlockCount: usize, reader: *BinaryReader) ![][]const u8 { + var palette = allocator.alloc([]const u8, paletteBlockCount); + + for(0..@intCast(paletteBlockCount)) |index| { + const blockNameSize = try reader.readInt(BlockIdSizeType); + const blockName = try reader.readSlice(blockNameSize); + palette[index] = blockName; + } + return palette; + } + fn storeBlockPalette(map: GameIdToBlueprintIdMapType, writer: *BinaryWriter) usize { + var blockPalette = main.stackAllocator.alloc([]const u8, map.count()); + defer main.stackAllocator.free(blockPalette); + + var iterator = map.iterator(); + while(iterator.next()) |entry| { + const block = Block{.typ = entry.key_ptr.*, .data = 0}; + const blockId = block.id(); + blockPalette[entry.value_ptr.*] = blockId; + } + + std.log.info("Blueprint block palette:", .{}); + + for(0..blockPalette.len) |index| { + const blockName = blockPalette[index]; + std.log.info("palette[{d}]: {s}", .{index, blockName}); + + writer.writeInt(BlockIdSizeType, @intCast(blockName.len)); + writer.writeSlice(blockName); + } + + return writer.data.items.len; + } + fn decompressBuffer(self: Blueprint, data: []const u8, blockPaletteSizeBytes: usize, compression: BlueprintCompression) ![]u8 { + const blockArraySizeBytes = self.blocks.width*self.blocks.depth*self.blocks.height*@sizeOf(BlockStorageType); + const decompressedDataSizeBytes = blockPaletteSizeBytes + blockArraySizeBytes; + + const decompressedData = main.stackAllocator.alloc(u8, decompressedDataSizeBytes); + + switch(compression) { + .deflate => { + const sizeAfterDecompression = try Compression.inflateTo(decompressedData, data); + std.debug.assert(sizeAfterDecompression == decompressedDataSizeBytes); + }, + } + return decompressedData; + } + fn compressOutputBuffer(_: Blueprint, allocator: NeverFailingAllocator, decompressedData: []u8) struct {mode: BlueprintCompression, data: []u8} { + const compressionMode: BlueprintCompression = .deflate; + switch(compressionMode) { + .deflate => { + return .{.mode = .deflate, .data = Compression.deflate(allocator, decompressedData, .default)}; + }, + } + } +}; diff --git a/src/game.zig b/src/game.zig index 8f7f2843..fdb03a62 100644 --- a/src/game.zig +++ b/src/game.zig @@ -16,6 +16,7 @@ const ConnectionManager = network.ConnectionManager; const vec = @import("vec.zig"); const Vec2f = vec.Vec2f; const Vec2d = vec.Vec2d; +const Vec3i = vec.Vec3i; const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const Vec3d = vec.Vec3d; @@ -464,6 +465,9 @@ pub const Player = struct { // MARK: Player pub var inventory: Inventory = undefined; pub var selectedSlot: u32 = 0; + pub var selectionPosition1: ?Vec3i = null; + pub var selectionPosition2: ?Vec3i = null; + pub var currentFriction: f32 = 0; pub var onGround: bool = false; diff --git a/src/main.zig b/src/main.zig index dd46fd2d..b7737064 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ pub const server = @import("server"); pub const audio = @import("audio.zig"); pub const assets = @import("assets.zig"); pub const blocks = @import("blocks.zig"); +pub const blueprint = @import("blueprint.zig"); pub const chunk = @import("chunk.zig"); pub const entity = @import("entity.zig"); pub const files = @import("files.zig"); diff --git a/src/network.zig b/src/network.zig index bd976fa7..f190ead8 100644 --- a/src/network.zig +++ b/src/network.zig @@ -978,12 +978,19 @@ pub const Protocols = struct { const type_gamemode: u8 = 0; const type_teleport: u8 = 1; const type_cure: u8 = 2; - const type_reserved2: u8 = 3; + const type_worldEditPos: u8 = 3; const type_reserved3: u8 = 4; const type_reserved4: u8 = 5; const type_reserved5: u8 = 6; const type_reserved6: u8 = 7; const type_timeAndBiome: u8 = 8; + + const WorldEditPosition = enum(u2) { + selectedPos1 = 0, + selectedPos2 = 1, + clear = 2, + }; + fn receive(conn: *Connection, reader: *utils.BinaryReader) !void { switch(try reader.readInt(u8)) { type_gamemode => { @@ -1000,7 +1007,27 @@ pub const Protocols = struct { type_cure => { // TODO: health and hunger }, - type_reserved2 => {}, + type_worldEditPos => { + const typ = try reader.readEnum(WorldEditPosition); + switch(typ) { + .selectedPos1, .selectedPos2 => { + const pos = Vec3i{ + try reader.readInt(i32), + try reader.readInt(i32), + try reader.readInt(i32), + }; + switch(typ) { + .selectedPos1 => game.Player.selectionPosition1 = pos, + .selectedPos2 => game.Player.selectionPosition2 = pos, + else => unreachable, + } + }, + .clear => { + game.Player.selectionPosition1 = null; + game.Player.selectionPosition2 = null; + }, + } + }, type_reserved3 => {}, type_reserved4 => {}, type_reserved5 => {}, @@ -1072,6 +1099,19 @@ pub const Protocols = struct { conn.sendImportant(id, &data); } + pub fn sendWorldEditPos(conn: *Connection, posType: WorldEditPosition, maybePos: ?Vec3i) void { + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, networkEndian, 25); + defer writer.deinit(); + writer.writeInt(u8, type_worldEditPos); + writer.writeEnum(WorldEditPosition, posType); + if(maybePos) |pos| { + writer.writeInt(i32, pos[0]); + writer.writeInt(i32, pos[1]); + writer.writeInt(i32, pos[2]); + } + conn.sendImportant(id, writer.data.items); + } + pub fn sendTimeAndBiome(conn: *Connection, world: *const main.server.ServerWorld) void { const zon = ZonElement.initObject(main.stackAllocator); defer zon.deinit(main.stackAllocator); diff --git a/src/renderer.zig b/src/renderer.zig index 5e3e0aec..fa29b60d 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -941,5 +941,16 @@ pub const MeshSelection = struct { // MARK: MeshSelection c.glPolygonOffset(-2, 0); drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax); } + if(game.Player.selectionPosition1) |pos1| { + if(game.Player.selectionPosition2) |pos2| { + const bottomLeft: Vec3i = @min(pos1, pos2); + const topRight: Vec3i = @max(pos1, pos2); + + c.glEnable(c.GL_POLYGON_OFFSET_LINE); + defer c.glDisable(c.GL_POLYGON_OFFSET_LINE); + c.glPolygonOffset(-2, 0); + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(bottomLeft)) - playerPos, .{0, 0, 0}, @floatFromInt(topRight - bottomLeft + Vec3i{1, 1, 1})); + } + } } }; diff --git a/src/server/command/_command.zig b/src/server/command/_command.zig index 0f8c10b7..4be8b29f 100644 --- a/src/server/command/_command.zig +++ b/src/server/command/_command.zig @@ -22,6 +22,7 @@ pub fn init() void { .usage = @field(commandList, decl.name).usage, .exec = &@field(commandList, decl.name).execute, }) catch unreachable; + std.log.info("Registered Command: /{s}", .{decl.name}); } } diff --git a/src/server/command/_list.zig b/src/server/command/_list.zig index e6b5fa3f..afee7f90 100644 --- a/src/server/command/_list.zig +++ b/src/server/command/_list.zig @@ -5,3 +5,10 @@ pub const invite = @import("invite.zig"); pub const kill = @import("kill.zig"); pub const time = @import("time.zig"); pub const tp = @import("tp.zig"); + +pub const pos1 = @import("worldedit/pos1.zig"); +pub const pos2 = @import("worldedit/pos2.zig"); +pub const deselect = @import("worldedit/deselect.zig"); +pub const copy = @import("worldedit/copy.zig"); +pub const paste = @import("worldedit/paste.zig"); +pub const blueprint = @import("worldedit/blueprint.zig"); diff --git a/src/server/command/worldedit/blueprint.zig b/src/server/command/worldedit/blueprint.zig new file mode 100644 index 00000000..a63a3922 --- /dev/null +++ b/src/server/command/worldedit/blueprint.zig @@ -0,0 +1,187 @@ +const std = @import("std"); + +const main = @import("root"); +const User = main.server.User; +const vec = main.vec; +const Vec3i = vec.Vec3i; + +const openDir = main.files.openDir; +const Dir = main.files.Dir; +const List = main.List; +const Block = main.blocks.Block; +const Blueprint = main.blueprint.Blueprint; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +pub const description = "Input-output operations on blueprints."; +pub const usage = + \\/blueprint save + \\/blueprint delete + \\/blueprint load + \\/blueprint list +; + +const BlueprintSubCommand = enum { + save, + delete, + load, + list, + unknown, + empty, + + fn fromString(string: []const u8) BlueprintSubCommand { + return std.meta.stringToEnum(BlueprintSubCommand, string) orelse { + if(string.len == 0) return .empty; + return .unknown; + }; + } +}; + +pub fn execute(args: []const u8, source: *User) void { + var argsList = List([]const u8).init(main.stackAllocator); + defer argsList.deinit(); + + var splitIterator = std.mem.splitScalar(u8, args, ' '); + while(splitIterator.next()) |a| { + argsList.append(a); + } + + if(argsList.items.len < 1) { + source.sendMessage("#ff0000Not enough arguments for /blueprint, expected at least 1.", .{}); + return; + } + const subcommand = BlueprintSubCommand.fromString(argsList.items[0]); + switch(subcommand) { + .save => blueprintSave(argsList.items, source), + .delete => blueprintDelete(argsList.items, source), + .load => blueprintLoad(argsList.items, source), + .list => blueprintList(source), + .unknown => { + source.sendMessage("#ff0000Unrecognized subcommand for /blueprint: '{s}'", .{argsList.items[0]}); + }, + .empty => { + source.sendMessage("#ff0000Missing subcommand for /blueprint, usage: {s} ", .{usage}); + }, + } +} + +fn blueprintSave(args: []const []const u8, source: *User) void { + if(args.len < 2) { + return source.sendMessage("#ff0000/blueprint save requires file-name argument.", .{}); + } + if(args.len >= 3) { + return source.sendMessage("#ff0000Too many arguments for /blueprint save. Expected 1 argument, file-name.", .{}); + } + + if(source.worldEditData.clipboard) |clipboard| { + const storedBlueprint = clipboard.store(main.stackAllocator); + defer main.stackAllocator.free(storedBlueprint); + + const fileName: []const u8 = ensureBlueprintExtension(main.stackAllocator, args[1]); + defer main.stackAllocator.free(fileName); + + var blueprintsDir = openBlueprintsDir(source) orelse return; + defer blueprintsDir.close(); + + blueprintsDir.write(fileName, storedBlueprint) catch |err| { + return sendWarningAndLog("Failed to write blueprint file '{s}' ({s})", .{fileName, @errorName(err)}, source); + }; + + sendInfoAndLog("Saved clipboard to blueprint file: {s}", .{fileName}, source); + } else { + source.sendMessage("#ff0000Error: No clipboard content to save.", .{}); + } +} + +fn sendWarningAndLog(comptime fmt: []const u8, args: anytype, user: *User) void { + std.log.warn(fmt, args); + user.sendMessage("#ff0000" ++ fmt, args); +} + +fn sendInfoAndLog(comptime fmt: []const u8, args: anytype, user: *User) void { + std.log.info(fmt, args); + user.sendMessage("#00ff00" ++ fmt, args); +} + +fn openBlueprintsDir(source: *User) ?Dir { + return openDir("blueprints") catch |err| blk: { + sendWarningAndLog("Failed to open 'blueprints' directory ({s})", .{@errorName(err)}, source); + break :blk null; + }; +} + +fn ensureBlueprintExtension(allocator: NeverFailingAllocator, fileName: []const u8) []const u8 { + if(!std.ascii.endsWithIgnoreCase(fileName, ".blp")) { + return std.fmt.allocPrint(allocator.allocator, "{s}.blp", .{fileName}) catch unreachable; + } else { + return allocator.dupe(u8, fileName); + } +} + +fn blueprintDelete(args: []const []const u8, source: *User) void { + if(args.len < 2) { + return source.sendMessage("#ff0000/blueprint delete requires file-name argument.", .{}); + } + if(args.len >= 3) { + return source.sendMessage("#ff0000Too many arguments for /blueprint delete. Expected 1 argument, file-name.", .{}); + } + + const fileName: []const u8 = ensureBlueprintExtension(main.stackAllocator, args[1]); + defer main.stackAllocator.free(fileName); + + var blueprintsDir = openBlueprintsDir(source) orelse return; + defer blueprintsDir.close(); + + blueprintsDir.dir.deleteFile(fileName) catch |err| { + return sendWarningAndLog("Failed to delete blueprint file '{s}' ({s})", .{fileName, @errorName(err)}, source); + }; + + sendWarningAndLog("Deleted blueprint file: {s}", .{fileName}, source); +} + +fn blueprintList(source: *User) void { + var blueprintsDir = std.fs.cwd().makeOpenPath("blueprints", .{.iterate = true}) catch |err| { + return sendWarningAndLog("Failed to open 'blueprints' directory ({s})", .{@errorName(err)}, source); + }; + defer blueprintsDir.close(); + + var directoryIterator = blueprintsDir.iterate(); + + while(directoryIterator.next() catch |err| { + return sendWarningAndLog("Failed to read blueprint directory ({s})", .{@errorName(err)}, source); + }) |entry| { + if(entry.kind != .file) break; + if(!std.ascii.endsWithIgnoreCase(entry.name, ".blp")) break; + + source.sendMessage("#ffffff- {s}", .{entry.name}); + } +} + +fn blueprintLoad(args: []const []const u8, source: *User) void { + if(args.len < 2) { + return source.sendMessage("#ff0000/blueprint load requires file-name argument.", .{}); + } + if(args.len >= 3) { + return source.sendMessage("#ff0000Too many arguments for /blueprint load. Expected 1 argument, file-name.", .{}); + } + + const fileName: []const u8 = ensureBlueprintExtension(main.stackAllocator, args[1]); + defer main.stackAllocator.free(fileName); + + var blueprintsDir = openBlueprintsDir(source) orelse return; + defer blueprintsDir.close(); + + const storedBlueprint = blueprintsDir.read(main.stackAllocator, fileName) catch |err| { + sendWarningAndLog("Failed to read blueprint file '{s}' ({s})", .{fileName, @errorName(err)}, source); + return; + }; + defer main.stackAllocator.free(storedBlueprint); + + if(source.worldEditData.clipboard) |oldClipboard| { + oldClipboard.deinit(main.globalAllocator); + } + source.worldEditData.clipboard = Blueprint.load(main.globalAllocator, storedBlueprint) catch |err| { + return sendWarningAndLog("Failed to load blueprint file '{s}' ({s})", .{fileName, @errorName(err)}, source); + }; + + sendInfoAndLog("Loaded blueprint file: {s}", .{fileName}, source); +} diff --git a/src/server/command/worldedit/copy.zig b/src/server/command/worldedit/copy.zig new file mode 100644 index 00000000..b5c4e530 --- /dev/null +++ b/src/server/command/worldedit/copy.zig @@ -0,0 +1,41 @@ +const std = @import("std"); + +const main = @import("root"); +const User = main.server.User; + +const Block = main.blocks.Block; +const Blueprint = main.blueprint.Blueprint; + +pub const description = "Copy selection to clipboard."; +pub const usage = "/copy"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /copy. Expected no arguments.", .{}); + return; + } + const pos1 = source.worldEditData.selectionPosition1 orelse { + return source.sendMessage("#ff0000Position 1 isn't set", .{}); + }; + const pos2 = source.worldEditData.selectionPosition2 orelse { + return source.sendMessage("#ff0000Position 2 isn't set", .{}); + }; + + source.sendMessage("Copying: {} {}", .{pos1, pos2}); + + const result = Blueprint.capture(main.globalAllocator, pos1, pos2); + switch(result) { + .success => { + if(source.worldEditData.clipboard != null) { + source.worldEditData.clipboard.?.deinit(main.globalAllocator); + } + source.worldEditData.clipboard = result.success; + + source.sendMessage("Copied selection to clipboard.", .{}); + }, + .failure => |e| { + source.sendMessage("#ff0000Error while copying block {}: {s}", .{e.pos, e.message}); + std.log.warn("Error while copying block {}: {s}", .{e.pos, e.message}); + }, + } +} diff --git a/src/server/command/worldedit/deselect.zig b/src/server/command/worldedit/deselect.zig new file mode 100644 index 00000000..23d47ac8 --- /dev/null +++ b/src/server/command/worldedit/deselect.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +const main = @import("root"); +const User = main.server.User; + +pub const description = "Clears pos1 and pos2 of selection."; +pub const usage = "/deselect"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /deselect. Expected no arguments.", .{}); + return; + } + + source.worldEditData.selectionPosition1 = null; + source.worldEditData.selectionPosition2 = null; + + main.network.Protocols.genericUpdate.sendWorldEditPos(source.conn, .clear, null); + source.sendMessage("Cleared selection.", .{}); +} diff --git a/src/server/command/worldedit/paste.zig b/src/server/command/worldedit/paste.zig new file mode 100644 index 00000000..11e050cb --- /dev/null +++ b/src/server/command/worldedit/paste.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +const main = @import("root"); +const User = main.server.User; +const vec = main.vec; +const Vec3i = vec.Vec3i; + +const copy = @import("copy.zig"); + +const Block = main.blocks.Block; +const Blueprint = main.blueprint.Blueprint; + +pub const description = "Paste clipboard content to current player position."; +pub const usage = "/paste"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /paste. Expected no arguments.", .{}); + return; + } + + if(source.worldEditData.clipboard) |clipboard| { + const pos: Vec3i = @intFromFloat(source.player.pos); + source.sendMessage("Pasting: {}", .{pos}); + clipboard.paste(pos); + } else { + source.sendMessage("#ff0000Error: No clipboard content to paste.", .{}); + } +} diff --git a/src/server/command/worldedit/pos1.zig b/src/server/command/worldedit/pos1.zig new file mode 100644 index 00000000..620af0ad --- /dev/null +++ b/src/server/command/worldedit/pos1.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +const main = @import("root"); +const User = main.server.User; +const Vec3i = main.vec.Vec3i; + +pub const description = "Select the player position as position 1."; +pub const usage = "/pos1"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /pos1. Expected no arguments.", .{}); + return; + } + + const pos: Vec3i = @intFromFloat(source.player.pos); + + source.worldEditData.selectionPosition1 = pos; + main.network.Protocols.genericUpdate.sendWorldEditPos(source.conn, .selectedPos1, pos); + + source.sendMessage("Position 1: {}", .{pos}); +} diff --git a/src/server/command/worldedit/pos2.zig b/src/server/command/worldedit/pos2.zig new file mode 100644 index 00000000..4836dad2 --- /dev/null +++ b/src/server/command/worldedit/pos2.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +const main = @import("root"); +const User = main.server.User; +const Vec3i = main.vec.Vec3i; + +pub const description = "Select the player position as position 2."; +pub const usage = "/pos2"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /pos2. Expected no arguments.", .{}); + return; + } + + const pos: Vec3i = @intFromFloat(source.player.pos); + + source.worldEditData.selectionPosition2 = pos; + main.network.Protocols.genericUpdate.sendWorldEditPos(source.conn, .selectedPos2, pos); + + source.sendMessage("Position 2: {}", .{pos}); +} diff --git a/src/server/server.zig b/src/server/server.zig index aaae697c..2e94b7bc 100644 --- a/src/server/server.zig +++ b/src/server/server.zig @@ -18,6 +18,18 @@ pub const storage = @import("storage.zig"); const command = @import("command/_command.zig"); +pub const WorldEditData = struct { + selectionPosition1: ?Vec3i = null, + selectionPosition2: ?Vec3i = null, + clipboard: ?main.blueprint.Blueprint = null, + + pub fn deinit(self: *WorldEditData) void { + if(self.clipboard != null) { + self.clipboard.?.deinit(main.globalAllocator); + } + } +}; + pub const User = struct { // MARK: User const maxSimulationDistance = 8; const simulationSize = 2*maxSimulationDistance; @@ -39,6 +51,7 @@ pub const User = struct { // MARK: User lastRenderDistance: u16 = 0, lastPos: Vec3i = @splat(0), gamemode: std.atomic.Value(main.game.Gamemode) = .init(.creative), + worldEditData: WorldEditData = .{}, inventoryClientToServerIdMap: std.AutoHashMap(u32, u32) = undefined, @@ -75,6 +88,8 @@ pub const User = struct { // MARK: User return; }; + self.worldEditData.deinit(); + main.items.Inventory.Sync.ServerSide.disconnectUser(self); std.debug.assert(self.inventoryClientToServerIdMap.count() == 0); // leak self.inventoryClientToServerIdMap.deinit(); diff --git a/src/utils.zig b/src/utils.zig index 2b8facd9..58bd5b8c 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -309,6 +309,12 @@ pub fn Array3D(comptime T: type) type { // MARK: Array3D std.debug.assert(x < self.width and y < self.depth and z < self.height); return &self.mem[(x*self.depth + y)*self.height + z]; } + + pub fn clone(self: Self, allocator: NeverFailingAllocator) Self { + const new = Self.init(allocator, self.width, self.depth, self.height); + @memcpy(new.mem, self.mem); + return new; + } }; }