package me.xmrvizzy.skyblocker.skyblock; import com.google.common.collect.ImmutableSet; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mojang.brigadier.CommandDispatcher; import me.xmrvizzy.skyblocker.SkyblockerMod; import me.xmrvizzy.skyblocker.config.SkyblockerConfig; import me.xmrvizzy.skyblocker.utils.NEURepo; import me.xmrvizzy.skyblocker.utils.PosUtils; import me.xmrvizzy.skyblocker.utils.Utils; import me.xmrvizzy.skyblocker.utils.render.RenderHelper; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.minecraft.client.MinecraftClient; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.text.Text; import net.minecraft.util.DyeColor; import net.minecraft.util.math.BlockPos; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.util.*; import java.util.concurrent.CompletableFuture; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class FairySouls { private static final Logger LOGGER = LoggerFactory.getLogger(FairySouls.class); private static CompletableFuture fairySoulsLoaded; private static int maxSouls = 0; private static final Map> fairySouls = new HashMap<>(); private static final Map>> foundFairies = new HashMap<>(); @SuppressWarnings("UnusedReturnValue") public static CompletableFuture runAsyncAfterFairySoulsLoad(Runnable runnable) { if (fairySoulsLoaded == null) { LOGGER.error("Fairy Souls have not being initialized yet! Please ensure the Fairy Souls module is initialized before modules calling this method in SkyblockerMod#onInitializeClient. This error can be safely ignore in a test environment."); return CompletableFuture.completedFuture(null); } return fairySoulsLoaded.thenRunAsync(runnable); } public static int getFairySoulsSize(@Nullable String location) { return location == null ? maxSouls : fairySouls.get(location).size(); } public static void init() { loadFairySouls(); ClientLifecycleEvents.CLIENT_STOPPING.register(FairySouls::saveFoundFairySouls); ClientCommandRegistrationCallback.EVENT.register(FairySouls::registerCommands); WorldRenderEvents.AFTER_TRANSLUCENT.register(FairySouls::render); ClientReceiveMessageEvents.GAME.register(FairySouls::onChatMessage); } private static void loadFairySouls() { fairySoulsLoaded = NEURepo.runAsyncAfterLoad(() -> { try (BufferedReader reader = new BufferedReader(new FileReader(NEURepo.LOCAL_REPO_DIR.resolve("constants").resolve("fairy_souls.json").toFile()))) { for (Map.Entry fairySoulJson : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) { if (fairySoulJson.getKey().equals("//") || fairySoulJson.getKey().equals("Max Souls")) { if (fairySoulJson.getKey().equals("Max Souls")) { maxSouls = fairySoulJson.getValue().getAsInt(); } continue; } ImmutableSet.Builder fairySoulsForLocation = ImmutableSet.builder(); for (JsonElement fairySoul : fairySoulJson.getValue().getAsJsonArray().asList()) { fairySoulsForLocation.add(PosUtils.parsePosString(fairySoul.getAsString())); } fairySouls.put(fairySoulJson.getKey(), fairySoulsForLocation.build()); } LOGGER.debug("[Skyblocker] Loaded fairy soul locations"); } catch (IOException e) { LOGGER.error("[Skyblocker] Failed to load fairy soul locations", e); } try (BufferedReader reader = new BufferedReader(new FileReader(SkyblockerMod.CONFIG_DIR.resolve("found_fairy_souls.json").toFile()))) { for (Map.Entry foundFairiesForProfileJson : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) { Map> foundFairiesForProfile = new HashMap<>(); for (Map.Entry foundFairiesForLocationJson : foundFairiesForProfileJson.getValue().getAsJsonObject().asMap().entrySet()) { Set foundFairiesForLocation = new HashSet<>(); for (JsonElement foundFairy : foundFairiesForLocationJson.getValue().getAsJsonArray().asList()) { foundFairiesForLocation.add(PosUtils.parsePosString(foundFairy.getAsString())); } foundFairiesForProfile.put(foundFairiesForLocationJson.getKey(), foundFairiesForLocation); } foundFairies.put(foundFairiesForProfileJson.getKey(), foundFairiesForProfile); } LOGGER.debug("[Skyblocker] Loaded found fairy souls"); } catch (FileNotFoundException ignored) { } catch (IOException e) { LOGGER.error("[Skyblocker] Failed to load found fairy souls", e); } }); } private static void saveFoundFairySouls(MinecraftClient client) { try (BufferedWriter writer = new BufferedWriter(new FileWriter(SkyblockerMod.CONFIG_DIR.resolve("found_fairy_souls.json").toFile()))) { JsonObject foundFairiesJson = new JsonObject(); for (Map.Entry>> foundFairiesForProfile : foundFairies.entrySet()) { JsonObject foundFairiesForProfileJson = new JsonObject(); for (Map.Entry> foundFairiesForLocation : foundFairiesForProfile.getValue().entrySet()) { JsonArray foundFairiesForLocationJson = new JsonArray(); for (BlockPos foundFairy : foundFairiesForLocation.getValue()) { foundFairiesForLocationJson.add(PosUtils.getPosString(foundFairy)); } foundFairiesForProfileJson.add(foundFairiesForLocation.getKey(), foundFairiesForLocationJson); } foundFairiesJson.add(foundFairiesForProfile.getKey(), foundFairiesForProfileJson); } SkyblockerMod.GSON.toJson(foundFairiesJson, writer); writer.close(); LOGGER.info("[Skyblocker] Saved found fairy souls"); } catch (IOException e) { LOGGER.error("[Skyblocker] Failed to write found fairy souls to file", e); } } private static void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { dispatcher.register(literal(SkyblockerMod.NAMESPACE) .then(literal("fairySouls") .then(literal("markAllInCurrentIslandFound").executes(context -> { FairySouls.markAllFairiesOnCurrentIslandFound(); context.getSource().sendFeedback(Text.translatable("skyblocker.fairySouls.markAllFound")); return 1; })) .then(literal("markAllInCurrentIslandMissing").executes(context -> { FairySouls.markAllFairiesOnCurrentIslandMissing(); context.getSource().sendFeedback(Text.translatable("skyblocker.fairySouls.markAllMissing")); return 1; })))); } private static void render(WorldRenderContext context) { SkyblockerConfig.FairySouls fairySoulsConfig = SkyblockerConfig.get().general.fairySouls; if (fairySoulsConfig.enableFairySoulsHelper && fairySoulsLoaded.isDone() && fairySouls.containsKey(Utils.getLocationRaw())) { for (BlockPos fairySoulPos : fairySouls.get(Utils.getLocationRaw())) { boolean fairySoulNotFound = isFairySoulMissing(fairySoulPos); if (!fairySoulsConfig.highlightFoundSouls && !fairySoulNotFound || fairySoulsConfig.highlightOnlyNearbySouls && fairySoulPos.getSquaredDistance(context.camera().getPos()) > 2500) { continue; } float[] colorComponents = fairySoulNotFound ? DyeColor.GREEN.getColorComponents() : DyeColor.RED.getColorComponents(); RenderHelper.renderFilledThroughWallsWithBeaconBeam(context, fairySoulPos, colorComponents, 0.5F); } } } private static void onChatMessage(Text text, boolean overlay) { String message = text.getString(); if (message.equals("You have already found that Fairy Soul!") || message.equals("§d§lSOUL! §fYou found a §dFairy Soul§f!")) { markClosestFairyFound(); } } private static void markClosestFairyFound() { if (!fairySoulsLoaded.isDone()) return; PlayerEntity player = MinecraftClient.getInstance().player; if (player == null) { LOGGER.warn("[Skyblocker] Failed to mark closest fairy soul as found because player is null"); return; } fairySouls.get(Utils.getLocationRaw()).stream() .filter(FairySouls::isFairySoulMissing) .min(Comparator.comparingDouble(fairySoulPos -> fairySoulPos.getSquaredDistance(player.getPos()))) .filter(fairySoulPos -> fairySoulPos.getSquaredDistance(player.getPos()) <= 16) .ifPresent(fairySoulPos -> { initializeFoundFairiesForCurrentProfileAndLocation(); foundFairies.get(Utils.getProfile()).get(Utils.getLocationRaw()).add(fairySoulPos); }); } private static boolean isFairySoulMissing(BlockPos fairySoulPos) { Map> foundFairiesForProfile = foundFairies.get(Utils.getProfile()); if (foundFairiesForProfile == null) { return true; } Set foundFairiesForProfileAndLocation = foundFairiesForProfile.get(Utils.getLocationRaw()); if (foundFairiesForProfileAndLocation == null) { return true; } return !foundFairiesForProfileAndLocation.contains(fairySoulPos); } public static void markAllFairiesOnCurrentIslandFound() { initializeFoundFairiesForCurrentProfileAndLocation(); foundFairies.get(Utils.getProfile()).get(Utils.getLocationRaw()).addAll(fairySouls.get(Utils.getLocationRaw())); } public static void markAllFairiesOnCurrentIslandMissing() { Map> foundFairiesForProfile = foundFairies.get(Utils.getProfile()); if (foundFairiesForProfile != null) { foundFairiesForProfile.remove(Utils.getLocationRaw()); } } private static void initializeFoundFairiesForCurrentProfileAndLocation() { initializeFoundFairiesForProfileAndLocation(Utils.getProfile(), Utils.getLocationRaw()); } private static void initializeFoundFairiesForProfileAndLocation(String profile, String location) { foundFairies.computeIfAbsent(profile, profileKey -> new HashMap<>()); foundFairies.get(profile).computeIfAbsent(location, locationKey -> new HashSet<>()); } }