diff --git a/.gitignore b/.gitignore index f567b8c..3156851 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ replay_*.log # include the music pack on github, not sure why I added this originally but adding it back now # src/main/resources/musicpack/music/ +issues.github-issues + diff --git a/README.md b/README.md index b42f08b..f8f139f 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,154 @@ Reactive Music trades Minecraft's default music for something dynamic, reactive, Reactive Music is based off a music pack I originally made for the Ambience mod back in 1.12. Now rebuilt to be a standalone mod for modern minecraft. Discord: https://discord.gg/vJ6BCjPR3R + +# ⚠️ This is an alpha experimental fork of Reactive Music. + +New features were added, and the codebase was _**heavily**_ refactored with a lot of changes to internals. And also a whole lot of Javadocs comments for convenience and hopefully, a quicker adoption and improvement of this refreshed codebase that will allow a new ecosystem of **mods made for other mods** using the powerful new tools and systems available to developers through Reactive Music's Songpack & Event system. + +Keep reading for a quick overview of the changes and additions. + +
+ +

Code abstraction

+
+ +The codebase of Reactive Music `v1.x.x` was monolithic in various places, making it difficult to alter the main flow of the code or add features. I have aimed to improve developer experience through the following changes: + +- The built-in events were moved to the new plugin system, allowing logic to be worked on within a final plugin class - making it easier to expand on these features and add new ones. +- The core logic behind the songpack loading & selection systems have been extracted into various new classes, making it easier to reimplement utility methods or hook into their logic at various points in the flow. + +
+
+ +

Audio subsystem

+
+ +The `PlayerThread` class was a single instance of an audio player - which did it's job very well. But also with heavy restriction. Now, audio players and threads are created through `PlayerManager` classes, which handle tick-based fading, support making external calls, and more importantly - allow *multiple audio streams to exist.* These new instances are fully configurable, and allow for a deeper dive into Reactive Music not only as an event based *music* system for Minecraft, but basically an event based *sound engine*. + +Some ideas that I have personally planned using this new functionality: + +- Right clicking mobs with an empty hand plays an audio dialogue. +- Various actions the player may randomly trigger self-talk dialogue. +- Adding more immersive sounds to various objects. +- Fire gets louder the more things that are on fire. +- ^ in the same way - more immersive water ambience near specific biomes. + +
+
+ +

API entrypoint

+
+ +Having a single entry point into Reactive Music's systems means it's easier to modify functionality, or hook into. This also makes developing new plugins for Reactive Music fairly straightforward - with the goal of making it easy to create new functionality around the songpack and event system. + +As this fork of the mod is in an experimental alpha state, the API may undergo breaking changes at any time. Be prepared for the possibility of having to refactor your code on new feature releases or API updates. + +
+
+ +

ReactiveMusicPlugin.java

+
+ +The main addition to this version of Reactive Music is the powerful plugin system. Using a service loader pattern, Reactive Music can now import external classes which follow the structure of the new `ReactiveMusicPlugin` class. To see examples of how this system works, take a look at the code for the built-in events now found in `plugins/` + +
+ +--- + +# Changelog 💃 09.09.25 + + Changes: + +* New `class` based command handler structure for client commands. +* Added various new commands, useful for debugging or to aid in songpack creation. + +--- +
+ [ 09.09.25 ] + + Changes: + +* New `class` based command handler structure for client commands. +* Added various new commands, useful for debugging or to aid in songpack creation. + +
+ +
+ [ 09.08.25 ] + + Changes: + + * New `class` -> `RMGainSupplier` for usage in the map of gain suppliers in the player implementation. + * New API interface `GainSupplier`. + * Changed `requestGainRecompute()` in `RMPlayer` to use gain suppliers instead of hardcoded values. + * Overlay built-in plugin and Minecraft jukebox ducking now uses gain suppliers in the primary audio player. + +
+ +
+ [ 09.05.25 ] + + Changes: + + * Changed the final plugin class' API from an `interface` to a `class` to use `extends` instead of `implements` + * New `class` -> `RMPluginIdentifier` instanced by plugins on construction + * New `class` -> `RMEventRecord` which holds the registrar plugin's `RMPluginIdentifier` + * New API interfaces `EventRecord` and `PluginIdentifier` + * Changed the `Map` in `RMSongpackEvent` to take types `` + * Code adjusted to use `EventRecord` instead of `SongpackEvent` where applicable. + +
+ +
+ [ 09.01.25 ] + + Fixes: + + * Whoops! That was really broken, wasn't it? + +
+
+ [ 08.31.25 ] + + Changes: + + * API handles have been modified. + * Some redundant methods removed. + +
+
+ [ 08.26.25 ] + + Fixes: + + * OverlayTrackPlugin (Built-in) now properly *doesn't* stop the primary music player, by keeping `currentEntry` and disabling parts of `ReactiveMusicCore`. It's a bit coupled for now, but the plan is to clean this up and use it as a base for more expandability through the API. + + Changes: + + * API overhaul, consumer vs internal boundary is clearer and cleaner. + * Lots of filename changes because of the above point. + +
+
+ [ 08.21.25 ] + Fixes: + + * Implemented a workaround for a bug where the audio player's gain was not set correctly before playing. The audio player is now primed with a small bit of silence before receiving samples. + +
+
+ [ 08.20.25 ] + + Fixes: + + * Fixed an issue that caused the core logic not to switch to a new song after a song had completed. + + Changes: + + * `currentSong` and `currentEntry` are now accessible through the API. + * Added a `skip` command, which *should* force the core logic to move on to the next song. + +
+ + diff --git a/build.gradle b/build.gradle index a39b76d..24ef5c0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,19 @@ plugins { + id 'java' id 'fabric-loom' version "${loom_version}" id 'maven-publish' } + +loom { + runs { + client { + vmArgs "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" + } + } +} + + version = project.mod_version + "+" + project.minecraft_version group = project.maven_group @@ -89,6 +100,9 @@ java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } jar { @@ -97,20 +111,55 @@ jar { } } -// configure the maven publication +// Ensure Loom’s remapped outputs get published publishing { - publications { - create("mavenJava", MavenPublication) { - artifactId = project.archives_base_name - from components.java - } - } + publications { + mavenJava(MavenPublication) { + groupId = project.group + artifactId = project.archivesBaseName + version = project.version + + artifact(remapJar) { + builtBy remapJar + } + artifact(remapSourcesJar) { + builtBy remapSourcesJar + } + + pom { + name = project.archivesBaseName + description = 'Mocha (re)Mix of Reactive Music!' + url = 'https://github.com/rocamocha/ReactiveMusic' + + licenses { + license { + name = 'GPLv3' + url = 'https://opensource.org/license/gpl-3-0' + } + } + developers { + developer { + id = 'rocamocha' + name = 'Jeric Rocamora' + } + } + scm { + url = 'https://github.com/rocamocha/ReactiveMusic' + connection = 'scm:git:https://github.com/rocamocha/ReactiveMusic.git' + developerConnection = 'scm:git:ssh://git@github.com/rocamocha/ReactiveMusic.git' + } + } + } + } + repositories { + maven { + name = "GitHubPackages" // can be anything + url = uri("https://maven.pkg.github.com/rocamocha/ReactiveMusic") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: project.findProperty("gpr.user") + password = System.getenv("GITHUB_TOKEN") ?: project.findProperty("gpr.key") + } + } + } +} - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } -} \ No newline at end of file diff --git a/docs/PLUGIN API/Beginner's-Guide-to-Plugin-Development.md b/docs/PLUGIN API/Beginner's-Guide-to-Plugin-Development.md new file mode 100644 index 0000000..1695e42 --- /dev/null +++ b/docs/PLUGIN API/Beginner's-Guide-to-Plugin-Development.md @@ -0,0 +1,63 @@ +In this section I will do my best to provide instruction, as well as recommendations, in how to start developing for Reactive Music from scratch. + +# Getting Started +To start developing for Reactive Music, you will need a few things. + +1. Your preferred text editor or IDE. +2. The Reactive Music `.jar` files. +3. A valid Fabric Minecraft project setup. +4. An idea for your plugin. +5. (Optionally) The `.jar` files for any other mods of which you will be accessing their API's. + +## Setting up your IDE +Let's start by getting your IDE set up! + +IDE stands for Integrated Development Environment. + +It’s a software application that provides developers with a comprehensive set of tools to write, test, and debug code all in one place. Instead of using separate tools for editing, compiling, debugging, and version control, an IDE integrates them into a single interface. + +There are many different IDE's out there - for today, I will recommend using VSCode. It's well adopted, feature full, and relatively easy to learn - in part because it's so widely used. + +Go ahead and install an IDE then move on to the next step. + +## Obtaining a Fabric mod template +Next we'll need to load up a project template onto your system. This is where the code for your plugin will live. You can select and download an official Fabric project template [here](https://fabricmc.net/develop/template/). Make sure to select the correct version of minecraft. + +At this point, it is recommended to also get familiar with and setup a version control system such as [git](https://git-scm.com). + +## Importing the builds into your environment +With a project template loaded, and your IDE fired up, you're almost ready to start developing. + +Download the `.jar` files for the version of Reactive Music you want to develop for and copy them into a new folder in your project's root directory. + +From here, you'll need to update your `build.gradle` file to include the `.jar` files. Find a block that looks like this: +```gradle +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" +} +``` +Add the paths to the `.jar` files you copied - for example, I like to keep my `.jar` files in a folder labeled `libs/`: +```gradle +// ReactiveMusic API jar in libs/ +modCompileOnly files("libs/reactivemusic-2.0.0-alpha.0.1+1.21.jar") + +// Immersive Aircraft jar in libs/ +modCompileOnly files("libs/immersive_aircraft-1.3.3+1.21.1-fabric.jar") +``` + +To keep it simple, if you're using VSCode - let's just close VSCode from here and reopen your project. This will get everything fired back up again so that VSCode will give you autocomplete suggestions - and will also let you compile! This is very important! + +## Developing your plugin +If you've made it to this point - congratulations! You're almost officially a Minecraft modder 😎 Take a look at some other plugins for examples on how to structure your codebase, and different features you can add or expand on. This is where it gets really complicated - so at this point you're past being a beginner if you can make it to the next step - Good luck, and have fun 💃 + +Make sure to read [[this overview|Overview of the API]] to get an idea of how Reactive Music's systems work and what you need to do to have Reactive Music import your plugin. + +## Building your plugin +Assuming you've done everything right, and your plugin's code is valid - go into the terminal portion of your IDE and use the command `gradlew build` +You'll be able to find your fresh `.jar` in `build/libs/` if it compiles. Install that into your Minecraft `mods` folder and test out your plugin! diff --git a/docs/PLUGIN API/Overview-of-the-API.md b/docs/PLUGIN API/Overview-of-the-API.md new file mode 100644 index 0000000..448d438 --- /dev/null +++ b/docs/PLUGIN API/Overview-of-the-API.md @@ -0,0 +1,23 @@ +Importing the API package gives you access to important classes and interfaces you'll need to start developing a Reactive Music plugin, or to hook into some of Reactive Music's core features as a mod developer. + +```java +import circuitlord.reactivemusic.api; +``` + +# [[ReactiveMusicAPI]] +This class is the main entrypoint for Reactive Music developers. You'll find many useful methods and object references that you can implement to unlock the full immersive audio potential of your mod or plugin! + +# [[ReactiveMusicPlugin]] +This is the service provider interface surface for the final class of a plugin, which should be configured to be imported by the service loader. It includes various `default` method calls meant to be overriden that are called at various points throughout Reactive Music's main flow. + +## ⚠️ _Setting up your plugin for import!_ +--- +_For Reactive Music to recognize your plugin, you must declare it in the `resources` folder of your project. Create the directory `resources/META-INF` and create the file `circuitlord.reactivemusic.api.ReactiveMusicPlugin`
+
+Declare the package of your final plugin class extending `ReactiveMusicPlugin` like so:_ +``` +yourname.yourmod.someplace.YourPluginClass +``` + + + diff --git a/docs/PLUGIN API/ReactiveMusicAPI.md b/docs/PLUGIN API/ReactiveMusicAPI.md new file mode 100644 index 0000000..57716bc --- /dev/null +++ b/docs/PLUGIN API/ReactiveMusicAPI.md @@ -0,0 +1,323 @@ +# __ReactiveMusicAPI__ + +**Packages:** + +* `circuitlord.reactivemusic.api` +* `circuitlord.reactivemusic.api.audio` +* `circuitlord.reactivemusic.api.eventsys` +* `circuitlord.reactivemusic.api.songpack` + + +A static, utility-style API for Reactive Music. It exposes global state (current song/entry, selection history) and access to the audio subsystem manager. + +_This class is `final` with a private constructor — it’s not intended to be instantiated or extended._ + +--- + +# __Key accessible type views__ + +* ## `SongpackZip` + + __Package:__ `circuitlord.reactivemusic.api.songpack` + + * `Path getPath()` + * `String getErrorString()` - for custom parsing and logging. + --- + * `List getEntries()`
+ Meant to be used with `ReactiveMusicAPI.Songpack.getAvailable()` for plugins that will utilize the songpack config for asset selection. + + + + +* ## `SongpackEvent` + + __Package:__ `circuitlord.reactivemusic.api.songpack` + + * `SongpackEvent get(String id)` + * `SongpackEvent register(String id)` + * `SongpackEvent[] values()` + --- + * `Map getMap()`
+ Returns the songpack event map for the client. This is already passed in `gameTick()` of the final plugin class loaded by the service loader, so you shouldn't need this unless you're doing something that cannot be handled there. + + +* ## `EventRecord` + + __Package:__ `circuitlord.reactivemusic.api.eventsys` + + * `String getEventId()` + * `PluginIdentifier getPluginId()` + + +* ## `PluginIdentifier` + + __Package:__ `circuitlord.reactivemusic.api.eventsys` + + * `String getNamespace()` + * `String getPath()` + * `String getId()` - Returns `namespace:path` + * `void setTitle()` - For planned feature - plugin info. + +* ## `RuntimeEntry` + + __Package:__ `circuitlord.reactivemusic.api.songpack` + + * `String getSongpack()` + * `String getEventString()` + * `String getErrorString()` + * `List getSongs()` - returns the list of the entry's songs. + * `boolean fallbackAllowed()` - returns the value of `fallbackAllowed` for the entry. + * `boolean shouldOverlay()` - returns the value of `useOverlay` for the entry. + * `boolean shouldStopMusicOnValid()` + * `boolean shouldStopMusicOnInvalid()` + * `boolean shouldStartMusicOnValid()` + * `float getForceChance` + * `List getConditions()` + * `boolean fallbackAllowed()` - returns the value of `allowFallback` for the entry. + * `boolean shouldOverlay()` - returns the value of `useOverlay` for the entry. + + +* ## `GainSupplier` + + __Package:__ `circuitlord.reactivemusic.api.audio` + + * ### Setters + * `void setGainPercent(float p)` + * `void setFadePercent(float p)` + * `void setFadeTarget(float p)` + * `void setFadeDuration(float tickDuration)` + + * ### Getters + * `float getGainPercent()` + * `float getFadePercent()` + * `float getFadeTarget()` + * `float getFadeStart()` - returns the `float p` of the last `setFadeTarget(float p)` call. + * `int getFadeDuration()` + --- + * `void clearFadeStart()` + * `float supplyComputedPercent` - used internally in the player implementation. + + + +* ## `ReactivePlayer` + + __Package:__ `circuitlord.reactivemusic.api.audio` + + * ### Playback Controls + * `void play()` - make sure to set the source before calling. + * `void stop()` + + * ### Source Controls + * `void setSong(String songId)`
+ Will resolve to a file in the `music` directory of the songpack. Accepts strings with or without `"music/"` and appends it to the start if not already included. + --- + * `void setFile(String fileId)`
+ Source setter that does not auto-append `"music/"` to the start. Useful for plugin developers who want to accept separate assets that are not music still within the songpack files. + --- + * `void setStream(java.util.function.Supplier streamSupplier)`
+ Advanced custom source - only use if you know what you are doing. + + * ### Gain Controls + * `ConcurrentHashMap getGainSuppliers()` - use `.put()` or `.computeIfAbsent()` to add a gain supplier. + * `void requestGainRecompute()` + * `void fade(float target, int tickDuration)`
+ Sets the fade target and duration values for the primary gain supplier (created by Reactive Music). Fading is handled by the Player Manager each tick. + --- + * `void setMute(boolean v)` + --- + + * ### Player Groups + * `void setGroup(String group)` + * `String getGroup()` + + * ### Value Queries + * `boolean isPlaying()` + * `boolean isIdle()` - also checks if the player is queued to play. + --- + * `boolean stopOnFadeOut()`
+ Returns whether the manager will call `.stop()` once the fade percent has reached the target of `0`. + --- + * `boolean resetOnFadeOut()`
+ Returns whether the manager will call `.reset()` once the fade percent has reached the target of `0`. + --- + * `float getRealGainDb()` + + * ### Value Setters + > ⚠️ The following controls are also used internally on the built-in players, and will affect the flow of the core logic if called from there. Please try to use them *only* on players *you* create. + * `stopOnFadeOut(boolean v)` + * `resetOnFadeOut(boolean v)` + + + + +* ## `ReactivePlayerManager` + + __Package:__ `circuitlord.reactivemusic.api.audio` + + > ⚠️ Unless you are doing something very complicated, you should not need to instance a *new* manager. Use the following controls from the main player manager. + * `ReactivePlayer create(String id, ReactivePlayerOptions opts)` + * `ReactivePlayer get(String id)` + * `Collection getAll()` + * `Collection getByGroup(String group)` + * `void setGroupDuck(String group, float percent)` + * `float getGroupDuck(String group)` + * `void closeAllForPlugin(String pluginNamespace)` + * `void closeAll()` + --- + > ⚠️ This will only be needed if you instance a new manager. Currently, it handles setting the gain stage for fading. + * `void tick()` + + + + +* ## `ReactivePlayerOptions` + + __Package:__ `circuitlord.reactivemusic.api.audio` + + * `ReactivePlayerOptions namespace(String ns)` + * `ReactivePlayerOptions group(String g)` + * `ReactivePlayerOptions loop(boolean v)` + * `ReactivePlayerOptions autostart(boolean v)` + * `ReactivePlayerOptions linkToMinecraftVolumes(boolean v)` + * `ReactivePlayerOptions quietWhenGamePaused(boolean v)` + * `ReactivePlayerOptions gainRefreshIntervalTicks(int ticks)` + --- + * ### Initial Setters + * `ReactivePlayerOptions gain(float pct)` + * `ReactivePlayerOptions duck(float pct)` + * `ReactivePlayerOptions fade(float pct)` + + + + + + +
+
+
+
+
+
+ +# __Interfaces__ +Accessible through dot chaining. +* `ReactiveMusicAPI.ModConfig` +* `ReactiveMusicAPI.EventSys` +* `ReactiveMusicAPI.Songpack` + +
+
+
+ +## `.ModConfig` +Access configuration settings for the mod, which are set by the player via the UI provided by YetAnotherConfigLib. +### Methods +* `static boolean debugModeEnabled()` + +## `.EventSys` +Access values set by the core logic of the __Event System__. +### Methods +* None yet - this interface will mainly be convenience functions for developers working with the event system in a more complex manner. + +## `.Songpack` +Access data of imported songpacks, and access values set by the built-in music switching logic. +### Methods +* `static SongpackZip getCurrent()` +* `static List getAvailable()` +* `static RuntimeEntry currentEntry()` +* `static String currentSong()` +* `static List recentSongs()` +* `static List validEntries()` +* `static List loadedEntries()` +* `static List previousValidEntries()` + + +
+
+
+
+
+
+ +# __Standalone Methods__ + +### `static ReactivePlayerManager audioManager()` + +Returns the audio subsystem manager (backed by a singleton `RMPlayerManager`). Use this to create players, group them, control ducking, and enumerate active players via `audio().getAll()`. + +
+
+
+
+
+
+ + +# __Usage Examples__ +This is the built-in actions plugin. In `init()`, we register new events that can be declared in a songpack entry. During `gameTick()` we check if the player is doing something that would trigger the event, and if so we place a `boolean` into the value of the map under the event's key. + +```java +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.eventsys.songpack.SongpackEvent; +import net.minecraft.entity.Entity; +import net.minecraft.entity.passive.HorseEntity; +import net.minecraft.entity.passive.PigEntity; +import net.minecraft.entity.vehicle.BoatEntity; +import net.minecraft.entity.vehicle.MinecartEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.Map; + +public final class ActionsPlugin extends ReactiveMusicPlugin { + + public ActionsPlugin() { + super("reactivemusic", "actions"); + } + + private static EventRecord FISHING, MINECART, BOAT, HORSE, PIG; + + @Override + public void init() { + registerSongpackEvents("FISHING","MINECART","BOAT","HORSE","PIG"); + + FISHING = SongpackEvent.get("FISHING"); + MINECART = SongpackEvent.get("MINECART"); + BOAT = SongpackEvent.get("BOAT"); + HORSE = SongpackEvent.get("HORSEING"); + PIG = SongpackEvent.get("PIG"); + } + + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (player == null) return; + + eventMap.put(FISHING, player.fishHook != null); + + Entity v = player.getVehicle(); + eventMap.put(MINECART, v instanceof MinecartEntity); + eventMap.put(BOAT, v instanceof BoatEntity); + eventMap.put(HORSE, v instanceof HorseEntity); + eventMap.put(PIG, v instanceof PigEntity); + } +} +``` +--- +Here, we are creating a new audio player for the built in Overlay Track feature, which uses the `useOverlay` option from the songpack config. +```java +ReactiveMusicAPI.audioManager().create( + "reactive:overlay", + ReactivePlayerOptions.create() + .namespace("reactive") + .group("overlay") + .loop(false) + .gain(1.0f) + .fade(0f) + .quietWhenGamePaused(false) + .linkToMinecraftVolumes(true) +); +```` diff --git a/gradle.properties b/gradle.properties index d731625..62b7ad5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,10 +15,14 @@ loom_version=1.11-SNAPSHOT # Fabric API fabric_version=0.102.0+1.21 -mod_version=1.2.0 -maven_group=circuitlord.reactivemusic +mod_version=2.0.0-alpha.1.2a +maven_group=rocamocha.mochamix archives_base_name=reactivemusic modmenu_version=11.0.3 yacl_version=3.6.2+1.21-fabric + +org.gradle.java.home=C:\\Program Files\\Java\\jdk-21 + + diff --git a/src/main/java/circuitlord/reactivemusic/MyMath.java b/src/main/java/circuitlord/reactivemusic/MyMath.java deleted file mode 100644 index 0d30284..0000000 --- a/src/main/java/circuitlord/reactivemusic/MyMath.java +++ /dev/null @@ -1,21 +0,0 @@ -package circuitlord.reactivemusic; - -public class MyMath { - - - public static float lerpConstant(float currentValue, float targetValue, float lerpRate) { - - lerpRate = Math.abs(lerpRate); - // If need to get smaller then invert - if (targetValue < currentValue) lerpRate = -lerpRate; - - float result = currentValue + lerpRate; - - if ((lerpRate > 0 && result > targetValue) || (lerpRate < 0 && result < targetValue)) { - result = targetValue; - } - - return result; - } - -} diff --git a/src/main/java/circuitlord/reactivemusic/PlayerThread.java b/src/main/java/circuitlord/reactivemusic/PlayerThread.java deleted file mode 100644 index 3dab344..0000000 --- a/src/main/java/circuitlord/reactivemusic/PlayerThread.java +++ /dev/null @@ -1,250 +0,0 @@ -package circuitlord.reactivemusic; - - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.option.GameOptions; -import net.minecraft.sound.SoundCategory; -import rm_javazoom.jl.player.AudioDevice; -import rm_javazoom.jl.player.JavaSoundAudioDevice; -import rm_javazoom.jl.player.advanced.AdvancedPlayer; -import net.minecraft.text.TranslatableTextContent; - -import java.io.IOException; -import java.io.InputStream; - -public class PlayerThread extends Thread { - - public static final float MIN_POSSIBLE_GAIN = -80F; - public static final float MIN_GAIN = -50F; - public static final float MAX_GAIN = 0F; - - //public static float[] fadeGains; - - static { -/* fadeGains = new float[ReactiveMusic.FADE_DURATION]; - float totaldiff = MIN_GAIN - MAX_GAIN; - float diff = totaldiff / fadeGains.length; - for(int i = 0; i < fadeGains.length; i++) - fadeGains[i] = MAX_GAIN + diff * i;*/ - - // Invert because we have fade ticks counting up now - //for (int i = fadeGains.length - 1; i >= 0; i--) { - // fadeGains[i] = MAX_GAIN + diff * (fadeGains.length - 1 - i); - //} - } - - public volatile static float gainPercentage = 1.0f; - public volatile static float musicDiscDuckPercentage = 1.0f; - - public static final float QUIET_VOLUME_PERCENTAGE = 0.7f; - public static final float QUIET_VOLUME_LERP_RATE = 0.02f; - public static float quietPercentage = 1.0f; - - public volatile static float realGain = 0; - - public volatile static String currentSong = null; - public volatile static String currentSongChoices = null; - - public volatile MusicPackResource currentSongResource = null; - - AdvancedPlayer player; - - private volatile boolean queued = false; - - private volatile boolean kill = false; - private volatile boolean playing = false; - - - boolean notQueuedOrPlaying() { - return !(queued || isPlaying()); - } - - boolean isPlaying() { - return playing && !player.getComplete(); - } - - public PlayerThread() { - setDaemon(true); - setName("ReactiveMusic Player Thread"); - start(); - } - - @Override - public void run() { - try { - while(!kill) { - - if(queued && currentSong != null) { - - currentSongResource = RMSongpackLoader.getInputStream(ReactiveMusic.currentSongpack.path, "music/" + currentSong + ".mp3", ReactiveMusic.currentSongpack.embedded); - if(currentSongResource == null || currentSongResource.inputStream == null) - continue; - - player = new AdvancedPlayer(currentSongResource.inputStream); - queued = false; - - } - - - if(player != null && player.getAudioDevice() != null) { - - // go to full volume - setGainPercentage(1.0f); - processRealGain(); - - ReactiveMusic.LOGGER.info("Playing " + currentSong); - playing = true; - player.play(); - - } - - } - } catch(Exception e) { - e.printStackTrace(); - } - } - - - - public void resetPlayer() { - playing = false; - - if(player != null) - player.queuedToStop = true; - - queued = false; - currentSong = null; - - if (currentSongResource != null && currentSongResource.fileSystem != null) { - try { - currentSongResource.close(); - } catch (Exception e) { - ReactiveMusic.LOGGER.error("Failed to close file system/input stream " + e.getMessage()); - } - } - - currentSongResource = null; - } - - public void play(String song) { - resetPlayer(); - - currentSong = song; - queued = true; - } - -/* public float getGain() { - if(player == null) - return gain; - - AudioDevice device = player.getAudioDevice(); - if(device != null && device instanceof JavaSoundAudioDevice) - return ((JavaSoundAudioDevice) device).getGain(); - return gain; - }*/ - -/* public void addGain(float gain) { - setGain(getGain() + gain); - }*/ - - public void setGainPercentage(float newGain) { - gainPercentage = Math.min(1.0f, Math.max(0.0f, newGain)); - } - - public void setMusicDiscDuckPercentage(float newGain) { - musicDiscDuckPercentage = newGain; - } - - public void processRealGain() { - - var client = MinecraftClient.getInstance(); - - GameOptions options = MinecraftClient.getInstance().options; - - boolean musicOptionsOpen = false; - - // Try to find the music options menu - TranslatableTextContent ScreenTitleContent = null; - if (client.currentScreen != null && client.currentScreen.getTitle() != null && client.currentScreen.getTitle().getContent() != null - && client.currentScreen.getTitle().getContent() instanceof TranslatableTextContent) { - - ScreenTitleContent = (TranslatableTextContent) client.currentScreen.getTitle().getContent(); - - if (ScreenTitleContent != null) { - musicOptionsOpen = ScreenTitleContent.getKey().equals("options.sounds.title"); - } - } - - - boolean doQuietMusic = client.isPaused() - && client.world != null - && !musicOptionsOpen; - - - float targetQuietMusicPercentage = doQuietMusic ? QUIET_VOLUME_PERCENTAGE : 1.0f; - quietPercentage = MyMath.lerpConstant(quietPercentage, targetQuietMusicPercentage, QUIET_VOLUME_LERP_RATE); - - - float minecraftGain = options.getSoundVolume(SoundCategory.MUSIC) * options.getSoundVolume(SoundCategory.MASTER); - - // my jank way of changing the volume curve to be less drastic - float minecraftDistFromMax = 1.0f - minecraftGain; - float minecraftGainAddScalar = (minecraftDistFromMax * 1.0f) * minecraftGain; - // cap to 1.0 - minecraftGain = Math.min(minecraftGain + minecraftGainAddScalar, 1.0f); - - //ReactiveMusic.LOGGER.info("minecraft fake gain: " + minecraftGain); - - - float newRealGain = MIN_GAIN + (MAX_GAIN - MIN_GAIN) * minecraftGain * gainPercentage * quietPercentage * musicDiscDuckPercentage; - - // Force to basically off if the user sets their volume off - if (minecraftGain <= 0) { - newRealGain = MIN_POSSIBLE_GAIN; - } - - //ReactiveMusic.LOGGER.info("Current gain: " + newRealGain); - - realGain = newRealGain; - if(player != null) { - AudioDevice device = player.getAudioDevice(); - if(device != null && device instanceof JavaSoundAudioDevice) { - try { - ((JavaSoundAudioDevice) device).setGain(newRealGain); - } catch(IllegalArgumentException e) { - ReactiveMusic.LOGGER.error(e.toString()); - } - } - } - - //if(musicGain == 0) - // play(null); - } - - -/* public float getRelativeVolume() { - return getRelativeVolume(getGain()); - }*/ - -/* public float getRelativeVolume(float gain) { - float width = MAX_GAIN - MIN_GAIN; - float rel = Math.abs(gain - MIN_GAIN); - return rel / Math.abs(width); - }*/ - -/* public int getFramesPlayed() { - return player == null ? 0 : player.getFrames(); - }*/ - - public void forceKill() { - try { - resetPlayer(); - interrupt(); - - finalize(); - kill = true; - } catch(Throwable e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/circuitlord/reactivemusic/ReactiveMusic.java b/src/main/java/circuitlord/reactivemusic/ReactiveMusic.java index 7b05329..1e7c4d0 100644 --- a/src/main/java/circuitlord/reactivemusic/ReactiveMusic.java +++ b/src/main/java/circuitlord/reactivemusic/ReactiveMusic.java @@ -1,119 +1,121 @@ package circuitlord.reactivemusic; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.audio.ReactivePlayer; +import circuitlord.reactivemusic.api.audio.ReactivePlayerManager; +import circuitlord.reactivemusic.api.audio.ReactivePlayerOptions; +import circuitlord.reactivemusic.api.eventsys.PluginIdentifier; +import circuitlord.reactivemusic.commands.HelpCommandHandlers; +import circuitlord.reactivemusic.commands.PlayerCommandHandlers; +import circuitlord.reactivemusic.commands.PluginCommandHandlers; +import circuitlord.reactivemusic.commands.SongpackCommandHandlers; import circuitlord.reactivemusic.config.ModConfig; -import circuitlord.reactivemusic.config.MusicDelayLength; -import circuitlord.reactivemusic.config.MusicSwitchSpeed; -import circuitlord.reactivemusic.entries.RMRuntimeEntry; +import circuitlord.reactivemusic.impl.audio.RMPlayerManager; +import circuitlord.reactivemusic.impl.eventsys.RMPluginIdentifier; +import circuitlord.reactivemusic.impl.songpack.RMSongpackLoader; import net.fabricmc.api.ModInitializer; - -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.option.GameOptions; import net.minecraft.client.sound.SoundInstance; +import net.minecraft.command.argument.serialize.IntegerArgumentSerializer; import net.minecraft.sound.SoundCategory; import net.minecraft.text.Text; import net.minecraft.util.math.Vec3d; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -import java.util.Random; - -public class ReactiveMusic implements ModInitializer { - - public static final String MOD_ID = "reactive_music"; - - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - private static final int WAIT_FOR_SWITCH_DURATION = 100; - public static final int FADE_DURATION = 150; - public static final int SILENCE_DURATION = 100; - - public static int additionalSilence = 0; - - public static PlayerThread thread; - - - public static SongpackZip currentSongpack = null; - - static boolean queuedToStopMusic = false; - static boolean queuedToPlayMusic = false; +import java.util.ServiceLoader; - //static List currentEntries = new ArrayList<>(); +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; - static String currentSong = null; - static RMRuntimeEntry currentEntry = null; +public class ReactiveMusic implements ModInitializer { - //static List currentGenericEntries = new ArrayList<>(); + public static final ServiceLoader PLUGINS = ServiceLoader.load(ReactiveMusicPlugin.class); - //static String nextSong; - static int waitForStopTicks = 0; - static int waitForNewSongTicks = 99999; - static int fadeOutTicks = 0; - //static int fadeInTicks = 0; - static int silenceTicks = 0; + public static final PluginIdentifier corePluginId = new RMPluginIdentifier("reactivemusic", "core"); + public static final ReactiveMusicDebug debugTools = new ReactiveMusicDebug(); + public static ModConfig modConfig; + public static int additionalSilence = 0; + private static ReactivePlayer musicPlayer; static int musicTrackedSoundsDuckTicks = 0; - static int slowTickUpdateCounter = 0; - - static boolean currentDimBlacklisted = false; - boolean doSilenceForNextQueuedSong = true; - - static List previousValidEntries = new ArrayList<>(); - - - static Random rand = new Random(); - - - public static ModConfig config; - - - // Add this static list to the class - //private static List validEntries = new ArrayList<>(); - - - private static List loadedEntries = new ArrayList<>(); - - public static final List trackedSoundsMuteMusic = new ArrayList(); + private class Mocha { + public static Screen lastScreen; + public static void log(MinecraftClient mc) { + ScreenChange(mc); + } + + private static void ScreenChange(MinecraftClient mc) { + Screen screen = mc.currentScreen; + if (screen != null && lastScreen != screen) { + ReactiveMusicDebug.LOGGER.info("currentScreen.getTitle(): " + screen.getTitle().toString()); + } + lastScreen = screen; + } + } - @Override - public void onInitialize() { - - LOGGER.info("Initializing Reactive Music..."); + /** + * Audio subsystem (player creation, grouping, ducking). + * @return The core Reactive Music audio player manager. Unless you are doing something + * very complicated, you should not need to instance a new manager. + */ + public static final ReactivePlayerManager audio() { return RMPlayerManager.get(); } + @Override public void onInitialize() { ModConfig.GSON.load(); - config = ModConfig.getConfig(); - + modConfig = ModConfig.getConfig(); + + ReactiveMusicState.logicFreeze.put(corePluginId, false); + ReactiveMusicDebug.LOGGER.info("Initializing Reactive Music"); + + // Create the primary audio player + musicPlayer = audio().create( + "reactive:music", + ReactivePlayerOptions.create() + .namespace("reactive") + .group("music") + .loop(false) + .gain(1.0f) + .fade(0.0f) + .duck(1.0f) + .quietWhenGamePaused(false) + ); + musicPlayer.getGainSuppliers().put("reactivemusic-fsi", ReactiveMusicState.foundSoundInstanceGainSupplier); SongPicker.initialize(); - - - thread = new PlayerThread(); - + + for (ReactiveMusicPlugin plugin: PLUGINS) { + plugin.init(); + } + RMSongpackLoader.fetchAvailableSongpacks(); - + boolean loadedUserSongpack = false; - + // try to load a saved songpack - if (!config.loadedUserSongpack.isEmpty()) { - + if (!modConfig.loadedUserSongpack.isEmpty()) { + ReactiveMusicDebug.LOGGER.info("Initialization is attempting to load user songpack."); for (var songpack : RMSongpackLoader.availableSongpacks) { - if (!songpack.config.name.equals(config.loadedUserSongpack)) continue; + if (songpack.config == null) continue; + if (!songpack.config.name.equals(modConfig.loadedUserSongpack)) continue; // something is broken in this songpack, don't load it if (songpack.blockLoading) continue; - setActiveSongpack(songpack); + ReactiveMusicCore.setActiveSongpack(songpack); loadedUserSongpack = true; break; @@ -126,503 +128,186 @@ public void onInitialize() { // for the cases where something is broken in the base songpack if (!RMSongpackLoader.availableSongpacks.get(0).blockLoading) { // first is the default songpack - setActiveSongpack(RMSongpackLoader.availableSongpacks.get(0)); + ReactiveMusicCore.setActiveSongpack(RMSongpackLoader.availableSongpacks.get(0)); } } + + + + + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { + dispatcher.register( + literal("reactivemusic") + + .executes(ctx -> { + MinecraftClient mc = ctx.getSource().getClient(); + Screen screen = ModConfig.createScreen(mc.currentScreen); + mc.send(() -> mc.setScreen(screen)); + return 1; + }) + + .then(literal("help") + .then(literal("songpack").executes(HelpCommandHandlers::songpackCommands)) + .then(literal("plugin").executes(HelpCommandHandlers::pluginCommands)) + .then(literal("player").executes(HelpCommandHandlers::playerCommands)) + ) + .then(literal("logBlockCounter") + .executes(ctx -> { + SongPicker.queuedToPrintBlockCounter = true; + return 1; + }) + ) - - - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("reactivemusic") - .executes(context -> { - MinecraftClient mc = context.getSource().getClient(); - Screen screen = ModConfig.createScreen(mc.currentScreen); - mc.send(() -> mc.setScreen(screen)); + .then(literal("blacklistDimension") + .executes(ctx -> { + String key = ctx.getSource().getClient().world.getRegistryKey().getValue().toString(); + if (modConfig.blacklistedDimensions.contains(key)) { + ctx.getSource().sendFeedback(Text.literal("ReactiveMusic: " + key + " was already in blacklist.")); return 1; } + ctx.getSource().sendFeedback(Text.literal("ReactiveMusic: Added " + key + " to blacklist.")); + modConfig.blacklistedDimensions.add(key); + ModConfig.saveConfig(); + return 1; + }) ) - - .then(ClientCommandManager.literal("logBlockCounter") - .executes(context -> { - - SongPicker.queuedToPrintBlockCounter = true; - + .then(literal("unblacklistDimension") + .executes(ctx -> { + String key = ctx.getSource().getClient().world.getRegistryKey().getValue().toString(); + if (!modConfig.blacklistedDimensions.contains(key)) { + ctx.getSource().sendFeedback(Text.literal("ReactiveMusic: " + key + " was not in blacklist.")); return 1; - }) + } + ctx.getSource().sendFeedback(Text.literal("ReactiveMusic: Removed " + key + " from blacklist.")); + modConfig.blacklistedDimensions.remove(key); + ModConfig.saveConfig(); + return 1; + }) ) - .then(ClientCommandManager.literal("blacklistDimension") - .executes(context -> { - - String key = context.getSource().getClient().world.getRegistryKey().getValue().toString(); - - if (config.blacklistedDimensions.contains(key)) { - context.getSource().sendFeedback(Text.literal("ReactiveMusic: " + key + " was already in blacklist.")); - return 1; - } - - context.getSource().sendFeedback(Text.literal("ReactiveMusic: Added " + key + " to blacklist.")); - - config.blacklistedDimensions.add(key); - ModConfig.saveConfig(); - - return 1; - }) + .then(literal("songpack") + .executes(HelpCommandHandlers::songpackCommands) + .then(literal("info") + .executes(SongpackCommandHandlers::songpackInfo)) + .then(literal("entry") + .then(literal("current") + .executes(SongpackCommandHandlers::currentEntryInfo)) + .then(literal("list") + .executes(SongpackCommandHandlers::listAllEntries)) + .then(argument("index", IntegerArgumentType.integer()) + .executes(SongpackCommandHandlers::indexedEntryInfo) + .then(literal("songs") + .executes(SongpackCommandHandlers::indexedEntrySongs)) + ) + ) ) - .then(ClientCommandManager.literal("unblacklistDimension") - .executes(context -> { - String key = context.getSource().getClient().world.getRegistryKey().getValue().toString(); - - if (!config.blacklistedDimensions.contains(key)) { - context.getSource().sendFeedback(Text.literal("ReactiveMusic: " + key + " was not in blacklist.")); - return 1; - } - - context.getSource().sendFeedback(Text.literal("ReactiveMusic: Removed " + key + " from blacklist.")); + .then(literal("plugin") + .executes(HelpCommandHandlers::pluginCommands) + .then(literal("list").executes(PluginCommandHandlers::listPlugins)) + .then(literal("enable") + .then(argument("pluginId", StringArgumentType.string()) + .executes(PluginCommandHandlers::enablePlugin) + ) + ) + .then(literal("disable") + .then(argument("pluginId", StringArgumentType.string()) + .executes(PluginCommandHandlers::disablePlugin) + ) + ) + ) - config.blacklistedDimensions.remove(key); - ModConfig.saveConfig(); + .then(literal("player") + .then(literal("list").executes(PlayerCommandHandlers::playerList)) + .then(literal("info") + .then(argument("namespace", StringArgumentType.string()) + .then(argument("path", StringArgumentType.string()) + .executes(PlayerCommandHandlers::playerInfo) + .then(argument("gainSupplierId", StringArgumentType.string()) + .executes(PlayerCommandHandlers::gainSupplierInfo) + ) + ) + ) + ) + ) - return 1; - }) + .then(literal("skip") + .executes(ctx -> { + ReactiveMusicState.currentEntry = null; + ReactiveMusicState.currentSong = null; + return 1; + }) ) + ); + }); + } - ) - ); - } + public static void newTick() { - - if (thread == null) return; - if (currentSongpack == null) return; - if (loadedEntries.isEmpty()) return; + if (musicPlayer == null) return; + if (ReactiveMusicState.currentSongpack == null) return; + if (ReactiveMusicState.loadedEntries.isEmpty()) return; MinecraftClient mc = MinecraftClient.getInstance(); if (mc == null) return; + Mocha.log(mc); // XXX~ My info logs! ~rocamocha // force a reasonable volume once on mod install, if you have full 100% everything it's way too loud - if (!config.hasForcedInitialVolume) { - config.hasForcedInitialVolume = true; + if (!modConfig.hasForcedInitialVolume) { + modConfig.hasForcedInitialVolume = true; ModConfig.saveConfig(); if (mc.options.getSoundVolume(SoundCategory.MASTER) > 0.5) { - LOGGER.info("Forcing master volume to a lower default, this will only happen once on mod-install to avoid loud defaults."); + ReactiveMusicDebug.LOGGER.info("Forcing master volume to a lower default, this will only happen once on mod-install to avoid loud defaults."); mc.options.getSoundVolumeOption(SoundCategory.MASTER).setValue(0.5); mc.options.write(); } } - - - // always tick this - SongPicker.tickBlockCounterMap(); - - slowTickUpdateCounter++; - if (slowTickUpdateCounter > 20) { - - currentDimBlacklisted = false; + + { + ReactiveMusicState.currentDimBlacklisted = false; // see if the dimension we're in is blacklisted -- update at same time as event map to keep them in sync if (mc != null && mc.world != null) { String curDim = mc.world.getRegistryKey().getValue().toString(); - for (String dim : config.blacklistedDimensions) { + for (String dim : modConfig.blacklistedDimensions) { if (dim.equals(curDim)) { - currentDimBlacklisted = true; + ReactiveMusicState.currentDimBlacklisted = true; break; } } } - SongPicker.tickEventMap(); - - slowTickUpdateCounter = 0; } + ReactiveMusicState.validEntries = ReactiveMusicCore.getValidEntries(); - // ------------------------- - - // clear playing state if not playing - if (thread.notQueuedOrPlaying()) { - resetPlayer(); + if (!ReactiveMusicState.logicFreeze.get(corePluginId)) { + ReactiveMusicCore.newTick(audio().getByGroup("music")); } - - - // ------------------------- - + + // TODO: Priority system for logic calls? + SongPicker.tickEventMap(); // ticks after core audio, so that plugin logic happens later + + audio().tick(); + processTrackedSoundsMuteMusic(); - - RMRuntimeEntry newEntry = null; - - List validEntries = getValidEntries(); - - // Pick the highest priority one - if (!validEntries.isEmpty()) { - newEntry = validEntries.get(0); - } - - processValidEvents(validEntries, previousValidEntries); - - - if (currentDimBlacklisted) - newEntry = null; - - - if (newEntry != null) { - - List selectedSongs = getSelectedSongs(newEntry, validEntries); - - - // wants to switch if our current entry doesn't exist -- or is not the same as the new one - boolean wantsToSwitch = currentEntry == null || newEntry != currentEntry; - - // if the new entry contains the same song as our current one, then do a "fake" swap to swap over to the new entry - if (wantsToSwitch && currentSong != null && newEntry.songs.contains(currentSong)) { - - LOGGER.info("doing fake swap to new event: " + newEntry.eventString); - - // do a fake swap - currentEntry = newEntry; - wantsToSwitch = false; - - // if this happens, also clear the queued state since we essentially did a switch - queuedToStopMusic = false; - queuedToPlayMusic = false; - } - - // make sure we're fully faded in if we faded out for any reason but this event is valid - if (thread.isPlaying() && !wantsToSwitch && fadeOutTicks > 0) { - fadeOutTicks--; - - // Copy the behavior from below where it fades out - thread.setGainPercentage(1f - (fadeOutTicks / (float)FADE_DURATION)); - } - - - - // ---- FADE OUT ---- - - if (wantsToSwitch && thread.isPlaying()) { - - waitForStopTicks++; - - boolean shouldFadeOutMusic = false; - - // handle fade-out if something's playing when a new event becomes valid - if (waitForStopTicks > getMusicStopSpeed(currentSongpack)) { - shouldFadeOutMusic = true; - } - - // if we're queued to force stop the music, do so here - if (queuedToStopMusic) { - shouldFadeOutMusic = true; - } - - if (shouldFadeOutMusic) { - tickFadeOut(); - } - } - else { - waitForStopTicks = 0; - } - - // ---- SWITCH SONG ---- - - if (wantsToSwitch && thread.notQueuedOrPlaying()) { - - waitForNewSongTicks++; - - boolean shouldStartNewSong = false; - - if (waitForNewSongTicks > getMusicDelay(currentSongpack)) { - shouldStartNewSong = true; - } - - // if we're queued to start a new song and we're not playing anything, do it - if (queuedToPlayMusic) { - shouldStartNewSong = true; - } - - if (shouldStartNewSong) { - - String picked = SongPicker.pickRandomSong(selectedSongs); - - changeCurrentSong(picked, newEntry); - } - - } - else { - waitForNewSongTicks = 0; - } - - - - - } - - // no entries are valid, we shouldn't be playing any music! - // this can happen if no entry is valid or the dimension is blacklisted - else { - - tickFadeOut(); - - } - - - - thread.processRealGain(); - - - previousValidEntries = validEntries; - + // Previously, this was in the core tick logic. + // Extracted so that the core logic can be frozen, but onValid and onInvalid can still trigger. + ReactiveMusicState.previousValidEntries = new java.util.ArrayList<>(ReactiveMusicState.validEntries); + ReactiveMusicCore.processValidEvents(ReactiveMusicState.validEntries, ReactiveMusicState.previousValidEntries); } - private static @NotNull List getSelectedSongs(RMRuntimeEntry newEntry, List validEntries) { - - // if we have non-recent songs then just return those - if (SongPicker.hasSongNotPlayedRecently(newEntry.songs)) { - return newEntry.songs; - } - - // Fallback behaviour - if (newEntry.allowFallback) { - for (int i = 1; i < validEntries.size(); i++) { - if (validEntries.get(i) == null) - continue; - - // check if we have songs not played recently and early out - if (SongPicker.hasSongNotPlayedRecently(validEntries.get(i).songs)) { - return validEntries.get(i).songs; - } - } - } - - - // we've played everything recently, just give up and return this event's songs - return newEntry.songs; - } - - - public static List getValidEntries() { - List validEntries = new ArrayList<>(); - - for (RMRuntimeEntry loadedEntry : loadedEntries) { - - boolean isValid = SongPicker.isEntryValid(loadedEntry); - - if (isValid) { - validEntries.add(loadedEntry); - } - } - - return validEntries; - } - - private static void processValidEvents(List validEntries, List previousValidEntries) { - - - for (var entry : previousValidEntries) { - - // if this event was valid before and is invalid now - if (entry.forceStopMusicOnInvalid && !validEntries.contains(entry)) { - LOGGER.info("trying forceStopMusicOnInvalid: " + entry.eventString); - - if (entry.cachedRandomChance <= entry.forceChance) { - - LOGGER.info("doing forceStopMusicOnInvalid: " + entry.eventString); - queuedToStopMusic = true; - } - - break; - } - } - - for (var entry : validEntries) { - - if (!previousValidEntries.contains(entry)) { - - // use the same random chance for all so they always happen together - entry.cachedRandomChance = rand.nextFloat(); - boolean randSuccess = entry.cachedRandomChance <= entry.forceChance; - - // if this event wasn't valid before and is now - if (entry.forceStopMusicOnValid) { - LOGGER.info("trying forceStopMusicOnValid: " + entry.eventString); - - if (randSuccess) { - LOGGER.info("doing forceStopMusicOnValid: " + entry.eventString); - queuedToStopMusic = true; - } - } - - if (entry.forceStartMusicOnValid) { - LOGGER.info("trying forceStartMusicOnValid: " + entry.eventString); - - if (randSuccess) { - LOGGER.info("doing forceStartMusicOnValid: " + entry.eventString); - queuedToPlayMusic = true; - } - } - - } - - - } - - - - - } - - - public static void tickFadeOut() { - - if (!thread.isPlaying()) - return; - - if (fadeOutTicks < FADE_DURATION) { - fadeOutTicks++; - thread.setGainPercentage(1f - (fadeOutTicks / (float)FADE_DURATION)); - } - else { - resetPlayer(); - } - } - - - public static void changeCurrentSong(String song, RMRuntimeEntry newEntry) { - - resetPlayer(); - - currentSong = song; - currentEntry = newEntry; - - // go full quiet while switching songs, we'll go back to 1.0f after we load the new song - thread.setGainPercentage(0.0f); - - if (song != null) { - LOGGER.info("Changing entry: " + newEntry.eventString + " Song name: " + song); - - thread.play(song); - } - else { - // TODO: maybe a better way to do this that doesn't spam? - //LOGGER.info("Changing entry: " + newEntry.eventString + " Doing silence... "); - - // this gets called earlier with resetPlayer - //thread.resetPlayer(); - } - - queuedToPlayMusic = false; - - } - - - - public static void setActiveSongpack(SongpackZip songpackZip) { - - // TODO: more than one songpack? - if (currentSongpack != null) { - deactivateSongpack(currentSongpack); - } - - resetPlayer(); - - currentSongpack = songpackZip; - - loadedEntries = songpackZip.runtimeEntries; - - // always start new music immediately - queuedToPlayMusic = true; - - } - - public static void deactivateSongpack(SongpackZip songpackZip) { - - // remove all entries that match that name - for (int i = loadedEntries.size() - 1; i >= 0; i--) { - if (loadedEntries.get(i).songpack == songpackZip.config.name) { - loadedEntries.remove(i); - } - } - - } - - public static int getMusicStopSpeed(SongpackZip songpack) { - - MusicSwitchSpeed speed = config.musicSwitchSpeed2; - - if (config.musicSwitchSpeed2 == MusicSwitchSpeed.SONGPACK_DEFAULT) { - speed = songpack.config.musicSwitchSpeed; - } - - if (config.debugModeEnabled) { - speed = MusicSwitchSpeed.INSTANT; - } - - switch (speed) { - case INSTANT: - return 100; - case SHORT: - return 250; - case NORMAL: - return 900; - case LONG: - return 2400; - } - - return 100; - - } - - public static int getMusicDelay(SongpackZip songpack) { - - MusicDelayLength delay = config.musicDelayLength2; - - if (config.musicDelayLength2 == MusicDelayLength.SONGPACK_DEFAULT) { - delay = songpack.config.musicDelayLength; - } - - if (config.debugModeEnabled) { - delay = MusicDelayLength.NONE; - } - - switch (delay) { - case NONE: - return 0; - case SHORT: - return 250; - case NORMAL: - return 900; - case LONG: - return 2400; - } - - return 100; - - } - - static void resetPlayer() { - - // if queued or playing - if (!thread.notQueuedOrPlaying()) { - thread.resetPlayer(); - } - - fadeOutTicks = 0; - queuedToStopMusic = false; - currentEntry = null; - currentSong = null; - } - - - - + // TODO: Add querying foundSoundInstance from API private static void processTrackedSoundsMuteMusic() { // remove if the song is null or not playing anymore @@ -630,7 +315,15 @@ private static void processTrackedSoundsMuteMusic() { GameOptions options = MinecraftClient.getInstance().options; - boolean foundSoundInstance = false; + if (ReactiveMusicState.foundSoundInstance == true) { + // only flip + ReactiveMusicState.foundSoundInstance = false; + } + + if (ReactiveMusicState.foundSoundInstance == false) { + // only call if flip happened + ReactiveMusicState.foundSoundInstanceGainSupplier.setFadeTarget(1f); + } for (SoundInstance soundInstance : trackedSoundsMuteMusic) { @@ -653,37 +346,10 @@ private static void processTrackedSoundsMuteMusic() { continue; } - foundSoundInstance = true; + ReactiveMusicState.foundSoundInstance = true; + ReactiveMusicState.foundSoundInstanceGainSupplier.setFadeTarget(0f); break; } - - - - - // only duck for jukebox if our volume is loud enough to where it would matter - if (foundSoundInstance) { - - if (musicTrackedSoundsDuckTicks < FADE_DURATION) { - musicTrackedSoundsDuckTicks++; - } - - } - else { - if (musicTrackedSoundsDuckTicks > 0) { - musicTrackedSoundsDuckTicks--; - } - } - - thread.setMusicDiscDuckPercentage(1f - (musicTrackedSoundsDuckTicks / (float)FADE_DURATION)); - - } - - - - - - - } \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/ReactiveMusicCore.java b/src/main/java/circuitlord/reactivemusic/ReactiveMusicCore.java new file mode 100644 index 0000000..6d7c4bd --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/ReactiveMusicCore.java @@ -0,0 +1,381 @@ +/** + * This file contains extracted code from ReactiveMusic.java that served as + * the main logic for songpack loading & selection features. + * + * It is now included in the API package so that plugin developers have convenient access to + * some functions that relate to parsing the data in songpack entries during runtime. + */ +package circuitlord.reactivemusic; + +import java.rmi.UnexpectedException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +import circuitlord.reactivemusic.ReactiveMusicDebug.ChangeLogger; +import circuitlord.reactivemusic.api.ReactiveMusicPlugin; +import circuitlord.reactivemusic.api.ReactiveMusicUtils; +import circuitlord.reactivemusic.api.audio.ReactivePlayer; +import circuitlord.reactivemusic.api.audio.ReactivePlayerManager; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import circuitlord.reactivemusic.api.songpack.SongpackZip; +import circuitlord.reactivemusic.config.MusicDelayLength; +import circuitlord.reactivemusic.config.MusicSwitchSpeed; +import circuitlord.reactivemusic.impl.songpack.RMSongpackZip; +import circuitlord.reactivemusic.plugins.OverlayTrackPlugin; + +public final class ReactiveMusicCore { + + private static final ChangeLogger CHANGE_LOGGER = ReactiveMusic.debugTools.new ChangeLogger(); + private static final ChangeLogger ENTRY_LOGGER = ReactiveMusic.debugTools.new ChangeLogger(); + public static final int FADE_DURATION = 150; + public static final int SILENCE_DURATION = 100; + + static boolean queuedToPlayMusic = false; + static boolean queuedToStopMusic = false; + static int waitForStopTicks = 0; + static int waitForNewSongTicks = 99999; + static int fadeOutTicks = 0; + static int silenceTicks = 0; + static Random rand = new Random(); + static float randomChance = rand.nextFloat(); + + /** + * This is the built-in logic for Reactive Music's song switcher. + * @param players Collection of audio players created by a PlayerManager. + * @see ReactivePlayerManager + */ + public static void newTick(Collection players) { + RuntimeEntry newEntry = null; + + // Pick the highest priority one + if (!ReactiveMusicState.validEntries.isEmpty()) { + for (RuntimeEntry entry : ReactiveMusicState.validEntries) { + if (entry == null) { + ENTRY_LOGGER.writeError("A NULL ENTRY HAS MADE IT INTO THE LIST OF VALID ENTRIES!!!", new UnexpectedException("How did this happen?")); + continue; + } + if (!entry.shouldOverlay()) { + if (OverlayTrackPlugin.usingOverlay()) { + ENTRY_LOGGER.writeInfo("Keeping the current entry under the overlay..."); + newEntry = ReactiveMusicState.currentEntry; + break; + } + ENTRY_LOGGER.writeInfo("Assigning new entry from valid entries..."); + newEntry = entry; + break; + } + } + } + else { + ENTRY_LOGGER.writeInfo("The list of valid entries is empty!"); + } + + for (ReactivePlayer player : players) { + if (player.isFinished() && !OverlayTrackPlugin.usingOverlay()) { + ENTRY_LOGGER.writeInfo("The player has finished. Clearing the current entry and song..."); + ReactiveMusicState.currentEntry = null; + ReactiveMusicState.currentSong = null; + } + } + + + if (ReactiveMusicState.currentDimBlacklisted) + newEntry = null; + + + if (newEntry != null) { + + List selectedSongs = SongPicker.getSelectedSongs(newEntry, ReactiveMusicState.validEntries); + + // wants to switch if our current entry doesn't exist -- or is not the same as the new one + boolean wantsToSwitch = !OverlayTrackPlugin.usingOverlay() && (ReactiveMusicState.currentEntry == null || !java.util.Objects.equals(ReactiveMusicState.currentEntry.getEventString(), newEntry.getEventString())); + + CHANGE_LOGGER.writeInfo(wantsToSwitch ? "Trying to switch the music." : "The music is no longer attempting to switch."); + + // if the new entry contains the same song as our current one, then do a "fake" swap to swap over to the new entry + if (wantsToSwitch && ReactiveMusicState.currentSong != null && newEntry.getSongs().contains(ReactiveMusicState.currentSong) && !queuedToStopMusic) { + ReactiveMusicDebug.LOGGER.info("doing fake swap to new event: " + newEntry.getEventString()); + // do a fake swap + ReactiveMusicState.currentEntry = newEntry; + wantsToSwitch = false; + // if this happens, also clear the queued state since we essentially did a switch + queuedToPlayMusic = false; + } + + boolean isPlaying = false; + for (ReactivePlayer player : players) { + if (player.isPlaying()) { + isPlaying = true; + break; + } + } + + // ---- FADE OUT ---- + if ((wantsToSwitch || queuedToStopMusic) && isPlaying && !OverlayTrackPlugin.usingOverlay()) { + waitForStopTicks++; + boolean shouldFadeOutMusic = false; + // handle fade-out if something's playing when a new event becomes valid + if (waitForStopTicks > getMusicStopSpeed(ReactiveMusicState.currentSongpack)) { + shouldFadeOutMusic = true; + } + // if we're queued to force stop the music, do so here + if (queuedToStopMusic) { + shouldFadeOutMusic = true; + } + + if (shouldFadeOutMusic) { + for (ReactivePlayer player : players) { + player.stopOnFadeOut(true); + player.resetOnFadeOut(true); + player.fade(0, FADE_DURATION); + } + } + } + else { + waitForStopTicks = 0; + } + + // ---- SWITCH SONG ---- + // TODO: Refactor the overlay check to something expandable. + // Also --> where else can that be done??? + // Potentially some really cool possibilities with more hooks like that. + // + if ((wantsToSwitch || queuedToPlayMusic) && !isPlaying && !OverlayTrackPlugin.usingOverlay()) { + waitForNewSongTicks++; + boolean shouldStartNewSong = false; + if (waitForNewSongTicks > getMusicDelay(ReactiveMusicState.currentSongpack)) { + shouldStartNewSong = true; + } + // if we're queued to start a new song and we're not playing anything, do it + if (queuedToPlayMusic) { + shouldStartNewSong = true; + } + if (shouldStartNewSong) { + String picked = ReactiveMusicUtils.pickRandomSong(selectedSongs); + for (ReactivePlayer player : players) { + changeCurrentSong(picked, newEntry, player); + } + waitForNewSongTicks = 0; + queuedToPlayMusic = false; + } + } + else { + waitForNewSongTicks = 0; + } + } + + // no entries are valid, we shouldn't be playing any music! + // this can happen if no entry is valid or the dimension is blacklisted + else { + CHANGE_LOGGER.writeInfo("There are no valid songpack entries!"); + for (ReactivePlayer player : players) { + player.stopOnFadeOut(true); + player.resetOnFadeOut(true); + player.fade(0, FADE_DURATION); + } + } + } + + public static List getValidEntries() { + List validEntries = new ArrayList<>(); + + for (RuntimeEntry loadedEntry : ReactiveMusicState.loadedEntries) { + + boolean isValid = SongPicker.isEntryValid(loadedEntry); + + if (isValid) { + validEntries.add(loadedEntry); + } + } + + return validEntries; + } + + public final static void processValidEvents(List validEntries, List previousValidEntries) { + + for (var entry : previousValidEntries) { + // if this event was valid before and is invalid now + if (validEntries.stream().noneMatch(e -> java.util.Objects.equals(e.getEventString(), entry.getEventString()))) { + + ReactiveMusicDebug.LOGGER.info("Triggering onInvalid() for songpack event plugins"); + for (ReactiveMusicPlugin plugin : ReactiveMusic.PLUGINS) plugin.onInvalid(entry); + + if (entry.shouldStopMusicOnInvalid()) { + ReactiveMusicDebug.LOGGER.info("trying forceStopMusicOnInvalid: " + entry.getEventString()); + if (randomChance <= entry.getForceChance()) { + ReactiveMusicDebug.LOGGER.info("doing forceStopMusicOnInvalid: " + entry.getEventString()); + queuedToStopMusic = true; + } + break; + } + } + } + + for (var entry : validEntries) { + + if (previousValidEntries.stream().noneMatch(e -> java.util.Objects.equals(e.getEventString(), entry.getEventString()))) { + // use the same random chance for all so they always happen together + boolean randSuccess = randomChance <= entry.getForceChance(); + + // if this event wasn't valid before and is now + ReactiveMusicDebug.LOGGER.info("Triggering onValid() for songpack event plugins"); + for (ReactiveMusicPlugin plugin : ReactiveMusic.PLUGINS) plugin.onValid(entry); + + if (entry.shouldStopMusicOnValid()) { + ReactiveMusicDebug.LOGGER.info("trying forceStopMusicOnValid: " + entry.getEventString()); + if (randSuccess) { + ReactiveMusicDebug.LOGGER.info("doing forceStopMusicOnValid: " + entry.getEventString()); + queuedToStopMusic = true; + } + } + if (entry.shouldStartMusicOnValid()) { + ReactiveMusicDebug.LOGGER.info("trying forceStartMusicOnValid: " + entry.getEventString()); + if (randSuccess) { + ReactiveMusicDebug.LOGGER.info("doing forceStartMusicOnValid: " + entry.getEventString()); + queuedToPlayMusic = true; + } + } + } + } + } + + + public static void changeCurrentSong(String song, RuntimeEntry newEntry, ReactivePlayer player) { + // No change? Do nothing. + if (java.util.Objects.equals(ReactiveMusicState.currentSong, song)) { + queuedToPlayMusic = false; + return; + } + + // Stop only if we’re switching tracks (not just metadata) + final boolean switchingTrack = !java.util.Objects.equals(ReactiveMusicState.currentSong, song); + if (switchingTrack && player != null && player.isPlaying()) { + player.stop(); // RMPlayerImpl stops underlying AdvancedPlayer.play() + } + + ReactiveMusicState.currentSong = song; + ReactiveMusicState.currentEntry = newEntry; + + if (player != null && song != null) { + + // if you do a fade-in elsewhere, set 0 here; otherwise set 1 + player.getGainSuppliers().get("reactivemusic").setGainPercent(1f); + player.getGainSuppliers().get("reactivemusic").setFadePercent(1f); + player.getGainSuppliers().get("reactivemusic").setFadeTarget(1f); + + player.getGainSuppliers().get("reactivemusic-duck").setGainPercent(1f); + player.getGainSuppliers().get("reactivemusic-duck").setFadePercent(1f); + player.getGainSuppliers().get("reactivemusic-duck").setFadeTarget(1f); + + player.requestGainRecompute(); + player.setSong(song); // resolves to music/.mp3 inside RMPlayerImpl + player.play(); // worker thread runs blocking play() internally + } + + queuedToPlayMusic = false; + } + + + + public static final void setActiveSongpack(RMSongpackZip songpackZip) { + + // TODO: Support more than one songpack? + if (ReactiveMusicState.currentSongpack != null) { + deactivateSongpack(ReactiveMusicState.currentSongpack); + } + + for (ReactivePlayer player : ReactiveMusic.audio().getAll()) { + resetPlayer(player); + } + + ReactiveMusicState.currentEntry = null; + ReactiveMusicState.currentSong = null; + + ReactiveMusicState.currentSongpack = songpackZip; + + ReactiveMusicState.loadedEntries = songpackZip.runtimeEntries; + + // always start new music immediately + queuedToPlayMusic = true; + + } + + public static final void deactivateSongpack(SongpackZip songpackZip) { + + // remove all entries that match that name + for (int i = ReactiveMusicState.loadedEntries.size() - 1; i >= 0; i--) { + if (ReactiveMusicState.loadedEntries.get(i).getSongpack() == songpackZip.getConfig().name) { + ReactiveMusicState.loadedEntries.remove(i); + } + } + + } + + public final static int getMusicStopSpeed(SongpackZip songpack) { + + MusicSwitchSpeed speed = ReactiveMusic.modConfig.musicSwitchSpeed2; + + if (ReactiveMusic.modConfig.musicSwitchSpeed2 == MusicSwitchSpeed.SONGPACK_DEFAULT) { + speed = songpack.getConfig().musicSwitchSpeed; + } + + if (ReactiveMusic.modConfig.debugModeEnabled) { + speed = MusicSwitchSpeed.INSTANT; + } + + switch (speed) { + case INSTANT: + return 100; + case SHORT: + return 250; + case NORMAL: + return 900; + case LONG: + return 2400; + default: + break; + } + + return 100; + + } + + public final static int getMusicDelay(SongpackZip songpack) { + + MusicDelayLength delay = ReactiveMusic.modConfig.musicDelayLength2; + + if (ReactiveMusic.modConfig.musicDelayLength2 == MusicDelayLength.SONGPACK_DEFAULT) { + delay = songpack.getConfig().musicDelayLength; + } + + if (ReactiveMusic.modConfig.debugModeEnabled) { + delay = MusicDelayLength.NONE; + } + + switch (delay) { + case NONE: + return 0; + case SHORT: + return 250; + case NORMAL: + return 900; + case LONG: + return 2400; + default: + break; + } + + return 100; + + } + + public static final void resetPlayer(ReactivePlayer player) { + if (player != null && player.isPlaying()) { + player.stop(); + player.reset(); + } + } + +} diff --git a/src/main/java/circuitlord/reactivemusic/ReactiveMusicDebug.java b/src/main/java/circuitlord/reactivemusic/ReactiveMusicDebug.java new file mode 100644 index 0000000..02b498b --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/ReactiveMusicDebug.java @@ -0,0 +1,157 @@ +package circuitlord.reactivemusic; + +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReactiveMusicDebug { + public ReactiveMusicDebug() {} + public static final ReactiveMusicDebug INSTANCE = new ReactiveMusicDebug(); + + public static final String NON_IMPL_WARN = """ + This feature is not implemented yet! + Track related issues and follow development @ + https://github.com/users/rocamocha/projects + """; + + public static final Text NON_IMPL_WARN_BUILT = new TextBuilder() + .line(ReactiveMusicDebug.NON_IMPL_WARN, Formatting.RED, Formatting.BOLD) + .build(); + + public static final Logger LOGGER = LoggerFactory.getLogger("reactive_music"); + public static final ChangeLogger CHANGE_LOGGER = INSTANCE.new ChangeLogger(); + + /** + * Useful for monitoring changes on an assignment that happens every tick, + * without causing the console to flail about and churn out hundreds of identical lines. + * ^ This is the main use case, and only works when the assignment is done from a single thread, + * and the value being monitored is viewed through the same logger instance. + * + * Trying to monitor multiple values at once with a single logger instance will not work + * in regards to reducing spam, as the last-seen value is shared across all monitored values. + * + * TODO: Implement a history function with queries to tick or repeat values, etc. + * TODO: Implement realtime tracking. + */ + public class ChangeLogger { + + private NewLog PreviousLog = null; + + public static enum LogType { INFO, ERROR, DEBUG } + + private class NewLog { + + private LogType logType; + private String msg; + private Throwable throwable; + + private NewLog(String msg) { + this.logType = LogType.INFO; + this.msg = msg; + this.throwable = null; + } + + private NewLog(String msg, Throwable throwable) { + this.logType = LogType.ERROR; + this.msg = msg; + this.throwable = throwable; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof NewLog)) return false; + NewLog that = (NewLog) o; + return logType == that.logType + && Objects.equals(msg, that.msg) + && Objects.equals(throwable, that.throwable); + } + + @Override public int hashCode() { + return Objects.hash(logType, msg, throwable); + } + } + + public void writeError(String msg, Throwable throwable) { + NewLog thisLog = new NewLog(msg, throwable); + if (thisLog.equals(PreviousLog)) return; + LOGGER.error(msg, throwable); + PreviousLog = thisLog; + } + + public void writeInfo(String msg) { + NewLog thisLog = new NewLog(msg); + if (thisLog.equals(PreviousLog)) return; + LOGGER.info(msg); + PreviousLog = thisLog; + } + } + + public class Wrapper { + private static ChangeLogger WRAPPER_LOGGER = INSTANCE.new ChangeLogger(); + + public static void fn(Runnable m) { + WRAPPER_LOGGER.writeInfo(Thread.currentThread().getStackTrace()[2].getMethodName()); + m.run(); + } + } + + /** + * Preconstruction of string literals with formatting for in-game debugging convenience. + * + * XXX ~ rocamocha ~ This idea should honestly be its own library - this is a self-reminder extract it and expand! + */ + public static class TextBuilder { + protected final MutableText root; + + public TextBuilder() { + this.root = Text.empty(); + } + + public TextBuilder header(String text) { + root.append( + Text.literal("====== " + text + " ======\n\n") + .formatted(Formatting.GOLD, Formatting.BOLD) + ); + + return this; + } + + public TextBuilder line(String value, Formatting... formats) { + root.append(Text.literal(value).formatted(formats)); + root.append(Text.literal("\n")); + return this; + } + + public TextBuilder line(String label, String value, Formatting valueColor) { + root.append(Text.literal(label + ": ").formatted(Formatting.YELLOW)); + root.append(Text.literal(value).formatted(valueColor, Formatting.BOLD)); + root.append(Text.literal("\n")); + return this; + } + + public TextBuilder raw(String text, Formatting... formats) { + root.append(Text.literal(text).formatted(formats)); + return this; + } + + public TextBuilder newline() { + root.append(Text.literal("\n")); + return this; + } + + public MutableText build() { + return root; + } + + } + + + + + + +} diff --git a/src/main/java/circuitlord/reactivemusic/ReactiveMusicState.java b/src/main/java/circuitlord/reactivemusic/ReactiveMusicState.java new file mode 100644 index 0000000..1a8bb64 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/ReactiveMusicState.java @@ -0,0 +1,42 @@ +package circuitlord.reactivemusic; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import circuitlord.reactivemusic.api.eventsys.PluginIdentifier; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import circuitlord.reactivemusic.api.songpack.SongpackZip; +import circuitlord.reactivemusic.impl.audio.RMGainSupplier; +import circuitlord.reactivemusic.api.audio.GainSupplier; +import circuitlord.reactivemusic.api.eventsys.EventRecord; + +public final class ReactiveMusicState { + + private ReactiveMusicState() {} + + public static final Logger LOGGER = LoggerFactory.getLogger("reactive_music"); + + public static SongpackZip currentSongpack = null; + public static Boolean currentDimBlacklisted = false; + + public static Boolean foundSoundInstance = false; + public static GainSupplier foundSoundInstanceGainSupplier = new RMGainSupplier(1f); + + public static Map logicFreeze = new HashMap<>(); + public static Map songpackEventMap = new HashMap<>(); + + public static RuntimeEntry currentEntry = null; + public static String currentSong = null; + + public static List validEntries = new ArrayList<>(); + public static List loadedEntries = new ArrayList<>(); + public static List previousValidEntries = new ArrayList<>(); + public static List recentlyPickedSongs = new ArrayList<>(); + +} + diff --git a/src/main/java/circuitlord/reactivemusic/SongPicker.java b/src/main/java/circuitlord/reactivemusic/SongPicker.java index bd7818b..c9bc7a1 100644 --- a/src/main/java/circuitlord/reactivemusic/SongPicker.java +++ b/src/main/java/circuitlord/reactivemusic/SongPicker.java @@ -1,54 +1,38 @@ package circuitlord.reactivemusic; - -import circuitlord.reactivemusic.config.ModConfig; -import circuitlord.reactivemusic.entries.RMRuntimeEntry; -import circuitlord.reactivemusic.mixin.BossBarHudAccessor; +import circuitlord.reactivemusic.ReactiveMusicDebug.ChangeLogger; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; import net.fabricmc.fabric.api.tag.convention.v2.ConventionalBiomeTags; -import net.minecraft.block.Block; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.CreditsScreen; import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.entity.Entity; - -import net.minecraft.entity.mob.HostileEntity; -import net.minecraft.entity.passive.HorseEntity; -import net.minecraft.entity.passive.PigEntity; -import net.minecraft.entity.passive.VillagerEntity; -import net.minecraft.entity.vehicle.BoatEntity; -import net.minecraft.entity.vehicle.MinecartEntity; - //import net.minecraft.registry.tag.BiomeTags; -import net.minecraft.registry.Registries; import net.minecraft.registry.tag.TagKey; -import net.minecraft.text.Text; import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import net.minecraft.world.biome.Biome; import java.lang.reflect.Field; import java.util.*; -public final class SongPicker { - +import org.jetbrains.annotations.NotNull; +public final class SongPicker { + private static final ChangeLogger CHANGE_LOGGER = ReactiveMusic.debugTools.new ChangeLogger(); - public static Map songpackEventMap = new EnumMap<>(SongpackEventType.class); + static int pluginTickCounter = 0; + // TODO: Put this stuff in the plugins, silly 😝 + //------------------------------------------------------------------------------------- public static Map, Boolean> biomeTagEventMap = new HashMap<>(); - public static Map recentEntityDamageSources = new HashMap<>(); - - - private static final Set BLOCK_COUNTER_BLACKLIST = Set.of("ore", "debris"); - public static boolean queuedToPrintBlockCounter = false; public static BlockPos cachedBlockCounterOrigin; - public static int currentBlockCounterX = 99999; - public static int currentBlockCounterY = 99999; + public static Map blockCounterMap = new HashMap<>(); public static Map cachedBlockChecker = new HashMap<>(); @@ -56,18 +40,9 @@ public final class SongPicker { public static String currentBiomeName = ""; public static String currentDimName = ""; - - private static final Random rand = new Random(); - - private static List recentlyPickedSongs = new ArrayList<>(); - public static final Field[] BIOME_TAG_FIELDS = ConventionalBiomeTags.class.getDeclaredFields(); public static final List> BIOME_TAGS = new ArrayList<>(); - public static Long TIME_FOR_FORGET_DAMAGE_SOURCE = 200L; - - public static boolean wasSleeping = false; - static { for (Field field : BIOME_TAG_FIELDS) { @@ -91,14 +66,7 @@ public static TagKey getBiomeTagFromField(Field field) { return null; } - - - public static void tickEventMap() { - - currentBiomeName = ""; - currentDimName = ""; - MinecraftClient mc = MinecraftClient.getInstance(); if (mc == null) return; @@ -106,434 +74,59 @@ public static void tickEventMap() { ClientPlayerEntity player = mc.player; World world = mc.world; - - songpackEventMap.put(SongpackEventType.MAIN_MENU, player == null || world == null); - songpackEventMap.put(SongpackEventType.CREDITS, mc.currentScreen instanceof CreditsScreen); - - // Early out if not in-game - if (player == null || world == null) return; - - // World processing - BlockPos playerPos = new BlockPos(player.getBlockPos()); - var biome = world.getBiome(playerPos); - - // Copied logic out from getIdAsString - currentBiomeName = (String)biome.getKey().map((key) -> { - return key.getValue().toString(); - }).orElse("[unregistered]"); - - boolean underground = !world.isSkyVisible(playerPos); - var indimension = world.getRegistryKey(); - - currentDimName = indimension.getValue().toString(); - - Entity riding = VersionHelper.GetRidingEntity(player); - - long time = world.getTimeOfDay() % 24000; - boolean night = time >= 13000 && time < 23000; - boolean sunset = time >= 12000 && time < 13000; - boolean sunrise = time >= 23000; - - - // TODO: someone help me I have no idea how to get the name of the world/server but if you know how then put it instead of "saved" - if (!wasSleeping && player.isSleeping()) { - ReactiveMusic.config.savedHomePositions.put("saved", player.getPos()); - - ModConfig.saveConfig(); - } - - wasSleeping = player.isSleeping(); - - - // special - - if (ReactiveMusic.config.savedHomePositions.containsKey("saved")) { - - Vec3d dist = player.getPos().subtract(ReactiveMusic.config.savedHomePositions.get("saved")); - - songpackEventMap.put(SongpackEventType.HOME, dist.length() < 45.0f); - } - else { - songpackEventMap.put(SongpackEventType.HOME, false); - } - - - - // Time - songpackEventMap.put(SongpackEventType.DAY, !night); - songpackEventMap.put(SongpackEventType.NIGHT, night); - songpackEventMap.put(SongpackEventType.SUNSET, sunset); - songpackEventMap.put(SongpackEventType.SUNRISE, sunrise); - - - // Actions - - songpackEventMap.put(SongpackEventType.DYING, player.getHealth() / player.getMaxHealth() < 0.35); - songpackEventMap.put(SongpackEventType.FISHING, player.fishHook != null); - - songpackEventMap.put(SongpackEventType.MINECART, riding instanceof MinecartEntity); - songpackEventMap.put(SongpackEventType.BOAT, riding instanceof BoatEntity); - songpackEventMap.put(SongpackEventType.HORSE, riding instanceof HorseEntity); - songpackEventMap.put(SongpackEventType.PIG, riding instanceof PigEntity); - - - songpackEventMap.put(SongpackEventType.OVERWORLD, indimension == World.OVERWORLD); - songpackEventMap.put(SongpackEventType.NETHER, indimension == World.NETHER); - songpackEventMap.put(SongpackEventType.END, indimension == World.END); - - - songpackEventMap.put(SongpackEventType.UNDERGROUND, indimension == World.OVERWORLD && underground && playerPos.getY() < 55); - songpackEventMap.put(SongpackEventType.DEEP_UNDERGROUND, indimension == World.OVERWORLD && underground && playerPos.getY() < 15); - songpackEventMap.put(SongpackEventType.HIGH_UP, indimension == World.OVERWORLD && !underground && playerPos.getY() > 128); - - songpackEventMap.put(SongpackEventType.UNDERWATER, player.isSubmergedInWater()); - - // Weather - songpackEventMap.put(SongpackEventType.RAIN, world.isRaining() && biome.value().getPrecipitation(playerPos) == Biome.Precipitation.RAIN); - songpackEventMap.put(SongpackEventType.SNOW, world.isRaining() && biome.value().getPrecipitation(playerPos) == Biome.Precipitation.SNOW); - - songpackEventMap.put(SongpackEventType.STORM, world.isThundering()); - - - var currentTags = biome.streamTags().toList(); - - // Update all ConventionalBiomeTags - for (TagKey tag : BIOME_TAGS) { - boolean found = false; - - // search by ID instead of comparing tagkey, doesn't work on non-fabric - for (TagKey curTag : currentTags) { - if (curTag.id() == tag.id()) { - found = true; - break; - } - } - - biomeTagEventMap.put(tag, found); - } - - - // process recent damage sources - - // remove past sources - recentEntityDamageSources.entrySet().removeIf(entry -> entry.getKey() == null || !entry.getKey().isAlive() || world.getTime() - entry.getValue() > TIME_FOR_FORGET_DAMAGE_SOURCE); - - // add new damage sources - var recentDamage = player.getRecentDamageSource(); - - if (recentDamage != null && recentDamage.getSource() != null) { - recentEntityDamageSources.put(recentDamage.getSource(), world.getTime()); - } - - - - - // Search for nearby entities that could be relevant to music - - { - int villagerCount = 0; - - double radiusXZ = 30.0; - double radiusY = 15.0; - - Box box = new Box(player.getX() - radiusXZ, player.getY() - radiusY, player.getZ() - radiusXZ, - player.getX() + radiusXZ, player.getY() + radiusY, player.getZ() + radiusXZ); - - List nearbyVillagerCheck = world.getEntitiesByClass(VillagerEntity.class, box, entity -> entity != null); - - for (VillagerEntity villagerEntity : nearbyVillagerCheck) { - villagerCount++; - } - - songpackEventMap.put(SongpackEventType.VILLAGE, villagerCount > 0); - - } - - { - List nearbyHostile = world.getEntitiesByClass(HostileEntity.class, - GetBoxAroundPlayer(player, 12.f, 6.f), - entity -> entity != null); - - songpackEventMap.put(SongpackEventType.NEARBY_MOBS, nearbyHostile.size() >= 1); - - } - - - //songpackEventMap.put(SongpackEventType.HOSTILE_MOBS, aggroMobsCount >= 4); - - //System.out.println("Villager count: " + villagerCount + ", Aggro mobs count: " + aggroMobsCount); - - - // try to get boss bars - boolean bossBarActive = false; - - if (mc.inGameHud != null && mc.inGameHud.getBossBarHud() != null) { - try { - - var bossBars = ((BossBarHudAccessor) mc.inGameHud.getBossBarHud()).getBossBars(); - - if (!bossBars.isEmpty()) { - bossBarActive = true; - } - } catch (Exception e) { - } - } - - - songpackEventMap.put(SongpackEventType.BOSS, bossBarActive); - - - songpackEventMap.put(SongpackEventType.GENERIC, true); - } - - - - public static void tickBlockCounterMap() { - - long startTime = System.currentTimeMillis(); - long startNano = System.nanoTime(); - - int RADIUS = 25; - - MinecraftClient mc = MinecraftClient.getInstance(); - ClientPlayerEntity player = mc.player; - World world = mc.world; - if (player == null || world == null) - return; - - - - // Advance y, then x -/* currentBlockCounterY++; - if (currentBlockCounterY > RADIUS) { - currentBlockCounterY = -RADIUS; - - currentBlockCounterX++; - - ReactiveMusic.LOGGER.info("blockchecker X:" + currentBlockCounterX); - if (currentBlockCounterX > RADIUS) { - currentBlockCounterX = -RADIUS; - - // ReactiveMusic.LOGGER.info("blockchecker X:" + currentBlockCounterX); + + pluginTickCounter++; + + for (ReactiveMusicPlugin plugin : ReactiveMusic.PLUGINS) { + + if (ReactiveMusicState.logicFreeze.computeIfAbsent(plugin.pluginId, k -> false)) { + ReactiveMusicState.LOGGER.info("Skipping execution for " + plugin.pluginId.getId()); + continue; } - }*/ - - // just X - currentBlockCounterX++; - if (currentBlockCounterX > RADIUS) { - currentBlockCounterX = -RADIUS; - } - - // finished iterating, reset - if (currentBlockCounterX == -RADIUS/* && currentBlockCounterY == -RADIUS*/) { - // ReactiveMusic.LOGGER.info("Finished checking for blocks, resetting! Total: " + blockCounterMap.size()); - - if (queuedToPrintBlockCounter) { - player.sendMessage(Text.of("[ReactiveMusic]: Logging Block Counter map!")); - - blockCounterMap.entrySet().stream() - .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) - .forEach(entry -> player.sendMessage(Text.of(entry.getKey() + ": " + entry.getValue()), false)); - - queuedToPrintBlockCounter = false; + plugin.newTick(); + if (player == null || world == null) { + continue; } - - // copy - cachedBlockChecker.clear(); - cachedBlockChecker.putAll(blockCounterMap); - - // reset - blockCounterMap.clear(); - cachedBlockCounterOrigin = player.getBlockPos(); - - } - - BlockPos.Mutable mutablePos = new BlockPos.Mutable(); - - for (int y = -RADIUS; y <= RADIUS; y++) { - for (int z = -RADIUS; z <= RADIUS; z++) { - - // don't allocate new blockpos everytime - mutablePos.set( - cachedBlockCounterOrigin.getX() + currentBlockCounterX, - cachedBlockCounterOrigin.getY() + y, - cachedBlockCounterOrigin.getZ() + z - ); - - - Block block = world.getBlockState(mutablePos).getBlock(); - String key = Registries.BLOCK.getId(block).toString(); - - boolean isBlacklisted = false; - for (String black : BLOCK_COUNTER_BLACKLIST) { - if (key.contains(black)) { - isBlacklisted = true; - break; - } - } - if (isBlacklisted) - continue; - - blockCounterMap.merge(key, 1, Integer::sum); - + + + // throttled tick + int interval = plugin.tickSchedule(); + if (interval <= 1 || (pluginTickCounter % interval) == 0L) { + plugin.gameTick(player, world, ReactiveMusicState.songpackEventMap); } } + if (player == null || world == null) initialize(); - - //ReactiveMusic.LOGGER.info("tickBlockCounterMap() took " + (System.currentTimeMillis() - startTime) + "ms"); - - long endNano = System.nanoTime(); - long elapsedNano = endNano - startNano; - double elapsedMs = elapsedNano / 1_000_000.0; - - //ReactiveMusic.LOGGER.info("tickBlockCounterMap() took (" + elapsedMs + "ms)"); - - - - - } - - - - private static Box GetBoxAroundPlayer(ClientPlayerEntity player, float radiusXZ, float radiusY) { - - return new Box(player.getX() - radiusXZ, player.getY() - radiusY, player.getZ() - radiusXZ, - player.getX() + radiusXZ, player.getY() + radiusY, player.getZ() + radiusXZ); + ReactiveMusicState.songpackEventMap.put(SongpackEvent.GENERIC, true); + ReactiveMusicState.songpackEventMap.put(SongpackEvent.MAIN_MENU, (player == null || world == null)); + ReactiveMusicState.songpackEventMap.put(SongpackEvent.CREDITS, (mc.currentScreen instanceof CreditsScreen)); } - public static void initialize() { - - songpackEventMap.clear(); - - for (SongpackEventType eventType : SongpackEventType.values()) { - songpackEventMap.put(eventType, false); + // build string -> type map from the internal registry + ReactiveMusicState.songpackEventMap.clear(); + for (EventRecord eventRecord : SongpackEvent.values()) { + ReactiveMusicState.songpackEventMap.put(eventRecord, false); } } + //---------------------------------------------------------------------------------------- + public static boolean isEntryValid(RuntimeEntry entry) { - - - private static final List reusableValidEntries = new ArrayList<>(); - - -/* public static List getAllValidEntries() { - - reusableValidEntries.clear(); - - for (int i = 0; i < SongLoader.activeSongpack.entries.length; i++) { - - SongpackEntry entry = SongLoader.activeSongpack.entries[i]; - if (entry == null) continue; - - boolean eventsMet = true; - - for (SongpackEventType songpackEvent : entry.songpackEvents) { - - if (!songpackEventMap.containsKey(songpackEvent)) - continue; - - if (!songpackEventMap.get(songpackEvent)) { - eventsMet = false; - break; - } - } - - for (TagKey biomeTagEvent : entry.biomeTagEvents) { - - if (!biomeTagEventMap.containsKey(biomeTagEvent)) - continue; - - if (!biomeTagEventMap.get(biomeTagEvent)) { - eventsMet = false; - break; - } - } - - if (eventsMet) { - reusableValidEntries.add(entry); - } - } - - return reusableValidEntries; - }*/ - - - static boolean hasSongNotPlayedRecently(List songs) { - for (String song : songs) { - if (!recentlyPickedSongs.contains(song)) { - return true; - } - } - return false; - } - - - static List getNotRecentlyPlayedSongs(String[] songs) { - List notRecentlyPlayed = new ArrayList<>(Arrays.asList(songs)); - notRecentlyPlayed.removeAll(recentlyPickedSongs); - return notRecentlyPlayed; - } - - - static String pickRandomSong(List songs) { - - if (songs.isEmpty()) { - return null; - } - - List cleanedSongs = new ArrayList<>(songs); - - cleanedSongs.removeAll(recentlyPickedSongs); - - - String picked; - - // If there's remaining songs, pick one of those - if (!cleanedSongs.isEmpty()) { - int randomIndex = rand.nextInt(cleanedSongs.size()); - picked = cleanedSongs.get(randomIndex); - } - - // Else we've played all these recently so just pick a new random one - else { - int randomIndex = rand.nextInt(songs.size()); - picked = songs.get(randomIndex); - } - - - // only track the past X songs - if (recentlyPickedSongs.size() >= 8) { - recentlyPickedSongs.remove(0); - } - - recentlyPickedSongs.add(picked); - - - return picked; - } - - - public static String getSongName(String song) { - return song == null ? "" : song.replaceAll("([^A-Z])([A-Z])", "$1 $2"); - } - - - public static boolean isEntryValid(RMRuntimeEntry entry) { - - for (var condition : entry.conditions) { + for (var condition : entry.getConditions()) { // each condition functions as an OR, if at least one of them is true then the condition is true boolean songpackEventsValid = false; - - for (SongpackEventType songpackEvent : condition.songpackEvents) { - if (songpackEventMap.containsKey(songpackEvent) && songpackEventMap.get(songpackEvent)) { + for (var eventRecord : condition.songpackEvents) { + if (eventRecord == null) continue; + CHANGE_LOGGER.writeInfo(ReactiveMusicState.songpackEventMap.containsKey(eventRecord) ? "The event record key was found in the event map!" : "Oh no!" ); + if (ReactiveMusicState.songpackEventMap.containsKey(eventRecord) && ReactiveMusicState.songpackEventMap.get(SongpackEvent.get(eventRecord.getEventId()))) { songpackEventsValid = true; break; } @@ -586,12 +179,23 @@ public static boolean isEntryValid(RMRuntimeEntry entry) { } - - - - - - - - + public static @NotNull List getSelectedSongs(RuntimeEntry newEntry, List validEntries) { + // if we have non-recent songs then just return those + if (ReactiveMusicUtils.hasSongNotPlayedRecently(newEntry.getSongs())) { + return newEntry.getSongs(); + } + // Fallback behaviour + if (newEntry.fallbackAllowed()) { + for (int i = 1; i < ReactiveMusicState.validEntries.size(); i++) { + if (ReactiveMusicState.validEntries.get(i) == null) + continue; + // check if we have songs not played recently and early out + if (ReactiveMusicUtils.hasSongNotPlayedRecently(ReactiveMusicState.validEntries.get(i).getSongs())) { + return ReactiveMusicState.validEntries.get(i).getSongs(); + } + } + } + // we've played everything recently, just give up and return this event's songs + return newEntry.getSongs(); + } } diff --git a/src/main/java/circuitlord/reactivemusic/SongpackEventType.java b/src/main/java/circuitlord/reactivemusic/SongpackEventType.java deleted file mode 100644 index baea764..0000000 --- a/src/main/java/circuitlord/reactivemusic/SongpackEventType.java +++ /dev/null @@ -1,61 +0,0 @@ -package circuitlord.reactivemusic; - -public enum SongpackEventType { - - NONE, - - MAIN_MENU, - CREDITS, - - - HOME, - - - // --- TIME --- - DAY, - NIGHT, - SUNRISE, - SUNSET, - - // --- Weather --- - RAIN, - SNOW, - STORM, - - - // --- world height --- - UNDERWATER, - UNDERGROUND, - DEEP_UNDERGROUND, - HIGH_UP, - - // --- Entities --- - MINECART, - BOAT, - HORSE, - PIG, - - //Actions - FISHING, - DYING, - - - // TODO: remove - OVERWORLD, - NETHER, - END, - - // MOBS - - BOSS, - VILLAGE, - - NEARBY_MOBS, - - - - - GENERIC - -} - diff --git a/src/main/java/circuitlord/reactivemusic/SongpackZip.java b/src/main/java/circuitlord/reactivemusic/SongpackZip.java deleted file mode 100644 index 2bac6fa..0000000 --- a/src/main/java/circuitlord/reactivemusic/SongpackZip.java +++ /dev/null @@ -1,28 +0,0 @@ -package circuitlord.reactivemusic; - -import circuitlord.reactivemusic.entries.RMRuntimeEntry; - -import java.nio.file.Path; -import java.util.List; - -public class SongpackZip { - - public SongpackConfig config; - - - public List runtimeEntries; - - - public Path path; - - public String errorString = ""; - public boolean blockLoading = false; - - // backwards compat - public boolean convertBiomeToBiomeTag = false; - - public boolean isv05OldSongpack = false; - - public boolean embedded = false; - -} diff --git a/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicAPI.java b/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicAPI.java new file mode 100644 index 0000000..99d0b9a --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicAPI.java @@ -0,0 +1,55 @@ +package circuitlord.reactivemusic.api; + +import java.util.List; + +import circuitlord.reactivemusic.*; +import circuitlord.reactivemusic.api.audio.ReactivePlayerManager; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import circuitlord.reactivemusic.api.songpack.SongpackZip; +import circuitlord.reactivemusic.impl.songpack.RMSongpackLoader; + +public interface ReactiveMusicAPI { + public interface ModConfig { + static boolean debugModeEnabled() { return ReactiveMusic.modConfig.debugModeEnabled; } + } + + /** + * API view for the anything related to the EVENT system should go here. + * This allows for expandability in the future past the event system, and better + * modularity. + */ + public interface EventSys { + } + + /** + * API view for anything related to SONGPACKS should go here. + * + * TODO: Add a method to inject a code-defined songpack object at runtime + * (for plugins that want to define their own songs/events without a zip file) + * + * This is kept separate from the "Song Selection" utilities in ReactiveMusicUtils + * because song selection is a more general-purpose utility that can be used + * by plugins and other systems that don't need to know about songpacks. + * + * In the future, if we add more songpack-related functionality, and abstract it + * away from the core ReactiveMusicState, we might want to add more to this interface + * to allow plugins to define and interact with those same structured systems. + * @see ReactiveMusicUtils + * @see SongpackZip + */ + public interface Songpack { + static SongpackZip getCurrent() { return ReactiveMusicState.currentSongpack; } + static List getAvailable() { return List.copyOf(RMSongpackLoader.availableSongpacks); } + + static RuntimeEntry currentEntry() { return ReactiveMusicState.currentEntry; } + static String currentSong() { return ReactiveMusicState.currentSong; } + static List recentSongs() { return ReactiveMusicState.recentlyPickedSongs; } + static List validEntries() { return List.copyOf(ReactiveMusicState.validEntries); } + static List loadedEntries() { return List.copyOf(ReactiveMusicState.loadedEntries); } + static List previousValidEntries() { return List.copyOf(ReactiveMusicState.previousValidEntries); } + } + + static ReactivePlayerManager audioManager() { return ReactiveMusic.audio(); } + + +} diff --git a/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicPlugin.java b/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicPlugin.java new file mode 100644 index 0000000..cf8612a --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicPlugin.java @@ -0,0 +1,100 @@ +package circuitlord.reactivemusic.api; + +import circuitlord.reactivemusic.ReactiveMusicState; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.eventsys.PluginIdentifier; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import circuitlord.reactivemusic.impl.eventsys.RMEventRecord; +import circuitlord.reactivemusic.impl.eventsys.RMPluginIdentifier; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.HashMap; +import java.util.Map; + +/** + * Your plugin class should implement this interface, as it hooks into the flow of Reactive Music's core programming. + * For your plugin to be recognized and loaded by Reactive Music, create a plaintext file with the class' full + * package path (ex. circuitlord.reactivemusic.plugins.WeatherAltitudePlugin) on it's own line in + * resources/META-INF/services + */ +public abstract class ReactiveMusicPlugin { + + public static final Map pluginMap = new HashMap<>(); + + public final RMPluginIdentifier pluginId; + private int tickInterval = 20; + /** + * Constructor ensures developers do not skip initialization. + * @param namespace + * @param path + */ + protected ReactiveMusicPlugin(String namespace, String path) { + this.pluginId = new RMPluginIdentifier(namespace, path); + } + + + /** + * If your plugin should provide new events, this is where they are declared. + * @param eventNames + */ + public void registerSongpackEvents(String... eventNames) { + for (String e : eventNames) { + SongpackEvent.register(new RMEventRecord(e.toUpperCase(), this.pluginId)); + } + } + + public final void freeze(PluginIdentifier pluginId) { ReactiveMusicState.logicFreeze.put(this.pluginId, true); } + public final void unfreeze(PluginIdentifier pluginId) { ReactiveMusicState.logicFreeze.put(this.pluginId, false); } + + /** + * Called during ModInitialize() + *

Use this method to register your new events to the Reactive Music event system. + * Songpack creators can use these events in their YAML files, it is up to the logic in + * the overrideable tick methods to set the event states.

+ * + * @see #tickSchedule() + * @see #gameTick(PlayerEntity, World, Map) + * @see #newTick() + * @see #onValid(RMRuntimeEntry) + * @see #onInvalid(RMRuntimeEntry) + */ + public void init() {}; + + /** + * Override this method to set a different schedule, or to schedule dynamically. + * @return The number of ticks that must pass before gameTick() is called each loop. + */ + public int tickSchedule() { return this.tickInterval; } // per-plugin configurable tick throttling + + /** + * Called when scheduled. Default schedule is 20 ticks, and can be configured. + * Provides player, world, and Reactive Music's eventMap for convenience. + * @param player + * @param world + * @param eventMap + * @see #tickSchedule() + */ + public void gameTick(PlayerEntity player, World world, Map eventMap) {}; + + /** + * Called every tick. + */ + public void newTick() {}; + + /** + * FIXME: Why isn't this getting called??? Help! + * Calls when entry flips from invalid -> valid. + * @param entry + */ + public void onValid(RuntimeEntry entry) {} + + /** + * FIXME: Why isn't this getting called??? Help! + * Calls when entry flips from valid -> invalid. + * @param entry + */ + public void onInvalid(RuntimeEntry entry) {} +} + diff --git a/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicUtils.java b/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicUtils.java new file mode 100644 index 0000000..66023e7 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/ReactiveMusicUtils.java @@ -0,0 +1,191 @@ +package circuitlord.reactivemusic.api; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; + +import java.util.Random; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import circuitlord.reactivemusic.ReactiveMusicState; + +/** + * One-file, plugin-facing utils for ReactiveMusic. + * - Server-safe (no direct client class references in signatures) + * - Client helpers are provided via a delegate you set from client code + * - Minimal allocations; safe for per-tick use + */ +public final class ReactiveMusicUtils { + + private ReactiveMusicUtils() {} + + /* ========================================================= + XXX================ SONG SELECTION ==================== + ========================================================= */ + + /** + * TODO: + * Song selection could be it's own class, with more features, + * but for now this is sufficient. + * + * Song selection as a class would allow things like multiple + * independent recent-song lists (e.g., per-plugin), + * or more advanced strategies (e.g., weighted random). + */ + + private static final Random rand = new Random(); + + public static boolean hasSongNotPlayedRecently(List songs) { + for (String song : songs) { + if (!ReactiveMusicState.recentlyPickedSongs.contains(song)) { + return true; + } + } + return false; + } + + + public static List getNotRecentlyPlayedSongs(String[] songs) { + List notRecentlyPlayed = new ArrayList<>(Arrays.asList(songs)); + notRecentlyPlayed.removeAll(ReactiveMusicState.recentlyPickedSongs); + return notRecentlyPlayed; + } + + + public static String pickRandomSong(List songs) { + + if (songs.isEmpty()) { + return null; + } + + List cleanedSongs = new ArrayList<>(songs); + + cleanedSongs.removeAll(ReactiveMusicState.recentlyPickedSongs); + + + String picked; + + // If there's remaining songs, pick one of those + if (!cleanedSongs.isEmpty()) { + int randomIndex = rand.nextInt(cleanedSongs.size()); + picked = cleanedSongs.get(randomIndex); + } + + // Else we've played all these recently so just pick a new random one + else { + int randomIndex = rand.nextInt(songs.size()); + picked = songs.get(randomIndex); + } + + // only track the past X songs + if (ReactiveMusicState.recentlyPickedSongs.size() >= 8) { + ReactiveMusicState.recentlyPickedSongs.remove(0); + } + ReactiveMusicState.recentlyPickedSongs.add(picked); + + return picked; + } + + + public static String getSongName(String song) { + return song == null ? "" : song.replaceAll("([^A-Z])([A-Z])", "$1 $2"); + } + + /* ========================================================= + XXX================ WORLD HELPERS ===================== + ========================================================= */ + + public static Box boxAround(Entity e, float radiusXZ, float radiusY) { + double x = e.getX(), y = e.getY(), z = e.getZ(); + return new Box( + x - radiusXZ, y - radiusY, z - radiusXZ, + x + radiusXZ, y + radiusY, z + radiusXZ + ); + } + + public static boolean isHighUp(BlockPos pos, int minY) { return pos.getY() >= minY; } + public static boolean isUnderground(World w, BlockPos pos, int maxY) { return pos.getY() <= maxY && !w.isSkyVisible(pos); } + public static boolean isDeepUnderground(World w, BlockPos pos, int maxY){ return pos.getY() <= maxY && !w.isSkyVisible(pos); } + + public static boolean isRainingAt(World w, BlockPos pos) { return w.isRaining() && w.getBiome(pos).value().getPrecipitation(pos) == Biome.Precipitation.RAIN; } + public static boolean isSnowingAt(World w, BlockPos pos) { return w.isRaining() && w.getBiome(pos).value().getPrecipitation(pos) == Biome.Precipitation.SNOW; } + public static boolean isStorm(World w) { return w.isThundering(); } + + /* ========================================================= + XXX============ SPHERICAL ENTITY QUERIES ============== + ========================================================= */ + + /** + * Broad-phase AABB + narrow-phase squared-distance sphere check. + * Returns all entities of the given type within a true sphere around the player. + * Uses Entity#squaredDistanceTo to avoid a sqrt() call, keeping it fast. + * + * @param type entity class to search for (e.g., HostileEntity.class) + * @param player center of the sphere + * @param radius sphere radius in blocks + * @param extraFilter optional additional filter (may be null) + */ + public static List getEntitiesInSphere( + Class type, PlayerEntity player, double radius, Predicate extraFilter) { + + final double r2 = radius * radius; + + // Broad-phase: chunk-efficient AABB around the player + Box box = player.getBoundingBox().expand(radius, radius, radius); + + // Narrow-phase: exact spherical test using squared distance (no sqrt) + return player.getWorld().getEntitiesByClass( + type, + box, + e -> e.isAlive() + && e.squaredDistanceTo(player) <= r2 + && (extraFilter == null || extraFilter.test(e)) + ); + } + + /** Convenience: true if any entity of type is within the spherical radius. */ + public static boolean anyInSphere( + Class type, PlayerEntity player, double radius, Predicate extraFilter) { + final double r2 = radius * radius; + Box box = player.getBoundingBox().expand(radius, radius, radius); + // Use early-exit variant to avoid allocating a list + return !player.getWorld().getEntitiesByClass( + type, + box, + e -> e.isAlive() + && e.squaredDistanceTo(player) <= r2 + && (extraFilter == null || extraFilter.test(e)) + ).isEmpty(); + } + + /** Ring/band query: entities between inner and outer radii (inclusive). */ + public static List getEntitiesInSphericalBand( + Class type, PlayerEntity player, double innerRadius, double outerRadius, + Predicate extraFilter) { + + if (innerRadius < 0) innerRadius = 0; + if (outerRadius < innerRadius) outerRadius = innerRadius; + + final double rMin2 = innerRadius * innerRadius; + final double rMax2 = outerRadius * outerRadius; + Box box = player.getBoundingBox().expand(outerRadius, outerRadius, outerRadius); + + return player.getWorld().getEntitiesByClass( + type, + box, + e -> { + if (!e.isAlive()) return false; + double d2 = e.squaredDistanceTo(player); + return d2 >= rMin2 + && d2 <= rMax2 + && (extraFilter == null || extraFilter.test(e)); + } + ); + } +} diff --git a/src/main/java/circuitlord/reactivemusic/api/audio/GainSupplier.java b/src/main/java/circuitlord/reactivemusic/api/audio/GainSupplier.java new file mode 100644 index 0000000..76e5c36 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/audio/GainSupplier.java @@ -0,0 +1,23 @@ +package circuitlord.reactivemusic.api.audio; + +public interface GainSupplier { + float supplyComputedPercent(); + + void setGainPercent(float p); + float getGainPercent(); + + void setFadePercent(float p); + float getFadePercent(); + + void setFadeTarget(float p); + float getFadeTarget(); + + void setFadeDuration(int tickDuration); + int getFadeDuration(); + + void clearFadeStart(); + float getFadeStart(); + + boolean isFadingOut(); + boolean isFadingIn(); +} diff --git a/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayer.java b/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayer.java new file mode 100644 index 0000000..e1943f4 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayer.java @@ -0,0 +1,49 @@ +package circuitlord.reactivemusic.api.audio; + +import java.util.concurrent.ConcurrentHashMap; + +public interface ReactivePlayer extends AutoCloseable { + String id(); // unique handle, e.g. "myplugin:ambient-1" + boolean isPlaying(); + // boolean isPaused(); + boolean isIdle(); + boolean isFinished(); + + void play(); // (re)start from beginning + void stop(); // stop + release decoder + // void pause(); // pause without releasing resources + // void resume(); + + // Source + void setSong(String songId); // e.g. "music/ForestTheme" -> resolves to music/ForestTheme.mp3 in active songpack + void setStream(java.util.function.Supplier streamSupplier); // custom source + void setFile(String fileId); + + // Gain / routing + float requestGainRecompute(); + ConcurrentHashMap getGainSuppliers(); + void setMute(boolean v); + float getRealGainDb(); // last applied dB to audio device + + // Grouping / coordination + void setGroup(String group); // e.g. "music", "ambient", "sfx" + String getGroup(); + + // Events + void onComplete(Runnable r); // fires when track completes + void onError(java.util.function.Consumer c); + + void close(); // same as stop(); also unregister + void reset(); + + // More controls & accessors + void fade(float target, int tickDuration); + void fade(String gainSupplierId, float target, int tickDuration); + + // Fade OUT specific + boolean stopOnFadeOut(); + boolean resetOnFadeOut(); + + void stopOnFadeOut(boolean v); + void resetOnFadeOut(boolean v); +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayerManager.java b/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayerManager.java new file mode 100644 index 0000000..b3eb2e8 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayerManager.java @@ -0,0 +1,23 @@ +package circuitlord.reactivemusic.api.audio; + +import java.util.Collection; + +public interface ReactivePlayerManager { + // Factory + ReactivePlayer create(String id, ReactivePlayerOptions opts); // throws if id already exists + + // Lookup / control + ReactivePlayer get(String id); // null if missing + Collection getAll(); + Collection getByGroup(String group); + + // Cross-player ducking (optional, simple + predictable) + void setGroupDuck(String group, float percent); // multiplies each player’s duck layer + float getGroupDuck(String group); + + // Lifecycle + void closeAllForPlugin(String pluginNamespace); + void closeAll(); // on shutdown + + void tick(); +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayerOptions.java b/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayerOptions.java new file mode 100644 index 0000000..aef89f9 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/audio/ReactivePlayerOptions.java @@ -0,0 +1,53 @@ +package circuitlord.reactivemusic.api.audio; + +/** Builder-style options for creating RMPlayers. */ +public final class ReactivePlayerOptions { + // --- sensible defaults --- + private String pluginNamespace = "default"; + private String group = "default"; + private boolean loop = false; + private boolean autostart = true; + private boolean linkToMinecraftVolumes = true; // MASTER * MUSIC coupling + private boolean quietWhenGamePaused = true; // “quiet” layer when paused + private int gainRefreshIntervalTicks = 10; // 0/1 = every tick + + private float initialGainPercent = 1.0f; // 0..1 + private float initialDuckPercent = 1.0f; // 0..1 + private float initialFadePercent = 0.0f; // 0..1 + + private ReactivePlayerOptions() {} + + /** Start a new options object with defaults. */ + public static ReactivePlayerOptions create() { return new ReactivePlayerOptions(); } + + // --- fluent setters (all return this) --- + public ReactivePlayerOptions namespace(String ns) { this.pluginNamespace = ns; return this; } + public ReactivePlayerOptions group(String g) { this.group = g; return this; } + public ReactivePlayerOptions loop(boolean v) { this.loop = v; return this; } + public ReactivePlayerOptions autostart(boolean v) { this.autostart = v; return this; } + + public ReactivePlayerOptions linkToMinecraftVolumes(boolean v) { this.linkToMinecraftVolumes = v; return this; } + public ReactivePlayerOptions quietWhenGamePaused(boolean v) { this.quietWhenGamePaused = v; return this; } + public ReactivePlayerOptions gainRefreshIntervalTicks(int ticks) { this.gainRefreshIntervalTicks = Math.max(0, ticks); return this; } + + /** Initial volume [0..1]. */ + public ReactivePlayerOptions gain(float pct) { this.initialGainPercent = clamp01(pct); return this; } + + /** Initial per-player duck [0..1]. Multiplies with any group duck. */ + public ReactivePlayerOptions duck(float pct) { this.initialDuckPercent = clamp01(pct); return this; } + public ReactivePlayerOptions fade(float pct) { this.initialFadePercent = clamp01(pct); return this; } + + // --- getters (used by the manager/impl) --- + public String pluginNamespace() { return pluginNamespace; } + public String group() { return group; } + public boolean loop() { return loop; } + public boolean autostart() { return autostart; } + public boolean linkToMinecraftVolumes() { return linkToMinecraftVolumes; } + public boolean quietWhenGamePaused() { return quietWhenGamePaused; } + public int gainRefreshIntervalTicks() { return gainRefreshIntervalTicks; } + public float initialGainPercent() { return initialGainPercent; } + public float initialDuckPercent() { return initialDuckPercent; } + public float initialFadePercent() { return initialFadePercent; } + + private static float clamp01(float f) { return (f < 0f) ? 0f : (f > 1f ? 1f : f); } +} diff --git a/src/main/java/circuitlord/reactivemusic/api/eventsys/EventRecord.java b/src/main/java/circuitlord/reactivemusic/api/eventsys/EventRecord.java new file mode 100644 index 0000000..c799f87 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/eventsys/EventRecord.java @@ -0,0 +1,8 @@ +package circuitlord.reactivemusic.api.eventsys; + +public interface EventRecord { + + String getEventId(); + PluginIdentifier getPluginId(); + +} diff --git a/src/main/java/circuitlord/reactivemusic/api/eventsys/PluginIdentifier.java b/src/main/java/circuitlord/reactivemusic/api/eventsys/PluginIdentifier.java new file mode 100644 index 0000000..754cae5 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/eventsys/PluginIdentifier.java @@ -0,0 +1,9 @@ +package circuitlord.reactivemusic.api.eventsys; + +public interface PluginIdentifier { + String getNamespace(); + String getPath(); + String getId(); + + void setTitle(String title); +} diff --git a/src/main/java/circuitlord/reactivemusic/api/songpack/RuntimeEntry.java b/src/main/java/circuitlord/reactivemusic/api/songpack/RuntimeEntry.java new file mode 100644 index 0000000..64af4fd --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/songpack/RuntimeEntry.java @@ -0,0 +1,34 @@ +package circuitlord.reactivemusic.api.songpack; + +import java.util.List; + +import circuitlord.reactivemusic.impl.songpack.RMEntryCondition; +import circuitlord.reactivemusic.impl.songpack.RMRuntimeEntry; + +/** Marker for type-safety without exposing internals.*/ +public interface RuntimeEntry { + /** + * Not implemented yet. TODO: Second parse of the yaml? + * The dynamic keys can't be typecast beforehand, so we need to get them as a raw map. + * @return External option defined in the yaml config. + * @see RMRuntimeEntry#setExternalOption(String key, Object value) + */ + Object getExternalOption(String key); + + String getSongpack(); + String getEventString(); + String getErrorString(); + List getSongs(); + + boolean fallbackAllowed(); + boolean shouldOverlay(); + + boolean shouldStopMusicOnValid(); + boolean shouldStopMusicOnInvalid(); + boolean shouldStartMusicOnValid(); + float getForceChance(); + + List getConditions(); + + +} diff --git a/src/main/java/circuitlord/reactivemusic/api/songpack/SongpackEvent.java b/src/main/java/circuitlord/reactivemusic/api/songpack/SongpackEvent.java new file mode 100644 index 0000000..3c5aa4b --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/songpack/SongpackEvent.java @@ -0,0 +1,27 @@ +package circuitlord.reactivemusic.api.songpack; + +import java.util.Map; + +import circuitlord.reactivemusic.impl.eventsys.RMEventRecord; +import circuitlord.reactivemusic.impl.songpack.RMSongpackEvent; + +/** + * This had to be structured as a coupling. + * This is the core of RM, please be careful if you are going to touch or change this. + * @see RMSongpackEvent + */ +public interface SongpackEvent { + // Do not leak impl here + Map getMap(); + + // Static API that delegates to the impl + static RMEventRecord get(String id) { return RMSongpackEvent.get(id); } + static RMEventRecord register(RMEventRecord eventRecord) { return RMSongpackEvent.register(eventRecord); } + static RMEventRecord[] values() { return RMSongpackEvent.values(); } + + // Optional: expose predefined constants as interface-typed fields + RMEventRecord NONE = RMSongpackEvent.NONE; + RMEventRecord MAIN_MENU = RMSongpackEvent.MAIN_MENU; + RMEventRecord CREDITS = RMSongpackEvent.CREDITS; + RMEventRecord GENERIC = RMSongpackEvent.GENERIC; +} diff --git a/src/main/java/circuitlord/reactivemusic/api/songpack/SongpackZip.java b/src/main/java/circuitlord/reactivemusic/api/songpack/SongpackZip.java new file mode 100644 index 0000000..c99cc8d --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/api/songpack/SongpackZip.java @@ -0,0 +1,18 @@ +package circuitlord.reactivemusic.api.songpack; + +import java.nio.file.Path; +import java.util.List; + +import circuitlord.reactivemusic.impl.songpack.RMSongpackConfig; + +public interface SongpackZip { + boolean isEmbedded(); + Path getPath(); + String getErrorString(); + void setErrorString(String s); + List getEntries(); + + String getName(); + String getAuthor(); + RMSongpackConfig getConfig(); +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/commands/HelpCommandHandlers.java b/src/main/java/circuitlord/reactivemusic/commands/HelpCommandHandlers.java new file mode 100644 index 0000000..55593d6 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/commands/HelpCommandHandlers.java @@ -0,0 +1,78 @@ +package circuitlord.reactivemusic.commands; + +import com.mojang.brigadier.context.CommandContext; + +import circuitlord.reactivemusic.ReactiveMusicDebug.TextBuilder; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public class HelpCommandHandlers { + + public static class HelpBuilder extends TextBuilder { + + private String commandTree; + + public HelpBuilder(String commmandTree) { + this.commandTree = "/" + commmandTree; + } + + @Override + public HelpBuilder header(String text) { + root.append( + Text.literal("====== " + text + " ======\n\n") + .formatted(Formatting.GOLD, Formatting.BOLD) + ); + return this; + } + + public HelpBuilder helpline(String command, String description, Formatting valueColor) { + root.append(Text.literal(commandTree + " ")); + root.append(Text.literal(command + " -> ").formatted(Formatting.GREEN, Formatting.BOLD)); + root.append(Text.literal(description).formatted(Formatting.BOLD, Formatting.ITALIC, valueColor)); + root.append(Text.literal("\n")); + return this; + } + + } + + public static int songpackCommands(CommandContext ctx) { + HelpBuilder help = new HelpBuilder("songpack"); + + help.header("SONGPACK COMMANDS") + + .helpline("info", "Not implemented.", Formatting.RED) + .helpline("entry current", "Info for the current songpack entry.", Formatting.WHITE) + .helpline("entry list", "Lists all entries. Valid entries are highlighted.", Formatting.WHITE); + + ctx.getSource().sendFeedback(help.build()); + return 1; + } + + public static int playerCommands(CommandContext ctx) { + HelpBuilder help = new HelpBuilder("player"); + + help.header("AUDIO COMMANDS") + + .helpline("list", "Provides a list of all audio players.", Formatting.WHITE) + .helpline("info ", "Info for the specified player.", Formatting.WHITE) + .helpline("info ", "Info for the specified supplier.", Formatting.WHITE); + + ctx.getSource().sendFeedback(help.build()); + return 1; + } + + public static int pluginCommands(CommandContext ctx) { + HelpBuilder help = new HelpBuilder("plugin"); + + help.header("PLUGIN COMMANDS") + + .helpline("list", "Provides a list of all plugins.", Formatting.WHITE) + .helpline("enable ", "Not implemented.", Formatting.RED) + .helpline("disable ", "Not implemented.", Formatting.RED); + + ctx.getSource().sendFeedback(help.build()); + return 1; + } + +} diff --git a/src/main/java/circuitlord/reactivemusic/commands/PlayerCommandHandlers.java b/src/main/java/circuitlord/reactivemusic/commands/PlayerCommandHandlers.java new file mode 100644 index 0000000..1cd9bcd --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/commands/PlayerCommandHandlers.java @@ -0,0 +1,73 @@ +package circuitlord.reactivemusic.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; + +import circuitlord.reactivemusic.ReactiveMusicDebug.TextBuilder; +import circuitlord.reactivemusic.api.ReactiveMusicAPI; +import circuitlord.reactivemusic.api.audio.GainSupplier; +import circuitlord.reactivemusic.api.audio.ReactivePlayer; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.util.Formatting; + +public class PlayerCommandHandlers { + + public static int playerList(CommandContext ctx) { + TextBuilder playerList = new TextBuilder(); + + playerList.header("AUDIO PLAYERS"); + for (ReactivePlayer player : ReactiveMusicAPI.audioManager().getAll()) { + playerList.line(player.id(), Formatting.AQUA); + } + + ctx.getSource().sendFeedback(playerList.build()); + return 1; + } + + public static int playerInfo(CommandContext ctx) { + + + String id = StringArgumentType.getString(ctx, "namespace") + ":" + StringArgumentType.getString(ctx, "path"); + ReactivePlayer player = ReactiveMusicAPI.audioManager().get(id); + TextBuilder playerInfo = new TextBuilder(); + + playerInfo.header("AUDIO PLAYER INFO") + + .line("id", player.id(), Formatting.AQUA) + .line("isPlaying", player.isPlaying() ? "YES" : "NO", player.isPlaying() ? Formatting.GREEN : Formatting.GRAY) + .line("stopOnFadeOut", player.stopOnFadeOut() ? "YES" : "NO", player.stopOnFadeOut() ? Formatting.GREEN : Formatting.GRAY) + .line("resetOnFadeOut", player.resetOnFadeOut() ? "YES" : "NO", player.resetOnFadeOut() ? Formatting.GREEN : Formatting.GRAY) + .line("gainSuppliers", "", Formatting.WHITE); + + player.getGainSuppliers().forEach((supplierId, gainSupplier) -> { + playerInfo.line(" --> " + supplierId, Float.toString(gainSupplier.supplyComputedPercent()), gainSupplier.supplyComputedPercent() > 0 ? Formatting.LIGHT_PURPLE : Formatting.GRAY); + }); + + ctx.getSource().sendFeedback(playerInfo.build()); + return 1; + } + + public static int gainSupplierInfo(CommandContext ctx) { + + String id = StringArgumentType.getString(ctx, "namespace") + ":" + StringArgumentType.getString(ctx, "path"); + String gainSupplierId = StringArgumentType.getString(ctx, "gainSupplierId"); + TextBuilder supplierInfo = new TextBuilder(); + ReactivePlayer player = ReactiveMusicAPI.audioManager().get(id); + GainSupplier gainSupplier = player.getGainSuppliers().get(gainSupplierId); + + supplierInfo.header("GAIN SUPPLIER") + .line("player", id, Formatting.WHITE) + .line("id", gainSupplierId, Formatting.AQUA) + .newline() + .line("computedPercent", Float.toString(gainSupplier.supplyComputedPercent()), Formatting.LIGHT_PURPLE) + .line("fadeStart", Float.toString(gainSupplier.getFadeStart()), Formatting.AQUA) + .line("fadeTarget", Float.toString(gainSupplier.getFadeTarget()), Formatting.AQUA) + .line("fadeDuration", Integer.toString(gainSupplier.getFadeDuration()), Formatting.BLUE) + .line("isFadingOut", gainSupplier.isFadingOut() ? "YES" : "NO", gainSupplier.isFadingOut() ? Formatting.GREEN : Formatting.GRAY) + .line("isFadingIn", gainSupplier.isFadingIn() ? "YES" : "NO", gainSupplier.isFadingIn() ? Formatting.GREEN : Formatting.GRAY); + + ctx.getSource().sendFeedback(supplierInfo.build()); + return 1; + } + +} diff --git a/src/main/java/circuitlord/reactivemusic/commands/PluginCommandHandlers.java b/src/main/java/circuitlord/reactivemusic/commands/PluginCommandHandlers.java new file mode 100644 index 0000000..ad6dfcb --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/commands/PluginCommandHandlers.java @@ -0,0 +1,35 @@ +package circuitlord.reactivemusic.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; + +import circuitlord.reactivemusic.ReactiveMusic; +import circuitlord.reactivemusic.ReactiveMusicDebug; +import circuitlord.reactivemusic.ReactiveMusicDebug.TextBuilder; +import circuitlord.reactivemusic.api.ReactiveMusicPlugin; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.util.Formatting; + +public final class PluginCommandHandlers { + + public static int listPlugins(CommandContext ctx) { + TextBuilder pluginList = new TextBuilder(); + for (ReactiveMusicPlugin plugin : ReactiveMusic.PLUGINS) { + pluginList.line(plugin.pluginId.getId(), Formatting.AQUA); + } + ctx.getSource().sendFeedback(pluginList.build()); + return 1; + } + + public static int enablePlugin(CommandContext ctx) { + String pluginId = StringArgumentType.getString(ctx, "pluginId"); + ctx.getSource().sendFeedback(ReactiveMusicDebug.NON_IMPL_WARN_BUILT); + return 1; + } + + public static int disablePlugin(CommandContext ctx) { + String pluginId = StringArgumentType.getString(ctx, "pluginId"); + ctx.getSource().sendFeedback(ReactiveMusicDebug.NON_IMPL_WARN_BUILT); + return 1; + } +} diff --git a/src/main/java/circuitlord/reactivemusic/commands/SongpackCommandHandlers.java b/src/main/java/circuitlord/reactivemusic/commands/SongpackCommandHandlers.java new file mode 100644 index 0000000..fd3e902 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/commands/SongpackCommandHandlers.java @@ -0,0 +1,121 @@ +package circuitlord.reactivemusic.commands; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.context.CommandContext; + +import circuitlord.reactivemusic.ReactiveMusicDebug; +import circuitlord.reactivemusic.ReactiveMusicState; +import circuitlord.reactivemusic.ReactiveMusicDebug.TextBuilder; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.util.Formatting; + +public class SongpackCommandHandlers { + + public static int songpackInfo(CommandContext ctx) { + ctx.getSource().sendFeedback(ReactiveMusicDebug.NON_IMPL_WARN_BUILT); + return 1; + } + + + public static int listValidEntries(CommandContext ctx) { + int n = 0; + TextBuilder validEntryList = new TextBuilder(); + + validEntryList.header("VALID ENTRIES"); + for (RuntimeEntry entry : ReactiveMusicState.validEntries) { + validEntryList.line(Integer.toString(n), entry.getEventString(), Formatting.AQUA); + n += 1; + } + validEntryList.raw("\n"+"There are a total of [ " + ReactiveMusicState.validEntries.size() + " ] valid entries", Formatting.BOLD, Formatting.LIGHT_PURPLE); + + ctx.getSource().sendFeedback(validEntryList.build()); + return 1; + } + + public static int listAllEntries(CommandContext ctx) { + int n = 0; + TextBuilder entryList = new TextBuilder(); + + entryList.header("SONGPACK ENTRIES"); + for (RuntimeEntry entry : ReactiveMusicState.loadedEntries) { + + boolean isValid = ReactiveMusicState.validEntries.contains(entry); + boolean isCurrent = ReactiveMusicState.currentEntry == entry; + Formatting formatting = isValid ? isCurrent ? Formatting.GREEN : Formatting.AQUA : Formatting.GRAY; + String entryString = entry.getEventString().length() >= 32 ? entry.getEventString().substring(0, 32) + "..." : entry.getEventString(); + entryList.line(Integer.toString(n), entryString, formatting); + n += 1; + + } + entryList.line("There are a total of [ " + ReactiveMusicState.validEntries.size() + " ] valid entries", Formatting.BOLD, Formatting.LIGHT_PURPLE) + + .raw("Use ", Formatting.WHITE).raw("/songpack entry ", Formatting.YELLOW, Formatting.BOLD) + .raw(" to see the more details about that entry."); + + ctx.getSource().sendFeedback(entryList.build()); + return 1; + } + + public static int currentEntryInfo(CommandContext ctx) { + RuntimeEntry e = ReactiveMusicState.currentEntry; + TextBuilder info = new TextBuilder(); + + info.header("CURRENT ENTRY") + + .line("events", e.getEventString(), Formatting.WHITE) + .line("allowFallback ", e.fallbackAllowed() ? "YES" : "NO", e.fallbackAllowed() ? Formatting.GREEN : Formatting.GRAY) + .line("useOverlay", e.shouldOverlay() ? "YES" : "NO", e.shouldOverlay() ? Formatting.GREEN : Formatting.GRAY ) + .line("forceStopMusicOnValid", e.shouldStopMusicOnValid() ? "YES" : "NO", e.shouldStopMusicOnValid() ? Formatting.GREEN : Formatting.GRAY) + .line("forceStopMusicOnInvalid", e.shouldStopMusicOnInvalid() ? "YES" : "NO", e.shouldStopMusicOnInvalid() ? Formatting.GREEN : Formatting.GRAY) + .line("forceStartMusicOnValid", e.shouldStartMusicOnValid() ? "YES" : "NO", e.shouldStartMusicOnValid() ? Formatting.GREEN : Formatting.GRAY) + .line("forceChance", Float.toString(e.getForceChance()), e.getForceChance() != 0 ? Formatting.AQUA : Formatting.GRAY) + .line("\n"+"Now playing:", ReactiveMusicState.currentSong, Formatting.ITALIC); + + ctx.getSource().sendFeedback(info.build()); + return 1; + } + + public static int indexedEntryInfo(CommandContext ctx) { + + int index = IntegerArgumentType.getInteger(ctx, "index"); + + RuntimeEntry e = ReactiveMusicState.loadedEntries.get(index); + TextBuilder info = new TextBuilder(); + + String indexAsString = Integer.toString(index); + info.header("ENTRY #" + indexAsString) + + .line("events", e.getEventString(), Formatting.WHITE) + .line("allowFallback ", e.fallbackAllowed() ? "YES" : "NO", e.fallbackAllowed() ? Formatting.GREEN : Formatting.GRAY) + .line("useOverlay", e.shouldOverlay() ? "YES" : "NO", e.shouldOverlay() ? Formatting.GREEN : Formatting.GRAY ) + .line("forceStopMusicOnValid", e.shouldStopMusicOnValid() ? "YES" : "NO", e.shouldStopMusicOnValid() ? Formatting.GREEN : Formatting.GRAY) + .line("forceStopMusicOnInvalid", e.shouldStopMusicOnInvalid() ? "YES" : "NO", e.shouldStopMusicOnInvalid() ? Formatting.GREEN : Formatting.GRAY) + .line("forceStartMusicOnValid", e.shouldStartMusicOnValid() ? "YES" : "NO", e.shouldStartMusicOnValid() ? Formatting.GREEN : Formatting.GRAY) + .line("forceChance", Float.toString(e.getForceChance()), e.getForceChance() != 0 ? Formatting.AQUA : Formatting.GRAY) + .line("songs", Integer.toString(e.getSongs().size()), Formatting.LIGHT_PURPLE); + + ctx.getSource().sendFeedback(info.build()); + return 1; + } + + public static int indexedEntrySongs(CommandContext ctx) { + + int index = IntegerArgumentType.getInteger(ctx, "index"); + String indexAsString = Integer.toString(index); + int n = 0; + + RuntimeEntry e = ReactiveMusicState.loadedEntries.get(index); + TextBuilder info = new TextBuilder(); + + info.header("ENTRY #" + indexAsString + " SONGS"); + + for (String songId : e.getSongs()) { + info.line(Integer.toString(n), songId, Formatting.WHITE); + n += 1; + } + + ctx.getSource().sendFeedback(info.build()); + return 1; + } +} diff --git a/src/main/java/circuitlord/reactivemusic/compat/modmenu/ModMenuIntegration.java b/src/main/java/circuitlord/reactivemusic/compat/modmenu/ModMenuIntegration.java index fdfca9b..5d6e08d 100644 --- a/src/main/java/circuitlord/reactivemusic/compat/modmenu/ModMenuIntegration.java +++ b/src/main/java/circuitlord/reactivemusic/compat/modmenu/ModMenuIntegration.java @@ -1,10 +1,9 @@ package circuitlord.reactivemusic.compat.modmenu; -//import circuitlord.reactivemusic.RMConfigScreen; import circuitlord.reactivemusic.compat.CompatUtils; import circuitlord.reactivemusic.config.ModConfig; -import circuitlord.reactivemusic.config.YAConfig; + import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import net.fabricmc.api.EnvType; diff --git a/src/main/java/circuitlord/reactivemusic/config/ModConfig.java b/src/main/java/circuitlord/reactivemusic/config/ModConfig.java index 8451cee..9c6b77c 100644 --- a/src/main/java/circuitlord/reactivemusic/config/ModConfig.java +++ b/src/main/java/circuitlord/reactivemusic/config/ModConfig.java @@ -1,10 +1,10 @@ package circuitlord.reactivemusic.config; -import circuitlord.reactivemusic.RMSongpackLoader; -import circuitlord.reactivemusic.ReactiveMusic; -//import circuitlord.reactivemusic.SongLoader; -import circuitlord.reactivemusic.SongpackZip; +import circuitlord.reactivemusic.ReactiveMusicCore; +import circuitlord.reactivemusic.ReactiveMusicState; +import circuitlord.reactivemusic.impl.songpack.RMSongpackLoader; +import circuitlord.reactivemusic.impl.songpack.RMSongpackZip; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.*; import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; @@ -120,9 +120,9 @@ public static Screen createScreen(Screen parent) { boolean isLoaded = false; - if (ReactiveMusic.currentSongpack != null) { + if (ReactiveMusicState.currentSongpack != null) { - isLoaded = Objects.equals(ReactiveMusic.currentSongpack.config.name, songpackZip.config.name); + isLoaded = Objects.equals(ReactiveMusicState.currentSongpack.getConfig().name, songpackZip.config.name); } if (songpackZip.blockLoading) { @@ -284,7 +284,7 @@ public static Screen createScreen(Screen parent) { - public static void setActiveSongpack(SongpackZip songpack) { + public static void setActiveSongpack(RMSongpackZip songpack) { if (songpack.embedded) { getConfig().loadedUserSongpack = ""; @@ -295,7 +295,7 @@ public static void setActiveSongpack(SongpackZip songpack) { GSON.save(); - ReactiveMusic.setActiveSongpack(songpack); + ReactiveMusicCore.setActiveSongpack(songpack); } diff --git a/src/main/java/circuitlord/reactivemusic/config/YAConfig.java b/src/main/java/circuitlord/reactivemusic/config/YAConfig.java index 982e788..1cb49d1 100644 --- a/src/main/java/circuitlord/reactivemusic/config/YAConfig.java +++ b/src/main/java/circuitlord/reactivemusic/config/YAConfig.java @@ -1,10 +1,7 @@ package circuitlord.reactivemusic.config; -import com.google.gson.GsonBuilder; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder; -import dev.isxander.yacl3.config.GsonConfigInstance; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.gui.screen.Screen; import net.minecraft.text.Text; diff --git a/src/main/java/circuitlord/reactivemusic/impl/audio/RMGainSupplier.java b/src/main/java/circuitlord/reactivemusic/impl/audio/RMGainSupplier.java new file mode 100644 index 0000000..0fa64f5 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/audio/RMGainSupplier.java @@ -0,0 +1,56 @@ +package circuitlord.reactivemusic.impl.audio; + +import circuitlord.reactivemusic.api.audio.GainSupplier; + +public class RMGainSupplier implements GainSupplier { + + //defaults + private volatile float gainPercent = 1f; + private volatile float fadePercent = 1f; + private volatile float fadeTarget = 1f; + private volatile float fadeStart = -1; // set on fade calls + private volatile int fadeDuration = 60; + + public RMGainSupplier(float initialPercent) { + this.gainPercent = initialPercent; + } + + public float supplyComputedPercent() { return gainPercent * fadePercent;} + + // gain % + public float getGainPercent() { return gainPercent; } + public void setGainPercent(float p) { gainPercent = p; } + + // fade % + public void setFadePercent(float p) { fadePercent = p; } + public float getFadePercent() { return fadePercent; } + + // fade target + public void setFadeTarget(float p) { + if (fadeTarget != p) { + fadeStart = fadePercent; // where were we? + } + fadeTarget = p; + } + public float getFadeTarget() { return fadeTarget; } + + // fade start % + public float getFadeStart() { return fadeStart; } + public void clearFadeStart() { fadeStart = -1; } + + // fade duration + public void setFadeDuration(int tickDuration) {} + public int getFadeDuration() { return fadeDuration; } + + //flags + public boolean isFadingOut() { return (fadeTarget == 0 && fadeStart > 0); } + public boolean isFadingIn() { return (fadeStart == 0 && fadeTarget > 0); } + + /** + * Wrapper for hooked functions from RMPlayerManager + * @see RMPlayerManager#tick() + */ + public void tick() { + + } +} diff --git a/src/main/java/circuitlord/reactivemusic/impl/audio/RMPlayer.java b/src/main/java/circuitlord/reactivemusic/impl/audio/RMPlayer.java new file mode 100644 index 0000000..36bbeac --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/audio/RMPlayer.java @@ -0,0 +1,562 @@ +package circuitlord.reactivemusic.impl.audio; + +import circuitlord.reactivemusic.ReactiveMusicDebug; +import circuitlord.reactivemusic.ReactiveMusicState; +import circuitlord.reactivemusic.api.audio.GainSupplier; +import circuitlord.reactivemusic.api.audio.ReactivePlayer; +import circuitlord.reactivemusic.api.audio.ReactivePlayerOptions; +import circuitlord.reactivemusic.impl.songpack.MusicPackResource; +import circuitlord.reactivemusic.impl.songpack.RMSongpackLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +import rm_javazoom.jl.player.advanced.AdvancedPlayer; +import rm_javazoom.jl.player.AudioDevice; +import rm_javazoom.jl.player.JavaSoundAudioDevice; + +import java.io.Closeable; +import java.io.InputStream; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RMPlayer implements ReactivePlayer, Closeable { + + public static final String MOD_ID = "reactive_music"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + // ----- identity / grouping ----- + private final String id; + private final String namespace; + private volatile String group; + + // ----- options / state ----- + private final GainSupplier primaryGainSupplier; + private final boolean linkToMcVolumes; + private final boolean quietWhenPaused; + private volatile boolean loop; + private volatile boolean mute; + + private volatile ConcurrentHashMap gainSuppliers = new ConcurrentHashMap<>(); + private final Supplier groupDuckSupplier; // from manager: returns 1.0f unless group ducked + private volatile boolean stopOnFadeOut = true; + private volatile boolean resetOnFadeOut = true; + + // ----- source ----- + private volatile String songId; // resolved via songpack (e.g., "music/Foo") + private volatile Supplier streamSupplier; // optional direct supplier + private volatile String fileId; + private MusicPackResource currentResource; + + // ----- thread & playback ----- + private volatile boolean kill; // thread exit + private volatile boolean queued; // new source queued + private volatile boolean queuedToStop; // stop request + private volatile boolean paused; // soft pause flag + private volatile boolean playing; // simplified “is playing” + private volatile boolean complete; // set by AdvancedPlayer when finished + private volatile float realGainDb; // last applied dB + + private AdvancedPlayer player; // JavaZoom player + private AudioDevice audio; // audio device for gain control + private Thread worker; // daemon worker thread + + // callbacks + private final CopyOnWriteArrayList completeHandlers = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList> errorHandlers = new CopyOnWriteArrayList<>(); + + // constants (match your thread’s range) + private static final float MIN_POSSIBLE_GAIN = -80f; + private static final float MIN_GAIN = -50f; + private static final float MAX_GAIN = 0f; + + // This is included just in case we need it down the road somewhere + @SuppressWarnings("unused") + private static String normalizeSongFileName(String logicalId) { + if (logicalId == null || logicalId.isBlank()) return null; + String name = logicalId.replace('\\','/'); // windows-safe + if (!name.contains("/")) name = "music/" + name; + if (!name.endsWith(".mp3")) name = name + ".mp3"; + return name; + } + + public RMPlayer(String id, ReactivePlayerOptions opts, Supplier groupDuckSupplier) { + this.id = Objects.requireNonNull(id); + this.namespace = opts.pluginNamespace() != null ? opts.pluginNamespace() : "core"; + this.group = opts.group() != null ? opts.group() : "music"; + this.linkToMcVolumes = opts.linkToMinecraftVolumes(); + this.quietWhenPaused = opts.quietWhenGamePaused(); + this.loop = opts.loop(); + + primaryGainSupplier = gainSuppliers.computeIfAbsent("reactivemusic", (k) -> { + return new RMGainSupplier(opts.initialGainPercent()); + }); + + gainSuppliers.put("reactivemusic-duck", new RMGainSupplier(opts.initialDuckPercent())); + + this.groupDuckSupplier = groupDuckSupplier != null ? groupDuckSupplier : () -> 1.0f; + + this.worker = new Thread(this::runLoop, "ReactiveMusic Player [" + id + "]"); + this.worker.setDaemon(true); + this.worker.start(); + + if (opts.autostart() && (songId != null || streamSupplier != null)) { + queued = true; + } + } + + //package helper - links to RMPlayerManagerImpl + String getNamespace() { + return this.namespace; + } + + /** Nudge the player to recompute its effective gain immediately. */ + void recomputeGainNow() { + requestGainRecompute(); + } + + // ===== RMPlayer ===== + + @Override public String id() { return id; } + + @Override public boolean isPlaying() { return playing && !complete; } + + // @Override public boolean isPaused() { return paused; } + + @Override public boolean isFinished() { return complete && !playing; } + + @Override public void setSong(String songId) { + this.songId = songId; + this.fileId = null; + this.streamSupplier = null; + } + + @Override public void setStream(Supplier stream) { + this.songId = null; + this.fileId = null; + this.streamSupplier = stream; + } + + @Override public void setFile(String fileName) { + this.songId = null; + this.fileId = fileName; + this.streamSupplier = null; + } + + @Override public void play() { + // restart from beginning of current source + queueStart(); + } + + @Override public void stop() { + LOGGER.info("Stopping player..."); + if(player != null) { + player.close(); + queuedToStop = true; + complete = true; + queued = false; + } + if (currentResource != null && currentResource.fileSystem != null) { + try { + currentResource.close(); + LOGGER.info("Resource closed!"); + } catch (Exception e) { + LOGGER.error("Failed to close file system/input stream " + e.getMessage()); + } + } + currentResource = null; + } + + /** Uses the primary gain supplier. */ + @Override public void fade(float target, int tickDuration) { + primaryGainSupplier.setFadeTarget(target); + primaryGainSupplier.setFadeDuration(tickDuration); + } + + @Override public void fade(String gainSupplierId, float target, int tickDuration) { + GainSupplier gainSupplier = getGainSuppliers().get(gainSupplierId); + gainSupplier.setFadeTarget(target); + gainSupplier.setFadeDuration(tickDuration); + } + + @Override public ConcurrentHashMap getGainSuppliers() { return gainSuppliers; } + + // XXX + // I know this next pattern isn't idiomatic... but this feels like it's going to get bloated otherwise + + // getters + @Override public boolean stopOnFadeOut() { return stopOnFadeOut; } + @Override public boolean resetOnFadeOut() { return resetOnFadeOut; } + + // setters + @Override public void stopOnFadeOut(boolean set) { stopOnFadeOut = set; } + @Override public void resetOnFadeOut(boolean set) { resetOnFadeOut = set; } + + + + + @Override public boolean isIdle() { + // Idle when we have no active/queued playback work + return !playing && !queued; + } + + // TODO: Figure out how to implement pausing. + // @Override public void pause() { paused = true; } + // @Override public void resume() { paused = false; } + + + @Override public void reset() { + primaryGainSupplier.setFadePercent(1f); + primaryGainSupplier.setFadeTarget(1f); + requestGainRecompute(); + } + + @Override public void setMute(boolean v) { mute = v; requestGainRecompute(); } + + @Override public float getRealGainDb() { return realGainDb; } + + @Override public void setGroup(String group) { this.group = group; requestGainRecompute(); } + @Override public String getGroup() { return group; } + + @Override public void onComplete(Runnable r) { if (r != null) completeHandlers.add(r); } + @Override public void onError(Consumer c) { if (c != null) errorHandlers.add(c); } + + @Override public void close() { + stop(); + kill = true; + if (worker != null) worker.interrupt(); + closeQuiet(player); + player = null; + audio = null; + } + + // ===== internal ===== + + private void queueStart() { + this.queuedToStop = false; + this.complete = false; + this.queued = true; // worker will open & play + this.paused = false; + } + + private void runLoop() { + while (!kill) { + try { + if (queued) { + InputStream in = null; + try { + if (streamSupplier != null) { + in = streamSupplier.get(); + currentResource = null; // external stream, nothing to close here + } else if (fileId != null) { + LOGGER.info(this.id + " -> playing from custom resource: " + fileId); + currentResource = openFromFile(fileId); // use a custom file found in the songpack + if (currentResource == null || currentResource.inputStream == null) { + queued = false; + continue; + } + } else { + currentResource = openFromSongpack(songId); + if (currentResource == null || currentResource.inputStream == null) { + queued = false; + continue; + } + in = currentResource.inputStream; // like your original PlayerThread + } + + ReactiveMusicDebug.LOGGER.info("A new audio device is activating..."); + audio = new FirstWritePrimerAudioDevice(250, () -> requestGainRecompute()); + player = new AdvancedPlayer(in, audio); + + + queued = false; + playing = true; + complete = false; + + + if (player.getAudioDevice() != null && !queuedToStop) { + player.play(); + } + } finally { + // Cleanup player & audio + LOGGER.info("[runLoop]: Closing player: " + this.namespace + ":" + this.group); + closeQuiet(player); + player = null; + audio = null; + playing = false; + complete = true; + + // Close MusicPackResource like your old resetPlayer() did + if (currentResource != null) { + try { currentResource.close(); } catch (Exception ignored) {} + currentResource = null; + } + } + + if (complete && !queuedToStop) { + completeHandlers.forEach(RMPlayer::safeRun); + if (loop && !kill) queued = true; + } + queuedToStop = false; + } + + Thread.sleep(5); + } catch (Throwable t) { + for (Consumer c : errorHandlers) safeRun(() -> c.accept(t)); + // reset on error + closeQuiet(player); + player = null; + audio = null; + playing = false; + queuedToStop = false; + queued = false; + complete = true; + } + } + } + + private static void closeQuiet(AdvancedPlayer p) { + try { if (p != null) p.close(); } catch (Throwable ignored) {} + } + + private static void safeRun(Runnable r) { + try { r.run(); } catch (Throwable ignored) {} + } + + private MusicPackResource openFromSongpack(String logicalId) { + if (logicalId == null) return null; + + // Accept "Foo", "music/Foo", or full "music/Foo.mp3" + String fileName; + if (logicalId.endsWith(".mp3")) { + fileName = logicalId; + } else if (logicalId.startsWith("music/")) { + fileName = logicalId + ".mp3"; + } else { + fileName = "music/" + logicalId + ".mp3"; + } + + LOGGER.info("[openFromSongpack]:" + fileName); + + return RMSongpackLoader.getInputStream( + ReactiveMusicState.currentSongpack.getPath(), + fileName, + ReactiveMusicState.currentSongpack.isEmbedded() + ); // loader returns MusicPackResource{ inputStream, fileSystem? }. + } + + private MusicPackResource openFromFile(String fileId) { + String fileName; + if (fileId == null) return null; + if (fileId.endsWith(".mp3")) { + fileName = fileId; + } else { + fileName = fileId + ".mp3"; + } + + LOGGER.info("[openFromFile]: " + fileName); + + return RMSongpackLoader.getInputStream( + ReactiveMusicState.currentSongpack.getPath(), + fileName, + ReactiveMusicState.currentSongpack.isEmbedded() + ); + } + + public float requestGainRecompute() { + if (audio == null) return 0f; + float minecraftGain = 1.0f; + + if (linkToMcVolumes) { + // MASTER * MUSIC from Options (same source you used previously) + minecraftGain = getMasterMusicProduct(); // extract from GameOptions + // your “less drastic” curve (same intent as your code) + minecraftGain = (float)Math.pow(minecraftGain, 0.85); + } + + float quietPct = 1f; + if (quietWhenPaused && isInGamePausedAndNotOnSoundScreen()) { + // you targeted ~70% with a gentle lerp; we keep the target value here + quietPct = 0.7f; + } + + float suppliedPercent = gainSuppliers.reduce(1L, (supplierId, supplier) -> supplier.supplyComputedPercent(), (a,b) -> a * b); + float effective = mute ? 0f : (suppliedPercent * groupDuckSupplier.get() * quietPct * minecraftGain); + float db = (minecraftGain == 0f || effective == 0f) + ? MIN_POSSIBLE_GAIN + : (MIN_GAIN + (MAX_GAIN - MIN_GAIN) * clamp01(effective)); + + // LOGGER.info(String.format( + // "RM gain: mute=%s gain=%.2f duck=%.2f group=%.2f quiet=%.2f mc=%.2f -> dB=%.1f", + // mute, gainPercent, duckPercent, groupDuckSupplier.get(), + // quietPct, minecraftGain, db + // )); + + try { + ((JavaSoundAudioDevice) audio).setGain(db); + realGainDb = db; + } catch (Throwable ignored) {} + + return db; + } + + private static float clamp01(float f) { return f < 0 ? 0 : Math.min(f, 1); } + + // ==== helpers copied from your thread’s logic ==== + + private static boolean isInGamePausedAndNotOnSoundScreen() { + MinecraftClient mc = MinecraftClient.getInstance(); + if (mc == null) return false; + Screen s = mc.currentScreen; + if (s == null) return false; + // You previously compared the translated title to "options.sounds.title" to avoid quieting on that screen + Text t = s.getTitle(); + if (t == null) return true; + String lower = t.getString().toLowerCase(); + // crude but effective: don’t “quiet” while on the sound options screen + boolean onSoundScreen = lower.contains("sound"); // adapt if you kept the exact key match + return !onSoundScreen; + } + + private static float getMasterMusicProduct() { + MinecraftClient mc = MinecraftClient.getInstance(); + if (mc == null || mc.options == null) return 1f; + // Replace with exact getters from 1.21.1 GameOptions + float master = (float) mc.options.getSoundVolume(net.minecraft.sound.SoundCategory.MASTER); + float music = (float) mc.options.getSoundVolume(net.minecraft.sound.SoundCategory.MUSIC); + return master * music; + } + + + + /** + * XXX + * Full disclosure I have no f***ing idea how this next part works, but it fixes the bug where the audio was + * blasting for the first bit since gain wasn't getting set before the audio device recieved samples, + * especially when running a lot of mods. + * + * Thanks, AI. + */ + + // -------- DROP-IN: put this inside RMPlayerImpl -------- + private final class FirstWritePrimerAudioDevice extends rm_javazoom.jl.player.JavaSoundAudioDevice { + private final int primeMs; + private final java.util.function.Supplier initialDbSupplier; + + private volatile boolean opened = false; + private volatile boolean primed = false; + private volatile boolean hwGainApplied = false; + + private javax.sound.sampled.AudioFormat fmt; + + // software gain fallback + private boolean swGainEnabled = false; + private float swGainScalar = 1.0f; // multiply samples by this if enabled + + FirstWritePrimerAudioDevice(int primeMs, java.util.function.Supplier initialDbSupplier) { + this.primeMs = Math.max(0, primeMs); + this.initialDbSupplier = initialDbSupplier; + } + + @Override + public void open(javax.sound.sampled.AudioFormat format) + throws rm_javazoom.jl.decoder.JavaLayerException { + super.open(format); + this.fmt = format; + this.opened = true; + System.err.println("[RMPlayer] open(): fmt=" + format + ", primeMs=" + primeMs); + + // Try to apply initial HW gain now that the line exists + applyInitialGainOrEnableSoftwareFallback(); + } + + @Override + public void write(short[] samples, int offs, int len) + throws rm_javazoom.jl.decoder.JavaLayerException { + // If mixer didn't call open(AudioFormat) before first write (some forks do this), + // do best-effort: synthesize a sensible format just for primer sizing. + if (!opened && fmt == null) { + fmt = new javax.sound.sampled.AudioFormat(44100f, 16, 2, true, false); + } + + // Inject primer BEFORE forwarding the very first audible samples. + if (!primed && primeMs > 0) { + primed = true; + int channels = fmt != null ? Math.max(1, fmt.getChannels()) : 2; + float rate = fmt != null ? Math.max(8000f, fmt.getSampleRate()) : 44100f; + int totalSamples = Math.max(channels, Math.round((primeMs / 1000f) * rate) * channels); + + final int CHUNK = 4096; + short[] zeros = new short[Math.min(totalSamples, CHUNK)]; + int remain = totalSamples; + System.err.println("[RMPlayer] primer: injecting " + primeMs + "ms silence (" + totalSamples + " samples)"); + while (remain > 0) { + int n = Math.min(remain, zeros.length); + super.write(zeros, 0, n); + remain -= n; + } + + // If we somehow reached here before open(), try gain now as well. + if (!hwGainApplied) { + applyInitialGainOrEnableSoftwareFallback(); + } + } + + if (len <= 0) return; + + if (swGainEnabled) { + // Software-attenuate the buffer on the way out (don’t mutate caller’s array) + short[] tmp = new short[len]; + for (int i = 0; i < len; i++) { + float v = samples[offs + i] * swGainScalar; + // clamp to 16-bit + if (v > 32767f) v = 32767f; + if (v < -32768f) v = -32768f; + tmp[i] = (short) v; + } + super.write(tmp, 0, len); + } else { + super.write(samples, offs, len); + } + } + + private void applyInitialGainOrEnableSoftwareFallback() { + if (hwGainApplied) return; + Float db = null; + try { + db = (initialDbSupplier != null) ? initialDbSupplier.get() : null; + if (db != null) { + // Try hardware gain + this.setGain(db); + hwGainApplied = true; + swGainEnabled = false; // no need for SW gain + // reflect to outer field to keep your UI/state in sync + try { RMPlayer.this.realGainDb = db; } catch (Throwable ignored) {} + System.err.println("[RMPlayer] HW gain applied: " + db + " dB"); + return; + } + } catch (Throwable t) { + // Hardware control missing or mixer refused it. Fall back to SW. + System.err.println("[RMPlayer] HW gain failed, enabling SW gain. Reason: " + t); + } + + // If we get here, enable SW attenuation only if we actually need attenuation + // (db < 0). If db is null or >= 0, we don’t attenuate in software. + if (db != null && db < 0f) { + swGainEnabled = true; + swGainScalar = (float) Math.pow(10.0, db / 20.0); // dB -> linear + System.err.println("[RMPlayer] SW gain enabled: " + db + " dB (scalar=" + swGainScalar + ")"); + } else { + swGainEnabled = false; + } + } + } + // -------- END DROP-IN -------- +} + diff --git a/src/main/java/circuitlord/reactivemusic/impl/audio/RMPlayerManager.java b/src/main/java/circuitlord/reactivemusic/impl/audio/RMPlayerManager.java new file mode 100644 index 0000000..a1e68ca --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/audio/RMPlayerManager.java @@ -0,0 +1,128 @@ +package circuitlord.reactivemusic.impl.audio; + +import circuitlord.reactivemusic.ReactiveMusicState; +import circuitlord.reactivemusic.api.audio.GainSupplier; +import circuitlord.reactivemusic.api.audio.ReactivePlayer; +import circuitlord.reactivemusic.api.audio.ReactivePlayerManager; +import circuitlord.reactivemusic.api.audio.ReactivePlayerOptions; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RMPlayerManager implements ReactivePlayerManager { + public static final Logger LOGGER = LoggerFactory.getLogger("reactive_music"); + + private static final RMPlayerManager INSTANCE = new RMPlayerManager(); + public static ReactivePlayerManager get() { return INSTANCE; } + + private final Map players = new ConcurrentHashMap<>(); + private final Map groupDuck = new ConcurrentHashMap<>(); + + private RMPlayerManager() {} + + @Override + public ReactivePlayer create(String id, ReactivePlayerOptions opts) { + if (players.containsKey(id)) throw new IllegalArgumentException("Player id exists: " + id); + RMPlayer p = new RMPlayer(id, opts, () -> groupDuck.getOrDefault(opts.group(), 1f)); + players.put(id, p); + if (opts.autostart()) p.play(); + return p; + } + + @Override public ReactivePlayer get(String id) { return players.get(id); } + + /** + * Includes an API hook for custom runnable code imported through a wrapper within the RMGainSupplier class. + * TODO: Implementation of API hook and wrapper + * + */ + @Override public void tick() { + for (ReactivePlayer player : players.values()) { + + // check if the primary gain supplier has stopped + GainSupplier primaryGainSupplier = player.getGainSuppliers().get("reactivemusic"); + if (primaryGainSupplier.isFadingOut() && primaryGainSupplier.getFadePercent() == 0f) { + + LOGGER.info(player.id() + " has stopped on fadeout"); + + // reached target – run arrival side effects + if (player.stopOnFadeOut()) player.stop(); + if (player.resetOnFadeOut()) player.reset(); + } + + // compute tick fading for suppliers in the player's map + player.getGainSuppliers().forEach((id, gainSupplier) -> { + + float fp = gainSupplier.getFadePercent(); + float ft = gainSupplier.getFadeTarget(); + float dur = gainSupplier.getFadeDuration(); + + // compute next value + float step = (ft > fp ? 1f : -1f) * (1f / dur); + float next = fp == ft ? fp : fp + step; + + // clamp overshoot and bounds + if ((step > 0 && next >= ft) || (step < 0 && next <= ft)) next = ft; + if (next < 0f) next = 0f; else if (next > 1f) next = 1f; + + gainSupplier.setFadePercent(next); + if (fp != ft) { + if (fp == 0 && step > 0) { + ReactiveMusicState.LOGGER.info(player.id() + " is fading in via gain supplier [" + id + "]"); + } + + if (fp == 1 && step < 0) { + ReactiveMusicState.LOGGER.info(player.id() + " is fading out via gain supplier [" + id + "]"); + } + } + }); + + player.requestGainRecompute(); + } + } + + @Override public Collection getAll() { + return Collections.unmodifiableCollection(players.values()); + } + + @Override public Collection getByGroup(String group) { + return players.values().stream() + .filter(p -> group.equals(p.getGroup())) + .map(p -> (ReactivePlayer) p) + .collect(Collectors.toList()); // use .toList() if you're on Java 16+ / 21 + } + + @Override public void setGroupDuck(String group, float percent) { + groupDuck.put(group, clamp01(percent)); + players.values().forEach(p -> { + if (group.equals(p.getGroup())) p.requestGainRecompute(); // make requestGainRecompute() package-private in RMPlayerImpl + }); + } + + @Override public float getGroupDuck(String group) { + return groupDuck.getOrDefault(group, 1f); + } + + @Override public void closeAllForPlugin(String namespace) { + players.values().removeIf(p -> { + boolean match = namespace.equals(p.getNamespace()); // add getNamespace() to RMPlayerImpl + if (match) p.close(); + return match; + }); + } + + @Override public void closeAll() { + players.values().forEach(ReactivePlayer::close); + players.clear(); + groupDuck.clear(); + } + + private static float clamp01(float f){ return f < 0 ? 0 : Math.min(f, 1); } +} + diff --git a/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMEventRecord.java b/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMEventRecord.java new file mode 100644 index 0000000..378164f --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMEventRecord.java @@ -0,0 +1,18 @@ +package circuitlord.reactivemusic.impl.eventsys; + +import circuitlord.reactivemusic.api.eventsys.EventRecord; + +public class RMEventRecord implements EventRecord { + + private String eventId; + private RMPluginIdentifier pluginId; + + public RMEventRecord(String eventId, RMPluginIdentifier pluginId) { + this.eventId = eventId; + this.pluginId = pluginId; + } + + public String getEventId() { return eventId; } + public RMPluginIdentifier getPluginId() { return pluginId; } + + } diff --git a/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMEventState.java b/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMEventState.java new file mode 100644 index 0000000..9d0e6be --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMEventState.java @@ -0,0 +1,32 @@ +package circuitlord.reactivemusic.impl.eventsys; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import net.minecraft.entity.player.PlayerEntity; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + + +/** Internal cache of last-known conditions per player. + * This is not implemented into any feature as of yet. + * It is here for possible server-side functionality using + * a bridging plugin for client-server communication. + */ +public final class RMEventState { + private RMEventState() {} + private static final Map> LAST = new ConcurrentHashMap<>(); + + public static void updateForPlayer(PlayerEntity player, Map conditions) { + if (player == null || conditions == null) return; + LAST.put(player.getUuid(), Collections.unmodifiableMap(new HashMap<>(conditions))); + } + + public static Map snapshot(UUID playerId) { + Map m = LAST.get(playerId); + return (m != null) ? m : Collections.emptyMap(); + } + + public static void clear(UUID playerId) { LAST.remove(playerId); } + public static void clearAll() { LAST.clear(); } +} diff --git a/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMPluginIdentifier.java b/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMPluginIdentifier.java new file mode 100644 index 0000000..22decff --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/eventsys/RMPluginIdentifier.java @@ -0,0 +1,27 @@ +package circuitlord.reactivemusic.impl.eventsys; + +import circuitlord.reactivemusic.api.eventsys.PluginIdentifier; + +/** + * Identifier class for the plugin registry. + * Will be set as the value within the registry's map. + */ +public class RMPluginIdentifier implements PluginIdentifier{ + + private String title; + private String namespace; + private String path; + + public RMPluginIdentifier(String ns, String p) { + this.namespace = ns; + this.path = p; + } + + public String getNamespace() { return namespace; } + public String getPath() { return path; } + public String getId() { return namespace + ":" + path; } + + public void setTitle(String title) { this.title = title; } + public String getTitle() { return title == null ? getId() : title; } + +} diff --git a/src/main/java/circuitlord/reactivemusic/MusicPackResource.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/MusicPackResource.java similarity index 85% rename from src/main/java/circuitlord/reactivemusic/MusicPackResource.java rename to src/main/java/circuitlord/reactivemusic/impl/songpack/MusicPackResource.java index fa3c531..46b522f 100644 --- a/src/main/java/circuitlord/reactivemusic/MusicPackResource.java +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/MusicPackResource.java @@ -1,4 +1,4 @@ -package circuitlord.reactivemusic; +package circuitlord.reactivemusic.impl.songpack; import java.io.InputStream; import java.nio.file.FileSystem; diff --git a/src/main/java/circuitlord/reactivemusic/entries/RMEntryBlockCondition.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMEntryBlockCondition.java similarity index 68% rename from src/main/java/circuitlord/reactivemusic/entries/RMEntryBlockCondition.java rename to src/main/java/circuitlord/reactivemusic/impl/songpack/RMEntryBlockCondition.java index 79f1bab..e0cac83 100644 --- a/src/main/java/circuitlord/reactivemusic/entries/RMEntryBlockCondition.java +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMEntryBlockCondition.java @@ -1,4 +1,4 @@ -package circuitlord.reactivemusic.entries; +package circuitlord.reactivemusic.impl.songpack; public class RMEntryBlockCondition { diff --git a/src/main/java/circuitlord/reactivemusic/entries/RMEntryCondition.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMEntryCondition.java similarity index 52% rename from src/main/java/circuitlord/reactivemusic/entries/RMEntryCondition.java rename to src/main/java/circuitlord/reactivemusic/impl/songpack/RMEntryCondition.java index 0ec013c..5ee556e 100644 --- a/src/main/java/circuitlord/reactivemusic/entries/RMEntryCondition.java +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMEntryCondition.java @@ -1,24 +1,19 @@ -package circuitlord.reactivemusic.entries; +package circuitlord.reactivemusic.impl.songpack; -import circuitlord.reactivemusic.SongpackEventType; import net.minecraft.registry.tag.TagKey; import net.minecraft.world.biome.Biome; import java.util.ArrayList; import java.util.List; -public class RMEntryCondition { +import circuitlord.reactivemusic.api.eventsys.EventRecord; - // the way conditions work just means that each condition requires there to be at least one true in each list (or empty list) for the whole condition to be valid - // This is how we handle ORs +public class RMEntryCondition { - public List songpackEvents = new ArrayList<>(); + public List songpackEvents = new ArrayList<>(); public List biomeTypes = new ArrayList<>(); - public List dimTypes = new ArrayList<>(); - public List> biomeTags = new ArrayList<>(); - public List blocks = new ArrayList<>(); } diff --git a/src/main/java/circuitlord/reactivemusic/entries/RMRuntimeEntry.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMRuntimeEntry.java similarity index 61% rename from src/main/java/circuitlord/reactivemusic/entries/RMRuntimeEntry.java rename to src/main/java/circuitlord/reactivemusic/impl/songpack/RMRuntimeEntry.java index a749085..b93f04e 100644 --- a/src/main/java/circuitlord/reactivemusic/entries/RMRuntimeEntry.java +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMRuntimeEntry.java @@ -1,59 +1,99 @@ -package circuitlord.reactivemusic.entries; +package circuitlord.reactivemusic.impl.songpack; import circuitlord.reactivemusic.SongPicker; -import circuitlord.reactivemusic.SongpackEntry; -import circuitlord.reactivemusic.SongpackEventType; -import circuitlord.reactivemusic.SongpackZip; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Set; -public class RMRuntimeEntry { +public class RMRuntimeEntry implements RuntimeEntry { public List conditions = new ArrayList<>(); - + public String songpack; - + public boolean allowFallback = false; - + public boolean useOverlay = false; + public boolean forceStopMusicOnValid = false; public boolean forceStopMusicOnInvalid = false; - public boolean forceStartMusicOnValid = false; - public float forceChance = 1.0f; - + public List songs = new ArrayList<>(); - + public String eventString = ""; - public String errorString = ""; - - public float cachedRandomChance = 1.0f; - - - public static RMRuntimeEntry create(SongpackZip songpack, SongpackEntry songpackEntry) { - - RMRuntimeEntry Entry = new RMRuntimeEntry(); - Entry.songpack = songpack.config.name;// songpackName; - - Entry.allowFallback = songpackEntry.allowFallback; - - Entry.forceStopMusicOnValid = songpackEntry.forceStopMusicOnValid || songpackEntry.forceStopMusicOnChanged; - Entry.forceStopMusicOnInvalid = songpackEntry.forceStopMusicOnInvalid || songpackEntry.forceStopMusicOnChanged; - - Entry.forceStartMusicOnValid = songpackEntry.forceStartMusicOnValid; - - Entry.forceChance = songpackEntry.forceChance; + + public HashMap entryMap = new HashMap<>(); + + public Object getExternalOption(String key) { + return entryMap.get(key); + } + + // should import values in the yaml that are *NOT* predefined + // this means plugin devs can create custom options for events + // that live in the YAML + // + // TODO: Not implemented - need to figure out how to change + // the RMSongpackLoader to import the unknown keys with SnakeYAML + // + // TODO: Maybe the built-ins should just use this pattern as well? + public void setExternalOption(String key, Object value) { + Set knownOptions = Set.of( + "allowFallback", + "useOverlay", + "forceStopMusicOnValid", + "forceStopMusicOnInvalid", + "forceStartMusicOnValid", + "forceChance", + // don't load the songs or events into this map either + "songs", + "events" + ); + + entryMap.put(key, value); + entryMap.keySet().removeAll(knownOptions); + } + + // getters + //-------------------------------------------------------------- + public String getEventString() { return eventString; } + public String getErrorString() { return errorString; } + public List getSongs() { return songs; } + public boolean fallbackAllowed() { return allowFallback; } + public boolean shouldOverlay() { return useOverlay; } + public float getForceChance() { return forceChance; } + public boolean shouldStopMusicOnValid() { return forceStopMusicOnValid; } + public boolean shouldStopMusicOnInvalid() { return forceStopMusicOnInvalid; } + public boolean shouldStartMusicOnValid() { return forceStartMusicOnValid; } + public String getSongpack() { return songpack; } + public List getConditions() { return conditions; } + + public RMRuntimeEntry(RMSongpackZip songpack, RMSongpackEntry songpackEntry) { + + this.songpack = songpack.config.name;// songpackName; + + this.allowFallback = songpackEntry.allowFallback; + this.useOverlay = songpackEntry.useOverlay; + + this.forceStopMusicOnValid = songpackEntry.forceStopMusicOnValid || songpackEntry.forceStopMusicOnChanged; + this.forceStopMusicOnInvalid = songpackEntry.forceStopMusicOnInvalid || songpackEntry.forceStopMusicOnChanged; + this.forceStartMusicOnValid = songpackEntry.forceStartMusicOnValid; + this.forceChance = songpackEntry.forceChance; if (songpackEntry.songs != null) { - Entry.songs = Arrays.stream(songpackEntry.songs).toList(); + this.songs = Arrays.stream(songpackEntry.songs).toList(); } for (int i = 0; i < songpackEntry.events.length; i++) { - Entry.eventString += songpackEntry.events[i] + "_"; + this.eventString += songpackEntry.events[i] + "_"; } for (String event : songpackEntry.events) { @@ -93,7 +133,7 @@ public static RMRuntimeEntry create(SongpackZip songpack, SongpackEntry songpack eventHasData = true; } else { - Entry.errorString += "Invalid syntax: " + eventSection + "!\n\n"; + this.errorString += "Invalid syntax: " + eventSection + "!\n\n"; } } @@ -147,7 +187,7 @@ else if (eventSection.startsWith("biometag=")) { // we didn't find a match if (!foundMatch) { - Entry.errorString += "Didn't find biometag with name: " + rawTagString + "!\n\n"; + this.errorString += "Didn't find biometag with name: " + rawTagString + "!\n\n"; } } @@ -166,17 +206,16 @@ else if (eventSection.startsWith("dim=")) { else { try { // try to cast to SongpackEvent - // needs uppercase for enum names - SongpackEventType eventType = Enum.valueOf(SongpackEventType.class, eventSection.toUpperCase()); + EventRecord eventRecord = SongpackEvent.get(eventSection.toUpperCase()); // it's a songpack event - if (eventType != SongpackEventType.NONE) { - condition.songpackEvents.add(eventType); + if (eventRecord != SongpackEvent.NONE) { + condition.songpackEvents.add(eventRecord); eventHasData = true; continue; } } catch (Exception e) { - Entry.errorString += "Could not find event with name " + eventSection + "!\n\n"; + this.errorString += "Could not find event with name " + eventSection + "!\n\n"; //e.printStackTrace(); } } @@ -187,12 +226,9 @@ else if (eventSection.startsWith("dim=")) { continue; } - Entry.conditions.add(condition); + this.conditions.add(condition); } - - return Entry; - } diff --git a/src/main/java/circuitlord/reactivemusic/SongpackConfig.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackConfig.java similarity index 76% rename from src/main/java/circuitlord/reactivemusic/SongpackConfig.java rename to src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackConfig.java index 59333fe..4043faf 100644 --- a/src/main/java/circuitlord/reactivemusic/SongpackConfig.java +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackConfig.java @@ -1,11 +1,9 @@ -package circuitlord.reactivemusic; +package circuitlord.reactivemusic.impl.songpack; import circuitlord.reactivemusic.config.MusicDelayLength; import circuitlord.reactivemusic.config.MusicSwitchSpeed; -import java.nio.file.Path; - -public class SongpackConfig { +public class RMSongpackConfig { public String name; public String version = ""; @@ -18,7 +16,7 @@ public class SongpackConfig { public MusicSwitchSpeed musicSwitchSpeed = MusicSwitchSpeed.NORMAL; - public SongpackEntry[] entries; + public RMSongpackEntry[] entries; } diff --git a/src/main/java/circuitlord/reactivemusic/SongpackEntry.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackEntry.java similarity index 57% rename from src/main/java/circuitlord/reactivemusic/SongpackEntry.java rename to src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackEntry.java index 0178504..caca3ff 100644 --- a/src/main/java/circuitlord/reactivemusic/SongpackEntry.java +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackEntry.java @@ -1,41 +1,34 @@ -package circuitlord.reactivemusic; +package circuitlord.reactivemusic.impl.songpack; -import net.minecraft.loot.entry.TagEntry; -import net.minecraft.registry.tag.TagKey; -import net.minecraft.world.biome.Biome; - -import java.util.ArrayList; -import java.util.List; - -public class SongpackEntry { +import java.util.HashMap; +public class RMSongpackEntry { + public HashMap entryMap = new HashMap<>(); + + // BUILT-INS: + // These are kept class-based to improve developer experience + // when working on the Reactive Music base mod + //--------------------------------------------------------------------------------- // expands out into songpack events and biometag events public String[] events; - - + public boolean allowFallback = false; - - // OnChanged just sets both Valid and Invalid versions to true + public boolean useOverlay = false; + public boolean forceStopMusicOnChanged = false; public boolean forceStopMusicOnValid = false; public boolean forceStopMusicOnInvalid = false; - + public boolean forceStartMusicOnValid = false; - public float forceChance = 1.0f; - public boolean startMusicOnEventValid = false; - + // deprecated for now public boolean stackable = false; - public String[] songs; - - + // deprecated public boolean alwaysPlay = false; public boolean alwaysStop = false; - - } diff --git a/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackEvent.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackEvent.java new file mode 100644 index 0000000..4b024bb --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackEvent.java @@ -0,0 +1,44 @@ +package circuitlord.reactivemusic.impl.songpack; + +import java.util.*; + +import circuitlord.reactivemusic.ReactiveMusicDebug; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import circuitlord.reactivemusic.impl.eventsys.RMEventRecord; +import circuitlord.reactivemusic.impl.eventsys.RMPluginIdentifier; + +/** + * This is coupled to the API's matching interface. + * @see SongpackEvent + */ +public final class RMSongpackEvent implements SongpackEvent { + + private static final Map REGISTRY = new HashMap<>(); + + /** If for some reason we need to get the event map outside of where it is provided... */ + @Override public Map getMap() { return REGISTRY; } + + + public static RMEventRecord register(RMEventRecord eventRecord) { + ReactiveMusicDebug.LOGGER.info("Registering [" + eventRecord.getPluginId().getId() + "] event: " + eventRecord.getEventId()); + return REGISTRY.computeIfAbsent(eventRecord.getEventId(), k -> {return eventRecord;}); + } + + private static RMEventRecord builtIn(String eventId) { + RMPluginIdentifier pluginId = new RMPluginIdentifier("reactivemusic", "standard_events"); + RMEventRecord eventRecord = new RMEventRecord(eventId, pluginId); + return register(eventRecord); + } + + public static RMEventRecord[] values() { + return REGISTRY.values().toArray(new RMEventRecord[0]); + } + + public static RMEventRecord get(String id) { return REGISTRY.get(id); } + + + public static final RMEventRecord NONE = builtIn("NONE"); + public static final RMEventRecord MAIN_MENU = builtIn("MAIN_MENU"); + public static final RMEventRecord CREDITS = builtIn("CREDITS"); + public static final RMEventRecord GENERIC = builtIn("GENERIC"); +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/RMSongpackLoader.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackLoader.java similarity index 72% rename from src/main/java/circuitlord/reactivemusic/RMSongpackLoader.java rename to src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackLoader.java index 7d0b3e2..dc2894a 100644 --- a/src/main/java/circuitlord/reactivemusic/RMSongpackLoader.java +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackLoader.java @@ -1,6 +1,7 @@ -package circuitlord.reactivemusic; +package circuitlord.reactivemusic.impl.songpack; -import circuitlord.reactivemusic.entries.RMRuntimeEntry; +import circuitlord.reactivemusic.ReactiveMusicDebug; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; import net.fabricmc.loader.api.FabricLoader; import org.rm_yaml.snakeyaml.Yaml; import org.rm_yaml.snakeyaml.constructor.Constructor; @@ -12,7 +13,20 @@ public class RMSongpackLoader { - public static List availableSongpacks = new ArrayList<>(); + /** + * Allows relative paths in the songpack, for folders other than "music" + * this is useful for songpack makers who want to use a different folder structure + * potentially for letting songpacks replace plugin assets as well, if the plugin dev + * implements loading from the songpack + */ + static String normalizeSongPath(String song) { + String withExt = song.endsWith(".mp3") ? song : (song + ".mp3"); + // If caller provided a path (contains '/'), use it as-is. + // Otherwise default to the "music/" subfolder. + return (song.startsWith("/") || song.startsWith("\\")) ? withExt : ("music/" + withExt); + } + + public static List availableSongpacks = new ArrayList<>(); public static MusicPackResource getInputStream(Path dirPath, String fileName, boolean embedded) { MusicPackResource resource = new MusicPackResource(); @@ -24,7 +38,7 @@ public static MusicPackResource getInputStream(Path dirPath, String fileName, bo } if (dirPath == null) { - ReactiveMusic.LOGGER.error("dirpath was null"); + ReactiveMusicDebug.LOGGER.error("dirpath was null"); return null; } @@ -42,7 +56,7 @@ public static MusicPackResource getInputStream(Path dirPath, String fileName, bo return resource; } } catch (IOException e) { - ReactiveMusic.LOGGER.error("Failed while loading file from zip " + e.getMessage()); + ReactiveMusicDebug.LOGGER.error("Failed while loading file from zip " + e.getMessage()); return null; } } @@ -55,10 +69,10 @@ public static MusicPackResource getInputStream(Path dirPath, String fileName, bo resource.inputStream = Files.newInputStream(filePath); return resource; } catch (IOException e) { - ReactiveMusic.LOGGER.error(e.toString()); + ReactiveMusicDebug.LOGGER.error(e.toString()); } } else { - ReactiveMusic.LOGGER.error("Couldn't find file! " + filePath); + ReactiveMusicDebug.LOGGER.error("Couldn't find file! " + filePath); } } @@ -85,7 +99,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { } }); } catch (IOException e) { - ReactiveMusic.LOGGER.error("Failed while visiting potential packs " + e.getMessage()); + ReactiveMusicDebug.LOGGER.error("Failed while visiting potential packs " + e.getMessage()); } for (Path packPath : potentialPacks) { @@ -100,7 +114,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { yamlFileNames = getYamlFiles(Files.list(root).toList()); } catch (IOException e) { - ReactiveMusic.LOGGER.error("Failed reading zip: " + e); + ReactiveMusicDebug.LOGGER.error("Failed reading zip: " + e); continue; } } @@ -112,20 +126,20 @@ else if (Files.isDirectory(packPath)) { yamlFileNames = getYamlFiles(Files.list(packPath).toList()); } catch (IOException e) { - ReactiveMusic.LOGGER.error("Failed reading directory: " + e); + ReactiveMusicDebug.LOGGER.error("Failed reading directory: " + e); continue; } } for (String yamlFile : yamlFileNames) { - SongpackZip songpackZip = loadSongpack(packPath, false, yamlFile); + RMSongpackZip songpackZip = loadSongpack(packPath, false, yamlFile); if (songpackZip != null) { availableSongpacks.add(songpackZip); } } } - ReactiveMusic.LOGGER.info("Took " + (System.currentTimeMillis() - startTime) + "ms to parse available songpacks, found " + availableSongpacks.size() + "!"); + ReactiveMusicDebug.LOGGER.info("Took " + (System.currentTimeMillis() - startTime) + "ms to parse available songpacks, found " + availableSongpacks.size() + "!"); } public static List getYamlFiles(List paths) { @@ -148,8 +162,8 @@ public static List getYamlFiles(List paths) { // New version of loadSongpack with YAML file name - public static SongpackZip loadSongpack(Path songpackPath, boolean embedded, String yamlFileName) { - SongpackZip songpackZip = new SongpackZip(); + public static RMSongpackZip loadSongpack(Path songpackPath, boolean embedded, String yamlFileName) { + RMSongpackZip songpackZip = new RMSongpackZip(); songpackZip.path = songpackPath; songpackZip.embedded = embedded; @@ -161,14 +175,14 @@ public static SongpackZip loadSongpack(Path songpackPath, boolean embedded, Stri Yaml yaml = new Yaml(); try { - songpackZip.config = yaml.loadAs(configResource.inputStream, SongpackConfig.class); + songpackZip.config = yaml.loadAs(configResource.inputStream, RMSongpackConfig.class); } catch (Exception e) { - songpackZip.config = new SongpackConfig(); + songpackZip.config = new RMSongpackConfig(); songpackZip.config.name = songpackPath != null ? songpackPath.getFileName().toString() : "Embedded"; songpackZip.errorString = e.toString() + "\n\n"; songpackZip.blockLoading = true; - ReactiveMusic.LOGGER.error("Failed to load properties! Embedded=" + embedded + " Exception:" + e.toString()); + ReactiveMusicDebug.LOGGER.error("Failed to load properties! Embedded=" + embedded + " Exception:" + e.toString()); } if (!Constructor.errorString.isEmpty()) { @@ -189,26 +203,26 @@ public static SongpackZip loadSongpack(Path songpackPath, boolean embedded, Stri } // Legacy call for default "ReactiveMusic.yaml" - public static SongpackZip loadSongpack(Path songpackPath, boolean embedded) { + public static RMSongpackZip loadSongpack(Path songpackPath, boolean embedded) { return loadSongpack(songpackPath, embedded, "ReactiveMusic.yaml"); } - private static List getRuntimeEntries(SongpackZip songpackZip) { - List runtimeEntries = new ArrayList<>(); + private static List getRuntimeEntries(RMSongpackZip songpackZip) { + List runtimeEntries = new ArrayList<>(); - for (var entry : songpackZip.config.entries) { + for (var entry : songpackZip.getConfig().entries) { if (entry == null) continue; - RMRuntimeEntry runtimeEntry = RMRuntimeEntry.create(songpackZip, entry); + RuntimeEntry runtimeEntry = new RMRuntimeEntry(songpackZip, entry); - if (!runtimeEntry.errorString.isEmpty()) { - songpackZip.errorString += runtimeEntry.errorString; + if (!runtimeEntry.getErrorString().isEmpty()) { + songpackZip.setErrorString(songpackZip.getErrorString() + runtimeEntry.getErrorString()); // allow it to keep loading if it passes the check below //continue; } - if (runtimeEntry.conditions.isEmpty()) continue; + if (runtimeEntry.getConditions().isEmpty()) continue; runtimeEntries.add(runtimeEntry); } @@ -216,7 +230,7 @@ private static List getRuntimeEntries(SongpackZip songpackZip) { return runtimeEntries; } - public static void verifySongpackZip(SongpackZip songpackZip) { + public static void verifySongpackZip(RMSongpackZip songpackZip) { if (songpackZip.config == null || songpackZip.config.entries == null) { songpackZip.errorString += "Entries are null or not formatted correctly! Make sure you.\n\n"; songpackZip.blockLoading = true; @@ -240,7 +254,7 @@ public static void verifySongpackZip(SongpackZip songpackZip) { for (int j = 0; j < songpackZip.config.entries[i].songs.length; j++) { String song = songpackZip.config.entries[i].songs[j]; - var inputStream = getInputStream(songpackZip.path, "music/" + song + ".mp3", songpackZip.embedded); + var inputStream = getInputStream(songpackZip.path, normalizeSongPath(song), songpackZip.embedded); if (inputStream == null) { StringBuilder eventName = new StringBuilder(); diff --git a/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackZip.java b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackZip.java new file mode 100644 index 0000000..d87cbbd --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/impl/songpack/RMSongpackZip.java @@ -0,0 +1,39 @@ +package circuitlord.reactivemusic.impl.songpack; + +import java.nio.file.Path; +import java.util.List; + +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; +import circuitlord.reactivemusic.api.songpack.SongpackZip; + +public class RMSongpackZip implements SongpackZip { + + public RMSongpackConfig config; + + + public List runtimeEntries; + + + public Path path; + + public String errorString = ""; + public boolean blockLoading = false; + + // backwards compat + public boolean convertBiomeToBiomeTag = false; + + public boolean isv05OldSongpack = false; + + public boolean embedded = false; + + public boolean isEmbedded() { return embedded; } + public RMSongpackConfig getConfig() { return config; } + public Path getPath() { return path; } + public String getErrorString() { return errorString; } + public void setErrorString(String s) { errorString = s; } + public List getEntries() { return List.copyOf(runtimeEntries); } + + public String getName() { return config.name; } + public String getAuthor() {return config.author; } + +} diff --git a/src/main/java/circuitlord/reactivemusic/mixin/SoundManagerMixin.java b/src/main/java/circuitlord/reactivemusic/mixin/SoundManagerMixin.java index 97362a0..f0beffe 100644 --- a/src/main/java/circuitlord/reactivemusic/mixin/SoundManagerMixin.java +++ b/src/main/java/circuitlord/reactivemusic/mixin/SoundManagerMixin.java @@ -1,6 +1,7 @@ package circuitlord.reactivemusic.mixin; import circuitlord.reactivemusic.ReactiveMusic; +import circuitlord.reactivemusic.ReactiveMusicDebug; import net.minecraft.client.sound.SoundInstance; import net.minecraft.client.sound.SoundManager; import org.spongepowered.asm.mixin.Mixin; @@ -27,11 +28,11 @@ private void play(SoundInstance soundInstance, CallbackInfo ci) { else if (path.contains("battle.pv")) { ReactiveMusic.trackedSoundsMuteMusic.add(soundInstance); - ReactiveMusic.LOGGER.info("Detected cobblemon battle event, adding to list!"); + ReactiveMusicDebug.LOGGER.info("Detected cobblemon battle event, adding to list!"); } - for (String muteSound : ReactiveMusic.config.soundsMuteMusic) { + for (String muteSound : ReactiveMusic.modConfig.soundsMuteMusic) { if (path.contains(muteSound)) { ReactiveMusic.trackedSoundsMuteMusic.add(soundInstance); break; diff --git a/src/main/java/circuitlord/reactivemusic/plugins/ActionsPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/ActionsPlugin.java new file mode 100644 index 0000000..20f547e --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/ActionsPlugin.java @@ -0,0 +1,48 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import net.minecraft.entity.Entity; +import net.minecraft.entity.passive.HorseEntity; +import net.minecraft.entity.passive.PigEntity; +import net.minecraft.entity.vehicle.BoatEntity; +import net.minecraft.entity.vehicle.MinecartEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.Map; + +public final class ActionsPlugin extends ReactiveMusicPlugin { + + public ActionsPlugin() { + super("reactivemusic", "actions"); + } + + private static EventRecord FISHING, MINECART, BOAT, HORSE, PIG; + + @Override + public void init() { + registerSongpackEvents("FISHING","MINECART","BOAT","HORSE","PIG"); + + FISHING = SongpackEvent.get("FISHING"); + MINECART = SongpackEvent.get("MINECART"); + BOAT = SongpackEvent.get("BOAT"); + HORSE = SongpackEvent.get("HORSEING"); + PIG = SongpackEvent.get("PIG"); + } + + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (player == null) return; + + eventMap.put(FISHING, player.fishHook != null); + + Entity v = player.getVehicle(); + eventMap.put(MINECART, v instanceof MinecartEntity); + eventMap.put(BOAT, v instanceof BoatEntity); + eventMap.put(HORSE, v instanceof HorseEntity); + eventMap.put(PIG, v instanceof PigEntity); + } +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/plugins/AtHomePlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/AtHomePlugin.java new file mode 100644 index 0000000..70244e2 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/AtHomePlugin.java @@ -0,0 +1,113 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.ReactiveMusic; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import circuitlord.reactivemusic.config.ModConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import java.util.Map; + +public final class AtHomePlugin extends ReactiveMusicPlugin { + public AtHomePlugin() { + super("reactivemusic", "at_home"); + } + + private static final float RADIUS = 45.0f; + + // Plugin-local state; no more SongPicker.wasSleeping + private static boolean wasSleeping = false; + + // Event handles + private static EventRecord HOME, HOME_OVERWORLD, HOME_NETHER, HOME_END; + + @Override + public void init() { + registerSongpackEvents("HOME", "HOME_OVERWORLD", "HOME_NETHER", "HOME_END"); + + HOME = SongpackEvent.get("HOME"); + HOME_OVERWORLD = SongpackEvent.get("HOME_OVERWORLD"); + HOME_NETHER = SongpackEvent.get("HOME_NETHER"); + HOME_END = SongpackEvent.get("HOME_END"); + } + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (player == null || world == null) return; + + // Keys: base (per save/server), and per-dimension + String baseKey = computeBaseWorldKey(); + String dimPath = world.getRegistryKey().getValue().getPath(); // overworld | the_nether | the_end | ... + String dimKey = baseKey + "_" + dimPath; + + // On sleep edge, save both base and dimension-specific homes + if (!wasSleeping && player.isSleeping()) { + var pos = player.getPos(); + ReactiveMusic.modConfig.savedHomePositions.put(baseKey, pos); + ReactiveMusic.modConfig.savedHomePositions.put(dimKey, pos); + // TODO: There is a better way to sereialize the positions that is built into the fabric mappings + // ???: Is it Persistent State? + ModConfig.saveConfig(); + } + wasSleeping = player.isSleeping(); + + // Emit base HOME (per save/server, regardless of dimension) + eventMap.put(HOME, isWithinHome(world, player, baseKey)); + + // Emit one of the three dimension-specific events (only for vanilla dims) + Identifier dimId = world.getRegistryKey().getValue(); + if (dimId.equals(World.OVERWORLD.getValue())) { + eventMap.put(HOME_OVERWORLD, isWithinHome(world, player, dimKey)); + eventMap.put(HOME_NETHER, false); + eventMap.put(HOME_END, false); + } else if (dimId.equals(World.NETHER.getValue())) { + eventMap.put(HOME_OVERWORLD, false); + eventMap.put(HOME_NETHER, isWithinHome(world, player, dimKey)); + eventMap.put(HOME_END, false); + } else if (dimId.equals(World.END.getValue())) { + eventMap.put(HOME_OVERWORLD, false); + eventMap.put(HOME_NETHER, false); + eventMap.put(HOME_END, isWithinHome(world, player, dimKey)); + } else { + // Non-vanilla dimension: keep the three vanilla-specific flags false + eventMap.put(HOME_OVERWORLD, false); + eventMap.put(HOME_NETHER, false); + eventMap.put(HOME_END, false); + } + } + + // --- helpers --- + + private static boolean isWithinHome(World world, PlayerEntity player, String key) { + var map = ReactiveMusic.modConfig.savedHomePositions; + if (!map.containsKey(key)) return false; + Vec3d dist = player.getPos().subtract(map.get(key)); + return dist.length() < RADIUS; + } + + /** Per-save (singleplayer) or per-server (multiplayer) identifier — no dimension. */ + private static String computeBaseWorldKey() { + MinecraftClient mc = MinecraftClient.getInstance(); + if (mc != null) { + if (mc.isInSingleplayer() && mc.getServer() != null && mc.getServer().getSaveProperties() != null) { + // Singleplayer: user-facing save name (from level.dat) + String pretty = mc.getServer().getSaveProperties().getLevelName(); + if (pretty != null && !pretty.isBlank()) return pretty; + } else { + // Multiplayer: server list entry (client-side safe) + ServerInfo entry = mc.getCurrentServerEntry(); + if (entry != null) { + if (entry.name != null && !entry.name.isBlank()) return entry.name; + if (entry.address != null && !entry.address.isBlank()) return entry.address; + } + } + } + return "unknown_world"; + } +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/plugins/BiomeIdentityPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/BiomeIdentityPlugin.java new file mode 100644 index 0000000..e4e3622 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/BiomeIdentityPlugin.java @@ -0,0 +1,32 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.SongPicker; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.registry.entry.RegistryEntry; + +public final class BiomeIdentityPlugin extends ReactiveMusicPlugin { + public BiomeIdentityPlugin() { + super("reactivemusic", "biome_id"); + } + @Override public void init() { /* no-op */ } + + @Override + public void gameTick(PlayerEntity player, World world, java.util.Map out) { + if (player == null || world == null) return; + + BlockPos pos = player.getBlockPos(); + RegistryEntry entry = world.getBiome(pos); + + // Mirror SongPicker’s original assignment of currentBiomeName + String name = entry.getKey() + .map(k -> k.getValue().toString()) + .orElse("[unregistered]"); + SongPicker.currentBiomeName = name; // isEntryValid() uses this + } +} + diff --git a/src/main/java/circuitlord/reactivemusic/plugins/BiomeTagPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/BiomeTagPlugin.java new file mode 100644 index 0000000..512e7fb --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/BiomeTagPlugin.java @@ -0,0 +1,43 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.SongPicker; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.tag.TagKey; + +import java.util.List; +import java.util.Map; + +public final class BiomeTagPlugin extends ReactiveMusicPlugin { + public BiomeTagPlugin() { + super("reactivemusic", "biome_tag"); + } + @Override public void init() { /* no-op (SongPicker already builds BIOME_TAGS/map) */ } + + @Override public void gameTick(PlayerEntity player, World world, Map out) { + if (player == null || world == null) return; + + BlockPos pos = player.getBlockPos(); + RegistryEntry biome = world.getBiome(pos); + + // Collect current tags once + List> currentTags = biome.streamTags().toList(); + + // Mirror SongPicker’s original per-tick loop: compare by tag.id() identity + for (TagKey tag : SongPicker.BIOME_TAGS) { + boolean found = false; + for (TagKey cur : currentTags) { + if (cur.id() == tag.id()) { // keep the same non-Fabric-safe identity check + found = true; + break; + } + } + SongPicker.biomeTagEventMap.put(tag, found); // isEntryValid() reads this + } + } +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/plugins/BlockCounterPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/BlockCounterPlugin.java new file mode 100644 index 0000000..fd0ad29 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/BlockCounterPlugin.java @@ -0,0 +1,100 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.SongPicker; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.block.Block; + +import java.util.*; + +public final class BlockCounterPlugin extends ReactiveMusicPlugin { + public BlockCounterPlugin() { + super("reactivemusic", "block_counter"); + } + + // --- config (mirrors your current setup) --- + private static final int RADIUS = 25; + private static final Set BLOCK_COUNTER_BLACKLIST = Set.of("ore", "debris"); + + // --- plugin-owned state (removed from SongPicker) --- + private static final Map blockCounterMap = new HashMap<>(); + private static BlockPos cachedBlockCounterOrigin; + private static int currentBlockCounterX = 99999; // start out-of-range to force snap to origin on first wrap + // Note: your Y sweep is commented-out in the original; we keep the same single-axis sweep. + + @Override public void init() { /* no-op */ } + @Override public int tickSchedule() { return 1; } + + @Override + public void gameTick(PlayerEntity player, World world, Map out) { + if (!(player instanceof ClientPlayerEntity) || world == null) return; + + // lazily initialize origin + if (cachedBlockCounterOrigin == null) { + cachedBlockCounterOrigin = player.getBlockPos(); + } + + long startNano = System.nanoTime(); + + // advance X + currentBlockCounterX++; + if (currentBlockCounterX > RADIUS) { + currentBlockCounterX = -RADIUS; + } + + // finished iterating, copy & reset + if (currentBlockCounterX == -RADIUS) { + // Print request + if (SongPicker.queuedToPrintBlockCounter) { + player.sendMessage(Text.of("[ReactiveMusic]: Logging Block Counter map!")); + blockCounterMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .forEach(e -> player.sendMessage(Text.of(e.getKey() + ": " + e.getValue()), false)); + SongPicker.queuedToPrintBlockCounter = false; + } + + // publish to the cache that isEntryValid() reads + SongPicker.cachedBlockChecker.clear(); + SongPicker.cachedBlockChecker.putAll(blockCounterMap); + + // reset for next sweep + blockCounterMap.clear(); + cachedBlockCounterOrigin = player.getBlockPos(); + } + + // scan a vertical column (Y) for all Z at the current X slice + BlockPos.Mutable mutablePos = new BlockPos.Mutable(); + for (int y = -RADIUS; y <= RADIUS; y++) { + for (int z = -RADIUS; z <= RADIUS; z++) { + mutablePos.set( + cachedBlockCounterOrigin.getX() + currentBlockCounterX, + cachedBlockCounterOrigin.getY() + y, + cachedBlockCounterOrigin.getZ() + z + ); + + Block block = world.getBlockState(mutablePos).getBlock(); + String key = Registries.BLOCK.getId(block).toString(); + + boolean blacklisted = false; + for (String s : BLOCK_COUNTER_BLACKLIST) { + if (key.contains(s)) { blacklisted = true; break; } + } + if (blacklisted) continue; + + blockCounterMap.merge(key, 1, Integer::sum); + } + } + + // timing (kept but not logged) + long elapsed = System.nanoTime() - startNano; + @SuppressWarnings("unused") + double elapsedMs = elapsed / 1_000_000.0; + // (optional) log if you want: ReactiveMusicDebug.LOGGER.info("BlockCounterPlugin tick: " + elapsedMs + "ms"); + } +} diff --git a/src/main/java/circuitlord/reactivemusic/plugins/BossBarPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/BossBarPlugin.java new file mode 100644 index 0000000..7a82259 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/BossBarPlugin.java @@ -0,0 +1,42 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import circuitlord.reactivemusic.mixin.BossBarHudAccessor; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.Map; + +public final class BossBarPlugin extends ReactiveMusicPlugin { + public BossBarPlugin() { + super("reactivemusic", "bossbar"); + } + + private static EventRecord BOSS; + private static MinecraftClient mc = MinecraftClient.getInstance(); + + @Override public void init() { + registerSongpackEvents("BOSS"); + + BOSS = SongpackEvent.get("BOSS"); + } + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + + if (BOSS == null) return; + + boolean active = false; + + if (mc.inGameHud != null && mc.inGameHud.getBossBarHud() != null) { + var bossBars = ((BossBarHudAccessor) mc.inGameHud.getBossBarHud()).getBossBars(); + active = !bossBars.isEmpty(); + } + + eventMap.put(BOSS, active); + } +} + diff --git a/src/main/java/circuitlord/reactivemusic/plugins/CombatPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/CombatPlugin.java new file mode 100644 index 0000000..90bdd97 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/CombatPlugin.java @@ -0,0 +1,32 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.Map; + +public final class CombatPlugin extends ReactiveMusicPlugin { + public CombatPlugin() { + super("reactivemusic", "combat"); + } + + private static EventRecord DYING; + private static final float THRESHOLD = 0.35f; + + @Override + public void init() { + registerSongpackEvents("DYING"); + + DYING = SongpackEvent.get("DYING"); + } + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (player == null || world == null) return; + boolean dying = (player.getHealth() / player.getMaxHealth()) < THRESHOLD; + eventMap.put(DYING, dying); + } +} diff --git a/src/main/java/circuitlord/reactivemusic/plugins/DimensionPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/DimensionPlugin.java new file mode 100644 index 0000000..5d31636 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/DimensionPlugin.java @@ -0,0 +1,42 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.SongPicker; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.Map; + +public final class DimensionPlugin extends ReactiveMusicPlugin { + public DimensionPlugin() { + super("reactivemusic", "dimension"); + } + private static EventRecord OVERWORLD, NETHER, END; + + @Override + public void init() { + registerSongpackEvents("OVERWORLD", "NETHER", "END"); + + OVERWORLD = SongpackEvent.get("OVERWORLD"); + NETHER = SongpackEvent.get("NETHER"); + END = SongpackEvent.get("END"); + } + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (world == null) return; + + var indimension = world.getRegistryKey(); + SongPicker.currentDimName = indimension.getValue().toString(); + + boolean isOverworld = indimension == World.OVERWORLD; + boolean isNether = indimension == World.NETHER; + boolean isEnd = indimension == World.END; + + eventMap.put(OVERWORLD, isOverworld); + eventMap.put(NETHER, isNether); + eventMap.put(END, isEnd); + } +} diff --git a/src/main/java/circuitlord/reactivemusic/plugins/OverlayTrackPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/OverlayTrackPlugin.java new file mode 100644 index 0000000..3d85a93 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/OverlayTrackPlugin.java @@ -0,0 +1,108 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.ReactiveMusic; +import circuitlord.reactivemusic.ReactiveMusicState; +import circuitlord.reactivemusic.SongPicker; +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.audio.ReactivePlayer; +import circuitlord.reactivemusic.api.audio.ReactivePlayerOptions; +import circuitlord.reactivemusic.api.songpack.RuntimeEntry; + +public final class OverlayTrackPlugin extends ReactiveMusicPlugin { + public OverlayTrackPlugin() { + super("reactivemusic","overlay"); + } + + ReactivePlayer musicPlayer; + ReactivePlayer overlayPlayer; + + @Override public void init() { + ReactiveMusicState.LOGGER.info("Initializing " + pluginId.getId() + " plugin"); + musicPlayer = ReactiveMusicAPI.audioManager().get("reactive:music"); + + ReactiveMusicAPI.audioManager().create( + "reactive:overlay", + ReactivePlayerOptions.create() + .namespace("reactive") + .group("overlay") + .loop(false) + .gain(1.0f) + .fade(0f) + .quietWhenGamePaused(false) + .linkToMinecraftVolumes(true) + ); + + + overlayPlayer = ReactiveMusic.audio().get("reactive:overlay"); + } + + @Override public void newTick() { + boolean usingOverlay = usingOverlay(); + + // guard the call + if (musicPlayer == null || overlayPlayer == null) { return; } + + if (usingOverlay) { + if (!overlayPlayer.isPlaying()) { + if (!ReactiveMusicState.validEntries.isEmpty()) { + overlayPlayer.setSong(ReactiveMusicUtils.pickRandomSong(SongPicker.getSelectedSongs(ReactiveMusicState.validEntries.get(0), ReactiveMusicState.validEntries))); + } + overlayPlayer.getGainSuppliers().get("reactivemusic").setFadePercent(0f); + overlayPlayer.play(); + } + overlayPlayer.fade(1f, 140); + musicPlayer.fade(0f, 70); + musicPlayer.stopOnFadeOut(false); + + } + if (!usingOverlay) { + overlayPlayer.fade(0f, 70); + overlayPlayer.stopOnFadeOut(true); + + // FIXME: This is coupling! Figure out how to get this out of here. + musicPlayer.stopOnFadeOut(true); + musicPlayer.resetOnFadeOut(true); + } + }; + + /** + * FIXME + * This is broken. It should be getting called from processValidEvents... but it isn't. + * @see ReactiveMusicPlugin#onValid(RMRuntimeEntry) + */ + @Override public void onValid(RuntimeEntry entry) { + // ReactiveMusicAPI.LOGGER.info("Overlay enabled"); + // if (entry.useOverlay) { + // ReactiveMusicAPI.freezeCore(); + // } + } + + /** + * FIXME + * This is broken. It should be getting called from processValidEvents... but it isn't. + * Or is it? It's not logging, but sometimes the main player breaks. + * @see ReactiveMusicPlugin#onInvalid(RMRuntimeEntry) + */ + @Override public void onInvalid(RuntimeEntry entry) { + // ReactiveMusicAPI.LOGGER.info("Overlay disabled"); + // if (entry.useOverlay) { + // ReactiveMusicAPI.unfreezeCore(); + // } + } + + /** + * Calling this from newTick() for now since the event processing calls are broken... + * Or is it? It's not logging, but sometimes the main player breaks. + * @return + */ + public static boolean usingOverlay() { + // FIXME: Overlay should only activate is the entry is higher prio + // ???: Should prio be checked here or in core logic? + for (RuntimeEntry entry : ReactiveMusicState.validEntries) { + if (entry.shouldOverlay()) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/circuitlord/reactivemusic/plugins/ProximityPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/ProximityPlugin.java new file mode 100644 index 0000000..6f886b9 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/ProximityPlugin.java @@ -0,0 +1,39 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import net.minecraft.entity.mob.HostileEntity; +import net.minecraft.entity.passive.VillagerEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.Map; + +public final class ProximityPlugin extends ReactiveMusicPlugin { + public ProximityPlugin() { + super("reactivemusic", "proximity"); + } + private static EventRecord NEARBY_MOBS, VILLAGE; + + @Override public void init() { + registerSongpackEvents("NEARBY_MOBS", "VILLAGE"); + NEARBY_MOBS = SongpackEvent.get("NEARBY_MOBS"); + VILLAGE = SongpackEvent.get("VILLAGE"); + } + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (player == null || world == null) return; + + // Nearby mobs + var hostiles = ReactiveMusicUtils.getEntitiesInSphere(HostileEntity.class, player, 12.0, null); + boolean mobsNearby = !hostiles.isEmpty(); + eventMap.put(NEARBY_MOBS, mobsNearby); + + // Village proximity (simple heuristic using VillageManager distance) + var villagers = ReactiveMusicUtils.getEntitiesInSphere(VillagerEntity.class, player, 30.0, null); + boolean inVillage = !villagers.isEmpty(); + eventMap.put(VILLAGE, inVillage); + } +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/plugins/TimeOfDayPlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/TimeOfDayPlugin.java new file mode 100644 index 0000000..e1d9917 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/TimeOfDayPlugin.java @@ -0,0 +1,43 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; + +import java.util.Map; + +public final class TimeOfDayPlugin extends ReactiveMusicPlugin { + public TimeOfDayPlugin() { + super("reactivemusic", "time_of_day"); + } + + private static EventRecord DAY, NIGHT, SUNSET, SUNRISE; + + @Override + public void init() { + registerSongpackEvents("DAY","NIGHT","SUNSET","SUNRISE"); + + DAY = SongpackEvent.get("DAY"); + NIGHT = SongpackEvent.get("NIGHT"); + SUNSET = SongpackEvent.get("SUNSET"); + SUNRISE = SongpackEvent.get("SUNRISE"); + } + + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (player == null || world == null) return; + + long time = world.getTimeOfDay() % 24000L; + boolean night = (time >= 13000L && time < 23000L); + boolean sunset = (time >= 12000L && time < 13000L); + boolean sunrise = (time >= 23000L); // mirrors your SongPicker logic + + eventMap.put(DAY, !night); + eventMap.put(NIGHT, night); + eventMap.put(SUNSET, sunset); + eventMap.put(SUNRISE, sunrise); + } +} \ No newline at end of file diff --git a/src/main/java/circuitlord/reactivemusic/plugins/WeatherAltitudePlugin.java b/src/main/java/circuitlord/reactivemusic/plugins/WeatherAltitudePlugin.java new file mode 100644 index 0000000..4fc8dc6 --- /dev/null +++ b/src/main/java/circuitlord/reactivemusic/plugins/WeatherAltitudePlugin.java @@ -0,0 +1,45 @@ +package circuitlord.reactivemusic.plugins; + +import circuitlord.reactivemusic.api.*; +import circuitlord.reactivemusic.api.eventsys.EventRecord; +import circuitlord.reactivemusic.api.songpack.SongpackEvent; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import java.util.Map; + +public final class WeatherAltitudePlugin extends ReactiveMusicPlugin { + public WeatherAltitudePlugin() { + super("reactivemusic", "weather_and_altitude"); + } + private static EventRecord RAIN, SNOW, STORM, UNDERWATER, UNDERGROUND, DEEP_UNDERGROUND, HIGH_UP; + + @Override + public void init() { + registerSongpackEvents("RAIN", "SNOW", "STORM", "UNDERWATER", "UNDERGROUND", "DEEP", "DEEP_UNDERGROUND", "HIGH_UP"); + + RAIN = SongpackEvent.get("RAIN"); + SNOW = SongpackEvent.get("SNOW"); + STORM = SongpackEvent.get("STORM"); + UNDERWATER = SongpackEvent.get("UNDERWATER"); + UNDERGROUND = SongpackEvent.get("UNDERGROUND"); + DEEP_UNDERGROUND = SongpackEvent.get("DEEP_UNDERGROUND"); + HIGH_UP = SongpackEvent.get("HIGH_UP"); + } + + @Override + public void gameTick(PlayerEntity player, World world, Map eventMap) { + if (player == null || world == null) return; + BlockPos pos = player.getBlockPos(); + + eventMap.put(STORM, ReactiveMusicUtils.isStorm(world)); + eventMap.put(RAIN, ReactiveMusicUtils.isRainingAt(world, pos)); + eventMap.put(SNOW, ReactiveMusicUtils.isSnowingAt(world, pos)); + eventMap.put(UNDERWATER, player.isSubmergedInWater()); + eventMap.put(UNDERGROUND, ReactiveMusicUtils.isUnderground(world, pos, 55)); + eventMap.put(DEEP_UNDERGROUND, ReactiveMusicUtils.isDeepUnderground(world, pos, 15)); + eventMap.put(HIGH_UP, ReactiveMusicUtils.isHighUp(pos, 128)); + } +} + diff --git a/src/main/java/rm_javazoom/jl/player/JavaSoundAudioDevice.java b/src/main/java/rm_javazoom/jl/player/JavaSoundAudioDevice.java index ecc8736..356155d 100644 --- a/src/main/java/rm_javazoom/jl/player/JavaSoundAudioDevice.java +++ b/src/main/java/rm_javazoom/jl/player/JavaSoundAudioDevice.java @@ -32,7 +32,6 @@ import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; -import circuitlord.reactivemusic.PlayerThread; import rm_javazoom.jl.decoder.Decoder; import rm_javazoom.jl.decoder.JavaLayerException; @@ -113,7 +112,6 @@ protected void createSource() throws JavaLayerException c.setValue(c.getMaximum()); }*/ source.start(); - setGain(PlayerThread.realGain); // XXX ~Vazkii } } catch (RuntimeException ex) { diff --git a/src/main/resources/META-INF/services/circuitlord.reactivemusic.api.ReactiveMusicPlugin b/src/main/resources/META-INF/services/circuitlord.reactivemusic.api.ReactiveMusicPlugin new file mode 100644 index 0000000..d02ba4c --- /dev/null +++ b/src/main/resources/META-INF/services/circuitlord.reactivemusic.api.ReactiveMusicPlugin @@ -0,0 +1,12 @@ +circuitlord.reactivemusic.plugins.ActionsPlugin +circuitlord.reactivemusic.plugins.AtHomePlugin +circuitlord.reactivemusic.plugins.BiomeIdentityPlugin +circuitlord.reactivemusic.plugins.BiomeTagPlugin +circuitlord.reactivemusic.plugins.BlockCounterPlugin +circuitlord.reactivemusic.plugins.BossBarPlugin +circuitlord.reactivemusic.plugins.CombatPlugin +circuitlord.reactivemusic.plugins.DimensionPlugin +circuitlord.reactivemusic.plugins.OverlayTrackPlugin +circuitlord.reactivemusic.plugins.ProximityPlugin +circuitlord.reactivemusic.plugins.TimeOfDayPlugin +circuitlord.reactivemusic.plugins.WeatherAltitudePlugin