8

The title is a little complex, so let me explain it more fully. I am writing a datapack where I want to be able to run some commands if there is a rectangle of iron bars placed which is complete. I also specifically don't want it to run if there are any breaks/intersections in the rectangle with other iron bar structures. I don't know the size of the rectangle or the y coordinate or anything but these facts: it will be a rectangle of iron bars all in the same y level which is in the overworld. In addition, there needs to be a block right below each bar. The end goal of this is that the player can place a rectangle of iron bars to mark out an area where only they can go, and other players inside this rectangle are hit with lightning until they leave. The other feature I'd like to have (although not necessary) is for the process to eliminate the possibility of a player having multiple areas. If this is not possible, however, it is no big deal. Here is what I've come up with so far:

Edit: I removed what I had originally to reflect progress made, mostly by Fabian in the comments.

Whenever a player places an iron bar, which could be detected with a scoreboard, a raycasting function is triggered to get the coordinates of the iron bar. After testing to make sure that there are two blocks of air over it and a solid block under it, using

execute if block ~ ~1 ~ air if block ~ ~2 ~ air unless block ~ ~-1 water unless block ~ ~-1 ~ cave_air unless block ~ ~-1 ~ lava...,

I spawn in two armor stands. One of them stands still to mark the starting position and the other one, rotated parallel to the iron bar it's on, teleports 1 block forwards along the rectangle, doing the above check for each position. It also makes sure that all bars are either straight or corners, and when it encounters a corner, the teleportation rotates it to have it continue around the rectangle.

An important part would be making sure the armor stand always starts facing the clockwise direction, so whenever it encounters a corner it can be set to turn 90 degrees to the right.

If any of the tests fail: if a solid block is missing, if a rail is missing, if there is a bar with an intersection... the armor stands are killed. They, of course, are spawned back in for another test the next time the player places a rail.

            >>>>
    A------x------B
   ^|             |
   ^|             |
    w             y
    |             |
    |             |
    D------z------C
       <<<<

One armor stand stands still at one of the midpoint letters (w, x, y, z), while the other moves around clockwise, turning the corners at A, B, C, and D with 90 degree turns, finally getting back to the letter it started at. No matter which letter it starts at, it should move clockwise.

The problem I'm now running into (besides actually implementing the process described above) is that when the armor stands make it all the way around, they need to mark themselves and the player with a unique identifier. This is so player A and player B, who both have rectangles, can't go into each other's rectangles as well as their own, which is what would happen if they had the same, non-unique tag. Commands make this hard because there is no way to pass a String to another command, and generating, saving, and using random numbers in the middle of commands is really difficult.

  • 2
    My face is currently the 🤔 emoji… – Fabian Röling Jun 9 at 21:25
  • Idea #1: Look for most negative X+Z point first, then go into the two positive directions and from there to the other positive directions. That should save you some of this turning logic. Idea #2: Use one or two armour stands to walk along the iron bars, makes it much easier to use relative coordinates and you can also store scoreboards in them and so on. Idea #3: Don't check for the shape of the iron bars, because they connect to many more things than just other iron bars. Instead use if block to check what's inside and outside the border. It would also be possible to detect ANY rectangle,… – Fabian Röling Jun 9 at 21:30
  • … even in very complex arrangement of rectangles, like these "how many rectangles are there" images, but that would definitely be more complex and could cause way more lag. Idea #4: Don't use raytracing or stuff like that, use a scoreboard of type used:iron_bars. And, if you want to get really fancy, an advancement with trigger of type placed_block, iron bars as condition and the function as reward, that way you need no repeating command blocks or … – Fabian Röling Jun 9 at 21:34
  • … repeating function at all. I'll figure out more tomorrow and try to implement it, this is a very interesting challenge. – Fabian Röling Jun 9 at 21:43
  • You do actually need raytracing to figure out which iron bar was placed. – Fabian Röling Jun 10 at 18:07
15

This was a nice programming challenge. I had fun, learned some things and discovered some Minecraft bugs. Thanks to vdvman1 in the Eigencraft Discord chat for commands help, mainly with edge cases of facing and anchored, for the idea not to use entities at all for raytracing and for the recursion tail optimisation tip.

Here is the complete data pack: https://drive.google.com/file/d/1aw_KfHyEQwtCiWCP4R3H6TYVczmLT1-s

The file structure:

rectangle
└pack.mcmeta
└data
 └rectangle
  └advancements
  │└place_iron_bar.json
  └functions
   └init.mcfunction
   └raycast.mcfunction
   └search_origin.mcfunction
   └x_first.mcfunction
   └z_second.mcfunction
   └z_first.mcfunction
   └x_second.mcfunction

pack.mcmeta is just the minimum required: {"pack":{"pack_format":3,"description":""}}
You can adjust it to display whatever you want, the format is explained here (archive).

place_iron_bar.json is an advancement that is triggered by placing an iron bar, which calls the init function (which resets the advancement):

{
 "criteria":{
  "place_iron_bar":{
   "trigger":"minecraft:placed_block",
   "conditions":{
    "block":"minecraft:iron_bars"
   }
  }
 },
 "rewards":{
  "function":"rectangle:init"
 }
}

init.mcfunction resets the advancement and then start the recursive raycast function with the correct alignment to your eyes:

#reset so that this doesn't only trigger once
advancement revoke @s only rectangle:place_iron_bar
#double anchor as a workaround for MC-124140
execute anchored eyes positioned ^ ^ ^ anchored feet run function rectangle:raycast

raycast.mcfunction moves the execution position forwards by 0.01 blocks until it hits iron bars, then starts search_origin. If you look very, very closely at the edge of a block when placing the iron bars, the raytracing might miss it, but that's unlikely. You could also intentionally make it miss, for example by standing right at a wall with a torch on it and placing the last iron bar behind you that way. But if you do that… well, then it's your own fault, I guess. It would be possible to just perfectly track every block around you and monitor every single change, but that would permanently cause an immense about of lag for almost no gain.
If the raytracing fails, it will keep going for 327 blocks by default, determined by the maxCommandChainLength gamerule.

execute if block ~ ~ ~ iron_bars run function rectangle:search_origin
execute unless block ~ ~ ~ iron_bars positioned ^ ^ ^.01 run function rectangle:raycast

search_origin.mcfunction is another recursive function (recursion is just the easiest way to make loops in Minecraft), this one goes into the negative X direction as long as it finds iron bars there and into negative Z direction as long as it finds iron bars there. If you have an arrangement like this…

…then it will go to the end of this chain. But since the rectangle search afterwards will fail anyway in this case, that doesn't matter much. The lag it causes it also negligible, I'm actually unable to see any spike in the FPS or TPS graph when placing an iron bar.
Once the point of origin is found, the execution branches off into two functions (which are actually executed strictly after each other, this becomes important later), one goes into the positive X direction first and then the positive Z direction, the other into positive Z direction first and then the positive X direction. There are also some validations for the start of the rectangle, otherwise for example a 1×1 arrangement of iron bars would be considered a rectangle.
In this version of the data pack there is actually still a bug which causes it to not find a rectangle of size 2×3, 2×4, 2×5, etc. 2×2 rectangles are recognised, but nothing that is longer in one direction. Fixing this bug would be complicated, but when I thought about it more, I actually liked this behaviour, because in a 2×3 arrangement, the middle two iron bars actually connect, making it not look like a single rectangle. Example:

#This function traverses a series of iron bars in negative X and Z direction to find the negative corner of a rectangle. If the shape is not a rectangle, it will prefer going in negative X direction over the negative Z direction and just end whereever it can't find another iron bar.
execute unless block ~-1 ~ ~ iron_bars unless block ~ ~ ~-1 iron_bars positioned ~1 ~ ~ if block ~ ~ ~ iron_bars run function rectangle:x_first
execute unless block ~-1 ~ ~ iron_bars unless block ~ ~ ~-1 iron_bars positioned ~ ~ ~1 if block ~ ~ ~ iron_bars run function rectangle:z_first
execute unless block ~ ~ ~1 iron_bars run kill @e[type=armor_stand,tag=z_end]
execute positioned ~-1 ~ ~ if block ~ ~ ~ iron_bars run function rectangle:search_origin
execute unless block ~-1 ~ ~ iron_bars positioned ~ ~ ~-1 if block ~ ~ ~ iron_bars run function rectangle:search_origin

x_first.mcfunction goes into positive X direction as long as it finds iron bars, then starts z_second, if there are iron bars in the positive Z direction. It also checks along the way if there are any iron bars to the side, which invalidate the rectangle. In that case it just stops executing, which will lead to no result at the end.

execute unless block ~1 ~ ~ iron_bars unless block ~ ~ ~-1 iron_bars positioned ~ ~ ~1 if block ~ ~ ~ iron_bars run function rectangle:z_second
execute unless block ~ ~ ~1 iron_bars unless block ~ ~ ~-1 iron_bars positioned ~1 ~ ~ if block ~ ~ ~ iron_bars run function rectangle:x_first

z_second.mcfunction goes into positive Z direction as long as there are iron bars and checks for any on the side that would make the rectangle invalid, then summons a marker armour stand at the end. This is necessary to check if both paths arrive at the same ending location.

Only after x_first and z_second are done, z_first.mcfunction is started. It does the same as x_first, but with X and Z swapped. It also kills the marker armour stand if it encounters something that invalidates the rectangle.

execute unless block ~ ~ ~1 iron_bars unless block ~-1 ~ ~ iron_bars positioned ~1 ~ ~ if block ~ ~ ~ iron_bars run function rectangle:x_second
execute if block ~-1 ~ ~ iron_bars run kill @e[type=armor_stand,tag=z_end]
execute if block ~1 ~ ~ iron_bars if block ~ ~ ~1 iron_bars run kill @e[type=armor_stand,tag=z_end]
execute unless block ~1 ~ ~ iron_bars unless block ~-1 ~ ~ iron_bars positioned ~ ~ ~1 if block ~ ~ ~ iron_bars run function rectangle:z_first

x_second.mcfunction does the same as z_second, but with X and Z swapped and it also kills the marker armour stand if it finds anything that invalidates the rectangle. If everything goes through without problems, it checks if its ending location is the same as the one of z_second, meaning that it arrived at the exact location of the marker armour stand. If it doesn't, it means for example that the positive X/Z corner of the rectangle is missing.

execute unless block ~1 ~ ~ iron_bars unless block ~ ~ ~1 iron_bars if entity @e[type=armor_stand,tag=z_end,distance=0] run say Rectangle found!
execute unless block ~1 ~ ~ iron_bars run kill @e[type=armor_stand,tag=z_end]
execute if block ~ ~ ~1 iron_bars run kill @e[type=armor_stand,tag=z_end]
execute if block ~ ~ ~-1 iron_bars run kill @e[type=armor_stand,tag=z_end]
execute unless block ~ ~ ~1 iron_bars unless block ~ ~ ~-1 iron_bars positioned ~1 ~ ~ if block ~ ~ ~ iron_bars run function rectangle:x_second

You can of course replace say Rectangle found! with anything that should be done if the rectangle is found.
If you need any of the positions, I recommend setting some scoreboard or whatever when the rectangle is found and then checking for it in the different functions after the final function call. Examples:

  • If you need the positive corner, just do something instead of say Rectangle found!.
  • If you need to do something on the negative Z edge of the rectangle, create some markers in search_origin with the same condition as the x_first call and in x_first anywhere without condition, then use them at the end of init if the rectangle was successfully validated, otherwise kill them there.
  • For other cases you might have to invert some of the checks, for example to do it for all but the last execution of a loop.

This datapack should be completely multiplayer compatible, not even two players standing in the exact same spot and placing an iron bar at the exact same time should cause any issue, since all the functions only start for one player once they're done for another player. There are also no reference like @p that could cause any issues, the executioner is always handed over from function to function.
I've also tried many different arrangement of iron bars, like two rectangles that share a corner or an edge, extra bits or missing bits in every possible location, different last placed iron bars, etc. It should hopefully be foolproof.
If the rectangle goes outside of loaded chunks, it will probably just fail as if the iron bars simply weren't there.

  • 4
    This is easily in my top 10 of answers I've spent the most time with. But I doubt that it will get into the top 10 of my most upvoted answers. But hey, I have bullet points, bold formatting and images, maybe that helps. :D It's also my 404th answer, luckily not a dead link. – Fabian Röling Jun 11 at 2:19
  • 1
    The functions already run as the player, so you can just make the final command (currently say Rectangle found!) do something for the player. What that is depends on what you want to do. If you want to be able to do anything, you need an ID system for players (many solutions for that are available online) and put some marker into the rectangle with the same ID. – Fabian Röling Jun 11 at 2:58
  • 1
    I broke it, on accident. When facing east or west and placing an iron bar, another iron bar to the left, and one more a block towards you from the left one, it says it finds a rectangle when there are only three bars. The only reason this is an issue is because a player might start placing at a corner and end up triggering it before they finish their rectangle. – Nik3141 Jun 11 at 3:21
  • 1
    You broke it on accident? You mean you found a bug? :D That's a good thing! I tried it myself, could immediately reproduce it, already know what it was and fixed in. I'll edit the answer now. – Fabian Röling Jun 11 at 10:56
  • 2
    I just noticed that I still have this datapack active in my test world, for months already. So I can confirm that there is actually no performance impact. :D – Fabian Röling Sep 8 at 8:30
0

For identifying players, which was a daunting challenge at first since a lot of things that I make require an ID of sorts, I actually spent a bit of time trying to find ways of identifying individual players- and it was actually a lot easier than you’d think.

First, summon an armor stand to hold ID-related data.

/summon armor_stand ~ ~ ~ {Tags:[“IDData”], Invisible:1b,Invulnerable:1b,NoGravity:1b,Marker:1b}

You’ll need a scoreboard to hold everyone’s ID:

/scoreboard objectives add ID dummy

From your chat just once run:

/scoreboard players add @e[tag=IDData] ID 1

Then we’ll need three command blocks, two repeating and a conditional chain block.

/scoreboard players add @a ID 0 /execute as @a[scores={ID=0}] run scoreboard operation @s ID = @e[tag=IDData,limit=1] ID (Conditional) /scoreboard players add @e[tag=IDData] ID 1

This makes it so that when a player without an ID joins, their score is set to 0 and then set equal to the next available number, adding one to the next available number.

  • 1
    This definitely works for an ID system, but is there a way to dynamically relate an armor stand spawned by Fabian Roling's datapack with the player's ID? Because if the player places an iron bar which completes a rectangle, there are two armor stands summoned at the positive and negative corners. Assuming the player who placed the armor stand has an ID of x, how can I store that x in the armor stands? I know I could use execute store result entity, but what nbt could I change? Pos[] would actually change the position of the armor stand, which would subvert its purpose as a placement marker. – Nik3141 Jun 17 at 17:54
  • 1
    The question was not about an ID system, but about detecting rectangles. Did you maybe accidentally answer the wrong question? – Fabian Röling Jul 24 at 16:50
  • @Fabian At the end of my question I mentioned also needing a way to identify which rectangle belongs to which player. I think this was in response to that. – Nik3141 Jul 24 at 17:18
  • @Nik3141 The player who placed the iron bar is simply @s, in every single function. – Fabian Röling Jul 24 at 18:05
  • @FabianRöling Yes, but if I tag @s with a tag, everyone who makes a rectangle has the same tag and thus the same rectangle. Therefore, I need to dynamically relate a connection between the armor stand spawned in and @s, which is distinct from the connection between a different player (who is also @s) and their armorstand, even if it is spawned in with the same set of functions. I figured out how to do it, but it is a different problem than this one, so I'll self answer a different question on it when I have time. – Nik3141 Jul 30 at 17:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.