Add /undo command (#1228)

* Add undo and redo commands

* Remove unintended change

* Remove redo code

* Clean up redo code

* Use CircularBufferQueue

* Fix formatting issues

* Update src/server/server.zig

Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>

* Apply review requests

---------

Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
This commit is contained in:
Krzysztof Wiśniewski 2025-03-24 21:47:14 +01:00 committed by GitHub
parent fba10a94c2
commit 12d05a06ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 93 additions and 2 deletions

View File

@ -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");

View File

@ -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.", .{});

View File

@ -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.", .{});
}
}

View File

@ -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;
}

View File

@ -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;
}
};
}