Files
SoundPadMod/src/main/java/com/example/soundmod/client/screen/SoundPlayerScreen.java
T

1572 lines
82 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.example.soundmod.client.screen;
import com.example.soundmod.client.audio.AudioPlaybackService;
import com.example.soundmod.common.ActivationMode;
import com.example.soundmod.common.AttenuationMode;
import com.example.soundmod.common.PitchModulationMode;
import com.example.soundmod.common.RedstoneMode;
import com.example.soundmod.common.ScenarioQueue;
import com.example.soundmod.common.SoundManifestEntry;
import com.example.soundmod.common.SoundSettings;
import com.example.soundmod.common.ZoneIntersectionPolicy;
import com.example.soundmod.common.ZoneShape;
import com.example.soundmod.core.SoundDebug;
import com.example.soundmod.gameplay.container.SoundPlayerContainer;
import com.example.soundmod.gameplay.tile.SoundPlayerTileEntity;
import com.example.soundmod.net.ClientTransferManager;
import com.example.soundmod.net.NetworkHandler;
import com.example.soundmod.net.packet.C2SUpdateConfigPacket;
import com.example.soundmod.sounddata.client.ClientSoundCache;
import com.mojang.blaze3d.matrix.MatrixStack;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.player.ClientPlayerEntity;
import net.minecraft.client.gui.screen.inventory.ContainerScreen;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.client.gui.widget.Widget;
import net.minecraft.client.gui.widget.button.Button;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.nbt.INBT;
import net.minecraft.nbt.JsonToNBT;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
// Palette
private static final int COLOR_BG_SHADOW = 0x90000000;
private static final int COLOR_BG_MAIN = 0xEE161B22;
private static final int COLOR_BG_INNER = 0xEE1F242C;
private static final int COLOR_BG_BOX = 0xCC11141B;
private static final int COLOR_BORDER = 0xFF30363D;
private static final int COLOR_ACCENT = 0xFF388BFD;
private static final int COLOR_TEXT_TITLE = 0xFFFFFF;
private static final int COLOR_TEXT_LABEL = 0xFFA0A6B2;
private static final int COLOR_TEXT_MUTED = 0xFF767F8D;
private static final int COLOR_GREEN = 0xFF3FB950;
private static final int COLOR_YELLOW = 0xFFD29922;
private static final int COLOR_RED = 0xFFF85149;
private static final String[] CATEGORIES = new String[]{"music", "ambient", "effect", "voice"};
private static final int SOUND_LIST_ROWS = 4;
private static final int ROW_HEIGHT = 12;
public static SoundPlayerScreen activeScreen;
public static boolean showZoneVisualizer = true;
private Tab activeTab = Tab.BASIC;
private SoundSettings localSettings = new SoundSettings();
private final List<ScenarioQueue.Step> sourceScenarioSteps = new ArrayList<>();
private final List<ScenarioQueue.Step> zoneScenarioSteps = new ArrayList<>();
private boolean sourceScenarioLoop;
private boolean zoneScenarioLoop;
private boolean sourceScenarioShuffle;
private boolean zoneScenarioShuffle;
private boolean editingZoneScenario;
private boolean sourceEnabled = true;
private boolean zoneEnabled = true;
private boolean wizardCompleted = true;
private ZoneShape zoneShape = ZoneShape.SPHERE;
private float zoneRadius = 12.0f;
private float zoneBoxX = 8.0f;
private float zoneBoxY = 4.0f;
private float zoneBoxZ = 8.0f;
private boolean zoneAutoStartOnEnter = true;
private boolean zoneStopOnExit = true;
private ZoneIntersectionPolicy zonePolicy = ZoneIntersectionPolicy.HIGHEST_PRIORITY;
private boolean zoneUseCustomCoords;
private int zoneSourceX;
private int zoneSourceY;
private int zoneSourceZ;
// Controls - Basic
private TextFieldWidget soundIdField;
private TextFieldWidget searchField;
private Button previewButton;
private Button clearFilterButton;
private Button volMinusBtn;
private TextFieldWidget volumeField;
private Button volPlusBtn;
private Button pitchMinusBtn;
private TextFieldWidget pitchField;
private Button pitchPlusBtn;
private TextFieldWidget radiusField;
private TextFieldWidget priorityField;
private Button loopButton;
private Button categoryButton;
// Controls - Effects
private Button use3dButton;
private Button attenuationButton;
private Button occlusionButton;
private TextFieldWidget occlusionStrengthField;
private Button reverbButton;
private TextFieldWidget reverbStrengthField;
private Button dopplerButton;
private Button crossfadeButton;
private TextFieldWidget fadeInField;
private TextFieldWidget fadeOutField;
private Button pitchModeButton;
private TextFieldWidget pitchModSpeedField;
private TextFieldWidget pitchModDepthField;
// Controls - Trigger
private Button activationModeButton;
private Button redstoneModeButton;
private Button sourceEnabledButton;
private Button zoneEnabledButton;
private TextFieldWidget startDelayField;
private TextFieldWidget repeatDelayField;
// Controls - Zone
private Button zoneShapeButton;
private Button zonePolicyButton;
private Button zoneVisualizerButton;
private TextFieldWidget zoneRadiusField;
private TextFieldWidget zoneBoxXField;
private TextFieldWidget zoneBoxYField;
private TextFieldWidget zoneBoxZField;
private Button zoneAutoStartButton;
private Button zoneStopOnExitButton;
private Button zoneUseCustomCoordsButton;
private Button myPosButton;
private TextFieldWidget zoneSourceXField;
private TextFieldWidget zoneSourceYField;
private TextFieldWidget zoneSourceZField;
// Controls - Scenario
private Button scenarioTargetButton;
private Button scenarioLoopButton;
private Button scenarioShuffleButton;
private TextFieldWidget scenarioSoundField;
private TextFieldWidget scenarioDelayField;
private Button scenarioUseCurrentBtn;
private Button scenarioSetButton;
private Button scenarioAddButton;
private Button scenarioRemoveButton;
private Button scenarioUpButton;
private Button scenarioDownButton;
// Fixed bottom bar controls
private Button saveButton;
private Button copyConfigButton;
private Button pasteConfigButton;
private Button playNowButton;
private Button stopNowButton;
// Tab buttons
private final List<Button> tabButtons = new ArrayList<>();
// Tab tracking
private final List<TextFieldWidget> basicFields = new ArrayList<>();
private final List<TextFieldWidget> effectsFields = new ArrayList<>();
private final List<TextFieldWidget> triggerFields = new ArrayList<>();
private final List<TextFieldWidget> zoneFields = new ArrayList<>();
private final List<TextFieldWidget> scenarioFields = new ArrayList<>();
private final List<Button> basicButtons = new ArrayList<>();
private final List<Button> effectsButtons = new ArrayList<>();
private final List<Button> triggerButtons = new ArrayList<>();
private final List<Button> zoneButtons = new ArrayList<>();
private final List<Button> scenarioButtons = new ArrayList<>();
private int selectedSoundListIndex = -1;
private int soundListScroll;
private long lastSoundClickTime;
private int selectedScenarioIndex = -1;
private int scenarioListScroll;
private int helpScroll;
private String lastSoundFilterQuery = "";
private long liveApplyStateUntilMs;
private String liveApplyText = "Не играет";
public SoundPlayerScreen(SoundPlayerContainer container, PlayerInventory inv, ITextComponent title) {
super(container, inv, title);
this.imageWidth = 280;
this.imageHeight = 248;
}
@Override
protected void init() {
super.init();
if (this.minecraft != null) {
this.minecraft.keyboardHandler.setSendRepeatsToGui(true);
}
// Hide default container title and inventory labels
this.titleLabelX = -10000;
this.titleLabelY = -10000;
this.inventoryLabelX = -10000;
this.inventoryLabelY = -10000;
SoundPlayerTileEntity tile = ((SoundPlayerContainer) this.menu).getTile();
if (tile != null) {
this.localSettings = SoundSettings.sanitizedCopy(tile.getSettings());
this.sourceScenarioSteps.clear();
this.sourceScenarioSteps.addAll(tile.getScenario().steps);
this.sourceScenarioLoop = tile.getScenario().loop;
this.sourceScenarioShuffle = tile.getScenario().shuffle;
this.zoneScenarioSteps.clear();
this.zoneScenarioSteps.addAll(tile.getZoneScenario().steps);
this.zoneScenarioLoop = tile.getZoneScenario().loop;
this.zoneScenarioShuffle = tile.getZoneScenario().shuffle;
this.sourceEnabled = tile.isSourceEnabled();
this.zoneEnabled = tile.isZoneEnabled();
this.wizardCompleted = tile.isWizardCompleted();
this.zoneShape = tile.getZoneShape();
this.zoneRadius = tile.getZoneRadius();
this.zoneBoxX = (float) tile.getZoneBoxHalfExtents().x;
this.zoneBoxY = (float) tile.getZoneBoxHalfExtents().y;
this.zoneBoxZ = (float) tile.getZoneBoxHalfExtents().z;
this.zoneAutoStartOnEnter = tile.isZoneAutoStartOnEnter();
this.zoneStopOnExit = tile.isZoneStopOnExit();
this.zonePolicy = tile.getZoneIntersectionPolicy();
this.zoneUseCustomCoords = tile.isZoneUseCustomCoords();
this.zoneSourceX = tile.getZoneSourceX();
this.zoneSourceY = tile.getZoneSourceY();
this.zoneSourceZ = tile.getZoneSourceZ();
}
activeScreen = this;
this.initFieldsAndButtons();
this.addTabButtons();
this.updateTabVisibility();
}
@Override
protected void renderLabels(MatrixStack matrixStack, int mouseX, int mouseY) {
// Suppress default container title ("Sound Player") and player inventory label ("Инвентарь")
}
private void addTabButtons() {
this.tabButtons.clear();
int x = this.leftPos + 9;
int y = this.topPos + 5;
this.tabButtons.add((Button) this.addButton(new Button(x, y, 46, 18, new StringTextComponent("Основа"), b -> this.switchTab(Tab.BASIC))));
this.tabButtons.add((Button) this.addButton(new Button(x + 48, y, 50, 18, new StringTextComponent("Эффекты"), b -> this.switchTab(Tab.EFFECTS))));
this.tabButtons.add((Button) this.addButton(new Button(x + 100, y, 48, 18, new StringTextComponent("Триггер"), b -> this.switchTab(Tab.TRIGGER))));
this.tabButtons.add((Button) this.addButton(new Button(x + 150, y, 40, 18, new StringTextComponent("Зона"), b -> this.switchTab(Tab.ZONE))));
this.tabButtons.add((Button) this.addButton(new Button(x + 192, y, 52, 18, new StringTextComponent("Сценарий"), b -> this.switchTab(Tab.SCENARIO))));
this.tabButtons.add((Button) this.addButton(new Button(x + 246, y, 16, 18, new StringTextComponent("?"), b -> this.switchTab(Tab.HELP))));
}
private void initFieldsAndButtons() {
// Clear all tracking lists to avoid duplicate entries on resize
this.basicFields.clear();
this.effectsFields.clear();
this.triggerFields.clear();
this.zoneFields.clear();
this.scenarioFields.clear();
this.basicButtons.clear();
this.effectsButtons.clear();
this.triggerButtons.clear();
this.zoneButtons.clear();
this.scenarioButtons.clear();
int x = this.leftPos + 10;
int y = this.topPos;
// --- TAB 1: BASIC ---
this.soundIdField = this.createField(x, y + 38, 194, this.localSettings.soundId);
this.previewButton = this.addButtonTracked(new Button(x + 208, y + 37, 62, 20, new StringTextComponent("▶ Тест"), b -> this.togglePreview()), this.basicButtons);
// Numeric row: Volume (- [field] +), Pitch (- [field] +), Radius, Priority
this.volMinusBtn = this.addButtonTracked(new Button(x, y + 74, 14, 18, new StringTextComponent("-"), b -> this.adjustVolume(-0.1f)), this.basicButtons);
this.volumeField = this.createField(x + 15, y + 74, 30, this.floatText(this.localSettings.volume, 1));
this.volPlusBtn = this.addButtonTracked(new Button(x + 46, y + 74, 14, 18, new StringTextComponent("+"), b -> this.adjustVolume(0.1f)), this.basicButtons);
this.pitchMinusBtn = this.addButtonTracked(new Button(x + 74, y + 74, 14, 18, new StringTextComponent("-"), b -> this.adjustPitch(-0.05f)), this.basicButtons);
this.pitchField = this.createField(x + 89, y + 74, 30, this.floatText(this.localSettings.pitch, 2));
this.pitchPlusBtn = this.addButtonTracked(new Button(x + 120, y + 74, 14, 18, new StringTextComponent("+"), b -> this.adjustPitch(0.05f)), this.basicButtons);
this.radiusField = this.createField(x + 144, y + 74, 52, this.floatText(this.localSettings.radius, 1));
this.priorityField = this.createField(x + 208, y + 74, 62, Integer.toString(this.localSettings.priority));
// Options row: Loop and Category
this.loopButton = this.addButtonTracked(new Button(x, y + 98, 104, 20, this.boolLabel("Цикл", this.localSettings.loop), b -> {
this.localSettings.loop = !this.localSettings.loop;
b.setMessage(this.boolLabel("Цикл", this.localSettings.loop));
this.sendLiveApply("loop");
}), this.basicButtons);
this.categoryButton = this.addButtonTracked(new Button(x + 112, y + 98, 158, 20, new StringTextComponent("Категория: " + this.categoryRu(this.localSettings.category)), b -> {
this.localSettings.category = this.cycleCategory(this.localSettings.category);
b.setMessage(new StringTextComponent("Категория: " + this.categoryRu(this.localSettings.category)));
this.sendLiveApply("category");
}), this.basicButtons);
// Search row
this.searchField = this.createField(x, y + 124, 194, "");
this.clearFilterButton = this.addButtonTracked(new Button(x + 208, y + 123, 62, 20, new StringTextComponent("Сброс"), b -> {
this.searchField.setValue("");
this.selectedSoundListIndex = -1;
this.soundListScroll = 0;
}), this.basicButtons);
// --- TAB 2: EFFECTS ---
this.use3dButton = this.addButtonTracked(new Button(x, y + 42, 128, 20, this.boolLabel("3D звук", this.localSettings.use3d), b -> {
this.localSettings.use3d = !this.localSettings.use3d;
b.setMessage(this.boolLabel("3D звук", this.localSettings.use3d));
this.sendLiveApply("use3d");
}), this.effectsButtons);
this.attenuationButton = this.addButtonTracked(new Button(x + 132, y + 42, 138, 20, new StringTextComponent("Затух.: " + this.attenuationRu(this.localSettings.attenuationMode)), b -> {
this.localSettings.attenuationMode = this.cycleAttenuation(this.localSettings.attenuationMode);
b.setMessage(new StringTextComponent("Затух.: " + this.attenuationRu(this.localSettings.attenuationMode)));
this.sendLiveApply("attenuation");
}), this.effectsButtons);
this.occlusionButton = this.addButtonTracked(new Button(x, y + 68, 132, 20, this.boolLabel("Окклюзия (стены)", this.localSettings.occlusion), b -> {
this.localSettings.occlusion = !this.localSettings.occlusion;
b.setMessage(this.boolLabel("Окклюзия (стены)", this.localSettings.occlusion));
this.sendLiveApply("occlusion");
}), this.effectsButtons);
this.occlusionStrengthField = this.createField(x + 214, y + 69, 56, this.floatText(this.localSettings.occlusionStrength, 2));
this.reverbButton = this.addButtonTracked(new Button(x, y + 94, 132, 20, this.boolLabel("Реверберация", this.localSettings.reverb), b -> {
this.localSettings.reverb = !this.localSettings.reverb;
b.setMessage(this.boolLabel("Реверберация", this.localSettings.reverb));
this.sendLiveApply("reverb");
}), this.effectsButtons);
this.reverbStrengthField = this.createField(x + 214, y + 95, 56, this.floatText(this.localSettings.reverbStrength, 2));
this.dopplerButton = this.addButtonTracked(new Button(x, y + 120, 128, 20, this.boolLabel("Допплер", this.localSettings.doppler), b -> {
this.localSettings.doppler = !this.localSettings.doppler;
b.setMessage(this.boolLabel("Допплер", this.localSettings.doppler));
this.sendLiveApply("doppler");
}), this.effectsButtons);
this.crossfadeButton = this.addButtonTracked(new Button(x + 132, y + 120, 138, 20, this.boolLabel("Кроссфейд", this.localSettings.crossfade), b -> {
this.localSettings.crossfade = !this.localSettings.crossfade;
b.setMessage(this.boolLabel("Кроссфейд", this.localSettings.crossfade));
this.sendLiveApply("crossfade");
}), this.effectsButtons);
this.fadeInField = this.createField(x + 88, y + 147, 44, Integer.toString(this.localSettings.fadeInTicks));
this.fadeOutField = this.createField(x + 226, y + 147, 44, Integer.toString(this.localSettings.fadeOutTicks));
this.pitchModeButton = this.addButtonTracked(new Button(x, y + 172, 114, 20, new StringTextComponent("Pitch: " + this.localSettings.pitchMode.getDisplayName()), b -> {
this.localSettings.pitchMode = this.cyclePitchMode(this.localSettings.pitchMode);
b.setMessage(new StringTextComponent("Pitch: " + this.localSettings.pitchMode.getDisplayName()));
this.sendLiveApply("pitchMode");
}), this.effectsButtons);
this.pitchModSpeedField = this.createField(x + 152, y + 173, 34, this.floatText(this.localSettings.pitchModSpeed, 1));
this.pitchModDepthField = this.createField(x + 228, y + 173, 34, this.floatText(this.localSettings.pitchModDepth, 2));
// --- TAB 3: TRIGGER ---
this.activationModeButton = this.addButtonTracked(new Button(x, y + 42, 128, 20, new StringTextComponent("Режим: " + this.modeRu(this.localSettings.activationMode)), b -> {
this.localSettings.activationMode = this.cycleMode(this.localSettings.activationMode);
b.setMessage(new StringTextComponent("Режим: " + this.modeRu(this.localSettings.activationMode)));
this.updateTabVisibility();
this.saveAndSend(false, false);
}), this.triggerButtons);
this.redstoneModeButton = this.addButtonTracked(new Button(x + 132, y + 42, 138, 20, new StringTextComponent("Сигнал: " + this.redstoneRu(this.localSettings.redstoneMode)), b -> {
this.localSettings.redstoneMode = this.cycleRedstone(this.localSettings.redstoneMode);
b.setMessage(new StringTextComponent("Сигнал: " + this.redstoneRu(this.localSettings.redstoneMode)));
this.saveAndSend(false, false);
}), this.triggerButtons);
this.startDelayField = this.createField(x + 94, y + 69, 40, Integer.toString(this.localSettings.startDelayTicks));
this.repeatDelayField = this.createField(x + 230, y + 69, 40, Integer.toString(this.localSettings.repeatDelayTicks));
this.sourceEnabledButton = this.addButtonTracked(new Button(x, y + 94, 128, 20, this.boolLabel("Источник", this.sourceEnabled), b -> {
this.sourceEnabled = !this.sourceEnabled;
b.setMessage(this.boolLabel("Источник", this.sourceEnabled));
this.saveAndSend(false, false);
}), this.triggerButtons);
this.zoneEnabledButton = this.addButtonTracked(new Button(x + 132, y + 94, 138, 20, this.boolLabel("Триггер зоны", this.zoneEnabled), b -> {
this.zoneEnabled = !this.zoneEnabled;
b.setMessage(this.boolLabel("Триггер зоны", this.zoneEnabled));
this.saveAndSend(false, false);
}), this.triggerButtons);
// --- TAB 4: ZONE ---
this.zoneShapeButton = this.addButtonTracked(new Button(x, y + 42, 84, 20, new StringTextComponent(this.zoneShape == ZoneShape.SPHERE ? "Сфера" : "Бокс"), b -> {
this.zoneShape = this.zoneShape == ZoneShape.SPHERE ? ZoneShape.BOX : ZoneShape.SPHERE;
b.setMessage(new StringTextComponent(this.zoneShape == ZoneShape.SPHERE ? "Сфера" : "Бокс"));
this.updateTabVisibility();
this.saveAndSend(false, false);
}), this.zoneButtons);
this.zonePolicyButton = this.addButtonTracked(new Button(x + 88, y + 42, 94, 20, new StringTextComponent("Приоритет: " + this.policyRu(this.zonePolicy)), b -> {
this.zonePolicy = this.cyclePolicy(this.zonePolicy);
b.setMessage(new StringTextComponent("Приоритет: " + this.policyRu(this.zonePolicy)));
this.saveAndSend(false, false);
}), this.zoneButtons);
this.zoneVisualizerButton = this.addButtonTracked(new Button(x + 186, y + 42, 84, 20, this.boolLabel("Вид зоны", showZoneVisualizer), b -> {
showZoneVisualizer = !showZoneVisualizer;
b.setMessage(this.boolLabel("Вид зоны", showZoneVisualizer));
}), this.zoneButtons);
this.zoneRadiusField = this.createField(x + 144, y + 69, 60, this.floatText(this.zoneRadius, 1));
this.zoneBoxXField = this.createField(x + 144, y + 69, 38, this.floatText(this.zoneBoxX, 1));
this.zoneBoxYField = this.createField(x + 186, y + 69, 38, this.floatText(this.zoneBoxY, 1));
this.zoneBoxZField = this.createField(x + 228, y + 69, 38, this.floatText(this.zoneBoxZ, 1));
this.zoneAutoStartButton = this.addButtonTracked(new Button(x, y + 94, 128, 20, this.boolLabel("Авто-вход", this.zoneAutoStartOnEnter), b -> {
this.zoneAutoStartOnEnter = !this.zoneAutoStartOnEnter;
b.setMessage(this.boolLabel("Авто-вход", this.zoneAutoStartOnEnter));
this.saveAndSend(false, false);
}), this.zoneButtons);
this.zoneStopOnExitButton = this.addButtonTracked(new Button(x + 132, y + 94, 138, 20, this.boolLabel("Стоп-выход", this.zoneStopOnExit), b -> {
this.zoneStopOnExit = !this.zoneStopOnExit;
b.setMessage(this.boolLabel("Стоп-выход", this.zoneStopOnExit));
this.saveAndSend(false, false);
}), this.zoneButtons);
this.zoneUseCustomCoordsButton = this.addButtonTracked(new Button(x, y + 120, 142, 20, this.boolLabel("Смещение центра", this.zoneUseCustomCoords), b -> {
this.zoneUseCustomCoords = !this.zoneUseCustomCoords;
b.setMessage(this.boolLabel("Смещение центра", this.zoneUseCustomCoords));
this.updateTabVisibility();
this.saveAndSend(false, false);
}), this.zoneButtons);
this.myPosButton = this.addButtonTracked(new Button(x + 146, y + 120, 124, 20, new StringTextComponent("Моя позиция"), b -> this.applyPlayerCoordsToZone()), this.zoneButtons);
this.zoneSourceXField = this.createField(x + 104, y + 147, 46, Integer.toString(this.zoneSourceX));
this.zoneSourceYField = this.createField(x + 164, y + 147, 46, Integer.toString(this.zoneSourceY));
this.zoneSourceZField = this.createField(x + 224, y + 147, 46, Integer.toString(this.zoneSourceZ));
// --- TAB 5: SCENARIO ---
this.scenarioTargetButton = this.addButtonTracked(new Button(x, y + 42, 80, 20, new StringTextComponent(this.editingZoneScenario ? "Зона" : "Блок"), b -> {
this.editingZoneScenario = !this.editingZoneScenario;
b.setMessage(new StringTextComponent(this.editingZoneScenario ? "Зона" : "Блок"));
this.selectedScenarioIndex = -1;
this.scenarioLoopButton.setMessage(this.boolLabel("Цикл", this.activeScenarioLoop()));
this.scenarioShuffleButton.setMessage(this.boolLabel("Случайно", this.activeScenarioShuffle()));
}), this.scenarioButtons);
this.scenarioLoopButton = this.addButtonTracked(new Button(x + 84, y + 42, 88, 20, this.boolLabel("Цикл", this.activeScenarioLoop()), b -> {
this.setActiveScenarioLoop(!this.activeScenarioLoop());
b.setMessage(this.boolLabel("Цикл", this.activeScenarioLoop()));
}), this.scenarioButtons);
this.scenarioShuffleButton = this.addButtonTracked(new Button(x + 176, y + 42, 94, 20, this.boolLabel("Случайно", this.activeScenarioShuffle()), b -> {
this.setActiveScenarioShuffle(!this.activeScenarioShuffle());
b.setMessage(this.boolLabel("Случайно", this.activeScenarioShuffle()));
}), this.scenarioButtons);
this.scenarioSoundField = this.createField(x, y + 66, 116, "");
this.scenarioUseCurrentBtn = this.addButtonTracked(new Button(x + 118, y + 65, 42, 20, new StringTextComponent("Тек."), b -> {
this.scenarioSoundField.setValue(this.soundIdField.getValue().trim());
}), this.scenarioButtons);
this.scenarioDelayField = this.createField(x + 210, y + 66, 34, "20");
this.scenarioSetButton = this.addButtonTracked(new Button(x + 248, y + 65, 22, 20, new StringTextComponent("✓"), b -> this.updateSelectedScenarioStep()), this.scenarioButtons);
this.scenarioAddButton = this.addButtonTracked(new Button(x, y + 88, 60, 20, new StringTextComponent("+ Шаг"), b -> this.addScenarioStep()), this.scenarioButtons);
this.scenarioRemoveButton = this.addButtonTracked(new Button(x + 64, y + 88, 56, 20, new StringTextComponent("- Шаг"), b -> this.removeScenarioStep()), this.scenarioButtons);
this.scenarioUpButton = this.addButtonTracked(new Button(x + 128, y + 88, 68, 20, new StringTextComponent("▲ Вверх"), b -> this.moveScenarioStep(-1)), this.scenarioButtons);
this.scenarioDownButton = this.addButtonTracked(new Button(x + 202, y + 88, 68, 20, new StringTextComponent("▼ Вниз"), b -> this.moveScenarioStep(1)), this.scenarioButtons);
// --- FIXED BOTTOM ROW (ALWAYS VISIBLE) ---
int by = y + this.imageHeight - 28;
this.saveButton = (Button) this.addButton(new Button(x, by, 64, 20, new StringTextComponent("Сохранить"), b -> this.saveAndSend(false, false)));
this.copyConfigButton = (Button) this.addButton(new Button(x + 68, by, 44, 20, new StringTextComponent("Копия"), b -> this.copyConfigToClipboard()));
this.pasteConfigButton = (Button) this.addButton(new Button(x + 116, by, 48, 20, new StringTextComponent("Вставка"), b -> this.pasteConfigFromClipboard()));
this.playNowButton = (Button) this.addButton(new Button(x + 168, by, 46, 20, new StringTextComponent("▶ Старт"), b -> this.saveAndSend(true, false)));
this.stopNowButton = (Button) this.addButton(new Button(x + 218, by, 52, 20, new StringTextComponent("⏹ Стоп"), b -> this.saveAndSend(false, true)));
// Field groupings
this.addFieldTracked(this.soundIdField, this.basicFields);
this.addFieldTracked(this.searchField, this.basicFields);
this.addFieldTracked(this.volumeField, this.basicFields);
this.addFieldTracked(this.pitchField, this.basicFields);
this.addFieldTracked(this.radiusField, this.basicFields);
this.addFieldTracked(this.priorityField, this.basicFields);
this.addFieldTracked(this.occlusionStrengthField, this.effectsFields);
this.addFieldTracked(this.reverbStrengthField, this.effectsFields);
this.addFieldTracked(this.fadeInField, this.effectsFields);
this.addFieldTracked(this.fadeOutField, this.effectsFields);
this.addFieldTracked(this.pitchModSpeedField, this.effectsFields);
this.addFieldTracked(this.pitchModDepthField, this.effectsFields);
this.addFieldTracked(this.startDelayField, this.triggerFields);
this.addFieldTracked(this.repeatDelayField, this.triggerFields);
this.addFieldTracked(this.zoneRadiusField, this.zoneFields);
this.addFieldTracked(this.zoneBoxXField, this.zoneFields);
this.addFieldTracked(this.zoneBoxYField, this.zoneFields);
this.addFieldTracked(this.zoneBoxZField, this.zoneFields);
this.addFieldTracked(this.zoneSourceXField, this.zoneFields);
this.addFieldTracked(this.zoneSourceYField, this.zoneFields);
this.addFieldTracked(this.zoneSourceZField, this.zoneFields);
this.addFieldTracked(this.scenarioSoundField, this.scenarioFields);
this.addFieldTracked(this.scenarioDelayField, this.scenarioFields);
}
private void switchTab(Tab next) {
this.activeTab = next;
this.updateTabVisibility();
}
private void updateTabVisibility() {
this.setVisible(this.basicFields, this.activeTab == Tab.BASIC);
this.setVisible(this.effectsFields, this.activeTab == Tab.EFFECTS);
this.setVisible(this.triggerFields, this.activeTab == Tab.TRIGGER);
this.setVisible(this.zoneFields, this.activeTab == Tab.ZONE);
this.setVisible(this.scenarioFields, this.activeTab == Tab.SCENARIO);
this.setVisibleButtons(this.basicButtons, this.activeTab == Tab.BASIC);
this.setVisibleButtons(this.effectsButtons, this.activeTab == Tab.EFFECTS);
this.setVisibleButtons(this.triggerButtons, this.activeTab == Tab.TRIGGER);
this.setVisibleButtons(this.zoneButtons, this.activeTab == Tab.ZONE);
this.setVisibleButtons(this.scenarioButtons, this.activeTab == Tab.SCENARIO);
if (this.redstoneModeButton != null) {
boolean modeVisible = this.activeTab == Tab.TRIGGER && this.localSettings.activationMode == ActivationMode.REDSTONE;
this.redstoneModeButton.visible = modeVisible;
this.redstoneModeButton.active = modeVisible;
}
// In Zone tab: toggle Sphere vs Box fields and Custom coords
if (this.activeTab == Tab.ZONE) {
boolean isSphere = this.zoneShape == ZoneShape.SPHERE;
this.zoneRadiusField.visible = isSphere;
this.zoneBoxXField.visible = !isSphere;
this.zoneBoxYField.visible = !isSphere;
this.zoneBoxZField.visible = !isSphere;
boolean custom = this.zoneUseCustomCoords;
this.zoneSourceXField.visible = custom;
this.zoneSourceYField.visible = custom;
this.zoneSourceZField.visible = custom;
}
}
private void adjustVolume(float delta) {
float v = Math.round((this.localSettings.volume + delta) * 10.0f) / 10.0f;
this.localSettings.volume = Math.max(0.0f, Math.min(2.0f, v));
this.volumeField.setValue(this.floatText(this.localSettings.volume, 1));
this.sendLiveApply("volume");
}
private void adjustPitch(float delta) {
float p = Math.round((this.localSettings.pitch + delta) * 100.0f) / 100.0f;
this.localSettings.pitch = Math.max(0.2f, Math.min(2.0f, p));
this.pitchField.setValue(this.floatText(this.localSettings.pitch, 2));
this.sendLiveApply("pitch");
}
private void togglePreview() {
if (AudioPlaybackService.get().isPlaying("gui_preview")) {
AudioPlaybackService.get().stop("gui_preview");
return;
}
this.applyFromFields();
String sid = this.localSettings.soundId.trim();
if (sid.isEmpty()) {
return;
}
ClientPlayerEntity player = Minecraft.getInstance().player;
BlockPos pos = player != null ? player.blockPosition() : BlockPos.ZERO;
SoundSettings previewSettings = SoundSettings.sanitizedCopy(this.localSettings);
previewSettings.loop = false;
AudioPlaybackService.get().play("gui_preview", pos, previewSettings);
}
private void applyPlayerCoordsToZone() {
ClientPlayerEntity player = Minecraft.getInstance().player;
if (player != null) {
this.zoneSourceX = (int) Math.floor(player.getX());
this.zoneSourceY = (int) Math.floor(player.getY());
this.zoneSourceZ = (int) Math.floor(player.getZ());
this.zoneUseCustomCoords = true;
this.zoneUseCustomCoordsButton.setMessage(this.boolLabel("Смещение центра", true));
this.zoneSourceXField.setValue(Integer.toString(this.zoneSourceX));
this.zoneSourceYField.setValue(Integer.toString(this.zoneSourceY));
this.zoneSourceZField.setValue(Integer.toString(this.zoneSourceZ));
this.updateTabVisibility();
this.saveAndSend(false, false);
}
}
private void moveScenarioStep(int direction) {
List<ScenarioQueue.Step> list = this.activeScenarioList();
if (this.selectedScenarioIndex < 0 || this.selectedScenarioIndex >= list.size()) {
return;
}
int target = this.selectedScenarioIndex + direction;
if (target >= 0 && target < list.size()) {
Collections.swap(list, this.selectedScenarioIndex, target);
this.selectedScenarioIndex = target;
this.ensureScenarioVisible(target);
}
}
private TextFieldWidget createField(int x, int y, int w, String value) {
TextFieldWidget f = new TextFieldWidget(this.font, x, y, w, 18, StringTextComponent.EMPTY);
f.setValue(value);
f.setMaxLength(256);
f.setTextColor(0xFFFFFF);
f.setBordered(true);
this.addButton((Widget) f);
return f;
}
private void addFieldTracked(TextFieldWidget field, List<TextFieldWidget> tabFields) {
tabFields.add(field);
}
private Button addButtonTracked(Button button, List<Button> tabButtons) {
Button b = (Button) this.addButton((Widget) button);
tabButtons.add(b);
return b;
}
private void setVisible(List<TextFieldWidget> fields, boolean visible) {
for (TextFieldWidget f : fields) {
f.visible = visible;
if (!visible) {
f.setFocus(false);
}
}
}
private void setVisibleButtons(List<Button> buttons, boolean visible) {
for (Button b : buttons) {
b.visible = visible;
b.active = visible;
}
}
private void saveAndSend(boolean requestPlayNow, boolean requestStopNow) {
this.applyFromFields();
CompoundNBT payload = this.exportPayload();
payload.putBoolean("requestPlayNow", requestPlayNow);
payload.putBoolean("requestStopNow", requestStopNow);
NetworkHandler.CHANNEL.sendToServer(new C2SUpdateConfigPacket(((SoundPlayerContainer) this.menu).getPos(), payload));
}
private void sendLiveApply(String reason) {
this.saveAndSend(false, false);
SoundPlayerTileEntity tile = ((SoundPlayerContainer) this.menu).getTile();
boolean playing = tile != null && tile.isPlaying();
this.liveApplyText = playing ? "Применено" : "Не играет";
this.liveApplyStateUntilMs = System.currentTimeMillis() + 2200L;
SoundDebug.log(SoundDebug.Level.L2, "GUI", "APPLY", "sound_player reason={} soundId={}", reason, this.localSettings.soundId);
}
private CompoundNBT exportPayload() {
this.applyFromFields();
CompoundNBT payload = new CompoundNBT();
payload.put("settings", (INBT) this.localSettings.toNbt());
ScenarioQueue sourceQ = new ScenarioQueue();
sourceQ.loop = this.sourceScenarioLoop;
sourceQ.shuffle = this.sourceScenarioShuffle;
sourceQ.steps.addAll(this.sourceScenarioSteps);
payload.put("scenario", (INBT) sourceQ.toNbt());
ScenarioQueue zoneQ = new ScenarioQueue();
zoneQ.loop = this.zoneScenarioLoop;
zoneQ.shuffle = this.zoneScenarioShuffle;
zoneQ.steps.addAll(this.zoneScenarioSteps);
payload.put("zoneScenario", (INBT) zoneQ.toNbt());
payload.putBoolean("sourceEnabled", this.sourceEnabled);
payload.putBoolean("zoneEnabled", this.zoneEnabled);
payload.putBoolean("wizardCompleted", this.wizardCompleted);
payload.putString("zoneShape", this.zoneShape.name());
payload.putFloat("zoneRadius", this.zoneRadius);
payload.putDouble("zoneBoxX", (double) this.zoneBoxX);
payload.putDouble("zoneBoxY", (double) this.zoneBoxY);
payload.putDouble("zoneBoxZ", (double) this.zoneBoxZ);
payload.putBoolean("zoneAutoStartOnEnter", this.zoneAutoStartOnEnter);
payload.putBoolean("zoneStopOnExit", this.zoneStopOnExit);
payload.putString("zoneIntersectionPolicy", this.zonePolicy.name());
payload.putBoolean("zoneUseCustomCoords", this.zoneUseCustomCoords);
payload.putInt("zoneSourceX", this.zoneSourceX);
payload.putInt("zoneSourceY", this.zoneSourceY);
payload.putInt("zoneSourceZ", this.zoneSourceZ);
return payload;
}
private void copyConfigToClipboard() {
String raw = this.exportPayload().toString();
String encoded = "SOUNDMOD_SP_V11:" + Base64.getEncoder().encodeToString(raw.getBytes(StandardCharsets.UTF_8));
if (this.minecraft != null) {
this.minecraft.keyboardHandler.setClipboard(encoded);
}
}
private void pasteConfigFromClipboard() {
if (this.minecraft == null) {
return;
}
String clip = this.minecraft.keyboardHandler.getClipboard();
String prefix = "SOUNDMOD_SP_V11:";
if (clip == null || !clip.startsWith(prefix)) {
return;
}
try {
String raw = new String(Base64.getDecoder().decode(clip.substring(prefix.length())), StandardCharsets.UTF_8);
CompoundNBT payload = JsonToNBT.parseTag(raw);
this.localSettings = SoundSettings.fromNbt(payload.getCompound("settings"));
ScenarioQueue sourceQ = ScenarioQueue.fromNbt(payload.getCompound("scenario"));
this.sourceScenarioSteps.clear();
this.sourceScenarioSteps.addAll(sourceQ.steps);
this.sourceScenarioLoop = sourceQ.loop;
this.sourceScenarioShuffle = sourceQ.shuffle;
ScenarioQueue zoneQ = ScenarioQueue.fromNbt(payload.getCompound("zoneScenario"));
this.zoneScenarioSteps.clear();
this.zoneScenarioSteps.addAll(zoneQ.steps);
this.zoneScenarioLoop = zoneQ.loop;
this.zoneScenarioShuffle = zoneQ.shuffle;
this.sourceEnabled = payload.contains("sourceEnabled") && payload.getBoolean("sourceEnabled");
this.zoneEnabled = payload.contains("zoneEnabled") && payload.getBoolean("zoneEnabled");
this.wizardCompleted = payload.getBoolean("wizardCompleted");
this.zoneShape = this.parseZoneShape(payload.getString("zoneShape"));
this.zoneRadius = payload.getFloat("zoneRadius");
this.zoneBoxX = (float) payload.getDouble("zoneBoxX");
this.zoneBoxY = (float) payload.getDouble("zoneBoxY");
this.zoneBoxZ = (float) payload.getDouble("zoneBoxZ");
this.zoneAutoStartOnEnter = payload.getBoolean("zoneAutoStartOnEnter");
this.zoneStopOnExit = payload.getBoolean("zoneStopOnExit");
this.zonePolicy = this.parseZonePolicy(payload.getString("zoneIntersectionPolicy"));
this.zoneUseCustomCoords = payload.getBoolean("zoneUseCustomCoords");
this.zoneSourceX = payload.getInt("zoneSourceX");
this.zoneSourceY = payload.getInt("zoneSourceY");
this.zoneSourceZ = payload.getInt("zoneSourceZ");
this.refreshControlsFromModel();
this.saveAndSend(false, false);
} catch (Exception ignored) {
}
}
private void applyFromFields() {
if (this.soundIdField != null) this.localSettings.soundId = this.soundIdField.getValue().trim();
if (this.volumeField != null) this.localSettings.volume = this.clampFloat(this.parseFloat(this.volumeField.getValue(), 1.0f), 0.0f, 2.0f);
if (this.pitchField != null) this.localSettings.pitch = this.clampFloat(this.parseFloat(this.pitchField.getValue(), 1.0f), 0.2f, 2.0f);
if (this.radiusField != null) this.localSettings.radius = this.clampFloat(this.parseFloat(this.radiusField.getValue(), 16.0f), 1.0f, 256.0f);
if (this.priorityField != null) this.localSettings.priority = this.clampInt(this.parseInt(this.priorityField.getValue(), 5), 0, 100);
if (this.occlusionStrengthField != null) this.localSettings.occlusionStrength = this.clampFloat(this.parseFloat(this.occlusionStrengthField.getValue(), 0.8f), 0.0f, 1.0f);
if (this.reverbStrengthField != null) this.localSettings.reverbStrength = this.clampFloat(this.parseFloat(this.reverbStrengthField.getValue(), 0.35f), 0.0f, 1.0f);
if (this.fadeInField != null) this.localSettings.fadeInTicks = this.clampInt(this.parseInt(this.fadeInField.getValue(), 0), 0, 72000);
if (this.fadeOutField != null) this.localSettings.fadeOutTicks = this.clampInt(this.parseInt(this.fadeOutField.getValue(), 0), 0, 72000);
if (this.pitchModSpeedField != null) this.localSettings.pitchModSpeed = this.clampFloat(this.parseFloat(this.pitchModSpeedField.getValue(), 1.0f), 0.05f, 20.0f);
if (this.pitchModDepthField != null) this.localSettings.pitchModDepth = this.clampFloat(this.parseFloat(this.pitchModDepthField.getValue(), 0.25f), 0.0f, 2.0f);
if (this.startDelayField != null) this.localSettings.startDelayTicks = this.clampInt(this.parseInt(this.startDelayField.getValue(), 0), 0, 72000);
if (this.repeatDelayField != null) this.localSettings.repeatDelayTicks = this.clampInt(this.parseInt(this.repeatDelayField.getValue(), 0), 0, 72000);
if (this.zoneRadiusField != null) this.zoneRadius = this.clampFloat(this.parseFloat(this.zoneRadiusField.getValue(), 12.0f), 1.0f, 128.0f);
if (this.zoneBoxXField != null) this.zoneBoxX = this.clampFloat(this.parseFloat(this.zoneBoxXField.getValue(), 8.0f), 0.5f, 64.0f);
if (this.zoneBoxYField != null) this.zoneBoxY = this.clampFloat(this.parseFloat(this.zoneBoxYField.getValue(), 4.0f), 0.5f, 64.0f);
if (this.zoneBoxZField != null) this.zoneBoxZ = this.clampFloat(this.parseFloat(this.zoneBoxZField.getValue(), 8.0f), 0.5f, 64.0f);
if (this.zoneSourceXField != null) this.zoneSourceX = this.parseInt(this.zoneSourceXField.getValue(), 0);
if (this.zoneSourceYField != null) this.zoneSourceY = this.parseInt(this.zoneSourceYField.getValue(), 0);
if (this.zoneSourceZField != null) this.zoneSourceZ = this.parseInt(this.zoneSourceZField.getValue(), 0);
}
private void refreshControlsFromModel() {
this.soundIdField.setValue(this.localSettings.soundId);
this.volumeField.setValue(this.floatText(this.localSettings.volume, 1));
this.pitchField.setValue(this.floatText(this.localSettings.pitch, 2));
this.radiusField.setValue(this.floatText(this.localSettings.radius, 1));
this.priorityField.setValue(Integer.toString(this.localSettings.priority));
this.reverbStrengthField.setValue(this.floatText(this.localSettings.reverbStrength, 2));
this.occlusionStrengthField.setValue(this.floatText(this.localSettings.occlusionStrength, 2));
this.fadeInField.setValue(Integer.toString(this.localSettings.fadeInTicks));
this.fadeOutField.setValue(Integer.toString(this.localSettings.fadeOutTicks));
this.startDelayField.setValue(Integer.toString(this.localSettings.startDelayTicks));
this.repeatDelayField.setValue(Integer.toString(this.localSettings.repeatDelayTicks));
this.zoneRadiusField.setValue(this.floatText(this.zoneRadius, 1));
this.zoneBoxXField.setValue(this.floatText(this.zoneBoxX, 1));
this.zoneBoxYField.setValue(this.floatText(this.zoneBoxY, 1));
this.zoneBoxZField.setValue(this.floatText(this.zoneBoxZ, 1));
this.zoneSourceXField.setValue(Integer.toString(this.zoneSourceX));
this.zoneSourceYField.setValue(Integer.toString(this.zoneSourceY));
this.zoneSourceZField.setValue(Integer.toString(this.zoneSourceZ));
this.loopButton.setMessage(this.boolLabel("Цикл", this.localSettings.loop));
this.categoryButton.setMessage(new StringTextComponent("Категория: " + this.categoryRu(this.localSettings.category)));
this.attenuationButton.setMessage(new StringTextComponent("Затух.: " + this.attenuationRu(this.localSettings.attenuationMode)));
this.use3dButton.setMessage(this.boolLabel("3D звук", this.localSettings.use3d));
this.dopplerButton.setMessage(this.boolLabel("Допплер", this.localSettings.doppler));
this.occlusionButton.setMessage(this.boolLabel("Окклюзия (стены)", this.localSettings.occlusion));
this.reverbButton.setMessage(this.boolLabel("Реверберация", this.localSettings.reverb));
this.crossfadeButton.setMessage(this.boolLabel("Кроссфейд", this.localSettings.crossfade));
this.activationModeButton.setMessage(new StringTextComponent("Режим: " + this.modeRu(this.localSettings.activationMode)));
this.redstoneModeButton.setMessage(new StringTextComponent("Сигнал: " + this.redstoneRu(this.localSettings.redstoneMode)));
this.sourceEnabledButton.setMessage(this.boolLabel("Источник", this.sourceEnabled));
this.zoneEnabledButton.setMessage(this.boolLabel("Триггер зоны", this.zoneEnabled));
this.zoneShapeButton.setMessage(new StringTextComponent(this.zoneShape == ZoneShape.SPHERE ? "Сфера" : "Бокс"));
this.zonePolicyButton.setMessage(new StringTextComponent("Приоритет: " + this.policyRu(this.zonePolicy)));
this.zoneAutoStartButton.setMessage(this.boolLabel("Авто-вход", this.zoneAutoStartOnEnter));
this.zoneStopOnExitButton.setMessage(this.boolLabel("Стоп-выход", this.zoneStopOnExit));
this.zoneUseCustomCoordsButton.setMessage(this.boolLabel("Смещение центра", this.zoneUseCustomCoords));
this.scenarioLoopButton.setMessage(this.boolLabel("Цикл", this.activeScenarioLoop()));
if (this.pitchModSpeedField != null) {
this.pitchModSpeedField.setValue(this.floatText(this.localSettings.pitchModSpeed, 1));
}
if (this.pitchModDepthField != null) {
this.pitchModDepthField.setValue(this.floatText(this.localSettings.pitchModDepth, 2));
}
if (this.pitchModeButton != null) {
this.pitchModeButton.setMessage(new StringTextComponent("Pitch: " + this.localSettings.pitchMode.getDisplayName()));
}
if (this.scenarioShuffleButton != null) {
this.scenarioShuffleButton.setMessage(this.boolLabel("Случайно", this.activeScenarioShuffle()));
}
if (this.zoneVisualizerButton != null) {
this.zoneVisualizerButton.setMessage(this.boolLabel("Вид зоны", showZoneVisualizer));
}
this.updateTabVisibility();
}
private PitchModulationMode cyclePitchMode(PitchModulationMode current) {
PitchModulationMode[] values = PitchModulationMode.values();
int next = (current.ordinal() + 1) % values.length;
return values[next];
}
private void addScenarioStep() {
String sid = this.scenarioSoundField.getValue().trim();
if (sid.isEmpty()) {
return;
}
int delay = this.clampInt(this.parseInt(this.scenarioDelayField.getValue(), 20), 0, 36000);
this.activeScenarioList().add(new ScenarioQueue.Step(sid, delay));
this.selectedScenarioIndex = this.activeScenarioList().size() - 1;
this.ensureScenarioVisible(this.selectedScenarioIndex);
this.scenarioSoundField.setValue("");
}
private void removeScenarioStep() {
List<ScenarioQueue.Step> list = this.activeScenarioList();
if (this.selectedScenarioIndex >= 0 && this.selectedScenarioIndex < list.size()) {
list.remove(this.selectedScenarioIndex);
this.selectedScenarioIndex = Math.min(this.selectedScenarioIndex, list.size() - 1);
this.ensureScenarioVisible(this.selectedScenarioIndex);
}
}
private List<ScenarioQueue.Step> activeScenarioList() {
return this.editingZoneScenario ? this.zoneScenarioSteps : this.sourceScenarioSteps;
}
private boolean activeScenarioLoop() {
return this.editingZoneScenario ? this.zoneScenarioLoop : this.sourceScenarioLoop;
}
private void setActiveScenarioLoop(boolean loop) {
if (this.editingZoneScenario) {
this.zoneScenarioLoop = loop;
} else {
this.sourceScenarioLoop = loop;
}
}
private boolean activeScenarioShuffle() {
return this.editingZoneScenario ? this.zoneScenarioShuffle : this.sourceScenarioShuffle;
}
private void setActiveScenarioShuffle(boolean shuffle) {
if (this.editingZoneScenario) {
this.zoneScenarioShuffle = shuffle;
} else {
this.sourceScenarioShuffle = shuffle;
}
}
private void updateSelectedScenarioStep() {
List<ScenarioQueue.Step> list = this.activeScenarioList();
if (this.selectedScenarioIndex >= 0 && this.selectedScenarioIndex < list.size()) {
String sid = this.scenarioSoundField.getValue().trim();
if (!sid.isEmpty()) {
int delay = this.clampInt(this.parseInt(this.scenarioDelayField.getValue(), 20), 0, 36000);
list.set(this.selectedScenarioIndex, new ScenarioQueue.Step(sid, delay));
this.saveAndSend(false, false);
}
}
}
public BlockPos getBlockPos() {
return this.menu != null ? this.menu.getPos() : null;
}
public ZoneShape getZoneShape() {
return this.zoneShape;
}
public float getZoneRadius() {
return this.zoneRadius;
}
public float getZoneBoxX() {
return this.zoneBoxX;
}
public float getZoneBoxY() {
return this.zoneBoxY;
}
public float getZoneBoxZ() {
return this.zoneBoxZ;
}
public boolean isZoneCustomCoords() {
return this.zoneUseCustomCoords;
}
public int getZoneSourceX() {
return this.zoneSourceX;
}
public int getZoneSourceY() {
return this.zoneSourceY;
}
public int getZoneSourceZ() {
return this.zoneSourceZ;
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
boolean enter = keyCode == 257 || keyCode == 335;
List<SoundManifestEntry> suggestions = this.filteredSoundEntries();
if (this.soundIdField.isFocused() || (this.searchField != null && this.searchField.isFocused())) {
if (keyCode == 264 && !suggestions.isEmpty()) {
this.selectedSoundListIndex = Math.min(suggestions.size() - 1, this.selectedSoundListIndex + 1);
this.ensureSoundVisible(this.selectedSoundListIndex);
return true;
}
if (keyCode == 265 && !suggestions.isEmpty()) {
this.selectedSoundListIndex = Math.max(0, this.selectedSoundListIndex - 1);
this.ensureSoundVisible(this.selectedSoundListIndex);
return true;
}
if (enter && this.selectedSoundListIndex >= 0 && this.selectedSoundListIndex < suggestions.size()) {
this.soundIdField.setValue(suggestions.get(this.selectedSoundListIndex).soundId);
this.sendLiveApply("sound_select_enter");
return true;
}
}
if (this.activeTab == Tab.SCENARIO && (this.scenarioSoundField.isFocused() || this.scenarioDelayField.isFocused())) {
if (enter) {
this.addScenarioStep();
return true;
}
}
if (enter && this.isLiveNumericField(this.getFocusedField())) {
this.sendLiveApply("numeric_enter");
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
private TextFieldWidget getFocusedField() {
if (this.volumeField != null && this.volumeField.isFocused()) return this.volumeField;
if (this.pitchField != null && this.pitchField.isFocused()) return this.pitchField;
if (this.radiusField != null && this.radiusField.isFocused()) return this.radiusField;
if (this.priorityField != null && this.priorityField.isFocused()) return this.priorityField;
if (this.reverbStrengthField != null && this.reverbStrengthField.isFocused()) return this.reverbStrengthField;
if (this.occlusionStrengthField != null && this.occlusionStrengthField.isFocused()) return this.occlusionStrengthField;
if (this.fadeInField != null && this.fadeInField.isFocused()) return this.fadeInField;
if (this.fadeOutField != null && this.fadeOutField.isFocused()) return this.fadeOutField;
return null;
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (this.activeTab == Tab.BASIC) {
int listX = this.leftPos + 10;
int listY = this.topPos + 146;
List<SoundManifestEntry> suggestions = this.filteredSoundEntries();
int shown = Math.min(SOUND_LIST_ROWS, Math.max(0, suggestions.size() - this.soundListScroll));
for (int i = 0; i < shown; ++i) {
int idx = this.soundListScroll + i;
int ry = listY + i * ROW_HEIGHT;
if (mouseX >= listX && mouseX <= (listX + 260) && mouseY >= ry && mouseY < (ry + ROW_HEIGHT)) {
long now = System.currentTimeMillis();
boolean doubleClick = (idx == this.selectedSoundListIndex) && (now - this.lastSoundClickTime < 380L);
this.lastSoundClickTime = now;
this.selectedSoundListIndex = idx;
this.soundIdField.setValue(suggestions.get(idx).soundId);
if (doubleClick) {
this.togglePreview();
}
return true;
}
}
}
if (this.activeTab == Tab.SCENARIO) {
int listX = this.leftPos + 10;
int listY = this.topPos + 112;
List<ScenarioQueue.Step> list = this.activeScenarioList();
int shown = Math.min(5, Math.max(0, list.size() - this.scenarioListScroll));
for (int i = 0; i < shown; ++i) {
int idx = this.scenarioListScroll + i;
int ry = listY + i * 14;
if (mouseX >= listX && mouseX <= (listX + 260) && mouseY >= ry && mouseY < (ry + 14)) {
this.selectedScenarioIndex = idx;
ScenarioQueue.Step step = list.get(idx);
this.scenarioSoundField.setValue(step.soundId);
this.scenarioDelayField.setValue(Integer.toString(step.delayTicks));
return true;
}
}
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
if (this.activeTab == Tab.BASIC) {
List<SoundManifestEntry> suggestions = this.filteredSoundEntries();
int maxScroll = Math.max(0, suggestions.size() - SOUND_LIST_ROWS);
if (maxScroll > 0) {
this.soundListScroll = this.clampInt(this.soundListScroll + (delta > 0.0 ? -1 : 1), 0, maxScroll);
return true;
}
}
if (this.activeTab == Tab.SCENARIO) {
List<ScenarioQueue.Step> list = this.activeScenarioList();
int maxScroll = Math.max(0, list.size() - 5);
if (maxScroll > 0) {
this.scenarioListScroll = this.clampInt(this.scenarioListScroll + (delta > 0.0 ? -1 : 1), 0, maxScroll);
return true;
}
}
if (this.activeTab == Tab.HELP) {
int maxScroll = Math.max(0, this.helpLines().size() - 10);
if (maxScroll > 0) {
this.helpScroll = this.clampInt(this.helpScroll + (delta > 0.0 ? -1 : 1), 0, maxScroll);
return true;
}
}
return super.mouseScrolled(mouseX, mouseY, delta);
}
@Override
public void onClose() {
if (activeScreen == this) {
activeScreen = null;
}
AudioPlaybackService.get().stop("gui_preview");
this.saveAndSend(false, false);
super.onClose();
}
@Override
public void removed() {
if (activeScreen == this) {
activeScreen = null;
}
super.removed();
}
@Override
protected void renderBg(MatrixStack matrixStack, float partialTicks, int mouseX, int mouseY) {
int x1 = this.leftPos;
int y1 = this.topPos;
int x2 = x1 + this.imageWidth;
int y2 = y1 + this.imageHeight;
// Shadow backdrop
SoundPlayerScreen.fill(matrixStack, x1 - 2, y1 - 2, x2 + 2, y2 + 2, COLOR_BG_SHADOW);
// Main panel
SoundPlayerScreen.fill(matrixStack, x1, y1, x2, y2, COLOR_BG_MAIN);
// Outer border
SoundPlayerScreen.fill(matrixStack, x1, y1, x2, y1 + 1, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x1, y2 - 1, x2, y2, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x1, y1, x1 + 1, y2, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x2 - 1, y1, x2, y2, COLOR_BORDER);
// Header separator
SoundPlayerScreen.fill(matrixStack, x1 + 6, y1 + 25, x2 - 6, y1 + 26, COLOR_BORDER);
// Content area background
SoundPlayerScreen.fill(matrixStack, x1 + 6, y1 + 27, x2 - 6, y2 - 50, COLOR_BG_INNER);
// Active tab underline indicator (accent colored bar spanning the active button's width)
for (int i = 0; i < this.tabButtons.size() && i < Tab.values().length; i++) {
if (this.activeTab.ordinal() == i) {
Button btn = this.tabButtons.get(i);
SoundPlayerScreen.fill(matrixStack, btn.x, btn.y + btn.getHeight() - 2, btn.x + btn.getWidth(), btn.y + btn.getHeight(), COLOR_ACCENT);
}
}
// Status bar area (dedicated dark bar background)
SoundPlayerScreen.fill(matrixStack, x1 + 6, y2 - 48, x2 - 6, y2 - 30, COLOR_BG_BOX);
SoundPlayerScreen.fill(matrixStack, x1 + 6, y2 - 49, x2 - 6, y2 - 48, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x1 + 6, y2 - 30, x2 - 6, y2 - 29, COLOR_BORDER);
}
@Override
public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) {
this.renderBackground(matrixStack);
super.render(matrixStack, mouseX, mouseY, partialTicks);
this.applyFromFields();
String soundId = this.localSettings.soundId.trim();
ClientSoundCache.Status status = soundId.isEmpty() ? ClientSoundCache.Status.MISSING : ClientSoundCache.get().status(soundId);
boolean playingNow = ((SoundPlayerContainer) this.menu).getTile() != null && ((SoundPlayerContainer) this.menu).getTile().isPlaying();
boolean previewPlaying = AudioPlaybackService.get().isPlaying("gui_preview");
// Update preview button label dynamically
if (this.previewButton != null) {
this.previewButton.setMessage(new StringTextComponent(previewPlaying ? "⏹ Стоп" : "▶ Тест"));
}
int x = this.leftPos + 10;
int y = this.topPos;
// Render tab contents text labels
switch (this.activeTab) {
case BASIC:
this.font.draw(matrixStack, new StringTextComponent("Файл звука:"), (float) x, (float) (y + 28), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Громкость"), (float) x, (float) (y + 63), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Pitch"), (float) (x + 74), (float) (y + 63), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Радиус (б)"), (float) (x + 144), (float) (y + 63), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Приоритет"), (float) (x + 208), (float) (y + 63), COLOR_TEXT_MUTED);
this.drawSoundSuggestions(matrixStack, mouseX, mouseY);
break;
case EFFECTS:
this.font.draw(matrixStack, new StringTextComponent("Эффекты и 3D позиционирование"), (float) x, (float) (y + 28), COLOR_TEXT_TITLE);
this.font.draw(matrixStack, new StringTextComponent("Сила:"), (float) (x + 176), (float) (y + 73), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Сила:"), (float) (x + 176), (float) (y + 99), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Fade In:"), (float) x, (float) (y + 151), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Fade Out:"), (float) (x + 148), (float) (y + 151), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Скор:"), (float) (x + 118), (float) (y + 177), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Глуб:"), (float) (x + 192), (float) (y + 177), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("⚡ Изменения применяются на лету во время звука"), (float) x, (float) (y + 195), COLOR_TEXT_MUTED);
break;
case TRIGGER:
this.font.draw(matrixStack, new StringTextComponent("Режимы активации проигрывателя"), (float) x, (float) (y + 28), COLOR_TEXT_TITLE);
this.font.draw(matrixStack, new StringTextComponent("Старт (тиков):"), (float) x, (float) (y + 73), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Интервал:"), (float) (x + 150), (float) (y + 73), COLOR_TEXT_LABEL);
this.drawTriggerHelpBox(matrixStack, x, y + 124);
break;
case ZONE:
this.font.draw(matrixStack, new StringTextComponent("Настройка триггерной зоны"), (float) x, (float) (y + 28), COLOR_TEXT_TITLE);
if (this.zoneShape == ZoneShape.SPHERE) {
this.font.draw(matrixStack, new StringTextComponent("Радиус зоны (блоков):"), (float) x, (float) (y + 73), COLOR_TEXT_LABEL);
} else {
this.font.draw(matrixStack, new StringTextComponent("Полуразмеры X / Y / Z:"), (float) x, (float) (y + 73), COLOR_TEXT_LABEL);
}
if (this.zoneUseCustomCoords) {
this.font.draw(matrixStack, new StringTextComponent("Центр звука:"), (float) x, (float) (y + 151), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("X"), (float) (x + 92), (float) (y + 151), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Y"), (float) (x + 152), (float) (y + 151), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Z"), (float) (x + 212), (float) (y + 151), COLOR_TEXT_MUTED);
}
break;
case SCENARIO:
this.font.draw(matrixStack, new StringTextComponent("Очередь воспроизведения"), (float) x, (float) (y + 28), COLOR_TEXT_TITLE);
this.font.draw(matrixStack, new StringTextComponent("Пауза:"), (float) (x + 172), (float) (y + 71), COLOR_TEXT_LABEL);
this.drawScenarioList(matrixStack, mouseX, mouseY);
break;
case HELP:
this.drawHelp(matrixStack);
break;
}
// --- STATUS BAR (Above action buttons) ---
int statusY = y + this.imageHeight - 43;
int statusColor = this.colorForStatus(status);
String statusPrefix = status == ClientSoundCache.Status.AVAILABLE ? "● " : (status == ClientSoundCache.Status.TRANSFERRING ? "⏳ " : "✖ ");
String statusText = statusPrefix + this.statusRu(status);
ClientTransferManager.TransferProgress progress = ClientTransferManager.get().progressBySoundId(soundId);
if (progress != null) {
statusText = "⏳ Загрузка: " + progress.percent + "% (" + (progress.receivedBytes / 1024) + "/" + (progress.expectedBytes / 1024) + " KB)";
statusColor = COLOR_YELLOW;
}
this.font.draw(matrixStack, new StringTextComponent(statusText), (float) x, (float) statusY, statusColor);
String blockState = playingNow ? "● Блок: Играет" : "○ Блок: Ожидание";
int blockStateColor = playingNow ? COLOR_GREEN : COLOR_TEXT_MUTED;
this.font.draw(matrixStack, new StringTextComponent(blockState), (float) (x + 160), (float) statusY, blockStateColor);
this.drawContextTooltips(matrixStack, mouseX, mouseY);
this.renderTooltip(matrixStack, mouseX, mouseY);
}
private void drawTriggerHelpBox(MatrixStack matrixStack, int x, int y) {
SoundPlayerScreen.fill(matrixStack, x, y, x + 260, y + 54, 0x55000000);
SoundPlayerScreen.fill(matrixStack, x, y, x + 260, y + 1, COLOR_BORDER);
this.font.draw(matrixStack, new StringTextComponent("Описание выбранного режима:"), (float) (x + 6), (float) (y + 6), COLOR_TEXT_TITLE);
String desc;
switch (this.localSettings.activationMode) {
case CLICK:
desc = "Запуск вручную: кликом ПКМ по блоку проигрывателя.";
break;
case REDSTONE:
desc = this.localSettings.redstoneMode == RedstoneMode.HOLD
? "Редстоун HOLD: звук играет, пока к блоку подведён сигнал."
: "Редстоун EDGE: звук запускается по импульсу сигнала.";
break;
case AUTO:
desc = "Авто: звук играет постоянно при загрузке мира и чанка.";
break;
case TIMER:
desc = "Таймер: циклический запуск с задержкой повтора.";
break;
default:
desc = "";
}
this.font.draw(matrixStack, new StringTextComponent(desc), (float) (x + 6), (float) (y + 22), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("С Shift+ПКМ по блоку можно переключать воспроизведение."), (float) (x + 6), (float) (y + 36), COLOR_TEXT_MUTED);
}
private boolean isLiveNumericField(TextFieldWidget field) {
return field == this.volumeField || field == this.pitchField || field == this.radiusField || field == this.reverbStrengthField || field == this.occlusionStrengthField || field == this.fadeInField || field == this.fadeOutField;
}
private void drawSoundSuggestions(MatrixStack matrixStack, int mouseX, int mouseY) {
List<SoundManifestEntry> suggestions = this.filteredSoundEntries();
int x = this.leftPos + 10;
int y = this.topPos + 146;
int listWidth = 260;
int listHeight = SOUND_LIST_ROWS * ROW_HEIGHT;
// Background box for list
SoundPlayerScreen.fill(matrixStack, x, y, x + listWidth, y + listHeight, COLOR_BG_BOX);
SoundPlayerScreen.fill(matrixStack, x, y, x + listWidth, y + 1, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x, y + listHeight - 1, x + listWidth, y + listHeight, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x, y, x + 1, y + listHeight, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x + listWidth - 1, y, x + listWidth, y + listHeight, COLOR_BORDER);
if (suggestions.isEmpty()) {
this.font.draw(matrixStack, new StringTextComponent("Звуки не найдены. Положите файлы в sounds/"), (float) (x + 4), (float) (y + 4), COLOR_TEXT_MUTED);
return;
}
int shown = Math.min(SOUND_LIST_ROWS, Math.max(0, suggestions.size() - this.soundListScroll));
for (int i = 0; i < shown; ++i) {
int idx = this.soundListScroll + i;
SoundManifestEntry e = suggestions.get(idx);
int rowY = y + i * ROW_HEIGHT;
boolean isHovered = mouseX >= x && mouseX <= (x + listWidth) && mouseY >= rowY && mouseY < (rowY + ROW_HEIGHT);
if (idx == this.selectedSoundListIndex) {
SoundPlayerScreen.fill(matrixStack, x + 1, rowY, x + listWidth - 1, rowY + ROW_HEIGHT, 0x44388BFD);
} else if (isHovered) {
SoundPlayerScreen.fill(matrixStack, x + 1, rowY, x + listWidth - 1, rowY + ROW_HEIGHT, 0x22FFFFFF);
}
String name = this.trimRow(e.soundId, 26);
ClientSoundCache.Status rowStatus = ClientSoundCache.get().status(e.soundId);
int nameColor = idx == this.selectedSoundListIndex ? COLOR_TEXT_TITLE : (isHovered ? 0xFFFFFFFF : 0xFFC9D1D9);
this.font.draw(matrixStack, new StringTextComponent(name), (float) (x + 4), (float) (rowY + 2), nameColor);
this.font.draw(matrixStack, new StringTextComponent(this.statusRu(rowStatus)), (float) (x + 190), (float) (rowY + 2), this.colorForStatus(rowStatus));
}
if (suggestions.size() > SOUND_LIST_ROWS) {
this.drawSimpleScrollbar(matrixStack, x + listWidth - 4, y, listHeight, this.soundListScroll, suggestions.size(), SOUND_LIST_ROWS);
}
}
private List<SoundManifestEntry> filteredSoundEntries() {
String query = this.searchField != null ? this.searchField.getValue().trim().toLowerCase() : "";
if (!query.equals(this.lastSoundFilterQuery)) {
this.lastSoundFilterQuery = query;
this.soundListScroll = 0;
this.selectedSoundListIndex = -1;
}
List<SoundManifestEntry> all = ClientSoundCache.get().entries();
ArrayList<SoundManifestEntry> out = new ArrayList<>();
for (SoundManifestEntry e : all) {
if (!query.isEmpty() && !e.soundId.toLowerCase().contains(query)) continue;
out.add(e);
}
return out;
}
private void drawScenarioList(MatrixStack matrixStack, int mouseX, int mouseY) {
int x = this.leftPos + 10;
int y = this.topPos + 112;
int listWidth = 260;
int listHeight = 5 * 14;
List<ScenarioQueue.Step> list = this.activeScenarioList();
SoundPlayerScreen.fill(matrixStack, x, y, x + listWidth, y + listHeight, COLOR_BG_BOX);
SoundPlayerScreen.fill(matrixStack, x, y, x + listWidth, y + 1, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x, y + listHeight - 1, x + listWidth, y + listHeight, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x, y, x + 1, y + listHeight, COLOR_BORDER);
SoundPlayerScreen.fill(matrixStack, x + listWidth - 1, y, x + listWidth, y + listHeight, COLOR_BORDER);
if (list.isEmpty()) {
this.font.draw(matrixStack, new StringTextComponent("Очередь пуста. Добавьте шаги кнопкой выше."), (float) (x + 6), (float) (y + 6), COLOR_TEXT_MUTED);
return;
}
int shown = Math.min(5, Math.max(0, list.size() - this.scenarioListScroll));
for (int i = 0; i < shown; ++i) {
int idx = this.scenarioListScroll + i;
ScenarioQueue.Step s = list.get(idx);
int ry = y + i * 14;
boolean isHovered = mouseX >= x && mouseX <= (x + listWidth) && mouseY >= ry && mouseY < (ry + 14);
if (idx == this.selectedScenarioIndex) {
SoundPlayerScreen.fill(matrixStack, x + 1, ry, x + listWidth - 1, ry + 14, 0x44388BFD);
} else if (isHovered) {
SoundPlayerScreen.fill(matrixStack, x + 1, ry, x + listWidth - 1, ry + 14, 0x22FFFFFF);
}
String row = (idx + 1) + ". " + s.soundId + " (пауза: " + (s.delayTicks / 20.0f) + "с)";
this.font.draw(matrixStack, new StringTextComponent(this.trimRow(row, 36)), (float) (x + 4), (float) (ry + 3), idx == this.selectedScenarioIndex ? COLOR_TEXT_TITLE : COLOR_TEXT_LABEL);
}
if (list.size() > 5) {
this.drawSimpleScrollbar(matrixStack, x + listWidth - 4, y, listHeight, this.scenarioListScroll, list.size(), 5);
}
}
private void drawHelp(MatrixStack matrixStack) {
int x = this.leftPos + 12;
int y = this.topPos + 32;
List<String> lines = this.helpLines();
int shown = Math.min(10, Math.max(0, lines.size() - this.helpScroll));
for (int i = 0; i < shown; ++i) {
int idx = this.helpScroll + i;
int color = idx == 0 ? COLOR_TEXT_TITLE : COLOR_TEXT_LABEL;
this.font.draw(matrixStack, new StringTextComponent(lines.get(idx)), (float) x, (float) (y + i * 14), color);
}
if (lines.size() > 10) {
this.drawSimpleScrollbar(matrixStack, this.leftPos + 268, y, 140, this.helpScroll, lines.size(), 10);
}
}
private List<String> helpLines() {
ArrayList<String> lines = new ArrayList<>();
lines.add("Справка по Sound Player:");
lines.add("• Основа: выберите soundId, настройте громкость и радиус.");
lines.add("• ▶ Тест: мгновенное предпрослушивание выбранного звука.");
lines.add("• 2x клик по списку: запускает предпрослушивание трека.");
lines.add("• Эффекты: 3D звук, приглушение за стенами (окклюзия), эхо.");
lines.add("• Модуляция Pitch: вибрато, глитч, tape stop (замедление), разгон.");
lines.add("• Триггер: настройка запуска редстоуном, кликом или таймером.");
lines.add("• Зона: музыка включается при входе в зону и гаснет при выходе.");
lines.add("• Вид зоны: отображает 3D границы зоны прямо в игровом мире.");
lines.add("• Моя позиция: смещает центр звучания зоны в точку игрока.");
lines.add("• Сценарий: последовательный или случайный плейлист треков.");
lines.add("• Копия/Вставка: перенос всей конфигурации между блоками.");
return lines;
}
private void drawSimpleScrollbar(MatrixStack ms, int x, int y, int h, int scroll, int total, int window) {
SoundPlayerScreen.fill(ms, x, y, x + 3, y + h, 0x44000000);
int thumbH = Math.max(8, h * window / total);
int maxScroll = total - window;
int thumbY = y + (maxScroll <= 0 ? 0 : scroll * (h - thumbH) / maxScroll);
SoundPlayerScreen.fill(ms, x, thumbY, x + 3, thumbY + thumbH, COLOR_ACCENT);
}
private String trimRow(String text, int maxChars) {
if (text == null) return "";
return text.length() <= maxChars ? text : text.substring(0, Math.max(0, maxChars - 3)) + "...";
}
private ITextComponent boolLabel(String title, boolean v) {
return new StringTextComponent(title + ": " + (v ? "ВКЛ" : "ВЫКЛ"));
}
private String categoryRu(String c) {
switch (c) {
case "music": return "музыка";
case "ambient": return "фон";
case "voice": return "голос";
case "effect": return "эффект";
default: return c;
}
}
private String cycleCategory(String c) {
for (int i = 0; i < CATEGORIES.length; ++i) {
if (!CATEGORIES[i].equals(c)) continue;
return CATEGORIES[(i + 1) % CATEGORIES.length];
}
return CATEGORIES[0];
}
private String attenuationRu(AttenuationMode m) {
switch (m) {
case LINEAR: return "линейное";
case EXPONENTIAL: return "экспонента";
case LOGARITHMIC: return "логарифм";
case NONE: return "выкл";
default: return m.name();
}
}
private AttenuationMode cycleAttenuation(AttenuationMode m) {
AttenuationMode[] vals = AttenuationMode.values();
return vals[(m.ordinal() + 1) % vals.length];
}
private String modeRu(ActivationMode m) {
switch (m) {
case CLICK: return "Клик";
case REDSTONE: return "Редстоун";
case AUTO: return "Авто";
case TIMER: return "Таймер";
default: return m.name();
}
}
private ActivationMode cycleMode(ActivationMode m) {
ActivationMode[] vals = ActivationMode.values();
return vals[(m.ordinal() + 1) % vals.length];
}
private String redstoneRu(RedstoneMode m) {
switch (m) {
case HOLD: return "Удержание";
case EDGE: return "Импульс";
default: return m.name();
}
}
private RedstoneMode cycleRedstone(RedstoneMode m) {
RedstoneMode[] vals = RedstoneMode.values();
return vals[(m.ordinal() + 1) % vals.length];
}
private String policyRu(ZoneIntersectionPolicy p) {
switch (p) {
case HIGHEST_PRIORITY: return "Макс";
case BLEND: return "Смесь";
case QUEUE: return "Очередь";
default: return p.name();
}
}
private ZoneIntersectionPolicy cyclePolicy(ZoneIntersectionPolicy p) {
ZoneIntersectionPolicy[] vals = ZoneIntersectionPolicy.values();
return vals[(p.ordinal() + 1) % vals.length];
}
private ZoneShape parseZoneShape(String raw) {
try {
return ZoneShape.valueOf(raw);
} catch (Exception e) {
return ZoneShape.SPHERE;
}
}
private ZoneIntersectionPolicy parseZonePolicy(String raw) {
try {
return ZoneIntersectionPolicy.valueOf(raw);
} catch (Exception e) {
return ZoneIntersectionPolicy.HIGHEST_PRIORITY;
}
}
private String statusRu(ClientSoundCache.Status s) {
switch (s) {
case AVAILABLE: return "Доступен";
case TRANSFERRING: return "Загрузка...";
case ERROR: return "Ошибка";
case MISSING: return "Отсутствует";
case UNSUPPORTED: return "Не подд.";
default: return s.name();
}
}
private int colorForStatus(ClientSoundCache.Status s) {
switch (s) {
case AVAILABLE: return COLOR_GREEN;
case TRANSFERRING: return COLOR_YELLOW;
case ERROR:
case MISSING:
case UNSUPPORTED: return COLOR_RED;
default: return COLOR_TEXT_MUTED;
}
}
private void drawContextTooltips(MatrixStack matrixStack, int mouseX, int mouseY) {
if (this.previewButton != null && this.previewButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Предпрослушивание выбранного звука"), mouseX, mouseY);
} else if (this.myPosButton != null && this.myPosButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Вставить текущие координаты игрока как центр звука"), mouseX, mouseY);
} else if (this.zoneVisualizerButton != null && this.zoneVisualizerButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Отображать 3D границы зоны в игровом мире"), mouseX, mouseY);
} else if (this.pitchModeButton != null && this.pitchModeButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Динамическое изменение тона звука во времени (Вибрато, Глитч, Tape Stop и др.)"), mouseX, mouseY);
} else if (this.copyConfigButton != null && this.copyConfigButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Скопировать все настройки блока в буфер обмена"), mouseX, mouseY);
} else if (this.pasteConfigButton != null && this.pasteConfigButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Вставить настройки блока из буфера обмена"), mouseX, mouseY);
} else if (this.playNowButton != null && this.playNowButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Принудительно запустить звук этого блока в мире"), mouseX, mouseY);
} else if (this.stopNowButton != null && this.stopNowButton.isHovered()) {
this.renderTooltip(matrixStack, new StringTextComponent("Остановить звук этого блока"), mouseX, mouseY);
}
}
private void ensureSoundVisible(int idx) {
if (idx < 0) return;
if (idx < this.soundListScroll) {
this.soundListScroll = idx;
} else if (idx >= this.soundListScroll + SOUND_LIST_ROWS) {
this.soundListScroll = idx - SOUND_LIST_ROWS + 1;
}
}
private void ensureScenarioVisible(int idx) {
if (idx < 0) return;
if (idx < this.scenarioListScroll) {
this.scenarioListScroll = idx;
} else if (idx >= this.scenarioListScroll + 5) {
this.scenarioListScroll = idx - 5 + 1;
}
}
private float parseFloat(String s, float def) {
try {
return Float.parseFloat(s.trim().replace(',', '.'));
} catch (Exception e) {
return def;
}
}
private int parseInt(String s, int def) {
try {
return Integer.parseInt(s.trim());
} catch (Exception e) {
return def;
}
}
private float clampFloat(float v, float min, float max) {
return Math.max(min, Math.min(max, v));
}
private int clampInt(int v, int min, int max) {
return Math.max(min, Math.min(max, v));
}
private String floatText(float v, int decimals) {
return String.format(java.util.Locale.US, "%." + decimals + "f", Float.valueOf(v));
}
private enum Tab {
BASIC,
EFFECTS,
TRIGGER,
ZONE,
SCENARIO,
HELP
}
}