Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.21] Handler Rework #1115

Draft
wants to merge 21 commits into
base: 1.21.x
Choose a base branch
from

Conversation

CodexAdrian
Copy link

Handler Rework

This PR is just a continuation of #944 which had some issues

Motivation

NeoForge's storage situation is not great. ItemHandler and FluidHandler are both extremely helpful to devs trying to create a unified standard for storage of resources, but aren't consistent with each other and rely on the usage of mutable resources with amounts attached for transfer. This creates situations where devs have to return copies of their resources in order to safely ensure a dev isnt trying to manipulate their storage directly, or copy the stack passed into the insert method before its inserted in order to ensure the implementor of insert method doesn't attempt to modify the stack when its inserted. Not to mention the inconvenient issues that arise when the resource (item + components) are joined in the same object where so much copying and mutation needs to occur to the stack when most of the time the developer just wants to change the amount the stack contains or insert the same stack into a container with a different amount.

The ideal solution to these problems would be creating a single unified storage class (for consistency) that uses immutable resources (for the mutation problems) whose count is separated from the resource (for the inconvenience problems).

Implementation

Much of the implementation was inspired by FabricMC/fabric#1356 and how it was subsequently built out in the later years, and is a continuation of Tech's initial PR to introduce immutable resources in #715

IResourceHandler

IResourceHandler is a new interface that will eventually fully replace usages of IItemHandler and IFluidHandler. It is a slotted container that stores an integer amount of a resource of type T. This type doesn't necessarily have to be an IResource class, but utilities will be provided for storage classes that do. In this implementation, T should be immutable and should be countless, as the amount is retrieved in a separate getAmount(slot) method.

IResource and FluidResource/ItemResource

IResource is the basis for the Neo provided storage capabilities. Its an immutable data component holder with a utility methods that return a new resource with modified components without mutating the resource to make it easy to interact with. Lastly, it contains an isBlank method to indicate whether or not the resource should be considered blank or empty, like if the resource is of Items.AIR or Fluids.EMPTY. The default resources provided by Neo are FluidResource and ItemResource and represent immutable types of a fluid/item and data components.

ResourceStack

ResourceStack is a utility class that contains an instance of IResource and an integer amount. It provides the same utility methods IResource methods provides for copying with modifications while also providing similar utilities for amount based copying. ResourceStack should largely replace the usage of ItemStack or FluidStack in the usage of code.

IItemContext

ItemContext is a new interface for general usage when interacting with inventories. Its centered around a main slot, whose contents IItemContext can be used to get its current state (resource and amount). This context also allows devs to manipulate the ItemResource and amount in the main slot, by extracting, inserting or exchanging items into it. Extracting will extract items of a given ItemResource and amount from the slot, returning how much was extracted from the main slot. Inserting into the context will not only insert into the main slot, but is also intended to insert whatever does not fit into the main slot into some kind of outer container. This can be an outer container like a buffer or a context could choose to drop the excess onto the ground, its up to the implementor of IItemContext to say where items go. Exchanging allows devs to extract however many items that are requested to be exchanged and then inserting a new resource in its place up to the amount extracted. Devs should also dump excess items that were not successfully inserted into the main slot into some kind of overflow as well (if you choose to support having an overflow for your items).

This new class largely replaces the need for any item specific capabilities, like IFluidHandlerItem, while also providing devs with much needed utility to insert excess into a slot other than itself. This should make it possible to, for example, have a stacked fluid container where only 1 of 16 items are drained. The fluid storage can simply call exchange with 1 empty storage container, without needing to worry about whether or not the stack is of stack size 1.

ItemContext also contains a helpful utility which you can use to retrieve the capability found on item in the main slot without needing to pass in an ItemStack

The Fallen

This PR also sets IItemHandler and IFluidHandler for deprecation and marked them for removal in 1.22. Currently these classes have been retrofit to extend IResourceHandler<ItemResource> and IResourceHandler<FluidResource> respectively, and will eventually be phased out entirely for just using the base storage classes. While consideration to keeping some of the old API around has been made, this PR does still make a breaking change in the removal of IFluidHandlerItem, and the modification of existing Item capabilities to have the aforementioned IItemContext as its new context class (which also replaces the need of having the getContainer method in IFluidHandlerItem).

Interaction changes

With immutable resources and IResourceHandler<ItemResource> and IItemContext, its safe to say that modifying the ItemStack in which a capability was retrieved from will not always achieve the desired effect. As such, developers should now take care to use ItemContext to make any modifications needed to the main stack using the methods provided in ItemContext, even if those changes are just component or size related changes. Developers should treat that ItemStack given to them in the capability provider as read only, and should also take care to note it is only accurate at the time of creation of the cap class, and may not be updated to reflect changes made with IItemContext. Devs should use the resource and amount provided in IItemContext as an accurate image of what the item in its slot currently looks like

Notes

This was a doozy to write, and might not be a complete overlook of this PR when it is time to merge it. I'll do my best to update the description as needed. This PR is (as of May 8th, 2024) not complete, but was opened so others could see what the changes I was making were and could comment on them while in dev.

@neoforged-pr-publishing
Copy link

  • Publish PR to GitHub Packages

*
* <p>Examples include item resource with air as an item.
*/
boolean isBlank(); // TODO: potentially use a different name
Copy link
Contributor

Choose a reason for hiding this comment

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

isAbsent() perhaps to match an existing precedent set by Optional.

Copy link
Contributor

Choose a reason for hiding this comment

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

isBlank better than isAbsent

Copy link
Contributor

Choose a reason for hiding this comment

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

isEmpty is better than both but Tech vetoed it

Copy link
Contributor

@Soaryn Soaryn Jun 20, 2024

Choose a reason for hiding this comment

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

I disagree isBlank is better, but at this point I say isEmpty is the more correct choice. Looking at every data structure I can find isEmpty is exactly what the intent is. (I was incorrect on isAbsent being an optional method).

I don't think Tech should be given reign to "veto" a more correct term really.

Copy link
Member

Choose a reason for hiding this comment

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

The reasoning is that a resource being blank doesn't necessarily correspond to a slot or stack being empty.

Copy link
Contributor

Choose a reason for hiding this comment

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

It isn't a matter of if the stack is empty, is if the resource is empty. Which in the case that it matches air, it is

Copy link
Contributor

@Soaryn Soaryn Jun 20, 2024

Choose a reason for hiding this comment

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

Optional<ItemStack>.isEmpty() isn't asking if the itemstack is empty for example, it is asking if the optional contains an itemstack

return innerStack.getFluidType().isVaporizedOnPlacement(level, pos, innerStack);
}

public void onVaporize(Player player, Level level, BlockPos pos) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So I understand these are to not bypass immutability of fluidstack, but I feel the use cases for each call is going to be a nightmare to support. getTemperature or viscosity for example would be things I'd want, but then need the API to expose.

Copy link
Author

Choose a reason for hiding this comment

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

yeah thats a valid concern. i added those there because they were methods that came up while i was in the middle of implementing some of the other default handlers in the rewrite. They may not be the best approach tho. Its not that expensive to just make a new fluid stack, so i guess that would be the solution, just do .toStack() and call the methods on that. in my ideal world the fluid resource would have all the same methods for info that the fluid stack did, but that might be too much for this PR

* A handler for placing and picking up fluid blocks in the world.
*/
public class BlockFluidHandler implements ISingleResourceHandler<FluidResource> {
private static final Logger LOGGER = LogManager.getLogger();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the right place for this? I would assume NeoForgeMod.LOGGER would be the correct one to use, but if the standard is to have a unique one here, then disregard :)

*
* @param <T> The type of resource
*/
public class EmptyHandler<T extends IResource> implements ISingleResourceHandler<T> {
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason we don't want to make this final?

@Override
public T getResource(int index) {
for (IResourceHandler<T> storage : getHandlers()) {
if (index < storage.size()) {
Copy link
Member

Choose a reason for hiding this comment

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

We probably only want to calculate storage.size() once here rather than doing it both here and when adjusting the index. Just in case, similar to this class' size implementation it isn't just a simple getter. On top of that we may want a couple private helper methods to simplify iterating over this and finding the correct index.

protected final T emptyResource;
protected final ResourceStack<T> emptyStack;

protected Predicate<T> validator = r -> true;
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer if this was final, and we just had a constructor overload that accepted the validator


@Override
public int getCapacity() {
return singleItemLimit * context.getAmount();
Copy link
Member

Choose a reason for hiding this comment

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

Is there a worry that this can overflow?

public int extract(int index, T resource, int amount, TransferAction action) {
for (IResourceHandler<T> storage : getHandlers()) {
if (index < storage.size()) {
if (storage.allowsExtraction()) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to check this? I feel like we should be able to just assume that the inner storage will short circuit if they aren't allowing extraction. That way if their check is for some reason not trivial, then it only has to be done once.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it is never necessary to check allowsXxx before doing xxx (which is supposed to check it anyway). Perhaps that should be added to the javadoc.

public int insert(int index, T resource, int amount, TransferAction action) {
for (IResourceHandler<T> storage : getHandlers()) {
if (index < storage.size()) {
if (storage.allowsInsertion()) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to check this? I feel like we should be able to just assume that the inner storage will short circuit if they aren't allowing insertion. That way if their check is for some reason not trivial, then it only has to be done once.

import javax.annotation.Nullable;
import java.util.function.Predicate;

public class HandlerUtil {
Copy link
Member

Choose a reason for hiding this comment

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

Helper classes should be final and have a private constructor.

* while {@link TransferAction#EXECUTE} will actually perform the action.
* @return The amount of the resource that was (or would have been, if simulated) inserted.
*/
int insert(T resource, int amount, TransferAction action);
Copy link
Member

Choose a reason for hiding this comment

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

We probably want to give this function a default implementation.

* while {@link TransferAction#EXECUTE} will actually perform the action.
* @return The amount of the resource that was (or would have been, if simulated) extracted.
*/
int extract(T resource, int amount, TransferAction action);
Copy link
Member

Choose a reason for hiding this comment

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

We probably want to give this function a default implementation.

Comment on lines +88 to +96
int getCapacity(int index, T resource);

/**
* Gets the theoretical maximum amount that the given index can hold of a resource.
*
* @param index The index to get the limit from.
* @return The limit of the resource at the given index.
*/
int getCapacity(int index);
Copy link
Member

Choose a reason for hiding this comment

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

Having both was not done before. To be discussed.

Comment on lines +118 to +129
boolean allowsInsertion(int index);

/**
* Checks if the given index allows extraction of a resource, regardless of the state of the handler.
* <p>
* As long as the handler could, under the right conditions, allow a resource to be extracted from the given index,
* this should return true.
*
* @param index The index to check.
* @return True if the resource can be extracted, false otherwise.
*/
boolean allowsExtraction(int index);
Copy link
Member

Choose a reason for hiding this comment

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

It might be worth discussing whether these methods should exist at all, since they didn't exist before?

*
* @param <T> The type of resource
*/
public class EmptyHandler<T extends IResource> implements ISingleResourceHandler<T> {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't the empty handler have 0 slots?

@Override
public int insert(int index, T resource, int amount, TransferAction action) {
if (amount <= 0 || resource.isEmpty()) return 0;
var contents = getContents().builder();
Copy link
Member

Choose a reason for hiding this comment

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

This allocation is not great I think. Transfer should be allocation-free in most cases.

*
* @param <T> The type of resource that this storage can accept.
*/
public class VoidHandler<T extends IResource> implements ISingleResourceHandler<T> {
Copy link
Member

Choose a reason for hiding this comment

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

I'd maybe call this Voiding.

import net.neoforged.neoforge.transfer.handlers.IResourceHandler;
import net.neoforged.neoforge.transfer.handlers.ResourceStorageContents;

public abstract class ResourceStorage<T extends IResource> implements IResourceHandler<T> {
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure that this abstraction is worth it. I'll have to look further into it.

return delegate.get();
}

public static class Modifiable<T extends IResource> extends DelegatingHandlerWrapper<T> implements IResourceHandlerModifiable<T> {
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to have this pattern for many helper classes? It might get a bit unwieldy.

@rubensworks
Copy link
Contributor

I just ran into this PR. Without having looked into it in detail, feel free to take inspiration from the Ingredient Components API that I've been working with for several years now: https://github.com/CyclopsMC/CommonCapabilitiesAPI/wiki/Ingredient-Components
It forms the basis for all ingredient interaction in Integrated Dynamics (storage inspection and efficient indexing), Integrated Tunnels (transfer), and Integrated Crafting (autocrafting).

@neoforged-automation
Copy link

@CodexAdrian, this pull request has conflicts, please resolve them for this PR to move forward.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ CodexAdrian
❌ Adrian O.V


Adrian O.V seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@neoforged-automation
Copy link

@CodexAdrian, this pull request has conflicts, please resolve them for this PR to move forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants