mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-08-03 03:06:55 -04:00
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:
parent
302544bbcb
commit
37eb01ec37
244
src/blueprint.zig
Normal file
244
src/blueprint.zig
Normal 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)};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -16,6 +16,7 @@ const ConnectionManager = network.ConnectionManager;
|
|||||||
const vec = @import("vec.zig");
|
const vec = @import("vec.zig");
|
||||||
const Vec2f = vec.Vec2f;
|
const Vec2f = vec.Vec2f;
|
||||||
const Vec2d = vec.Vec2d;
|
const Vec2d = vec.Vec2d;
|
||||||
|
const Vec3i = vec.Vec3i;
|
||||||
const Vec3f = vec.Vec3f;
|
const Vec3f = vec.Vec3f;
|
||||||
const Vec4f = vec.Vec4f;
|
const Vec4f = vec.Vec4f;
|
||||||
const Vec3d = vec.Vec3d;
|
const Vec3d = vec.Vec3d;
|
||||||
@ -464,6 +465,9 @@ pub const Player = struct { // MARK: Player
|
|||||||
pub var inventory: Inventory = undefined;
|
pub var inventory: Inventory = undefined;
|
||||||
pub var selectedSlot: u32 = 0;
|
pub var selectedSlot: u32 = 0;
|
||||||
|
|
||||||
|
pub var selectionPosition1: ?Vec3i = null;
|
||||||
|
pub var selectionPosition2: ?Vec3i = null;
|
||||||
|
|
||||||
pub var currentFriction: f32 = 0;
|
pub var currentFriction: f32 = 0;
|
||||||
|
|
||||||
pub var onGround: bool = false;
|
pub var onGround: bool = false;
|
||||||
|
@ -6,6 +6,7 @@ pub const server = @import("server");
|
|||||||
pub const audio = @import("audio.zig");
|
pub const audio = @import("audio.zig");
|
||||||
pub const assets = @import("assets.zig");
|
pub const assets = @import("assets.zig");
|
||||||
pub const blocks = @import("blocks.zig");
|
pub const blocks = @import("blocks.zig");
|
||||||
|
pub const blueprint = @import("blueprint.zig");
|
||||||
pub const chunk = @import("chunk.zig");
|
pub const chunk = @import("chunk.zig");
|
||||||
pub const entity = @import("entity.zig");
|
pub const entity = @import("entity.zig");
|
||||||
pub const files = @import("files.zig");
|
pub const files = @import("files.zig");
|
||||||
|
@ -978,12 +978,19 @@ pub const Protocols = struct {
|
|||||||
const type_gamemode: u8 = 0;
|
const type_gamemode: u8 = 0;
|
||||||
const type_teleport: u8 = 1;
|
const type_teleport: u8 = 1;
|
||||||
const type_cure: u8 = 2;
|
const type_cure: u8 = 2;
|
||||||
const type_reserved2: u8 = 3;
|
const type_worldEditPos: u8 = 3;
|
||||||
const type_reserved3: u8 = 4;
|
const type_reserved3: u8 = 4;
|
||||||
const type_reserved4: u8 = 5;
|
const type_reserved4: u8 = 5;
|
||||||
const type_reserved5: u8 = 6;
|
const type_reserved5: u8 = 6;
|
||||||
const type_reserved6: u8 = 7;
|
const type_reserved6: u8 = 7;
|
||||||
const type_timeAndBiome: u8 = 8;
|
const type_timeAndBiome: u8 = 8;
|
||||||
|
|
||||||
|
const WorldEditPosition = enum(u2) {
|
||||||
|
selectedPos1 = 0,
|
||||||
|
selectedPos2 = 1,
|
||||||
|
clear = 2,
|
||||||
|
};
|
||||||
|
|
||||||
fn receive(conn: *Connection, reader: *utils.BinaryReader) !void {
|
fn receive(conn: *Connection, reader: *utils.BinaryReader) !void {
|
||||||
switch(try reader.readInt(u8)) {
|
switch(try reader.readInt(u8)) {
|
||||||
type_gamemode => {
|
type_gamemode => {
|
||||||
@ -1000,7 +1007,27 @@ pub const Protocols = struct {
|
|||||||
type_cure => {
|
type_cure => {
|
||||||
// TODO: health and hunger
|
// 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_reserved3 => {},
|
||||||
type_reserved4 => {},
|
type_reserved4 => {},
|
||||||
type_reserved5 => {},
|
type_reserved5 => {},
|
||||||
@ -1072,6 +1099,19 @@ pub const Protocols = struct {
|
|||||||
conn.sendImportant(id, &data);
|
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 {
|
pub fn sendTimeAndBiome(conn: *Connection, world: *const main.server.ServerWorld) void {
|
||||||
const zon = ZonElement.initObject(main.stackAllocator);
|
const zon = ZonElement.initObject(main.stackAllocator);
|
||||||
defer zon.deinit(main.stackAllocator);
|
defer zon.deinit(main.stackAllocator);
|
||||||
|
@ -941,5 +941,16 @@ pub const MeshSelection = struct { // MARK: MeshSelection
|
|||||||
c.glPolygonOffset(-2, 0);
|
c.glPolygonOffset(-2, 0);
|
||||||
drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax);
|
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}));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -22,6 +22,7 @@ pub fn init() void {
|
|||||||
.usage = @field(commandList, decl.name).usage,
|
.usage = @field(commandList, decl.name).usage,
|
||||||
.exec = &@field(commandList, decl.name).execute,
|
.exec = &@field(commandList, decl.name).execute,
|
||||||
}) catch unreachable;
|
}) catch unreachable;
|
||||||
|
std.log.info("Registered Command: /{s}", .{decl.name});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,3 +5,10 @@ pub const invite = @import("invite.zig");
|
|||||||
pub const kill = @import("kill.zig");
|
pub const kill = @import("kill.zig");
|
||||||
pub const time = @import("time.zig");
|
pub const time = @import("time.zig");
|
||||||
pub const tp = @import("tp.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");
|
||||||
|
187
src/server/command/worldedit/blueprint.zig
Normal file
187
src/server/command/worldedit/blueprint.zig
Normal 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);
|
||||||
|
}
|
41
src/server/command/worldedit/copy.zig
Normal file
41
src/server/command/worldedit/copy.zig
Normal 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});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
20
src/server/command/worldedit/deselect.zig
Normal file
20
src/server/command/worldedit/deselect.zig
Normal 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.", .{});
|
||||||
|
}
|
29
src/server/command/worldedit/paste.zig
Normal file
29
src/server/command/worldedit/paste.zig
Normal 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.", .{});
|
||||||
|
}
|
||||||
|
}
|
22
src/server/command/worldedit/pos1.zig
Normal file
22
src/server/command/worldedit/pos1.zig
Normal 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});
|
||||||
|
}
|
22
src/server/command/worldedit/pos2.zig
Normal file
22
src/server/command/worldedit/pos2.zig
Normal 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});
|
||||||
|
}
|
@ -18,6 +18,18 @@ pub const storage = @import("storage.zig");
|
|||||||
|
|
||||||
const command = @import("command/_command.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
|
pub const User = struct { // MARK: User
|
||||||
const maxSimulationDistance = 8;
|
const maxSimulationDistance = 8;
|
||||||
const simulationSize = 2*maxSimulationDistance;
|
const simulationSize = 2*maxSimulationDistance;
|
||||||
@ -39,6 +51,7 @@ pub const User = struct { // MARK: User
|
|||||||
lastRenderDistance: u16 = 0,
|
lastRenderDistance: u16 = 0,
|
||||||
lastPos: Vec3i = @splat(0),
|
lastPos: Vec3i = @splat(0),
|
||||||
gamemode: std.atomic.Value(main.game.Gamemode) = .init(.creative),
|
gamemode: std.atomic.Value(main.game.Gamemode) = .init(.creative),
|
||||||
|
worldEditData: WorldEditData = .{},
|
||||||
|
|
||||||
inventoryClientToServerIdMap: std.AutoHashMap(u32, u32) = undefined,
|
inventoryClientToServerIdMap: std.AutoHashMap(u32, u32) = undefined,
|
||||||
|
|
||||||
@ -75,6 +88,8 @@ pub const User = struct { // MARK: User
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.worldEditData.deinit();
|
||||||
|
|
||||||
main.items.Inventory.Sync.ServerSide.disconnectUser(self);
|
main.items.Inventory.Sync.ServerSide.disconnectUser(self);
|
||||||
std.debug.assert(self.inventoryClientToServerIdMap.count() == 0); // leak
|
std.debug.assert(self.inventoryClientToServerIdMap.count() == 0); // leak
|
||||||
self.inventoryClientToServerIdMap.deinit();
|
self.inventoryClientToServerIdMap.deinit();
|
||||||
|
@ -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);
|
std.debug.assert(x < self.width and y < self.depth and z < self.height);
|
||||||
return &self.mem[(x*self.depth + y)*self.height + z];
|
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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user