diff options
| author | Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> | 2024-05-14 22:58:56 -0400 | 
|---|---|---|
| committer | Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> | 2024-05-24 19:51:46 -0400 | 
| commit | 7bf3958477c179185f9532cebffc36f218aa3bd8 (patch) | |
| tree | 375325e936948bf088de79c049fd26d91ccb9e24 | |
| parent | e9bd404a36ce3a69e62e97986d42e7f3635ab67d (diff) | |
| download | Skyblocker-7bf3958477c179185f9532cebffc36f218aa3bd8.tar.gz Skyblocker-7bf3958477c179185f9532cebffc36f218aa3bd8.tar.bz2 Skyblocker-7bf3958477c179185f9532cebffc36f218aa3bd8.zip | |
Port waypoints config screens
15 files changed, 747 insertions, 107 deletions
| diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/CheckboxWidgetAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/CheckboxWidgetAccessor.java new file mode 100644 index 00000000..5ec4a8e8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/CheckboxWidgetAccessor.java @@ -0,0 +1,11 @@ +package de.hysky.skyblocker.mixins.accessors; + +import net.minecraft.client.gui.widget.CheckboxWidget; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(CheckboxWidget.class) +public interface CheckboxWidgetAccessor { +    @Accessor +    void setChecked(boolean checked); +} 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..932bc144 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/AbstractWaypointsScreen.java @@ -0,0 +1,65 @@ +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, 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; +    } + +    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..157b0b15 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java @@ -0,0 +1,126 @@ +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, List<T> entries, Consumer<T> selectCallback, T selected) { +        super(minecraftClient, width, (entries.size() + 1) * ENTRY_HEIGHT + 8, 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); + +        context.fill(getX(), getY(), getX() + width, getY() + headerHeight, 0xFF000000); +        context.drawHorizontalLine(getX(), getX() + width, getY(), 0xFFFFFFFF); +        context.drawHorizontalLine(getX(), getX() + width, getY() + headerHeight, 0xFFFFFFFF); +        context.drawVerticalLine(getX(), getY(), getY() + headerHeight, 0xFFFFFFFF); +        context.drawVerticalLine(getX() + width, getY(), getY() + headerHeight, 0xFFFFFFFF); + +        if (open) { +            context.fill(getX(), getY() + headerHeight + 1, getX() + width, getY() + height, 0xFF000000); +            context.drawHorizontalLine(getX(), getX() + width, getY() + height, 0xFFFFFFFF); +            context.drawVerticalLine(getX(), getY() + headerHeight, getY() + height, 0xFFFFFFFF); +            context.drawVerticalLine(getX() + width, getY() + headerHeight, getY() + height, 0xFFFFFFFF); + +            super.renderWidget(context, mouseX, mouseY, delta); +        } else { +            renderHeader(context, getRowLeft(), getY() + 4 - (int) getScrollAmount()); +        } + +        context.getMatrices().pop(); +    } + +    protected void select(T entry) { +        selected = entry; +        open = false; +        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 index 8eeae2aa..18096117 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java @@ -2,19 +2,25 @@ 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; @@ -24,19 +30,26 @@ 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 { -    private static final Logger LOGGER = LoggerFactory.getLogger(Waypoints.class); +    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"); -    static final Multimap<String, WaypointCategory> waypoints = MultimapBuilder.hashKeys().arrayListValues().build(); +    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() { @@ -49,22 +62,47 @@ public class Waypoints {          }      } -    public static List<WaypointCategory> fromSkytilsBase64(String base64) { -        return fromSkytilsJson(new String(Base64.getDecoder().decode(base64))); +    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) { -        return SKYTILS_CODEC.parse(JsonOps.INSTANCE, SkyblockerMod.GSON.fromJson(waypointCategories, JsonObject.class).getAsJsonArray("categories")).resultOrPartial(LOGGER::error).orElseThrow(); +    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(Collection<WaypointCategory> waypointCategories) { +    public static String toSkytilsBase64(List<WaypointCategory> waypointCategories) {          return Base64.getEncoder().encodeToString(toSkytilsJson(waypointCategories).getBytes());      } -    public static String toSkytilsJson(Collection<WaypointCategory> waypointCategories) { +    public static String toSkytilsJson(List<WaypointCategory> waypointCategories) {          JsonObject waypointCategoriesJson = new JsonObject(); -        waypointCategoriesJson.add("categories", SKYTILS_CODEC.encodeStart(JsonOps.INSTANCE, List.copyOf(waypointCategories)).resultOrPartial(LOGGER::error).orElseThrow()); -        return SkyblockerMod.GSON.toJson(waypointCategoriesJson); +        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) { @@ -77,11 +115,17 @@ public class Waypoints {          }      } +    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) { -        Collection<WaypointCategory> categories = waypoints.get(Utils.getLocationRaw()); -        for (WaypointCategory category : categories) { -            if (category != null) { -                category.render(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 index 7a01a494..3e18d673 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java @@ -1,41 +1,31 @@  package de.hysky.skyblocker.skyblock.waypoint; -import de.hysky.skyblocker.debug.Debug; -import de.hysky.skyblocker.utils.Utils; +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.ButtonWidget; -import net.minecraft.client.gui.widget.ClickableWidget; -import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.gui.widget.*;  import net.minecraft.text.Text;  import net.minecraft.util.math.BlockPos;  import java.util.ArrayList; +import java.util.Arrays;  import java.util.List; -import java.util.Objects;  import java.util.Optional;  public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.AbstractWaypointEntry> { -    private final WaypointsScreen screen; -    private final String island; -    private final List<WaypointCategory> waypoints; +    private final AbstractWaypointsScreen<?> screen; +    private String island; +    private List<WaypointCategory> waypoints; -    public WaypointsListWidget(MinecraftClient client, WaypointsScreen screen, int width, int height, int y, int itemHeight) { +    public WaypointsListWidget(MinecraftClient client, AbstractWaypointsScreen<?> screen, int width, int height, int y, int itemHeight) {          super(client, width, height, y, itemHeight);          this.screen = screen; -        island = Utils.getLocationRaw(); -        waypoints = (List<WaypointCategory>) screen.waypoints.get(island); -        for (WaypointCategory category : waypoints) { -            WaypointCategoryEntry categoryEntry = new WaypointCategoryEntry(category); -            addEntry(categoryEntry); -            for (NamedWaypoint waypoint : category.waypoints()) { -                addEntry(new WaypointEntry(categoryEntry, waypoint)); -            } -        } +        setIsland(screen.island);      }      @Override @@ -44,8 +34,8 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A      }      @Override -    protected int getScrollbarPositionX() { -        return super.getScrollbarPositionX() + 50; +    protected int getScrollbarX() { +        return super.getScrollbarX();      }      Optional<WaypointCategoryEntry> getCategory() { @@ -57,6 +47,12 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A          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(); @@ -74,17 +70,35 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A          children().add(entryIndex, categoryEntry);      } -    @Override -    protected boolean isSelectedEntry(int index) { -        return Debug.debugEnabled() ? Objects.equals(getSelectedOrNull(), children().get(index)) : super.isSelectedEntry(index); +    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(); +            } +        }      } -    protected static abstract class AbstractWaypointEntry extends ElementListWidget.Entry<AbstractWaypointEntry> { +    protected abstract static class AbstractWaypointEntry extends Entry<AbstractWaypointEntry> {      }      protected class WaypointCategoryEntry extends AbstractWaypointEntry { -        private final WaypointCategory category; +        private WaypointCategory category;          private final List<ClickableWidget> children; +        private final CheckboxWidget enabled; +        private final TextFieldWidget nameField;          private final ButtonWidget buttonNewWaypoint;          private final ButtonWidget buttonDelete; @@ -94,6 +108,10 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A          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; @@ -107,7 +125,7 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A                  }                  category.waypoints().add(waypointEntry.waypoint);                  WaypointsListWidget.this.children().add(entryIndex, waypointEntry); -            }).width(75).build(); +            }).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)) { @@ -115,8 +133,8 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A                  }                  WaypointsListWidget.this.children().remove(this);                  waypoints.remove(category); -            }).width(50).build(); -            children = List.of(buttonNewWaypoint, buttonDelete); +            }).width(38).build(); +            children = List.of(enabled, nameField, buttonNewWaypoint, buttonDelete);          }          @Override @@ -129,20 +147,36 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A              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) { -            context.drawTextWithShadow(client.textRenderer, category.name(), width / 2 - 150, y + 5, 0xFFFFFF); -            buttonNewWaypoint.setPosition(x + entryWidth - 133, y); -            buttonDelete.setPosition(x + entryWidth - 54, y); -            buttonNewWaypoint.render(context, mouseX, mouseY, tickDelta); -            buttonDelete.render(context, mouseX, mouseY, 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 final NamedWaypoint waypoint; +        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) { @@ -152,11 +186,27 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A          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.setChangedListener(this::updateX); +            yField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("Y")); +            yField.setText(Integer.toString(waypoint.pos.getY())); +            yField.setChangedListener(this::updateY); +            zField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("Z")); +            zField.setText(Integer.toString(waypoint.pos.getZ())); +            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(50).build(); -            children = List.of(buttonDelete); +            }).width(38).build(); +            children = List.of(enabled, nameField, xField, yField, zField, colorField, buttonDelete);          }          @Override @@ -169,14 +219,73 @@ public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.A              return children;          } +        public 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); +            } +        } + +        public void updateX(String xString) { +            updateInt(xString, waypoint.pos.getX(), waypoint::withX); +        } + +        public void updateY(String yString) { +            updateInt(yString, waypoint.pos.getY(), waypoint::withY); +        } + +        public void updateZ(String zString) { +            updateInt(zString, waypoint.pos.getZ(), waypoint::withZ); +        } + +        public void updateInt(String newValueString, int currentValue, Int2ObjectFunction<NamedWaypoint> wither) { +            try { +                int index = category.category.waypoints().indexOf(waypoint); +                int newValue = Integer.parseInt(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); +            } +        } + +        public void updateColor(String colorString) { +            try { +                int index = category.category.waypoints().indexOf(waypoint); +                int colorInt = Integer.parseInt(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); +            } +        } +          @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, waypoint.getName(), width / 2 - 125, y + 5, 0xFFFFFF); -            context.drawTextWithShadow(client.textRenderer, waypoint.pos.toShortString(), width / 2 - 25, y + 5, 0xFFFFFF); -            float[] colorComponents = waypoint.getColorComponents(); -            context.drawTextWithShadow(client.textRenderer, String.format("#%02X%02X%02X", (int) (colorComponents[0] * 255), (int) (colorComponents[1] * 255), (int) (colorComponents[2] * 255)), width / 2 + 25, y + 5, 0xFFFFFF); -            buttonDelete.setPosition(x + entryWidth - 54, y); -            buttonDelete.render(context, mouseX, mouseY, 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 index 9f82f7a2..23a24361 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java @@ -1,9 +1,8 @@  package de.hysky.skyblocker.skyblock.waypoint; -import com.google.common.collect.Multimap; -import com.google.common.collect.MultimapBuilder; -import de.hysky.skyblocker.utils.waypoint.WaypointCategory; +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; @@ -11,31 +10,21 @@ import net.minecraft.client.gui.widget.SimplePositioningWidget;  import net.minecraft.screen.ScreenTexts;  import net.minecraft.text.Text; -public class WaypointsScreen extends Screen { -    private final Screen parent; -    final Multimap<String, WaypointCategory> waypoints = MultimapBuilder.hashKeys().arrayListValues().build(); -    private WaypointsListWidget waypointsListWidget; +public class WaypointsScreen extends AbstractWaypointsScreen<Screen> {      private ButtonWidget buttonNew;      private ButtonWidget buttonDone; -    protected WaypointsScreen() { -        this(null); -    } -      public WaypointsScreen(Screen parent) { -        super(Text.translatable("skyblocker.waypoints.config")); -        this.parent = parent; -        Waypoints.waypoints.forEach((island, category) -> waypoints.put(island, new WaypointCategory(category))); +        super(Text.translatable("skyblocker.waypoints.config"), parent, Waypoints.waypointsDeepCopy());      }      @Override      protected void init() {          super.init(); -        waypointsListWidget = addDrawableChild(new WaypointsListWidget(client, this, width, height - 96, 32, 24));          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 -> {}).build()); +        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 -> { @@ -54,17 +43,34 @@ public class WaypointsScreen extends Screen {          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);      } -    private void updateButtons() {} - -    @SuppressWarnings("DataFlowIssue")      @Override      public void close() { -        client.setScreen(parent); +        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); +    } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java index f959de78..2f02b51f 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java @@ -13,6 +13,7 @@ import net.minecraft.text.TextCodecs;  import net.minecraft.util.math.BlockPos;  import net.minecraft.util.math.Vec3d; +import java.util.Objects;  import java.util.function.Supplier;  public class NamedWaypoint extends Waypoint { @@ -23,6 +24,7 @@ public class NamedWaypoint extends Waypoint {                      colorComponentsList -> colorComponentsList.size() == 3 ? DataResult.success(Floats.toArray(colorComponentsList)) : DataResult.error(() -> "Expected 3 color components, got " + colorComponentsList.size() + " instead"),                      Floats::asList              ).fieldOf("colorComponents").forGetter(secretWaypoint -> secretWaypoint.colorComponents), +            Codec.FLOAT.fieldOf("alpha").forGetter(secretWaypoint -> secretWaypoint.alpha),              Codec.BOOL.fieldOf("shouldRender").forGetter(Waypoint::shouldRender)      ).apply(instance, NamedWaypoint::new));      public static final Codec<NamedWaypoint> SKYTILS_CODEC = RecordCodecBuilder.create(instance -> instance.group( @@ -30,46 +32,78 @@ public class NamedWaypoint extends Waypoint {              Codec.INT.fieldOf("y").forGetter(waypoint -> waypoint.pos.getY()),              Codec.INT.fieldOf("z").forGetter(waypoint -> waypoint.pos.getZ()),              Codec.either(Codec.STRING, Codec.INT).xmap(either -> either.map(str -> str, Object::toString), Either::left).fieldOf("name").forGetter(waypoint -> waypoint.name.getString()), -            Codec.INT.fieldOf("color").forGetter(waypoint -> (int) (waypoint.colorComponents[0] * 255) << 16 | (int) (waypoint.colorComponents[1] * 255) << 8 | (int) (waypoint.colorComponents[2] * 255)), +            Codec.INT.fieldOf("color").forGetter(waypoint -> (int) (waypoint.alpha * 255) << 24 | (int) (waypoint.colorComponents[0] * 255) << 16 | (int) (waypoint.colorComponents[1] * 255) << 8 | (int) (waypoint.colorComponents[2] * 255)),              Codec.BOOL.fieldOf("enabled").forGetter(Waypoint::shouldRender)      ).apply(instance, NamedWaypoint::fromSkytils)); -    protected final Text name; -    protected final Vec3d centerPos; +    public final Text name; +    public final Vec3d centerPos;      public NamedWaypoint(BlockPos pos, String name, float[] colorComponents) {          this(pos, name, colorComponents, true);      }      public NamedWaypoint(BlockPos pos, String name, float[] colorComponents, boolean shouldRender) { -        this(pos, Text.of(name), colorComponents, shouldRender); +        this(pos, name, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, shouldRender);      } -    public NamedWaypoint(BlockPos pos, Text name, float[] colorComponents, boolean shouldRender) { -        this(pos, name, () -> SkyblockerConfigManager.get().general.waypoints.waypointType, colorComponents, shouldRender); +    public NamedWaypoint(BlockPos pos, String name, float[] colorComponents, float alpha, boolean shouldRender) { +        this(pos, Text.of(name), colorComponents, alpha, shouldRender);      } -    public NamedWaypoint(BlockPos pos, String name, Supplier<Type> typeSupplier, float[] colorComponents, boolean shouldRender) { -        this(pos, Text.of(name), typeSupplier, colorComponents, shouldRender); +    public NamedWaypoint(BlockPos pos, Text name, float[] colorComponents, float alpha, boolean shouldRender) { +        this(pos, name, () -> SkyblockerConfigManager.get().uiAndVisuals.waypoints.waypointType, colorComponents, alpha, shouldRender);      }      public NamedWaypoint(BlockPos pos, Text name, Supplier<Type> typeSupplier, float[] colorComponents) { -        this(pos, name, typeSupplier, colorComponents, true); +        this(pos, name, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, true);      } -    public NamedWaypoint(BlockPos pos, Text name, Supplier<Type> typeSupplier, float[] colorComponents, boolean shouldRender) { -        super(pos, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, DEFAULT_LINE_WIDTH, true, shouldRender); +    public NamedWaypoint(BlockPos pos, Text name, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, boolean shouldRender) { +        super(pos, typeSupplier, colorComponents, alpha, DEFAULT_LINE_WIDTH, true, shouldRender);          this.name = name;          this.centerPos = pos.toCenterPos();      }      public static NamedWaypoint fromSkytils(int x, int y, int z, String name, int color, boolean enabled) { -        return new NamedWaypoint(new BlockPos(x, y, z), name, new float[]{((color & 0x00FF0000) >> 16) / 255f, ((color & 0x0000FF00) >> 8) / 255f, (color & 0x000000FF) / 255f}, enabled); +        float alpha = ((color & 0xFF000000) >>> 24) / 255f; +        if (alpha == 0) { +            alpha = DEFAULT_HIGHLIGHT_ALPHA; +        } +        return new NamedWaypoint(new BlockPos(x, y, z), name, new float[]{((color & 0x00FF0000) >> 16) / 255f, ((color & 0x0000FF00) >> 8) / 255f, (color & 0x000000FF) / 255f}, alpha, enabled); +    } + +    public NamedWaypoint copy() { +        return new NamedWaypoint(pos, name, typeSupplier, getColorComponents(), alpha, shouldRender()); +    } + +    @Override +    public NamedWaypoint withX(int x) { +        return new NamedWaypoint(new BlockPos(x, pos.getY(), pos.getZ()), name, typeSupplier, getColorComponents(), alpha, shouldRender()); +    } + +    @Override +    public NamedWaypoint withY(int y) { +        return new NamedWaypoint(pos.withY(y), name, typeSupplier, getColorComponents(), alpha, shouldRender()); +    } + +    @Override +    public NamedWaypoint withZ(int z) { +        return new NamedWaypoint(new BlockPos(pos.getX(), pos.getY(), z), name, typeSupplier, getColorComponents(), alpha, shouldRender()); +    } + +    @Override +    public NamedWaypoint withColor(float[] colorComponents, float alpha) { +        return new NamedWaypoint(pos, name, typeSupplier, colorComponents, alpha, shouldRender());      }      public Text getName() {          return name;      } +    public NamedWaypoint withName(String name) { +        return new NamedWaypoint(pos, Text.literal(name), typeSupplier, getColorComponents(), alpha, shouldRender()); +    } +      protected boolean shouldRenderName() {          return true;      } @@ -83,7 +117,12 @@ public class NamedWaypoint extends Waypoint {      }      @Override +    public int hashCode() { +        return Objects.hash(super.hashCode(), name); +    } + +    @Override      public boolean equals(Object obj) { -        return this == obj || obj instanceof NamedWaypoint waypoint && name.equals(waypoint.name); +        return this == obj || super.equals(obj) && obj instanceof NamedWaypoint waypoint && name.equals(waypoint.name);      }  } diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java index 75e2edcf..c991fb9c 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java @@ -3,10 +3,12 @@ package de.hysky.skyblocker.utils.waypoint;  import de.hysky.skyblocker.utils.render.RenderHelper;  import de.hysky.skyblocker.utils.render.Renderable;  import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.util.StringIdentifiable;  import net.minecraft.util.math.BlockPos;  import net.minecraft.util.math.Box;  import java.util.Arrays; +import java.util.Objects;  import java.util.function.Supplier;  public class Waypoint implements Renderable { @@ -16,9 +18,9 @@ public class Waypoint implements Renderable {      final Box box;      final Supplier<Type> typeSupplier;      protected final float[] colorComponents; -    final float alpha; -    final float lineWidth; -    final boolean throughWalls; +    public final float alpha; +    public final float lineWidth; +    public final boolean throughWalls;      private boolean shouldRender;      public Waypoint(BlockPos pos, Type type, float[] colorComponents) { @@ -56,6 +58,22 @@ public class Waypoint implements Renderable {          this.shouldRender = shouldRender;      } +    public Waypoint withX(int x) { +        return new Waypoint(new BlockPos(x, pos.getY(), pos.getZ()), typeSupplier, getColorComponents(), alpha, lineWidth, throughWalls, shouldRender()); +    } + +    public Waypoint withY(int y) { +        return new Waypoint(pos.withY(y), typeSupplier, getColorComponents(), alpha, lineWidth, throughWalls, shouldRender()); +    } + +    public Waypoint withZ(int z) { +        return new Waypoint(new BlockPos(pos.getX(), pos.getY(), z), typeSupplier, getColorComponents(), alpha, lineWidth, throughWalls, shouldRender()); +    } + +    public Waypoint withColor(float[] colorComponents, float alpha) { +        return new Waypoint(pos, typeSupplier, colorComponents, alpha, lineWidth, throughWalls, shouldRender()); +    } +      public boolean shouldRender() {          return shouldRender;      } @@ -72,6 +90,10 @@ public class Waypoint implements Renderable {          this.shouldRender = !this.shouldRender;      } +    public void setShouldRender(boolean shouldRender) { +        this.shouldRender = shouldRender; +    } +      public float[] getColorComponents() {          return colorComponents;      } @@ -96,11 +118,16 @@ public class Waypoint implements Renderable {      }      @Override +    public int hashCode() { +        return Objects.hash(pos, typeSupplier.get(), Arrays.hashCode(colorComponents), alpha, lineWidth, throughWalls, shouldRender); +    } + +    @Override      public boolean equals(Object obj) {          return super.equals(obj) || obj instanceof Waypoint other && pos.equals(other.pos) && typeSupplier.get() == other.typeSupplier.get() && Arrays.equals(colorComponents, other.colorComponents) && alpha == other.alpha && lineWidth == other.lineWidth && throughWalls == other.throughWalls && shouldRender == other.shouldRender;      } -    public enum Type { +    public enum Type implements StringIdentifiable {          WAYPOINT,          OUTLINED_WAYPOINT,          HIGHLIGHT, @@ -108,6 +135,11 @@ public class Waypoint implements Renderable {          OUTLINE;          @Override +        public String asString() { +            return name().toLowerCase(); +        } + +        @Override          public String toString() {              return switch (this) {                  case WAYPOINT -> "Waypoint"; diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java index b1ac6135..db2a6d82 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java @@ -4,8 +4,10 @@ import com.mojang.serialization.Codec;  import com.mojang.serialization.codecs.RecordCodecBuilder;  import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import java.util.ArrayList;  import java.util.List; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors;  public record WaypointCategory(String name, String island, List<NamedWaypoint> waypoints) {      public static final Codec<WaypointCategory> CODEC = RecordCodecBuilder.create(instance -> instance.group( @@ -19,13 +21,23 @@ public record WaypointCategory(String name, String island, List<NamedWaypoint> w              NamedWaypoint.SKYTILS_CODEC.listOf().fieldOf("waypoints").forGetter(WaypointCategory::waypoints)      ).apply(instance, WaypointCategory::new)); -    public WaypointCategory(WaypointCategory waypointCategory) { -        this(waypointCategory.name(), waypointCategory.island(), new ArrayList<>(waypointCategory.waypoints())); +    public static UnaryOperator<WaypointCategory> filter(Predicate<NamedWaypoint> predicate) { +        return waypointCategory -> new WaypointCategory(waypointCategory.name(), waypointCategory.island(), waypointCategory.waypoints().stream().filter(predicate).toList()); +    } + +    public WaypointCategory withName(String name) { +        return new WaypointCategory(name, island(), waypoints()); +    } + +    public WaypointCategory deepCopy() { +        return new WaypointCategory(name(), island(), waypoints().stream().map(NamedWaypoint::copy).collect(Collectors.toList()));      }      public void render(WorldRenderContext context) {          for (NamedWaypoint waypoint : waypoints) { -            waypoint.render(context); +            if (waypoint.shouldRender()) { +                waypoint.render(context); +            }          }      }  } diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index a212ddfa..98a7b2c9 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -724,6 +724,21 @@    "skyblocker.waypoints.newCategory": "New Waypoint Category",    "skyblocker.waypoints.new": "New Waypoint",    "skyblocker.waypoints.share": "Share", +  "skyblocker.waypoints.shareWaypoints": "Share Waypoints", +  "skyblocker.waypoints.importWaypointsSkytils": "Import Waypoints (Skytils)", +  "skyblocker.waypoints.importWaypointsSkytils.tooltip": "Import Waypoints from Clipboard (Skytils Format)", +  "skyblocker.waypoints.importWaypointsSnoopy": "Import Waypoints (Snoopy)", +  "skyblocker.waypoints.importWaypointsSnoopy.tooltip": "Import Waypoints from Clipboard (Snoopy Format) (Coming Soon)", +  "skyblocker.waypoints.exportWaypointsSkytils": "Export Waypoints (Skytils)", +  "skyblocker.waypoints.exportWaypointsSkytils.tooltip": "Export Waypoints to Clipboard (Skytils Format)", +  "skyblocker.waypoints.importSuccess": "Waypoints Imported", +  "skyblocker.waypoints.importSuccessText": "Successfully imported %d waypoints from %d categories.", +  "skyblocker.waypoints.importError": "Error Importing Waypoints", +  "skyblocker.waypoints.importErrorText": "Failed to import waypoints. See logs for details.", +  "skyblocker.waypoints.exportSuccess": "Waypoints Exported", +  "skyblocker.waypoints.exportSuccessText": "Successfully exported %d waypoints from %d categories.", +  "skyblocker.waypoints.exportError": "Error Exporting Waypoints", +  "skyblocker.waypoints.exportErrorText": "Failed to export waypoints. See logs for details.",    "skyblocker.waypoints.deleteQuestion": "Are you sure you want to remove this waypoint?",    "skyblocker.waypoints.deleteWarning": "Waypoint '%s' will be lost forever! (A long time!)", diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index 828cc206..9b96ba61 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -39,6 +39,7 @@      "YggdrasilMinecraftSessionServiceMixin",      "YggdrasilServicesKeyInfoMixin",      "accessors.BeaconBlockEntityRendererInvoker", +    "accessors.CheckboxWidgetAccessor",      "accessors.DrawContextInvoker",      "accessors.EndermanEntityAccessor",      "accessors.FrustumInvoker", diff --git a/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointCategoryTest.java b/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointCategoryTest.java index 65304e0c..f9a17bff 100644 --- a/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointCategoryTest.java +++ b/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointCategoryTest.java @@ -23,14 +23,14 @@ public class WaypointCategoryTest {      void testCodecEncode() {          WaypointCategory category = new WaypointCategory("category", "hub", List.of(new NamedWaypoint(BlockPos.ORIGIN, "waypoint", new float[]{0f, 0.5f, 1f}, false), new NamedWaypoint(new BlockPos(-1, 0, 1), "waypoint", new float[]{0f, 0f, 0f}, true)));          Object categoryJson = WaypointCategory.CODEC.encodeStart(JsonOps.INSTANCE, category).result().orElseThrow(); -        String expectedJson = "{\"name\":\"category\",\"island\":\"hub\",\"waypoints\":[{\"pos\":[0,0,0],\"name\":\"waypoint\",\"colorComponents\":[0.0,0.5,1.0],\"shouldRender\":false},{\"pos\":[-1,0,1],\"name\":\"waypoint\",\"colorComponents\":[0.0,0.0,0.0],\"shouldRender\":true}]}"; +        String expectedJson = "{\"name\":\"category\",\"island\":\"hub\",\"waypoints\":[{\"colorComponents\":[0.0,0.5,1.0],\"alpha\":0.5,\"shouldRender\":false,\"pos\":[0,0,0],\"name\":\"waypoint\"},{\"colorComponents\":[0.0,0.0,0.0],\"alpha\":0.5,\"shouldRender\":true,\"pos\":[-1,0,1],\"name\":\"waypoint\"}]}";          Assertions.assertEquals(expectedJson, categoryJson.toString());      }      @Test      void testCodecDecode() { -        String categoryJson = "{\"name\":\"category\",\"island\":\"hub\",\"waypoints\":[{\"pos\":[0,0,0],\"name\":\"waypoint\",\"colorComponents\":[0.0,0.5,1.0],\"shouldRender\":false},{\"pos\":[-1,0,1],\"name\":\"waypoint\",\"colorComponents\":[0.0,0.0,0.0],\"shouldRender\":true}]}"; +        String categoryJson = "{\"name\":\"category\",\"island\":\"hub\",\"waypoints\":[{\"colorComponents\":[0.0,0.5,1.0],\"alpha\":0.5,\"shouldRender\":false,\"pos\":[0,0,0],\"name\":\"waypoint\"},{\"colorComponents\":[0.0,0.0,0.0],\"alpha\":0.5,\"shouldRender\":true,\"pos\":[-1,0,1],\"name\":\"waypoint\"}]}";          WaypointCategory category = WaypointCategory.CODEC.parse(JsonOps.INSTANCE, SkyblockerMod.GSON.fromJson(categoryJson, JsonElement.class)).result().orElseThrow();          WaypointCategory expectedCategory = new WaypointCategory("category", "hub", List.of(new NamedWaypoint(BlockPos.ORIGIN, "waypoint", new float[]{0f, 0.5f, 1f}, false), new NamedWaypoint(new BlockPos(-1, 0, 1), "waypoint", new float[]{0f, 0f, 0f}, true))); diff --git a/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointsTest.java b/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointsTest.java index ad237718..820b687d 100644 --- a/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointsTest.java +++ b/src/test/java/de/hysky/skyblocker/utils/waypoint/WaypointsTest.java @@ -5,25 +5,119 @@ import net.minecraft.util.math.BlockPos;  import org.junit.jupiter.api.Assertions;  import org.junit.jupiter.api.Test; -import java.util.Collection;  import java.util.List;  public class WaypointsTest {      @Test      void testFromSkytilsBase64() { -        String waypointCategoriesSkytilsBase64 = "eyJjYXRlZ29yaWVzIjpbeyJuYW1lIjoiY2F0ZWdvcnkiLCJ3YXlwb2ludHMiOlt7Im5hbWUiOiJ3YXlwb2ludCIsIngiOjAsInkiOjAsInoiOjAsImVuYWJsZWQiOmZhbHNlLCJjb2xvciI6MzMwMjMsImFkZGVkQXQiOjF9LHsibmFtZSI6MSwieCI6LTEsInkiOjAsInoiOjEsImVuYWJsZWQiOnRydWUsImNvbG9yIjowLCJhZGRlZEF0IjoxfV0sImlzbGFuZCI6Imh1YiJ9XX0="; -        Collection<WaypointCategory> waypointCategories = Waypoints.fromSkytilsBase64(waypointCategoriesSkytilsBase64); -        Collection<WaypointCategory> expectedWaypointCategories = List.of(new WaypointCategory("category", "hub", List.of(new NamedWaypoint(BlockPos.ORIGIN, "waypoint", new float[]{0f, 0.5f, 1f}, false), new NamedWaypoint(new BlockPos(-1, 0, 1), "1", new float[]{0f, 0f, 0f}, true)))); +        String waypointCategoriesSkytilsBase64 = "eyJjYXRlZ29yaWVzIjpbeyJuYW1lIjoiY2F0ZWdvcnkiLCJ3YXlwb2ludHMiOlt7Im5hbWUiOiJ3YXlwb2ludCIsIngiOjAsInkiOjAsInoiOjAsImVuYWJsZWQiOmZhbHNlLCJjb2xvciI6LTg3MjM4MjIwOSwiYWRkZWRBdCI6MX0seyJuYW1lIjoxLCJ4IjotMSwieSI6MCwieiI6MSwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOjAsImFkZGVkQXQiOjF9XSwiaXNsYW5kIjoiaHViIn1dfQ=="; +        List<WaypointCategory> waypointCategories = Waypoints.fromSkytilsBase64(waypointCategoriesSkytilsBase64, ""); +        List<WaypointCategory> expectedWaypointCategories = List.of(new WaypointCategory("category", "hub", List.of(new NamedWaypoint(BlockPos.ORIGIN, "waypoint", new float[]{0f, 0.5019608f, 1f}, 0.8f, false), new NamedWaypoint(new BlockPos(-1, 0, 1), "1", new float[]{0f, 0f, 0f}, true))));          Assertions.assertEquals(expectedWaypointCategories, waypointCategories);      }      @Test      void testToSkytilsBase64() { -        Collection<WaypointCategory> waypointCategories = List.of(new WaypointCategory("category", "hub", List.of(new NamedWaypoint(BlockPos.ORIGIN, "waypoint", new float[]{0f, 0.5f, 1f}, false), new NamedWaypoint(new BlockPos(-1, 0, 1), "1", new float[]{0f, 0f, 0f}, true)))); +        List<WaypointCategory> waypointCategories = List.of(new WaypointCategory("category", "hub", List.of(new NamedWaypoint(BlockPos.ORIGIN, "waypoint", new float[]{0f, 0.5f, 1f}, 0.8f, false), new NamedWaypoint(new BlockPos(-1, 0, 1), "1", new float[]{0f, 0f, 0f}, true))));          String waypointCategoriesSkytilsBase64 = Waypoints.toSkytilsBase64(waypointCategories); -        String expectedWaypointCategoriesSkytilsBase64 = "ewogICJjYXRlZ29yaWVzIjogWwogICAgewogICAgICAibmFtZSI6ICJjYXRlZ29yeSIsCiAgICAgICJpc2xhbmQiOiAiaHViIiwKICAgICAgIndheXBvaW50cyI6IFsKICAgICAgICB7CiAgICAgICAgICAibmFtZSI6ICJ3YXlwb2ludCIsCiAgICAgICAgICAiY29sb3IiOiAzMjc2NywKICAgICAgICAgICJlbmFibGVkIjogZmFsc2UsCiAgICAgICAgICAieCI6IDAsCiAgICAgICAgICAieSI6IDAsCiAgICAgICAgICAieiI6IDAKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogIjEiLAogICAgICAgICAgImNvbG9yIjogMCwKICAgICAgICAgICJlbmFibGVkIjogdHJ1ZSwKICAgICAgICAgICJ4IjogLTEsCiAgICAgICAgICAieSI6IDAsCiAgICAgICAgICAieiI6IDEKICAgICAgICB9CiAgICAgIF0KICAgIH0KICBdCn0="; +        String expectedWaypointCategoriesSkytilsBase64 = "eyJjYXRlZ29yaWVzIjpbeyJuYW1lIjoiY2F0ZWdvcnkiLCJpc2xhbmQiOiJodWIiLCJ3YXlwb2ludHMiOlt7Im5hbWUiOiJ3YXlwb2ludCIsImNvbG9yIjotODcyMzgyNDY1LCJlbmFibGVkIjpmYWxzZSwieCI6MCwieSI6MCwieiI6MH0seyJuYW1lIjoiMSIsImNvbG9yIjoyMTMwNzA2NDMyLCJlbmFibGVkIjp0cnVlLCJ4IjotMSwieSI6MCwieiI6MX1dfV19";          Assertions.assertEquals(expectedWaypointCategoriesSkytilsBase64, waypointCategoriesSkytilsBase64);      } + +    //https://sharetext.me/gq22cbhdmo +    @Test +    void testFromSkytilsBase64GlacialCaveWaypoints() { +        String waypointCategoriesSkytilsBase64 = "eyJjYXRlZ29yaWVzIjogW3sibmFtZSI6ICJGcm96ZW4gVHJlYXN1cmUgTG9jYXRpb25zIiwid2F5cG9pbnRzIjogW3sibmFtZSI6ICIyNCIsIngiOiA2NCwieSI6IDc4LCJ6IjogMjgsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwODU0MDY3MTksImFkZGVkQXQiOiAxNjY5OTk5NzUwNjc3fSx7Im5hbWUiOiAiOSIsIngiOiA0NSwieSI6IDc5LCJ6IjogNDksImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwODUyNzM1OTksImFkZGVkQXQiOiAxNjY5OTk5NTEwMTA3fSx7Im5hbWUiOiAiMjAiLCJ4IjogNjAsInkiOiA3NiwieiI6IDUxLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiA5NTMzNTE5MzUsImFkZGVkQXQiOiAxNjY5OTk5NzQ5MzI3fSx7Im5hbWUiOiAiMjMiLCJ4IjogNjMsInkiOiA3NiwieiI6IDk1LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDIwNDYxMDUyLCJhZGRlZEF0IjogMTY2OTk5OTc1MDQ3N30seyJuYW1lIjogIjIyIiwieCI6IDYzLCJ5IjogNzYsInoiOiA1MiwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTA1MjQ0MjYxMSwiYWRkZWRBdCI6IDE2Njk5OTk3NTAyMjd9LHsibmFtZSI6ICI0MCIsIngiOiA5NCwieSI6IDc3LCJ6IjogNDIsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDk4NDYxMjg1NywiYWRkZWRBdCI6IDE2NzAwMDAyMjcwMjR9LHsibmFtZSI6ICIzOCIsIngiOiA5MSwieSI6IDc3LCJ6IjogMjcsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwNTI3NzAyOTIsImFkZGVkQXQiOiAxNjcwMDAwMjI2NjI1fSx7Im5hbWUiOiAiMTUiLCJ4IjogNTAsInkiOiA4MCwieiI6IDg4LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDcxMjUxMTk5LCJhZGRlZEF0IjogMTY2OTk5OTUxMTUwNH0seyJuYW1lIjogIjE0IiwieCI6IDUwLCJ5IjogNzksInoiOiAzNCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTEwNDkzNjcwMywiYWRkZWRBdCI6IDE2Njk5OTk1MTEzMDZ9LHsibmFtZSI6ICIxOSIsIngiOiA1OCwieSI6IDc5LCJ6IjogODksImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDExNTU2NjE4MTgsImFkZGVkQXQiOiAxNjY5OTk5NTE3ODEwfSx7Im5hbWUiOiAiMzAiLCJ4IjogNzgsInkiOiA3NCwieiI6IDk5LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTE5NDIwNDAzLCJhZGRlZEF0IjogMTY3MDAwMDAyMTgyM30seyJuYW1lIjogIjExIiwieCI6IDQ2LCJ5IjogODAsInoiOiA4NCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTA3MTY0NDY2MiwiYWRkZWRBdCI6IDE2Njk5OTk1MTA3MDh9LHsibmFtZSI6ICI0MyIsIngiOiA5NywieSI6IDgxLCJ6IjogNzcsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwNTE5ODM4NjUsImFkZGVkQXQiOiAxNjcwMDAwMjI3Njc2fSx7Im5hbWUiOiAiMTciLCJ4IjogNTUsInkiOiA3OSwieiI6IDM0LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTA1MTk5MDk4LCJhZGRlZEF0IjogMTY2OTk5OTUxMTkwNX0seyJuYW1lIjogIjQiLCJ4IjogMzksInkiOiA4MCwieiI6IDczLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTUzMjM2NDc5LCJhZGRlZEF0IjogMTY2OTk5OTE5ODkyN30seyJuYW1lIjogIjQxIiwieCI6IDk1LCJ5IjogNzYsInoiOiA1OCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTE1MTk5MTgwMSwiYWRkZWRBdCI6IDE2NzAwMDAyMjcyMjV9LHsibmFtZSI6ICI0MiIsIngiOiA5NywieSI6IDc1LCJ6IjogNzAsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwNTE3MjE3MDYsImFkZGVkQXQiOiAxNjcwMDAwMjI3NDczfSx7Im5hbWUiOiAiMTAiLCJ4IjogNDUsInkiOiA3OSwieiI6IDcwLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDcyMzY1NTYxLCJhZGRlZEF0IjogMTY2OTk5OTUxMDUwOH0seyJuYW1lIjogIjI4IiwieCI6IDc1LCJ5IjogODIsInoiOiAyMCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTA1MjkwMTM1OSwiYWRkZWRBdCI6IDE2Njk5OTk5ODY0MjZ9LHsibmFtZSI6ICIzIiwieCI6IDM2LCJ5IjogODAsInoiOiA4MCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogOTUyNjI5NTAzLCJhZGRlZEF0IjogMTY2OTk5OTE5ODcyN30seyJuYW1lIjogIjciLCJ4IjogNDMsInkiOiA3NywieiI6IDUwLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTIwNTM0MjcxLCJhZGRlZEF0IjogMTY2OTk5OTE5OTQ2N30seyJuYW1lIjogIjgiLCJ4IjogNDMsInkiOiA3OSwieiI6IDczLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTUyNzEwMzk5LCJhZGRlZEF0IjogMTY2OTk5OTMxMTAyOX0seyJuYW1lIjogIjIiLCJ4IjogMzUsInkiOiA4LCJ6IjogNzEsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwNTQzNDMxNjQsImFkZGVkQXQiOiAxNjY5OTk5MTk4NTY3fSx7Im5hbWUiOiAiMzQiLCJ4IjogODksInkiOiA3NywieiI6IDg0LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDg2NDU1Nzk3LCJhZGRlZEF0IjogMTY3MDAwMDAyMjUyOX0seyJuYW1lIjogIjI2IiwieCI6IDczLCJ5IjogNzYsInoiOiAzMSwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTExOTE1Nzc1OSwiYWRkZWRBdCI6IDE2Njk5OTk3NTEwNzd9LHsibmFtZSI6ICIxMiIsIngiOiA0NywieSI6IDc3LCJ6IjogNjUsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwNzE4NDEyNzcsImFkZGVkQXQiOiAxNjY5OTk5NTEwOTA4fSx7Im5hbWUiOiAiMTYiLCJ4IjogNTIsInkiOiA3NSwieiI6IDQ1LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTM4NjIyNDU3LCJhZGRlZEF0IjogMTY2OTk5OTUxMTcwM30seyJuYW1lIjogIjMzIiwieCI6IDgyLCJ5IjogNzgsInoiOiAyNiwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTEwMzAzNDM2NywiYWRkZWRBdCI6IDE2NzAwMDAwMjIzNzl9LHsibmFtZSI6ICIyMSIsIngiOiA2MSwieSI6IDc4LCJ6IjogOTIsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwMjExODE5NDIsImFkZGVkQXQiOiAxNjY5OTk5NzQ5ODc3fSx7Im5hbWUiOiAiMjciLCJ4IjogNzMsInkiOiA3OSwieiI6IDUyLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDUxMDAwODI0LCJhZGRlZEF0IjogMTY2OTk5OTk4NjIzMH0seyJuYW1lIjogIjQ2IiwieCI6IDEwMywieSI6IDc0LCJ6IjogOTgsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDExMTg1Njg0MjUsImFkZGVkQXQiOiAxNjcwMDAwMjI4MjIzfSx7Im5hbWUiOiAiNDciLCJ4IjogMTA0LCJ5IjogNzgsInoiOiA2OCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogOTUzNDc5OTM1LCJhZGRlZEF0IjogMTY3MDAwMDM1Nzk3NH0seyJuYW1lIjogIjYiLCJ4IjogNDIsInkiOiA3NywieiI6IDU4LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDE4NDk0OTcwLCJhZGRlZEF0IjogMTY2OTk5OTE5OTMyNX0seyJuYW1lIjogIjUiLCJ4IjogNDEsInkiOiA3OSwieiI6IDgxLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTM2MzI4MTkxLCJhZGRlZEF0IjogMTY2OTk5OTE5OTEyMX0seyJuYW1lIjogIjM2IiwieCI6IDkwLCJ5IjogNzcsInoiOiA0NiwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTExOTA5MDQzMSwiYWRkZWRBdCI6IDE2NzAwMDAwMjI5Mjh9LHsibmFtZSI6ICIxIiwieCI6IDMyLCJ5IjogODAsInoiOiA3NCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTM4ODc3MzM3MSwiYWRkZWRBdCI6IDE2Njk5OTkxMDI4ODJ9LHsibmFtZSI6ICIzMSIsIngiOiA3OCwieSI6IDc3LCJ6IjogNDAsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwMTkyMTU4NjAsImFkZGVkQXQiOiAxNjcwMDAwMDIxOTc1fSx7Im5hbWUiOiAiMjkiLCJ4IjogNzYsInkiOiA3NiwieiI6IDU1LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMTUzNTY0NjU1LCJhZGRlZEF0IjogMTY2OTk5OTk4NjYyN30seyJuYW1lIjogIjI1IiwieCI6IDY2LCJ5IjogODEsInoiOiAyOCwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTA1MjcwNDc1MCwiYWRkZWRBdCI6IDE2Njk5OTk3NTA5Mjd9LHsibmFtZSI6ICIzNSIsIngiOiA5MCwieSI6IDc3LCJ6IjogMzgsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwMTkzNDY5MzMsImFkZGVkQXQiOiAxNjcwMDAwMDIyNzI0fSx7Im5hbWUiOiAiMTgiLCJ4IjogNTUsInkiOiA4MCwieiI6IDM4LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDg4NjE4NDkzLCJhZGRlZEF0IjogMTY2OTk5OTUxMjE1N30seyJuYW1lIjogIjM5IiwieCI6IDkyLCJ5IjogNzQsInoiOiAxMDgsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwODY4NDkwMTQsImFkZGVkQXQiOiAxNjcwMDAwMjI2ODc5fSx7Im5hbWUiOiAiMTMiLCJ4IjogNTAsInkiOiA3NiwieiI6IDUyLCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDcxOTcyMzQ0LCJhZGRlZEF0IjogMTY2OTk5OTUxMTEwMn0seyJuYW1lIjogIjQ0IiwieCI6IDk4LCJ5IjogNzcsInoiOiA3NiwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTA4NTg2NTk3OCwiYWRkZWRBdCI6IDE2NzAwMDAyMjc4ODF9LHsibmFtZSI6ICIzMiIsIngiOiA3OSwieSI6IDgwLCJ6IjogNzMsImVuYWJsZWQiOiB0cnVlLCJjb2xvciI6IDEwNTI3NzAyOTgsImFkZGVkQXQiOiAxNjcwMDAwMDIyMTc0fSx7Im5hbWUiOiAiMzciLCJ4IjogOTEsInkiOiA3NiwieiI6IDM4LCJlbmFibGVkIjogdHJ1ZSwiY29sb3IiOiAxMDUxMTMxMzkxLCJhZGRlZEF0IjogMTY3MDAwMDIyNjQyM30seyJuYW1lIjogIjQ1IiwieCI6IDk4LCJ5IjogNzgsInoiOiA3NSwiZW5hYmxlZCI6IHRydWUsImNvbG9yIjogMTIwMjg0NzczNywiYWRkZWRBdCI6IDE2NzAwMDAyMjgwNzN9XSwiaXNsYW5kIjogIndpbnRlciJ9XX0="; +        List<WaypointCategory> waypointCategories = Waypoints.fromSkytilsBase64(waypointCategoriesSkytilsBase64, ""); +        List<WaypointCategory> expectedWaypointCategories = List.of(new WaypointCategory("Frozen Treasure Locations", "winter", List.of( +                new NamedWaypoint(new BlockPos(64, 78, 28), "24", new float[]{177 / 255f, 253 / 255f, 255 / 255f}, 64 / 255f, true), +                new NamedWaypoint(new BlockPos(45, 79, 49), "9", new float[]{175 / 255f, 245 / 255f, 255 / 255f}, 64 / 255f, true), +                new NamedWaypoint(new BlockPos(60, 76, 51), "20", new float[]{210 / 255f, 254 / 255f, 255 / 255f}, 56 / 255f, true), +                new NamedWaypoint(new BlockPos(63, 76, 95), "23", new float[]{210 / 255f, 255 / 255f, 252 / 255f}, 60 / 255f, true), +                new NamedWaypoint(new BlockPos(63, 76, 52), "22", new float[]{186 / 255f, 255 / 255f, 243 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(94, 77, 42), "40", new float[]{175 / 255f, 255 / 255f, 249 / 255f}, 58 / 255f, true), +                new NamedWaypoint(new BlockPos(91, 77, 27), "38", new float[]{191 / 255f, 255 / 255f, 244 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(50, 80, 88), "15", new float[]{217 / 255f, 254 / 255f, 255 / 255f}, 63 / 255f, true), +                new NamedWaypoint(new BlockPos(50, 79, 34), "14", new float[]{219 / 255f, 254 / 255f, 255 / 255f}, 65 / 255f, true), +                new NamedWaypoint(new BlockPos(58, 79, 89), "19", new float[]{225 / 255f, 255 / 255f, 250 / 255f}, 68 / 255f, true), +                new NamedWaypoint(new BlockPos(78, 74, 99), "30", new float[]{184 / 255f, 255 / 255f, 243 / 255f}, 66 / 255f, true), +                new NamedWaypoint(new BlockPos(46, 80, 84), "11", new float[]{223 / 255f, 255 / 255f, 246 / 255f}, 63 / 255f, true), +                new NamedWaypoint(new BlockPos(97, 81, 77), "43", new float[]{179 / 255f, 255 / 255f, 249 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(55, 79, 34), "17", new float[]{223 / 255f, 255 / 255f, 250 / 255f}, 65 / 255f, true), +                new NamedWaypoint(new BlockPos(39, 80, 73), "4", new float[]{188 / 255f, 253 / 255f, 255 / 255f}, 68 / 255f, true), +                new NamedWaypoint(new BlockPos(95, 76, 58), "41", new float[]{169 / 255f, 255 / 255f, 249 / 255f}, 68 / 255f, true), +                new NamedWaypoint(new BlockPos(97, 75, 70), "42", new float[]{175 / 255f, 255 / 255f, 234 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(45, 79, 70), "10", new float[]{234 / 255f, 255 / 255f, 249 / 255f}, 63 / 255f, true), +                new NamedWaypoint(new BlockPos(75, 82, 20), "28", new float[]{193 / 255f, 255 / 255f, 239 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(36, 80, 80), "3", new float[]{199 / 255f, 248 / 255f, 255 / 255f}, 56 / 255f, true), +                new NamedWaypoint(new BlockPos(43, 77, 50), "7", new float[]{201 / 255f, 254 / 255f, 255 / 255f}, 66 / 255f, true), +                new NamedWaypoint(new BlockPos(43, 79, 73), "8", new float[]{180 / 255f, 246 / 255f, 255 / 255f}, 68 / 255f, true), +                new NamedWaypoint(new BlockPos(35, 8, 71), "2", new float[]{215 / 255f, 255 / 255f, 252 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(89, 77, 84), "34", new float[]{193 / 255f, 255 / 255f, 245 / 255f}, 64 / 255f, true), +                new NamedWaypoint(new BlockPos(73, 76, 31), "26", new float[]{180 / 255f, 253 / 255f, 255 / 255f}, 66 / 255f, true), +                new NamedWaypoint(new BlockPos(47, 77, 65), "12", new float[]{226 / 255f, 255 / 255f, 253 / 255f}, 63 / 255f, true), +                new NamedWaypoint(new BlockPos(52, 75, 45), "16", new float[]{221 / 255f, 255 / 255f, 249 / 255f}, 67 / 255f, true), +                new NamedWaypoint(new BlockPos(82, 78, 26), "33", new float[]{190 / 255f, 247 / 255f, 255 / 255f}, 65 / 255f, true), +                new NamedWaypoint(new BlockPos(61, 78, 92), "21", new float[]{221 / 255f, 255 / 255f, 246 / 255f}, 60 / 255f, true), +                new NamedWaypoint(new BlockPos(73, 79, 52), "27", new float[]{164 / 255f, 255 / 255f, 248 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(103, 74, 98), "46", new float[]{171 / 255f, 255 / 255f, 233 / 255f}, 66 / 255f, true), +                new NamedWaypoint(new BlockPos(104, 78, 68), "47", new float[]{212 / 255f, 242 / 255f, 255 / 255f}, 56 / 255f, true), +                new NamedWaypoint(new BlockPos(42, 77, 58), "6", new float[]{180 / 255f, 255 / 255f, 250 / 255f}, 60 / 255f, true), +                new NamedWaypoint(new BlockPos(41, 79, 81), "5", new float[]{186 / 255f, 253 / 255f, 255 / 255f}, 67 / 255f, true), +                new NamedWaypoint(new BlockPos(90, 77, 46), "36", new float[]{179 / 255f, 246 / 255f, 255 / 255f}, 66 / 255f, true), +                new NamedWaypoint(new BlockPos(32, 80, 74), "1", new float[]{198 / 255f, 255 / 255f, 251 / 255f}, 82 / 255f, true), +                new NamedWaypoint(new BlockPos(78, 77, 40), "31", new float[]{191 / 255f, 255 / 255f, 244 / 255f}, 60 / 255f, true), +                new NamedWaypoint(new BlockPos(76, 76, 55), "29", new float[]{193 / 255f, 255 / 255f, 239 / 255f}, 68 / 255f, true), +                new NamedWaypoint(new BlockPos(66, 81, 28), "25", new float[]{190 / 255f, 255 / 255f, 238 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(90, 77, 38), "35", new float[]{193 / 255f, 255 / 255f, 245 / 255f}, 60 / 255f, true), +                new NamedWaypoint(new BlockPos(55, 80, 38), "18", new float[]{226 / 255f, 255 / 255f, 253 / 255f}, 64 / 255f, true), +                new NamedWaypoint(new BlockPos(92, 74, 108), "39", new float[]{199 / 255f, 255 / 255f, 246 / 255f}, 64 / 255f, true), +                new NamedWaypoint(new BlockPos(50, 76, 52), "13", new float[]{228 / 255f, 255 / 255f, 248 / 255f}, 63 / 255f, true), +                new NamedWaypoint(new BlockPos(98, 77, 76), "44", new float[]{184 / 255f, 255 / 255f, 250 / 255f}, 64 / 255f, true), +                new NamedWaypoint(new BlockPos(79, 80, 73), "32", new float[]{191 / 255f, 255 / 255f, 250 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(91, 76, 38), "37", new float[]{166 / 255f, 253 / 255f, 255 / 255f}, 62 / 255f, true), +                new NamedWaypoint(new BlockPos(98, 78, 75), "45", new float[]{177 / 255f, 255 / 255f, 249 / 255f}, 71 / 255f, true) +        ))); + +        Assertions.assertEquals(expectedWaypointCategories, waypointCategories); +    } + +    //https://pastebin.com/c4PjUZjJ +    @Test +    void testFromSkytilsBase64CrystalHollowsWaypoints() { +        String waypointsSkytilsBase64 = "W3sibmFtZSI6IlNob3V0b3V0IFRlYmV5IGFuZCB0cmV2YW55YSIsIngiOjUxMCwieSI6NTMsInoiOjM5MywiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMDciLCJ4Ijo0NzksInkiOjM5LCJ6Ijo0MDgsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjExIiwieCI6NDk1LCJ5IjozNCwieiI6NDE4LCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiIwOSIsIngiOjQ4NSwieSI6MzMsInoiOjQwMiwiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMjMiLCJ4Ijo1MTQsInkiOjU1LCJ6IjozODMsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjA2IiwieCI6NDgzLCJ5Ijo0MiwieiI6NDA1LCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiIwMSIsIngiOjUwMiwieSI6NDgsInoiOjQwMywiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMTgiLCJ4Ijo1MDMsInkiOjU2LCJ6Ijo0MzYsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjAwIC0gU3RhcnQiLCJ4Ijo1MDMsInkiOjQ4LCJ6Ijo0MDAsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjEzIiwieCI6NDc4LCJ5Ijo0NCwieiI6NDE5LCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiIxOSIsIngiOjUwMSwieSI6NTcsInoiOjQzOCwiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMDIiLCJ4Ijo0OTYsInkiOjQ1LCJ6Ijo0MDcsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjA0IiwieCI6NDk1LCJ5Ijo1MywieiI6NDA0LCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiIwNSIsIngiOjQ3OSwieSI6NDksInoiOjQwNywiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMTIiLCJ4Ijo1MDQsInkiOjQxLCJ6Ijo0MTksImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjAzIiwieCI6NDkwLCJ5Ijo0NSwieiI6MzkyLCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiIxMCIsIngiOjQ4OCwieSI6MzIsInoiOjQyMSwiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMjIiLCJ4Ijo1MDcsInkiOjUyLCJ6IjozODYsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjE2IiwieCI6NDg4LCJ5Ijo1NSwieiI6NDIxLCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiI5OSAtIEVuZCIsIngiOjUxMCwieSI6NTIsInoiOjM5MywiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMTUiLCJ4Ijo0ODYsInkiOjU1LCJ6Ijo0MjgsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjE0IiwieCI6NDc1LCJ5Ijo0NCwieiI6NDI5LCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiIxNyIsIngiOjUwOSwieSI6NTAsInoiOjQzMiwiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn0seyJuYW1lIjoiMjAiLCJ4Ijo1MDUsInkiOjU4LCJ6Ijo0MjYsImlzbGFuZCI6ImNyeXN0YWxfaG9sbG93cyIsImVuYWJsZWQiOnRydWUsImNvbG9yIjotNjU1MzZ9LHsibmFtZSI6IjA4IiwieCI6NDgwLCJ5IjozOCwieiI6NDA1LCJpc2xhbmQiOiJjcnlzdGFsX2hvbGxvd3MiLCJlbmFibGVkIjp0cnVlLCJjb2xvciI6LTY1NTM2fSx7Im5hbWUiOiIyMSIsIngiOjQ5NywieSI6NTUsInoiOjM5MSwiaXNsYW5kIjoiY3J5c3RhbF9ob2xsb3dzIiwiZW5hYmxlZCI6dHJ1ZSwiY29sb3IiOi02NTUzNn1d"; +        List<WaypointCategory> waypointCategories = Waypoints.fromSkytilsBase64(waypointsSkytilsBase64, "crystal_hollows"); +        List<WaypointCategory> expectedWaypointCategories = List.of(new WaypointCategory("New Category", "crystal_hollows", List.of( +                new NamedWaypoint(new BlockPos(510, 53, 393), "Shoutout Tebey and trevanya", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(479, 39, 408), "07", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(495, 34, 418), "11", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(485, 33, 402), "09", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(514, 55, 383), "23", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(483, 42, 405), "06", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(502, 48, 403), "01", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(503, 56, 436), "18", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(503, 48, 400), "00 - Start", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(478, 44, 419), "13", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(501, 57, 438), "19", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(496, 45, 407), "02", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(495, 53, 404), "04", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(479, 49, 407), "05", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(504, 41, 419), "12", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(490, 45, 392), "03", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(488, 32, 421), "10", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(507, 52, 386), "22", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(488, 55, 421), "16", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(510, 52, 393), "99 - End", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(486, 55, 428), "15", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(475, 44, 429), "14", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(509, 50, 432), "17", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(505, 58, 426), "20", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(480, 38, 405), "08", new float[]{1, 0, 0}, 1, true), +                new NamedWaypoint(new BlockPos(497, 55, 391), "21", new float[]{1, 0, 0}, 1, true) +        ))); + +        Assertions.assertEquals(expectedWaypointCategories, waypointCategories); +    }  } | 
