Add /replace command (#1424)

## Description

This pull request adds `/replace` command. The main difference from
`/set` with `/mask global` is that mask specified for `/replace` is
inlined, local and positive, i.e. only blocks matching the mask
specified as part of `/replace` command will be affected. `/replace`
ignores global masks.

```
/replace <old> <new>
```

`<old>` - expression following mask syntax (#1284), blocks which match
will be affected
`<new>` - expression following pattern syntax (#1237) used to fill
blocks matched by `<old>`

## Examples

`/replace cubyz:air cubyz:void`
`/replace $leaf|cubyz:air cubyz:stone,cubyz:grass`

## Links

Related to: #1214 
Depends on: #1337 
Depends on: #1284

---------

Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
This commit is contained in:
Krzysztof Wiśniewski 2025-05-20 19:03:12 +02:00 committed by GitHub
parent c52b231983
commit b79784a0aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 68 additions and 4 deletions

View File

@ -315,12 +315,14 @@ pub const Blueprint = struct {
},
}
}
pub fn set(self: *Blueprint, pattern: Pattern, mask: ?Mask) void {
pub fn replace(self: *Blueprint, whitelist: ?Mask, blacklist: ?Mask, newBlocks: Pattern) void {
for(0..self.blocks.width) |x| {
for(0..self.blocks.depth) |y| {
for(0..self.blocks.height) |z| {
if(mask) |_mask| if(_mask.match(self.blocks.get(x, y, z))) continue;
self.blocks.set(x, y, z, pattern.blocks.sample(&main.seed).block);
const current = self.blocks.get(x, y, z);
if(whitelist) |m| if(!m.match(current)) continue;
if(blacklist) |m| if(m.match(current)) continue;
self.blocks.set(x, y, z, newBlocks.blocks.sample(&main.seed).block);
}
}
}

View File

@ -17,3 +17,4 @@ pub const blueprint = @import("worldedit/blueprint.zig");
pub const rotate = @import("worldedit/rotate.zig");
pub const set = @import("worldedit/set.zig");
pub const mask = @import("worldedit/mask.zig");
pub const replace = @import("worldedit/replace.zig");

View File

@ -0,0 +1,61 @@
const std = @import("std");
const main = @import("main");
const Vec3i = main.vec.Vec3i;
const User = main.server.User;
const Block = main.blocks.Block;
const Blueprint = main.blueprint.Blueprint;
const Pattern = main.blueprint.Pattern;
const Mask = main.blueprint.Mask;
pub const description = "Replace blocks in the world edit selection.";
pub const usage = "/replace <old mask> <new pattern>";
pub fn execute(args: []const u8, source: *User) void {
var argsSplit = std.mem.splitScalar(u8, args, ' ');
const oldMaskString = argsSplit.next() orelse {
return source.sendMessage("#ff0000Missing required <old> argument.", .{});
};
const newPatternString = argsSplit.next() orelse {
return source.sendMessage("#ff0000Missing required <new> argument.", .{});
};
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", .{});
};
const oldMask = Mask.initFromString(main.stackAllocator, oldMaskString) catch |err| {
return source.sendMessage("#ff0000Error parsing mask: {s}", .{@errorName(err)});
};
defer oldMask.deinit(main.stackAllocator);
const newPattern = Pattern.initFromString(main.stackAllocator, newPatternString) catch |err| {
return source.sendMessage("#ff0000Error parsing pattern: {s}", .{@errorName(err)});
};
defer newPattern.deinit(main.stackAllocator);
const posStart: Vec3i = @min(pos1, pos2);
const posEnd: Vec3i = @max(pos1, pos2);
const selection = Blueprint.capture(main.globalAllocator, posStart, posEnd);
switch(selection) {
.success => |blueprint| {
source.worldEditData.undoHistory.push(.init(blueprint, posStart, "replace"));
source.worldEditData.redoHistory.clear();
var modifiedBlueprint = blueprint.clone(main.stackAllocator);
defer modifiedBlueprint.deinit(main.stackAllocator);
modifiedBlueprint.replace(oldMask, null, newPattern);
modifiedBlueprint.paste(posStart, .{.preserveVoid = true});
},
.failure => |err| {
source.sendMessage("#ff0000Error: Could not capture selection. (at {}, {s})", .{err.pos, err.message});
},
}
}

View File

@ -41,7 +41,7 @@ pub fn execute(args: []const u8, source: *User) void {
var modifiedBlueprint = blueprint.clone(main.stackAllocator);
defer modifiedBlueprint.deinit(main.stackAllocator);
modifiedBlueprint.set(pattern, source.worldEditData.mask);
modifiedBlueprint.replace(null, source.worldEditData.mask, pattern);
modifiedBlueprint.paste(posStart, .{.preserveVoid = true});
},
.failure => |err| {