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

Data Attachment API #3476

Merged
merged 43 commits into from
Jan 19, 2024
Merged

Data Attachment API #3476

merged 43 commits into from
Jan 19, 2024

Conversation

Syst3ms
Copy link
Contributor

@Syst3ms Syst3ms commented Dec 19, 2023

Trying to revive #2172, with some ideas from @Technici4n's NF implementation.

The Data Attachment API provides a unified and convenient way for modders to attach arbitrary pieces of data to various game objects (as of writing: entities, block entities, worlds and chunks). Importantly, all of the logic specific to each game object type is abstracted away behind an interface and is not the modder's concern.

Additionally, attached data can optionally be made to persist across server restarts. A subsequent PR should work to implement a robust and flexible client-server syncing mechanism, staying true to the API's "target type should not matter in the API" philosophy.

API Overview

The API contains three classes: AttachmentType,AttachmentRegistry and AttachmentTarget.

An AttachmentType<A> is a type of way to attach objects of type A to game objects. It specifies whether the attachments are persistent and/or synced, and if one of these is enabled, provides a Codec<A> for convenient encoding/decoding. The API assumes the objects of type A are immutable or, more generally, do not share any state with other objects used in separate attachments.

Attachment types are created and registered using AttachmentRegistry. This class provides simple create, createDefaulted and createPersistent methods for quick creation and registration of attachments, but when finer control is needed, provides an AttachmentRegistry.Builder to set each desired property separately.

Finally, AttachmentTarget defines the game objects to which data can be attached. Currently, Entity, BlockEntity, ServerWorld and WorldChunk are made to implement it using interface injection and mixin. It provides methods for getting, setting, modifying and removing attachments, as well as hasAttached for check if an attachment exists.

Example use

Attachment types are created and registered using methods in AttachmentRegistry, and should be stored in static final fields, like so:

public class ModAttachmentTypes {
    public static final AttachmentType<String> INTERNAL_NAME = AttachmentRegistry.create(
        new Identifier("example:internal_name")
    ); // String attachment type, no defaults, not persisted, not synced
    public static final AttachmentType<Integer> MANA = AttachmentRegistry.createDefaulted(
        new Identifier("example:mana"), 
        () -> 0
    ); // Integer attachment with default value 0
    public static final AttachmentType<String> PERSISTENT_NAME = AttachmentRegistry.createPersistent(
        new Identifier("example:persistent_name"),
        Codec.STRING
    );  // String attachment, persists across server restarts
    public static final AttachmentType<Integer> PERSISTENT_MANA = AttachmentRegistry.<Integer>builder() // Builder for finer control
       .codec(Codec.INT) // required codec for persistence
       .persistent() // persistent
       .copyOnDeath() // will persist over entity death and respawn
       .initializer(() -> 0) // default value
       .buildAndRegister(new Identifier("example:persistent_mana"));
}

Once attachments have been registered, they can be used on all AttachmentTargets. In this example, we assume all of the relevant targets start with no attachments.

Entity entity = /* omitted */;
ServerPlayerEntity player = /* omitted */;

String internalName = entity.getAttached(ModAttachmentTypes.INTERNAL_NAME); // null
entity.setAttached(ModAttachmentTypes.INTERNAL_NAME, "silly entity");
internalName = entity.getAttached(ModAttachmentTypes.INTERNAL_NAME); // "silly entity"

boolean hasAttachment = player.hasAttached(ModAttachmentTypes.MANA); // false
int mana = player.getAttachedOrCreate(ModAttachmentTypes.MANA); // 0, auto-initialized !
hasAttachment = player.hasAttached(ModAttachmentTypes.MANA); // now true
player.modifyAttached(ModAttachmentTypes.MANA, mana -> mana+1); // increment mana

player.setAttached(ModAttachmentTypes.PERSISTENT_MANA, 10); // will be saved

Future enhancements

Client-server syncing would be a great feature to have, but has been left out of this PR on account of being a lot trickier than the rest, and not as essential. It should instead be the subject of a future PR. However, this feature is important because syncing rendering data with the client seems like a very obvious addition to this API. I'll outline a couple of the constraints any potential addition should try to fit into:

  • The syncing API should be target-agnostic, i.e not expose or involve too many mechanics related to the underlying target game objects, in line with the overall design of AttachmentTarget.
  • It should allow partial syncing, i.e give modders the ability to only sync a part of attachments with the client. This is useful when only a couple fields in a complex attached datatypes are useful for rendering, as to not send useless data.
  • It should give control over which clients the data is synced to. For instance, attached data relating to HUD rendering should only be synced with its target.
  • Optionally, it could allow incremental syncing, i.e try to sync only what has changed (imagine an attached Map, only the added/removed key-value pairs need to be synced). While I don't see a way to make this fully automatic, it could perhaps be helped with some helper interfaces for attached datatypes to implement.

Maintainer note: the unit tests test persistence for entities and blockentities as those can be mocked without too much effort, but mocking worlds and chunks is such a pain that I opted to make those into a separate testmod. It works by launching a server (on the same world) twice from the client to check the saving/loading.

TODO:

  • Javadoc
  • Tests
  • Implement server-client syncing Scrapped in favor of future PR

Sorry, something went wrong.

Copy link
Member

@modmuss50 modmuss50 left a comment

Choose a reason for hiding this comment

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

Looks like a reasonable start, it should be nice and easy to write some good unit tests for this, alongside a testmod.

- removed AttachmentSerializer in favor of codecs
- renamed serializability to "persistence"
- made persistence and syncability independent switches
- reworked convenience registry methods to use Suppliers from the get-go
@Syst3ms
Copy link
Contributor Author

Syst3ms commented Dec 20, 2023

We need to figure out a couple things regarding how the API of AttachmentTarget should look. There are two main paradigms with endless in-between delineations:

  1. All attachments have a mandatory initializer (usually in the form of a Supplier), and if no data is attached (i.e hasAttached returns false), then getAttached automatically calls the initializer and returns the result.
    • Pros:
      • Aligns with the implementation in Neoforge by @Technici4n
      • Avoids nullability
      • Many types (like primitives) have very obvious default values
    • Cons:
      • Requires a lot of hasAttached/getAttached boilerplate to deal with no attached data, e.g if another method expects a nullable parameter, one would probably have to use a ternary.
      • Many types have no good default/dummy value, and the auto-initializing behavior may be undesirable then.
  2. Attachments are not automatically initialized, and getAttached returns null if no data is attached.
    • Pros:
      • Avoids implicit initialization (whether that is a pro is arguable)
      • Avoids initializing complex types with no good dummy values
      • Akin to Map#get, which is a very familiar pattern
    • Cons:
      • Having to deal with nullable values
      • Would require a getOrCreate helper method, which can be annoying boilerplate in many cases.

Naturally, many of these cons can be solved or at least alleviated with suitable helper methods. In 1, a getAttachedOrNull can be provided to emulate 2's behavior at little extra cost. In 2, a getAttachedOrThrow method removes nullability concerns where they are uncalled for, but can also introduce hard failure (exception).

An in-between may be to provide partial/optional auto-initialization capability. For example, an optional initializer that, if given, is called when there is no attached data as in 1. Or, for a less intrusive alternative, an optional 'default' initializer that is used as the parameter of getOrCreate, for a more concise target.getOrCreate(attachment) callsite. But again, that can introduce exceptions. To remedy this and also potentially other issues, it is possible to introduce subinterfaces of Attachment to prevent misuse through the type system, but this may be gross overengineering.

In any case, the choice should be guided by which paradigm will cause the least boilerplate in the majority of cases. Whatever behavior getAttached ends up having, it should be what's likely to be most commonly used. Any additional insight on the matter will be helpful here. @modmuss50 @maityyy

@modmuss50 modmuss50 added the enhancement New feature or request label Dec 21, 2023
@Syst3ms
Copy link
Contributor Author

Syst3ms commented Dec 21, 2023

After some discussion, I have implemented the following compromise:

By default, AttachmentType does not automatically initialize attachments on first query. However, various factory methods allow the user to create a DefaultedAttachmentType<A>, which comes with an initializer to provide a default (non-null) value. Accordingly, AttachmentTarget has a new, non-nullable getAttached overload taking in a DefaultedAttachmentType that will automatically initialize non-existing attachments.

As a result, whether the attachment automatically initializes or not, the call site looks like target.getAttached(TYPE). To prevent users from calling the wrong getAttached (for example due to a not-specific-enough type when storing the registered type, which the javadoc will warn against doing), a runtime check is performed along with a warning. This should prevent improper use of the system.

@maityyy
Copy link
Contributor

maityyy commented Dec 22, 2023

What if I registered my AttachmentType with an initializer but my field is of type AttachmentType instead of DefaultedAttachmentType? Then all my calls will escape initialization (because at compile time it will be getAttached(AttachmentType) instead of getAttached(DefaultedAttachmentType)) when there is no value and I will most likely get an NPE (you also pointed this out in the docs). I don't like the idea of sub-interfaces, it's very non-obvious and difficult to expand in the future.

I can suggest the following:

  1. Rename getAttached(DefaultedAttachmentType) to be different from getAttached(AttachmentType) — then modders will likely realize they are calling the wrong method
  2. Get rid of DefaultedAttachmentType and its getAttached overload, call the initializer if it is not null in the only remaining getAttached every time (like fastutil's maps)
  3. The same thing, but in a separate method called getAttachedOrDefault
  • 2 & 3 will always have the @Nullable annotation

For now I prefer option 2, but it's not ideal, so I'd like to think more...

@Syst3ms
Copy link
Contributor Author

Syst3ms commented Dec 22, 2023

I mentioned this potential pitfall, and if you check, the (normal) getAttached implementation issues a warning and auto-inits if you pass a defaulted one.

But you're right about bad extensibility, the cursory amount of thought I've given syncing tells me that is quickly going to be unsustainable, so back to the drawing board.

That being said, the issue with 2 is that static checkers will complain about nullability even for defaulted attachments (could be solved with a getAttachedOrThrow).

The problem with 3 is overarching, and it's that I'm unsure how common defaulted will be vs non-defaulted, and I don't want to have a longer method name be used for the most common case.

@Syst3ms Syst3ms marked this pull request as ready for review December 23, 2023 22:22
@modmuss50 modmuss50 added the merge me please Pull requests that are ready to merge label Jan 5, 2024
@apple502j apple502j removed the merge me please Pull requests that are ready to merge label Jan 6, 2024
Copy link
Contributor

@apple502j apple502j left a comment

Choose a reason for hiding this comment

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

Glad this wasn't merged yet. Sorry for late review.

@Syst3ms Syst3ms mentioned this pull request Jan 6, 2024
@Syst3ms Syst3ms requested a review from apple502j January 6, 2024 12:58
@modmuss50 modmuss50 requested a review from apple502j January 9, 2024 13:03
@Syst3ms
Copy link
Contributor Author

Syst3ms commented Jan 15, 2024

Two notes:

  • It was brought to my attention that on respawn, client players are recreated and copied, but not through any of handled copy methods, so a mixin was added in c37c705 to take care of it. Unlike general mob conversion, this is completely covered by the mixin. Nevertheless, is it worth adding to the event API?
  • Currently, all mob conversions are treated as deaths for attachment copy, which makes use of the copyOnDeath flag. This makes sense for most conversions, but what about tadpoles growing up? Answer: we'll just treat it the same.

@modmuss50 modmuss50 added the merge me please Pull requests that are ready to merge label Jan 18, 2024
@modmuss50 modmuss50 merged commit 25e1b47 into FabricMC:1.20.4 Jan 19, 2024
modmuss50 pushed a commit that referenced this pull request Jan 19, 2024
* Data Attachment API

* javadoc

* Remove AttachmentSerializer & independent syncability and persistence

- removed AttachmentSerializer in favor of codecs
- renamed serializability to "persistence"
- made persistence and syncability independent switches
- reworked convenience registry methods to use Suppliers from the get-go

* Move some serialization-related methods to impl

- changed logger name

* rename Attachment to AttachmentType

* Added DefaultedAttachmentType and reworded Javadoc

* add warning in getAttached

* javadoc

* fix defaulted API

* Add unit tests

* remove DefaultedAttachmentType, add helper methods

bikeshedding inbound

* add more unit tests

* add testmod

it works

* stash syncing for a further PR

* missed license header

* address most reviews

* more reviews

* naming convention

* fix tyop

* fix invalid file name error

* simplify API in the absence of sync

It was established that the presence of a codec might be useful for other things than persistence, and while this seems to couple the two, the API can be changed later in a backward-compatible way if need be.

* couple codec with persistence

committing to the change I mentioned previously

* little fixes

* Fix mixins + requests

- Copy attachments on entity copy,& with a customizable flag in the case of player respawn
- Call relevant change-notifying methods when calling setAttached on BEs and chunks
- Change persistence key
- Fix mixin visibility

* Write tests for entity copy

* replace mixin by COPY_FROM event

* missed license header

* more advanced copy mechanics

- attachments require an EntityCopyHandler to be copied across entities
- a copy handler is automatically derived if there's a codec
- updated javadoc for chunk and BE targets

* Revert "more advanced copy mechanics"

This reverts commit 3f53b55.

* replaced complicated API by a stern warning

- also handled cross-world entity teleportation

* add gametest

* fix compilation

* flipped boolean

* forgot some more bools to flip

* requests

* fix FMJ

* fix BE mixin and add gametest

* add client player entity copying

* Use new mob conversion event

---------

Co-authored-by: modmuss <modmuss50@gmail.com>

(cherry picked from commit 25e1b47)
@mrjasonn mrjasonn mentioned this pull request Jan 19, 2024
7 tasks
Syst3ms added a commit to Syst3ms/fabric that referenced this pull request Feb 10, 2024
modmuss50 pushed a commit to Syst3ms/fabric that referenced this pull request Feb 13, 2024
modmuss50 pushed a commit that referenced this pull request Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request last call If you care, make yourself heard right away! merge me please Pull requests that are ready to merge new module Pull requests that introduce new modules
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

7 participants