aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de
diff options
context:
space:
mode:
authorWanja <105125277+Manchick0@users.noreply.github.com>2024-12-30 02:20:17 +0100
committerGitHub <noreply@github.com>2024-12-30 09:20:17 +0800
commit19a1df637df912fc2a99170e7b74abed6338ac5a (patch)
tree949a61b3dbf04f66572e22e8096e5301c424c5fe /src/main/java/de
parent145ba0948b03f10b6720fc87e0eecd7c8c5538a4 (diff)
downloadSkyblocker-19a1df637df912fc2a99170e7b74abed6338ac5a.tar.gz
Skyblocker-19a1df637df912fc2a99170e7b74abed6338ac5a.tar.bz2
Skyblocker-19a1df637df912fc2a99170e7b74abed6338ac5a.zip
Add Speed Presets for configurable speed settings (#1111)
* Add Speed Presets for configurable speed settings Introduced a Speed Presets system with customizable presets for the rancher boots. Added a GUI to manage presets and integrated support for the `setmaxspeed` command, and the sign-editor to use these presets. Besides all of that, refactored item protection rendering logic to show it everywhere, where applicable, and render it in front of the item, as well as updated the texture being used. * Reverted the proposed changes to the protected item texture I apologize for the changes, since I didn't know that the texture was applied elsewhere. This commit reverts all the changes to the size of the texture and the way it is rendered, to hopefully prevent any issues. * Apply suggestions from code review Co-authored-by: Kevin <92656833+kevinthegreat1@users.noreply.github.com> * Refactor modification checks for SpeedPreset entries. Removed the `hasBeenModified` flag, and replaced it with direct comparisons of initial and current values. * Clean up pointless changes * Refactor SpeedPresets to use Object2IntMap. Replaced LinkedHashMap with Object2IntMap for efficiency and consistency in managing speed presets. Adjusted related methods and logic to accommodate the new data structure. Updated SpeedPresetListWidget to handle changes and comparisons using the revised map implementation. * Refactor SpeedPresets --------- Co-authored-by: Kevin <92656833+kevinthegreat1@users.noreply.github.com>
Diffstat (limited to 'src/main/java/de')
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java18
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java9
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/CommandTreeS2CPacketMixin.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java11
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java59
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetListWidget.java180
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresets.java146
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetsScreen.java75
8 files changed, 476 insertions, 24 deletions
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
index b041a9fe..944e9d2e 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
@@ -8,6 +8,7 @@ import de.hysky.skyblocker.config.configs.GeneralConfig;
import de.hysky.skyblocker.skyblock.item.slottext.SlotTextMode;
import de.hysky.skyblocker.skyblock.item.tooltip.adders.CraftPriceTooltip;
import de.hysky.skyblocker.skyblock.shortcut.ShortcutsConfigScreen;
+import de.hysky.skyblocker.skyblock.speedPreset.SpeedPresetsScreen;
import dev.isxander.yacl3.api.*;
import dev.isxander.yacl3.api.controller.FloatSliderControllerBuilder;
import net.minecraft.client.MinecraftClient;
@@ -58,6 +59,23 @@ public class GeneralCategory {
.controller(ConfigUtils::createBooleanController)
.build())
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("skyblocker.config.general.speedPresets"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.general.speedPresets.enableSpeedPresets"))
+ .binding(defaults.general.speedPresets.enableSpeedPresets,
+ () -> config.general.speedPresets.enableSpeedPresets,
+ newValue -> config.general.speedPresets.enableSpeedPresets = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(ButtonOption.createBuilder()
+ .name(Text.translatable("skyblocker.config.general.speedPresets.config"))
+ .text(Text.translatable("text.skyblocker.open"))
+ .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new SpeedPresetsScreen(screen)))
+ .build())
+ .build())
+
//Shortcuts
.group(OptionGroup.createBuilder()
.name(Text.translatable("skyblocker.config.general.shortcuts"))
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
index 912dd769..bb66625a 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
@@ -22,6 +22,9 @@ public class GeneralConfig {
@SerialEntry
public boolean acceptReparty = true;
+ @SerialEntry
+ public SpeedPresets speedPresets = new SpeedPresets();
+
@SerialEntry
public Shortcuts shortcuts = new Shortcuts();
@@ -68,6 +71,12 @@ public class GeneralConfig {
@SerialEntry
public Object2ObjectOpenHashMap<String, CustomArmorAnimatedDyes.AnimatedDye> customAnimatedDyes = new Object2ObjectOpenHashMap<>();
+ public static class SpeedPresets {
+
+ @SerialEntry
+ public boolean enableSpeedPresets = true;
+ }
+
public static class Shortcuts {
@SerialEntry
public boolean enableShortcuts = true;
diff --git a/src/main/java/de/hysky/skyblocker/mixins/CommandTreeS2CPacketMixin.java b/src/main/java/de/hysky/skyblocker/mixins/CommandTreeS2CPacketMixin.java
index b96d5a5f..83605ca9 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/CommandTreeS2CPacketMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/CommandTreeS2CPacketMixin.java
@@ -7,6 +7,7 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import de.hysky.skyblocker.skyblock.SackItemAutocomplete;
import de.hysky.skyblocker.skyblock.ViewstashAutocomplete;
import de.hysky.skyblocker.skyblock.WarpAutocomplete;
+import de.hysky.skyblocker.skyblock.speedPreset.SpeedPresets;
import de.hysky.skyblocker.utils.Utils;
import net.minecraft.command.CommandSource;
import org.spongepowered.asm.mixin.Mixin;
@@ -18,6 +19,7 @@ public class CommandTreeS2CPacketMixin {
public CommandNode<? extends CommandSource> modifyCommandSuggestions(CommandNode<CommandSource> original) {
if (Utils.isOnHypixel() && original instanceof LiteralCommandNode<?> literalCommandNode) {
return switch (literalCommandNode.getLiteral()) {
+ case String s when s.equals("setmaxspeed") -> SpeedPresets.getCommandNode();
case String s when s.equals("warp") && WarpAutocomplete.commandNode != null -> WarpAutocomplete.commandNode;
case String s when s.equals("getfromsacks") && SackItemAutocomplete.longCommandNode != null -> SackItemAutocomplete.longCommandNode;
case String s when s.equals("gfs") && SackItemAutocomplete.shortCommandNode != null -> SackItemAutocomplete.shortCommandNode;
diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
index c833d06c..d092de6e 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
@@ -2,9 +2,8 @@ package de.hysky.skyblocker.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
-import com.mojang.blaze3d.systems.RenderSystem;
-import de.hysky.skyblocker.SkyblockerMod;
+import com.mojang.blaze3d.systems.RenderSystem;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.InventorySearch;
@@ -36,7 +35,6 @@ import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.slot.Slot;
import net.minecraft.screen.slot.SlotActionType;
import net.minecraft.text.Text;
-import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
@@ -64,9 +62,6 @@ public abstract class HandledScreenMixin<T extends ScreenHandler> extends Screen
private static final int OUT_OF_BOUNDS_SLOT = -999;
@Unique
- private static final Identifier ITEM_PROTECTION = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
-
- @Unique
private static final Set<String> FILLER_ITEMS = Set.of(
" ", // Empty menu item
"Locked Page",
@@ -335,10 +330,10 @@ public abstract class HandledScreenMixin<T extends ScreenHandler> extends Screen
private void skyblocker$drawOnItem(DrawContext context, Slot slot, CallbackInfo ci) {
if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds)
ItemRarityBackgrounds.tryDraw(slot.getStack(), context, slot.x, slot.y);
- // Item protection
+ // Item Protection
if (ItemProtection.isItemProtected(slot.getStack())) {
RenderSystem.enableBlend();
- context.drawTexture(RenderLayer::getGuiTextured, ITEM_PROTECTION, slot.x, slot.y, 0, 0, 16, 16, 16, 16);
+ context.drawTexture(RenderLayer::getGuiTextured, ItemProtection.ITEM_PROTECTION_TEX, slot.x, slot.y, 0, 0, 16, 16, 16, 16);
RenderSystem.disableBlend();
}
// Search
diff --git a/src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java
index 04342f37..14769a76 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java
@@ -3,9 +3,13 @@ package de.hysky.skyblocker.mixins;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.calculators.SignCalculator;
+import de.hysky.skyblocker.skyblock.speedPreset.SpeedPresets;
import de.hysky.skyblocker.utils.Utils;
import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.AbstractSignEditScreen;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -15,32 +19,55 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.llamalad7.mixinextras.sugar.Local;
-import java.util.Objects;
-
@Mixin(AbstractSignEditScreen.class)
-public abstract class SignEditScreenMixin {
+public abstract class SignEditScreenMixin extends Screen {
+
@Shadow
@Final
private String[] messages;
- @Inject(method = "render", at = @At("HEAD"))
+ protected SignEditScreenMixin(Text title) {
+ super(title);
+ }
+
+ @Inject(method = "render", at = @At("HEAD"))
private void skyblocker$render(CallbackInfo ci, @Local(argsOnly = true) DrawContext context) {
- //if the sign is being used to enter number send it to the sign calculator
- if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.inputCalculator.enabled && Objects.equals(messages[1], "^^^^^^^^^^^^^^^")) {
- SignCalculator.renderCalculator(context, messages[0], context.getScaledWindowWidth() / 2, 55);
- }
+ if (Utils.isOnSkyblock()) {
+ var config = SkyblockerConfigManager.get();
+ if (messages[1].equals("^^^^^^") && config.general.speedPresets.enableSpeedPresets) {
+ var presets = SpeedPresets.getInstance();
+ if (presets.hasPreset(messages[0])) {
+ context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(String.format("%s ยป %d", messages[0], presets.getPreset(messages[0]))).formatted(Formatting.GREEN),
+ context.getScaledWindowWidth() / 2, 55, 0xFFFFFFFF);
+ }
+ }
+ //if the sign is being used to enter number send it to the sign calculator
+ if (messages[1].equals("^^^^^^^^^^^^^^^") && config.uiAndVisuals.inputCalculator.enabled) {
+ SignCalculator.renderCalculator(context, messages[0], context.getScaledWindowWidth() / 2, 55);
+ }
+ }
}
@Inject(method = "finishEditing", at = @At("HEAD"))
private void skyblocker$finishEditing(CallbackInfo ci) {
- //if the sign is being used to enter number get number from calculator for if maths has been done
- if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.inputCalculator.enabled && Objects.equals(messages[1], "^^^^^^^^^^^^^^^")) {
- boolean isPrice = messages[2].contains("price");
- String value = SignCalculator.getNewValue(isPrice);
- if (value.length() >= 15) {
- value = value.substring(0, 15);
- }
- messages[0] = value;
+ var config = SkyblockerConfigManager.get();
+ if (Utils.isOnSkyblock()) {
+ //if the sign is being used to enter the speed cap, retrieve the value from speed presets.
+ if (messages[1].equals("^^^^^^") && config.general.speedPresets.enableSpeedPresets) {
+ var presets = SpeedPresets.getInstance();
+ if (presets.hasPreset(messages[0])) {
+ messages[0] = String.valueOf(presets.getPreset(messages[0]));
+ }
+ }
+ //if the sign is being used to enter number get number from calculator for if maths has been done
+ if (messages[1].equals("^^^^^^^^^^^^^^^") && config.uiAndVisuals.inputCalculator.enabled) {
+ boolean isPrice = messages[2].contains("price");
+ String value = SignCalculator.getNewValue(isPrice);
+ if (value.length() >= 15) {
+ value = value.substring(0, 15);
+ }
+ messages[0] = value;
+ }
}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetListWidget.java
new file mode 100644
index 00000000..bfe22a6c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetListWidget.java
@@ -0,0 +1,180 @@
+package de.hysky.skyblocker.skyblock.speedPreset;
+
+import it.unimi.dsi.fastutil.objects.ObjectIntPair;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Drawable;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.Selectable;
+import net.minecraft.client.gui.widget.*;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class SpeedPresetListWidget extends ElementListWidget<SpeedPresetListWidget.AbstractEntry> {
+
+ private static final Pattern NUMBER = Pattern.compile("^-?\\d+(\\.\\d+)?$");
+ // Alphanumeric sequence that doesn't start with a number.
+ private static final Pattern TITLE = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]*$");
+
+ public SpeedPresetListWidget(int width, int height, int y) {
+ super(MinecraftClient.getInstance(), width, height, y, 25);
+ var presets = SpeedPresets.getInstance();
+ addEntry(new TitleEntry());
+ if (presets.getPresetCount() > 0)
+ presets.forEach((title, speed) ->
+ this.addEntry(new SpeedPresetEntry(title, String.valueOf(speed))));
+ else
+ this.addEntry(new SpeedPresetEntry("", ""));
+ }
+
+ @Override
+ public int getRowWidth() {
+ return super.getRowWidth() + 104;
+ }
+
+ public boolean hasBeenChanged() {
+ var presets = SpeedPresets.getInstance();
+ // If there are fewer children than presets, some were removed, and all further checks are pointless
+ if (children().size() < presets.getPresetCount()) return true;
+ var childrenMap = this.children().stream()
+ .filter(SpeedPresetEntry.class::isInstance)
+ .map(SpeedPresetEntry.class::cast)
+ .map(SpeedPresetEntry::getMapping)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(ObjectIntPair::key, ObjectIntPair::valueInt));
+ return !presets.arePresetsEqual(childrenMap);
+ }
+
+ public void updatePosition() {
+ children().forEach(AbstractEntry::updatePosition);
+ }
+
+ public void newEntry() {
+ var entry = new SpeedPresetEntry("", "");
+ this.addEntry(entry);
+ this.centerScrollOn(entry);
+ this.setSelected(entry);
+ this.setFocused(entry);
+ }
+
+ public void save() {
+ var presets = SpeedPresets.getInstance();
+ presets.clear();
+ children().stream().filter(SpeedPresetEntry.class::isInstance).map(SpeedPresetEntry.class::cast).forEach(SpeedPresetEntry::save);
+ presets.savePresets(); // Write down the changes.
+ }
+
+ public abstract static class AbstractEntry extends ElementListWidget.Entry<AbstractEntry> {
+
+ protected void updatePosition() {}
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
+ this.children().forEach(child -> {
+ if (child instanceof Widget widget)
+ widget.setY(y);
+ if (child instanceof Drawable drawable)
+ drawable.render(context, mouseX, mouseY, tickDelta);
+ });
+ }
+ }
+
+ public class TitleEntry extends AbstractEntry {
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
+ // The line height is 25, the height of a single character is always 9.
+ // 25 - 9 = 16, 16 / 2 = 8, therefore the Y-offset should be 8.
+ context.drawCenteredTextWithShadow(client.textRenderer, Text.translatable("skyblocker.config.general.speedPresets.config.title"), width / 2 - 50, y + 8, 0xFFFFFF);
+ context.drawCenteredTextWithShadow(client.textRenderer, Text.translatable("skyblocker.config.general.speedPresets.config.speed"), width / 2 + 50, y + 8, 0xFFFFFF);
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return List.of();
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of();
+ }
+ }
+
+ public class SpeedPresetEntry extends AbstractEntry {
+
+ protected final TextFieldWidget titleInput;
+ protected final TextFieldWidget speedInput;
+ protected final ButtonWidget removeButton;
+
+ public SpeedPresetEntry(String title, String speed) {
+ var client = SpeedPresetListWidget.this.client;
+
+ // All Xs and Ys are then set using the initPosition() method.
+ this.titleInput = new TextFieldWidget(client.textRenderer, 0, 0, 120, 20, Text.empty());
+ this.titleInput.setTextPredicate(str -> str.isEmpty() || TITLE.matcher(str).matches());
+ this.titleInput.setText(title);
+ this.titleInput.setMaxLength(16);
+ this.titleInput.setPlaceholder(Text.literal("newPreset").formatted(Formatting.DARK_GRAY));
+ this.speedInput = new TextFieldWidget(client.textRenderer, 0, 0, 50, 20, Text.empty());
+
+ this.speedInput.setTextPredicate(str -> str.isEmpty() || NUMBER.matcher(str).matches());
+ this.speedInput.setText(speed);
+ this.speedInput.setMaxLength(3);
+ this.speedInput.setPlaceholder(Text.literal("0").formatted(Formatting.DARK_GRAY));
+
+ this.removeButton = ButtonWidget.builder(Text.literal("-"),
+ (btn) -> SpeedPresetListWidget.this.removeEntry(this))
+ .dimensions(0, 0, 20, 20)
+ .build();
+
+ this.updatePosition();
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return List.of(titleInput, speedInput, removeButton);
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of(titleInput, speedInput, removeButton);
+ }
+
+ public void save() {
+ var mapping = getMapping();
+ if (mapping != null)
+ SpeedPresets.getInstance().setPreset(mapping.key(), mapping.valueInt());
+ }
+
+ protected boolean isEmpty() {
+ return titleInput.getText().isEmpty() && speedInput.getText().isEmpty();
+ }
+
+ @Override
+ protected void updatePosition() {
+ var grid = new GridWidget();
+ grid.setSpacing(2);
+ grid.add(titleInput, 0, 0, 1, 3);
+ grid.add(speedInput, 0, 3, 1, 2);
+ grid.add(removeButton, 0, 5, 1, 1);
+ grid.refreshPositions();
+ SimplePositioningWidget.setPos(grid, 0, 0, width, itemHeight, 0.5f, 0.5f);
+ }
+
+ @Nullable
+ protected ObjectIntPair<String> getMapping() {
+ if (isEmpty()) return null;
+ try {
+ return ObjectIntPair.of(titleInput.getText(), Integer.parseInt(speedInput.getText()));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresets.java b/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresets.java
new file mode 100644
index 00000000..b3af7c41
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresets.java
@@ -0,0 +1,146 @@
+package de.hysky.skyblocker.skyblock.speedPreset;
+
+import com.google.common.io.Files;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.tree.CommandNode;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.annotations.Init;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.fabricmc.fabric.api.client.message.v1.ClientSendMessageEvents;
+import net.minecraft.command.CommandSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.regex.Pattern;
+
+public class SpeedPresets {
+
+ private static final Pattern COMMAND_PATTERN = Pattern.compile("^setmaxspeed\\s([a-zA-Z][a-zA-Z0-9_]*)$");
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SpeedPresets.class);
+ private static final Codec<Map<String, Integer>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, Codec.INT);
+ private static final File PRESETS_FILE = new File(SkyblockerMod.CONFIG_DIR.toFile(), "speed_presets.json");
+
+ private static SpeedPresets instance;
+
+ private final Object2IntMap<String> presets;
+
+ private SpeedPresets() {
+ this.presets = new Object2IntOpenHashMap<>();
+ this.loadPresets();
+ }
+
+ public static SpeedPresets getInstance() {
+ return instance == null ? instance = new SpeedPresets() : instance;
+ }
+
+ public static CommandNode<FabricClientCommandSource> getCommandNode() {
+ return ClientCommandManager.literal("setmaxspeed")
+ .requires(source -> Utils.isOnSkyblock())
+ .then(ClientCommandManager.argument("preset", StringArgumentType.string())
+ .suggests((ctx, builder) -> {
+ if (SkyblockerConfigManager.get().general.speedPresets.enableSpeedPresets) {
+ return CommandSource.suggestMatching(getInstance().presets.keySet(), builder);
+ }
+ return builder.buildFuture();
+ })).build();
+ }
+
+ @Init
+ public static void init() {
+ ClientSendMessageEvents.MODIFY_COMMAND.register((command) -> {
+ var matcher = COMMAND_PATTERN.matcher(command);
+ if (matcher.matches() && SkyblockerConfigManager.get().general.speedPresets.enableSpeedPresets) {
+ var presets = getInstance();
+ var preset = matcher.group(1);
+ if (presets.presets.containsKey(preset)) {
+ return String.format("setmaxspeed %d", presets.getPreset(preset));
+ }
+ }
+ return command;
+ });
+ }
+
+ public void clear() {
+ this.presets.clear();
+ }
+
+ public boolean hasPreset(String name) {
+ return this.presets.containsKey(name);
+ }
+
+ public int getPreset(String name) {
+ return this.presets.getOrDefault(name, 0);
+ }
+
+ public void setPreset(String name, int value) {
+ this.presets.put(name, value);
+ savePresets();
+ }
+
+ public void forEach(BiConsumer<String, Integer> consumer) {
+ this.presets.forEach(consumer);
+ }
+
+ public boolean arePresetsEqual(Map<String, Integer> presets) {
+ return this.presets.equals(presets);
+ }
+
+ public int getPresetCount() {
+ return this.presets.size();
+ }
+
+ public void loadPresets() {
+ try (var reader = Files.newReader(PRESETS_FILE, StandardCharsets.UTF_8)) {
+ var element = JsonParser.parseReader(reader);
+ MAP_CODEC.parse(JsonOps.INSTANCE, element).resultOrPartial(LOGGER::error).ifPresent(this.presets::putAll);
+ } catch (FileNotFoundException e) {
+ LOGGER.warn("[Skyblocker Speed Presets] Couldn't find speed presets file, creating one automatically...");
+ this.loadDefaults();
+ this.savePresets();
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker Speed Presets] Couldn't load speed presets", e);
+ }
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public void savePresets() {
+ try {
+ if (!PRESETS_FILE.exists()) PRESETS_FILE.createNewFile();
+ try (var writer = Files.newWriter(PRESETS_FILE, StandardCharsets.UTF_8)) {
+ var element = MAP_CODEC.encodeStart(JsonOps.INSTANCE, this.presets).resultOrPartial(LOGGER::error)
+ .orElse(new JsonObject());
+ writer.write(SkyblockerMod.GSON.toJson(element) + "\n");
+ }
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker Speed Presets] Couldn't create speed presets file", e);
+ }
+ }
+
+ // According to: https://www.reddit.com/r/HypixelSkyblock/comments/14kkz07/speed_vs_farming_fortune/
+ public void loadDefaults() {
+ this.presets.clear();
+ this.presets.put("default", 100);
+ this.presets.put("crops", 93);
+ this.presets.put("cocoa", 155);
+ this.presets.put("mushroom", 233);
+ this.presets.put("cane", 327);
+ this.presets.put("squash", 327);
+ this.presets.put("cactus", 464);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetsScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetsScreen.java
new file mode 100644
index 00000000..2f7d462b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/speedPreset/SpeedPresetsScreen.java
@@ -0,0 +1,75 @@
+package de.hysky.skyblocker.skyblock.speedPreset;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ConfirmScreen;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.*;
+import net.minecraft.screen.ScreenTexts;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class SpeedPresetsScreen extends Screen {
+
+ protected final Screen parent;
+ protected SpeedPresetListWidget list;
+
+ public SpeedPresetsScreen(Screen parent) {
+ super(Text.translatable("skyblocker.config.general.speedPresets.config"));
+ this.parent = parent;
+ }
+
+ @Override
+ protected void init() {
+ if (this.list == null)
+ this.list = new SpeedPresetListWidget(0, 0, 24);
+ this.list.setDimensions(this.width, this.height - 24 - 32);
+ this.list.updatePosition();
+ this.addDrawableChild(this.list);
+
+ var grid = new GridWidget();
+ grid.setSpacing(4);
+ var doneButton = ButtonWidget.builder(ScreenTexts.DONE,
+ button -> {
+ this.list.save();
+ assert this.client != null;
+ this.client.setScreen(parent);
+ })
+ .width(Math.max(textRenderer.getWidth(ScreenTexts.DONE) + 8, 100))
+ .build();
+ grid.add(doneButton, 0, 0, 1, 2);
+ var plusButton = ButtonWidget.builder(Text.literal("+"),
+ button -> list.newEntry())
+ .width(20)
+ .build();
+ grid.add(plusButton, 0, 2, 1, 1);
+ grid.refreshPositions();
+ SimplePositioningWidget.setPos(grid, 0, this.height - 24, this.width, 24, 0.5f, 0.5f);
+ grid.forEachChild(this::addDrawableChild);
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.render(context, mouseX, mouseY, delta);
+ assert this.client != null;
+ var renderer = this.client.textRenderer;
+ context.drawCenteredTextWithShadow(renderer, this.title, this.width / 2,
+ 8, 0xFFFFFF);
+ }
+
+ @Override
+ public void close() {
+ assert this.client != null;
+ if (this.list.hasBeenChanged()) {
+ client.setScreen(new ConfirmScreen(confirmedAction -> {
+ if (confirmedAction) {
+ this.client.setScreen(parent);
+ } else {
+ this.client.setScreen(this);
+ }
+ }, Text.translatable("text.skyblocker.quit_config"), Text.translatable("text.skyblocker.quit_config_sure"), Text.translatable("text.skyblocker.quit_discard")
+ .formatted(Formatting.RED), ScreenTexts.CANCEL));
+ return;
+ }
+ this.client.setScreen(parent);
+ }
+}