quilt-standard-libraries
quilt-standard-libraries copied to clipboard
Component API Implementation
After 2 weeks of unhealthy head-banging I have finally finished this!
This API adds:
- A new module to the data library, called 'component'.
- A new class on the qsl_base module, called Maybe. It represents a monad that either holds a value or doesn't.
A bit of an explanation:
This module allows you attach data, functions, ticking functions to game objects. Right now the following objects are implemented:
- Block Entities
- Entities
- Chunks
- ~~ItemStacks~~(Removed from the Initial Release)
- LevelProperties
The data can be serialized using NBT and can also be network synced(although that feature is a bit unstable at the moment).
Component Injection
During the mod loading stage, mods are able to inject into difference game objects, provided those objects implement the ComponentProvider interface. Those injections are cached using the target class/classes and are then able to be quickly looked up.
But what about the case where cached injections don't suffice? one may ask.
Well that's also covered using dynamic injection.
Along with the class that you are targeting, you can also provide a predicate that will match the type of the class.
That predicate is then used when a provider is initialized to make sure it matches and/or can be injected into the target.
Proceed with caution: The predicate is checked, at the end of the constructor of the highest class in the hierarchy. This means that only the properties initialized up to that point will be available for querying!.
While that may not seem like a problem for things like ItemStacks, which generally aren't extended, Entities for example have a wide variety of classes with different fields. However, when injecting dynamically, one will only have access to the Entity class fields as well as the type information of the object.
Custom implementations
I have made the API really flexible, so that anyone can just create custom implementations of most provided objects. Component containers, component providers, components, sync headers, all of those can be extended and tweaked as per need.
Network sync and data saving
Data is currently saved using NBT and a save call that is provided from the provider to its container. Data is currently synced using a system I have implemented. Needless to say, this system needs some tweaking as well as a lot of testing to make sure that:
- No data is lost through the network.
- ~~There are no runtime crashes caused by the system.~~
- ~~Components are properly synced even in edge cases.~~
- ~~Components are synced when the object is first loaded.~~
I think that after all that is done, this will be able to be released.
Finally
Feel free to ask any questions about the implementation and provide feedback wherever possible. This project was a massive undertaking for one person and that is why I decided to PR it now that it's reaching its final stages. The codebase is big and documentation is needed, I understand that. However I still think I needed to share it with everyone. Thank you for your time!
newlines!
Is there an issue with leaving a newline after the class declaration?
Consistency, mostly - none of the current QSL classes have a newline after the class declaration (and heck, even some of the classes in this PR don't have a newline).
Okay will do then. Time to mess with IJ settings!
I think there is a way to include code style settings in the repo? well, at least for IDEA users
We do have an .editorconfig
file, though it doesn't contain any IJ-specific settings.
I see
I'm concerned about firing the event for attaching ItemStack components from the ItemStack constructor.
Minecraft Forge does something similar with its capabilities, and it leads to
new ItemStack
and things that call it such ascopy
andsplit
being giant performance hazards, as suddenly this fires off an event.
How would you support dynamic injections in this case though? Technically speaking, you could do a cached injection and only expose the component if the required criteria is met.
We could redirect the calls in copy and split though. Hm interesting...
Quilt component API 👀 👀 👀
Right now, there's not really anything to get components off of non-BE blocks/blockstates. This is, of course, a bit tough because blockstates aren't quite instanced in the same way BEs or item stacks are, but I feel like it is important to provide, given that stuff like composters or redstone in vanilla set a convention that you should be able to make something simple without having to use a full block entity. Using a BlockComponentProvider
interface with access to a full world/pos/state would work, but that makes injection either impossible or very unperformant. I might look into making a lookup-like system for this at some point, unless someone else has a better idea.
@LemmaEOF That is an interesting idea. However, I'd love to hear how you are planning storing the components. Or are they gonna be static components? (In case you haven't seen the code, static components are basically component singletons)
I actually mark stuff with NotNull for the kotlin devs out there. The kotlin compiler is able to parse NotNull and allow the kotlin side to just use the normal call operator(.) and not the safe-call one(?.).
I actually mark stuff with NotNull for the kotlin devs out there. The kotlin compiler is able to parse NotNull and allow the kotlin side to just use the normal call operator(.) and not the safe-call one(?.).
Might need to talk of this with the Kotlin team then.
I have not looked at this in too much detail yet, but I would highly discourage attaching a ComponentProvider to item stacks. The ItemStack module was removed in Cardinal Components API because it caused a lot of issues with other mods dealing with ItemStack serialization, had poor performance, required a lot of special casing, and was generally unneeded after the API Lookup API became a thing.
I would highly discourage attaching a ComponentProvider to item stacks
I can understand why that would be an issue. Honestly I run into multiple problems while creating the itemstack implementation of the ComponentProvider interface. ItemStacks are a weird bunch and they are instantiated quite often. Might have to do some intense thinking about this.
However I must also say that I'd like to personally experiment with ItemStacks, because they are weird. Who knows lazy instantiation might be a life saver. Or some other performance improvement down the line. We can call the itemstack implementation experimental even when network sync is fully tested. I want ItemStacks to work and to be honest even if they don't I'd like to see them fail myself when testing with them.
Gonna put this here, if something starts blocking out this PR we always can delay said implementation to a later PR so we can do more thinking on it ;)
Sometimes incremental implementations are easier to manage.
'said implementation' aka ItemStack, am I correct in that?
'said implementation' aka ItemStack, am I correct in that?
yup, you don't have to get everything at once (and sometimes incremental implementation is easier to review too ahah :p)
If we are talking safety, then Entity and BlockEntity could literally be released right now. Chunks needs a bit work, to do with initial syncing. LevelProperties aka 'Level' needs a way to encode itself safely. ItemStacks are technically not synced through network on our part. That is because we handle ItemStack component nbt under the normal 'nbt' fields in their class. Since that field is synced through PacketByteBuf#writeItemStack we needn't worry about network sync. Issues like frequent instantiation are what are holding ItemStacks back.
yup, you don't have to get everything at once (and sometimes incremental implementation is easier to review too ahah :p)
'We can call the itemstack implementation experimental even when network sync is fully tested.' Literally my words. I couldn't agree more.
easier to review too
By the way, I did think about how much hell reviewing this PR would be. Sorry everyone for probably taking a good chunk of time from your day.
easier to review too
By the way, I did think about how much hell reviewing this PR would be. Sorry everyone for probably taking a good chunk of time from your day.
It's fine, the API looks cool, I haven't yet jumped too much into reviewing but from afar it's cool stuff!
from afar it's cool stuff!
It is. And I think I've made it quite powerful. You see, I'm the simple by default, powerful when needed kind of person when it comes to APIs(and other things but that's not relevant here).
I feel as if Lazy
should be moved to qsl_base
as a public API class... but that's out of scope for this PR.
I feel as if
Lazy
should be moved toqsl_base
as a public API class... but that's out of scope for this PR.
Well if we are talking moving things out, then I'd suggest NetworkCodec be moved to the networking API. It's actually really useful.
Oh, by the way. Dynamic injections don't use the event system anymore.
Consider the initial feature-set done! Feel free to start breaking this!