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. *
* 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