Repository: PixelGuys/Cubyz
Pull request #1298: Add selection wand
Author: login: Argmaster
Created: 2025-04-11T09:36:01Z | Merged: 2025-04-25T18:49:37Z

This pull request adds dead simple selection wand tool as an additional exception to how items work alongside chisel. 

Left click to set position 1, right click to set position 2.

![image](https://github.com/user-attachments/assets/92b19c57-c738-4ae2-9947-06f485d8a97a)


Maybe, just maybe it would be good to create some vtable based system for this kind of things in the future?

Related to: #1212



Diff:
diff --git a/assets/cubyz/items/selection_wand.zig.zon b/assets/cubyz/items/selection_wand.zig.zon
new file mode 100644
index 0000000000..0ab5a68510
--- /dev/null
+++ b/assets/cubyz/items/selection_wand.zig.zon
@@ -0,0 +1,4 @@
+.{
+	.texture = "selection_wand.png",
+	.stackSize = 1,
+}
diff --git a/assets/cubyz/items/textures/selection_wand.png b/assets/cubyz/items/textures/selection_wand.png
new file mode 100644
index 0000000000..6032041c45
Binary files /dev/null and b/assets/cubyz/items/textures/selection_wand.png differ
diff --git a/src/network.zig b/src/network.zig
index 24cfa80e61..9fcb39908e 100644
--- a/src/network.zig
+++ b/src/network.zig
@@ -973,19 +973,28 @@ pub const Protocols = struct {
 				},
 				.worldEditPos => {
 					const typ = try reader.readEnum(WorldEditPosition);
-					switch(typ) {
-						.selectedPos1, .selectedPos2 => {
-							const pos = try reader.readVec(Vec3i);
-							switch(typ) {
-								.selectedPos1 => game.Player.selectionPosition1 = pos,
-								.selectedPos2 => game.Player.selectionPosition2 = pos,
-								else => unreachable,
-							}
-						},
-						.clear => {
-							game.Player.selectionPosition1 = null;
-							game.Player.selectionPosition2 = null;
-						},
+					const pos: ?Vec3i = switch(typ) {
+						.selectedPos1, .selectedPos2 => try reader.readVec(Vec3i),
+						.clear => null,
+					};
+					if(conn.isServerSide()) {
+						switch(typ) {
+							.selectedPos1 => conn.user.?.worldEditData.selectionPosition1 = pos.?,
+							.selectedPos2 => conn.user.?.worldEditData.selectionPosition2 = pos.?,
+							.clear => {
+								conn.user.?.worldEditData.selectionPosition1 = null;
+								conn.user.?.worldEditData.selectionPosition2 = null;
+							},
+						}
+					} else {
+						switch(typ) {
+							.selectedPos1 => game.Player.selectionPosition1 = pos,
+							.selectedPos2 => game.Player.selectionPosition2 = pos,
+							.clear => {
+								game.Player.selectionPosition1 = null;
+								game.Player.selectionPosition2 = null;
+							},
+						}
 					}
 				},
 				.timeAndBiome => {
@@ -1789,6 +1798,10 @@ pub const Connection = struct { // MARK: Connection
 		return self.connectionState.load(.unordered) == .connected;
 	}
 
+	fn isServerSide(conn: *Connection) bool {
+		return conn.user != null;
+	}
+
 	fn handlePacketLoss(self: *Connection, loss: LossStatus) void {
 		if(loss == .noLoss) return;
 		self.slowStart = false;
diff --git a/src/renderer.zig b/src/renderer.zig
index eeedcf6012..490173a184 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -840,6 +840,11 @@ pub const MeshSelection = struct { // MARK: MeshSelection
 								}
 							}
 						}
+						if(std.mem.eql(u8, baseItem.id, "cubyz:selection_wand")) {
+							game.Player.selectionPosition2 = selectedPos;
+							main.network.Protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos2, selectedPos);
+							return;
+						}
 					},
 					.tool => |tool| {
 						_ = tool; // TODO: Tools might change existing blocks.
@@ -851,6 +856,14 @@ pub const MeshSelection = struct { // MARK: MeshSelection
 
 	pub fn breakBlock(inventory: main.items.Inventory, slot: u32, deltaTime: f64) void {
 		if(selectedBlockPos) |selectedPos| {
+			const stack = inventory.getStack(slot);
+			const isSelectionWand = stack.item != null and stack.item.? == .baseItem and std.mem.eql(u8, stack.item.?.baseItem.id, "cubyz:selection_wand");
+			if(isSelectionWand) {
+				game.Player.selectionPosition1 = selectedPos;
+				main.network.Protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos1, selectedPos);
+				return;
+			}
+
 			if(@reduce(.Or, lastSelectedBlockPos != selectedPos)) {
 				mesh_storage.removeBreakingAnimation(lastSelectedBlockPos);
 				lastSelectedBlockPos = selectedPos;
@@ -863,7 +876,6 @@ pub const MeshSelection = struct { // MARK: MeshSelection
 
 			main.items.Inventory.Sync.ClientSide.mutex.lock();
 			if(!game.Player.isCreative()) {
-				const stack = inventory.getStack(slot);
 				var damage: f32 = 1;
 				const isTool = stack.item != null and stack.item.? == .tool;
 				if(isTool) {


Reviews:
user: login: IntegratedQuantum
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-24T16:28:53Z
user: login: Argmaster
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T19:00:43Z
user: login: Argmaster
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T19:01:23Z
user: login: IntegratedQuantum
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-24T19:43:44Z
user: login: IntegratedQuantum
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T18:49:29Z

Review comments:
diff_hunk: @@ -976,8 +976,9 @@ pub const Protocols = struct {
 		const UpdateType = enum(u8) {
 			gamemode = 0,
 			teleport = 1,
-			worldEditPos = 2,
-			timeAndBiome = 3,
+			worldEditPosToClient = 2,
+			worldEditPosToServer = 3,
path: src/network.zig
user: login: IntegratedQuantum
body: Why not merge them?

If you have seem separate, you'd have to check if they are on the right side anyways, otherwise this could be exploited to annoy the server host.
created_at: 2025-04-24T16:27:47Z
author_association: MEMBER
diff_hunk: @@ -851,6 +857,15 @@ pub const MeshSelection = struct { // MARK: MeshSelection
 
 	pub fn breakBlock(inventory: main.items.Inventory, slot: u32, deltaTime: f64) void {
 		if(selectedBlockPos) |selectedPos| {
+			const stack = inventory.getStack(slot);
+			const isSelectionWand = stack.item != null and stack.item.? == .baseItem and std.mem.eql(u8, stack.item.?.baseItem.id, "cubyz:selection_wand");
+			if(isSelectionWand) {
+				std.log.debug("Position 1: {}", .{selectedPos});
path: src/renderer.zig
user: login: IntegratedQuantum
body: Do we need the debug prints? After all it's displayed visually anyways, and no one is going to read the logs for this,
created_at: 2025-04-24T16:28:43Z
author_association: MEMBER
diff_hunk: @@ -851,6 +857,15 @@ pub const MeshSelection = struct { // MARK: MeshSelection
 
 	pub fn breakBlock(inventory: main.items.Inventory, slot: u32, deltaTime: f64) void {
 		if(selectedBlockPos) |selectedPos| {
+			const stack = inventory.getStack(slot);
+			const isSelectionWand = stack.item != null and stack.item.? == .baseItem and std.mem.eql(u8, stack.item.?.baseItem.id, "cubyz:selection_wand");
+			if(isSelectionWand) {
+				std.log.debug("Position 1: {}", .{selectedPos});
path: src/renderer.zig
user: login: Argmaster
body: No, probably we can get rid of them, since now it works.
created_at: 2025-04-24T19:00:43Z
author_association: COLLABORATOR
diff_hunk: @@ -976,8 +976,9 @@ pub const Protocols = struct {
 		const UpdateType = enum(u8) {
 			gamemode = 0,
 			teleport = 1,
-			worldEditPos = 2,
-			timeAndBiome = 3,
+			worldEditPosToClient = 2,
+			worldEditPosToServer = 3,
path: src/network.zig
user: login: Argmaster
body: Yeah, well, I didn't know how to check which side is receiving
created_at: 2025-04-24T19:01:23Z
author_association: COLLABORATOR
diff_hunk: @@ -972,20 +972,38 @@ pub const Protocols = struct {
 					game.Player.setPosBlocking(try reader.readVec(Vec3d));
 				},
 				.worldEditPos => {
-					const typ = try reader.readEnum(WorldEditPosition);
-					switch(typ) {
-						.selectedPos1, .selectedPos2 => {
-							const pos = try reader.readVec(Vec3i);
-							switch(typ) {
-								.selectedPos1 => game.Player.selectionPosition1 = pos,
-								.selectedPos2 => game.Player.selectionPosition2 = pos,
-								else => unreachable,
-							}
-						},
-						.clear => {
-							game.Player.selectionPosition1 = null;
-							game.Player.selectionPosition2 = null;
-						},
+					if(isServerSide(conn)) {
path: src/network.zig
user: login: IntegratedQuantum
body: SHould be a member function
```suggestion
					if(conn.isServerSide()) {
```
Also since you added this helper function, please in the future change it in all places that currently use `conn.user != null` for this purpose.
created_at: 2025-04-24T19:39:12Z
author_association: MEMBER
diff_hunk: @@ -972,20 +972,38 @@ pub const Protocols = struct {
 					game.Player.setPosBlocking(try reader.readVec(Vec3d));
 				},
 				.worldEditPos => {
-					const typ = try reader.readEnum(WorldEditPosition);
-					switch(typ) {
-						.selectedPos1, .selectedPos2 => {
-							const pos = try reader.readVec(Vec3i);
-							switch(typ) {
-								.selectedPos1 => game.Player.selectionPosition1 = pos,
-								.selectedPos2 => game.Player.selectionPosition2 = pos,
-								else => unreachable,
-							}
-						},
-						.clear => {
-							game.Player.selectionPosition1 = null;
-							game.Player.selectionPosition2 = null;
-						},
+					if(isServerSide(conn)) {
+						const typ = try reader.readEnum(WorldEditPosition);
path: src/network.zig
user: login: IntegratedQuantum
body: This happens in both branches, why not move it outside the branch?
created_at: 2025-04-24T19:40:18Z
author_association: MEMBER
diff_hunk: @@ -972,20 +972,38 @@ pub const Protocols = struct {
 					game.Player.setPosBlocking(try reader.readVec(Vec3d));
 				},
 				.worldEditPos => {
-					const typ = try reader.readEnum(WorldEditPosition);
-					switch(typ) {
-						.selectedPos1, .selectedPos2 => {
-							const pos = try reader.readVec(Vec3i);
-							switch(typ) {
-								.selectedPos1 => game.Player.selectionPosition1 = pos,
-								.selectedPos2 => game.Player.selectionPosition2 = pos,
-								else => unreachable,
-							}
-						},
-						.clear => {
-							game.Player.selectionPosition1 = null;
-							game.Player.selectionPosition2 = null;
-						},
+					if(isServerSide(conn)) {
+						const typ = try reader.readEnum(WorldEditPosition);
+						switch(typ) {
+							.selectedPos1, .selectedPos2 => {
+								const pos = try reader.readVec(Vec3i);
path: src/network.zig
user: login: IntegratedQuantum
body: Instead of having this in both branches, and making nested switches, how about doing the following before the branch like this:
```zig
const pos: ?Vec3i = switch(typ) {
	.selectedPos1, .selectedPos2 => try reader.readVec(Vec3i),
	.clear => null,
};
```
created_at: 2025-04-24T19:42:22Z
author_association: MEMBER

PR comments:
user: login: Argmaster
created_at: 2025-04-24T19:05:12Z
body: Suggestions applied, updated branch with latest changes from master.
author_association: COLLABORATOR
user: login: Argmaster
created_at: 2025-04-24T20:08:33Z
body: Suggestions applied
author_association: COLLABORATOR
user: login: IntegratedQuantum
created_at: 2025-04-25T18:50:08Z
body: Now we just need a better texture...
author_association: MEMBER
user: login: Argmaster
created_at: 2025-04-25T18:51:47Z
body: I announced I am no pixel artist so many times I no longer bother to do that 😄 
author_association: COLLABORATOR