Skip to content

Item updater#871

Open
Huskydog9988 wants to merge 29 commits into
CivMC:mainfrom
Huskydog9988:item-updater
Open

Item updater#871
Huskydog9988 wants to merge 29 commits into
CivMC:mainfrom
Huskydog9988:item-updater

Conversation

@Huskydog9988
Copy link
Copy Markdown
Contributor

Reopening #754 in preparation of custom item rework

Protonull and others added 21 commits February 4, 2025 20:43
+ refactors ItemMetaConverterHack
This means functions can predicate potentially expensive followup code (like updating the client about something) on whether there's something to update.
This was considered too speculative. Unfortunately, there's really no other way to achieve the same type-limiting goal (ChatGPT tells me this concept is also called "Programming to an Interface", "Information Hiding", and "Minimized Coupling") without delving into NMS, which *does* have the interface (weird how PaperMC doesn't). This does means that migrators can cast the NMS DataComponentHolder back to an NMS ItemStack... but then again migrators could have reflectively accessed the internal item within ItemDataComponentHolder, so 🤷‍♂️.
I was basically fretting over this because I knew, I just knew, that someone would write an ItemStack migration that's chock full of ItemUtils calls instead of a ItemMeta migration with MetaUtils calls. That said, even though I do honestly believe that a migration context should be more limited, it's still nonetheless an abstraction on top of ItemUpdater, which unashamedly updates ItemStacks. That said, I've annotated the ItemStack migration interface as experimental and denied it a convenient registration shortcut as a form of "friction as a deterrent".
This will help with doing befores/afters with item updates
This uses the event-driven nature of ItemUpdater and the default implementations, effectively using Bukkit's event system as a pseudo itemUpdater registry. Which is a bit silly but preferable to the alternative.
Per request, this has been removed as unnecessary, that the HashMap overhead is fine.
Turns out the Bukkit method is pretty convenient.
The "items" package was *right there*, why wasn't the "CustomItem" class put in there? 😭
Genuinely cannot wait for JEP Draft 8303099 to be merged 🫠
Plus added a deliberate NMS item migration option
This will allow custom items and compact items to have independent version values.
# Conflicts:
#	plugins/finale-paper/src/main/java/com/github/maxopoly/finale/misc/ArmourModifier.java
#	plugins/heliodor-paper/src/main/java/net/civmc/heliodor/meteoriciron/FactoryUpgrade.java
#	plugins/heliodor-paper/src/main/java/net/civmc/heliodor/meteoriciron/MeteoricIron.java
okay but actually this time
import vg.civcraft.mc.civmodcore.inventory.items.updater.migrations.ItemMigration;
import vg.civcraft.mc.civmodcore.inventory.items.updater.migrations.ItemMigrations;

public abstract class CustomItemsUpdater implements ItemUpdater {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this class initialised?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its really weird. The class is meant to be extended, by a migrator for an item, which then calls

itself, or something similar to that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. Give #754 a read through. It started off as a really simple predicate-with-side-effects: it takes an item and returns a boolean of whether the item was modified or not. You'd do all your checks within that predicate, returning early wherever possible.

Then I added event handler classes which handle specific events and updating their respective items. For example, UpdatePlayerItemsOnJoin listens for player joins and then invokes the predicate for each item in the player's inventory and ender chest. UpdateInventoryItemsOnOpen listens for players opening chests (and other contains), updating its items. UpdateContainerItemsOnLoad listens for chunk loads and updates all the items in its container blocks.

package uk.protonull.civ.example;

import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import vg.civcraft.mc.civmodcore.ACivMod;
import vg.civcraft.mc.civmodcore.inventory.items.updater.listeners.UpdatePlayerItemsOnJoin;

public final class ExamplePlugin extends ACivMod {
    @Override
    public void onEnable() {
        super.onEnable();
        registerListener(new ItemUpdateListener());
    }

    private static final class ItemUpdateListener implements UpdatePlayerItemsOnJoin /** , UpdateInventoryItemsOnOpen */ {
        @Override
        public void updateItem(final @NotNull ItemStack item) {
            // if (item.needsUpdating()) {
            //     item.updateSomehow();
            // }
        }
    }
}

If you wanted your item-updater to run whenever a chest was opened, all you needed to do was uncomment that implements part. All the work was done for you.

However, Okx wanted more of a DataFixer-esque system. Take a look at net.minecraft.util.datafix.DataFixers for what that looks like. As mentioned here: instead of entirely replacing what was already built, I instead created the CustomItemUpdater as a layer of abstraction on-top of it. This meant there was a simpler system if you wanted it, and a complexer system if you needed it.

However, I do admit that the type shenanigans were a mistake. It seemed clever at the time but it's definitely a struggle to read, even for me now and I wrote it. Basically, since UpdatePlayerItemsOnJoin extends ItemUpdater but does not implement it, you can effectively combine/union them with an implementation of ItemUpdater via casting, eg:

package uk.protonull.civ.example;

import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import vg.civcraft.mc.civmodcore.ACivMod;
import vg.civcraft.mc.civmodcore.inventory.items.updater.listeners.UpdatePlayerItemsOnJoin;

public final class ExamplePlugin extends ACivMod {
    @Override
    public void onEnable() {
        super.onEnable();
        registerListener((UpdatePlayerItemsOnJoin) ExamplePlugin::updateItem);
    }

    private static void updateItem(final @NotNull ItemStack item) {
        // if (item.needsUpdating()) {
        //     item.updateSomehow();
        // }
    }
}

) {
// This is a deliberate 0th migration that ensures that any item
// being migrated has a custom-item key and item version.
this.migrations.put(0, (ItemMigration.OfItem) (item) -> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is a first migration necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding that items like fossils are registered custom items, so they would need a migration like this to be put into the system essentially. The problem is I have no idea how they are targeted by a migration if they lack a custom item key. IIRC the migration code just looks for items with a custom item key?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two purposes to the item updater system:

  1. Provide a means to update legacy custom items (those that are "custom" by virtue of having names and/or lore) to be registered custom items.

  2. Provide a means to update registered custom items through migrations.

The 0th migration just ensures that EVERY registered custom item has both a custom-item key and migration version, as stated explicitly in the comments. It establishes a baseline of what data can be accessed from the item without concern for NPEs. It also prevents stacking issues where one person's item has an explicit 0th migration-version set, and another whose 0th migration-version is implied by its absence (related: null != emptyList()).

The 0th migration is not strictly necessary but it does offer a benefit, "speculative" as that may be. That said, I maintain that the vast majority of this migration system would be utterly unnecessary if Okx simply agreed to permit ephemeral display data (ie, setting names and lore to items at network-time): this whole migration system exists to fix typos in lore, or to remove outdated parts of lore, etc. It's silly.

@Huskydog9988
Copy link
Copy Markdown
Contributor Author

To clean up the api, and make the usage clearer, I made a demo implementation Huskydog9988#1

@Huskydog9988 Huskydog9988 requested a review from okx-code May 16, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

3 participants