aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-01-30 00:16:54 +0100
committerLinnea Gräf <nea@nea.moe>2024-01-30 00:16:54 +0100
commitbecc801855c9d2ef30843b1f31c6f921bb6c2f07 (patch)
treeca9aae5de19a6d4f0463699be678dd88c2eddb2f
parent8d0f0ffb7c1707080608fb9bd7e663ca6cc1f34f (diff)
downloadmoddevwiki-becc801855c9d2ef30843b1f31c6f921bb6c2f07.tar.gz
moddevwiki-becc801855c9d2ef30843b1f31c6f921bb6c2f07.tar.bz2
moddevwiki-becc801855c9d2ef30843b1f31c6f921bb6c2f07.zip
Inventories Part II
-rw-r--r--docs/inventories.md199
1 files changed, 197 insertions, 2 deletions
diff --git a/docs/inventories.md b/docs/inventories.md
index daac28e..b10f2e6 100644
--- a/docs/inventories.md
+++ b/docs/inventories.md
@@ -100,6 +100,201 @@ boolean WRONG_METHOD = slot.getSlotIndex() < container.getLowerChestInventory().
```
!!! important
- You need to use `slot.slotNumber` or the `i` index you used for iterating here. Using the `getSlotIndex` is meaningless, since that index is the index *inside* of the inventory (so the first hotbar slot is always index `0`, just like the first slot of a chest)
+ You need to use `slot.slotNumber` or the `i` index you used for iterating here. Using the `getSlotIndex` is meaningless, since that index is the index *inside* of the `IInventory` (so the first hotbar slot is always index `0`, just like the first slot of a chest)
+
+Overall I think that the index based method is a lot less pretty. Not only is it not a real invariant of the `Container` class for those two inventories to exist in this order (In theory you could have a `Container` that puts the `slotNumber` of the `InventoryPlayer` first, and then the chest contents. This is not the case in vanilla code, however), but it also very prone to mistakes, such as messing up `<` and `<=`. We also lose all help from the type system. That `int` has no types associated with it, so especially when passing arounds `int`s like that, they use meaning very quickly, so we have to write a lot more documentation to keep our code understandable. The `inventory instanceof InventoryPlayer` is very explicit and our code reads almost like documentation itself: "is this slot inside of the players inventory or not".
+
+## Inside of Items
+
+Just logging out items to the command line is neat and all, but in most cases you will want to programatically inspect items.
+
+So, for the final time in this article, let's do a disambiguation: `ItemStack` versus `Item`. This one is hopefully a simple one.
+
+`ItemStack` represents a concrete stack. It has a size, metadata (custom name, custom lore, ExtraAttributes). If you have two item stacks in a chest somewhere, you will have two instances of `ItemStack` that reference those *exact* two item stacks.
+
+`Item` on the other hand represents a *type* of an item. For example, a `diamond_sword` or a `dirt`. Some things that you might think of as an "item type" is actually grouped together under one `Item` instance. Different coloured objects, such as wool or dyes are all just one `Item.dye` and which dye you are referencing is part of the `ItemStack` metadata. You will find most `Item`s inside of the `Items` class (`Items.apple`). However, items that correspond to a `Block` are usually not found in there. Instead, you can use `Item.getItemFromBlock(Blocks.dirt)` to get those `Item` types. Note that you will always get the same exact object instance from this method, so you can use `==` on those returned objects. Also be aware that some more exotic blocks (such as doors) might have individual `Item`s that end up placing a completely unrelated `Block`. For example: there is a `Items.wheat_seeds` which places a `Blocks.wheat` when right clicked on farmland, but calling `Item.getItemFromBlock(Blocks.wheat)` will get you a null instead of your `Items.wheat_seed`. For those "placer" `Item`s you will usually want to work in whatever medium is native for what you are doing (`Item` for inventories and entities, `Block` for reading world data).
+
+How do you get data out of an `ItemStack` now. There are two ways of going about this: APIs or NBT.
+
+### Item APIs
+
+Item APIs are arguably easier to use, so you might be tempted to just always use the, but they have some disadvantages I will talk about soon.
+
+```java
+logger.info("Slot " + i + ":");
+logger.info(" Item: " + stack.getItem());
+logger.info(" Display Name: " + stack.getDisplayName());
+logger.info(" Stack Size: " + stack.stackSize);
+logger.info(" Lore:");
+for (String loreLine : stack.getTooltip(Minecraft.getMinecraft().thePlayer, false)) {
+ logger.info(" - " + loreLine);
+}
+```
+
+This prints out all the information very nicely:
+
+``` title="Output"
+[22:50:07] [main/INFO] (ExampleMod) Slot 1:
+[22:50:07] [main/INFO] (ExampleMod) Item: net.minecraft.item.ItemBlock@68a94e58
+[22:50:07] [main/INFO] (ExampleMod) Display Name: §9Superboom TNT
+[22:50:07] [main/INFO] (ExampleMod) Stack Size: 51
+[22:50:07] [main/INFO] (ExampleMod) Lore:
+[22:50:07] [main/INFO] (ExampleMod) - §o§9Superboom TNT§r
+[22:50:07] [main/INFO] (ExampleMod) - §5§o§7Breaks weak walls. Can be used to
+[22:50:07] [main/INFO] (ExampleMod) - §5§o§7blow up Crypts in §cThe Catacombs §7and
+[22:50:07] [main/INFO] (ExampleMod) - §5§o§7§5Crystal Hollows§7.
+[22:50:07] [main/INFO] (ExampleMod) - §5§o
+[22:50:07] [main/INFO] (ExampleMod) - §5§o§9§lRARE
+```
+
+We get a bit of a hiccup with the `Item`. Turns out just system out printing a `Item` isn't great. You can call `.getRegistryName()` to fix this however:
+
+``` title="Output"
+[22:52:30] [main/INFO] (ExampleMod) Item: minecraft:tnt
+```
+
+### NBT APIs
+
+But, we soon run into problems. Two kinds of problems: logical and performance. Using the standard APIs for lore and display name invoke Forge events, which causes a *lot* of other code to run, exponentially more code the more mods you have. This is not only slow (since those other mods might do some expensive calculations), but will also obscure information. Some mods might append some information to the bottom of the tooltip, thereby not making the rarity the last line of the lore anymore, for example.
+
+So we turn to the API that doesn't call mods: NBT. NBT (Named Binary Tag) is a data format for storing essentially complex key value objects, similar to JSON. Instead of using verbose (human readable) representation for numbers, strings, bytes, booleans, lists, dictionaries NBT uses binary. There is a format called SNBT that represents NBT data in a human readable way, which looks like slightly modified JSON, which i will also use for NBT in here.
+
+Minecraft uses NBT to store all the information about items, blocks and entities in the background. Most of that data is only available on the server (thereby inaccessible inside of a client mod) and sent to the client via some other mean. The big exception to that are items. `ItemStack`s are sent (almost) entirely via NBT.
+
+```java
+byte STRING_NBT_TAG = new NBTTagString().getId(); // (1)!
+NBTTagCompound tagCompound = stack.getTagCompound();// (2)!
+if (tagCompound == null) continue; // (3)!
+String displayName = tagCompound.getCompoundTag("display").getString("Name"); // (4)!
+NBTTagList loreList = tagCompound.getCompoundTag("display").getTagList("Lore", STRING_NBT_TAG); // (5)!
+for (int i1 = 0; i1 < loreList.tagCount(); i1++) { // (6)!
+ String loreLine = loreList.getStringTagAt(i1); // (7)!
+}
+```
+
+1. First let's save the tag id of a string. This is essentially the "type" of a string when using NBTs.
+2. Access the NBT associated with an `ItemStack`. This will always be a `NBTTagCompound` which is equivalent to a JSON `object`
+3. The `NBTTagCompound` of an `ItemStack` can be null.
+4. First access the `NBTTagCompound` that is the "display". Then in that sub object access the string at "Name".
+5. First access the `NBTTagCompound` that is the "display". Then in that sub object access a list with the elements of type `NBTTagString`
+6. We can get the length of the list with `tagCount()`
+7. Now we can access each line of lore from the list using `getStringTagAt`
+ Given how long this code is, I usually have a helper method for these types of operations in my code:
+ ```java
+ public static <U extends NBTBase, T> List<T> listFromNBT(NBTTagList nbtList, Function<U, T> reader) {
+ List<T> ts = new ArrayList<>(nbtList.tagCount());
+ for (int i = 0; i < nbtList.tagCount(); i++) {
+ ts.add(reader.apply((U) nbtList.get(i)));
+ }
+ return ts;
+ }
+ ```
+ ```java
+ NBTTagList loreList = tagCompound.getCompoundTag("display").getTagList("Lore", STRING_NBT_TAG);
+ List<String> loreStrings = listFromNBT(loreList, NBTTagString::getString);
+ ```
+ This is a very powerful method that makes working with nbt lists a lot easier, but it also very easy to cause RuntimeExceptions this way. In the end I personally think that NBT is always a mess of potential runtime exceptions. There are some ways to make it more bearable, but it will always be error prone.
+
+
+You can already see how our code is getting longer. And this isn't the only problem with NBTs. Some NBT elements might not be there even tho you expect them to be. There are two ways how this manifests. A `null` in case of the root `stack.getTagCompound()` or just missing properties inside of a `TagCompound`. In case of missing properties this will just silently default construct a matching object. This is already a problem here, since we will get an empty string if we don't have a display name set (instead of null, or a fallback to the item name). It would be great if we could have a more explicit "absent" value, but sadly NBT does not offer that. Instead you will need to manually and error pronely check with `hasKey`.
+
+Another problem are those many string keys. Not only is it hard to remember them and look them up, but there is also 0 feedback at compile time for typos or any other faults in those strings. You will instead either crash at runtime, or more likely silently get empty (faulty) data.
+
+All of this makes NBT extremely unattractive to work with. But if we want our code to work correctly, even with other mod installed, or if we want our code to run fast, then we will need to use NBT more often than we would like.
+
+And it is not all bad. NBT also allows us access to bonus data that normal `ItemStack` APIs don't have access to. Enter `ExtraAttributes`.
+
+### ExtraAttributes
+
+`ExtraAttributes` is a set of extra NBT data that is sent along with most `ItemStack`s on Hypixel. It contains a lot of things from item ids to pet exp to enchants and reforges. It is essentially the machine readable counter part to the lore. Much like the lore it is not suuuper consistent, but usually survives more versions without changes. In the end we are always at the mercy of hypixel.
+
+
+```json
+{
+ id: "minecraft:diamond_pickaxe",
+ Count: 1b,
+ tag: {
+ ench: [{
+ lvl: 9s,
+ id: 32s
+ }],
+ Unbreakable: 1b,
+ HideFlags: 254,
+ display: {
+ Lore: [ ... ],
+ Name: "§aDiamond Pickaxe"
+ },
+ ExtraAttributes: {
+ id: "DIAMOND_PICKAXE",
+ enchantments: {
+ efficiency: 9
+ },
+ uuid: "28d1c00d-2112-453a-82c1-c35a28bebf6f",
+ timestamp: 1691931000000L
+ }
+ },
+ Damage: 0s
+}
+```
+
+We can see at the root the actual item metadata (the `Count`, `id` and `Damage`). Those are part of the `ItemStack` and are always parsed by the `ItemStack` APIs. The NBT we get access to with `getTagCompound` is the `tag` part of this SNBT.
+
+The `ench` tag contains the *vanilla* enchantments. Those are used by vanilla code. Hypixel does not send all enchantments this way, only the ones that affect client behaviour, such as efficiency and depth strider.
+
+The `Unbreakable` tag hides the durability bar. This makes your items not show the durability bar for a split second whenever you mine a block.
+
+The `HideFlags` tag prevents minecraft from adding information to the lore. Each bit represents something different, like "Hide the fact that is item is marked as Unbreakable" or "hide the enchantments on this item".
+
+We looked at the `display` tag earlier.
+
+Lastly there is `ExtraAttributes`. This section is not vanilla at all. It is instead Hypixel's own internal data structures. This is used by Hypixels code to represent information about the item that go beyond what Minecraft can express. It contains an `id` that is the official hypixel id (which may be different from the vanilla id), `enchantments` (which is a tag compound mapping enchantment ids to levels), uuids which are a unique identifier for each item that exist (for example: no two ASPECT_OF_THE_END have the same uuid, you get a new one every time you craft) and so much more.
+
+Some of this information is found on almost every item (such as `id`), some of this data is item specific and some of it is shared between only a few items.
+
+Generally you can find out quite a lot about an item by looking at its `ExtraAttributes`. I can't go over everything here, but let's look at one more example:
+
+
+```json
+{
+ id: "minecraft:skull",
+ Count: 1b,
+ tag: {
+ SkullOwner: {
+ Id: "ecc8937f-a09e-4f06-a10a-efadfaff1e3b",
+ hypixelPopulated: 1b,
+ Properties: {
+ textures: [{
+ Value: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzA3MWE3NmY2NjlkYjVlZDZkMzJiNDhiYjJkYmE1NWQ1MzE3ZDdmNDUyMjVjYjMyNjdlYzQzNWNmYTUxNCJ9fX0="
+ }]
+ },
+ Name: "§ecc8937f-a09e-4f06-a10a-efadfaff1e3b"
+ },
+ display: {
+ Lore: [ ... ],
+ Name: "§7[Lvl 100] §6Elephant"
+ },
+ ExtraAttributes: {
+ petInfo: "{\"type\":\"ELEPHANT\",\"active\":false,\"exp\":4.0048701025808394E7,\"tier\":\"LEGENDARY\",\"hideInfo\":false,\"heldItem\":\"GREEN_BANDANA\",\"candyUsed\":0,\"uuid\":\"8d35c8fe-1351-47f6-8609-e0b0fbcb077d\",\"uniqueId\":\"5cdc7008-009e-4730-8f52-cd970491ecc4\",\"hideRightClick\":false,\"noMove\":false}",
+ id: "PET",
+ uuid: "8d35c8fe-1351-47f6-8609-e0b0fbcb077d"
+ }
+ },
+ Damage: 3s
+}
+```
+
+This pet is a skull, which has some interesting information in the `SkullOwner` tag. That tag contains the texture for the skull. This can be used occassionally when identifying custom items that don't have a `ExtraAttributes.id` tag.
+
+
+Also we can tell that the `id` of this pet is just `PET`. You might expect it to be `ELEPHANT_PET` from mods such as NEU, but those ids are just made up extensions to the `id` by Hypixel. The pet type is actually stored in the `petInfo` string, which is actually a string containing a normal JSON object that needs to be decoded on top of the NBT data parsing.
+
+You can see all kinds of useful info in there, like `candyUsed` which is normally a stat hidden on level 100 pets. It contains information about the `type`, obviously, but also the total `exp` and the `heldItem` (with an item `id` instead of just a name).
+
+This kind of way of inspecting data is really powerful and makes a lot of mods possible in the first place. But NBTs are messy and I would highly recommend you transfer NBTs into normal Java objects [as soon as possible](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/).
+
+## Closing out - The GuiOpenEvent
+
+And finally let's circle back to our first section, in which we started with the `ClientTickEvent`. In the end `ClientTickEvent` works just fine. But also, it leaves us lacking. The performance isn't great and we sometimes just miss items. Normally we could use a `GuiOpenEvent`. This fires whenever a gui is opened, so we could in theory just read all of our data once, when we open a chest. Sadly that doesn't work out, since Hypixel only sends the items after the GUI has opened. We might get some or all of the items in a single tick, but we can't be so sure about that when there are people with a 300, 400 or even 500 ms ping to Hypixel. There are many solutions to this problem: simply waiting a set amount of ticks after a `GuiOpenEvent` is probably the easiest one. That one is obviously a bit sloppy, but is also very simple to implement. Another solutions, would be mixing into `NetHandlerPlayClient.handleSetSlot` and listening for the bottommost rightmost item to be set (slotCount - 1) (this one is a lot cleaner, works faster and almost never gives us any partial inventory states), which still fails if Hypixel decides to use empty item stacks for that slot. Almost always there will be a glass pane or another item, but when a Hypixel GUI decides against using glass panes your code might not just run at all. You could maybe decide to mix those methods: either the last index is set, or we waited 5 ticks after the `GuiOpenEvent`. There are probably more options out there to explore and it is up to you how ridiculous you want to make your system for detecting inventory opens. In the end Hypixel doesn't specifically provide an API for mod developers, so we have to make due with what happens to work for us.
+
+
-Overall i think that the index based method is a lot less pretty. Not only is it not a real invariant of the `Container` class for those two inventories to exist in this order (In theory you could have a `Container` that puts the `slotNumber` of the `InventoryPlayer` first, and then the chest contents. This is not the case in vanilla code, however), but it also very prone to mistakes, such as messing up `<` and `<=`. We also lose all help from the type system. That `int` has no types associated with it, so especially when passing arounds `int`s like that, they use meaning very quickly, so we have to write a lot more documentation to keep our code understandable. The `inventory instanceof InventoryPlayer` is very explicit and our code reads almost like documentation itself: "is this slot inside of the players inventory or not".