aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRaven Szewczyk <git@eigenraven.me>2024-08-15 22:44:13 +0100
committerGitHub <noreply@github.com>2024-08-15 21:44:13 +0000
commit5decfda1dd1b3bbfdb1098f409aa50162ea32dc2 (patch)
treefd3dd1c237a0c8c4d4953dd7e529f5b5d89e6e34 /src
parent799d5edf98e7e9f6152f432cdc48eac858398c8a (diff)
downloadGT5-Unofficial-5decfda1dd1b3bbfdb1098f409aa50162ea32dc2.tar.gz
GT5-Unofficial-5decfda1dd1b3bbfdb1098f409aa50162ea32dc2.tar.bz2
GT5-Unofficial-5decfda1dd1b3bbfdb1098f409aa50162ea32dc2.zip
Electric jukebox (#2827)
* GT music system * Minor fix for some glitches when switching dimensions with P2Ps on both sides * Most features implemented except headphones * Implement wireless headphones * Disable debug mode * Spotless --------- Co-authored-by: Martin Robertz <dream-master@gmx.net>
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/github/bartimaeusnek/bartworks/common/net/BW_Network.java9
-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
-rw-r--r--src/main/java/gregtech/client/ElectricJukeboxSound.java95
-rw-r--r--src/main/java/gregtech/client/ISeekingSound.java14
-rw-r--r--src/main/java/gregtech/client/SeekingOggCodec.java98
-rw-r--r--src/main/java/gregtech/common/GT_Client.java18
-rw-r--r--src/main/java/gregtech/common/GT_Network.java9
-rw-r--r--src/main/java/gregtech/common/GT_Proxy.java17
-rw-r--r--src/main/java/gregtech/common/items/GT_WirelessHeadphones.java118
-rw-r--r--src/main/java/gregtech/common/misc/GT_Command.java23
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/basic/GT_MetaTileEntity_BetterJukebox.java707
-rw-r--r--src/main/java/gregtech/loaders/preload/GT_Loader_Item_Block_And_Fluid.java2
-rw-r--r--src/main/java/gregtech/loaders/preload/GT_Loader_MetaTileEntities.java40
-rw-r--r--src/main/java/gregtech/mixin/Mixin.java6
-rw-r--r--src/main/java/gtPlusPlus/core/block/machine/Machine_SuperJukebox.java18
-rw-r--r--src/main/resources/META-INF/ggfab_at.cfg2
-rw-r--r--src/main/resources/META-INF/gregtech_at.cfg (renamed from src/main/resources/META-INF/tectech_at.cfg)8
-rw-r--r--src/main/resources/assets/gregtech/lang/en_US.lang6
-rw-r--r--src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_SIDE_JUKEBOX.pngbin0 -> 149 bytes
-rw-r--r--src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_TOP_JUKEBOX.pngbin0 -> 208 bytes
-rw-r--r--src/main/resources/assets/gregtech/textures/gui/overlay_button/shuffle.pngbin0 -> 354 bytes
-rw-r--r--src/main/resources/assets/gregtech/textures/items/gt.WirelessHeadphones.pngbin0 -> 232 bytes
-rw-r--r--src/main/resources/assets/miscutils/lang/en_US.lang1
-rw-r--r--src/main/resources/soundmeta/durations.json16
-rw-r--r--src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerInnerMixin.java33
-rw-r--r--src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerMixin.java52
35 files changed, 2070 insertions, 10 deletions
diff --git a/src/main/java/com/github/bartimaeusnek/bartworks/common/net/BW_Network.java b/src/main/java/com/github/bartimaeusnek/bartworks/common/net/BW_Network.java
index b5e002ef29..01085e2b31 100644
--- a/src/main/java/com/github/bartimaeusnek/bartworks/common/net/BW_Network.java
+++ b/src/main/java/com/github/bartimaeusnek/bartworks/common/net/BW_Network.java
@@ -121,6 +121,15 @@ public class BW_Network extends MessageToMessageCodec<FMLProxyPacket, GT_Packet_
}
@Override
+ public void sendToAll(@Nonnull GT_Packet aPacket) {
+ this.mChannel.get(Side.SERVER)
+ .attr(FMLOutboundHandler.FML_MESSAGETARGET)
+ .set(FMLOutboundHandler.OutboundTarget.ALL);
+ this.mChannel.get(Side.SERVER)
+ .writeAndFlush(aPacket);
+ }
+
+ @Override
public void sendToServer(@Nonnull GT_Packet aPacket) {
this.mChannel.get(Side.CLIENT)
.attr(FMLOutboundHandler.FML_MESSAGETARGET)
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;
+ }
+ }
+
+}
diff --git a/src/main/java/gregtech/client/ElectricJukeboxSound.java b/src/main/java/gregtech/client/ElectricJukeboxSound.java
new file mode 100644
index 0000000000..0ea81ab537
--- /dev/null
+++ b/src/main/java/gregtech/client/ElectricJukeboxSound.java
@@ -0,0 +1,95 @@
+package gregtech.client;
+
+import net.minecraft.client.audio.ISound;
+import net.minecraft.client.audio.ITickableSound;
+import net.minecraft.util.ResourceLocation;
+
+public class ElectricJukeboxSound implements ISound, ISeekingSound, ITickableSound {
+
+ public final ResourceLocation soundResource;
+ public float volume = 1.0F;
+ public float pitch = 1.0F;
+ public float xPosition;
+ public float yPosition;
+ public float zPosition;
+ public boolean repeating = false;
+ public int repeatDelay = 0;
+ public ISound.AttenuationType attenuationType = AttenuationType.LINEAR;
+ public boolean donePlaying = false;
+
+ public final long seekMs;
+
+ public ElectricJukeboxSound(ResourceLocation resource, long seekMs) {
+ this.soundResource = resource;
+ this.seekMs = seekMs;
+ }
+
+ public ElectricJukeboxSound(ResourceLocation soundResource, float volume, long seekMs, float xPosition,
+ float yPosition, float zPosition) {
+ this(soundResource, seekMs);
+ this.volume = volume;
+ this.xPosition = xPosition;
+ this.yPosition = yPosition;
+ this.zPosition = zPosition;
+ }
+
+ @Override
+ public long getSeekMillisecondOffset() {
+ return seekMs;
+ }
+
+ @Override
+ public ResourceLocation getPositionedSoundLocation() {
+ return soundResource;
+ }
+
+ @Override
+ public boolean canRepeat() {
+ return repeating;
+ }
+
+ @Override
+ public int getRepeatDelay() {
+ return repeatDelay;
+ }
+
+ @Override
+ public float getVolume() {
+ return volume;
+ }
+
+ @Override
+ public float getPitch() {
+ return pitch;
+ }
+
+ @Override
+ public float getXPosF() {
+ return xPosition;
+ }
+
+ @Override
+ public float getYPosF() {
+ return yPosition;
+ }
+
+ @Override
+ public float getZPosF() {
+ return zPosition;
+ }
+
+ @Override
+ public AttenuationType getAttenuationType() {
+ return attenuationType;
+ }
+
+ @Override
+ public boolean isDonePlaying() {
+ return donePlaying;
+ }
+
+ @Override
+ public void update() {
+ // no-op
+ }
+}
diff --git a/src/main/java/gregtech/client/ISeekingSound.java b/src/main/java/gregtech/client/ISeekingSound.java
new file mode 100644
index 0000000000..091b56f802
--- /dev/null
+++ b/src/main/java/gregtech/client/ISeekingSound.java
@@ -0,0 +1,14 @@
+package gregtech.client;
+
+import net.minecraft.client.audio.ISound;
+
+/**
+ * Metadata on a sound object used to seek it when starting playback.
+ */
+public interface ISeekingSound extends ISound {
+
+ /**
+ * @return The number of milliseconds to seek by.
+ */
+ long getSeekMillisecondOffset();
+}
diff --git a/src/main/java/gregtech/client/SeekingOggCodec.java b/src/main/java/gregtech/client/SeekingOggCodec.java
new file mode 100644
index 0000000000..7949578f4f
--- /dev/null
+++ b/src/main/java/gregtech/client/SeekingOggCodec.java
@@ -0,0 +1,98 @@
+package gregtech.client;
+
+import java.net.URL;
+
+import javax.sound.sampled.AudioFormat;
+
+import net.minecraft.util.ResourceLocation;
+
+import org.apache.commons.lang3.StringUtils;
+
+import paulscode.sound.SoundBuffer;
+import paulscode.sound.codecs.CodecJOrbis;
+
+/**
+ * A somewhat hacky codec that allows starting music playback from the middle of a Ogg Vorbis file.
+ * Registers for URLs of the form: {@literal jar:blah/blah.jar!blah/music.ogg?seek_ms=5000&ext=.gt5oggseek}
+ */
+public class SeekingOggCodec extends CodecJOrbis {
+
+ public static final String EXTENSION = "gt5oggseek";
+
+ private volatile boolean fullyInitialized = false;
+ private volatile SoundBuffer nextBuffer = null;
+
+ /**
+ * Encodes the given millisecond seek amount into a URL/resource name suffix that can be appended to the sound path
+ * to start playing from that point onwards.
+ */
+ public static String getEncodedSeekSuffxix(long milliseconds) {
+ return String.format("?seek_ms=%d&ext=." + EXTENSION, milliseconds);
+ }
+
+ /**
+ * @return The given path with the seeking metadata stripped from the URL
+ */
+ public static String stripSeekMetadata(String path) {
+ while (path.endsWith("." + EXTENSION)) {
+ int qMark = path.lastIndexOf('?');
+ if (qMark == -1) {
+ break;
+ }
+ path = path.substring(0, qMark);
+ }
+ return path;
+ }
+
+ /**
+ * Turns the input sound ResourceLocation into one that is seeked forward the given number of milliseconds
+ */
+ public static ResourceLocation seekResource(ResourceLocation loc, long milliseconds) {
+ String original = loc.getResourcePath();
+ original = stripSeekMetadata(original);
+ return new ResourceLocation(loc.getResourceDomain(), original + getEncodedSeekSuffxix(milliseconds));
+ }
+
+ @Override
+ public boolean initialize(URL url) {
+ final String textUrl = url.toString();
+ final String[] queryParts = url.getQuery()
+ .split("&");
+ long seekMs = 0;
+ for (String part : queryParts) {
+ if (!part.startsWith("seek_ms=")) {
+ continue;
+ }
+ part = StringUtils.removeStart(part, "seek_ms=");
+ seekMs = Long.parseLong(part);
+ }
+
+ if (!super.initialize(url)) {
+ return false;
+ }
+
+ final AudioFormat format = this.getAudioFormat();
+ final long samplesPerS = (long) format.getSampleRate();
+ final int bytesPerSample = (format.getChannels() * format.getSampleSizeInBits() / 8);
+
+ long remainingBytes = seekMs * samplesPerS * bytesPerSample / 1000L;
+
+ while (remainingBytes > 0) {
+ final SoundBuffer buf = read();
+ if (buf == null || buf.audioData == null) {
+ return false;
+ }
+ remainingBytes -= buf.audioData.length;
+ }
+
+ synchronized (this) {
+ fullyInitialized = true;
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized boolean initialized() {
+ return fullyInitialized;
+ }
+}
diff --git a/src/main/java/gregtech/common/GT_Client.java b/src/main/java/gregtech/common/GT_Client.java
index 0bf578e303..79c8256c04 100644
--- a/src/main/java/gregtech/common/GT_Client.java
+++ b/src/main/java/gregtech/common/GT_Client.java
@@ -35,6 +35,7 @@ import net.minecraft.util.MovingObjectPosition;
import net.minecraft.world.ChunkCoordIntPair;
import net.minecraft.world.World;
import net.minecraftforge.client.event.DrawBlockHighlightEvent;
+import net.minecraftforge.client.event.sound.SoundSetupEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.util.ForgeDirection;
import net.minecraftforge.oredict.OreDictionary;
@@ -84,9 +85,11 @@ import gregtech.api.util.GT_ClientPreference;
import gregtech.api.util.GT_CoverBehaviorBase;
import gregtech.api.util.GT_Log;
import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_MusicSystem;
import gregtech.api.util.GT_PlayedSound;
import gregtech.api.util.GT_Utility;
import gregtech.api.util.WorldSpawnedEventBuilder;
+import gregtech.client.SeekingOggCodec;
import gregtech.common.blocks.GT_Item_Machines;
import gregtech.common.render.GT_CapeRenderer;
import gregtech.common.render.GT_FlaskRenderer;
@@ -105,6 +108,8 @@ import gregtech.loaders.misc.GT_Bees;
import gregtech.loaders.preload.GT_PreLoad;
import gregtech.nei.NEI_GT_Config;
import ic2.api.tile.IWrenchable;
+import paulscode.sound.SoundSystemConfig;
+import paulscode.sound.SoundSystemException;
// Referenced classes of package gregtech.common:
// GT_Proxy
@@ -692,6 +697,16 @@ public class GT_Client extends GT_Proxy implements Runnable {
}
}
+ @SubscribeEvent
+ @SuppressWarnings("unused") // used by the event bus
+ public void onSoundSetup(SoundSetupEvent event) {
+ try {
+ SoundSystemConfig.setCodec(SeekingOggCodec.EXTENSION, SeekingOggCodec.class);
+ } catch (SoundSystemException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
@Override
public void run() {
GT_Log.out.println("GT_Mod: Downloading Cape List.");
@@ -727,6 +742,7 @@ public class GT_Client extends GT_Proxy implements Runnable {
public void onClientConnectedToServerEvent(FMLNetworkEvent.ClientConnectedToServerEvent aEvent) {
mFirstTick = true;
mReloadCount++;
+ GT_MusicSystem.ClientSystem.reset();
// For utility methods elsewhere.
calculateMaxPlasmaTurbineEfficiency();
}
@@ -891,6 +907,8 @@ public class GT_Client extends GT_Proxy implements Runnable {
@SubscribeEvent
public void onClientTickEvent(cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent aEvent) {
if (aEvent.phase == cpw.mods.fml.common.gameevent.TickEvent.Phase.END) {
+ GT_MusicSystem.ClientSystem.tick();
+
if (changeDetected > 0) changeDetected--;
final int newHideValue = shouldHeldItemHideThings();
if (newHideValue != hideValue) {
diff --git a/src/main/java/gregtech/common/GT_Network.java b/src/main/java/gregtech/common/GT_Network.java
index 95385c92ea..bf5826e9ae 100644
--- a/src/main/java/gregtech/common/GT_Network.java
+++ b/src/main/java/gregtech/common/GT_Network.java
@@ -108,6 +108,15 @@ public class GT_Network extends MessageToMessageCodec<FMLProxyPacket, GT_Packet>
}
@Override
+ public void sendToAll(GT_Packet aPacket) {
+ this.mChannel.get(Side.SERVER)
+ .attr(FMLOutboundHandler.FML_MESSAGETARGET)
+ .set(FMLOutboundHandler.OutboundTarget.ALL);
+ this.mChannel.get(Side.SERVER)
+ .writeAndFlush(aPacket);
+ }
+
+ @Override
public void sendToServer(GT_Packet aPacket) {
this.mChannel.get(Side.CLIENT)
.attr(FMLOutboundHandler.FML_MESSAGETARGET)
diff --git a/src/main/java/gregtech/common/GT_Proxy.java b/src/main/java/gregtech/common/GT_Proxy.java
index 7e83ae97a7..8e0c636c98 100644
--- a/src/main/java/gregtech/common/GT_Proxy.java
+++ b/src/main/java/gregtech/common/GT_Proxy.java
@@ -67,6 +67,7 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.monster.EntityEnderman;
import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.projectile.EntityArrow;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
@@ -146,6 +147,7 @@ import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
import gregtech.api.items.GT_MetaGenerated_Item;
import gregtech.api.items.GT_MetaGenerated_Tool;
+import gregtech.api.net.GT_Packet_MusicSystemData;
import gregtech.api.objects.GT_ChunkManager;
import gregtech.api.objects.GT_ItemStack;
import gregtech.api.objects.GT_UO_DimensionList;
@@ -161,6 +163,7 @@ import gregtech.api.util.GT_CoverBehaviorBase;
import gregtech.api.util.GT_LanguageManager;
import gregtech.api.util.GT_Log;
import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_MusicSystem;
import gregtech.api.util.GT_OreDictUnificator;
import gregtech.api.util.GT_Recipe;
import gregtech.api.util.GT_RecipeRegistrator;
@@ -1383,6 +1386,8 @@ public abstract class GT_Proxy implements IGT_Mod, IGuiHandler, IFuelHandler {
GT_Log.out.println("GT_Mod: ServerStarting-Phase started!");
GT_Log.ore.println("GT_Mod: ServerStarting-Phase started!");
+ GT_MusicSystem.ServerSystem.reset();
+
this.mUniverse = null;
this.isFirstServerWorldTick = true;
for (FluidContainerRegistry.FluidContainerData tData : FluidContainerRegistry
@@ -1433,6 +1438,7 @@ public abstract class GT_Proxy implements IGT_Mod, IGuiHandler, IFuelHandler {
}
public void onServerStopping() {
+ GT_MusicSystem.ServerSystem.reset();
File tSaveDirectory = getSaveDirectory();
GregTech_API.sWirelessRedstone.clear();
if (tSaveDirectory != null) {
@@ -1564,6 +1570,15 @@ public abstract class GT_Proxy implements IGT_Mod, IGuiHandler, IFuelHandler {
}
@SubscribeEvent
+ public void onPlayerJoinEvent(cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent event) {
+ if (event.player == null || event.player.isClientWorld()
+ || !(event.player instanceof EntityPlayerMP mpPlayer)) {
+ return;
+ }
+ GT_Values.NW.sendToPlayer(new GT_Packet_MusicSystemData(GT_MusicSystem.ServerSystem.serialize()), mpPlayer);
+ }
+
+ @SubscribeEvent
public void onOreGenEvent(OreGenEvent.GenerateMinable aGenerator) {
if ((this.mDisableVanillaOres) && ((aGenerator.generator instanceof WorldGenMinable))
&& (PREVENTED_ORES.contains(aGenerator.type))) {
@@ -2223,9 +2238,9 @@ public abstract class GT_Proxy implements IGT_Mod, IGuiHandler, IFuelHandler {
if (aEvent.side.isServer()) {
if (aEvent.phase == TickEvent.Phase.START) {
TICK_LOCK.lock();
-
} else {
TICK_LOCK.unlock();
+ GT_MusicSystem.ServerSystem.tick();
}
// Making sure it is being freed up in order to prevent exploits or Garbage Collection mishaps.
diff --git a/src/main/java/gregtech/common/items/GT_WirelessHeadphones.java b/src/main/java/gregtech/common/items/GT_WirelessHeadphones.java
new file mode 100644
index 0000000000..98a6eddb2c
--- /dev/null
+++ b/src/main/java/gregtech/common/items/GT_WirelessHeadphones.java
@@ -0,0 +1,118 @@
+package gregtech.common.items;
+
+import java.util.List;
+import java.util.UUID;
+
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ChatComponentTranslation;
+import net.minecraft.util.StatCollector;
+import net.minecraft.world.World;
+import net.minecraftforge.common.util.Constants;
+
+import baubles.api.BaubleType;
+import baubles.api.IBauble;
+import gregtech.api.enums.ItemList;
+import gregtech.api.items.GT_Generic_Item;
+import gregtech.api.metatileentity.BaseMetaTileEntity;
+import gregtech.common.tileentities.machines.basic.GT_MetaTileEntity_BetterJukebox;
+
+public class GT_WirelessHeadphones extends GT_Generic_Item implements IBauble {
+
+ public static final String NBTKEY_JUKEBOX_COORDINATES = "jukeboxCoords";
+
+ public GT_WirelessHeadphones() {
+ super("WirelessHeadphones", "Wireless Headphones", null);
+ setMaxStackSize(1);
+
+ ItemList.WirelessHeadphones.set(this);
+ }
+
+ @Override
+ public void addInformation(ItemStack aStack, EntityPlayer aPlayer, List<String> aList, boolean aF3_H) {
+ if (aStack == null) {
+ return;
+ }
+ final NBTTagCompound tag = aStack.getTagCompound();
+ if (tag == null || !tag.hasKey(NBTKEY_JUKEBOX_COORDINATES, Constants.NBT.TAG_STRING)) {
+ aList.add(StatCollector.translateToLocal("GT5U.machines.betterjukebox.headphonesunbound"));
+ } else {
+ aList.add(StatCollector.translateToLocal("GT5U.machines.betterjukebox.headphonesbound"));
+ aList.add(tag.getString(NBTKEY_JUKEBOX_COORDINATES));
+ }
+ }
+
+ @Override
+ public boolean doesSneakBypassUse(World aWorld, int aX, int aY, int aZ, EntityPlayer aPlayer) {
+ return false;
+ }
+
+ @Override
+ public boolean onItemUseFirst(ItemStack stack, EntityPlayer player, World world, int x, int y, int z, int side,
+ float hitX, float hitY, float hitZ) {
+ final TileEntity pointedTe = world.getTileEntity(x, y, z);
+ if (!(pointedTe instanceof BaseMetaTileEntity mte)) {
+ return false;
+ }
+ if (!(mte.getMetaTileEntity() instanceof GT_MetaTileEntity_BetterJukebox jukebox)) {
+ return false;
+ }
+ final UUID uuid = jukebox.jukeboxUuid;
+ if (uuid == GT_MetaTileEntity_BetterJukebox.UNSET_UUID) {
+ return false;
+ }
+ if (!world.isRemote) {
+ final NBTTagCompound tag = (stack.getTagCompound() == null) ? new NBTTagCompound() : stack.getTagCompound();
+ tag.setLong(GT_MetaTileEntity_BetterJukebox.NBTKEY_UUID_LOW, uuid.getLeastSignificantBits());
+ tag.setLong(GT_MetaTileEntity_BetterJukebox.NBTKEY_UUID_HIGH, uuid.getMostSignificantBits());
+ tag.setString(
+ NBTKEY_JUKEBOX_COORDINATES,
+ String.format("(%d, %d, %d) @ %d", x, y, z, world.provider.dimensionId));
+ stack.setTagCompound(tag);
+
+ player.addChatMessage(new ChatComponentTranslation("GT5U.machines.betterjukebox.headphonesbound"));
+ }
+ return true;
+ }
+
+ public static UUID getBoundJukeboxUUID(ItemStack stack) {
+ if (stack == null || stack.getTagCompound() == null) {
+ return null;
+ }
+ final NBTTagCompound tag = stack.getTagCompound();
+ if (!tag.hasKey(GT_MetaTileEntity_BetterJukebox.NBTKEY_UUID_LOW, Constants.NBT.TAG_ANY_NUMERIC)
+ || !tag.hasKey(GT_MetaTileEntity_BetterJukebox.NBTKEY_UUID_HIGH, Constants.NBT.TAG_ANY_NUMERIC)) {
+ return null;
+ }
+ final long idLow = tag.getLong(GT_MetaTileEntity_BetterJukebox.NBTKEY_UUID_LOW);
+ final long idHigh = tag.getLong(GT_MetaTileEntity_BetterJukebox.NBTKEY_UUID_HIGH);
+ return new UUID(idHigh, idLow);
+ }
+
+ @Override
+ public BaubleType getBaubleType(ItemStack itemStack) {
+ return BaubleType.UNIVERSAL;
+ }
+
+ @Override
+ public void onWornTick(ItemStack itemStack, EntityLivingBase entityLivingBase) {}
+
+ @Override
+ public void onEquipped(ItemStack itemStack, EntityLivingBase entityLivingBase) {}
+
+ @Override
+ public void onUnequipped(ItemStack itemStack, EntityLivingBase entityLivingBase) {}
+
+ @Override
+ public boolean canEquip(ItemStack itemStack, EntityLivingBase entityLivingBase) {
+ return true;
+ }
+
+ @Override
+ public boolean canUnequip(ItemStack itemStack, EntityLivingBase entityLivingBase) {
+ return true;
+ }
+}
diff --git a/src/main/java/gregtech/common/misc/GT_Command.java b/src/main/java/gregtech/common/misc/GT_Command.java
index 3bf73b6300..8e342aa928 100644
--- a/src/main/java/gregtech/common/misc/GT_Command.java
+++ b/src/main/java/gregtech/common/misc/GT_Command.java
@@ -19,9 +19,11 @@ import net.minecraft.util.EnumChatFormatting;
import com.gtnewhorizon.structurelib.StructureLib;
+import cpw.mods.fml.relauncher.FMLLaunchHandler;
import gregtech.GT_Mod;
import gregtech.api.enums.GT_Values;
import gregtech.api.objects.GT_ChunkManager;
+import gregtech.api.util.GT_MusicSystem;
import gregtech.api.util.GT_Utility;
import gregtech.common.GT_Pollution;
import gregtech.common.misc.spaceprojects.SpaceProjectManager;
@@ -35,13 +37,13 @@ public final class GT_Command extends CommandBase {
@Override
public String getCommandUsage(ICommandSender sender) {
- return "Usage: gt <subcommand>. Valid subcommands are: toggle, chunks, pollution.";
+ return "Usage: gt <subcommand>. Valid subcommands are: toggle, chunks, pollution, global_energy_add, global_energy_set, global_energy_join, dump_music_durations.";
}
private void printHelp(ICommandSender sender) {
sender.addChatMessage(
new ChatComponentText(
- "Usage: gt <toggle|chunks|pollution|global_energy_add|global_energy_set|global_energy_join>"));
+ "Usage: gt <toggle|chunks|pollution|global_energy_add|global_energy_set|global_energy_join|dump_music_durations>"));
sender.addChatMessage(new ChatComponentText("\"toggle D1\" - toggles general.Debug (D1)"));
sender.addChatMessage(new ChatComponentText("\"toggle D2\" - toggles general.Debug2 (D2)"));
sender.addChatMessage(new ChatComponentText("\"toggle debugCleanroom\" - toggles cleanroom debug log"));
@@ -96,6 +98,9 @@ public final class GT_Command extends CommandBase {
sender.addChatMessage(
new ChatComponentText(
"Usage:" + EnumChatFormatting.RED + " global_energy_display " + EnumChatFormatting.BLUE + "[Name]"));
+ sender.addChatMessage(
+ new ChatComponentText(
+ "\"dump_music_durations\" - dumps soundmeta/durations.json for all registered records in the game to the log. Client-only"));
}
@Override
@@ -110,7 +115,8 @@ public final class GT_Command extends CommandBase {
"global_energy_add",
"global_energy_set",
"global_energy_join",
- "global_energy_display")
+ "global_energy_display",
+ "dump_music_durations")
.anyMatch(s -> s.startsWith(test)))) {
Stream
.of(
@@ -120,7 +126,8 @@ public final class GT_Command extends CommandBase {
"global_energy_add",
"global_energy_set",
"global_energy_join",
- "global_energy_display")
+ "global_energy_display",
+ "dump_music_durations")
.filter(s -> test.isEmpty() || s.startsWith(test))
.forEach(l::add);
} else if (test.equals("toggle")) {
@@ -330,6 +337,14 @@ public final class GT_Command extends CommandBase {
+ "."));
}
+ case "dump_music_durations" -> {
+ if (!FMLLaunchHandler.side()
+ .isClient()) {
+ sender
+ .addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "This command is client-only."));
+ }
+ GT_MusicSystem.ClientSystem.dumpAllRecordDurations();
+ }
default -> {
sender
.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "Invalid command/syntax detected."));
diff --git a/src/main/java/gregtech/common/tileentities/machines/basic/GT_MetaTileEntity_BetterJukebox.java b/src/main/java/gregtech/common/tileentities/machines/basic/GT_MetaTileEntity_BetterJukebox.java
new file mode 100644
index 0000000000..da14d7c7cb
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/basic/GT_MetaTileEntity_BetterJukebox.java
@@ -0,0 +1,707 @@
+package gregtech.common.tileentities.machines.basic;
+
+import static gregtech.api.enums.GT_Values.V;
+import static gregtech.api.enums.Textures.BlockIcons.MACHINE_CASINGS;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_PIPE_OUT;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_SIDE_JUKEBOX;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_TOP_JUKEBOX;
+import static gregtech.api.metatileentity.BaseTileEntity.TOOLTIP_DELAY;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+import net.minecraft.item.ItemRecord;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.MathHelper;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.util.Constants;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import org.joml.Vector4i;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizons.modularui.api.drawable.FallbackableUITexture;
+import com.gtnewhorizons.modularui.api.drawable.UITexture;
+import com.gtnewhorizons.modularui.api.math.Pos2d;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.api.screen.UIBuildContext;
+import com.gtnewhorizons.modularui.common.widget.CycleButtonWidget;
+import com.gtnewhorizons.modularui.common.widget.DrawableWidget;
+import com.gtnewhorizons.modularui.common.widget.FakeSyncWidget;
+import com.gtnewhorizons.modularui.common.widget.ProgressBar;
+import com.gtnewhorizons.modularui.common.widget.SliderWidget;
+import com.gtnewhorizons.modularui.common.widget.SlotWidget;
+
+import appeng.api.implementations.tiles.ISoundP2PHandler;
+import appeng.me.GridAccessException;
+import appeng.me.cache.helpers.TunnelCollection;
+import appeng.me.helpers.AENetworkProxy;
+import appeng.parts.p2p.PartP2PSound;
+import gregtech.api.enums.GTVoltageIndex;
+import gregtech.api.enums.GT_Values;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.modularui.IAddUIWidgets;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_BasicMachine;
+import gregtech.api.recipe.BasicUIProperties;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_MusicSystem;
+import gregtech.common.gui.modularui.UIHelper;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+
+public class GT_MetaTileEntity_BetterJukebox extends GT_MetaTileEntity_BasicMachine
+ implements IAddUIWidgets, ISoundP2PHandler {
+
+ // Stored state
+ public UUID jukeboxUuid = UNSET_UUID;
+ public boolean loopMode = true;
+ public boolean shuffleMode = false;
+ public int playbackSlot = 0;
+ public float playbackVolume = BalanceMath.VANILLA_JUKEBOX_RANGE;
+ public float p2pVolume = BalanceMath.VANILLA_JUKEBOX_RANGE;
+ public long discProgressMs = 0;
+ /** Makes all music discs play for 4 seconds */
+ public boolean superFastDebugMode = false;
+ // Computed state
+ private final Vector4i interdimPositionCache = new Vector4i(); // XYZ, Dimension ID
+ private GT_MusicSystem.MusicSource musicSource = null;
+ private boolean powered = false;
+ private long discStartMs = 0;
+ public long discDurationMs = 1;
+ private ItemRecord currentlyPlaying = null;
+
+ // Constants
+ public static final UUID UNSET_UUID = UUID.nameUUIDFromBytes(new byte[] { 0 });
+ public static final int INPUT_SLOTS = 21;
+ private static final Random SHUFFLER = new Random();
+
+ public enum HeadphoneLimit {
+
+ BLOCK_RANGE,
+ INSIDE_DIMENSION,
+ BETWEEN_DIMENSIONS;
+
+ public static final ImmutableList<HeadphoneLimit> ENTRIES = ImmutableList.copyOf(values());
+ }
+
+ public static final class BalanceMath {
+
+ public static int MAX_TIER = GTVoltageIndex.IV;
+ public static float VANILLA_JUKEBOX_RANGE = 4.0f; // 64 blocks
+
+ private static final float[] LISTENING_VOLUME = new float[] { //
+ VANILLA_JUKEBOX_RANGE, // ULV (unpowered fallback)
+ VANILLA_JUKEBOX_RANGE + 1.0f, // LV, 80 blocks
+ VANILLA_JUKEBOX_RANGE + 2.0f, // MV, 96 blocks
+ VANILLA_JUKEBOX_RANGE + 4.0f, // HV, 118 blocks
+ VANILLA_JUKEBOX_RANGE + 5.0f, // EV, 144 blocks
+ VANILLA_JUKEBOX_RANGE + 6.0f, // IV, 160 blocks, equivalent to default load distance of 10 chunks
+ };
+
+ private static final int[] HEADPHONE_BLOCK_RANGE = new int[] { //
+ 64, // ULV (unpowered fallback)
+ 128, // LV
+ 160, // MV
+ 320, // HV
+ 9001, // EV, alreadu unlimited here - this value is ignored
+ 9002, // IV, already unlimited here - this value is ignored
+ };
+
+ public static float listeningVolume(int tier) {
+ tier = MathHelper.clamp_int(tier, 0, MAX_TIER);
+ return LISTENING_VOLUME[tier];
+ }
+
+ public static int headphoneBlockRange(int tier) {
+ tier = MathHelper.clamp_int(tier, 0, MAX_TIER);
+ return HEADPHONE_BLOCK_RANGE[tier];
+ }
+
+ public static HeadphoneLimit headphoneLimit(int tier) {
+ if (tier <= GTVoltageIndex.HV) {
+ return HeadphoneLimit.BLOCK_RANGE;
+ } else if (tier == GTVoltageIndex.EV) {
+ return HeadphoneLimit.INSIDE_DIMENSION;
+ } else {
+ return HeadphoneLimit.BETWEEN_DIMENSIONS;
+ }
+ }
+
+ public static float volumeToAttenuationDistance(float range) {
+ // SoundManager.playSound logic
+ return 16.0f * range;
+ }
+
+ public static float attenuationDistanceToVolume(float blockRange) {
+ return blockRange / 16.0f;
+ }
+
+ public static long eutUsage(int tier) {
+ tier = MathHelper.clamp_int(tier, 0, MAX_TIER);
+ return V[tier] / 16;
+ }
+ }
+
+ private static String[] buildDescription(int aTier) {
+ ArrayList<String> strings = new ArrayList<>(4);
+ strings.add("Plays music better than your average vanilla jukebox.");
+ if (BalanceMath.headphoneLimit(aTier) != HeadphoneLimit.BLOCK_RANGE) {
+ strings.add(EnumChatFormatting.BLUE + "The raw power of Hatsune Miku in your ears");
+ }
+ strings.add(
+ String.format(
+ "Range: %s%.1f blocks",
+ EnumChatFormatting.WHITE,
+ BalanceMath.volumeToAttenuationDistance(BalanceMath.listeningVolume(aTier))));
+ strings.add(switch (BalanceMath.headphoneLimit(aTier)) {
+ case BLOCK_RANGE -> String.format(
+ "Headphone signal range: %s%d blocks",
+ EnumChatFormatting.WHITE,
+ BalanceMath.headphoneBlockRange(aTier));
+ case INSIDE_DIMENSION -> String
+ .format("Headphones work anywhere in %sthe same dimension", EnumChatFormatting.WHITE);
+ case BETWEEN_DIMENSIONS -> String
+ .format("Headphones work anywhere, in %sany dimension", EnumChatFormatting.WHITE);
+ });
+ strings.add(String.format("Cost: %s%d EU/t", EnumChatFormatting.WHITE, BalanceMath.eutUsage(aTier)));
+ strings.add(GT_Values.AuthorEigenRaven);
+ return strings.toArray(new String[0]);
+ }
+
+ public GT_MetaTileEntity_BetterJukebox(int aID, String aName, String aNameRegional, int aTier) {
+ super(aID, aName, aNameRegional, aTier, 1, buildDescription(aTier), INPUT_SLOTS, 1);
+ playbackVolume = BalanceMath.listeningVolume(aTier);
+ }
+
+ public GT_MetaTileEntity_BetterJukebox(String aName, int aTier, String[] aDescription, ITexture[][][] aTextures) {
+ super(aName, aTier, 1, aDescription, aTextures, INPUT_SLOTS, 1);
+ playbackVolume = BalanceMath.listeningVolume(aTier);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_BetterJukebox(this.mName, this.mTier, this.mDescriptionArray, this.mTextures);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity baseMetaTileEntity, ForgeDirection sideDirection,
+ ForgeDirection facingDirection, int colorIndex, boolean active, boolean redstoneLevel) {
+ if (sideDirection == baseMetaTileEntity.getFrontFacing()) {
+ return new ITexture[] { MACHINE_CASINGS[mTier][colorIndex + 1], TextureFactory.of(OVERLAY_PIPE_OUT) };
+ }
+ if (sideDirection != ForgeDirection.UP) {
+ return new ITexture[] { MACHINE_CASINGS[mTier][colorIndex + 1], TextureFactory.of(OVERLAY_SIDE_JUKEBOX) };
+ }
+ return new ITexture[] { MACHINE_CASINGS[mTier][colorIndex + 1], TextureFactory.builder()
+ .addIcon(OVERLAY_TOP_JUKEBOX)
+ .extFacing()
+ .build() };
+ }
+
+ @Override
+ public void onFirstTick(IGregTechTileEntity aBaseMetaTileEntity) {
+ super.onFirstTick(aBaseMetaTileEntity);
+ if (aBaseMetaTileEntity.isClientSide()) {
+ return;
+ }
+ final Vector4i interdimPosition = interdimPositionCache;
+ interdimPosition.x = aBaseMetaTileEntity.getXCoord();
+ interdimPosition.y = aBaseMetaTileEntity.getYCoord();
+ interdimPosition.z = aBaseMetaTileEntity.getZCoord();
+ interdimPosition.w = aBaseMetaTileEntity.getWorld().provider.dimensionId;
+ if (jukeboxUuid == UNSET_UUID) {
+ jukeboxUuid = UUID.randomUUID();
+ markDirty();
+ }
+ if (musicSource == null) {
+ musicSource = GT_MusicSystem.ServerSystem.registerOrGetMusicSource(jukeboxUuid);
+ musicSource.originPosition.set(interdimPosition);
+ musicSource.headphoneLimit = BalanceMath.headphoneLimit(mTier);
+ musicSource.headphoneBlockRange = BalanceMath.headphoneBlockRange(mTier);
+ musicSource.startedPlayingAtMs = System.currentTimeMillis();
+ updateEmitterList();
+ }
+ if (doesSlotContainValidRecord(playbackSlot)
+ && mInventory[playbackSlot].getItem() instanceof ItemRecord record) {
+ final ResourceLocation resource = record.getRecordResource(record.recordName);
+ currentlyPlaying = record;
+ // Assume a safe disc duration of 500 seconds if not known in the registry
+ discDurationMs = GT_MusicSystem.getMusicRecordDurations()
+ .getOrDefault(resource, 500_000);
+ discStartMs = System.currentTimeMillis() - discProgressMs;
+ musicSource.setRecord(
+ new ResourceLocation(resource.getResourceDomain(), "records." + resource.getResourcePath()),
+ discProgressMs);
+ }
+ }
+
+ @Override
+ public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTimer) {
+ try {
+ if (aBaseMetaTileEntity.isClientSide() || !aBaseMetaTileEntity.isAllowedToWork() || musicSource == null) {
+ if (currentlyPlaying != null) {
+ stopCurrentSong(System.currentTimeMillis());
+ }
+ return;
+ }
+ final Vector4i interdimPosition = interdimPositionCache;
+ interdimPosition.x = aBaseMetaTileEntity.getXCoord();
+ interdimPosition.y = aBaseMetaTileEntity.getYCoord();
+ interdimPosition.z = aBaseMetaTileEntity.getZCoord();
+ interdimPosition.w = aBaseMetaTileEntity.getWorld().provider.dimensionId;
+ final long now = System.currentTimeMillis();
+
+ if (superFastDebugMode && discDurationMs > 4000) {
+ discDurationMs = 4000;
+ }
+
+ // power check
+ final boolean hasMinimumEU = aBaseMetaTileEntity.isUniversalEnergyStored(getMinimumStoredEU());
+ if (currentlyPlaying != null && hasMinimumEU
+ && aBaseMetaTileEntity.decreaseStoredEnergyUnits(BalanceMath.eutUsage(mTier), false)) {
+ if (!powered) { // just got power again
+ powered = true;
+ musicSource.modified = true;
+ musicSource.headphoneLimit = BalanceMath.headphoneLimit(mTier);
+ musicSource.headphoneBlockRange = BalanceMath.headphoneBlockRange(mTier);
+ updateEmitterList();
+ }
+ } else if ((!hasMinimumEU || currentlyPlaying != null) && powered) { // was powered, but no longer is
+ powered = false;
+ musicSource.modified = true;
+ musicSource.headphoneLimit = HeadphoneLimit.BLOCK_RANGE;
+ musicSource.headphoneBlockRange = BalanceMath.headphoneBlockRange(0);
+ updateEmitterList();
+ }
+
+ // check if current disc finished
+ if (currentlyPlaying != null) {
+ discProgressMs = now - discStartMs;
+ final boolean hasValidRecord = doesSlotContainValidRecord(playbackSlot);
+ final boolean wasDiscSwapped = hasValidRecord
+ && mInventory[getInputSlot() + playbackSlot].getItem() != currentlyPlaying;
+ if (discProgressMs >= discDurationMs || !hasValidRecord || wasDiscSwapped) {
+ stopCurrentSong(now);
+ if (!loopMode) {
+ // should be empty, but swap just in case it's not
+ final ItemStack oldOut = mInventory[getOutputSlot()];
+ mInventory[getOutputSlot()] = mInventory[getInputSlot() + playbackSlot];
+ mInventory[getInputSlot() + playbackSlot] = oldOut;
+ markDirty();
+ }
+ if (!(hasValidRecord && wasDiscSwapped)) {
+ // don't switch slots if someone just put a new disc in the active slot
+ pickNextSlot();
+ }
+ } else {
+ // keep on playing
+ return;
+ }
+ }
+
+ if (playbackSlot < 0 || playbackSlot >= INPUT_SLOTS
+ || ((aTimer % 10) == 0 && !doesSlotContainValidRecord(playbackSlot))) {
+ pickNextSlot();
+ }
+
+ final boolean hasValidRecord = doesSlotContainValidRecord(playbackSlot);
+ final boolean canStartPlaying = loopMode || isOutputEmpty();
+ if (!hasValidRecord) {
+ stopCurrentSong(now);
+ } else if (canStartPlaying
+ && mInventory[getInputSlot() + playbackSlot].getItem() instanceof ItemRecord record) {
+ final ResourceLocation resource = record.getRecordResource(record.recordName);
+ currentlyPlaying = record;
+ musicSource.setRecord(
+ new ResourceLocation(resource.getResourceDomain(), "records." + resource.getResourcePath()));
+ // Assume a safe disc duration of 500 seconds if not known in the registry
+ discDurationMs = GT_MusicSystem.getMusicRecordDurations()
+ .getOrDefault(resource, 500_000);
+ discProgressMs = 0;
+ discStartMs = now;
+ }
+ } finally {
+ super.onPostTick(aBaseMetaTileEntity, aTimer);
+ }
+ }
+
+ private void stopCurrentSong(long nowMs) {
+ if (currentlyPlaying == null) {
+ return;
+ }
+ musicSource.setRecord(null);
+ currentlyPlaying = null;
+ discDurationMs = 1;
+ discProgressMs = 0;
+ discStartMs = nowMs;
+ markDirty();
+ }
+
+ private void pickNextSlot() {
+ playbackSlot = MathHelper.clamp_int(playbackSlot, 0, INPUT_SLOTS);
+ if (shuffleMode) {
+ final int[] validSlots = new int[INPUT_SLOTS];
+ int validSlotCount = 0;
+ for (int i = 0; i < INPUT_SLOTS; i++) {
+ if (i != playbackSlot && doesSlotContainValidRecord(i)) {
+ validSlots[validSlotCount++] = i;
+ }
+ }
+ switch (validSlotCount) {
+ case 0 -> {}
+ case 1 -> {
+ playbackSlot = validSlots[0];
+ }
+ default -> {
+ playbackSlot = validSlots[SHUFFLER.nextInt(validSlotCount)];
+ }
+ }
+ } else {
+ int attempt = 0;
+ int nextSlot = playbackSlot;
+ do {
+ attempt++;
+ nextSlot = (nextSlot + 1) % INPUT_SLOTS;
+ } while (!doesSlotContainValidRecord(nextSlot) && attempt <= INPUT_SLOTS);
+ if (attempt <= INPUT_SLOTS) {
+ playbackSlot = nextSlot;
+ }
+ }
+ }
+
+ public boolean doesSlotContainValidRecord(int slot) {
+ return mInventory[getInputSlot() + slot] != null
+ && mInventory[getInputSlot() + slot].getItem() instanceof ItemRecord;
+ }
+
+ @Override
+ public void onRemoval() {
+ final IGregTechTileEntity baseTE = getBaseMetaTileEntity();
+ if (baseTE == null) {
+ return;
+ }
+ if (!baseTE.isServerSide()) {
+ return;
+ }
+ if (jukeboxUuid == UNSET_UUID) {
+ return;
+ }
+ GT_MusicSystem.ServerSystem.removeMusicSource(jukeboxUuid);
+ }
+
+ @Override
+ public long getMinimumStoredEU() {
+ return BalanceMath.eutUsage(mTier) * 20;
+ }
+
+ @Override
+ public long maxEUStore() {
+ return 512L + BalanceMath.eutUsage(mTier) * 50;
+ }
+
+ @Override
+ public long maxAmperesIn() {
+ return 1;
+ }
+
+ @Override
+ public ITexture[][][] getTextureSet(ITexture[] aTextures) {
+ return null;
+ }
+
+ public static final String NBTKEY_UUID_LOW = "jukeboxUUIDLow";
+ public static final String NBTKEY_UUID_HIGH = "jukeboxUUIDHigh";
+ public static final String NBTKEY_LOOP_MODE = "loopMode";
+ public static final String NBTKEY_SHUFFLE_MODE = "shuffleMode";
+ public static final String NBTKEY_PLAYBACK_SLOT = "playbackSlot";
+ public static final String NBTKEY_VOLUME_PLAY = "playbackVolume";
+ public static final String NBTKEY_VOLUME_P2P = "p2pVolume";
+ public static final String NBTKEY_DISC_PROGRESS_MS = "discProgressMs";
+
+ @Override
+ public String[] getInfoData() {
+ return new String[] { "Jukebox UUID: " + ((jukeboxUuid == UNSET_UUID) ? "unset" : jukeboxUuid),
+ "Loop mode: " + loopMode, "Shuffle mode: " + shuffleMode, "Played the disc for [ms]: " + discProgressMs,
+ "Current disc duration [ms]: " + discDurationMs,
+ "Playback range [blocks]: " + BalanceMath.volumeToAttenuationDistance(playbackVolume),
+ "P2P range [blocks]: " + BalanceMath.volumeToAttenuationDistance(playbackVolume),
+ "Raw playback strength: " + playbackVolume, "Raw p2p strength: " + p2pVolume };
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ super.saveNBTData(aNBT);
+ if (jukeboxUuid != UNSET_UUID) {
+ aNBT.setLong(NBTKEY_UUID_LOW, jukeboxUuid.getLeastSignificantBits());
+ aNBT.setLong(NBTKEY_UUID_HIGH, jukeboxUuid.getMostSignificantBits());
+ aNBT.setBoolean(NBTKEY_LOOP_MODE, loopMode);
+ aNBT.setBoolean(NBTKEY_SHUFFLE_MODE, shuffleMode);
+ aNBT.setInteger(NBTKEY_PLAYBACK_SLOT, playbackSlot);
+ aNBT.setFloat(NBTKEY_VOLUME_PLAY, playbackVolume);
+ aNBT.setFloat(NBTKEY_VOLUME_P2P, p2pVolume);
+ aNBT.setLong(NBTKEY_DISC_PROGRESS_MS, discProgressMs);
+ }
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ super.loadNBTData(aNBT);
+ if (aNBT.hasKey(NBTKEY_UUID_LOW, Constants.NBT.TAG_ANY_NUMERIC)
+ && aNBT.hasKey(NBTKEY_UUID_HIGH, Constants.NBT.TAG_ANY_NUMERIC)) {
+ jukeboxUuid = new UUID(aNBT.getLong(NBTKEY_UUID_HIGH), aNBT.getLong(NBTKEY_UUID_LOW));
+ }
+ if (aNBT.hasKey(NBTKEY_LOOP_MODE, Constants.NBT.TAG_ANY_NUMERIC)) {
+ loopMode = aNBT.getBoolean(NBTKEY_LOOP_MODE);
+ }
+ if (aNBT.hasKey(NBTKEY_SHUFFLE_MODE, Constants.NBT.TAG_ANY_NUMERIC)) {
+ shuffleMode = aNBT.getBoolean(NBTKEY_SHUFFLE_MODE);
+ }
+ if (aNBT.hasKey(NBTKEY_PLAYBACK_SLOT, Constants.NBT.TAG_ANY_NUMERIC)) {
+ playbackSlot = aNBT.getInteger(NBTKEY_PLAYBACK_SLOT);
+ }
+ if (aNBT.hasKey(NBTKEY_VOLUME_PLAY, Constants.NBT.TAG_ANY_NUMERIC)) {
+ playbackVolume = aNBT.getFloat(NBTKEY_VOLUME_PLAY);
+ }
+ if (aNBT.hasKey(NBTKEY_VOLUME_P2P, Constants.NBT.TAG_ANY_NUMERIC)) {
+ p2pVolume = aNBT.getFloat(NBTKEY_VOLUME_P2P);
+ }
+ if (aNBT.hasKey(NBTKEY_DISC_PROGRESS_MS, Constants.NBT.TAG_ANY_NUMERIC)) {
+ discProgressMs = aNBT.getLong(NBTKEY_DISC_PROGRESS_MS);
+ }
+
+ final float maxVolume = BalanceMath.listeningVolume(mTier);
+ playbackVolume = MathHelper.clamp_float(playbackVolume, 0.0f, maxVolume);
+ p2pVolume = MathHelper.clamp_float(p2pVolume, 0.0f, maxVolume);
+ }
+
+ @Override
+ public boolean useModularUI() {
+ return true;
+ }
+
+ @Override
+ protected BasicUIProperties getUIProperties() {
+ return super.getUIProperties().toBuilder()
+ .itemInputPositionsGetter(count -> UIHelper.getGridPositions(count, 7, 6, 7, 3))
+ .itemOutputPositionsGetter(count -> UIHelper.getGridPositions(count, 153, 24, 1))
+ .specialItemPositionGetter(() -> new Pos2d(115, 62))
+ .progressBarPos(Pos2d.cartesian(133, 24))
+ .progressBarTexture(new FallbackableUITexture(GT_UITextures.PROGRESSBAR_ARROW))
+ .build();
+ }
+
+ @Override
+ protected void addProgressBar(ModularWindow.Builder builder, BasicUIProperties uiProperties) {
+ builder.widget(
+ setNEITransferRect(
+ new ProgressBar().setProgress(() -> discProgressMs / (float) Math.max(1, discDurationMs))
+ .setTexture(uiProperties.progressBarTexture.get(), uiProperties.progressBarImageSize)
+ .setDirection(uiProperties.progressBarDirection)
+ .setPos(uiProperties.progressBarPos)
+ .setSize(uiProperties.progressBarSize)
+ .setUpdateTooltipEveryTick(true)
+ .attachSyncer(
+ new FakeSyncWidget.LongSyncer(() -> this.discProgressMs, val -> this.discProgressMs = val),
+ builder)
+ .attachSyncer(
+ new FakeSyncWidget.LongSyncer(() -> this.discDurationMs, val -> this.discDurationMs = val),
+ builder)
+ .dynamicTooltip(
+ () -> Collections.singletonList(
+ String.format("%,.2f / %,.2f", discProgressMs / 1000.0f, discDurationMs / 1000.0f))),
+ uiProperties.neiTransferRectId));
+ addProgressBarSpecialTextures(builder, uiProperties);
+ }
+
+ @Override
+ protected SlotWidget createChargerSlot(int x, int y) {
+ return super.createChargerSlot(97, 62);
+ }
+
+ @Override
+ public void addUIWidgets(ModularWindow.Builder builder, UIBuildContext buildContext) {
+ super.addUIWidgets(builder, buildContext);
+ final BasicUIProperties props = getUIProperties();
+ final List<Pos2d> inputSlots = props.itemInputPositionsGetter.apply(mInputSlotCount);
+ // Loop
+ builder.widget(
+ new CycleButtonWidget().setToggle(() -> loopMode, val -> loopMode = val)
+ .setStaticTexture(GT_UITextures.OVERLAY_BUTTON_CYCLIC)
+ .setVariableBackground(GT_UITextures.BUTTON_STANDARD_TOGGLE)
+ .setGTTooltip(() -> mTooltipCache.getData("GT5U.machines.betterjukebox.loop.tooltip"))
+ .setTooltipShowUpDelay(TOOLTIP_DELAY)
+ .setPos(153, 6)
+ .setSize(18, 18));
+ // Shuffle
+ builder.widget(new CycleButtonWidget().setToggle(() -> shuffleMode, val -> {
+ shuffleMode = val;
+ if (shuffleMode) {
+ playbackSlot = -1;
+ } else {
+ playbackSlot = 0;
+ }
+ })
+ .setStaticTexture(GT_UITextures.OVERLAY_BUTTON_SHUFFLE)
+ .setVariableBackground(GT_UITextures.BUTTON_STANDARD_TOGGLE)
+ .setGTTooltip(() -> mTooltipCache.getData("GT5U.machines.betterjukebox.shuffle.tooltip"))
+ .setTooltipShowUpDelay(TOOLTIP_DELAY)
+ .setPos(153, 42)
+ .setSize(18, 18));
+ // Currently playing slot highlight using the hotbar active texture
+ final DrawableWidget slotHighlight = new DrawableWidget();
+ builder.widget(
+ slotHighlight
+ .setDrawable(
+ new UITexture(
+ new ResourceLocation("minecraft", "textures/gui/widgets.png"),
+ 0.0f,
+ 22.0f / 256.0f,
+ 24.0f / 256.0f,
+ 46.0f / 256.0f))
+ .setSize(24, 24)
+ .attachSyncer(new FakeSyncWidget.IntegerSyncer(() -> this.playbackSlot, val -> {
+ this.playbackSlot = val;
+ slotHighlight.checkNeedsRebuild();
+ }), builder)
+ .setPosProvider(
+ (screenSize, window, parent) -> inputSlots.get(MathHelper.clamp_int(playbackSlot, 0, INPUT_SLOTS))
+ .add(-3, -3)));
+ // Attenuation distance (controls internal "volume")
+ // Caching tooltip data caches the formatted p2p range value, so we have to use the uncached variant here.
+ builder.widget(
+ new SliderWidget()
+ .setBounds(0.0f, BalanceMath.volumeToAttenuationDistance(BalanceMath.listeningVolume(mTier)))
+ .setGetter(this::getPlaybackBlockRange)
+ .setSetter(this::setPlaybackBlockRange)
+ .dynamicTooltip(
+ () -> mTooltipCache.getUncachedTooltipData(
+ "GT5U.machines.betterjukebox.attenuationDistance.tooltip",
+ (int) getPlaybackBlockRange()).text)
+ .setUpdateTooltipEveryTick(true)
+ .setPos(44, 63)
+ .setSize(52, 8));
+ builder.widget(
+ new SliderWidget()
+ .setBounds(0.0f, BalanceMath.volumeToAttenuationDistance(BalanceMath.listeningVolume(mTier)))
+ .setGetter(this::getP2PBlockRange)
+ .setSetter(this::setP2PBlockRange)
+ .dynamicTooltip(
+ () -> mTooltipCache.getUncachedTooltipData(
+ "GT5U.machines.betterjukebox.p2pAttenuationDistance.tooltip",
+ (int) getP2PBlockRange()).text)
+ .setUpdateTooltipEveryTick(true)
+ .setPos(44, 71)
+ .setSize(52, 8));
+ }
+
+ private float getPlaybackBlockRange() {
+ return BalanceMath.volumeToAttenuationDistance(playbackVolume);
+ }
+
+ private float getP2PBlockRange() {
+ return BalanceMath.volumeToAttenuationDistance(p2pVolume);
+ }
+
+ private void setPlaybackBlockRange(float blockRange) {
+ float volume = BalanceMath.attenuationDistanceToVolume(blockRange);
+ volume = MathHelper.clamp_float(volume, 0.0f, BalanceMath.listeningVolume(mTier));
+ if (volume != playbackVolume) {
+ playbackVolume = volume;
+ if (getBaseMetaTileEntity().isServerSide()) {
+ updateEmitterList();
+ }
+ }
+ }
+
+ private void setP2PBlockRange(float blockRange) {
+ float volume = BalanceMath.attenuationDistanceToVolume(blockRange);
+ volume = MathHelper.clamp_float(volume, 0.0f, BalanceMath.listeningVolume(mTier));
+ if (volume != p2pVolume) {
+ p2pVolume = volume;
+ if (getBaseMetaTileEntity().isServerSide()) {
+ updateEmitterList();
+ }
+ }
+ }
+
+ private final EnumMap<ForgeDirection, PartP2PSound> attachedSoundP2P = new EnumMap<>(ForgeDirection.class);
+ private final ObjectArrayList<PartP2PSound> combinedOutputsListCache = new ObjectArrayList<>(new PartP2PSound[0]);
+
+ private void updateEmitterList() {
+ final GT_MusicSystem.MusicSource target = musicSource;
+ if (target == null) {
+ return;
+ }
+ final ObjectArrayList<PartP2PSound> emitters = combinedOutputsListCache;
+ emitters.clear();
+
+ attachedSoundP2P.forEach((ignored, p2p) -> {
+ if (p2p != null) {
+ try {
+ p2p.getOutputs()
+ .forEach(emitters::add);
+ } catch (GridAccessException e) {
+ // skip
+ }
+ }
+ });
+
+ IGregTechTileEntity te = getBaseMetaTileEntity();
+ if (te == null) {
+ return;
+ }
+ final Vector4i position = new Vector4i();
+ target.resizeEmitterArray(1 + emitters.size());
+ position.set(te.getXCoord(), te.getYCoord(), te.getZCoord(), te.getWorld().provider.dimensionId);
+ final float actualVolume = MathHelper
+ .clamp_float(playbackVolume, 0.0f, BalanceMath.listeningVolume(powered ? mTier : 0));
+ target.setEmitter(0, position, actualVolume);
+ final float actualP2PVolume = MathHelper
+ .clamp_float(p2pVolume, 0.0f, powered ? BalanceMath.listeningVolume(mTier) : 0.0f);
+ for (int i = 0; i < emitters.size(); i++) {
+ final PartP2PSound p2p = emitters.get(i);
+ final AENetworkProxy proxy = p2p.getProxy();
+ final TileEntity emitterTe = p2p.getTile();
+ final ForgeDirection dir = p2p.getSide();
+ position.set(
+ emitterTe.xCoord + dir.offsetX,
+ emitterTe.yCoord + dir.offsetY,
+ emitterTe.zCoord + dir.offsetZ,
+ emitterTe.getWorldObj().provider.dimensionId);
+ final boolean active = proxy.isActive();
+ target.setEmitter(1 + i, position, active ? actualP2PVolume : 0);
+ }
+ }
+
+ @Override
+ public boolean allowSoundProxying(PartP2PSound p2p) {
+ return false; // the jukebox proxies sounds by itself
+ }
+
+ @Override
+ public void onSoundP2PAttach(PartP2PSound p2p) {
+ attachedSoundP2P.put(p2p.getSide(), p2p);
+ updateEmitterList();
+ }
+
+ @Override
+ public void onSoundP2PDetach(PartP2PSound p2p) {
+ attachedSoundP2P.put(p2p.getSide(), null);
+ updateEmitterList();
+ }
+
+ @Override
+ public void onSoundP2POutputUpdate(PartP2PSound p2p, TunnelCollection<PartP2PSound> outputs) {
+ updateEmitterList();
+ }
+
+}
diff --git a/src/main/java/gregtech/loaders/preload/GT_Loader_Item_Block_And_Fluid.java b/src/main/java/gregtech/loaders/preload/GT_Loader_Item_Block_And_Fluid.java
index 4b412e2796..0b25dc76cd 100644
--- a/src/main/java/gregtech/loaders/preload/GT_Loader_Item_Block_And_Fluid.java
+++ b/src/main/java/gregtech/loaders/preload/GT_Loader_Item_Block_And_Fluid.java
@@ -87,6 +87,7 @@ import gregtech.common.items.GT_MetaGenerated_Tool_01;
import gregtech.common.items.GT_NeutronReflector_Item;
import gregtech.common.items.GT_TierDrone;
import gregtech.common.items.GT_VolumetricFlask;
+import gregtech.common.items.GT_WirelessHeadphones;
import gregtech.common.tileentities.render.TileDrone;
import gregtech.common.tileentities.render.TileLaser;
import gregtech.common.tileentities.render.TileWormhole;
@@ -160,6 +161,7 @@ public class GT_Loader_Item_Block_And_Fluid implements Runnable {
new GT_MetaGenerated_Item_99();
new GT_MetaGenerated_Tool_01();
new GT_FluidDisplayItem();
+ new GT_WirelessHeadphones();
// Tiered recipe materials actually appear to be set in GT_MetaTileEntity_BasicMachine_GT_Recipe, making these
// unused
diff --git a/src/main/java/gregtech/loaders/preload/GT_Loader_MetaTileEntities.java b/src/main/java/gregtech/loaders/preload/GT_Loader_MetaTileEntities.java
index c77e74eb1f..ab3ea53279 100644
--- a/src/main/java/gregtech/loaders/preload/GT_Loader_MetaTileEntities.java
+++ b/src/main/java/gregtech/loaders/preload/GT_Loader_MetaTileEntities.java
@@ -118,6 +118,11 @@ import static gregtech.api.enums.MetaTileEntityIDs.BENDING_MACHINE_UIV;
import static gregtech.api.enums.MetaTileEntityIDs.BENDING_MACHINE_UMV;
import static gregtech.api.enums.MetaTileEntityIDs.BENDING_MACHINE_UV;
import static gregtech.api.enums.MetaTileEntityIDs.BENDING_MACHINE_ZPM;
+import static gregtech.api.enums.MetaTileEntityIDs.BETTER_JUKEBOX_EV;
+import static gregtech.api.enums.MetaTileEntityIDs.BETTER_JUKEBOX_HV;
+import static gregtech.api.enums.MetaTileEntityIDs.BETTER_JUKEBOX_IV;
+import static gregtech.api.enums.MetaTileEntityIDs.BETTER_JUKEBOX_LV;
+import static gregtech.api.enums.MetaTileEntityIDs.BETTER_JUKEBOX_MV;
import static gregtech.api.enums.MetaTileEntityIDs.BREWERY_EV;
import static gregtech.api.enums.MetaTileEntityIDs.BREWERY_HV;
import static gregtech.api.enums.MetaTileEntityIDs.BREWERY_IV;
@@ -986,6 +991,7 @@ import gregtech.common.tileentities.machines.GT_MetaTileEntity_Hatch_Input_ME;
import gregtech.common.tileentities.machines.GT_MetaTileEntity_Hatch_OutputBus_ME;
import gregtech.common.tileentities.machines.GT_MetaTileEntity_Hatch_Output_ME;
import gregtech.common.tileentities.machines.basic.GT_MetaTileEntity_AdvSeismicProspector;
+import gregtech.common.tileentities.machines.basic.GT_MetaTileEntity_BetterJukebox;
import gregtech.common.tileentities.machines.basic.GT_MetaTileEntity_Boxinator;
import gregtech.common.tileentities.machines.basic.GT_MetaTileEntity_Charger;
import gregtech.common.tileentities.machines.basic.GT_MetaTileEntity_IndustrialApiary;
@@ -2569,6 +2575,39 @@ public class GT_Loader_MetaTileEntities implements Runnable { // TODO CHECK CIRC
8).getStackForm(1L));
}
+ private static void registerBetterJukebox() {
+ ItemList.BetterJukebox_LV.set(
+ new GT_MetaTileEntity_BetterJukebox(
+ BETTER_JUKEBOX_LV.ID,
+ "basicmachine.betterjukebox.tier.01",
+ "Basic Electric Jukebox",
+ 1).getStackForm(1L));
+ ItemList.BetterJukebox_MV.set(
+ new GT_MetaTileEntity_BetterJukebox(
+ BETTER_JUKEBOX_MV.ID,
+ "basicmachine.betterjukebox.tier.02",
+ "Advanced Electric Jukebox",
+ 2).getStackForm(1L));
+ ItemList.BetterJukebox_HV.set(
+ new GT_MetaTileEntity_BetterJukebox(
+ BETTER_JUKEBOX_HV.ID,
+ "basicmachine.betterjukebox.tier.03",
+ "Advanced Electric Jukebox II",
+ 3).getStackForm(1L));
+ ItemList.BetterJukebox_EV.set(
+ new GT_MetaTileEntity_BetterJukebox(
+ BETTER_JUKEBOX_EV.ID,
+ "basicmachine.betterjukebox.tier.04",
+ "Extreme Music Mixer",
+ 4).getStackForm(1L));
+ ItemList.BetterJukebox_IV.set(
+ new GT_MetaTileEntity_BetterJukebox(
+ BETTER_JUKEBOX_IV.ID,
+ "basicmachine.betterjukebox.tier.05",
+ "Duke Mix'em 3D",
+ 5).getStackForm(1L));
+ }
+
private static void registerChestBuffer() {
ItemList.Automation_ChestBuffer_ULV.set(
new GT_MetaTileEntity_ChestBuffer(
@@ -10067,6 +10106,7 @@ public class GT_Loader_MetaTileEntities implements Runnable { // TODO CHECK CIRC
registerElectrolyzer();
registerCircuitAssembler();
registerTurboCharger4By4();
+ registerBetterJukebox();
ItemList.AdvDebugStructureWriter.set(
new GT_MetaTileEntity_AdvDebugStructureWriter(
diff --git a/src/main/java/gregtech/mixin/Mixin.java b/src/main/java/gregtech/mixin/Mixin.java
index 340868e015..3e0ad23f0a 100644
--- a/src/main/java/gregtech/mixin/Mixin.java
+++ b/src/main/java/gregtech/mixin/Mixin.java
@@ -19,6 +19,12 @@ import cpw.mods.fml.relauncher.FMLLaunchHandler;
public enum Mixin {
// Minecraft
+ SoundManagerMixin(new Builder("Seeking sound playback")
+ .addMixinClasses("minecraft.SoundManagerMixin", "minecraft.SoundManagerInnerMixin")
+ .addTargetedMod(VANILLA)
+ .setApplyIf(() -> true)
+ .setPhase(Phase.EARLY)
+ .setSide(Side.CLIENT)),
WorldMixin(new Builder("Block update detection").addMixinClasses("minecraft.WorldMixin")
.addTargetedMod(VANILLA)
.setApplyIf(() -> true)
diff --git a/src/main/java/gtPlusPlus/core/block/machine/Machine_SuperJukebox.java b/src/main/java/gtPlusPlus/core/block/machine/Machine_SuperJukebox.java
index 470fa98a9b..6b3c87fac4 100644
--- a/src/main/java/gtPlusPlus/core/block/machine/Machine_SuperJukebox.java
+++ b/src/main/java/gtPlusPlus/core/block/machine/Machine_SuperJukebox.java
@@ -1,5 +1,7 @@
package gtPlusPlus.core.block.machine;
+import java.util.List;
+
import net.minecraft.block.Block;
import net.minecraft.block.BlockJukebox;
import net.minecraft.client.renderer.texture.IIconRegister;
@@ -8,11 +10,13 @@ import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.ISidedInventory;
import net.minecraft.item.Item;
+import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemRecord;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.IIcon;
+import net.minecraft.util.StatCollector;
import net.minecraft.world.World;
import cpw.mods.fml.common.registry.GameRegistry;
@@ -31,6 +35,18 @@ public class Machine_SuperJukebox extends BlockJukebox {
@SideOnly(Side.CLIENT)
private IIcon mIcon;
+ public static class SuperJukeboxItemBlock extends ItemBlock {
+
+ public SuperJukeboxItemBlock(Block block) {
+ super(block);
+ }
+
+ @Override
+ public void addInformation(ItemStack stack, EntityPlayer player, List<String> tooltips, boolean f3HEnabled) {
+ tooltips.add(StatCollector.translateToLocal("tile.blockSuperJukebox.deprecated"));
+ }
+ }
+
public Machine_SuperJukebox() {
this.setBlockName("blockSuperJukebox");
this.setCreativeTab(CreativeTabs.tabRedstone);
@@ -38,7 +54,7 @@ public class Machine_SuperJukebox extends BlockJukebox {
setResistance(10.0F);
setStepSound(soundTypePiston);
setBlockTextureName("jukebox");
- GameRegistry.registerBlock(this, "blockSuperJukebox");
+ GameRegistry.registerBlock(this, SuperJukeboxItemBlock.class, "blockSuperJukebox");
}
/**
diff --git a/src/main/resources/META-INF/ggfab_at.cfg b/src/main/resources/META-INF/ggfab_at.cfg
deleted file mode 100644
index a6dc456eb5..0000000000
--- a/src/main/resources/META-INF/ggfab_at.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-public net.minecraft.nbt.NBTTagList field_74747_a # tagList
-public net.minecraft.nbt.NBTTagCompound field_74784_a # tagMap \ No newline at end of file
diff --git a/src/main/resources/META-INF/tectech_at.cfg b/src/main/resources/META-INF/gregtech_at.cfg
index fa76d13c1f..065f0c4a26 100644
--- a/src/main/resources/META-INF/tectech_at.cfg
+++ b/src/main/resources/META-INF/gregtech_at.cfg
@@ -1,3 +1,11 @@
+# GT
+public net.minecraft.client.audio.SoundHandler field_147697_e # sndRegistry
+public net.minecraft.client.audio.SoundHandler field_147694_f # sndManager
+public net.minecraft.item.ItemRecord field_150928_b # field_150928_b / all registered records
+# GGFab
+public net.minecraft.nbt.NBTTagList field_74747_a # tagList
+public net.minecraft.nbt.NBTTagCompound field_74784_a # tagMap
+# TecTech
public net.minecraft.block.Block field_149781_w #blockResistance
public net.minecraft.block.Block field_149782_v #blockHardness
protected net.minecraft.client.gui.FontRenderer field_111274_c #unicodePageLocations
diff --git a/src/main/resources/assets/gregtech/lang/en_US.lang b/src/main/resources/assets/gregtech/lang/en_US.lang
index e07e319299..43845d9b37 100644
--- a/src/main/resources/assets/gregtech/lang/en_US.lang
+++ b/src/main/resources/assets/gregtech/lang/en_US.lang
@@ -445,6 +445,12 @@ GT5U.machines.dronecentre.turnoff=Successfully turn off all machines!
GT5U.machines.dronecentre.noconnection=No valid connection
GT5U.machines.dronecentre.enableRender=Enable Drone Render
GT5U.machines.dronecentre.disableRender=Disable Drone Render
+GT5U.machines.betterjukebox.attenuationDistance.tooltip=Sound attenuation distance: %d blocks
+GT5U.machines.betterjukebox.p2pAttenuationDistance.tooltip=P2P sound attenuation distance: %d blocks
+GT5U.machines.betterjukebox.loop.tooltip=Loop mode (keep discs in input inventory)
+GT5U.machines.betterjukebox.shuffle.tooltip=Shuffle mode (play random discs instead of in order)
+GT5U.machines.betterjukebox.headphonesbound=Headphones bound to jukebox
+GT5U.machines.betterjukebox.headphonesunbound=Shift-click on an electric jukebox to bind headphones
GT5U.machines.laser_hatch.amperage=Amperage
GT5U.recipe_filter.empty_representation_slot.tooltip=§7Click with a machine to set filter
diff --git a/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_SIDE_JUKEBOX.png b/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_SIDE_JUKEBOX.png
new file mode 100644
index 0000000000..97fde2fa1e
--- /dev/null
+++ b/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_SIDE_JUKEBOX.png
Binary files differ
diff --git a/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_TOP_JUKEBOX.png b/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_TOP_JUKEBOX.png
new file mode 100644
index 0000000000..188aaa7391
--- /dev/null
+++ b/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_TOP_JUKEBOX.png
Binary files differ
diff --git a/src/main/resources/assets/gregtech/textures/gui/overlay_button/shuffle.png b/src/main/resources/assets/gregtech/textures/gui/overlay_button/shuffle.png
new file mode 100644
index 0000000000..376174b2c1
--- /dev/null
+++ b/src/main/resources/assets/gregtech/textures/gui/overlay_button/shuffle.png
Binary files differ
diff --git a/src/main/resources/assets/gregtech/textures/items/gt.WirelessHeadphones.png b/src/main/resources/assets/gregtech/textures/items/gt.WirelessHeadphones.png
new file mode 100644
index 0000000000..bcf563dd87
--- /dev/null
+++ b/src/main/resources/assets/gregtech/textures/items/gt.WirelessHeadphones.png
Binary files differ
diff --git a/src/main/resources/assets/miscutils/lang/en_US.lang b/src/main/resources/assets/miscutils/lang/en_US.lang
index 6fe80a2360..baece34a33 100644
--- a/src/main/resources/assets/miscutils/lang/en_US.lang
+++ b/src/main/resources/assets/miscutils/lang/en_US.lang
@@ -50,6 +50,7 @@ tile.blockVolumetricFlaskSetter.name=Volumetric Flask Configurator
tile.blockSuperLight.name=Shining Star
tile.blockPestKiller.name=Pest Killer
tile.blockSuperJukebox.name=Sir Mixalot [Jukebox]
+tile.blockSuperJukebox.deprecated=§cDEPRECATED, Use the Electric Jukeboxes instead
//Tools
item.itemBufferCore.name=Energy Core
diff --git a/src/main/resources/soundmeta/durations.json b/src/main/resources/soundmeta/durations.json
new file mode 100644
index 0000000000..845e6d0833
--- /dev/null
+++ b/src/main/resources/soundmeta/durations.json
@@ -0,0 +1,16 @@
+{
+ "soundDurationsMs": {
+ "minecraft:11": 71112,
+ "minecraft:13": 178086,
+ "minecraft:blocks": 345914,
+ "minecraft:cat": 185343,
+ "minecraft:chirp": 185584,
+ "minecraft:far": 174462,
+ "minecraft:mall": 197219,
+ "minecraft:mellohi": 96199,
+ "minecraft:stal": 150858,
+ "minecraft:strad": 188168,
+ "minecraft:wait": 237886,
+ "minecraft:ward": 251393
+ }
+}
diff --git a/src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerInnerMixin.java b/src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerInnerMixin.java
new file mode 100644
index 0000000000..6150f495b6
--- /dev/null
+++ b/src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerInnerMixin.java
@@ -0,0 +1,33 @@
+package gregtech.mixin.mixins.early.minecraft;
+
+import net.minecraft.client.resources.IResource;
+import net.minecraft.client.resources.IResourceManager;
+import net.minecraft.util.ResourceLocation;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+
+import gregtech.client.SeekingOggCodec;
+
+@Mixin(targets = "net.minecraft.client.audio.SoundManager$2$1")
+public abstract class SoundManagerInnerMixin {
+
+ @WrapOperation(
+ method = "getInputStream",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/client/resources/IResourceManager;getResource(Lnet/minecraft/util/ResourceLocation;)Lnet/minecraft/client/resources/IResource;"))
+ IResource gt5u$stripSeekParams(IResourceManager instance, ResourceLocation location,
+ Operation<IResource> original) {
+ if (location.getResourcePath()
+ .endsWith(SeekingOggCodec.EXTENSION)) {
+ location = new ResourceLocation(
+ location.getResourceDomain(),
+ SeekingOggCodec.stripSeekMetadata(location.getResourcePath()));
+ }
+ return original.call(instance, location);
+ }
+}
diff --git a/src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerMixin.java b/src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerMixin.java
new file mode 100644
index 0000000000..f65d247a4e
--- /dev/null
+++ b/src/mixin/java/gregtech/mixin/mixins/early/minecraft/SoundManagerMixin.java
@@ -0,0 +1,52 @@
+package gregtech.mixin.mixins.early.minecraft;
+
+import net.minecraft.client.audio.ISound;
+import net.minecraft.client.audio.SoundManager;
+import net.minecraft.client.audio.SoundPoolEntry;
+import net.minecraft.util.ResourceLocation;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+
+import gregtech.api.util.GT_MusicSystem;
+import gregtech.client.ISeekingSound;
+import gregtech.client.SeekingOggCodec;
+
+@Mixin(SoundManager.class)
+public class SoundManagerMixin {
+
+ @WrapOperation(
+ method = "playSound",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/client/audio/SoundPoolEntry;getSoundPoolEntryLocation()Lnet/minecraft/util/ResourceLocation;"))
+ ResourceLocation gt5u$wrap(SoundPoolEntry instance, Operation<ResourceLocation> original,
+ @Local(argsOnly = true) ISound sound) {
+ ResourceLocation result = original.call(instance);
+ if (sound instanceof ISeekingSound seekingSound) {
+ result = SeekingOggCodec.seekResource(result, seekingSound.getSeekMillisecondOffset());
+ }
+ return result;
+ }
+
+ @Inject(method = "stopAllSounds", at = @At("HEAD"))
+ void gt5u$notifyOfSoundStop(CallbackInfo ci) {
+ GT_MusicSystem.ClientSystem.onSoundBatchStop();
+ }
+
+ @Inject(method = "pauseAllSounds", at = @At("HEAD"))
+ void gt5u$notifyOfSoundPause(CallbackInfo ci) {
+ GT_MusicSystem.ClientSystem.onSoundBatchPause();
+ }
+
+ @Inject(method = "resumeAllSounds", at = @At("HEAD"))
+ void gt5u$notifyOfSoundResume(CallbackInfo ci) {
+ GT_MusicSystem.ClientSystem.onSoundBatchResume();
+ }
+}