aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/gregtech/api')
-rw-r--r--src/main/java/gregtech/api/enums/GT_Values.java5
-rw-r--r--src/main/java/gregtech/api/enums/ItemList.java10
-rw-r--r--src/main/java/gregtech/api/enums/MetaTileEntityIDs.java5
-rw-r--r--src/main/java/gregtech/api/enums/Textures.java3
-rw-r--r--src/main/java/gregtech/api/gui/modularui/GT_UITextures.java2
-rw-r--r--src/main/java/gregtech/api/metatileentity/CommonMetaTileEntity.java34
-rw-r--r--src/main/java/gregtech/api/net/GT_PacketTypes.java1
-rw-r--r--src/main/java/gregtech/api/net/GT_Packet_MusicSystemData.java58
-rw-r--r--src/main/java/gregtech/api/net/IGT_NetworkHandler.java4
-rw-r--r--src/main/java/gregtech/api/util/GT_MusicSystem.java666
10 files changed, 786 insertions, 2 deletions
diff --git a/src/main/java/gregtech/api/enums/GT_Values.java b/src/main/java/gregtech/api/enums/GT_Values.java
index a27c5ee43b..a7fde53b66 100644
--- a/src/main/java/gregtech/api/enums/GT_Values.java
+++ b/src/main/java/gregtech/api/enums/GT_Values.java
@@ -677,6 +677,11 @@ public class GT_Values {
+ "Gold";
public static final String AuthorVolence = "Author: " + EnumChatFormatting.AQUA + "Volence";
+ public static final String AuthorEigenRaven = "Author: " + EnumChatFormatting.DARK_PURPLE
+ + "Eigen"
+ + EnumChatFormatting.BOLD
+ + "Raven";
+
public static final String AuthorNotAPenguin = "Author: " + EnumChatFormatting.WHITE
+ EnumChatFormatting.BOLD
+ "Not"
diff --git a/src/main/java/gregtech/api/enums/ItemList.java b/src/main/java/gregtech/api/enums/ItemList.java
index 77fc88bd30..6533aca304 100644
--- a/src/main/java/gregtech/api/enums/ItemList.java
+++ b/src/main/java/gregtech/api/enums/ItemList.java
@@ -2518,7 +2518,15 @@ public enum ItemList implements IItemContainer {
Automation_ChestBuffer_UEV,
Automation_ChestBuffer_UIV,
Automation_ChestBuffer_UMV,
- Automation_ChestBuffer_UXV,;
+ Automation_ChestBuffer_UXV,
+ BetterJukebox_LV,
+ BetterJukebox_MV,
+ BetterJukebox_HV,
+ BetterJukebox_EV,
+ BetterJukebox_IV,
+ WirelessHeadphones,
+ // semicolon after the comment to reduce merge conflicts
+ ;
public static final ItemList[] DYE_ONLY_ITEMS = { Color_00, Color_01, Color_02, Color_03, Color_04, Color_05,
Color_06, Color_07, Color_08, Color_09, Color_10, Color_11, Color_12, Color_13, Color_14, Color_15 },
diff --git a/src/main/java/gregtech/api/enums/MetaTileEntityIDs.java b/src/main/java/gregtech/api/enums/MetaTileEntityIDs.java
index 6772f4dc12..464a8b2710 100644
--- a/src/main/java/gregtech/api/enums/MetaTileEntityIDs.java
+++ b/src/main/java/gregtech/api/enums/MetaTileEntityIDs.java
@@ -1301,6 +1301,11 @@ public enum MetaTileEntityIDs {
lsc(13106),
tfftHatch(13109),
WORMHOLE_GENERATOR_CONTROLLER(13115),
+ BETTER_JUKEBOX_LV(14301),
+ BETTER_JUKEBOX_MV(14302),
+ BETTER_JUKEBOX_HV(14303),
+ BETTER_JUKEBOX_EV(14304),
+ BETTER_JUKEBOX_IV(14305),
MegaChemicalReactor(13366),
MegaOilCracker(13367),
ExtremeEntityCrusherController(14201),
diff --git a/src/main/java/gregtech/api/enums/Textures.java b/src/main/java/gregtech/api/enums/Textures.java
index 049fd97361..53125ec454 100644
--- a/src/main/java/gregtech/api/enums/Textures.java
+++ b/src/main/java/gregtech/api/enums/Textures.java
@@ -637,6 +637,7 @@ public class Textures {
OVERLAY_TOP_STEAM_FURNACE_GLOW,
OVERLAY_TOP_STEAM_ALLOY_SMELTER,
OVERLAY_TOP_STEAM_ALLOY_SMELTER_GLOW,
+ OVERLAY_TOP_JUKEBOX,
OVERLAY_TOP_STEAM_MACERATOR,
OVERLAY_TOP_STEAM_MACERATOR_GLOW,
@@ -757,6 +758,7 @@ public class Textures {
OVERLAY_SIDE_SCANNER_GLOW,
OVERLAY_SIDE_INDUSTRIAL_APIARY,
OVERLAY_SIDE_INDUSTRIAL_APIARY_GLOW,
+ OVERLAY_SIDE_JUKEBOX,
OVERLAY_TOP_POTIONBREWER_ACTIVE,
OVERLAY_TOP_POTIONBREWER_ACTIVE_GLOW,
@@ -1895,6 +1897,7 @@ public class Textures {
TURBINE_SMALL,
TURBINE_LARGE,
TURBINE_HUGE,
+ WIRELESS_HEADPHONES,
POCKET_MULTITOOL_CLOSED,
POCKET_MULTITOOL_BRANCHCUTTER,
POCKET_MULTITOOL_FILE,
diff --git a/src/main/java/gregtech/api/gui/modularui/GT_UITextures.java b/src/main/java/gregtech/api/gui/modularui/GT_UITextures.java
index b456026dbd..98612ad936 100644
--- a/src/main/java/gregtech/api/gui/modularui/GT_UITextures.java
+++ b/src/main/java/gregtech/api/gui/modularui/GT_UITextures.java
@@ -338,6 +338,8 @@ public class GT_UITextures {
public static final UITexture OVERLAY_BUTTON_ARROW_GREEN_DOWN = UITexture
.fullImage(GregTech.ID, "gui/overlay_button/arrow_green_down");
public static final UITexture OVERLAY_BUTTON_CYCLIC = UITexture.fullImage(GregTech.ID, "gui/overlay_button/cyclic");
+ public static final UITexture OVERLAY_BUTTON_SHUFFLE = UITexture
+ .fullImage(GregTech.ID, "gui/overlay_button/shuffle");
public static final UITexture OVERLAY_BUTTON_EMIT_ENERGY = UITexture
.fullImage(GregTech.ID, "gui/overlay_button/emit_energy");
public static final UITexture OVERLAY_BUTTON_EMIT_REDSTONE = UITexture
diff --git a/src/main/java/gregtech/api/metatileentity/CommonMetaTileEntity.java b/src/main/java/gregtech/api/metatileentity/CommonMetaTileEntity.java
index 5a2e88b242..d0b7a2e194 100644
--- a/src/main/java/gregtech/api/metatileentity/CommonMetaTileEntity.java
+++ b/src/main/java/gregtech/api/metatileentity/CommonMetaTileEntity.java
@@ -13,6 +13,9 @@ import com.gtnewhorizons.modularui.api.screen.ModularWindow;
import com.gtnewhorizons.modularui.api.screen.UIBuildContext;
import appeng.api.crafting.ICraftingIconProvider;
+import appeng.api.implementations.tiles.ISoundP2PHandler;
+import appeng.me.cache.helpers.TunnelCollection;
+import appeng.parts.p2p.PartP2PSound;
import gregtech.GT_Mod;
import gregtech.api.GregTech_API;
import gregtech.api.enums.ItemList;
@@ -29,7 +32,7 @@ import gregtech.api.util.GT_Log;
import gregtech.api.util.GT_Utility;
public abstract class CommonMetaTileEntity extends CoverableTileEntity
- implements IGregTechTileEntity, ICraftingIconProvider {
+ implements IGregTechTileEntity, ICraftingIconProvider, ISoundP2PHandler {
protected boolean mNeedsBlockUpdate = true, mNeedsUpdate = true, mSendClientData = false, mInventoryChanged = false;
@@ -337,4 +340,33 @@ public abstract class CommonMetaTileEntity extends CoverableTileEntity
}
return builder.build();
}
+
+ @Override
+ public boolean allowSoundProxying(PartP2PSound p2p) {
+ if (hasValidMetaTileEntity() && getMetaTileEntity() instanceof ISoundP2PHandler metaHandler) {
+ return metaHandler.allowSoundProxying(p2p);
+ }
+ return ISoundP2PHandler.super.allowSoundProxying(p2p);
+ }
+
+ @Override
+ public void onSoundP2PAttach(PartP2PSound p2p) {
+ if (hasValidMetaTileEntity() && getMetaTileEntity() instanceof ISoundP2PHandler metaHandler) {
+ metaHandler.onSoundP2PAttach(p2p);
+ }
+ }
+
+ @Override
+ public void onSoundP2PDetach(PartP2PSound p2p) {
+ if (hasValidMetaTileEntity() && getMetaTileEntity() instanceof ISoundP2PHandler metaHandler) {
+ metaHandler.onSoundP2PDetach(p2p);
+ }
+ }
+
+ @Override
+ public void onSoundP2POutputUpdate(PartP2PSound p2p, TunnelCollection<PartP2PSound> outputs) {
+ if (hasValidMetaTileEntity() && getMetaTileEntity() instanceof ISoundP2PHandler metaHandler) {
+ metaHandler.onSoundP2POutputUpdate(p2p, outputs);
+ }
+ }
}
diff --git a/src/main/java/gregtech/api/net/GT_PacketTypes.java b/src/main/java/gregtech/api/net/GT_PacketTypes.java
index 18fc5134ba..f59a7918a9 100644
--- a/src/main/java/gregtech/api/net/GT_PacketTypes.java
+++ b/src/main/java/gregtech/api/net/GT_PacketTypes.java
@@ -31,6 +31,7 @@ public enum GT_PacketTypes {
MULTI_TILE_ENTITY(18, new GT_Packet_MultiTileEntity(true)),
SEND_OREGEN_PATTERN(19, new GT_Packet_SendOregenPattern()),
TOOL_SWITCH_MODE(20, new GT_Packet_ToolSwitchMode()),
+ MUSIC_SYSTEM_DATA(21, new GT_Packet_MusicSystemData()),
// merge conflict prevention comment, keep a trailing comma above
;
diff --git a/src/main/java/gregtech/api/net/GT_Packet_MusicSystemData.java b/src/main/java/gregtech/api/net/GT_Packet_MusicSystemData.java
new file mode 100644
index 0000000000..13ebf49205
--- /dev/null
+++ b/src/main/java/gregtech/api/net/GT_Packet_MusicSystemData.java
@@ -0,0 +1,58 @@
+package gregtech.api.net;
+
+import net.minecraft.world.IBlockAccess;
+
+import com.google.common.io.ByteArrayDataInput;
+
+import gregtech.api.util.GT_MusicSystem;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+public class GT_Packet_MusicSystemData extends GT_Packet_New {
+
+ ByteBuf storedData;
+
+ public GT_Packet_MusicSystemData() {
+ super(true);
+ }
+
+ public GT_Packet_MusicSystemData(ByteBuf data) {
+ super(false);
+ this.storedData = data;
+ }
+
+ @Override
+ public byte getPacketID() {
+ return GT_PacketTypes.MUSIC_SYSTEM_DATA.id;
+ }
+
+ @Override
+ public void encode(ByteBuf aOut) {
+ if (storedData == null) {
+ return;
+ }
+ storedData.markReaderIndex();
+ final int len = storedData.readableBytes();
+ aOut.writeInt(len);
+ aOut.writeBytes(storedData);
+ storedData.resetReaderIndex();
+ }
+
+ @Override
+ public GT_Packet_New decode(ByteArrayDataInput aData) {
+ final int len = aData.readInt();
+ final byte[] fullData = new byte[len];
+ aData.readFully(fullData);
+ return new GT_Packet_MusicSystemData(Unpooled.wrappedBuffer(fullData));
+ }
+
+ @Override
+ public void process(IBlockAccess aWorld) {
+ if (aWorld == null || storedData == null) {
+ return;
+ }
+ storedData.markReaderIndex();
+ GT_MusicSystem.ClientSystem.loadUpdatedSources(storedData);
+ storedData.resetReaderIndex();
+ }
+}
diff --git a/src/main/java/gregtech/api/net/IGT_NetworkHandler.java b/src/main/java/gregtech/api/net/IGT_NetworkHandler.java
index 07c4a37030..8436a89e9b 100644
--- a/src/main/java/gregtech/api/net/IGT_NetworkHandler.java
+++ b/src/main/java/gregtech/api/net/IGT_NetworkHandler.java
@@ -12,6 +12,10 @@ public interface IGT_NetworkHandler {
void sendToAllAround(GT_Packet aPacket, TargetPoint aPosition);
+ default void sendToAll(GT_Packet aPacket) {
+ throw new UnsupportedOperationException("sendToAll not implemented");
+ }
+
void sendToServer(GT_Packet aPacket);
void sendPacketToAllPlayersInRange(World aWorld, GT_Packet aPacket, int aX, int aZ);
diff --git a/src/main/java/gregtech/api/util/GT_MusicSystem.java b/src/main/java/gregtech/api/util/GT_MusicSystem.java
new file mode 100644
index 0000000000..3a171e0395
--- /dev/null
+++ b/src/main/java/gregtech/api/util/GT_MusicSystem.java
@@ -0,0 +1,666 @@
+package gregtech.api.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.UUID;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.SoundEventAccessorComposite;
+import net.minecraft.client.audio.SoundRegistry;
+import net.minecraft.inventory.IInventory;
+import net.minecraft.item.ItemRecord;
+import net.minecraft.item.ItemStack;
+import net.minecraft.launchwrapper.Launch;
+import net.minecraft.util.ResourceLocation;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.jetbrains.annotations.ApiStatus;
+import org.joml.Vector4i;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.jcraft.jorbis.VorbisFile;
+
+import baubles.api.BaublesApi;
+import cpw.mods.fml.common.network.ByteBufUtils;
+import gregtech.GT_Mod;
+import gregtech.api.enums.GT_Values;
+import gregtech.api.net.GT_Packet_MusicSystemData;
+import gregtech.client.ElectricJukeboxSound;
+import gregtech.common.items.GT_WirelessHeadphones;
+import gregtech.common.tileentities.machines.basic.GT_MetaTileEntity_BetterJukebox;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+
+/**
+ * A system that keeps track of jukebox music tracks playing in different locations.
+ * Compared to vanilla jukebox handling, this allows music to resume playing after reloading the chunk the jukeboxes are
+ * in.
+ * It also allows the headphone item to modify the hearing range of a given disc, including other dimensions.
+ * <p>
+ * Vector4i coordinates point to X,Y,Z,Dimension of the source
+ *
+ * @author eigenraven
+ */
+public final class GT_MusicSystem {
+
+ private GT_MusicSystem() {}
+
+ public static final class MusicSource {
+
+ /** Flag keeping track of when the data of this source needs to be sent to clients again */
+ public boolean modified;
+ public final UUID sourceID;
+ /** Currently playing track */
+ public ResourceLocation currentRecord;
+ /** Headphone range */
+ public GT_MetaTileEntity_BetterJukebox.HeadphoneLimit headphoneLimit;
+ /**
+ * {@link System#currentTimeMillis()} at the time this record started playing, in server time
+ */
+ public long startedPlayingAtMs;
+ /** Time the record was playing for at the time it was serialized. */
+ public long playingForMs;
+ /** The origin of this source used for wireless headphone range calculations */
+ public final Vector4i originPosition = new Vector4i();
+ /** Number of blocks from {@link MusicSource#originPosition} the headphones can work */
+ public int headphoneBlockRange;
+ /** Densely packed parameters for each "emitter" associated with the source, for fast iteration */
+ public int[] emitterParameters; // [{x,y,z,dim,volume}, {x, ...}, {x, ...}, ...]
+ /** Offsets into the parameters array */
+ public static final int EMITTER_X = 0;
+ /** Offsets into the parameters array */
+ public static final int EMITTER_Y = 1;
+ /** Offsets into the parameters array */
+ public static final int EMITTER_Z = 2;
+ /** Offsets into the parameters array */
+ public static final int EMITTER_DIMENSION = 3;
+ /** 100 times the floating point "volume" used by the sound system for range calculations */
+ public static final int EMITTER_VOLUME_X_100 = 4;
+ /** Iteration stride for packed emitter parameters */
+ public static final int EMITTER_STRIDE = EMITTER_VOLUME_X_100 + 1;
+
+ public MusicSource(UUID sourceID) {
+ this.sourceID = sourceID;
+ }
+
+ public void resizeEmitterArray(int count) {
+ int len = count * EMITTER_STRIDE;
+ if (emitterParameters == null || emitterParameters.length != len) {
+ if (emitterParameters == null) {
+ emitterParameters = new int[len];
+ } else {
+ emitterParameters = Arrays.copyOf(emitterParameters, len);
+ }
+ modified = true;
+ }
+ }
+
+ public void setEmitter(int index, Vector4i position, float volume) {
+ int arrIndex = index * EMITTER_STRIDE;
+ if (arrIndex < 0 || arrIndex >= emitterParameters.length) {
+ throw new IndexOutOfBoundsException(
+ "Trying to access emitter with index " + index
+ + " in an array of "
+ + emitterParameters.length / EMITTER_STRIDE);
+ }
+
+ if (emitterParameters[arrIndex + EMITTER_X] != position.x) {
+ modified = true;
+ emitterParameters[arrIndex + EMITTER_X] = position.x;
+ }
+ if (emitterParameters[arrIndex + EMITTER_Y] != position.y) {
+ modified = true;
+ emitterParameters[arrIndex + EMITTER_Y] = position.y;
+ }
+ if (emitterParameters[arrIndex + EMITTER_Z] != position.z) {
+ modified = true;
+ emitterParameters[arrIndex + EMITTER_Z] = position.z;
+ }
+ if (emitterParameters[arrIndex + EMITTER_DIMENSION] != position.w) {
+ modified = true;
+ emitterParameters[arrIndex + EMITTER_DIMENSION] = position.w;
+ }
+ final int intVolume = (int) (volume * 100.0f);
+ if (emitterParameters[arrIndex + EMITTER_VOLUME_X_100] != intVolume) {
+ modified = true;
+ emitterParameters[arrIndex + EMITTER_VOLUME_X_100] = intVolume;
+ }
+ }
+
+ /** x squared */
+ private static int sq(int x) {
+ return x * x;
+ }
+
+ /** @return Index of closest emitter in range, or -1 if none is nearby. */
+ public int closestEmitter(int x, int y, int z, int dim) {
+ int closest = -1;
+ int closestDistanceSq = Integer.MAX_VALUE;
+ final int emittersCount = emitterParameters.length / EMITTER_STRIDE;
+ for (int i = 0; i < emittersCount; i++) {
+ final int offset = i * EMITTER_STRIDE;
+ final int eDim = emitterParameters[offset + EMITTER_DIMENSION];
+ if (eDim != dim) {
+ continue;
+ }
+ final int eX = emitterParameters[offset + EMITTER_X];
+ final int eY = emitterParameters[offset + EMITTER_Y];
+ final int eZ = emitterParameters[offset + EMITTER_Z];
+ final int distanceSq = sq(x - eX) + sq(y - eY) + sq(z - eZ);
+ if (distanceSq < closestDistanceSq) {
+ closestDistanceSq = distanceSq;
+ closest = i;
+ }
+ }
+ return closest;
+ }
+
+ public boolean inHeadphoneRange(int x, int y, int z, int dim) {
+ return switch (headphoneLimit) {
+ case BETWEEN_DIMENSIONS -> true;
+ case INSIDE_DIMENSION -> dim == originPosition.w;
+ case BLOCK_RANGE -> dim == originPosition.w
+ && originPosition.distanceSquared(x, y, z, dim) <= sq(headphoneBlockRange);
+ };
+ }
+
+ public void encode(final ByteBuf target) {
+ target.writeLong(sourceID.getMostSignificantBits());
+ target.writeLong(sourceID.getLeastSignificantBits());
+ if (currentRecord != null) {
+ final int duration = getMusicRecordDurations().getOrDefault(currentRecord, Integer.MAX_VALUE);
+ if (playingForMs >= duration) {
+ // Record already finished playing, let's not send it to the client anymore.
+ target.writeBoolean(false);
+ } else {
+ target.writeBoolean(true);
+ ByteBufUtils.writeUTF8String(target, currentRecord.getResourceDomain());
+ ByteBufUtils.writeUTF8String(target, currentRecord.getResourcePath());
+ }
+ } else {
+ target.writeBoolean(false);
+ }
+ target.writeByte((byte) headphoneLimit.ordinal());
+ ByteBufUtils.writeVarInt(target, headphoneBlockRange, 5);
+ target.writeLong(startedPlayingAtMs);
+ target.writeLong(playingForMs);
+ ByteBufUtils.writeVarInt(target, originPosition.x, 5);
+ ByteBufUtils.writeVarInt(target, originPosition.y, 5);
+ ByteBufUtils.writeVarInt(target, originPosition.z, 5);
+ ByteBufUtils.writeVarInt(target, originPosition.w, 5);
+ ByteBufUtils.writeVarInt(target, emitterParameters.length, 5);
+ for (int emitterParameter : emitterParameters) {
+ ByteBufUtils.writeVarInt(target, emitterParameter, 5);
+ }
+ }
+
+ public static MusicSource decode(final ByteBuf bytes) {
+ final long uuidMsb = bytes.readLong();
+ final long uuidLsb = bytes.readLong();
+ final MusicSource source = new MusicSource(new UUID(uuidMsb, uuidLsb));
+ final boolean hasRecord = bytes.readBoolean();
+ if (hasRecord) {
+ final String domain = ByteBufUtils.readUTF8String(bytes);
+ final String path = ByteBufUtils.readUTF8String(bytes);
+ source.currentRecord = new ResourceLocation(domain, path);
+ }
+ source.headphoneLimit = GT_MetaTileEntity_BetterJukebox.HeadphoneLimit.ENTRIES.get(bytes.readByte());
+ source.headphoneBlockRange = ByteBufUtils.readVarInt(bytes, 5);
+ source.startedPlayingAtMs = bytes.readLong();
+ source.playingForMs = bytes.readLong();
+ final int originX = ByteBufUtils.readVarInt(bytes, 5);
+ final int originY = ByteBufUtils.readVarInt(bytes, 5);
+ final int originZ = ByteBufUtils.readVarInt(bytes, 5);
+ final int originW = ByteBufUtils.readVarInt(bytes, 5);
+ source.originPosition.set(originX, originY, originZ, originW);
+ final int emittersLength = ByteBufUtils.readVarInt(bytes, 5);
+ source.emitterParameters = new int[emittersLength];
+ for (int i = 0; i < emittersLength; i++) {
+ source.emitterParameters[i] = ByteBufUtils.readVarInt(bytes, 5);
+ }
+
+ return source;
+ }
+
+ public void setRecord(final ResourceLocation record) {
+ setRecord(record, 0);
+ }
+
+ public void setRecord(final ResourceLocation record, long seekOffset) {
+ modified = true;
+ currentRecord = record;
+ playingForMs = seekOffset;
+ startedPlayingAtMs = System.currentTimeMillis() - seekOffset;
+ }
+ }
+
+ public static final class ServerSystem {
+
+ static final Object2ObjectOpenHashMap<UUID, MusicSource> musicSources = new Object2ObjectOpenHashMap<>(32);
+ static boolean musicSourcesDirty = false;
+
+ // Everything is synchronized to allow calling into here from the client when singleplayer synchronization is
+ // needed.
+
+ public static synchronized MusicSource registerOrGetMusicSource(UUID uuid) {
+ return musicSources.computeIfAbsent(uuid, (UUID id) -> {
+ musicSourcesDirty = true;
+ return new MusicSource(id);
+ });
+ }
+
+ public static synchronized void removeMusicSource(UUID uuid) {
+ musicSources.remove(uuid);
+ musicSourcesDirty = true;
+ }
+
+ public static synchronized void reset() {
+ musicSources.clear();
+ musicSourcesDirty = true;
+ }
+
+ public static synchronized ByteBuf serialize() {
+ final ByteBuf out = Unpooled.buffer();
+ ByteBufUtils.writeVarInt(out, musicSources.size(), 5);
+ musicSources.forEach((uuid, source) -> source.encode(out));
+ return out;
+ }
+
+ private static boolean tickAnyDirty;
+
+ public static synchronized void tick() {
+ final long now = System.currentTimeMillis();
+ tickAnyDirty = false;
+ musicSources.forEach((uuid, source) -> {
+ source.playingForMs = now - source.startedPlayingAtMs;
+ tickAnyDirty |= source.modified;
+ source.modified = false;
+ });
+ if (tickAnyDirty || musicSourcesDirty) {
+ musicSourcesDirty = false;
+ GT_Values.NW.sendToAll(new GT_Packet_MusicSystemData(serialize()));
+ }
+ }
+
+ static synchronized void onPauseMs(long pauseDurationMs) {
+ musicSources.forEach((uuid, source) -> { source.startedPlayingAtMs += pauseDurationMs; });
+ }
+ }
+
+ public static final class ClientSystem {
+
+ private static final class ClientSourceData {
+
+ /** Currently playing sound data */
+ public ElectricJukeboxSound currentSound = null;
+ /** Currently playing sound data */
+ public ResourceLocation currentSoundResource = null;
+ /**
+ * Server's timer value of when the music started, mostly meaningless except for checking for replays of the
+ * same music file.
+ */
+ public long originalStartTime = 0;
+ /**
+ * Computed client value of {@link System#currentTimeMillis()} of the time when the playback would have
+ * started if it was synchronized with the server.
+ */
+ public long clientReferenceStartTime = 0;
+ /** Flag for mark and sweep removal of outdated sounds */
+ public boolean markFlag = false;
+
+ public void resetMark() {
+ markFlag = false;
+ }
+
+ public void mark() {
+ markFlag = true;
+ }
+
+ public void clearSound(final Minecraft mc) {
+ currentSoundResource = null;
+ if (currentSound != null) {
+ mc.getSoundHandler()
+ .stopSound(currentSound);
+ currentSound = null;
+ originalStartTime = 0;
+ }
+ }
+
+ public boolean equalSound(final MusicSource source) {
+ if (source == null || source.currentRecord == null) {
+ return currentSoundResource == null;
+ } else {
+ return source.currentRecord.equals(currentSoundResource)
+ && originalStartTime == source.startedPlayingAtMs;
+ }
+ }
+
+ public void resetSound(final Minecraft mc, final MusicSource source, final boolean onHeadphones) {
+ clearSound(mc);
+ if (source == null || source.emitterParameters.length == 0) {
+ return;
+ }
+ int closestEmitter = onHeadphones ? 0
+ : source.closestEmitter(
+ (int) Math.floor(mc.thePlayer.posX),
+ (int) Math.floor(mc.thePlayer.posY),
+ (int) Math.floor(mc.thePlayer.posZ),
+ currentDimension);
+ if (closestEmitter < 0) {
+ return;
+ }
+ this.currentSoundResource = source.currentRecord;
+ this.originalStartTime = source.startedPlayingAtMs;
+ this.clientReferenceStartTime = System.currentTimeMillis() - source.playingForMs;
+ if (currentSoundResource != null) {
+ this.currentSound = makeRecord(source, closestEmitter);
+ if (onHeadphones) {
+ this.currentSound.volume = 1.0e20f;
+ }
+ mc.getSoundHandler()
+ .playSound(this.currentSound);
+ }
+ }
+
+ public void updateSound(final Minecraft mc, final MusicSource source, final boolean onHeadphones) {
+ if (source == null || currentSound == null || source.emitterParameters.length == 0) {
+ return;
+ }
+ int closestEmitter = onHeadphones ? 0
+ : source.closestEmitter(
+ (int) Math.floor(mc.thePlayer.posX),
+ (int) Math.floor(mc.thePlayer.posY),
+ (int) Math.floor(mc.thePlayer.posZ),
+ currentDimension);
+ if (closestEmitter < 0) {
+ currentSound.volume = 0.0f;
+ return;
+ }
+ final int offset = closestEmitter * MusicSource.EMITTER_STRIDE;
+ currentSound.xPosition = source.emitterParameters[offset + MusicSource.EMITTER_X];
+ currentSound.yPosition = source.emitterParameters[offset + MusicSource.EMITTER_Y];
+ currentSound.zPosition = source.emitterParameters[offset + MusicSource.EMITTER_Z];
+ currentSound.volume = onHeadphones ? 1.0e20f
+ : source.emitterParameters[offset + MusicSource.EMITTER_VOLUME_X_100] / 100.0f;
+ }
+ }
+
+ /** Latest music source list as synchronized from the server */
+ public static final Object2ObjectOpenHashMap<UUID, MusicSource> musicSources = new Object2ObjectOpenHashMap<>();
+
+ private static final Object2ObjectOpenHashMap<UUID, ClientSourceData> activelyPlayingMusic = new Object2ObjectOpenHashMap<>(
+ 16);
+
+ private static final ObjectOpenHashSet<UUID> wornHeadphones = new ObjectOpenHashSet<>();
+
+ private static int currentDimension = Integer.MIN_VALUE;
+
+ private static boolean soundsPaused = false;
+ private static long pauseTimeMs = 0;
+ private static int tickCounter = 0;
+
+ public static void loadUpdatedSources(ByteBuf bytes) {
+ final int sourceCount = ByteBufUtils.readVarInt(bytes, 5);
+ musicSources.clear();
+ for (int i = 0; i < sourceCount; i++) {
+ final MusicSource source = MusicSource.decode(bytes);
+ musicSources.put(source.sourceID, source);
+ }
+ }
+
+ private static ElectricJukeboxSound makeRecord(MusicSource source, int emitter) {
+ final int x = source.emitterParameters[emitter * MusicSource.EMITTER_STRIDE + MusicSource.EMITTER_X];
+ final int y = source.emitterParameters[emitter * MusicSource.EMITTER_STRIDE + MusicSource.EMITTER_Y];
+ final int z = source.emitterParameters[emitter * MusicSource.EMITTER_STRIDE + MusicSource.EMITTER_Z];
+ final float volume = source.emitterParameters[emitter * MusicSource.EMITTER_STRIDE
+ + MusicSource.EMITTER_VOLUME_X_100] / 100.0f;
+ return new ElectricJukeboxSound(source.currentRecord, volume, source.playingForMs, x, y, z);
+ }
+
+ public static void dumpAllRecordDurations() {
+ try {
+ final Minecraft mc = Minecraft.getMinecraft();
+ final SoundRegistry sm = mc.getSoundHandler().sndRegistry;
+ final SoundDurationsJson json = new SoundDurationsJson();
+ @SuppressWarnings("unchecked")
+ final Map<String, ItemRecord> allRecords = ItemRecord.field_150928_b;
+ // Cursed hack because JOrbis does not support seeking in anything other than filesystem files.
+ // This is only a dev tool, so it can be a bit slow and use real files here.
+ final File tempFile = File.createTempFile("mcdecode", ".ogg");
+ for (final ItemRecord record : allRecords.values()) {
+ try {
+ final ResourceLocation res = record.getRecordResource(record.recordName);
+ SoundEventAccessorComposite registryEntry = (SoundEventAccessorComposite) sm.getObject(res);
+ if (registryEntry == null) {
+ registryEntry = (SoundEventAccessorComposite) sm.getObject(
+ new ResourceLocation(res.getResourceDomain(), "records." + res.getResourcePath()));
+ }
+ final ResourceLocation realPath = registryEntry.func_148720_g()
+ .getSoundPoolEntryLocation();
+ try (final InputStream is = mc.getResourceManager()
+ .getResource(realPath)
+ .getInputStream(); final OutputStream os = FileUtils.openOutputStream(tempFile)) {
+ IOUtils.copy(is, os);
+ os.close();
+ final VorbisFile vf = new VorbisFile(tempFile.getAbsolutePath());
+ final float totalSeconds = vf.time_total(-1);
+ json.soundDurationsMs.put(res.toString(), (int) Math.ceil(totalSeconds * 1000.0f));
+ }
+ } catch (Exception e) {
+ GT_Mod.GT_FML_LOGGER.warn("Skipping {}", record.recordName, e);
+ }
+ }
+ GT_Mod.GT_FML_LOGGER.info(
+ "Sound durations json: \n{}",
+ new GsonBuilder().setPrettyPrinting()
+ .create()
+ .toJson(json));
+ tempFile.delete();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void reset() {
+ musicSources.clear();
+ tick();
+ }
+
+ public static void tick() {
+ final Minecraft mc = Minecraft.getMinecraft();
+ if (mc == null || mc.renderGlobal == null
+ || mc.theWorld == null
+ || mc.thePlayer == null
+ || mc.theWorld.provider == null) {
+ return;
+ }
+ tickCounter++;
+ final long now = System.currentTimeMillis();
+ currentDimension = mc.theWorld.provider.dimensionId;
+
+ headphoneCheck: if ((tickCounter % 20) == 0) {
+ wornHeadphones.clear();
+ final IInventory baubles = BaublesApi.getBaubles(mc.thePlayer);
+ if (baubles == null) {
+ break headphoneCheck;
+ }
+ final int baublesSize = baubles.getSizeInventory();
+ for (int i = 0; i < baublesSize; i++) {
+ final ItemStack item = baubles.getStackInSlot(i);
+ if (item != null && item.getItem() instanceof GT_WirelessHeadphones headphones) {
+ final UUID id = headphones.getBoundJukeboxUUID(item);
+ if (id != null) {
+ wornHeadphones.add(id);
+ }
+ }
+ }
+ }
+
+ activelyPlayingMusic.forEach((uuid, data) -> data.resetMark());
+
+ // Update and mark all present music streams
+ musicSources.forEach((uuid, musicSource) -> {
+ final ClientSourceData data = activelyPlayingMusic
+ .computeIfAbsent(uuid, ignored -> new ClientSourceData());
+ data.mark();
+ if (data.currentSound != null && !mc.getSoundHandler()
+ .isSoundPlaying(data.currentSound)
+ && (now - data.clientReferenceStartTime)
+ < getMusicRecordDurations().getOrDefault(data.currentSoundResource, Integer.MAX_VALUE)) {
+ data.currentSound = null;
+ data.currentSoundResource = null;
+ }
+ final boolean onHeadphones = wornHeadphones.contains(uuid);
+ if (!data.equalSound(musicSource)) {
+ data.resetSound(mc, musicSource, onHeadphones);
+ } else {
+ data.updateSound(mc, musicSource, onHeadphones);
+ }
+ });
+
+ // Sweep no longer present music streams
+ final var entries = activelyPlayingMusic.object2ObjectEntrySet()
+ .fastIterator();
+ while (entries.hasNext()) {
+ final ClientSourceData entry = entries.next()
+ .getValue();
+ if (!entry.markFlag) {
+ entry.clearSound(mc);
+ entries.remove();
+ }
+ }
+ }
+
+ @ApiStatus.Internal
+ public static void onSoundBatchStop() {
+ // All music was forcibly stopped, we can forget about the currently playing music
+ // and let the update loop re-start them on next tick
+ long now = System.currentTimeMillis();
+ activelyPlayingMusic.forEach((uuid, data) -> {
+ data.currentSound = null;
+ data.currentSoundResource = null;
+ final MusicSource source = musicSources.get(uuid);
+ if (source == null) {
+ return;
+ }
+ source.playingForMs = now - data.clientReferenceStartTime;
+ });
+ }
+
+ @ApiStatus.Internal
+ public static void onSoundBatchPause() {
+ if (soundsPaused) {
+ return;
+ }
+ soundsPaused = true;
+ pauseTimeMs = System.currentTimeMillis();
+ }
+
+ @ApiStatus.Internal
+ public static void onSoundBatchResume() {
+ if (!soundsPaused) {
+ return;
+ }
+ final Minecraft mc = Minecraft.getMinecraft();
+ if (mc == null || mc.renderGlobal == null
+ || mc.theWorld == null
+ || mc.thePlayer == null
+ || mc.theWorld.provider == null) {
+ return;
+ }
+ soundsPaused = false;
+
+ if (!(mc.isSingleplayer() && !mc.getIntegratedServer()
+ .getPublic())) {
+ return;
+ }
+ final long now = System.currentTimeMillis();
+ final long pauseDurationMs = now - pauseTimeMs;
+
+ // We manipulate server state here, because we've checked this is singleplayer pausing.
+ GT_MusicSystem.ServerSystem.onPauseMs(pauseDurationMs);
+ musicSources.forEach((uuid, source) -> { source.startedPlayingAtMs += pauseDurationMs; });
+ activelyPlayingMusic.forEach((uuid, data) -> {
+ data.originalStartTime += pauseDurationMs;
+ data.clientReferenceStartTime += pauseDurationMs;
+ });
+
+ }
+ }
+
+ private static final Object2IntOpenHashMap<ResourceLocation> musicRecordDurations = new Object2IntOpenHashMap<>();
+ private static volatile boolean musicRecordsInitialized;
+
+ /** For GSON consumption */
+ public static class SoundDurationsJson {
+
+ public Map<String, Integer> soundDurationsMs = new TreeMap<>();
+ }
+
+ public static Object2IntOpenHashMap<ResourceLocation> getMusicRecordDurations() {
+ if (musicRecordsInitialized) {
+ return musicRecordDurations;
+ }
+ // double-checked locking for efficiency
+ synchronized (musicRecordDurations) {
+ if (musicRecordsInitialized) {
+ return musicRecordDurations;
+ }
+
+ final Gson gson = new Gson();
+
+ try {
+ final ArrayList<URL> candidates = Collections.list(
+ GT_MusicSystem.class.getClassLoader()
+ .getResources("soundmeta/durations.json"));
+ final Path configPath = Launch.minecraftHome.toPath()
+ .resolve("config")
+ .resolve("soundmeta")
+ .resolve("durations.json");
+ if (Files.exists(configPath)) {
+ candidates.add(
+ configPath.toUri()
+ .toURL());
+ }
+ for (final URL url : candidates) {
+ try {
+ final String objectJson = IOUtils.toString(url);
+ final SoundDurationsJson object = gson.fromJson(objectJson, SoundDurationsJson.class);
+ if (object == null || object.soundDurationsMs == null || object.soundDurationsMs.isEmpty()) {
+ continue;
+ }
+ for (final var entry : object.soundDurationsMs.entrySet()) {
+ musicRecordDurations.put(
+ new ResourceLocation(entry.getKey()),
+ entry.getValue()
+ .intValue());
+ }
+ } catch (Exception e) {
+ GT_Mod.GT_FML_LOGGER.error("Could not parse sound durations from {}", url, e);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ musicRecordsInitialized = true;
+ return musicRecordDurations;
+ }
+ }
+
+}