From 9c5d7117f01b9df1020b706f83328c63d89d1420 Mon Sep 17 00:00:00 2001 From: P3pp3rF1y Date: Wed, 8 Jan 2025 14:55:20 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20Added=20advanced=20jukebox?= =?UTF-8?q?=20upgrade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- .../client/gui/SettingsTabBase.java | 2 +- .../sophisticatedcore/client/gui/Tab.java | 4 +- .../init/ModCoreDataComponents.java | 13 + .../sophisticatedcore/init/ModPayloads.java | 4 +- .../jukebox/JukeboxUpgradeConfig.java | 16 + .../jukebox/JukeboxUpgradeContainer.java | 89 +++++- .../upgrades/jukebox/JukeboxUpgradeItem.java | 141 ++------- .../upgrades/jukebox/JukeboxUpgradeTab.java | 163 ++++++++-- .../jukebox/JukeboxUpgradeWrapper.java | 287 ++++++++++++++++++ .../upgrades/jukebox/RepeatMode.java | 50 +++ .../jukebox/ServerStorageSoundHandler.java | 31 +- .../SoundFinishedNotificationPayload.java | 27 ++ .../jukebox/SoundStopNotificationPayload.java | 27 -- .../upgrades/jukebox/StorageSoundHandler.java | 3 +- .../assets/sophisticatedcore/lang/en_us.json | 10 + .../sophisticatedcore/textures/gui/icons.png | Bin 11392 -> 11884 bytes 17 files changed, 665 insertions(+), 204 deletions(-) create mode 100644 src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeConfig.java create mode 100644 src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeWrapper.java create mode 100644 src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/RepeatMode.java create mode 100644 src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundFinishedNotificationPayload.java delete mode 100644 src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundStopNotificationPayload.java diff --git a/gradle.properties b/gradle.properties index c69cbd6b..d32a071a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ loader_version_range=[4,) mod_id=sophisticatedcore mod_name=Sophisticated Core mod_license=GNU General Public License v3.0 -mod_version=1.0.13 +mod_version=1.1.0 mod_group_id=sophisticatedcore mod_authors=P3pp3rF1y mod_description=A library / shared functionality mod for Sophisticated Storage and Backpacks diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/SettingsTabBase.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/SettingsTabBase.java index 67a89373..3a371990 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/SettingsTabBase.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/SettingsTabBase.java @@ -20,7 +20,7 @@ public abstract class SettingsTabBase> extends Tab { private static final int RIGHT_BORDER_WIDTH = 6; - private static final int BOTTOM_BORDER_HEIGHT = 7; + private static final int BOTTOM_BORDER_HEIGHT = 6; protected final T screen; protected Dimension openTabDimension = new Dimension(0, 0); diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/Tab.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/Tab.java index 2e28ea31..b28cada3 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/Tab.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/client/gui/Tab.java @@ -88,8 +88,10 @@ public Optional getTabRectangle() { @Override protected void renderBg(GuiGraphics guiGraphics, Minecraft minecraft, int mouseX, int mouseY) { int halfHeight = height / 2; + int oddHeightAddition = height % 2; + int secondHalfHeight = halfHeight + oddHeightAddition; guiGraphics.blit(GuiHelper.GUI_CONTROLS, x, y, (float) TEXTURE_WIDTH - width, 0, width, halfHeight, TEXTURE_WIDTH, TEXTURE_HEIGHT); - guiGraphics.blit(GuiHelper.GUI_CONTROLS, x, y + halfHeight, (float) TEXTURE_WIDTH - width, (float) TEXTURE_HEIGHT - halfHeight, width, halfHeight, TEXTURE_WIDTH, TEXTURE_HEIGHT); + guiGraphics.blit(GuiHelper.GUI_CONTROLS, x, y + halfHeight, (float) TEXTURE_WIDTH - width, (float) TEXTURE_HEIGHT - secondHalfHeight, width, secondHalfHeight, TEXTURE_WIDTH, TEXTURE_HEIGHT); guiGraphics.blit(GuiHelper.GUI_CONTROLS, x - 3, y, TEXTURE_WIDTH / 2, TEXTURE_HEIGHT - height, 3, height); } diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModCoreDataComponents.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModCoreDataComponents.java index 9e69ebc0..b94c5b4a 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModCoreDataComponents.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModCoreDataComponents.java @@ -17,6 +17,7 @@ import net.p3pp3rf1y.sophisticatedcore.upgrades.FilterAttributes; import net.p3pp3rf1y.sophisticatedcore.upgrades.feeding.HungerLevel; import net.p3pp3rf1y.sophisticatedcore.upgrades.filter.Direction; +import net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox.RepeatMode; import net.p3pp3rf1y.sophisticatedcore.upgrades.xppump.AutomationDirection; import net.p3pp3rf1y.sophisticatedcore.util.SimpleItemContent; @@ -152,6 +153,18 @@ public class ModCoreDataComponents { public static final Supplier> ENABLED = DATA_COMPONENT_TYPES.register("enabled", () -> new DataComponentType.Builder().persistent(Codec.BOOL).networkSynchronized(ByteBufCodecs.BOOL).build()); + public static final Supplier> REPEAT_MODE = DATA_COMPONENT_TYPES.register("repeat_mode", + () -> new DataComponentType.Builder().persistent(RepeatMode.CODEC).networkSynchronized(RepeatMode.STREAM_CODEC).build()); + + public static final Supplier> SHUFFLE = DATA_COMPONENT_TYPES.register("shuffle", + () -> new DataComponentType.Builder().persistent(Codec.BOOL).networkSynchronized(ByteBufCodecs.BOOL).build()); + + public static final Supplier> DISC_SLOT_ACTIVE = DATA_COMPONENT_TYPES.register("disc_slot_active", + () -> new DataComponentType.Builder().persistent(Codec.INT).networkSynchronized(ByteBufCodecs.INT).build()); + + public static final Supplier> DISC_FINISH_TIME = DATA_COMPONENT_TYPES.register("disc_finish_time", + () -> new DataComponentType.Builder().persistent(Codec.LONG).networkSynchronized(ByteBufCodecs.VAR_LONG).build()); + public static void register(IEventBus modBus) { DATA_COMPONENT_TYPES.register(modBus); } diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModPayloads.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModPayloads.java index af203896..031240e5 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModPayloads.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/init/ModPayloads.java @@ -5,7 +5,7 @@ import net.p3pp3rf1y.sophisticatedcore.SophisticatedCore; import net.p3pp3rf1y.sophisticatedcore.network.*; import net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox.PlayDiscPayload; -import net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox.SoundStopNotificationPayload; +import net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox.SoundFinishedNotificationPayload; import net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox.StopDiscPlaybackPayload; import net.p3pp3rf1y.sophisticatedcore.upgrades.tank.TankClickPayload; @@ -21,7 +21,7 @@ public static void registerPayloads(final RegisterPayloadHandlersEvent event) { registrar.playToClient(SyncPlayerSettingsPayload.TYPE, SyncPlayerSettingsPayload.STREAM_CODEC, SyncPlayerSettingsPayload::handlePayload); registrar.playToClient(PlayDiscPayload.TYPE, PlayDiscPayload.STREAM_CODEC, PlayDiscPayload::handlePayload); registrar.playToClient(StopDiscPlaybackPayload.TYPE, StopDiscPlaybackPayload.STREAM_CODEC, StopDiscPlaybackPayload::handlePayload); - registrar.playToServer(SoundStopNotificationPayload.TYPE, SoundStopNotificationPayload.STREAM_CODEC, SoundStopNotificationPayload::handlePayload); + registrar.playToServer(SoundFinishedNotificationPayload.TYPE, SoundFinishedNotificationPayload.STREAM_CODEC, SoundFinishedNotificationPayload::handlePayload); registrar.playToServer(TankClickPayload.TYPE, TankClickPayload.STREAM_CODEC, TankClickPayload::handlePayload); registrar.playToServer(TransferItemsPayload.TYPE, TransferItemsPayload.STREAM_CODEC, TransferItemsPayload::handlePayload); registrar.playToClient(SyncTemplateSettingsPayload.TYPE, SyncTemplateSettingsPayload.STREAM_CODEC, SyncTemplateSettingsPayload::handlePayload); diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeConfig.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeConfig.java new file mode 100644 index 00000000..4388b7a9 --- /dev/null +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeConfig.java @@ -0,0 +1,16 @@ +package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; + +import net.neoforged.neoforge.common.ModConfigSpec; + +public class JukeboxUpgradeConfig { + public final ModConfigSpec.IntValue numberOfSlots; + public final ModConfigSpec.IntValue slotsInRow; + + public JukeboxUpgradeConfig(ModConfigSpec.Builder builder, String upgradeName, String path, int defaultNumberOfSlots) { + builder.comment(upgradeName + " Settings").push(path); + numberOfSlots = builder.comment("Number of slots for discs in jukebox upgrade").defineInRange("numberOfSlots", defaultNumberOfSlots, 1, 16); + slotsInRow = builder.comment("Number of lots displayed in a row").defineInRange("slotsInRow", 4, 1, 6); + + builder.pop(); + } +} diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeContainer.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeContainer.java index 2e47afc7..bc01de9e 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeContainer.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeContainer.java @@ -1,42 +1,60 @@ package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; +import net.minecraft.core.Holder; import net.minecraft.nbt.CompoundTag; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.JukeboxSong; +import net.minecraft.world.level.Level; import net.neoforged.neoforge.items.SlotItemHandler; import net.p3pp3rf1y.sophisticatedcore.common.gui.StorageContainerMenuBase; import net.p3pp3rf1y.sophisticatedcore.common.gui.UpgradeContainerBase; import net.p3pp3rf1y.sophisticatedcore.common.gui.UpgradeContainerType; import net.p3pp3rf1y.sophisticatedcore.util.NBTHelper; -public class JukeboxUpgradeContainer extends UpgradeContainerBase { +import java.util.Optional; + +public class JukeboxUpgradeContainer extends UpgradeContainerBase { private static final String ACTION_DATA = "action"; - public JukeboxUpgradeContainer(Player player, int upgradeContainerId, JukeboxUpgradeItem.Wrapper upgradeWrapper, UpgradeContainerType type) { + public JukeboxUpgradeContainer(Player player, int upgradeContainerId, JukeboxUpgradeWrapper upgradeWrapper, UpgradeContainerType type) { super(player, upgradeContainerId, upgradeWrapper, type); - slots.add(new SlotItemHandler(upgradeWrapper.getDiscInventory(), 0, -100, -100) { - @Override - public void setChanged() { - super.setChanged(); - if (upgradeWrapper.isPlaying()) { - upgradeWrapper.stop(player); + for (int slot = 0; slot < upgradeWrapper.getDiscInventory().getSlots(); slot++) { + slots.add(new SlotItemHandler(upgradeWrapper.getDiscInventory(), slot, -100, -100) { + @Override + public void setChanged() { + super.setChanged(); + if (upgradeWrapper.isPlaying() && getSlotIndex() == upgradeWrapper.getDiscSlotActive()) { + upgradeWrapper.stop(player); + } } - } - }); + }); + } } @Override public void handlePacket(CompoundTag data) { if (data.contains(ACTION_DATA)) { String actionName = data.getString(ACTION_DATA); - if (actionName.equals("play")) { - if (player.containerMenu instanceof StorageContainerMenuBase storageContainerMenu) { - storageContainerMenu.getBlockPosition().ifPresentOrElse(pos -> upgradeWrapper.play(player.level(), pos), () -> upgradeWrapper.play(storageContainerMenu.getEntity().orElse(player))); + switch (actionName) { + case "play" -> { + if (player.containerMenu instanceof StorageContainerMenuBase storageContainerMenu) { + storageContainerMenu.getBlockPosition().ifPresentOrElse(pos -> upgradeWrapper.play(player.level(), pos), () -> upgradeWrapper.play(storageContainerMenu.getEntity().orElse(player))); + } } - } else if (actionName.equals("stop")) { - upgradeWrapper.stop(player); + case "stop" -> upgradeWrapper.stop(player); + case "next" -> upgradeWrapper.next(); + case "previous" -> upgradeWrapper.previous(); } } + if (data.contains("shuffle")) { + upgradeWrapper.setShuffleEnabled(data.getBoolean("shuffle")); + } + + if (data.contains("repeat")) { + NBTHelper.getEnumConstant(data, "repeat", RepeatMode::fromName).ifPresent(upgradeWrapper::setRepeatMode); + } } public void play() { @@ -46,4 +64,45 @@ public void play() { public void stop() { sendDataToServer(() -> NBTHelper.putString(new CompoundTag(), ACTION_DATA, "stop")); } + + public void next() { + sendDataToServer(() -> NBTHelper.putString(new CompoundTag(), ACTION_DATA, "next")); + } + + public void previous() { + sendDataToServer(() -> NBTHelper.putString(new CompoundTag(), ACTION_DATA, "previous")); + } + + public boolean isShuffleEnabled() { + return upgradeWrapper.isShuffleEnabled(); + } + + public void toggleShuffle() { + boolean newValue = !upgradeWrapper.isShuffleEnabled(); + upgradeWrapper.setShuffleEnabled(newValue); + sendBooleanToServer("shuffle", newValue); + } + + public RepeatMode getRepeatMode() { + return upgradeWrapper.getRepeatMode(); + } + + public void toggleRepeat() { + RepeatMode newValue = upgradeWrapper.getRepeatMode().next(); + upgradeWrapper.setRepeatMode(newValue); + sendDataToServer(() -> NBTHelper.putEnumConstant(new CompoundTag(), "repeat", newValue)); + } + + public Optional getDiscSlotActive() { + int discSlotActive = upgradeWrapper.getDiscSlotActive(); + return discSlotActive > -1 ? Optional.of(slots.get(discSlotActive)) : Optional.empty(); + } + + public long getDiscFinishTime() { + return upgradeWrapper.getDiscFinishTime(); + } + + public Optional> getJukeboxSong(Level level) { + return upgradeWrapper.getJukeboxSongHolder(level); + } } diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeItem.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeItem.java index 05c7cd6c..5d25be52 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeItem.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeItem.java @@ -1,35 +1,28 @@ package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; -import net.minecraft.core.BlockPos; -import net.minecraft.core.component.DataComponents; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.JukeboxSong; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.Vec3; -import net.neoforged.neoforge.items.ComponentItemHandler; -import net.neoforged.neoforge.items.IItemHandler; -import net.p3pp3rf1y.sophisticatedcore.api.IStorageWrapper; -import net.p3pp3rf1y.sophisticatedcore.init.ModCoreDataComponents; -import net.p3pp3rf1y.sophisticatedcore.upgrades.*; +import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.TranslationHelper; +import net.p3pp3rf1y.sophisticatedcore.upgrades.IUpgradeCountLimitConfig; +import net.p3pp3rf1y.sophisticatedcore.upgrades.UpgradeGroup; +import net.p3pp3rf1y.sophisticatedcore.upgrades.UpgradeItemBase; +import net.p3pp3rf1y.sophisticatedcore.upgrades.UpgradeType; -import javax.annotation.Nullable; import java.util.List; -import java.util.UUID; -import java.util.function.BiConsumer; -import java.util.function.Consumer; +import java.util.function.IntSupplier; -public class JukeboxUpgradeItem extends UpgradeItemBase { - public static final UpgradeType TYPE = new UpgradeType<>(Wrapper::new); +public class JukeboxUpgradeItem extends UpgradeItemBase { + public static final UpgradeGroup UPGRADE_GROUP = new UpgradeGroup("jukebox_upgrades", TranslationHelper.INSTANCE.translUpgradeGroup("jukebox_upgrades")); + public static final UpgradeType TYPE = new UpgradeType<>(JukeboxUpgradeWrapper::new); + private final IntSupplier numberOfSlots; + private final IntSupplier slotsInRow; - public JukeboxUpgradeItem(IUpgradeCountLimitConfig upgradeTypeLimitConfig) { + public JukeboxUpgradeItem(IUpgradeCountLimitConfig upgradeTypeLimitConfig, IntSupplier numberOfSlots, IntSupplier slotsInRow) { super(upgradeTypeLimitConfig); + this.numberOfSlots = numberOfSlots; + this.slotsInRow = slotsInRow; } @Override - public UpgradeType getType() { + public UpgradeType getType() { return TYPE; } @@ -38,101 +31,17 @@ public List getUpgradeConflicts() { return List.of(); } - public static class Wrapper extends UpgradeWrapperBase implements ITickableUpgrade { - private static final int KEEP_ALIVE_SEND_INTERVAL = 5; - private final ComponentItemHandler discInventory; - private long lastKeepAliveSendTime = 0; - private boolean isPlaying; - - protected Wrapper(IStorageWrapper storageWrapper, ItemStack upgrade, Consumer upgradeSaveHandler) { - super(storageWrapper, upgrade, upgradeSaveHandler); - discInventory = new ComponentItemHandler(upgrade, DataComponents.CONTAINER, 1) { - @Override - protected void onContentsChanged(int slot, ItemStack oldStack, ItemStack newStack) { - super.onContentsChanged(slot, oldStack, newStack); - save(); - } - - @Override - public boolean isItemValid(int slot, ItemStack stack) { - return stack.isEmpty() || stack.has(DataComponents.JUKEBOX_PLAYABLE); - } - }; - isPlaying = upgrade.getOrDefault(ModCoreDataComponents.IS_PLAYING, false); - } - - public void setDisc(ItemStack disc) { - discInventory.setStackInSlot(0, disc); - } - - public ItemStack getDisc() { - return discInventory.getStackInSlot(0); - } - - public void play(Level level, BlockPos pos) { - play(level, (serverLevel, storageUuid) -> JukeboxSong.fromStack(level.registryAccess(), getDisc()) - .ifPresent(song -> ServerStorageSoundHandler.startPlayingDisc(serverLevel, pos, storageUuid, song, () -> setIsPlaying(false)))); - } - - public void play(Entity entity) { - play(entity.level(), (world, storageUuid) -> JukeboxSong.fromStack(entity.level().registryAccess(), getDisc()) - .ifPresent(song -> ServerStorageSoundHandler.startPlayingDisc(world, entity.position(), storageUuid, entity.getId(), song, () -> setIsPlaying(false)))); - } - - private void play(Level level, BiConsumer play) { - if (!(level instanceof ServerLevel) || getDisc().isEmpty()) { - return; - } - storageWrapper.getContentsUuid().ifPresent(storageUuid -> play.accept((ServerLevel) level, storageUuid)); - setIsPlaying(true); - } - - private void setIsPlaying(boolean playing) { - isPlaying = playing; - upgrade.set(ModCoreDataComponents.IS_PLAYING, playing); - if (isPlaying) { - storageWrapper.getRenderInfo().setUpgradeRenderData(JukeboxUpgradeRenderData.TYPE, new JukeboxUpgradeRenderData(true)); - } else { - removeRenderData(); - } - save(); - } - - private void removeRenderData() { - storageWrapper.getRenderInfo().removeUpgradeRenderData(JukeboxUpgradeRenderData.TYPE); - } - - public void stop(LivingEntity entity) { - if (!(entity.level() instanceof ServerLevel)) { - return; - } - storageWrapper.getContentsUuid().ifPresent(storageUuid -> - ServerStorageSoundHandler.stopPlayingDisc(entity.level(), entity.position(), storageUuid) - ); - setIsPlaying(false); - } - - public IItemHandler getDiscInventory() { - return discInventory; - } - - @Override - public void tick(@Nullable Entity entity, Level level, BlockPos pos) { - if (isPlaying && lastKeepAliveSendTime < level.getGameTime() - KEEP_ALIVE_SEND_INTERVAL) { - storageWrapper.getContentsUuid().ifPresent(storageUuid -> - ServerStorageSoundHandler.updateKeepAlive(storageUuid, level, entity != null ? entity.position() : Vec3.atCenterOf(pos), () -> setIsPlaying(false)) - ); - lastKeepAliveSendTime = level.getGameTime(); - } - } + @Override + public UpgradeGroup getUpgradeGroup() { + return UPGRADE_GROUP; + } - public boolean isPlaying() { - return isPlaying; - } + public int getNumberOfSlots() { + return numberOfSlots.getAsInt(); + } - @Override - public void onBeforeRemoved() { - removeRenderData(); - } + public int getSlotsInRow() { + return slotsInRow.getAsInt(); } + } diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeTab.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeTab.java index bed19e8b..f24e7a6f 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeTab.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeTab.java @@ -1,57 +1,170 @@ package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; +import com.mojang.blaze3d.systems.RenderSystem; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.Holder; import net.minecraft.network.chat.Component; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.JukeboxSong; import net.p3pp3rf1y.sophisticatedcore.client.gui.StorageScreenBase; import net.p3pp3rf1y.sophisticatedcore.client.gui.UpgradeSettingsTab; import net.p3pp3rf1y.sophisticatedcore.client.gui.controls.Button; import net.p3pp3rf1y.sophisticatedcore.client.gui.controls.ButtonDefinition; -import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.Dimension; -import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.GuiHelper; -import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.Position; -import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.TextureBlitData; -import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.TranslationHelper; -import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.UV; +import net.p3pp3rf1y.sophisticatedcore.client.gui.controls.ToggleButton; +import net.p3pp3rf1y.sophisticatedcore.client.gui.utils.*; + +import java.util.Map; +import java.util.Optional; import static net.p3pp3rf1y.sophisticatedcore.client.gui.utils.GuiHelper.*; -public class JukeboxUpgradeTab extends UpgradeSettingsTab { +public abstract class JukeboxUpgradeTab extends UpgradeSettingsTab { private static final TextureBlitData PLAY_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(16, 64), Dimension.SQUARE_16); - private static final ButtonDefinition PLAY = new ButtonDefinition(Dimension.SQUARE_16, DEFAULT_BUTTON_BACKGROUND, DEFAULT_BUTTON_HOVERED_BACKGROUND, PLAY_FOREGROUND, + private static final ButtonDefinition PLAY = new ButtonDefinition(Dimension.SQUARE_18, DEFAULT_BUTTON_BACKGROUND, DEFAULT_BUTTON_HOVERED_BACKGROUND, PLAY_FOREGROUND, Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("play"))); private static final TextureBlitData STOP_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(0, 64), Dimension.SQUARE_16); - private static final ButtonDefinition STOP = new ButtonDefinition(Dimension.SQUARE_16, DEFAULT_BUTTON_BACKGROUND, DEFAULT_BUTTON_HOVERED_BACKGROUND, STOP_FOREGROUND, + private static final ButtonDefinition STOP = new ButtonDefinition(Dimension.SQUARE_18, DEFAULT_BUTTON_BACKGROUND, DEFAULT_BUTTON_HOVERED_BACKGROUND, STOP_FOREGROUND, Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("stop"))); - public JukeboxUpgradeTab(JukeboxUpgradeContainer upgradeContainer, Position position, StorageScreenBase screen) { - super(upgradeContainer, position, screen, TranslationHelper.INSTANCE.translUpgrade("jukebox"), TranslationHelper.INSTANCE.translUpgradeTooltip("jukebox")); + private static final TextureBlitData SHUFFLE_ON_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(96, 80), Dimension.SQUARE_16); + private static final TextureBlitData SHUFFLE_OFF_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(112, 80), Dimension.SQUARE_16); + private static final ButtonDefinition.Toggle SHUFFLE = new ButtonDefinition.Toggle<>(Dimension.SQUARE_18, DEFAULT_BUTTON_BACKGROUND, + Map.of( + true, new ToggleButton.StateData(SHUFFLE_ON_FOREGROUND, Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("shuffle_on"))), + false, new ToggleButton.StateData(SHUFFLE_OFF_FOREGROUND, Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("shuffle_off"))) + ), DEFAULT_BUTTON_HOVERED_BACKGROUND); - addHideableChild(new Button(new Position(x + 3, y + 44), STOP, button -> { - if (button == 0) { - getContainer().stop(); - } - })); - addHideableChild(new Button(new Position(x + 21, y + 44), PLAY, button -> { - if (button == 0) { - getContainer().play(); - } - })); + private static final TextureBlitData REPEAT_ALL_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(128, 80), Dimension.SQUARE_16); + private static final TextureBlitData REPEAT_ONE_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(144, 80), Dimension.SQUARE_16); + private static final TextureBlitData NO_REPEAT_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(160, 80), Dimension.SQUARE_16); + private static final ButtonDefinition.Toggle REPEAT = new ButtonDefinition.Toggle<>(Dimension.SQUARE_18, DEFAULT_BUTTON_BACKGROUND, + Map.of( + RepeatMode.ALL, new ToggleButton.StateData(REPEAT_ALL_FOREGROUND, Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("repeat_all"))), + RepeatMode.ONE, new ToggleButton.StateData(REPEAT_ONE_FOREGROUND, Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("repeat_one"))), + RepeatMode.NO, new ToggleButton.StateData(NO_REPEAT_FOREGROUND, Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("no_repeat"))) + ), DEFAULT_BUTTON_HOVERED_BACKGROUND); + + private static final TextureBlitData PREVIOUS_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(48, 96), Dimension.SQUARE_16); + private static final ButtonDefinition PREVIOUS = new ButtonDefinition(Dimension.SQUARE_18, DEFAULT_BUTTON_BACKGROUND, DEFAULT_BUTTON_HOVERED_BACKGROUND, PREVIOUS_FOREGROUND, + Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("previous_disc"))); + + private static final TextureBlitData NEXT_FOREGROUND = new TextureBlitData(ICONS, new Position(1, 1), Dimension.SQUARE_256, new UV(32, 96), Dimension.SQUARE_16); + private static final ButtonDefinition NEXT = new ButtonDefinition(Dimension.SQUARE_18, DEFAULT_BUTTON_BACKGROUND, DEFAULT_BUTTON_HOVERED_BACKGROUND, NEXT_FOREGROUND, + Component.translatable(TranslationHelper.INSTANCE.translUpgradeButton("next_disc"))); + private static final int BUTTON_PADDING = 3; + public static final int TOP_Y = 24; + + private final int slotsInRow; + + public JukeboxUpgradeTab(JukeboxUpgradeContainer upgradeContainer, Position position, StorageScreenBase screen, int slotsInRow, Component tabLabel, Component closedTooltip) { + super(upgradeContainer, position, screen, tabLabel, closedTooltip); + this.slotsInRow = slotsInRow; } @Override protected void renderBg(GuiGraphics guiGraphics, Minecraft minecraft, int mouseX, int mouseY) { super.renderBg(guiGraphics, minecraft, mouseX, mouseY); if (getContainer().isOpen()) { - GuiHelper.renderSlotsBackground(guiGraphics, x + 3, y + 24, 1, 1); + GuiHelper.renderSlotsBackground(guiGraphics, x + 3, y + 24, slotsInRow, getContainer().getSlots().size() / slotsInRow, getContainer().getSlots().size() % slotsInRow); } } @Override protected void moveSlotsToTab() { - Slot discSlot = getContainer().getSlots().get(0); - discSlot.x = x - screen.getGuiLeft() + 4; - discSlot.y = y - screen.getGuiTop() + 25; + int slotIndex = 0; + for (Slot discSlot : getContainer().getSlots()) { + discSlot.x = x - screen.getGuiLeft() + 4 + (slotIndex % slotsInRow) * 18; + discSlot.y = y - screen.getGuiTop() + TOP_Y + 1 + (slotIndex / slotsInRow) * 18; + slotIndex++; + } + } + + protected int getBottomSlotY() { + return TOP_Y + (getContainer().getSlots().size() / slotsInRow) * 18 + (getContainer().getSlots().size() % slotsInRow > 0 ? 18 : 0); + } + + public static class Basic extends JukeboxUpgradeTab { + public Basic(JukeboxUpgradeContainer upgradeContainer, Position position, StorageScreenBase screen) { + super(upgradeContainer, position, screen, 4, TranslationHelper.INSTANCE.translUpgrade("jukebox"), TranslationHelper.INSTANCE.translUpgradeTooltip("jukebox")); + int bottomSlotY = getBottomSlotY(); + addHideableChild(new Button(new Position(x + 3, y + bottomSlotY + BUTTON_PADDING), STOP, button -> { + if (button == 0) { + getContainer().stop(); + } + })); + addHideableChild(new Button(new Position(x + 21, y + bottomSlotY + BUTTON_PADDING), PLAY, button -> { + if (button == 0) { + getContainer().play(); + } + })); + } + } + + public static class Advanced extends JukeboxUpgradeTab { + public Advanced(JukeboxUpgradeContainer upgradeContainer, Position position, StorageScreenBase screen, int slotsInRow) { + super(upgradeContainer, position, screen, slotsInRow, TranslationHelper.INSTANCE.translUpgrade("advanced_jukebox"), TranslationHelper.INSTANCE.translUpgradeTooltip("advanced_jukebox")); + int bottomSlotY = getBottomSlotY(); + addHideableChild(new Button(new Position(x + 3, y + bottomSlotY + BUTTON_PADDING), PREVIOUS, button -> { + if (button == 0) { + getContainer().previous(); + } + })); + addHideableChild(new Button(new Position(x + 21, y + bottomSlotY + BUTTON_PADDING), STOP, button -> { + if (button == 0) { + getContainer().stop(); + } + })); + addHideableChild(new Button(new Position(x + 39, y + bottomSlotY + BUTTON_PADDING), PLAY, button -> { + if (button == 0) { + getContainer().play(); + } + })); + addHideableChild(new Button(new Position(x + 57, y + bottomSlotY + BUTTON_PADDING), NEXT, button -> { + if (button == 0) { + getContainer().next(); + } + })); + addHideableChild(new ToggleButton<>(new Position(x + 12, y + bottomSlotY + BUTTON_PADDING + 20), SHUFFLE, button -> { + if (button == 0) { + getContainer().toggleShuffle(); + } + }, () -> getContainer().isShuffleEnabled())); + + addHideableChild(new ToggleButton<>(new Position(x + 48, y + bottomSlotY + BUTTON_PADDING + 20), REPEAT, button -> { + if (button == 0) { + getContainer().toggleRepeat(); + } + }, () -> getContainer().getRepeatMode())); + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, Minecraft minecraft, int mouseX, int mouseY) { + super.renderBg(guiGraphics, minecraft, mouseX, mouseY); + getContainer().getDiscSlotActive().ifPresent(slot -> renderPlaytimeOverLay(guiGraphics, 0x55_00CC00, screen.getLeftX() + slot.x, screen.getTopY() + slot.y, 16, 16)); + } + + private float getPlaybackRemainingProgress() { + long finishTime = getContainer().getDiscFinishTime(); + int remaining = (int) (finishTime - minecraft.level.getGameTime()); + + Optional> song = getContainer().getJukeboxSong(minecraft.level); + + return song.map(jukeboxSongHolder -> (remaining / (float) jukeboxSongHolder.value().lengthInTicks())).orElse(0f); + } + + private void renderPlaytimeOverLay(GuiGraphics guiGraphics, int slotColor, int xPos, int yPos, int width, int height) { + float remainingProgress = getPlaybackRemainingProgress(); + if (remainingProgress <= 0) { + return; + } + int progressOver = width - (int) (width * remainingProgress); + + RenderSystem.disableDepthTest(); + RenderSystem.colorMask(true, true, true, false); + guiGraphics.fillGradient(xPos + progressOver, yPos, xPos + width, yPos + height, 0, slotColor, slotColor); + RenderSystem.colorMask(true, true, true, true); + RenderSystem.enableDepthTest(); + } } } diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeWrapper.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeWrapper.java new file mode 100644 index 00000000..ee49f06a --- /dev/null +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/JukeboxUpgradeWrapper.java @@ -0,0 +1,287 @@ +package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.JukeboxSong; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.items.ComponentItemHandler; +import net.neoforged.neoforge.items.IItemHandler; +import net.p3pp3rf1y.sophisticatedcore.api.IStorageWrapper; +import net.p3pp3rf1y.sophisticatedcore.init.ModCoreDataComponents; +import net.p3pp3rf1y.sophisticatedcore.upgrades.ITickableUpgrade; +import net.p3pp3rf1y.sophisticatedcore.upgrades.UpgradeWrapperBase; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Consumer; + +public class JukeboxUpgradeWrapper extends UpgradeWrapperBase implements ITickableUpgrade { + private static final int KEEP_ALIVE_SEND_INTERVAL = 5; + private final ComponentItemHandler discInventory; + private long lastKeepAliveSendTime = 0; + private boolean isPlaying; + + private final LinkedList playlist = new LinkedList<>(); + private final LinkedList history = new LinkedList<>(); + + private final Set discsRemoved = new HashSet<>(); + private final Set discsAdded = new HashSet<>(); + + @Nullable + private Entity entityPlaying = null; + @Nullable + private Level levelPlaying = null; + @Nullable + private BlockPos posPlaying = null; + + private final Runnable onFinishedCallback = this::onDiscFinished; + + protected JukeboxUpgradeWrapper(IStorageWrapper storageWrapper, ItemStack upgrade, Consumer upgradeSaveHandler) { + super(storageWrapper, upgrade, upgradeSaveHandler); + discInventory = new ComponentItemHandler(upgrade, DataComponents.CONTAINER, upgradeItem.getNumberOfSlots()) { + @Override + protected void onContentsChanged(int slot, ItemStack oldStack, ItemStack newStack) { + super.onContentsChanged(slot, oldStack, newStack); + save(); + if (oldStack.isEmpty() && !newStack.isEmpty()) { + discsAdded.add(slot); + discsRemoved.remove(slot); + } else if (!oldStack.isEmpty() && newStack.isEmpty()) { + discsRemoved.add(slot); + discsAdded.remove(slot); + } + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + return stack.isEmpty() || stack.has(DataComponents.JUKEBOX_PLAYABLE); + } + }; + isPlaying = upgrade.getOrDefault(ModCoreDataComponents.IS_PLAYING, false); + } + + public boolean isShuffleEnabled() { + return upgrade.getOrDefault(ModCoreDataComponents.SHUFFLE, false); + } + + public void setShuffleEnabled(boolean shuffleEnabled) { + upgrade.set(ModCoreDataComponents.SHUFFLE, shuffleEnabled); + save(); + + initPlaylist(true); + } + + public RepeatMode getRepeatMode() { + return upgrade.getOrDefault(ModCoreDataComponents.REPEAT_MODE, RepeatMode.NO); + } + + public void setRepeatMode(RepeatMode repeatMode) { + upgrade.set(ModCoreDataComponents.REPEAT_MODE, repeatMode); + save(); + } + + public ItemStack getDisc() { + return getDiscSlotActive() > -1 ? discInventory.getStackInSlot(getDiscSlotActive()) : ItemStack.EMPTY; + } + + public int getDiscSlotActive() { + return upgrade.getOrDefault(ModCoreDataComponents.DISC_SLOT_ACTIVE, -1); + } + + private void setDiscSlotActive(int discSlotActive) { + upgrade.set(ModCoreDataComponents.DISC_SLOT_ACTIVE, discSlotActive); + save(); + } + + public void play(Level level, BlockPos pos) { + if (isPlaying) { + return; + } + + levelPlaying = level; + posPlaying = pos; + playNext(); + } + + public void play(Entity entity) { + if (isPlaying) { + return; + } + entityPlaying = entity; + playNext(); + } + + private void playDisc() { + Level level = entityPlaying != null ? entityPlaying.level() : levelPlaying; + if (!(level instanceof ServerLevel serverLevel) || (posPlaying == null && entityPlaying == null)) { + return; + } + if (getDisc().isEmpty()) { + return; + } + + storageWrapper.getContentsUuid().ifPresent(storageUuid -> getJukeboxSongHolder(level).ifPresent(song -> { + if (entityPlaying != null) { + ServerStorageSoundHandler.startPlayingDisc(serverLevel, entityPlaying.position(), storageUuid, entityPlaying.getId(), song, onFinishedCallback); + } else { + ServerStorageSoundHandler.startPlayingDisc(serverLevel, posPlaying, storageUuid, song, onFinishedCallback); + } + upgrade.set(ModCoreDataComponents.DISC_FINISH_TIME, level.getGameTime() + song.value().lengthInTicks()); + })); + setIsPlaying(true); + } + + public Optional> getJukeboxSongHolder(Level level) { + return JukeboxSong.fromStack(level.registryAccess(), getDisc()); + } + + private void onDiscFinished() { + if (getRepeatMode() == RepeatMode.ONE) { + playDisc(); + } else if (getRepeatMode() == RepeatMode.ALL) { + playNext(); + } else { + playNext(false); + } + } + + private void setIsPlaying(boolean playing) { + isPlaying = playing; + upgrade.set(ModCoreDataComponents.IS_PLAYING, playing); + if (isPlaying) { + storageWrapper.getRenderInfo().setUpgradeRenderData(JukeboxUpgradeRenderData.TYPE, new JukeboxUpgradeRenderData(true)); + } else { + removeRenderData(); + setDiscSlotActive(-1); + } + save(); + } + + private void removeRenderData() { + storageWrapper.getRenderInfo().removeUpgradeRenderData(JukeboxUpgradeRenderData.TYPE); + } + + public void stop(LivingEntity entity) { + if (!(entity.level() instanceof ServerLevel)) { + return; + } + storageWrapper.getContentsUuid().ifPresent(storageUuid -> + ServerStorageSoundHandler.stopPlayingDisc(entity.level(), entity.position(), storageUuid) + ); + setIsPlaying(false); + upgrade.remove(ModCoreDataComponents.DISC_FINISH_TIME); + setDiscSlotActive(-1); + } + + public IItemHandler getDiscInventory() { + return discInventory; + } + + @Override + public void tick(@Nullable Entity entity, Level level, BlockPos pos) { + if (!level.isClientSide()) { + if (!discsRemoved.isEmpty()) { + discsRemoved.forEach(index -> { + playlist.remove(index); + history.remove(index); + }); + discsRemoved.clear(); + } + if (!discsAdded.isEmpty()) { + playlist.addAll(discsAdded); + discsAdded.clear(); + } + } + + if (isPlaying && lastKeepAliveSendTime < level.getGameTime() - KEEP_ALIVE_SEND_INTERVAL) { + storageWrapper.getContentsUuid().ifPresent(storageUuid -> + ServerStorageSoundHandler.updateKeepAlive(storageUuid, level, entity != null ? entity.position() : Vec3.atCenterOf(pos), () -> setIsPlaying(false)) + ); + lastKeepAliveSendTime = level.getGameTime(); + } + } + + public boolean isPlaying() { + return isPlaying; + } + + @Override + public void onBeforeRemoved() { + removeRenderData(); + } + + public void next() { + if (!isPlaying) { + return; + } + playNext(); + } + + public void playNext() { + playNext(true); + } + + public void playNext(boolean startOverIfAtTheEnd) { + if (playlist.isEmpty() && startOverIfAtTheEnd) { + initPlaylist(false); + } + if (playlist.isEmpty()) { + return; + } + if (getDiscSlotActive() != -1) { + history.add(getDiscSlotActive()); + if (history.size() > discInventory.getSlots()) { + history.poll(); + } + } + Integer discIndex = playlist.poll(); + if (discIndex == null) { + return; + } + setDiscSlotActive(discIndex); + + playDisc(); + } + + private void initPlaylist(boolean excludeActive) { + playlist.clear(); + for (int i = 0; i < discInventory.getSlots(); i++) { + if (!discInventory.getStackInSlot(i).isEmpty() && (!excludeActive || !isPlaying || i != getDiscSlotActive())) { + playlist.add(i); + } + } + if (isShuffleEnabled()) { + Collections.shuffle(playlist); + } + } + + public void previous() { + if (!isPlaying) { + return; + } + playPrevious(); + } + + public void playPrevious() { + if (history.isEmpty()) { + return; + } + playlist.addFirst(getDiscSlotActive()); + Integer discIndex = history.pollLast(); + if (discIndex == null) { + return; + } + setDiscSlotActive(discIndex); + playDisc(); + } + + public long getDiscFinishTime() { + return upgrade.getOrDefault(ModCoreDataComponents.DISC_FINISH_TIME, 0L); + } +} diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/RepeatMode.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/RepeatMode.java new file mode 100644 index 00000000..a3e4d462 --- /dev/null +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/RepeatMode.java @@ -0,0 +1,50 @@ +package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; + +import com.google.common.collect.ImmutableMap; +import com.mojang.serialization.Codec; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.StringRepresentable; +import net.neoforged.neoforge.network.codec.NeoForgeStreamCodecs; + +import java.util.Map; + +public enum RepeatMode implements StringRepresentable { + ALL("all"), + ONE("one"), + NO("no"); + + public static final Codec CODEC = StringRepresentable.fromEnum(RepeatMode::values); + public static final StreamCodec STREAM_CODEC = NeoForgeStreamCodecs.enumCodec(RepeatMode.class); + + private final String name; + + RepeatMode(String name) { + this.name = name; + } + + @Override + public String getSerializedName() { + return name; + } + + public RepeatMode next() { + return VALUES[(ordinal() + 1) % VALUES.length]; + } + + private static final Map NAME_VALUES; + private static final RepeatMode[] VALUES; + + static { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (RepeatMode value : RepeatMode.values()) { + builder.put(value.getSerializedName(), value); + } + NAME_VALUES = builder.build(); + VALUES = values(); + } + + public static RepeatMode fromName(String name) { + return NAME_VALUES.getOrDefault(name, NO); + } +} diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/ServerStorageSoundHandler.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/ServerStorageSoundHandler.java index 6dad2d39..a958cb1d 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/ServerStorageSoundHandler.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/ServerStorageSoundHandler.java @@ -54,17 +54,17 @@ public static void updateKeepAlive(UUID storageUuid, Level level, Vec3 position, } } - public static void onSoundStopped(Level level, UUID storageUuid) { - removeKeepAliveInfo(level, storageUuid); + public static void onSoundFinished(Level level, UUID storageUuid) { + removeKeepAliveInfo(level, storageUuid, true); } private static class KeepAliveInfo { - private final WeakReference onStopHandler; + private final WeakReference onFinishedHandler; private long lastKeepAliveTime; private Vec3 lastPosition; - private KeepAliveInfo(Runnable onStopHandler, long lastKeepAliveTime, Vec3 lastPosition) { - this.onStopHandler = new WeakReference<>(onStopHandler); + private KeepAliveInfo(Runnable onFinishedHandler, long lastKeepAliveTime, Vec3 lastPosition) { + this.onFinishedHandler = new WeakReference<>(onFinishedHandler); this.lastKeepAliveTime = lastKeepAliveTime; this.lastPosition = lastPosition; } @@ -82,18 +82,18 @@ public void update(long gameTime, Vec3 position) { lastPosition = position; } - public void runOnStop() { - Runnable handler = onStopHandler.get(); + public void runOnFinished() { + Runnable handler = onFinishedHandler.get(); if (handler != null) { handler.run(); } } } - public static void startPlayingDisc(ServerLevel serverLevel, BlockPos position, UUID storageUuid, Holder song, Runnable onStopHandler) { + public static void startPlayingDisc(ServerLevel serverLevel, BlockPos position, UUID storageUuid, Holder song, Runnable onFinishedHandler) { Vec3 pos = Vec3.atCenterOf(position); PacketDistributor.sendToPlayersNear(serverLevel, null, pos.x, pos.y, pos.z, 128, new PlayDiscPayload(storageUuid, song, position)); - putKeepAliveInfo(serverLevel, storageUuid, onStopHandler, pos); + putKeepAliveInfo(serverLevel, storageUuid, onFinishedHandler, pos); } public static void startPlayingDisc(ServerLevel serverLevel, Vec3 position, UUID storageUuid, int entityId, Holder song, Runnable onStopHandler) { @@ -101,19 +101,22 @@ public static void startPlayingDisc(ServerLevel serverLevel, Vec3 position, UUID putKeepAliveInfo(serverLevel, storageUuid, onStopHandler, position); } - private static void putKeepAliveInfo(ServerLevel serverLevel, UUID storageUuid, Runnable onStopHandler, Vec3 pos) { - worldStorageSoundKeepAlive.computeIfAbsent(serverLevel.dimension(), dim -> new HashMap<>()).put(storageUuid, new KeepAliveInfo(onStopHandler, serverLevel.getGameTime(), pos)); + private static void putKeepAliveInfo(ServerLevel serverLevel, UUID storageUuid, Runnable onFinishedHandler, Vec3 pos) { + worldStorageSoundKeepAlive.computeIfAbsent(serverLevel.dimension(), dim -> new HashMap<>()).put(storageUuid, new KeepAliveInfo(onFinishedHandler, serverLevel.getGameTime(), pos)); } public static void stopPlayingDisc(Level level, Vec3 position, UUID storageUuid) { - removeKeepAliveInfo(level, storageUuid); + removeKeepAliveInfo(level, storageUuid, false); sendStopMessage(level, position, storageUuid); } - private static void removeKeepAliveInfo(Level level, UUID storageUuid) { + private static void removeKeepAliveInfo(Level level, UUID storageUuid, boolean finished) { ResourceKey dim = level.dimension(); if (worldStorageSoundKeepAlive.containsKey(dim) && worldStorageSoundKeepAlive.get(dim).containsKey(storageUuid)) { - worldStorageSoundKeepAlive.get(dim).remove(storageUuid).runOnStop(); + KeepAliveInfo keepAliveInfo = worldStorageSoundKeepAlive.get(dim).remove(storageUuid); + if (finished) { + keepAliveInfo.runOnFinished(); + } } } diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundFinishedNotificationPayload.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundFinishedNotificationPayload.java new file mode 100644 index 00000000..c093be13 --- /dev/null +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundFinishedNotificationPayload.java @@ -0,0 +1,27 @@ +package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; + +import io.netty.buffer.ByteBuf; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.p3pp3rf1y.sophisticatedcore.SophisticatedCore; + +import java.util.UUID; + +public record SoundFinishedNotificationPayload(UUID storageUuid) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(SophisticatedCore.getRL("sound_finished_notification")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + UUIDUtil.STREAM_CODEC, + SoundFinishedNotificationPayload::storageUuid, + SoundFinishedNotificationPayload::new); + + @Override + public Type type() { + return TYPE; + } + + public static void handlePayload(SoundFinishedNotificationPayload payload, IPayloadContext context) { + ServerStorageSoundHandler.onSoundFinished(context.player().level(), payload.storageUuid); + } +} diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundStopNotificationPayload.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundStopNotificationPayload.java deleted file mode 100644 index 3b8c3018..00000000 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/SoundStopNotificationPayload.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.p3pp3rf1y.sophisticatedcore.upgrades.jukebox; - -import io.netty.buffer.ByteBuf; -import net.minecraft.core.UUIDUtil; -import net.minecraft.network.codec.StreamCodec; -import net.minecraft.network.protocol.common.custom.CustomPacketPayload; -import net.neoforged.neoforge.network.handling.IPayloadContext; -import net.p3pp3rf1y.sophisticatedcore.SophisticatedCore; - -import java.util.UUID; - -public record SoundStopNotificationPayload(UUID storageUuid) implements CustomPacketPayload { - public static final Type TYPE = new Type<>(SophisticatedCore.getRL("sound_stop_notification")); - public static final StreamCodec STREAM_CODEC = StreamCodec.composite( - UUIDUtil.STREAM_CODEC, - SoundStopNotificationPayload::storageUuid, - SoundStopNotificationPayload::new); - - @Override - public Type type() { - return TYPE; - } - - public static void handlePayload(SoundStopNotificationPayload payload, IPayloadContext context) { - ServerStorageSoundHandler.onSoundStopped(context.player().level(), payload.storageUuid); - } -} diff --git a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/StorageSoundHandler.java b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/StorageSoundHandler.java index c4e14d86..b120ae63 100644 --- a/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/StorageSoundHandler.java +++ b/src/main/java/net/p3pp3rf1y/sophisticatedcore/upgrades/jukebox/StorageSoundHandler.java @@ -35,7 +35,6 @@ public static void playStorageSound(UUID storageUuid, SoundInstance sound) { public static void stopStorageSound(UUID storageUuid) { if (storageSounds.containsKey(storageUuid)) { Minecraft.getInstance().getSoundManager().stop(storageSounds.remove(storageUuid)); - PacketDistributor.sendToServer(new SoundStopNotificationPayload(storageUuid)); } } @@ -44,7 +43,7 @@ public static void tick(LevelTickEvent.Post event) { lastPlaybackChecked = event.getLevel().getGameTime(); storageSounds.entrySet().removeIf(entry -> { if (!Minecraft.getInstance().getSoundManager().isActive(entry.getValue())) { - PacketDistributor.sendToServer(new SoundStopNotificationPayload(entry.getKey())); + PacketDistributor.sendToServer(new SoundFinishedNotificationPayload(entry.getKey())); return true; } return false; diff --git a/src/main/resources/assets/sophisticatedcore/lang/en_us.json b/src/main/resources/assets/sophisticatedcore/lang/en_us.json index c7652649..57f28f1e 100644 --- a/src/main/resources/assets/sophisticatedcore/lang/en_us.json +++ b/src/main/resources/assets/sophisticatedcore/lang/en_us.json @@ -13,6 +13,7 @@ "item.sophisticatedcore.storage.tooltip.shift": "Left Shift", "upgrade_group.sophisticatedcore.stack_upgrades": "Stack Upgrades", "upgrade_group.sophisticatedcore.cooking_upgrades": "Furnace Upgrades", + "upgrade_group.sophisticatedcore.jukebox_upgrades": "Jukebox Upgrades", "gui.sophisticatedcore.settings.no_sort": "No Sort", "gui.sophisticatedcore.settings.no_sort.tooltip": "No Sort Slot Settings", "gui.sophisticatedcore.settings.no_sort.tooltip_detail": "Allows selecting slots that are ignored by sorting\nOpen tab to modify slot settings", @@ -46,6 +47,7 @@ "gui.sophisticatedcore.upgrades.crafting": "Craft", "gui.sophisticatedcore.upgrades.stonecutter": "Stonecutter", "gui.sophisticatedcore.upgrades.jukebox": "Jukebox", + "gui.sophisticatedcore.upgrades.advanced_jukebox": "Jukebox", "gui.sophisticatedcore.upgrades.tank": "Tank", "gui.sophisticatedcore.upgrades.pump": "Pump", "gui.sophisticatedcore.upgrades.advanced_pump": "Adv. Pump", @@ -86,6 +88,7 @@ "gui.sophisticatedcore.upgrades.chipped_alchemy_bench.tooltip": "Alchemy Bench", "gui.sophisticatedcore.upgrades.chipped_tinkering_table.tooltip": "Tinkering Table", "gui.sophisticatedcore.upgrades.jukebox.tooltip": "Jukebox", + "gui.sophisticatedcore.upgrades.advanced_jukebox.tooltip": "Advanced Jukebox", "gui.sophisticatedcore.upgrades.tank.tooltip": "Tank", "gui.sophisticatedcore.upgrades.pump.tooltip": "Pump", "gui.sophisticatedcore.upgrades.advanced_pump.tooltip": "Advanced Pump", @@ -158,6 +161,13 @@ "gui.sophisticatedcore.upgrades.buttons.previous_result": "Previous", "gui.sophisticatedcore.upgrades.buttons.next_result": "Next", "gui.sophisticatedcore.upgrades.buttons.select_result": "Select Result", + "gui.sophisticatedcore.upgrades.buttons.shuffle_on": "Shuffle Enabled", + "gui.sophisticatedcore.upgrades.buttons.shuffle_off": "Shuffle Disabled", + "gui.sophisticatedcore.upgrades.buttons.repeat_all": "Repeat All", + "gui.sophisticatedcore.upgrades.buttons.repeat_one": "Repeat One", + "gui.sophisticatedcore.upgrades.buttons.no_repeat": "Repeat Disabled", + "gui.sophisticatedcore.upgrades.buttons.previous_disc": "Previous", + "gui.sophisticatedcore.upgrades.buttons.next_disc": "Next", "gui.sophisticatedcore.upgrades.controls.xp_level_select": "%s lvls", "gui.sophisticatedcore.upgrades.controls.xp_level_select.tooltip": "Level at which Pump Stops", "gui.sophisticatedcore.upgrades.controls.xp_level_select.tooltip.controls": "Scroll to change", diff --git a/src/main/resources/assets/sophisticatedcore/textures/gui/icons.png b/src/main/resources/assets/sophisticatedcore/textures/gui/icons.png index 5eec75cc38ae276bf4b53cf07cf2c0d91ad4141f..ea0a199a172a75fc66da1ac3375153cb3c9f093b 100644 GIT binary patch literal 11884 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 literal 11392 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