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 <mgiakimenko@outlook.com>
Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
This commit is contained in:
Krzysztof Wiśniewski 2025-03-20 22:07:26 +01:00 committed by GitHub
parent 302544bbcb
commit 37eb01ec37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 652 additions and 2 deletions

244
src/blueprint.zig Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <file-name>
\\/blueprint delete <file-name>
\\/blueprint load <file-name>
\\/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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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