/* * 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.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.commands.ClientCommandBase; import io.github.moulberry.notenoughupdates.core.util.render.RenderUtils; import io.github.moulberry.notenoughupdates.util.Constants; import io.github.moulberry.notenoughupdates.util.SBInfo; import net.minecraft.client.Minecraft; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.util.BlockPos; import net.minecraft.util.ChatComponentText; import net.minecraft.util.EnumChatFormatting; import net.minecraftforge.client.event.ClientChatReceivedEvent; import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; public class FairySouls { private static FairySouls instance = null; private static final String unknownProfile = "unknown"; private boolean trackSouls; private boolean showSouls; private HashMap>> allProfilesFoundSouls = new HashMap<>(); private List allSoulsInCurrentLocation; private Set foundSoulsInLocation; private TreeMap missingSoulsDistanceSqMap; private List closestMissingSouls; private String currentLocation; private BlockPos lastPlayerPos; public static FairySouls getInstance() { if (instance == null) { instance = new FairySouls(); } return instance; } @SubscribeEvent public void onWorldLoad(WorldEvent.Load event) { currentLocation = null; trackSouls = NotEnoughUpdates.INSTANCE.config.misc.trackFairySouls; showSouls = NotEnoughUpdates.INSTANCE.config.misc.fariySoul; } public void initializeLocation() { if (!trackSouls || currentLocation == null) { return; } foundSoulsInLocation = null; closestMissingSouls = new ArrayList<>(); missingSoulsDistanceSqMap = new TreeMap<>(); lastPlayerPos = BlockPos.ORIGIN; allSoulsInCurrentLocation = loadLocationFairySoulsFromConfig(currentLocation); if (allSoulsInCurrentLocation == null) { return; } foundSoulsInLocation = getFoundSoulsForProfile() .computeIfAbsent(currentLocation, k -> new HashSet<>()); refreshMissingSoulInfo(true); } private void refreshMissingSoulInfo(boolean force) { if (allSoulsInCurrentLocation == null) return; BlockPos currentPlayerPos = Minecraft.getMinecraft().thePlayer.getPosition(); if (lastPlayerPos.equals(currentPlayerPos) && !force) { return; } lastPlayerPos = currentPlayerPos; missingSoulsDistanceSqMap.clear(); for (int i = 0; i < allSoulsInCurrentLocation.size(); i++) { if (foundSoulsInLocation.contains(i)) { continue; } BlockPos pos = allSoulsInCurrentLocation.get(i); double distSq = pos.distanceSq(lastPlayerPos); missingSoulsDistanceSqMap.put(distSq, pos); } closestMissingSouls.clear(); if (missingSoulsDistanceSqMap.isEmpty()) { return; } // rebuild the list of the closest ones int maxSouls = 15; int souls = 0; for (BlockPos pos : missingSoulsDistanceSqMap.values()) { closestMissingSouls.add(pos); if (++souls >= maxSouls) break; } } private int interpolateColors(int color1, int color2, double factor) { int r1 = ((color1 >> 16) & 0xff); int g1 = ((color1 >> 8) & 0xff); int b1 = (color1 & 0xff); int r2 = (color2 >> 16) & 0xff; int g2 = (color2 >> 8) & 0xff; int b2 = color2 & 0xff; int r3 = r1 + (int) Math.round(factor * (r2 - r1)); int g3 = g1 + (int) Math.round(factor * (g2 - g1)); int b3 = b1 + (int) Math.round(factor * (b2 - b1)); return (r3 & 0xff) << 16 | (g3 & 0xff) << 8 | (b3 & 0xff); } private double normalize(double value, double min, double max) { return ((value - min) / (max - min)); } @SubscribeEvent public void onRenderLast(RenderWorldLastEvent event) { if (!showSouls || !trackSouls || currentLocation == null || closestMissingSouls.isEmpty()) { return; } int closeColor = 0x772991; // 0xa839ce int farColor = 0xCEB4D1; double farSoulDistSq = lastPlayerPos.distanceSq(closestMissingSouls.get(closestMissingSouls.size() - 1)); for (BlockPos currentSoul : closestMissingSouls) { double currentDistSq = lastPlayerPos.distanceSq(currentSoul); double factor = normalize(currentDistSq, 0.0, farSoulDistSq); int rgb = interpolateColors(closeColor, farColor, Math.min(0.40, factor)); RenderUtils.renderBeaconBeamOrBoundingBox(currentSoul, rgb, 1.0f, event.partialTicks); } } public void setShowFairySouls(boolean enabled) { NotEnoughUpdates.INSTANCE.config.misc.fariySoul = enabled; showSouls = enabled; } public void setTrackFairySouls(boolean enabled) { NotEnoughUpdates.INSTANCE.config.misc.trackFairySouls = enabled; trackSouls = enabled; } public void markClosestSoulFound() { if (!trackSouls) return; int closestIndex = -1; double closestDistSq = 10 * 10; for (int i = 0; i < allSoulsInCurrentLocation.size(); i++) { BlockPos pos = allSoulsInCurrentLocation.get(i); double distSq = pos.distanceSq(Minecraft.getMinecraft().thePlayer.getPosition()); if (distSq < closestDistSq) { closestDistSq = distSq; closestIndex = i; } } if (closestIndex != -1) { foundSoulsInLocation.add(closestIndex); refreshMissingSoulInfo(true); } } public void markAllAsFound() { if (!trackSouls) { print(EnumChatFormatting.RED + "Fairy soul tracking is turned off, turn it on using /neu"); return; } if (allSoulsInCurrentLocation == null) { print(EnumChatFormatting.RED + "No fairy souls found in your current world"); return; } for (int i = 0; i < allSoulsInCurrentLocation.size(); i++) { foundSoulsInLocation.add(i); } refreshMissingSoulInfo(true); print(EnumChatFormatting.DARK_PURPLE + "Marked all fairy souls as found"); } public void markAllAsMissing() { if (!trackSouls) { print(EnumChatFormatting.RED + "Fairy soul tracking is turned off, turn it on using /neu"); return; } if (allSoulsInCurrentLocation == null) { print(EnumChatFormatting.RED + "No fairy souls found in your current world"); return; } foundSoulsInLocation.clear(); refreshMissingSoulInfo(true); print(EnumChatFormatting.DARK_PURPLE + "Marked all fairy souls as not found"); } private HashMap> getFoundSoulsForProfile() { String profile = SBInfo.getInstance().currentProfile; if (profile == null) { if (allProfilesFoundSouls.containsKey(unknownProfile)) return allProfilesFoundSouls.get(unknownProfile); } else { profile = profile.toLowerCase(Locale.getDefault()); if (allProfilesFoundSouls.containsKey(unknownProfile)) { HashMap> unknownProfileData = allProfilesFoundSouls.remove(unknownProfile); allProfilesFoundSouls.put(profile, unknownProfileData); return unknownProfileData; } if (allProfilesFoundSouls.containsKey(profile)) { return allProfilesFoundSouls.get(profile); } else { // Create a new entry for this profile HashMap> profileData = new HashMap<>(); allProfilesFoundSouls.put(profile, profileData); return profileData; } } return new HashMap<>(); } private static List loadLocationFairySoulsFromConfig(String currentLocation) { JsonObject fairySoulList = Constants.FAIRYSOULS; if (fairySoulList == null) { return null; } if (!fairySoulList.has(currentLocation) || !fairySoulList.get(currentLocation).isJsonArray()) { return null; } JsonArray locations = fairySoulList.get(currentLocation).getAsJsonArray(); List locationSouls = new ArrayList<>(); for (int i = 0; i < locations.size(); i++) { try { String coord = locations.get(i).getAsString(); String[] split = coord.split(","); if (split.length == 3) { String xS = split[0]; String yS = split[1]; String zS = split[2]; int x = Integer.parseInt(xS); int y = Integer.parseInt(yS); int z = Integer.parseInt(zS); locationSouls.add(new BlockPos(x, y, z)); } } catch (Exception e) { e.printStackTrace(); return null; } } if (locationSouls.size() == 0) { return null; } return locationSouls; } public void loadFoundSoulsForAllProfiles(File file, Gson gson) { allProfilesFoundSouls = new HashMap<>(); String fileContent; try { fileContent = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) .lines() .collect(Collectors.joining(System.lineSeparator())); } catch (FileNotFoundException e) { // it is possible that the collected_fairy_souls.json won't exist return; } try { //noinspection UnstableApiUsage Type multiProfileSoulsType = new TypeToken>>>() {}.getType(); allProfilesFoundSouls = gson.fromJson(fileContent, multiProfileSoulsType); if (allProfilesFoundSouls == null){ allProfilesFoundSouls = new HashMap<>(); } } catch (JsonSyntaxException e) { //The file is in the old format, convert it to the new one and set the profile to unknown try { //noinspection UnstableApiUsage Type singleProfileSoulsType = new TypeToken>>() {}.getType(); allProfilesFoundSouls.put(unknownProfile, gson.fromJson(fileContent, singleProfileSoulsType)); } catch (JsonSyntaxException e2) { System.err.println("Can't read file containing collected fairy souls, resetting."); } } } public void saveFoundSoulsForAllProfiles(File file, Gson gson) { try { //noinspection ResultOfMethodCallIgnored file.createNewFile(); try ( BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), StandardCharsets.UTF_8 )) ) { writer.write(gson.toJson(allProfilesFoundSouls)); } } catch (IOException e) { e.printStackTrace(); } } public void tick() { if (!trackSouls) return; String location = SBInfo.getInstance().getLocation(); if (location == null || location.isEmpty()) return; if (!location.equals(currentLocation)) { currentLocation = location; initializeLocation(); } refreshMissingSoulInfo(false); } private static void print(String s) { Minecraft.getMinecraft().thePlayer.addChatMessage(new ChatComponentText(s)); } private static void printHelp() { print(""); print(EnumChatFormatting.DARK_PURPLE.toString() + EnumChatFormatting.BOLD + " NEU Fairy Soul Waypoint Guide"); print(EnumChatFormatting.LIGHT_PURPLE + "Shows waypoints for every fairy soul in your world"); print(EnumChatFormatting.LIGHT_PURPLE + "Clicking a fairy soul automatically removes it from the list"); if (!NotEnoughUpdates.INSTANCE.config.hidden.dev) { print(EnumChatFormatting.DARK_RED + "" + EnumChatFormatting.OBFUSCATED + "Ab" + EnumChatFormatting.RESET + EnumChatFormatting.DARK_RED + "!" + EnumChatFormatting.RESET + EnumChatFormatting.RED + " This feature cannot and will not work in Dungeons. " + EnumChatFormatting.DARK_RED + "!" + EnumChatFormatting.OBFUSCATED + "Ab"); } print(EnumChatFormatting.GOLD.toString() + EnumChatFormatting.BOLD + " Commands:"); print(EnumChatFormatting.YELLOW + "/neusouls help - Display this message"); print(EnumChatFormatting.YELLOW + "/neusouls on/off - Enable/disable showing waypoint markers"); print(EnumChatFormatting.YELLOW + "/neusouls clear/unclear - Marks every waypoint in your current world as completed/uncompleted"); print(""); } @SubscribeEvent public void onChatReceived(ClientChatReceivedEvent event) { if (!trackSouls || event.type == 2) return; if (event.message.getUnformattedText().equals("You have already found that Fairy Soul!") || event.message.getUnformattedText().equals( "SOUL! You found a Fairy Soul!")) { markClosestSoulFound(); } } public static class FairySoulsCommand extends ClientCommandBase { public FairySoulsCommand() { super("neusouls"); } @Override public List getCommandAliases() { return Collections.singletonList("fairysouls"); } @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { if (args.length != 1) { printHelp(); return; } String subcommand = args[0].toLowerCase(); switch (subcommand) { case "help": printHelp(); break; case "on": case "enable": if (!FairySouls.instance.trackSouls) { print( EnumChatFormatting.RED + "Fairy soul tracking is off, enable it using /neu before using this command"); return; } print(EnumChatFormatting.DARK_PURPLE + "Enabled fairy soul waypoints"); FairySouls.getInstance().setShowFairySouls(true); break; case "off": case "disable": FairySouls.getInstance().setShowFairySouls(false); print(EnumChatFormatting.DARK_PURPLE + "Disabled fairy soul waypoints"); break; case "clear": FairySouls.getInstance().markAllAsFound(); break; case "unclear": FairySouls.getInstance().markAllAsMissing(); break; default: print(EnumChatFormatting.RED + "Unknown subcommand: " + subcommand); } } } }