diff --git a/src/server/command/_list.zig b/src/server/command/_list.zig index 9b5646ca..77e407ab 100644 --- a/src/server/command/_list.zig +++ b/src/server/command/_list.zig @@ -6,6 +6,7 @@ pub const kill = @import("kill.zig"); pub const time = @import("time.zig"); pub const tp = @import("tp.zig"); +pub const undo = @import("worldedit/undo.zig"); pub const pos1 = @import("worldedit/pos1.zig"); pub const pos2 = @import("worldedit/pos2.zig"); pub const deselect = @import("worldedit/deselect.zig"); diff --git a/src/server/command/worldedit/paste.zig b/src/server/command/worldedit/paste.zig index 11e050cb..a9d6b2c7 100644 --- a/src/server/command/worldedit/paste.zig +++ b/src/server/command/worldedit/paste.zig @@ -22,6 +22,21 @@ pub fn execute(args: []const u8, source: *User) void { if(source.worldEditData.clipboard) |clipboard| { const pos: Vec3i = @intFromFloat(source.player.pos); source.sendMessage("Pasting: {}", .{pos}); + + const undo = Blueprint.capture(main.globalAllocator, pos, .{ + pos[0] + @as(i32, @intCast(clipboard.blocks.width)) - 1, + pos[1] + @as(i32, @intCast(clipboard.blocks.depth)) - 1, + pos[2] + @as(i32, @intCast(clipboard.blocks.height)) - 1, + }); + switch(undo) { + .success => |blueprint| { + source.worldEditData.undoHistory.push(.init(blueprint, pos, "paste")); + }, + .failure => { + source.sendMessage("#ff0000Error: Could not capture undo history.", .{}); + }, + } + clipboard.paste(pos); } else { source.sendMessage("#ff0000Error: No clipboard content to paste.", .{}); diff --git a/src/server/command/worldedit/undo.zig b/src/server/command/worldedit/undo.zig new file mode 100644 index 00000000..69e8e09d --- /dev/null +++ b/src/server/command/worldedit/undo.zig @@ -0,0 +1,23 @@ +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 = "Undo last change done to world with world editing commands."; +pub const usage = "/undo"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /undo. Expected no arguments.", .{}); + return; + } + if(source.worldEditData.undoHistory.pop()) |action| { + action.blueprint.paste(action.position); + source.sendMessage("#00ff00Un-done last {s}.", .{action.message}); + } else { + source.sendMessage("#ccccccNothing to undo.", .{}); + } +} diff --git a/src/server/server.zig b/src/server/server.zig index bd8623ed..03964c0c 100644 --- a/src/server/server.zig +++ b/src/server/server.zig @@ -12,6 +12,9 @@ const Vec3d = vec.Vec3d; const Vec3i = vec.Vec3i; const BinaryReader = main.utils.BinaryReader; const BinaryWriter = main.utils.BinaryWriter; +const Blueprint = main.blueprint.Blueprint; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const CircularBufferQueue = main.utils.CircularBufferQueue; pub const ServerWorld = @import("world.zig").ServerWorld; pub const terrain = @import("terrain/terrain.zig"); @@ -21,14 +24,58 @@ pub const storage = @import("storage.zig"); const command = @import("command/_command.zig"); pub const WorldEditData = struct { + const maxWorldEditHistoryCapacity: u32 = 1024; + selectionPosition1: ?Vec3i = null, selectionPosition2: ?Vec3i = null, - clipboard: ?main.blueprint.Blueprint = null, + clipboard: ?Blueprint = null, + undoHistory: History, + const History = struct { + changes: CircularBufferQueue(Value), + + const Value = struct { + blueprint: Blueprint, + position: Vec3i, + message: []const u8, + + pub fn init(blueprint: Blueprint, position: Vec3i, message: []const u8) Value { + return .{.blueprint = blueprint, .position = position, .message = main.globalAllocator.dupe(u8, message)}; + } + pub fn deinit(self: Value) void { + main.globalAllocator.free(self.message); + self.blueprint.deinit(main.globalAllocator); + } + }; + pub fn init() History { + return .{.changes = .init(main.globalAllocator, maxWorldEditHistoryCapacity)}; + } + pub fn deinit(self: *History) void { + self.clear(); + self.changes.deinit(); + } + pub fn clear(self: *History) void { + while(self.changes.dequeue()) |item| item.deinit(); + } + pub fn push(self: *History, value: Value) void { + if(self.changes.reachedCapacity()) { + if(self.changes.dequeue()) |oldValue| oldValue.deinit(); + } + + self.changes.enqueue(value); + } + pub fn pop(self: *History) ?Value { + return self.changes.dequeue_front(); + } + }; + pub fn init() WorldEditData { + return .{.undoHistory = History.init()}; + } pub fn deinit(self: *WorldEditData) void { if(self.clipboard != null) { self.clipboard.?.deinit(main.globalAllocator); } + self.undoHistory.deinit(); } }; @@ -53,7 +100,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 = .{}, + worldEditData: WorldEditData = undefined, inventoryClientToServerIdMap: std.AutoHashMap(u32, u32) = undefined, @@ -71,6 +118,7 @@ pub const User = struct { // MARK: User self.interpolation.init(@ptrCast(&self.player.pos), @ptrCast(&self.player.vel)); self.conn = try Connection.init(manager, ipPort, self); self.increaseRefCount(); + self.worldEditData = .init(); network.Protocols.handShake.serverSide(self.conn); return self; } diff --git a/src/utils.zig b/src/utils.zig index 60c73721..933b00e5 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -391,6 +391,10 @@ pub fn CircularBufferQueue(comptime T: type) type { // MARK: CircularBufferQueue pub fn empty(self: *Self) bool { return self.startIndex == self.endIndex; } + + pub fn reachedCapacity(self: *Self) bool { + return self.startIndex == (self.endIndex + 1) & self.mask; + } }; }