/* * 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.autosubscribe.NEUAutoSubscribe; import io.github.moulberry.notenoughupdates.core.config.ConfigUtil; import io.github.moulberry.notenoughupdates.core.util.StringUtils; import io.github.moulberry.notenoughupdates.core.util.render.RenderUtils; import io.github.moulberry.notenoughupdates.util.Constants; import io.github.moulberry.notenoughupdates.util.SBInfo; import io.github.moulberry.notenoughupdates.util.Utils; import lombok.var; import net.minecraft.client.Minecraft; import net.minecraft.util.BlockPos; 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.EventPriority; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.lang.reflect.Type; import java.nio.file.Files; import java.util.ArrayList; 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; @NEUAutoSubscribe 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; } public boolean isTrackSouls() { return trackSouls; } public boolean isShowSouls() { return showSouls; } @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)); rgb = rgb | 0x66000000; RenderUtils.renderBeaconBeamOrBoundingBox(currentSoul, rgb, 1.0f, event.partialTicks); if (NotEnoughUpdates.INSTANCE.config.misc.fairySoulWaypointDistance) RenderUtils.renderWayPoint(currentSoul, 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 || allSoulsInCurrentLocation == null || allSoulsInCurrentLocation.isEmpty()) 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 != null) { 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 (BufferedReader br = Files.newBufferedReader(file.toPath())) { fileContent = br.lines().collect(Collectors.joining(System.lineSeparator())); } catch (IOException 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) { ConfigUtil.saveConfig(allProfilesFoundSouls, file, gson); } 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) { Utils.addChatMessage(s); } @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) public void onChatReceived(ClientChatReceivedEvent event) { if (!trackSouls || event.type == 2) return; var cleanString = StringUtils.cleanColour(event.message.getUnformattedText()); if (cleanString.equals("You have already found that Fairy Soul!") || cleanString.equals("SOUL! You found a Fairy Soul!")) { markClosestSoulFound(); } } }