4

[Tutorial] Searching in arbitrary NBT-lists

TheBeber's Avatar TheBeber1/14/23 1:24 pm history
4 emeralds 149 1
1/15/2023 5:42 am
-LEO-'s Avatar -LEO-
Hey 👋

I've discovered a way to search through a list of NBT-Data, which may be arbitrary in length and randomly sorted. I recently stumbled upon this problem while trying to update my datapack "Bonk Villager". I don't know, if this is any use for anyone and I'm not aware of other solutions, but sharing a bit of knowledge never hurts anyone.
Please note, that code blocks starting with "### TEMPLATE FUNCTION ###" are, as the name implies, just templates and might have to be adjusted to suit your specific needs. In such templates, you might stumble across placeholders, which are enclosed in square brackets like "[​replace me!]". But text in square brackets might also be literal (i.e. a list index like "[​0]" or "[-1]"). You might also want to rename some variables and references, to suit your needs. Therefore be cautious, when reading, and most importantly pay attention to the context. If something is unclear, just ask in the comments.
That being said, let's start right with...

1. The problem

Let's suppose, you want to change an element in some NBT-data depending on some of it's properties, that is structured like this:{TargetList: [{Element1Tag1: data, Element1Tag2: data}, {Element2Tag1: data, Element2Tag2: data}, ...]}This structure is very common. For example it appears in entity "Passenger" data, item tag data ("CanDestroy", "AttibuteModifiers", "CanPlaceOn", "Enchantments", etc.) and(, the thing that I recently had to deal with, therefore it's listed here,) the "Gossips"-data of Villagers.. Of course, you can try to address the the data directly by index, but that requires prior knowledge of the structure and you'll have to ensure that only your datapack modifies the data.
Now, in a regular programming or scripting languages, you can use loops and dynamicly use variables, to achieve a similar goal. Unfortunately Minecraft-functions don't have something like that (well technically there are loops, but you can't easily limit the amount of iterations to the length of the list), which makes it quite a challenge to implement an effective algorithm for that purpose. Luckily, there is still a solution, which I believe to be a quite efficient one. (Also a challenge is quite fun.) So let's finally look at...

2. The solution

We're gonna work extensively with the `/execute`, `/scoreboard` and `/data` command as well as NBT-storage, so I suggest you to familiarize yourself with those, if you're not yet that well-versed in using them.
The algorithm is split up into four parts:
  1. Initialization
  2. Looping search
  3. Further steps, that involve the search result
  4. Final functionality
Step one is setting up the initial values in storage and scoreboard. This might involve only one function (or two functions if you have a "load"-function).
### TEMPLATE FUNCTION ###
# as: [Target object/entity]; at: anywhere; Additional context may apply.
# Preparation for looping NBT-data search. Scoreboard objectives and static values, such as search
# terms may be initialized inside a different load-function.

# Initializing scoreboard (this may be done inside a load-function):
scoreboard objectives add loop_nbt_search dummy
scoreboard players set %not_found% loop_nbt_search 1
scoreboard players set %search_incomplete% loop_nbt_search 1

# Initializing statics (This also may be done inside a load-function):
data modify storage your_namespace:loop_search SearchTermStatic1 set value "can_be_anything"
data modify storage your_namespace:loop_search SearchTermStatic2 set value 2

# You may want to use a context dependent search term:
data modify storage your_namespace:loop_search SearchTermDynamic set from [reference]

# Copy corresponding list into storage (as to not unnecessarily mess with the actual data.)
data modify storage your_namespace:loop_search ListToSearch set from [reference to target list]

# It is also important to save the first element in the list, in order to stop the search, once every
# element has been inspected.
data modify storage your_namespace:loop_search StopIndicator set from [reference to target list][0]

# Finally execute the next function performing the search:
function your_namespace:loop_search_function
With the initialization being done, the next part is performing the actual search:
### TEMPLATE FUNCTION ###
# as: [Target object/entity]; at: anywhere; Additional context may apply.
# This is where the magic happens. This part may be repeated as many times, as the list has elements.

# Reinitialize all the neccessary additional values, which are the scores and test search terms, as
# they might be altered during the search. Dynamic search terms may also be completely altered, if
# they change during the process.
scoreboard players set %not_found% loop_nbt_search 0
scoreboard players set %search_incomplete% loop_nbt_search 1
data modify storage your_namespace:loop_search SearchTermStatic1Test set from storage your_namespace:loop_search SearchTermStatic1
data modify storage your_namespace:loop_search SearchTermStatic2Test set from storage your_namespace:loop_search SearchTermStatic2
data modify storage your_namespace:loop_search SearchTermDynamicTest set from storage your_namespace:loop_search SearchTermDynamic
data modify storage your_namespace:loop_search StopIndicatorTest set from storage your_namespace:loop_search StopIndicator

# Perform a comparison between the test search terms and the values inside the last element in the
# copy of the target list. This uses the property of the "/data modify [...]" command to fail, if the
# source and target data is identical. If the values are identical, the score of %not_found% becomes
# 0. The subsequent check is only then performed, if the previous test is positive, as to not
# overwrite a previous negative result.
execute if score %not_found% loop_nbt_search matches 0 store success score %not_found% loop_nbt_search run data modify storage your_namespace:loop_search SearchTermStatic1Test set from storage your_namespace:loop_search ListToSearch[-1].Tag1
execute if score %not_found% loop_nbt_search matches 0 store success score %not_found% loop_nbt_search run data modify storage your_namespace:loop_search SearchTermDynamicTest set from storage your_namespace:loop_search ListToSearch[-1].Tag2
execute if score %not_found% loop_nbt_search matches 0 store success score %not_found% loop_nbt_search run data modify storage your_namespace:loop_search SearchTermStatic2Test set from storage your_namespace:loop_search ListToSearch[-1].Tag3

# Shift the last list element to the front, as if the list would wrap around itself. (e.g.:
# [1, 2, 3, 4] -> [4, 1, 2, 3]).
data modify storage your_namespace:loop_search ListToSearch prepend from storage your_namespace:loop_search ListToSearch[-1]
data remove storage your_namespace:loop_search ListToSearch[-1]

# The next step is to set, weather the search is complete or not. If the first element is
# at index 0, we know, that we checked every element in the list. The search can also be con-
# cluded, if the score of %not_found% is 1. Otherwise you can leave out the second command, if you
# want multiple results.
execute store success score %search_incomplete% loop_nbt_search run data modify your_namespace:loop_search StopIndicatorTest set from storage your_namespace:loop_search ListToSearch[0]
execute if score %search_incomplete% loop_nbt_search matches 1 if score %not_found% loop_nbt_search matches 0 run scoreboard players set %search_incomplete% loop_nbt_search 0

# If a suitable element was found, run the function, that does something (anything you like really)
# with it. The result should always be the first element in the list.
execute if score %not_found% loop_nbt_search matches 0 run function your_namespace:[a suitable function]

# Loop, this function, if the search is incomplete.
execute if score %search_incomplete% loop_nbt_search matches 1 run function your_namespace:loop_search_function

# Lastly, if you wish to overwrite the original source data with the final result, you can do this
# here or in a separate function. The result is in "storage your_namespace:loop_search ListToSearch".
execute if score %search_incomplete% matches 0 run [write data to target/call final function]
Phew... that is quite a bit to wrap ones head around. But it's not too complicated, once you break it down into the basics:
  1. Reset all variables, that need to have a specified value.
  2. Compare the values of the last element in the list against previously defined values, you want to look for, and set a boolean, depending on the result.
  3. Shift all list elements to the next index. The last elements winds up at index 0.
  4. [​Optional] Do the things you need to do with the result (the first element in the list).
  5. Loop back to step one, until the first element is the same as the original list. (Or until you have a result.)
  6. [​Optional] If the search is marked as completed execute a final function.
You can also alter the function to give a result, if only one comparison is positive, or to give multiple results. But the concept is identical, so I won't go over it here. (But if you have any question on how to do it, put it in the comments.)
So...

3. In conclusion

This might be a very specific problem to have, but I hope this post helps, once it occurs. I should also mention, that at the time of writing this, the most current Minecraft version is 1.19.3 and as such, this solution might be obsolete or might have to be altered for different (future) versions. (This solution works at least from version 1.16 up to 1.19.3.) I'll probably also put a link to a dedicated and clean example here in the future. For now, you can see an application of this concept in my datapack Bonk Villager. (Specifically the functions: "load", "when_bonked", "search_gossip_entry" and "curb_gossip")
This my first tutorial/developer-resource so feel free to tell me in the comments, if you have any questions, something is wrong, or where I can make things more clear. Additionally I'd like to see, what applications or even variations (for example something using stack like operations could work) you can come up with.

Have a lovely day and until next time I stumble upon an obscure problem, that I believe deserving of a tutorial ( ^_^)/
Posted by TheBeber's Avatar
TheBeber
Level 23 : Expert Button Pusher
3

Create an account or sign in to comment.

1

-LEO-
01/15/2023 5:42 am
Level 50 : Grandmaster Waffle
-LEO-'s Avatar
Instead of a "Stop Indicator" you can get the array length of your search list:
/execute store result score %search_list_length% loop_nbt_search run data get storage your_namespace:loop_search ListToSearch With this, you can control your loop with scoreboards. This can be handy when the list is altered or choosing random elements etc. In this case, it wouldn't make much of a difference. Scoreboards are a bit more efficient, but with such low command count, you should choose the option that fits your project the most.
2
Planet Minecraft

Website

© 2010 - 2024
www.planetminecraft.com

Welcome