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

1483 lines
78 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.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.inventory.container.Container;
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_ACCENT = 0xFF388BFD;
private static final int COLOR_ACCENT_HOVER = 0xFF58A6FF;
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 = 5;
private static final int ROW_HEIGHT = 12;
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<>();
public static SoundPlayerScreen activeScreen;
public static boolean showZoneVisualizer = true;
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 TextFieldWidget volumeField;
private TextFieldWidget pitchField;
private TextFieldWidget radiusField;
private TextFieldWidget priorityField;
private Button previewButton;
private Button loopButton;
private Button categoryButton;
private Button clearFilterButton;
private Button volMinusBtn;
private Button volPlusBtn;
private Button pitchMinusBtn;
private Button pitchPlusBtn;
// Controls - Effects
private Button use3dButton;
private Button attenuationButton;
private Button occlusionButton;
private Button reverbButton;
private Button dopplerButton;
private Button crossfadeButton;
private TextFieldWidget occlusionStrengthField;
private TextFieldWidget reverbStrengthField;
private TextFieldWidget fadeInField;
private TextFieldWidget fadeOutField;
// 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 zoneAutoStartButton;
private Button zoneStopOnExitButton;
private Button zoneUseCustomCoordsButton;
private Button myPosButton;
private Button zoneVisualizerButton;
private TextFieldWidget zoneRadiusField;
private TextFieldWidget zoneBoxXField;
private TextFieldWidget zoneBoxYField;
private TextFieldWidget zoneBoxZField;
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 = 270;
this.imageHeight = 260;
}
protected void init() {
super.init();
if (this.minecraft != null) {
this.minecraft.keyboardHandler.setSendRepeatsToGui(true);
}
this.titleLabelX = 10000;
this.titleLabelY = 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();
}
private void addTabButtons() {
this.tabButtons.clear();
int x = this.leftPos + 8;
int y = this.topPos + 6;
this.tabButtons.add((Button) this.addButton((Widget) new Button(x, y, 44, 18, new StringTextComponent("Основа"), b -> this.switchTab(Tab.BASIC))));
this.tabButtons.add((Button) this.addButton((Widget) new Button(x + 46, y, 48, 18, new StringTextComponent("Эффекты"), b -> this.switchTab(Tab.EFFECTS))));
this.tabButtons.add((Button) this.addButton((Widget) new Button(x + 96, y, 46, 18, new StringTextComponent("Триггер"), b -> this.switchTab(Tab.TRIGGER))));
this.tabButtons.add((Button) this.addButton((Widget) new Button(x + 144, y, 38, 18, new StringTextComponent("Зона"), b -> this.switchTab(Tab.ZONE))));
this.tabButtons.add((Button) this.addButton((Widget) new Button(x + 184, y, 50, 18, new StringTextComponent("Сценарий"), b -> this.switchTab(Tab.SCENARIO))));
this.tabButtons.add((Button) this.addButton((Widget) new Button(x + 236, y, 18, 18, new StringTextComponent("?"), b -> this.switchTab(Tab.HELP))));
}
private void initFieldsAndButtons() {
int x = this.leftPos + 10;
int y = this.topPos;
// --- TAB 1: BASIC ---
this.soundIdField = this.createField(x, y + 40, 186, this.localSettings.soundId);
this.previewButton = this.addButtonTracked(new Button(x + 190, y + 39, 60, 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, 13, 18, new StringTextComponent("-"), b -> this.adjustVolume(-0.1f)), this.basicButtons);
this.volumeField = this.createField(x + 14, y + 74, 32, this.floatText(this.localSettings.volume, 1));
this.volPlusBtn = this.addButtonTracked(new Button(x + 47, y + 74, 13, 18, new StringTextComponent("+"), b -> this.adjustVolume(0.1f)), this.basicButtons);
this.pitchMinusBtn = this.addButtonTracked(new Button(x + 65, y + 74, 13, 18, new StringTextComponent("-"), b -> this.adjustPitch(-0.05f)), this.basicButtons);
this.pitchField = this.createField(x + 79, y + 74, 32, this.floatText(this.localSettings.pitch, 2));
this.pitchPlusBtn = this.addButtonTracked(new Button(x + 112, y + 74, 13, 18, new StringTextComponent("+"), b -> this.adjustPitch(0.05f)), this.basicButtons);
this.radiusField = this.createField(x + 132, y + 74, 52, this.floatText(this.localSettings.radius, 1));
this.priorityField = this.createField(x + 192, y + 74, 58, Integer.toString(this.localSettings.priority));
// Options row: Loop and Category
this.loopButton = this.addButtonTracked(new Button(x, y + 96, 90, 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 + 94, y + 96, 156, 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 + 130, 186, "");
this.clearFilterButton = this.addButtonTracked(new Button(x + 190, y + 129, 60, 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, 122, 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 + 126, y + 42, 124, 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, 122, 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 + 180, y + 69, 70, this.floatText(this.localSettings.occlusionStrength, 2));
this.reverbButton = this.addButtonTracked(new Button(x, y + 94, 122, 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 + 180, y + 95, 70, this.floatText(this.localSettings.reverbStrength, 2));
this.dopplerButton = this.addButtonTracked(new Button(x, y + 120, 122, 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 + 126, y + 120, 124, 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, y + 160, 120, Integer.toString(this.localSettings.fadeInTicks));
this.fadeOutField = this.createField(x + 126, y + 160, 124, Integer.toString(this.localSettings.fadeOutTicks));
// --- TAB 3: TRIGGER ---
this.activationModeButton = this.addButtonTracked(new Button(x, y + 42, 130, 20, new StringTextComponent("Режим: " + this.modeRu(this.localSettings.activationMode)), b -> {
this.localSettings.activationMode = this.cycleActivation(this.localSettings.activationMode);
b.setMessage(new StringTextComponent("Режим: " + this.modeRu(this.localSettings.activationMode)));
this.redstoneModeButton.visible = this.localSettings.activationMode == ActivationMode.REDSTONE;
this.redstoneModeButton.active = this.localSettings.activationMode == ActivationMode.REDSTONE;
this.saveAndSend(false, false);
}), this.triggerButtons);
this.redstoneModeButton = this.addButtonTracked(new Button(x + 134, y + 42, 116, 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, y + 78, 120, Integer.toString(this.localSettings.startDelayTicks));
this.repeatDelayField = this.createField(x + 126, y + 78, 124, Integer.toString(this.localSettings.repeatDelayTicks));
this.sourceEnabledButton = this.addButtonTracked(new Button(x, y + 114, 122, 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 + 126, y + 114, 124, 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, 122, 20, new StringTextComponent("Форма: " + this.shapeRu(this.zoneShape)), b -> {
this.zoneShape = this.zoneShape == ZoneShape.SPHERE ? ZoneShape.BOX : ZoneShape.SPHERE;
b.setMessage(new StringTextComponent("Форма: " + this.shapeRu(this.zoneShape)));
this.saveAndSend(false, false);
}), this.zoneButtons);
this.zonePolicyButton = this.addButtonTracked(new Button(x + 126, y + 42, 124, 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.zoneRadiusField = this.createField(x + 85, y + 68, 60, this.floatText(this.zoneRadius, 1));
this.zoneBoxXField = this.createField(x + 18, y + 68, 42, this.floatText(this.zoneBoxX, 1));
this.zoneBoxYField = this.createField(x + 78, y + 68, 42, this.floatText(this.zoneBoxY, 1));
this.zoneBoxZField = this.createField(x + 138, y + 68, 42, this.floatText(this.zoneBoxZ, 1));
this.zoneVisualizerButton = this.addButtonTracked(new Button(x + 150, y + 68, 100, 20, this.boolLabel("Вид в мире", showZoneVisualizer), b -> {
showZoneVisualizer = !showZoneVisualizer;
b.setMessage(this.boolLabel("Вид в мире", showZoneVisualizer));
}), this.zoneButtons);
this.zoneAutoStartButton = this.addButtonTracked(new Button(x, y + 94, 122, 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 + 126, y + 94, 124, 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, 136, 20, this.boolLabel("Смещение звука", this.zoneUseCustomCoords), b -> {
this.zoneUseCustomCoords = !this.zoneUseCustomCoords;
b.setMessage(this.boolLabel("Смещение звука", this.zoneUseCustomCoords));
this.saveAndSend(false, false);
}), this.zoneButtons);
this.myPosButton = this.addButtonTracked(new Button(x + 140, y + 120, 110, 20, new StringTextComponent("Моя позиция"), b -> this.applyPlayerCoordsToZone()), this.zoneButtons);
this.zoneSourceXField = this.createField(x + 18, y + 146, 58, Integer.toString(this.zoneSourceX));
this.zoneSourceYField = this.createField(x + 98, y + 146, 58, Integer.toString(this.zoneSourceY));
this.zoneSourceZField = this.createField(x + 178, y + 146, 58, Integer.toString(this.zoneSourceZ));
// --- TAB 5: SCENARIO ---
this.scenarioTargetButton = this.addButtonTracked(new Button(x, y + 42, 78, 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 + 80, y + 42, 84, 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 + 166, y + 42, 84, 20, this.boolLabel("Случайно", this.activeScenarioShuffle()), b -> {
this.setActiveScenarioShuffle(!this.activeScenarioShuffle());
b.setMessage(this.boolLabel("Случайно", this.activeScenarioShuffle()));
}), this.scenarioButtons);
this.scenarioSoundField = this.createField(x, y + 70, 116, "");
this.scenarioUseCurrentBtn = this.addButtonTracked(new Button(x + 118, y + 69, 44, 20, new StringTextComponent("Тек."), b -> {
this.scenarioSoundField.setValue(this.soundIdField.getValue().trim());
}), this.scenarioButtons);
this.scenarioDelayField = this.createField(x + 164, y + 70, 38, "20");
this.scenarioSetButton = this.addButtonTracked(new Button(x + 204, y + 69, 46, 20, new StringTextComponent("Задать"), b -> this.updateSelectedScenarioStep()), this.scenarioButtons);
this.scenarioAddButton = this.addButtonTracked(new Button(x, y + 96, 60, 20, new StringTextComponent("+ Шаг"), b -> this.addScenarioStep()), this.scenarioButtons);
this.scenarioRemoveButton = this.addButtonTracked(new Button(x + 64, y + 96, 60, 20, new StringTextComponent("- Шаг"), b -> this.removeScenarioStep()), this.scenarioButtons);
this.scenarioUpButton = this.addButtonTracked(new Button(x + 128, y + 96, 58, 20, new StringTextComponent("▲ Вверх"), b -> this.moveScenarioStep(-1)), this.scenarioButtons);
this.scenarioDownButton = this.addButtonTracked(new Button(x + 190, y + 96, 60, 20, new StringTextComponent("▼ Вниз"), b -> this.moveScenarioStep(1)), this.scenarioButtons);
// --- FIXED BOTTOM ROW (ALWAYS VISIBLE) ---
int by = y + this.imageHeight - 26;
this.saveButton = (Button) this.addButton((Widget) new Button(x - 2, by, 64, 20, new StringTextComponent("Сохранить"), b -> this.saveAndSend(false, false)));
this.copyConfigButton = (Button) this.addButton((Widget) new Button(x + 64, by, 44, 20, new StringTextComponent("Копия"), b -> this.copyConfigToClipboard()));
this.pasteConfigButton = (Button) this.addButton((Widget) new Button(x + 110, by, 48, 20, new StringTextComponent("Вставка"), b -> this.pasteConfigFromClipboard()));
this.playNowButton = (Button) this.addButton((Widget) new Button(x + 160, by, 46, 20, new StringTextComponent("▶ Старт"), b -> this.saveAndSend(true, false)));
this.stopNowButton = (Button) this.addButton((Widget) new Button(x + 208, by, 44, 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.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
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;
}
}
private void togglePreview() {
if (AudioPlaybackService.get().isPlaying("gui_preview")) {
AudioPlaybackService.get().stop("gui_preview");
this.previewButton.setMessage(new StringTextComponent("▶ Тест"));
} else {
String sid = this.soundIdField.getValue().trim();
if (sid.isEmpty()) {
return;
}
this.applyFromFields();
ClientPlayerEntity player = Minecraft.getInstance().player;
BlockPos p = player != null ? player.blockPosition() : ((SoundPlayerContainer) this.menu).getPos();
AudioPlaybackService.get().play("gui_preview", p, this.localSettings);
this.previewButton.setMessage(new StringTextComponent("⏹ Стоп"));
}
}
private void adjustVolume(float delta) {
this.applyFromFields();
this.localSettings.volume = this.clampFloat(this.localSettings.volume + delta, 0.0f, 8.0f);
this.volumeField.setValue(this.floatText(this.localSettings.volume, 1));
this.sendLiveApply("volume_adjust");
}
private void adjustPitch(float delta) {
this.applyFromFields();
this.localSettings.pitch = this.clampFloat(this.localSettings.pitch + delta, 0.05f, 4.0f);
this.pitchField.setValue(this.floatText(this.localSettings.pitch, 2));
this.sendLiveApply("pitch_adjust");
}
private void applyPlayerCoordsToZone() {
ClientPlayerEntity p = Minecraft.getInstance().player;
if (p != null) {
this.zoneSourceX = (int) Math.floor(p.getX());
this.zoneSourceY = (int) Math.floor(p.getY());
this.zoneSourceZ = (int) Math.floor(p.getZ());
this.zoneSourceXField.setValue(Integer.toString(this.zoneSourceX));
this.zoneSourceYField.setValue(Integer.toString(this.zoneSourceY));
this.zoneSourceZField.setValue(Integer.toString(this.zoneSourceZ));
this.zoneUseCustomCoords = true;
this.zoneUseCustomCoordsButton.setMessage(this.boolLabel("Смещение звука", true));
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);
this.children.add(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((Object) 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() {
this.localSettings.soundId = this.soundIdField.getValue().trim();
this.localSettings.volume = this.clampFloat(this.parseFloat(this.volumeField.getValue(), this.localSettings.volume), 0.0f, 8.0f);
this.localSettings.pitch = this.clampFloat(this.parseFloat(this.pitchField.getValue(), this.localSettings.pitch), 0.05f, 4.0f);
this.localSettings.radius = this.clampFloat(this.parseFloat(this.radiusField.getValue(), this.localSettings.radius), 0.1f, 512.0f);
this.localSettings.priority = this.clampInt(this.parseInt(this.priorityField.getValue(), this.localSettings.priority), 0, 100);
this.localSettings.reverbStrength = this.clampFloat(this.parseFloat(this.reverbStrengthField.getValue(), this.localSettings.reverbStrength), 0.0f, 1.0f);
this.localSettings.occlusionStrength = this.clampFloat(this.parseFloat(this.occlusionStrengthField.getValue(), this.localSettings.occlusionStrength), 0.0f, 1.0f);
this.localSettings.fadeInTicks = this.clampInt(this.parseInt(this.fadeInField.getValue(), this.localSettings.fadeInTicks), 0, 36000);
this.localSettings.fadeOutTicks = this.clampInt(this.parseInt(this.fadeOutField.getValue(), this.localSettings.fadeOutTicks), 0, 36000);
this.localSettings.startDelayTicks = this.clampInt(this.parseInt(this.startDelayField.getValue(), this.localSettings.startDelayTicks), 0, 36000);
this.localSettings.repeatDelayTicks = this.clampInt(this.parseInt(this.repeatDelayField.getValue(), this.localSettings.repeatDelayTicks), 0, 36000);
this.zoneRadius = this.clampFloat(this.parseFloat(this.zoneRadiusField.getValue(), this.zoneRadius), 1.0f, 256.0f);
this.zoneBoxX = this.clampFloat(this.parseFloat(this.zoneBoxXField.getValue(), this.zoneBoxX), 0.5f, 128.0f);
this.zoneBoxY = this.clampFloat(this.parseFloat(this.zoneBoxYField.getValue(), this.zoneBoxY), 0.5f, 128.0f);
this.zoneBoxZ = this.clampFloat(this.parseFloat(this.zoneBoxZField.getValue(), this.zoneBoxZ), 0.5f, 128.0f);
this.zoneSourceX = this.parseInt(this.zoneSourceXField.getValue(), this.zoneSourceX);
this.zoneSourceY = this.parseInt(this.zoneSourceYField.getValue(), this.zoneSourceY);
this.zoneSourceZ = this.parseInt(this.zoneSourceZField.getValue(), this.zoneSourceZ);
}
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.shapeRu(this.zoneShape)));
this.zoneAutoStartButton.setMessage(this.boolLabel("Авто-вход", this.zoneAutoStartOnEnter));
this.zoneStopOnExitButton.setMessage(this.boolLabel("Стоп-выход", this.zoneStopOnExit));
this.zonePolicyButton.setMessage(new StringTextComponent("Приоритет: " + this.policyRu(this.zonePolicy)));
this.zoneUseCustomCoordsButton.setMessage(this.boolLabel("Смещение звука", this.zoneUseCustomCoords));
this.scenarioTargetButton.setMessage(new StringTextComponent(this.editingZoneScenario ? "Шаги: Зона" : "Шаги: Источник"));
this.scenarioLoopButton.setMessage(this.boolLabel("Цикл очереди", this.activeScenarioLoop()));
}
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;
}
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);
return true;
}
}
for (TextFieldWidget field : this.allFields()) {
if (!field.visible || !field.keyPressed(keyCode, scanCode, modifiers)) continue;
if (enter && this.isLiveNumericField(field)) {
this.sendLiveApply("enter_numeric");
}
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
public boolean charTyped(char codePoint, int modifiers) {
for (TextFieldWidget field : this.allFields()) {
if (!field.visible || !field.charTyped(codePoint, modifiers)) continue;
return true;
}
return super.charTyped(codePoint, modifiers);
}
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (this.activeTab == Tab.BASIC) {
int listX = this.leftPos + 10;
int listY = this.topPos + 152;
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 + 242) && 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 + 124;
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 + 242) && 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);
}
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() - 8);
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();
}
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, 0xFF30363D);
SoundPlayerScreen.fill(matrixStack, x1, y2 - 1, x2, y2, 0xFF30363D);
SoundPlayerScreen.fill(matrixStack, x1, y1, x1 + 1, y2, 0xFF30363D);
SoundPlayerScreen.fill(matrixStack, x2 - 1, y1, x2, y2, 0xFF30363D);
// Header separator
SoundPlayerScreen.fill(matrixStack, x1 + 6, y1 + 26, x2 - 6, y1 + 27, 0xFF30363D);
// Content area background
SoundPlayerScreen.fill(matrixStack, x1 + 6, y1 + 28, x2 - 6, y2 - 50, COLOR_BG_INNER);
// Active tab underline indicator
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.getHeight(), btn.y + btn.getHeight(), COLOR_ACCENT);
}
}
// Status bar area
SoundPlayerScreen.fill(matrixStack, x1 + 6, y2 - 48, x2 - 6, y2 - 28, COLOR_BG_BOX);
SoundPlayerScreen.fill(matrixStack, x1 + 6, y2 - 49, x2 - 6, y2 - 48, 0xFF30363D);
}
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 + 30), 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 + 65), (float) (y + 63), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Радиус (б)"), (float) (x + 132), (float) (y + 63), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Приоритет"), (float) (x + 192), (float) (y + 63), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Поиск в списке:"), (float) x, (float) (y + 120), COLOR_TEXT_LABEL);
this.drawSoundSuggestions(matrixStack, mouseX, mouseY);
break;
case EFFECTS:
this.font.draw(matrixStack, new StringTextComponent("Аудио эффекты и пространственный звук"), (float) x, (float) (y + 30), COLOR_TEXT_TITLE);
this.font.draw(matrixStack, new StringTextComponent("Сила (0.0 - 1.0):"), (float) (x + 128), (float) (y + 73), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Сила (0.0 - 1.0):"), (float) (x + 128), (float) (y + 99), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Fade In (появление, тиков):"), (float) x, (float) (y + 148), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Fade Out (затухание, тиков):"), (float) (x + 126), (float) (y + 148), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("⚡ Эффекты обновляются сразу во время игры"), (float) x, (float) (y + 188), COLOR_TEXT_MUTED);
break;
case TRIGGER:
this.font.draw(matrixStack, new StringTextComponent("Условия запуска звука"), (float) x, (float) (y + 30), COLOR_TEXT_TITLE);
this.font.draw(matrixStack, new StringTextComponent("Задержка старта (тиков, 20т = 1 сек):"), (float) x, (float) (y + 66), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("Интервал повтора (тиков, 0 = без повтора):"), (float) (x + 126), (float) (y + 66), COLOR_TEXT_LABEL);
this.drawTriggerHelpBox(matrixStack, x, y + 142);
break;
case ZONE:
this.font.draw(matrixStack, new StringTextComponent("Настройка триггерной зоны"), (float) x, (float) (y + 30), 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:"), (float) x, (float) (y + 73), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Y:"), (float) (x + 64), (float) (y + 73), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Z:"), (float) (x + 124), (float) (y + 73), COLOR_TEXT_MUTED);
}
this.font.draw(matrixStack, new StringTextComponent("Координаты центра звука:"), (float) x, (float) (y + 144), COLOR_TEXT_LABEL);
this.font.draw(matrixStack, new StringTextComponent("X:"), (float) x, (float) (y + 151), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Y:"), (float) (x + 80), (float) (y + 151), COLOR_TEXT_MUTED);
this.font.draw(matrixStack, new StringTextComponent("Z:"), (float) (x + 160), (float) (y + 151), COLOR_TEXT_MUTED);
break;
case SCENARIO:
this.font.draw(matrixStack, new StringTextComponent("Очередь воспроизведения (Сценарий)"), (float) x, (float) (y + 30), COLOR_TEXT_TITLE);
this.drawScenarioList(matrixStack, mouseX, mouseY);
break;
case HELP:
this.drawHelp(matrixStack);
break;
}
// --- STATUS BAR (Above action buttons) ---
int statusY = y + this.imageHeight - 45;
int statusColor = this.colorForStatus(status);
String statusPrefix = status == ClientSoundCache.Status.AVAILABLE ? "● " : (status == ClientSoundCache.Status.TRANSFERRING ? "⏳ " : "✖ ");
String statusText = statusPrefix + this.statusRu(status);
this.font.draw(matrixStack, new StringTextComponent(statusText), (float) x, (float) statusY, statusColor);
ClientTransferManager.TransferProgress progress = ClientTransferManager.get().progressBySoundId(soundId);
if (progress != null) {
String prog = progress.percent + "% (" + (progress.receivedBytes / 1024) + "/" + (progress.expectedBytes / 1024) + " KB)";
this.font.draw(matrixStack, new StringTextComponent(prog), (float) (x + 80), (float) statusY, COLOR_YELLOW);
}
String blockState = playingNow ? "● Блок: Играет" : "○ Блок: Ожидание";
int blockStateColor = playingNow ? COLOR_GREEN : COLOR_TEXT_MUTED;
this.font.draw(matrixStack, new StringTextComponent(blockState), (float) (x + 150), (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 + 250, y + 42, 0x55000000);
this.font.draw(matrixStack, new StringTextComponent("Описание выбранного режима:"), (float) (x + 4), (float) (y + 4), 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 + 4), (float) (y + 18), COLOR_TEXT_LABEL);
}
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 + 152;
int listWidth = 244;
int listHeight = SOUND_LIST_ROWS * ROW_HEIGHT;
// Background box for list
SoundPlayerScreen.fill(matrixStack, x - 1, y - 1, x + listWidth + 1, y + listHeight + 1, COLOR_BG_BOX);
SoundPlayerScreen.fill(matrixStack, x - 1, y - 1, x + listWidth + 1, y, 0xFF30363D);
SoundPlayerScreen.fill(matrixStack, x - 1, y + listHeight, x + listWidth + 1, y + listHeight + 1, 0xFF30363D);
if (suggestions.isEmpty()) {
this.font.draw(matrixStack, new StringTextComponent("Звуки не найдены. Проверьте customsounds/"), (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, rowY, x + listWidth, rowY + ROW_HEIGHT, 0x44388BFD);
} else if (isHovered) {
SoundPlayerScreen.fill(matrixStack, x, rowY, x + listWidth, rowY + ROW_HEIGHT, 0x22FFFFFF);
}
String name = this.trimRow(e.soundId, 24);
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 + 170), (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);
}
this.font.draw(matrixStack, new StringTextComponent("💡 Двойной клик по звуку для предпрослушивания"), (float) x, (float) (y + listHeight + 4), COLOR_TEXT_MUTED);
}
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);
}
if (this.selectedSoundListIndex >= out.size()) {
this.selectedSoundListIndex = out.isEmpty() ? -1 : out.size() - 1;
}
this.soundListScroll = this.clampInt(this.soundListScroll, 0, Math.max(0, out.size() - SOUND_LIST_ROWS));
return out;
}
private void drawScenarioList(MatrixStack matrixStack, int mouseX, int mouseY) {
int x = this.leftPos + 10;
int y = this.topPos + 124;
int listWidth = 244;
int listHeight = 5 * 14;
SoundPlayerScreen.fill(matrixStack, x - 1, y - 1, x + listWidth + 1, y + listHeight + 1, COLOR_BG_BOX);
List<ScenarioQueue.Step> list = this.activeScenarioList();
int shown = Math.min(5, Math.max(0, list.size() - this.scenarioListScroll));
if (list.isEmpty()) {
this.font.draw(matrixStack, new StringTextComponent("Очередь пуста. Добавьте шаг выше."), (float) (x + 6), (float) (y + 6), COLOR_TEXT_MUTED);
}
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, ry, x + listWidth, ry + 14, 0x44388BFD);
} else if (isHovered) {
SoundPlayerScreen.fill(matrixStack, x, ry, x + listWidth, ry + 14, 0x22FFFFFF);
}
String row = (idx + 1) + ". " + s.soundId + " (пауза: " + (s.delayTicks / 20.0f) + "с / " + s.delayTicks + "т)";
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);
}
this.font.draw(matrixStack, new StringTextComponent("Всего шагов: " + list.size()), (float) (x + 160), (float) (this.topPos + 30), COLOR_TEXT_MUTED);
}
private void drawHelp(MatrixStack matrixStack) {
int x = this.leftPos + 12;
int y = this.topPos + 32;
List<String> lines = this.helpLines();
int shown = Math.min(8, 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 * 13), color);
}
if (lines.size() > 8) {
this.drawSimpleScrollbar(matrixStack, this.leftPos + 258, y, 104, this.helpScroll, lines.size(), 8);
}
}
private List<String> helpLines() {
ArrayList<String> lines = new ArrayList<>();
lines.add("Справка по Sound Player:");
lines.add("• Основа: выберите soundId, настройте громкость и радиус.");
lines.add("• ▶ Тест: мгновенное прослушивание выбранного звука.");
lines.add("• Эффекты: 3D звук, приглушение за стенами (окклюзия), эхо.");
lines.add("• Триггер: настройка запуска редстоуном, кликом или таймером.");
lines.add("• Зона: музыка включается при входе игрока в зону и выключается при выходе.");
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 + 4, y + h, 0x44000000);
int thumb = Math.max(8, (int) ((double) window / (double) total * (double) h));
int maxScroll = Math.max(1, total - window);
int thumbY = y + (int) ((double) scroll / (double) maxScroll * (double) Math.max(0, h - thumb));
SoundPlayerScreen.fill(ms, x, thumbY, x + 4, thumbY + thumb, COLOR_ACCENT);
}
private int colorForStatus(ClientSoundCache.Status status) {
switch (status) {
case AVAILABLE:
return COLOR_GREEN;
case TRANSFERRING:
return COLOR_YELLOW;
case ERROR:
case MISSING:
case UNSUPPORTED:
return COLOR_RED;
}
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.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 List<TextFieldWidget> allFields() {
ArrayList<TextFieldWidget> all = new ArrayList<>();
all.addAll(this.basicFields);
all.addAll(this.effectsFields);
all.addAll(this.triggerFields);
all.addAll(this.zoneFields);
all.addAll(this.scenarioFields);
return all;
}
private ActivationMode cycleActivation(ActivationMode current) {
ActivationMode[] all = ActivationMode.values();
return all[(current.ordinal() + 1) % all.length];
}
private RedstoneMode cycleRedstone(RedstoneMode current) {
return current == RedstoneMode.EDGE ? RedstoneMode.HOLD : RedstoneMode.EDGE;
}
private AttenuationMode cycleAttenuation(AttenuationMode current) {
AttenuationMode[] all = AttenuationMode.values();
return all[(current.ordinal() + 1) % all.length];
}
private String cycleCategory(String current) {
for (int i = 0; i < CATEGORIES.length; ++i) {
if (!CATEGORIES[i].equals(current)) continue;
return CATEGORIES[(i + 1) % CATEGORIES.length];
}
return CATEGORIES[0];
}
private ZoneIntersectionPolicy cyclePolicy(ZoneIntersectionPolicy current) {
ZoneIntersectionPolicy[] all = ZoneIntersectionPolicy.values();
return all[(current.ordinal() + 1) % all.length];
}
private ZoneShape parseZoneShape(String raw) {
try {
return ZoneShape.valueOf(raw);
} catch (Exception ignored) {
return ZoneShape.SPHERE;
}
}
private ZoneIntersectionPolicy parseZonePolicy(String raw) {
try {
return ZoneIntersectionPolicy.valueOf(raw);
} catch (Exception ignored) {
return ZoneIntersectionPolicy.HIGHEST_PRIORITY;
}
}
private StringTextComponent boolLabel(String key, boolean value) {
return new StringTextComponent(key + ": " + (value ? "ВКЛ" : "ВЫКЛ"));
}
private String floatText(float value, int digits) {
return String.format("%." + digits + "f", Float.valueOf(value)).replace(',', '.');
}
private float parseFloat(String raw, float fallback) {
try {
return Float.parseFloat(raw.trim().replace(',', '.'));
} catch (Exception ignored) {
return fallback;
}
}
private int parseInt(String raw, int fallback) {
try {
return Integer.parseInt(raw.trim());
} catch (Exception ignored) {
return fallback;
}
}
private float clampFloat(float value, float min, float max) {
return Math.max(min, Math.min(max, value));
}
private int clampInt(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
private String trimRow(String text, int maxChars) {
if (text.length() <= maxChars) {
return text;
}
return text.substring(0, Math.max(0, maxChars - 1)) + "…";
}
private String redstoneRu(RedstoneMode mode) {
return mode == RedstoneMode.HOLD ? "HOLD" : "EDGE";
}
private String categoryRu(String category) {
if ("music".equals(category)) return "музыка";
if ("ambient".equals(category)) return "фон";
if ("effect".equals(category)) return "эффект";
if ("voice".equals(category)) return "голос";
return category;
}
private String attenuationRu(AttenuationMode mode) {
switch (mode) {
case NONE: return "нет";
case LINEAR: return "линейное";
case EXPONENTIAL: return "экспоненц.";
case LOGARITHMIC: return "логарифм.";
}
return mode.name();
}
private String modeRu(ActivationMode mode) {
switch (mode) {
case CLICK: return "Клик";
case REDSTONE: return "Редстоун";
case AUTO: return "Авто";
case TIMER: return "Таймер";
}
return mode.name();
}
private String shapeRu(ZoneShape shape) {
return shape == ZoneShape.BOX ? "Куб" : "Сфера";
}
private String policyRu(ZoneIntersectionPolicy policy) {
switch (policy) {
case HIGHEST_PRIORITY: return "Приоритет";
case BLEND: return "Смешивание";
case QUEUE: return "Очередь";
}
return policy.name();
}
private String statusRu(ClientSoundCache.Status status) {
switch (status) {
case AVAILABLE: return "Доступен";
case TRANSFERRING: return "Загрузка…";
case ERROR: return "Ошибка";
case MISSING: return "Отсутствует";
case UNSUPPORTED: return "Не поддеж.";
}
return status.name();
}
private enum Tab {
BASIC,
EFFECTS,
TRIGGER,
ZONE,
SCENARIO,
HELP
}
}