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

Fabric Component Access #1067

Closed
wants to merge 14 commits into from
Closed

Conversation

grondag
Copy link
Contributor

@grondag grondag commented Sep 9, 2020

Component Provider

A component provider is a functional role - there is no ComponentProvider interface or class. Component providers are game objects (currently blocks, items or entities) associated with one or more "Component Type" instances. Components are also conceptual and can be of any type - there is no Component interface.

Many readers will note some resemblance here to entity attribute frameworks like Cardinal Components or the old capabilities system in Forge. The resemblance is only approximate: this API facilitates access to components but handles no aspects of implementation or persistence. It solves the problem of "how do I get the thing from the other thing when I don't know how the first thing is implemented in the other thing" problem.

This API is in no way a general-purpose entity attribute library, nor is it meant to replace one. The intent here is to decouple component access from component implementation, and to do so without adding or requiring external dependences. It should be complimentary to CCA, LBA or similar frameworks, and also works just as well when components are implemented directly as part of a BlockEntity, Entity, etc.

Registering Component Types

Creating a new component type is straightforward:

ComponentType<Thing> THING_COMPONENT = ComponentRegistry.INSTANCE.createComponent(new Identifier(ExampleMod.MOD_ID, "thing"), EMPTY_THING);

Note that two components can share the same type. This may be useful when the same component type could have a different purpose or meaning for the same provider.

ComponentType<Thing> OTHER_THING_COMPONENT = ComponentRegistry.INSTANCE.createComponent(new Identifier(ExampleMod.MOD_ID, "other_thing"), EMPTY_THING);

The second parameter to ComponentRegistry.createComponent() is the value that should be returned when the component is absent, and made visible via ComponentType.absent(). Avoid using null here - cleaner code results when the absent value is a no-effect dummy instance.

Using Components

Obtaining a component instance is a two-step process:

  1. Call a variation of ComponentType.getAccess() to retrieve a ComponentAccess instance. Currently there are methods to get access from blocks (or block positions within a world), entities, and from Items, which may or may not be held by a player. Future versions may add access methods for other game objects if there are interesting use cases for them.

  2. Use a variant of ComponentType.get() to get the actual component instance, or the absent value if the component is unavailable.

ComponentType.get() accepts two parameters, both of which can be omitted:

  • Direction For components that are accessible via a specific side. Pass null (or call a get() variant without this argument) for components that have no side or to get the non-specific instance. Component types that do not have sides should ignore this parameter and access attempts from a specific side should always provide it, unless it is somehow known to be unnecessary.

  • Identifier Components may optionally have named instances and accept access requests for a specific named instance. This may be useful, for example, with machines that have more than one input/output buffer that may be accessible from the same side. Such a machine could return a different instance depending on which named buffer was requested. Use of this feature is optional and implementation-specific; This API currently makes no attempt to standardize these identifiers or their meanings.

Often, access to a component is for a single use. In these cases, ComponentType.acceptIfPresent() can simplify code. It works like get() but also accepts a Consumer for the component type and if it finds a non-absent component instance it applies the consumer and returns true. It returns false when the component is absent.

Similar convenience is offered by ComponentType.applyIfPresent(). It accepts a Function over the component type, and returns the result of that function if a non-absent component instance is found, or null otherwise.

Providing Components

The API can only return component instances that have been mapped via ComponentType.registerProvider(), like so:

THING_COMPONENT.registerProvider(ctx -> ((ThingBlockEntity) ctx.blockEntity()).getThing(), THING_BLOCK_ENTITY);

While access to components requires two steps, provisioning is handled by a single function. All of the information about the world/block/item/entity/side/id is marshaled into a BlockComponentContext, EntityComponentContext or ItemComponentContext instance that the function consumes.

Most block implementations will use a BlockEntity as their component holder/provider, but this is not required. The provider function must map the context data to a component instance. How that happens is unspecified. The BlockEntity value in the context can be null, but World and BlockPos values will always be present.

Item Actions

The API includes an opt-in system for adding behaviors to items that to perform some action when used on blocks that contain or are associated with a component.

ComponentType.registerAction associates a potential action with a component type and one or more items. The first argument is a BiPredicate that accepts an ItemComponentContext and a component instance. The intent is that multiple consumers can be registered for the same item/component, and processing will stop after any consumer returns true. Order of execution is unspecified, but this should not matter much in practice - the player can only be holding one item and clicking on one block at a time.

Note these actions have no effect unless mods invoke the action handler in a block's onUse method (or wherever is appropriate) using Component.applyActions or Component.applyActionsWithHeld. This should only be done server-side.

@i509VCB i509VCB added enhancement New feature or request new module Pull requests that introduce new modules reviews needed This PR needs more reviews labels Sep 9, 2020
Copy link
Contributor

@i509VCB i509VCB left a comment

Choose a reason for hiding this comment

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

Good start, there is a lot we still need to cover to get this ready.

There is a lot of useful documentation in the PR description that could probably work well as code examples in javadoc to guide experienced users who are approaching this system having generally never used it before.

*
* @return The player holding the item in which the component resides
*/
/* @Nullable */ ServerPlayerEntity player();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason this couldn't just be an Entity?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm, you mean like if a Zombie is holding an item?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yea

Copy link
Contributor Author

@grondag grondag Sep 9, 2020

Choose a reason for hiding this comment

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

The player version is specifically for items held by a player. For a Zombie, you'd provide a StackGetter/StackSetter functions that know which zombie (or other entity) and how to access the item stack in it.

We could add more specific handlers for mob-held items, items in an item frame, items floating in the world, etc, but where to stop?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the concept of equipment is introduced to entities at around living entity, so that should be a fine place to go down to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll need to think about it, but we could probably expand Player to be Living Entity. Will still need the variant that accept a null Entity for things like ItemFrame items, ItemEntity, etc.

@i509VCB i509VCB requested a review from a team September 9, 2020 05:21
…ic/impl/component/access/ComponentTypeImpl.java

Co-authored-by: i509VCB <i509vcb@gmail.com>
@i509VCB
Copy link
Contributor

i509VCB commented Sep 9, 2020

Hmm, thought about this a bit, how much work would it be to make a custom ComponentAccess refer to a unique source such as the abyss for example. I would like to consider working in a way where that would be somewhat possible.

@grondag
Copy link
Contributor Author

grondag commented Sep 9, 2020

Hmm, thought about this a bit, how much work would it be to make a custom ComponentAccess refer to a unique source such as the abyss for example. I would like to consider working in a way where that would be somewhat possible.

I don't understand what this means. Could you elaborate?

*
* @return Position of the block component within the world.
*/
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.

I'm fine with both, but I'm guessing get prefixes are more common throughout FAPI.

private final Function<ItemComponentContext, ?> defaultItemMapping;
private final Function<EntityComponentContext, ?> defaultEntityMapping;

private final IdentityHashMap<Block, Function<BlockComponentContext, ?>> blockMappings = new IdentityHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

IdentityHashMap does not technically fulfill the Map contract, so I'd say it's fine to declare it differently.

* @param mapping mapping function that derives a component instance from an access context
* @param predicate Mapping will apply to all entity types that match this test
*/
void registerEntityProvider(Function<EntityComponentContext, T> mapping, Predicate<EntityType<?>> predicate);
Copy link
Contributor

Choose a reason for hiding this comment

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

This will not work. EntityTypes do not have a reference to a Class (much like ComponentType right now), so mods cannot test whether the entities of this type implement something or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had not thought to interrogate the entity classes here, but I can see how that would be useful for inferring the presence of an interface. Would probably be better to accept both EntityType and the Entity class. I don't think passing an Entity instance is desirable.

Would that be acceptable?

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, I believe Entity class is all that's needed for most purposes.

@SuppressWarnings("unchecked")
@Override
public <E extends Entity> ComponentAccess<T> getAccess(E entity) {
applyDeferredEntityRegistrations();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bit fragile, mods could create dummy entities during init.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to confirm, you mean they may call this during init with a transient Entity instance before registration is final. Is that right?

Does Fabric yet have an event or other supported means to defer this until registries are final? I'd have to look.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's indeed what I thought about. I don't know if there is a better way that doesn't involve entrypoints though.

@sfPlayer1
Copy link
Contributor

I think it is too early to jump into the review, there are some fundamental design aspects to be discussed.

For me the problem space (not the particular PR) may have the following features:

  1. Adding API to foreign game object types (or own types that can't be directly modified due to dependency or design constraints)
  2. Getting to that API externally
  3. Allowing the API to be uniform across unrelated game object types
  4. Binding the API to specific game objects
  5. Retrieving and Storing data from/to game objects
  6. Persisting and/or syncing that data

This PR approximately handles 1.-4. The features 5-6 have no business being directly coupled to it, however it needs to be possible to link them for efficiency reasons (store the bound API object with the data -> save some steps).

3 and 4 are very expensive and needlessly mandatory. 3 in particular is quite weirdly implemented in that Direction has been integrated as an universal concept, while it is only applicable to blocks. It also assumes the Direction and a random Identifier are universally suitable to retrieve the fully bound API access.

I believe this is better done by splitting 1+2 from 3+4, making the latter optional and fully customizable.

1 can be some sort of type to (usually) singleton binding like ApiRegistry.register(SomeBlock.class, new MyApiImpl()), ApiRegistry.register(someBlockInstance, new MyApiImpl()), ApiRegistry.register(new ID("mymod:someblock"), new MyApiImpl())[1] or just implementing the API directly (SomeBlock implements MyApi).

2 would be ApiProvider<MyApi, Block> provider = ApiRegistry.getProvider(MyApi.class, Block.class); MyApi api = provider.get(someBlock)[2]

For 3+4 I'd at most offer a couple free-standing base classes similar to your *ContextImpl that store the relevant interaction parameters and offer the actual API via a sub class[3]. It'd be easy to instead use a custom class that asks for more appropriate application specific context. In the above example the uses of MyApi become MyApiProvider, offering a method MyApi get(World world, BlockPos pos, Direction side) { return new BlockContext(world, pos, side) implements MyApi { void doSomething() {..}}}[4]. This new MyApi can then satisfy 3 and/or 4. Whether it is feasible to provide a reasonable helper/basis for the MyApiProvider implementation, potentially including the registration for 1., is tbd.

I'm still unsure whether the use of such unifying wrappers should be encouraged due to the overhead. APIs can usually work around the lack of unification by providing overloads, most of the time the caller is in a specific context already and for generic APIs the other side uses some prefabricated implementation.

I did not understand at all what those "Actions" that can somehow be applied are supposed to be. The various acceptIfPresent and applyIfPresent overloads are IMO rather pointless. Not only are they very likely to allocate via capturing lambdas and boxing, but also pollute the API and don't fit that nicely in the call sites I remember.

[1] Using instances of Block or Identifiers there is closer of "types" in the logical sense, not Java types. The Java types (classes) overloads register to all instances.

[2] ApiProvider is just for faster lookup and type safety.

[3] AFAIK inheritance is the only option that is both as efficient as possible and offers a clean API. There is nothing preventing other choices though, like yielding a context object to pass to methods in a separate class or allocating a separate object.

[3] new BlockContext(..) {} would obviously have to be a regular or nested class, anonymous classes can't implement extra interfaces.

@grondag grondag mentioned this pull request Sep 11, 2020
@grondag
Copy link
Contributor Author

grondag commented Sep 11, 2020

Replaced by #1072

Closing this because the changes are too extensive for pleasant reviewing.

@grondag grondag closed this Sep 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request new module Pull requests that introduce new modules reviews needed This PR needs more reviews
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants