diff --git a/develop/sounds/dynamic-sounds.md b/develop/sounds/dynamic-sounds.md index 8bf033e52..92a620d30 100644 --- a/develop/sounds/dynamic-sounds.md +++ b/develop/sounds/dynamic-sounds.md @@ -8,28 +8,24 @@ authors: # Create Dynamic and Interactive Sounds{#create-dynamic-and-interactive-sounds} ::: warning -This page covers an advanced topic and builds on top of the [Playing Sounds](../sounds/custom) and the [Creating Custom Sounds](../sounds/custom) pages! - -At this point you should have a solid grasp on Java basics and object oriented programming. - -Having knowledge on Entities, BlockEntities and custom networking will also help a lot in understanding the use case and the applications of advanced sounds. +This page builds on top of the [Playing Sounds](../sounds/custom) and the [Creating Custom Sounds](../sounds/custom) pages! ::: -## The Issue with Normal `SoundEvents` {#the-issue-with-normal-soundevents} +## Problems with `SoundEvents` {#problems-with-soundevents} As we have learned in the [Using Sounds](../sounds/using-sounds) page, it is preferable to use `SoundEvent`s on a logical server side, even if it is a bit counter intuitive. After all, a client needs to handle the sound, which is transmitted e.g. to your headphones, right? -This way of thinking is correct. Technically the client side needs to handle the audio. However, for simple `SoundEvent` playing, the server side prepared a pretty big step in between which might not be obvious at the first glance. Which clients should be able to hear that sound? +This way of thinking is correct. Technically the client side needs to handle the audio. However, for simple `SoundEvent` playing, the server side prepared a big step in between which might not be obvious at first glance. Which clients should be able to hear that sound? -Using the sound on the logical server side, it will take the task of broadcasting `SoundEvent`s away from you. To simplify it, every client (`ClientPlayerEntity`) in tracking range, gets send a network packet to play this specific sound. The sound event is basically broadcasted from the logical server side, to every participating client, without you having to think about it at all. The sound is played once, with the specified volume and pitch values. +Using the sound on the logical server side will solve the issue of broadcasting `SoundEvent`s. To put simple, every client (`ClientPlayerEntity`) in tracking range, gets send a network packet to play this specific sound. The sound event is basically broadcasted from the logical server side, to every participating client, without you having to think about it at all. The sound is played once, with the specified volume and pitch values. But what if this is not enough for you? What if the sound needs to loop, change volume and pitch dynamically while playing and all that based on values which come from things like `Entities` or `BlockEntities`? -The simple way of using the `SoundEvent` on the logical server side is not enough for this use case anymore. +The simple way of using the `SoundEvent` on the logical server side is not enough for this use case. ## Preparing the Audio File {#preparing-the-audio-file} -We are going to create a new **looping** audio for another `SoundEvent`. If you can find an audio file which is looping seamlessly already, you can just follow the steps from [Creating Custom Sounds](../sounds/custom). If your sound is not looping perfectly yet, we will have to prepare it for that a little bit. +We are going to create a new **looping** audio for another `SoundEvent`. If you can find an audio file which is looping seamlessly already, you can just follow the steps from [Creating Custom Sounds](../sounds/custom). If the sound is not looping perfectly yet, we will have to prepare it for that. Again, most modern DAWs (Digital Audio Workstation Software) should be capable of this, but i like to use [Reaper](https://www.reaper.fm/) if the audio editing is a bit more involved. @@ -52,7 +48,7 @@ We can hear and see, that the engine gets started in the beginning and stopped a ### Removing Disruptive Audio Elements {#removing-disruptive-audio-elements} -If we listen closely, there is a small beeping noise in the background, which could've come from the machine. I think, that this would not sound great in-game, so lets try to remove it. +If we listen closely, there is a beeping noise in the background, which could've come from the machine. I think, that this wouldn't sound great in-game, so lets try to remove it. It is a constant sound which keeps its frequency over the length of the audio. So a simple EQ filter should be enough to filter it out. @@ -73,6 +69,12 @@ If you are not a trained audio engineer, this part is mostly about experimenting ![Lowered the bad frequency](/assets/develop/sounds/dynamic-sounds/step_4.png) +Also other effects can be achieved with a simple EQ filter. For example. cutting high and/or low frequencies can give the impression of radio transmitted sounds. + +You can also layer more audio files, change the pitch, add some reverb or use more elaborate sound effects like "bit-crusher". Sound design can be fun, especially if you find good sounding variations of your audio by accident. Experimenting is key and maybe your sound might end up even better than before. + +We will only continue with the EQ filter, which we used to cut out the problematic frequency. + ### Comparison {#comparison} Lets compare the original file with the cleaned up version. @@ -96,12 +98,11 @@ With an EQ filter we were able to remove it almost completely. It is definitely If we let the sound play to the end and let it start from the beginning again, we can clearly hear the transition happening. The goal is to get rid of this by applying a smooth transition. Start by cutting a piece from the end, which is as big as you want the transition to be and place it on the beginning of a new audio track. - -In Reaper, simply move the cursor to the position of the cut, select the audio and press S. +In Reaper you can split the audio by simply moving the cursor to the position of the cut and pressing S. ![Cut the end and move it to a new track](/assets/develop/sounds/dynamic-sounds/step_6.png) -You may have to copy the audio effect of the first audio track over to the second one too. +You may have to copy the EQ audio effect of the first audio track over to the second one too. Now let the end piece of the new track fade out and let the start of the first audio track fade in. @@ -109,115 +110,256 @@ Now let the end piece of the new track fade out and let the start of the first a ### Exporting {#exporting} -Now export the audio with the two audio tracks, but with only one audio channel (Mono) and create a new `SoundEvent` for that `.ogg` file in your mod. +Export the audio with the two audio tracks, but with only one audio channel (Mono) and create a new `SoundEvent` for that `.ogg` file in your mod. If you are not sure how to do that, take a look at the [Creating Custom Sounds](../sounds/custom) page. -This is now the finished looping engine audio for the `SoundEvent`. +This is now the finished looping engine audio for the `SoundEvent` called `ENGINE_LOOP`. -## Using the `SoundEvent` on the Client Side{#using-the-sound-event-on-the-client-side} +## Using a `SoundInstance`{#using-a-soundinstance} -Sounds on the client side use a `SoundInstance`. make sure to use the `SoundManager` for it only on the client side. +To play sounds on the client side a `SoundInstance` is needed. They still make use of `SoundEvent` though. -If you only want to play something like a click on an UI element, you can make use of the already existing `PositionedSoundInstance`. +If you only want to play something like a click on an UI element, there is already the existing `PositionedSoundInstance` class. -This will only be played on the client, which executed this part of the code. +Keep in mind that this will only be played on the client, which executed this part of the code. @[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java) -A `SoundInstance` can be more powerful then just playing Sounds once. +::: warning +Please note that in the `AbstractSoundInstance` class, which `SoundInstance`s inherit from, has the `@Environment(EnvType.CLIENT)` annotation. + +This means that this class (and all its sub-classes) will only be available to the client side. + +If you try using that in a logical server side context, you may not notice the issue in Singleplayer at first, +but a server in a Multiplayer environment will crash, since it won't be able to find that part of the code at all. + +If you struggle with those issues, it is recommended to create your mod from the [Online template generator](https://fabricmc.net/develop/template/) +with the `Split client and common sources` option turned on. +::: + +--- + +A `SoundInstance` can be more powerful then just playing sounds once. Check out the `AbstractSoundInstance` class and what kind of values it can keep track of. Besides the usual volume and pitch variables, it also holds XYZ coordinates and if it should repeat itself after finishing the `SoundEvent`. -Then taking a look at its sub class, `MovingSoundInstance` we get the `TickableSoundInstance` interface too, which keeps track of the `SoundInstance` ticks. +Then taking a look at its sub class, `MovingSoundInstance` we get the `TickableSoundInstance` interface introduced too, which adds ticking functionality to the `SoundInstance`. -Simply create a new class for your custom `SoundInstance` and extend from `MovingSoundInstance`. +So to make use of those utilities, simply create a new class for your custom `SoundInstance` and extend from `MovingSoundInstance`. @[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java) Using your custom `Entity` or `BlockEntity` instead of that basic `LivingEntity` instance can give you even more control e.g. in the `tick()` method based -on getter methods. +on accessor methods but you don't necessarily need a reference to a sound source like that. Instead you could also access a `BlockPos` from somewhere else +or even set it by hand once in the constructor only. + +Just keep in mind that all of the referenced objects in the `SoundInstance` are the versions from the client side. +In specific situations a logical server side entity's properties can differ from its client side counter part. +If you notice that your values don't line up, make sure that your values are synchronized +either with entity's `TrackedData`, `BlockEntity` S2C packets or complete custom S2C network packets. -After you finished creating your custom `SoundInstance`, just call it from the client side place, where you wan't to call it from. +After you finished creating your custom `SoundInstance` it is ready to be used anywhere as long as it's been executed on the client side using the sound manager. In the same way you can also stop the custom `SoundInstance` manually, if necessary. @[code lang=java transcludeWith=:::2](@/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java) The sound loop will be played now only for the client, which ran that SoundInstance. In this case the sound will follow the `ClientPlayerEntity` itself. -## Dynamic and Interactive Sounds{#dynamic-and-interactive-sounds} +This concludes the explanation of creating and using a simple custom `SoundInstance`. -If you plan on making several different `SoundInstances`, which behave in different ways, I would recommend to create a new `AbstractDynamicSoundInstance` class, -which implements the default behavior and let the actual custom `SoundInstance` classes extend from it. +## Advanced SoundInstances{#advanced-soundinstances} -If you just plan on using a single one, then you can skip the abstract super class, and instead implement that functionality in your custom `SoundInstance` class directly. +::: warning +The following content covers an advanced topic. -### Theory{#theory} +At this point you should have a solid grasp on Java, object oriented programming, generics and callback systems. -Let's think about what our goal with the `SoundInstance` is. +Having knowledge on `Entities`, `BlockEntities` and custom networking will also help a lot in understanding the use case and the applications of advanced sounds. +::: -1. The sound should loop as long as the linked custom "EngineBlockEntity" is running -2. The sound position should move around, following its custom "EngineBlockEntity" position (more useful on Entities) -3. The engine sound should have smooth transitions. Turning it on or off should never be abrupt. +To show an example of how more elaborate `SoundInstance` systems can be created, we will add extra functionality, abstractions +and utilities to make working with such sounds in a bigger scope, easier and more dynamic and flexible. -To summarize, we need to keep track of a (client sided) instance of a custom BlockEntity, -adjust volume and pitch values, while the `SoundInstance` is running based on values from that custom BlockEntity and implement "Transition States". +### Theory{#theory} -Technically you could stop running `SoundInstance`s with the client's `SoundManager` directly too, but this will cause the SoundInstance to go silent instantly. -Our goal is, when a stopping signal comes in, to not stop the sound instantly but to execute an ending phase of its "Transition State". +Let's think about what our goal with the `SoundInstance` is. -A "Transition State" is a newly created enum, which contains three values. They will be used to keep track on what phase the sound should be in. +- The sound should loop as long as the linked custom `EngineBlockEntity` is running +- The `SoundInstance` should move around, following its custom `EngineBlockEntity`'s position _(The `BlockEntity` won't move so this might be more useful on `Entities`)_ +- We need smooth transitions. Turning it on or off should never be abrupt. +- Change volume and pitch based on external factors (e.g. from the source of the sound) -- `STARTING` phase: sound starts silent, but slowly increases in volume -- `RUNNING` phase: sound is running normally -- `ENDING` phase: sound starts at the original volume and slowly decreases until it is silent +To summarize, we need to keep track of an instance of a custom `BlockEntity`, +adjust volume and pitch values while the `SoundInstance` is running based on values from that custom `BlockEntity` and implement "Transition States". -Only after `ENDING` phase finished, the custom `SoundInstance` is allowed to be stopped. +If you plan on making several different `SoundInstances`, which behave in different ways, I would recommend creating a new abstract `AbstractDynamicSoundInstance` class, +which implements the default behavior and let the actual custom `SoundInstance` classes extend from it. -Putting all of those building blocks together also raises the question, where we even start and stop sounds, -and where the signals e.g. from custom S2C Network Packets get processed. +If you just plan on using a single one, you can skip the abstract super class, and instead implement that functionality in your custom `SoundInstance` class directly. -For that a new DynamicSoundManager class can be created, to easily interact with this sound system. +In addition it will be a good idea to have a centralized place, where the `SoundInstance`'s are being tracked, played and stopped. This means that it needs to handle incoming +signals, e.g. from custom S2C Network Packets, list all currently running instances and handle special cases, for example which sounds are allowed to play at the same time and which sounds +could potentially disable other sounds upon activation. +For that a new `DynamicSoundManager` class can be created, to easily interact with this sound system. Overall our sound system might look like this, when we are done. ![Structure of the custom Sound System](/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg) -### Creating the New `SoundInstance` {#creating-the-new-soundinstance} +::: info +All of those enums, interfaces and classes will be newly created. Adjust the system and utilities to your specific use case as you see fit. +This is only an example of how you can approach such topics. +::: + +### `DynamicSoundSource` Interface {#dynamicsoundsource-interface} + +If you choose to create a new, more modular, custom `AbstractDynamicSoundInstance` class as a super class, +you may want to reference not only a single type of Entity but different ones, or even BlockEntities too. + +In that case, making use of abstraction is the key. +Instead of referencing e.g. a custom `BlockEntity` directly, only keeping track of an Interface, which provides the data, solves that problem. + +Going forward we will make use of a custom Interface called `DynamicSoundSource`. It is implemented in all classes which want to make use of that dynamic sound functionality, +like your custom `BlockEntity`, Entities or even, using Mixins, on already existing classes, like `ZombieEntity`. + +It basically represents only the necessary data of the sound source. + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/sound/DynamicSoundSource.java) + +After creating this interface, make sure to implement it in the necessary classes too. + +::: info +This is a utility, which may be used on both the client and the logical server side. + +So this Interface should be stored in the common packages instead of the client only packages, if you make use of the +"split sources" option +::: + +### `TransitionState` Enum {#transitionstate-enum} + +As mentioned earlier, you could stop running `SoundInstance`s with the client's `SoundManager` directly, but this will cause the SoundInstance to go silent instantly. +Our goal is, when a stopping signal comes in, to not stop the sound but to execute an ending phase of its "Transition State". Only after the ending phase is finished +the custom `SoundInstance` should be stopped. -Start of by passing over the necessary sound source over the constructor. Your `SoundInstance` can keep track of client sided objects, so having e.g. your -custom Entity stored in there is not a bad idea. +A `TransitionState` is a newly created enum, which contains three values. They will be used to keep track on what phase the sound should be in. + +- `STARTING` Phase: sound starts silent, but slowly increases in volume +- `RUNNING` Phase: sound is running normally +- `ENDING` Phase: sound starts at the original volume and slowly decreases until it is silent + +Technically a simple enum with the phases can be enough. ```java -public class EngineSoundInstance extends MovingSoundInstance { - private final EngineBlockEntity EngineBlockEntity; - - public EngineSoundInstance(EngineBlockEntity EngineBlockEntity, SoundEvent soundEvent, SoundCategory soundCategory) { - super(soundEvent, soundCategory, SoundInstance.createRandom()); - this.EngineBlockEntity = EngineBlockEntity; - } - //... +public enum TransitionState { + STARTING, RUNNING, ENDING } ``` -If you choose to make use of a new, more modular, custom `AbstractDynamicSoundInstance` class as a super class, -you may want to use that class not only on a single type of Entity but on different ones, or even on BlockEntities too. In that case making use of abstraction is the key. +But when those values are sent over the network you might want to define an `Identifier` for them or even add other custom values, like priorities etc. -Instead of referencing a custom BlockEntity directly, only keeping track of an Interface, which provides the data, might be better in this case. -Going forward we will make use of a custom Interface called `DynamicSoundSource`. It is implemented in all classes which want to make use of that dynamic sound functionality. -It basically represents the Entity, BlockEntity or other objects which define the sound behavior. +@[code lang=java transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/sound/TransitionState.java) -If you don't want to do that, just use your custom BlockEntity directly instead. +::: info +Again, if you make use of "split sources" you need to think about where you will be using this enum. +Technically, only the custom `SoundInstance`s which are only available on the client side, will use those enum values. -@[code lang=java transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/sound/DynamicSoundSource.java) +But if this enum is used anywhere else, e.g. in custom network packets, you may have to put this enum also into the common packages +instead of the client only packages. +::: + +### SoundInstanceCallback Interface {#soundinstancecallback-interface} + +This interface is used as a callback. For now we only need a `onFinished` method but you can add your own methods if you need to send +other signals too. + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/sound/instance/SoundInstanceCallback.java) + +Implement this interface on any class, which should be able to handle the incoming signals, for example the [newly created `AbstractDynamicSoundInstance`](#abstractdynamicsoundinstance-class), +and actually create the functionality in the custom `SoundInstance` itself. + +### `AbstractDynamicSoundInstance` Class {#abstractdynamicsoundinstance-class} -In the custom abstract `SoundInstance` parent class we will now use that Interface instead of the direct BlockEntity. +Let's finally get started on the core of the dynamic sound instances system. The `AbstractDynamicSoundInstance` is a newly created `abstract` class. +It implements the default defining features and utilities of our custom `SoundInstances`, which will inherit from it. + +We can take the `CustomSoundInstance` from [earlier](#using-a-soundinstance) and improve on that. +Instead of the `LivingEntity` we will now reference our `DynamicSoundSource`. +In addition we will define more properties. + +- `TransitionState` to keep track of the currently phase +- tick durations of how long the start and the end phases should last +- minimum and maximum values for volume and pitch +- boolean value to notify if this instance has been finished and can be cleaned up +- tick holders to keep track of the current sound's progress. +- a callback which sends a signal back to the `DynamicSoundManager` for the final clean up, when the `SoundInstance` is actually finished @[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java) -### \ No newline at end of file +Then set up the default starting values for the custom `SoundInstance` in the constructor of the abstract class. + +@[code lang=java transcludeWith=:::2](@/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java) + +After the constructor is finished you need to allow the `SoundInstance` to be able to play. + +@[code lang=java transcludeWith=:::3](@/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java) + +Now comes the important part for this dynamic `SoundInstance`. Based on the current tick of the instance it can apply different values and behaviors. + +@[code lang=java transcludeWith=:::4](@/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java) + +As you can see, we haven't applied the volume and pitch modulation here yet. We only apply the shared behavior. +So in this `AbstractDynamicSoundInstance` class we only provide the basic structure and the tools for the +sub classes which can decide themselves, which kind of sound modulation they actually wan't to apply. + +So let's take a look at some examples of such sound modulation methods. + +@[code lang=java transcludeWith=:::5](@/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java) + +As you can see, normalized values in combination with (linear) interpolation help out to shape the values to the preferred audio limits. +Keep in mind that if you add multiple methods, which change the same value, you will need to observe and adjust how they work together with each other. + +Now we just need to add the remaining utility methods and we are done with the `AbstractDynamicSoundInstance` class. + +@[code lang=java transcludeWith=:::6](@/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java) + +### Example `SoundInstance` Implementation {#example-soundinstance-implementation} + +If we take a look at the actual custom `SoundInstance` class, which extends from the newly created `AbstractDynamicSoundInstance`, we only need to think about +what conditions would bring the sound to a stop and what sound modulation we want to apply. + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java) + +### DynamicSoundManager Class {#dynamicsoundmanager-class} + +We discussed [earlier](#using-a-soundinstance) how to play and stop a `SoundInstance`. To clean up, centralize and manage those interactions you can create your own +`SoundInstance` handler which builds on top of that. + +This new `DynamicSoundManager` class will manage the custom `SoundInstances` so it will also only be available to the client side (single thread). On top of that a client should only ever allow one instance of this class to exist. Multiple sound managers for a single client wouldn't make much sense and complicate the interactions even more. +So lets use a "Singleton Design Pattern". + +@[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java) + +After getting the basic structure right, you can add the methods, which are needed to interact with the sound system. + +- playing sounds +- stopping sounds +- checking if a sound is currently playing + +@[code lang=java transcludeWith=:::2](@/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java) + +Instead of only having a list of all currently playing `SoundInstances` you could also keep track of which sound sources are playing which sounds. +For example an engine having two engine sounds at the same time would make no sense, while multiple engines playing their respective engine sounds +is a valid edge case. For the sake of simplicity we just created a `List` but in many cases a `HashMap` of `DynamicSoundSource` and a `AbstractDynamicSoundInstance` might be a better choice. + +### Using the Advanced Sound System {#using-the-advanced-sound-system} + +To use this sound system simply make use of either the `DynamicSoundManager` methods or the `SoundInstance` methods. Using `onStartedTrackingBy` and `onStoppedTrackingBy` +from entities or just custom S2C networking you can now start and stop your custom dynamic `SoundInstance`s. + + diff --git a/public/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg b/public/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg index 5e93de1ae..4e3ed7e0e 100644 Binary files a/public/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg and b/public/assets/develop/sounds/dynamic-sounds/custom-dynamic-sound-handling.jpg differ diff --git a/public/assets/develop/sounds/dynamic-sounds/engine-block-sound.webm b/public/assets/develop/sounds/dynamic-sounds/engine-block-sound.webm new file mode 100644 index 000000000..d4e6970df Binary files /dev/null and b/public/assets/develop/sounds/dynamic-sounds/engine-block-sound.webm differ diff --git a/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java b/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java index b213f82ea..917c59b07 100644 --- a/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java +++ b/reference/latest/src/client/java/com/example/docs/FabricDocsDynamicSound.java @@ -6,16 +6,18 @@ import net.minecraft.sound.SoundEvents; import net.fabricmc.api.ClientModInitializer; +import com.example.docs.network.ReceiveS2C; import com.example.docs.sound.CustomSounds; -import com.example.docs.sound.DynamicSoundManager; import com.example.docs.sound.instance.CustomSoundInstance; public class FabricDocsDynamicSound implements ClientModInitializer { - public static final DynamicSoundManager SOUND_MANAGER = DynamicSoundManager.getInstance(); - @Override public void onInitializeClient() { + ReceiveS2C.initialize(); + } + + private void playSimpleSoundInstance() { // :::1 MinecraftClient client = MinecraftClient.getInstance(); client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F)); diff --git a/reference/latest/src/client/java/com/example/docs/network/ReceiveS2C.java b/reference/latest/src/client/java/com/example/docs/network/ReceiveS2C.java new file mode 100644 index 000000000..627575a57 --- /dev/null +++ b/reference/latest/src/client/java/com/example/docs/network/ReceiveS2C.java @@ -0,0 +1,41 @@ +package com.example.docs.network; + +import net.minecraft.client.world.ClientWorld; +import net.minecraft.sound.SoundCategory; + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import com.example.docs.block.entity.custom.EngineBlockEntity; +import com.example.docs.networking.payload.EngineSoundInstancePacket; +import com.example.docs.sound.AbstractDynamicSoundInstance; +import com.example.docs.sound.CustomSounds; +import com.example.docs.sound.DynamicSoundManager; +import com.example.docs.sound.instance.EngineSoundInstance; + +public class ReceiveS2C { + static { + ClientPlayNetworking.registerGlobalReceiver(EngineSoundInstancePacket.IDENTIFIER, ReceiveS2C::handleEngineSound); + } + + // ::: 1 + private static void handleEngineSound(EngineSoundInstancePacket packet, ClientPlayNetworking.Context context) { + ClientWorld world = context.client().world; + if (world == null) return; + if (!(world.getBlockEntity(packet.blockEntityPos()) instanceof EngineBlockEntity engineBlockEntity)) return; + + DynamicSoundManager soundManager = DynamicSoundManager.getInstance(); + + if (packet.shouldStart()) { + soundManager.play(new EngineSoundInstance(engineBlockEntity, + CustomSounds.ENGINE_LOOP, SoundCategory.BLOCKS, + 60, 30, 1.2f, 0.8f, 1.4f, + soundManager) + ); + } else { + soundManager.getPlayingSoundInstance(CustomSounds.ENGINE_LOOP).ifPresent(AbstractDynamicSoundInstance::end); + } + } + // ::: 1 + + public static void initialize() { + } +} diff --git a/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java b/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java index 00e86bf61..c6187b8cc 100644 --- a/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java +++ b/reference/latest/src/client/java/com/example/docs/sound/AbstractDynamicSoundInstance.java @@ -4,22 +4,140 @@ import net.minecraft.client.sound.SoundInstance; import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.MathHelper; + +import com.example.docs.sound.instance.SoundInstanceCallback; // :::1 public abstract class AbstractDynamicSoundInstance extends MovingSoundInstance { - private final DynamicSoundSource soundSource; + protected final DynamicSoundSource soundSource; // Entities, BlockEntities, ... + protected TransitionState transitionState; // current TransitionState of the SoundInstance + + protected final int startTransitionTicks, endTransitionTicks; // duration of starting and ending phases + + // possible volume range when adjusting sound values + protected final float maxVolume; // only max value since the minimum is always 0 + // possible pitch range when adjusting sound values + protected final float minPitch, maxPitch; + + protected int currentTick = 0, transitionTick = 0; // current tick values for the instance + + protected final SoundInstanceCallback callback; // callback for soundInstance states + + // ... + // :::1 + + // :::2 + // ... + + // set up default settings of the SoundInstance in this constructor + protected AbstractDynamicSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundCategory soundCategory, + int startTransitionTicks, int endTransitionTicks, float maxVolume, float minPitch, float maxPitch, + SoundInstanceCallback callback) { - protected AbstractDynamicSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundCategory soundCategory) { super(soundEvent, soundCategory, SoundInstance.createRandom()); + + // store important references to other objects this.soundSource = soundSource; + this.callback = callback; + // store the limits for the SoundInstance + this.maxVolume = maxVolume; + this.minPitch = minPitch; + this.maxPitch = maxPitch; + this.startTransitionTicks = startTransitionTicks; // starting phase duration + this.endTransitionTicks = endTransitionTicks; // ending phase duration + // set start values + this.volume = 0.0f; + this.pitch = minPitch; + this.repeat = true; + this.transitionState = TransitionState.STARTING; + this.setPositionToEntity(); } + // ... - // :::1 + // :::2 + // :::3 + @Override + public boolean shouldAlwaysPlay() { + // override to true, so that the SoundInstance can start + // or add your own condition to the SoundInstance, if necessary + return true; + } + // :::3 + + // :::4 @Override public void tick() { + // handle states where sound might be actually stopped instantly + if (this.soundSource == null) { + this.callback.onFinished(this); + } + + // basic tick behaviour + this.currentTick++; + this.setPositionToEntity(); + + // SoundInstance phase switching + switch (this.transitionState) { + case STARTING -> { + this.transitionTick++; + // go into next phase if starting phase finished its duration + if (this.transitionTick > this.startTransitionTicks) { + this.transitionTick = 0; // reset tick for future ending phase + this.transitionState = TransitionState.RUNNING; + } + } + case ENDING -> { + this.transitionTick++; + // set SoundInstance as finished if ending phase finished its duration + if (this.transitionTick > this.endTransitionTicks) { + this.callback.onFinished(this); + } + } + } + + // apply volume and pitch modulation here, + // if you use a normal SoundInstance class + } + // :::4 + + // :::5 + // increase or decrease volume and pitch based on the current phase of the sound + protected void modulateSoundForTransition() { + float normalizedTick = switch (transitionState) { + case STARTING -> (float) this.transitionTick / this.startTransitionTicks; + case ENDING -> 1.0f - ((float) this.transitionTick / this.endTransitionTicks); + default -> 1.0f; + }; + + this.volume = MathHelper.lerp(normalizedTick, 0.0f, this.maxVolume); + } + // increase or decrease pitch based on the sound source's stress value + protected void modulateSoundForStress() { + this.pitch = MathHelper.lerp(this.soundSource.getNormalizedStress(), this.minPitch, this.maxPitch); } + // :::5 + + // :::6 + // moves the sound instance position to the sound source's position + protected void setPositionToEntity() { + this.x = soundSource.getPosition().getX(); + this.y = soundSource.getPosition().getY(); + this.z = soundSource.getPosition().getZ(); + } + + // Sets the SoundInstance into its ending phase. + // This is especially useful for external access to this SoundInstance + public void end() { + this.transitionTick = 0; + this.transitionState = TransitionState.ENDING; + } + // :::6 + // :::1 } +// :::1 + diff --git a/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java b/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java index db6283cf0..22ba450c0 100644 --- a/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java +++ b/reference/latest/src/client/java/com/example/docs/sound/DynamicSoundManager.java @@ -1,42 +1,75 @@ package com.example.docs.sound; -import net.minecraft.client.MinecraftClient; - import java.util.ArrayList; import java.util.List; +import java.util.Optional; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.sound.SoundEvent; -public class DynamicSoundManager { +import com.example.docs.sound.instance.SoundInstanceCallback; - private static DynamicSoundManager instance; +// :::1 +public class DynamicSoundManager implements SoundInstanceCallback { + // An instance of the client to use Minecraft's default SoundManager private static final MinecraftClient client = MinecraftClient.getInstance(); - - private final List activeSounds = new ArrayList<>(); + // static field to store the current instance for the Singleton Design Pattern + private static DynamicSoundManager instance; + // The list which keeps track of all currently playing dynamic SoundInstances + private final List activeSounds = new ArrayList<>(); private DynamicSoundManager() { // private constructor to make sure that the normal // instantiation of that object is not used externally } - /** - * This "Singleton Design Pattern" makes sure that, at runtime, - * only one instance of this class can exist. - *

- * If this class has been used once already, it keeps its instance stored - * in the static instance variable and return it.
- * Otherwise, the instance variable is not initialized yet (null). - * It will create a new instance, use it - * and store it in the static variable for next uses. - */ + // when accessing this class for the first time a new instance + // is created and stored. If this is called again only the already + // existing instance will be returned, instead of creating a new instance public static DynamicSoundManager getInstance() { - if (instance == null) return new DynamicSoundManager(); + if (instance == null) { + instance = new DynamicSoundManager(); + } return instance; } + // :::1 + + // :::2 + // Plays a sound instance, if it doesn't already exist in the list + public void play(T soundInstance) { + if (this.activeSounds.contains(soundInstance)) return; + + client.getSoundManager().play(soundInstance); + this.activeSounds.add(soundInstance); + } + + // Stops a sound immediately. in most cases it is preferred to use + // the sound's ending phase, which will clean it up after completion + public void stop(T soundInstance) { + client.getSoundManager().stop(soundInstance); + this.activeSounds.remove(soundInstance); + } - public void play(AbstractDynamicSoundInstance sound) { - client.getSoundManager().play(sound); + // Finds a SoundInstance from a SoundEvent, if it exists and is currently playing + public Optional getPlayingSoundInstance(SoundEvent soundEvent) { + for (var activeSound : this.activeSounds) { + // SoundInstances use their SoundEvent's id by default + if (activeSound.getId().equals(soundEvent.getId())) { + return Optional.of(activeSound); + } + } + return Optional.empty(); } + // :::2 - public void stop(AbstractDynamicSoundInstance sound) { + // :::1 + // This is where the callback signal of a finished custom SoundInstance will arrive. + // For now, we can just stop and remove the sound from the list, but you can add + // your own functionality too + @Override + public void onFinished(T soundInstance) { + this.stop(soundInstance); } } +// :::1 diff --git a/reference/latest/src/client/java/com/example/docs/sound/TransitionState.java b/reference/latest/src/client/java/com/example/docs/sound/TransitionState.java deleted file mode 100644 index 1a87ac688..000000000 --- a/reference/latest/src/client/java/com/example/docs/sound/TransitionState.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.docs.sound; - -public enum TransitionState { - STARTING, RUNNING, ENDING -} diff --git a/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java b/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java index e24da9602..de321b2c5 100644 --- a/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java +++ b/reference/latest/src/client/java/com/example/docs/sound/instance/CustomSoundInstance.java @@ -13,14 +13,15 @@ public class CustomSoundInstance extends MovingSoundInstance { private final LivingEntity entity; - // Here we pass over the sound source of the SoundInstance and store it in the instance. + public CustomSoundInstance(LivingEntity entity, SoundEvent soundEvent, SoundCategory soundCategory) { super(soundEvent, soundCategory, SoundInstance.createRandom()); - - // here we can set up values when the sound is about to start. - this.repeat = true; + // In this constructor we also add the sound source (LivingEntity) of + // the SoundInstance and store it in the current object this.entity = entity; - setPositionToEntity(); + // set up default values when the sound is about to start + this.repeat = true; + this.setPositionToEntity(); } @Override @@ -31,15 +32,22 @@ public void tick() { return; } // move sound position over to the new position for every tick - setPositionToEntity(); + this.setPositionToEntity(); + } + + @Override + public boolean shouldAlwaysPlay() { + // override to true, so that the SoundInstance can start + // or add your own condition to the SoundInstance, if necessary + return true; } // small utility method to move the sound instance position // to the sound source's position private void setPositionToEntity() { - this.x = entity.getX(); - this.y = entity.getY(); - this.z = entity.getZ(); + this.x = this.entity.getX(); + this.y = this.entity.getY(); + this.z = this.entity.getZ(); } } // :::1 \ No newline at end of file diff --git a/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java b/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java index 1c728856c..777d5203e 100644 --- a/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java +++ b/reference/latest/src/client/java/com/example/docs/sound/instance/EngineSoundInstance.java @@ -1,17 +1,41 @@ package com.example.docs.sound.instance; -import com.example.docs.sound.AbstractDynamicSoundInstance; - -import com.example.docs.sound.DynamicSoundSource; - import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvent; -import net.minecraft.util.math.random.Random; +import com.example.docs.block.entity.custom.EngineBlockEntity; +import com.example.docs.sound.AbstractDynamicSoundInstance; +import com.example.docs.sound.DynamicSoundSource; + +// :::1 public class EngineSoundInstance extends AbstractDynamicSoundInstance { - protected EngineSoundInstance(DynamicSoundSource source, SoundEvent soundEvent, SoundCategory soundCategory, Random random) { - super(source, soundEvent, soundCategory); + + // Here we just use the default constructor parameters. + // If you want to specifically set values here already, + // you can clean up the constructor parameters a bit + public EngineSoundInstance(DynamicSoundSource soundSource, SoundEvent soundEvent, SoundCategory soundCategory, + int startTransitionTicks, int endTransitionTicks, float maxVolume, float minPitch, float maxPitch, + SoundInstanceCallback callback) { + super(soundSource, soundEvent, soundCategory, startTransitionTicks, endTransitionTicks, maxVolume, minPitch, maxPitch, callback); } + @Override + public void tick() { + // check conditions which set this sound automatically into the ending phase + if (soundSource instanceof EngineBlockEntity blockEntity && blockEntity.isRemoved()) { + this.end(); + } + + // apply the default tick behaviour from the parent class + super.tick(); + + // modulate volume and pitch of the SoundInstance + this.modulateSoundForTransition(); + this.modulateSoundForStress(); + } + // you can also add sound modulation methods here, + // which should be only accessible to this + // specific SoundInstance } +// :::1 \ No newline at end of file diff --git a/reference/latest/src/client/java/com/example/docs/sound/instance/SoundInstanceCallback.java b/reference/latest/src/client/java/com/example/docs/sound/instance/SoundInstanceCallback.java new file mode 100644 index 000000000..e25e73627 --- /dev/null +++ b/reference/latest/src/client/java/com/example/docs/sound/instance/SoundInstanceCallback.java @@ -0,0 +1,11 @@ +package com.example.docs.sound.instance; + +import com.example.docs.sound.AbstractDynamicSoundInstance; + +// :::1 +public interface SoundInstanceCallback { + // deliver the custom SoundInstance, from which this signal originates, + // using the method parameters + void onFinished(T soundInstance); +} +// :::1 \ No newline at end of file diff --git a/reference/latest/src/main/java/com/example/docs/block/custom/EngineBlock.java b/reference/latest/src/main/java/com/example/docs/block/custom/EngineBlock.java index 62e3042b0..1b0c16164 100644 --- a/reference/latest/src/main/java/com/example/docs/block/custom/EngineBlock.java +++ b/reference/latest/src/main/java/com/example/docs/block/custom/EngineBlock.java @@ -1,12 +1,17 @@ package com.example.docs.block.custom; import com.mojang.serialization.MapCodec; +import net.minecraft.block.BlockRenderType; import net.minecraft.block.BlockState; import net.minecraft.block.BlockWithEntity; import net.minecraft.block.entity.BlockEntity; import net.minecraft.block.entity.BlockEntityTicker; import net.minecraft.block.entity.BlockEntityType; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.tag.ItemTags; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.sound.SoundEvents; import net.minecraft.util.ActionResult; import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.math.BlockPos; @@ -42,11 +47,37 @@ public BlockEntityTicker getTicker(World world, Block @Override protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) { - if (!(world.getBlockEntity(pos) instanceof EngineBlockEntity blockEntity)) + if (!(world.getBlockEntity(pos) instanceof EngineBlockEntity engineBlockEntity)) return super.onUse(state, world, pos, player, hit); - if (blockEntity.isTicking()) return super.onUse(state, world, pos, player, hit); - blockEntity.setTick(0); // starts ticking - return ActionResult.SUCCESS; + if (player.getMainHandStack().isIn(ItemTags.COALS)) { + if (engineBlockEntity.setFuelIfPossible(engineBlockEntity.getFuel() + 40)) { + player.getMainHandStack().decrementUnlessCreative(1, player); + playSound(world, SoundEvents.ITEM_AXE_STRIP, pos); + return ActionResult.SUCCESS; + } + return ActionResult.PASS; + } + else { + if (engineBlockEntity.isRunning()) { + engineBlockEntity.setNormalizedStress(engineBlockEntity.getNormalizedStress() + 0.2f); + return ActionResult.SUCCESS; + } else if (engineBlockEntity.getFuel() > 0) { + playSound(world, SoundEvents.BLOCK_LEVER_CLICK, pos); + engineBlockEntity.turnOn(); + return ActionResult.SUCCESS; + } + } + return ActionResult.PASS; + } + + @Override + protected BlockRenderType getRenderType(BlockState state) { + return BlockRenderType.MODEL; + } + + private static void playSound(World world, SoundEvent soundEvent, BlockPos pos) { + if (world.isClient()) return; + world.playSound(null, pos, soundEvent, SoundCategory.BLOCKS, 0.8f, 1f); } } diff --git a/reference/latest/src/main/java/com/example/docs/block/entity/custom/EngineBlockEntity.java b/reference/latest/src/main/java/com/example/docs/block/entity/custom/EngineBlockEntity.java index 4e64d401b..263313558 100644 --- a/reference/latest/src/main/java/com/example/docs/block/entity/custom/EngineBlockEntity.java +++ b/reference/latest/src/main/java/com/example/docs/block/entity/custom/EngineBlockEntity.java @@ -1,60 +1,132 @@ package com.example.docs.block.entity.custom; -import com.example.docs.sound.DynamicSoundSource; - import net.minecraft.block.BlockState; import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.listener.ClientPlayPacketListener; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; +import net.minecraft.registry.RegistryWrapper; import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import com.example.docs.block.entity.ModBlockEntities; +import com.example.docs.networking.payload.EngineSoundInstancePacket; +import com.example.docs.sound.DynamicSoundSource; public class EngineBlockEntity extends BlockEntity implements DynamicSoundSource { - public static final int MAX_TICK_AMOUNT = 120; // can represent something like fuel capacity - - private int tick = -1; // -1 is turned off, it will only tick if its 0 or bigger + public static final int MAX_FUEL = 200; + private int tick = -1; // starts turned off + private int fuel = 0; private float normalizedStress = 0; public EngineBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.ENGINE_BLOCK_ENTITY, pos, state); } + public void setTick(int tick) { + this.tick = tick; + } + + @Override public int getTick() { - return tick; + return this.tick; } - public void setTick(int tick) { - this.tick = tick; + public static void tick(World world, BlockPos pos, BlockState state, EngineBlockEntity engineBlockEntity) { + if (engineBlockEntity.getTick() < 0) return; + + engineBlockEntity.setTick(engineBlockEntity.getTick() + 1); + engineBlockEntity.setFuelIfPossible(engineBlockEntity.getFuel() - 1); + engineBlockEntity.setNormalizedStress(engineBlockEntity.getNormalizedStress() - 0.02f); + + if (!world.isClient() && engineBlockEntity.getFuel() > 0) { + PlayerLookup.tracking(engineBlockEntity).forEach(player -> { + String engineState = "Engine Fuel: %s | Stress: %s".formatted( + engineBlockEntity.getFuel(), + String.format("%.02f", engineBlockEntity.getNormalizedStress()) + ); + player.sendMessage(Text.literal(engineState), true); + }); + } + + if (engineBlockEntity.getFuel() <= 0) { + engineBlockEntity.turnOff(); + engineBlockEntity.setFuelIfPossible(0); + } } - public boolean isTicking() { - return this.getTick() > -1; + @Override + public float getNormalizedStress() { + return MathHelper.clamp(normalizedStress, 0, 1); } public void setNormalizedStress(float normalizedStress) { this.normalizedStress = Math.clamp(normalizedStress, 0, 1); } - public static void tick(World world, BlockPos pos, BlockState state, EngineBlockEntity blockEntity) { - if (blockEntity.getTick() < 0) return; - blockEntity.setTick(blockEntity.getTick() + 1); - if (blockEntity.getTick() >= MAX_TICK_AMOUNT) blockEntity.setTick(-1); + public int getFuel() { + return MathHelper.clamp(this.fuel, 0, MAX_FUEL); + } + + public boolean setFuelIfPossible(int fuel) { + boolean consumeItem = this.getFuel() != MAX_FUEL; + this.fuel = MathHelper.clamp(fuel, 0, MAX_FUEL); + return consumeItem; } @Override - public int getTicks() { - return getTick(); + public Vec3d getPosition() { + return this.getPos().toCenterPos(); + } + + public void turnOn() { + if (this.getFuel() > 0) { + this.setTick(0); + this.setNormalizedStress(0); + this.sendPacketToTrackingClients(new EngineSoundInstancePacket(true, this.getPos())); + this.syncToChunk(); + } + } + + public void turnOff() { + this.tick = -1; + this.sendPacketToTrackingClients(new EngineSoundInstancePacket(false, this.getPos())); + this.syncToChunk(); } + public boolean isRunning() { + return this.getTick() > -1; + } + + private void sendPacketToTrackingClients(CustomPayload payload) { + if (payload == null || !(this.getWorld() instanceof ServerWorld)) return; + PlayerLookup.tracking(this).forEach(player -> ServerPlayNetworking.send(player, payload)); + } + + // S2C BlockEntity sync boilerplate + public void syncToChunk() { + if (!(getWorld() instanceof ServerWorld serverWorld)) return; + serverWorld.getChunkManager().markForUpdate(this.getPos()); + } + + @Nullable @Override - public BlockPos getPosition() { - return this.getPos(); + public Packet toUpdatePacket() { + return BlockEntityUpdateS2CPacket.create(this); } @Override - public float getNormalizedStress() { - return normalizedStress; + public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registryLookup) { + return createNbt(registryLookup); } } diff --git a/reference/latest/src/main/java/com/example/docs/item/custom/CustomSoundItem.java b/reference/latest/src/main/java/com/example/docs/item/custom/CustomSoundItem.java index 7d15908e8..d786c1e93 100644 --- a/reference/latest/src/main/java/com/example/docs/item/custom/CustomSoundItem.java +++ b/reference/latest/src/main/java/com/example/docs/item/custom/CustomSoundItem.java @@ -19,7 +19,7 @@ public CustomSoundItem(Settings settings) { @Override public ActionResult useOnEntity(ItemStack stack, PlayerEntity user, LivingEntity entity, Hand hand) { // As stated above, don't use the playSound() method on the client side - // ... it wont work! + // ... it won't work! if (!entity.getWorld().isClient()) { // Play the sound as if it was coming from the entity. entity.playSound(SoundEvents.ENTITY_PILLAGER_AMBIENT, 2f, 0.7f); diff --git a/reference/latest/src/main/java/com/example/docs/networking/FabricDocsReferenceNetworking.java b/reference/latest/src/main/java/com/example/docs/networking/FabricDocsReferenceNetworking.java new file mode 100644 index 000000000..b55378b25 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/networking/FabricDocsReferenceNetworking.java @@ -0,0 +1,19 @@ +package com.example.docs.networking; + +import com.example.docs.FabricDocsReference; +import net.fabricmc.api.ModInitializer; + +import net.minecraft.util.Identifier; + +public class FabricDocsReferenceNetworking implements ModInitializer { + public static final String MOD_ID = FabricDocsReference.MOD_ID; + + @Override + public void onInitialize() { + NetworkPayloads.initialize(); + } + + public static Identifier getId(String input) { + return Identifier.of(MOD_ID, input); + } +} diff --git a/reference/latest/src/main/java/com/example/docs/networking/NetworkPayloads.java b/reference/latest/src/main/java/com/example/docs/networking/NetworkPayloads.java new file mode 100644 index 000000000..7a23884a9 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/networking/NetworkPayloads.java @@ -0,0 +1,28 @@ +package com.example.docs.networking; + + +import com.example.docs.networking.payload.EngineSoundInstancePacket; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; + +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; + +@SuppressWarnings("SameParameterValue") +public class NetworkPayloads { + static { + registerS2C(EngineSoundInstancePacket.IDENTIFIER, EngineSoundInstancePacket.CODEC); + } + + private static void registerS2C(CustomPayload.Id packetIdentifier, PacketCodec codec) { + PayloadTypeRegistry.playS2C().register(packetIdentifier, codec); + } + + private static void registerC2S(CustomPayload.Id packetIdentifier, PacketCodec codec) { + PayloadTypeRegistry.playC2S().register(packetIdentifier, codec); + } + + public static void initialize() { + } +} diff --git a/reference/latest/src/main/java/com/example/docs/networking/payload/EngineSoundInstancePacket.java b/reference/latest/src/main/java/com/example/docs/networking/payload/EngineSoundInstancePacket.java new file mode 100644 index 000000000..eef649505 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/networking/payload/EngineSoundInstancePacket.java @@ -0,0 +1,27 @@ +package com.example.docs.networking.payload; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.math.BlockPos; + +import com.example.docs.sound.FabricDocsReferenceSounds; + +public record EngineSoundInstancePacket(boolean shouldStart, BlockPos blockEntityPos) implements CustomPayload { + + + public static final CustomPayload.Id IDENTIFIER = + new CustomPayload.Id<>(FabricDocsReferenceSounds.identifierOf("sound_instance")); + + @Override + public Id getId() { + return IDENTIFIER; + } + + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.BOOL, EngineSoundInstancePacket::shouldStart, + BlockPos.PACKET_CODEC, EngineSoundInstancePacket::blockEntityPos, + EngineSoundInstancePacket::new + ); +} diff --git a/reference/latest/src/main/java/com/example/docs/sound/DynamicSoundSource.java b/reference/latest/src/main/java/com/example/docs/sound/DynamicSoundSource.java index f75dad692..e723478f9 100644 --- a/reference/latest/src/main/java/com/example/docs/sound/DynamicSoundSource.java +++ b/reference/latest/src/main/java/com/example/docs/sound/DynamicSoundSource.java @@ -1,17 +1,18 @@ package com.example.docs.sound; -import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; // :::1 public interface DynamicSoundSource { - // gets access to how many ticks have passed for that instance - int getTicks(); + // gets access to how many ticks have passed for e.g. a BlockEntity instance + int getTick(); - // gets access to where this instance is placed in the world - BlockPos getPosition(); + // gets access to where currently this instance is placed in the world + Vec3d getPosition(); - // holds a normalized value (range of 0-1) showing, how much stress this instance is experiencing - // Tt is more or less just an arbitrary value, which will cause the sound to change its pitch while playing. + // holds a normalized (range of 0-1) value, showing how much stress this instance is currently experiencing + // It is more or less just an arbitrary value, which will cause the sound to change its pitch while playing. float getNormalizedStress(); } // :::1 diff --git a/reference/latest/src/main/java/com/example/docs/sound/FabricDocsReferenceSounds.java b/reference/latest/src/main/java/com/example/docs/sound/FabricDocsReferenceSounds.java index dbb925e9f..cfb84ee73 100644 --- a/reference/latest/src/main/java/com/example/docs/sound/FabricDocsReferenceSounds.java +++ b/reference/latest/src/main/java/com/example/docs/sound/FabricDocsReferenceSounds.java @@ -20,11 +20,15 @@ public class FabricDocsReferenceSounds implements ModInitializer { public void onInitialize() { // This is the basic registering. Use a new class for registering sounds // instead, to keep the ModInitializer implementing class clean! - Registry.register(Registries.SOUND_EVENT, Identifier.of(MOD_ID, "metal_whistle"), - SoundEvent.of(Identifier.of(MOD_ID, "metal_whistle"))); + Registry.register(Registries.SOUND_EVENT, Identifier.of(MOD_ID, "metal_whistle_simple"), + SoundEvent.of(Identifier.of(MOD_ID, "metal_whistle_simple"))); // ... the cleaner approach. // [!code focus] - // CustomSounds.initialize(); // [!code focus] + CustomSounds.initialize(); // [!code focus] + } + + public static Identifier identifierOf(String path) { + return Identifier.of(FabricDocsReference.MOD_ID, path); } } // :::2 diff --git a/reference/latest/src/main/java/com/example/docs/sound/TransitionState.java b/reference/latest/src/main/java/com/example/docs/sound/TransitionState.java new file mode 100644 index 000000000..5e81f1e04 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/sound/TransitionState.java @@ -0,0 +1,23 @@ +package com.example.docs.sound; + +import com.example.docs.FabricDocsReference; + +import net.minecraft.util.Identifier; + +// :::1 +public enum TransitionState { + STARTING("starting_phase"), + RUNNING("idle_phase"), + ENDING("ending_phase"); + + private final Identifier identifier; + + TransitionState(String name) { + this.identifier = Identifier.of(FabricDocsReference.MOD_ID, name); + } + + public Identifier getIdentifier() { + return identifier; + } +} +// :::1 diff --git a/reference/latest/src/main/resources/fabric.mod.json b/reference/latest/src/main/resources/fabric.mod.json index 6dc596cbd..e1c4bbee3 100644 --- a/reference/latest/src/main/resources/fabric.mod.json +++ b/reference/latest/src/main/resources/fabric.mod.json @@ -17,7 +17,8 @@ "com.example.docs.item.FabricDocsReferenceItems", "com.example.docs.block.FabricDocsReferenceBlocks", "com.example.docs.block.entity.FabricDocsReferenceBlockEntities", - "com.example.docs.component.FabricDocsReferenceComponents" + "com.example.docs.component.FabricDocsReferenceComponents", + "com.example.docs.networking.FabricDocsReferenceNetworking" ], "client": [ "com.example.docs.FabricDocsReferenceClient",