Skip to main content Skip to table of contents
Lost and Found

The Lost and Found #

Updated 2023-02-27

I sometimes wished Minecraft had a difficulty level in between, “keep all your items when you die,” and, “you have five minutes before everything disappears forever.” I want the game to make me fear death, but recreating all my enchanted items is a pain I do not always have the spoons for.

So I created the Lost and Found, a villager who will recover your tools and armor if anything happens to them. They will give your precious items back to you, for a fee of course. The fee is one diamond and two emeralds for each item. A full set of armor and tools costs nine diamonds and eighteen emeralds to recover. The villager can also recover a few other things like Shulker Boxes and linked compasses. But they don’t recover other items like stone, food, torches, and diamonds.

Don’t want to pay? You still have five minutes to recover your tools for free! The villager only recovers items when they’re about to despawn or when they take damage (such as from fire, lava, explosions, falling anvils, or the void). Items will not be recovered unless you die while carrying them; any items you drop will despawn as usual.

Download #

If you want to use the data pack in your own world, you can find specifics on how to install and use it along with downloads on the github page. Remember that data packs are only compatible with the Java Edition of Minecraft.

Behind the Scenes #

The basic function of this data pack was simple, but it had some edge cases that were a challenge to work around.

When the Villager is Unloaded #

When I die, I’m often a long way from my home, sometimes in a different dimension. So the villager isn’t always going to be loaded when I die.

There’s a tempting way to deal with this, which is to use the /forceload command. This command prevents the targeted chunks from being unloaded. That would work, but there’s a problem: it targets chunks, not entities. If the entity wanders into a different chunk, it will unload. We can load all the chunks around the entity, but now we’re loading a lot of chunks and there’s no easy way to clean up the chunks that no longer need to be loaded if the villager wanders (or is moved by the player) to a different area.

In the past, I’ve worked on systems that use forceload remove all periodically and then immediately re-add the forceloaded chucks around all entities that need to stay loaded. I’ve had this system randomly lose track of entities before and I never figured out why. It also becomes problematic if multiple data packs are trying to use forceload to track different entities. In the end, I’ve ended up avoiding forceload for keeping entities loaded if there’s any other way.

In this case, there’s another way to deal with it. Instead of moving the items directly from the ground into the villager’s offers list, I can move them to a persistent location and then copy them to the villager whenever it is available. That’s not too hard to deal with because the /data command can write data into a special location called Storage which is always available.

The sequence of events looks like this. Detect an item that needs to be recovered and copy its data into Storage. Then kill the item so the player can’t pick it up. Whenever the Lost and Found villager is loaded, have it check Storage to see if there are any items to be recovered. If there are, copy them to the villager’s offers array and remove the item data from Storage.

Despawning, Fire, and Lava #

Detecting that an item is about to despawn is easy. I can use a selector @s[nbt={Age:5990s}] to detect that an item entity is about to despawn. Each item despawns when its Age tag exceeds 6000, which is five minutes at the game’s speed of 20 ticks per second. The nbt filter can only detect exact values, but that’s okay in this case the items aren’t going to be randomly skipping over values.

I can do the same thing to detect when items fall into lava or fire. Each dropped item entity starts with 5 health. Fire and lava will slowly reduce this to zero, at which point the item despawns. So @s[nbt={Health:1s}] will catch the item just before it vanishes. As long as the item doesn’t skip over damage values, this will work fine.

Unfortunately, explosions exist.

Charged Creepers, Anvils, and Store Result #

Earlier drafts simply didn’t protect items if they died to sources of large damage, such as creeper explosions and falling anvils. There was no way to catch an item in time if it died in a single tick and even if the item could withstand multiple creeper blasts, the nbt filter I described above only detects exact values. I would need to have a check for every possible damage value the item could receive, which is possible but would be an ugly solution.

There are two halves to this problem. First, stopping the item dying in a single frame. Second, detecting any damage value.

My solution to the first was easy: data modify entity @s Health set value 1000s. That’s it. I expected there to be some max health value that I would have to deal with separately, but there isn’t. I can just set the health value to something ridiculous.

The solution I used for the second was to convert the nbt data value into a scoreboard value. The Scoreboard system in Minecraft was originally intended to track things like how many times a player has died, how many times they’ve killed other players, how many skeletons they’ve killed, how many diamond blocks they’ve mined, and similar. But it’s possible to create a scoreboard objective that doesn’t track anything (called a dummy objective), allowing it to be used by commands to track any kind of data. Scoreboard values can also be assigned to any entity, not just players. Importantly, because of how they were intended to be used, it is easy to filter for players/entities with ranges of scores.

But the /scoreboard command doesn’t have a way to set an entity’s score according to arbitrary nbt data. But my old friend execute returns to save the day. The execute command has a store subcommand that can store the result of various other commands in several places, including the scoreboard. Keeping in mind that I set the health value of the player’s dropped items to 1000, I just need the following two lines:

execute as @s store result score @s temp run data get entity @s Health
execute as @s[scores={temp=..994}] run function lostandfound:lostandfound_saveitem

The first line copies (stores) the item’s Health value to a scoreboard objective called “temp”, which is a dummy objective. The second line runs the function to recover the item only if its temp score is 994 or below. This is a pattern I’ve used many times when I need to check nbt tags for a range of possible values.

And that works! As long as the item doesn’t take 1000 damage all at once, I can recover it.

But the greatest challenge was still ahead of me.

The Saga of The Void #

Almost there, but there’s one challenge left. Items in the void don’t take damage. They just get deleted. Saving items in the void was the most frustrating part of this experience. It’s hard to write about this because there were many dead ends and some missed opportunities that I didn’t realize would be better solutions until later. The solution I settled on does work, but isn’t the simplest solution.

If you die by falling into the void (which is normally only possible in The End dimension), then your items appear for one tick before getting deleted. There was a while when I wasn’t even sure if the items were spawning because my code didn’t detect them near the player on the tick the player died. As far as commands can tell, when a player dies while falling into the void, their items appear at the player’s previous location.

The cool solution would have been to read the player’s velocity and offset the item check by its opposite. I think all the steps involved in that are solvable, but I was out of spoons, so I just measured the player’s terminal velocity and expanded the search radius to ensure I could get the items. I think searching within 4 blocks would be enough, but I went for 5 just to be certain.

That does create a failure mode where the Lost and Found will recover any valuable items lying on the ground near the player when they die. It’s a safe failure mode, and I decided I didn’t care enough to fix it.

So my code could now find the items, but at this point the items would get deleted by the void without triggering the recovery code.

Plan A: Detecting Void #

This is where the story gets frustrating for me.

My first idea was to try and find a command that could directly detect if an item was in the void. I spent a long time experimenting with different commands like setblock, fill, and data get block, all of which fail if you target blocks below the world. I can detect when a command fails, but these commands can all fail for other reasons and I couldn’t find a way to determine the cause of the failure.

So that was a dead end.

Plan B: Measuring Y #

The other option was to check the y position of the items (the y axis points up in Minecraft). In earlier versions of Minecraft, that would have worked flawlessly, with negative values always being in the void. But since 1.18, the Overworld now extends down to y level -64. That’s the same level where items get deleted in The End.

I could make the code only check for void in The End, but worlds can be customized and I myself have even created custom dimensions with exposed void. So I wasn’t happy about this solution, but I couldn’t find anything else, so I decided to do it.

I had written some test code to fire off rockets when the player went below a specific y level, so I knew that part worked. I just had to switch the code to test items that the Lost and Found was tracking and rescue them just before they reached level -64.

And it didn’t work. My code couldn’t detect when the items went below the y level I was testing for.

After some frantic tests, I discovered that my code could detect players below a certain y level, but not items. There’s no reason it should be different for items, but I decided it must be a bug in the game and threw my code out. It turned out later that I was wrong, but I didn’t realize the mistake I was making until after finishing the data pack.

And Back to Plan A #

So I went back to my first idea and kept looking for ways to directly check if an item was below the world. I finally decided to investigate execute if blocks. It checks if two regions contain identical blocks. If I check a region against itself, it will always succeed if the region is within the world and will always fail outside it.

Inverting the result ended up being harder than I expected, but I’ll gloss that over in the interest of time. I also added an offset so the item would only be recovered when it’s 30 blocks below the world, but it worked! Almost.

And It Turned Out Plan B Worked All Along #

The code I had at this point would recover items in the void, but it didn’t care whether it was void below the world or void above the build limit. So, to check which one it was, I needed to test the item’s y value. This was a problem I had already failed to solve before.

How often would a player be dying above the world? I could just be alright with items up there getting recovered immediately instead of waiting for them to despawn.

But it frustrated me and I had thought of another way I could check y positions. I decided to see if I could get it to work.

I revisited the execute store result score pattern I described earlier. The game does have official, easier ways of testing positions, but since those weren’t working for me I decided to bypass the official methods and pull the value I wanted directly from the item’s nbt data. I could just execute store result score @s temp run data get entity @s Pos[1] to move the item’s y position into a scoreboard objective. And it worked!

I suppose I was lucky I didn’t find this solution earlier because it forced me to find a better way of detecting items in the void. Either way, I ran all the checks to ensure everything was working. For the first time, all the checks passed and I breathed a sigh of relief and called it an ending.