1572 lines
82 KiB
Java
1572 lines
82 KiB
Java
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
|
||
}
|
||
}
|