From 5408eea706c8459a7f7ee6017564685fb21ca371 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Tue, 5 Dec 2023 20:32:15 +0100 Subject: [PATCH 01/11] 20.3 update and cap rework (#21) --- content/news/20.3capability-rework.md | 408 ++++++++++++++++++++++++++ content/news/20.3release.md | 92 ++++++ 2 files changed, 500 insertions(+) create mode 100644 content/news/20.3capability-rework.md create mode 100644 content/news/20.3release.md diff --git a/content/news/20.3capability-rework.md b/content/news/20.3capability-rework.md new file mode 100644 index 0000000..459540c --- /dev/null +++ b/content/news/20.3capability-rework.md @@ -0,0 +1,408 @@ +--- +title: "The Capability rework" +date: 2023-12-05T20:30:00+01:00 +categories: +- News +author: technici4n +summary: | + This post gives an overview of the changes made to the capability system of NeoForge 20.3. +description: | + This post gives an overview of the changes made to the capability system of NeoForge 20.3. +--- + +# Introduction +Our [initial 20.3 release](../20.3release/) comes with a fundamental redesign of the capability system, +with the goal of fixing all the issues that were found in the previous iteration after years of usage. + +Most importantly, there are now two different systems to replace what was previously known as "capabilities": +- **Data attachments** allow adding arbitrary **data** to block entities, chunks, entities, and item stacks. +- **Capabilities** allow querying **behavior** instances from blocks, entities, and item stacks. + +# Data attachments +The attachment system allows mods to attach arbitrary data objects to block entities, chunks, entities, and stacks. + +To use the system, you need to register an `AttachmentType`. +The attachment type contains: +- a default value supplier to create the instance when the data is first accessed, or to compare stacks that have the data and stacks that don't have it; +- an optional serializer if the attachment should be persisted; +- additional configuration options for the attachment, for example the `copyOnDeath` flag. + +There are a few ways to provide an attachment serializer: directly implementing `IAttachmentSerializer`, implementing `INBTSerializable` and using the static `AttachmentSerializer.serializable()` method to create the builder, or providing a codec to the builder. (This latter option is not recommended for item stacks due to relatively slowness). + +In any case, we recommend using a `DeferredRegister` for registration: +```java +// Create the DeferredRegister for attachment types +private static final DeferredRegister> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.ATTACHMENT_TYPES, MOD_ID); + +// Serialization via INBTSerializable +private static final Supplier> HANDLER = ATTACHMENT_TYPES.register( + "handler", () -> AttachmentType.serializable(() -> new ItemStackHandler(1)).build()); +// Serialization via codec +private static final Supplier> MANA = ATTACHMENT_TYPES.register( + "mana", () -> AttachmentType.builder(() -> 0).serialize(Codec.INT).build()); +// No serialization +private static final Supplier> SOME_CACHE = ATTACHMENT_TYPES.register( + "some_cache", () -> AttachmentType.builder(() -> new SomeCache()).build() +); + +// Don't forget to register the DeferredRegister to your mod bus: +ATTACHMENT_TYPES.register(modBus); +``` + +Once the attachment type is registered, it can be used on any holder object. +Calling `getData` if no data is present will attach a new default instance. + +```java +// Get the ItemStackHandler if it already exists, else attach a new one: +ItemStackHandler stackHandler = stack.getData(HANDLER); +// Get the current player mana if it is available, else attach 0: +int playerMana = player.getData(MANA); +// And so on... +``` + +If attaching a default instance is not desired, a `hasData` check can be added: +```java +// Check if the stack has the HANDLER attachment before doing anything. +if (stack.hasData(HANDLER)) { + ItemStackHandler stackHandler = stack.getData(HANDLER); + // Do something with stack.getData(HANDLER). +} +``` + +The data can also be updated with `setData`: +```java +// Increment mana by 10. +player.setData(MANA, player.getData(MANA) + 10); +``` + +Usually, block entities and chunks need to be marked as dirty when they are modified (with `setChanged` and `setUnsaved(true)`). This is done automatically for calls to `setData`: +```java +chunk.setData(MANA, chunk.getData(MANA) + 10); // will call setUnsaved automatically +``` +but if you modify some data that you obtained from `getData` then you must mark block entities and chunks as dirty explicitly: +```java +var mana = chunk.getData(MUTABLE_MANA); +mana.set(10); +chunk.setUnsaved(true); // must be done manually because we did not use setData +``` + +Before we move on to capabilities, here are a few points to take note of with respect to the data attachment system: +- **Level attachments were removed**: please use SavedData instead. +- Serializable item stack attachments are always synced with the client now. +- Entity attachments are copied when a player is teleported back from the end. (Previously this was not the case). +- Entity attachments that have `copyOnDeath` set in their builder will automatically be copied on player death (and on mob conversion). + +### Future work for attachments +We have plans to work on the following improvements to the attachment system over the coming weeks: +- **Attachments in recipe JSONs**: Just like we add support for count and NBT to recipe results, we will add support to specify data attachments in recipe result JSONs. +- **Syncable data attachments**: Currently, all serializable item stack attachments are synced automatically from the logical server to the logical client. +We will look into opt-in syncing for block entity, chunk, and entity attachments in the future. +- **Custom copy handler**: Currently, all data attachments are copied by serializing to NBT and then deserializing a new copy. +This is a good default, but we want to allow modders to provide their own copy implementation for better performance. + +We are open to other suggestions as well, don't hesitate to get in touch! + +# Capabilities +Capabilities are designed to separate **what** a block, entity or item stack can do from **how** it does it. +If you are wondering whether capabilities are the right tool for a job, ask yourself the following questions: +1. Do I only care about **what** a block, entity or item stack can do, but not about **how** it does it? +2. Is the **what**, the behavior only available for some blocks, entities, or item stacks, but not all of them? +3. Is the **how**, the implementation of that behavior, dependent on the specific block, entity or item stack? + +Here are a few examples of good capability usage: +- *"I want to count how many items are in some entity, but I do not know how the entity might store them."* - Yes, use the `IItemHandler` capability. +- *"I want to fill some item stack with power, but I do not know how the item stack might store it."* - Yes, use the `IEnergyStorage` capability. +- *"I want to apply some color to whatever block a player is currently targeting, but I do not know how the block will be transformed"*. - Yes. NeoForge does not provide a capability to color blocks, but you can implement one yourself. + +Here is an example of discouraged capability usage: +- *"I want to check if an entity is within the range of my machine."* - No, use a helper method instead. + +NeoForge supports capabilities for blocks, entities, and item stacks. + +Capabilities allow looking up implementations of some APIs with some dispatching logic. The following kinds of capabilities are implemented in NeoForge: +- `BlockCapability`: capabilities for blocks and block entities; behavior depends on the specific `Block`. +- `EntityCapability`: capabilities for entities: behavior dependends on the specific `EntityType`. +- `ItemCapability`: capabilities for item stacks: behavior depends on the specific `Item`. + +### Creating the capabilities +Creating a capability is a single function call. The following parameters must be provided: +- The name of the capability. Creating a capability with the same name multiple times will always return the same object. +- The behavior type that is being queried. This is the `T` type parameter. +- The type for additional context in the query. This is the `C` type parameter. + +For example, here is how a capability for side-aware block `IItemHandler`s might be declared: + +```java +public static final BlockCapability ITEM_HANDLER_BLOCK = + BlockCapability.create( + // Provide a name to uniquely identify the capability. + new ResourceLocation("mymod", "item_handler"), + // Provide the queried type. Here, we want to look up `IItemHandler` instances. + IItemHandler.class, + // Provide the context type. We will allow the query to receive an extra `Direction side` parameter. + Direction.class); +``` + +A `@Nullable Direction` is so common for blocks that there is a dedicated helper: +```java +public static final BlockCapability ITEM_HANDLER_BLOCK = + BlockCapability.createSided( + // Provide a name to uniquely identify the capability. + new ResourceLocation("mymod", "item_handler"), + // Provide the queried type. Here, we want to look up `IItemHandler` instances. + IItemHandler.class); +``` + +If no context is required, `Void` should be used. +There is also a dedicated helper for context-less capabilities: +```java +public static final BlockCapability ITEM_HANDLER_NO_CONTEXT = + BlockCapability.createVoid( + // Provide a name to uniquely identify the capability. + new ResourceLocation("mymod", "item_handler_no_context"), + // Provide the queried type. Here, we want to look up `IItemHandler` instances. + IItemHandler.class); +``` + +For entities and item stacks, similar methods exist in `EntityCapability` and `ItemCapability` respectively. + +### Querying capabilities +Once we have our `BlockCapability`, `EntityCapability`, or `ItemCapability` object in a static field, we can query a capability. + +Entities and item stacks have essentially the same API as before, but with a `@Nullable T` return type instead of `LazyOptional`. +Simply call `getCapability` with the capability object and the context: +```java +var object = entity.getCapability(CAP, context); +if (object != null) { + // Use object +} +``` +```java +var object = stack.getCapability(CAP, context); +if (object != null) { + // Use object +} +``` + +Block capabilities are used differently, to accommodate for capabilities provided by blocks without block entities. +The query is performed on a `level`: +```java +var object = level.getCapability(CAP, pos, context); +if (object != null) { + // Use object +} +``` + +If the block entity and/or the block state is known, they can be passed to save on query time: +```java +var object = level.getCapability(CAP, pos, blockState, blockEntity, context); +if (object != null) { + // Use object +} +``` + +To give a more concrete example, here is how one might query an `IItemHandler` capability for a block, from the `Direction.NORTH` side: +```java +IItemHandler handler = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.NORTH); +if (handler != null) { + // Use the handler for some item-related operation. +} +``` + +### Block capability caching +For efficient queries and automatic caching, use `BlockCapabilityCache`. +This is a more powerful replacement for the old `LazyOptional` invalidation system. + +When a capability is looked up, the system will perform the following steps under the hood: +1. Fetch block entity and block state if they were not supplied. +2. Fetch registered capability providers. (More on this below). +3. Iterate the providers and ask them if they can provide the capability. +4. One of the providers will return a capability instance, potentially allocating a new object. + +The implementation is rather efficient, but for queries that are performed frequently, +for example every game tick, these steps can take a significant amount of server time. +The `BlockCapabilityCache` system provides a dramatic speedup for capabilities that are frequently queried at a given position. + +Generally, a `BlockCapabilityCache` will be created once and then stored in a field. +The cache must be provided with the capability to query, the level, the position, and the query context. + +```java +// Declare the field: +private BlockCapabilityCache capCache; + +// Later, for example in `onLoad` for a block entity: +this.capCache = BlockCapabilityCache.create( + Capabilities.ItemHandler.BLOCK, // capability to cache + level, // level + pos, // target position + Direction.NORTH // context +); +``` + +Querying the cache is then done with `getCapability()`: +```java +IItemHandler handler = this.capCache.getCapability(); +if (handler != null) { + // Use the handler for some item-related operation. +} +``` + +**The cache is automatically cleared by the garbage collector, there is no need to unregister it.** + +It is also possible to receive notifications when the capability object changes! +This includes capabilities changing (`oldHandler != newHandler`), becoming unavailable (`null`) or becoming available again (not `null` anymore). + +The cache then needs to be created with two additional parameters: +- A validity check, that is used to determine if the cache is still valid. +In the simplest usage as a block entity field, `() -> !this.isRemoved()` will do. +- An invalidation listener, that is called when the capability changes. +This is where you can react to capability changes, removals, or appearances. + +```java +// With optional invalidation listener: +this.capCache = BlockCapabilityCache.create( + Capabilities.ItemHandler.BLOCK, // capability to cache + level, // level + pos, // target position + Direction.NORTH, // context + () -> !this.isRemoved(), // validity check (because the cache might outlive the object it belongs to) + () -> onCapInvalidate() // invalidation listener +); +``` + +For this system to work, **modders must call `level.invalidateCapabilities(pos)` whenever a capability changes, appears, or disappears**. +```java +// whenever a capability changes, appears, or disappears: +level.invalidateCapabilities(pos); +``` + +NeoForge already handles common cases such as chunk load/unloads and block entity creation/removal, +but other cases need to be handled explicitly by modders. +For example, modders must invalidate capabilities in the following cases: +- If the configuration of a capability-providing block entity changes. +- If a capability-providing block (without a block entity) is placed or changes state, by overriding `onPlace`. +- If a capability-providing block (without a block entity) is removed, by overriding `onRemove`. + +For a plain block example, refer to the `ComposterBlock.java` file. + +For more information, refer to the javadoc of `IBlockCapabilityProvider`. + +### Registering capabilities +A capability _provider_ is what ultimately supplies a capability. +A capability provider is function that can either return a capability instance, or `null` if it cannot provide the capability. +Providers are specific to: +- the given capability that they are providing for, and +- the block instance, block entity type, entity type, or item instance that they are providing for. + +They need to be registered in the `RegisterCapabilitiesEvent`. + +Block providers are registered with `registerBlock`. For example: +```java +private static void registerCapabilities(RegisterCapabilitiesEvent event) { + event.registerBlock( + Capabilities.ItemHandler.BLOCK, // capability to register for + (level, pos, state, be, side) -> , + // blocks to register for + MY_ITEM_HANDLER_BLOCK, + MY_OTHER_ITEM_HANDLER_BLOCK); +} +``` + +In general, registration will be specific to some block entity types, so the `registerBlockEntity` helper method is provided as well: +```java + event.registerBlockEntity( + Capabilities.ItemHandler.BLOCK, // capability to register for + MY_BLOCK_ENTITY_TYPE, // block entity type to register for + (myBlockEntity, side) -> ); +``` + +Entity registration is similar, using `registerEntity`: +```java +event.registerEntity( + Capabilities.ItemHandler.ENTITY, // capability to register for + MY_ENTITY_TYPE, // entity type to register for + (myEntity, context) -> ); +``` + +Item registration is similar too. Note that the provider receives the stack: +```java +event.registerItem( + Capabilities.ItemHandler.ITEM, // capability to register for + (itemStack, context) -> , + // items to register for + MY_ITEM, + MY_OTHER_ITEM); +``` + +If for some reason you need to register a provider for all blocks, entities, or items, +you will need to iterate the corresponding registry and register the provider for each object. + +For example, NeoForge uses this system to register a fluid handler capability for all buckets: +```java +// For reference, you can find this code in the `CapabilityHooks` class. +for (Item item : BuiltInRegistries.ITEM) { + if (item.getClass() == BucketItem.class) { + event.registerItem(Capabilities.FluidHandler.ITEM, (stack, ctx) -> new FluidBucketWrapper(stack), item); + } +} +``` + +Providers are asked for a capability in the order that they are registered. +Should you want to run before a provider that NeoForge already registers for one of your objects, +register your `RegisterCapabilitiesEvent` handler with a higher priority. +For example: +```java +modBus.addListener(RegisterCapabilitiesEvent.class, event -> { + event.registerItem( + Capabilities.FluidHandler.ITEM, + (stack, ctx) -> new MyCustomFluidBucketWrapper(stack), + // blocks to register for + MY_CUSTOM_BUCKET); +}, EventPriority.HIGH); // use HIGH priority to register before NeoForge! +``` +See `CapabilityHooks` for a list of the providers registered by NeoForge itself. + +### Entities, IItemHandler and Direction +_You can skip this section if you don't use the item handler entity capability._ + +There are now two capabilities for item handlers on entities: +- `Capabilities.ItemHandler.ENTITY`: exposes the **full inventory** of some entity. +- `Capabilities.ItemHandler.ENTITY_AUTOMATION`: exposes the **automation-accessible inventory**. Hoppers and droppers are patched to support that capability. + +Here is a migration guide from the old system that used a single capability, and distinguished using the `Direction` parameter: +#### Minecart and chest inventories +If you want to support automation-aware inventories: +| Old Syntax | New Syntax | +| -----------| ----------- | +| `entity.getCapability(...)` | `entity.getCapability(Capabilities.ItemHandler.ENTITY_AUTOMATION)` | + +Otherwise: +| Old Syntax | New Syntax | +| -----------| ----------- | +| `entity.getCapability(...)` | `entity.getCapability(Capabilities.ItemHandler.ENTITY)` | + +#### Horse inventory +| Old Syntax | New Syntax | +| -----------| ----------- | +| `horse.getCapability(..., ...)` | `horse.getCapability(Capabilities.ItemHandler.ENTITY)` | + +#### Living entities +| Old Syntax | New Syntax | +| -----------| ----------- | +| `entity.getCapability(..., any vertical direction)` | `new EntityHandsInvWrapper(livingEntity)` | +| `entity.getCapability(..., any horizontal direction)` | `new EntityArmorInvWrapper(livingEntity)` | +| `entity.getCapability(..., null)` | `livingEntity.getCapability(Capabilities.ItemHandler.ENTITY)` | + +#### Players +| Old Syntax | New Syntax | +| -----------| ----------- | +| `player.getCapability(..., any vertical direction)` | `new PlayerMainInvWrapper(player.getInventory())` | +| `player.getCapability(..., any horizontal direction)` | `new CombinedInvWrapper(new PlayerArmorInvWrapper(player.getInventory()), new PlayerOffhandInvWrapper(player.getInventory()))` | +| `player.getCapability(..., null)` | `player.getCapability(Capabilities.ItemHandler.ENTITY)` | + +### Future plans for capabilities +Composters now support the item handler capability. However, cauldrons still do not support the fluid handler capability. This will be addressed in the coming weeks, and mods using the block fluid handler capability will work with cauldrons out of the box. + +We have reviewed and tested this capability overhaul extensively. Nonetheless, we expect that issues will be discovered after the release. Please don't hesitate to get in touch with us, be it on Discord or GitHub! + +That's all for now, happy porting! diff --git a/content/news/20.3release.md b/content/news/20.3release.md new file mode 100644 index 0000000..2a12d11 --- /dev/null +++ b/content/news/20.3release.md @@ -0,0 +1,92 @@ +--- +title: "NeoForge 20.3 for Minecraft 1.20.3" +date: 2023-12-05T20:25:00+01:00 +categories: +- News +author: neoforgedteam +summary: | + All you need to know about NeoForge 20.3, now released for Minecraft 1.20.3. +description: | + All you need to know about NeoForge 20.3, now released for Minecraft 1.20.3. +--- + +The first beta release of NeoForge for Minecraft 1.20.3, NeoForge 20.3.1-beta is now released! +Please try it out, play with it, develop with it, and give us feedback! +**We are not stable yet**, so expect a few breaking changes in the coming weeks. + +For players, you can grab the latest installer directly from https://neoforged.net/. + +The rest of this blog post is for modders. +Let's talk about recent updates to NeoForge that modders porting to 20.3 should be aware of. +Minecraft 1.20.3 itself also comes with a few technical changes, however that will not be covered in this post. + +**** + +## Capability rework +The most impactful change in 20.3 is the rework of the capability system. +You can read all about it in [our dedicated blog post](../20.3capability-rework/). + +## Noteworthy recent additions +Let's discuss a few changes that are already available in the 20.2 release series, +and of course in all 20.3 releases, +but you might not have heard about. + +### Access transformers inside `mods.toml` +Mods can now ship multiple access transformer files, by declaring them in their `mods.toml`: +```toml +[[accessTransformers]] +file="modid_base.at" + +[[accessTransformers]] +file="modid_extra.at" +``` +If no such entry is present, FML will fall back to `META-INF/accesstransformer.cfg` like before. +We will continue to accept both formats for the foreseeable future. + +Additionally, ATs still need to be specified for NeoGradle to apply them in your development environments: +```groovy +minecraft { + accessTransformers { + file('src/main/resources/modid_base.cfg') + file('src/main/resources/modid_extra.cfg') + } +} +``` + +### MixinExtras ships with NeoForge +MixinExtras by LlamaLad7 now ships with NeoForge and is enabled automatically. There is nothing required in your build.gradle with regards to MixinExtras anymore. + +MixinExtras is a completary library to Mixin, designed to help modders write their Mixins in a more expressive and compatible way. +If you use mixins in your mod, we encourage you to give the [MixinExtras wiki](https://github.com/LlamaLad7/MixinExtras/wiki) a read. + +If you want to override the bundled MixinExtras version with a newer one, you can do so by JiJing the newer version the same way you used to JiJ regular MixinExtras before this update. + +### Mixin configs inside `mods.toml` +Mixin configs can now be specified directly inside your `mods.toml` file, for example: +```toml +[[mixins]] +config="modid_base.mixins.json" + +[[mixins]] +config="modid_extra.mixins.json" +``` + +We recommend using this new format in your mods. +No Gradle support is needed anymore to use Mixins, +and this opens the door to selectively-enabled mixin configs in the future. + +## Coming Soon +Before we can stabilize 20.3, +we will be releasing an overhaul of our networking hooks and protocol to account for the configuration phase recently introduced by Mojang. +If you are interested in the discussion, we encourage you to join our Discord server and look at the `#brainstorming` forum channel. + +As usual, we also welcome all kinds of smaller contributions. +The breaking change window for 20.3 will remain open for a few weeks. +Now is a great time to start working on your Pull Requests. + +## 1.20.2 Plans +NeoForge for 1.20.2 is now stable, starting from version `20.2.86` released a few days ago. +Due to limited community adoption, +we will only accept backports for critical bugfixes, +provided that they are first submitted to and accepted for the 1.20.3 branch. + From e209770364f782155179e716eaef9bf4f06715c0 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:49:05 +0100 Subject: [PATCH 02/11] Clarify that NeoForge already provides common capabilities --- content/news/20.3capability-rework.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/content/news/20.3capability-rework.md b/content/news/20.3capability-rework.md index 459540c..c99a842 100644 --- a/content/news/20.3capability-rework.md +++ b/content/news/20.3capability-rework.md @@ -106,7 +106,7 @@ We are open to other suggestions as well, don't hesitate to get in touch! Capabilities are designed to separate **what** a block, entity or item stack can do from **how** it does it. If you are wondering whether capabilities are the right tool for a job, ask yourself the following questions: 1. Do I only care about **what** a block, entity or item stack can do, but not about **how** it does it? -2. Is the **what**, the behavior only available for some blocks, entities, or item stacks, but not all of them? +2. Is the **what**, the behavior, only available for some blocks, entities, or item stacks, but not all of them? 3. Is the **how**, the implementation of that behavior, dependent on the specific block, entity or item stack? Here are a few examples of good capability usage: @@ -124,9 +124,24 @@ Capabilities allow looking up implementations of some APIs with some dispatching - `EntityCapability`: capabilities for entities: behavior dependends on the specific `EntityType`. - `ItemCapability`: capabilities for item stacks: behavior depends on the specific `Item`. -### Creating the capabilities -Creating a capability is a single function call. The following parameters must be provided: -- The name of the capability. Creating a capability with the same name multiple times will always return the same object. +### Creating capabilities +NeoForge already defines common capabilities, which we recommend for compatibility with other mods. +For example: +```java +// Standard item handler BlockCapability +Capabilities.ItemHandler.BLOCK +// Standard item handler ItemCapability +Capabilities.ItemHandler.ITEM + +// See the `Capabilities` class for the full list. +``` + +If these are not sufficient, you can create your own capabilities. +Creating a capability is a single function call, and the resulting object should be stored in a `static final` field. +The following parameters must be provided: +- The name of the capability. +Creating a capability with the same name multiple times will always return the same object. +Capabilities with different names are **completely independent**, and can be used for different purposes. - The behavior type that is being queried. This is the `T` type parameter. - The type for additional context in the query. This is the `C` type parameter. From 3053abff4e21369d68a476cdddd7162bc633028e Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:30:33 +0100 Subject: [PATCH 03/11] slight clarifications for BlockCapabilityCache usage --- content/news/20.3capability-rework.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/content/news/20.3capability-rework.md b/content/news/20.3capability-rework.md index c99a842..8a17f54 100644 --- a/content/news/20.3capability-rework.md +++ b/content/news/20.3capability-rework.md @@ -225,7 +225,7 @@ if (handler != null) { ``` ### Block capability caching -For efficient queries and automatic caching, use `BlockCapabilityCache`. +For efficient queries and automatic caching, use `BlockCapabilityCache` instead of directly calling `level.getCapability`. This is a more powerful replacement for the old `LazyOptional` invalidation system. When a capability is looked up, the system will perform the following steps under the hood: @@ -238,7 +238,8 @@ The implementation is rather efficient, but for queries that are performed frequ for example every game tick, these steps can take a significant amount of server time. The `BlockCapabilityCache` system provides a dramatic speedup for capabilities that are frequently queried at a given position. -Generally, a `BlockCapabilityCache` will be created once and then stored in a field. +Generally, a `BlockCapabilityCache` will be created once and then stored in a field of the object performing frequent capability queries. +When exactly you store the cache is up to you. The cache must be provided with the capability to query, the level, the position, and the query context. ```java From 171390cc4c9387e67f61fef433ebf3563feac073 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:22:08 +0100 Subject: [PATCH 04/11] Update blog post for 20.4 --- content/news/20.3release.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/content/news/20.3release.md b/content/news/20.3release.md index 2a12d11..89cad51 100644 --- a/content/news/20.3release.md +++ b/content/news/20.3release.md @@ -1,15 +1,23 @@ --- -title: "NeoForge 20.3 for Minecraft 1.20.3" +title: "NeoForge 20.3 for Minecraft 1.20.3 and 1.20.4" date: 2023-12-05T20:25:00+01:00 categories: - News author: neoforgedteam summary: | All you need to know about NeoForge 20.3, now released for Minecraft 1.20.3. + Now updated for Minecraft 1.20.4 as well. description: | All you need to know about NeoForge 20.3, now released for Minecraft 1.20.3. + Now updated for Minecraft 1.20.4 as well. --- +_Note: With the recent release of the Minecraft 1.20.4 hotfix version, +we are dropping support for 1.20.3 and encourage all modders to update to NeoForge 20.4. +The future plans mentioned here have not changed, but they will target 20.4 instead of 20.3._ + +**** + The first beta release of NeoForge for Minecraft 1.20.3, NeoForge 20.3.1-beta is now released! Please try it out, play with it, develop with it, and give us feedback! **We are not stable yet**, so expect a few breaking changes in the coming weeks. From 3cfb86c3ecf1e6cb6e7c5d37eae89446bc95a772 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:51:54 +0100 Subject: [PATCH 05/11] Fix index page (#26) --- content/_index.md | 24 ++++-------------------- layouts/shortcodes/projects.html | 7 +++++++ 2 files changed, 11 insertions(+), 20 deletions(-) create mode 100644 layouts/shortcodes/projects.html diff --git a/content/_index.md b/content/_index.md index 8915b15..8cec828 100644 --- a/content/_index.md +++ b/content/_index.md @@ -12,14 +12,11 @@ description: | # NeoForge installer files You can find a direct link to our latest installer files below. -{{< files "1.20.1" >}} - -Note: this file is still called forge because we're trying to maintain compatibility with launchers, -assuming they don't hardcode things too much. +{{}} -## The new version is here +{{< files "1.20.1" >}} -{{}} +Note: the file is still called forge in 1.20.1 to maintain compatibility with launchers. # Using NeoForge for mod development @@ -29,20 +26,7 @@ assuming they don't hardcode things too much. 2. Clone this project locally, and get started! Our [Documentation] and the [Modding Wiki] are great places to learn more. ## If you have an existing mod project -You can use neoforge in your existing mod development as well. To do so: -1. Update your repositories to use `https://maven.neoforged.net/releases` instead of `https://maven.minecraftforge.net` -2. Update your ForgeGradle to use NeoGradle 6.0.13 or above: - ``` - plugins { - ... - id 'net.neoforged.gradle' version '[6.0.13, 6.2)' - } - ``` -3. Update your `minecraft` dependency to use `net.neoforged:forge` and the version as shown above. - -### Some things to note -1. At the present time, mods built by either system should be intercompatible between forks. -2. Do note that recently, there were some changes in the recommended way to use ForgeGradle/NeoGradle. Note especially the way that `settings.gradle` has changed, as well as the removal of the `buildscript` section in `build.gradle`. Refer to [The MDK] for an example and more. +Refer to [The MDK] for an example of how to update your build script to use NeoForge. [The MDK]: https://github.com/neoforged/MDK [Documentation]: https://docs.neoforged.net diff --git a/layouts/shortcodes/projects.html b/layouts/shortcodes/projects.html new file mode 100644 index 0000000..3c539e9 --- /dev/null +++ b/layouts/shortcodes/projects.html @@ -0,0 +1,7 @@ + \ No newline at end of file From b05c82a7799e84be59cd61ae546e4d2c35cf0da9 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 31 Dec 2023 22:15:40 +0100 Subject: [PATCH 06/11] Post - Networking Rewrite (#28) --- .github/workflows/build-pr.yml | 7 +- .gitignore | 1 + .gitmodules | 3 + content/news/20.4networking-rework.md | 219 ++++++++++++++++++++++++++ data/authors/oriononline.yaml | 7 + hugo.toml | 2 +- static/img/authors/oriononline.png | Bin 0 -> 155827 bytes themes/hugo-notice | 1 + 8 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 .gitmodules create mode 100644 content/news/20.4networking-rework.md create mode 100644 data/authors/oriononline.yaml create mode 100644 static/img/authors/oriononline.png create mode 160000 themes/hugo-notice diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index c6d4954..e2a5a94 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -11,15 +11,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Setup hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: "0.119.0" - name: Build - run: hugo + run: hugo -F - uses: matyrobbrt/cloudflare-pr-previews/upload@v1 name: Upload website with: - build-dir: ./public/ \ No newline at end of file + build-dir: ./public/ diff --git a/.gitignore b/.gitignore index 8dd6d56..9528df3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ public/ .hugo_build.lock +.idea diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..66592a1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "themes/hugo-notice"] + path = themes/hugo-notice + url = https://github.com/martignoni/hugo-notice.git diff --git a/content/news/20.4networking-rework.md b/content/news/20.4networking-rework.md new file mode 100644 index 0000000..1ae2f73 --- /dev/null +++ b/content/news/20.4networking-rework.md @@ -0,0 +1,219 @@ +--- +title: "The Networking Refactor" +date: 2023-12-31T21:00:00+00:00 +publishDate: 2023-12-31T21:00:00+00:00 +categories: +- News +author: oriononline +summary: | + This post gives an overview of the changes made to the networking system of NeoForge 20.4. +description: | + This post gives an overview of the changes made to the networking system of NeoForge 20.4. +--- + +# Introduction +## Reworked networking +Welcome to the reworked networking blog post for NeoForge, these changes are available in NeoForge 20.4.70-beta and above. +The post will describe the changes to NeoForge to enable the configuration network protocol designed by Modmuss50, a maintainer of Fabric, with some minor modifications. I'm sharing more on those below. + +## SimpleChannel and EventChannel +Forge had two different ways of implementing a custom network channel. A simple registration-based approach is known as SimpleChannel, and a system that fires events for each packet received is known as EventChannel. +To simplify the API and make interacting with new packet layouts easier, it was decided to rework both implementations into a single system, combining the best of both worlds. + +# Implementation +## New network payload handling +The system is based on the `CustomPacketPayload` definition that Mojang uses to represent the content of custom packets. Internally, they register their custom payloads, used mainly for debugging, to a map. We need to extend this map so that a modder can send and receive a custom implementation of this `CustomPacketPayload`. This is what a significant part of the rework achieves. Modders can introduce new CustomPacketPayload implementations by registering them during the `RegisterPacketHandlerEvent`. + +### The registrar +Any modder can request a registrar for any namespace they desire; however, it is recommended that a mod only requests a registrar for its namespace. Once a registrar is retrieved from the event, it can be configured with two different options: `versioned(String version)` to configure a version for all payloads registered after the call and `optional()` to mark all payloads registered after that call as not requiring a receiving side. An example of the registrar configuration can be found here: +```java +@SubscribeEvent +public static void register(final RegisterPacketHandlerEvent event) { + final IPayloadRegistrar registrar = event.registrar("my_mod") + .versioned("1.2.3") + .optional(); +} +``` +{{< notice note >}} +The registrar is a semi-immutable object; calling `versioned(String version)` or `optional()` on an instance of the registrar will cause a new instance with the desired configuration to be created. +{{< /notice >}} + +{{< notice warning >}} +The registrar loses its validity the moment the scope of the event has been left. Registering payload handlers outside the event handling scope will result in those payloads not being known to the system and not being sent over the connection. Additionally, the attempt to register them outside the event scope will trigger an exception. +{{< /notice >}} + +The registrar offers six different endpoints, three pairs of two, to register new payloads. A pair exists for the play phase, one pair for the special configuration sub-phase, and one pair of methods for both. + +{{< notice info >}} +It is impossible to register custom payloads that should be sent during the login phase of the connection. And the new code offers, as such, no infrastructure to achieve this. +{{< /notice >}} + +Within each pair of registration methods for a phase, two variants (as such, six methods) are available. One that registers the same handler for both sides of the connection and one that takes a consumer, allowing for the configuration of single-sided or differentially handled payloads. + +An example of the signature of the methods for the configuration phase is as follows: +```java + IPayloadRegistrar configuration(ResourceLocation id, FriendlyByteBuf.Reader reader, IConfigurationPayloadHandler handler); + + IPayloadRegistrar configuration(ResourceLocation id, FriendlyByteBuf.Reader reader, Consumer>> handler); +``` +For the play phase, similar methods exist. +For payloads that are supposed to be sent during both the play phase and the configuration phase, there also exists a pair. +However, here, the handler is a common supertype of the handling callbacks, which has a reduced superset of the information available to the two different types. + +### Payload discrimination +When considering this system, you might, rightfully so, ask how the system keeps the different payload types apart from each other. +It writes a discriminator ID to the connection before invoking the write method for a payload. +Similarly, on the client side, the discriminator is read first, then a reader is looked up so that the rest of the payload can be read. + + +The value of the discriminator during writing is retrieved from the `CustomPacketPayload#id()` method, and cannot be `null`. +The value against which the id, read from the connection, is compared to find a reader, is the one that is given to the registrar as the first argument. +It is as such of the upmost importance that both the registrar and the `id()` method of the payload instance receive the same resource location. + +{{< notice tip >}} +We recommend that you store your ID in a `public static final ResourceLocation ID = new ResourceLocation("mod_id", "payload_id")` field, and reference that in both places. +{{< /notice >}} + +{{< notice warning >}} +Given that the id is used as a discriminator, it is important that you use a unique value, especially for the path of each payload type. If you try to register the same id twice, the registrar will throw an exception. If you try to register an id with a namespace other than the one the registrar is for, the registrar will throw an exception. You are free to request registrars for other namespaces than your own. +{{< /notice >}} + +### Payload reading +Payload reading happens via the vanilla method, implementing the `FriendlyByteBuf.Reader` functional interface. During the registration of the payload type, as can be seen above, you need to pass an implementation of this interface so that a new instance of the payload can be created by the system when a custom payload packet with it as payload arrives at the receiving end. + +{{< notice tip >}} +As we recommend that your payload implementations are `record`s in Java, instead of classes, we also recommend that you create a custom constructor with the record, to read the records fields from the buffer. This constructor can then be passed as a method reference for the reader implementation. So if `SimplePayload(String something) {}` is your normal record, then adding `SimplePayload(FriendlyByteBuf buf) { this(buf.readUtf()); }` as a constructor to the `SimplePayload` record will allow you to pass it as a method reference `SimplePayload::new` to the registrar when it asks for an implementation of `FriendlyByteBuf.Reader` +{{< /notice >}} + +{{< notice warning >}} +There is no guarantee regarding which thread the reading or writing callback is invoked upon. It is as such important to note that the method can be called on many threads in parallel if the same packet is processed by many connections simultaneously. +{{< /notice >}} + +### Payload writing +The `CustomPacketPayload` interface contains a method: `write(FriendlyByteBuf)` method. This method is invoked when it is time to write your payload to the network connection. There is no guarantee regarding what thread invokes the writer. + +{{< notice warning >}} +As with payload reading there is no guarantee regarding which thread the writing callback is invoked upon. It is as such important to note that the method can be called on many threads in parallel if the same packet is sent on many connections simultaneously. +{{< /notice >}} + +{{< notice warning >}} +Payloads are only read and written if sent over a connection. This means that the host of a single-player world (even if exposed to LAN) has packets and, as such, payloads transferred in memory. This means that for those payloads, no write method is invoked, and no reader is called. Only the handler is invoked! +{{< /notice >}} + +### Payload handling +Once a payload has been written, transmitted and read, the payload handler is invoked. This handler is again looked up using the id of the payload, and then invoked with the context of the receiving end. Each handler takes two arguments the: payload instance, and the context. + +{{< notice warning >}} +Payloads are processed on the network thread, and can as such happen in parallel with other payloads of the same type being handled. If you need to ensure that the payload is processed on the main thread, serially, see the `ISynchronizedWorkHandler` available in the context under the `workHandler()` method. +{{< /notice >}} + +#### The context +The context contains information, callbacks and entry points, to access the surrounding network system, the main thread, as well as handling processing other packets, or completing configuration tasks. + +##### ReplyHandler +The reply handler can be used to quickly send a payload back to the sender. It is, for example, useful to send and answer to a query packet, or to send an acknowledgment that you received and processed the payload. You still need to register the return payload. + +##### PacketHandler +In case you implement a packet splitting mechanism, whether that splits on full vanilla packets, or custom packet payloads, the `IPacketHandler` interface gives you access to the start of the processing pipeline, allowing you to process other payloads immediately. + +{{< notice note >}} +This does not transmit payloads, it purely allows for the receiving end to process additional packets or payloads constructed in memory during the processing of your payload. +{{< /notice >}} + +The packet handler also gives you access to a `disconnect(Component)` method, allowing you to terminate the connection, and showing the given component as reason to the user. + +##### WorkHandler +The work handler allows you to schedule work on the main thread of the receiving side. This might be the `Minecraft` class instance if the logical receiving side is the client, or the `MinecraftServer` instance if the receiving side is the server. + +The system uses `CompletableFuture` instances, so you are able to schedule different follow-up tasks. + +##### PacketFlow +The context will indicate, via the packet flow, what the receiving side currently is. If it is a serverbound flow, then the handler is currently being invoked in the context of the server. Is it a clientbound flow, then the handler is currently being invoked in the context of the client. + +##### ConnectionProtocol +The current active connection protocol is useful if you have raw bytes of a packet wrapped in your payload, allowing your handler to decode the inner packet or payload, before passing it to the `IPacketHandler` for processing. + +##### ChannelHandlerContext +The Netty channel handling context that is currently processing the payload is also given as context. +This context can be used to retrieve the raw underlying connection via `ConnectionUtils`, or, be used to process raw bytes of inner packets and payloads. + +##### Player +An `Optional` containing a player is also provided. +If the handler is invoked on the server side, then this is the player that sent the payload. +If the handler is invoked on the client side, then this is the local player, if it is available. + +##### Level +An `Optional` containing the level the player is in is provided too. + +##### TaskCompletedHandler +This is a special contextual value only available to payloads during the configuration phase, that indicates that a specific configuration task has been completed, and that the next one can be started. + +##### Future additions to the contexts +We are fully aware that these entry points might not be all information you, as a modder, need to process a packet. In general, it is pretty easy to extend the interfaces and records. **They were specifically designed to allow for simple PRs in the future to add to them**, so please do not hesitate to create a quick PR to add your needed data to the context. + +### Packet Sending +In tandem with the refactoring of the channel registration mechanic we added new tools and systems to allow you to more easily send a custom payload to different targets. + +#### PacketDistributor +This wrapper class now has the ability to process custom payloads solely. Its instances and targets can be passed around since they are immutable. Several methods on extension classes will accept these, to facilitate easy transfer of payloads. + +#### Extension objects +We extended several vanilla types allowing them to accept payloads as well, not just packets. Examples are chunk sections, listeners, entities and players. + +### Netty information +We store a lot of information related to the connection, for example the negotiated payload types, on the connection object itself. As such, we added several attributes to store this information in. These attributes are considered internal API, use them at your own risk. + +## Configuration tasks on client join +### Tasks +Vanilla now provides a centralized way to perform tasks and jobs that need to be performed when a player joins. The player won't be instantiated or added to a world until these tasks are completed. + +Under normal circumstances these tasks are implementations of the `ConfigurationTask` interface, with a single method: `start(Consumer>)`. However, this is subpar in our situation. Modders should never really have to touch raw Packets to perform their duties, only payloads. And as such it was decided to have modders implement the `ICustomConfigurationTask` interface from NeoForge. This provides a wrapper around the `ConfigurationTask` signature and allows sending payloads instead of packets, by implementing `run(Consumer)` instead. The given consumer will then automatically convert the payload to a packet and send it to the client that is being configured. + +{{< notice note >}} +In practice, an instance of `ICustomConfigurationTask` is also an instance of `ConfigurationTask` as one extends the other. But to provide the ability for it to be a functional interface, the `start` method is implemented by default. You should not override it. +{{< /notice >}} + +### OnGameConfigurationEvent +This event is fired to collect all tasks that should be run, and allows for the registration of `ICustomConfigurationTask` instances to the listener. It is not possible to register the vanilla `ConfigurationTask` instances. + +{{< notice tip >}} +This event is fired on the mod bus, to preserve dependency order. Given that configuration tasks can only be running in order of registration, you can safely assume that configuration tasks of your dependencies have been running before yours. +{{< /notice >}} + +### NeoForge packet changes: +Moved configuration sync, registry sync, and tier registry sync to configuration phase tasks. + +## Bundle packet processing +In 1.19.4 Mojang introduced the bundling system for packets, which is a core component that allows packets to be processed together. We anticipate that modders may want to use this system, so we adapted it to accept custom payloads. You will find a `sendBundled(CustomPacketPayload... payloads)` method on the `ServerGamePacketListener`. + +{{< notice warning >}} +Packet bundling is only supported during the play phase of the network protocol. It cannot be used during the configuration phase of the protocol. +{{< /notice >}} + +## Opening menus with custom data +In the past, NeoForge supported opening UIs from the server side with additional data, via `NetworkHooks.openScreen(...)`. This system has been moved and is now part of the server `ServerPlayer` extension. You can call the method `openMenu` with the same parameters. + +## Spawning entities with custom data +The previous networking implementation allowed spawning custom entities via an overridable method in the `Entity` class: `getAddEntityPacket.` Modders that wanted to support custom additional data to be processed on the client side when the entity spawned could override this method and return a packet from the method using `NetworkHooks.getEntitySpawningPacket(...)`. + +This system has now been refactored (as Mojang spawns and processes the entity packets with a bundle). The core of this new framework is the method `sendPairingData(ServerPlayer, Consumer)` on the `Entity` class. There are now two methods to configure this. + +### Using a custom payload +By overriding the method and invoking the consumer with a custom payload, you are guaranteeing that your payload will be processed immediately after the spawning packet. You are free to do whatever you want, however, we recommend you at least transfer the entity id, as vanilla does, to retrieve the entity instance when your packet arrives. + +### Using the `IEntityWithComplexSpawn` interface +Implementing this interface on your entity forces you to implement two methods: `writeSpawnData` and `readSpawnData`. These methods, are invoked when an entity spawn bundle is generated and when the entity has been spawned, respectively. + +### Removal of custom entity creation code in `EntityType` +It is no longer possible to use the entity spawn packet code mentioned above to create a different entity class on the client side. This was due to the refactoring of the payload mechanics, and the reliance on the vanilla spawn bundle. + +## NeoForge packet splitter +The NeoForge packet splitter can now be used with any packet (other than the splitting packet itself). + +## Mod list transfer +Currently, the new protocol does not support sending the mod list across the network, and only works on a channel and registry content basis. However, we do intend to work with the Fabric team on expanding the protocol in such a way that the server is made aware of what kind of mods the client has installed. +We deem this necessary to allow server owners to block mods that would allow cheating, for example, an XRay mod, etc. +This will be added in another PR after further reviewing our stance and implementation possibilities. + +## Further documentation +More documentation on using the networking features can be found in the [Networking](https://docs.neoforged.net/docs/networking/) section of the documentation. diff --git a/data/authors/oriononline.yaml b/data/authors/oriononline.yaml new file mode 100644 index 0000000..15385c1 --- /dev/null +++ b/data/authors/oriononline.yaml @@ -0,0 +1,7 @@ +name: oriononline +bio: | + OrionOnline (or Orion for short) is a maintainer for NeoForged, primary maintainer of the Toolchain, as well as a member of the Rendering team. +avatar: /img/authors/oriononline.png +social: + mastodon: https://anvil.social/@oriononline + github: https://github.com/marchermans diff --git a/hugo.toml b/hugo.toml index 2226258..189271a 100644 --- a/hugo.toml +++ b/hugo.toml @@ -1,7 +1,7 @@ baseURL = 'https://neoforged.net/' languageCode = 'en-us' title = 'The NeoForged project' -theme = "mainroad" +theme = ["hugo-notice", "mainroad"] [Params] description = "Files, news and information from the NeoForged Project" # Description of your site diff --git a/static/img/authors/oriononline.png b/static/img/authors/oriononline.png new file mode 100644 index 0000000000000000000000000000000000000000..9b3e3f16cfb9f4ae7a8a8fe64f439d35df03d555 GIT binary patch literal 155827 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