diff options
| author | Rime <81419447+Emirlol@users.noreply.github.com> | 2025-02-27 23:49:13 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-28 04:49:13 +0800 |
| commit | e2659c8ced1685fa0c231195db53df45500a6144 (patch) | |
| tree | 4a1c21e14153f8ec893d588509d43a72a322d2d4 /src/main/java/de | |
| parent | fc478774143a73aa1470e6348d75896231fa21ca (diff) | |
| download | Skyblocker-e2659c8ced1685fa0c231195db53df45500a6144.tar.gz Skyblocker-e2659c8ced1685fa0c231195db53df45500a6144.tar.bz2 Skyblocker-e2659c8ced1685fa0c231195db53df45500a6144.zip | |
Corpse profit tracker (#1152)
* Corpse profit tracker initial commit
* A bunch of stuff
- Extract `CorpseLoot` and `Reward` inner classes into separate classes
- Use translatables where it makes sense
- Add config option to toggle corpse profit tracker (can't believe I forgot this on corpse tracker as well)
- Some minor command adjustments and fixes to both `PowderMiningTracker` and `CorpseProfitTracker`
- Refactored `CorpseProfitHistoryScreen` into `CorpseProfitScreen`. The button that switched from one to the other is now a view switch button that changes the displayed list, and functionality of both screens is combined in one.
- Add hover/click events to the profit text sent when a corpse is looted that opens the screen
* Round the chat message so it's more copy-friendly
* Fix keys' total price not being multiplied by key amount
* Fix `pricePerUnit` display of items to match keys
This way, they don't display the price per unit when the amount is 1.
Diffstat (limited to 'src/main/java/de')
13 files changed, 1266 insertions, 169 deletions
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java index 3eed81da..4b7d198d 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java @@ -6,7 +6,7 @@ import de.hysky.skyblocker.config.configs.MiningConfig; import de.hysky.skyblocker.config.screens.powdertracker.PowderFilterConfigScreen; import de.hysky.skyblocker.skyblock.dwarven.CrystalsHudWidget; import de.hysky.skyblocker.skyblock.dwarven.CarpetHighlighter; -import de.hysky.skyblocker.skyblock.dwarven.PowderMiningTracker; +import de.hysky.skyblocker.skyblock.dwarven.profittrackers.PowderMiningTracker; import de.hysky.skyblocker.skyblock.tabhud.widget.CommsWidget; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.ColorControllerBuilder; @@ -122,12 +122,23 @@ public class MiningCategory { newValue -> config.mining.crystalHollows.chestHighlightColor = newValue) .controller(v -> ColorControllerBuilder.create(v).allowAlpha(true)) .build()) - .option(ButtonOption.createBuilder() - .name(Text.translatable("skyblocker.config.mining.crystalHollows.powderTrackerFilter")) - .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.crystalHollows.powderTrackerFilter.@Tooltip"))) - .text(Text.translatable("text.skyblocker.open")) - .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new PowderFilterConfigScreen(screen, new ObjectImmutableList<>(PowderMiningTracker.getName2IdMap().keySet())))) - .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.mining.crystalHollows.enablePowderTracker")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.crystalHollows.enablePowderTracker.@Tooltip"))) + .binding(defaults.mining.crystalHollows.enablePowderTracker, + () -> config.mining.crystalHollows.enablePowderTracker, + newValue -> { + config.mining.crystalHollows.enablePowderTracker = newValue; + if (newValue) PowderMiningTracker.INSTANCE.recalculateAll(); + }) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(ButtonOption.createBuilder() + .name(Text.translatable("skyblocker.config.mining.crystalHollows.powderTrackerFilter")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.crystalHollows.powderTrackerFilter.@Tooltip"))) + .text(Text.translatable("text.skyblocker.open")) + .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new PowderFilterConfigScreen(screen, new ObjectImmutableList<>(PowderMiningTracker.getName2IdMap().keySet())))) + .build()) .build()) //Crystal Hollows Map @@ -287,6 +298,14 @@ public class MiningCategory { newValue -> config.mining.glacite.autoShareCorpses = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.mining.glacite.enableCorpseProfitTracker")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.glacite.enableCorpseProfitTracker.@Tooltip"))) + .binding(defaults.mining.glacite.enableCorpseProfitTracker, + () -> config.mining.glacite.enableCorpseProfitTracker, + newValue -> config.mining.glacite.enableCorpseProfitTracker = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .build()) .build(); } diff --git a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java index 34c59429..33ed1b05 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java @@ -173,6 +173,9 @@ public class MiningConfig { @SerialEntry public boolean autoShareCorpses = false; + + @SerialEntry + public boolean enableCorpseProfitTracker = true; } /** diff --git a/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java b/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java index fbd2668a..a5c90a2a 100644 --- a/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java +++ b/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java @@ -1,7 +1,7 @@ package de.hysky.skyblocker.config.screens.powdertracker; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.dwarven.PowderMiningTracker; +import de.hysky.skyblocker.skyblock.dwarven.profittrackers.PowderMiningTracker; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; @@ -63,7 +63,7 @@ public class PowderFilterConfigScreen extends Screen { public void saveFilters() { SkyblockerConfigManager.get().mining.crystalHollows.powderTrackerFilter = filters; SkyblockerConfigManager.save(); - PowderMiningTracker.recalculateAll(); + PowderMiningTracker.INSTANCE.recalculateAll(); } @Override diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CorpseFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CorpseFinder.java index f3dd9aab..3f4ec90a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CorpseFinder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CorpseFinder.java @@ -1,13 +1,12 @@ package de.hysky.skyblocker.skyblock.dwarven; import com.mojang.brigadier.Command; -import com.mojang.brigadier.context.CommandContext; -import com.mojang.serialization.Codec; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.skyblock.dwarven.CorpseType.CorpseTypeArgumentType; import de.hysky.skyblocker.utils.*; import de.hysky.skyblocker.utils.command.argumenttypes.blockpos.ClientBlockPosArgumentType; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; @@ -19,7 +18,6 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; 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.argument.EnumArgumentType; import net.minecraft.entity.Entity; import net.minecraft.entity.EquipmentSlot; import net.minecraft.entity.decoration.ArmorStandEntity; @@ -27,7 +25,6 @@ import net.minecraft.text.ClickEvent; import net.minecraft.text.HoverEvent; import net.minecraft.text.Text; import net.minecraft.util.Formatting; -import net.minecraft.util.StringIdentifiable; import net.minecraft.util.Util; import net.minecraft.util.math.BlockPos; import org.apache.commons.lang3.EnumUtils; @@ -49,10 +46,6 @@ public class CorpseFinder { private static final String PREFIX = "[Skyblocker Corpse Finder] "; private static final Logger LOGGER = LoggerFactory.getLogger(CorpseFinder.class); private static final Map<CorpseType, List<Corpse>> corpsesByType = new EnumMap<>(CorpseType.class); - private static final String LAPIS_HELMET = "LAPIS_ARMOR_HELMET"; - private static final String UMBER_HELMET = "ARMOR_OF_YOG_HELMET"; - private static final String TUNGSTEN_HELMET = "MINERAL_HELMET"; - private static final String VANGUARD_HELMET = "VANGUARD_HELMET"; @Init public static void init() { @@ -78,9 +71,9 @@ public class CorpseFinder { .then(literal("corpseHelper") .then(literal("shareLocation") .then(argument("blockPos", ClientBlockPosArgumentType.blockPos()) - .then(argument("corpseType", CorpseType.CorpseTypeArgumentType.corpseType()) + .then(argument("corpseType", CorpseTypeArgumentType.corpseType()) .executes(context -> { - shareLocation(ClientBlockPosArgumentType.getBlockPos(context, "blockPos"), CorpseType.CorpseTypeArgumentType.getCorpseType(context, "corpseType")); + shareLocation(ClientBlockPosArgumentType.getBlockPos(context, "blockPos"), CorpseTypeArgumentType.getCorpseType(context, "corpseType")); return Command.SINGLE_SUCCESS; }) ) @@ -250,50 +243,6 @@ public class CorpseFinder { } } - enum CorpseType implements StringIdentifiable { - LAPIS(LAPIS_HELMET, Formatting.BLUE), // dark blue looks bad and these two never exist in same shaft - UMBER(UMBER_HELMET, Formatting.RED), - TUNGSTEN(TUNGSTEN_HELMET, Formatting.GRAY), - VANGUARD(VANGUARD_HELMET, Formatting.BLUE), - UNKNOWN("UNKNOWN", Formatting.YELLOW); - private static final Codec<CorpseType> CODEC = StringIdentifiable.createCodec(CorpseType::values); - private final String helmetItemId; - private final Formatting color; - - CorpseType(String helmetItemId, Formatting color) { - this.helmetItemId = helmetItemId; - this.color = color; - } - - static CorpseType fromHelmetItemId(String helmetItemId) { - for (CorpseType value : values()) { - if (value.helmetItemId.equals(helmetItemId)) { - return value; - } - } - return UNKNOWN; - } - - @Override - public String asString() { - return name().toLowerCase(); - } - - static class CorpseTypeArgumentType extends EnumArgumentType<CorpseType> { - protected CorpseTypeArgumentType() { - super(CODEC, CorpseType::values); - } - - static CorpseTypeArgumentType corpseType() { - return new CorpseTypeArgumentType(); - } - - static <S> CorpseType getCorpseType(CommandContext<S> context, String name) { - return context.getArgument(name, CorpseType.class); - } - } - } - static class Corpse { private final ArmorStandEntity entity; /** diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CorpseType.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CorpseType.java new file mode 100644 index 00000000..b48ff153 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CorpseType.java @@ -0,0 +1,71 @@ +package de.hysky.skyblocker.skyblock.dwarven; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.serialization.Codec; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.command.argument.EnumArgumentType; +import net.minecraft.util.Formatting; +import net.minecraft.util.StringIdentifiable; + +public enum CorpseType implements StringIdentifiable { + LAPIS("LAPIS_ARMOR_HELMET", null, Formatting.BLUE), // dark blue looks bad and these two never exist in same shaft + UMBER("ARMOR_OF_YOG_HELMET", "UMBER_KEY", Formatting.GOLD), + TUNGSTEN("MINERAL_HELMET", "TUNGSTEN_KEY", Formatting.GRAY), + VANGUARD("VANGUARD_HELMET", "SKELETON_KEY", Formatting.AQUA), + UNKNOWN("UNKNOWN", null, Formatting.RED); + + public static final Codec<CorpseType> CODEC = StringIdentifiable.createCodec(CorpseType::values); + public final String helmetItemId; + public final String keyItemId; + public final Formatting color; + + CorpseType(String helmetItemId, String keyItemId, Formatting color) { + this.helmetItemId = helmetItemId; + this.keyItemId = keyItemId; + this.color = color; + } + + static CorpseType fromHelmetItemId(String helmetItemId) { + for (CorpseType value : values()) { + if (value.helmetItemId.equals(helmetItemId)) { + return value; + } + } + return UNKNOWN; + } + + @Override + public String asString() { + return name().toLowerCase(); + } + + /** + * @return the price of the key item for this corpse type + * @throws IllegalStateException when there's no price found for the key item, or when the corpse type is UNKNOWN + */ + public double getKeyPrice() throws IllegalStateException { + return switch (this) { + case UNKNOWN -> throw new IllegalStateException("There's no key or key price for the UNKNOWN corpse type!"); + case LAPIS -> 0; // Lapis corpses don't need a key + default -> { + var result = ItemUtils.getItemPrice(keyItemId); + if (!result.rightBoolean()) throw new IllegalStateException("No price found for key item `" + keyItemId + "`!"); + yield result.leftDouble(); + } + }; + } + + public static class CorpseTypeArgumentType extends EnumArgumentType<CorpseType> { + protected CorpseTypeArgumentType() { + super(CODEC, CorpseType::values); + } + + static CorpseTypeArgumentType corpseType() { + return new CorpseTypeArgumentType(); + } + + static <S> CorpseType getCorpseType(CommandContext<S> context, String name) { + return context.getArgument(name, CorpseType.class); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/profittrackers/AbstractProfitTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/profittrackers/AbstractProfitTracker.java new file mode 100644 index 00000000..172764e6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/profittrackers/AbstractProfitTracker.java @@ -0,0 +1,26 @@ +package de.hysky.skyblocker.skyblock.dwarven.profittrackers; + +import de.hysky.skyblocker.SkyblockerMod; + +import java.nio.file.Path; +import java.util.regex.Pattern; + +/** + * Abstract class for profit trackers that use the chat messages. + * <br> + * There isn't meant to be much inheritance from this class, it's more of a util class that provides some common methods. + */ +public abstract class AbstractProfitTracker { + private static final String REWARD_TRACKERS_DIR = "reward-trackers"; + protected static final Pattern REWARD_PATTERN = Pattern.compile(" {4}(.*?) ?x?([\\d,]*)"); + protected static final Pattern HOTM_XP_PATTERN = Pattern.compile(" {4}\\+[\\d,]+ HOTM Experience"); + protected static final Pattern GEMSTONE_SYMBOLS = Pattern.compile("[α☘☠✎✧❁❂❈❤⸕] "); + + protected static String replaceGemstoneSymbols(String reward) { + return GEMSTONE_SYMBOLS.matcher(reward).replaceAll(""); + } + + protected Path getRewardFilePath(String fileName) { + return SkyblockerMod.CONFIG_DIR.resolve(REWARD_TRACKERS_DIR).resolve(fileName); // 2 resolve calls to avoid the need for a possibly confusing / placement + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/PowderMiningTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/profittrackers/PowderMiningTracker.java index 121422d5..10b9d776 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/PowderMiningTracker.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/profittrackers/PowderMiningTracker.java @@ -1,5 +1,6 @@ -package de.hysky.skyblocker.skyblock.dwarven; +package de.hysky.skyblocker.skyblock.dwarven.profittrackers; +import com.mojang.brigadier.Command; import com.mojang.serialization.Codec; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.annotations.Init; @@ -9,13 +10,11 @@ import de.hysky.skyblocker.events.HudRenderEvents; import de.hysky.skyblocker.events.ItemPriceUpdateEvent; import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; -import de.hysky.skyblocker.utils.CodecUtils; -import de.hysky.skyblocker.utils.ItemUtils; -import de.hysky.skyblocker.utils.Location; -import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.*; import de.hysky.skyblocker.utils.profile.ProfiledData; import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair; import it.unimi.dsi.fastutil.objects.*; +import it.unimi.dsi.fastutil.objects.Object2IntMap.Entry; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; @@ -30,105 +29,125 @@ import org.jetbrains.annotations.Unmodifiable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Path; import java.text.NumberFormat; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; -import java.util.regex.Pattern; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; -public class PowderMiningTracker { +public final class PowderMiningTracker extends AbstractProfitTracker { + public static final PowderMiningTracker INSTANCE = new PowderMiningTracker(); private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Powder Mining Tracker"); - private static final Pattern GEMSTONE_SYMBOLS = Pattern.compile("[α☘☠✎✧❁❂❈❤⸕] "); - private static final Pattern REWARD_PATTERN = Pattern.compile(" {4}(.*?) ?x?([\\d,]*)"); private static final Codec<Object2IntMap<String>> REWARDS_CODEC = CodecUtils.object2IntMapCodec(Codec.STRING); private static final Object2ObjectArrayMap<String, String> NAME2ID_MAP = new Object2ObjectArrayMap<>(50); - // This constructor takes in a comparator that is triggered to decide where to add the element in the tree map - // This causes it to be sorted at all times. This is for rendering them in a sort of easy-to-read manner. - private static final Object2IntAVLTreeMap<Text> SHOWN_REWARDS = new Object2IntAVLTreeMap<>(Comparator.<Text>comparingInt(text -> comparePriority(text.getString())).thenComparing(Text::getString)); - - /** - * Holds the total reward maps for all accounts and profiles. {@link #currentProfileRewards} is a subset of this map, updated on profile change. - */ - private static final ProfiledData<Object2IntMap<String>> ALL_REWARDS = new ProfiledData<>(getRewardFilePath(), REWARDS_CODEC); - /** * <p> * Holds the total amount of each reward obtained for the current profile. - * If any items are filtered out, they are still added to this map but not to the {@link #SHOWN_REWARDS} map. - * Once the filter is changed, the {@link #SHOWN_REWARDS} map is cleared and recalculated based on this map. + * If any items are filtered out, they are still added to this map but not to the {@link #shownRewards} map. + * Once the filter is changed, the {@link #shownRewards} map is cleared and recalculated based on this map. * </p> * <p>This is similar to how {@link ChatHud#messages} and {@link ChatHud#visibleMessages} behave.</p> * * @implNote This is a map of item IDs to the amount of that item obtained. */ @SuppressWarnings("JavadocReference") - private static Object2IntMap<String> currentProfileRewards = new Object2IntOpenHashMap<>(); - private static boolean insideChestMessage = false; - private static double profit = 0; + private Object2IntMap<String> currentProfileRewards = new Object2IntOpenHashMap<>(); + + // This constructor takes in a comparator that is triggered to decide where to add the element in the tree map + // This causes it to be sorted at all times. This is for rendering them in a sort of easy-to-read manner. + private final Object2IntAVLTreeMap<Text> shownRewards = new Object2IntAVLTreeMap<>(Comparator.<Text>comparingInt(text -> comparePriority(text.getString())).thenComparing(Text::getString)); + + /** + * Holds the total reward maps for all accounts and profiles. {@link #currentProfileRewards} is a subset of this map, updated on profile change. + */ + private final ProfiledData<Object2IntMap<String>> allRewards = new ProfiledData<>(getRewardFilePath("powder-mining.json"), REWARDS_CODEC); + private boolean insideChestMessage = false; + private double profit = 0; + + private PowderMiningTracker() {} // Singleton @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private static boolean isEnabled() { + public boolean isEnabled() { return SkyblockerConfigManager.get().mining.crystalHollows.enablePowderTracker; } @Init public static void init() { - ChatEvents.RECEIVE_STRING.register(PowderMiningTracker::onChatMessage); + ChatEvents.RECEIVE_STRING.register(INSTANCE::onChatMessage); HudRenderEvents.AFTER_MAIN_HUD.register(PowderMiningTracker::render); - - ItemPriceUpdateEvent.ON_PRICE_UPDATE.register(() -> { - if (isEnabled()) recalculatePrices(); - }); - - ALL_REWARDS.init(); - - SkyblockEvents.PROFILE_CHANGE.register(PowderMiningTracker::onProfileChange); - SkyblockEvents.PROFILE_INIT.register(PowderMiningTracker::onProfileInit); - - //TODO: Sort out proper commands for this - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register( - literal(SkyblockerMod.NAMESPACE) - .then( - literal("clearrewards") - .executes(context -> { - SHOWN_REWARDS.clear(); - currentProfileRewards.clear(); - profit = 0; - return 1; - }) + ItemPriceUpdateEvent.ON_PRICE_UPDATE.register(INSTANCE::onPriceUpdate); + + INSTANCE.allRewards.init(); + + // @formatter:off // Don't you hate it when your format style for chained method calls makes a chain like this incredibly ugly? + ClientCommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> dispatcher.register( + literal(SkyblockerMod.NAMESPACE) + .then(literal("rewardTrackers") + .then(literal("powderMining") + .then(literal("list") + .executes(ctx -> { + if (INSTANCE.currentProfileRewards.isEmpty()) { + ctx.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.powderTracker.emptyHistory").formatted(Formatting.RED))); + return Command.SINGLE_SUCCESS; + } else if (INSTANCE.shownRewards.isEmpty()) { + ctx.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.powderTracker.rewardsFilteredOut").formatted(Formatting.RED))); + return Command.SINGLE_SUCCESS; + } + + for (Entry<Text> entry : INSTANCE.shownRewards.object2IntEntrySet()) { + ctx.getSource().sendFeedback( + Text.empty() + .append(entry.getKey()) + .append(Text.literal(": ").formatted(Formatting.GRAY)) + .append(Text.literal(String.valueOf(entry.getIntValue())))); + } + ctx.getSource().sendFeedback(Text.translatable("skyblocker.powderTracker.profit", NumberFormat.getInstance().format(INSTANCE.profit)).formatted(Formatting.GOLD)); + return Command.SINGLE_SUCCESS; + }) ) - .then( - literal("listrewards") - .executes(context -> { - var set = SHOWN_REWARDS.object2IntEntrySet(); - for (Object2IntMap.Entry<Text> entry : set) { - MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(entry.getKey().copy().append(" ").append(Text.of(String.valueOf(entry.getIntValue())))); - } - return 1; - }) + .then(literal("reset") + .executes(ctx -> { + INSTANCE.currentProfileRewards.clear(); + INSTANCE.allRewards.save(); + ctx.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.powderTracker.historyReset").formatted(Formatting.GREEN))); + return Command.SINGLE_SUCCESS; + }) ) - )); + ) + ) + )); // @formatter:on + + SkyblockEvents.PROFILE_CHANGE.register(INSTANCE::onProfileChange); + SkyblockEvents.PROFILE_INIT.register(INSTANCE::onProfileInit); + } + + private void onProfileChange(String prevProfileId, String newProfileId) { + onProfileInit(newProfileId); + } + + private void onProfileInit(String profileId) { + if (!isEnabled()) return; + currentProfileRewards = allRewards.computeIfAbsent(Object2IntArrayMap::new); + recalculateAll(); } - private static void onChatMessage(String text) { - if (Utils.getLocation() != Location.CRYSTAL_HOLLOWS || !isEnabled()) return; + private void onChatMessage(String message) { + if (Utils.getLocation() != Location.CRYSTAL_HOLLOWS || !INSTANCE.isEnabled()) return; // Reward messages end with a separator like so - if (insideChestMessage && text.equals("▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")) { + if (insideChestMessage && message.equals("▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")) { insideChestMessage = false; return; } - if (!insideChestMessage && (text.equals(" CHEST LOCKPICKED ") || (SkyblockerConfigManager.get().mining.crystalHollows.countNaturalChestsInTracker && text.equals(" LOOT CHEST COLLECTED ")))) { + if (!insideChestMessage && (message.equals(" CHEST LOCKPICKED ") || (SkyblockerConfigManager.get().mining.crystalHollows.countNaturalChestsInTracker && message.equals(" LOOT CHEST COLLECTED ")))) { insideChestMessage = true; return; } if (!insideChestMessage) return; - Matcher matcher = REWARD_PATTERN.matcher(text); + Matcher matcher = REWARD_PATTERN.matcher(message); if (!matcher.matches()) return; String itemName = matcher.group(1); int amount = NumberUtils.toInt(matcher.group(2).replace(",", ""), 1); @@ -142,35 +161,25 @@ public class PowderMiningTracker { calculateProfitForItem(itemId, amount); } - private static void onProfileChange(String prevProfileId, String newProfileId) { - onProfileInit(newProfileId); - } - - private static void onProfileInit(String profileId) { - if (!isEnabled()) return; - currentProfileRewards = ALL_REWARDS.computeIfAbsent(Object2IntArrayMap::new); - recalculateAll(); - } - - private static void incrementReward(String itemName, String itemId, int amount) { + private void incrementReward(String itemName, String itemId, int amount) { currentProfileRewards.mergeInt(itemId, amount, Integer::sum); - if (SkyblockerConfigManager.get().mining.crystalHollows.powderTrackerFilter.contains(itemName)) return; - if (itemId.equals("GEMSTONE_POWDER")) { - SHOWN_REWARDS.merge(Text.literal("Gemstone Powder").formatted(Formatting.LIGHT_PURPLE), amount, Integer::sum); - } else { - ItemStack stack = ItemRepository.getItemStack(itemId); - if (stack == null) { - LOGGER.warn("Item stack for id `{}` is null! This might be caused by failed item repository downloads.", itemId); - return; + if (!SkyblockerConfigManager.get().mining.crystalHollows.powderTrackerFilter.contains(itemName)) { + if (itemId.equals("GEMSTONE_POWDER")) { + shownRewards.merge(Text.literal("Gemstone Powder").formatted(Formatting.LIGHT_PURPLE), amount, Integer::sum); + } else { + ItemStack stack = ItemRepository.getItemStack(itemId); + if (stack == null) { + LOGGER.warn("Item stack for id `{}` is null! This might be caused by failed item repository downloads.", itemId); + return; + } + shownRewards.merge(stack.getName(), amount, Integer::sum); } - SHOWN_REWARDS.merge(stack.getName(), amount, Integer::sum); } } - private static int comparePriority(String string) { - string = GEMSTONE_SYMBOLS.matcher(string).replaceAll(""); // Removes the gemstone symbol from the string to make it easier to compare + private static int comparePriority(String itemName) { // Puts gemstone powder at the top of the list, then gold and diamond essence, then gemstones by ascending rarity and then whatever else. - return switch (string) { + return switch (replaceGemstoneSymbols(itemName)) { case "Gemstone Powder" -> 1; case "Gold Essence" -> 2; case "Diamond Essence" -> 3; @@ -182,10 +191,14 @@ public class PowderMiningTracker { }; } + private void onPriceUpdate() { + if (isEnabled()) recalculatePrices(); + } + /** * Normally, the price is calculated on a per-reward basis as they are obtained. This is what this method does. */ - private static void calculateProfitForItem(String itemId, int amount) { + private void calculateProfitForItem(String itemId, int amount) { DoubleBooleanPair price = ItemUtils.getItemPrice(itemId); if (price.rightBoolean()) profit += price.leftDouble() * amount; } @@ -193,35 +206,36 @@ public class PowderMiningTracker { /** * When the bz/ah prices are updated, this method recalculates the profit for all rewards at once. */ - private static void recalculatePrices() { + private void recalculatePrices() { profit = 0; - ObjectSortedSet<Object2IntMap.Entry<Text>> set = SHOWN_REWARDS.object2IntEntrySet(); - for (Object2IntMap.Entry<Text> entry : set) { + ObjectSortedSet<Entry<Text>> set = shownRewards.object2IntEntrySet(); + for (Entry<Text> entry : set) { calculateProfitForItem(getItemId(entry.getKey().getString()), entry.getIntValue()); } } /** - * Resets the shown rewards and profit to 0 and recalculates rewards for the current profile based on the config filter. + * <p>Resets the shown rewards and profit to 0 and recalculates rewards for the current profile based on the config filter.</p> + * <p>This is also called from the config when the feature is enabled, as the periodic recalculation doesn't happen when the feature is disabled.</p> */ - public static void recalculateAll() { - SHOWN_REWARDS.clear(); - ObjectSet<Object2IntMap.Entry<String>> set = currentProfileRewards.object2IntEntrySet(); + public void recalculateAll() { + shownRewards.clear(); + ObjectSet<Entry<String>> set = currentProfileRewards.object2IntEntrySet(); // The filters are actually item names so that they would look nice and not need a lot of mapping under the screen code // Here they are converted to item IDs for comparison - List<String> filters = SkyblockerConfigManager.get().mining.crystalHollows.powderTrackerFilter.stream().map(PowderMiningTracker::getItemId).toList(); - for (Object2IntMap.Entry<String> entry : set) { + List<String> filters = SkyblockerConfigManager.get().mining.crystalHollows.powderTrackerFilter.stream().map(INSTANCE::getItemId).toList(); + for (Entry<String> entry : set) { if (filters.contains(entry.getKey())) continue; if (entry.getKey().equals("GEMSTONE_POWDER")) { - SHOWN_REWARDS.put(Text.literal("Gemstone Powder").formatted(Formatting.LIGHT_PURPLE), entry.getIntValue()); + shownRewards.put(Text.literal("Gemstone Powder").formatted(Formatting.LIGHT_PURPLE), entry.getIntValue()); } else { ItemStack stack = ItemRepository.getItemStack(entry.getKey()); if (stack == null) { LOGGER.warn("Item stack for id `{}` is null! This might be caused by failed item repository downloads.", entry.getKey()); continue; } - SHOWN_REWARDS.put(stack.getName(), entry.getIntValue()); + shownRewards.put(stack.getName(), entry.getIntValue()); } } recalculatePrices(); @@ -232,6 +246,7 @@ public class PowderMiningTracker { return Object2ObjectMaps.unmodifiable(NAME2ID_MAP); } + // TODO: Perhaps make a little something in the skyblocker-assets repo for this in case it needs updating in the future static { NAME2ID_MAP.put("Gemstone Powder", "GEMSTONE_POWDER"); // Not an actual item, but since we're using IDs for mapping to colored text we need to have this here @@ -297,23 +312,20 @@ public class PowderMiningTracker { } @NotNull - private static String getItemId(String itemName) { + private String getItemId(String itemName) { return NAME2ID_MAP.getOrDefault(itemName, ""); } - private static Path getRewardFilePath() { - return SkyblockerMod.CONFIG_DIR.resolve("reward-trackers/powder-mining.json"); - } - + // TODO: Make this a hud widget without the background (optional), needs to be moveable private static void render(DrawContext context, RenderTickCounter tickCounter) { - if (Utils.getLocation() != Location.CRYSTAL_HOLLOWS || !isEnabled()) return; + if (Utils.getLocation() != Location.CRYSTAL_HOLLOWS || !INSTANCE.isEnabled()) return; int y = MinecraftClient.getInstance().getWindow().getScaledHeight() / 2 - 100; - var set = SHOWN_REWARDS.object2IntEntrySet(); - for (Object2IntMap.Entry<Text> entry : set) { + |
