diff options
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/waypoint')
7 files changed, 827 insertions, 1 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/AbstractWaypointsScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/AbstractWaypointsScreen.java new file mode 100644 index 00000000..da6f52c8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/AbstractWaypointsScreen.java @@ -0,0 +1,73 @@ +package de.hysky.skyblocker.skyblock.waypoint; + +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import de.hysky.skyblocker.utils.Location; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.waypoint.NamedWaypoint; +import de.hysky.skyblocker.utils.waypoint.WaypointCategory; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +import java.util.Arrays; + +public abstract class AbstractWaypointsScreen<T extends Screen> extends Screen { + protected final T parent; + protected final Multimap<String, WaypointCategory> waypoints; + protected String island; + protected WaypointsListWidget waypointsListWidget; + protected DropdownWidget<Location> islandWidget; + + public AbstractWaypointsScreen(Text title, T parent) { + this(title, parent, MultimapBuilder.hashKeys().arrayListValues().build()); + } + + public AbstractWaypointsScreen(Text title, T parent, Multimap<String, WaypointCategory> waypoints) { + this(title, parent, waypoints, Utils.getLocationRaw()); + } + + public AbstractWaypointsScreen(Text title, T parent, Multimap<String, WaypointCategory> waypoints, String island) { + super(title); + this.parent = parent; + this.waypoints = waypoints; + this.island = island; + } + + @Override + protected void init() { + super.init(); + waypointsListWidget = addDrawableChild(new WaypointsListWidget(client, this, width, height - 96, 32, 24)); + islandWidget = addDrawableChild(new DropdownWidget<>(client, width - 160, 8, 150, height - 8, Arrays.asList(Location.values()), this::islandChanged, Location.from(island))); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (islandWidget.mouseClicked(mouseX, mouseY, button)) { + return true; + } + boolean mouseClicked = super.mouseClicked(mouseX, mouseY, button); + updateButtons(); + return mouseClicked; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (islandWidget.isMouseOver(mouseX, mouseY) && islandWidget.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) { + return true; + } + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + protected void islandChanged(Location location) { + island = location.id(); + waypointsListWidget.setIsland(island); + } + + protected abstract boolean isEnabled(NamedWaypoint waypoint); + + protected abstract void enabledChanged(NamedWaypoint waypoint, boolean enabled); + + protected void updateButtons() { + waypointsListWidget.updateButtons(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java new file mode 100644 index 00000000..724cb461 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java @@ -0,0 +1,141 @@ +package de.hysky.skyblocker.skyblock.waypoint; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.text.Style; +import net.minecraft.text.Text; + +import java.util.List; +import java.util.function.Consumer; + +public class DropdownWidget<T> extends ElementListWidget<DropdownWidget.Entry<T>> { + private static final MinecraftClient client = MinecraftClient.getInstance(); + public static final int ENTRY_HEIGHT = 15; + protected final List<T> entries; + protected final Consumer<T> selectCallback; + protected T prevSelected; + protected T selected; + protected boolean open; + + public DropdownWidget(MinecraftClient minecraftClient, int x, int y, int width, int maxHeight, List<T> entries, Consumer<T> selectCallback, T selected) { + super(minecraftClient, width, Math.min((entries.size() + 1) * ENTRY_HEIGHT + 8, maxHeight), y, ENTRY_HEIGHT); + setX(x); + this.entries = entries; + this.selectCallback = selectCallback; + this.selected = selected; + setRenderHeader(true, ENTRY_HEIGHT + 4); + for (T entry : entries) { + addEntry(new Entry<>(this, entry)); + } + } + + @Override + public int getRowLeft() { + return getX(); + } + + @Override + public int getRowWidth() { + return getWidth(); + } + + @Override + protected boolean clickedHeader(int x, int y) { + open = !open; + return true; + } + + @Override + protected void renderHeader(DrawContext context, int x, int y) { + context.drawTextWithShadow(client.textRenderer, selected.toString(), x + 4, y + 2, 0xFFFFFFFF); + } + + @Override + public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.getMatrices().push(); + context.getMatrices().translate(0, 0, 100); + + int y = getY() - (int) getScrollAmount(); + int height = getMaxPosition(); + + context.fill(getX(), y, getX() + width, y + headerHeight, 0xFF000000); + context.drawHorizontalLine(getX(), getX() + width, y, 0xFFFFFFFF); + context.drawHorizontalLine(getX(), getX() + width, y + headerHeight, 0xFFFFFFFF); + context.drawVerticalLine(getX(), y, y + headerHeight, 0xFFFFFFFF); + context.drawVerticalLine(getX() + width, y, y + headerHeight, 0xFFFFFFFF); + + if (open) { + context.fill(getX(), y + headerHeight + 1, getX() + width, y + height, 0xFF000000); + context.drawHorizontalLine(getX(), getX() + width, y + height, 0xFFFFFFFF); + context.drawVerticalLine(getX(), y + headerHeight, y + height, 0xFFFFFFFF); + context.drawVerticalLine(getX() + width, y + headerHeight, y + height, 0xFFFFFFFF); + + super.renderWidget(context, mouseX, mouseY, delta); + } else { + renderHeader(context, getRowLeft(), y + 4); + } + + context.getMatrices().pop(); + } + + @Override + protected void drawMenuListBackground(DrawContext context) {} + + @Override + protected void drawHeaderAndFooterSeparators(DrawContext context) {} + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + return open && super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + protected void select(T entry) { + selected = entry; + open = false; + setScrollAmount(0); + if (selected != prevSelected) { + selectCallback.accept(entry); + prevSelected = selected; + } + } + + static class Entry<T> extends ElementListWidget.Entry<Entry<T>> { + private final DropdownWidget<T> dropdownWidget; + private final T entry; + + public Entry(DropdownWidget<T> dropdownWidget, T entry) { + this.dropdownWidget = dropdownWidget; + this.entry = entry; + } + + @Override + public List<? extends Selectable> selectableChildren() { + return List.of(); + } + + @Override + public List<? extends Element> children() { + return List.of(); + } + + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + context.drawTextWithShadow(client.textRenderer, Text.literal(entry.toString()).fillStyle(Style.EMPTY.withUnderline(hovered)), x + 14, y + 2, 0xFFFFFFFF); + if (dropdownWidget.selected == this.entry) { + context.drawTextWithShadow(client.textRenderer, "✔", x + 4, y + 2, 0xFFFFFFFF); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && dropdownWidget.open) { + dropdownWidget.select(entry); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java index bbc9a655..f8930882 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java @@ -408,7 +408,7 @@ public class OrderedWaypoints { } @Override - protected float[] getColorComponents() { + public float[] getColorComponents() { if (this.colorComponents.length != 3) { return switch (this.relativeIndex) { case PREVIOUS -> RED; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java new file mode 100644 index 00000000..18096117 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java @@ -0,0 +1,132 @@ +package de.hysky.skyblocker.skyblock.waypoint; + +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Multimaps; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import de.hysky.skyblocker.utils.waypoint.WaypointCategory; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.toast.SystemToast; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public class Waypoints { + public static final Logger LOGGER = LoggerFactory.getLogger(Waypoints.class); + private static final Codec<List<WaypointCategory>> CODEC = WaypointCategory.CODEC.listOf(); + private static final Codec<List<WaypointCategory>> SKYTILS_CODEC = WaypointCategory.SKYTILS_CODEC.listOf(); + protected static final SystemToast.Type WAYPOINTS_TOAST_TYPE = new SystemToast.Type(); + + private static final Path waypointsFile = FabricLoader.getInstance().getConfigDir().resolve(SkyblockerMod.NAMESPACE).resolve("waypoints.json"); + protected static final Multimap<String, WaypointCategory> waypoints = MultimapBuilder.hashKeys().arrayListValues().build(); + + public static void init() { + loadWaypoints(); + ClientLifecycleEvents.CLIENT_STOPPING.register(Waypoints::saveWaypoints); + WorldRenderEvents.AFTER_TRANSLUCENT.register(Waypoints::render); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("waypoints").executes(Scheduler.queueOpenScreenCommand(() -> new WaypointsScreen(MinecraftClient.getInstance().currentScreen)))))); + } + + public static void loadWaypoints() { + waypoints.clear(); + try (BufferedReader reader = Files.newBufferedReader(waypointsFile)) { + List<WaypointCategory> waypoints = CODEC.parse(JsonOps.INSTANCE, SkyblockerMod.GSON.fromJson(reader, JsonArray.class)).resultOrPartial(LOGGER::error).orElseThrow(); + waypoints.forEach(waypointCategory -> Waypoints.waypoints.put(waypointCategory.island(), waypointCategory)); + } catch (Exception e) { + LOGGER.error("[Skyblocker Waypoints] Encountered exception while loading waypoints", e); + } + } + + public static List<WaypointCategory> fromSkytilsBase64(String base64, String defaultIsland) { + try { + if (base64.startsWith("<Skytils-Waypoint-Data>(V")) { + int version = Integer.parseInt(base64.substring(26, base64.indexOf(')'))); + if (version == 1) { + return fromSkytilsJson(new String(Base64.getDecoder().decode(base64.substring(base64.indexOf(':') + 1))), defaultIsland); + } else { + LOGGER.error("[Skyblocker Waypoints] Unknown Skytils waypoint data version: " + version); + } + } else return fromSkytilsJson(new String(Base64.getDecoder().decode(base64)), defaultIsland); + } catch (NumberFormatException e) { + LOGGER.error("[Skyblocker Waypoints] Encountered exception while parsing Skytils waypoint data version", e); + } catch (IllegalArgumentException e) { + LOGGER.error("[Skyblocker Waypoints] Encountered exception while decoding Skytils waypoint data", e); + } + return Collections.emptyList(); + } + + public static List<WaypointCategory> fromSkytilsJson(String waypointCategories, String defaultIsland) { + JsonArray waypointCategoriesJson; + try { + waypointCategoriesJson = SkyblockerMod.GSON.fromJson(waypointCategories, JsonObject.class).getAsJsonArray("categories"); + } catch (JsonSyntaxException e) { + JsonObject waypointCategoryJson = new JsonObject(); + waypointCategoryJson.addProperty("name", "New Category"); + waypointCategoryJson.addProperty("island", defaultIsland); + waypointCategoryJson.add("waypoints", SkyblockerMod.GSON.fromJson(waypointCategories, JsonArray.class)); + waypointCategoriesJson = new JsonArray(); + waypointCategoriesJson.add(waypointCategoryJson); + } + return SKYTILS_CODEC.parse(JsonOps.INSTANCE, waypointCategoriesJson).resultOrPartial(LOGGER::error).orElseThrow(); + } + + public static String toSkytilsBase64(List<WaypointCategory> waypointCategories) { + return Base64.getEncoder().encodeToString(toSkytilsJson(waypointCategories).getBytes()); + } + + public static String toSkytilsJson(List<WaypointCategory> waypointCategories) { + JsonObject waypointCategoriesJson = new JsonObject(); + waypointCategoriesJson.add("categories", SKYTILS_CODEC.encodeStart(JsonOps.INSTANCE, waypointCategories).resultOrPartial(LOGGER::error).orElseThrow()); + return SkyblockerMod.GSON_COMPACT.toJson(waypointCategoriesJson); + } + + public static void saveWaypoints(MinecraftClient client) { + try (BufferedWriter writer = Files.newBufferedWriter(waypointsFile)) { + JsonElement waypointsJson = CODEC.encodeStart(JsonOps.INSTANCE, List.copyOf(waypoints.values())).resultOrPartial(LOGGER::error).orElseThrow(); + SkyblockerMod.GSON.toJson(waypointsJson, writer); + LOGGER.info("[Skyblocker Waypoints] Saved waypoints"); + } catch (Exception e) { + LOGGER.error("[Skyblocker Waypoints] Encountered exception while saving waypoints", e); + } + } + + public static Multimap<String, WaypointCategory> waypointsDeepCopy() { + return waypoints.values().stream().map(WaypointCategory::deepCopy).collect(Multimaps.toMultimap(WaypointCategory::island, Function.identity(), () -> MultimapBuilder.hashKeys().arrayListValues().build())); + } + + public static void render(WorldRenderContext context) { + if (SkyblockerConfigManager.get().uiAndVisuals.waypoints.enableWaypoints) { + Collection<WaypointCategory> categories = waypoints.get(Utils.getLocationRaw()); + for (WaypointCategory category : categories) { + if (category != null) { + category.render(context); + } + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java new file mode 100644 index 00000000..045c694d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java @@ -0,0 +1,318 @@ +package de.hysky.skyblocker.skyblock.waypoint; + +import de.hysky.skyblocker.mixins.accessors.CheckboxWidgetAccessor; +import de.hysky.skyblocker.utils.waypoint.NamedWaypoint; +import de.hysky.skyblocker.utils.waypoint.WaypointCategory; +import it.unimi.dsi.fastutil.ints.Int2ObjectFunction; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +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.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.AbstractWaypointEntry> { + private final AbstractWaypointsScreen<?> screen; + private String island; + private List<WaypointCategory> waypoints; + + public WaypointsListWidget(MinecraftClient client, AbstractWaypointsScreen<?> screen, int width, int height, int y, int itemHeight) { + super(client, width, height, y, itemHeight); + this.screen = screen; + setIsland(screen.island); + } + + @Override + public int getRowWidth() { + return super.getRowWidth() + 100; + } + + @Override + protected int getScrollbarX() { + return super.getScrollbarX(); + } + + Optional<WaypointCategoryEntry> getCategory() { + if (getSelectedOrNull() instanceof WaypointCategoryEntry category) { + return Optional.of(category); + } else if (getSelectedOrNull() instanceof WaypointEntry waypointEntry) { + return Optional.of(waypointEntry.category); + } + return Optional.empty(); + } + + void setIsland(String island) { + this.island = island; + waypoints = (List<WaypointCategory>) screen.waypoints.get(island); + updateEntries(); + } + + void addWaypointCategoryAfterSelected() { + WaypointCategoryEntry categoryEntry = new WaypointCategoryEntry(); + Optional<WaypointCategoryEntry> selectedCategoryEntryOptional = getCategory(); + int index = waypoints.size(); + int entryIndex = children().size(); + if (selectedCategoryEntryOptional.isPresent()) { + WaypointCategoryEntry selectedCategoryEntry = selectedCategoryEntryOptional.get(); + index = waypoints.indexOf(selectedCategoryEntry.category) + 1; + entryIndex = children().indexOf(selectedCategoryEntry) + 1; + while (entryIndex < children().size() && !(children().get(entryIndex) instanceof WaypointCategoryEntry)) { + entryIndex++; + } + } + waypoints.add(index, categoryEntry.category); + children().add(entryIndex, categoryEntry); + } + + void updateEntries() { + clearEntries(); + for (WaypointCategory category : waypoints) { + WaypointCategoryEntry categoryEntry = new WaypointCategoryEntry(category); + addEntry(categoryEntry); + for (NamedWaypoint waypoint : category.waypoints()) { + addEntry(new WaypointEntry(categoryEntry, waypoint)); + } + } + } + + void updateButtons() { + for (Entry<AbstractWaypointEntry> entry : children()) { + if (entry instanceof WaypointCategoryEntry categoryEntry && categoryEntry.enabled.isChecked() != categoryEntry.category.waypoints().stream().allMatch(screen::isEnabled)) { + ((CheckboxWidgetAccessor) categoryEntry.enabled).setChecked(!categoryEntry.enabled.isChecked()); + } else if (entry instanceof WaypointEntry waypointEntry && waypointEntry.enabled.isChecked() != screen.isEnabled(waypointEntry.waypoint)) { + waypointEntry.enabled.onPress(); + } + } + } + + private BlockPos getDefaultPos() { + return client.crosshairTarget instanceof BlockHitResult blockHitResult && client.crosshairTarget.getType() == HitResult.Type.BLOCK ? blockHitResult.getBlockPos() : client.player != null ? client.player.getBlockPos() : BlockPos.ORIGIN; + } + + protected abstract static class AbstractWaypointEntry extends Entry<AbstractWaypointEntry> { + } + + protected class WaypointCategoryEntry extends AbstractWaypointEntry { + private WaypointCategory category; + private final List<ClickableWidget> children; + private final CheckboxWidget enabled; + private final TextFieldWidget nameField; + private final ButtonWidget buttonNewWaypoint; + private final ButtonWidget buttonDelete; + + public WaypointCategoryEntry() { + this(new WaypointCategory("New Category", island, new ArrayList<>())); + } + + public WaypointCategoryEntry(WaypointCategory category) { + this.category = category; + enabled = CheckboxWidget.builder(Text.literal(""), client.textRenderer).checked(!category.waypoints().isEmpty() && category.waypoints().stream().allMatch(screen::isEnabled)).callback((checkbox, checked) -> category.waypoints().forEach(waypoint -> screen.enabledChanged(waypoint, checked))).build(); + nameField = new TextFieldWidget(client.textRenderer, 70, 20, Text.literal("Name")); + nameField.setText(category.name()); + nameField.setChangedListener(this::updateName); + buttonNewWaypoint = ButtonWidget.builder(Text.translatable("skyblocker.waypoints.new"), buttonNewWaypoint -> { + WaypointEntry waypointEntry = new WaypointEntry(this); + int entryIndex; + if (getSelectedOrNull() instanceof WaypointEntry selectedWaypointEntry && selectedWaypointEntry.category == this) { + entryIndex = WaypointsListWidget.this.children().indexOf(selectedWaypointEntry) + 1; + } else { + entryIndex = WaypointsListWidget.this.children().indexOf(this) + 1; + while (entryIndex < WaypointsListWidget.this.children().size() && !(WaypointsListWidget.this.children().get(entryIndex) instanceof WaypointCategoryEntry)) { + entryIndex++; + } + } + category.waypoints().add(waypointEntry.waypoint); + WaypointsListWidget.this.children().add(entryIndex, waypointEntry); + }).width(72).build(); + buttonDelete = ButtonWidget.builder(Text.translatable("selectServer.deleteButton"), buttonDelete -> { + int entryIndex = WaypointsListWidget.this.children().indexOf(this) + 1; + while (entryIndex < WaypointsListWidget.this.children().size() && !(WaypointsListWidget.this.children().get(entryIndex) instanceof WaypointCategoryEntry)) { + WaypointsListWidget.this.children().remove(entryIndex); + } + WaypointsListWidget.this.children().remove(this); + waypoints.remove(category); + }).width(38).build(); + children = List.of(enabled, nameField, buttonNewWaypoint, buttonDelete); + } + + @Override + public List<? extends Selectable> selectableChildren() { + return children; + } + + @Override + public List<? extends Element> children() { + return children; + } + + private void updateName(String name) { + int index = waypoints.indexOf(category); + category = category.withName(name); + if (index >= 0) { + waypoints.set(index, category); + } + } + + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + enabled.setPosition(x, y + 1); + nameField.setPosition(x + 22, y); + buttonNewWaypoint.setPosition(x + entryWidth - 115, y); + buttonDelete.setPosition(x + entryWidth - 38, y); + for (ClickableWidget child : children) { + child.render(context, mouseX, mouseY, tickDelta); + } + } + } + + protected class WaypointEntry extends AbstractWaypointEntry { + private final WaypointCategoryEntry category; + private NamedWaypoint waypoint; + private final List<ClickableWidget> children; + private final CheckboxWidget enabled; + private final TextFieldWidget nameField; + private final TextFieldWidget xField; + private final TextFieldWidget yField; + private final TextFieldWidget zField; + private final TextFieldWidget colorField; + private final ButtonWidget buttonDelete; + + public WaypointEntry(WaypointCategoryEntry category) { + this(category, new NamedWaypoint(getDefaultPos(), "New Waypoint", new float[]{0f, 1f, 0f})); + } + + public WaypointEntry(WaypointCategoryEntry category, NamedWaypoint waypoint) { + this.category = category; + this.waypoint = waypoint; + enabled = CheckboxWidget.builder(Text.literal(""), client.textRenderer).checked(screen.isEnabled(waypoint)).callback((checkbox, checked) -> screen.enabledChanged(waypoint, checked)).build(); + nameField = new TextFieldWidget(client.textRenderer, 65, 20, Text.literal("Name")); + nameField.setText(waypoint.getName().getString()); + nameField.setChangedListener(this::updateName); + xField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("X")); + xField.setText(Integer.toString(waypoint.pos.getX())); + xField.setTextPredicate(this::checkInt); + xField.setChangedListener(this::updateX); + yField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("Y")); + yField.setText(Integer.toString(waypoint.pos.getY())); + yField.setTextPredicate(this::checkInt); + yField.setChangedListener(this::updateY); + zField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("Z")); + zField.setText(Integer.toString(waypoint.pos.getZ())); + zField.setTextPredicate(this::checkInt); + zField.setChangedListener(this::updateZ); + colorField = new TextFieldWidget(client.textRenderer, 56, 20, Text.literal("Color")); + colorField.setText(String.format("%02X%02X%02X%02X", (int) (waypoint.alpha * 255), (int) (waypoint.getColorComponents()[0] * 255), (int) (waypoint.getColorComponents()[1] * 255), (int) (waypoint.getColorComponents()[2] * 255))); + colorField.setChangedListener(this::updateColor); + buttonDelete = ButtonWidget.builder(Text.translatable("selectServer.deleteButton"), button -> { + category.category.waypoints().remove(waypoint); + WaypointsListWidget.this.children().remove(this); + }).width(38).build(); + children = List.of(enabled, nameField, xField, yField, zField, colorField, buttonDelete); + } + + @Override + public List<? extends Selectable> selectableChildren() { + return children; + } + + @Override + public List<? extends Element> children() { + return children; + } + + private void updateName(String name) { + if (waypoint.name.getString().equals(name)) return; + int index = category.category.waypoints().indexOf(waypoint); + waypoint = waypoint.withName(name); + if (index >= 0) { + category.category.waypoints().set(index, waypoint); + } + } + + private boolean checkInt(String string) { + try { + parseEmptiableInt(string); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + private void updateX(String xString) { + updateInt(xString, waypoint.pos.getX(), waypoint::withX); + } + + private void updateY(String yString) { + updateInt(yString, waypoint.pos.getY(), waypoint::withY); + } + + private void updateZ(String zString) { + updateInt(zString, waypoint.pos.getZ(), waypoint::withZ); + } + + private void updateInt(String newValueString, int currentValue, Int2ObjectFunction<NamedWaypoint> wither) { + try { + int index = category.category.waypoints().indexOf(waypoint); + int newValue = parseEmptiableInt(newValueString); + if (newValue == currentValue) return; + waypoint = wither.apply(newValue); + if (index >= 0) { + category.category.waypoints().set(index, waypoint); + } + } catch (NumberFormatException e) { + Waypoints.LOGGER.warn("[Skyblocker Waypoints] Failed to parse integer: {}", newValueString, e); + } + } + + private void updateColor(String colorString) { + try { + int index = category.category.waypoints().indexOf(waypoint); + int colorInt = parseEmptiableInt(colorString, 16); + float[] colorComponents = {((colorInt & 0x00FF0000) >> 16) / 255f, ((colorInt & 0x0000FF00) >> 8) / 255f, (colorInt & 0x000000FF) / 255f}; + float alpha = ((colorInt & 0xFF000000) >>> 24) / 255f; + if (Arrays.equals(waypoint.getColorComponents(), colorComponents) && waypoint.alpha == alpha) return; + waypoint = waypoint.withColor(colorComponents, alpha); + if (index >= 0) { + category.category.waypoints().set(index, waypoint); + } + } catch (NumberFormatException e) { + Waypoints.LOGGER.warn("[Skyblocker Waypoints] Failed to parse color: {}", colorString, e); + } + } + + private int parseEmptiableInt(String value) throws NumberFormatException { + return value.isEmpty() || value.equals("-") ? 0 : Integer.parseInt(value); + } + + @SuppressWarnings("SameParameterValue") + private int parseEmptiableInt(String value, int radix) throws NumberFormatException { + return value.isEmpty() || value.equals("-") ? 0 : Integer.parseInt(value, radix); + } + + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + context.drawTextWithShadow(client.textRenderer, "X:", width / 2 - 56, y + 6, 0xFFFFFF); + context.drawTextWithShadow(client.textRenderer, "Y:", width / 2 - 19, y + 6, 0xFFFFFF); + context.drawTextWithShadow(client.textRenderer, "Z:", width / 2 + 18, y + 6, 0xFFFFFF); + context.drawTextWithShadow(client.textRenderer, "#", x + entryWidth - 105, y + 6, 0xFFFFFF); + enabled.setPosition(x + 10, y + 1); + nameField.setPosition(x + 32, y); + xField.setPosition(width / 2 - 48, y); + yField.setPosition(width / 2 - 11, y); + zField.setPosition(width / 2 + 26, y); + colorField.setPosition(x + entryWidth - 99, y); + buttonDelete.setPosition(x + entryWidth - 38, y); + for (ClickableWidget child : children) { + child.render(context, mouseX, mouseY, tickDelta); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java new file mode 100644 index 00000000..23a24361 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java @@ -0,0 +1,76 @@ +package de.hysky.skyblocker.skyblock.waypoint; + +import de.hysky.skyblocker.utils.waypoint.NamedWaypoint; +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.ButtonWidget; +import net.minecraft.client.gui.widget.GridWidget; +import net.minecraft.client.gui.widget.SimplePositioningWidget; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; + +public class WaypointsScreen extends AbstractWaypointsScreen<Screen> { + private ButtonWidget buttonNew; + private ButtonWidget buttonDone; + + public WaypointsScreen(Screen parent) { + super(Text.translatable("skyblocker.waypoints.config"), parent, Waypoints.waypointsDeepCopy()); + } + + @Override + protected void init() { + super.init(); + GridWidget gridWidget = new GridWidget(); + gridWidget.getMainPositioner().marginX(5).marginY(2); + GridWidget.Adder adder = gridWidget.createAdder(2); + adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.share"), buttonShare -> client.setScreen(new WaypointsShareScreen(this, waypoints))).build()); + buttonNew = adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.newCategory"), buttonNew -> waypointsListWidget.addWaypointCategoryAfterSelected()).build()); + adder.add(ButtonWidget.builder(ScreenTexts.CANCEL, button -> close()).build()); + buttonDone = adder.add(ButtonWidget.builder(ScreenTexts.DONE, button -> { + saveWaypoints(); + close(); + }).build()); + gridWidget.refreshPositions(); + SimplePositioningWidget.setPos(gridWidget, 0, this.height - 64, this.width, 64); + gridWidget.forEachChild(this::addDrawableChild); + updateButtons(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, 0xFFFFFF); + } + + @Override + protected boolean isEnabled(NamedWaypoint waypoint) { + return waypoint.shouldRender(); + } + + @Override + protected void enabledChanged(NamedWaypoint waypoint, boolean enabled) { + waypoint.setShouldRender(enabled); + } + + private void saveWaypoints() { + Waypoints.waypoints.clear(); + Waypoints.waypoints.putAll(waypoints); + Waypoints.saveWaypoints(client); + } + + @Override + public void close() { + assert client != null; + if (!waypoints.equals(Waypoints.waypoints)) { + client.setScreen(new ConfirmScreen(confirmedAction -> client.setScreen(confirmedAction ? parent : this), + Text.translatable("text.skyblocker.quit_config"), + Text.translatable("text.skyblocker.quit_config_sure"), + Text.translatable("text.skyblocker.quit_discard"), + ScreenTexts.CANCEL + )); + } else { + client.setScreen(parent); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java new file mode 100644 index 00000000..aee21ec8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java @@ -0,0 +1,86 @@ +package de.hysky.skyblocker.skyblock.waypoint; + +import com.google.common.collect.Multimap; +import de.hysky.skyblocker.utils.waypoint.NamedWaypoint; +import de.hysky.skyblocker.utils.waypoint.WaypointCategory; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.GridWidget; +import net.minecraft.client.gui.widget.SimplePositioningWidget; +import net.minecraft.client.toast.SystemToast; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class WaypointsShareScreen extends AbstractWaypointsScreen<WaypointsScreen> { + private final Set<NamedWaypoint> selectedWaypoints = new HashSet<>(); + + protected WaypointsShareScreen(WaypointsScreen parent, Multimap<String, WaypointCategory> waypoints) { + super(Text.translatable("skyblocker.waypoints.shareWaypoints"), parent, waypoints, parent.island); + } + + @Override + protected void init() { + super.init(); + GridWidget gridWidget = new GridWidget(); + gridWidget.getMainPositioner().marginX(5).marginY(2); + GridWidget.Adder adder = gridWidget.createAdder(2); + adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.importWaypointsSkytils"), buttonImport -> { + try { + List<WaypointCategory> waypointCategories = Waypoints.fromSkytilsBase64(client.keyboard.getClipboard(), island); + for (WaypointCategory waypointCategory : waypointCategories) { + selectedWaypoints.addAll(waypointCategory.waypoints()); + waypoints.put(waypointCategory.island(), waypointCategory); + } + waypointsListWidget.updateEntries(); + SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.importSuccess"), Text.translatable("skyblocker.waypoints.importSuccessText", waypointCategories.stream().map(WaypointCategory::waypoints).mapToInt(List::size).sum(), waypointCategories.size())); + } catch (Exception e) { + Waypoints.LOGGER.error("[Skyblocker Waypoints] Encountered exception while parsing Skytils waypoint data", e); + SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.importError"), Text.translatable("skyblocker.waypoints.importErrorText")); + } + }).tooltip(Tooltip.of(Text.translatable("skyblocker.waypoints.importWaypointsSkytils.tooltip"))).build()); + adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.importWaypointsSnoopy"), buttonImport -> { + }).tooltip(Tooltip.of(Text.translatable("skyblocker.waypoints.importWaypointsSnoopy.tooltip"))).build()); + adder.add(ButtonWidget.builder(ScreenTexts.BACK, buttonBack -> close()).build()); + adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.exportWaypointsSkytils"), buttonExport -> { + try { + List<WaypointCategory> waypointCategories = waypoints.values().stream().filter(waypointCategory -> waypointCategory.island().equals(island)).map(WaypointCategory.filter(selectedWaypoints::contains)).filter(waypointCategory -> !waypointCategory.waypoints().isEmpty()).toList(); + client.keyboard.setClipboard(Waypoints.toSkytilsBase64(waypointCategories)); + SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.exportSuccess"), Text.translatable("skyblocker.waypoints.exportSuccessText", waypointCategories.stream().map(WaypointCategory::waypoints).mapToInt(List::size).sum(), waypointCategories.size())); + } catch (Exception e) { + Waypoints.LOGGER.error("[Skyblocker Waypoints] Encountered exception while serializing Skytils waypoint data", e); + SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.exportError"), Text.translatable("skyblocker.waypoints.exportErrorText")); + } + }).tooltip(Tooltip.of(Text.translatable("skyblocker.waypoints.exportWaypointsSkytils.tooltip"))).build()); + gridWidget.refreshPositions(); + SimplePositioningWidget.setPos(gridWidget, 0, this.height - 64, this.width, 64); + gridWidget.forEachChild(this::addDrawableChild); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, 0xFFFFFF); + } + + @Override + protected boolean isEnabled(NamedWaypoint waypoint) { + return selectedWaypoints.contains(waypoint); + } + + @Override + protected void enabledChanged(NamedWaypoint waypoint, boolean enabled) { + if (enabled) selectedWaypoints.add(waypoint); + else selectedWaypoints.remove(waypoint); + } + + @SuppressWarnings("DataFlowIssue") + @Override + public void close() { + client.setScreen(parent); + } +} |