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 super T> 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 super T> 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 super T> 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