aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/api/util/GTMusicSystem.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/gregtech/api/util/GTMusicSystem.java')
-rw-r--r--src/main/java/gregtech/api/util/GTMusicSystem.java667
1 files changed, 667 insertions, 0 deletions
diff --git a/src/main/java/gregtech/api/util/GTMusicSystem.java b/src/main/java/gregtech/api/util/GTMusicSystem.java
new file mode 100644
index 0000000000..362c397e67
--- /dev/null
+++ b/src/main/java/gregtech/api/util/GTMusicSystem.java
@@ -0,0 +1,667 @@
+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.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.Loader;
+import cpw.mods.fml.common.network.ByteBufUtils;
+import gregtech.GTMod;
+import gregtech.api.enums.GTValues;
+import gregtech.api.net.GTPacketMusicSystemData;
+import gregtech.client.ElectricJukeboxSound;
+import gregtech.common.items.ItemWirelessHeadphones;
+import gregtech.common.tileentities.machines.basic.MTEBetterJukebox;
+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 GTMusicSystem {
+
+ private GTMusicSystem() {}
+
+ 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 MTEBetterJukebox.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 = MTEBetterJukebox.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;
+ GTValues.NW.sendToAll(new GTPacketMusicSystemData(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) {
+ GTMod.GT_FML_LOGGER.warn("Skipping {}", record.recordName, e);
+ }
+ }
+ GTMod.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 ItemWirelessHeadphones 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.
+ GTMusicSystem.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(
+ GTMusicSystem.class.getClassLoader()
+ .getResources("soundmeta/durations.json"));
+ final Path configPath = Loader.instance()
+ .getConfigDir()
+ .toPath()
+ .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) {
+ GTMod.GT_FML_LOGGER.error("Could not parse sound durations from {}", url, e);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ musicRecordsInitialized = true;
+ return musicRecordDurations;
+ }
+ }
+
+}