mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-20 14:16:26 -04:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41cf6c9efb | ||
|
|
15b73cedb6 | ||
|
|
56e8ed5a92 | ||
|
|
99cfe4f0c3 | ||
|
|
557ace977a | ||
|
|
1ba55b9f90 | ||
|
|
e1960c2b0b | ||
|
|
4bee9412ce | ||
|
|
264c7659f6 | ||
|
|
0c320905e1 | ||
|
|
b92148d0f9 | ||
|
|
6f1ac0c10b | ||
|
|
692d42e626 | ||
|
|
f73e93a5a8 | ||
|
|
21ae3dc9e8 |
@@ -0,0 +1,15 @@
|
||||
package com.cobblemon.mod.common.duck;
|
||||
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface SoundBufferLibraryDuck {
|
||||
|
||||
CompletableFuture<AudioStream> getStreamWithLoopPoints(ResourceLocation audioLocation, int loopStart, int loopEnd);
|
||||
|
||||
CompletableFuture<AudioStream> getStreamWithIntro(ResourceLocation introLocation, ResourceLocation endLocation, boolean isLoop);
|
||||
|
||||
CompletableFuture<AudioStream> getStreamWithIntro(ResourceLocation introLocation, ResourceLocation endLocation, int loopStart, int loopEnd);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cobblemon.mod.common.duck;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public interface SoundDuck {
|
||||
|
||||
int getLoopStart();
|
||||
|
||||
int getLoopEnd();
|
||||
|
||||
ResourceLocation getIntro();
|
||||
|
||||
void setLoopStart(int startInSamples);
|
||||
|
||||
void setLoopEnd(int endInSamples);
|
||||
|
||||
void setIntro(ResourceLocation intro);
|
||||
|
||||
boolean hasLoopPoints();
|
||||
|
||||
boolean hasIntro();
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.bridges;
|
||||
package com.cobblemon.mod.common.duck;
|
||||
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.sounds.Music;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Mixin(Minecraft.class)
|
||||
public class MinecraftMixin {
|
||||
|
||||
/** Used by MusicManager to dispatch the music to play. */
|
||||
@WrapOperation(
|
||||
method = "getSituationalMusic",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/Biome;getBackgroundMusic()Ljava/util/Optional;")
|
||||
)
|
||||
public Optional<Music> getBackgroundMusic(Biome instance, Operation<Optional<Music>> original) {
|
||||
return original.call(instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon;
|
||||
import com.cobblemon.mod.common.client.sound.streams.CompoundAudioStream;
|
||||
import com.cobblemon.mod.common.client.sound.streams.LoopPointAudioStream;
|
||||
import com.cobblemon.mod.common.duck.SoundBufferLibraryDuck;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.JOrbisAudioStream;
|
||||
import net.minecraft.client.sounds.LoopingAudioStream;
|
||||
import net.minecraft.client.sounds.SoundBufferLibrary;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
@Mixin(SoundBufferLibrary.class)
|
||||
public class SoundBufferLibraryMixin implements SoundBufferLibraryDuck {
|
||||
|
||||
@Shadow
|
||||
private ResourceProvider resourceManager;
|
||||
|
||||
/**
|
||||
* Creates a LoopPointAudioStream with defined loop points.
|
||||
*
|
||||
* @param audioLocation The audio file to open for stream.
|
||||
* @param loopStart The starting point of the stream's loop.
|
||||
* @param loopEnd The ending point of the stream's loop.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<AudioStream> getStreamWithLoopPoints(ResourceLocation audioLocation, int loopStart, int loopEnd) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
InputStream introStream = this.resourceManager.open(audioLocation);
|
||||
return new LoopPointAudioStream(JOrbisAudioStream::new, introStream, loopStart, loopEnd);
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, Util.nonCriticalIoPool());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CompoundAudioStream from a JOrbisAudioStream intro and LoopPointAudioStream end.
|
||||
*
|
||||
* @param introLocation The audio file to open for the intro stream.
|
||||
* @param endLocation The audio file to open for the end stream.
|
||||
* @param loopStart The starting point of the ending LoopPointAudioStream's loop.
|
||||
* @param loopEnd The ending point of the ending LoopPointAudioStream's loop.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<AudioStream> getStreamWithIntro(ResourceLocation introLocation, ResourceLocation endLocation, int loopStart, int loopEnd) {
|
||||
return createCompoundStream(
|
||||
introLocation,
|
||||
endLocation,
|
||||
(inputStream) -> new LoopPointAudioStream(JOrbisAudioStream::new, inputStream, loopStart, loopEnd)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CompoundAudioStream from a JOrbisAudioStream intro and a JOrbisAudioStream or LoopingAudioStream end.
|
||||
*
|
||||
* @param introLocation The audio file to open for the intro stream.
|
||||
* @param endLocation The audio file to open for the end stream.
|
||||
* @param isLoop Whether the end stream loops (LoopingAudioStream) or not (JOrbisAudioStream).
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<AudioStream> getStreamWithIntro(ResourceLocation introLocation, ResourceLocation endLocation, boolean isLoop) {
|
||||
return createCompoundStream(
|
||||
introLocation,
|
||||
endLocation,
|
||||
!isLoop ? JOrbisAudioStream::new : (inputStream) -> new LoopingAudioStream(JOrbisAudioStream::new, inputStream)
|
||||
);
|
||||
}
|
||||
|
||||
private CompletableFuture<AudioStream> createCompoundStream(ResourceLocation introLocation, ResourceLocation endLocation, LoopingAudioStream.AudioStreamProvider endProvider) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
InputStream introInputStream = this.resourceManager.open(introLocation);
|
||||
InputStream endInputStream = this.resourceManager.open(endLocation);
|
||||
|
||||
AudioStream introStream = new JOrbisAudioStream(introInputStream);
|
||||
AudioStream endStream = endProvider.create(endInputStream);
|
||||
return new CompoundAudioStream(introStream, endStream);
|
||||
} catch (Exception e) {
|
||||
Cobblemon.LOGGER.error(e.getMessage());
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, Util.nonCriticalIoPool());
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,24 @@
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon;
|
||||
import com.cobblemon.mod.common.api.riding.sound.RideAttenuationModel;
|
||||
import com.cobblemon.mod.common.api.riding.sound.RideLoopSound;
|
||||
import com.cobblemon.mod.common.client.sound.BattleMusicController;
|
||||
import com.cobblemon.mod.common.duck.ChannelDuck;
|
||||
import com.cobblemon.mod.common.duck.SoundBufferLibraryDuck;
|
||||
import com.cobblemon.mod.common.duck.SoundDuck;
|
||||
import com.cobblemon.mod.common.duck.SoundEngineDuck;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import com.mojang.blaze3d.audio.Channel;
|
||||
import net.minecraft.client.resources.sounds.Sound;
|
||||
import net.minecraft.client.resources.sounds.SoundInstance;
|
||||
import net.minecraft.client.resources.sounds.TickableSoundInstance;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.ChannelAccess;
|
||||
import net.minecraft.client.sounds.SoundBufferLibrary;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
@@ -30,12 +36,10 @@ import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Mixin(SoundEngine.class)
|
||||
public abstract class SoundEngineMixin implements SoundEngineDuck {
|
||||
@@ -120,4 +124,23 @@ public abstract class SoundEngineMixin implements SoundEngineDuck {
|
||||
cb.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/** Dispatch custom stream for sounds that have an intro. WrapOperation because the Fabric API has its own Redirect that we don't want to use. */
|
||||
@WrapOperation(method = "play", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/sounds/SoundBufferLibrary;getStream(Lnet/minecraft/resources/ResourceLocation;Z)Ljava/util/concurrent/CompletableFuture;"))
|
||||
public CompletableFuture<AudioStream> getStream(SoundBufferLibrary bufferLibrary, ResourceLocation id, boolean looping, Operation<CompletableFuture<AudioStream>> original, SoundInstance soundInstance) {
|
||||
SoundDuck soundDuck = (SoundDuck)soundInstance.getSound();
|
||||
SoundBufferLibraryDuck libraryDuck = (SoundBufferLibraryDuck)bufferLibrary;
|
||||
if (soundDuck.hasIntro() && soundDuck.hasLoopPoints() && looping) {
|
||||
return libraryDuck.getStreamWithIntro(soundDuck.getIntro(), id, soundDuck.getLoopStart(), soundDuck.getLoopEnd());
|
||||
}
|
||||
else if (soundDuck.hasIntro()) {
|
||||
return libraryDuck.getStreamWithIntro(soundDuck.getIntro(), id, looping);
|
||||
}
|
||||
else if (soundDuck.hasLoopPoints() && looping) {
|
||||
return libraryDuck.getStreamWithLoopPoints(id, soundDuck.getLoopStart(), soundDuck.getLoopEnd());
|
||||
}
|
||||
else {
|
||||
return original.call(bufferLibrary, id, looping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.duck.SoundDuck;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import net.minecraft.client.resources.sounds.Sound;
|
||||
import net.minecraft.client.resources.sounds.SoundEventRegistrationSerializer;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
@Mixin(SoundEventRegistrationSerializer.class)
|
||||
public class SoundEventRegistrationSerializerMixin {
|
||||
|
||||
/** Injects custom fields into Sound resource for loop behavior. */
|
||||
@ModifyReturnValue(method = "getSound", at = @At("RETURN"))
|
||||
private Sound getSound(Sound original, JsonObject json) {
|
||||
int loopStart = GsonHelper.getAsInt(json, "loopStart", 0);
|
||||
int loopEnd = GsonHelper.getAsInt(json, "loopEnd", 0);
|
||||
ResourceLocation intro = ResourceLocation.parse(GsonHelper.getAsString(json, "intro", ""));
|
||||
((SoundDuck)original).setLoopStart(loopStart);
|
||||
((SoundDuck)original).setLoopEnd(loopEnd);
|
||||
((SoundDuck)original).setIntro(intro);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.duck.SoundDuck;
|
||||
import net.minecraft.client.resources.sounds.Sound;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
import static net.minecraft.client.resources.sounds.Sound.SOUND_LISTER;
|
||||
|
||||
@Mixin(Sound.class)
|
||||
public abstract class SoundMixin implements SoundDuck {
|
||||
|
||||
int loopStart = 0;
|
||||
|
||||
int loopEnd = 0;
|
||||
|
||||
ResourceLocation intro = null;
|
||||
|
||||
@Override
|
||||
public ResourceLocation getIntro() {
|
||||
return SOUND_LISTER.idToFile(this.intro);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLoopStart() {
|
||||
return loopStart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLoopEnd() {
|
||||
return loopEnd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoopStart(int startInSamples) {
|
||||
this.loopStart = startInSamples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoopEnd(int endInSamples) {
|
||||
this.loopEnd = endInSamples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIntro(ResourceLocation intro) {
|
||||
this.intro = intro;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLoopPoints() {
|
||||
return (this.loopEnd >= 0 && this.loopStart >= 0) && (this.loopEnd + this.loopStart != 0) && (loopStart < loopEnd || loopStart == 0 || loopEnd == 0);
|
||||
}
|
||||
// this is used to determine if the LoopPointAudioStream is necessary or if the legacy LoopingAudioStream can be used
|
||||
|
||||
@Override
|
||||
public boolean hasIntro() {
|
||||
return this.intro != null && !this.intro.getPath().isBlank();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
package com.cobblemon.mod.common.mixin;
|
||||
|
||||
import com.cobblemon.mod.common.bridges.StructureProcessorListBridge;
|
||||
import com.cobblemon.mod.common.duck.StructureProcessorListBridge;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
+3
-4
@@ -24,15 +24,14 @@ import net.minecraft.world.entity.LivingEntity
|
||||
interface EntityQueryRequirement : Requirement {
|
||||
override fun check(pokemon: Pokemon): Boolean {
|
||||
val queriedEntity = pokemon.entity ?: pokemon.getOwnerPlayer() ?: return false
|
||||
return this.check(pokemon, queriedEntity)
|
||||
return this.check(queriedEntity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given [Pokemon] & [LivingEntity] satisfies the requirement.
|
||||
* Checks if the given [LivingEntity] satisfies the requirement.
|
||||
*
|
||||
* @param pokemon The [Pokemon] being queried.
|
||||
* @param queriedEntity The [LivingEntity] that can be compared in context of a [Level].
|
||||
* @return If the requirement was satisfied.
|
||||
*/
|
||||
fun check(pokemon: Pokemon, queriedEntity: LivingEntity): Boolean
|
||||
fun check(queriedEntity: LivingEntity): Boolean
|
||||
}
|
||||
@@ -207,6 +207,7 @@ object BattleBuilder {
|
||||
val side1Theme = players[0].getBattleTheme()
|
||||
val side2Theme = players[2].getBattleTheme()
|
||||
|
||||
// the music that's played for a side is driven by the opposite leader's theme
|
||||
side1Actors.forEach { it.battleTheme = side2Theme }
|
||||
side2Actors.forEach { it.battleTheme = side1Theme }
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ object CobblemonClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
BattleMusicController.onTick()
|
||||
ClientPlayerIcon.onTick()
|
||||
PartyOverlayDataControl.tick(event.client.isPaused)
|
||||
}
|
||||
|
||||
+6
-1
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.client.sound.BattleMusicController
|
||||
import com.cobblemon.mod.common.client.sound.instances.BattleMusicInstance
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
|
||||
import com.cobblemon.mod.common.ost.BattleThemes
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
|
||||
@@ -26,8 +27,12 @@ object BattleMusicHandler : ClientNetworkPacketHandler<BattleMusicPacket> {
|
||||
override fun handle(packet: BattleMusicPacket, client: Minecraft) {
|
||||
val soundManager = client.soundManager
|
||||
val currMusic = BattleMusicController.music
|
||||
|
||||
// TODO to support legacy behavior, use ResourceLocation for BattleTrackConfig THEN SoundEvent
|
||||
|
||||
val newMusic = packet.music?.let {
|
||||
val event = SoundEvent.createVariableRangeEvent(it)
|
||||
val location = BattleThemes.getByIdentifier(it)?.dispatchTrack()? ?: it
|
||||
val event = SoundEvent.createVariableRangeEvent(location)
|
||||
BattleMusicInstance(event, packet.volume, packet.pitch)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
package com.cobblemon.mod.common.client.sound
|
||||
|
||||
import com.cobblemon.mod.common.client.sound.instances.BattleMusicInstance
|
||||
import com.cobblemon.mod.common.ost.BattleMusicCue
|
||||
import com.cobblemon.mod.common.ost.BattleThemeConfig
|
||||
import com.cobblemon.mod.common.util.pauseSounds
|
||||
import com.cobblemon.mod.common.util.resumeSounds
|
||||
import net.minecraft.client.Minecraft
|
||||
@@ -23,6 +25,15 @@ import net.minecraft.sounds.SoundEvents
|
||||
*/
|
||||
object BattleMusicController {
|
||||
|
||||
/** The active [BattleThemeConfig] for dispatching [BattleMusicInstance]s and subscribing to [BattleMusicCue]s. */
|
||||
private var config: BattleThemeConfig? = null
|
||||
|
||||
/** The queued [BattleMusicCue]. */
|
||||
private var queuedCue: BattleMusicCue? = null
|
||||
|
||||
/** The active [BattleMusicCue]. */
|
||||
private var activeCue: BattleMusicCue? = null
|
||||
|
||||
/** The last [BattleMusicInstance] played. */
|
||||
var music = BattleMusicInstance(SoundEvents.EMPTY, 0.0F, 0.0F)
|
||||
private set
|
||||
@@ -56,4 +67,13 @@ object BattleMusicController {
|
||||
/** Fade out and end the current [BattleMusicInstance]. */
|
||||
fun endMusic() = music.setFade()
|
||||
|
||||
/** Processes the [queuedCue]. */
|
||||
private fun processCue(cue: BattleMusicCue) {
|
||||
|
||||
}
|
||||
|
||||
/** */
|
||||
fun onTick() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.cobblemon.mod.common.client.sound.streams
|
||||
|
||||
import net.minecraft.client.sounds.AudioStream
|
||||
import java.nio.ByteBuffer
|
||||
import javax.sound.sampled.AudioFormat
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
/**
|
||||
* An [AudioStream] composed of multiple streams stitched together.
|
||||
*
|
||||
* Bytes of audio are read according to the active stream's implementation of [AudioStream.read].
|
||||
*
|
||||
* @param streams The [AudioStream]s to chain sequentially. Each stream must share the same [AudioFormat].
|
||||
*
|
||||
* @author Segfault Guy
|
||||
* @since October 19th, 2025
|
||||
*/
|
||||
class CompoundAudioStream @Throws(IllegalArgumentException::class) constructor(vararg streams: AudioStream): AudioStream {
|
||||
|
||||
private val streams = streams.iterator()
|
||||
|
||||
/** The [AudioStream] currently playing. */
|
||||
private var runningStream: AudioStream = this.streams.next()
|
||||
|
||||
private val format = this.runningStream.format
|
||||
|
||||
override fun getFormat() = format
|
||||
|
||||
init {
|
||||
/* Enqueuing buffers of different formats to the same source is not permissible in OpenAL 1.1 standard. */
|
||||
streams.filterNot { runningStream.format.matches(it.format) }.takeIf { it.isNotEmpty() }?.let { incompatibleStreams ->
|
||||
val message = StringBuilder("Cannot compose CompoundAudioStream from streams of different formats:\n${runningStream.format}")
|
||||
incompatibleStreams.forEach {
|
||||
message.append("\n${it.format}")
|
||||
}
|
||||
throw IllegalArgumentException(message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads [size] bytes of PCM, from the current [runningStream], for 1 second worth of audio. */
|
||||
override fun read(size: Int): ByteBuffer? {
|
||||
var buffer = this.runningStream.read(size)
|
||||
if (!buffer.hasRemaining() && this.streams.hasNext()) {
|
||||
this.runningStream.close()
|
||||
this.runningStream = this.streams.next()
|
||||
buffer = this.runningStream.read(size)
|
||||
}
|
||||
return buffer // it's fine if the buffer is not completely full - the stream will sound seamless
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
this.runningStream.close()
|
||||
while (streams.hasNext()) streams.next().close()
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
package com.cobblemon.mod.common.client.sound.streams
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.mojang.blaze3d.audio.Channel
|
||||
import net.minecraft.Util
|
||||
import net.minecraft.client.sounds.AudioStream
|
||||
import net.minecraft.client.sounds.LoopingAudioStream
|
||||
import org.lwjgl.BufferUtils
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.sound.sampled.AudioFormat
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* An [AudioStream] divided into an intro substream and a loop substream, determined by loop points provided with the Sound resource.
|
||||
*
|
||||
* Similar to [LoopingAudioStream], the loop will play indefinitely until stopped.
|
||||
*
|
||||
* @param provider Provider for creating the internal [AudioStream] that decodes the audio.
|
||||
* @param primaryInputStream The initial [InputStream] of the encoded audio to play.
|
||||
* @param loopStart The starting point of the main audio loop in SAMPLES. If 0, loop will run from start until the [loopEnd].
|
||||
* @param loopEnd The ending point of the main audio loop in SAMPLES. If 0, loop will run to completion.
|
||||
*
|
||||
* @author Segfault Guy
|
||||
* @since October 19th, 2025
|
||||
*/
|
||||
class LoopPointAudioStream @Throws(IllegalArgumentException::class) constructor(
|
||||
private val provider: LoopingAudioStream.AudioStreamProvider,
|
||||
primaryInputStream: InputStream,
|
||||
loopStart: Int = 0,
|
||||
loopEnd: Int = 0
|
||||
): AudioStream {
|
||||
|
||||
private val DEBUG_FLAG = true
|
||||
|
||||
/** Amount of buffers of decoded PCM, from the start of the loop, to cache. */
|
||||
private val CACHE_LIMIT = Channel.QUEUED_BUFFER_COUNT // CACHE_LIMIT matches the number of buffers pre-read before feeding to OpenAL
|
||||
|
||||
/** Buffers of PCM that start the loop. */
|
||||
private val bufferCache = mutableListOf<ByteBuffer>() // cache size: 4 buffers, each of size ~176400 (70kb of cache) which is fine
|
||||
|
||||
/** The total number of bytes saved in the [bufferCache]. */
|
||||
private var cacheSize = 0
|
||||
|
||||
/** The number of buffers read from the [bufferCache] */
|
||||
private var bufferCounter = 0
|
||||
|
||||
/** The bytes bytween the end of the cache and the start of the rewinded [decoderStream]. */
|
||||
private var preliminaryBuffer: ByteBuffer? = null
|
||||
|
||||
/** Whether the [preliminaryBuffer] has been read on this pass. */
|
||||
private var readPrem: Boolean = false
|
||||
|
||||
/** The OGG Vorbis. This is encoded audio. */
|
||||
private lateinit var encodedStream: BufferedInputStream
|
||||
|
||||
/** The Vorbis decoder. This decodes PCM from the [encodedStream] on calls to [AudioStream.read]. */
|
||||
private lateinit var decoderStream: AudioStream
|
||||
|
||||
/** The starting point of the loop in BYTES. */
|
||||
private var loopStart by Delegates.notNull<Int>()
|
||||
|
||||
/** The ending point of the loop in BYTES. */
|
||||
private var loopEnd by Delegates.notNull<Int>()
|
||||
|
||||
/** Whether this is the first pass of the [encodedStream] and we haven't looped yet. */
|
||||
private var firstPass = true
|
||||
|
||||
/** Whether [decoderStream] has been rewinded to [loopStart]. */
|
||||
private var loopPrepared = false
|
||||
|
||||
/** The total number of bytes that have been read from the decoded stream. */
|
||||
private var runningRead = 0
|
||||
|
||||
init {
|
||||
if (loopStart > 0 && loopEnd > 0 && loopEnd < loopStart) throw IllegalArgumentException("Invalid loop points: non-zero end precedes non-zero start")
|
||||
if (loopStart < 0 || loopEnd < 0) throw IllegalArgumentException("Invalid loop points: only non-negative values allowed")
|
||||
|
||||
this.encodedStream = BufferedInputStream(primaryInputStream)
|
||||
this.encodedStream.mark(Int.Companion.MAX_VALUE)
|
||||
this.buildDecoderStream()
|
||||
|
||||
this.loopStart = this.samplesToBytes(loopStart)
|
||||
this.loopEnd = this.samplesToBytes(loopEnd)
|
||||
}
|
||||
|
||||
override fun getFormat(): AudioFormat? {
|
||||
return this.decoderStream.format
|
||||
}
|
||||
|
||||
/** Reads 1 second worth of PCM (bytes) for an OpenAL buffer (this will be AT LEAST 176400 bytes for stereo audio formats) */
|
||||
override fun read(size: Int): ByteBuffer? {
|
||||
|
||||
// if decoderStream has not finished rewinding, there's a problem
|
||||
if (!this.firstPass && !this.loopPrepared) {
|
||||
Cobblemon.LOGGER.warn("Decoder not rewinded!")
|
||||
}
|
||||
|
||||
this.debug("read " + size + " | runningRead: " + this.runningRead + " | loopStart: " + this.loopStart + " | loopEnd: " + this.loopEnd)
|
||||
|
||||
val buffer = this.getCachedOrRawBuffer(size)
|
||||
val sizeAfterRead = this.runningRead + buffer.remaining()
|
||||
|
||||
// if this read reaches the end loop point
|
||||
if (loopEnd > 0 && sizeAfterRead >= this.loopEnd) {
|
||||
val remainderBeforeEnd = this.loopEnd - this.runningRead
|
||||
// limit buffer if it goes past the loop point (cached buffers don't apply as they are always within the loop points)
|
||||
if (buffer.remaining() > remainderBeforeEnd)
|
||||
{
|
||||
this.debug("limit and reset read")
|
||||
buffer.limit(remainderBeforeEnd)
|
||||
}
|
||||
else {
|
||||
this.debug("reset read")
|
||||
}
|
||||
this.resetLoop()
|
||||
}
|
||||
// if loopEnd is 0 and the entire stream is read, or exaggerated and the stream is exhausted before reaching the point
|
||||
else if (!buffer.hasRemaining()) {
|
||||
this.debug("exhaustive read")
|
||||
this.resetLoop()
|
||||
}
|
||||
// standard read
|
||||
else {
|
||||
this.debug("standard read")
|
||||
this.runningRead += buffer.remaining();
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a [ByteBuffer] of varying size from the [bufferCache] if this is not the [firstPass] and a cached buffer
|
||||
* is available, or reads approximately [size] bytes from the [decoderStream].
|
||||
*/
|
||||
private fun getCachedOrRawBuffer(size: Int): ByteBuffer {
|
||||
val cacheExists = CACHE_LIMIT > 0 && !this.bufferCache.isEmpty() && this.bufferCounter < this.bufferCache.size
|
||||
|
||||
// get buffer from cache
|
||||
if (cacheExists && !this.firstPass) {
|
||||
val toReturn = this.bufferCache.get(this.bufferCounter++)
|
||||
this.debug("get cached buffer: " + toReturn.remaining())
|
||||
return toReturn
|
||||
}
|
||||
// TODO coalesce this with bufferCache
|
||||
else if (!this.readPrem && this.preliminaryBuffer != null && !this.firstPass) {
|
||||
this.readPrem = true
|
||||
val toReturn = this.preliminaryBuffer!!
|
||||
this.debug("read prem buffer: " + toReturn.remaining())
|
||||
return toReturn
|
||||
}
|
||||
// decode a buffer of approximate size
|
||||
else {
|
||||
val toReturn = this.decoderStream.read(size)
|
||||
this.debug("read raw: " + toReturn.remaining())
|
||||
this.cacheBuffer(toReturn)
|
||||
return toReturn
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches [buffer] in the [bufferCache] during the [firstPass] if the [CACHE_LIMIT] has not been reached and the buffer
|
||||
* contains bytes within the [loopStart] and the [loopEnd] boundaries.
|
||||
*/
|
||||
private fun cacheBuffer(buffer: ByteBuffer) {
|
||||
val canCacheBuffer = CACHE_LIMIT > 0 && loopStart > 0 && this.bufferCache.size < CACHE_LIMIT && this.firstPass
|
||||
val canStartCaching = this.runningRead + buffer.remaining() > this.loopStart
|
||||
if (!canCacheBuffer || !canStartCaching) return
|
||||
|
||||
// offset used - so that only the tail of a buffer that contains the loopStart point is cached
|
||||
val remainderBeforeStart = Math.max(this.loopStart - this.runningRead, 0)
|
||||
var remainderBeforeEnd = 0
|
||||
|
||||
// only cache the head of a buffer that goes past the loopEnd point
|
||||
if (loopEnd > 0 && buffer.remaining() + this.runningRead > this.loopEnd) {
|
||||
remainderBeforeEnd = this.loopEnd - this.runningRead - remainderBeforeStart
|
||||
// NOTE: read will resetLoop and stop caching if CACHE_LIMIT has not been reached but loopEnd has
|
||||
}
|
||||
// otherwise, cache the entire remainder of the buffer
|
||||
else {
|
||||
remainderBeforeEnd = buffer.remaining() - remainderBeforeStart
|
||||
}
|
||||
|
||||
if (remainderBeforeEnd <= 0) return
|
||||
|
||||
// copy to avoid mutating provided buffer
|
||||
val buffed = BufferUtils.createByteBuffer(remainderBeforeEnd)
|
||||
buffed.put(0, buffer, remainderBeforeStart, remainderBeforeEnd)
|
||||
buffed.rewind()
|
||||
|
||||
this.debug("cache buffer | remainderBeforeStart: " + remainderBeforeStart + " | remainderBeforeEnd: " + remainderBeforeEnd + " | size: " + buffed.remaining())
|
||||
|
||||
this.bufferCache.add(buffed)
|
||||
this.cacheSize += buffed.remaining()
|
||||
|
||||
this.debug("cacheSize: " + cacheSize + " bufferCacheSize: " + this.bufferCache.size)
|
||||
}
|
||||
|
||||
/** Converts PCM samples to bytes. */
|
||||
private fun samplesToBytes(sampleAmount: Int): Int {
|
||||
val format = this.format ?: return 0
|
||||
return sampleAmount * format.getSampleSizeInBits() / 8 * format.getChannels()
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind [decoderStream] to start from [loopStart].
|
||||
*
|
||||
* NOTE: Assumes that [bufferCache] is finalized.
|
||||
*/
|
||||
private fun resetLoop() {
|
||||
// reset state info
|
||||
this.loopPrepared = false
|
||||
this.runningRead = this.loopStart
|
||||
this.bufferCounter = 0
|
||||
this.firstPass = false
|
||||
|
||||
this.debug("reset")
|
||||
|
||||
// if the entire loop is small enough to fit the cache, skip rewinding the decoderStream
|
||||
if (this.bufferCache.size > 0 && this.bufferCache.size < CACHE_LIMIT) {
|
||||
this.debug("comprehensive cache")
|
||||
this.loopPrepared = true
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Seeking is implemented agnostic of the OGG Vorbis decoder. Seek to the loop start by decoding PCM in bulk from
|
||||
the beginning of the OGG Vorbis up until the desired sample offset is reached. Then for playing the loop, continue
|
||||
streaming decoded PCM into OpenAL buffers.
|
||||
*/
|
||||
this.decoderStream.close()
|
||||
this.encodedStream.reset()
|
||||
this.buildDecoderStream()
|
||||
|
||||
// if loopStart is 0, there's no cache, so loop starts by reading from the new decoderStream
|
||||
if (this.bufferCache.size == 0) {
|
||||
this.debug("nonexistent cache")
|
||||
this.loopPrepared = true
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
After the first pass, encodedStream will have the entire Vorbis OGG buffered and there won't be any subsequent reads
|
||||
from disk. However, there's still latency in decoding a lot of PCM. This needs to be done asynchronously, otherwise
|
||||
the client will momentarily freeze each time the loop resets. In the unlikely case that the decoderStream is not
|
||||
rewinded by the time the Channel needs to read a buffer, it will be read from bufferCache.
|
||||
*/
|
||||
CompletableFuture.supplyAsync<Any?>({
|
||||
try {
|
||||
val decoderStart = this.loopStart + this.cacheSize
|
||||
val buffer = decoderStream.read(decoderStart)
|
||||
|
||||
// because the deecoderStream will only read an approximation, need to account for additional bytes
|
||||
if (this.preliminaryBuffer == null && buffer.remaining() > decoderStart) {
|
||||
this.debug("rewind buffer size: " + buffer.remaining() + " overshot by: " + (buffer.remaining() - decoderStart))
|
||||
this.preliminaryBuffer = BufferUtils.createByteBuffer(buffer.remaining() - decoderStart)
|
||||
this.preliminaryBuffer!!.put(0, buffer, decoderStart, buffer.remaining() - decoderStart)
|
||||
this.preliminaryBuffer!!.rewind()
|
||||
// decoderStream.read is deterministic, so this should only be set once
|
||||
}
|
||||
this.readPrem = false
|
||||
this.loopPrepared = true
|
||||
this.debug("decoderStream rewinded")
|
||||
return@supplyAsync null
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}, Util.nonCriticalIoPool())
|
||||
}
|
||||
|
||||
private fun buildDecoderStream() {
|
||||
this.decoderStream = provider.create(LoopingAudioStream.NoCloseBuffer(this.encodedStream))
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
this.decoderStream.close()
|
||||
this.encodedStream.close() // closes the InputStream used
|
||||
}
|
||||
|
||||
private fun debug(log: String) {
|
||||
if (this.DEBUG_FLAG) Cobblemon.LOGGER.error(log)
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -20,8 +20,10 @@ import net.minecraft.resources.ResourceLocation
|
||||
*
|
||||
* Handled by [com.cobblemon.mod.common.client.net.battle.BattleMusicHandler].
|
||||
*
|
||||
* @property music The location of the sound to play during a battle. If null, gracefully ends the currently playing sound.
|
||||
* @property restartExisting If true and the location of [music] matches the currently playing sound, will restart from the beginning.
|
||||
* @property music The location of a [SoundEvent], or the location of the []to dispatch a sound from, to play during a battle.
|
||||
* If null, gracefully ends the currently playing sound.
|
||||
*
|
||||
* @property restartExisting If true and [music] matches the location of the currently playing sound, will restart from the beginning.
|
||||
*
|
||||
* @author Segfault Guy
|
||||
* @since April 20th, 2023
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.cobblemon.mod.common.net.messages.client.data
|
||||
|
||||
class SoundTrackConfigRegistrySyncPacket {
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.cobblemon.mod.common.ost
|
||||
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
|
||||
/**
|
||||
* Event to signal a transition in battle music. When received by the client, will either replace the currently playing
|
||||
* battle music with a new instance, or pause and temporarily switches from the current instance to play a [soundEvent].
|
||||
*
|
||||
* @author Segfault Guy
|
||||
* @since December 16, 2925
|
||||
*/
|
||||
data class BattleMusicCue(
|
||||
/** The identifier of this cue. */
|
||||
val id: ResourceLocation,
|
||||
/** The track or sound effect to play. */
|
||||
val soundEvent: SoundEvent,
|
||||
/** Whether the [soundEvent] should replace and become the main track. */
|
||||
val replace: Boolean,
|
||||
/** Determines which cue's [soundEvent] plays if multiple cues are scheduled at once. Priority is lost when cue becomes
|
||||
* the main track. */
|
||||
val priority: Int,
|
||||
/** Time in seconds before the [soundEvent] starts playing. */
|
||||
val delay: Int,
|
||||
/** If the currently playing track should fade out, and the [soundEvent] should fade in. */
|
||||
val fade: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cobblemon.mod.common.ost
|
||||
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
/**
|
||||
* A configuration that dispatches a [BattleTrack] to use when playing battle music.
|
||||
*
|
||||
* @author Segfault Guy
|
||||
* @since January 6th, 2026
|
||||
*/
|
||||
class BattleThemeConfig(
|
||||
id: ResourceLocation,
|
||||
/** */
|
||||
override val tracks: Collection<BattleTrack>,
|
||||
/** 2nd-priority [BattleMusicCue]s that are subscribed by every track in the configuration. */
|
||||
val cues: Collection<BattleMusicCue>,
|
||||
) : SoundTrackConfig(id, tracks)
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.cobblemon.mod.common.ost
|
||||
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
|
||||
object BattleThemes : JsonDataRegistry<BattleThemeConfig> {
|
||||
|
||||
override val resourcePath = "battle_themes"
|
||||
|
||||
|
||||
override val id: ResourceLocation = cobblemonResource(this.resourcePath)
|
||||
override val type = PackType.SERVER_DATA
|
||||
override val observable = SimpleObservable<BattleThemeConfig>()
|
||||
|
||||
override val gson: Gson = GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.create()
|
||||
|
||||
override val typeToken = TypeToken.get(BattleThemeConfig::class.java)
|
||||
|
||||
/** */
|
||||
private val battleSoundTrackConfigs = mutableMapOf<ResourceLocation, BattleThemeConfig>()
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, BattleThemeConfig>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getByIdentifier(identifier: ResourceLocation): BattleThemeConfig? = this.battleSoundTrackConfigs[identifier]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cobblemon.mod.common.ost
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class BattleTrack(
|
||||
val battle_conditions
|
||||
) : SoundTrack {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.cobblemon.mod.common.ost
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.EntityQueryRequirement
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
/**
|
||||
* r
|
||||
*
|
||||
* @author Segfault Guy
|
||||
* @since December 16, 2025
|
||||
*/
|
||||
open class SoundTrack {
|
||||
|
||||
val ambient: ResourceLocation
|
||||
val wildLocation: ResourceLocation
|
||||
val trainerLocation: ResourceLocation
|
||||
val priority: Int = 1
|
||||
val requirements: MutableSet<EntityQueryRequirement>
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cobblemon.mod.common.ost
|
||||
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
/**
|
||||
* A configuration that dispatches a [SoundTrack] to use when playing music.
|
||||
*
|
||||
* @author Segfault Guy
|
||||
* @since January 6th, 2026
|
||||
*/
|
||||
open class SoundTrackConfig(
|
||||
/** The identifier of this configuration. */
|
||||
val id: ResourceLocation,
|
||||
/** The */
|
||||
open val tracks: Collection<SoundTrack>,
|
||||
) {
|
||||
|
||||
fun dispatchTrack(): BattleTrack {
|
||||
// TODO based on requirements
|
||||
}
|
||||
}
|
||||
|
||||
// should this be templated instead?
|
||||
+2
@@ -67,6 +67,8 @@ object CobblemonRequirementAdapter : RequirementAdapter {
|
||||
this.registerType(PropertyRangeRequirement.ADAPTER_VARIANT, PropertyRangeRequirement::class)
|
||||
this.registerType(AdvancementRequirement.ADAPTER_VARIANT, AdvancementRequirement::class)
|
||||
this.registerType(OwnerHoldsItemRequirement.ADAPTER_VARIANT, OwnerHoldsItemRequirement::class)
|
||||
this.registerType(DepthRequirement.ADAPTER_VARIANT, DepthRequirement::class)
|
||||
this.registerType(SkyRequirement.ADAPTER_VARIANT, SkyRequirement::class)
|
||||
}
|
||||
|
||||
val gson: Gson by lazy {
|
||||
|
||||
+2
-3
@@ -9,13 +9,12 @@
|
||||
package com.cobblemon.mod.common.pokemon.requirements
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.EntityQueryRequirement
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
||||
/**
|
||||
* A [EntityQueryRequirement] for when a [Pokemon] is expected to be in a certain area.
|
||||
* A [EntityQueryRequirement] for when a [LivingEntity] is expected to be in a certain area.
|
||||
*
|
||||
* @property box The [AABB] expected to be in.
|
||||
* @author Licious
|
||||
@@ -27,5 +26,5 @@ class AreaRequirement : EntityQueryRequirement {
|
||||
}
|
||||
|
||||
val box: AABB = AABB.unitCubeFromLowerCorner(Vec3.ZERO)
|
||||
override fun check(pokemon: Pokemon, queriedEntity: LivingEntity) = box.contains(queriedEntity.position())
|
||||
override fun check(queriedEntity: LivingEntity) = box.contains(queriedEntity.position())
|
||||
}
|
||||
+2
-2
@@ -18,7 +18,7 @@ import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.level.biome.Biome
|
||||
|
||||
/**
|
||||
* A [Requirement] for when a [Pokemon] is expected to be in a certain [Biome].
|
||||
* A [Requirement] for when a [LivingEntity] is expected to be in a certain [Biome].
|
||||
*
|
||||
* @property biomeCondition The [ResourceLocation] of the [Biome] the queried entity is expected to be in.
|
||||
* @author Licious
|
||||
@@ -27,7 +27,7 @@ import net.minecraft.world.level.biome.Biome
|
||||
class BiomeRequirement : EntityQueryRequirement {
|
||||
val biomeCondition: RegistryLikeCondition<Biome>? = null
|
||||
val biomeAnticondition: RegistryLikeCondition<Biome>? = null
|
||||
override fun check(pokemon: Pokemon, queriedEntity: LivingEntity): Boolean {
|
||||
override fun check(queriedEntity: LivingEntity): Boolean {
|
||||
val biome = queriedEntity.level().getBiome(queriedEntity.blockPosition()).value()
|
||||
val registry = queriedEntity.level().registryAccess().registryOrThrow(Registries.BIOME)
|
||||
return (biomeCondition == null || biomeCondition.fits(biome, registry)) && (biomeAnticondition == null || !biomeAnticondition.fits(biome, registry))
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.cobblemon.mod.common.pokemon.requirements
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.EntityQueryRequirement
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
|
||||
/**
|
||||
* An [EntityQueryRequirement] that evaluates whether a [LivingEntity] is at a certain block level.
|
||||
*
|
||||
* @property minY The minimum block level to satisfy this requirement. If null, defaults to the dimension's min height.
|
||||
* @property maxY The maximum block level to satisfy this requirement. If null, defaults to the dimension's max height.
|
||||
* @author Segfault Guy
|
||||
* @since December 16, 2025
|
||||
*/
|
||||
class DepthRequirement(val minY: Int?, val maxY: Int?) : EntityQueryRequirement {
|
||||
|
||||
override fun check(queriedEntity: LivingEntity): Boolean {
|
||||
val minLevelY = queriedEntity.level().minBuildHeight
|
||||
val maxLevelY = queriedEntity.level().maxBuildHeight
|
||||
val test = minY?.coerceIn(minLevelY, maxLevelY) ?: minLevelY
|
||||
val test2 = maxY?.coerceIn(minLevelY, maxLevelY) ?: maxLevelY
|
||||
|
||||
return queriedEntity.blockPosition().y in test..test2
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ADAPTER_VARIANT = "depth"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@ class MoonPhaseRequirement(moonPhase: MoonPhase) : EntityQueryRequirement {
|
||||
|
||||
val moonPhase: MoonPhase = moonPhase
|
||||
|
||||
override fun check(pokemon: Pokemon, queriedEntity: LivingEntity): Boolean {
|
||||
override fun check(queriedEntity: LivingEntity): Boolean {
|
||||
return try {
|
||||
val moonPhase = MoonPhase.ofWorld(queriedEntity.level())
|
||||
this.moonPhase == moonPhase
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.cobblemon.mod.common.pokemon.requirements
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.requirement.EntityQueryRequirement
|
||||
import net.minecraft.world.entity.LivingEntity
|
||||
|
||||
/**
|
||||
* An [EntityQueryRequirement] that evaluates whether a [LivingEntity] is exposed to a dimension's sky.
|
||||
*
|
||||
* @param invert If true, this requirement evaluates whether the entity CANNOT see a dimension's sky.
|
||||
* @author Segfault Guy
|
||||
* @since December 16, 2025
|
||||
*/
|
||||
class SkyRequirement(val invert: Boolean = false) : EntityQueryRequirement {
|
||||
|
||||
override fun check(queriedEntity: LivingEntity) = queriedEntity.level().canSeeSky(queriedEntity.blockPosition()).xor(invert)
|
||||
|
||||
companion object {
|
||||
const val ADAPTER_VARIANT = "sky"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -16,7 +16,7 @@ import net.minecraft.world.entity.LivingEntity
|
||||
import net.minecraft.world.level.levelgen.structure.Structure
|
||||
|
||||
/**
|
||||
* A [EntityQueryRequirement] for when a [Pokemon] is expected to be in a certain structure.
|
||||
* A [EntityQueryRequirement] for when a [LivingEntity] is expected to be in a certain structure.
|
||||
*
|
||||
* @property structureCondition The [Identifier] of the structure the queried entity is expected to be in.
|
||||
* @author whatsy
|
||||
@@ -25,7 +25,7 @@ import net.minecraft.world.level.levelgen.structure.Structure
|
||||
class StructureRequirement : EntityQueryRequirement {
|
||||
val structureCondition: RegistryLikeCondition<Structure>? = null
|
||||
val structureAnticondition: RegistryLikeCondition<Structure>? = null
|
||||
override fun check(pokemon: Pokemon, queriedEntity: LivingEntity): Boolean {
|
||||
override fun check(queriedEntity: LivingEntity): Boolean {
|
||||
val structures = queriedEntity.level().getChunk(queriedEntity.blockPosition()).allReferences
|
||||
val registry = queriedEntity.level().registryAccess().registryOrThrow(Registries.STRUCTURE)
|
||||
return (structureCondition == null || structures.any { structureCondition.fits(it.key, registry) }) && (structureAnticondition == null || !structures.any { structureAnticondition.fits(it.key, registry) })
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import net.minecraft.world.entity.LivingEntity
|
||||
*/
|
||||
class TimeRangeRequirement : EntityQueryRequirement {
|
||||
val range = TimeRange(0..23999)
|
||||
override fun check(pokemon: Pokemon, queriedEntity: LivingEntity) = this.range.contains((queriedEntity.level().dayTime() % DAY_DURATION).toInt())
|
||||
override fun check(queriedEntity: LivingEntity) = this.range.contains((queriedEntity.level().dayTime() % DAY_DURATION).toInt())
|
||||
companion object {
|
||||
const val ADAPTER_VARIANT = "time_range"
|
||||
private const val DAY_DURATION = 24000
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ class WeatherRequirement : EntityQueryRequirement {
|
||||
val isRaining: Boolean? = null
|
||||
val isThundering: Boolean? = null
|
||||
|
||||
override fun check(pokemon: Pokemon, queriedEntity: LivingEntity): Boolean {
|
||||
override fun check(queriedEntity: LivingEntity): Boolean {
|
||||
val world = queriedEntity.level()
|
||||
return when {
|
||||
isRaining == true && !world.isRaining -> false
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
/**
|
||||
* A [EntityQueryRequirement] for when a [Pokemon] is expected to be in a [Level].
|
||||
* A [EntityQueryRequirement] for when a [LivingEntity] is expected to be in a [Level].
|
||||
*
|
||||
* @property identifier The [ResourceLocation] of the [Level] the queried entity is expected to be in.
|
||||
* @author Licious
|
||||
@@ -26,5 +26,5 @@ class WorldRequirement : EntityQueryRequirement {
|
||||
const val ADAPTER_VARIANT = "world"
|
||||
}
|
||||
val identifier: ResourceLocation = ResourceLocation.parse("minecraft:the_overworld")
|
||||
override fun check(pokemon: Pokemon, queriedEntity: LivingEntity) = queriedEntity.level().dimension().location() == this.identifier
|
||||
override fun check(queriedEntity: LivingEntity) = queriedEntity.level().dimension().location() == this.identifier
|
||||
}
|
||||
@@ -151,4 +151,9 @@ transitive-accessible method net/minecraft/world/food/FoodData add (IF)V
|
||||
|
||||
transitive-accessible method net/minecraft/client/model/HumanoidModel setupAttackAnimation (Lnet/minecraft/world/entity/LivingEntity;F)V
|
||||
transitive-accessible method net/minecraft/client/model/HumanoidModel poseRightArm (Lnet/minecraft/world/entity/LivingEntity;)V
|
||||
transitive-accessible method net/minecraft/client/model/HumanoidModel poseLeftArm (Lnet/minecraft/world/entity/LivingEntity;)V
|
||||
transitive-accessible method net/minecraft/client/model/HumanoidModel poseLeftArm (Lnet/minecraft/world/entity/LivingEntity;)V
|
||||
|
||||
transitive-accessible class net/minecraft/client/sounds/LoopingAudioStream$NoCloseBuffer
|
||||
transitive-accessible method net/minecraft/client/sounds/LoopingAudioStream$NoCloseBuffer <init> (Ljava/io/InputStream;)V
|
||||
|
||||
transitive-accessible field com/mojang/blaze3d/audio/Channel QUEUED_BUFFER_COUNT I
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
"ItemPropertiesMixin",
|
||||
"MusicManagerMixin",
|
||||
"ParticleManagerMixin",
|
||||
"SoundBufferLibraryMixin",
|
||||
"SoundEngineMixin",
|
||||
"SoundEventRegistrationSerializerMixin",
|
||||
"SoundManagerMixin",
|
||||
"SoundMixin",
|
||||
"VillagerProfessionLayerMixin",
|
||||
"accessor.EntryListWidgetAccessor",
|
||||
"accessor.KeyBindingAccessor",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.cobblemon.mod.neoforge.mixin.client;
|
||||
|
||||
import com.cobblemon.mod.common.duck.SoundBufferLibraryDuck;
|
||||
import com.cobblemon.mod.common.duck.SoundDuck;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import net.minecraft.client.resources.sounds.Sound;
|
||||
import net.minecraft.client.resources.sounds.SoundInstance;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.SoundBufferLibrary;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Mixin(SoundEngine.class)
|
||||
public class SoundEngineMixin {
|
||||
|
||||
/**
|
||||
* Dispatch custom stream for sounds that have an intro. This is needed because NeoForge overwrites the SoundBufferLibrary$getStream invocation.
|
||||
* Because this is a WrapOperation it's compatible with NeoForge's PlayStreamingSourceEvent.
|
||||
*/
|
||||
@WrapOperation(method = "play", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/sounds/SoundInstance;getStream(Lnet/minecraft/client/sounds/SoundBufferLibrary;Lnet/minecraft/client/resources/sounds/Sound;Z)Ljava/util/concurrent/CompletableFuture;"))
|
||||
public CompletableFuture<AudioStream> getStream(SoundInstance soundInstance, SoundBufferLibrary bufferLibrary, Sound sound, boolean looping, Operation<CompletableFuture<AudioStream>> original) {
|
||||
SoundDuck soundDuck = (SoundDuck)sound;
|
||||
SoundBufferLibraryDuck libraryDuck = (SoundBufferLibraryDuck)bufferLibrary;
|
||||
if (soundDuck.hasIntro() || (soundDuck.hasLoopPoints() && looping)) {
|
||||
return libraryDuck.getCompoundStream();
|
||||
}
|
||||
else {
|
||||
return original.call(soundInstance, bufferLibrary, sound, looping);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user