diff options
38 files changed, 2090 insertions, 12 deletions
@@ -40,6 +40,22 @@ the chance of strange errors. Some textures/ideas have been taken from future versions of GT and texture pack authors for GTNH. Credit goes to Jimbno for the UU-Tex texture pack and its contributions to the base pack here: https://github.com/Jimbno/UU-Tex. +## Music duration metadata + +The electric jukebox requires duration metadata to specify how many milliseconds each disk plays for. +These can be included in mods' jar resources under `soundmeta/durations.json`, or in the pack config directory at `config/soundmeta/durations.json`. +The format is a simple key-value map of sound IDs mapping to millisecond counts, and can be generated from the client automatically using `/gt dump_music_durations`. + +```json +{ + "soundDurationsMs": { + "minecraft:11": 71112, + "minecraft:13": 178086, + "minecraft:blocks": 345914 + } +} +``` + ## License GT5-Unofficial is free software: you can redistribute it and/or modify it under the terms of the diff --git a/dependencies.gradle b/dependencies.gradle index bcc5705cd7..ef6223d7fb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -49,6 +49,9 @@ dependencies { compileOnlyApi('com.github.GTNewHorizons:ThaumicTinkerer:2.10.1:dev') compileOnlyApi("com.github.GTNewHorizons:Mobs-Info:0.4.1-GTNH:dev") compileOnlyApi("com.github.GTNewHorizons:Navigator:1.0.6:dev") + implementation('com.github.GTNewHorizons:Baubles:1.0.4:dev') {transitive=false} + // Required to prevent an older bauble api from Extra Utilities from loading first in the javac classpath + compileOnly('com.github.GTNewHorizons:Baubles:1.0.4:dev') {transitive=false} devOnlyNonPublishable("com.github.GTNewHorizons:Infernal-Mobs:1.8.1-GTNH:dev") @@ -89,7 +92,6 @@ dependencies { compileOnly("com.github.GTNewHorizons:CraftTweaker:3.3.1:dev") { transitive = false } compileOnly("com.github.GTNewHorizons:BetterLoadingScreen:1.7.0-GTNH:dev") { transitive = false } - implementation('com.github.GTNewHorizons:Baubles:1.0.4:dev') {transitive=false} compileOnly('com.github.GTNewHorizons:SC2:2.1.1:dev') {transitive=false} compileOnly('com.github.GTNewHorizons:Binnie:2.4.1:dev') {transitive = false} compileOnly('curse.maven:PlayerAPI-228969:2248928') {transitive=false} diff --git a/gradle.properties b/gradle.properties index 07b09c6bc5..7af9b10202 100644 --- a/gradle.properties +++ b/gradle.properties @@ -80,7 +80,7 @@ apiPackage = # Specify the configuration file for Forge's access transformers here. It must be placed into /src/main/resources/META-INF/ # There can be multiple files in a space-separated list. # Example value: mymodid_at.cfg nei_at.cfg -accessTransformersFile = ggfab_at.cfg tectech_at.cfg +accessTransformersFile = gregtech_at.cfg # Provides setup for Mixins if enabled. If you don't know what mixins are: Keep it disabled! usesMixins = true 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 Binary files differnew file mode 100644 index 0000000000..97fde2fa1e --- /dev/null +++ b/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_SIDE_JUKEBOX.png 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 Binary files differnew file mode 100644 index 0000000000..188aaa7391 --- /dev/null +++ b/src/main/resources/assets/gregtech/textures/blocks/iconsets/OVERLAY_TOP_JUKEBOX.png 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 Binary files differnew file mode 100644 index 0000000000..376174b2c1 --- /dev/null +++ b/src/main/resources/assets/gregtech/textures/gui/overlay_button/shuffle.png diff --git a/src/main/resources/assets/gregtech/textures/items/gt.WirelessHeadphones.png b/src/main/resources/assets/gregtech/textures/items/gt.WirelessHeadphones.png Binary files differnew file mode 100644 index 0000000000..bcf563dd87 --- /dev/null +++ b/src/main/resources/assets/gregtech/textures/items/gt.WirelessHeadphones.png 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(); + } +} |