/* * Copyright (C) 2022 NotEnoughUpdates contributors * * This file is part of NotEnoughUpdates. * * NotEnoughUpdates is free software: you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * NotEnoughUpdates is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with NotEnoughUpdates. If not, see . */ package io.github.moulberry.notenoughupdates.miscfeatures; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.core.util.StringUtils; import io.github.moulberry.notenoughupdates.core.util.render.RenderUtils; import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent; import io.github.moulberry.notenoughupdates.events.RepositoryReloadEvent; import io.github.moulberry.notenoughupdates.miscfeatures.customblockzones.LocationChangeEvent; import io.github.moulberry.notenoughupdates.miscgui.GuiNavigation; import io.github.moulberry.notenoughupdates.options.NEUConfig; import io.github.moulberry.notenoughupdates.util.JsonUtils; import io.github.moulberry.notenoughupdates.util.NotificationHandler; import io.github.moulberry.notenoughupdates.util.SBInfo; import io.github.moulberry.notenoughupdates.util.Utils; import io.github.moulberry.notenoughupdates.util.brigadier.DslKt; import kotlin.Unit; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.util.BlockPos; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.Vec3i; import net.minecraftforge.client.event.ClientChatReceivedEvent; import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.event.entity.EntityJoinWorldEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.InputEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import org.lwjgl.input.Keyboard; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; public class Navigation { private List teleporters = new ArrayList<>(); private Map areaNames = new HashMap<>(); private Map warps = new HashMap<>(); private Map waypoints = new HashMap<>(); public Map getWaypoints() { return waypoints; } public static class WarpPoint { public final BlockPos blockPos; public final String warpName, modeName; public WarpPoint(double x, double y, double z, String warpName, String modeName) { this.blockPos = new BlockPos(x, y, z); this.warpName = warpName; this.modeName = modeName; } } public static class Teleporter { public final double x, y, z; public final String from, to; public Teleporter(double x, double y, double z, String from, String to) { this.x = x; this.y = y; this.z = z; this.from = from; this.to = to; } } private final NotEnoughUpdates neu; public Navigation(NotEnoughUpdates notEnoughUpdates) { neu = notEnoughUpdates; } /* JsonObject (x,y,z,island,displayname) */ private JsonObject currentlyTrackedWaypoint = null; private BlockPos position = null; private String island = null; private String displayName = null; private String internalname = null; private String warpAgainTo = null; private Instant lastWarpAttemptedTime = null; private String lastWarpAttempted; private Instant warpAgainTiming = null; private Teleporter nextTeleporter = null; public boolean isValidWaypoint(JsonObject object) { return object != null && object.has("x") && object.has("y") && object.has("z") && object.has("island") && object.has("displayname") && object.has("internalname"); } public void trackWaypoint(String trackNow) { if (trackNow == null) { trackWaypoint((JsonObject) null); } else { JsonObject jsonObject = waypoints.get(trackNow); if (jsonObject == null) { showError( "Could not track waypoint " + trackNow + ". This is likely due to an outdated or broken repository.", true ); return; } trackWaypoint(jsonObject); } } public void trackWaypoint(JsonObject trackNow) { if (trackNow != null && !isValidWaypoint(trackNow)) { showError("Could not track waypoint. This is likely due to an outdated or broken repository.", true); return; } if (!neu.config.hidden.hasOpenedWaypointMenu) NotificationHandler.displayNotification(Arrays.asList( "You just tracked a waypoint.", "Press [N] to open the waypoint menu to untrack it", "or to find other waypoints to track.", "Press [X] to close this message." ), true, false); currentlyTrackedWaypoint = trackNow; updateData(); } @SubscribeEvent public void onRepositoryReload(RepositoryReloadEvent event) { JsonObject obj = Utils.getConstant("islands", neu.manager.gson); List teleporters = JsonUtils.getJsonArrayOrEmpty(obj, "teleporters", jsonElement -> { JsonObject teleporterObj = jsonElement.getAsJsonObject(); return new Teleporter( teleporterObj.get("x").getAsDouble(), teleporterObj.get("y").getAsDouble(), teleporterObj.get("z").getAsDouble(), teleporterObj.get("from").getAsString(), teleporterObj.get("to").getAsString() ); }); for (Teleporter teleporter : teleporters) { if (teleporter.from.equals(teleporter.to)) { showError("Found self referencing teleporter: " + teleporter.from, true); } } this.teleporters = teleporters; this.waypoints = NotEnoughUpdates.INSTANCE.manager .getItemInformation().values().stream() .filter(this::isValidWaypoint) .collect(Collectors.toMap(it -> it.get("internalname").getAsString(), it -> it)); this.areaNames = JsonUtils.transformJsonObjectToMap(obj.getAsJsonObject("area_names"), JsonElement::getAsString); this.warps = JsonUtils.getJsonArrayOrEmpty(obj, "island_warps", jsonElement -> { JsonObject warpObject = jsonElement.getAsJsonObject(); return new WarpPoint( warpObject.get("x").getAsDouble(), warpObject.get("y").getAsDouble(), warpObject.get("z").getAsDouble(), warpObject.get("warp").getAsString(), warpObject.get("mode").getAsString() ); }).stream().collect(Collectors.toMap(it -> it.warpName, it -> it)); } @SubscribeEvent public void onKeybindPressed(InputEvent.KeyInputEvent event) { if (!Keyboard.getEventKeyState()) return; int key = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); if (neu.config.misc.keybindWaypoint == key) { if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT)) { if (currentlyTrackedWaypoint != null) { useWarpCommand(); } } else { Minecraft.getMinecraft().displayGuiScreen(new GuiNavigation()); } } } public Map getWarps() { return warps; } @SubscribeEvent public void onChatMessage(ClientChatReceivedEvent event) { if (event.type == 2) return; if (StringUtils.cleanColour(event.message.getUnformattedText()).startsWith("§r§eYou may now fast travel to")) { getNonUnlockedWarpScrolls().clear(); return; } if (!"§r§cYou haven't unlocked this fast travel destination!§r".equals(event.message.getFormattedText())) return; Instant lwa = lastWarpAttemptedTime; if (lwa == null) return; if (Duration.between(lwa, Instant.now()).compareTo(Duration.ofSeconds(1)) > 0) return; lastWarpAttemptedTime = null; getNonUnlockedWarpScrolls().add(lastWarpAttempted); Utils.addChatMessage("§e[NEU] This warp has been marked as non available."); Utils.addChatMessage( "§e[NEU] Try navigating to that same position again to check if you have another warp available on that island."); Utils.addChatMessage("§e[NEU] To reset, type /neuclearwarps"); } @SubscribeEvent public void onCommands(RegisterBrigadierCommandEvent event) { event.command("neuclearwarps", builder -> DslKt.thenExecute(builder, context -> { getNonUnlockedWarpScrolls().clear(); DslKt.reply(context, "Reset all blocked warps."); return Unit.INSTANCE; })); } private Set getNonUnlockedWarpScrolls() { NEUConfig.HiddenProfileSpecific profileSpecific = neu.config.getProfileSpecific(); if (profileSpecific == null) return new HashSet<>(); return profileSpecific.nonUnlockedWarpScrolls; } public WarpPoint getClosestWarp(String mode, Vec3i position, boolean checkAvailable) { double minDistance = -1; Set nonUnlockedWarpScrolls = getNonUnlockedWarpScrolls(); WarpPoint minWarp = null; for (WarpPoint value : warps.values()) { if (value.modeName.equals(mode)) { if (checkAvailable && nonUnlockedWarpScrolls.contains(value.warpName)) continue; double distance = value.blockPos.distanceSq(position); if (distance < minDistance || minWarp == null) { minDistance = distance; minWarp = value; } } } return minWarp; } public void useWarpCommand() { EntityPlayerSP thePlayer = Minecraft.getMinecraft().thePlayer; if (currentlyTrackedWaypoint == null || thePlayer == null) return; WarpPoint closestWarp = getClosestWarp(island, position, true); if (closestWarp == null) { showError("Could not find an unlocked warp that could be used.", false); return; } if (!island.equals(SBInfo.getInstance().mode)) { warpAgainTiming = Instant.now(); warpAgainTo = closestWarp.warpName; } else if (thePlayer.getDistanceSq(position) < closestWarp.blockPos.distanceSq(position)) { showError("You are already on the same island and nearer than the closest unlocked warp scroll.", false); return; } lastWarpAttemptedTime = Instant.now(); lastWarpAttempted = closestWarp.warpName; thePlayer.sendChatMessage("/warp " + closestWarp.warpName); } @SubscribeEvent public void onTeleportDone(EntityJoinWorldEvent event) { if (neu.config.misc.warpTwice && event.entity == Minecraft.getMinecraft().thePlayer && warpAgainTo != null && warpAgainTiming != null && warpAgainTiming.plusSeconds(1).isAfter(Instant.now())) { warpAgainTiming = null; String savedWarpAgain = warpAgainTo; warpAgainTo = null; Minecraft.getMinecraft().thePlayer.sendChatMessage("/warp " + savedWarpAgain); } } public String getNameForAreaMode(String mode) { return areaNames.get(mode); } public String getNameForAreaModeOrUnknown(String mode) { return areaNames.getOrDefault(mode, "Unknown"); } public void untrackWaypoint() { trackWaypoint((JsonObject) null); } public JsonObject getTrackedWaypoint() { return currentlyTrackedWaypoint; } public String getIsland() { return island; } public String getDisplayName() { return displayName; } public BlockPos getPosition() { return position; } public String getInternalname() { return internalname; } private void updateData() { if (currentlyTrackedWaypoint == null) { position = null; island = null; displayName = null; nextTeleporter = null; internalname = null; return; } position = new BlockPos( currentlyTrackedWaypoint.get("x").getAsDouble(), currentlyTrackedWaypoint.get("y").getAsDouble(), currentlyTrackedWaypoint.get("z").getAsDouble() ); internalname = currentlyTrackedWaypoint.get("internalname").getAsString(); island = currentlyTrackedWaypoint.get("island").getAsString(); displayName = currentlyTrackedWaypoint.get("displayname").getAsString(); recalculateNextTeleporter(SBInfo.getInstance().mode); } @SubscribeEvent public void onLocationChange(LocationChangeEvent event) { recalculateNextTeleporter(event.newLocation); } public Teleporter recalculateNextTeleporter(String from) { String to = island; if (from == null || to == null) return null; List nextTeleporter = findNextTeleporter0(from, to, new HashSet<>()); if (nextTeleporter == null || nextTeleporter.isEmpty()) { this.nextTeleporter = null; } else { this.nextTeleporter = nextTeleporter.get(0); } return this.nextTeleporter; } private List findNextTeleporter0(String from, String to, Set visited) { if (from.equals(to)) return new ArrayList<>(); if (visited.contains(from)) return null; visited.add(from); int minPathLength = 0; List minPath = null; for (Teleporter teleporter : teleporters) { if (!teleporter.from.equals(from)) continue; List nextTeleporter0 = findNextTeleporter0(teleporter.to, to, visited); if (nextTeleporter0 == null) continue; if (minPath == null || nextTeleporter0.size() < minPathLength) { minPathLength = nextTeleporter0.size(); nextTeleporter0.add(0, teleporter); minPath = nextTeleporter0; } } visited.remove(from); return minPath; } private void showError(String message, boolean log) { EntityPlayerSP player = Minecraft.getMinecraft().thePlayer; if (player != null) Utils.addChatMessage(EnumChatFormatting.DARK_RED + "[NEU-Waypoint] " + message); if (log) new RuntimeException("[NEU-Waypoint] " + message).printStackTrace(); } @SubscribeEvent public void onEvent(TickEvent.ClientTickEvent event) { if (event.phase == TickEvent.Phase.END && currentlyTrackedWaypoint != null && NotEnoughUpdates.INSTANCE.config.misc.untrackCloseWaypoints && island.equals(SBInfo.getInstance().mode)) { EntityPlayerSP thePlayer = Minecraft.getMinecraft().thePlayer; if (thePlayer != null && thePlayer.getDistanceSq(position) < 16) { untrackWaypoint(); AbiphoneContactHelper.getInstance().resetMarker(); } } } @SubscribeEvent public void onRenderLast(RenderWorldLastEvent event) { if (currentlyTrackedWaypoint != null) { if (island.equals(SBInfo.getInstance().mode)) { RenderUtils.renderWayPoint(displayName, position, event.partialTicks); } else if (nextTeleporter != null) { String to = nextTeleporter.to; String toName = getNameForAreaModeOrUnknown(to); RenderUtils.renderWayPoint( Arrays.asList("Teleporter to " + toName, "(towards " + displayName + "§r)"), new BlockPos( nextTeleporter.x, nextTeleporter.y, nextTeleporter.z ), event.partialTicks ); } } } }