From 312442d3d4cd3cb2fb7da479ce808c6028881ad0 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 29 Jan 2024 18:04:43 +0100 Subject: [PATCH 01/41] rework registries and resource location documentation --- docs/blocks/index.md | 31 ++- docs/concepts/registries.md | 304 ++++++++++++++------------ docs/concepts/resources.md | 21 -- docs/gameeffects/sounds.md | 2 +- docs/items/index.md | 47 ++++ docs/misc/resourcelocation.md | 46 ++++ docs/resources/client/index.md | 2 +- docs/resources/client/models/index.md | 2 +- docs/resources/server/glm.md | 2 +- docs/resources/server/index.md | 2 +- 10 files changed, 295 insertions(+), 164 deletions(-) delete mode 100644 docs/concepts/resources.md create mode 100644 docs/misc/resourcelocation.md diff --git a/docs/blocks/index.md b/docs/blocks/index.md index 09567c52..bfac39ab 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -8,10 +8,10 @@ One Block to Rule Them All Before we get started, it is important to understand that there is only ever one of each block in the game. A world consists of thousands of references to that one block in different locations. In other words, the same block is just displayed a lot of times. -Due to this, a block should only ever be instantiated once, and that is during registration. Once the block is registered, you can then use the registered reference as needed. Consider this example (see the [Registration][registration] page if you do not know what you are looking at): +Due to this, a block should only ever be instantiated once, and that is during [registration]. Once the block is registered, you can then use the registered reference as needed. Consider this example: ```java -//BLOCKS is a DeferredRegister.Blocks +//BLOCKS is a DeferredRegister public static final DeferredBlock MY_BLOCK = BLOCKS.register("my_block", () -> new Block(...)); ``` @@ -80,6 +80,33 @@ Directly using `Block` only allows for very basic blocks. If you want to add fun If you want to make a block that has different variants (think a slab that has a bottom, top, and double variant), you should use [blockstates]. And finally, if you want a block that stores additional data (think a chest that stores its inventory), a [block entity][blockentities] should be used. The rule of thumb here is that if you have a finite and reasonably small amount of states (= a few hundred states at most), use blockstates, and if you have an infinite or near-infinite amount of states, use a block entity. +### `DeferredRegister.Blocks` + +All registries use `DeferredRegister` to register their contents, and blocks are no exceptions. However, due to the fact that adding new blocks is such an essential feature of an overwhelming amount of mods, NeoForge provides the `DeferredRegister.Blocks` helper class that extends `DeferredRegister` and provides some block-specific helpers: + +```java +public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(ExampleMod.MOD_ID); + +public static final Supplier EXAMPLE_BLOCK = BLOCKS.registerBlock( + "example_block", + Block::new, // The factory that the properties will be passed into. + new BlockBehaviour.Properties() // The properties to use. +); +``` + +Internally, this will simply call `BLOCKS.register("example_block", () -> new Block(new BlockBehaviour.Properties()))` by applying the properties parameter to the provided block factory (which is commonly the constructor). + +If you want to use `Block::new`, you can leave out the factory entirely: + +```java +public static final Supplier EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock( + "example_block", + new BlockBehaviour.Properties() // The properties to use. +); +``` + +This does the exact same as the previous example, but is slightly shorter. Of course, if you want to use a subclass of `Block` and not `Block` itself, you will have to use the previous method instead. + ### Resources If you register your block and place it in the world, you will find it to be missing things like a texture. This is because textures, among others, are handled by Minecraft's resource system. diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 22325c43..c1a2211f 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -1,200 +1,232 @@ -Registries -========== +# Registries -Registration is the process of taking the objects of a mod (such as items, blocks, sounds, etc.) and making them known to the game. Registering things is important, as without registration the game will simply not know about these objects, which will cause unexplainable behaviors and crashes. +Registration is the process of taking the objects of a mod (such as [items][item], [blocks][block], entities, etc.) and making them known to the game. Registering things is important, as without registration the game will simply not know about these objects, which will cause unexplainable behaviors and crashes. -Most things that require registration in the game are handled by the Forge registries. A registry is an object similar to a map that assigns values to keys. Forge uses registries with [`ResourceLocation`][ResourceLocation] keys to register objects. This allows the `ResourceLocation` to act as the "registry name" for objects. +A registry is, simply put, a wrapper around a map that maps registry names (read on) to registered objects, often called registry entries. Registry names must be unique within the same registry, but the same registry name may be present in multiple registries. The most common example for this are blocks (in the `BLOCKS` registry) that have an item form with the same registry name (in the `ITEMS` registry). -Every type of registrable object has its own registry. To see all registries wrapped by Forge, see the `ForgeRegistries` class. All registry names within a registry must be unique. However, names in different registries will not collide. For example, there's a `Block` registry, and an `Item` registry. A `Block` and an `Item` may be registered with the same name `example:thing` without colliding; however, if two different `Block`s or `Item`s were registered with the same exact name, the second object will override the first. +Every registered object has a unique name, called its registry name. The name is represented as a [`ResourceLocation`][resloc]. For example, the registry name of the dirt block is `minecraft:dirt`, and the registry name of the zombie is `minecraft:zombie`. Modded objects will of course not use the `minecraft` namespace; their mod id will be used instead. -Methods for Registering ------------------- +## Vanilla vs. Modded -There are two proper ways to register objects: the `DeferredRegister` class, and the `RegisterEvent` lifecycle event. +To understand some of the design decisions that were made in NeoForge's registry system, we will first look at how Minecraft does this. We will use the block registry as an example, as most other registries work the same way. -### DeferredRegister +Registries generally register [singletons][singleton]. This means that all registry entries exist exactly once. For example, all stone blocks you see throughout the game are actually the same stone block, displayed many times. If you need the stone block, you can get it by referencing the registered block instance. -`DeferredRegister` is the recommended way to register objects. It allows the use and convenience of static initializers while avoiding the issues associated with it. It simply maintains a list of suppliers for entries and registers the objects from those suppliers during `RegisterEvent`. +Minecraft registers all blocks in the `Blocks` class. Through the `register` method, `Registry#register()` is called, with the block registry at `BuiltInRegistries.BLOCK` being the first parameter. After all blocks are registered, Minecraft performs various checks based on the list of blocks, for example the self check that verifies that all blocks have a model loaded. -An example of a mod registering a custom block: +The main reason all of this works is that `Blocks` is classloaded early enough by Minecraft. Mods are not automatically classloaded by Minecraft, and thus workarounds are needed. -```java -private static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID); +## Methods for Registering -public static final RegistryObject ROCK_BLOCK = BLOCKS.register("rock", () -> new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE))); +NeoForge offers two ways to register objects: the `DeferredRegister` class, and the `RegisterEvent`. Note that the former is a wrapper around the latter, and is thus recommended in order to prevent mistakes. -public ExampleMod() { - BLOCKS.register(FMLJavaModLoadingContext.get().getModEventBus()); -} -``` +### `DeferredRegister` -### `RegisterEvent` - -`RegisterEvent` is the second way to register objects. This [event] is fired for each registry after the mod constructors and before the loading of configs. Objects are registered using `#register` by passing in the registry key, the name of the registry object, and the object itself. There is an additional `#register` overload which takes in a consumed helper to register an object with a given name. It is recommended to use this method to avoid unnecessary object creation. - -Here is an example: (the event handler is registered on the *mod event bus*) +We begin by creating our `DeferredRegister`: ```java -@SubscribeEvent -public void register(RegisterEvent event) { - event.register(ForgeRegistries.Keys.BLOCKS, - helper -> { - helper.register(new ResourceLocation(MODID, "example_block_1"), new Block(...)); - helper.register(new ResourceLocation(MODID, "example_block_2"), new Block(...)); - helper.register(new ResourceLocation(MODID, "example_block_3"), new Block(...)); - // ... - } - ); -} +public static final DeferredRegister BLOCKS = DeferredRegister.create( + // The registry we want to use. + // Minecraft's registries can be found in BuiltInRegistries, NeoForge's registries can be found in NeoForgeRegistries. + // Mods may also add their own registries, refer to the individual mod's documentation or source code for where to find them. + BuiltInRegistries.BLOCKS, + // Our mod id. + ExampleMod.MOD_ID +); ``` -### Registries that aren't Forge Registries - -Not all registries are wrapped by Forge. These can be static registries, like `LootItemConditionType`, which are safe to use. There are also dynamic registries, like `ConfiguredFeature` and some other worldgen registries, which are typically represented in JSON. `DeferredRegister#create` has an overload which allows modders to specify the registry key of which vanilla registry to create a `RegistryObject` for. The registry method and attaching to the mod event bus is the same as other `DeferredRegister`s. - -:::danger -Dynamic registry objects can **only** be registered through data files (e.g. JSON). They **cannot** be registered in-code. -::: +We can then add our registry entries as static final fields (see [the article on Blocks][block] for what parameters to add in `new Block()`): ```java -private static final DeferredRegister REGISTER = DeferredRegister.create(Registries.LOOT_CONDITION_TYPE, "examplemod"); - -public static final RegistryObject EXAMPLE_LOOT_ITEM_CONDITION_TYPE = REGISTER.register("example_loot_item_condition_type", () -> new LootItemConditionType(...)); +public static final DeferredHolder EXAMPLE_BLOCK = BLOCKS.register( + "example_block" // Our registry name. + () -> new Block(...) // A supplier of the object we want to register. +); ``` +The class `DeferredHolder` holds our object. The type parameter `R` is the type of the registry we are registering to (in our case `Block`). The type parameter `T` is the type of our supplier. Since we directly register a `Block` in this example, we provide `Block` as the second parameter. If we were to register an object of a subclass of `Block`, for example `SlabBlock`, we would provide `SlabBlock` here instead. + :::note -Some classes cannot by themselves be registered. Instead, `*Type` classes are registered, and used in the formers' constructors. For example, [`BlockEntity`][blockentity] has `BlockEntityType`, and `Entity` has `EntityType`. These `*Type` classes are factories that simply create the containing type on demand. +Some modders prefer to keep their `DeferredRegister`s in the same class as their registered objects. Others prefer keeping all `DeferredRegister`s in a separate class for readability. This is mostly a design decision, however if you decide to do the latter, make sure to classload the classes the objects are in, for example through an empty static method. +::: + +`DeferredHolder` is a subclass of `Supplier`. To get our registered object when we need it, we can call `DeferredHolder#get()`. The fact that `DeferredHolder` extends `Supplier` also allows us to use `Supplier` as the type of our field. That way, the above code block becomes the following: -These factories are created through the use of their `*Type$Builder` classes. An example: (`REGISTER` refers to a `DeferredRegister`) ```java -public static final RegistryObject> EXAMPLE_BLOCK_ENTITY = REGISTER.register( - "example_block_entity", () -> BlockEntityType.Builder.of(ExampleBlockEntity::new, EXAMPLE_BLOCK.get()).build(null) +public static final Supplier EXAMPLE_BLOCK = BLOCKS.register( + "example_block" // Our registry name. + () -> new Block(...) // A supplier of the object we want to register. ); ``` -::: - -Referencing Registered Objects ------------------------------- -Registered objects should not be stored in fields when they are created and registered. They are to be always newly created and registered whenever `RegisterEvent` is fired for that registry. This is to allow dynamic loading and unloading of mods in a future version of Forge. +Be aware that a few places explicitly require a `DeferredHolder` and will not just accept any `Supplier`. If you need a `DeferredHolder`, it is best to change the type of your `Supplier` back to `DeferredHolder`. -Registered objects must always be referenced through a `RegistryObject` or a field with `@ObjectHolder`. +Finally, since the entire system is a wrapper around registry events, we need to tell the `DeferredRegister` to attach itself to the registry events as needed: -### Using RegistryObjects +```java +//This is our mod constructor +public ExampleMod(IModEventBus bus) { + ExampleBlocksClass.BLOCKS.register(bus); + //Other stuff here +} +``` -`RegistryObject`s can be used to retrieve references to registered objects once they are available. These are used by `DeferredRegister` to return a reference to the registered objects. Their references are updated after `RegisterEvent` is called for their registry, along with the `@ObjectHolder` annotations. +:::info +There are specialized variants of `DeferredRegister`s for blocks and items that provide helper methods, called [`DeferredRegister.Blocks`][defregblocks] and [`DeferredRegister.Items`][defregitems], respectively. +::: -To get a `RegistryObject`, call `RegistryObject#create` with a `ResourceLocation` and the `IForgeRegistry` of the registrable object. Custom registries can also be used by supplying the registry name instead. Store the `RegistryObject` in a `public static final` field, and call `#get` whenever you need the registered object. +### `RegisterEvent` -An example of using `RegistryObject`: +`RegisterEvent` is the second way to register objects. This [event][event] is fired for each registry, after the mod constructors (since those are where `DeferredRegister`s register their internal event handlers) and before the loading of configs. `RegisterEvent` is fired on the mod event bus. ```java -public static final RegistryObject BOW = RegistryObject.create(new ResourceLocation("minecraft:bow"), ForgeRegistries.ITEMS); - -// assume that 'neomagicae:mana_type' is a valid registry, and 'neomagicae:coffeinum' is a valid object within that registry -public static final RegistryObject COFFEINUM = RegistryObject.create(new ResourceLocation("neomagicae", "coffeinum"), new ResourceLocation("neomagicae", "mana_type"), "neomagicae"); +@SubscribeEvent +public void register(RegisterEvent event) { + event.register( + // This is the registry key of the registry. + // Get these from Registries for vanilla registries, or from NeoForgeRegistries.Keys for NeoForge registries. + Registries.BLOCKS, + // Register your objects here. + registry -> { + registry.register(new ResourceLocation(MODID, "example_block_1"), new Block(...)); + registry.register(new ResourceLocation(MODID, "example_block_2"), new Block(...)); + registry.register(new ResourceLocation(MODID, "example_block_3"), new Block(...)); + } + ); +} ``` -### Using @ObjectHolder +## Custom Registries -Registered objects from registries can be injected into the `public static` fields by annotating classes or fields with `@ObjectHolder` and supplying enough information to construct a `ResourceLocation` to identify a specific object in a specific registry. +Custom registries allow you to specify additional systems that addon mods for your mod may want to plug into. For example, if your mod were to add spells, you could make the spells a registry and thus allow other mods to add spells to your mod, without you having to do anything else. It also allows you to do some things, such as syncing the entries, automatically. -The rules for `@ObjectHolder` are as follows: +Let's start by creating the [registry key][resourcekey] and the registry itself: -* If the class is annotated with `@ObjectHolder`, its value will be the default namespace for all fields within if not explicitly defined -* If the class is annotated with `@Mod`, the modid will be the default namespace for all annotated fields within if not explicitly defined -* A field is considered for injection if: - * it has at least the modifiers `public static`; - * the **field** is annotated with `@ObjectHolder`, and: - * the name value is explicitly defined; and - * the registry name value is explicitly defined - * _A compile-time exception is thrown if a field does not have a corresponding registry or name._ -* _An exception is thrown if the resulting `ResourceLocation` is incomplete or invalid (non-valid characters in path)_ -* If no other errors or exceptions occur, the field will be injected -* If all of the above rules do not apply, no action will be taken (and a message may be logged) +```java +// We use spells as an example for the registry here, without any details about what a spell actually is (as it doesn't matter). +// Of course, all mentions of spells can and should be replaced with whatever your registry actually is. +public static final ResourceKey> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(new ResourceLocation("yourmodid", "spells")); +public static final Registry SPELL_REGISTRY = new RegistryBuilder<>(SPELL_REGISTRY_KEY) + // If you want the registry to sync its values. + .sync(true) + // The default key. Similar to minecraft:air for blocks. + .defaultKey(new ResourceLocation("yourmodid", "empty")) + // Effectively limits the max count. Generally discouraged, but may make sense in settings such as networking. + .maxId(256) + // Build the registry. + .create(); +``` -`@ObjectHolder`-annotated fields are injected with their values after `RegisterEvent` is fired for their registry, along with the `RegistryObject`s. +Then, tell the game that the registry exists by registering them to the root registry in `NewRegistryEvent`: -:::note -If the object does not exist in the registry when it is to be injected, a debug message will be logged and no value will be injected. -::: +```java +@SubscribeEvent +static void registerRegistries(NewRegistryEvent event) { + event.register(SPELL_REGISTRY); +} +``` -As these rules are rather complicated, here are some examples: +You can now register new registry contents like with any other registry, through both `DeferredRegister` and `RegisterEvent`: ```java -class Holder { - @ObjectHolder(registryName = "minecraft:enchantment", value = "minecraft:flame") - public static final Enchantment flame = null; // Annotation present. [public static] is required. [final] is optional. - // Registry name is explicitly defined: "minecraft:enchantment" - // Resource location is explicitly defined: "minecraft:flame" - // To inject: "minecraft:flame" from the [Enchantment] registry - - public static final Biome ice_flat = null; // No annotation on the field. - // Therefore, the field is ignored. - - @ObjectHolder("minecraft:creeper") - public static Entity creeper = null; // Annotation present. [public static] is required. - // The registry has not been specified on the field. - // Therefore, THIS WILL PRODUCE A COMPILE-TIME EXCEPTION. - - @ObjectHolder(registryName = "potion") - public static final Potion levitation = null; // Annotation present. [public static] is required. [final] is optional. - // Registry name is explicitly defined: "minecraft:potion" - // Resource location is not specified on the field - // Therefore, THIS WILL PRODUCE A COMPILE-TIME EXCEPTION. +public static final DeferredRegister SPELLS = DeferredRegister.create("yourmodid", SPELL_REGISTRY); +public static final Supplier EXAMPLE_SPELL = SPELLS.register("example_spell", () -> new Spell(...)); + +// Alternatively: +@SubscribeEvent +public static void register(RegisterEvent event) { + event.register(SPELL_REGISTRY, registry -> { + registry.register(new ResourceLocation("yourmodid", "example_spell"), () -> new Spell(...)); + }); } ``` -Creating Custom Forge Registries --------------------------------- +## Datapack Registries -Custom registries can usually just be a simple map of key to value. This is a common style; however, it forces a hard dependency on the registry being present. It also requires that any data that needs to be synced between sides must be done manually. Custom Forge Registries provide a simple alternative for creating soft dependents along with better management and automatic syncing between sides (unless told otherwise). Since the objects also use a Forge registry, registration becomes standardized in the same way. +A datapack registry (also known as a dynamic registry or, after its main use case, worldgen registry) is a special kind of registry that loads data from [datapack][datapack] JSONs (hence the name) at world load, instead of loading them when the game starts. Default datapack registries include most worldgen registries, as well as any custom registry (see below) that is marked as a datapack registry. -Custom Forge Registries are created with the help of a `RegistryBuilder`, through either `NewRegistryEvent` or the `DeferredRegister`. The `RegistryBuilder` class takes various parameters (such as the registry's name, id range, and various callbacks for different events happening on the registry). New registries are registered to the `RegistryManager` after `NewRegistryEvent` finishes firing. +Datapack registries allow their contents to be specified in JSON files. This means that no code (other than [datagen][datagen] if you don't want to write the JSON files yourself) is necessary. Every datapack registry has a [`Codec`][codec] associated with it, which is used for serialization, and each registry's id determines its datapack path: -Any newly created registry should use its associated [registration method][registration] to register the associated objects. +- Minecraft's datapack registries use the format `data/yourmodid/registrypath` (for example `data/yourmodid/worldgen/biomes`, where `worldgen/biomes` is the registry path). +- All other datapack registries (NeoForge or modded) use the format `data/yourmodid/registrynamespace/registrypath` (for example `data/yourmodid/neoforge/loot_modifiers`, where `neoforge` is the registry namespace and `loot_modifiers` is the registry path). -### Using NewRegistryEvent +Datapack registries can be obtained from a `RegistryAccess`. This `RegistryAccess` can be retrieved by calling `ServerLevel#registryAccess()` if on the server, by calling `Minecraft.getInstance().connection#registryAccess()` if on the client, or from a `RegistryOps`. -When using `NewRegistryEvent`, calling `#create` with a `RegistryBuilder` will return a supplier-wrapped registry. The supplied registry can be accessed after `NewRegistryEvent` has finished posting to the mod event bus. Getting the custom registry from the supplier before `NewRegistryEvent` finishes firing will result in a `null` value. +### `RegistryOps` -#### New Datapack Registries +`RegistryOps` is a special [`DynamicOps`][dynamicops] made specifically for (de)serializing datapack registries. It provides additional registry context and enables the use of special codecs that can only be used with `RegistryOps`. Data generation of datapack registry elements must always be done through `RegistryOps` to convert elements to `JsonElement`s. -New datapack registries can be added using the `DataPackRegistryEvent$NewRegistry` event on the mod event bus. The registry is created via `#dataPackRegistry` by passing in the `ResourceKey` representing the registry name and the `Codec` used to encode and decode the data from JSON. An optional `Codec` can be provided to sync the datapack registry to the client. +A `RegistryOps` can be created via `RegistryOps.create(JsonElement.INSTANCE, RegistryAccess.builtinCopy())`. `RegistryAccess.builtinCopy()` creates a set of writable datapack registries, which is necessary for datagenning unregistered objects. All data generation done in a `GatherDataEvent` handler must use the same `RegistryAccess` and `RegistryOps` instances, otherwise obscure errors will occur. -:::note -Datapack Registries cannot be created with `DeferredRegister`. They can only be created through the event. -::: +### `Holder`s -### With DeferredRegister +As mentioned before, (normal) registries rely on `DeferredHolder`s, which are a special kind of `Holder`. A `Holder` vaguely resembles a `Pair` that either starts with a key and has a value bound later, or starts with a value and may have a key bound later. Datapack registries extensively rely on (non-deferred) `Holder`s to reference registry elements of other registries. For example, `Biome`s refer to `Holder`s, and `PlacedFeature`s refer to `Holder`s. -The `DeferredRegister` method is once again another wrapper around the above event. Once a `DeferredRegister` is created in a constant field using the `#create` overload which takes in the registry name and the mod id, the registry can be constructed via `DeferredRegister#makeRegistry`. This takes in a supplied `RegistryBuilder` containing any additional configurations. The method already populates `#setName` by default. Since this method can be returned at any time, a supplied version of an `IForgeRegistry` is returned instead. Getting the custom registry from the supplier before `NewRegistryEvent` is fired will result in a `null` value. +During data generation, we can use `RegistryOps#registry` to get a registry, and `Registry#getOrCreateHolderOrThrow()` to produce key-only reference holders (we only need the key in this case, since holder codecs only encode keys when using a `RegistryOps` in order to prevent circular dependencies). -:::caution -`DeferredRegister#makeRegistry` must be called before the `DeferredRegister` is added to the mod event bus via `#register`. `#makeRegistry` also uses the `#register` method to create the registry during `NewRegistryEvent`. -::: - -Handling Missing Entries ------------------------- +### `JsonCodecProvider` -There are cases where certain registry objects will cease to exist whenever a mod is updated or, more likely, removed. It is possible to specify actions to handle the missing mapping through the third of the registry events: `MissingMappingsEvent`. Within this event, a list of missing mappings can be obtained either by `#getMappings` given a registry key and mod id or all mappings via `#getAllMappings` given a registry key. +NeoForge provides a data provider for datapack registry elements that, given a registry key and a map of objects to generate, generates all JSON files for the objects in the map. -:::caution -`MissingMappingsEvent` is fired on the **Forge** event bus. -::: +```java +@SubscribeEvent +static void onGatherData(GatherDataEvent event) { + DataGenerator generator = event.getDataGenerator(); + ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); + RegistryOps registryOps = RegistryOps.create(JsonElement.INSTANCE, RegistryAccess.builtinCopy()); + + Map map = Map.of( + // Whatever entries you want. For example: + new ResourceLocation("yourmodid", "sponge_everywhere"), new PlacedFeature(...) + ); + + JsonCodecProvider provider = JsonCodecProvider.forDatapackRegistry( + generator, + existingFileHelper, + "yourmodid", + registryOps, + // The registry you want to generate in. + Registry.PLACED_FEATURE_REGISTRY, + // The elements to generate. + map + ); + + generator.addProvider(event.includeServer(), provider); +} +``` -For each `Mapping`, one of four mapping types can be selected to handle the missing entry: +### Custom Datapack Registries -| Action | Description | -| :---: | :--- | -| IGNORE | Ignores the missing entry and abandons the mapping. | -| WARN | Generates a warning in the log. | -| FAIL | Prevents the world from loading. | -| REMAP | Remaps the entry to an already registered, non-null object. | +Custom datapack registries are created through `RegistryBuilder` like all other registries, but are registered to `DataPackRegistryEvent.NewRegistry` instead of `NewRegistryEvent`. Reiterating the spells example from before, registering our spell registry as a datapack registry looks something like this: -If no action is specified, then the default action will occur by notifying the user about the missing entry and whether they still would like to load the world. All actions besides remapping will prevent any other registry object from taking the place of the existing id in case the associated entry ever gets added back into the game. +```java +@SubscribeEvent +static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) { + event.register( + // The registry key. + SPELL_REGISTRY_KEY, + // The codec of the registry contents. + Spell.CODEC, + // The network codec of the registry contents. Often identical to the normal codec. + // May be a reduced variant of the normal codec that omits data that is not needed on the client. + // May be null. If null, registry entries will not be synced to the client at all. + // May be omitted, which is functionally identical to passing null (a method overload + // with two parameters is called that passes null to the normal three parameter method). + Spell.CODEC + ); +} +``` -[ResourceLocation]: ./resources.md#resourcelocation -[registration]: #methods-for-registering -[event]: ./events.md +[block]: ../blocks/index.md [blockentity]: ../blockentities/index.md +[codec]: ../datastorage/codecs.md +[datagen]: ../datagen/index.md +[datapack]: ../resources/server/index.md +[defregblocks]: ../blocks/index.md#deferredregisterblocks +[defregitems]: ../items/index.md#deferredregisteritems +[dynamicops]: ../datastorage/codecs.md#dynamicops +[event]: ./events.md +[item]: ../items/index.md +[resloc]: ../misc/resourcelocation.md +[resourcekey]: ../misc/resourcelocation.md#resourcekeys +[singleton]: https://en.wikipedia.org/wiki/Singleton_pattern diff --git a/docs/concepts/resources.md b/docs/concepts/resources.md deleted file mode 100644 index d0d0b207..00000000 --- a/docs/concepts/resources.md +++ /dev/null @@ -1,21 +0,0 @@ -Resources -========= - -A resource is extra data used by the game, and is stored in a data file, instead of being in the code. -Minecraft has two primary resource systems active: one on the logical client used for visuals such as models, textures, and localization called `assets`, and one on the logical server used for gameplay such as recipes and loot tables called `data`. -[Resource packs][respack] control the former, while [Datapacks][datapack] control the latter. - -In the default mod development kit, assets and data directories are located under the `src/main/resources` directory of the project. - -When multiple resource packs or data packs are enabled, they are merged. Generally, files from packs at the top of the stack override those below; however, for certain files, such as localization files and tags, data is actually merged contentwise. Mods define resource and data packs in their `resources` directories, but they are seen as subsets of the "Mod Resources" pack. Mod resource packs cannot be disabled, but they can be overridden by other resource packs. Mod datapacks can be disabled with the vanilla `/datapack` command. - -All resources should have snake case paths and filenames (lowercase, using "_" for word boundaries), which is enforced in 1.11 and above. - -`ResourceLocation` ------------------- - -Minecraft identifies resources using `ResourceLocation`s. A `ResourceLocation` contains two parts: a namespace and a path. It generally points to the resource at `assets///`, where `ctx` is a context-specific path fragment that depends on how the `ResourceLocation` is being used. When a `ResourceLocation` is written/read as from a string, it is seen as `:`. If the namespace and the colon are left out, then when the string is read into an `ResourceLocation` the namespace will always default to `"minecraft"`. A mod should put its resources into a namespace with the same name as its mod id (e.g. a mod with the id `examplemod` should place its resources in `assets/examplemod` and `data/examplemod` respectively, and `ResourceLocation`s pointing to those files would look like `examplemod:`.). This is not a requirement, and in some cases it can be desirable to use a different (or even more than one) namespace. `ResourceLocation`s are used outside the resource system, too, as they happen to be a great way to uniquely identify objects (e.g. [registries][]). - -[respack]: ../resources/client/index.md -[datapack]: ../resources/server/index.md -[registries]: ./registries.md diff --git a/docs/gameeffects/sounds.md b/docs/gameeffects/sounds.md index 070f108b..c38bac81 100644 --- a/docs/gameeffects/sounds.md +++ b/docs/gameeffects/sounds.md @@ -104,7 +104,7 @@ Note that each takes a `SoundEvent`, the ones registered above. Additionally, th - **Server Behavior**: Method is client-only. - **Usage**: Just like the ones in `Level`, these two overrides in the player classes seem to be for code that runs together on both sides. The client handles playing the sound to the user, while the server handles everyone else hearing it without re-playing to the original user. -[loc]: ../concepts/resources.md#resourcelocation +[loc]: ../misc/resourcelocation.md [wiki]: https://minecraft.wiki/w/Sounds.json [datagen]: ../datagen/client/sounds.md [registration]: ../concepts/registries.md#methods-for-registering diff --git a/docs/items/index.md b/docs/items/index.md index 12deb27a..ef37d90e 100644 --- a/docs/items/index.md +++ b/docs/items/index.md @@ -54,6 +54,53 @@ Directly using `Item` only allows for very basic items. If you want to add funct The two most common use cases for items are left-clicking and right-clicking. For left-clicking, see [Breaking a Block][breaking] and Attacking an Entity (WIP). For right-clicking, see [The Interaction Pipeline][interactionpipeline]. +### `DeferredRegister.Items` + +All registries use `DeferredRegister` to register their contents, and items are no exceptions. However, due to the fact that adding new items is such an essential feature of an overwhelming amount of mods, NeoForge provides the `DeferredRegister.Items` helper class that extends `DeferredRegister` and provides some item-specific helpers: + +```java +public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(ExampleMod.MOD_ID); + +public static final Supplier EXAMPLE_ITEM = ITEMS.registerItem( + "example_item", + Item::new, // The factory that the properties will be passed into. + new Item.Properties() // The properties to use. +); +``` + +Internally, this will simply call `ITEMS.register("example_item", () -> new Item(new Item.Properties()))` by applying the properties parameter to the provided item factory (which is commonly the constructor). + +If you want to use `Item::new`, you can leave out the factory entirely: + +```java +public static final Supplier EXAMPLE_ITEM = ITEMS.registerItem( + "example_item", + new ItemBehaviour.Properties() // The properties to use. +); +``` + +This does the exact same as the previous example, but is slightly shorter. Of course, if you want to use a subclass of `Item` and not `Item` itself, you will have to use the previous method instead. + +Both of these methods also have `simple` counterparts that omit the `new Item.Properties()` parameter: + +```java +public static final Supplier EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item", Item::new); +// Variant that also omits the Item::new parameter +public static final Supplier EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item"); +``` + +Finally, there's also shortcuts for block items: + +```java +public static final Supplier EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem("example_block", ExampleBlocksClass.EXAMPLE_BLOCK, new Item.Properties()); +// Variant that omits the properties parameter: +public static final Supplier EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem("example_block", ExampleBlocksClass.EXAMPLE_BLOCK); +// Variant that omits the name parameter, instead using the block's registry name: +public static final Supplier EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(ExampleBlocksClass.EXAMPLE_BLOCK, new Item.Properties()); +// Variant that omits both the name and the properties: +public static final Supplier EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(ExampleBlocksClass.EXAMPLE_BLOCK); +``` + ### Resources If you register your item and get your item (via `/give` or through a [creative tab][creativetabs]), you will find it to be missing a proper model and texture. This is because textures and models are handled by Minecraft's resource system. diff --git a/docs/misc/resourcelocation.md b/docs/misc/resourcelocation.md new file mode 100644 index 00000000..9b2d07fb --- /dev/null +++ b/docs/misc/resourcelocation.md @@ -0,0 +1,46 @@ +# Resource Locations + +`ResourceLocation`s are one of the most important things in Minecraft. They are used as keys in [registries][registries], as identifiers for data or resource files, as references to models in code, and in a lot of other places. A `ResourceLocation` consists of two parts: a namespace and a path, separated by a `:`. + +The namespace denotes what mod or datapack the location refers to. For example, a mod with the mod id `examplemod` will use the `examplemod` namespace. Minecraft uses the `minecraft` namespace. Extra namespaces can be defined at will simply by creating a corresponding data folder, this is usually done by datapacks to keep their logic separate from the point where they integrate with vanilla. + +The path is a reference to whatever object you want, inside your namespace. For example, `minecraft:cow` is a reference to something named `cow` in the `minecraft` namespace - usually this location would be used to get the cow entity from the entity registry. Another example would be `examplemod:example_item`, which would probably be used to get your mod's `example_item` from the item registry. + +`ResourceLocation`s may only contain lowercase letters, digits, underscores, dots and hyphens. Paths may additionally contain forward slashes. Note that due to Java module restrictions, mod ids may not contain hyphens, which by extension means that mod namespaces may not contain hyphens either (they are still permitted in paths). + +:::info +A `ResourceLocation` on its own says nothing about what kind of objects we are using it for. Objects named `minecraft:dirt` exist in multiple places, for example. It is up to whatever receives the `ResourceLocation` to associate an object with it. +::: + +A new `ResourceLocation` can be created by calling `new ResourceLocation("examplemod", "example_item")` or `new ResourceLocation("examplemod:example_item")`. If the latter is used with a string that does not contain a `:`, the entire string will be used as the path, and `minecraft` will be used as the namespace. So for example, `new ResourceLocation("example_item")` will result in `minecraft:example_item`. + +The namespace and path of a `ResourceLocation` can be retrieved using `ResourceLocation#getNamespace()` and `#getPath()`, respectively, and the combined form can be retrieved through `ResourceLocation#toString`. + +`ResourceLocation`s are immutable. All utility methods on `ResourceLocation`, such as `withPrefix` or `withSuffix`, return a new `ResourceLocation`. + +## Resolving `ResourceLocation`s + +Some places, for example registries, use `ResourceLocation`s directly. Some other places, however, will resolve the `ResourceLocation` as needed. For example: + +- `ResourceLocation`s are used as identifiers for GUI background. For example, the furnace GUI uses the resource location `minecraft:textures/gui/container/furnace.png`. This maps to the file `assets/minecraft/textures/gui/container/furnace.png` on disk. Note that the `.png` suffix is required in this resource location. +- `ResourceLocation`s are used as identifiers for block models. For example, the block model of dirt uses the resource location `minecraft:models/block/dirt`. This maps to the file `assets/minecraft/models/block/dirt.json` on disk. Note that the `.json` suffix is NOT required in this resource location. +- `ResourceLocation`s are used as identifiers for recipes. For example, the iron block crafting recipe uses the resource location `minecraft:iron_block`. This maps to the file `data/minecraft/recipes/iron_block.json` on disk. Note that the `.json` suffix is not required here. Note as well that this resource location automatically maps into the `recipes` subfolder. + +Whether the `ResourceLocation` expects a file suffix, or what exactly the resource location resolves to, depends on the use case. + +## `ModelResourceLocation`s + +`ModelResourceLocation`s are a special kind of resource location that includes a third part, called the variant. Minecraft uses these mainly to differentiate between different variants of item models, where the different variants are used in different display contexts (for example with tridents, which have different models in first person, third person and inventories). + +The variant is appended to the regular resource location, along with a `#`. For example, the full name of the diamond sword's item model is `minecraft:diamond_sword#inventory`. However, in most contexts, the `inventory` variant can be omitted. + +`ModelResourceLocation` is a [client only][sides] class. This means that servers referencing this class will crash with a `NoClassDefError`. + +## `ResourceKey`s + +`ResourceKey`s combine a registry id with a registry name. An example would be a registry key with the registry id `minecraft:item` and the registry name `minecraft:diamond_sword`. Unlike a `ResourceLocation`, `ResourceKey`s actually refer to a unique element, thus being able to clearly identify an element. They are most commonly used in contexts where many different registries come in contact with one another. A common use case are datapacks, especially worldgen. + +A new `ResourceKey` can be created through the static method `ResourceKey#create(ResourceKey>, ResourceLocation)`. The second parameter here is the registry name, while the first parameter is what is known as a registry key. Registry keys are a special kind of `ResourceKey` whose registry is the root registry (i.e. the registry of all other registries). A registry key can be created via `ResourceKey#createRegistryKey(ResourceLocation)` with the desired registry's id. + +[registries]: ../concepts/registries.md +[sides]: ../concepts/sides.md diff --git a/docs/resources/client/index.md b/docs/resources/client/index.md index f8ea1ed7..c81a5637 100644 --- a/docs/resources/client/index.md +++ b/docs/resources/client/index.md @@ -12,4 +12,4 @@ Additional reading: [Resource Locations][resourcelocation] [respack]: https://minecraft.wiki/w/Resource_Pack [createrespack]: https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack -[resourcelocation]: ../../concepts/resources.md#ResourceLocation +[resourcelocation]: ../../misc/resourcelocation.md diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index a999c562..d9f2df9c 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -17,7 +17,7 @@ JSON models only support cuboid elements; there is no way to express a triangula Textures, like models, are contained within resource packs and are referred to with `ResourceLocation`s. In Minecraft, the [UV coordinates][uv] (0,0) are taken to mean the **top-left** corner. UVs are *always* from 0 to 16. If a texture is larger or smaller, the coordinates are scaled to fit. A texture should also be square, and the side length of a texture should be a power of two, as doing otherwise breaks mipmapping (e.g. 1x1, 2x2, 8x8, 16x16, and 128x128 are good. 5x5 and 30x30 are not recommended because they are not powers of 2. 5x10 and 4x8 are completely broken as they are not square.). Textures should only ever be not a square if it is [animated][animated]. [models]: https://minecraft.wiki/w/Tutorials/Models#File_path -[resloc]: ../../../concepts/resources.md#resourcelocation +[resloc]: ../../../misc/resourcelocation.md [statemodel]: https://minecraft.wiki/w/Tutorials/Models#Block_states [itemmodels]: https://minecraft.wiki/w/Tutorials/Models#Item_models [state]: ../../../blocks/states.md diff --git a/docs/resources/server/glm.md b/docs/resources/server/glm.md index 8676f1f1..b935aac0 100644 --- a/docs/resources/server/glm.md +++ b/docs/resources/server/glm.md @@ -140,7 +140,7 @@ public static final RegistryObject> = REGISTRAR.register( [Examples][examples] can be found on the Forge Git repository, including silk touch and smelting effects. [tags]: ./tags.md -[resloc]: ../../concepts/resources.md#ResourceLocation +[resloc]: ../../misc/resourcelocation.md [codec]: #the-loot-modifier-codec [registered]: ../../concepts/registries.md#methods-for-registering [codecdef]: ../../datastorage/codecs.md diff --git a/docs/resources/server/index.md b/docs/resources/server/index.md index bc5b9313..3c097338 100644 --- a/docs/resources/server/index.md +++ b/docs/resources/server/index.md @@ -11,4 +11,4 @@ Additional reading: [Resource Locations][resourcelocation] [datapack]: https://minecraft.wiki/w/Data_pack [createdatapack]: https://minecraft.wiki/w/Tutorials/Creating_a_data_pack -[resourcelocation]: ../../concepts/resources.md#ResourceLocation +[resourcelocation]: ../../misc/resourcelocation.md From aafbf1baf838fb11fdbc927843d41c852c2d85ae Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 29 Jan 2024 20:05:38 +0100 Subject: [PATCH 02/41] rework side documentation --- docs/concepts/sides.md | 138 ++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 92 deletions(-) diff --git a/docs/concepts/sides.md b/docs/concepts/sides.md index c0cd4e79..c6a12221 100644 --- a/docs/concepts/sides.md +++ b/docs/concepts/sides.md @@ -1,121 +1,75 @@ -Sides in Minecraft -=================== +# Sides -A very important concept to understand when modding Minecraft are the two sides: *client* and *server*. There are many, many common misconceptions and mistakes regarding siding, which can lead to bugs that might not crash the game, but can rather have unintended and often unpredictable effects. +Like many other programs, Minecraft follows a client-server concept, where the client is responsible for displaying the data, while the server is responsible for updating them. When using these terms, we have a fairly intuitive understanding of what we mean... right? -Different Kinds of Sides ------------------------- +Turns out, not so much. A lot of the confusion stems from Minecraft having two different concepts of sides, depending on the context: the physical and the logical side. -When we say "client" or "server", it usually follows with a fairly intuitive understanding of what part of the game we are talking about. After all, a client is what the user interacts with, and a server is where the user connects for a multiplayer game. Easy, right? +## Logical vs. Physical Side -As it turns out, there can be some ambiguity even with two such terms. Here we disambiguate the four possible meanings of "client" and "server": +### The Physical Side -* Physical client - The *physical client* is the entire program that runs whenever you launch Minecraft from the launcher. All threads, processes, and services that run during the game's graphical, interactable lifetime are part of the physical client. -* Physical server - Often known as the dedicated server, the *physical server* is the entire program that runs whenever you launch any sort of `minecraft_server.jar` that does not bring up a playable GUI. -* Logical server - The *logical server* is what runs game logic: mob spawning, weather, updating inventories, health, AI, and all other game mechanics. The logical server is present within a physical server, but it also can run inside a physical client together with a logical client, as a single player world. The logical server always runs in a thread named the `Server Thread`. -* Logical client - The *logical client* is what accepts input from the player and relays it to the logical server. In addition, it also receives information from the logical server and makes it available graphically to the player. The logical client runs in the `Render Thread`, though often several other threads are spawned to handle things like audio and chunk render batching. +When you open your Minecraft launcher, select a Minecraft installation and press play, you boot up a **physical client**. The word "physical" is used here in the sense of "this is a client program". This especially means that client-side functionality, such as all the rendering stuff, is available here and can be used as needed. The **physical server**, on the other hand, is what opens when you launch a Minecraft server JAR. While the Minecraft server comes with a rudimentary GUI, it is missing all client-only functionality. Most notably, this means that various client classes are missing from the server JAR. Calling these classes on the physical server will lead to missing class errors, i.e. crashes, so we need to safeguard against this. -In the NeoForge codebase, the physical side is represented by an enum called `Dist`, while the logical side is represented by an enum called `LogicalSide`. - -Performing Side-Specific Operations ------------------------------------ - -### `Level#isClientSide` +### The Logical Side -This boolean check will be your most used way to check sides. Querying this field on a `Level` object establishes the **logical** side the level belongs to. That is, if this field is `true`, the level is currently running on the logical client. If the field is `false`, the level is running on the logical server. It follows that the physical server will always contain `false` in this field, but we cannot assume that `false` implies a physical server, since this field can also be `false` for the logical server inside a physical client (in other words, a single player world). - -Use this check whenever you need to determine if game logic and other mechanics should be run. For example, if you want to damage the player every time they click your block, or have your machine process dirt into diamonds, you should only do so after ensuring `#isClientSide` is `false`. Applying game logic to the logical client can cause desynchronization (ghost entities, desynchronized stats, etc.) in the best case, and crashes in the worst case. +The logical side is mainly focused on the internal program structure of Minecraft. The **logical server** is where the game logic runs. Things like time and weather changing, entity ticking, entity spawning, etc. all run on the server. All kinds of data, such as inventory contents, are the server's responsibility as well. The **logical client**, on the other hand, is responsible for displaying everything there is to display. Minecraft keeps all the client code in an isolated `net.minecraft.client` package, and runs it in a separate thread called the Render Thread, while everything else is considered common (i.e. client and server) code. -This check should be used as your go-to default. Aside from `DistExecutor`, rarely will you need the other ways of determining side and adjusting behavior. +### What's the Difference? -### `DistExecutor` +The difference between physical and logical sides is best exemplified by two scenarios: -Considering the use of a single "universal" jar for client and server mods, and the separation of the physical sides into two jars, an important question comes to mind: How do we use code that is only present on one physical side? All code in `net.minecraft.client` is only present on the physical client. If any class you write references those names in any way, they will crash the game when that respective class is loaded in an environment where those names do not exist. A very common mistake in beginners is to call `Minecraft.getInstance().()` in block or block entity classes, which will crash any physical server as soon as the class is loaded. +- The player joins a **multiplayer** world. This is fairly straightforward: The player's logical and physical client connects to a logical server somewhere else - the player does not care where; so long as they can connect, that's all the client knows of, and all the client needs to know. +- The player joins a **singleplayer** world. This is where things get interesting. The player's physical client spins up a logical server and then, now in the role of the logical client, connects to that logical server on the same machine. If you are familiar with networking, you can think of it as a connection to `localhost` (only conceptually; there are no actual sockets or similar involved). -How do we resolve this? Luckily, FML has `DistExecutor`, which provides various methods to run different methods on different physical sides, or a single method only on one side. +These two scenarios also show the main problem with this: If a logical server can work with your code, that alone doesn't guarantee that a physical server will be able to work with as well. This is why you should always test with dedicated servers to check for unexpected behavior. `NoClassDefFoundError`s and `ClassNotFoundException`s due to incorrect client and server separation are among the most common errors there are in modding. Another common mistake is working with static fields and accessing them from both logical sides; this is particularly tricky because there's usually no indication that something is wrong. -:::note -It is important to understand that FML checks based on the **physical** side. A single player world (logical server + logical client within a physical client) will always use `Dist.CLIENT`! +:::tip +If you need to transfer data from one side to another, you must [send a packet][networking]. ::: -`DistExecutor` works by taking in a supplied supplier executing a method, effectively preventing classloading by taking advantage of the [`invokedynamic` JVM instruction][invokedynamic]. The executed method should be static and within a different class. Additionally, if no parameters are present for the static method, a method reference should be used instead of a supplier executing a method. - -There are two main methods within `DistExecutor`: `#runWhenOn` and `#callWhenOn`. The methods take in the physical side the executing method should run on and the supplied executing method which either runs or returns a result respectively. - -These two methods are subdivided further into `#safe*` and `#unsafe*` variants. Safe and unsafe variants are misnomers for their purposes. The main difference is that when in a development environment, the `#safe*` methods will validate that the supplied executing method is a lambda returning a method reference to another class with an error being thrown otherwise. Within the production environment, `#safe*` and `#unsafe*` are functionally the same. - -```java -// In a client class: ExampleClass -public static void unsafeRunMethodExample(Object param1, Object param2) { - // ... -} - -public static Object safeCallMethodExample() { - // ... -} - -// In some common class -DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> ExampleClass.unsafeRunMethodExample(var1, var2)); - -DistExecutor.safeCallWhenOn(Dist.CLIENT, () -> ExampleClass::safeCallMethodExample); - -``` +In the NeoForge codebase, the physical side is represented by an enum called `Dist`, while the logical side is represented by an enum called `LogicalSide`. -:::caution -Due to a change in how `invokedynamic` works in Java 9+, all `#safe*` variants of the `DistExecutor` methods throw the original exception wrapped within a `BootstrapMethodError` in the development environment. `#unsafe*` variants or a check to [`FMLEnvironment#dist`][dist] should be used instead. +:::info +Historically, server JARs have had classes the client did not. This is not the case anymore in modern versions; physical servers are a subset of physical clients, if you will. ::: -### Thread Groups +## Performing Side-Specific Operations -If `Thread.currentThread().getThreadGroup() == SidedThreadGroups.SERVER` is true, it is likely the current thread is on the logical server. Otherwise, it is likely on the logical client. This is useful to retrieve the **logical** side when you do not have access to a `Level` object to check `isClientSide`. It *guesses* which logical side you are on by looking at the group of the currently running thread. Because it is a guess, this method should only be used when other options have been exhausted. In nearly every case, you should prefer checking `Level#isClientSide`. +### `Level#isClientSide()` -### `FMLEnvironment#dist` and `@OnlyIn` +This boolean check will be your most used way to check sides. Querying this field on a `Level` object establishes the **logical** side the level belongs to: If this field is `true`, the level is running on the logical client. If the field is `false`, the level is running on the logical server. It follows that the physical server will always contain `false` in this field, but we cannot assume that `false` implies a physical server, since this field can also be `false` for the logical server inside a physical client (i.e. a singleplayer world). -`FMLEnvironment#dist` holds the **physical** side your code is running on. Since it is determined at startup, it does not rely on guessing to return its result. The number of use cases for this is limited, however. - -Annotating a method or field with the `@OnlyIn(Dist)` annotation indicates to the loader that the respective member should be completely stripped out of the definition not on the specified **physical** side. Usually, these are only seen when browsing through the decompiled Minecraft code, indicating methods that the Mojang obfuscator stripped out. There is **NO** reason for using this annotation directly. Use `DistExecutor` or a check on `FMLEnvironment#dist` instead. - -Common Mistakes ---------------- - -### Reaching Across Logical Sides - -Whenever you want to send information from one logical side to another, you must **always** use network packets. It is incredibly tempting, when in a single player scenario, to directly transfer data from the logical server to the logical client. - -This is actually very commonly inadvertently done through static fields. Since the logical client and logical server share the same JVM in a single player scenario, both threads writing to and reading from static fields will cause all sorts of race conditions and the classical issues associated with threading. - -This mistake can also be made explicitly by accessing physical client-only classes such as `Minecraft` from common code that runs or can run on the logical server. This mistake is easy to miss for beginners who debug in a physical client. The code will work there, but it will immediately crash on a physical server. - - -Writing One-Sided Mods ----------------------- - -In recent versions, Minecraft Forge has removed a "sidedness" attribute from the mods.toml. This means that your mods are expected to work whether they are loaded on the physical client or the physical server. So for one-sided mods, you would typically register your event handlers inside a `DistExecutor#safeRunWhenOn` or `DistExecutor#unsafeRunWhenOn` instead of directly calling the relevant registration methods in your mod constructor. Basically, if your mod is loaded on the wrong side, it should simply do nothing, listen to no events, and so on. A one-sided mod by nature should not register blocks, items, ... since they would need to be available on the other side, too. +Use this check whenever you need to determine if game logic and other mechanics should be run. For example, if you want to damage the player every time they click your block, or have your machine process dirt into diamonds, you should only do so after ensuring `#isClientSide` is `false`. Applying game logic to the logical client can cause desynchronization (ghost entities, desynchronized stats, etc.) in the best case, and crashes in the worst case. -Additionally, if your mod is one-sided, it typically does not forbid the user from joining a server that is lacking that mod. Therefore, you should set the `displayTest` property in your [mods.toml][structuring] to whatever value is necessary. +:::tip +This check should be used as your go-to default. Whenever you have a `Level` available, use this check. +::: -```toml -[[mods]] - # ... +### `FMLEnvironment.dist` - # MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. - # IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. - # IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. - # NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. - # IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. - displayTest="IGNORE_ALL_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) -``` +`FMLEnvironment.dist` is the **physical** counterpart to a `Level#isClientSide()` check. If this field is `Dist.CLIENT`, you are on a physical client. If the field is `Dist.SERVER`, you are on a physical server. -If a custom display test is to be used, then the `displayTest` option should be set to `NONE`, and an `IExtensionPoint$DisplayTest` extension should be registered: +Checking the physical environment is important when dealing with client-only classes. All calls to client-only code should always be encased in a check for `Dist.CLIENT`, and then call to a separate class to prevent accidental classloading: ```java -//Make sure the mod being absent on the other network side does not cause the client to display the server as incompatible -ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> NetworkConstants.IGNORESERVERONLY, (a, b) -> true)); -``` +public class SomeCommonClass { + public void someCommonMethod() { + //SomeClientClass will be loaded if and only if you are on a physical client + if (FMLEnvironment.dist == Dist.CLIENT) { + SomeClientClass.someClientMethod(); + } + } +} -This tells the client that it should ignore the server version being absent, and the server that it should not tell the client this mod should be present. So this snippet works both for client- and server-only-sided mods. +public class SomeClientClass { + public void someClientMethod() { + Minecraft.getInstance().whatever(); + } +} +``` +:::tip +Mods are generally expected to work on either side. This especially means that if you are developing a client-only mod, you should verify that the mod actually runs on a physical client, and no-op in the event that it does not. +::: -[invokedynamic]: https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-6.html#jvms-6.5.invokedynamic -[dist]: #fmlenvironmentdist-and-onlyin -[structuring]: ../gettingstarted/modfiles.md#modstoml +[networking]: ../networking/index.md From a7483045414cef99e73ad73e716b512b8740bc59 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 29 Jan 2024 22:21:36 +0100 Subject: [PATCH 03/41] rework events and lifecycle documentation --- docs/concepts/events.md | 208 +++++++++++++++++++++---------------- docs/concepts/lifecycle.md | 77 -------------- docs/concepts/sides.md | 2 +- 3 files changed, 118 insertions(+), 169 deletions(-) delete mode 100644 docs/concepts/lifecycle.md diff --git a/docs/concepts/events.md b/docs/concepts/events.md index 148eee0d..aaf7dce9 100644 --- a/docs/concepts/events.md +++ b/docs/concepts/events.md @@ -1,148 +1,174 @@ -Events -====== +# Events -Forge uses an event bus that allows mods to intercept events from various Vanilla and mod behaviors. +One of NeoForge's main features is the event system. Events are fired for various things that happen in the game. For example, there are events for when the player right clicks, when a player or another entity jumps, when blocks are rendered, when the game is loaded, etc. A modder can subscribe event handlers to each of these events, and then perform their desired behavior inside these event handlers. -Example: An event can be used to perform an action when a Vanilla stick is right-clicked. +Events are fired on their respective event bus. The most important bus is `NeoForge.EVENT_BUS`. Besides that, during startup, a mod bus is spawned for each loaded mod and passed into the mod's constructor; mod bus event handlers can run in parallel, dramatically increasing startup speed. See [below][modbus] for more information. -The main event bus used for most events is located at `NeoForge#EVENT_BUS`. There is another event bus for mod specific events that you should only use in specific cases. More information about this bus can be [found below](#mod-event-bus). +## Registering an Event Handler -Every event is fired on one of these busses: most events are fired on the main forge event bus, but some are fired on the mod specific event buses. +There are multiple ways to register event handlers. Common for all of those ways is that every event handler is a method with a single event parameter and no result (i.e. return type `void`). -An event handler is some method that has been registered to an event bus. +### `IEventBus#addListener` -Creating an Event Handler -------------------------- - -Event handlers methods have a single parameter and do not return a result. The method could be static or instance depending on implementation. - -Event handlers can be directly registered using `IEventBus#addListener` for or `IEventBus#addGenericListener` for generic events (as denoted by subclassing `GenericEvent`). Either listener adder takes in a consumer representing the method reference. Generic event handlers need to specify the class of the generic as well. Event handlers must be registered within the constructor of the main mod class. +The simplest way to register method handlers is by registering their method reference, like so: ```java -// In the main mod class ExampleMod - -// This event is on the mod bus -private void modEventHandler(RegisterEvent event) { - // Do things here -} - -// This event is on the forge bus -private static void forgeEventHandler(ExplosionEvent.Detonate event) { - // ... +@Mod("yourmodid") +public class YourMod { + public YourMod(IEventBus modBus) { + NeoForge.EVENT_BUS.addListener(YourMod::onLivingJump); + } + + // Heals an entity by half a heart every time they jump. + private static void onLivingJump(LivingJumpEvent event) { + event.getEntity().heal(1); + } } - -// In the mod constructor -modEventBus.addListener(this::modEventHandler); -forgeEventBus.addListener(ExampleMod::forgeEventHandler); ``` -### Instance Annotated Event Handlers +### `@SubscribeEvent` -This event handler listens for the `EntityItemPickupEvent`, which is, as the name states, posted to the event bus whenever an `Entity` picks up an item. +Alternatively, event handlers can be annotation-driven by creating an event handler method and annotating it with `@SubscribeEvent`. Then, you can pass an instance of the encompassing class to the event bus, registering all `@SubscribeEvent`-annotated event handlers of that instance: ```java -public class MyForgeEventHandler { +public class EventHandler { @SubscribeEvent - public void pickupItem(EntityItemPickupEvent event) { - System.out.println("Item picked up!"); - } + public void onLivingJump(LivingJumpEvent event) { + event.getEntity().heal(1); + } } -``` - -To register this event handler, use `NeoForge.EVENT_BUS.register(...)` and pass it an instance of the class the event handler is within. If you want to register this handler to the mod specific event bus, you should use `FMLJavaModLoadingContext.get().getModEventBus().register(...)` instead. -### Static Annotated Event Handlers +@Mod("yourmodid") +public class YourMod { + public YourMod(IEventBus modBus) { + NeoForge.EVENT_BUS.addListener(new EventHandler()); + } +} +``` -An event handler may also be static. The handling method is still annotated with `@SubscribeEvent`. The only difference from an instance handler is that it is also marked `static`. In order to register a static event handler, an instance of the class won't do. The `Class` itself has to be passed in. An example: +You can also do it statically. Simply make all event handlers static, and instead of a class instance, pass in the class itself: ```java -public class MyStaticForgeEventHandler { +public class EventHandler { @SubscribeEvent - public static void arrowNocked(ArrowNockEvent event) { - System.out.println("Arrow nocked!"); - } + public static void onLivingJump(LivingJumpEvent event) { + event.getEntity().heal(1); + } } -``` -which must be registered like this: `NeoForge.EVENT_BUS.register(MyStaticForgeEventHandler.class)`. +@Mod("yourmodid") +public class YourMod { + public YourMod(IEventBus modBus) { + NeoForge.EVENT_BUS.addListener(EventHandler.class); + } +} +``` -### Automatically Registering Static Event Handlers +:::info +`@SubscribeEvent` annotated methods must not be `private`. Any out of `package-private`, `protected` and `public` will work. +::: -A class may be annotated with the `@Mod$EventBusSubscriber` annotation. Such a class is automatically registered to `NeoForge#EVENT_BUS` when the `@Mod` class itself is constructed. This is essentially equivalent to adding `NeoForge.EVENT_BUS.register(AnnotatedClass.class);` at the end of the `@Mod` class's constructor. +### `@Mod.EventBusSubscriber` -You can pass the bus you want to listen to the `@Mod$EventBusSubscriber` annotation. It is recommended you also specify the mod id, since the annotation process may not be able to figure it out, and the bus you are registering to, since it serves as a reminder to make sure you are on the correct one. You can also specify the `Dist`s or physical sides to load this event subscriber on. This can be used to not load client specific event subscribers on the dedicated server. +We can go one step further and also annotate the event handler class with `@Mod.EventBusSubscriber`. This annotation is discovered automatically by NeoForge, allowing you to remove all event-related code from the mod constructor. In essence, it is equivalent to calling `NeoForge.EVENT_BUS.register(EventHandler.class)` at the end of the mod constructor. This means that all handlers must be static, too. -An example for a static event listener listening to `RenderLevelStageEvent` which will only be called on the client: +While not required, it is highly recommended to specify the `modid` parameter in the annotation, in order to make debugging easier (especially when it comes to mod conflicts). ```java -@Mod.EventBusSubscriber(modid = "mymod", bus = Bus.FORGE, value = Dist.CLIENT) -public class MyStaticClientOnlyEventHandler { - @SubscribeEvent - public static void drawLast(RenderLevelStageEvent event) { - System.out.println("Drawing!"); - } +@Mod.EventBusSubscriber(modid = "yourmodid") +public class EventHandler { + @SubscribeEvent + public static void onLivingJump(LivingJumpEvent event) { + event.getEntity().heal(1); + } } ``` -:::note -This does not register an instance of the class; it registers the class itself (i.e. the event handling methods must be static). -::: +## Event Options -Canceling ---------- +### Fields and Methods -If an event can be canceled, it will be marked with the `@Cancelable` annotation, and the method `Event#isCancelable()` will return `true`. The cancel state of a cancelable event may be modified by calling `Event#setCanceled(boolean canceled)`, wherein passing the boolean value `true` is interpreted as canceling the event, and passing the boolean value `false` is interpreted as "un-canceling" the event. However, if the event cannot be canceled (as defined by `Event#isCancelable()`), an `UnsupportedOperationException` will be thrown regardless of the passed boolean value, since the cancel state of a non-cancelable event event is considered immutable. +Probably the most obvious part. Most events contain context for the event handler to use, such as an entity causing the event or a level the event occurs in. + +### Hierarchy + +In order to use the advantages of inheritance, some events do not directly extend `Event`, but one of its subclasses, for example `BlockEvent` (which contains block context for block-related events) or `EntityEvent` (which similarly contains entity context) and its subclasses `LivingEvent` (for `LivingEntity`-specific context) and `PlayerEvent` (for `Player`-specific context). These context-providing super events are `abstract` and cannot be listened to. :::danger -Not all events can be canceled! Attempting to cancel an event that is not cancelable will result in an unchecked `UnsupportedOperationException` being thrown, which is expected to result in the game crashing! Always check that an event can be canceled using `Event#isCancelable()` before attempting to cancel it! +If you listen to an `abstract` event, your game will crash, as this is not what you want. Listen to one of its sub events instead. ::: -Results -------- +### Cancellable Events + +Some events implement the `ICancellableEvent` interface. These events can be cancelled using `#setCanceled(boolean canceled)`, and the cancellation status can be checked using `#isCanceled()`. If an event is cancelled, other event handlers for this event will not run, and some kind of behavior that is associated with "cancelling" is enabled. For example, cancelling `LivingJumpEvent` will prevent the jump. + +Event handlers can opt to explicitly receive cancelled events. This is done by setting the `receiveCanceled` parameter in `IEventBus#addListener` (or `@SubscribeEvent`, depending on your way of attaching the event handlers) to true. -Some events have an `Event$Result`. A result can be one of three things: `DENY` which stops the event, `DEFAULT` which uses the Vanilla behavior, and `ALLOW` which forces the action to take place, regardless if it would have originally. The result of an event can be set by calling `#setResult` with an `Event$Result` on the event. Not all events have results; an event with a result will be annotated with `@HasResult`. +### Results + +Some events have a `Result`. A `Result` can be one of three things: `DENY` which stops the event, `ALLOW` which force-runs the event, and `DEFAULT` which uses the Vanilla behavior. The result of an event can be set by calling `Event#setResult`. Not all events have results; an event with a result will be annotated with `@HasResult`. :::caution -Different events may use results in different ways, refer to the event's JavaDoc before using the result. +Results are deprecated and will be replaced by more specific per-event results soon. ::: -Priority --------- +### Priority -Event handler methods (marked with `@SubscribeEvent`) have a priority. You can set the priority of an event handler method by setting the `priority` value of the annotation. The priority can be any value of the `EventPriority` enum (`HIGHEST`, `HIGH`, `NORMAL`, `LOW`, and `LOWEST`). Event handlers with priority `HIGHEST` are executed first and from there in descending order until `LOWEST` events which are executed last. +Event handlers can optionally get assigned a priority. The `EventPriority` enum contains five values: `HIGHEST`, `HIGH`, `NORMAL` (default), `LOW` and `LOWEST`. Events are executed from highest to lowest priority, with undefined order for two events of the same priority. -Sub Events ----------- +Priorities can be defined by setting the `priority` parameter in `IEventBus#addListener` or `@SubscribeEvent`, depending on how you attach event handlers. -Many events have different variations of themselves. These can be different but all based around one common factor (e.g. `PlayerEvent`) or can be an event that has multiple phases (e.g. `PotionBrewEvent`). Take note that if you listen to the parent event class, you will receive calls to your method for *all* subclasses. +### Sided Events -Mod Event Bus -------------- +Some events are only fired on one [side][side]. Common examples include the various render events, which are only fired on the client. Since client-only events generally need to access other client-only parts of the Minecraft codebase, they need to be registered accordingly. -To get access to the mod event bus for your own mod, declare a constructor parameter of type `IModEventBus` in your [mod entrypoint][ctor-injection]. +Event handlers that use `IEventBus#addListener()` should use a `FMLEnvironment.dist` check and a separate client-only class, as outlined in the article on sides. -The mod event bus is primarily used for listening to lifecycle events in which mods should initialize. Each event on the mod bus is required to implement `IModBusEvent`. Many of these events are also ran in parallel so mods can be initialized at the same time. This does mean you can't directly execute code from other mods in these events. Use the `InterModComms` system for that. +Event handlers that use `@Mod.EventBusSubscriber` can specify the side as the `value` parameter of the annotation, for example `@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = "yourmodid")`. -These are the four most commonly used lifecycle events that are called during mod initialization on the mod event bus: +## Event Buses -* `FMLCommonSetupEvent` -* `FMLClientSetupEvent` & `FMLDedicatedServerSetupEvent` -* `InterModEnqueueEvent` -* `InterModProcessEvent` +While most events are posted on the `NeoForge.EVENT_BUS`, some events are posted on the mod event bus instead. These are generally called mod bus events. Mod bus events can be distinguished from regular events by their superinterface `IModBusEvent`. -:::note -The `FMLClientSetupEvent` and `FMLDedicatedServerSetupEvent` are only called on their respective distribution. -::: +The mod event bus is passed to you as a parameter in the mod constructor, and you can then subscribe mod bus events to it. If you use `@Mod.EventBusSubscriber`, you can also set the bus as an annotation parameter, like so: `@Mod.EventBusSubscriber(bus = Bus.MOD, modid = "yourmodid")`. The default bus is `Bus.FORGE`. + +### The Mod Lifecycle + +Most mod bus events are what is known as lifecycle events. Lifecycle events run once in every mod's lifecycle during startup. Many of them are fired in parallel; if you want to run code from one of these events on the main thread, enqueue them using `#enqueueWork(Runnable runnable)`. -These four lifecycle events are all ran in parallel since they all are a subclass of `ParallelDispatchEvent`. If you want to run code on the main thread during any `ParallelDispatchEvent`, you can use the `#enqueueWork` to do so. +The lifecycle generally follows the following order: -Next to the lifecycle events, there are a few miscellaneous events that are fired on the mod event bus where you can register, set up, or initialize various things. Most of these events are not ran in parallel in contrast to the lifecycle events. A few examples: +- The mod constructor is called. Register your event handlers here, or in the next step. +- All `@Mod.EventBusSubscriber`s are called. +- `FMLConstructModEvent` is fired. +- The registry events are fired, these include [`NewRegistryEvent`][newregistry], [`DataPackRegistryEvent.NewRegistry`][newdatapackregistry] and, for each registry, [`RegisterEvent`][registerevent]. +- `FMLCommonSetupEvent` is fired. This is where various miscellaneous setup happens. +- The [sided][side] setup is fired: `FMLClientSetupEvent` if on a physical client, and `FMLDedicatedServerSetupEvent` if on a physical server. +- `InterModComms` are handled (see below). +- `FMLLoadCompleteEvent` is fired. -* `RegisterColorHandlersEvent` -* `ModelEvent$BakingCompleted` -* `TextureStitchEvent` -* `RegisterEvent` +#### `InterModComms` -A good rule of thumb: events are fired on the mod event bus when they should be handled during initialization of a mod. +`InterModComms` is a system that allows modders to send messages to other mods for compatibility features. The class holds the messages for mods, all methods are thread-safe to call. The system is mainly driven by two events: `InterModEnqueueEvent` and `InterModProcessEvent`. + +During `InterModEnqueueEvent`, you can use `InterModComms#sendTo` to send messages to other mods. These methods accept the id of the mod to send the message to, the key associated with the message data (to distinguish between different messages), and a `Supplier` holding the message data. The sender can be optionally specified as well. + +Then, during `InterModProcessEvent`, you can use `InterModComms#getMessages` to get a stream of all received messages as `IMCMessage` objects. These hold the sender of the data, the intended receiver of the data, the data key, and the supplier for the actual data. + +### Other Mod Bus Events + +Next to the lifecycle events, there are a few miscellaneous events that are fired on the mod event bus, mostly for legacy reasons. These are generally events where you can register, set up, or initialize various things. Most of these events are not ran in parallel in contrast to the lifecycle events. A few examples: + +- `RegisterColorHandlersEvent` +- `ModelEvent.BakingCompleted` +- `TextureStitchEvent` + +:::warning +Most of these events will be moved to `NeoForge.EVENT_BUS` eventually. +::: -[ctor-injection]: ../gettingstarted/modfiles.md#javafml-and-mod +[modbus]: #event-buses +[newdatapackregistry]: registries.md#custom-datapack-registries +[newregistry]: registries.md#custom-registries +[registerevent]: registries.md#registerevent +[side]: sides.md diff --git a/docs/concepts/lifecycle.md b/docs/concepts/lifecycle.md deleted file mode 100644 index a5b5090e..00000000 --- a/docs/concepts/lifecycle.md +++ /dev/null @@ -1,77 +0,0 @@ -Mod Lifecycle -============== - -During the mod loading process, the various lifecycle events are fired on the mod-specific event bus. Many actions are performed during these events, such as [registering objects][registering], preparing for [data generation][datagen], or [communicating with other mods][imc]. - -Event listeners should be registered either using `@EventBusSubscriber(bus = Bus.MOD)` or in the mod constructor: - -```java -@Mod.EventBusSubscriber(modid = "mymod", bus = Mod.EventBusSubscriber.Bus.MOD) -public class MyModEventSubscriber { - @SubscribeEvent - static void onCommonSetup(FMLCommonSetupEvent event) { ... } -} - -@Mod("mymod") -public class MyMod { - public MyMod() { - FMLModLoadingContext.get().getModEventBus().addListener(this::onCommonSetup); - } - - private void onCommonSetup(FMLCommonSetupEvent event) { ... } -} -``` - -:::caution -Most of the lifecycle events are fired in parallel: all mods will concurrently receive the same event. - -Mods *must* take care to be thread-safe, like when calling other mods' APIs or accessing vanilla systems. Defer code for later execution via `ParallelDispatchEvent#enqueueWork`. -::: - -Registry Events ---------------- - -The registry events are fired after the mod instance construction. There are three: `NewRegistryEvent`, `DataPackRegistryEvent$NewRegistry` and `RegisterEvent`. These events are fired synchronously during mod loading. - -`NewRegistryEvent` allows modders to register their own custom registries, using the `RegistryBuilder` class. - -`DataPackRegistryEvent$NewRegistry` allows modders to register custom datapack registries by providing a `Codec` to encode and decode the object from JSON. - -`RegisterEvent` is for [registering objects][registering] into the registries. The event is fired for each registry. - -Data Generation ---------------- - -If the game is setup to run [data generators][datagen], then the `GatherDataEvent` will be the last event to fire. This event is for registering mods' data providers to their associated data generator. This event is also fired synchronously. - -Common Setup ------------- - -`FMLCommonSetupEvent` is for actions that are common to both physical client and server. -This event is fired for multiple mods in parallel, so be careful. -You can use `event.enqueueWork(() -> /* do something */)` to run code that is not thread-safe. - -Sided Setup ------------ - -The sided-setup events are fired on their respective [physical sides][sides]: `FMLClientSetupEvent` on the physical client, and `FMLDedicatedServerSetupEvent` for the dedicated server. This is where physical side-specific initialization should occur, such as registering client-side key bindings. - -InterModComms -------------- - -This is where messages can be sent to mods for cross-mod compatibility. There are two events: `InterModEnqueueEvent` and `InterModProcessEvent`. - -`InterModComms` is the class responsible for holding messages for mods. The methods are safe to call during the lifecycle events, as it is backed by a `ConcurrentMap`. - -During the `InterModEnqueueEvent`, use `InterModComms#sendTo` to send messages to different mods. These methods take in the mod id that will be sent the message, the key associated with the message data, and a supplier holding the message data. Additionally, the sender of the message can also be specified, but by default it will be the mod id of the caller. - -Then during the `InterModProcessEvent`, use `InterModComms#getMessages` to get a stream of all received messages. The mod id supplied will almost always be the mod id of the mod the method is called on. Additionally, a predicate can be specified to filter out the message keys. This will return a stream of `IMCMessage`s which hold the sender of the data, the receiver of the data, the data key, and the supplied data itself. - -:::tip -There are two other lifecycle events: `FMLConstructModEvent`, fired directly after mod instance construction but before the `RegisterEvent`, and `FMLLoadCompleteEvent`, fired after the `InterModComms` events, for when the mod loading process is complete. -::: - -[registering]: ./registries.md#methods-for-registering -[datagen]: ../datagen/index.md -[imc]: ./lifecycle.md#intermodcomms -[sides]: ./sides.md diff --git a/docs/concepts/sides.md b/docs/concepts/sides.md index c6a12221..f1c61260 100644 --- a/docs/concepts/sides.md +++ b/docs/concepts/sides.md @@ -8,7 +8,7 @@ Turns out, not so much. A lot of the confusion stems from Minecraft having two d ### The Physical Side -When you open your Minecraft launcher, select a Minecraft installation and press play, you boot up a **physical client**. The word "physical" is used here in the sense of "this is a client program". This especially means that client-side functionality, such as all the rendering stuff, is available here and can be used as needed. The **physical server**, on the other hand, is what opens when you launch a Minecraft server JAR. While the Minecraft server comes with a rudimentary GUI, it is missing all client-only functionality. Most notably, this means that various client classes are missing from the server JAR. Calling these classes on the physical server will lead to missing class errors, i.e. crashes, so we need to safeguard against this. +When you open your Minecraft launcher, select a Minecraft installation and press play, you boot up a **physical client**. The word "physical" is used here in the sense of "this is a client program". This especially means that client-side functionality, such as all the rendering stuff, is available here and can be used as needed. In contrast, the **physical server**, also known as dedicated server, is what opens when you launch a Minecraft server JAR. While the Minecraft server comes with a rudimentary GUI, it is missing all client-only functionality. Most notably, this means that various client classes are missing from the server JAR. Calling these classes on the physical server will lead to missing class errors, i.e. crashes, so we need to safeguard against this. ### The Logical Side From 4b2d0213a13e7854a00a0bbe170bb2f039dee266 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 29 Jan 2024 22:34:12 +0100 Subject: [PATCH 04/41] fix broken links --- docs/datagen/server/loottables.md | 2 +- docs/datagen/server/tags.md | 2 +- docs/gettingstarted/index.md | 2 +- docs/gettingstarted/modfiles.md | 5 ++--- docs/gui/screens.md | 2 +- docs/items/index.md | 2 +- docs/items/mobeffects.md | 2 +- docs/misc/config.md | 2 +- docs/misc/gametest.mdx | 2 +- docs/misc/keymappings.md | 4 ++-- docs/resources/server/loottables.md | 2 +- docs/resources/server/recipes/incode.md | 6 +++--- 12 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/datagen/server/loottables.md b/docs/datagen/server/loottables.md index 2436a2d2..e7d5eb98 100644 --- a/docs/datagen/server/loottables.md +++ b/docs/datagen/server/loottables.md @@ -141,4 +141,4 @@ Scoreboard providers are a special type of number providers defined by `Scoreboa [loottable]: ../../resources/server/loottables.md [datagen]: ../index.md#data-providers -[registered]: ../../concepts/registries.md#registries-that-arent-forge-registries +[registered]: ../../concepts/registries.md diff --git a/docs/datagen/server/tags.md b/docs/datagen/server/tags.md index 5603a548..f52824a6 100644 --- a/docs/datagen/server/tags.md +++ b/docs/datagen/server/tags.md @@ -118,4 +118,4 @@ public AttributeTagsProvider(PackOutput output, CompletableFuture EXAMPLE_TAB = CREATIVE_MODE_TABS.r [interactionpipeline]: interactionpipeline.md [loottables]: ../resources/server/loottables.md [mobeffectinstance]: mobeffects.md#mobeffectinstances -[modbus]: ../concepts/events.md#mod-event-bus +[modbus]: ../concepts/events.md#event-buses [nbt]: ../datastorage/nbt.md [registering]: ../concepts/registries.md#methods-for-registering [resources]: ../resources/client/index.md diff --git a/docs/items/mobeffects.md b/docs/items/mobeffects.md index 1af79dfe..a8c9e98a 100644 --- a/docs/items/mobeffects.md +++ b/docs/items/mobeffects.md @@ -192,7 +192,7 @@ BrewingRecipeRegistry.addRecipe( This should be called some time during setup, for example during [`FMLCommonSetupEvent`][commonsetup]. Make sure to wrap this into an `event.enqueueWork()` call, as the brewing recipe registry is not thread-safe. [block]: ../blocks/index.md -[commonsetup]: ../concepts/events.md#mod-event-bus +[commonsetup]: ../concepts/events.md#event-buses [datapack]: ../resources/server/index.md [events]: ../concepts/events.md [item]: index.md diff --git a/docs/misc/config.md b/docs/misc/config.md index 4d51af03..6ba9abdc 100644 --- a/docs/misc/config.md +++ b/docs/misc/config.md @@ -136,4 +136,4 @@ These events are called for all configurations for the mod; the `ModConfig` obje [toml]: https://toml.io/ [nightconfig]: https://github.com/TheElectronWill/night-config [type]: https://github.com/neoforged/FancyModLoader/blob/19d6326b810233e683f1beb3d28e41372e1e89d1/core/src/main/java/net/neoforged/fml/config/ModConfig.java#L83-L111 -[events]: ../concepts/events.md#creating-an-event-handler +[events]: ../concepts/events.md#registering-an-event-handler diff --git a/docs/misc/gametest.mdx b/docs/misc/gametest.mdx index 90b1f047..7562122c 100644 --- a/docs/misc/gametest.mdx +++ b/docs/misc/gametest.mdx @@ -284,5 +284,5 @@ property 'forge.enableGameTest', 'true' [test]: #running-game-tests [namespaces]: #enabling-other-namespaces -[event]: ../concepts/events.md#creating-an-event-handler +[event]: ../concepts/events.md#registering-an-event-handler [buildscript]: ../gettingstarted/index.md#simple-buildgradle-customizations diff --git a/docs/misc/keymappings.md b/docs/misc/keymappings.md index 5d72a8cf..f4fb2b8f 100644 --- a/docs/misc/keymappings.md +++ b/docs/misc/keymappings.md @@ -150,8 +150,8 @@ public boolean mouseClicked(double x, double y, int button) { If you do not own the screen which you are trying to check a **mouse** for, you can listen to the `Pre` or `Post` events of `ScreenEvent$MouseButtonPressed` on the [**Forge event bus**][forgebus] instead. ::: -[modbus]: ../concepts/events.md#mod-event-bus +[modbus]: ../concepts/events.md#event-buses [controls]: https://minecraft.wiki/w/Options#Controls [tk]: ../concepts/internationalization.md#translatablecontents [keyinput]: https://www.glfw.org/docs/3.3/input_guide.html#input_key -[forgebus]: ../concepts/events.md#creating-an-event-handler +[forgebus]: ../concepts/events.md#registering-an-event-handler diff --git a/docs/resources/server/loottables.md b/docs/resources/server/loottables.md index 52d7eed6..2e80c2f1 100644 --- a/docs/resources/server/loottables.md +++ b/docs/resources/server/loottables.md @@ -107,5 +107,5 @@ Forge adds an additional `LootItemCondition` which checks whether the given `Loo [datapack]: https://minecraft.wiki/w/Data_pack [wiki]: https://minecraft.wiki/w/Loot_table -[event]: ../../concepts/events.md#creating-an-event-handler +[event]: ../../concepts/events.md#registering-an-event-handler [glm]: ./glm.md diff --git a/docs/resources/server/recipes/incode.md b/docs/resources/server/recipes/incode.md index 26d1f7ce..00497a10 100644 --- a/docs/resources/server/recipes/incode.md +++ b/docs/resources/server/recipes/incode.md @@ -60,6 +60,6 @@ public static final BannerPattern EXAMPLE_PATTERN = REGISTER.register("example_p ``` [recipe]: ./custom.md#recipe -[cancel]: ../../../concepts/events.md#canceling -[attached]: ../../../concepts/events.md#creating-an-event-handler -[registering]: ../../../concepts/registries.md#registries-that-arent-forge-registries +[cancel]: ../../../concepts/events.md#cancellable-events +[attached]: ../../../concepts/events.md#registering-an-event-handler +[registering]: ../../../concepts/registries.md From c49ff5536d08a75b7a6bd19f4e6739c4b55f32ff Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 31 Jan 2024 16:37:12 +0100 Subject: [PATCH 05/41] fix some mistakes regarding `DeferredRegister.Blocks` --- docs/blocks/index.md | 45 ++++++++++++++++++++++++------------- docs/concepts/registries.md | 2 +- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/docs/blocks/index.md b/docs/blocks/index.md index bfac39ab..54e7a094 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -1,17 +1,23 @@ -Blocks -====== +# Blocks Blocks are essential to the Minecraft world. They make up all the terrain, structures, and machines. Chances are if you are interested in making a mod, then you will want to add some blocks. This page will guide you through the creation of blocks, and some of the things you can do with them. -One Block to Rule Them All --------------------------- +## One Block to Rule Them All Before we get started, it is important to understand that there is only ever one of each block in the game. A world consists of thousands of references to that one block in different locations. In other words, the same block is just displayed a lot of times. -Due to this, a block should only ever be instantiated once, and that is during [registration]. Once the block is registered, you can then use the registered reference as needed. Consider this example: +Due to this, a block should only ever be instantiated once, and that is during [registration]. Once the block is registered, you can then use the registered reference as needed. + +Unlike most other registries, blocks use a specialized version of `DeferredRegister`, called `DeferredRegister.Blocks`. `DeferredRegister.Blocks` acts basically like a `DeferredRegister`, but with some minor differences: + +- They are created via `DeferredRegister.createBlocks("yourmodid")` instead of the regular `DeferredRegister.create(...)` method. +- `#register` returns a `DeferredBlock`, which extends `DeferredHolder`. `T` is the type of the class of the block we are registering. +- There are a few helper methods for registering block. See [below] for more details. + +So now, let's register our blocks: ```java -//BLOCKS is a DeferredRegister +//BLOCKS is a DeferredRegister.Blocks public static final DeferredBlock MY_BLOCK = BLOCKS.register("my_block", () -> new Block(...)); ``` @@ -33,8 +39,13 @@ Do not call `new Block()` outside registration! As soon as you do that, things c - If you still manage to have a dangling block instance, the game will not recognize it while syncing and saving, and replace it with air. ::: -Creating Blocks ---------------- +## Creating Blocks + +As discussed before, we start by creating our `DeferredRegister.Blocks`: + +```java +public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid"); +``` ### Basic Blocks @@ -54,7 +65,8 @@ For simple blocks which need no special functionality (think cobblestone, wooden So for example, a simple implementation would look something like this: ```java - public static final DeferredBlock MY_BETTER_BLOCK = BLOCKS.register( +//BLOCKS is a DeferredRegister.Blocks +public static final DeferredBlock MY_BETTER_BLOCK = BLOCKS.register( "my_better_block", () -> new Block(BlockBehaviour.Properties.of() //highlight-start @@ -80,14 +92,14 @@ Directly using `Block` only allows for very basic blocks. If you want to add fun If you want to make a block that has different variants (think a slab that has a bottom, top, and double variant), you should use [blockstates]. And finally, if you want a block that stores additional data (think a chest that stores its inventory), a [block entity][blockentities] should be used. The rule of thumb here is that if you have a finite and reasonably small amount of states (= a few hundred states at most), use blockstates, and if you have an infinite or near-infinite amount of states, use a block entity. -### `DeferredRegister.Blocks` +### `DeferredRegister.Blocks` helpers -All registries use `DeferredRegister` to register their contents, and blocks are no exceptions. However, due to the fact that adding new blocks is such an essential feature of an overwhelming amount of mods, NeoForge provides the `DeferredRegister.Blocks` helper class that extends `DeferredRegister` and provides some block-specific helpers: +We already discussed how to create a `DeferredRegister.Blocks`, and that it returns `DeferredBlock`s. Now, let's have a look at what other utilities the specialized `DeferredRegister` has to offer. Let's start with `#registerBlock`: ```java -public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(ExampleMod.MOD_ID); +public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid"); -public static final Supplier EXAMPLE_BLOCK = BLOCKS.registerBlock( +public static final DeferredBlock EXAMPLE_BLOCK = BLOCKS.registerBlock( "example_block", Block::new, // The factory that the properties will be passed into. new BlockBehaviour.Properties() // The properties to use. @@ -99,7 +111,7 @@ Internally, this will simply call `BLOCKS.register("example_block", () -> new Bl If you want to use `Block::new`, you can leave out the factory entirely: ```java -public static final Supplier EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock( +public static final DeferredBlock EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock( "example_block", new BlockBehaviour.Properties() // The properties to use. ); @@ -113,8 +125,7 @@ If you register your block and place it in the world, you will find it to be mis To apply a simple texture to a block, you must add a blockstate JSON, a model JSON, and a texture PNG. See the section on [resources] for more information. -Using Blocks ------------- +## Using Blocks Blocks are very rarely directly used to do things. In fact, probably two of the most common operations in all of Minecraft - getting the block at a position, and setting a block at a position - use blockstates, not blocks. The general design approach is to have the block define behavior, but have the behavior actually run through blockstates. Due to this, `BlockState`s are often passed to methods of `Block` as a parameter. For more information on how blockstates are used, and on how to get one from a block, see [Using Blockstates][usingblockstates]. @@ -214,6 +225,8 @@ Random ticks occur every tick for a set amount of blocks in a chunk. That set am Random ticking is used by a wide range of mechanics in Minecraft, such as plant growth, ice and snow melting, or copper oxidizing. +[above]: #one-block-to-rule-them-all +[below]: #deferredregisterblocks-helpers [blockentities]: ../blockentities/index.md [blockstates]: states.md [events]: ../concepts/events.md diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index c1a2211f..29fc1ed0 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -222,7 +222,7 @@ static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) [codec]: ../datastorage/codecs.md [datagen]: ../datagen/index.md [datapack]: ../resources/server/index.md -[defregblocks]: ../blocks/index.md#deferredregisterblocks +[defregblocks]: ../blocks/index.md#deferredregisterblocks-helpers [defregitems]: ../items/index.md#deferredregisteritems [dynamicops]: ../datastorage/codecs.md#dynamicops [event]: ./events.md From 242abf0d96e89f6057f20b801fdf788d4c8359e7 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 31 Jan 2024 16:39:18 +0100 Subject: [PATCH 06/41] add side checks in the event handlers --- docs/concepts/events.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/concepts/events.md b/docs/concepts/events.md index aaf7dce9..84147be6 100644 --- a/docs/concepts/events.md +++ b/docs/concepts/events.md @@ -21,7 +21,11 @@ public class YourMod { // Heals an entity by half a heart every time they jump. private static void onLivingJump(LivingJumpEvent event) { - event.getEntity().heal(1); + Entity entity = event.getEntity(); + // Only heal on the server side + if (!entity.level().isClientSide()) { + entity.heal(1); + } } } ``` @@ -34,7 +38,10 @@ Alternatively, event handlers can be annotation-driven by creating an event hand public class EventHandler { @SubscribeEvent public void onLivingJump(LivingJumpEvent event) { - event.getEntity().heal(1); + Entity entity = event.getEntity(); + if (!entity.level().isClientSide()) { + entity.heal(1); + } } } @@ -52,7 +59,10 @@ You can also do it statically. Simply make all event handlers static, and instea public class EventHandler { @SubscribeEvent public static void onLivingJump(LivingJumpEvent event) { - event.getEntity().heal(1); + Entity entity = event.getEntity(); + if (!entity.level().isClientSide()) { + entity.heal(1); + } } } @@ -64,10 +74,6 @@ public class YourMod { } ``` -:::info -`@SubscribeEvent` annotated methods must not be `private`. Any out of `package-private`, `protected` and `public` will work. -::: - ### `@Mod.EventBusSubscriber` We can go one step further and also annotate the event handler class with `@Mod.EventBusSubscriber`. This annotation is discovered automatically by NeoForge, allowing you to remove all event-related code from the mod constructor. In essence, it is equivalent to calling `NeoForge.EVENT_BUS.register(EventHandler.class)` at the end of the mod constructor. This means that all handlers must be static, too. @@ -79,7 +85,10 @@ While not required, it is highly recommended to specify the `modid` parameter in public class EventHandler { @SubscribeEvent public static void onLivingJump(LivingJumpEvent event) { - event.getEntity().heal(1); + Entity entity = event.getEntity(); + if (!entity.level().isClientSide()) { + entity.heal(1); + } } } ``` From a4971bd17bf8429cf1638746a3a01ca2c3dbed10 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 31 Jan 2024 16:41:37 +0100 Subject: [PATCH 07/41] fix some of the mistakes in registries.md --- docs/blocks/index.md | 2 +- docs/concepts/registries.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/blocks/index.md b/docs/blocks/index.md index 54e7a094..91123610 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -94,7 +94,7 @@ If you want to make a block that has different variants (think a slab that has a ### `DeferredRegister.Blocks` helpers -We already discussed how to create a `DeferredRegister.Blocks`, and that it returns `DeferredBlock`s. Now, let's have a look at what other utilities the specialized `DeferredRegister` has to offer. Let's start with `#registerBlock`: +We already discussed how to create a `DeferredRegister.Blocks` [above], as well as that it returns `DeferredBlock`s. Now, let's have a look at what other utilities the specialized `DeferredRegister` has to offer. Let's start with `#registerBlock`: ```java public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid"); diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 29fc1ed0..448cddf3 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -66,6 +66,7 @@ Finally, since the entire system is a wrapper around registry events, we need to ```java //This is our mod constructor public ExampleMod(IModEventBus bus) { + //highlight-next-line ExampleBlocksClass.BLOCKS.register(bus); //Other stuff here } @@ -109,7 +110,7 @@ public static final ResourceKey> SPELL_REGISTRY_KEY = ResourceKe public static final Registry SPELL_REGISTRY = new RegistryBuilder<>(SPELL_REGISTRY_KEY) // If you want the registry to sync its values. .sync(true) - // The default key. Similar to minecraft:air for blocks. + // The default key. Similar to minecraft:air for blocks. This is optional. .defaultKey(new ResourceLocation("yourmodid", "empty")) // Effectively limits the max count. Generally discouraged, but may make sense in settings such as networking. .maxId(256) From 9950d51f11066a7280d4cdd5d9608dec446cb403 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 21 Feb 2024 15:02:22 +0100 Subject: [PATCH 08/41] remove mention of registry int id syncing (it is discouraged) --- docs/concepts/registries.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 448cddf3..e2f1b624 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -108,8 +108,6 @@ Let's start by creating the [registry key][resourcekey] and the registry itself: // Of course, all mentions of spells can and should be replaced with whatever your registry actually is. public static final ResourceKey> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(new ResourceLocation("yourmodid", "spells")); public static final Registry SPELL_REGISTRY = new RegistryBuilder<>(SPELL_REGISTRY_KEY) - // If you want the registry to sync its values. - .sync(true) // The default key. Similar to minecraft:air for blocks. This is optional. .defaultKey(new ResourceLocation("yourmodid", "empty")) // Effectively limits the max count. Generally discouraged, but may make sense in settings such as networking. From a1bd87d87fd11427ab8140295f5a5c9d25ec0706 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 21 Feb 2024 15:53:28 +0100 Subject: [PATCH 09/41] update datapack registry datagen --- docs/concepts/registries.md | 134 ++++++++++++++-------- docs/datagen/index.md | 4 +- docs/datagen/server/datapackregistries.md | 128 --------------------- 3 files changed, 90 insertions(+), 176 deletions(-) delete mode 100644 docs/datagen/server/datapackregistries.md diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index e2f1b624..c3d4ee99 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -149,50 +149,7 @@ Datapack registries allow their contents to be specified in JSON files. This mea - Minecraft's datapack registries use the format `data/yourmodid/registrypath` (for example `data/yourmodid/worldgen/biomes`, where `worldgen/biomes` is the registry path). - All other datapack registries (NeoForge or modded) use the format `data/yourmodid/registrynamespace/registrypath` (for example `data/yourmodid/neoforge/loot_modifiers`, where `neoforge` is the registry namespace and `loot_modifiers` is the registry path). -Datapack registries can be obtained from a `RegistryAccess`. This `RegistryAccess` can be retrieved by calling `ServerLevel#registryAccess()` if on the server, by calling `Minecraft.getInstance().connection#registryAccess()` if on the client, or from a `RegistryOps`. - -### `RegistryOps` - -`RegistryOps` is a special [`DynamicOps`][dynamicops] made specifically for (de)serializing datapack registries. It provides additional registry context and enables the use of special codecs that can only be used with `RegistryOps`. Data generation of datapack registry elements must always be done through `RegistryOps` to convert elements to `JsonElement`s. - -A `RegistryOps` can be created via `RegistryOps.create(JsonElement.INSTANCE, RegistryAccess.builtinCopy())`. `RegistryAccess.builtinCopy()` creates a set of writable datapack registries, which is necessary for datagenning unregistered objects. All data generation done in a `GatherDataEvent` handler must use the same `RegistryAccess` and `RegistryOps` instances, otherwise obscure errors will occur. - -### `Holder`s - -As mentioned before, (normal) registries rely on `DeferredHolder`s, which are a special kind of `Holder`. A `Holder` vaguely resembles a `Pair` that either starts with a key and has a value bound later, or starts with a value and may have a key bound later. Datapack registries extensively rely on (non-deferred) `Holder`s to reference registry elements of other registries. For example, `Biome`s refer to `Holder`s, and `PlacedFeature`s refer to `Holder`s. - -During data generation, we can use `RegistryOps#registry` to get a registry, and `Registry#getOrCreateHolderOrThrow()` to produce key-only reference holders (we only need the key in this case, since holder codecs only encode keys when using a `RegistryOps` in order to prevent circular dependencies). - -### `JsonCodecProvider` - -NeoForge provides a data provider for datapack registry elements that, given a registry key and a map of objects to generate, generates all JSON files for the objects in the map. - -```java -@SubscribeEvent -static void onGatherData(GatherDataEvent event) { - DataGenerator generator = event.getDataGenerator(); - ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); - RegistryOps registryOps = RegistryOps.create(JsonElement.INSTANCE, RegistryAccess.builtinCopy()); - - Map map = Map.of( - // Whatever entries you want. For example: - new ResourceLocation("yourmodid", "sponge_everywhere"), new PlacedFeature(...) - ); - - JsonCodecProvider provider = JsonCodecProvider.forDatapackRegistry( - generator, - existingFileHelper, - "yourmodid", - registryOps, - // The registry you want to generate in. - Registry.PLACED_FEATURE_REGISTRY, - // The elements to generate. - map - ); - - generator.addProvider(event.includeServer(), provider); -} -``` +Datapack registries can be obtained from a `RegistryAccess`. This `RegistryAccess` can be retrieved by calling `ServerLevel#registryAccess()` if on the server, or `Minecraft.getInstance().connection#registryAccess()` if on the client (the latter only works if you are actually connected to a world, as otherwise the connection will be null). The result of these calls can then be used like any other registry to get specific elements, or to iterate over the contents. ### Custom Datapack Registries @@ -216,14 +173,99 @@ static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) } ``` +### Data Generation for Datapack Registries + +Since writing all the JSON files by hand would be tedious and error-prone, NeoForge provides a [data provider][datagenindex] to generate the JSON files for you. This works for both built-in and your own datapack registries. + +First, we create a `RegistrySetBuilder` and add our entries to it (one `RegistrySetBuilder` can hold entries for multiple registries): + +```java +new RegistrySetBuilder() + .add(Registries.CONFIGURED_FEATURE, bootstrap -> { + // Register configured features through the bootstrap context (see below) + }) + .add(Registries.PLACED_FEATURE, bootstrap -> { + // Register placed features through the bootstrap context (see below) + }); +``` + +The `bootstrap` lambda parameter is what we actually use to register our objects. It has the type `BootstrapContext`. To register an object, we call `#register` on it, like so: + +```java +// The resource key of our object. +public static final ResourceKey> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create( + Registries.CONFIGURED_FEATURE, + new ResourceLocation(MOD_ID, "example_configured_feature") +); + +new RegistrySetBuilder() + .add(Registries.CONFIGURED_FEATURE, bootstrap -> { + bootstrap.register( + // The resource key of our configured feature. + EXAMPLE_CONFIGURED_FEATURE, + // The actual configured feature. + new ConfiguredFeature<>(Feature.ORE, new OreConfiguration(...)) + ); + }) + .add(Registries.PLACED_FEATURE, bootstrap -> { + // ... + }); +``` + +The `BootstrapContext` can also be used to lookup entries from another registry if needed: + +```java +public static final ResourceKey> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create( + Registries.CONFIGURED_FEATURE, + new ResourceLocation(MOD_ID, "example_configured_feature") +); +public static final ResourceKey EXAMPLE_PLACED_FEATURE = ResourceKey.create( + Registries.PLACED_FEATURE, + new ResourceLocation(MOD_ID, "example_placed_feature") +); + +new RegistrySetBuilder() + .add(Registries.CONFIGURED_FEATURE, bootstrap -> { + bootstrap.register(EXAMPLE_CONFIGURED_FEATURE, ...); + }) + .add(Registries.PLACED_FEATURE, bootstrap -> { + HolderGetter> otherRegistry = bootstrap.lookup(Registries.CONFIGURED_FEATURE); + bootstrap.register(EXAMPLE_PLACED_FEATURE, new PlacedFeature( + otherRegistry.getOrThrow(EXAMPLE_CONFIGURED_FEATURE), // Get the configured feature + List.of() // No-op when placement happens - replace with whatever your placement parameters are + )); + }); +``` + +Finally, we use our `RegistrySetBuilder` in an actual data provider, and register that data provider to the event: + +```java +@SubscribeEvent +static void onGatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // Only run datapack generation when server data is being generated + event.includeServer(), + // Create the provider + output -> new DatapackBuiltinEntriesProvider( + output, + event.getLookupProvider(), + // Our registry set builder to generate the data from. + new RegistrySetBuilder().add(...), + // A set of mod ids we are generating. Usually only your own mod id. + Set.of("yourmodid") + ) + ); +} +``` + [block]: ../blocks/index.md [blockentity]: ../blockentities/index.md [codec]: ../datastorage/codecs.md -[datagen]: ../datagen/index.md +[datagen]: #data-generation-for-datapack-registries +[datagenindex]: ../datagen/index.md [datapack]: ../resources/server/index.md [defregblocks]: ../blocks/index.md#deferredregisterblocks-helpers [defregitems]: ../items/index.md#deferredregisteritems -[dynamicops]: ../datastorage/codecs.md#dynamicops [event]: ./events.md [item]: ../items/index.md [resloc]: ../misc/resourcelocation.md diff --git a/docs/datagen/index.md b/docs/datagen/index.md index 983a2eb5..358ca636 100644 --- a/docs/datagen/index.md +++ b/docs/datagen/index.md @@ -55,7 +55,7 @@ The `GatherDataEvent` is fired on the mod event bus when the data generator is b **These classes are under the `net.neoforged.neoforge.common.data` package**: * [`GlobalLootModifierProvider`][glmgen] - for [global loot modifiers][glm]; implement `#start` -* [`DatapackBuiltinEntriesProvider`][datapackregistriesgen] for datapack registry objects (i.e. world generation features, biomes, and more); pass in `RegistrySetBuilder` to the constructor +* [`DatapackBuiltinEntriesProvider`][datapackregistriesgen] - for datapack registry objects; see the linked article **These classes are under the `net.minecraft.data` package**: @@ -75,7 +75,7 @@ The `GatherDataEvent` is fired on the mod event bus when the data generator is b [blockstategen]: ./client/modelproviders.md#block-state-provider [glmgen]: ./server/glm.md [glm]: ../resources/server/glm.md -[datapackregistriesgen]: ./server/datapackregistries.md +[datapackregistriesgen]: ../concepts/registries.md#data-generation-for-datapack-registries [loottablegen]: ./server/loottables.md [loottable]: ../resources/server/loottables.md [recipegen]: ./server/recipes.md diff --git a/docs/datagen/server/datapackregistries.md b/docs/datagen/server/datapackregistries.md deleted file mode 100644 index 3f105921..00000000 --- a/docs/datagen/server/datapackregistries.md +++ /dev/null @@ -1,128 +0,0 @@ -Datapack Registry Object Generation -================================== - -Datapack registry objects can be generated for a mod by constructing a new `DatapackBuiltinEntriesProvider` and providing a `RegistrySetBuilder` with the new objects to register. The provider must be [added][datagen] to the `DataGenerator`. - -!!! note - `DatapackBuiltinEntriesProvider` is a Forge extension on top of `RegistriesDatapackGenerator` which properly handles referencing existing datapack registry objects without exploding the entry. So, this documentation will use `DatapackBuiltinEntriesProvider`. - -```java -// On the MOD event bus -@SubscribeEvent -public void gatherData(GatherDataEvent event) { - event.getGenerator().addProvider( - // Tell generator to run only when server data are generating - event.includeServer(), - output -> new DatapackBuiltinEntriesProvider( - output, - event.getLookupProvider(), - // The builder containing the datapack registry objects to generate - new RegistrySetBuilder().add(/* ... */), - // Set of mod ids to generate the datapack registry objects of - Set.of(MOD_ID) - ) - ); -} -``` - -`RegistrySetBuilder` --------------------- - -A `RegistrySetBuilder` is responsible for building all datapack registry objects to be used within the game. The builder can add a new entry for a registry, which can then register objects to that registry. - -First, a new instance of a `RegistrySetBuilder` can be initialized by calling the constructor. Then, the `#add` method (which takes in the `ResourceKey` of the registry, a `RegistryBootstrap` consumer containing the `BootstapContext` to register the objects, and an optional `Lifecycle` argument to indicate the registry's current lifecycle status) can be called to handle a specific registry for registration. - -```java -new RegistrySetBuilder() - // Create configured features - .add(Registries.CONFIGURED_FEATURE, bootstrap -> { - // Register configured features here - }) - // Create placed features - .add(Registries.PLACED_FEATURE, bootstrap -> { - // Register placed features here - }); -``` - -!!! note - Datapack registries created through Forge can also generate their objects using this builder by also passing in the associated `ResourceKey`. - -Registering with `BootstapContext` ----------------------------------- - -The `#register` method in the `BootstapContext` provided by the builder can be used to register objects. It takes in the `ResourceKey` representing the registry name of the object, the object to register, and an optional `Lifecycle` argument to indicate the registry object's current lifecycle status. - -```java -public static final ResourceKey> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create( - Registries.CONFIGURED_FEATURE, - new ResourceLocation(MOD_ID, "example_configured_feature") -); - -// In some constant location or argument -new RegistrySetBuilder() - // Create configured features - .add(Registries.CONFIGURED_FEATURE, bootstrap -> { - // Register configured features here - bootstrap.register( - // The resource key for the configured feature - EXAMPLE_CONFIGURED_FEATURE, - new ConfiguredFeature<>( - Feature.ORE, // Create an ore feature - new OreConfiguration( - List.of(), // Does nothing - 8 // in veins of at most 8 - ) - ) - ); - }) - // Create placed features - .add(Registries.PLACED_FEATURE, bootstrap -> { - // Register placed features here - }); -``` - -### Datapack Registry Object Lookup - -Sometimes datapack registry objects may want to use other datapack registry objects or tags containing datapack registry objects. In those cases, you can look up another datapack registry using `BootstapContext#lookup` to get a `HolderGetter`. From there, you can get a `Holder$Reference` to the datapack registry object or a `HolderSet$Named` for the tag via `#getOrThrow` by passing in the associated key. - -```java -public static final ResourceKey> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create( - Registries.CONFIGURED_FEATURE, - new ResourceLocation(MOD_ID, "example_configured_feature") -); - -public static final ResourceKey EXAMPLE_PLACED_FEATURE = ResourceKey.create( - Registries.PLACED_FEATURE, - new ResourceLocation(MOD_ID, "example_placed_feature") -); - -// In some constant location or argument -new RegistrySetBuilder() - // Create configured features - .add(Registries.CONFIGURED_FEATURE, bootstrap -> { - // Register configured features here - bootstrap.register( - // The resource key for the configured feature - EXAMPLE_CONFIGURED_FEATURE, - new ConfiguredFeature(/* ... */) - ); - }) - // Create placed features - .add(Registries.PLACED_FEATURE, bootstrap -> { - // Register placed features here - - // Get configured feature registry - HolderGetter> configured = bootstrap.lookup(Registries.CONFIGURED_FEATURE); - - bootstrap.register( - // The resource key for the placed feature - EXAMPLE_PLACED_FEATURE, - new PlacedFeature( - configured.getOrThrow(EXAMPLE_CONFIGURED_FEATURE), // Get the configured feature - List.of() // and do nothing to the placement location - ) - ) - }); -``` - -[datagen]: ../index.md#data-providers \ No newline at end of file From 31b5bf26a66ef3e73a026753c51f0c3788b8ba9b Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 21 Feb 2024 16:19:20 +0100 Subject: [PATCH 10/41] registry querying --- docs/concepts/registries.md | 45 +++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index c3d4ee99..266bd959 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -97,6 +97,47 @@ public void register(RegisterEvent event) { } ``` +## Querying Registries + +Sometimes, you will find yourself in situations where you want to get a registered object by a given id. Or, you want to get the id of a certain registered object. Since registries are basically maps of ids (`ResourceLocation`s) to distinct objects, i.e. a reversible map, both of these operations work: + +```java +Registries.BLOCKS.get(new ResourceLocation("minecraft", "dirt")); // returns the dirt block +Registries.BLOCKS.getKey(Blocks.DIRT); // returns the resource location "minecraft:dirt" + +// Assume that ExampleBlocksClass.EXAMPLE_BLOCK.get() is a Supplier with the id "yourmodid:example_block" +Registries.BLOCKS.get(new ResourceLocation("yourmodid", "example_block")); // returns the example block +Registries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // returns the resource location "yourmodid:example_block" +``` + +If you just want to check for the presence of an object, this is also possible, though only with keys: + +```java +Registries.BLOCKS.containsKey(new ResourceLocation("minecraft", "dirt")); // true +Registries.BLOCKS.containsKey(new ResourceLocation("create", "brass_ingot")); // true only if Create is installed +``` + +As the last example shows, this is possible with any mod id, and thus a perfect way to check if a certain item from another mod exists. + +Finally, we can also iterate over all entries in a registry, either over the keys or over the entries (entries use the Java `Map.Entry` type): + +```java +for (ResourceLocation id : Registries.BLOCKS.keySet()) { + // ... +} +for (Map.Entry entry : Registries.BLOCKS.entrySet()) { + // ... +} +``` + +:::note +Query operations should always use vanilla `Registry`s, not `DeferredRegister`s. This is because `DeferredRegister`s are merely registration utilities and effectively do not exist after registration has finished. +::: + +:::danger +Query operations are only safe to use after registration has finished. **DO NOT QUERY REGISTRIES WHILE REGISTRATION IS STILL ONGOING!** +::: + ## Custom Registries Custom registries allow you to specify additional systems that addon mods for your mod may want to plug into. For example, if your mod were to add spells, you could make the spells a registry and thus allow other mods to add spells to your mod, without you having to do anything else. It also allows you to do some things, such as syncing the entries, automatically. @@ -142,7 +183,7 @@ public static void register(RegisterEvent event) { ## Datapack Registries -A datapack registry (also known as a dynamic registry or, after its main use case, worldgen registry) is a special kind of registry that loads data from [datapack][datapack] JSONs (hence the name) at world load, instead of loading them when the game starts. Default datapack registries include most worldgen registries, as well as any custom registry (see below) that is marked as a datapack registry. +A datapack registry (also known as a dynamic registry or, after its main use case, worldgen registry) is a special kind of registry that loads data from [datapack][datapack] JSONs (hence the name) at world load, instead of loading them when the game starts. Default datapack registries most notably include most worldgen registries, among a few others. Datapack registries allow their contents to be specified in JSON files. This means that no code (other than [datagen][datagen] if you don't want to write the JSON files yourself) is necessary. Every datapack registry has a [`Codec`][codec] associated with it, which is used for serialization, and each registry's id determines its datapack path: @@ -175,7 +216,7 @@ static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) ### Data Generation for Datapack Registries -Since writing all the JSON files by hand would be tedious and error-prone, NeoForge provides a [data provider][datagenindex] to generate the JSON files for you. This works for both built-in and your own datapack registries. +Since writing all the JSON files by hand is both tedious and error-prone, NeoForge provides a [data provider][datagenindex] to generate the JSON files for you. This works for both built-in and your own datapack registries. First, we create a `RegistrySetBuilder` and add our entries to it (one `RegistrySetBuilder` can hold entries for multiple registries): From ff5d5b12eb61997f84fbd250188cfcdfeaa055ee Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 21 Feb 2024 16:32:56 +0100 Subject: [PATCH 11/41] add back int ID syncing, with a big disclaimer --- docs/concepts/registries.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 266bd959..24c143be 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -149,6 +149,9 @@ Let's start by creating the [registry key][resourcekey] and the registry itself: // Of course, all mentions of spells can and should be replaced with whatever your registry actually is. public static final ResourceKey> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(new ResourceLocation("yourmodid", "spells")); public static final Registry SPELL_REGISTRY = new RegistryBuilder<>(SPELL_REGISTRY_KEY) + // If you want to enable integer id syncing, for networking. + // Never use integer ids yourself, even if this is enabled! This is purely intended to reduce bandwidth. + .sync(true) // The default key. Similar to minecraft:air for blocks. This is optional. .defaultKey(new ResourceLocation("yourmodid", "empty")) // Effectively limits the max count. Generally discouraged, but may make sense in settings such as networking. From 965be163cd5dea5fa1c765b29a295f2330015347 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 21 Feb 2024 16:43:11 +0100 Subject: [PATCH 12/41] update sync disclaimer --- docs/concepts/registries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 24c143be..3c123712 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -150,7 +150,7 @@ Let's start by creating the [registry key][resourcekey] and the registry itself: public static final ResourceKey> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(new ResourceLocation("yourmodid", "spells")); public static final Registry SPELL_REGISTRY = new RegistryBuilder<>(SPELL_REGISTRY_KEY) // If you want to enable integer id syncing, for networking. - // Never use integer ids yourself, even if this is enabled! This is purely intended to reduce bandwidth. + // These should only be used in networking contexts, for example in packets or purely networking-related NBT data. .sync(true) // The default key. Similar to minecraft:air for blocks. This is optional. .defaultKey(new ResourceLocation("yourmodid", "empty")) From 2e65131562c8f9bf76cbcab96e38e50d4092ca02 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 22 Feb 2024 22:42:48 +0100 Subject: [PATCH 13/41] address most feedback by ChampionAsh --- docs/blocks/index.md | 4 ++-- docs/concepts/events.md | 12 ++++++------ docs/items/index.md | 6 +++++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/blocks/index.md b/docs/blocks/index.md index 91123610..ebc67c57 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -102,7 +102,7 @@ public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBloc public static final DeferredBlock EXAMPLE_BLOCK = BLOCKS.registerBlock( "example_block", Block::new, // The factory that the properties will be passed into. - new BlockBehaviour.Properties() // The properties to use. + BlockBehaviour.Properties.of() // The properties to use. ); ``` @@ -113,7 +113,7 @@ If you want to use `Block::new`, you can leave out the factory entirely: ```java public static final DeferredBlock EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock( "example_block", - new BlockBehaviour.Properties() // The properties to use. + BlockBehaviour.Properties.of() // The properties to use. ); ``` diff --git a/docs/concepts/events.md b/docs/concepts/events.md index 84147be6..877dbafa 100644 --- a/docs/concepts/events.md +++ b/docs/concepts/events.md @@ -2,7 +2,7 @@ One of NeoForge's main features is the event system. Events are fired for various things that happen in the game. For example, there are events for when the player right clicks, when a player or another entity jumps, when blocks are rendered, when the game is loaded, etc. A modder can subscribe event handlers to each of these events, and then perform their desired behavior inside these event handlers. -Events are fired on their respective event bus. The most important bus is `NeoForge.EVENT_BUS`. Besides that, during startup, a mod bus is spawned for each loaded mod and passed into the mod's constructor; mod bus event handlers can run in parallel, dramatically increasing startup speed. See [below][modbus] for more information. +Events are fired on their respective event bus. The most important bus is `NeoForge.EVENT_BUS`. Besides that, during startup, a mod bus is spawned for each loaded mod and passed into the mod's constructor. Many mod bus events are fired in parallel (as opposed to main bus events that always run on the same thread), dramatically increasing startup speed. See [below][modbus] for more information. ## Registering an Event Handler @@ -36,7 +36,7 @@ Alternatively, event handlers can be annotation-driven by creating an event hand ```java public class EventHandler { - @SubscribeEvent + @SubscribeEvent public void onLivingJump(LivingJumpEvent event) { Entity entity = event.getEntity(); if (!entity.level().isClientSide()) { @@ -97,14 +97,14 @@ public class EventHandler { ### Fields and Methods -Probably the most obvious part. Most events contain context for the event handler to use, such as an entity causing the event or a level the event occurs in. +Fields and methods are probably the most obvious part of an event. Most events contain context for the event handler to use, such as an entity causing the event or a level the event occurs in. ### Hierarchy In order to use the advantages of inheritance, some events do not directly extend `Event`, but one of its subclasses, for example `BlockEvent` (which contains block context for block-related events) or `EntityEvent` (which similarly contains entity context) and its subclasses `LivingEvent` (for `LivingEntity`-specific context) and `PlayerEvent` (for `Player`-specific context). These context-providing super events are `abstract` and cannot be listened to. :::danger -If you listen to an `abstract` event, your game will crash, as this is not what you want. Listen to one of its sub events instead. +If you listen to an `abstract` event, your game will crash, as this is never what you want. You always want to listen to one of the subevents instead. ::: ### Cancellable Events @@ -125,7 +125,7 @@ Results are deprecated and will be replaced by more specific per-event results s Event handlers can optionally get assigned a priority. The `EventPriority` enum contains five values: `HIGHEST`, `HIGH`, `NORMAL` (default), `LOW` and `LOWEST`. Events are executed from highest to lowest priority, with undefined order for two events of the same priority. -Priorities can be defined by setting the `priority` parameter in `IEventBus#addListener` or `@SubscribeEvent`, depending on how you attach event handlers. +Priorities can be defined by setting the `priority` parameter in `IEventBus#addListener` or `@SubscribeEvent`, depending on how you attach event handlers. Note that priorities are ignored for events that are fired in parallel. ### Sided Events @@ -173,7 +173,7 @@ Next to the lifecycle events, there are a few miscellaneous events that are fire - `TextureStitchEvent` :::warning -Most of these events will be moved to `NeoForge.EVENT_BUS` eventually. +Most of these events are planned to be moved to the main event bus in a future version. ::: [modbus]: #event-buses diff --git a/docs/items/index.md b/docs/items/index.md index ba74b916..1c45deb7 100644 --- a/docs/items/index.md +++ b/docs/items/index.md @@ -75,7 +75,7 @@ If you want to use `Item::new`, you can leave out the factory entirely: ```java public static final Supplier EXAMPLE_ITEM = ITEMS.registerItem( "example_item", - new ItemBehaviour.Properties() // The properties to use. + new Item.Properties() // The properties to use. ); ``` @@ -101,6 +101,10 @@ public static final Supplier EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpl public static final Supplier EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(ExampleBlocksClass.EXAMPLE_BLOCK); ``` +:::note +If you keep your blocks in a separate class, you should classload your blocks class before your items class. +::: + ### Resources If you register your item and get your item (via `/give` or through a [creative tab][creativetabs]), you will find it to be missing a proper model and texture. This is because textures and models are handled by Minecraft's resource system. From 1ed292c0f7cc49e8594c54ed8cfc6940c719424b Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 <52419336+IchHabeHunger54@users.noreply.github.com> Date: Fri, 23 Feb 2024 00:42:07 +0100 Subject: [PATCH 14/41] Update docs/blocks/index.md Co-authored-by: Dennis C --- docs/blocks/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blocks/index.md b/docs/blocks/index.md index ebc67c57..3622a17d 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -8,7 +8,7 @@ Before we get started, it is important to understand that there is only ever one Due to this, a block should only ever be instantiated once, and that is during [registration]. Once the block is registered, you can then use the registered reference as needed. -Unlike most other registries, blocks use a specialized version of `DeferredRegister`, called `DeferredRegister.Blocks`. `DeferredRegister.Blocks` acts basically like a `DeferredRegister`, but with some minor differences: +Unlike most other registries, blocks can use a specialized version of `DeferredRegister`, called `DeferredRegister.Blocks`. `DeferredRegister.Blocks` acts basically like a `DeferredRegister`, but with some minor differences: - They are created via `DeferredRegister.createBlocks("yourmodid")` instead of the regular `DeferredRegister.create(...)` method. - `#register` returns a `DeferredBlock`, which extends `DeferredHolder`. `T` is the type of the class of the block we are registering. From e2fbc3190cf2792bdfad5bde46dd3d70450dc705 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 <52419336+IchHabeHunger54@users.noreply.github.com> Date: Fri, 23 Feb 2024 00:42:20 +0100 Subject: [PATCH 15/41] Update docs/concepts/registries.md Co-authored-by: Dennis C --- docs/concepts/registries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 3c123712..c1cc30a0 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -59,7 +59,7 @@ public static final Supplier EXAMPLE_BLOCK = BLOCKS.register( ); ``` -Be aware that a few places explicitly require a `DeferredHolder` and will not just accept any `Supplier`. If you need a `DeferredHolder`, it is best to change the type of your `Supplier` back to `DeferredHolder`. +Be aware that a few places explicitly require a `Holder` or `DeferredHolder` and will not just accept any `Supplier`. If you need either of those two, it is best to change the type of your `Supplier` back to `Holder` or `DeferredHolder` as necessary. Finally, since the entire system is a wrapper around registry events, we need to tell the `DeferredRegister` to attach itself to the registry events as needed: From 40c17a949a53cbcf89b23c543310023ebaa4d5f5 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Fri, 23 Feb 2024 01:00:23 +0100 Subject: [PATCH 16/41] implement most of XFact's feedback --- docs/concepts/events.md | 2 +- docs/concepts/registries.md | 17 +++++++++-------- docs/concepts/sides.md | 2 +- docs/items/index.md | 8 ++++---- docs/misc/resourcelocation.md | 8 +++++--- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/concepts/events.md b/docs/concepts/events.md index 877dbafa..64b0b6f2 100644 --- a/docs/concepts/events.md +++ b/docs/concepts/events.md @@ -123,7 +123,7 @@ Results are deprecated and will be replaced by more specific per-event results s ### Priority -Event handlers can optionally get assigned a priority. The `EventPriority` enum contains five values: `HIGHEST`, `HIGH`, `NORMAL` (default), `LOW` and `LOWEST`. Events are executed from highest to lowest priority, with undefined order for two events of the same priority. +Event handlers can optionally get assigned a priority. The `EventPriority` enum contains five values: `HIGHEST`, `HIGH`, `NORMAL` (default), `LOW` and `LOWEST`. Event handlers are executed from highest to lowest priority. If they have the same priority, they fire in registration order on the main bus, which is roughly related to mod load order, and in exact mod load order on the mod bus (see below). Priorities can be defined by setting the `priority` parameter in `IEventBus#addListener` or `@SubscribeEvent`, depending on how you attach event handlers. Note that priorities are ignored for events that are fired in parallel. diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index c1cc30a0..27c5959f 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -18,7 +18,7 @@ The main reason all of this works is that `Blocks` is classloaded early enough b ## Methods for Registering -NeoForge offers two ways to register objects: the `DeferredRegister` class, and the `RegisterEvent`. Note that the former is a wrapper around the latter, and is thus recommended in order to prevent mistakes. +NeoForge offers two ways to register objects: the `DeferredRegister` class, and the `RegisterEvent`. Note that the former is a wrapper around the latter, and is recommended in order to prevent mistakes. ### `DeferredRegister` @@ -85,8 +85,9 @@ There are specialized variants of `DeferredRegister`s for blocks and items that public void register(RegisterEvent event) { event.register( // This is the registry key of the registry. - // Get these from Registries for vanilla registries, or from NeoForgeRegistries.Keys for NeoForge registries. - Registries.BLOCKS, + // Get these from BuiltInRegistries for vanilla registries, + // or from NeoForgeRegistries.Keys for NeoForge registries. + BuiltInRegistries.BLOCKS, // Register your objects here. registry -> { registry.register(new ResourceLocation(MODID, "example_block_1"), new Block(...)); @@ -102,12 +103,12 @@ public void register(RegisterEvent event) { Sometimes, you will find yourself in situations where you want to get a registered object by a given id. Or, you want to get the id of a certain registered object. Since registries are basically maps of ids (`ResourceLocation`s) to distinct objects, i.e. a reversible map, both of these operations work: ```java -Registries.BLOCKS.get(new ResourceLocation("minecraft", "dirt")); // returns the dirt block -Registries.BLOCKS.getKey(Blocks.DIRT); // returns the resource location "minecraft:dirt" +BuiltInRegistries.BLOCKS.get(new ResourceLocation("minecraft", "dirt")); // returns the dirt block +BuiltInRegistries.BLOCKS.getKey(Blocks.DIRT); // returns the resource location "minecraft:dirt" // Assume that ExampleBlocksClass.EXAMPLE_BLOCK.get() is a Supplier with the id "yourmodid:example_block" -Registries.BLOCKS.get(new ResourceLocation("yourmodid", "example_block")); // returns the example block -Registries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // returns the resource location "yourmodid:example_block" +BuiltInRegistries.BLOCKS.get(new ResourceLocation("yourmodid", "example_block")); // returns the example block +BuiltInRegistries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // returns the resource location "yourmodid:example_block" ``` If you just want to check for the presence of an object, this is also possible, though only with keys: @@ -131,7 +132,7 @@ for (Map.Entry entry : Registries.BLOCKS.entrySet()) { ``` :::note -Query operations should always use vanilla `Registry`s, not `DeferredRegister`s. This is because `DeferredRegister`s are merely registration utilities and effectively do not exist after registration has finished. +Query operations always use vanilla `Registry`s, not `DeferredRegister`s. This is because `DeferredRegister`s are merely registration utilities. ::: :::danger diff --git a/docs/concepts/sides.md b/docs/concepts/sides.md index f1c61260..48712d43 100644 --- a/docs/concepts/sides.md +++ b/docs/concepts/sides.md @@ -18,7 +18,7 @@ The logical side is mainly focused on the internal program structure of Minecraf The difference between physical and logical sides is best exemplified by two scenarios: -- The player joins a **multiplayer** world. This is fairly straightforward: The player's logical and physical client connects to a logical server somewhere else - the player does not care where; so long as they can connect, that's all the client knows of, and all the client needs to know. +- The player joins a **multiplayer** world. This is fairly straightforward: The player's physical (and logical) client connects to a physical (and logical) server somewhere else - the player does not care where; so long as they can connect, that's all the client knows of, and all the client needs to know. - The player joins a **singleplayer** world. This is where things get interesting. The player's physical client spins up a logical server and then, now in the role of the logical client, connects to that logical server on the same machine. If you are familiar with networking, you can think of it as a connection to `localhost` (only conceptually; there are no actual sockets or similar involved). These two scenarios also show the main problem with this: If a logical server can work with your code, that alone doesn't guarantee that a physical server will be able to work with as well. This is why you should always test with dedicated servers to check for unexpected behavior. `NoClassDefFoundError`s and `ClassNotFoundException`s due to incorrect client and server separation are among the most common errors there are in modding. Another common mistake is working with static fields and accessing them from both logical sides; this is particularly tricky because there's usually no indication that something is wrong. diff --git a/docs/items/index.md b/docs/items/index.md index 1c45deb7..dab3959d 100644 --- a/docs/items/index.md +++ b/docs/items/index.md @@ -70,10 +70,10 @@ public static final Supplier EXAMPLE_ITEM = ITEMS.registerItem( Internally, this will simply call `ITEMS.register("example_item", () -> new Item(new Item.Properties()))` by applying the properties parameter to the provided item factory (which is commonly the constructor). -If you want to use `Item::new`, you can leave out the factory entirely: +If you want to use `Item::new`, you can leave out the factory entirely and use the `simple` method variant: ```java -public static final Supplier EXAMPLE_ITEM = ITEMS.registerItem( +public static final Supplier EXAMPLE_ITEM = ITEMS.registerSimpleItem( "example_item", new Item.Properties() // The properties to use. ); @@ -81,10 +81,10 @@ public static final Supplier EXAMPLE_ITEM = ITEMS.registerItem( This does the exact same as the previous example, but is slightly shorter. Of course, if you want to use a subclass of `Item` and not `Item` itself, you will have to use the previous method instead. -Both of these methods also have `simple` counterparts that omit the `new Item.Properties()` parameter: +Both of these methods also have overloads that omit the `new Item.Properties()` parameter: ```java -public static final Supplier EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item", Item::new); +public static final Supplier EXAMPLE_ITEM = ITEMS.registerItem("example_item", Item::new); // Variant that also omits the Item::new parameter public static final Supplier EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item"); ``` diff --git a/docs/misc/resourcelocation.md b/docs/misc/resourcelocation.md index 9b2d07fb..f317a734 100644 --- a/docs/misc/resourcelocation.md +++ b/docs/misc/resourcelocation.md @@ -2,7 +2,7 @@ `ResourceLocation`s are one of the most important things in Minecraft. They are used as keys in [registries][registries], as identifiers for data or resource files, as references to models in code, and in a lot of other places. A `ResourceLocation` consists of two parts: a namespace and a path, separated by a `:`. -The namespace denotes what mod or datapack the location refers to. For example, a mod with the mod id `examplemod` will use the `examplemod` namespace. Minecraft uses the `minecraft` namespace. Extra namespaces can be defined at will simply by creating a corresponding data folder, this is usually done by datapacks to keep their logic separate from the point where they integrate with vanilla. +The namespace denotes what mod, resource pack or datapack the location refers to. For example, a mod with the mod id `examplemod` will use the `examplemod` namespace. Minecraft uses the `minecraft` namespace. Extra namespaces can be defined at will simply by creating a corresponding data folder, this is usually done by datapacks to keep their logic separate from the point where they integrate with vanilla. The path is a reference to whatever object you want, inside your namespace. For example, `minecraft:cow` is a reference to something named `cow` in the `minecraft` namespace - usually this location would be used to get the cow entity from the entity registry. Another example would be `examplemod:example_item`, which would probably be used to get your mod's `example_item` from the item registry. @@ -23,14 +23,14 @@ The namespace and path of a `ResourceLocation` can be retrieved using `ResourceL Some places, for example registries, use `ResourceLocation`s directly. Some other places, however, will resolve the `ResourceLocation` as needed. For example: - `ResourceLocation`s are used as identifiers for GUI background. For example, the furnace GUI uses the resource location `minecraft:textures/gui/container/furnace.png`. This maps to the file `assets/minecraft/textures/gui/container/furnace.png` on disk. Note that the `.png` suffix is required in this resource location. -- `ResourceLocation`s are used as identifiers for block models. For example, the block model of dirt uses the resource location `minecraft:models/block/dirt`. This maps to the file `assets/minecraft/models/block/dirt.json` on disk. Note that the `.json` suffix is NOT required in this resource location. +- `ResourceLocation`s are used as identifiers for block models. For example, the block model of dirt uses the resource location `minecraft:block/dirt`. This maps to the file `assets/minecraft/models/block/dirt.json` on disk. Note that the `.json` suffix is not required here. Note as well that this resource location automatically maps into the `models` subfolder. - `ResourceLocation`s are used as identifiers for recipes. For example, the iron block crafting recipe uses the resource location `minecraft:iron_block`. This maps to the file `data/minecraft/recipes/iron_block.json` on disk. Note that the `.json` suffix is not required here. Note as well that this resource location automatically maps into the `recipes` subfolder. Whether the `ResourceLocation` expects a file suffix, or what exactly the resource location resolves to, depends on the use case. ## `ModelResourceLocation`s -`ModelResourceLocation`s are a special kind of resource location that includes a third part, called the variant. Minecraft uses these mainly to differentiate between different variants of item models, where the different variants are used in different display contexts (for example with tridents, which have different models in first person, third person and inventories). +`ModelResourceLocation`s are a special kind of resource location that includes a third part, called the variant. Minecraft uses these mainly to differentiate between different variants of models, where the different variants are used in different display contexts (for example with tridents, which have different models in first person, third person and inventories). The variant is always `inventory` for items, and the comma-delimited string of property-value pairs for blockstates (for example `facing=north,waterlogged=false`, empty for blocks with no blockstate properties). The variant is appended to the regular resource location, along with a `#`. For example, the full name of the diamond sword's item model is `minecraft:diamond_sword#inventory`. However, in most contexts, the `inventory` variant can be omitted. @@ -42,5 +42,7 @@ The variant is appended to the regular resource location, along with a `#`. For A new `ResourceKey` can be created through the static method `ResourceKey#create(ResourceKey>, ResourceLocation)`. The second parameter here is the registry name, while the first parameter is what is known as a registry key. Registry keys are a special kind of `ResourceKey` whose registry is the root registry (i.e. the registry of all other registries). A registry key can be created via `ResourceKey#createRegistryKey(ResourceLocation)` with the desired registry's id. +`ResourceKey`s are interned at creation. This means that comparing by reference equality (`==`) is possible and encouraged, but their creation is comparatively expensive. + [registries]: ../concepts/registries.md [sides]: ../concepts/sides.md From 7a24414dad609180c3f6cabab101abdd6490c8a3 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Fri, 23 Feb 2024 01:13:58 +0100 Subject: [PATCH 17/41] fix datapack registry creation --- docs/concepts/registries.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 27c5959f..4c9939ac 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -198,12 +198,14 @@ Datapack registries can be obtained from a `RegistryAccess`. This `RegistryAcces ### Custom Datapack Registries -Custom datapack registries are created through `RegistryBuilder` like all other registries, but are registered to `DataPackRegistryEvent.NewRegistry` instead of `NewRegistryEvent`. Reiterating the spells example from before, registering our spell registry as a datapack registry looks something like this: +Custom datapack registries do not require a `Registry` to be constructed. Instead, they just need a registry key and at least one [`Codec`][codec] to (de-)serialize its contents. Reiterating on the spells example from before, registering our spell registry as a datapack registry looks something like this: ```java +public static final ResourceKey> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(new ResourceLocation("yourmodid", "spells")); + @SubscribeEvent -static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) { - event.register( +public static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) { + event.dataPackRegistry( // The registry key. SPELL_REGISTRY_KEY, // The codec of the registry contents. From 55873a11245fe70f12daa07fd9c4520100b57e29 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Fri, 23 Feb 2024 16:10:40 +0100 Subject: [PATCH 18/41] remove recommendation of separate DR and DH classes --- docs/concepts/registries.md | 4 ---- docs/items/index.md | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 4c9939ac..83381c3a 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -46,10 +46,6 @@ public static final DeferredHolder EXAMPLE_BLOCK = BLOCKS.register The class `DeferredHolder` holds our object. The type parameter `R` is the type of the registry we are registering to (in our case `Block`). The type parameter `T` is the type of our supplier. Since we directly register a `Block` in this example, we provide `Block` as the second parameter. If we were to register an object of a subclass of `Block`, for example `SlabBlock`, we would provide `SlabBlock` here instead. -:::note -Some modders prefer to keep their `DeferredRegister`s in the same class as their registered objects. Others prefer keeping all `DeferredRegister`s in a separate class for readability. This is mostly a design decision, however if you decide to do the latter, make sure to classload the classes the objects are in, for example through an empty static method. -::: - `DeferredHolder` is a subclass of `Supplier`. To get our registered object when we need it, we can call `DeferredHolder#get()`. The fact that `DeferredHolder` extends `Supplier` also allows us to use `Supplier` as the type of our field. That way, the above code block becomes the following: ```java diff --git a/docs/items/index.md b/docs/items/index.md index dab3959d..73ff7a79 100644 --- a/docs/items/index.md +++ b/docs/items/index.md @@ -102,7 +102,7 @@ public static final Supplier EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpl ``` :::note -If you keep your blocks in a separate class, you should classload your blocks class before your items class. +If you keep your registered blocks in a separate class, you should classload your blocks class before your items class. ::: ### Resources From cdf58f2f23423f8341c5c00d37fc08415b502d2c Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Fri, 23 Feb 2024 16:41:34 +0100 Subject: [PATCH 19/41] fix an oversight --- docs/concepts/registries.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 83381c3a..5e901f77 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -110,8 +110,8 @@ BuiltInRegistries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // retu If you just want to check for the presence of an object, this is also possible, though only with keys: ```java -Registries.BLOCKS.containsKey(new ResourceLocation("minecraft", "dirt")); // true -Registries.BLOCKS.containsKey(new ResourceLocation("create", "brass_ingot")); // true only if Create is installed +BuiltInRegistries.BLOCKS.containsKey(new ResourceLocation("minecraft", "dirt")); // true +BuiltInRegistries.BLOCKS.containsKey(new ResourceLocation("create", "brass_ingot")); // true only if Create is installed ``` As the last example shows, this is possible with any mod id, and thus a perfect way to check if a certain item from another mod exists. From 380db4041bb063948beef79cfee86289b2e138e0 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 <52419336+IchHabeHunger54@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:17:23 +0100 Subject: [PATCH 20/41] Update docs/blocks/index.md Co-authored-by: ChampionAsh5357 --- docs/blocks/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blocks/index.md b/docs/blocks/index.md index 3622a17d..5a0c0760 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -106,7 +106,7 @@ public static final DeferredBlock EXAMPLE_BLOCK = BLOCKS.registerBlock( ); ``` -Internally, this will simply call `BLOCKS.register("example_block", () -> new Block(new BlockBehaviour.Properties()))` by applying the properties parameter to the provided block factory (which is commonly the constructor). +Internally, this will simply call `BLOCKS.register("example_block", () -> new Block(BlockBehaviour.Properties.of()))` by applying the properties parameter to the provided block factory (which is commonly the constructor). If you want to use `Block::new`, you can leave out the factory entirely: From 2a369a74d9e5b42f3a687bedeb33b55a2a44de37 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Sun, 25 Feb 2024 19:46:35 +0100 Subject: [PATCH 21/41] BootstapContext :screm: --- docs/concepts/registries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index 5e901f77..ebf04461 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -255,7 +255,7 @@ new RegistrySetBuilder() }); ``` -The `BootstrapContext` can also be used to lookup entries from another registry if needed: +The `BootstrapContext` (name is typoed as `BootstapContext` in 1.20.4 and below) can also be used to lookup entries from another registry if needed: ```java public static final ResourceKey> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create( From b5a78bb1efce03065b86866ad2f8f3df22e1e0c7 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 26 Feb 2024 15:43:02 +0100 Subject: [PATCH 22/41] fix two wrong mentions of Registries --- docs/concepts/registries.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index ebf04461..d4d97e13 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -119,10 +119,10 @@ As the last example shows, this is possible with any mod id, and thus a perfect Finally, we can also iterate over all entries in a registry, either over the keys or over the entries (entries use the Java `Map.Entry` type): ```java -for (ResourceLocation id : Registries.BLOCKS.keySet()) { +for (ResourceLocation id : BuiltInRegistries.BLOCKS.keySet()) { // ... } -for (Map.Entry entry : Registries.BLOCKS.entrySet()) { +for (Map.Entry entry : BuiltInRegistries.BLOCKS.entrySet()) { // ... } ``` From b6dcbb9f583e206f1ff6f0b94e0b94e28808de49 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 26 Feb 2024 16:28:03 +0100 Subject: [PATCH 23/41] fix exception name --- docs/misc/resourcelocation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/misc/resourcelocation.md b/docs/misc/resourcelocation.md index f317a734..89cc8e4f 100644 --- a/docs/misc/resourcelocation.md +++ b/docs/misc/resourcelocation.md @@ -34,7 +34,7 @@ Whether the `ResourceLocation` expects a file suffix, or what exactly the resour The variant is appended to the regular resource location, along with a `#`. For example, the full name of the diamond sword's item model is `minecraft:diamond_sword#inventory`. However, in most contexts, the `inventory` variant can be omitted. -`ModelResourceLocation` is a [client only][sides] class. This means that servers referencing this class will crash with a `NoClassDefError`. +`ModelResourceLocation` is a [client only][sides] class. This means that servers referencing this class will crash with a `NoClassDefFoundError`. ## `ResourceKey`s From 3c74c22aa224666eb997932310453bd8318900af Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 4 Mar 2024 14:08:26 +0100 Subject: [PATCH 24/41] merge resources and datagen index pages into one rewritten index page --- docs/concepts/registries.md | 2 +- docs/datagen/index.md | 86 --------------- docs/gettingstarted/structuring.md | 2 +- docs/resources/_category_.json | 3 - docs/resources/index.md | 171 +++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 91 deletions(-) delete mode 100644 docs/datagen/index.md delete mode 100644 docs/resources/_category_.json create mode 100644 docs/resources/index.md diff --git a/docs/concepts/registries.md b/docs/concepts/registries.md index d4d97e13..5dea1fab 100644 --- a/docs/concepts/registries.md +++ b/docs/concepts/registries.md @@ -305,7 +305,7 @@ static void onGatherData(GatherDataEvent event) { [blockentity]: ../blockentities/index.md [codec]: ../datastorage/codecs.md [datagen]: #data-generation-for-datapack-registries -[datagenindex]: ../datagen/index.md +[datagenindex]: ../resources/index.md#data-generation [datapack]: ../resources/server/index.md [defregblocks]: ../blocks/index.md#deferredregisterblocks-helpers [defregitems]: ../items/index.md#deferredregisteritems diff --git a/docs/datagen/index.md b/docs/datagen/index.md deleted file mode 100644 index 358ca636..00000000 --- a/docs/datagen/index.md +++ /dev/null @@ -1,86 +0,0 @@ -Data Generators -=============== - -Data generators are a way to programmatically generate the assets and data of mods. It allows the definition of the contents of these files in the code and their automatic generation, without worrying about the specifics. - -The data generator system is loaded by the main class `net.minecraft.data.Main`. Different command-line arguments can be passed to customize which mods' data are gathered, what existing files are considered, etc. The class responsible for data generation is `net.minecraft.data.DataGenerator`. - -The default configurations in the MDK `build.gradle` adds the `runData` task for running the data generators. - -Existing Files --------------- -All references to textures or other data files not generated for data generation must reference existing files on the system. This is to ensure that all referenced textures are in the correct places, so typos can be found and corrected. - -`ExistingFileHelper` is the class responsible for validating the existence of those data files. An instance can be retrieved from `GatherDataEvent#getExistingFileHelper`. - -The `--existing ` argument allows the specified folder and its subfolders to be used when validating the existence of files. Additionally, the `--existing-mod ` argument allows the resources of a loaded mod to be used for validation. By default, only the vanilla datapack and resources are available to the `ExistingFileHelper`. - -Generator Modes ---------------- - -The data generator can be configured to run 4 different data generations, which are configured from the command-line parameters, and can be checked from `GatherDataEvent#include***` methods. - -* __Client Assets__ - * Generates client-only files in `assets`: block/item models, blockstate JSONs, language files, etc. - * __`--client`__, `#includeClient` -* __Server Data__ - * Generates server-only files in `data`: recipes, advancements, tags, etc. - * __`--server`__, `#includeServer` -* __Development Tools__ - * Runs some development tools: converting SNBT to NBT and vice-versa, etc. - * __`--dev`__, `#includeDev` -* __Reports__ - * Dumps all registered blocks, items, commands, etc. - * __`--reports`__, `#includeReports` - -All of the generators can be included using `--all`. - -Data Providers --------------- - -Data providers are the classes that actually define what data will be generated and provided. All data providers implement `DataProvider`. Minecraft has abstract implementations for most assets and data, so modders need only to extend and override the specified method. - -The `GatherDataEvent` is fired on the mod event bus when the data generator is being created, and the `DataGenerator` can be obtained from the event. Create and register data providers using `DataGenerator#addProvider`. - -### Client Assets -* [`net.neoforged.neoforge.common.data.LanguageProvider`][langgen] - for [language strings][lang]; implement `#addTranslations` -* [`net.neoforged.neoforge.common.data.SoundDefinitionsProvider`][soundgen] - for [`sounds.json`][sounds]; implement `#registerSounds` -* [`net.neoforged.neoforge.client.model.generators.ModelProvider`][modelgen] - for [models]; implement `#registerModels` - * [`ItemModelProvider`][itemmodelgen] - for item models - * [`BlockModelProvider`][blockmodelgen] - for block models -* [`net.neoforged.neoforge.client.model.generators.BlockStateProvider`][blockstategen] - for blockstate JSONs and their block and item models; implement `#registerStatesAndModels` - -### Server Data - -**These classes are under the `net.neoforged.neoforge.common.data` package**: - -* [`GlobalLootModifierProvider`][glmgen] - for [global loot modifiers][glm]; implement `#start` -* [`DatapackBuiltinEntriesProvider`][datapackregistriesgen] - for datapack registry objects; see the linked article - -**These classes are under the `net.minecraft.data` package**: - -* [`loot.LootTableProvider`][loottablegen] - for [loot tables][loottable]; pass in `LootTableProvider$SubProviderEntry`s to the constructor -* [`recipes.RecipeProvider`][recipegen] - for [recipes] and their unlocking advancements; implement `#buildRecipes` -* [`tags.TagsProvider`][taggen] - for [tags]; implement `#addTags` -* [`advancements.AdvancementProvider`][advgen] - for [advancements]; pass in `AdvancementSubProvider`s to the constructor - -[langgen]: ./client/localization.md -[lang]: https://minecraft.wiki/w/Language -[soundgen]: ./client/sounds.md -[sounds]: https://minecraft.wiki/w/Sounds.json -[modelgen]: ./client/modelproviders.md -[models]: ../resources/client/models/index.md -[itemmodelgen]: ./client/modelproviders.md#itemmodelprovider -[blockmodelgen]: ./client/modelproviders.md#blockmodelprovider -[blockstategen]: ./client/modelproviders.md#block-state-provider -[glmgen]: ./server/glm.md -[glm]: ../resources/server/glm.md -[datapackregistriesgen]: ../concepts/registries.md#data-generation-for-datapack-registries -[loottablegen]: ./server/loottables.md -[loottable]: ../resources/server/loottables.md -[recipegen]: ./server/recipes.md -[recipes]: ../resources/server/recipes/index.md -[taggen]: ./server/tags.md -[tags]: ../resources/server/tags.md -[advgen]: ./server/advancements.md -[advancements]: ../resources/server/advancements.md diff --git a/docs/gettingstarted/structuring.md b/docs/gettingstarted/structuring.md index cdb65a5e..c91ba153 100644 --- a/docs/gettingstarted/structuring.md +++ b/docs/gettingstarted/structuring.md @@ -76,5 +76,5 @@ There are many methods for performing a certain task: registering an object, lis [group]: index.md#the-group-id [naming]: https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html -[datagen]: ../datagen/index.md +[datagen]: ../resources/index.md#data-generation [sides]: ../concepts/sides.md diff --git a/docs/resources/_category_.json b/docs/resources/_category_.json deleted file mode 100644 index 678d589f..00000000 --- a/docs/resources/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Resources" -} \ No newline at end of file diff --git a/docs/resources/index.md b/docs/resources/index.md new file mode 100644 index 00000000..149268a6 --- /dev/null +++ b/docs/resources/index.md @@ -0,0 +1,171 @@ +# Resources + +Resources are external files that are used by the game, but are not code. The most prominent kinds of resources are textures, however, many other types of resources exist in the Minecraft ecosystem. + +Minecraft generally has two kinds of resources: client-side resources, known as assets, and server-side resources, known as data. Assets are mostly display-only information, for example textures, display models, translations, or sounds, while data includes various things that affect gameplay, such as loot tables, recipes, or worldgen information. They are loaded from resource packs and data packs, respectively. NeoForge generates a built-in resource and data pack for every mod. + +Both resource and data packs normally require a [`pack.mcmeta` file][packmcmeta], this was also the case in past Forge versions. However, NeoForge generates these at runtime for you, so you don't need to worry about it anymore. + +## Assets + +_See also: [Resource Packs][mcwikiresourcepacks] on the [Minecraft Wiki][mcwiki]_ + +Assets, or client-side resources, are all resources that are only relevant on the [client][sides]. They are loaded from resource packs, sometimes also known by the old term texture packs (stemming from old versions when they could only affect textures). A resource pack is basically an `assets` folder. The `assets` folder contains subfolders for the various namespaces the resource pack includes; every namespace is one subfolder. For example, a resource pack for a mod with the id `coolmod` will probably contain a `coolmod` namespace, but may additionally include other namespaces, such as `minecraft`. + +NeoForge automatically collects all mod resource packs into the `Mod resources` pack, which sits at the bottom of the Selected Packs side in the resource packs menu. It is currently not possible to disable the `Mod resources` pack. However, resource packs that sit above the `Mod resources` pack override resources defined in a resource pack below them. This mechanic allows resource pack makers to override your mod's resources, and also allows mod developers to override Minecraft resources if needed. + +Resource packs can contain textures, sounds and translation files, along with block models, item models, and blockstate model definitions. (TODO add links) + +## Data + +_See also: [Data Packs][mcwikidatapacks] on the [Minecraft Wiki][mcwiki]_ + +In contrast to assets, data is the term for all [server][sides] resources. Similar to resource packs, data is loaded through data packs (or datapacks). Like a resource pack, a data pack consists of a [`pack.mcmeta` file][packmcmeta] and a root folder, named `data`. Then, again like with resource packs, that `data` folder contains subfolders for the various namespaces the resource pack includes; every namespace is one subfolder. For example, a data pack for a mod with the id `coolmod` will probably contain a `coolmod` namespace, but may additionally include other namespaces, such as `minecraft`. + +NeoForge automatically applies all mod data packs to a new world upon creation. It is currently not possible to disable mod data packs. However, most data files can be overridden (and thus be removed by replacing them with an empty file) by a data pack with a higher priority. Additional data packs can be enabled or disabled by placing them in a world's `datapacks` subfolder and then enabling or disabling them through the [`/datapack`][datapackcmd] command. + +:::info +There is currently no built-in way to apply a set of custom data packs to every world. However, there are a number of mods that achieve this. +::: + +Data packs may contain folders with files affecting the following things (TODO add links): + +| Folder name | Contents | +|-------------------------------------------|----------------| +| `advancements` | Advancements | +| `damage_type` | Damage types | +| `loot_tables` | Loot tables | +| `recipes` | Recipes | +| `structures` | Structures | +| `tags` | Tags | +| `dimension`, `dimension_type`, `worldgen` | Worldgen files | + +Additionally, they may also contain subfolders for some systems that integrate with commands. These systems are rarely used in conjunction with mods, but worth mentioning regardless: + +| Folder name | Contents | +|------------------|--------------------------------| +| `chat_type` | [Chat types][chattype] | +| `functions` | [Functions][function] | +| `item_modifiers` | [Item modifiers][itemmodifier] | +| `predicates` | [Predicates][predicate] | + +## `pack.mcmeta` + +_See also: [`pack.mcmeta` (Resource Pack)][packmcmetaresourcepack] and [`pack.mcmeta` (Data Pack)][packmcmetadatapack] on the [Minecraft Wiki][mcwiki]_ + +`pack.mcmeta` files hold the metadata of a resource or data pack. For mods, NeoForge makes this file obsolete, as the `pack.mcmeta` is generated synthetically. In case you still need a `pack.mcmeta` file, the full specification can be found in the linked Minecraft Wiki articles. + +## Data Generation + +Data generation, colloquially known as datagen, is a way to programmatically generate JSON resource files, in order to avoid the tedious and error-prone process of writing them by hand. The name is a bit misleading, as it works for assets as well as data. + +Datagen is run through the Data run configuration, which is generated for you alongside the Client and Server run configurations. The data run configuration follows the [mod lifecycle][lifecycle] until after the registry events are fired. It then fires the [`GatherDataEvent`][event], in which you can register your to-be-generated objects in the form of data providers, writes said objects to disk, and ends the process. + +All data providers extend the `DataProvider` interface and usually require one method to be overridden. The following is a list of all data generators Minecraft and NeoForge offer (the linked articles add further information, such as helper methods, TODO add links): + +| Class | Method | Generates | Side | Notes | +|--------------------------------------|----------------------------------|--------------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BlockStateProvider` | `registerStatesAndModels()` | Blockstate model definitions, block models | Client | | +| `ItemModelProvider` | `registerModels()` | Item models | Client | | +| `LanguageProvider` | `addTranslations()` | Translations | Client | Also requires passing the language in the constructor. | +| `ParticleDescriptionProvider` | `addDescriptions()` | Particle definitions | Client | | +| `SoundDefinitionsProvider` | `registerSounds()` | Sound definitions | Client | | +| `AdvancementProvider` | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | +| `LootTableProvider` | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | +| `RecipeProvider` | `buildRecipes(RecipeOutput)` | Recipes | Server | | +| Various subclasses of `TagsProvider` | `addTags(HolderLookup.Provider)` | Tags | Server | Several specialized subclasses exist, for example `BlockTagsProvider`. If the one you need doesn't exist, extend `TagsProvider` (or `IntrinsicHolderTagsProvider` if applicable) with your tag type as the generic parameter. | +| `DatapackBuiltinEntriesProvider` | N/A | Datapack builtin entries, e.g. worldgen | Server | See linked article for details. | +| `DataMapProvider` | `gather()` | Data map entries | Server | | +| `GlobalLootModifierProvider` | `start()` | Global loot modifiers | Server | | + +All of these providers follow the same pattern. First, you create a subclass and add your own resources to be generated. Then, you add the provider to the event in an [event handler][eventhandler]. An example using a `RecipeProvider`: + +```java +public class MyRecipeProvider extends RecipeProvider { + public MyRecipeProvider(PackOutput output) { + super(output); + } + + @Override + protected void buildRecipes(RecipeOutput output) { + // Register your recipes here. + } +} + +@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, modid = "examplemod") +public class MyDatagenHandler { + @SubscribeEvent + public static void gatherData(GatherDataEvent event) { + // Data generators may require some of these as constructor parameters. + // See below for more details on each of these. + DataGenerator generator = event.getGenerator(); + PackOutput output = generator.getPackOutput(); + ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); + CompletableFuture lookupProvider = event.getLookupProvider(); + + // Register the provider. + generator.addProvider( + // A boolean that determines whether the data should actually be generated. + // The event provides methods that determine this: + // event.includeClient(), event.includeServer(), + // event.includeDev() and event.includeReports(). + // Since recipes are server data, we only run them in a server datagen. + event.includeServer(), + // Our provider. + new MyRecipeProvider(output) + ); + // Other data providers here. + } +} +``` + +The event offers some context for you to use: + +- `event.getGenerator()` returns the `DataGenerator` that you register the providers to. +- `event.getPackOutput()` returns a `PackOutput` that is used by some providers to determine their file output location. +- `event.getExistingFileHelper()` returns an `ExistingFileHelper` that is used by providers for things that can reference other files (for example block models, which can specify a parent file). +- `event.getLookupProvider()` returns a `CompletableFuture` that is mainly used by tags and datagen registries to reference other, potentially not yet existing elements. +- `event.includeClient()`, `event.includeServer()`, `event.includeDev()` and `event.includeReports()` are `boolean` methods that allow you to check whether specific command line arguments (see below) are enabled. + +### Command Line Arguments + +The data generator can accept several command line arguments: + +- `--mod examplemod`: Tells the data generator to run datagen for this mod. Automatically added by NeoGradle for the owning mod id, add this if you e.g. have multiple mods in one project. +- `--output path/to/folder`: Tells the data generator to output into the given folder. It is recommended to use Gradle's `file(...).getAbsolutePath()` to generate an absolute path for you (with a path relative to the project root directory). Defaults to `file('src/generated/resources').getAbsolutePath()`. +- `--existing path/to/folder`: Tells the data generator to consider the given folder when checking for existing files. Like with the output, it is recommended to use Gradle's `file(...).getAbsolutePath()`. +- `--existing-mod examplemod`: Tells the data generator to consider the resources in the given mod's JAR file when checking for existing files. +- Generator modes (all of these are boolean arguments and do not need any additional arguments): + - `--includeClient`: Whether to generate client resources (assets). Check at runtime with `GatherDataEvent#includeClient()`. + - `--includeServer`: Whether to generate server resources (data). Check at runtime with `GatherDataEvent#includeServer()`. + - `--includeDev`: Whether to run dev tools. Generally shouldn't be used by mods. Check at runtime with `GatherDataEvent#includeDev()`. + - `--includeReports`: Whether to dump a list of registered objects. Check at runtime with `GatherDataEvent#includeReports()`. + - `--all`: Enable all generator modes. + +All arguments can be added to the run configurations by adding the following to your `build.gradle`: + +```groovy +runs { + // other run configurations here + + data { + programArguments.addAll '--arg1', 'value1', '--arg2', 'value2', '--all' // boolean args have no value + } +} +``` + +[chattype]: https://minecraft.wiki/w/Chat_type +[datapackcmd]: https://minecraft.wiki/w/Commands/datapack +[event]: ../concepts/events.md +[eventhandler]: ../concepts/events.md#registering-an-event-handler +[function]: https://minecraft.wiki/w/Function_(Java_Edition) +[itemmodifier]: https://minecraft.wiki/w/Item_modifier +[lifecycle]: ../concepts/events.md#the-mod-lifecycle +[mcwiki]: https://minecraft.wiki +[mcwikidatapacks]: https://minecraft.wiki/w/Data_pack +[mcwikiresourcepacks]: https://minecraft.wiki/w/Resource_pack +[packmcmeta]: #packmcmeta +[packmcmetadatapack]: https://minecraft.wiki/w/Data_pack#pack.mcmeta +[packmcmetaresourcepack]: https://minecraft.wiki/w/Resource_pack#Contents +[predicate]: https://minecraft.wiki/w/Predicate +[sides]: ../concepts/sides.md From d72f76d17cc2a93a5d26eace767806b177fa94a6 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Tue, 5 Mar 2024 00:34:22 +0100 Subject: [PATCH 25/41] write a proper i18n article, replacing the separate i18n, component and language articles --- docs/blocks/index.md | 2 +- docs/concepts/internationalization.md | 90 ------------- docs/datagen/client/localization.md | 41 ------ docs/gettingstarted/modfiles.md | 4 +- docs/gui/screens.md | 2 +- docs/items/index.md | 2 +- docs/misc/components.md | 110 ---------------- docs/misc/keymappings.md | 2 +- docs/resources/client/_category_.json | 3 + docs/resources/client/i18n.md | 181 ++++++++++++++++++++++++++ docs/resources/client/index.md | 15 --- docs/resources/index.md | 2 +- 12 files changed, 191 insertions(+), 263 deletions(-) delete mode 100644 docs/concepts/internationalization.md delete mode 100644 docs/datagen/client/localization.md delete mode 100644 docs/misc/components.md create mode 100644 docs/resources/client/_category_.json create mode 100644 docs/resources/client/i18n.md delete mode 100644 docs/resources/client/index.md diff --git a/docs/blocks/index.md b/docs/blocks/index.md index 5a0c0760..f92c8c92 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -234,7 +234,7 @@ Random ticking is used by a wide range of mechanics in Minecraft, such as plant [item]: ../items/index.md [randomtick]: #random-ticking [registration]: ../concepts/registries.md#methods-for-registering -[resources]: ../resources/client/index.md +[resources]: ../resources/index.md#assets [sounds]: ../gameeffects/sounds.md [usingblocks]: #using-blocks [usingblockstates]: states.md#using-blockstates diff --git a/docs/concepts/internationalization.md b/docs/concepts/internationalization.md deleted file mode 100644 index e6a9123d..00000000 --- a/docs/concepts/internationalization.md +++ /dev/null @@ -1,90 +0,0 @@ -# Internationalization and Localization - -Internationalization, i18n for short, is a way of designing code so that it requires no changes to be adapted for various languages. Localization is the process of adapting displayed text to the user's language. - -I18n is implemented using _translation keys_. A translation key is a string that identifies a piece of displayable text in no specific language. For example, `block.minecraft.dirt` is the translation key referring to the name of the Dirt block. This way, displayable text may be referenced with no concern for a specific language. The code requires no changes to be adapted in a new language. - -Localization will happen in the game's locale. In a Minecraft client the locale is specified by the language settings. On a dedicated server, the only supported locale is `en_us`. A list of available locales can be found on the [Minecraft Wiki][langs]. - -## Language files - -Language files are located by `assets/[namespace]/lang/[locale].json` (e.g. all US English translations provided by `examplemod` would be within `assets/examplemod/lang/en_us.json`). The file format is simply a json map from translation keys to values. The file must be encoded in UTF-8. Old .lang files can be converted to json using a [converter][converter]. - -```js -{ - "item.examplemod.example_item": "Example Item Name", - "block.examplemod.example_block": "Example Block Name", - "commands.examplemod.examplecommand.error": "Example Command Errored!" -} -``` - -## Usage with Blocks and Items - -Block, Item and a few other Minecraft classes have built-in translation keys used to display their names. These translation keys are specified by overriding `#getDescriptionId`. Item also has `#getDescriptionId(ItemStack)` which can be overridden to provide different translation keys depending on ItemStack NBT. - -By default, `#getDescriptionId` will return `block.` or `item.` prepended to the registry name of the block or item, with the colon replaced by a dot. `BlockItem`s override this method to take their corresponding `Block`'s translation key by default. For example, an item with ID `examplemod:example_item` effectively requires the following line in a language file: - -```js -{ - "item.examplemod.example_item": "Example Item Name" -} -``` - -:::note -The only purpose of a translation key is internationalization. Do not use them for logic. Use registry names instead. -::: - -## Mod metadata - -Historically, A mod's name and description have been defined in [`mods.toml` files][modinfo] without translation support. Starting with Neoforge v20.4.179, translation files can override mod info using the following keys: - -| Translation Key | Overrides | Example | -|:---------------:|:---------:|:--------| -| `fml.menu.mods.info.displayname.[modid]` | `[[mods]]` -> `displayName` | `fml.menu.mods.info.displayname.examplemod` | -| `fml.menu.mods.info.description.[modid]` | `[[mods]]` -> `description` | `fml.menu.mods.info.description.examplemod` | - -For example, a mod with the id `mymod` could localize its name and description by adding the following lines to a language file: - -```js -{ - "fml.menu.mods.info.displayname.mymod": "My Mod", - "fml.menu.mods.info.description.mymod": "My Mod is super cool and localized!" -} -``` - -:::warning -It is still recommended to define `displayName` and `description` in your [`mods.toml` file][modinfo], because the localization keys are not guaranteed to be supported everywhere. This is especially true for third-party tools. -::: - -## Localization methods - -:::caution -A common issue is having the server localize for clients. The server can only localize in its own locale, which does not necessarily match the locale of connected clients. - -To respect the language settings of clients, the server should have clients localize text in their own locale using `TranslatableContents` or other methods preserving the language neutral translation keys. -::: - -### `net.minecraft.client.resources.language.I18n` (client only) - -**This I18n class can only be found on a Minecraft client!** It is intended to be used by code that only runs on the client. Attempts to use this on a server will throw exceptions and crash. - -- `get(String, Object...)` localizes in the client's locale with formatting. The first parameter is a translation key, and the rest are formatting arguments for `String.format(String, Object...)`. - -### `TranslatableContents` - -`TranslatableContents` is a `ComponentContents` that is localized and formatted lazily. It is very useful when sending messages to players because it will be automatically localized in their own locale. - -The first parameter of the `TranslatableContents(String, Object...)` constructor is a translation key, and the rest are used for [formatting]. - -A `MutableComponent` can be created using `Component#translatable` by passing in the `TranslatableContents`'s parameters. It can also be created using `MutableComponent#create` by passing in the `ComponentContents` itself. -Read [components] for more details. - -### `TextComponentHelper` - -- `createComponentTranslation(CommandSource, String, Object...)` is useful for sending messages between clients and the server. If the receiver is a vanilla client, the method will eagerly localize and format the provided translation key in sender's locale, or American English if no locale is loaded; the modded server may allow vanilla clients to join, and they will lack localization data required to localize the message itself. Otherwise, the method will create the component with `TranslatableContents`. - -[langs]: https://minecraft.wiki/w/Language#Languages -[converter]: https://tterrag.com/lang2json/ -[modinfo]: ../gettingstarted/modfiles.md#mod-specific-properties -[formatting]: ../misc/components.md#text-formatting -[components]: ../misc/components.md diff --git a/docs/datagen/client/localization.md b/docs/datagen/client/localization.md deleted file mode 100644 index c497b338..00000000 --- a/docs/datagen/client/localization.md +++ /dev/null @@ -1,41 +0,0 @@ -Language Generation -=================== - -[Language files][lang] can be generated for a mod by subclassing `LanguageProvider` and implementing `#addTranslations`. Each `LanguageProvider` subclass created represents a separate [locale] (`en_us` represents American English, `es_es` represents Spanish, etc.). After implementation, the provider must be [added][datagen] to the `DataGenerator`. - -```java -// On the MOD event bus -@SubscribeEvent -public void gatherData(GatherDataEvent event) { - event.getGenerator().addProvider( - // Tell generator to run only when client assets are generating - event.includeClient(), - // Localizations for American English - output -> new MyLanguageProvider(output, MOD_ID, "en_us") - ); -} -``` - -`LanguageProvider` ------------------- - -Each language provider is simple a map of strings where each translation key is mapped to a localized name. A translation key mapping can be added using `#add`. Additionally, there are methods which use the translation key of a `Block`, `Item`, `ItemStack`, `Enchantment`, `MobEffect`, and `EntityType`. - -```java -// In LanguageProvider#addTranslations -this.addBlock(EXAMPLE_BLOCK, "Example Block"); -this.add("object.examplemod.example_object", "Example Object"); -``` - -:::tip -Localized names which contain alphanumeric values not in American English can be supplied as is. The provider automatically translates the characters into their unicode equivalents to be read by the game. - -```java -// Encdoded as 'Example with a d\u00EDacritic' -this.addItem("example.diacritic", "Example with a díacritic"); -``` -::: - -[lang]: ../../concepts/internationalization.md -[locale]: https://minecraft.wiki/w/Language#Languages -[datagen]: ../index.md#data-providers diff --git a/docs/gettingstarted/modfiles.md b/docs/gettingstarted/modfiles.md index 444e4f6a..a22d2a05 100644 --- a/docs/gettingstarted/modfiles.md +++ b/docs/gettingstarted/modfiles.md @@ -110,7 +110,7 @@ modId = "examplemod2" | `displayTest` | string | `"MATCH_VERSION"` | See [sides]. | `displayTest="NONE"` | :::note -Some properties (`displayName` and `description`) can also be localized using language files. See [Internationalization and Localization][i18n] for more detail. +Some properties (`displayName` and `description`) can also be localized using language files. See [Translating Mod Metadata][i18n] for more detail. ::: #### Features @@ -170,7 +170,7 @@ There must be a 1-to-1 matching of mods in the `mods.toml` file and `@Mod` entry [events]: ../concepts/events.md [features]: #features [group]: #the-group-id -[i18n]: ../concepts/internationalization.md#mod-metadata +[i18n]: ../resources/client/i18n.md#translating-mod-metadata [javafml]: #javafml-and-mod [jei]: https://www.curseforge.com/minecraft/mc-mods/jei [lowcodefml]: #lowcodefml diff --git a/docs/gui/screens.md b/docs/gui/screens.md index cb6479d6..4bde01e1 100644 --- a/docs/gui/screens.md +++ b/docs/gui/screens.md @@ -341,6 +341,6 @@ private void registerScreens(RegisterMenuScreensEvent event) { [network]: ../networking/index.md [screen]: #the-screen-subtype [argb]: https://en.wikipedia.org/wiki/RGBA_color_model#ARGB32 -[component]: ../concepts/internationalization.md#translatablecontents +[component]: ../resources/client/i18n.md#components [keymapping]: ../misc/keymappings.md#inside-a-gui [modbus]: ../concepts/events.md#event-buses diff --git a/docs/items/index.md b/docs/items/index.md index 73ff7a79..0e1221d4 100644 --- a/docs/items/index.md +++ b/docs/items/index.md @@ -200,5 +200,5 @@ public static final Supplier EXAMPLE_TAB = CREATIVE_MODE_TABS.r [modbus]: ../concepts/events.md#event-buses [nbt]: ../datastorage/nbt.md [registering]: ../concepts/registries.md#methods-for-registering -[resources]: ../resources/client/index.md +[resources]: ../resources/index.md#assets [sides]: ../concepts/sides.md diff --git a/docs/misc/components.md b/docs/misc/components.md deleted file mode 100644 index 296ea9db..00000000 --- a/docs/misc/components.md +++ /dev/null @@ -1,110 +0,0 @@ -Text Components -================== - -A `Component` is a holder for text which can be formatted and chained with other components via its subtype `MutableComponent`. A component can be created using one of the available static helpers: - -| Method Name | Description | -|----------------|------------------------------------------------------------------------------------------------------------------------------------| -| `literal` | it creates a component which simply wraps the passed in text. | -| `nullToEmpty` | it's the same as `#literal` except it creates an empty component if null has been passed | -| `translatable` | it creates a component which is represented as localized text to user, read [internationalization] for more details. | -| `empty` | it creates an empty component | -| `keybind` | it creates a component which is represented as the name of current keyboard key of the passed [key mapping][keymapping]. | -| `nbt` | it creates a component for representing nbt data specified by `path` inside `dataSource` | -| `score` | it creates a component for representing the `objective`'s score of an entity specified by the [entity selector][selectors] `name`. | -| `selector` | it creates a component for displaying the list of names of entities selected by the [entity selector][selectors] `pattern`. | - -A component's text contents are represented by `ComponentContents`. Notably, the subtype `TranslatableContents` not only supports [localization][internationalization] but also [text formatting][formatting]. - -Applying Style --------------- - -Components can be formatted (e.g., bold, click actions, color) via `Style`s. `Style`s are immutable, creating a new `Style` each time when modified. The empty style `Style#EMPTY` can be used as a base for configuration. - -Multiple styles can be merged together with `#applyTo(Style other)`; `other` will override all non-configured of the current object. - -After configuring a style, it can be applied to a component with either `MutableComponent#setStyle` for overwriting, or `#withStyle` for merging: -```java -// Creates MutableComponent wrapping literal "Hello!" -MutableComponent text = Component.literal("Hello!"); - -// Copies empty style and sets color to blue and makes it italic -Style blueItalic = Style.EMPTY - .withColor(0x0000FF) - .withItalic(true); - -// Copies empty style and sets color to red -Style red = Style.EMPTY - .withColor(0xFF0000); - -// Copies empty style and makes it bold -Style bold = Style.EMPTY - .withBold(true); - -// Copies empty style and makes it both underlined and strikethrough -Style doubleLines = Style.EMPTY - .withUnderlined(true) - .withStrikethrough(true); - -// Sets style of the text to be blue and italic -text.setStyle(blueItalic); - -// Overwrites blue and italic style to be red, bold, underlined, and strikethrough -text.withStyle(red).withStyle(bold).withStyle(doubleLines); -``` -This creates a red, bold text with two lines: -![red_hello] - -Chaining Components -------------------- - -`MutableComponent#append` can chain multiple components together. Chained components can be retrieved with `MutableComponent#getSiblings`. - -`Component` stores its siblings like a tree and is traversed in preorder; the parent style is merged with those of its siblings. -![tree] - -The code below will create a component with the same structure in the above example: -```java -// Create text only components -MutableComponent first = Component.literal("first "); -MutableComponent second = Component.literal("second "); -MutableComponent third = Component.literal("third "); -MutableComponent fourth = Component.literal("fourth "); -MutableComponent fifth = Component.literal("fifth "); -MutableComponent sixth = Component.literal("sixth "); -MutableComponent seventh = Component.literal("seventh "); - -// Create components with style -MutableComponent red = Component.litearl("red ").withStyle(Style.EMPTY.withColor(0xFF0000)); -MutableComponent blue = Component.literal("blue ").withStyle(Style.EMPTY.withColor(0x0000FF)); -MutableComponent bold = Component.literal("bold ").withStyle(Style.EMPTY.withBold(true)); - -// Structure created components in the same way as the image -red.append(first).append(blue).append(seventh); -blue.append(second).append(third).append(bold); -bold.append(fourth).append(fifth).append(sixth); -``` -![style_annotated] - -Text Formatting ---------------- - -Text formatting is the process of inserting data as text into predefined larger text. It can be used for displaying coordinates, showing unit measurements, etc. **Format specifiers** are used for indicating where a text can be inserted. - -`TranslatableContents` allows two types of format specifiers: `%s` and `%n$s`. The component uses the second parameter onwards, denoted as `args` , for holding what object to insert in place of a format specifier. - -`%s` is replaced with elements of `args` in order they appear, i.e., the first `%s` is replaced with the first element of `args`, and so on. -`%n$s` is positional specifier; each positional specifier can denote which element in `args` will replace the specifier via the number `n`. -* Formatting `x:%s y:%s z:%s` with `[1, 2, 3]` as `args` results in `x:1 y:2 z:3` -* Formatting `Time: %1$s ms` with `17` as `args` results in `Time: 17 ms` -* Formatting `Player name: %2$s, HP: %1$s` with `[10.2, Dev]` as `args` results in `Player name: Dev, HP: 10.2` - -Any `Component` element within `args` will be transformed into a formatted text string. - -[internationalization]: ../concepts/internationalization.md -[selectors]: https://minecraft.wiki/w/Target_selectors -[red_hello]: /img/component_red_hello.png -[style_annotated]: /img/component_style_annotated.png -[formatting]: #text-formatting -[tree]: /img/component_graph.png -[keymapping]: ./keymappings.md diff --git a/docs/misc/keymappings.md b/docs/misc/keymappings.md index f4fb2b8f..ecbf2152 100644 --- a/docs/misc/keymappings.md +++ b/docs/misc/keymappings.md @@ -152,6 +152,6 @@ If you do not own the screen which you are trying to check a **mouse** for, you [modbus]: ../concepts/events.md#event-buses [controls]: https://minecraft.wiki/w/Options#Controls -[tk]: ../concepts/internationalization.md#translatablecontents +[tk]: ../resources/client/i18n.md#components [keyinput]: https://www.glfw.org/docs/3.3/input_guide.html#input_key [forgebus]: ../concepts/events.md#registering-an-event-handler diff --git a/docs/resources/client/_category_.json b/docs/resources/client/_category_.json new file mode 100644 index 00000000..f785d798 --- /dev/null +++ b/docs/resources/client/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Client" +} \ No newline at end of file diff --git a/docs/resources/client/i18n.md b/docs/resources/client/i18n.md new file mode 100644 index 00000000..88481f2f --- /dev/null +++ b/docs/resources/client/i18n.md @@ -0,0 +1,181 @@ +# I18n and L10n + +I18n (short for internationalization) is the way of designing a program to work with multiple languages. L10n (short for localization) is the process of translating text into the user's language. Minecraft implements these using `Component`s. + +## `Component`s + +A `Component` is a piece of text with metadata, with the metadata including things such as text formatting. It can be created in one of the following ways (all of the following are static methods in the `Component` interface): + +| Method | Description | +|----------------|-------------------------------------------------------------------------------------------------------| +| `empty` | Creates an empty component. | +| `literal` | Creates a component with the given text and directly displays that text without translating. | +| `nullToEmpty` | Creates an empty component when given null, and a literal component otherwise. | +| `translatable` | Creates a translatable component. The given string is then resolved as a translation key (see below). | +| `keybind` | Creates a component containing the (translated) display name of the given keybind. | +| `nbt` | Creates a component representing the [NBT][nbt] at the given path. | +| `score` | Creates a component containing a scoreboard objective value. | +| `selector` | Creates a component containing a list of entity names for a given [entity selector][selector]. | + +`Component.translatable()` additionally has a vararg parameter that accepts string interpolation elements that work exactly like the ones in Java's `String#format`. + +Every `Component` can be resolved using `#getString()`. Resolving is generally lazy, meaning that the server can specify a `Component`, send it to the clients, and the clients will each resolve the `Component`s on their own (where different languages may result in different text). Many places in Minecraft will also directly accept `Component`s and take care of resolving for you. + +:::caution +Never let a server translate a `Component`. Always send `Component`s to the client and resolve them there. +::: + +### Text Formatting + +`Component`s can be formatted using `Style`s. `Style`s are immutable, creating a new `Style` object when modified, and thus allowing them to be created once and then be reused as needed. + +`Style.EMPTY` can generally be used as a base to work off. Multiple `Style`s can be merged with `Style#applyTo(Style other)`, where the given `Style` will set its configured values on the `Style` it is called on. `Style`s can then be applied to components like so: + +```java +Component text = Component.literal("Hello World!"); + +// Create a new style. +Style blue = Style.EMPTY.withColor(0x0000FF); +// Styles use a builder-like pattern. +Style blueItalic = Style.EMPTY.withColor(0x0000FF).withItalic(true); +// Besides italic, we can also make styles bold, underlined, strikethrough, or obfuscated. +Style bold = Style.EMPTY.withBold(true); +Style underlined = Style.EMPTY.withUnderlined(true); +Style strikethrough = Style.EMPTY.withStrikethrough(true); +Style obfuscated = Style.EMPTY.withObfuscated(true); +// Let's merge some styles together! +Style merged = blueItalic.applyTo(bold).applyTo(strikethrough); + +// Set a style on a component. +text.setStyle(merged); +// Merge a new style into it. +text.withStyle(Style.EMPTY.withColor(0xFF0000)); +``` + +Another, more elaborate option of formatting is to use click and hover events: + +```java +// We have a total of 6 options for a click event, and a total of 3 options for a hover event. +ClickEvent clickEvent; +HoverEvent hoverEvent; + +// Opens the given URL in your default browser when clicked. +clickEvent = new ClickEvent(ClickEvent.Action.OPEN_URL, "http://example.com/"); +// Opens the given file when clicked. For security reasons, this cannot be sent from a server. +clickEvent = new ClickEvent(ClickEvent.Action.OPEN_FILE, "C:/example.txt"); +// Runs the given command when clicked. +clickEvent = new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/gamemode creative"); +// Suggests the given command in the chat when clicked. +clickEvent = new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/gamemode creative"); +// Changes a book page when clicked. Irrelevant outside of a book screen context. +clickEvent = new ClickEvent(ClickEvent.Action.CHANGE_PAGE, "1"); +// Copies the given text to the clipboard. +clickEvent = new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "Hello World!"); + +// Shows the given component when hovered. May be formatted as well. +// Keep in mind that click or hover events won't work in a hover tooltip for obvious reasons. +hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("Hello World!")); +// Shows a complete tooltip of the given item stack when hovered. +// See the possible constructors of ItemStackInfo. +hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_ITEM, new HoverEvent.ItemStackInfo(...)); +// Shows a complete tooltip of the given entity when hovered. +// See the possible constructors of EntityTooltipInfo. +hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_ENTITY, new HoverEvent.EntityTooltipInfo(...)); + +// Apply the events to a style. +Style clickable = Style.EMPTY.withClickEvent(clickEvent); +Style hoverable = Style.EMPTY.withHoverEvent(hoverEvent); +``` + +## Language Files + +Language files are JSON files that contain mappings from translation keys (see below) to actual names. They are located at `assets//lang/language_name.json`. For example, US English translations for a mod with id `examplemod` would be located at `assets/examplemod/lang/en_us.json`. A full list of languages supported by Minecraft can be found [here][mcwikilang]. + +A language file generally looks like this: + +```json +{ + "translation.key.1": "Translation 1", + "translation.key.2": "Translation 2" +} +``` + +### Translation Keys + +Translation keys are the keys used in translations. In many cases, they follow the format `registry.modid.name`. For example, a mod with the id `examplemod` that provides a block named `example_block` will probably want to provide translations for the key `block.examplemod.example_block`. However, you can use basically any string as a translation key. + +If a translation key does not have an associated translation in the given language, the game will fall back to US English (`en_us`), if that is not already the selected language. If US English does not have a translation either, the translation will fail silently, and the raw translation key will be displayed instead. + +Some places in Minecraft offer you helper methods to get a translation keys. For example, both blocks and items provide `#getDescriptionId` methods. These can not only be queried, but also overridden if needed. A common use case are items that have different names depending on their [NBT][nbt] value. These will usually override the variant of `#getDescriptionId` that has an [`ItemStack`][itemstack] parameter, and return different values based on the stack's NBT. Another common use case are `BlockItem`s, which override the method to use the associated block's translation key instead. + +:::tip +The only purpose of translation keys is for localization. Do not use them for game logic, that's what [registry names][regname] are for. +::: + +### Translating Mod Metadata + +Starting with NeoForge 20.4.179, translation files can override certain parts of [mod info][modstoml] using the following keys (where `modid` is to be replaced with the actual mod id): + +| | Translation Key | Overriding | +|--------------|----------------------------------------|------------------------------------------------------------------------------| +| Display Name | `fml.menu.mods.info.displayname.modid` | A field named `displayName` may be placed in the `[[mods]]` section instead. | +| Description | `fml.menu.mods.info.description.modid` | A field named `description` may be placed in the `[[mods]]` section instead. | + +### Data Generation + +Language files can be [datagenned][datagen]. To do so, extend the `LanguageProvider` class and add your translations in the `addTranslations()` method: + +```java +public class MyLanguageProvider extends LanguageProvider { + public MyLanguageProvider(PackOutput output) { + super( + // Provided by the GatherDataEvent. + output, + // Your mod id. + "examplemod", + // The locale to use. You may use multiple language providers for different locales. + "en_us" + ); + } + + @Override + protected void addTranslations() { + // Adds a translation with the given key and the given value. + add("translation.key.1", "Translation 1"); + + // Helpers are available for various common object types. Every helper has two variants: an add() variant + // for the object itself, and an addTypeHere() variant that accepts a supplier for the object. + // The different names for the supplier variants are required due to generic type erasure. + // All following examples assume the existence of the values as suppliers of the needed type. + + // Adds a block translation. + add(MyBlocks.EXAMPLE_BLOCK.get(), "Example Block"); + addBlock(MyBlocks.EXAMPLE_BLOCK, "Example Block"); + // Adds an item translation. + add(MyItems.EXAMPLE_ITEM.get(), "Example Item"); + addItem(MyItems.EXAMPLE_ITEM, "Example Item"); + // Adds an item stack translation. This is mainly for items that have NBT-specific names. + add(MyItems.EXAMPLE_ITEM_STACK.get(), "Example Item"); + addItemStack(MyItems.EXAMPLE_ITEM_STACK, "Example Item"); + // Adds an entity type translation. + add(MyEntityTypes.EXAMPLE_ENTITY_TYPE.get(), "Example Entity"); + addEntityType(MyEntityTypes.EXAMPLE_ENTITY_TYPE, "Example Entity"); + // Adds an enchantment translation. + add(MyEnchantments.EXAMPLE_ENCHANTMENT.get(), "Example Enchantment"); + addEnchantment(MyEnchantments.EXAMPLE_ENCHANTMENT, "Example Enchantment"); + // Adds a mob effect translation. + add(MyMobEffects.EXAMPLE_MOB_EFFECT.get(), "Example Effect"); + addEffect(MyMobEffects.EXAMPLE_MOB_EFFECT, "Example Effect"); + } +} +``` + +Then, register the provider like any other provider in the `GatherDataEvent`. + +[datagen]: ../index.md#data-generation +[itemstack]: ../../items/index.md#itemstacks +[mcwikilang]: https://minecraft.wiki/w/Language +[modstoml]: ../../gettingstarted/modfiles.md#modstoml +[nbt]: ../../datastorage/nbt.md +[regname]: ../../concepts/registries.md +[selector]: https://minecraft.wiki/w/Target_selectors diff --git a/docs/resources/client/index.md b/docs/resources/client/index.md deleted file mode 100644 index c81a5637..00000000 --- a/docs/resources/client/index.md +++ /dev/null @@ -1,15 +0,0 @@ -Resource Packs -============== - -[Resource Packs][respack] allow for the customization of client resources through the `assets` directory. This includes textures, models, sounds, localizations, and others. Your mod (as well as Forge itself) can also have resource packs. Any user can therefore modify all the textures, models, and other assets defined within this directory. - -### Creating a Resource Pack -Resource Packs are stored within your project's resources. The `assets` directory contains the contents of the pack, while the pack itself is defined by the `pack.mcmeta` alongside the `assets` folder. -Your mod can have multiple asset domains, since you can add or modify already existing resource packs, like vanilla's, Forge's, or another mod's. -You can then follow the steps found [at the Minecraft Wiki][createrespack] to create any resource pack. - -Additional reading: [Resource Locations][resourcelocation] - -[respack]: https://minecraft.wiki/w/Resource_Pack -[createrespack]: https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack -[resourcelocation]: ../../misc/resourcelocation.md diff --git a/docs/resources/index.md b/docs/resources/index.md index 149268a6..c92b28f6 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -1,6 +1,6 @@ # Resources -Resources are external files that are used by the game, but are not code. The most prominent kinds of resources are textures, however, many other types of resources exist in the Minecraft ecosystem. +Resources are external files that are used by the game, but are not code. The most prominent kinds of resources are textures, however, many other types of resources exist in the Minecraft ecosystem. Of course, all these resources require a consumer on the code side, so the consuming systems are grouped in this section as well. Minecraft generally has two kinds of resources: client-side resources, known as assets, and server-side resources, known as data. Assets are mostly display-only information, for example textures, display models, translations, or sounds, while data includes various things that affect gameplay, such as loot tables, recipes, or worldgen information. They are loaded from resource packs and data packs, respectively. NeoForge generates a built-in resource and data pack for every mod. From bc2c4f5901a52e856aeb4f799ee33b591f175597 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Sun, 17 Mar 2024 13:42:31 +0100 Subject: [PATCH 26/41] rewrite the models article and merge model extension articles into it, add new texture article --- docs/datagen/client/modelproviders.md | 4 +- docs/rendering/modelextensions/facedata.md | 117 -------- docs/rendering/modelextensions/rendertypes.md | 81 ----- docs/rendering/modelextensions/transforms.md | 76 ----- docs/resources/client/models/index.md | 281 ++++++++++++++++-- .../resources/client/models/itemproperties.md | 65 ---- docs/resources/client/models/tinting.md | 32 -- docs/resources/client/textures.md | 44 +++ docs/resources/index.md | 38 ++- 9 files changed, 333 insertions(+), 405 deletions(-) delete mode 100644 docs/rendering/modelextensions/facedata.md delete mode 100644 docs/rendering/modelextensions/rendertypes.md delete mode 100644 docs/rendering/modelextensions/transforms.md delete mode 100644 docs/resources/client/models/itemproperties.md delete mode 100644 docs/resources/client/models/tinting.md create mode 100644 docs/resources/client/textures.md diff --git a/docs/datagen/client/modelproviders.md b/docs/datagen/client/modelproviders.md index 5075164c..5eef5bf6 100644 --- a/docs/datagen/client/modelproviders.md +++ b/docs/datagen/client/modelproviders.md @@ -416,8 +416,8 @@ public CompletableFuture run(CachedOutput cache) { [datagen]: ../index.md#data-providers [efh]: ../index.md#existing-files [loader]: #custom-model-loader-builders -[color]: ../../resources/client/models/tinting.md#blockcoloritemcolor -[overrides]: ../../resources/client/models/itemproperties.md +[color]: ../../resources/client/models/index.md#tinting +[overrides]: ../../resources/client/models/index.md#overrides [blockstateprovider]: #block-state-provider [blockstate]: https://minecraft.wiki/w/Tutorials/Models#Block_states [blockmodels]: #blockmodelprovider diff --git a/docs/rendering/modelextensions/facedata.md b/docs/rendering/modelextensions/facedata.md deleted file mode 100644 index c7d01f00..00000000 --- a/docs/rendering/modelextensions/facedata.md +++ /dev/null @@ -1,117 +0,0 @@ -Face Data -========= - -In a vanilla "elements" model, additional data about an element's faces can be specified at either the element level or the face level. Faces which do not specify their own face data will fall back to the element's face data or a default if no face data is specified at the element level. - -To use this extension for a generated item model, the model must be loaded through the `forge:item_layers` model loader due to the vanilla item model generator not being extended to read this additional data. - -All values of the face data are optional. - -Elements Model --------------- - -In vanilla "elements" models, the face data applies to the face it is specified in or all faces of the element it is specified in which don't have their own face data. - -:::note -If `forge_data` is specified on a face, it will not inherit any parameters from the element-level `forge_data` declaration. -::: - -The additional data can be specified in the two ways shown in this example: -```js -{ - "elements": [ - { - "forge_data": { - "color": "0xFFFF0000", - "block_light": 15, - "sky_light": 15, - "ambient_occlusion": false - }, - "faces": { - "north": { - "forge_data": { - "color": "0xFFFF0000", - "block_light": 15, - "sky_light": 15, - "ambient_occlusion": false - }, - // ... - }, - // ... - }, - // ... - } - ] -} -``` - -Generated Item Model --------------------- - -In item models generated using the `forge:item_layers` loader, face data is specified for each texture layer and applies to all of the geometry (front/back facing quads and edge quads). - -The `forge_data` field must be located at the top level of the model JSON, with each key-value pair associating a face data object to a layer index. - -In the following example, layer 1 will be tinted red and glow at full brightness: -```js -{ - "textures": { - "layer0": "minecraft:item/stick", - "layer1": "minecraft:item/glowstone_dust" - }, - "forge_data": { - "1": { - "color": "0xFFFF0000", - "block_light": 15, - "sky_light": 15, - "ambient_occlusion": false - } - } -} -``` - -Parameters ----------- - -### Color - -Specifying a color value with the `color` entry will apply that color as a tint to the quads. Defaults to `0xFFFFFFFF` (white, fully opaque). The color must be in the `ARGB` format packed into a 32-bit integer and can be specified as either a hexadecimal string (`"0xAARRGGBB"`) or as a decimal integer literal (JSON does not support hexadecimal integer literals). - -:::caution -The four color components are multiplied with the texture's pixels. Omitting the alpha component is equivalent to making it 0, which will make the geometry fully transparent. -::: - -This can be used as a replacement for tinting with [`BlockColor` and `ItemColor`][tinting] if the color values are constant. - -### Block and Sky Light - -Specifying a block and/or sky light value with the `block_light` and `sky_light` entry respectively will override the respective light value of the quads. Both values default to 0. The values must be in the range 0-15 (inclusive) and are treated as a minimum value for the respective light type when the face is rendered, meaning that a higher in-world value of the respective light type will override the specified value. - -The specified light values are purely client-side and affect neither the server's light level nor the brightness of surrounding blocks. - -### Ambient Occlusion - -Specifying the `ambient_occlusion` flag will configure [AO] for the quads. Defaults to `true`. The behaviour of this flag is equivalent to the top-level `ambientocclusion` flag of the vanilla format. - -![Ambient occlusion in action][ao_img] -*Ambient occlusion enabled on the left and disabled on the right, demonstrated with the Smooth Lighting graphics setting* - -:::note -If the top-level AO flag is set to false, specifying this flag as true on an element or face won't be able to override the top-level flag. -```js -{ - "ambientocclusion": false, - "elements": [ - { - "forge_data": { - "ambient_occlusion": true // Has no effect - } - } - ] -} -``` -::: - -[tinting]: ../../resources/client/models/tinting.md -[AO]: https://en.wikipedia.org/wiki/Ambient_occlusion -[ao_img]: /img/ambientocclusion_annotated.png \ No newline at end of file diff --git a/docs/rendering/modelextensions/rendertypes.md b/docs/rendering/modelextensions/rendertypes.md deleted file mode 100644 index ad6af417..00000000 --- a/docs/rendering/modelextensions/rendertypes.md +++ /dev/null @@ -1,81 +0,0 @@ -Render Types -============ - -Adding the `render_type` entry at the top level of the JSON suggests to the loader what render type the model should use. If not specified, the loader gets to pick the render type(s) used, often falling back to the render types returned by `ItemBlockRenderTypes#getRenderLayers()`. - -Custom model loaders may ignore this field entirely. - -:::note -Since 1.19 this is preferred over the deprecated method of setting the applicable render type(s) via `ItemBlockRenderTypes#setRenderLayer()` for blocks. -::: - -Example of a model for a cutout block with the glass texture - -```js -{ - "render_type": "minecraft:cutout", - "parent": "block/cube_all", - "textures": { - "all": "block/glass" - } -} -``` - -Vanilla Values --------------- - -The following options with the respective chunk and entity render type are supplied by Forge (`NamedRenderTypeManager#preRegisterVanillaRenderTypes()`): - -* `minecraft:solid` - * Chunk render type: `RenderType#solid()` - * Entity render type: `ForgeRenderTypes#ITEM_LAYERED_SOLID` - * Used for fully solid blocks (i.e. Stone) -* `minecraft:cutout` - * Chunk render type: `RenderType#cutout()` - * Entity render type: `ForgeRenderTypes#ITEM_LAYERED_CUTOUT` - * Used for blocks where any given pixel is either fully transparent or fully opaque (i.e. Glass Block) -* `minecraft:cutout_mipped` - * Chunk render type: `RenderType#cutoutMipped()` - * Entity render type: `ForgeRenderTypes#ITEM_LAYERED_CUTOUT` - * Chunk and entity render type differ due to mipmapping on the entity render type making items look weird - * Used for blocks where any given pixel is either fully transparent or fully opaque and the texture should be scaled down at larger distances ([mipmapping]) to avoid visual artifacts (i.e. Leaves) -* `minecraft:cutout_mipped_all` - * Chunk render type: `RenderType#cutoutMipped()` - * Entity render type: `ForgeRenderTypes#ITEM_LAYERED_CUTOUT_MIPPED` - * Used in similar cases as `minecraft:cutout_mipped` when the item representation should also have mipmapping applied -* `minecraft:translucent` - * Chunk render type: `RenderType#translucent()` - * Entity render type: `ForgeRenderTypes#ITEM_LAYERED_TRANSLUCENT` - * Used for blocks where any given pixel may be partially transparent (i.e. Stained Glass) -* `minecraft:tripwire` - * Chunk render type: `RenderType#tripwire()` - * Entity render type: `ForgeRenderTypes#ITEM_LAYERED_TRANSLUCENT` - * Chunk and entity render type differ due to the tripwire render type not being feasible as an entity render type - * Used for blocks with the special requirement of being rendered to the weather render target (i.e. Tripwire) - -Custom Values -------------- - -Custom named render types to be specified in a model can be registered in the `RegisterNamedRenderTypesEvent`. This event is fired on the mod event bus. - -A custom named render type consists of two or three components: - -* A chunk render type - any of the types in the list returned by `RenderType.chunkBufferLayers()` can be used -* A render type with the `DefaultVertexFormat.NEW_ENTITY` vertex format ("entity render type") -* A render type with the `DefaultVertexFormat.NEW_ENTITY` vertex format for use when the *Fabulous!* graphics mode is selected (optional) - -The chunk render type is used when a block using this named render type is rendered as part of the chunk geometry. -The required entity render type is used when an item using this named render type is rendered in the Fast and Fancy graphics modes (inventory, ground, item frame, etc.). -The optional entity render type is used the same way as the required entity render type when the *Fabulous!* graphics mode is selected. This render type is needed in cases where the required entity render type does not work in the *Fabulous!* graphics mode (typically only applies to translucent render types). - -```java -public static void onRegisterNamedRenderTypes(RegisterNamedRenderTypesEvent event) -{ - event.register("special_cutout", RenderType.cutout(), Sheets.cutoutBlockSheet()); - event.register("special_translucent", RenderType.translucent(), Sheets.translucentCullBlockSheet(), Sheets.translucentItemSheet()); -} -``` - -These can then be addressed in JSON as `:special_cutout` and `:special_translucent`. - -[mipmapping]: https://en.wikipedia.org/wiki/Mipmap \ No newline at end of file diff --git a/docs/rendering/modelextensions/transforms.md b/docs/rendering/modelextensions/transforms.md deleted file mode 100644 index 100ee467..00000000 --- a/docs/rendering/modelextensions/transforms.md +++ /dev/null @@ -1,76 +0,0 @@ -Root Transforms -=============== - -Adding the `transform` entry at the top level of a model JSON suggests to the loader that a transformation should be applied to all geometry right before the rotations in the [blockstate] file in the case of a block model, and before the [display transforms][displaytransform] in the case of an item model. The transformation is available through `IGeometryBakingContext#getRootTransform()` in `IUnbakedGeometry#bake()`. - -Custom model loaders may ignore this field entirely. - -The root transforms can be specified in two formats: - -1. A JSON object containing a singular `matrix` entry containing a raw transformation matrix in the form of a nested JSON array with the last row omitted (3*4 matrix, row major order). The matrix is the composition of the translation, left rotation, scale, right rotation and the transformation origin in that order. Example demonstrating the structure: - ```js - "transform": { - "matrix": [ - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ] - ] - } - ``` -2. A JSON object containing any combination of the following optional entries: - * `origin`: origin point used for the rotations and scaling - * `translation`: relative translation - * `rotation` or `left_rotation`: rotation around the translated origin to be applied before scaling - * `scale`: scale relative to the translated origin - * `right_rotation` or `post_rotation`: rotation around the translated origin to be applied after scaling - -Element-wise specification -------------------------- - -If the transformation is specified as a combination of the entries mentioned in option 4, these entries will be applied in the order of `translation`, `left_rotation`, `scale`, `right_rotation`. -The transformation is moved to the specified origin as a last step. - -```js -{ - "transform": { - "origin": "center", - "translation": [ 0, 0.5, 0 ], - "rotation": { "y": 45 } - }, - // ... -} -``` - -The elements are expected to be defined as follows: - -### Origin - -The origin can be specified either as an array of 3 floating point values representing a three-dimensional vector: `[ x, y, z ]` or as one of the three default values: - -* `"corner"` (0, 0, 0) -* `"center"` (.5, .5, .5) -* `"opposing-corner"` (1, 1, 1) - -If the origin is not specified, it defaults to `"opposing-corner"`. - -### Translation - -The translation must be specified as an array of 3 floating point values representing a three-dimensional vector: `[ x, y, z ]` and defaults to (0, 0, 0) if not present. - -### Left and Right Rotation - -The rotations can be specified in any one of the following four ways: - -* Single JSON object with a single axis => rotation degree mapping: `{ "x": 90 }` -* Array of an arbitrary amount of JSON objects with the above format (applied in the order they are specified in): `[ { "x": 90 }, { "y": 45 }, { "x": -22.5 } ]` -* Array of 3 floating point values specifying the rotation in degrees around each axis: `[ 90, 180, 45 ]` -* Array of 4 floating point values specifying a quaternion directly: `[ 0.38268346, 0, 0, 0.9238795 ]` (example equals 45 degrees around the X axis) - -If the respective rotation is not specified, it will default to no rotation. - -### Scale - -The scale must be specified as an array of 3 floating point values representing a three-dimensional vector: `[ x, y, z ]` and defaults to (1, 1, 1) if not present. - -[blockstate]: https://minecraft.wiki/w/Tutorials/Models#Block_states -[displaytransform]: ../modelloaders/transform.md \ No newline at end of file diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index d9f2df9c..3804c7c5 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -1,25 +1,272 @@ -Models -====== +# Models -The [model system][models] is Minecraft's way of giving blocks and items their shapes. Through the model system, blocks and items are mapped to their models, which define how they look. One of the main goals of the model system is to allow not only textures but the entire shape of a block/item to be changed by resource packs. Indeed, any mod that adds items or blocks also contains a mini-resource pack for their blocks and items. +Models are JSON files that determine the visual shape and texture(s) of a block or item. A model contains of cuboid elements, each with their own size, that then get assigned a texture to each face. -Model Files ------------ +Each item gets an item model assigned to it by its registry name. For example, an item with the registry name `examplemod:example_item` would get the model at `assets/examplemod/models/item/example_item.json` assigned to it. For blocks, this is a bit more complicated, as they get assigned a blockstate file first. See [below][bsfile] for more information. -Models and textures are linked through [`ResourceLocation`][resloc]s but are stored in the `ModelManager` using `ModelResourceLocation`s. Models are referenced in different locations through the block or item's registry name depending on whether they are referencing [block states][statemodel] or [item models][itemmodels]. Blocks will have their `ModelResourceLocation` represent their registry name along with a stringified version of its current [`BlockState`][state] while items will use their registry name followed by `inventory`. +## Specification -:::note -JSON models only support cuboid elements; there is no way to express a triangular wedge or anything like it. To have more complicated models, another format must be used. +_See also: [Model][mcwikimodel] on the [Minecraft Wiki][mcwiki]_ + +A model is a JSON file with the following optional properties in the root tag: + +- `loader`: NeoForge-added. Sets a custom model loader. See [Custom Model Loaders][custommodelloader] for more information. +- `parent`: Sets a parent model, in the form of a [resource location][rl] relative to the `models` folder. All parent properties will be applied and then overridden by the properties set in the declaring model. Common parents include: + - `minecraft:block/block`: The common parent of all block models. + - `minecraft:block/cube`: Parent of all models that use a 1x1x1 cube model. + - `minecraft:block/cube_all`: Variant of the cube model that uses the same texture on all six sides, for example cobblestone or planks. + - `minecraft:block/cube_bottom_top`: Variant of the cube model that uses the same texture on all four horizontal sides, and separate textures on the top and the bottom. Common examples include sandstone or chiseled quartz. + - `minecraft:block/cube_column`: Variant of the cube model that has a side texture and a bottom and top texture. Examples include wooden logs, as well as quartz and purpur pillars. + - `minecraft:block/cross`: Model that uses two 90° rotated planes with the same texture, forming an X when viewed from above (hence the name). Examples include most plants, e.g. grass, saplings and flowers. + - `minecraft:item/generated`: Parent for classic 2D flat item models. Used by most items in the game. + - `minecraft:item/handheld`: Parent for 2D flat item models that appear to be actually held by the player. Used predominantly by tools. + - Block items commonly (but not always) use their corresponding block models as parent. For example, the cobblestone item model uses the parent `minecraft:block/cobblestone`. +- `ambientocclusion`: Whether to enable [ambient occlusion][ao] or not. Only effective on block models. Defaults to `true`. If your custom block model has weird shading, try setting this to `false`. +- `render_type`: See [Render Types][rendertype]. +- `gui_light`: Can be `"front"` or `"side"`. If `"front"`, light will come from the front, useful for flat 2D models. If `"side"`, light will come from the side, useful for 3D models (especially block models). Defaults to `"side"`. Only effective on item models. +- `textures`: A sub-object that maps names (known as texture variables) to [texture locations][textures]. Texture variables can then be used in [elements]. They can also be specified in elements, but left unspecified in order for child models to specify them. + - Block models should additionally specify a `particle` texture. This texture is used when falling on, running across, or breaking the block. + - Item models can also use layer textures, named `layer0`, `layer1`, etc., where layers with a higher index are rendered above those with a lower index (e.g. `layer1` would be rendered above `layer0`). Only works if the parent is `item/generated`. +- `elements`: A list of cuboid [elements]. +- `overrides`: A list of [override models][overrides]. Only effective on item models. +- `display`: A sub-object that holds the different display options for different perspectives. Only effective on item models, but often specified in block models so that item models can inherit the display options. Possible perspectives include `thirdperson_righthand`, `thirdperson_lefthand`, `firstperson_righthand`, `firstperson_lefthand`, `gui`, `head`, `ground`, and `fixed` (for item frames and similar modded display blocks). Every perspective is an optional sub-object that may contain the following options: + - `rotation`: The rotation of the model, specified as `[x, y, z]`. Rotations are handled after translations and before scaling. + - `translation`: The translation of the model, specified as `[x, y, z]`. Translations are handled before rotations and scaling. + - `scale`: The scale of the model, specified as `[x, y, z]`. Scaling is handled after translations and rotations. +- `transform`: See [Root Transforms][roottransforms]. + +:::tip +If you're having trouble finding out how exactly to specify something, have a look at a vanilla model that does something similar. ::: -### Textures +### Render Types + +Using the optional NeoForge-added `render_type` field, you can set a render type for your model. If this is not set (as is the case in all vanilla models), the game will fall back to a list of hardcoded render types. If that list doesn't contain the render type for that block either, it will fall back to `minecraft:solid`. Vanilla provides the following default render types: + +- `minecraft:solid`: Used for fully solid blocks, such as stone. +- `minecraft:cutout`: Used for blocks where all pixels are either fully solid or fully transparent, i.e. with either full or no transparency, for example glass. +- `minecraft:cutout_mipped`: Variant of `minecraft:cutout` that will scale down textures at large distances to avoid visual artifacts ([mipmapping]). Used for example by leaves. +- `minecraft:cutout_mipped_all`: Variant of `minecraft:cutout_mipped` where a corresponding item should be mipmapped as well. +- `minecraft:translucent`: Used for blocks where any pixel may be partially transparent, for example stained glass. +- `minecraft:tripwire`: Used by blocks with the special requirement of being rendered to the weather target, i.e. tripwire. + +Selecting the correct render type is a question of performance to some degree. Solid rendering is faster than cutout rendering, and cutout rendering is faster than translucent rendering. Because of this, you should specify the "strictest" render type applicable for your use case, as it will also be the fastest. + +If you want, you can also add your own render types. To do so, subscribe to the [mod bus][modbus] [event] `RegisterNamedRenderTypesEvent` and `#register` your render types. `#register` has three or four parameters: + +- The name of the render type. Will be prefixed with your mod id. For example, using `"my_cutout"` here will provide `examplemod:my_cutout` as a new render type for you to use (provided that your mod id is `examplemod`, of course). +- The chunk render type. Any of the types in the list returned by `RenderType.chunkBufferLayers()` can be used. +- The entity render type. Must be a render type with the `DefaultVertexFormat.NEW_ENTITY` vertex format. +- Optional: The fabulous render type. Must be a render type with the `DefaultVertexFormat.NEW_ENTITY` vertex format. Will be used instead of the regular entity render type if the graphics mode is set to _Fabulous!_. If omitted, falls back to the regular render type. Generally recommended to set if the render type uses transparency in some way. + +### Elements + +An element is a JSON representation of a cuboid object. It has the following properties: + +- `from`: The coordinate of the start corner of the cuboid, specified as `[x, y, z]`. Specified in 1/16 block units. For example, `[0, 0, 0]` would be the "bottom left" corner, `[8, 8, 8]` would be the center, and `[16, 16, 16]` would be the "top right" corner of the block. +- `to`: The coordinate of the start corner of the cuboid, specified as `[x, y, z]`. Like `from`, this is specified in 1/16 block units. +- `neoforge_data`: See [Extra Face Data][extrafacedata]. +- `faces`: An object containing data for of up to 6 faces, named `north`, `south`, `east`, `west`, `up` and `down`, respectively. Every face has the following data: + - `uv`: The uv of the face, specified as `[u1, v1, u2, v2]`, where `u1, v1` is the top left uv coordinates and `u2, v2` is the bottom right uv coordinates. + - `texture`: The texture to use for the face. Must be a texture variable prefixed with a `#`. For example, if your model had a texture named `wood`, you would use `#wood` to reference that texture. Technically optional, will use the missing texture if absent. + - `rotation`: Optional. Rotates the texture clockwise by 90, 180 or 270 degrees. + - `cullface`: Optional. Tells the render engine to skip rendering the face when there is a full block touching it in the specified direction. The direction can be `north`, `south`, `east`, `west`, `up` or `down`. + - `tintindex`: Optional. Specifies a tint index that may be used by a color handler, see [Tinting][tinting] for more information. Defaults to -1, which means no tinting. + - `neoforge_data`: See [Extra Face Data][extrafacedata]. + +Additionally, it can specify the following optional properties: + +- `shade`: Only for block models. Optional. Whether shadows should be rendered on this face or not. Defaults to true. +- `rotation`: A rotation of the object, specified as a sub object containing the following data: + - `angle`: The rotation angle, in degrees. Can be -45 through 45 in steps of 22.5 degrees. + - `axis`: The axis to rotate around. It is currently not possible to rotate an object around more than one axis. + - `origin`: Optional. The origin point to rotate around, specified as `[x, y, z]`. Note that these are absolute values, i.e. they are not relative to the cube's position. If unspecified, will use `[0, 0, 0]`. + +#### Extra Face Data + +Extra face data (`neoforge_data`) can be applied to both an element and a single face of an element. It is optional in all contexts where it is available. If both element-level and face-level extra face data is specified, the face-level data will override the element-level data. Extra data can specify the following data: + +- `color`: Tints the face with the given color. Must be an ARGB value. Can be specified as a string or as a decimal integer (JSON does not support hex literals). Defaults to `0xFFFFFFFF`. This can be used as a replacement for tinting if the color values are constant. +- `block_light`: Overrides the block light value used for this face. Defaults to 0. +- `sky_light`: Overrides the sky light value used for this face. Defaults to 0. +- `ambient_occlusion`: Disables or enables ambient occlusion for this face. Defaults to the value set in the model. + +Using the custom `neoforge:item_layers` loader, you can also specify extra face data to apply to all the geometry in an `item/generated` model. In the following example, layer 1 will be tinted red and glow at full brightness: + +```json5 +{ + "loader": "neoforge:item_layers", + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/stick", + "layer1": "minecraft:item/glowstone_dust" + }, + "forge_data": { + "1": { + "color": "0xFFFF0000", + "block_light": 15, + "sky_light": 15, + "ambient_occlusion": false + } + } +} +``` + +### Overrides + +Item overrides can assign a different model to an item based on a float value, called the override value. For example, bows and crossbows use this to change the texture depending on how long they have been drawn. Overrides have both a model and a code side to them. + +The model can specify one or multiple override models that should be used when the override value is equal to or greater than the given threshold value. For example, the bow uses two different properties `pulling` and `pull`. `pulling` is treated as a boolean value, with 1 being interpreted as pulling and 0 as not pulling, while `pull` represents how much the bow is currently pulled. It then uses these properties to specify usage of three alternative models when charged to below 65% (`pulling` 1, no `pull` value), 65% (`pulling` 1, `pull` 0.65) and 90% (`pulling` 1, `pull` 0.9). If multiple models apply (because the value keeps on becoming bigger), the one closest to the actual value is used. The overrides look as follows: + +```json5 +{ + // other stuff here + "overrides": [ + { + // pulling = 1 + "predicate": { + "pulling": 1 + }, + "model": "item/bow_pulling_0" + }, + { + // pulling = 1, pull >= 0.65 + "predicate": { + "pulling": 1, + "pull": 0.65 + }, + "model": "item/bow_pulling_1" + }, + // pulling = 1, pull >= 0.9 + { + "predicate": { + "pulling": 1, + "pull": 0.9 + }, + "model": "item/bow_pulling_2" + } + ] +} +``` + +The code side of things is pretty simple. Assuming that we want to add a property named `examplemod:property` to our item, we would use the following code in a [client-side][side] [event handler][eventhandler]: + +```java +@SubscribeEvent +public static void onClientSetup(FMLClientSetupEvent event) { + event.enqueueWork(() -> { // ItemProperties#register is not threadsafe, so we need to call it on the main thread + ItemProperties.register( + // The item to apply the property to. + ExampleItems.EXAMPLE_ITEM, + // The id of the property. + new ResourceLocation("examplemod", "property"), + // A reference to a method that calculates the override value. + // Parameters are the used item stack, the level context, the player using the item, + // and a random seed you can use. + (stack, level, player, seed) -> someMethodThatReturnsAFloat() + ); + }); +} +``` + +### Root Transforms + +Adding the `transform` property at the top level of a model tells the loader that a transformation to all geometry should be applied right before the rotations in a [blockstate file][bsfile] (for block models) or the transformations in a `display` block (for item models) are applied. This is added by NeoForge. + +The root transforms can be specified in two ways. The first way would be as a single property named `matrix` containing a transformation 3x4 matrix (row major order, last row is omitted) in the form of a nested JSON array. The matrix is the composition of the translation, left rotation, scale, right rotation and the transformation origin in that order. An example would look like this: + +```json5 +{ + // ... + "transform": { + "matrix": [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0] + ] + } +} +``` + +The second way is to specify a JSON object containing any combination of the following entries, applied in that order: + +- `translation`: The relative translation. Specified as a three-dimensional vector (`[x, y, z]`) and defaults to `[0, 0, 0]` if absent. +- `rotation` or `left_rotation`: Rotation around the translated origin to be applied before scaling. Defaults to no rotation. Specified in one of the following ways: + - A JSON object with a single axis to rotation mapping, e.g. `{"x": 90}` + - An array of JSON objects with a single axis to rotation mapping each, applied in the order they are specified in, e.g. `[{"x": 90}, {"y": 45}, {"x": -22.5}]` + - An array with three values that each specify the rotation around each axis, e.g. `[90, 45, -22.5]` + - An array with four values directly specifying a quaternion, e.g. `[0.38268346, 0, 0, 0.9238795]` (= 45 degrees around the X axis) +- `scale`: The scale relative to the translated origin. Specified as a three-dimensional vector (`[x, y, z]`) and defaults to `[1, 1, 1]` if absent. +- `post_rotation` or `right_rotation`: Rotation around the translated origin to be applied after scaling. Defaults to no rotation. Specified the same as `rotation`. +- `origin`: Origin point used for rotation and scaling. The transformation is also moved here as a final step. Specified either as a three-dimensional vector (`[x, y, z]`) or using one of the three builtin values `"corner"` (= `[0, 0, 0]`), `"center"` (= `[0.5, 0.5, 0.5]`) or `"opposing-corner"` (= `[1, 1, 1]`, default). + +## Blockstate Files + +_See also: [Blockstate files][mcwikiblockstate] on the [Minecraft Wiki][mcwiki]_ + +Blockstate files are used by the game to assign different models to different [blockstates]. There must be exactly one blockstate file per block registered to the game. Specifying block models for blockstates works in two mutually exclusive ways: via variants or via multipart. + +Inside a `variants` block, there is an element for each blockstate. This is the predominant way of associating blockstates with models, used by the vast majority of blocks. The string representation of the blockstate (without the block name, so for example `"type=top,waterlogged=false"` for a non-waterlogged top slab, or `""` for a block with no properties) is the key, while the element is either a single model object or an array of model objects. If an array of model objects is used, a model will be randomly chosen from it. A model object consists of the following data: + +- `model`: A path to a model file location, relative to the namespace's `models` folder, for example `minecraft:block/cobblestone`. +- `x` and `y`: Rotation of the model on the x-axis/y-axis. Limited to steps of 90 degrees. Optional each, defaults to 0. +- `uvlock`: Whether to lock the UVs of the model when rotating or not. Optional, defaults to false. +- `weight`: Only useful with arrays of model objects. Gives the object a weight, used when choosing a random model object. Optional, defaults to 1. + +In contrast, inside a `multipart` block, elements are combined depending on the properties of the blockstate. This method is mainly used by fences and walls, who enable the four directional parts based on boolean properties. A multipart element consists of two parts: a `when` block and an `apply` block. + +- The `when` block specifies either a string representation of a blockstate or a list of properties that must be met for the element to apply. The lists can either be named `"OR"` or `"AND"`, performing the respective logical operation on its contents. Both single blockstate and list values can additionally specify multiple actual values by separating them with `|` (for example `facing=east|facing=west`). +- The `apply` block specifies the model object or an array of model objects to use. This works exactly like with a `variants` block. + +## Tinting + +Some blocks, such as grass or leaves, change their texture color based on their location and/or properties. [Model elements][elements] can specify a tint index on their faces, which will allow a color handler to handle the respective faces. The code side of things works through two events, one for block color handlers and one for item color handlers. They both work pretty similar, so let's have a look at a block handler first: + +```java +@SubscribeEvent +public static void registerBlockColorHandlers(RegisterColorHandlersEvent.Block event) { + // Parameters are the block's state, the level the block is in, the block's position, and the tint index. + event.register((state, level, pos, tintIndex) -> { + // Replace with your own calculation. See the BlockColors class for vanilla references. + // Generally, if the tint index is -1, it means that no tinting should take place + // and a default value should be used instead. + return 0xFFFFFF; + }); +} +``` + +Item handlers work pretty much the same, except for some naming and the lambda parameters: + +```java +@SubscribeEvent +public static void registerItemColorHandlers(RegisterColorHandlersEvent.Item event) { + // Parameters are the item stack and the tint index. + event.register((stack, tintIndex) -> { + // Like above, replace with your own calculation. Vanilla values are in the ItemColors class. + // Also like above, tint index -1 means no tint and should use a default value instead. + return 0xFFFFFF; + }); +} +``` -Textures, like models, are contained within resource packs and are referred to with `ResourceLocation`s. In Minecraft, the [UV coordinates][uv] (0,0) are taken to mean the **top-left** corner. UVs are *always* from 0 to 16. If a texture is larger or smaller, the coordinates are scaled to fit. A texture should also be square, and the side length of a texture should be a power of two, as doing otherwise breaks mipmapping (e.g. 1x1, 2x2, 8x8, 16x16, and 128x128 are good. 5x5 and 30x30 are not recommended because they are not powers of 2. 5x10 and 4x8 are completely broken as they are not square.). Textures should only ever be not a square if it is [animated][animated]. +Be aware that the `item/generated` model specifies tint indices for its various layers - `layer0` has tint index 0, `layer1` has tint index 1, etc. Also, remember that block items are items, not blocks, and require an item color handler to be colored. -[models]: https://minecraft.wiki/w/Tutorials/Models#File_path -[resloc]: ../../../misc/resourcelocation.md -[statemodel]: https://minecraft.wiki/w/Tutorials/Models#Block_states -[itemmodels]: https://minecraft.wiki/w/Tutorials/Models#Item_models -[state]: ../../../blocks/states.md -[uv]: https://en.wikipedia.org/wiki/UV_mapping -[animated]: https://minecraft.wiki/w/Resource_Pack?so=search#Animation +[ao]: https://en.wikipedia.org/wiki/Ambient_occlusion +[bsfile]: #blockstate-files +[custommodelloader]: ../../../rendering/modelloaders/index.md +[elements]: #elements +[event]: ../../../concepts/events.md +[eventhandler]: ../../../concepts/events.md#registering-an-event-handler +[extrafacedata]: #extra-face-data +[mcwiki]: https://minecraft.wiki +[mcwikiblockstate]: https://minecraft.wiki/w/Tutorials/Models#Block_states +[mcwikimodel]: https://minecraft.wiki/w/Model +[mipmapping]: https://en.wikipedia.org/wiki/Mipmap +[modbus]: ../../../concepts/events.md#event-buses +[overrides]: #overrides +[rendertype]: #render-types +[roottransforms]: #root-transforms +[rl]: ../../../misc/resourcelocation.md +[side]: ../../../concepts/sides.md +[textures]: ../textures.md +[tinting]: #tinting diff --git a/docs/resources/client/models/itemproperties.md b/docs/resources/client/models/itemproperties.md deleted file mode 100644 index bf0a43ca..00000000 --- a/docs/resources/client/models/itemproperties.md +++ /dev/null @@ -1,65 +0,0 @@ -Item Properties -=============== - -Item properties are a way for the "properties" of items to be exposed to the model system. An example is the bow, where the most important property is how far the bow has been pulled. This information is then used to choose a model for the bow, creating an animation for pulling it. - -An item property assigns a certain `float` value to every `ItemStack` it is registered for, and vanilla item model definitions can use these values to define "overrides", where an item defaults to a certain model, but if an override matches, it overrides the model and uses another. They are useful mainly because they are continuous. For example, bows use item properties to define their pull animation. The item models are decided by the 'float' number predicates, it is not limited but generally between `0.0F` and `1.0F`. This allows resource packs to add as many models as they want for the bow pulling animation along that spectrum, instead of being stuck with four "slots" for their models in the animation. The same is true of the compass and clock. - -Adding Properties to Items --------------------------- - -`ItemProperties#register` is used to add a property to a certain item. The `Item` parameter is the item the property is being attached to (e.g. `ExampleItems#APPLE`). The `ResourceLocation` parameter is the name given to the property (e.g. `new ResourceLocation("pull")`). The `ItemPropertyFunction` is a functional interface that takes the `ItemStack`, the `ClientLevel` it is in (may be null), the `LivingEntity` that holds it (may be null), and the `int` containing the id of the holding entity (may be `0`), returning the `float` value for the property. For modded item properties, it is recommended that the mod id of the mod is used as the namespace (e.g. `examplemod:property` and not just `property`, as that really means `minecraft:property`). These should be done in `FMLClientSetupEvent`. -There's also another method `ItemProperties#registerGeneric` that is used to add properties to all items, and it does not take `Item` as its parameter since all items will apply this property. - -:::caution -Use `FMLClientSetupEvent#enqueueWork` to proceed with the tasks, since the data structures in `ItemProperties` are not thread-safe. -::: - -:::note -`ItemPropertyFunction` is deprecated by Mojang in favor of using the subinterface `ClampedItemPropertyFunction` which clamps the result between `0` and `1`. -::: - -Using Overrides ---------------- - -The format of an override can be seen on the [wiki][format], and a good example can be found in `model/item/bow.json`. For reference, here is a hypothetical example of an item with an `examplemod:power` property. If the values have no match, the default is the current model, but if there are multiple matches, the last match in the list will be selected. - -:::caution -A predicate applies to all values *greater than or equal to* the given value. -::: - -```js -{ - "parent": "item/generated", - "textures": { - // Default - "layer0": "examplemod:items/example_partial" - }, - "overrides": [ - { - // power >= .75 - "predicate": { - "examplemod:power": 0.75 - }, - "model": "examplemod:item/example_powered" - } - ] -} -``` - -And here is a hypothetical snippet from the supporting code. Unlike the older versions (lower than 1.16.x), this needs to be done on client side only because `ItemProperties` does not exist on server. - -```java -private void setup(final FMLClientSetupEvent event) -{ - event.enqueueWork(() -> - { - ItemProperties.register(ExampleItems.APPLE, - new ResourceLocation(ExampleMod.MODID, "pulling"), (stack, level, living, id) -> { - return living != null && living.isUsingItem() && living.getUseItem() == stack ? 1.0F : 0.0F; - }); - }); -} -``` - -[format]: https://minecraft.wiki/w/Tutorials/Models#Item_models diff --git a/docs/resources/client/models/tinting.md b/docs/resources/client/models/tinting.md deleted file mode 100644 index d5347239..00000000 --- a/docs/resources/client/models/tinting.md +++ /dev/null @@ -1,32 +0,0 @@ -Coloring Textures -================= - -Many blocks and items in vanilla change their texture color depending on where they are or what properties they have, such as grass. Models support specifying "tint indices" on faces, which are integers that can then be handled by `BlockColor`s and `ItemColor`s. See the [wiki][] for information on how tint indices are defined in vanilla models. - -### `BlockColor`/`ItemColor` - -Both of these are single-method interfaces. `BlockColor` takes a `BlockState`, a (nullable) `BlockAndTintGetter`, and a (nullable) `BlockPos`. `ItemColor` takes an `ItemStack`. Both of them take an `int` parameter `tintIndex`, which is the tint index of the face being colored. Both of them return an `int`, a color multiplier. This `int` is treated as 4 unsigned bytes, alpha, red, green, and blue, in that order, from most significant byte to least. For each pixel in the tinted face, the value of each color channel is `(int)((float) base * multiplier / 255.0)`, where `base` is the original value for the channel, and `multiplier` is the associated byte from the color multiplier. Note that blocks do not use the alpha channel. For example, the grass texture, untinted, looks white and gray. The `BlockColor` and `ItemColor` for grass return color multipliers with low red and blue components, but high alpha and green components, (at least in warm biomes) so when the multiplication is performed, the green is brought out and the red/blue diminished. - -If an item inherits from the `builtin/generated` model, each layer ("layer0", "layer1", etc.) has a tint index corresponding to its layer index. - -### Creating Color Handlers - -`BlockColor`s need to be registered to the `BlockColors` instance of the game. `BlockColors` can be acquired through `RegisterColorHandlersEvent$Block`, and an `BlockColor` can be registered by `#register`. Note that this does not cause the `BlockItem` for the given block to be colored. `BlockItem`s are items and need to be colored with an `ItemColor`. - -```java -@SubscribeEvent -public void registerBlockColors(RegisterColorHandlersEvent.Block event){ - event.register(myBlockColor, coloredBlock1, coloredBlock2, ...); -} -``` - -`ItemColor`s need to be registered to the `ItemColors` instance of the game. `ItemColors` can be acquired through `RegisterColorHandlersEvent$Item`, and an `ItemColor` can be registered by `#register`. This method is overloaded to also take `Block`s, which simply registers the color handler for the item `Block#asItem` (i.e. the block's `BlockItem`). - -```java -@SubscribeEvent -public void registerItemColors(RegisterColorHandlersEvent.Item event){ - event.register(myItemColor, coloredItem1, coloredItem2, ...); -} -``` - -[wiki]: https://minecraft.wiki/w/Tutorials/Models#Block_models diff --git a/docs/resources/client/textures.md b/docs/resources/client/textures.md new file mode 100644 index 00000000..5a638c76 --- /dev/null +++ b/docs/resources/client/textures.md @@ -0,0 +1,44 @@ +# Textures + +All textures in Minecraft are PNG files located within a namespace's `textures` folder. JPG, GIF and other image formats are not supported. The path of [resource locations][rl] referring to textures is generally relative to the `textures` folder, so for example, the resource location `examplemod:block/example_block` refers to the texture file at `assets/examplemod/textures/block/example_block.png`. + +Textures should generally be in sizes that are powers of two, for example 16x16 or 32x32. Unlike older versions, modern Minecraft natively supports block and item texture sizes greater than 16x16. For textures that are not in powers of two that you render yourself anyway (for example GUI backgrounds), create an empty file in the next available power-of-two size (often 256x256), and add your texture in the top left corner of that file, leaving the rest of the file empty. The actual size of the drawn texture can then be set in the code that uses the texture. + +## Texture Metadata + +Texture metadata can be specified in a file named exactly the same as the texture, with an additional `.mcmeta` suffix. For example, an animated texture at `textures/block/example.png` would need an accompanying `textures/block/example.png.mcmeta` file. The `.mcmeta` file has the following format: + +```json5 +{ + // Whether the texture will be blurred if needed. Defaults to false. Currently unused (but functional). + "blur": true, + // Whether the texture will be clamped if needed. Defaults to false. Currently unused (but functional). + "clamp": true, + // See below. + "animation": {} +} +``` + +## Animated Textures + +Minecraft natively supports animated textures for blocks and items. Animated textures consist of a texture file where the different animation stages are located below each other (for example, an animated 16x16 texture with 8 phases would be represented through a 16x128 PNG file). + +To actually be animated and not just be displayed as a distorted texture, there must be an `animation` object in the texture metadata. The sub-object can be empty, but may contain the following optional entries: + +```json5 +{ + "animation": { + // A custom order in which the frames are played. If omitted, the frames are played top to bottom. + "frames": [1, 0], + // How long one frame stays before switching to the next animation stage, in frames. Defaults to 1. + "frametime": 5, + // Whether to interpolate between animation stages. Defaults to false. + "interpolate": true, + // Width and height of one animation stage. If omitted, uses the texture width for both of these. + "width": 12, + "height": 12 + } +} +``` + +[rl]: ../../misc/resourcelocation.md diff --git a/docs/resources/index.md b/docs/resources/index.md index c92b28f6..e4196e0b 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -6,6 +6,8 @@ Minecraft generally has two kinds of resources: client-side resources, known as Both resource and data packs normally require a [`pack.mcmeta` file][packmcmeta], this was also the case in past Forge versions. However, NeoForge generates these at runtime for you, so you don't need to worry about it anymore. +If you are confused about the format of something, an easy way to change that is to have a look at the vanilla resources. Your NeoForge development environment not only contains vanilla code, but also vanilla resources. They can be found in the External Resources section (IntelliJ)/Project Libraries section (Eclipse), under the name `ng_dummy_ng.net.minecraft:client:client-extra:` (for Minecraft resources) or `ng_dummy_ng.net.neoforged:neoforge:` (for NeoForge resources). + ## Assets _See also: [Resource Packs][mcwikiresourcepacks] on the [Minecraft Wiki][mcwiki]_ @@ -14,7 +16,7 @@ Assets, or client-side resources, are all resources that are only relevant on th NeoForge automatically collects all mod resource packs into the `Mod resources` pack, which sits at the bottom of the Selected Packs side in the resource packs menu. It is currently not possible to disable the `Mod resources` pack. However, resource packs that sit above the `Mod resources` pack override resources defined in a resource pack below them. This mechanic allows resource pack makers to override your mod's resources, and also allows mod developers to override Minecraft resources if needed. -Resource packs can contain textures, sounds and translation files, along with block models, item models, and blockstate model definitions. (TODO add links) +Resource packs can contain [models][models], [blockstate files][bsfile], [textures][textures], [sounds][sounds], [particle definitions][particles] and [translation files][translations]. ## Data @@ -63,20 +65,20 @@ Datagen is run through the Data run configuration, which is generated for you al All data providers extend the `DataProvider` interface and usually require one method to be overridden. The following is a list of all data generators Minecraft and NeoForge offer (the linked articles add further information, such as helper methods, TODO add links): -| Class | Method | Generates | Side | Notes | -|--------------------------------------|----------------------------------|--------------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `BlockStateProvider` | `registerStatesAndModels()` | Blockstate model definitions, block models | Client | | -| `ItemModelProvider` | `registerModels()` | Item models | Client | | -| `LanguageProvider` | `addTranslations()` | Translations | Client | Also requires passing the language in the constructor. | -| `ParticleDescriptionProvider` | `addDescriptions()` | Particle definitions | Client | | -| `SoundDefinitionsProvider` | `registerSounds()` | Sound definitions | Client | | -| `AdvancementProvider` | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | -| `LootTableProvider` | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | -| `RecipeProvider` | `buildRecipes(RecipeOutput)` | Recipes | Server | | -| Various subclasses of `TagsProvider` | `addTags(HolderLookup.Provider)` | Tags | Server | Several specialized subclasses exist, for example `BlockTagsProvider`. If the one you need doesn't exist, extend `TagsProvider` (or `IntrinsicHolderTagsProvider` if applicable) with your tag type as the generic parameter. | -| `DatapackBuiltinEntriesProvider` | N/A | Datapack builtin entries, e.g. worldgen | Server | See linked article for details. | -| `DataMapProvider` | `gather()` | Data map entries | Server | | -| `GlobalLootModifierProvider` | `start()` | Global loot modifiers | Server | | +| Class | Method | Generates | Side | Notes | +|--------------------------------------|----------------------------------|-----------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BlockStateProvider` | `registerStatesAndModels()` | Blockstate files, block models | Client | | +| `ItemModelProvider` | `registerModels()` | Item models | Client | | +| `LanguageProvider` | `addTranslations()` | Translations | Client | Also requires passing the language in the constructor. | +| `ParticleDescriptionProvider` | `addDescriptions()` | Particle definitions | Client | | +| `SoundDefinitionsProvider` | `registerSounds()` | Sound definitions | Client | | +| `AdvancementProvider` | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | +| `LootTableProvider` | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | +| `RecipeProvider` | `buildRecipes(RecipeOutput)` | Recipes | Server | | +| Various subclasses of `TagsProvider` | `addTags(HolderLookup.Provider)` | Tags | Server | Several specialized subclasses exist, for example `BlockTagsProvider`. If the one you need doesn't exist, extend `TagsProvider` (or `IntrinsicHolderTagsProvider` if applicable) with your tag type as the generic parameter. | +| `DatapackBuiltinEntriesProvider` | N/A | Datapack builtin entries, e.g. worldgen | Server | See linked article for details. | +| `DataMapProvider` | `gather()` | Data map entries | Server | | +| `GlobalLootModifierProvider` | `start()` | Global loot modifiers | Server | | All of these providers follow the same pattern. First, you create a subclass and add your own resources to be generated. Then, you add the provider to the event in an [event handler][eventhandler]. An example using a `RecipeProvider`: @@ -154,6 +156,7 @@ runs { } ``` +[bsfile]: client/models/index.md#blockstate-files [chattype]: https://minecraft.wiki/w/Chat_type [datapackcmd]: https://minecraft.wiki/w/Commands/datapack [event]: ../concepts/events.md @@ -164,8 +167,13 @@ runs { [mcwiki]: https://minecraft.wiki [mcwikidatapacks]: https://minecraft.wiki/w/Data_pack [mcwikiresourcepacks]: https://minecraft.wiki/w/Resource_pack +[models]: client/models/index.md [packmcmeta]: #packmcmeta [packmcmetadatapack]: https://minecraft.wiki/w/Data_pack#pack.mcmeta [packmcmetaresourcepack]: https://minecraft.wiki/w/Resource_pack#Contents +[particles]: ../gameeffects/particles.md [predicate]: https://minecraft.wiki/w/Predicate [sides]: ../concepts/sides.md +[sounds]: ../gameeffects/sounds.md +[textures]: client/textures.md +[translations]: client/i18n.md#language-files From bcb0f4b26053d0e0b1e65414472ff9b14f51e429 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Sun, 17 Mar 2024 20:51:27 +0100 Subject: [PATCH 27/41] model datagen article --- docs/resources/client/models/datagen.md | 280 ++++++++++++++++++++++++ docs/resources/client/models/index.md | 5 + 2 files changed, 285 insertions(+) create mode 100644 docs/resources/client/models/datagen.md diff --git a/docs/resources/client/models/datagen.md b/docs/resources/client/models/datagen.md new file mode 100644 index 00000000..c60745a6 --- /dev/null +++ b/docs/resources/client/models/datagen.md @@ -0,0 +1,280 @@ +# Model Datagen + +Like most JSON data, block and item models can be [datagenned][datagen]. Since some things are common between item and block models, so is some of the datagen code. + +## Model Datagen Classes + +### `ModelBuilder` + +Every model starts out as a `ModelBuilder` of some sort - usually a `BlockModelBuilder` or an `ItemModelBuilder`, depending on what you are generating. It contains all the properties of the model: its parent, its textures, its elements, its transforms, its loader, etc. Each of the properties can be set by a method: + +| Method | Effect | +|--------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `#texture(String key, ResourceLocation texture)` | Adds a texture variable with the given key and the given texture location. Has an overload where the second parameter is a `String`. | +| `#renderType(ResourceLocation renderType)` | Sets the render type. Has an overload where the parameter is a `String`. | +| `#ao(boolean ao)` | Sets whether to use ambient occlusion or not. | +| `#guiLight(GuiLight light)` | Sets the GUI light. May be `GuiLight.FRONT` or `GuiLight.SIDE`. | +| `#element()` | Adds a new `ElementBuilder` (equivalent to adding a new [element][elements] to the model). Returns said `ElementBuilder` for further modification. | +| `#transforms()` | Returns the builder's `TransformVecBuilder`, used for setting the `display` on a model. | +| `#customLoader(BiFunction customLoaderFactory)` | Using the given factory, makes this model use a [custom loader][custommodelloader], and thus, a custom loader builder. This changes the builder type, and as such may use different methods, depending on the loader's implementation. NeoForge provides a few custom loaders out of the box, see the linked article for more info (including datagen). | + +:::tip +While elaborate and complex models can be created through datagen, it is recommended to instead use modeling software such as [Blockbench][blockbench] to create more complex models and then have the exported models be used, either directly or as parents for other models. +::: + +### `ModelProvider` + +Both block and item model datagen utilize subclasses of `ModelProvider`, named `BlockModelProvider` and `ItemModelProvider`, respectively. While item model datagen directly extends `ItemModelProvider`, block model datagen uses the `BlockStateProvider` base class, which has an internal `BlockModelProvider` that can be accessed via `BlockStateProvider#models()`. The most important part of `ModelProvider` is the `getBuilder(String path)` method, which returns a `BlockModelBuilder` (or `ItemModelBuilder`) at the given location. + +However, `ModelProvider` also contains various helper methods. The most important helper method is probably `withExistingParent(String name, ResourceLocation parent)`, which returns a new builder (via `getBuilder(name)`) and sets the given `ResourceLocation` as model parent. Two other very common helpers are `mcLoc(String name)`, which returns a `ResourceLocation` with the namespace `minecraft` and the given name as path, and `modLoc(String name)`, which does the same but with the provider's mod id (so usually your mod id) instead of `minecraft`. Furthermore, it provides various helper methods that are shortcuts for `#withExistingParent` for common things such as slabs, stairs, fences, doors, etc. + +### `ModelFile` + +Finally, the last important class is `ModelFile`. A `ModelFile` is an in-code representation of a model JSON on disk. `ModelFile` is an abstract class and has two inner subclasses `ExistingModelFile` and `UncheckedModelFile`. An `ExistingModelFile`'s existence is verified using an `ExistingFileHelper`, while an `UncheckedModelFile` is assumed to be existent without further checking. In addition, a `ModelBuilder` is considered to be a `ModelFile` as well. + +## Block Model Datagen + +Now, to actually generate blockstate and block model files, extend `BlockStateProvider` and override the `registerStatesAndModels()` method. Note that block models will always be placed in the `models/block` subfolder, but references are relative to `models` (i.e. they must always be prefixed with `block/`). In most cases, it makes sense to choose from one of the many predefined helper methods: + +```java +public class MyBlockStateProvider extends BlockStateProvider { + // Parameter values are provided by GatherDataEvent. + public MyBlockStateProvider(PackOutput output, ExistingFileHelper existingFileHelper) { + // Replace "examplemod" with your own mod id. + super(output, "examplemod", existingFileHelper); + } + + @Override + protected void registerStatesAndModels() { + // Placeholders, their usages should be replaced with real values. See above for how to use the model builder, + // and below for the helpers the model builder offers. + ModelFile exampleModel = models().withExistingParent("minecraft:block/cobblestone"); + Block block = MyBlocksClass.EXAMPLE_BLOCK.get(); + ResourceLocation exampleTexture = modLoc("block/example_texture"); + ResourceLocation bottomTexture = modLoc("block/example_texture_bottom"); + ResourceLocation topTexture = modLoc("block/example_texture_top"); + ResourceLocation sideTexture = modLoc("block/example_texture_front"); + ResourceLocation frontTexture = modLoc("block/example_texture_front"); + + // Create a simple block model with the same texture on each side. + // The texture must be located at assets//textures/block/.png, where + // and are the block's registry name's namespace and path, respectively. + // Used by the majority of (full) blocks, such as planks, cobblestone or bricks. + simpleBlock(block); + // Overload that accepts a model file to use. + simpleBlock(block, exampleModel); + // Overload that accepts one or multiple (vararg) ConfiguredModel objects. + // See below for more info about ConfiguredModel. + simpleBlock(block, ConfiguredModel.builder().build()); + // Adds the given model file as an identically named item model, for a block item to pick up. + simpleBlockItem(block, exampleModel); + // Shorthand for calling #simpleBlock() (model file overload) and #simpleBlockItem. + simpleBlockWithItem(block, exampleModel); + + // Adds a log block model. Requires two textures at assets//textures/block/.png and + // assets//textures/block/_top.png, representing the side and top of the log, respectively. + // Note that the block input here is limited to RotatedPillarBlock, which is the class vanilla logs use. + logBlock(block); + // Like #logBlock, but the textures are named _side.png and _end.png instead of + // .png and _end.png, respectively. Used by quartz pillars and similar blocks. + // Has an overload that allow you to specify a different texture base name, that is then suffixed + // with _side and _end as needed, an overload that allows you to specify two resource locations + // for the side and end textures, and an overload that allows specifying side and end model files. + axisBlock(block); + // Variants of #logBlock and #axisBlock that additionally allow for render types to be specified. + // Comes in string and resource location variants for the render type, + // in all combinations with all variants of #logBlock and #axisBlock. + logBlock(block, "minecraft:cutout"); + axisBlock(block, mcLoc("cutout_mipped")); + + // Specifies a horizontally-rotatable block model with a side texture, a front texture, and a top texture. + // The bottom will use the side texture as well. If you don't need the front or top texture, + // just pass in the side texture twice. Used by e.g. furnaces and similar blocks. + horizontalBlock(block, sideTexture, frontTexture, topTexture); + // Specifies a horizontally-rotatable block model with a model file that will be rotated as needed. + // Has an overload that instead of a model file accepts a Function, + // allowing for different rotations to use different models. Used e.g. by the crafting table. + horizontalBlock(block, exampleModel); + // Specifies a horizontally-rotatable block model that is attached to a face, e.g. for buttons or levers. + // Accounts for the fact that these can also be placed on the ground and the ceiling, and rotates them accordingly. + // Like #horizontalBlock, has an overload that accepts a Function instead. + horizontalFaceBlock(block, exampleModel); + // Similar to #horizontalBlock, but for blocks that are rotatable in all six directions, including up and down. + // Again, has an overload that accepts a Function instead. + directionalBlock(block, exampleModel); + } +} +``` + +Additionally, helpers for the following common block models exist in `BlockStateProvider`: + +- Stairs +- Slabs +- Buttons +- Pressure Plates +- Signs +- Fences +- Fence Gates +- Walls +- Panes +- Doors +- Trapdoors + +In some cases, the blockstates don't need special casing, but the models do. For this case, the `BlockModelProvider`, accessible via `BlockStateProvider#models()`, provides a few additional helpers, all of which accept a name as the first parameter and most of which are in some way related to full cubes. They will typically be used as model file parameters for e.g. `simpleBlock`. The helpers include supporting methods for the ones in `BlockStateProvider`, as well as: + +- `withExistingParent`: Already mentioned before, this method returns a new model builder with the given parent. The parent must either already exist or be created before the model. +- `getExistingFile`: Performs a lookup in the model provider's `ExistingFileHelper`, returning the corresponding `ModelFile` if present and throwing an `IllegalStateException` otherwise. +- `singleTexture`: Accepts a parent and a single texture location, returning a model with the given parent, and with the texture variable `texture` set to the given texture location. +- `sideBottomTop`: Accepts a parent and three texture locations, returning a model with the given parent and the side, bottom and top textures set to the three texture locations. +- `cube`: Accepts six texture resource locations for the six sides, returning a full cube model with the six sides set to the six textures. +- `cubeAll`: Accepts a texture location, returning a full cube model with the given texture applied to all six sides. A mix between `singleTexture` and `cube`, if you will. +- `cubeTop`: Accepts two texture locations, returning a full cube model with the first texture applied to the sides and the bottom, and the second texture applied to the top. +- `cubeBottomTop`: Accepts three texture locations, returning a full cube model with the side, bottom and top textures set to the three texture locations. A mix between `cube` and `sideBottomTop`, if you will. +- `cubeColumn` and `cubeColumnHorizontal`: Accepts two texture locations, returning a "standing" or "laying" pillar cube model with the side and end textures set to the two texture locations. Used by `BlockStateProvider#logBlock`, `BlockStateProvider#axisBlock` and their variants. +- `orientable`: Accepts three texture locations, returning a cube with a "front" texture. The three texture locations are the side, front and top texture, respectively. +- `orientableVertical`: Variant of `orientable` that omits the top parameter, instead using the side parameter as well. +- `orientableWithBottom`: Variant of `orientable` that has a fourth parameter for a bottom texture between the front and top parameter. +- `crop`: Accepts a texture location, returning a crop-like model with the given texture, as used by the four vanilla crops. +- `cross`: Accepts a texture location, returning a cross model with the given texture, as used by flowers, saplings and many other foliage blocks. +- `torch`: Accepts a texture location, returning a torch model with the given texture. +- `wall_torch`: Accepts a texture location, returning a wall torch model with the given texture (wall torches are separate blocks from standing torches). +- `carpet`: Accepts a texture location, returning a carpet model with the given texture. + +Finally, don't forget to register your block state provider to the event: + +```java +@SubscribeEvent +public static void gatherData(GatherDataEvent event) { + DataGenerator generator = event.getGenerator(); + PackOutput output = generator.getPackOutput(); + ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); + + // other providers here + generator.addProvider( + event.includeClient(), + new MyBlockStateProvider(output, existingFileHelper) + ); +} +``` + +### `ConfiguredModel.Builder` + +If the default helpers won't do it for you, you can also directly build model objects using a `ConfiguredModel.Builder` and then use them in a `VariantBlockStateBuilder` to build a `variants` blockstate file, or in a `MultiPartBlockStateBuilder` to build a `multipart` blockstate file: + +```java +// Create a ConfiguredModel.Builder. Alternatively, you can use one of the ways demonstrated below +// (VariantBlockStateBuilder.PartialBlockstate#modelForState or MultiPartBlockStateBuilder#part) where applicable. +ConfiguredModel.Builder builder = ConfiguredModel.builder() +// Use a model file. As mentioned previously, can either be an ExistingModelFile, an UncheckedModelFile, +// or some sort of ModelBuilder. See above for how to use ModelBuilder. + .modelFile(models().withExistingParent("minecraft:block/cobblestone")) + // Set rotations around the x and y axes. + .rotationX(90) + .rotationY(180) + // Set a uvlock. + .uvlock(true) + // Set a weight. + .weight(5); +// Build the configured model. The return type is an array to account for multiple possible models in the same blockstate. +ConfiguredModel[] model = builder.build(); + +// Get a variant block state builder. +VariantBlockStateBuilder variantBuilder = getVariantBuilder(MyBlocksClass.EXAMPLE_BLOCK.get()); +// Create a partial state and set properties on it. +VariantBlockStateBuilder.PartialBlockstate partialState = variantBuilder.partialState(); +// Add one or multiple models for a partial blockstate. The models are a vararg parameter. +variantBuilder.addModels(partialState, + // Specify at least one ConfiguredModel.Builder, as seen above. Create through #modelForState(). + partialState.modelForState() + .modelFile(models().withExistingParent("minecraft:block/cobblestone")) + .uvlock(true) +); +// Alternatively, forAllStates(Function) creates a model for every state. +// The passed function will be called once for each possible state. +variantBuilder.forAllStates(state -> { + // Return a ConfiguredModel depending on the state's properties. + // For example, the following code will rotate the model depending on the horizontal rotation of the block. + return ConfiguredModel.builder() + .modelFile(models().withExistingParent("minecraft:block/cobblestone")) + .rotationY((int) state.getValue(BlockStateProperties.HORIZONTAL_FACING).toYRot()) + .build(); +}); + +// Get a multipart block state builder. +MultiPartBlockStateBuilder multipartBuilder = getMultipartBuilder(MyBlocksClass.EXAMPLE_BLOCK.get()); +// Add a new part. Starts with .part() and ends with .end(). +multipartBuilder.addPart(multipartBuilder.part() + // Step one: Build the model. multipartBuilder.part() returns a ConfiguredModel.Builder, + // meaning that all methods seen above can be used here as well. + .modelFile("minecraft:block/cobblestone") + // Call .addModel(). Now that the model is built, we can proceed to step two: add the part data. + .addModel() + // Add a condition for the part. Requires a property + // and at least one property value; property values are a vararg. + .condition(BlockStateProperties.FACING, Direction.NORTH, Direction.SOUTH) + // Set the multipart conditions to be ORed instead of the default ANDing. + .useOr() + // Creates a nested condition group. + .nestedGroup() + // Adds a condition to the nested group. + .condition(BlockStateProperties.FACING, Direction.NORTH) + // Sets only this condition group to be ORed instead of ANDed. + .useOr() + // Creates yet another nested condition group. There is no limit on how many groups can be nested. + .nestedGroup() + // Ends the nested condition group, returning to the owning part builder or condition group level. + // Called twice here since we currently have two nested groups. + .endNestedGroup() + .endNestedGroup() + // End the part builder and add the resulting part to the multipart builder. + .end() +); +``` + +## Item Model Datagen + +Generating item models is considerably simpler, which is mainly due to the fact that we operate directly on an `ItemModelProvider` instead of using an intermediate class like `BlockStateProvider`, which is of course because item models don't have an equivalent to blockstate files and are instead used directly. + +Similar to above, we create a class and have it extend the base provider, in this case `ItemModelProvider`. Since we are directly in a subclass of `ModelProvider`, all `models()` calls become `this` (or are omitted). + +```java +public class MyItemModelProvider extends ItemModelProvider { + public MyItemModelProvider(PackOutput output, ExistingFileHelper existingFileHelper) { + super(output, "examplemod", existingFileHelper); + } + + @Override + protected void registerModels() { + // Block items generally use their corresponding block models as parent. + withExistingParent(MyItemsClass.EXAMPLE_BLOCK_ITEM.get(), modLoc("block/example_block")); + // Items generally use a simple parent and one texture. The most common parents are item/generated and item/handheld. + // If you want a more complex model, you can use getBuilder() and then work from that, like you would with block models. + withExistingParent(MyItemsClass.EXAMPLE_ITEM.get(), mcLoc("item/generated")).texture("layer0", "item/example_item"); + // The above line is so common that there is a shortcut for it. Note that the item registry name and the + // texture path, relative to textures/item, must match. + basicItem(MyItemsClass.EXAMPLE_ITEM.get()); + } +} +``` + +And like all data providers, don't forget to register your provider to the event: + +```java +@SubscribeEvent +public static void gatherData(GatherDataEvent event) { + DataGenerator generator = event.getGenerator(); + PackOutput output = generator.getPackOutput(); + ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); + + // other providers here + generator.addProvider( + event.includeClient(), + new MyItemModelProvider(output, existingFileHelper) + ); +} +``` + +[blockbench]: https://www.blockbench.net +[custommodelloader]: ../../../rendering/modelloaders/index.md +[datagen]: ../../index.md#data-generation +[elements]: index.md#elements diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index 3804c7c5..cda4c32d 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -65,6 +65,11 @@ An element is a JSON representation of a cuboid object. It has the following pro - `from`: The coordinate of the start corner of the cuboid, specified as `[x, y, z]`. Specified in 1/16 block units. For example, `[0, 0, 0]` would be the "bottom left" corner, `[8, 8, 8]` would be the center, and `[16, 16, 16]` would be the "top right" corner of the block. - `to`: The coordinate of the start corner of the cuboid, specified as `[x, y, z]`. Like `from`, this is specified in 1/16 block units. + +:::tip +Values in `from` and `to` are limited by Minecraft to the range `[-16, 32]`. However, it is highly discouraged to exceed `[0, 16]`, as that will lead to lighting and/or culling issues. +::: + - `neoforge_data`: See [Extra Face Data][extrafacedata]. - `faces`: An object containing data for of up to 6 faces, named `north`, `south`, `east`, `west`, `up` and `down`, respectively. Every face has the following data: - `uv`: The uv of the face, specified as `[u1, v1, u2, v2]`, where `u1, v1` is the top left uv coordinates and `u2, v2` is the bottom right uv coordinates. From 3b602be12a53a706c1d07095a2245bfe8ba5558a Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Sun, 17 Mar 2024 20:53:03 +0100 Subject: [PATCH 28/41] update contributing guidelines to use json5 instead of js for json comment highlighting --- src/pages/contributing.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/contributing.mdx b/src/pages/contributing.mdx index e2e223d2..6cee77db 100644 --- a/src/pages/contributing.mdx +++ b/src/pages/contributing.mdx @@ -163,7 +163,7 @@ sidebar_position: 2 Categories are folders within the documentation. They inherit titles and positional data from `index.md`. If an `index.md` is not within a subfolder, a `_category_.json` file should be created, specifying the `label` representing the name of the section, and `position` representing where on the sidebar it should go. -```js +```json5 { // Name of the category to display "label": "Example Title", @@ -232,7 +232,7 @@ When referencing elements outside of code blocks, they should be surrounded with `#SOME_CONSTANT` ``` -Code blocks should specify the language after the triple backtick (`` ``` ``). When writing a JSON block, the JavaScript (`js`) syntax highlighter should be used to allow comments. +Code blocks should specify the language after the triple backtick (`` ``` ``). When writing a JSON block, the JSON5 (`json5`) syntax highlighter should be used to allow comments. ````md @@ -243,7 +243,7 @@ public void run() { ``` -```js +```json5 { // Comments are allowed here "text": "Hiya" From fd6289db35d0d514493adfccf3e0e3f6ec5f0963 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Tue, 19 Mar 2024 01:15:28 +0100 Subject: [PATCH 29/41] custom model loaders: part 1 --- docs/datagen/client/modelproviders.md | 427 ------------------- docs/resources/client/models/datagen.md | 11 +- docs/resources/client/models/index.md | 2 +- docs/resources/client/models/modelloaders.md | 272 ++++++++++++ 4 files changed, 279 insertions(+), 433 deletions(-) delete mode 100644 docs/datagen/client/modelproviders.md create mode 100644 docs/resources/client/models/modelloaders.md diff --git a/docs/datagen/client/modelproviders.md b/docs/datagen/client/modelproviders.md deleted file mode 100644 index 5eef5bf6..00000000 --- a/docs/datagen/client/modelproviders.md +++ /dev/null @@ -1,427 +0,0 @@ -Model Generation -================ - -[Models] can be generated for models or block states by default. Each provides a method of generating the necessary JSONs (`ModelBuilder#toJson` for models and `IGeneratedBlockState#toJson` for block states). After implementation, the [associated providers][provider] must be [added][datagen] to the `DataGenerator`. - -```java -// On the MOD event bus -@SubscribeEvent -public void gatherData(GatherDataEvent event) { - DataGenerator gen = event.getGenerator(); - ExistingFileHelper efh = event.getExistingFileHelper(); - - gen.addProvider( - // Tell generator to run only when client assets are generating - event.includeClient(), - output -> new MyItemModelProvider(output, MOD_ID, efh) - ); - gen.addProvider( - event.includeClient(), - output -> new MyBlockStateProvider(output, MOD_ID, efh) - ); -} -``` - -Model Files ------------ - -A `ModelFile` acts as the base for all models referenced or generated by a provider. Each model file stores the location relative to the `models` subdirectory and can assert whether the file exists. - -### Existing Model Files - -`ExistingModelFile` is a subclass of `ModelFile` which checks via [`ExistingFileHelper#exists`][efh] whether the model already exists within the `models` subdirectory. All non-generated models are usually referenced through `ExistingModelFile`s. - -### Unchecked Model Files - -`UncheckedModelFile` is a subclass of `ModelFile` which assumes the specified model exists in some location. - -:::note -There should be no cases where an `UncheckedModelFile` is used to reference a model. If there is, then the associated resources are not properly being tracked by `ExistingFileHelper`. -::: - -Model Builders --------------- - -A `ModelBuilder` represents a to-be-generated `ModelFile`. It contains all the data about a model: its parent, faces, textures, transformations, lighting, and [loader]. - -:::tip -While a complex model can be generated, it is recommended that those models be constructed using a modeling software beforehand. Then, the data provider can generate the children models with specific textures applied through the defined references in the parent complex model. -::: - -The parent (via `ModelBuilder#parent`) of the builder can be any `ModelFile`: generated or existing. Generated files are added to `ModelProvider`s as soon as the builder is created. The builder itself can be passed in as a parent, or the `ResourceLocation` can supplied alternatively. - -:::caution -If the parent is not generated before the child model when passing in a `ResourceLocation`, then an exception will be thrown. -::: - -Each element (via `ModelBuilder#element`) within a model is defined as cube using two three-dimensional points (`ElementBuilder#from` and `#to` respectively) where each axis is limited to the values `[-16,32]` (between -16 and 32 inclusive). Each face (`ElementBuilder#face`) of the cube can specify when the face is culled (`FaceBuilder#cullface`), [tint index][color] (`FaceBuilder#tintindex`), texture reference from the `textures` keys (`FaceBuilder#texture`), UV coordinate on the texture (`FaceBuilder#uvs`), and rotation in 90 degree intervals (`FaceBuilder#rotation`). - -:::note -It recommended for block models which have elements that exceed a bound of `[0,16]` on any axis to separate into multiple blocks, such as for a multiblock structure, to avoid lighting and culling issues. -::: - -Each cube can additionally be rotated (`ElementBuilder#rotation`) around a specified point (`RotationBuilder#origin`) for a given axis (`RotationBuilder#axis`) in 22.5 degree intervals (`RotationBuilder#angle`). The cube can scale all faces in relation to the entire model as well (`RotationBuilder#rescale`). The cube can also determine whether its shadows should be rendered (`ElementBuilder#shade`). - -Each model defines a list of texture keys (`ModelBuilder#texture`) which points to either a location or a reference. Each key can then be referenced in any element by prefixing using a `#` (a texture key of `example` can be referenced in an element using `#example`). A location specifies where a texture is in `assets//textures/.png`. A reference is used by any models parenting the current model as keys to define textures for later. - -The model can additionally be transformed (`ModelBuilder#transforms`) for any defined perspective (in the left hand in first person, in the gui, on the ground, etc.). For any perspective (`TransformsBuilder#transform`), the rotation (`TransformVecBuilder#rotation`), translation (`TransformVecBuilder#translation`), and scale (`TransformVecBuilder#scale`) can be set. - -Finally, the model can set whether to use ambient occlusion in a level (`ModelBuilder#ao`) and from what location to light and shade the model from `ModelBuilder#guiLight`. - -### `BlockModelBuilder` - -A `BlockModelBuilder` represents a block model to-be-generated. In addition to the `ModelBuilder`, a transform to the entire model (`BlockModelBuilder#rootTransform`) can be generated. The root can be translated (`RootTransformBuilder#transform`), rotated (`RootTransformBuilder#rotation`, `RootTransformBuilder#postRotation`), and scaled (`RootTransformBuilder#scale`) either individually or all in one transformation (`RootTransformBuilder#transform`) around some origin (`RootTransformBuilder#origin`). - -### `ItemModelBuilder` - -An `ItemModelBuilder` represents an item model to-be-generated. In addition to the `ModelBuilder`, [overrides] (`OverrideBuilder#override`) can be generated. Each override applied to a model can apply conditions which represent for a given property that must be above the specified value (`OverrideBuilder#predicate`). If the conditions are met, then the specified model (`OverrideBuilder#model`) will be rendered instead of this model. - -Model Providers ---------------- - -The `ModelProvider` subclasses are responsible for generating the constructed `ModelBuilder`s. The provider takes in the generator, mod id, subdirectory in the `models` folder to generate within, a `ModelBuilder` factory, and the existing file helper. Each provider subclass must implement `#registerModels`. - -The provider contains basic methods which either create the `ModelBuilder` or provides convenience for getting texture or model references: - -Method | Description -:---: | :--- -`getBuilder` | Creates a new `ModelBuilder` within the provider's subdirectory for the given mod id. -`withExistingParent` | Creates a new `ModelBuilder` for the given parent. Should be used when the parent is not generated by the builder. -`mcLoc` | Creates a `ResourceLocation` for the path in the `minecraft` namespace. -`modLoc` | Creates a `ResourceLocation` for the path in the given mod id's namespace. - -Additionally, there are several helpers for easily generating common models using vanilla templates. Most are for block models with only a few being universal. - -:::note -Although the models are within a specific subdirectory, that does **not** mean that the model cannot be referenced by a model in another subdirectory. Usually, it is indicative of that model being used for that type of object. -::: - -### `BlockModelProvider` - -The `BlockModelProvider` is used for generating block models via `BlockModelBuilder` in the `block` folder. Block models should typically parent `minecraft:block/block` or one of its children models for use with item models. - -:::note -Block models and its item model counterpart are typically not generated through a direct subclass of `BlockModelProvider` and `ItemModelProvider` but through [`BlockStateProvider`][blockstateprovider]. -::: - -### `ItemModelProvider` - -The `ItemModelProvider` is used for generating block models via `ItemModelBuilder` in the `item` folder. Most item models parent `item/generated` and use `layer0` to specify their texture, which can be done using `#singleTexture`. - -:::note -`item/generated` can support five texture layers stacked on top of each other: `layer0`, `layer1`, `layer2`, `layer3`, and `layer4`. -::: - -```java -// In some ItemModelProvider#registerModels - -// Will generate 'assets//models/item/example_item.json' -// Parent will be 'minecraft:item/generated' -// For the texture key 'layer0' -// It will be at 'assets//textures/item/example_item.png' -this.basicItem(EXAMPLE_ITEM.get()); -``` - -:::note -Item models for blocks should typically parent an existing block model instead of generating a separate model for an item. -::: - -Block State Provider --------------------- - -A `BlockStateProvider` is responsible for generating [block state JSONs][blockstate] in `blockstates`, block models in `models/block`, and item models in `models/item` for said blocks. The provider takes in the data generator, mod id, and existing file helper. Each `BlockStateProvider` subclass must implement `#registerStatesAndModels`. - -The provider contains basic methods for generating block state JSONs and block models. Item models must be generated separately as a block state JSON may define multiple models to use in different contexts. There are a number of common methods, however, that that the modder should be aware of when dealing with more complex tasks: - -Method | Description -:---: | :--- -`models` | Gets the [`BlockModelProvider`][blockmodels] used to generate the item block models. -`itemModels` | Gets the [`ItemModelProvider`][itemmodels] used to generate the item block models. -`modLoc` | Creates a `ResourceLocation` for the path in the given mod id's namespace. -`mcLoc` | Creates a `ResourceLocation` for the path in the `minecraft` namespace. -`blockTexture` | References a texture within `textures/block` which has the same name as the block. -`simpleBlockItem` | Creates an item model for a block given the associated model file. -`simpleBlockWithItem` | Creates a single block state for a block model and an item model using the block model as its parent. - -A block state JSON is made up of variants or conditions. Each variant or condition references a `ConfiguredModelList`: a list of `ConfiguredModel`s. Each configured model contains the model file (via `ConfiguredModel$Builder#modelFile`), the X and Y rotation in 90 degree intervals (via `#rotationX` and `rotationY` respectively), whether the texture can rotate when the model is rotated by the block state JSON (via `#uvLock`), and the weight of the model appearing compared to other models in the list (via `#weight`). - -The builder (`ConfiguredModel#builder`) can also create an array of `ConfiguredModel`s by creating the next model using `#nextModel` and repeating the settings until `#build` is called. - -### `VariantBlockStateBuilder` - -Variants can be generated using `BlockStateProvider#getVariantBuilder`. Each variant specifies a list of [properties] (`PartialBlockstate`) which when matches a `BlockState` in a level, will display a model chosen from the corresponding model list. An exception is thrown if there is a `BlockState` which is not covered by any variant defined. Only one variant can be true for any `BlockState`. - -A `PartialBlockstate` is typically defined using one of three methods: - -Method | Description -:---: | :--- -`partialState` | Creates a `PartialBlockstate` to be defined. -`forAllStates` | Defines a function where a given `BlockState` can be represented by an array of `ConfiguredModel`s. -`forAllStatesExcept` | Defines a function similar to `#forAllStates`; however, it also specifies which properties do not affect the models rendered. - -For a `PartialBlockstate`, the properties defined can be specified (`#with`). The configured models can be set (`#setModels`), appended to the existing models (`#addModels`), or built (`#modelForState` and then `ConfiguredModel$Builder#addModel` once finished instead of `#ConfiguredModel$Builder#build`). - -```java -// In some BlockStateProvider#registerStatesAndModels - -// EXAMPLE_BLOCK_1: Has Property BlockStateProperties#AXIS -this.getVariantBuilder(EXAMPLE_BLOCK_1) // Get variant builder - .partialState() // Construct partial state - .with(AXIS, Axis.Y) // When BlockState AXIS = Y - .modelForState() // Set models when AXIS = Y - .modelFile(yModelFile1) // Can show 'yModelFile1' - .nextModel() // Adds another model when AXIS = Y - .modelFile(yModelFile2) // Can show 'yModelFile2' - .weight(2) // Will show 'yModelFile2' 2/3 of the time - .addModel() // Finalizes models when AXIS = Y - .with(AXIS, Axis.Z) // When BlockState AXIS = Z - .modelForState() // Set models when AXIS = Z - .modelFile(hModelFile) // Can show 'hModelFile' - .addModel() // Finalizes models when AXIS = Z - .with(AXIS, Axis.X) // When BlockState AXIS = X - .modelForState() // Set models when AXIS = X - .modelFile(hModelFile) // Can show 'hModelFile' - .rotationY(90) // Rotates 'hModelFile' 90 degrees on the Y axis - .addModel(); // Finalizes models when AXIS = X - -// EXAMPLE_BLOCK_2: Has Property BlockStateProperties#HORIZONTAL_FACING -this.getVariantBuilder(EXAMPLE_BLOCK_2) // Get variant builder - .forAllStates(state -> // For all possible states - ConfiguredModel.builder() // Creates configured model builder - .modelFile(modelFile) // Can show 'modelFile' - .rotationY((int) state.getValue(HORIZONTAL_FACING).toYRot()) // Rotates 'modelFile' on the Y axis depending on the property - .build() // Creates the array of configured models - ); - -// EXAMPLE_BLOCK_3: Has Properties BlockStateProperties#HORIZONTAL_FACING, BlockStateProperties#WATERLOGGED -this.getVariantBuilder(EXAMPLE_BLOCK_3) // Get variant builder - .forAllStatesExcept(state -> // For all HORIZONTAL_FACING states - ConfiguredModel.builder() // Creates configured model builder - .modelFile(modelFile) // Can show 'modelFile' - .rotationY((int) state.getValue(HORIZONTAL_FACING).toYRot()) // Rotates 'modelFile' on the Y axis depending on the property - .build(), // Creates the array of configured models - WATERLOGGED); // Ignores WATERLOGGED property -``` - -### `MultiPartBlockStateBuilder` - -Multiparts can be generated using `BlockStateProvider#getMultipartBuilder`. Each part (`MultiPartBlockStateBuilder#part`) specifies a group of conditions of properties which when matches a `BlockState` in a level, will display a model from the model list. All condition groups that match the `BlockState` will display their chosen model overlaid on each other. - -For any part (obtained via `ConfiguredModel$Builder#addModel`), a condition can be added (via `#condition`) when a property is one of the specified values. Conditions must all succeed or, when `#useOr` is set, at least one must succeed. Conditions can be grouped (via `#nestedGroup`) as long as the current grouping only contains other groups and no single conditions. Groups of conditions can be left using `#endNestedGroup` and a given part can be finished via `#end`. - -```java -// In some BlockStateProvider#registerStatesAndModels - -// Redstone Wire -this.getMultipartBuilder(REDSTONE) // Get multipart builder - .part() // Create part - .modelFile(redstoneDot) // Can show 'redstoneDot' - .addModel() // 'redstoneDot' is displayed when... - .useOr() // At least one of these conditions are true - .nestedGroup() // true when all grouped conditions are true - .condition(WEST_REDSTONE, NONE) // true when WEST_REDSTONE is NONE - .condition(EAST_REDSTONE, NONE) // true when EAST_REDSTONE is NONE - .condition(SOUTH_REDSTONE, NONE) // true when SOUTH_REDSTONE is NONE - .condition(NORTH_REDSTONE, NONE) // true when NORTH_REDSTONE is NONE - .endNestedGroup() // End group - .nestedGroup() // true when all grouped conditions are true - .condition(EAST_REDSTONE, SIDE, UP) // true when EAST_REDSTONE is SIDE or UP - .condition(NORTH_REDSTONE, SIDE, UP) // true when NORTH_REDSTONE is SIDE or UP - .endNestedGroup() // End group - .nestedGroup() // true when all grouped conditions are true - .condition(EAST_REDSTONE, SIDE, UP) // true when EAST_REDSTONE is SIDE or UP - .condition(SOUTH_REDSTONE, SIDE, UP) // true when SOUTH_REDSTONE is SIDE or UP - .endNestedGroup() // End group - .nestedGroup() // true when all grouped conditions are true - .condition(WEST_REDSTONE, SIDE, UP) // true when WEST_REDSTONE is SIDE or UP - .condition(SOUTH_REDSTONE, SIDE, UP) // true when SOUTH_REDSTONE is SIDE or UP - .endNestedGroup() // End group - .nestedGroup() // true when all grouped conditions are true - .condition(WEST_REDSTONE, SIDE, UP) // true when WEST_REDSTONE is SIDE or UP - .condition(NORTH_REDSTONE, SIDE, UP) // true when NORTH_REDSTONE is SIDE or UP - .endNestedGroup() // End group - .end() // Finish part - .part() // Create part - .modelFile(redstoneSide0) // Can show 'redstoneSide0' - .addModel() // 'redstoneSide0' is displayed when... - .condition(NORTH_REDSTONE, SIDE, UP) // NORTH_REDSTONE is SIDE or UP - .end() // Finish part - .part() // Create part - .modelFile(redstoneSideAlt0) // Can show 'redstoneSideAlt0' - .addModel() // 'redstoneSideAlt0' is displayed when... - .condition(SOUTH_REDSTONE, SIDE, UP) // SOUTH_REDSTONE is SIDE or UP - .end() // Finish part - .part() // Create part - .modelFile(redstoneSideAlt1) // Can show 'redstoneSideAlt1' - .rotationY(270) // Rotates 'redstoneSideAlt1' 270 degrees on the Y axis - .addModel() // 'redstoneSideAlt1' is displayed when... - .condition(EAST_REDSTONE, SIDE, UP) // EAST_REDSTONE is SIDE or UP - .end() // Finish part - .part() // Create part - .modelFile(redstoneSide1) // Can show 'redstoneSide1' - .rotationY(270) // Rotates 'redstoneSide1' 270 degrees on the Y axis - .addModel() // 'redstoneSide1' is displayed when... - .condition(WEST_REDSTONE, SIDE, UP) // WEST_REDSTONE is SIDE or UP - .end() // Finish part - .part() // Create part - .modelFile(redstoneUp) // Can show 'redstoneUp' - .addModel() // 'redstoneUp' is displayed when... - .condition(NORTH_REDSTONE, UP) // NORTH_REDSTONE is UP - .end() // Finish part - .part() // Create part - .modelFile(redstoneUp) // Can show 'redstoneUp' - .rotationY(90) // Rotates 'redstoneUp' 90 degrees on the Y axis - .addModel() // 'redstoneUp' is displayed when... - .condition(EAST_REDSTONE, UP) // EAST_REDSTONE is UP - .end() // Finish part - .part() // Create part - .modelFile(redstoneUp) // Can show 'redstoneUp' - .rotationY(180) // Rotates 'redstoneUp' 180 degrees on the Y axis - .addModel() // 'redstoneUp' is displayed when... - .condition(SOUTH_REDSTONE, UP) // SOUTH_REDSTONE is UP - .end() // Finish part - .part() // Create part - .modelFile(redstoneUp) // Can show 'redstoneUp' - .rotationY(270) // Rotates 'redstoneUp' 270 degrees on the Y axis - .addModel() // 'redstoneUp' is displayed when... - .condition(WEST_REDSTONE, UP) // WEST_REDSTONE is UP - .end(); // Finish part -``` - -Model Loader Builders ---------------------- - -Custom model loaders can also be generated for a given `ModelBuilder`. Custom model loaders subclass `CustomLoaderBuilder` and can be applied to a `ModelBuilder` via `#customLoader`. The factory method passed in creates a new loader builder to which configurations can be made. After all the changes have been finished, the custom loader can return back to the `ModelBuilder` via `CustomLoaderBuilder#end`. - -Model Builder | Factory Method | Description -:---: | :---: | :--- -`DynamicFluidContainerModelBuilder` | `#begin` | Generates a bucket model for the specified fluid. -`CompositeModelBuilder` | `#begin` | Generates a model composed of models. -`ItemLayersModelBuilder` | `#begin` | Generates a Forge implementation of an `item/generated` model. -`SeparateTransformsModelBuilder` | `#begin` | Generates a model which changes based on the specified [transform]. -`ObjModelBuilder` | `#begin` | Generates an [OBJ model][obj]. - -```java -// For some BlockModelBuilder builder -builder.customLoader(ObjModelBuilder::begin) // Custom loader 'forge:obj' - .modelLocation(modLoc("models/block/model.obj")) // Set the OBJ model location - .flipV(true) // Flips the V coordinate in the supplied .mtl texture - .end() // Finish custom loader configuration -.texture("particle", mcLoc("block/dirt")) // Set particle texture to dirt -.texture("texture0", mcLoc("block/dirt")); // Set 'texture0' texture to dirt -``` - -Custom Model Loader Builders ----------------------------- - -Custom loader builders can be created by extending `CustomLoaderBuilder`. The constructor can still have a `protected` visibility with the `ResourceLocation` hardcoded to the loader id registered via `ModelEvent$RegisterGeometryLoaders#register`. The builder can then be initialized via a static factory method or the constructor if made `public`. - -```java -public class ExampleLoaderBuilder> extends CustomLoaderBuilder { - public static > ExampleLoaderBuilder begin(T parent, ExistingFileHelper existingFileHelper) { - return new ExampleLoaderBuilder<>(parent, existingFileHelper); - } - - protected ExampleLoaderBuilder(T parent, ExistingFileHelper existingFileHelper) { - super(new ResourceLocation(MOD_ID, "example_loader"), parent, existingFileHelper); - } -} -``` - -Afterwards, any configurations specified by the loader should be added as chainable methods. - -```java -// In ExampleLoaderBuilder -public ExampleLoaderBuilder exampleInt(int example) { - // Set int - return this; -} - -public ExampleLoaderBuilder exampleString(String example) { - // Set string - return this; -} -``` - -If any additional configuration is specified, `#toJson` should be overridden to write the additional properties. - -```java -// In ExampleLoaderBuilder -@Override -public JsonObject toJson(JsonObject json) { - json = super.toJson(json); // Handle base loader properties - // Encode custom loader properties - return json; -} -``` - -Custom Model Providers ----------------------- - -Custom model providers require a `ModelBuilder` subclass, which defines the base of the model to generate, and a `ModelProvider` subclass, which generates the models. - -The `ModelBuilder` subclass contains any special properties to which can be applied specifically to those types of models (item models can have overrides). If any additional properties are added, `#toJson` needs to be overridden to write the additional information. - -```java -public class ExampleModelBuilder extends ModelBuilder { - // ... -} -``` - -The `ModelProvider` subclass requires no special logic. The constructor should hardcode the subdirectory within the `models` folder and the `ModelBuilder` to represent the to-be-generated models. - -```java -public class ExampleModelProvider extends ModelProvider { - - public ExampleModelProvider(PackOutput output, String modid, ExistingFileHelper existingFileHelper) { - // Models will be generated to 'assets//models/example' if no 'modid' is specified in '#getBuilder' - super(output, modid, "example", ExampleModelBuilder::new, existingFileHelper); - } -} -``` - -Custom Model Consumers ----------------------- - -Custom model consumers like [`BlockStateProvider`][blockstateprovider] can be created by manually generating the models themselves. The `ModelProvider` subclass used to generate the models should be specified and made available. - -```java -public class ExampleModelConsumerProvider implements IDataProvider { - - public ExampleModelConsumerProvider(PackOutput output, String modid, ExistingFileHelper existingFileHelper) { - this.example = new ExampleModelProvider(output, modid, existingFileHelper); - } -} -``` - -Once the data provider is running, the models within the `ModelProvider` subclass can be generated using `ModelProvider#generateAll`. - -```java -// In ExampleModelConsumerProvider -@Override -public CompletableFuture run(CachedOutput cache) { - // Populate the model provider - CompletableFuture exampleFutures = this.example.generateAll(cache); // Generate the models - - // Run logic and create CompletableFuture(s) for writing files - // ... - - // Assume we have a new CompletableFuture providerFuture - return CompletableFuture.allOf(exampleFutures, providerFuture); -} -``` - -[provider]: #model-providers -[models]: ../../resources/client/models/index.md -[datagen]: ../index.md#data-providers -[efh]: ../index.md#existing-files -[loader]: #custom-model-loader-builders -[color]: ../../resources/client/models/index.md#tinting -[overrides]: ../../resources/client/models/index.md#overrides -[blockstateprovider]: #block-state-provider -[blockstate]: https://minecraft.wiki/w/Tutorials/Models#Block_states -[blockmodels]: #blockmodelprovider -[itemmodels]: #itemmodelprovider -[properties]: ../../blocks/states.md#implementing-block-states -[transform]: ../../rendering/modelloaders/transform.md -[obj]: ../../rendering/modelloaders/index.md#wavefront-obj-models diff --git a/docs/resources/client/models/datagen.md b/docs/resources/client/models/datagen.md index c60745a6..c0011e06 100644 --- a/docs/resources/client/models/datagen.md +++ b/docs/resources/client/models/datagen.md @@ -72,7 +72,7 @@ public class MyBlockStateProvider extends BlockStateProvider { simpleBlockWithItem(block, exampleModel); // Adds a log block model. Requires two textures at assets//textures/block/.png and - // assets//textures/block/_top.png, representing the side and top of the log, respectively. + // assets//textures/block/_top.png, referencing the side and top texture, respectively. // Note that the block input here is limited to RotatedPillarBlock, which is the class vanilla logs use. logBlock(block); // Like #logBlock, but the textures are named _side.png and _end.png instead of @@ -96,10 +96,10 @@ public class MyBlockStateProvider extends BlockStateProvider { // allowing for different rotations to use different models. Used e.g. by the crafting table. horizontalBlock(block, exampleModel); // Specifies a horizontally-rotatable block model that is attached to a face, e.g. for buttons or levers. - // Accounts for the fact that these can also be placed on the ground and the ceiling, and rotates them accordingly. + // Accounts for placing the block on the ground and on the ceiling, and rotates them accordingly. // Like #horizontalBlock, has an overload that accepts a Function instead. horizontalFaceBlock(block, exampleModel); - // Similar to #horizontalBlock, but for blocks that are rotatable in all six directions, including up and down. + // Similar to #horizontalBlock, but for blocks that are rotatable in all directions, including up and down. // Again, has an overload that accepts a Function instead. directionalBlock(block, exampleModel); } @@ -175,7 +175,8 @@ ConfiguredModel.Builder builder = ConfiguredModel.builder() .uvlock(true) // Set a weight. .weight(5); -// Build the configured model. The return type is an array to account for multiple possible models in the same blockstate. +// Build the configured model. The return type is an array +// to account for multiple possible models in the same blockstate. ConfiguredModel[] model = builder.build(); // Get a variant block state builder. @@ -275,6 +276,6 @@ public static void gatherData(GatherDataEvent event) { ``` [blockbench]: https://www.blockbench.net -[custommodelloader]: ../../../rendering/modelloaders/index.md +[custommodelloader]: modelloaders.md#datagen [datagen]: ../../index.md#data-generation [elements]: index.md#elements diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index cda4c32d..d15a6dc4 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -26,7 +26,7 @@ A model is a JSON file with the following optional properties in the root tag: - `gui_light`: Can be `"front"` or `"side"`. If `"front"`, light will come from the front, useful for flat 2D models. If `"side"`, light will come from the side, useful for 3D models (especially block models). Defaults to `"side"`. Only effective on item models. - `textures`: A sub-object that maps names (known as texture variables) to [texture locations][textures]. Texture variables can then be used in [elements]. They can also be specified in elements, but left unspecified in order for child models to specify them. - Block models should additionally specify a `particle` texture. This texture is used when falling on, running across, or breaking the block. - - Item models can also use layer textures, named `layer0`, `layer1`, etc., where layers with a higher index are rendered above those with a lower index (e.g. `layer1` would be rendered above `layer0`). Only works if the parent is `item/generated`. + - Item models can also use layer textures, named `layer0`, `layer1`, etc., where layers with a higher index are rendered above those with a lower index (e.g. `layer1` would be rendered above `layer0`). Only works if the parent is `item/generated`, and only works for up to 5 layers (`layer0` through `layer4`). - `elements`: A list of cuboid [elements]. - `overrides`: A list of [override models][overrides]. Only effective on item models. - `display`: A sub-object that holds the different display options for different perspectives. Only effective on item models, but often specified in block models so that item models can inherit the display options. Possible perspectives include `thirdperson_righthand`, `thirdperson_lefthand`, `firstperson_righthand`, `firstperson_lefthand`, `gui`, `head`, `ground`, and `fixed` (for item frames and similar modded display blocks). Every perspective is an optional sub-object that may contain the following options: diff --git a/docs/resources/client/models/modelloaders.md b/docs/resources/client/models/modelloaders.md new file mode 100644 index 00000000..0a6a75f2 --- /dev/null +++ b/docs/resources/client/models/modelloaders.md @@ -0,0 +1,272 @@ +# Custom Model Loaders + +A model is simply a shape. It can be a cube, a collection of cubes, a collection of triangles, or any other geometrical shape (or collection of geometrical shape). For most contexts, it is not relevant how a model is defined, as everything will end up as a `BakedModel` in memory anyway. As such, NeoForge adds the ability to register custom model loaders that can transform any model you want into a `BakedModel` for the game to use. + +The entry point for a block model remains the model JSON file. However, you can specify a `loader` field in the root of the JSON that will swap out the default loader for your own loader. A custom model loader may ignore all fields the default loader requires. + +## Builtin Model Loaders + +Besides the default model loader, NeoForge offers a total of six builtin loaders, each serving a different purpose. + +### Composite Model + +### Dynamic Bucket Model + +### Elements Model + +### Item Layer Model + +### OBJ Model + +### Separate Transforms Model + +## Creating Custom Model Loaders + +To create your own model loader, you need three classes, plus an event handler: + +- A geometry loader class +- A geometry class +- A dynamic model class +- A [client-side][sides] [event handler][event] for `ModelEvent.RegisterGeometryLoaders` that registers the geometry loader + +To illustrate how these classes are connected, we will follow a model being loaded: + +- During model loading, a model JSON with the `loader` property set to your loader is passed to your geometry loader. The geometry loader then reads the model JSON and returns a geometry object using the model JSON's properties. +- During model baking, the geometry is baked, returning a dynamic model. +- During model rendering, the dynamic model is used for rendering. + +Let's illustrate this further through a basic class setup. The geometry loader class is named `MyGeometryLoader`, the geometry class is named `MyGeometry`, and the dynamic model class is named `MyDynamicModel`: + +```java +public class MyGeometryLoader implements IGeometryLoader { + // It is highly recommended to use a singleton pattern for geometry loaders, as all models can be loaded through one loader. + public static final MyGeometryLoader INSTANCE = new MyGeometryLoader(); + // The id we will use to register this loader. Also used in the loader datagen class. + public static final ResourceLocation ID = new ResourceLocation("examplemod", "my_custom_loader"); + + // In accordance with the singleton pattern, make the constructor private. + private MyGeometryLoader() {} + + @Override + public MyGeometry read(JsonObject jsonObject, JsonDeserializationContext context) throws JsonParseException { + // Use the given JsonObject and, if needed, the JsonDeserializationContext to get properties from the model JSON. + // The MyGeometry constructor may have constructor parameters (see below). + return new MyGeometry(); + } +} + +public class MyGeometry implements IUnbakedGeometry { + // The constructor may have any parameters you need, and store them in fields for further usage below. + // If the constructor has parameters, the constructor call in MyGeometryLoader#read must match them. + public MyGeometry() {} + + // Method responsible for model baking, returning our dynamic model. Parameters in this method are: + // - The geometry baking context. Contains many properties that we will pass into the model, e.g. light and ao values. + // - The model baker. Can be used for baking sub-models. + // - The sprite getter. Maps materials (= texture variables) to TextureAtlasSprites. Materials can be obtained from the context. + // For example, to get a model's particle texture, call spriteGetter.apply(context.getMaterial("particle")); + // - The model state. This holds the properties from the blockstate file, e.g. rotations and the uvlock boolean. + // - The item overrides. This is the code representation of an "overrides" block in an item model. + // - The resource location of the model. + @Override + public BakedModel bake(IGeometryBakingContext context, ModelBaker baker, Function spriteGetter, ModelState modelState, ItemOverrides overrides, ResourceLocation modelLocation) { + // See info on the parameters below. + return new MyDynamicModel(context.useAmbientOcclusion(), context.isGui3d(), context.useBlockLight(), + spriteGetter.apply(context.getMaterial("particle")), overrides); + } +} + +public class MyDynamicModel implements IDynamicBakedModel { + // Sprite of the missing texture. Can be used as a fallback when needed. + private static final TextureAtlasSprite MISSING_TEXTURE = + new Material(TextureAtlas.LOCATION_BLOCKS, MissingTextureAtlasSprite.getLocation()).sprite(); + + // Whether to use ambient occlusion when rendering. Provided by the geometry baking context. + private final boolean useAmbientOcclusion; + // Whether to use 3d rendering in a GUI. Provided by the geometry baking context. + private final boolean isGui3d; + // Whether to use block light. Provided by the geometry baking context. + private final boolean usesBlockLight; + // The particle sprite to use when breaking, falling on, or walking over a block. Irrelevant on item models. + private final TextureAtlasSprite particle; + // The item overrides to use. Irrelevant on block models. + private final ItemOverrides overrides; + + // The constructor does not require any parameters other than the ones for instantiating the final fields. + // It may specify any additional parameters to store in fields you deem necessary for your model to work. + public MyDynamicModel(boolean useAmbientOcclusion, boolean isGui3d, boolean usesBlockLight, + TextureAtlasSprite particle, ItemOverrides overrides) { + this.useAmbientOcclusion = useAmbientOcclusion; + this.isGui3d = isGui3d; + this.usesBlockLight = usesBlockLight; + this.particle = particle; + this.overrides = overrides; + } + + // Use our attributes. + @Override + public boolean useAmbientOcclusion() { + return useAmbientOcclusion; + } + + @Override + public boolean isGui3d() { + return isGui3d; + } + + @Override + public boolean usesBlockLight() { + return usesBlockLight; + } + + @Override + public TextureAtlasSprite getParticleIcon() { + // Return MISSING_TEXTURE if you don't need a particle, e.g. when in an item model context. + return particle; + } + + @Override + public ItemOverrides getOverrides() { + // Return ItemOverrides.EMPTY when in a block model context. + return overrides; + } + + // Override this to true if you also want to use a custom renderer instead of the builtin render engine. + @Override + public boolean isCustomRenderer() { + return false; + } + + // This is where the magic happens. Return a list of the quads to render here. Parameters are: + // - The blockstate being rendered. + // - The side being rendered. + // - A client-bound random source you can use for randomizing stuff. + // - The extra face data to use. + // - The render type of the model. + @Override + public List getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) { + List quads = new ArrayList<>(); + // add elements to the quads list as needed here + return quads; + } +} +``` + +### Datagen + +Of course, we can also [datagen] our models. To do so, we need a class that extends `CustomLoaderBuilder`: + +```java +// This assumes a block model. Use ItemModelBuilder as the generic parameter instead +// if you're making a custom item model. +public class MyLoaderBuilder extends CustomLoaderBuilder { + public MyLoaderBuilder(BlockModelBuilder parent, ExistingFileHelper existingFileHelper) { + super( + // Your model loader's id. + MyGeometryLoader.ID, + // The parent builder we use. This is always the first constructor parameter. + parent, + // The existing file helper we use. This is always the second constructor parameter. + existingFileHelper, + // Whether the loader allows inline vanilla elements as a fallback if the loader is absent. + false + ); + } + + // Add fields and setters for the fields here. The fields can then be used below. + + // Serialize the model to JSON. + @Override + public JsonObject toJson(JsonObject json) { + // Add your fields to the given JsonObject. + // Then call super, which adds the loader property and some other things. + return super.toJson(json); + } +} +``` + +To use this loader builder, do the following during block (or item) [model datagen][modeldatagen]: + +```java +// This assumes a BlockStateProvider. Use getBuilder("my_cool_block") directly in an ItemModelProvider. +// The parameter for customLoader() is a BiFunction. The parameters of the BiFunction +// are the result of the getBuilder() call and the provider's ExistingFileHelper. +MyLoaderBuilder loaderBuilder = models().getBuilder("my_cool_block").customLoader(MyLoaderBuilder::new); +``` + +Then, call your field setters on the `loaderBuilder`. + +### Reusing the Default Model Loader + +In some contexts, it makes sense to reuse the vanilla model loader and just building your model logic on top of that instead of outright replacing it. We can do so using a neat trick: In the model loader, we simply remove the `loader` property and send it back to the model deserializer, tricking it into thinking that it is a regular model now. We then pass it to the geometry, bake the model geometry there (like the default geometry handler would) and pass it along to the dynamic model, where we can then use the model's quads in whatever way we want: + +```java +public class MyGeometryLoader implements IGeometryLoader { + public static final MyGeometryLoader INSTANCE = new MyGeometryLoader(); + public static final ResourceLocation ID = new ResourceLocation(...); + + private MyGeometryLoader() {} + + @Override + public MyGeometry read(JsonObject jsonObject, JsonDeserializationContext context) throws JsonParseException { + // Trick the deserializer into thinking this is a normal model by removing the loader field and then passing it back into the deserializer. + jsonObject.remove("loader"); + BlockModel base = context.deserialize(jsonObject, BlockModel.class); + // other stuff here if needed + return new MyGeometry(base); + } +} + +public class MyGeometry implements IUnbakedGeometry { + private final BlockModel base; + + // Store the block model for usage below. + public MyGeometry(BlockModel base) { + this.base = base; + } + + @Override + public BakedModel bake(IGeometryBakingContext context, ModelBaker baker, Function spriteGetter, ModelState modelState, ItemOverrides overrides, ResourceLocation modelLocation) { + BakedModel bakedBase = new ElementsModel(base.getElements()).bake(context, baker, spriteGetter, modelState, overrides, modelLocation); + return new MyDynamicModel(bakedBase, /* other parameters here */); + } + + // Method responsible for correctly resolving parent properties. Unneeded if we don't reuse the default loader properties, but needed if we do, so here we go. + @Override + public void resolveParents(Function modelGetter, IGeometryBakingContext context) { + base.resolveParents(modelGetter); + } +} + +public class MyDynamicModel implements IDynamicBakedModel { + private final BakedModel base; + // other fields here + + public MyDynamicModel(BakedModel base, /* other parameters here */) { + this.base = base; + // set other fields here + } + + // other override methods here + + @Override + public List getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) { + List quads = new ArrayList<>(); + // Add the base model's quads. Can also do something different with the quads here, depending on what you need. + quads.add(base.getQuads(state, side, rand, extraData, renderType)); + // add other elements to the quads list as needed here + return quads; + } + + // Apply the base model's transforms to our model as well. + @Override + public BakedModel applyTransform(ItemDisplayContext transformType, PoseStack poseStack, boolean applyLeftHandTransform) { + return base.applyTransform(transformType, poseStack, applyLeftHandTransform); + } +} +``` + +[datagen]: ../../index.md#data-generation +[event]: ../../../concepts/events.md#registering-an-event-handler +[modeldatagen]: datagen.md +[sides]: ../../../concepts/sides.md From 72defafd97561d232fad5cecb703119ce9b8cc31 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Tue, 19 Mar 2024 21:58:58 +0100 Subject: [PATCH 30/41] custom model loaders: part 2 --- .../rendering/modelextensions/_category_.json | 3 - docs/rendering/modelextensions/visibility.md | 52 ----- docs/rendering/modelloaders/index.md | 27 --- docs/resources/client/models/index.md | 15 +- docs/resources/client/models/modelloaders.md | 193 +++++++++++++++++- 5 files changed, 199 insertions(+), 91 deletions(-) delete mode 100644 docs/rendering/modelextensions/_category_.json delete mode 100644 docs/rendering/modelextensions/visibility.md delete mode 100644 docs/rendering/modelloaders/index.md diff --git a/docs/rendering/modelextensions/_category_.json b/docs/rendering/modelextensions/_category_.json deleted file mode 100644 index 528a320b..00000000 --- a/docs/rendering/modelextensions/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Model Extensions" -} \ No newline at end of file diff --git a/docs/rendering/modelextensions/visibility.md b/docs/rendering/modelextensions/visibility.md deleted file mode 100644 index d9a1f71b..00000000 --- a/docs/rendering/modelextensions/visibility.md +++ /dev/null @@ -1,52 +0,0 @@ -Part Visibility -=============== - -Adding the `visibility` entry at the top level of a model JSON allows control over the visibility of different parts of the model to decide whether they should be baked into the final [`BakedModel`][bakedmodel]. The definition of a "part" is dependent on the model loader loading this model and custom model loaders are free to ignore this entry completely. Out of the model loaders provided by Forge only the [composite model loader][composite] and the [OBJ model loader][obj] make use of this functionality. The visibility entries are specified as `"part name": boolean` entries. - -Example of a composite model with two parts, the second of which will not be baked into the final model, and two child models overriding this visibility to have only the first part and both parts visible respectively: -```js -// mycompositemodel.json -{ - "loader": "forge:composite", - "children": { - "part_one": { - "parent": "mymod:mypartmodel_one" - }, - "part_two": { - "parent": "mymod:mypartmodel_two" - } - }, - "visibility": { - "part_two": false - } -} - -// mycompositechild_one.json -{ - "parent": "mymod:mycompositemodel", - "visibility": { - "part_one": false, - "part_two": true - } -} - -// mycompositechild_two.json -{ - "parent": "mymod:mycompositemodel", - "visibility": { - "part_two": true - } -} -``` - -The visibility of a given part is determined by checking whether the model specifies a visibility for this part and, if not present, recursively checking the model's parent until either an entry is found or there is no further parent to check, in which case it defaults to true. - -This allows setups like the following where multiple models use different parts of a single composite model: - -1. A composite model specifies multiple components -2. Multiple models specify this composite model as their parent -3. These child models individually specify different visibilities for the parts - -[bakedmodel]: ../modelloaders/bakedmodel.md -[composite]: ../modelloaders/index.md/#composite-models -[obj]: ../modelloaders/index.md/#wavefront-obj-models \ No newline at end of file diff --git a/docs/rendering/modelloaders/index.md b/docs/rendering/modelloaders/index.md deleted file mode 100644 index 9856d688..00000000 --- a/docs/rendering/modelloaders/index.md +++ /dev/null @@ -1,27 +0,0 @@ -Custom Model Loaders -==================== - -A "model" is simply a shape. It can be a simple cube, it can be several cubes, it can be a truncated icosidodecahedron, or anything in between. Most models you'll see will be in the vanilla JSON format. Models in other formats are loaded into `IUnbakedGeometry`s by an `IGeometryLoader` at runtime. Forge provides default implementations for WaveFront OBJ files, buckets, composite models, models in different render layers, and a reimplementation of Vanilla's `builtin/generated` item model. Most things do not care about what loaded the model or what format it's in as they are all eventually represented by an `BakedModel` in code. - -:::caution -Specifying a custom model loader through the top-level `loader` entry in a model JSON will cause the `elements` entry to be ignored unless it is consumed by the custom loader. All other vanilla entries will still be loaded and available in the unbaked `BlockModel` representation and may be consumed outside of the custom loader. -::: - -WaveFront OBJ Models --------------------- - -Forge adds a loader for the `.obj` file format. To use these models, the JSON must reference the `forge:obj` loader. This loader accepts any model location that is in a registered namespace and whose path ends in `.obj`. The `.mtl` file should be placed in the same location with the same name as the `.obj` to be used automatically. The `.mtl` file will probably have to be manually edited to change the paths pointing to textures defined within the JSON. Additionally, the V axis for textures may be flipped depending on the external program that created the model (i.e. V = 0 may be the bottom edge, not the top). This may be rectified in the modelling program itself or done in the model JSON like so: - -```js -{ - // Add the following line on the same level as a 'model' declaration - "loader": "forge:obj", - "flip_v": true, - "model": "examplemod:models/block/model.obj", - "textures": { - // Can refer to in .mtl using #texture0 - "texture0": "minecraft:block/dirt", - "particle": "minecraft:block/dirt" - } -} -``` diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index d15a6dc4..49db651f 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -256,9 +256,22 @@ public static void registerItemColorHandlers(RegisterColorHandlersEvent.Item eve Be aware that the `item/generated` model specifies tint indices for its various layers - `layer0` has tint index 0, `layer1` has tint index 1, etc. Also, remember that block items are items, not blocks, and require an item color handler to be colored. +## Registering Additional Models + +Models that are not associated with a block or item in some way, but are still required in other contexts (e.g. [block entity renderers][ber]), can be registered through `ModelEvent.RegisterAdditional`: + +```java +// Client-side mod bus event handler +@SubscribeEvent +public static void registerAdditional(ModelEvent.RegisterAdditional event) { + event.register(new ResourceLocation("examplemod", "block/example_unused_model")); +} +``` + [ao]: https://en.wikipedia.org/wiki/Ambient_occlusion +[ber]: ../../../blockentities/ber.md [bsfile]: #blockstate-files -[custommodelloader]: ../../../rendering/modelloaders/index.md +[custommodelloader]: modelloaders.md [elements]: #elements [event]: ../../../concepts/events.md [eventhandler]: ../../../concepts/events.md#registering-an-event-handler diff --git a/docs/resources/client/models/modelloaders.md b/docs/resources/client/models/modelloaders.md index 0a6a75f2..2e5205ef 100644 --- a/docs/resources/client/models/modelloaders.md +++ b/docs/resources/client/models/modelloaders.md @@ -6,20 +6,173 @@ The entry point for a block model remains the model JSON file. However, you can ## Builtin Model Loaders -Besides the default model loader, NeoForge offers a total of six builtin loaders, each serving a different purpose. +Besides the default model loader, NeoForge offers a total of seven builtin loaders, each serving a different purpose. ### Composite Model -### Dynamic Bucket Model +A composite model can be used to specify different model parts in the parent and only apply some of them in a child. This is best illustrated by an example. Consider the following parent model at `examplemod:example_composite_model`: + +```json5 +{ + "loader": "neoforge:composite", + // Specify model parts. + "children": { + "part_1": { + "parent": "examplemod:some_model_1" + }, + "part_2": { + "parent": "examplemod:some_model_2" + } + }, + "visibility": { + // Disable part 2 by default. + "part_2": false + } +} +``` + +Then, we can disable and enable individual parts in a child model of `examplemod:example_composite_model`: + +```json5 +{ + "parent": "examplemod:example_composite_model", + // Override visibility. If a part is missing, it will use the parent model's visibility value. + "visibility": { + "part_1": false, + "part_2": true + } +} +``` + +To [datagen][modeldatagen] this model, use the custom loader class `CompositeModelBuilder`. + +### Dynamic Fluid Container Model + +The dynamic fluid container model, also called dynamic bucket model after its most common use case, is used for items that represent a fluid container (such as a bucket or a tank) and want to show the fluid within the model. This only works if there is a fixed amount of fluids (e.g. only lava and powder snow) that can be used, use a [block entity renderer][ber] instead if the fluid is arbitrary. + +```json5 +{ + "loader": "neoforge:fluid_container", + // Required. Must be a valid fluid id. + "fluid": "minecraft:water", + // Optional, defaults to false. Whether to flip the model upside down, for gaseous fluids. + "flip_gas": true, + // Optional, defaults to true. Whether to have the cover act as the mask. + "cover_is_mask": false, + // Optional, defaults to true. Whether to apply the fluid's luminosity to the item model. + "apply_fluid_luminosity": false +} +``` + +To [datagen][modeldatagen] this model, use the custom loader class `DynamicFluidContainerModelBuilder`. Be aware that for legacy support reasons, this class also provides a method to set the `apply_tint` property, which is no longer used. ### Elements Model +An elements model consists of block model [elements][elements] and an optional [root transform][transform]. Intended mainly for usage outside regular model rendering, for example within a [BER][ber]. + +```json5 +{ + "loader": "neoforge:elements", + "elements": [...], + "transform": {...} +} +``` + +### Empty Model + +An empty model just renders nothing at all. + +```json5 +{ + "loader": "neoforge:empty" +} +``` + ### Item Layer Model +Item layer models are a variant of the standard `item/generated` model that offer the following additional features: + +- Unlimited amount of layers (instead of the default 5) +- Per-layer [render types][rendertype] + +```json5 +{ + "loader": "neoforge:item_layers", + "textures": { + "layer0": "...", + "layer1": "...", + "layer2": "...", + "layer3": "...", + "layer4": "...", + "layer5": "...", + }, + "render_types": { + // Map render types to layer numbers. For example, layers 0, 2 and 4 will use cutout. + "minecraft:cutout": [0, 2, 4], + "minecraft:cutout_mipped": [1, 3], + "minecraft:translucent": [5] + }, + // other stuff the default loader allows here +} +``` + +To [datagen][modeldatagen] this model, use the custom loader class `ItemLayerModelBuilder`. + ### OBJ Model +The OBJ model loader allows you to use Wavefront `.obj` 3D models in the game, allowing for arbitrary shapes (including triangles, circles, etc.) to be included in a model. The `.obj` model must be placed in the `models` folder (or a subfolder thereof), and a `.mtl` file with the same name must be provided (or set manually), so for example, an OBJ model at `models/block/example.obj` must have a corresponding MTL file at `models/block/example.mtl`. + +```json5 +{ + "loader": "neoforge:obj", + // Required. Reference to the model file. Note that this is relative to the namespace root, not the model folder. + "model": "examplemod:models/example.obj", + // Normally, .mtl files must be put into the same location as the .obj file, with only the file ending differing. + // This will cause the loader to automatically pick them up. However, you can also set the location + // of the .mtl file manually if needed. + "mtl_override": "examplemod:models/example_other_name.mtl", + // These textures can be referenced in the .mtl file as #texture0, #particle, etc. + // This usually requires manual editing of the .mtl file. + "textures": { + "texture0": "minecraft:block/cobblestone", + "particle": "minecraft:block/stone" + }, + // Enable or disable automatic culling of the model. Optional, defaults to true. + "automatic_culling": false, + // Whether to shade the model or not. Optional, defaults to true. + "shade_quads": false, + // Some modeling programs will assume V=0 to be bottom instead of the top. This property flips the Vs upside-down. + // Optional, defaults to false. + "flip_v": true, + // Whether to enable emissivity or not. Optional, defaults to true. + "emissive_ambient": false +} +``` + +To [datagen][modeldatagen] this model, use the custom loader class `ObjModelBuilder`. + ### Separate Transforms Model +A separate transforms model can be used to switch between different models based on the perspective. The perspectives are the same as for the `display` block in a [normal model][model]. This works by specifying a base model (as a fallback) and then specifying per-perspective override models. Note that each of these can be fully-fledged models if you so desire, but it is usually easiest to just refer to another model by using a child model of that model, like so: + +```json5 +{ + "loader": "neoforge:separate_transforms", + // Use the cobblestone model normally. + "base": { + "parent": "minecraft:block/cobblestone" + }, + // Use the stone model only when dropped. + "perspectives": { + "ground": { + "parent": "minecraft:block/stone" + } + } +} +``` + +To [datagen][modeldatagen] this model, use the custom loader class `SeparateTransformsModelBuilder`. + ## Creating Custom Model Loaders To create your own model loader, you need three classes, plus an event handler: @@ -81,13 +234,14 @@ public class MyDynamicModel implements IDynamicBakedModel { private static final TextureAtlasSprite MISSING_TEXTURE = new Material(TextureAtlas.LOCATION_BLOCKS, MissingTextureAtlasSprite.getLocation()).sprite(); - // Whether to use ambient occlusion when rendering. Provided by the geometry baking context. + // Whether to use ambient occlusion when rendering. Provided by the geometry baking context. Irrelevant on item models. private final boolean useAmbientOcclusion; - // Whether to use 3d rendering in a GUI. Provided by the geometry baking context. + // Whether to use 3d rendering in a GUI, or use a flat 2d renderer instead. Provided by the geometry baking context. private final boolean isGui3d; // Whether to use block light. Provided by the geometry baking context. private final boolean usesBlockLight; - // The particle sprite to use when breaking, falling on, or walking over a block. Irrelevant on item models. + // The particle sprite to use when breaking, falling on, or walking over a block, + // or when an item is eaten or broken. private final TextureAtlasSprite particle; // The item overrides to use. Irrelevant on block models. private final ItemOverrides overrides; @@ -131,18 +285,21 @@ public class MyDynamicModel implements IDynamicBakedModel { return overrides; } - // Override this to true if you also want to use a custom renderer instead of the builtin render engine. + // Override this to true if you also want to use a custom block entity renderer instead of the default renderer. + // See the page on block entity renderers for more information. @Override public boolean isCustomRenderer() { return false; } // This is where the magic happens. Return a list of the quads to render here. Parameters are: - // - The blockstate being rendered. - // - The side being rendered. + // - The blockstate being rendered. May be null if rendering an item. + // - The side being culled against. May be null, which means no culling should occur. // - A client-bound random source you can use for randomizing stuff. // - The extra face data to use. // - The render type of the model. + // NOTE: This is called very often, usually several times per block and frame. + // This should be as fast as possible and use caching wherever applicable. @Override public List getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) { List quads = new ArrayList<>(); @@ -152,6 +309,16 @@ public class MyDynamicModel implements IDynamicBakedModel { } ``` +When all is done, don't forget to actually register your loader, otherwise all the work will have been for nothing: + +```java +// Client-side mod bus event handler +@SubscribeEvent +public static void registerGeometryLoaders(ModelEvent.RegisterGeometryLoaders event) { + event.register(MyGeometryLoader.ID, MyGeometryLoader.INSTANCE); +} +``` + ### Datagen Of course, we can also [datagen] our models. To do so, we need a class that extends `CustomLoaderBuilder`: @@ -196,6 +363,10 @@ MyLoaderBuilder loaderBuilder = models().getBuilder("my_cool_block").customLoade Then, call your field setters on the `loaderBuilder`. +#### Visibility + +The default implementation of `CustomLoaderBuilder` holds methods for applying visibility. You may choose to use or ignore the `visibility` property in your model loader. Currently, only the [composite model loader][composite] makes use of this property. + ### Reusing the Default Model Loader In some contexts, it makes sense to reuse the vanilla model loader and just building your model logic on top of that instead of outright replacing it. We can do so using a neat trick: In the model loader, we simply remove the `loader` property and send it back to the model deserializer, tricking it into thinking that it is a regular model now. We then pass it to the geometry, bake the model geometry there (like the default geometry handler would) and pass it along to the dynamic model, where we can then use the model's quads in whatever way we want: @@ -266,7 +437,13 @@ public class MyDynamicModel implements IDynamicBakedModel { } ``` +[ber]: ../../../blockentities/ber.md +[composite]: #composite-model [datagen]: ../../index.md#data-generation +[elements]: index.md#elements [event]: ../../../concepts/events.md#registering-an-event-handler +[model]: index.md#specification [modeldatagen]: datagen.md +[rendertype]: index.md#render-types [sides]: ../../../concepts/sides.md +[transform]: index.md#root-transforms From 28fe6e1f430e99c5f85cc7c6a911420df9906d57 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 20 Mar 2024 18:19:58 +0100 Subject: [PATCH 31/41] baked models --- docs/rendering/_category_.json | 3 - docs/rendering/modelloaders/bakedmodel.md | 58 --------- docs/rendering/modelloaders/itemoverrides.md | 49 -------- docs/rendering/modelloaders/transform.md | 37 ------ docs/resources/client/models/bakedmodel.md | 117 +++++++++++++++++++ docs/resources/client/models/datagen.md | 3 +- docs/resources/client/models/index.md | 3 +- docs/resources/client/models/modelloaders.md | 21 ++-- 8 files changed, 129 insertions(+), 162 deletions(-) delete mode 100644 docs/rendering/_category_.json delete mode 100644 docs/rendering/modelloaders/bakedmodel.md delete mode 100644 docs/rendering/modelloaders/itemoverrides.md delete mode 100644 docs/rendering/modelloaders/transform.md create mode 100644 docs/resources/client/models/bakedmodel.md diff --git a/docs/rendering/_category_.json b/docs/rendering/_category_.json deleted file mode 100644 index 461293cc..00000000 --- a/docs/rendering/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Rendering" -} \ No newline at end of file diff --git a/docs/rendering/modelloaders/bakedmodel.md b/docs/rendering/modelloaders/bakedmodel.md deleted file mode 100644 index 91be720f..00000000 --- a/docs/rendering/modelloaders/bakedmodel.md +++ /dev/null @@ -1,58 +0,0 @@ -`BakedModel` -============= - -`BakedModel` is the result of calling `UnbakedModel#bake` for the vanilla model loader or `IUnbakedGeometry#bake` for custom model loaders. Unlike `UnbakedModel` or `IUnbakedGeometry`, which purely represents a shape without any concept of items or blocks, `BakedModel` is not as abstract. It represents geometry that has been optimized and reduced to a form where it is (almost) ready to go to the GPU. It can also process the state of an item or block to change the model. - -In a majority of cases, it is not really necessary to implement this interface manually. One can instead use one of the existing implementations. - -### `getOverrides` - -Returns the [`ItemOverrides`][overrides] to use for this model. This is only used if this model is being rendered as an item. - -### `useAmbientOcclusion` - -If the model is rendered as a block in the level, the block in question does not emit any light, and ambient occlusion is enabled. This causes the model to be rendered with [ambient occlusion](ambocc). - -### `isGui3d` - -If the model is rendered as an item in an inventory, on the ground as an entity, on an item frame, etc., this makes the model look "flat." In GUIs, this also disables the lighting. - -### `isCustomRenderer` - -:::caution -Unless you know what you're doing, just `return false` from this and continue on. -::: - -When rendering this as an item, returning `true` causes the model to not be rendered, instead falling back to `BlockEntityWithoutLevelRenderer#renderByItem`. For certain vanilla items such as chests and banners, this method is hardcoded to copy data from the item into a `BlockEntity`, before using a `BlockEntityRenderer` to render that BE in place of the item. For all other items, it will use the `BlockEntityWithoutLevelRenderer` instance provided by `IClientItemExtensions#getCustomRenderer`. Refer to [BlockEntityWithoutLevelRenderer][bewlr] page for more information. - -### `getParticleIcon` - -Whatever texture should be used for the particles. For blocks, this shows when an entity falls on it, when it breaks, etc. For items, this shows when it breaks or when it's eaten. - -!!! important - The vanilla method with no parameters has been deprecated in favor of `#getParticleIcon(ModelData)` since model data can have an effect on how a particular model might be rendered. - -### `getTransforms` - -Deprecated in favor of implementing `#applyTransform`. The default implementation is fine if `#applyTransform` is implemented. See [Transform][transform]. - -### `applyTransform` - -See [Transform][transform]. - -### `getQuads` - -This is the main method of `BakedModel`. It returns a list of `BakedQuad`s: objects which contain the low-level vertex data that will be used to render the model. If the model is being rendered as a block, then the `BlockState` passed in is non-null. If the model is being rendered as an item, the `ItemOverrides` returned from `#getOverrides` is responsible for handling the state of the item, and the `BlockState` parameter will be `null`. - -The `Direction` passed in is used for face culling. If the block against the given side of another block being rendered is opaque, then the faces associated with that side are not rendered. If that parameter is `null`, all faces not associated with a side are returned (that will never be culled). - -The `rand` parameter is an instance of Random. - -It also takes in a non null `ModelData` instance. This can be used to define extra data when rendering the specific model via `ModelProperty`s. For example, one such property is `CompositeModel$Data`, which is used to store any additional submodel data for a model using the `forge:composite` model loader. - -Note that this method is called very often: once for every combination of non-culled face and supported block render layer (anywhere between 0 to 28 times) *per block in a level*. This method should be as fast as possible, and should probably cache heavily. - -[overrides]: ./itemoverrides.md -[ambocc]: https://en.wikipedia.org/wiki/Ambient_occlusion -[bewlr]: ../../items/bewlr.md -[transform]: ./transform.md diff --git a/docs/rendering/modelloaders/itemoverrides.md b/docs/rendering/modelloaders/itemoverrides.md deleted file mode 100644 index 63ec2d88..00000000 --- a/docs/rendering/modelloaders/itemoverrides.md +++ /dev/null @@ -1,49 +0,0 @@ -`ItemOverrides` -================== - -`ItemOverrides` provides a way for an [`BakedModel`][baked] to process the state of an `ItemStack` and return a new `BakedModel`; thereafter, the returned model replaces the old one. `ItemOverrides` represents an arbitrary function `(BakedModel, ItemStack, ClientLevel, LivingEntity, int)` → `BakedModel`, making it useful for dynamic models. In vanilla, it is used to implement item property overrides. - -### `ItemOverrides()` - -Given a list of `ItemOverride`s, the constructor copies and bakes the list. The baked overrides may be accessed with `#getOverrides`. - -### `resolve` - -This takes an `BakedModel`, an `ItemStack`, a `ClientLevel`, a `LivingEntity`, and an `int` to produce another `BakedModel` to use for rendering. This is where models can handle the state of their items. - -This should not mutate the level. - -### `getOverrides` - -Returns an immutable list containing all the [`BakedOverride`][override]s used by this `ItemOverrides`. If none are applicable, this returns the empty list. - -## `BakedOverride` - -This class represents a vanilla item override, which holds several `ItemOverrides$PropertyMatcher` for the properties on an item and a model to use in case those matchers are satisfied. They are the objects in the `overrides` array of a vanilla item JSON model: - -```js -{ - // Inside a vanilla JSON item model - "overrides": [ - { - // This is an ItemOverride - "predicate": { - // This is the Map, containing the names of properties and their minimum values - "example1:prop": 0.5 - }, - // This is the 'location', or target model, of the override, which is used if the predicate above matches - "model": "example1:item/model" - }, - { - // This is another ItemOverride - "predicate": { - "example2:prop": 1 - }, - "model": "example2:item/model" - } - ] -} -``` - -[baked]: ./bakedmodel.md -[override]: #bakedoverride diff --git a/docs/rendering/modelloaders/transform.md b/docs/rendering/modelloaders/transform.md deleted file mode 100644 index 58a55a5f..00000000 --- a/docs/rendering/modelloaders/transform.md +++ /dev/null @@ -1,37 +0,0 @@ -Transform -========== - -When an [`BakedModel`][bakedmodel] is being rendered as an item, it can apply special handling depending on which transform it is being rendered in. "Transform" means in what context the model is being rendered. The possible transforms are represented in code by the `ItemDisplayContext` enum. There are two systems for handling transform: the deprecated vanilla system, constituted by `BakedModel#getTransforms`, `ItemTransforms`, and `ItemTransform`, and the Forge system, embodied by the method `IForgeBakedModel#applyTransform`. The vanilla code is patched to favor using `applyTransform` over the vanilla system whenever possible. - -`ItemDisplayContext` ---------------- - -`NONE` - Used for the display entity by default when no context is set and by Forge when a `Block`'s `RenderShape` is set to `#ENTITYBLOCK_ANIMATED`. - -`THIRD_PERSON_LEFT_HAND`/`THIRD_PERSON_RIGHT_HAND`/`FIRST_PERSON_LEFT_HAND`/`FIRST_PERSON_RIGHT_HAND` - The first person values represent when the player is holding the item in their own hand. The third person values represent when another player is holding the item and the client is looking at them in the 3rd person. Hands are self-explanatory. - -`HEAD` - Represents when any player is wearing the item in the helmet slot (e.g. pumpkins). - -`GUI` - Represents when the item is being rendered in a `Screen`. - -`GROUND` - Represents when the item is being rendered in the level as an `ItemEntity`. - -`FIXED` - Used for item frames. - -The Vanilla Way ---------------- - -The vanilla way of handling transform is through `BakedModel#getTransforms`. This method returns an `ItemTransforms`, which is a simple object that contains various `ItemTransform`s as `public final` fields. An `ItemTransform` represents a rotation, a translation, and a scale to be applied to the model. The `ItemTransforms` is a container for these, holding one for each of the `ItemDisplayContext`s except `NONE`. In the vanilla implementation, calling `#getTransform` for `NONE` results in the default transform, `ItemTransform#NO_TRANSFORM`. - -The entire vanilla system for handling transforms is deprecated by Forge, and most implementations of `BakedModel` should simply `return ItemTransforms#NO_TRANSFORMS` (which is the default implementation) from `BakedModel#getTransforms`. Instead, they should implement `#applyTransform`. - -The Forge Way -------------- - -The Forge way of handling transforms is `#applyTransform`, a method patched into `BakedModel`. It supersedes the `#getTransforms` method. - -#### `BakedModel#applyTransform` - -Given a `ItemDisplayContext`, `PoseStack`, and a boolean to determine whether to apply the transform for the left hand, this method produces an `BakedModel` to be rendered. Because the returned `BakedModel` can be a totally new model, this method is more flexible than the vanilla method (e.g. a piece of paper that looks flat in hand but crumpled on the ground). - -[bakedmodel]: ./bakedmodel.md diff --git a/docs/resources/client/models/bakedmodel.md b/docs/resources/client/models/bakedmodel.md new file mode 100644 index 00000000..d0c8bb8a --- /dev/null +++ b/docs/resources/client/models/bakedmodel.md @@ -0,0 +1,117 @@ +# Baked Models + +`BakedModel`s are the in-code representation of a shape with textures. They can be obtained from multiple sources, for example by calling `UnbakedModel#bake` (default model loader) or `IUnbakedGeometry#bake` ([custom model loaders][modelloader]). Some [block entity renderers][ber] also make use of baked models. There is no limit to how complex a model may be. + +## Methods of `BakedModel` + +### `getQuads` + +The most important method of a baked model is `getQuads`. This method is responsible for returning a list of `BakedQuad`s, which can then be sent to the GPU. A quad compares to a triangle in a modeling program (and in most other games), however due to Minecraft's general focus on squares, the developers elected to use quads (4 vertices) instead of triangles (3 vertices) for rendering in Minecraft. `getQuads` has five parameters that can be used: + +- A `BlockState`: The [blockstate] being rendered. May be null, indicating that an item is being rendered. +- A `Direction`: The direction of the face being culled against. May be null, indicating that no culling should occur. Will always be null for items. +- A `RandomSource`: A client-bound random source you can use for randomization. +- A `ModelData`: The extra model data to use. This may contain additional data from the block entity needed for rendering. +- A `RenderType`: The [render type][rendertype] to use for rendering the block. May be null, indicating that the default render type should be used. + +Be aware that this method is called very often (several times per model and frame), and as such should cache as much as possible. + +### `applyTransform` and `getTransforms` + +`applyTransform` allows for applying custom logic when applying perspective transformations to the model, including returning a completely separate model. This method is added by NeoForge as a replacement for the vanilla `getTransforms()` method, which only allows you to customize the transforms themselves, but not the way they are applied. However, `applyTransform`'s default implementation defers to `getTransforms`, so if you only need custom transforms, you can also override `getTransforms` and be done with it. `applyTransforms` offers three parameters: + +- An `ItemDisplayContext`: The [perspective] the model is being transformed to. +- A `PoseStack`: The pose stack used for rendering. +- A `boolean`: Whether to use modified values for left-hand rendering instead of the default right hand rendering; `true` if the rendered hand is the left hand (off hand, or main hand if left hand mode is enabled in the options) + +### Others + +Other methods in `BakedModel` that you may override and/or query include: + +| Signature | Effect | +|-------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `boolean useAmbientOcclusion()` | Whether to use [ambient occlusion][ao] or not. Has an override that accepts a `BlockState` parameter, and an override that accepts a `BlockState` and a `RenderType` parameter. | +| `boolean isGui3d()` | Whether to use a 3d renderer for rendering in GUIs or not. Also affects GUI lighting. | +| `boolean usesBlockLight()` | Whether to use block light when lighting the model or not. | +| `boolean isCustomRenderer()` | If true, skips normal rendering and calls an associated [block entity renderer][ber]'s `renderByItem` method instead. If false, renders through the default renderer. | +| `ItemOverrides getOverrides()` | Returns the [`ItemOverrides`][itemoverrides] associated with this model. This is generally only relevant on item models. | +| `ModelData getModelData(BlockAndTintGetter, BlockPos, BlockState, ModelData)` | Returns the model data to use for the model. Mainly used for [block entity renderers][ber]. | +| `TextureAtlasSprite getParticleIcon(ModelData)` | Returns the particle sprite to use for the model. May use the model data to use different particle sprites for different model data values. NeoForge-added, replacing the vanilla overload with no parameters. | + +## Perspectives + +Minecraft's render engine recognizes a total of 8 perspective types. These are used in a model JSON's `display` block, and represented in code through the `ItemDisplayContext` enum. + +| Enum value | JSON key | Usage | +|---------------------------|---------------------------|------------------------------------------------------------------------------------------------------------------| +| `NONE` | `"none"` | Fallback purposes in code, should not be used in JSON | +| `THIRD_PERSON_RIGHT_HAND` | `"thirdperson_righthand"` | Right hand in third person (F5 view, or on other players) | +| `THIRD_PERSON_LEFT_HAND` | `"thirdperson_lefthand"` | Left hand in third person (F5 view, or on other players) | +| `FIRST_PERSON_RIGHT_HAND` | `"firstperson_righthand"` | Right hand in first person | +| `FIRST_PERSON_LEFT_HAND` | `"firstperson_lefthand"` | Left hand in first person | +| `HEAD` | `"head"` | When in a player's head armor slot (often only achievable via commands) | +| `GUI` | `"gui"` | Inventories, player hotbar | +| `GROUND` | `"ground"` | Dropped items; note that the rotation of the dropped item is handled by the dropped item renderer, not the model | +| `FIXED` | `"fixed"` | Item frames | + +## `ItemOverrides` + +`ItemOverrides` is a class that provides a way for baked models to process the state of an [`ItemStack`][itemstack] and return a new baked model through the `#resolve` method. `#resolve` has five parameters: + +- A `BakedModel`: The original model. +- An `ItemStack`: The affected item stack. +- A `ClientLevel`: The level the model is being rendered in. This should only be used for querying the level, not mutating it in any way. May be null. +- A `LivingEntity`: The entity the model is rendered on. May be null, e.g. when rendering from a [block entity renderer][ber]. +- An `int`: A seed for randomizing. + +`ItemOverrides` also hold the model's override options as `BakedOverride`s. An object of `BakedOverride` is an in-code representation of a model's [`overrides`][overrides] block. It can be used by baked models to return different models depending on its contents. A list of all `BakedOverride`s of an `ItemOverrides` instance can be retrieved through `ItemOverrides#getOverrides()`. + +## `BakedModelWrapper` + +A `BakedModelWrapper` can be used to modify an already existing `BakedModel`. `BakedModelWrapper` is a subclass of `BakedModel` that accepts another `BakedModel` (the "original" model) in the constructor and by default redirects all methods to the original model. Your implementation can then override only select methods, like so: + +```java +// The generic parameter may optionally be a more specific subclass of BakedModel. +// If it is, the constructor parameter must match that type. +public class MyBakedModelWrapper extends BakedModelWrapper { + // Pass the original model to super. + public MyBakedModelWrapper(BakedModel originalModel) { + super(originalModel); + } + + // Override whatever methods you want here. You may also access originalModel if needed. +} +``` + +After writing your model wrapper class, you must apply the wrappers to the models it should affect. Do so in a [client-side][sides] [event handler][event] for `ModelEvent.ModifyBakingResult`: + +```java +@SubscribeEvent +public static void modifyBakingResult(ModelEvent.ModifyBakingResult event) { + // For block models + event.getModels().computeIfPresent( + // The model resource location of the model to modify. Get it from + // BlockModelShaper#stateToModelLocation with the blockstate to be affected as a parameter. + BlockModelShaper.stateToModelLocation(MyBlocksClass.EXAMPLE_BLOCK.defaultBlockState()), + // A BiFunction with the location and the original models as parameters, returning the new model. + (location, model) -> new MyBakedModelWrapper(model); + ); + // For item models + event.getModels().computeIfPresent( + // The model resource location of the model to modify. + new ModelResourceLocation("examplemod", "example_item", "inventory"), + // A BiFunction with the location and the original models as parameters, returning the new model. + (location, model) -> new MyBakedModelWrapper(model); + ); +} +``` + +[ao]: https://en.wikipedia.org/wiki/Ambient_occlusion +[ber]: ../../../blockentities/ber.md +[blockstate]: ../../../blocks/states.md +[itemoverrides]: #itemoverrides +[itemstack]: ../../../items/index.md#itemstacks +[modelloader]: modelloaders.md +[overrides]: index.md#overrides +[perspective]: #perspectives +[rendertype]: index.md#render-types diff --git a/docs/resources/client/models/datagen.md b/docs/resources/client/models/datagen.md index c0011e06..f64b56b6 100644 --- a/docs/resources/client/models/datagen.md +++ b/docs/resources/client/models/datagen.md @@ -12,7 +12,7 @@ Every model starts out as a `ModelBuilder` of some sort - usually a `BlockModelB |--------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `#texture(String key, ResourceLocation texture)` | Adds a texture variable with the given key and the given texture location. Has an overload where the second parameter is a `String`. | | `#renderType(ResourceLocation renderType)` | Sets the render type. Has an overload where the parameter is a `String`. | -| `#ao(boolean ao)` | Sets whether to use ambient occlusion or not. | +| `#ao(boolean ao)` | Sets whether to use [ambient occlusion][ao] or not. | | `#guiLight(GuiLight light)` | Sets the GUI light. May be `GuiLight.FRONT` or `GuiLight.SIDE`. | | `#element()` | Adds a new `ElementBuilder` (equivalent to adding a new [element][elements] to the model). Returns said `ElementBuilder` for further modification. | | `#transforms()` | Returns the builder's `TransformVecBuilder`, used for setting the `display` on a model. | @@ -275,6 +275,7 @@ public static void gatherData(GatherDataEvent event) { } ``` +[ao]: https://en.wikipedia.org/wiki/Ambient_occlusion [blockbench]: https://www.blockbench.net [custommodelloader]: modelloaders.md#datagen [datagen]: ../../index.md#data-generation diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index 49db651f..36a4f070 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -29,7 +29,7 @@ A model is a JSON file with the following optional properties in the root tag: - Item models can also use layer textures, named `layer0`, `layer1`, etc., where layers with a higher index are rendered above those with a lower index (e.g. `layer1` would be rendered above `layer0`). Only works if the parent is `item/generated`, and only works for up to 5 layers (`layer0` through `layer4`). - `elements`: A list of cuboid [elements]. - `overrides`: A list of [override models][overrides]. Only effective on item models. -- `display`: A sub-object that holds the different display options for different perspectives. Only effective on item models, but often specified in block models so that item models can inherit the display options. Possible perspectives include `thirdperson_righthand`, `thirdperson_lefthand`, `firstperson_righthand`, `firstperson_lefthand`, `gui`, `head`, `ground`, and `fixed` (for item frames and similar modded display blocks). Every perspective is an optional sub-object that may contain the following options: +- `display`: A sub-object that holds the different display options for different [perspectives], see linked article for possible keys. Only effective on item models, but often specified in block models so that item models can inherit the display options. Every perspective is an optional sub-object that may contain the following options: - `rotation`: The rotation of the model, specified as `[x, y, z]`. Rotations are handled after translations and before scaling. - `translation`: The translation of the model, specified as `[x, y, z]`. Translations are handled before rotations and scaling. - `scale`: The scale of the model, specified as `[x, y, z]`. Scaling is handled after translations and rotations. @@ -282,6 +282,7 @@ public static void registerAdditional(ModelEvent.RegisterAdditional event) { [mipmapping]: https://en.wikipedia.org/wiki/Mipmap [modbus]: ../../../concepts/events.md#event-buses [overrides]: #overrides +[perspectives]: bakedmodel.md#perspectives [rendertype]: #render-types [roottransforms]: #root-transforms [rl]: ../../../misc/resourcelocation.md diff --git a/docs/resources/client/models/modelloaders.md b/docs/resources/client/models/modelloaders.md index 2e5205ef..15aef8b1 100644 --- a/docs/resources/client/models/modelloaders.md +++ b/docs/resources/client/models/modelloaders.md @@ -179,16 +179,16 @@ To create your own model loader, you need three classes, plus an event handler: - A geometry loader class - A geometry class -- A dynamic model class +- A dynamic [baked model][bakedmodel] class - A [client-side][sides] [event handler][event] for `ModelEvent.RegisterGeometryLoaders` that registers the geometry loader To illustrate how these classes are connected, we will follow a model being loaded: - During model loading, a model JSON with the `loader` property set to your loader is passed to your geometry loader. The geometry loader then reads the model JSON and returns a geometry object using the model JSON's properties. -- During model baking, the geometry is baked, returning a dynamic model. -- During model rendering, the dynamic model is used for rendering. +- During model baking, the geometry is baked, returning a dynamic baked model. +- During model rendering, the dynamic baked model is used for rendering. -Let's illustrate this further through a basic class setup. The geometry loader class is named `MyGeometryLoader`, the geometry class is named `MyGeometry`, and the dynamic model class is named `MyDynamicModel`: +Let's illustrate this further through a basic class setup. The geometry loader class is named `MyGeometryLoader`, the geometry class is named `MyGeometry`, and the dynamic baked model class is named `MyDynamicModel`: ```java public class MyGeometryLoader implements IGeometryLoader { @@ -234,22 +234,16 @@ public class MyDynamicModel implements IDynamicBakedModel { private static final TextureAtlasSprite MISSING_TEXTURE = new Material(TextureAtlas.LOCATION_BLOCKS, MissingTextureAtlasSprite.getLocation()).sprite(); - // Whether to use ambient occlusion when rendering. Provided by the geometry baking context. Irrelevant on item models. + // Attributes for use in the methods below. Optional, the methods may also use constant values if applicable. private final boolean useAmbientOcclusion; - // Whether to use 3d rendering in a GUI, or use a flat 2d renderer instead. Provided by the geometry baking context. private final boolean isGui3d; - // Whether to use block light. Provided by the geometry baking context. private final boolean usesBlockLight; - // The particle sprite to use when breaking, falling on, or walking over a block, - // or when an item is eaten or broken. private final TextureAtlasSprite particle; - // The item overrides to use. Irrelevant on block models. private final ItemOverrides overrides; // The constructor does not require any parameters other than the ones for instantiating the final fields. // It may specify any additional parameters to store in fields you deem necessary for your model to work. - public MyDynamicModel(boolean useAmbientOcclusion, boolean isGui3d, boolean usesBlockLight, - TextureAtlasSprite particle, ItemOverrides overrides) { + public MyDynamicModel(boolean useAmbientOcclusion, boolean isGui3d, boolean usesBlockLight, TextureAtlasSprite particle, ItemOverrides overrides) { this.useAmbientOcclusion = useAmbientOcclusion; this.isGui3d = isGui3d; this.usesBlockLight = usesBlockLight; @@ -257,7 +251,7 @@ public class MyDynamicModel implements IDynamicBakedModel { this.overrides = overrides; } - // Use our attributes. + // Use our attributes. Refer to the article on baked models for more information on the method's effects. @Override public boolean useAmbientOcclusion() { return useAmbientOcclusion; @@ -437,6 +431,7 @@ public class MyDynamicModel implements IDynamicBakedModel { } ``` +[bakedmodel]: bakedmodel.md [ber]: ../../../blockentities/ber.md [composite]: #composite-model [datagen]: ../../index.md#data-generation From 01123e282c3ce965d03ad6b8bc29e8ad20c07720 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 21 Mar 2024 00:52:37 +0100 Subject: [PATCH 32/41] sounds article, and some restructuring --- docs/blocks/index.md | 2 +- docs/datagen/_category_.json | 3 + docs/datagen/{server => }/advancements.md | 0 docs/datagen/client/_category_.json | 3 - docs/datagen/client/sounds.md | 84 ------ docs/datagen/{server => }/glm.md | 0 docs/datagen/{server => }/loottables.md | 0 docs/datagen/{server => }/recipes.md | 0 docs/datagen/server/_category_.json | 3 - docs/datagen/{server => }/tags.md | 0 docs/gameeffects/sounds.md | 111 -------- docs/resources/client/sounds.md | 308 ++++++++++++++++++++++ docs/resources/index.md | 2 +- 13 files changed, 313 insertions(+), 203 deletions(-) create mode 100644 docs/datagen/_category_.json rename docs/datagen/{server => }/advancements.md (100%) delete mode 100644 docs/datagen/client/_category_.json delete mode 100644 docs/datagen/client/sounds.md rename docs/datagen/{server => }/glm.md (100%) rename docs/datagen/{server => }/loottables.md (100%) rename docs/datagen/{server => }/recipes.md (100%) delete mode 100644 docs/datagen/server/_category_.json rename docs/datagen/{server => }/tags.md (100%) delete mode 100644 docs/gameeffects/sounds.md create mode 100644 docs/resources/client/sounds.md diff --git a/docs/blocks/index.md b/docs/blocks/index.md index f92c8c92..b8892507 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -235,6 +235,6 @@ Random ticking is used by a wide range of mechanics in Minecraft, such as plant [randomtick]: #random-ticking [registration]: ../concepts/registries.md#methods-for-registering [resources]: ../resources/index.md#assets -[sounds]: ../gameeffects/sounds.md +[sounds]: ../resources/client/sounds.md [usingblocks]: #using-blocks [usingblockstates]: states.md#using-blockstates diff --git a/docs/datagen/_category_.json b/docs/datagen/_category_.json new file mode 100644 index 00000000..e3a3b926 --- /dev/null +++ b/docs/datagen/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Datagen" +} \ No newline at end of file diff --git a/docs/datagen/server/advancements.md b/docs/datagen/advancements.md similarity index 100% rename from docs/datagen/server/advancements.md rename to docs/datagen/advancements.md diff --git a/docs/datagen/client/_category_.json b/docs/datagen/client/_category_.json deleted file mode 100644 index c1b8c6d9..00000000 --- a/docs/datagen/client/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Client" -} \ No newline at end of file diff --git a/docs/datagen/client/sounds.md b/docs/datagen/client/sounds.md deleted file mode 100644 index 6f0ec913..00000000 --- a/docs/datagen/client/sounds.md +++ /dev/null @@ -1,84 +0,0 @@ -Sound Definition Generation -=========================== - -The `sounds.json` file can be generated for a mod by subclassing `SoundDefinitionsProvider` and implementing `#registerSounds`. After implementation, the provider must be [added][datagen] to the `DataGenerator`. - -```java -// On the MOD event bus -@SubscribeEvent -public void gatherData(GatherDataEvent event) { - event.getGenerator().addProvider( - // Tell generator to run only when client assets are generating - event.includeClient(), - output -> new MySoundDefinitionsProvider(output, MOD_ID, event.getExistingFileHelper()) - ); -} -``` - -Adding a Sound --------------- - -A sound definition can be generated by specifying the sound name and definition via `#add`. The sound name can either be provided from a [`SoundEvent`][soundevent], `ResourceLocation`, or string. - -:::caution -The sound name supplied will always assume the namespace is the mod id supplied to the constructor of the provider. There is no validation performed on the namespace of the sound name! -::: - -### `SoundDefinition` - -The `SoundDefinition` can be created using `#definition`. The definition contains the data to define a sound instance. - -A definition specifies a few methods: - -Method | Description -:---: | :--- -`with` | Adds a sound(s) which may be played when the definition is selected. -`subtitle` | Sets the translation key of the definition. -`replace` | When `true`, removes the sounds already defined by other `sounds.json` for this definition instead of appending to it. - -### `SoundDefinition$Sound` - -A sound supplied to the `SoundDefinition` can be specified using `SoundDefinitionsProvider#sound`. These methods take in the reference of the sound and a `SoundType` if specified. - -The `SoundType` can be one of two values: - -Sound Type | Definition -:---: | :--- -`SOUND` | Specifies a reference to the sound located at `assets//sounds/.ogg`. -`EVENT` | Specifies a reference to the name of another sound defined by the `sounds.json`. - -Each `Sound` created from `SoundDefinitionsProvider#sound` can specify additional configurations on how to load and play the sound provided: - -Method | Description -:---: | :--- -`volume` | Sets the volume scale of the sound, must be greater than `0`. -`pitch` | Sets the pitch scale of the sound, must be greater than `0`. -`weight` | Sets the likelihood of the sound getting played when the sound is selected. -`stream` | When `true`, reads the sound from file instead of loading the sound into memory. Recommended for long sounds: background music, music discs, etc. -`attenuationDistance` | Sets the number of blocks the sound can be heard from. -`preload` | When `true`, immediately loads the sound into memory as soon as the resource pack is loaded. - -```java -// In some SoundDefinitionsProvider#registerSounds -this.add(EXAMPLE_SOUND_EVENT, definition() - .subtitle("sound.examplemod.example_sound") // Set translation key - .with( - sound(new ResourceLocation(MODID, "example_sound_1")) // Set first sound - .weight(4) // Has a 4 / 5 = 80% chance of playing - .volume(0.5), // Scales all volumes called on this sound by half - sound(new ResourceLocation(MODID, "example_sound_2")) // Set second sound - .stream() // Streams the sound - ) -); - -this.add(EXAMPLE_SOUND_EVENT_2, definition() - .subtitle("sound.examplemod.example_sound") // Set translation key - .with( - sound(EXAMPLE_SOUND_EVENT.getLocation(), SoundType.EVENT) // Adds sounds from 'EXAMPLE_SOUND_EVENT' - .pitch(0.5) // Scales all pitches called on this sound by half - ) -); -``` - -[datagen]: ../index.md#data-providers -[soundevent]: ../../gameeffects/sounds.md#creating-sound-events diff --git a/docs/datagen/server/glm.md b/docs/datagen/glm.md similarity index 100% rename from docs/datagen/server/glm.md rename to docs/datagen/glm.md diff --git a/docs/datagen/server/loottables.md b/docs/datagen/loottables.md similarity index 100% rename from docs/datagen/server/loottables.md rename to docs/datagen/loottables.md diff --git a/docs/datagen/server/recipes.md b/docs/datagen/recipes.md similarity index 100% rename from docs/datagen/server/recipes.md rename to docs/datagen/recipes.md diff --git a/docs/datagen/server/_category_.json b/docs/datagen/server/_category_.json deleted file mode 100644 index 8de78b39..00000000 --- a/docs/datagen/server/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Server" -} \ No newline at end of file diff --git a/docs/datagen/server/tags.md b/docs/datagen/tags.md similarity index 100% rename from docs/datagen/server/tags.md rename to docs/datagen/tags.md diff --git a/docs/gameeffects/sounds.md b/docs/gameeffects/sounds.md deleted file mode 100644 index c38bac81..00000000 --- a/docs/gameeffects/sounds.md +++ /dev/null @@ -1,111 +0,0 @@ -Sounds -====== - -Terminology ------------ - -| Term | Description | -|----------------|----------------| -| Sound Events | Something that triggers a sound effect. Examples include `minecraft:block.anvil.hit` or `botania:spreader_fire`. | -| Sound Category | The category of the sound, for example `player`, `block` or simply `master`. The sliders in the sound settings GUI represent these categories. | -| Sound File | The literal file on disk that is played: an .ogg file. | - -`sounds.json` -------------- - -This JSON defines sound events, and defines which sound files they play, the subtitle, etc. Sound events are identified with [`ResourceLocation`][loc]s. `sounds.json` should be located at the root of a resource namespace (`assets//sounds.json`), and it defines sound events in that namespace (`assets//sounds.json` defines sound events in the namespace `namespace`.). - -A full specification is available on the vanilla [wiki][], but this example highlights the important parts: - -```js -{ - "open_chest": { - "subtitle": "mymod.subtitle.open_chest", - "sounds": [ "mymod:open_chest_sound_file" ] - }, - "epic_music": { - "sounds": [ - { - "name": "mymod:music/epic_music", - "stream": true - } - ] - } -} -``` - -Underneath the top-level object, each key corresponds to a sound event. Note that the namespace is not given, as it is taken from the namespace of the JSON itself. Each event specifies a localization key to be shown when subtitles are enabled. Finally, the actual sound files to be played are specified. Note that the value is an array; if multiple sound files are specified, the game will randomly choose one to play whenever the sound event is triggered. - -The two examples represent two different ways to specify a sound file. The [wiki] has precise details, but generally, long sound files such as background music or music discs should use the second form, because the "stream" argument tells Minecraft to not load the entire sound file into memory but to stream it from disk. The second form can also specify the volume, pitch, and weight of a sound file. - -In all cases, the path to a sound file for namespace `namespace` and path `path` is `assets//sounds/.ogg`. Therefore `mymod:open_chest_sound_file` points to `assets/mymod/sounds/open_chest_sound_file.ogg`, and `mymod:music/epic_music` points to `assets/mymod/sounds/music/epic_music.ogg`. - -A `sounds.json` can be [data generated][datagen]. - -Creating Sound Events ---------------------- - -In order to reference sounds on the server, a `SoundEvent` holding a corresponding entry in `sounds.json` must be created. This `SoundEvent` must then be [registered][registration]. Normally, the location used to create a sound event should be set as it's registry name. - -The `SoundEvent` acts as a reference to the sound and is passed around to play them. If a mod has an API, it should expose its `SoundEvent`s in the API. - -:::note -As long as a sound is registered within the `sounds.json`, it can still be referenced on the logical client regardless of whether there is a referencing `SoundEvent`. -::: - -Playing Sounds --------------- - -Vanilla has lots of methods for playing sounds, and it is unclear which to use at times. - -Note that each takes a `SoundEvent`, the ones registered above. Additionally, the terms *"Server Behavior"* and *"Client Behavior"* refer to the respective [**logical** side][sides]. - -### `Level` - -1. `playSound(Player, BlockPos, SoundEvent, SoundSource, volume, pitch)` - - Simply forwards to [overload (2)](#level-playsound-pxyzecvp), adding 0.5 to each coordinate of the `BlockPos` given. - -2. `playSound(Player, double x, double y, double z, SoundEvent, SoundSource, volume, pitch)` - - **Client Behavior**: If the passed in player is *the* client player, plays the sound event to the client player. - - **Server Behavior**: Plays the sound event to everyone nearby **except** the passed in player. Player can be `null`. - - **Usage**: The correspondence between the behaviors implies that these two methods are to be called from some player-initiated code that will be run on both logical sides at the same time: the logical client handles playing it to the user, and the logical server handles everyone else hearing it without re-playing it to the original user. They can also be used to play any sound in general at any position server-side by calling it on the logical server and passing in a `null` player, thus letting everyone hear it. - -3. `playLocalSound(double x, double y, double z, SoundEvent, SoundSource, volume, pitch, distanceDelay)` - - **Client Behavior**: Just plays the sound event in the client level. If `distanceDelay` is `true`, then delays the sound based on how far it is from the player. - - **Server Behavior**: Does nothing. - - **Usage**: This method only works client-side, and thus is useful for sounds sent in custom packets, or other client-only effect-type sounds. Used for thunder. - -### `ClientLevel` - -1. `playLocalSound(BlockPos, SoundEvent, SoundSource, volume, pitch, distanceDelay)` - - Simply forwards to `Level`'s [overload (3)](#level-playsound-xyzecvpd), adding 0.5 to each coordinate of the `BlockPos` given. - -### `Entity` - -1. `playSound(SoundEvent, volume, pitch)` - - Forwards to `Level`'s [overload (2)](#level-playsound-pxyzecvp), passing in `null` as the player. - - **Client Behavior**: Does nothing. - - **Server Behavior**: Plays the sound event to everyone at this entity's position. - - **Usage**: Emitting any sound from any non-player entity server-side. - -### `Player` - -1. `playSound(SoundEvent, volume, pitch)` (overriding the one in [`Entity`](#entity-playsound-evp)) - - Forwards to `Level`'s [overload (2)](#level-playsound-pxyzecvp), passing in `this` as the player. - - **Client Behavior**: Does nothing, see override in [`LocalPlayer`](#localplayer-playsound-evp). - - **Server Behavior**: Plays the sound to everyone nearby *except* this player. - - **Usage**: See [`LocalPlayer`](#localplayer-playsound-evp). - -### `LocalPlayer` - -1. `playSound(SoundEvent, volume, pitch)` (overriding the one in [`Player`](#player-playsound-evp)) - - Forwards to `Level`'s [overload (2)](#level-playsound-pxyzecvp), passing in `this` as the player. - - **Client Behavior**: Just plays the Sound Event. - - **Server Behavior**: Method is client-only. - - **Usage**: Just like the ones in `Level`, these two overrides in the player classes seem to be for code that runs together on both sides. The client handles playing the sound to the user, while the server handles everyone else hearing it without re-playing to the original user. - -[loc]: ../misc/resourcelocation.md -[wiki]: https://minecraft.wiki/w/Sounds.json -[datagen]: ../datagen/client/sounds.md -[registration]: ../concepts/registries.md#methods-for-registering -[sides]: ../concepts/sides.md diff --git a/docs/resources/client/sounds.md b/docs/resources/client/sounds.md new file mode 100644 index 00000000..9590e917 --- /dev/null +++ b/docs/resources/client/sounds.md @@ -0,0 +1,308 @@ +# Sounds + +Sounds, while not required for anything, can make a mod feel much more nuanced and alive. Minecraft offers you various ways to register and play sounds, which will be laid out in this article. + +## Terminology + +The Minecraft sound engine uses a variety of terms to refer to different things: + +- **Sound event**: A sound event is an in-code trigger that tells the sound engine to play a certain sound. `SoundEvent`s are also the things you register to the game. +- **Sound category** or **sound source**: Sound categories are rough groupings of sounds that can be individually toggled. The sliders in the sound options GUI represent these categories: `master`, `block`, `player` etc. In code, they can be found in the `SoundSource` enum. +- **Sound definition**: A mapping of a sound event to one or multiple sound objects, plus some optional metadata. Sound definitions are located in a namespace's [`sounds.json` file][soundsjson]. +- **Sound object**: A JSON object consisting of a sound file location, plus some optional metadata. +- **Sound file**: An on-disk sound file. Minecraft only supports `.ogg` sound files. + +:::danger +Due to the implementation of OpenAL (Minecraft's audio library), for your sound to have attenuation - that is, for it to get quieter and louder depending on the player's distance to it -, your sound file must be mono (single channel). Stereo (multichannel) sound files will not be subject to attenuation and always play at the player's location, making them ideal for ambient sounds and background music. See also [MC-146721][bug]. +::: + +## Creating `SoundEvent`s + +`SoundEvent`s are [registered objects][registration], meaning that they must be registered to the game through a `DeferredRegister` and be singletons: + +```java +public class MySoundsClass { + // Assuming that your mod id is examplemod + public static final DeferredRegister SOUND_EVENTS = + DeferredRegister.create(BuiltInRegistries.SOUND_EVENT, "examplemod"); + + // All vanilla sounds use variable range events. + public static final Supplier MY_SOUND = SOUND_EVENTS.register( + "my_sound", // must match the resource location on the next line + () -> SoundEvent.createVariableRangeEvent(new ResourceLocation("examplemod", "my_sound")) + ); + + // There is a currently unused method to register fixed range (= non-attenuating) events as well: + public static final Supplier MY_FIXED_SOUND = SOUND_EVENTS.register("my_fixed_sound", + // 16 is the default range of sounds. Be aware that due to OpenAL limitations, + // values above 16 have no effect and will be capped to 16. + () -> SoundEvent.createFixedRangeEvent(new ResourceLocation("examplemod", "my_fixed_sound"), 16) + ); +} +``` + +Of course, don't forget to add your registry to the [mod event bus][modbus] in the [mod constructor][modctor]: + +```java +public ExampleMod(IEventBus modBus) { + MySoundsClass.SOUND_EVENTS.register(modBus); + // other things here +} +``` + +And voilà, you have a sound event! + +## `sounds.json` + +Now, to connect your sound event to actual sound files, we need to create sound definitions. All sound definitions for a namespace are stored in a single file named `sounds.json`, also known as the sound definitions file, directly in the namespace's root. Every sound definition is a mapping of sound event id (e.g. `my_sound`) to a JSON sound object. Note that the sound event ids do not specify a namespace, as that is already determined by the namespace the sound definitions file is in. An example `sounds.json` would look something like this: + +```json5 +{ + // Sound definition for the sound event "examplemod:my_sound" + "my_sound": { + // List of sound objects. If this contains more than one element, an element will be chosen randomly. + "sounds": [ + // Only name is required, all other properties are optional. + { + // Location of the sound file, relative to the namespace's sounds folder. + // This example references a sound at assets/examplemod/sounds/sound_1.ogg. + "name": "examplemod:sound_1", + // May be "sound" or "event". "sound" causes the name to refer to a sound file. + // "event" causes the name to refer to another sound event. Defaults to "sound". + "type": "sound", + // The volume this sound will be played at. Must be between 0.0 and 1.0 (default). + "volume": 0.8, + // The pitch value the sound will be played at. + // Must be between 0.0 and 2.0. Defaults to 1.0. + "pitch": 1.1, + // Weight of this sound when choosing a sound from the sounds list. Defaults to 1. + "weight": 3, + // If true, the sound will be streamed from the file instead of loaded all at once. + // Recommended for sound files that are more than a few seconds long. Defaults to false. + "stream": true, + // Manual override for the attenuation distance. Defaults to 16. Ignored by fixed range sound events. + "attenuation_distance": 8, + // If true, the sound will be loaded into memory on pack load, instead of when the sound is played. + // Vanilla uses this for underwater ambience sounds. Defaults to false. + "preload": true + }, + // Shortcut for { "name": "examplemod:sound_2" } + "examplemod:sound_2" + ] + }, + "my_fixed_sound": { + // Optional. If true, replaces sounds from other resource packs instead of adding to them. + // See the Merging chapter below for more information. + "replace": true, + // The translation key of the subtitle displayed when this sound event is triggered. + "subtitle": "examplemod.my_fixed_sound", + "sounds": [ + "examplemod:sound_1", + "examplemod:sound_2" + ] + } +} +``` + +### Merging + +Unlike most other resource files, `sounds.json` do not overwrite values in packs below them. Instead, they are merged together and then interpreted as one combined `sounds.json` file. Consider sounds `sound_1`, `sound_2`, `sound_3` and `sound_4` being defined in two `sounds.json` files from two different resource packs RP1 and RP2, where RP2 is placed below RP1: + +`sounds.json` in RP1: + +```json5 +{ + "sound_1": { + "sounds": [ + "sound_1" + ] + }, + "sound_2": { + "replace": true, + "sounds": [ + "sound_2" + ] + }, + "sound_3": { + "sounds": [ + "sound_3" + ] + }, + "sound_4": { + "replace": true, + "sounds": [ + "sound_4" + ] + } +} +``` + +`sounds.json` in RP2: + +```json5 +{ + "sound_1": { + "sounds": [ + "sound_5" + ] + }, + "sound_2": { + "sounds": [ + "sound_6" + ] + }, + "sound_3": { + "replace": true, + "sounds": [ + "sound_7" + ] + }, + "sound_4": { + "replace": true, + "sounds": [ + "sound_8" + ] + } +} +``` + +The combined (merged) `sounds.json` file the game would then go on and use to load sounds would look something look this (only in memory, this file is never written anywhere): + +```json5 +{ + "sound_1": { + // replace false and false: add from lower pack, then from upper pack + "sounds": [ + "sound_5", + "sound_1" + ] + }, + "sound_2": { + // replace true in upper pack and false in lower pack: add from upper pack only + "sounds": [ + "sound_2" + ] + }, + "sound_3": { + // replace false in upper pack and true in lower pack: add from lower pack, then from upper pack + // Would still discard values from a third resource pack sitting below RP2 + "sounds": [ + "sound_7", + "sound_3" + ] + }, + "sound_4": { + // replace true and true: add from upper pack only + "sounds": [ + "sound_8", + "sound_4" + ] + } +} +``` + +## Playing Sounds + +Minecraft offers various methods to play sounds, and it is sometimes unclear which one should be used. All methods accept a `SoundEvent`, which can either be your own or a vanilla one (vanilla sound events are found in the `SoundEvents` class). For the following method descriptions, client and server refer to the [logical client and logical server][sides], respectively. + +### `Level` + +- `playSound(Player player, double x, double y, double z, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)` + - Client behavior: If the player passed in is the local player, play the sound event to the player at the given location, otherwise no-op. + - Server behavior: If the player passed in is NOT the local player, play the sound event to the player at the given location, otherwise no-op. + - Usage: Call from client-initiated code that will run on both sides. The server not playing it to the initiating player prevents playing the sound event twice to them. Alternatively, call from server-initiated code (e.g. a [block entity][be]) with a `null` player to play the sound to everyone. +- `playSound(Player, player, BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)` + - Forwards to the first method with `x`, `y` and `z` taking the values of `pos.getX() + 0.5`, `pos.getY() + 0.5` and `pos.getZ() + 0.5`, respectively. +- `playLocalSound(double x, double y, double z, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch, boolean distanceDelay)` + - Client behavior: Plays the sound to the player at the given location. Does not send anything to the server. If `distanceDelay` is `true`, delays the sound based on the distance to the player. + - Server behavior: No-op. + - Usage: Called from custom packets sent from the server. Vanilla uses this for thunder sounds. + +### `ClientLevel` + +- `playLocalSound(BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch, boolean distanceDelay)` + - Forwards to `Level#playLocalSound` with `x`, `y` and `z` taking the values of `pos.getX() + 0.5`, `pos.getY() + 0.5` and `pos.getZ() + 0.5`, respectively. + +### `Entity` + +- `playSound(SoundEvent soundEvent, float volume, float pitch)` + - Forwards to `Level#playSound` with `null` as the player, `SoundSource.ENTITY` as the sound source, the entity's position for x/y/z, and the other parameters passed in. + +### `Player` + +- `playSound(SoundEvent soundEvent, float volume, float pitch)` (overrides the method in `Entity`) + - Forwards to `Level#playSound` with `this` as the player, `SoundSource.PLAYER` as the sound source, the player's position for x/y/z, and the other parameters passed in. As such, the client/server behavior mimics the one from `Level#playSound`: + - Client behavior: Play the sound event to the client player at the given location. + - Server behavior: Play the sound event to everyone near the given location except the client player. + +## Datagen + +Sound files themselves can of course not be [datagenned][datagen], but `sounds.json` files can. To do so, we extend `SoundDefinitionsProvider` and override the `registerSounds()` method: + +```java +public class MySoundDefinitionsProvider extends SoundDefinitionsProvider { + // Parameters can be obtained from GatherDataEvent. + public MySoundDefinitionsProvider(PackOutput output, ExistingFileHelper existingFileHelper) { + // Use your actual mod id instead of "examplemod". + super(output, "examplemod", existingFileHelper); + } + + @Override + public void registerSounds() { + // Accepts a Supplier, a SoundEvent, or a ResourceLocation as the first parameter. + add(MySoundsClass.MY_SOUND, SoundDefinition.definition() + // Add sound objects to the sound definition. Parameter is a vararg. + .with( + // Accepts either a string or a ResourceLocation as the first parameter. + // The second parameter can be either SOUND or EVENT, and can be omitted if the former. + sound("examplemod:sound_1", SoundDefinition.SoundType.SOUND) + // Sets the volume. Also has a double counterpart. + .volume(0.8f) + // Sets the pitch. Also has a double counterpart. + .pitch(1.2f) + // Sets the weight. + .weight(2) + // Sets the attenuation distance. + .attenuationDistance(8) + // Enables streaming. + // Also has a parameterless overload that defers to stream(true). + .stream(true) + // Enables preloading. + // Also has a parameterless overload that defers to preload(true). + .preload(true), + // The shortest we can get. + sound("examplemod:sound_2") + ) + // Sets the subtitle. + .subtitle("sound.examplemod.sound_1") + // Enables replacing. + .replace(true) + ); + } +} +``` + +As with every data provider, don't forget to register the provider to the event: + +```java +@SubscribeEvent +public static void gatherData(GatherDataEvent event) { + DataGenerator generator = event.getGenerator(); + PackOutput output = generator.getPackOutput(); + ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); + + // other providers here + generator.addProvider( + event.includeClient(), + new MySoundDefinitionsProvider(output, existingFileHelper) + ); +} +``` + +[bug]: https://bugs.mojang.com/browse/MC-146721 +[datagen]: ../index.md#data-generation +[modbus]: ../../concepts/events.md#event-buses +[modctor]: ../../gettingstarted/modfiles.md#javafml-and-mod +[registration]: ../../concepts/registries.md +[sides]: ../../concepts/sides.md#the-logical-side +[soundsjson]: #soundsjson diff --git a/docs/resources/index.md b/docs/resources/index.md index e4196e0b..5e2434f8 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -174,6 +174,6 @@ runs { [particles]: ../gameeffects/particles.md [predicate]: https://minecraft.wiki/w/Predicate [sides]: ../concepts/sides.md -[sounds]: ../gameeffects/sounds.md +[sounds]: client/sounds.md [textures]: client/textures.md [translations]: client/i18n.md#language-files From 7561f8081900641dec20e6949a865c7c30fca39c Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 21 Mar 2024 01:08:45 +0100 Subject: [PATCH 33/41] cleanup some related articles --- docs/blocks/index.md | 7 +++-- docs/resources/client/i18n.md | 2 +- docs/resources/client/sounds.md | 4 +++ docs/resources/index.md | 33 +++++++++++++--------- static/img/ambientocclusion_annotated.png | Bin 98157 -> 0 bytes static/img/component_graph.png | Bin 58733 -> 0 bytes static/img/component_red_hello.png | Bin 25813 -> 0 bytes static/img/component_style_annotated.png | Bin 44361 -> 0 bytes 8 files changed, 28 insertions(+), 18 deletions(-) delete mode 100644 static/img/ambientocclusion_annotated.png delete mode 100644 static/img/component_graph.png delete mode 100644 static/img/component_red_hello.png delete mode 100644 static/img/component_style_annotated.png diff --git a/docs/blocks/index.md b/docs/blocks/index.md index b8892507..95ed54dd 100644 --- a/docs/blocks/index.md +++ b/docs/blocks/index.md @@ -121,9 +121,7 @@ This does the exact same as the previous example, but is slightly shorter. Of co ### Resources -If you register your block and place it in the world, you will find it to be missing things like a texture. This is because textures, among others, are handled by Minecraft's resource system. - -To apply a simple texture to a block, you must add a blockstate JSON, a model JSON, and a texture PNG. See the section on [resources] for more information. +If you register your block and place it in the world, you will find it to be missing things like a texture. This is because [textures], among others, are handled by Minecraft's resource system. To apply the texture to the block, you must provide a [model] and a [blockstate file][bsfile] that associates the block with the texture and a shape. Give the linked articles a read for more information. ## Using Blocks @@ -229,12 +227,15 @@ Random ticking is used by a wide range of mechanics in Minecraft, such as plant [below]: #deferredregisterblocks-helpers [blockentities]: ../blockentities/index.md [blockstates]: states.md +[bsfile]: ../resources/client/models/index.md#blockstate-files [events]: ../concepts/events.md [interactionpipeline]: ../items/interactionpipeline.md [item]: ../items/index.md +[model]: ../resources/client/models/index.md [randomtick]: #random-ticking [registration]: ../concepts/registries.md#methods-for-registering [resources]: ../resources/index.md#assets [sounds]: ../resources/client/sounds.md +[textures]: ../resources/client/textures.md [usingblocks]: #using-blocks [usingblockstates]: states.md#using-blockstates diff --git a/docs/resources/client/i18n.md b/docs/resources/client/i18n.md index 88481f2f..24cb5d37 100644 --- a/docs/resources/client/i18n.md +++ b/docs/resources/client/i18n.md @@ -121,7 +121,7 @@ Starting with NeoForge 20.4.179, translation files can override certain parts of | Display Name | `fml.menu.mods.info.displayname.modid` | A field named `displayName` may be placed in the `[[mods]]` section instead. | | Description | `fml.menu.mods.info.description.modid` | A field named `description` may be placed in the `[[mods]]` section instead. | -### Data Generation +### Datagen Language files can be [datagenned][datagen]. To do so, extend the `LanguageProvider` class and add your translations in the `addTranslations()` method: diff --git a/docs/resources/client/sounds.md b/docs/resources/client/sounds.md index 9590e917..b633c225 100644 --- a/docs/resources/client/sounds.md +++ b/docs/resources/client/sounds.md @@ -54,6 +54,8 @@ And voilà, you have a sound event! ## `sounds.json` +_See also: [sounds.json][mcwikisounds] on the [Minecraft Wiki][mcwiki]_ + Now, to connect your sound event to actual sound files, we need to create sound definitions. All sound definitions for a namespace are stored in a single file named `sounds.json`, also known as the sound definitions file, directly in the namespace's root. Every sound definition is a mapping of sound event id (e.g. `my_sound`) to a JSON sound object. Note that the sound event ids do not specify a namespace, as that is already determined by the namespace the sound definitions file is in. An example `sounds.json` would look something like this: ```json5 @@ -301,6 +303,8 @@ public static void gatherData(GatherDataEvent event) { [bug]: https://bugs.mojang.com/browse/MC-146721 [datagen]: ../index.md#data-generation +[mcwiki]: https://minecraft.wiki +[mcwikisounds]: https://minecraft.wiki/w/Sounds.json [modbus]: ../../concepts/events.md#event-buses [modctor]: ../../gettingstarted/modfiles.md#javafml-and-mod [registration]: ../../concepts/registries.md diff --git a/docs/resources/index.md b/docs/resources/index.md index 5e2434f8..f6d057b7 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -65,20 +65,20 @@ Datagen is run through the Data run configuration, which is generated for you al All data providers extend the `DataProvider` interface and usually require one method to be overridden. The following is a list of all data generators Minecraft and NeoForge offer (the linked articles add further information, such as helper methods, TODO add links): -| Class | Method | Generates | Side | Notes | -|--------------------------------------|----------------------------------|-----------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `BlockStateProvider` | `registerStatesAndModels()` | Blockstate files, block models | Client | | -| `ItemModelProvider` | `registerModels()` | Item models | Client | | -| `LanguageProvider` | `addTranslations()` | Translations | Client | Also requires passing the language in the constructor. | -| `ParticleDescriptionProvider` | `addDescriptions()` | Particle definitions | Client | | -| `SoundDefinitionsProvider` | `registerSounds()` | Sound definitions | Client | | -| `AdvancementProvider` | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | -| `LootTableProvider` | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | -| `RecipeProvider` | `buildRecipes(RecipeOutput)` | Recipes | Server | | -| Various subclasses of `TagsProvider` | `addTags(HolderLookup.Provider)` | Tags | Server | Several specialized subclasses exist, for example `BlockTagsProvider`. If the one you need doesn't exist, extend `TagsProvider` (or `IntrinsicHolderTagsProvider` if applicable) with your tag type as the generic parameter. | -| `DatapackBuiltinEntriesProvider` | N/A | Datapack builtin entries, e.g. worldgen | Server | See linked article for details. | -| `DataMapProvider` | `gather()` | Data map entries | Server | | -| `GlobalLootModifierProvider` | `start()` | Global loot modifiers | Server | | +| Class | Method | Generates | Side | Notes | +|------------------------------------------------------|----------------------------------|-----------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`BlockStateProvider`][blockstateprovider] | `registerStatesAndModels()` | Blockstate files, block models | Client | | +| [`ItemModelProvider`][itemmodelprovider] | `registerModels()` | Item models | Client | | +| [`LanguageProvider`][langprovider] | `addTranslations()` | Translations | Client | Also requires passing the language in the constructor. | +| `ParticleDescriptionProvider` | `addDescriptions()` | Particle definitions | Client | | +| [`SoundDefinitionsProvider`][soundprovider] | `registerSounds()` | Sound definitions | Client | | +| `AdvancementProvider` | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | +| `LootTableProvider` | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | +| `RecipeProvider` | `buildRecipes(RecipeOutput)` | Recipes | Server | | +| Various subclasses of `TagsProvider` | `addTags(HolderLookup.Provider)` | Tags | Server | Several specialized subclasses exist, for example `BlockTagsProvider`. If the one you need doesn't exist, extend `TagsProvider` (or `IntrinsicHolderTagsProvider` if applicable) with your tag type as the generic parameter. | +| [`DatapackBuiltinEntriesProvider`][datapackprovider] | N/A | Datapack builtin entries, e.g. worldgen | Server | See linked article for details. | +| `DataMapProvider` | `gather()` | Data map entries | Server | | +| `GlobalLootModifierProvider` | `start()` | Global loot modifiers | Server | | All of these providers follow the same pattern. First, you create a subclass and add your own resources to be generated. Then, you add the provider to the event in an [event handler][eventhandler]. An example using a `RecipeProvider`: @@ -156,13 +156,17 @@ runs { } ``` +[blockstateprovider]: client/models/datagen.md#block-model-datagen [bsfile]: client/models/index.md#blockstate-files [chattype]: https://minecraft.wiki/w/Chat_type [datapackcmd]: https://minecraft.wiki/w/Commands/datapack +[datapackprovider]: ../concepts/registries.md#data-generation-for-datapack-registries [event]: ../concepts/events.md [eventhandler]: ../concepts/events.md#registering-an-event-handler [function]: https://minecraft.wiki/w/Function_(Java_Edition) +[itemmodelprovider]: client/models/datagen.md#item-model-datagen [itemmodifier]: https://minecraft.wiki/w/Item_modifier +[langprovider]: client/i18n.md#datagen [lifecycle]: ../concepts/events.md#the-mod-lifecycle [mcwiki]: https://minecraft.wiki [mcwikidatapacks]: https://minecraft.wiki/w/Data_pack @@ -174,6 +178,7 @@ runs { [particles]: ../gameeffects/particles.md [predicate]: https://minecraft.wiki/w/Predicate [sides]: ../concepts/sides.md +[soundprovider]: client/sounds.md#datagen [sounds]: client/sounds.md [textures]: client/textures.md [translations]: client/i18n.md#language-files diff --git a/static/img/ambientocclusion_annotated.png b/static/img/ambientocclusion_annotated.png deleted file mode 100644 index 17009b076dcc793c929a9c0b62c1ecf42a40aa05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98157 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 Nd~V2&=0L%j{{SSvH(me$ diff --git a/static/img/component_red_hello.png b/static/img/component_red_hello.png deleted file mode 100644 index 36be33eaefbc8bfac61c89e17d64c562ed6b09db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25813 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

UxviR%uIQ`+w(ZpBLlDv|aAzqx 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 diff --git a/static/img/component_style_annotated.png b/static/img/component_style_annotated.png deleted file mode 100644 index bae2a322d1429a3326d987017ab4fadf54e46f56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44361 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

HvN1Ca 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 From f6971bf7058a6d82e54f41ff5c9b46d460d8ef8a Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Tue, 26 Mar 2024 02:32:28 +0100 Subject: [PATCH 34/41] particles --- docs/gameeffects/_category_.json | 3 - docs/gameeffects/particles.md | 129 --------------- docs/resources/client/particles.md | 254 +++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+), 132 deletions(-) delete mode 100644 docs/gameeffects/_category_.json delete mode 100644 docs/gameeffects/particles.md create mode 100644 docs/resources/client/particles.md diff --git a/docs/gameeffects/_category_.json b/docs/gameeffects/_category_.json deleted file mode 100644 index 818267c2..00000000 --- a/docs/gameeffects/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Game Effects" -} \ No newline at end of file diff --git a/docs/gameeffects/particles.md b/docs/gameeffects/particles.md deleted file mode 100644 index 91a216bd..00000000 --- a/docs/gameeffects/particles.md +++ /dev/null @@ -1,129 +0,0 @@ -Particles -========= - -Particles are an effect within the game used as polish to better improve immersion. Their usefulness also requires great caution because of their methods of creation and reference. - -Creating a Particle -------------------- - -Particles are broken up between its [**client only**][sides] implementation to display the particle and its common implementation to reference the particle or sync data from the server. - -| Class | Side | Description | -| :--- | :---: | :--- | -| ParticleType | BOTH | The registry object of a particle's type definition used to reference the particle on either side | -| ParticleOptions | BOTH | A data holder used to sync information from the network or a command to the associated client(s) | -| ParticleProvider | CLIENT | A factory registered by the `ParticleType` used to construct a `Particle` from the associated `ParticleOptions`. -| Particle | CLIENT | The renderable logic to display on the associated client(s) | - -### ParticleType - -A `ParticleType` is the registry object defining what a particular particle type is and provides an available reference to the specific particle on both sides. As such, every `ParticleType` must be [registered][registration]. - -Each `ParticleType` takes in two parameters: an `overrideLimiter` which determines whether the particle renders regardless of distance, and a `ParticleOptions$Deserializer` which is used to read the sent `ParticleOptions` on the client. As the base `ParticleType` is abstract, a single method needs to be implemented: `#codec`. This represents how to encode and decode the associated `ParticleOptions` of the type. - -:::note -`ParticleType#codec` is only used within the biome codec for vanilla implementations. -::: - -In most cases, there is no need to have any particle data sent to the client. For these instances, it is easier to create a new instance of `SimpleParticleType`: an implementation of `ParticleType` and `ParticleOptions` which does not send any custom data to the client besides the type. Most vanilla implementations use `SimpleParticleType` besides redstone dust for coloring and block/item dependent particles. - -:::info -A `ParticleType` is not needed to make a particle spawn if only referenced on the client. However, it is necessary to use any of the prebuilt logic within `ParticleEngine` or spawn a particle from the server. -::: - -### ParticleOptions - -An `ParticleOptions` represents the data that each particle takes in. It is also used to send data from particles spawned via the server. All particle spawning methods take in a `ParticleOptions` such that it knows the type of the particle and the data associated with spawning one. - -`ParticleOptions` is broken down into three methods: - -| Method | Description | -| :--- | :--- | -| getType | Gets the type definition of the particle, or the `ParticleType` -| writeToNetwork | Writes the particle data to a buffer on the server to send to the client -| writeToString | Writes the particle data to a string - -These objects are either constructed on the fly as needed, or they are singletons as a result of being a `SimpleParticleType`. - -#### ParticleOptions$Deserializer - -To receive the `ParticleOptions` on the client, or to reference the data within a command, the particle data must be deserialized via `ParticleOptions$Deserializer`. Each method within `ParticleOptions$Deserializer` has a parity encoding method within `ParticleOptions`: - -| Method | ParticleOptions Encoder | Description | -| :--- | :---: | :--- | -| fromCommand | writeToString | Decodes a particle data from a string, usually from a command. | -| fromNetwork | writeToNetwork | Decodes a particle data from a buffer on the client. | - -This object, when needing to send custom particle data, is passed into the constructor of the `ParticleType`. - -### Particle - -A `Particle` provides the rendering logic needed to draw said data onto the screen. To create any `Particle`, two methods must be implemented: - -| Method | Description | -| :--- | :--- | -| render | Renders the particle onto the screen. | -| getRenderType | Gets the render type of the particle. | - -A common subclass of `Particle` to render textures is `TextureSheetParticle`. While `#getRenderType` needs to be implemented, whatever the texture sprite is set will be rendered at the particle's location. - -#### ParticleRenderType - -`ParticleRenderType` is a variation on `RenderType` which constructs the startup and teardown phase for every particle of that type and then renders them all at once via the `Tesselator`. There are six different render types a particle can be in. - -| Render Type | Description | -| :--- | :--- | -| TERRAIN_SHEET | Renders a particle whose texture is located within the available blocks. | -| PARTICLE_SHEET_OPAQUE | Renders a particle whose texture is opaque and located within the available particles. | -| PARTICLE_SHEET_TRANSLUCENT | Renders a particle whose texture is translucent and located within the available particles. | -| PARTICLE_SHEET_LIT | Same as `PARTICLE_SHEET_OPAQUE` except without using the particle shader. | -| CUSTOM | Provides setup for blending and depth mask but provides no rendering functionality as that would be implemented within `Particle#render`. | -| NO_RENDER | The particle will never render. | - -Implementing a custom render type will be left as an exercise to the reader. - -### ParticleProvider - -Finally, a particle is usually created via an `ParticleProvider`. A factory has a single method `#createParticle` which is used to create a particle given the particle data, client level, position, and movement delta. Since a `Particle` is not beholden to any particular `ParticleType`, it can be reused in different factories as necessary. - -An `ParticleProvider` must be registered by subscribing to the `RegisterParticleProvidersEvent` on the **mod event bus**. Within the event, the factory can be registered via `#registerSpecial` by supplying an instance of the factory to the method. - -:::danger -`RegisterParticleProvidersEvent` should only be called on the client and thus sided off in some isolated client class, referenced by either `DistExecutor` or `@EventBusSubscriber`. -::: - -#### ParticleDescription, SpriteSet, and SpriteParticleRegistration - -There are three particle render types that cannot use the above method of registration: `PARTICLE_SHEET_OPAQUE`, `PARTICLE_SHEET_TRANSLUCENT`, and `PARTICLE_SHEET_LIT`. This is because all three of these particle render types use a sprite set that is loaded by the `ParticleEngine` directly. As such, the textures supplied must be obtained and registered through a different method. This will assume your particle is a subtype of `TextureSheetParticle` as that is the only vanilla implementation for this logic. - -To add a texture to a particle, a new JSON file must be added to `assets//particles`. This is known as the `ParticleDescription`. The name of this file will represent the registry name of the `ParticleType` the factory is being attached to. Each particle JSON is an object. The object stores a single key `textures` which holds an array of `ResourceLocation`s. Any `:` texture represented here will point to a texture at `assets//textures/particle/.png`. - -```js -{ - "textures": [ - // Will point to a texture located in - // assets/mymod/textures/particle/particle_texture.png - "mymod:particle_texture", - // Textures should by ordered by drawing order - // e.g. particle_texture will render first, then particle_texture2 - // after some time - "mymod:particle_texture2" - ] -} -``` - -To reference a particle texture, the subtype of `TextureSheetParticle` should either take in an `SpriteSet` or a `TextureAtlasSprite` obtained from `SpriteSet`. `SpriteSet` holds a list of textures which refer to the sprites as defined by our `ParticleDescription`. `SpriteSet` has two methods, both of which grab a `TextureAtlasSprite` in different methods. The first method takes in two integers. The backing implementation allows the sprite to have a texture change as it ages. The second method takes in a `Random` instance to get a random texture from the sprite set. The sprite can be set within `TextureSheetParticle` by using one of the helper methods that takes in the `SpriteSet`: `#pickSprite` which uses the random method of picking a texture, and `#setSpriteFromAge` which uses the percentage method of two integers to pick the texture. - -To register these particle textures, a `SpriteParticleRegistration` needs to be supplied to the `RegisterParticleProvidersEvent#registerSpriteSet` method. This method takes in an `SpriteSet` holding the associated sprite set for the particle and creates an `ParticleProvider` to create the particle. The simplest method of implementation can be done by implementing `ParticleProvider` on some class and having the constructor take in an `SpriteSet`. Then the `SpriteSet` can be passed to the particle as normal. - -:::note -If you are registering a `TextureSheetParticle` subtype which only contains one texture, then you can supply a `ParticleProvider$Sprite` instead to the `#registerSprite` method, which has essentially the same functional interface method as `ParticleProvider`. -::: - -Spawning a Particle -------------------- - -Particles can be spawned from either level instance. However, each side has a specific way to spawn a particle. If on the `ClientLevel`, `#addParticle` can be called to spawn a particle or `#addAlwaysVisibleParticle` can be called to spawn a particle that is visible from any distance. If on the `ServerLevel`, `#sendParticles` can be called to send a packet to the client to spawn the particle. Calling the two `ClientLevel` methods on the server will result in nothing. - -[sides]: ../concepts/sides.md -[registration]: ../concepts/registries.md#methods-for-registering diff --git a/docs/resources/client/particles.md b/docs/resources/client/particles.md new file mode 100644 index 00000000..0199f1b9 --- /dev/null +++ b/docs/resources/client/particles.md @@ -0,0 +1,254 @@ +# Particles + +Particles are 2d effects that polish the game and add immersion. They can be spawned both client and server [side], but being mostly visual in nature, critical parts exist only on the physical (and logical) client side. + +## Registering Particles + +### `ParticleType` + +Particles are registered using `ParticleType`s. These work similar to `EntityType`s or `BlockEntityType`s, in that there's a `Particle` class - every spawned particle is an instance of that class -, and then there's the `ParticleType` class, holding some common information, that is used for registration. `ParticleType`s are a [registry], which means that we want to register them using a `DeferredRegister` like all other registered objects: + +```java +public class MyParticleTypes { + // Assuming that your mod id is examplemod + public static final DeferredRegister> PARTICLE_TYPES = + DeferredRegister.create(BuiltInRegistries.PARTICLE_TYPE, "examplemod"); + + // The easiest way to add new particle types is reusing vanilla's SimpleParticleType. + // Implementing a custom ParticleType is also possible, see below. + public static final Supplier MY_PARTICLE = PARTICLE_TYPES.register( + // The name of the particle type. + "my_particle", + // The supplier. The boolean parameter denotes whether setting the Particles option in the + // video settings to Minimal will affect this particle type or not; this is false for + // most vanilla particles, but true for e.g. explosions, campfire smoke, or squid ink. + () -> new SimpleParticleType(false) + ); +} +``` + +:::info +A `ParticleType` is only necessary if you need to work with particles on the server side. The client can also use `Particle`s directly. +::: + +### `Particle` + +A `Particle` is what is later spawned into the world and displayed to the player. While you may extend `Particle` and implement things yourself, in many cases it will be better to extend `TextureSheetParticle` instead, as this class provides helpers for things such as animating and scaling, and also does the actual rendering for you (all of which you'd need to implement yourself if extending `Particle` directly). + +Most properties of `Particle`s are controlled by fields such as `gravity`, `lifetime`, `hasPhysics`, `friction`, etc. The only two methods that make sense to implement yourself are `tick` and `move`, both of which do exactly what you'd expect. As such, custom particle classes are often short, consisting e.g. only of a constructor that sets some fields and lets the superclass handle the rest. A basic implementation would look somewhat like this: + +```java +public class MyParticle extends TextureSheetParticle { + private final SpriteSet spriteSet; + + // First four parameters are self-explanatory. The SpriteSet parameter is provided by the + // ParticleProvider, see below. You may also add additional parameters as needed, e.g. xSpeed/ySpeed/zSpeed. + public MyParticle(ClientLevel level, double x, double y, double z, SpriteSet spriteSet) { + super(level, x, y, z); + this.spriteSet = spriteSet; + this.gravity = 0; // Our particle floats in midair now, because why not. + } + + @Override + public void tick() { + // Set the sprite for the current particle age, i.e. advance the animation. + setSpriteFromAge(spriteSet); + // Let super handle further movement. You may replace this with your own movement if needed. + // You may also override move() if you only want to modify the built-in movement. + super.tick(); + } +} +``` + +### `ParticleProvider` + +Next, particle types must register a `ParticleProvider`. `ParticleProvider` is a client-only class responsible for actually creating our `Particle`s through the `createParticle` method. While more elaborate code can be included here, many particle providers are as simple as this: + +```java +// The generic type of ParticleProvider must match the type of the particle type this provider is for. +public class MyParticleProvider implements ParticleProvider { + // A set of particle sprites. + private final SpriteSet spriteSet; + + // The registration function passes a SpriteSet, so we accept that and store it for further use. + public MyParticleProvider(SpriteSet spriteSet) { + this.spriteSet = spriteSet; + } + + // This is where the magic happens. We return a new particle each time this method is called! + // The type of the first parameter matches the generic type passed to the super interface. + @Override + public Particle createParticle(SimpleParticleType type, ClientLevel level, + double x, double y, double z, double xSpeed, double ySpeed, double zSpeed) { + // We don't use the type and speed, and pass in everything else. You may of course use them if needed. + return new MyParticle(level, x, y, z, spriteSet); + } +} +``` + +Your particle provider must then be associated with the particle type in the [client-side][side] [mod bus][modbus] [event] `RegisterParticleProvidersEvent`: + +```java +@SubscribeEvent +public static void registerParticleProviders(RegisterParticleProvidersEvent event) { + // There are multiple ways to register providers, all differing in the functional type they provide in the + // second parameter. For example, #registerSpriteSet represents a Function>: + event.registerSpriteSet(MyParticleTypes.MY_PARTICLE.get(), MyParticleProvider::new); + // Other methods include #registerSprite, which is essentially a Supplier, + // and #registerSpecial, which maps to a Supplier. See the source code of the event for further info. +} +``` + +### Particle Definitions + +Finally, we must associate our particle type with a texture. Similar to how items are associated with an item model, we associate our particle type with what is known as a particle definition (or particle description). A particle definition is a JSON file in the `assets//particles` directory and has the same name as the particle type (so for example `my_particle.json` for the above example). The particle definition JSON has the following format: + +```json5 +{ + // A list of textures that will be played in order. Will loop if necessary. + // Texture locations are relative to the textures/particle folder. + "textures": [ + "examplemod:my_particle_0", + "examplemod:my_particle_1", + "examplemod:my_particle_2", + "examplemod:my_particle_3" + ] +} +``` + +### Datagen + +These JSON files can also be [datagenned][datagen] by extending `ParticleDescriptionProvider` and overriding the `#addDescriptions()` method: + +```java +public class MyParticleDescriptionProvider extends ParticleDescriptionProvider { + // Get the parameters from GatherDataEvent. + public AMParticleDefinitionsProvider(PackOutput output, ExistingFileHelper existingFileHelper) { + super(output, existingFileHelper); + } + + // Assumes that all the referenced particles actually exists. Replace "examplemod" with your mod id. + @Override + protected void addDescriptions() { + // Adds a single sprite particle definition with the file at + // assets/examplemod/textures/particle/my_single_particle.png. + sprite(MyParticleTypes.MY_SINGLE_PARTICLE.get(), new ResourceLocation("examplemod", "my_single_particle")); + // Adds a multi sprite particle definition, with a vararg parameter. Alternatively accepts a list. + spriteSet(MyParticleTypes.MY_MULTI_PARTICLE.get(), + new ResourceLocation("examplemod", "my_multi_particle_0"), + new ResourceLocation("examplemod", "my_multi_particle_1"), + new ResourceLocation("examplemod", "my_multi_particle_2") + ); + // Alternative for the above, appends "_" to the base name given, for the given amount of textures. + spriteSet(MyParticleTypes.MY_ALT_MULTI_PARTICLE.get(), + // The base name. + new ResourceLocation("examplemod", "my_multi_particle"), + // The amount of textures. + 3, + // Whether to reverse the list, i.e. start at the last element instead of the first. + false + ); + } +} +``` + +Don't forget to add the provider to the `GatherDataEvent`: + +```java +@SubscribeEvent +public static void gatherData(GatherDataEvent event) { + DataGenerator generator = event.getGenerator(); + PackOutput output = generator.getPackOutput(); + ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); + + // other providers here + generator.addProvider( + event.includeClient(), + new MyParticleDescriptionProvider(output, existingFileHelper) + ); +} +``` + +### Custom `ParticleType`s + +While for most cases `SimpleParticleType` suffices, it is sometimes necessary to attach additional data to the particle on the server side. This is where a custom `ParticleType` and an associated custom `ParticleOptions` are required. Let's start with the `ParticleOptions`, as that is where the information is actually stored: + +```java +public class MyParticleOptions implements ParticleOptions { + // Does not need any parameters, but may define any fields necessary for the particle to work. + public MyParticleOptions() {} + + @Override + public void writeToNetwork(FriendlyByteBuf buf) { + // Write your custom info to the given buffer. + } + + @Override + public String writeToString() { + // Return a stringified version of your custom info, for use in commands. + // We don't have any info in this type, so we return the empty string. + return ""; + } + + // The deserializer object to use. We will discuss how to use this in a moment. + public static final ParticleOptions.Deserializer DESERIALIZER = + new ParticleOptions.Deserializer() { + public MyParticleOptions fromCommand(ParticleType type, StringReader reader) + throws CommandSyntaxException { + // You may deserialize things using the given StringReader and pass them to your + // particle options object if needed. + return new MyParticleOptions(); + } + + public MyParticleOptions fromNetwork(ParticleType type, FriendlyByteBuf buf) { + // Similar to above, deserialize any needed info from the given buffer. + return new MyParticleOptions(); + } + }; +} +``` + +We then use this `ParticleOptions` implementation in our custom `ParticleType`... + +```java +public class MyParticleType extends ParticleType { + // The boolean parameter again determines whether to limit particles at lower particle settings. + // See implementation of the MyParticleTypes class near the top of the article for more information. + public MyParticleType(boolean overrideLimiter) { + // Pass the deserializer to super. + super(overrideLimiter, MyParticleOptions.DESERIALIZER); + } + + // Mojang is moving towards codecs for particle types, so expect the old deserializer approach to vanish soon. + // We define our codec and then return it in the codec() method. Since our example uses no parameters + // for serialization, we use the empty codec. Refer to the Codecs article for more information on how to use them. + public static final Codec CODEC = Codec.EMPTY; + + @Override + public Codec codec() { + return CODEC; + } +} +``` + +... and reference it during registration: + +```java +public static final Supplier MY_CUSTOM_PARTICLE = PARTICLE_TYPES.register( + "my_custom_particle", + () -> new MyParticleType(false)); +``` + +## Spawning Particles + +As a reminder from before, the server only knows `ParticleType`s and `ParticleOption`s, while the client works directly with `Particle`s provided by `ParticleProvider`s that are associated with a `ParticleType`. Consequently, the ways in which particles are spawned are vastly different depending on the side you are on. + +- **Common code**: Call `Level#addParticle` or `Level#addAlwaysVisibleParticle`. This is the preferred way of creating particles that are visible to everyone. +- **Client code**: Use the common code way. Alternatively, create a `new Particle()` with the particle class of your choice and call `Minecraft.getInstance().particleEngine#add(Particle)` with that particle. Note that particles added this way will only display for the client and thus not be visible to other players. +- **Server code**: Call `ServerLevel#sendParticles`. Used in vanilla by the `/particle` command. + +[datagen]: ../index.md#data-generation +[event]: ../../concepts/events.md +[modbus]: ../../concepts/events.md#event-buses +[registry]: ../../concepts/registries.md +[side]: ../../concepts/sides.md From 138bb3b4c4fada47e34963be23efff5110e041a3 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Tue, 26 Mar 2024 02:37:37 +0100 Subject: [PATCH 35/41] fix links to old particle article --- docs/resources/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/resources/index.md b/docs/resources/index.md index f6d057b7..1d6e89b4 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -70,7 +70,7 @@ All data providers extend the `DataProvider` interface and usually require one m | [`BlockStateProvider`][blockstateprovider] | `registerStatesAndModels()` | Blockstate files, block models | Client | | | [`ItemModelProvider`][itemmodelprovider] | `registerModels()` | Item models | Client | | | [`LanguageProvider`][langprovider] | `addTranslations()` | Translations | Client | Also requires passing the language in the constructor. | -| `ParticleDescriptionProvider` | `addDescriptions()` | Particle definitions | Client | | +| [`ParticleDescriptionProvider`][particleprovider] | `addDescriptions()` | Particle definitions | Client | | | [`SoundDefinitionsProvider`][soundprovider] | `registerSounds()` | Sound definitions | Client | | | `AdvancementProvider` | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | | `LootTableProvider` | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | @@ -175,7 +175,8 @@ runs { [packmcmeta]: #packmcmeta [packmcmetadatapack]: https://minecraft.wiki/w/Data_pack#pack.mcmeta [packmcmetaresourcepack]: https://minecraft.wiki/w/Resource_pack#Contents -[particles]: ../gameeffects/particles.md +[particles]: client/particles.md +[particleprovider]: client/particles.md#datagen [predicate]: https://minecraft.wiki/w/Predicate [sides]: ../concepts/sides.md [soundprovider]: client/sounds.md#datagen From 3fde6216c755b2d8132a55e188a99b796eb76bbb Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 28 Mar 2024 18:48:02 +0100 Subject: [PATCH 36/41] apply feedback by @ChampionAsh5357 --- docs/resources/client/models/bakedmodel.md | 2 +- docs/resources/client/models/datagen.md | 3 +- docs/resources/client/models/index.md | 16 +++-- docs/resources/client/models/modelloaders.md | 21 ++++++- docs/resources/index.md | 62 ++++++++++++++------ 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/docs/resources/client/models/bakedmodel.md b/docs/resources/client/models/bakedmodel.md index d0c8bb8a..96cb2682 100644 --- a/docs/resources/client/models/bakedmodel.md +++ b/docs/resources/client/models/bakedmodel.md @@ -12,7 +12,7 @@ The most important method of a baked model is `getQuads`. This method is respons - A `Direction`: The direction of the face being culled against. May be null, indicating that no culling should occur. Will always be null for items. - A `RandomSource`: A client-bound random source you can use for randomization. - A `ModelData`: The extra model data to use. This may contain additional data from the block entity needed for rendering. -- A `RenderType`: The [render type][rendertype] to use for rendering the block. May be null, indicating that the default render type should be used. +- A `RenderType`: The [render type][rendertype] to use for rendering the block. May be null, indicating that the default render type should be used. The default render type is looked up in `ItemBlockRenderTypes`, and if no render type is found there either, `minecraft:solid` is used. Do never call on `ItemBlockRenderTypes` yourself! Be aware that this method is called very often (several times per model and frame), and as such should cache as much as possible. diff --git a/docs/resources/client/models/datagen.md b/docs/resources/client/models/datagen.md index f64b56b6..d1a42549 100644 --- a/docs/resources/client/models/datagen.md +++ b/docs/resources/client/models/datagen.md @@ -11,7 +11,7 @@ Every model starts out as a `ModelBuilder` of some sort - usually a `BlockModelB | Method | Effect | |--------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `#texture(String key, ResourceLocation texture)` | Adds a texture variable with the given key and the given texture location. Has an overload where the second parameter is a `String`. | -| `#renderType(ResourceLocation renderType)` | Sets the render type. Has an overload where the parameter is a `String`. | +| `#renderType(ResourceLocation renderType)` | Sets the render type. Has an overload where the parameter is a `String`. For a list of valid values, see the `RenderType` class. | | `#ao(boolean ao)` | Sets whether to use [ambient occlusion][ao] or not. | | `#guiLight(GuiLight light)` | Sets the GUI light. May be `GuiLight.FRONT` or `GuiLight.SIDE`. | | `#element()` | Adds a new `ElementBuilder` (equivalent to adding a new [element][elements] to the model). Returns said `ElementBuilder` for further modification. | @@ -249,6 +249,7 @@ public class MyItemModelProvider extends ItemModelProvider { // Block items generally use their corresponding block models as parent. withExistingParent(MyItemsClass.EXAMPLE_BLOCK_ITEM.get(), modLoc("block/example_block")); // Items generally use a simple parent and one texture. The most common parents are item/generated and item/handheld. + // In this example, the item texture would be located at assets/examplemod/textures/item/example_item.png. // If you want a more complex model, you can use getBuilder() and then work from that, like you would with block models. withExistingParent(MyItemsClass.EXAMPLE_ITEM.get(), mcLoc("item/generated")).texture("layer0", "item/example_item"); // The above line is so common that there is a shortcut for it. Note that the item registry name and the diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index 36a4f070..8d6a7fc0 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -29,10 +29,10 @@ A model is a JSON file with the following optional properties in the root tag: - Item models can also use layer textures, named `layer0`, `layer1`, etc., where layers with a higher index are rendered above those with a lower index (e.g. `layer1` would be rendered above `layer0`). Only works if the parent is `item/generated`, and only works for up to 5 layers (`layer0` through `layer4`). - `elements`: A list of cuboid [elements]. - `overrides`: A list of [override models][overrides]. Only effective on item models. -- `display`: A sub-object that holds the different display options for different [perspectives], see linked article for possible keys. Only effective on item models, but often specified in block models so that item models can inherit the display options. Every perspective is an optional sub-object that may contain the following options: - - `rotation`: The rotation of the model, specified as `[x, y, z]`. Rotations are handled after translations and before scaling. - - `translation`: The translation of the model, specified as `[x, y, z]`. Translations are handled before rotations and scaling. - - `scale`: The scale of the model, specified as `[x, y, z]`. Scaling is handled after translations and rotations. +- `display`: A sub-object that holds the different display options for different [perspectives], see linked article for possible keys. Only effective on item models, but often specified in block models so that item models can inherit the display options. Every perspective is an optional sub-object that may contain the following options, which are applied in that order: + - `translation`: The translation of the model, specified as `[x, y, z]`. + - `rotation`: The rotation of the model, specified as `[x, y, z]`. + - `scale`: The scale of the model, specified as `[x, y, z]`. - `transform`: See [Root Transforms][roottransforms]. :::tip @@ -106,7 +106,7 @@ Using the custom `neoforge:item_layers` loader, you can also specify extra face "layer0": "minecraft:item/stick", "layer1": "minecraft:item/glowstone_dust" }, - "forge_data": { + "neoforge_data": { "1": { "color": "0xFFFF0000", "block_light": 15, @@ -121,7 +121,7 @@ Using the custom `neoforge:item_layers` loader, you can also specify extra face Item overrides can assign a different model to an item based on a float value, called the override value. For example, bows and crossbows use this to change the texture depending on how long they have been drawn. Overrides have both a model and a code side to them. -The model can specify one or multiple override models that should be used when the override value is equal to or greater than the given threshold value. For example, the bow uses two different properties `pulling` and `pull`. `pulling` is treated as a boolean value, with 1 being interpreted as pulling and 0 as not pulling, while `pull` represents how much the bow is currently pulled. It then uses these properties to specify usage of three alternative models when charged to below 65% (`pulling` 1, no `pull` value), 65% (`pulling` 1, `pull` 0.65) and 90% (`pulling` 1, `pull` 0.9). If multiple models apply (because the value keeps on becoming bigger), the one closest to the actual value is used. The overrides look as follows: +The model can specify one or multiple override models that should be used when the override value is equal to or greater than the given threshold value. For example, the bow uses two different properties `pulling` and `pull`. `pulling` is treated as a boolean value, with 1 being interpreted as pulling and 0 as not pulling, while `pull` represents how much the bow is currently pulled. It then uses these properties to specify usage of three alternative models when charged to below 65% (`pulling` 1, no `pull` value), 65% (`pulling` 1, `pull` 0.65) and 90% (`pulling` 1, `pull` 0.9). If multiple models apply (because the value keeps on becoming bigger), the first in the list matches, so make sure your order is correct. The overrides look as follows: ```json5 { @@ -174,6 +174,10 @@ public static void onClientSetup(FMLClientSetupEvent event) { } ``` +:::info +Vanilla Minecraft only allows for float values between 0 and 1. NeoForge patches this to allow arbitrary float values. +::: + ### Root Transforms Adding the `transform` property at the top level of a model tells the loader that a transformation to all geometry should be applied right before the rotations in a [blockstate file][bsfile] (for block models) or the transformations in a `display` block (for item models) are applied. This is added by NeoForge. diff --git a/docs/resources/client/models/modelloaders.md b/docs/resources/client/models/modelloaders.md index 15aef8b1..f419c81d 100644 --- a/docs/resources/client/models/modelloaders.md +++ b/docs/resources/client/models/modelloaders.md @@ -55,12 +55,31 @@ The dynamic fluid container model, also called dynamic bucket model after its mo "loader": "neoforge:fluid_container", // Required. Must be a valid fluid id. "fluid": "minecraft:water", + // The loader generally expects two textures: base and fluid. + "textures": { + // The base container texture, i.e. the equivalent of an empty bucket. + "base": "examplemod:item/custom_container", + // The fluid texture, i.e. the equivalent of water in a bucket. + "fluid": "examplemod:item/custom_container_fluid" + }, // Optional, defaults to false. Whether to flip the model upside down, for gaseous fluids. "flip_gas": true, // Optional, defaults to true. Whether to have the cover act as the mask. "cover_is_mask": false, // Optional, defaults to true. Whether to apply the fluid's luminosity to the item model. - "apply_fluid_luminosity": false + "apply_fluid_luminosity": false, +} +``` + +Very often, dynamic fluid container models will directly use the bucket model. This is done by specifying the `neoforge:item_bucket` parent model, like so: + +```json5 +{ + "loader": "neoforge:fluid_container", + "parent": "neoforge:item/bucket", + // Replace with your own fluid. + "fluid": "minecraft:water" + // Optional properties here. Note that the textures are handled by the parent. } ``` diff --git a/docs/resources/index.md b/docs/resources/index.md index 1d6e89b4..3dc46418 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -6,7 +6,7 @@ Minecraft generally has two kinds of resources: client-side resources, known as Both resource and data packs normally require a [`pack.mcmeta` file][packmcmeta], this was also the case in past Forge versions. However, NeoForge generates these at runtime for you, so you don't need to worry about it anymore. -If you are confused about the format of something, an easy way to change that is to have a look at the vanilla resources. Your NeoForge development environment not only contains vanilla code, but also vanilla resources. They can be found in the External Resources section (IntelliJ)/Project Libraries section (Eclipse), under the name `ng_dummy_ng.net.minecraft:client:client-extra:` (for Minecraft resources) or `ng_dummy_ng.net.neoforged:neoforge:` (for NeoForge resources). +If you are confused about the format of something, an easy way to change that is to have a look at the vanilla resources. Your NeoForge development environment not only contains vanilla code, but also vanilla resources. They can be found in the External Resources section (IntelliJ)/Project Libraries section (Eclipse), under the name `ng_dummy_ng.net.minecraft:client:client-extra:` (for Minecraft resources) or `ng_dummy_ng.net.neoforged:neoforge:` (for NeoForge resources). ## Assets @@ -32,15 +32,16 @@ There is currently no built-in way to apply a set of custom data packs to every Data packs may contain folders with files affecting the following things (TODO add links): -| Folder name | Contents | -|-------------------------------------------|----------------| -| `advancements` | Advancements | -| `damage_type` | Damage types | -| `loot_tables` | Loot tables | -| `recipes` | Recipes | -| `structures` | Structures | -| `tags` | Tags | -| `dimension`, `dimension_type`, `worldgen` | Worldgen files | +| Folder name | Contents | +|-----------------------------------------------------------------------|------------------------------| +| `advancements` | [Advancements][advancements] | +| `damage_type` | Damage types | +| `loot_tables` | [Loot tables][loottables] | +| `recipes` | [Recipes][recipes] | +| `structures` | Structures | +| `tags` | [Tags][tags] | +| `dimension`, `dimension_type`, `worldgen`, `neoforge/biome_modifiers` | Worldgen files | +| `neoforge/global_loot_modifiers` | [Global loot modifiers][glm] | Additionally, they may also contain subfolders for some systems that integrate with commands. These systems are rarely used in conjunction with mods, but worth mentioning regardless: @@ -72,13 +73,13 @@ All data providers extend the `DataProvider` interface and usually require one m | [`LanguageProvider`][langprovider] | `addTranslations()` | Translations | Client | Also requires passing the language in the constructor. | | [`ParticleDescriptionProvider`][particleprovider] | `addDescriptions()` | Particle definitions | Client | | | [`SoundDefinitionsProvider`][soundprovider] | `registerSounds()` | Sound definitions | Client | | -| `AdvancementProvider` | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | -| `LootTableProvider` | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | -| `RecipeProvider` | `buildRecipes(RecipeOutput)` | Recipes | Server | | -| Various subclasses of `TagsProvider` | `addTags(HolderLookup.Provider)` | Tags | Server | Several specialized subclasses exist, for example `BlockTagsProvider`. If the one you need doesn't exist, extend `TagsProvider` (or `IntrinsicHolderTagsProvider` if applicable) with your tag type as the generic parameter. | +| [`AdvancementProvider`][advancementprovider] | `generate()` | Advancements | Server | Make sure to use the NeoForge variant, not the Minecraft one. | +| [`LootTableProvider`][loottableprovider] | `generate()` | Loot tables | Server | Requires extra methods and classes to work properly, see linked article for details. | +| [`RecipeProvider`][recipeprovider] | `buildRecipes(RecipeOutput)` | Recipes | Server | | +| [Various subclasses of `TagsProvider`][tagsprovider] | `addTags(HolderLookup.Provider)` | Tags | Server | Several specialized subclasses exist, for example `BlockTagsProvider`. If the one you need doesn't exist, extend `TagsProvider` (or `IntrinsicHolderTagsProvider` if applicable) with your tag type as the generic parameter. | | [`DatapackBuiltinEntriesProvider`][datapackprovider] | N/A | Datapack builtin entries, e.g. worldgen | Server | See linked article for details. | -| `DataMapProvider` | `gather()` | Data map entries | Server | | -| `GlobalLootModifierProvider` | `start()` | Global loot modifiers | Server | | +| [`DataMapProvider`][datamapprovider] | `gather()` | Data map entries | Server | | +| [`GlobalLootModifierProvider`][glmprovider] | `start()` | Global loot modifiers | Server | | All of these providers follow the same pattern. First, you create a subclass and add your own resources to be generated. Then, you add the provider to the event in an [event handler][eventhandler]. An example using a `RecipeProvider`: @@ -156,18 +157,41 @@ runs { } ``` +For example, to replicate the default arguments, you could specify the following: + +```groovy +runs { + // other run configurations here + + data { + programArguments.addAll '--mod', 'examplemod', // insert your own mod id + '--output', file('src/generated/resources').getAbsolutePath(), + '--includeClient', + '--includeServer' + } +} +``` + +[advancementprovider]: ../datagen/advancements.md +[advancements]: server/advancements.md [blockstateprovider]: client/models/datagen.md#block-model-datagen [bsfile]: client/models/index.md#blockstate-files [chattype]: https://minecraft.wiki/w/Chat_type +[datamap]: ../datamaps/index.md +[datamapprovider]: ../datamaps/index.md#datagen [datapackcmd]: https://minecraft.wiki/w/Commands/datapack [datapackprovider]: ../concepts/registries.md#data-generation-for-datapack-registries [event]: ../concepts/events.md [eventhandler]: ../concepts/events.md#registering-an-event-handler [function]: https://minecraft.wiki/w/Function_(Java_Edition) +[glm]: server/glm.md +[glmprovider]: ../datagen/glm.md [itemmodelprovider]: client/models/datagen.md#item-model-datagen [itemmodifier]: https://minecraft.wiki/w/Item_modifier [langprovider]: client/i18n.md#datagen [lifecycle]: ../concepts/events.md#the-mod-lifecycle +[loottableprovider]: ../datagen/loottables.md +[loottables]: server/loottables.md [mcwiki]: https://minecraft.wiki [mcwikidatapacks]: https://minecraft.wiki/w/Data_pack [mcwikiresourcepacks]: https://minecraft.wiki/w/Resource_pack @@ -175,11 +199,15 @@ runs { [packmcmeta]: #packmcmeta [packmcmetadatapack]: https://minecraft.wiki/w/Data_pack#pack.mcmeta [packmcmetaresourcepack]: https://minecraft.wiki/w/Resource_pack#Contents -[particles]: client/particles.md [particleprovider]: client/particles.md#datagen +[particles]: client/particles.md [predicate]: https://minecraft.wiki/w/Predicate +[recipeprovider]: ../datagen/recipes.md +[recipes]: server/recipes/index.md [sides]: ../concepts/sides.md [soundprovider]: client/sounds.md#datagen [sounds]: client/sounds.md +[tags]: server/tags.md +[tagsprovider]: ../datagen/tags.md [textures]: client/textures.md [translations]: client/i18n.md#language-files From b3b1a7baa4a73a31b386474734faaa4febf39820 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 28 Mar 2024 18:49:42 +0100 Subject: [PATCH 37/41] remove TODOs --- docs/resources/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/index.md b/docs/resources/index.md index 3dc46418..af979238 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -30,7 +30,7 @@ NeoForge automatically applies all mod data packs to a new world upon creation. There is currently no built-in way to apply a set of custom data packs to every world. However, there are a number of mods that achieve this. ::: -Data packs may contain folders with files affecting the following things (TODO add links): +Data packs may contain folders with files affecting the following things: | Folder name | Contents | |-----------------------------------------------------------------------|------------------------------| @@ -64,7 +64,7 @@ Data generation, colloquially known as datagen, is a way to programmatically gen Datagen is run through the Data run configuration, which is generated for you alongside the Client and Server run configurations. The data run configuration follows the [mod lifecycle][lifecycle] until after the registry events are fired. It then fires the [`GatherDataEvent`][event], in which you can register your to-be-generated objects in the form of data providers, writes said objects to disk, and ends the process. -All data providers extend the `DataProvider` interface and usually require one method to be overridden. The following is a list of all data generators Minecraft and NeoForge offer (the linked articles add further information, such as helper methods, TODO add links): +All data providers extend the `DataProvider` interface and usually require one method to be overridden. The following is a list of all data generators Minecraft and NeoForge offer (the linked articles add further information, such as helper methods): | Class | Method | Generates | Side | Notes | |------------------------------------------------------|----------------------------------|-----------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| From a57f32d264bf9913cc841175a96d44ebff52e396 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Sun, 31 Mar 2024 21:05:11 +0200 Subject: [PATCH 38/41] Champ's reviews, part 2 --- docs/resources/client/models/bakedmodel.md | 18 +++++++++--------- docs/resources/client/models/index.md | 5 +++-- docs/resources/client/particles.md | 8 +++++++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/resources/client/models/bakedmodel.md b/docs/resources/client/models/bakedmodel.md index 96cb2682..240d61bd 100644 --- a/docs/resources/client/models/bakedmodel.md +++ b/docs/resources/client/models/bakedmodel.md @@ -28,15 +28,15 @@ Be aware that this method is called very often (several times per model and fram Other methods in `BakedModel` that you may override and/or query include: -| Signature | Effect | -|-------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `boolean useAmbientOcclusion()` | Whether to use [ambient occlusion][ao] or not. Has an override that accepts a `BlockState` parameter, and an override that accepts a `BlockState` and a `RenderType` parameter. | -| `boolean isGui3d()` | Whether to use a 3d renderer for rendering in GUIs or not. Also affects GUI lighting. | -| `boolean usesBlockLight()` | Whether to use block light when lighting the model or not. | -| `boolean isCustomRenderer()` | If true, skips normal rendering and calls an associated [block entity renderer][ber]'s `renderByItem` method instead. If false, renders through the default renderer. | -| `ItemOverrides getOverrides()` | Returns the [`ItemOverrides`][itemoverrides] associated with this model. This is generally only relevant on item models. | -| `ModelData getModelData(BlockAndTintGetter, BlockPos, BlockState, ModelData)` | Returns the model data to use for the model. Mainly used for [block entity renderers][ber]. | -| `TextureAtlasSprite getParticleIcon(ModelData)` | Returns the particle sprite to use for the model. May use the model data to use different particle sprites for different model data values. NeoForge-added, replacing the vanilla overload with no parameters. | +| Signature | Effect | +|-------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `boolean useAmbientOcclusion()` | Whether to use [ambient occlusion][ao] or not. Has an override that accepts a `BlockState` parameter, and an override that accepts a `BlockState` and a `RenderType` parameter. | +| `boolean isGui3d()` | Whether to use a 3d renderer for rendering in GUIs or not. Also affects GUI lighting. | +| `boolean usesBlockLight()` | Whether to use block light when lighting the model or not. | +| `boolean isCustomRenderer()` | If true, skips normal rendering and calls an associated [block entity renderer][ber]'s `renderByItem` method instead. If false, renders through the default renderer. | +| `ItemOverrides getOverrides()` | Returns the [`ItemOverrides`][itemoverrides] associated with this model. This is generally only relevant on item models. | +| `ModelData getModelData(BlockAndTintGetter, BlockPos, BlockState, ModelData)` | Returns the model data to use for the model. Mainly used for [block entity renderers][ber]. | +| `TextureAtlasSprite getParticleIcon(ModelData)` | Returns the particle sprite to use for the model. May use the model data to use different particle sprites for different model data values. NeoForge-added, replacing the vanilla `getParticleIcon()` overload with no parameters. | ## Perspectives diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index 8d6a7fc0..3ed3ca1f 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -237,8 +237,9 @@ public static void registerBlockColorHandlers(RegisterColorHandlersEvent.Block e // Parameters are the block's state, the level the block is in, the block's position, and the tint index. event.register((state, level, pos, tintIndex) -> { // Replace with your own calculation. See the BlockColors class for vanilla references. - // Generally, if the tint index is -1, it means that no tinting should take place - // and a default value should be used instead. + // All vanilla uses assume alpha 255 (= 1f), but modded consumers may also account + // for alpha values specified here. Generally, if the tint index is -1, + // it means that no tinting should take place and a default value should be used instead. return 0xFFFFFF; }); } diff --git a/docs/resources/client/particles.md b/docs/resources/client/particles.md index 0199f1b9..ff8176b8 100644 --- a/docs/resources/client/particles.md +++ b/docs/resources/client/particles.md @@ -116,9 +116,15 @@ Finally, we must associate our particle type with a texture. Similar to how item } ``` +Note that a particle definition file is only necessary when using a sprite set particle. Single sprite particles directly map to the texture file at `assets//textures/particle/.png`, and special particle providers can do whatever you want anyway. + +:::danger +A mismatched list of sprite set particle factories and particle definition files, i.e. a particle description without a corresponding particle factory, or vice versa, will throw an exception! +::: + ### Datagen -These JSON files can also be [datagenned][datagen] by extending `ParticleDescriptionProvider` and overriding the `#addDescriptions()` method: +Particle definition files can also be [datagenned][datagen] by extending `ParticleDescriptionProvider` and overriding the `#addDescriptions()` method: ```java public class MyParticleDescriptionProvider extends ParticleDescriptionProvider { From 40de6980f5e42b956dff92fc01b1cae79dfce199 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 8 Apr 2024 19:04:01 +0200 Subject: [PATCH 39/41] apply Embeddedt's and XFactHD's feedback --- docs/resources/client/i18n.md | 6 +-- docs/resources/client/models/bakedmodel.md | 49 +++++++++++++------- docs/resources/client/models/datagen.md | 12 ++--- docs/resources/client/models/index.md | 40 +++++++++------- docs/resources/client/models/modelloaders.md | 32 +++++++------ docs/resources/client/particles.md | 6 +-- docs/resources/client/sounds.md | 9 ++-- docs/resources/client/textures.md | 32 +++++++++++-- docs/resources/index.md | 4 +- 9 files changed, 120 insertions(+), 70 deletions(-) diff --git a/docs/resources/client/i18n.md b/docs/resources/client/i18n.md index 24cb5d37..e5be489e 100644 --- a/docs/resources/client/i18n.md +++ b/docs/resources/client/i18n.md @@ -17,7 +17,7 @@ A `Component` is a piece of text with metadata, with the metadata including thin | `score` | Creates a component containing a scoreboard objective value. | | `selector` | Creates a component containing a list of entity names for a given [entity selector][selector]. | -`Component.translatable()` additionally has a vararg parameter that accepts string interpolation elements that work exactly like the ones in Java's `String#format`. +`Component.translatable()` additionally has a vararg parameter that accepts string interpolation elements. This works similar to Java's `String#format`, but always uses `%s` instead of `%i`, `%d`, `%f` and any other format specifier, calling `#toString()` where needed. Every `Component` can be resolved using `#getString()`. Resolving is generally lazy, meaning that the server can specify a `Component`, send it to the clients, and the clients will each resolve the `Component`s on their own (where different languages may result in different text). Many places in Minecraft will also directly accept `Component`s and take care of resolving for you. @@ -29,7 +29,7 @@ Never let a server translate a `Component`. Always send `Component`s to the clie `Component`s can be formatted using `Style`s. `Style`s are immutable, creating a new `Style` object when modified, and thus allowing them to be created once and then be reused as needed. -`Style.EMPTY` can generally be used as a base to work off. Multiple `Style`s can be merged with `Style#applyTo(Style other)`, where the given `Style` will set its configured values on the `Style` it is called on. `Style`s can then be applied to components like so: +`Style.EMPTY` can generally be used as a base to work off. Multiple `Style`s can be merged with `Style#applyTo(Style other)`, which returns a new `Style` that takes settings from the `Style` the `applyTo()` method was called on unless the respective setting is absent, in which case the setting from the `Style` passed in as a parameter is used. `Style`s can then be applied to components like so: ```java Component text = Component.literal("Hello World!"); @@ -104,7 +104,7 @@ A language file generally looks like this: Translation keys are the keys used in translations. In many cases, they follow the format `registry.modid.name`. For example, a mod with the id `examplemod` that provides a block named `example_block` will probably want to provide translations for the key `block.examplemod.example_block`. However, you can use basically any string as a translation key. -If a translation key does not have an associated translation in the given language, the game will fall back to US English (`en_us`), if that is not already the selected language. If US English does not have a translation either, the translation will fail silently, and the raw translation key will be displayed instead. +If a translation key does not have an associated translation in the selected language, the game will fall back to US English (`en_us`), if that is not already the selected language. If US English does not have a translation either, the translation will fail silently, and the raw translation key will be displayed instead. Some places in Minecraft offer you helper methods to get a translation keys. For example, both blocks and items provide `#getDescriptionId` methods. These can not only be queried, but also overridden if needed. A common use case are items that have different names depending on their [NBT][nbt] value. These will usually override the variant of `#getDescriptionId` that has an [`ItemStack`][itemstack] parameter, and return different values based on the stack's NBT. Another common use case are `BlockItem`s, which override the method to use the associated block's translation key instead. diff --git a/docs/resources/client/models/bakedmodel.md b/docs/resources/client/models/bakedmodel.md index 240d61bd..0edaa0a2 100644 --- a/docs/resources/client/models/bakedmodel.md +++ b/docs/resources/client/models/bakedmodel.md @@ -1,6 +1,8 @@ # Baked Models -`BakedModel`s are the in-code representation of a shape with textures. They can be obtained from multiple sources, for example by calling `UnbakedModel#bake` (default model loader) or `IUnbakedGeometry#bake` ([custom model loaders][modelloader]). Some [block entity renderers][ber] also make use of baked models. There is no limit to how complex a model may be. +`BakedModel`s are the in-code representation of a shape with textures. They can originate from multiple sources, for example from a call to `UnbakedModel#bake` (default model loader) or `IUnbakedGeometry#bake` ([custom model loaders][modelloader]). Some [block entity renderers][ber] also make use of baked models. There is no limit to how complex a model may be. + +Models are stored in the `ModelManager`, which can be accessed through `Minecraft.getInstance().modelManager`. Then, you can call `ModelManager#getModel` to get a certain model by its [`ResourceLocation`][rl] or [`ModelResourceLocation`][mrl]. Mods will basically always reuse a model that was previously automatically loaded and baked. ## Methods of `BakedModel` @@ -9,12 +11,12 @@ The most important method of a baked model is `getQuads`. This method is responsible for returning a list of `BakedQuad`s, which can then be sent to the GPU. A quad compares to a triangle in a modeling program (and in most other games), however due to Minecraft's general focus on squares, the developers elected to use quads (4 vertices) instead of triangles (3 vertices) for rendering in Minecraft. `getQuads` has five parameters that can be used: - A `BlockState`: The [blockstate] being rendered. May be null, indicating that an item is being rendered. -- A `Direction`: The direction of the face being culled against. May be null, indicating that no culling should occur. Will always be null for items. +- A `Direction`: The direction of the face being culled against. May be null, which means quads that cannot be occluded should be returned. - A `RandomSource`: A client-bound random source you can use for randomization. -- A `ModelData`: The extra model data to use. This may contain additional data from the block entity needed for rendering. -- A `RenderType`: The [render type][rendertype] to use for rendering the block. May be null, indicating that the default render type should be used. The default render type is looked up in `ItemBlockRenderTypes`, and if no render type is found there either, `minecraft:solid` is used. Do never call on `ItemBlockRenderTypes` yourself! +- A `ModelData`: The extra model data to use. This may contain additional data from the block entity needed for rendering. Supplied by `BakedModel#getModelData`. +- A `RenderType`: The [render type][rendertype] to use for rendering the block. May be null, indicating that the default render type should be used. The default render type is looked up in `ItemBlockRenderTypes`, and if no render type is found there either, `minecraft:solid` is used. You should never call on `ItemBlockRenderTypes` yourself! -Be aware that this method is called very often (several times per model and frame), and as such should cache as much as possible. +Models should heavily cache. This is because even though chunks are only rebuilt when a block in them changes, the computations done in this method still need to be as fast as possible and should ideally be cached heavily due to the amount of times this method will be called per chunk section (up to seven times per RenderType used by a given model * amount of RenderTypes used by the respective model * 4096 blocks per chunk section). In addition, [BERs][ber] or entity renderers may actually call this method several times per frame. ### `applyTransform` and `getTransforms` @@ -24,27 +26,30 @@ Be aware that this method is called very often (several times per model and fram - A `PoseStack`: The pose stack used for rendering. - A `boolean`: Whether to use modified values for left-hand rendering instead of the default right hand rendering; `true` if the rendered hand is the left hand (off hand, or main hand if left hand mode is enabled in the options) +:::note +`applyTransform` and `getTransforms` only apply to item models. +::: + ### Others Other methods in `BakedModel` that you may override and/or query include: -| Signature | Effect | -|-------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `boolean useAmbientOcclusion()` | Whether to use [ambient occlusion][ao] or not. Has an override that accepts a `BlockState` parameter, and an override that accepts a `BlockState` and a `RenderType` parameter. | -| `boolean isGui3d()` | Whether to use a 3d renderer for rendering in GUIs or not. Also affects GUI lighting. | -| `boolean usesBlockLight()` | Whether to use block light when lighting the model or not. | -| `boolean isCustomRenderer()` | If true, skips normal rendering and calls an associated [block entity renderer][ber]'s `renderByItem` method instead. If false, renders through the default renderer. | -| `ItemOverrides getOverrides()` | Returns the [`ItemOverrides`][itemoverrides] associated with this model. This is generally only relevant on item models. | -| `ModelData getModelData(BlockAndTintGetter, BlockPos, BlockState, ModelData)` | Returns the model data to use for the model. Mainly used for [block entity renderers][ber]. | -| `TextureAtlasSprite getParticleIcon(ModelData)` | Returns the particle sprite to use for the model. May use the model data to use different particle sprites for different model data values. NeoForge-added, replacing the vanilla `getParticleIcon()` overload with no parameters. | +| Signature | Effect | +|-------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `TriState useAmbientOcclusion()` | Whether to use [ambient occlusion][ao] or not. Accepts a `BlockState`, `RenderType` and `ModelData` parameter and returns a `TriState` which allows not only force-disabling AO but also force-enabling AO. Has two overloads that each return a `boolean` parameter and accept either only a `BlockState` or no parameters at all; both of these are deprecated for removal in favor of the first variant. | +| `boolean isGui3d()` | Whether this model renders as 3d or flat in GUI slots. | +| `boolean usesBlockLight()` | Whether to use 3D lighting (`true`) or flat lighting from the front (`false`) when lighting the model. | +| `boolean isCustomRenderer()` | If true, skips normal rendering and calls an associated [`BlockEntityWithoutLevelRenderer`][bewlr]'s `renderByItem` method instead. If false, renders through the default renderer. | +| `ItemOverrides getOverrides()` | Returns the [`ItemOverrides`][itemoverrides] associated with this model. This is only relevant on item models. | +| `ModelData getModelData(BlockAndTintGetter, BlockPos, BlockState, ModelData)` | Returns the model data to use for the model. This method is passed an existing `ModelData` that is either the result of `BlockEntity#getModelData()` if the block has an associated block entity, or `ModelData.EMPTY` if that is not the case. This method can be used for blocks that need model data, but do not have a block entity, for example for blocks with connected textures. | +| `TextureAtlasSprite getParticleIcon(ModelData)` | Returns the particle sprite to use for the model. May use the model data to use different particle sprites for different model data values. NeoForge-added, replacing the vanilla `getParticleIcon()` overload with no parameters. | ## Perspectives -Minecraft's render engine recognizes a total of 8 perspective types. These are used in a model JSON's `display` block, and represented in code through the `ItemDisplayContext` enum. +Minecraft's render engine recognizes a total of 8 perspective types (9 if you include the in-code fallback) for item rendering. These are used in a model JSON's `display` block, and represented in code through the `ItemDisplayContext` enum. | Enum value | JSON key | Usage | |---------------------------|---------------------------|------------------------------------------------------------------------------------------------------------------| -| `NONE` | `"none"` | Fallback purposes in code, should not be used in JSON | | `THIRD_PERSON_RIGHT_HAND` | `"thirdperson_righthand"` | Right hand in third person (F5 view, or on other players) | | `THIRD_PERSON_LEFT_HAND` | `"thirdperson_lefthand"` | Left hand in third person (F5 view, or on other players) | | `FIRST_PERSON_RIGHT_HAND` | `"firstperson_righthand"` | Right hand in first person | @@ -53,13 +58,14 @@ Minecraft's render engine recognizes a total of 8 perspective types. These are u | `GUI` | `"gui"` | Inventories, player hotbar | | `GROUND` | `"ground"` | Dropped items; note that the rotation of the dropped item is handled by the dropped item renderer, not the model | | `FIXED` | `"fixed"` | Item frames | +| `NONE` | `"none"` | Fallback purposes in code, should not be used in JSON | ## `ItemOverrides` `ItemOverrides` is a class that provides a way for baked models to process the state of an [`ItemStack`][itemstack] and return a new baked model through the `#resolve` method. `#resolve` has five parameters: - A `BakedModel`: The original model. -- An `ItemStack`: The affected item stack. +- An `ItemStack`: The item stack being rendered. - A `ClientLevel`: The level the model is being rendered in. This should only be used for querying the level, not mutating it in any way. May be null. - A `LivingEntity`: The entity the model is rendered on. May be null, e.g. when rendering from a [block entity renderer][ber]. - An `int`: A seed for randomizing. @@ -89,7 +95,7 @@ After writing your model wrapper class, you must apply the wrappers to the model @SubscribeEvent public static void modifyBakingResult(ModelEvent.ModifyBakingResult event) { // For block models - event.getModels().computeIfPresent( + event.getModels().computeIfPresent( // The model resource location of the model to modify. Get it from // BlockModelShaper#stateToModelLocation with the blockstate to be affected as a parameter. BlockModelShaper.stateToModelLocation(MyBlocksClass.EXAMPLE_BLOCK.defaultBlockState()), @@ -106,12 +112,19 @@ public static void modifyBakingResult(ModelEvent.ModifyBakingResult event) { } ``` +:::warning +It is generally encouraged to use a [custom model loader][modelloader] over baked model wrappers when possible. Custom model loaders can also use `BakedModelWrapper`s if needed. +::: + [ao]: https://en.wikipedia.org/wiki/Ambient_occlusion [ber]: ../../../blockentities/ber.md +[bewlr]: ../../../items/bewlr.md [blockstate]: ../../../blocks/states.md [itemoverrides]: #itemoverrides [itemstack]: ../../../items/index.md#itemstacks [modelloader]: modelloaders.md +[mrl]: ../../../misc/resourcelocation.md#modelresourcelocations [overrides]: index.md#overrides [perspective]: #perspectives [rendertype]: index.md#render-types +[rl]: ../../../misc/resourcelocation.md diff --git a/docs/resources/client/models/datagen.md b/docs/resources/client/models/datagen.md index d1a42549..a80d5971 100644 --- a/docs/resources/client/models/datagen.md +++ b/docs/resources/client/models/datagen.md @@ -24,7 +24,7 @@ While elaborate and complex models can be created through datagen, it is recomme ### `ModelProvider` -Both block and item model datagen utilize subclasses of `ModelProvider`, named `BlockModelProvider` and `ItemModelProvider`, respectively. While item model datagen directly extends `ItemModelProvider`, block model datagen uses the `BlockStateProvider` base class, which has an internal `BlockModelProvider` that can be accessed via `BlockStateProvider#models()`. The most important part of `ModelProvider` is the `getBuilder(String path)` method, which returns a `BlockModelBuilder` (or `ItemModelBuilder`) at the given location. +Both block and item model datagen utilize subclasses of `ModelProvider`, named `BlockModelProvider` and `ItemModelProvider`, respectively. While item model datagen directly extends `ItemModelProvider`, block model datagen uses the `BlockStateProvider` base class, which has an internal `BlockModelProvider` that can be accessed via `BlockStateProvider#models()`. Additionally, `BlockStateProvider` also has its own internal `ItemModelProvider`, accessible via `BlockStateProvider#itemModels()`. The most important part of `ModelProvider` is the `getBuilder(String path)` method, which returns a `BlockModelBuilder` (or `ItemModelBuilder`) at the given location. However, `ModelProvider` also contains various helper methods. The most important helper method is probably `withExistingParent(String name, ResourceLocation parent)`, which returns a new builder (via `getBuilder(name)`) and sets the given `ResourceLocation` as model parent. Two other very common helpers are `mcLoc(String name)`, which returns a `ResourceLocation` with the namespace `minecraft` and the given name as path, and `modLoc(String name)`, which does the same but with the provider's mod id (so usually your mod id) instead of `minecraft`. Furthermore, it provides various helper methods that are shortcuts for `#withExistingParent` for common things such as slabs, stairs, fences, doors, etc. @@ -66,7 +66,7 @@ public class MyBlockStateProvider extends BlockStateProvider { // Overload that accepts one or multiple (vararg) ConfiguredModel objects. // See below for more info about ConfiguredModel. simpleBlock(block, ConfiguredModel.builder().build()); - // Adds the given model file as an identically named item model, for a block item to pick up. + // Adds an item model file with the block's name, parenting the given model file, for a block item to pick up. simpleBlockItem(block, exampleModel); // Shorthand for calling #simpleBlock() (model file overload) and #simpleBlockItem. simpleBlockWithItem(block, exampleModel); @@ -76,7 +76,7 @@ public class MyBlockStateProvider extends BlockStateProvider { // Note that the block input here is limited to RotatedPillarBlock, which is the class vanilla logs use. logBlock(block); // Like #logBlock, but the textures are named _side.png and _end.png instead of - // .png and _end.png, respectively. Used by quartz pillars and similar blocks. + // .png and _top.png, respectively. Used by quartz pillars and similar blocks. // Has an overload that allow you to specify a different texture base name, that is then suffixed // with _side and _end as needed, an overload that allows you to specify two resource locations // for the side and end textures, and an overload that allows specifying side and end model files. @@ -84,8 +84,8 @@ public class MyBlockStateProvider extends BlockStateProvider { // Variants of #logBlock and #axisBlock that additionally allow for render types to be specified. // Comes in string and resource location variants for the render type, // in all combinations with all variants of #logBlock and #axisBlock. - logBlock(block, "minecraft:cutout"); - axisBlock(block, mcLoc("cutout_mipped")); + logBlockWithRenderType(block, "minecraft:cutout"); + axisBlockWithRenderType(block, mcLoc("cutout_mipped")); // Specifies a horizontally-rotatable block model with a side texture, a front texture, and a top texture. // The bottom will use the side texture as well. If you don't need the front or top texture, @@ -93,7 +93,7 @@ public class MyBlockStateProvider extends BlockStateProvider { horizontalBlock(block, sideTexture, frontTexture, topTexture); // Specifies a horizontally-rotatable block model with a model file that will be rotated as needed. // Has an overload that instead of a model file accepts a Function, - // allowing for different rotations to use different models. Used e.g. by the crafting table. + // allowing for different rotations to use different models. Used e.g. by the stonecutter. horizontalBlock(block, exampleModel); // Specifies a horizontally-rotatable block model that is attached to a face, e.g. for buttons or levers. // Accounts for placing the block on the ground and on the ceiling, and rotates them accordingly. diff --git a/docs/resources/client/models/index.md b/docs/resources/client/models/index.md index 3ed3ca1f..968b8e4d 100644 --- a/docs/resources/client/models/index.md +++ b/docs/resources/client/models/index.md @@ -1,6 +1,6 @@ # Models -Models are JSON files that determine the visual shape and texture(s) of a block or item. A model contains of cuboid elements, each with their own size, that then get assigned a texture to each face. +Models are JSON files that determine the visual shape and texture(s) of a block or item. A model consists of cuboid elements, each with their own size, that then get assigned a texture to each face. Each item gets an item model assigned to it by its registry name. For example, an item with the registry name `examplemod:example_item` would get the model at `assets/examplemod/models/item/example_item.json` assigned to it. For blocks, this is a bit more complicated, as they get assigned a blockstate file first. See [below][bsfile] for more information. @@ -17,9 +17,10 @@ A model is a JSON file with the following optional properties in the root tag: - `minecraft:block/cube_all`: Variant of the cube model that uses the same texture on all six sides, for example cobblestone or planks. - `minecraft:block/cube_bottom_top`: Variant of the cube model that uses the same texture on all four horizontal sides, and separate textures on the top and the bottom. Common examples include sandstone or chiseled quartz. - `minecraft:block/cube_column`: Variant of the cube model that has a side texture and a bottom and top texture. Examples include wooden logs, as well as quartz and purpur pillars. - - `minecraft:block/cross`: Model that uses two 90° rotated planes with the same texture, forming an X when viewed from above (hence the name). Examples include most plants, e.g. grass, saplings and flowers. - - `minecraft:item/generated`: Parent for classic 2D flat item models. Used by most items in the game. - - `minecraft:item/handheld`: Parent for 2D flat item models that appear to be actually held by the player. Used predominantly by tools. + - `minecraft:block/cross`: Model that uses two planes with the same texture, one rotated 45° clockwise and the other rotated 45° counter-clockwise, forming an X when viewed from above (hence the name). Examples include most plants, e.g. grass, saplings and flowers. + - `minecraft:item/generated`: Parent for classic 2D flat item models. Used by most items in the game. Ignores an `elements` block since its quads are generated from the textures. + - `minecraft:item/handheld`: Parent for 2D flat item models that appear to be actually held by the player. Used predominantly by tools. Submodel of `item/generated`, which causes it to ignore the `elements` block as well. + - `minecraft:builtin/entity`: Specifies no textures other than `particle`. If this is the parent, [`BakedModel#isCustomRenderer()`][iscustomrenderer] returns `true` to allow use of a [`BlockEntityWithoutLevelRenderer`][bewlr]. - Block items commonly (but not always) use their corresponding block models as parent. For example, the cobblestone item model uses the parent `minecraft:block/cobblestone`. - `ambientocclusion`: Whether to enable [ambient occlusion][ao] or not. Only effective on block models. Defaults to `true`. If your custom block model has weird shading, try setting this to `false`. - `render_type`: See [Render Types][rendertype]. @@ -33,6 +34,7 @@ A model is a JSON file with the following optional properties in the root tag: - `translation`: The translation of the model, specified as `[x, y, z]`. - `rotation`: The rotation of the model, specified as `[x, y, z]`. - `scale`: The scale of the model, specified as `[x, y, z]`. + - `right_rotation`: NeoForge-added. A second rotation that is applied after scaling, specified as `[x, y, z]`. - `transform`: See [Root Transforms][roottransforms]. :::tip @@ -41,12 +43,12 @@ If you're having trouble finding out how exactly to specify something, have a lo ### Render Types -Using the optional NeoForge-added `render_type` field, you can set a render type for your model. If this is not set (as is the case in all vanilla models), the game will fall back to a list of hardcoded render types. If that list doesn't contain the render type for that block either, it will fall back to `minecraft:solid`. Vanilla provides the following default render types: +Using the optional NeoForge-added `render_type` field, you can set a render type for your model. If this is not set (as is the case in all vanilla models), the game will fall back to the render types hardcoded in `ItemBlockRenderTypes`. If `ItemBlockRenderTypes` doesn't contain the render type for that block either, it will fall back to `minecraft:solid`. Vanilla provides the following default render types: - `minecraft:solid`: Used for fully solid blocks, such as stone. -- `minecraft:cutout`: Used for blocks where all pixels are either fully solid or fully transparent, i.e. with either full or no transparency, for example glass. -- `minecraft:cutout_mipped`: Variant of `minecraft:cutout` that will scale down textures at large distances to avoid visual artifacts ([mipmapping]). Used for example by leaves. -- `minecraft:cutout_mipped_all`: Variant of `minecraft:cutout_mipped` where a corresponding item should be mipmapped as well. +- `minecraft:cutout`: Used for blocks where any pixel is either fully solid or fully transparent, i.e. with either full or no transparency, for example glass. +- `minecraft:cutout_mipped`: Variant of `minecraft:cutout` that will scale down textures at large distances to avoid visual artifacts ([mipmapping]). Does not apply the mipmapping to item rendering, as it is usually undesired on items and may cause artifacts. Used for example by leaves. +- `minecraft:cutout_mipped_all`: Variant of `minecraft:cutout_mipped` which applies mipmapping to item models as well. - `minecraft:translucent`: Used for blocks where any pixel may be partially transparent, for example stained glass. - `minecraft:tripwire`: Used by blocks with the special requirement of being rendered to the weather target, i.e. tripwire. @@ -64,7 +66,7 @@ If you want, you can also add your own render types. To do so, subscribe to the An element is a JSON representation of a cuboid object. It has the following properties: - `from`: The coordinate of the start corner of the cuboid, specified as `[x, y, z]`. Specified in 1/16 block units. For example, `[0, 0, 0]` would be the "bottom left" corner, `[8, 8, 8]` would be the center, and `[16, 16, 16]` would be the "top right" corner of the block. -- `to`: The coordinate of the start corner of the cuboid, specified as `[x, y, z]`. Like `from`, this is specified in 1/16 block units. +- `to`: The coordinate of the end corner of the cuboid, specified as `[x, y, z]`. Like `from`, this is specified in 1/16 block units. :::tip Values in `from` and `to` are limited by Minecraft to the range `[-16, 32]`. However, it is highly discouraged to exceed `[0, 16]`, as that will lead to lighting and/or culling issues. @@ -81,7 +83,7 @@ Values in `from` and `to` are limited by Minecraft to the range `[-16, 32]`. How Additionally, it can specify the following optional properties: -- `shade`: Only for block models. Optional. Whether shadows should be rendered on this face or not. Defaults to true. +- `shade`: Only for block models. Optional. Whether the faces of this element should have direction-dependent shading on it or not. Defaults to true. - `rotation`: A rotation of the object, specified as a sub object containing the following data: - `angle`: The rotation angle, in degrees. Can be -45 through 45 in steps of 22.5 degrees. - `axis`: The axis to rotate around. It is currently not possible to rotate an object around more than one axis. @@ -121,7 +123,7 @@ Using the custom `neoforge:item_layers` loader, you can also specify extra face Item overrides can assign a different model to an item based on a float value, called the override value. For example, bows and crossbows use this to change the texture depending on how long they have been drawn. Overrides have both a model and a code side to them. -The model can specify one or multiple override models that should be used when the override value is equal to or greater than the given threshold value. For example, the bow uses two different properties `pulling` and `pull`. `pulling` is treated as a boolean value, with 1 being interpreted as pulling and 0 as not pulling, while `pull` represents how much the bow is currently pulled. It then uses these properties to specify usage of three alternative models when charged to below 65% (`pulling` 1, no `pull` value), 65% (`pulling` 1, `pull` 0.65) and 90% (`pulling` 1, `pull` 0.9). If multiple models apply (because the value keeps on becoming bigger), the first in the list matches, so make sure your order is correct. The overrides look as follows: +The model can specify one or multiple override models that should be used when the override value is equal to or greater than the given threshold value. For example, the bow uses two different properties `pulling` and `pull`. `pulling` is treated as a boolean value, with 1 being interpreted as pulling and 0 as not pulling, while `pull` represents how much the bow is currently pulled. It then uses these properties to specify usage of three alternative models when charged to below 65% (`pulling` 1, no `pull` value), 65% (`pulling` 1, `pull` 0.65) and 90% (`pulling` 1, `pull` 0.9). If multiple models apply (because the value keeps on becoming bigger), the last element of the list matches, so make sure your order is correct. The overrides look as follows: ```json5 { @@ -215,12 +217,13 @@ _See also: [Blockstate files][mcwikiblockstate] on the [Minecraft Wiki][mcwiki]_ Blockstate files are used by the game to assign different models to different [blockstates]. There must be exactly one blockstate file per block registered to the game. Specifying block models for blockstates works in two mutually exclusive ways: via variants or via multipart. -Inside a `variants` block, there is an element for each blockstate. This is the predominant way of associating blockstates with models, used by the vast majority of blocks. The string representation of the blockstate (without the block name, so for example `"type=top,waterlogged=false"` for a non-waterlogged top slab, or `""` for a block with no properties) is the key, while the element is either a single model object or an array of model objects. If an array of model objects is used, a model will be randomly chosen from it. A model object consists of the following data: - -- `model`: A path to a model file location, relative to the namespace's `models` folder, for example `minecraft:block/cobblestone`. -- `x` and `y`: Rotation of the model on the x-axis/y-axis. Limited to steps of 90 degrees. Optional each, defaults to 0. -- `uvlock`: Whether to lock the UVs of the model when rotating or not. Optional, defaults to false. -- `weight`: Only useful with arrays of model objects. Gives the object a weight, used when choosing a random model object. Optional, defaults to 1. +Inside a `variants` block, there is an element for each blockstate. This is the predominant way of associating blockstates with models, used by the vast majority of blocks. +- The key is the string representation of the blockstate without the block name, so for example `"type=top,waterlogged=false"` for a non-waterlogged top slab, or `""` for a block with no properties. It is worth noting that unused properties may be omitted. For example, if the `waterlogged` property has no influence on the model chosen, two objects `type=top,waterlogged=false` and `type=top,waterlogged=true` may be collapsed into one `type=top` object. This also means that an empty string is valid for every block. +- The value is either a single model object or an array of model objects. If an array of model objects is used, a model will be randomly chosen from it. A model object consists of the following data: + - `model`: A path to a model file location, relative to the namespace's `models` folder, for example `minecraft:block/cobblestone`. + - `x` and `y`: Rotation of the model on the x-axis/y-axis. Limited to steps of 90 degrees. Optional each, defaults to 0. + - `uvlock`: Whether to lock the UVs of the model when rotating or not. Optional, defaults to false. + - `weight`: Only useful with arrays of model objects. Gives the object a weight, used when choosing a random model object. Optional, defaults to 1. In contrast, inside a `multipart` block, elements are combined depending on the properties of the blockstate. This method is mainly used by fences and walls, who enable the four directional parts based on boolean properties. A multipart element consists of two parts: a `when` block and an `apply` block. @@ -235,6 +238,7 @@ Some blocks, such as grass or leaves, change their texture color based on their @SubscribeEvent public static void registerBlockColorHandlers(RegisterColorHandlersEvent.Block event) { // Parameters are the block's state, the level the block is in, the block's position, and the tint index. + // The level and position may be null. event.register((state, level, pos, tintIndex) -> { // Replace with your own calculation. See the BlockColors class for vanilla references. // All vanilla uses assume alpha 255 (= 1f), but modded consumers may also account @@ -275,12 +279,14 @@ public static void registerAdditional(ModelEvent.RegisterAdditional event) { [ao]: https://en.wikipedia.org/wiki/Ambient_occlusion [ber]: ../../../blockentities/ber.md +[bewlr]: ../../../items/bewlr.md [bsfile]: #blockstate-files [custommodelloader]: modelloaders.md [elements]: #elements [event]: ../../../concepts/events.md [eventhandler]: ../../../concepts/events.md#registering-an-event-handler [extrafacedata]: #extra-face-data +[iscustomrenderer]: bakedmodel.md#others [mcwiki]: https://minecraft.wiki [mcwikiblockstate]: https://minecraft.wiki/w/Tutorials/Models#Block_states [mcwikimodel]: https://minecraft.wiki/w/Model diff --git a/docs/resources/client/models/modelloaders.md b/docs/resources/client/models/modelloaders.md index f419c81d..dcb86d90 100644 --- a/docs/resources/client/models/modelloaders.md +++ b/docs/resources/client/models/modelloaders.md @@ -6,7 +6,7 @@ The entry point for a block model remains the model JSON file. However, you can ## Builtin Model Loaders -Besides the default model loader, NeoForge offers a total of seven builtin loaders, each serving a different purpose. +Besides the default model loader, NeoForge offers several builtin loaders, each serving a different purpose. ### Composite Model @@ -48,7 +48,7 @@ To [datagen][modeldatagen] this model, use the custom loader class `CompositeMod ### Dynamic Fluid Container Model -The dynamic fluid container model, also called dynamic bucket model after its most common use case, is used for items that represent a fluid container (such as a bucket or a tank) and want to show the fluid within the model. This only works if there is a fixed amount of fluids (e.g. only lava and powder snow) that can be used, use a [block entity renderer][ber] instead if the fluid is arbitrary. +The dynamic fluid container model, also called dynamic bucket model after its most common use case, is used for items that represent a fluid container (such as a bucket or a tank) and want to show the fluid within the model. This only works if there is a fixed amount of fluids (e.g. only lava and powder snow) that can be used, use a [`BlockEntityWithoutLevelRenderer`][bewlr] instead if the fluid is arbitrary. ```json5 { @@ -246,12 +246,19 @@ public class MyGeometry implements IUnbakedGeometry { return new MyDynamicModel(context.useAmbientOcclusion(), context.isGui3d(), context.useBlockLight(), spriteGetter.apply(context.getMaterial("particle")), overrides); } + + // Method responsible for correctly resolving parent properties. Required if this model loads any nested models or reuses the vanilla loader on itself (see below). + @Override + public void resolveParents(Function modelGetter, IGeometryBakingContext context) { + base.resolveParents(modelGetter); + } } +// BakedModelWrapper can be used as well to return default values for most methods, allowing you to only override what actually needs to be overridden. public class MyDynamicModel implements IDynamicBakedModel { - // Sprite of the missing texture. Can be used as a fallback when needed. - private static final TextureAtlasSprite MISSING_TEXTURE = - new Material(TextureAtlas.LOCATION_BLOCKS, MissingTextureAtlasSprite.getLocation()).sprite(); + // Material of the missing texture. Its sprite can be used as a fallback when needed. + private static final Material MISSING_TEXTURE = + new Material(TextureAtlas.LOCATION_BLOCKS, MissingTextureAtlasSprite.getLocation()); // Attributes for use in the methods below. Optional, the methods may also use constant values if applicable. private final boolean useAmbientOcclusion; @@ -288,7 +295,7 @@ public class MyDynamicModel implements IDynamicBakedModel { @Override public TextureAtlasSprite getParticleIcon() { - // Return MISSING_TEXTURE if you don't need a particle, e.g. when in an item model context. + // Return MISSING_TEXTURE.sprite() if you don't need a particle, e.g. when in an item model context. return particle; } @@ -298,8 +305,7 @@ public class MyDynamicModel implements IDynamicBakedModel { return overrides; } - // Override this to true if you also want to use a custom block entity renderer instead of the default renderer. - // See the page on block entity renderers for more information. + // Override this to true if you want to use a custom block entity renderer instead of the default renderer. @Override public boolean isCustomRenderer() { return false; @@ -307,11 +313,11 @@ public class MyDynamicModel implements IDynamicBakedModel { // This is where the magic happens. Return a list of the quads to render here. Parameters are: // - The blockstate being rendered. May be null if rendering an item. - // - The side being culled against. May be null, which means no culling should occur. + // - The side being culled against. May be null, which means quads that cannot be occluded should be returned. // - A client-bound random source you can use for randomizing stuff. - // - The extra face data to use. - // - The render type of the model. - // NOTE: This is called very often, usually several times per block and frame. + // - The extra data to use. Originates from a block entity (if present), or from BakedModel#getModelData(). + // - The render type for which quads are being requested. + // NOTE: This may be called many times in quick succession, up to several times per block. // This should be as fast as possible and use caching wherever applicable. @Override public List getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) { @@ -415,7 +421,6 @@ public class MyGeometry implements IUnbakedGeometry { return new MyDynamicModel(bakedBase, /* other parameters here */); } - // Method responsible for correctly resolving parent properties. Unneeded if we don't reuse the default loader properties, but needed if we do, so here we go. @Override public void resolveParents(Function modelGetter, IGeometryBakingContext context) { base.resolveParents(modelGetter); @@ -452,6 +457,7 @@ public class MyDynamicModel implements IDynamicBakedModel { [bakedmodel]: bakedmodel.md [ber]: ../../../blockentities/ber.md +[bewlr]: ../../../items/bewlr.md [composite]: #composite-model [datagen]: ../../index.md#data-generation [elements]: index.md#elements diff --git a/docs/resources/client/particles.md b/docs/resources/client/particles.md index ff8176b8..1ed0126e 100644 --- a/docs/resources/client/particles.md +++ b/docs/resources/client/particles.md @@ -1,6 +1,6 @@ # Particles -Particles are 2d effects that polish the game and add immersion. They can be spawned both client and server [side], but being mostly visual in nature, critical parts exist only on the physical (and logical) client side. +Particles are 2D effects that polish the game and add immersion. They can be spawned both client and server [side], but being mostly visual in nature, critical parts exist only on the physical (and logical) client side. ## Registering Particles @@ -227,8 +227,8 @@ public class MyParticleType extends ParticleType { // Mojang is moving towards codecs for particle types, so expect the old deserializer approach to vanish soon. // We define our codec and then return it in the codec() method. Since our example uses no parameters - // for serialization, we use the empty codec. Refer to the Codecs article for more information on how to use them. - public static final Codec CODEC = Codec.EMPTY; + // for serialization, we use an empty unit codec. Refer to the Codecs article for more information. + public static final Codec CODEC = Codec.unit(new MyParticleOptions()); @Override public Codec codec() { diff --git a/docs/resources/client/sounds.md b/docs/resources/client/sounds.md index b633c225..c3e7d69f 100644 --- a/docs/resources/client/sounds.md +++ b/docs/resources/client/sounds.md @@ -196,8 +196,7 @@ The combined (merged) `sounds.json` file the game would then go on and use to lo "sound_4": { // replace true and true: add from upper pack only "sounds": [ - "sound_8", - "sound_4" + "sound_8" ] } } @@ -211,9 +210,9 @@ Minecraft offers various methods to play sounds, and it is sometimes unclear whi - `playSound(Player player, double x, double y, double z, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)` - Client behavior: If the player passed in is the local player, play the sound event to the player at the given location, otherwise no-op. - - Server behavior: If the player passed in is NOT the local player, play the sound event to the player at the given location, otherwise no-op. + - Server behavior: A packet instructing the client to play the sound event to the player at the given location is sent to all players except the one passed in. - Usage: Call from client-initiated code that will run on both sides. The server not playing it to the initiating player prevents playing the sound event twice to them. Alternatively, call from server-initiated code (e.g. a [block entity][be]) with a `null` player to play the sound to everyone. -- `playSound(Player, player, BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)` +- `playSound(Player player, BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)` - Forwards to the first method with `x`, `y` and `z` taking the values of `pos.getX() + 0.5`, `pos.getY() + 0.5` and `pos.getZ() + 0.5`, respectively. - `playLocalSound(double x, double y, double z, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch, boolean distanceDelay)` - Client behavior: Plays the sound to the player at the given location. Does not send anything to the server. If `distanceDelay` is `true`, delays the sound based on the distance to the player. @@ -235,7 +234,7 @@ Minecraft offers various methods to play sounds, and it is sometimes unclear whi - `playSound(SoundEvent soundEvent, float volume, float pitch)` (overrides the method in `Entity`) - Forwards to `Level#playSound` with `this` as the player, `SoundSource.PLAYER` as the sound source, the player's position for x/y/z, and the other parameters passed in. As such, the client/server behavior mimics the one from `Level#playSound`: - Client behavior: Play the sound event to the client player at the given location. - - Server behavior: Play the sound event to everyone near the given location except the client player. + - Server behavior: Play the sound event to everyone near the given location except the player this method was called on. ## Datagen diff --git a/docs/resources/client/textures.md b/docs/resources/client/textures.md index 5a638c76..f9579a2c 100644 --- a/docs/resources/client/textures.md +++ b/docs/resources/client/textures.md @@ -6,14 +6,40 @@ Textures should generally be in sizes that are powers of two, for example 16x16 ## Texture Metadata -Texture metadata can be specified in a file named exactly the same as the texture, with an additional `.mcmeta` suffix. For example, an animated texture at `textures/block/example.png` would need an accompanying `textures/block/example.png.mcmeta` file. The `.mcmeta` file has the following format: +Texture metadata can be specified in a file named exactly the same as the texture, with an additional `.mcmeta` suffix. For example, an animated texture at `textures/block/example.png` would need an accompanying `textures/block/example.png.mcmeta` file. The `.mcmeta` file has the following format (all optional): ```json5 { - // Whether the texture will be blurred if needed. Defaults to false. Currently unused (but functional). + // Whether the texture will be blurred if needed. Defaults to false. + // Currently specified by the codec, but unused otherwise both in the files and in code. "blur": true, - // Whether the texture will be clamped if needed. Defaults to false. Currently unused (but functional). + // Whether the texture will be clamped if needed. Defaults to false. + // Currently specified by the codec, but unused otherwise both in the files and in code. "clamp": true, + "gui": { + // Specifies how the texture will be scaled if needed. Can be one of these three: + "scaling": "stretch", // default + "scaling": { + "tile": { + "width": 16, + "height": 16 + } + }, + "scaling": { + // Like "tile", but allows specifying the border offsets. + "nine_slice": { + "width": 16, + "height": 16, + // May also be a single int that is used as the value for all four sides. + "border": { + "left": 0, + "top": 0, + "right": 0, + "bottom": 0 + } + } + } + }, // See below. "animation": {} } diff --git a/docs/resources/index.md b/docs/resources/index.md index af979238..e6f61097 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -6,7 +6,7 @@ Minecraft generally has two kinds of resources: client-side resources, known as Both resource and data packs normally require a [`pack.mcmeta` file][packmcmeta], this was also the case in past Forge versions. However, NeoForge generates these at runtime for you, so you don't need to worry about it anymore. -If you are confused about the format of something, an easy way to change that is to have a look at the vanilla resources. Your NeoForge development environment not only contains vanilla code, but also vanilla resources. They can be found in the External Resources section (IntelliJ)/Project Libraries section (Eclipse), under the name `ng_dummy_ng.net.minecraft:client:client-extra:` (for Minecraft resources) or `ng_dummy_ng.net.neoforged:neoforge:` (for NeoForge resources). +If you are confused about the format of something, have a look at the vanilla resources. Your NeoForge development environment not only contains vanilla code, but also vanilla resources. They can be found in the External Resources section (IntelliJ)/Project Libraries section (Eclipse), under the name `ng_dummy_ng.net.minecraft:client:client-extra:` (for Minecraft resources) or `ng_dummy_ng.net.neoforged:neoforge:` (for NeoForge resources). ## Assets @@ -64,7 +64,7 @@ Data generation, colloquially known as datagen, is a way to programmatically gen Datagen is run through the Data run configuration, which is generated for you alongside the Client and Server run configurations. The data run configuration follows the [mod lifecycle][lifecycle] until after the registry events are fired. It then fires the [`GatherDataEvent`][event], in which you can register your to-be-generated objects in the form of data providers, writes said objects to disk, and ends the process. -All data providers extend the `DataProvider` interface and usually require one method to be overridden. The following is a list of all data generators Minecraft and NeoForge offer (the linked articles add further information, such as helper methods): +All data providers extend the `DataProvider` interface and usually require one method to be overridden. The following is a list of noteworthy data generators Minecraft and NeoForge offer (the linked articles add further information, such as helper methods): | Class | Method | Generates | Side | Notes | |------------------------------------------------------|----------------------------------|-----------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| From 05803d8c7fc08a5ff7f7611a62919c9800951f75 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 17 Apr 2024 19:31:23 +0200 Subject: [PATCH 40/41] apply feedback regarding BakedModel#getRenderTypes --- docs/resources/client/models/bakedmodel.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/resources/client/models/bakedmodel.md b/docs/resources/client/models/bakedmodel.md index 0edaa0a2..951126d3 100644 --- a/docs/resources/client/models/bakedmodel.md +++ b/docs/resources/client/models/bakedmodel.md @@ -14,7 +14,7 @@ The most important method of a baked model is `getQuads`. This method is respons - A `Direction`: The direction of the face being culled against. May be null, which means quads that cannot be occluded should be returned. - A `RandomSource`: A client-bound random source you can use for randomization. - A `ModelData`: The extra model data to use. This may contain additional data from the block entity needed for rendering. Supplied by `BakedModel#getModelData`. -- A `RenderType`: The [render type][rendertype] to use for rendering the block. May be null, indicating that the default render type should be used. The default render type is looked up in `ItemBlockRenderTypes`, and if no render type is found there either, `minecraft:solid` is used. You should never call on `ItemBlockRenderTypes` yourself! +- A `RenderType`: The [render type][rendertype] to use for rendering the block. May be null, indicating that the quads for all render types used by this model should be returned. Otherwise, it is one of the render types returned by `BakedModel#getRenderTypes` (see below). Models should heavily cache. This is because even though chunks are only rebuilt when a block in them changes, the computations done in this method still need to be as fast as possible and should ideally be cached heavily due to the amount of times this method will be called per chunk section (up to seven times per RenderType used by a given model * amount of RenderTypes used by the respective model * 4096 blocks per chunk section). In addition, [BERs][ber] or entity renderers may actually call this method several times per frame. @@ -43,6 +43,8 @@ Other methods in `BakedModel` that you may override and/or query include: | `ItemOverrides getOverrides()` | Returns the [`ItemOverrides`][itemoverrides] associated with this model. This is only relevant on item models. | | `ModelData getModelData(BlockAndTintGetter, BlockPos, BlockState, ModelData)` | Returns the model data to use for the model. This method is passed an existing `ModelData` that is either the result of `BlockEntity#getModelData()` if the block has an associated block entity, or `ModelData.EMPTY` if that is not the case. This method can be used for blocks that need model data, but do not have a block entity, for example for blocks with connected textures. | | `TextureAtlasSprite getParticleIcon(ModelData)` | Returns the particle sprite to use for the model. May use the model data to use different particle sprites for different model data values. NeoForge-added, replacing the vanilla `getParticleIcon()` overload with no parameters. | +| `ChunkRenderTypeSet getRenderTypes(BlockState, RandomSource, ModelData)` | Returns a `ChunkRenderTypeSet` containing the render type(s) to use for rendering the block model. By default falls back to the normal model-bound render type lookup, which always yields a set with one element. Note that `ChunkRenderTypeSet` is not actually a set, but an ordered `Iterable` (so more of a list). Only used for block models, item models use the overload below. | +| `List getRenderTypes(ItemStack, boolean)` | Returns a `List` containing the render type(s) to use for rendering the item model. By default falls back to the normal model-bound render type lookup, which always yields a list with one element. Only used for item models, block models use the overload above. | ## Perspectives From ed4e02b12ca5787c40afec1b98b7ad7c1205635b Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 18 Apr 2024 23:34:06 +0200 Subject: [PATCH 41/41] apply another round of XFact's feedback --- docs/resources/client/models/bakedmodel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/client/models/bakedmodel.md b/docs/resources/client/models/bakedmodel.md index 951126d3..58ffd073 100644 --- a/docs/resources/client/models/bakedmodel.md +++ b/docs/resources/client/models/bakedmodel.md @@ -43,7 +43,7 @@ Other methods in `BakedModel` that you may override and/or query include: | `ItemOverrides getOverrides()` | Returns the [`ItemOverrides`][itemoverrides] associated with this model. This is only relevant on item models. | | `ModelData getModelData(BlockAndTintGetter, BlockPos, BlockState, ModelData)` | Returns the model data to use for the model. This method is passed an existing `ModelData` that is either the result of `BlockEntity#getModelData()` if the block has an associated block entity, or `ModelData.EMPTY` if that is not the case. This method can be used for blocks that need model data, but do not have a block entity, for example for blocks with connected textures. | | `TextureAtlasSprite getParticleIcon(ModelData)` | Returns the particle sprite to use for the model. May use the model data to use different particle sprites for different model data values. NeoForge-added, replacing the vanilla `getParticleIcon()` overload with no parameters. | -| `ChunkRenderTypeSet getRenderTypes(BlockState, RandomSource, ModelData)` | Returns a `ChunkRenderTypeSet` containing the render type(s) to use for rendering the block model. By default falls back to the normal model-bound render type lookup, which always yields a set with one element. Note that `ChunkRenderTypeSet` is not actually a set, but an ordered `Iterable` (so more of a list). Only used for block models, item models use the overload below. | +| `ChunkRenderTypeSet getRenderTypes(BlockState, RandomSource, ModelData)` | Returns a `ChunkRenderTypeSet` containing the render type(s) to use for rendering the block model. A `ChunkRenderTypeSet` is a set-backed ordered `Iterable`. By default falls back to [getting the render type from the model JSON][rendertype]. Only used for block models, item models use the overload below. | | `List getRenderTypes(ItemStack, boolean)` | Returns a `List` containing the render type(s) to use for rendering the item model. By default falls back to the normal model-bound render type lookup, which always yields a list with one element. Only used for item models, block models use the overload above. | ## Perspectives @@ -115,7 +115,7 @@ public static void modifyBakingResult(ModelEvent.ModifyBakingResult event) { ``` :::warning -It is generally encouraged to use a [custom model loader][modelloader] over baked model wrappers when possible. Custom model loaders can also use `BakedModelWrapper`s if needed. +It is generally encouraged to use a [custom model loader][modelloader] over wrapping baked models in `ModelEvent.ModifyBakingResult` when possible. Custom model loaders can also use `BakedModelWrapper`s if needed. ::: [ao]: https://en.wikipedia.org/wiki/Ambient_occlusion