From 76fab51660b27f649cb960628bb66cec357c77eb Mon Sep 17 00:00:00 2001 From: Taken Date: Sat, 2 May 2026 21:05:03 +0200 Subject: [PATCH 1/7] Moved denicker to allow for easier adding of others --- src/main/java/com/roxiun/mellow/Mellow.java | 2 +- .../mellow/commands/AuroraDenickCommand.java | 142 ++++++++++++++++++ .../roxiun/mellow/commands/DenickCommand.java | 140 +---------------- .../roxiun/mellow/commands/MellowCommand.java | 2 +- .../roxiun/mellow/config/MellowOneConfig.java | 86 ++++++++--- .../mellow/feature/nicks/NumberDenicker.java | 17 +-- 6 files changed, 212 insertions(+), 177 deletions(-) create mode 100644 src/main/java/com/roxiun/mellow/commands/AuroraDenickCommand.java diff --git a/src/main/java/com/roxiun/mellow/Mellow.java b/src/main/java/com/roxiun/mellow/Mellow.java index bdede05..5d5b695 100644 --- a/src/main/java/com/roxiun/mellow/Mellow.java +++ b/src/main/java/com/roxiun/mellow/Mellow.java @@ -242,7 +242,7 @@ public void run() { new RefreshCommand(inGameTabStatsSyncService) ); ClientCommandHandler.instance.registerCommand( - new DenickCommand(config, auroraApi) + new AuroraDenickCommand(config, auroraApi) ); ClientCommandHandler.instance.registerCommand( new SkinDenickCommand(playerCache) diff --git a/src/main/java/com/roxiun/mellow/commands/AuroraDenickCommand.java b/src/main/java/com/roxiun/mellow/commands/AuroraDenickCommand.java new file mode 100644 index 0000000..e44f810 --- /dev/null +++ b/src/main/java/com/roxiun/mellow/commands/AuroraDenickCommand.java @@ -0,0 +1,142 @@ +package com.roxiun.mellow.commands; + +import com.roxiun.mellow.api.aurora.AuroraApi; +import com.roxiun.mellow.config.MellowOneConfig; +import com.roxiun.mellow.core.async.AsyncExecutor; +import com.roxiun.mellow.core.async.MainThreadDispatcher; +import com.roxiun.mellow.util.ChatUtils; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import net.minecraft.command.CommandBase; +import net.minecraft.command.ICommandSender; +import net.minecraft.util.BlockPos; + +public class AuroraDenickCommand extends CommandBase { + + private final MellowOneConfig config; + private final AuroraApi auroraApi; + + public AuroraDenickCommand(MellowOneConfig config, AuroraApi auroraApi) { + this.config = config; + this.auroraApi = auroraApi; + } + + @Override + public String getCommandName() { + return "auroradenick"; + } + + @Override + public String getCommandUsage(ICommandSender sender) { + return "/auroradenick "; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) { + if (args.length != 2) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid usage. Use: " + getCommandUsage(sender) + ); + return; + } + + String type = args[0]; + if ( + !type.equalsIgnoreCase("finals") && !type.equalsIgnoreCase("beds") + ) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid type. Use 'finals' or 'beds'." + ); + return; + } + + String numberStr = args[1]; + try { + Integer.parseInt(numberStr.replace(",", "")); + } catch (NumberFormatException e) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid number: " + numberStr + ); + return; + } + + ChatUtils.sendCommandMessage(sender, "§aSearching for players..."); + + AsyncExecutor.getInstance().command(() -> { + try { + int range = config.getAuroraDenickRange(type); + int max = config.getAuroraDenickMaxResults(); + + AuroraApi.AuroraResponse response = auroraApi.queryStats( + type, + numberStr, + range, + max, + config.auroraApiKey + ); + + MainThreadDispatcher.run(() -> { + if (response != null && response.success) { + if (response.data.isEmpty()) { + ChatUtils.sendCommandMessage( + sender, + "§cNo players found." + ); + } else { + String players = response.data + .stream() + .map( + p -> + "§a" + + p.name + + " §7(distance: " + + p.distance + + ")" + ) + .collect(Collectors.joining(", ")); + ChatUtils.sendCommandMessage( + sender, + "§aFound players: " + players + ); + } + } else { + ChatUtils.sendCommandMessage( + sender, + "§cError fetching data from Aurora API." + ); + } + }); + } catch (IOException e) { + MainThreadDispatcher.run(() -> { + ChatUtils.sendCommandMessage( + sender, + "§cAn error occurred while fetching data." + ); + }); + e.printStackTrace(); + } + }); + } + + @Override + public List addTabCompletionOptions( + ICommandSender sender, + String[] args, + BlockPos pos + ) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, "finals", "beds"); + } + return null; + } + + @Override + public int getRequiredPermissionLevel() { + return 0; + } +} + diff --git a/src/main/java/com/roxiun/mellow/commands/DenickCommand.java b/src/main/java/com/roxiun/mellow/commands/DenickCommand.java index 0f1893a..7a24499 100644 --- a/src/main/java/com/roxiun/mellow/commands/DenickCommand.java +++ b/src/main/java/com/roxiun/mellow/commands/DenickCommand.java @@ -1,154 +1,22 @@ package com.roxiun.mellow.commands; -import com.roxiun.mellow.api.aurora.AuroraApi; -import com.roxiun.mellow.config.MellowOneConfig; -import com.roxiun.mellow.core.async.AsyncExecutor; -import com.roxiun.mellow.core.async.MainThreadDispatcher; -import com.roxiun.mellow.util.ChatUtils; -import java.io.IOException; -import java.util.List; -import java.util.stream.Collectors; import net.minecraft.command.CommandBase; +import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; -import net.minecraft.util.BlockPos; public class DenickCommand extends CommandBase { - - private final MellowOneConfig config; - private final AuroraApi auroraApi; - - public DenickCommand(MellowOneConfig config, AuroraApi auroraApi) { - this.config = config; - this.auroraApi = auroraApi; - } - @Override public String getCommandName() { - return "denick"; + return ""; } @Override public String getCommandUsage(ICommandSender sender) { - return "/denick "; - } - - @Override - public void processCommand(ICommandSender sender, String[] args) { - if (args.length != 2) { - ChatUtils.sendCommandMessage( - sender, - "§cInvalid usage. Use: " + getCommandUsage(sender) - ); - return; - } - - String type = args[0]; - if ( - !type.equalsIgnoreCase("finals") && !type.equalsIgnoreCase("beds") - ) { - ChatUtils.sendCommandMessage( - sender, - "§cInvalid type. Use 'finals' or 'beds'." - ); - return; - } - - String numberStr = args[1]; - try { - Integer.parseInt(numberStr.replace(",", "")); - } catch (NumberFormatException e) { - ChatUtils.sendCommandMessage( - sender, - "§cInvalid number: " + numberStr - ); - return; - } - - ChatUtils.sendCommandMessage(sender, "§aSearching for players..."); - - AsyncExecutor.getInstance().command(() -> { - try { - int[] rangeValues = { 100, 200, 500, 1000 }; - int[] maxValues = { 5, 10, 20 }; - - int rangeIndex = type.equals("finals") - ? config.finalsRange - : config.bedsRange; - int maxIndex = config.maxResults; - - if ( - rangeIndex < 0 || rangeIndex >= rangeValues.length - ) rangeIndex = 1; // Default to 200 - if (maxIndex < 0 || maxIndex >= maxValues.length) maxIndex = 0; // Default to 5 - - int range = rangeValues[rangeIndex]; - int max = maxValues[maxIndex]; - - AuroraApi.AuroraResponse response = auroraApi.queryStats( - type, - numberStr, - range, - max, - config.auroraApiKey - ); - - MainThreadDispatcher.run(() -> { - if (response != null && response.success) { - if (response.data.isEmpty()) { - ChatUtils.sendCommandMessage( - sender, - "§cNo players found." - ); - } else { - String players = response.data - .stream() - .map( - p -> - "§a" + - p.name + - " §7(distance: " + - p.distance + - ")" - ) - .collect(Collectors.joining(", ")); - ChatUtils.sendCommandMessage( - sender, - "§aFound players: " + players - ); - } - } else { - ChatUtils.sendCommandMessage( - sender, - "§cError fetching data from Aurora API." - ); - } - }); - } catch (IOException e) { - MainThreadDispatcher.run(() -> { - ChatUtils.sendCommandMessage( - sender, - "§cAn error occurred while fetching data." - ); - }); - e.printStackTrace(); - } - }); + return ""; } @Override - public List addTabCompletionOptions( - ICommandSender sender, - String[] args, - BlockPos pos - ) { - if (args.length == 1) { - return getListOfStringsMatchingLastWord(args, "finals", "beds"); - } - return null; - } + public void processCommand(ICommandSender sender, String[] args) throws CommandException { - @Override - public int getRequiredPermissionLevel() { - return 0; } } diff --git a/src/main/java/com/roxiun/mellow/commands/MellowCommand.java b/src/main/java/com/roxiun/mellow/commands/MellowCommand.java index 3dbbcb4..b2c2b6b 100644 --- a/src/main/java/com/roxiun/mellow/commands/MellowCommand.java +++ b/src/main/java/com/roxiun/mellow/commands/MellowCommand.java @@ -100,7 +100,7 @@ public void processCommand(ICommandSender sender, String[] args) { ); sender.addChatMessage( new ChatComponentText( - "§r§5/denick :§d Manually denick a player based on finals or beds.§r" + "§r§5/auroradenick :§d Manually denick a player based on finals or beds using Aurora.§r" ) ); sender.addChatMessage( diff --git a/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java b/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java index 54cd84a..e8055b3 100644 --- a/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java +++ b/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java @@ -1004,6 +1004,16 @@ public class MellowOneConfig extends Config { ) public String auroraApiKey = ""; + @Text( + name = "Frosty API Key", + placeholder = "Enter your Frosty API key", + category = "API Keys", + subcategory = "Frosty", + secure = true, + multiline = false + ) + public String frostyApiKey = ""; + @Text( name = "Luna API Key", placeholder = "Enter your Luna API key", @@ -1298,20 +1308,42 @@ public class MellowOneConfig extends Config { ) public static boolean ignoredSeraphPingInfo; - // Number denicker @Info( - text = "This module attempts to denick players based the number of finals and beds broken from chat messages. Configure the Aurora key in API Keys > Aurora.", - type = InfoType.INFO, - size = OptionSize.DUAL, - category = "Number Denicker" + text = "This module attempts to denick players based the number of finals and beds broken from chat messages. Configure the keys in API Keys > Aurora / Frosty.", + type = InfoType.INFO, + size = OptionSize.DUAL, + category = "Number Denicker", + subcategory = "General" ) - public static boolean ignoredNumberDenickerInfo; // Useless. Java limitations with @annotation. + public static boolean ignoredNumberDenickerInfo; + + @Dropdown( + name = "Provider", + options = { "Aurora", "Frosty" }, + category = "Number Denicker", + subcategory = "General" + ) + public int numberDenickerProvider = 0; // 0 = Aurora, 1 = Frosty + + @Switch(name = "Enable Number Denicker", category = "Number Denicker", subcategory = "General") + public boolean numberDenicker = false; + + @Number( + name = "Minimum Finals to Check", + category = "Number Denicker", + subcategory = "General", + min = 0, + max = 500000, + step = 1000 + ) + public int minFinalsForDenick = 15000; @Button( name = "Run /api view on the bot to get your key", text = "Discord Bot", size = OptionSize.DUAL, - category = "Number Denicker" + category = "Number Denicker", + subcategory = "Aurora" ) Runnable auroraLinkButton = () -> { NetworkUtils.browseLink( @@ -1319,47 +1351,39 @@ public class MellowOneConfig extends Config { ); }; - @Switch(name = "Enable Number Denicker", category = "Number Denicker") - public boolean numberDenicker = false; - - @Switch(name = "Print all potential players", category = "Number Denicker") + @Switch(name = "Print all potential players", category = "Number Denicker", subcategory = "Aurora") public boolean numberDenickerFuzzy = true; @Info( text = "Turning all potential players off, will only print players with both matching beds and finals.", type = InfoType.INFO, size = OptionSize.DUAL, - category = "Number Denicker" + category = "Number Denicker", + subcategory = "Aurora" ) public static boolean ignoredNumberDenickerFuzzyInfo; @Dropdown( name = "Finals Range", options = { "0", "50", "100", "200", "500" }, - category = "Number Denicker" + category = "Number Denicker", + subcategory = "Aurora" ) public int finalsRange = 3; // Index for 100 @Dropdown( name = "Beds Range", options = { "0", "50", "100", "200", "500" }, - category = "Number Denicker" - ) - public int bedsRange = 1; // Index for 50 - - @Number( - name = "Minimum Finals to Check", category = "Number Denicker", - min = 0, - max = 500000, - step = 1000 + subcategory = "Aurora" ) - public int minFinalsForDenick = 15000; + public int bedsRange = 1; // Index for 50 @Dropdown( name = "Max Results", options = { "5", "10", "20" }, - category = "Number Denicker" + category = "Number Denicker", + subcategory = "Aurora" ) public int maxResults = 0; // Index for 5 @@ -1522,6 +1546,19 @@ public MellowOneConfig() { hideIf("hitboxBrightnessOffset", () -> hitboxBrightnessMode != 0); } + public int getAuroraDenickRange(String type) { + int[] rangeValues = { 0, 50, 100, 200, 500, 1000 }; + int rangeIndex = "finals".equalsIgnoreCase(type) + ? finalsRange + : bedsRange; + return rangeValues[clampIndex(rangeIndex, rangeValues.length)]; + } + + public int getAuroraDenickMaxResults() { + int[] maxValues = { 5, 10, 20 }; + return maxValues[clampIndex(maxResults, maxValues.length)]; + } + private void sanitizeDropdownIndexes() { // BedWars tab stats dropdowns: Team..Client (15 options) customStat1 = clampIndex(customStat1, 15); @@ -1571,6 +1608,7 @@ private void sanitizeDropdownIndexes() { 3 ); pingProvider = clampIndex(pingProvider, 4); + numberDenickerProvider = clampIndex(numberDenickerProvider, 2); winstreakMinStars = clampIndex(winstreakMinStars, 51); winstreakMinFkdr = clampIndex(winstreakMinFkdr, 18); finalsRange = clampIndex(finalsRange, 5); diff --git a/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java b/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java index 5227910..6b81d66 100644 --- a/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java +++ b/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java @@ -133,21 +133,8 @@ private void processNumbers(String type, String nickName, String number) { AsyncExecutor.getInstance().profileIo(() -> { try { - int[] rangeValues = { 0, 50, 100, 200, 500, 1000 }; - int[] maxValues = { 5, 10, 20 }; - - int rangeIndex = type.equals("finals") - ? config.finalsRange - : config.bedsRange; - int maxIndex = config.maxResults; - - if ( - rangeIndex < 0 || rangeIndex >= rangeValues.length - ) rangeIndex = 1; // Default to 200 - if (maxIndex < 0 || maxIndex >= maxValues.length) maxIndex = 0; // Default to 5 - - int range = rangeValues[rangeIndex]; - int max = maxValues[maxIndex]; + int range = config.getAuroraDenickRange(type); + int max = config.getAuroraDenickMaxResults(); AuroraApi.AuroraResponse response = auroraApi.queryStats( type, From 1c45a6bd4990f0ce07e414e11f59d8cabad493e1 Mon Sep 17 00:00:00 2001 From: Taken Date: Sat, 2 May 2026 22:34:17 +0200 Subject: [PATCH 2/7] Added frosty api classes --- .../roxiun/mellow/api/frosty/FrostyApi.java | 112 +++++++++++++++++ .../mellow/api/frosty/FrostyReponse.java | 101 +++++++++++++++ .../mellow/api/frosty/FrostyApiTest.java | 116 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 src/main/java/com/roxiun/mellow/api/frosty/FrostyApi.java create mode 100644 src/main/java/com/roxiun/mellow/api/frosty/FrostyReponse.java create mode 100644 src/test/java/com/roxiun/mellow/api/frosty/FrostyApiTest.java diff --git a/src/main/java/com/roxiun/mellow/api/frosty/FrostyApi.java b/src/main/java/com/roxiun/mellow/api/frosty/FrostyApi.java new file mode 100644 index 0000000..a4e9af3 --- /dev/null +++ b/src/main/java/com/roxiun/mellow/api/frosty/FrostyApi.java @@ -0,0 +1,112 @@ +package com.roxiun.mellow.api.frosty; + +import com.google.gson.Gson; +import com.roxiun.mellow.util.cache.TimedValueCache; +import java.io.IOException; +import java.util.Locale; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class FrostyApi { + + private static final long QUERY_CACHE_TTL_MS = 60_000L; + private static final String BASE_URL = + "https://api.sukie.net/v1/bedwars/cosmetics"; + + private final OkHttpClient client; + private final Gson gson; + private final TimedValueCache queryCache = + new TimedValueCache<>(QUERY_CACHE_TTL_MS); + + public FrostyApi() { + this(new OkHttpClient(), new Gson()); + } + + FrostyApi(OkHttpClient client) { + this(client, new Gson()); + } + + FrostyApi(OkHttpClient client, Gson gson) { + this.client = client == null ? new OkHttpClient() : client; + this.gson = gson == null ? new Gson() : gson; + } + + public FrostyReponse queryStats( + int finalKills, + int bedsBroken, + String apiKey + ) throws IOException { + return queryCosmetics(finalKills, bedsBroken, apiKey); + } + + public FrostyReponse queryCosmetics( + int finalKills, + int bedsBroken, + String apiKey + ) throws IOException { + if (apiKey == null || apiKey.isEmpty()) { + return null; + } + + if (finalKills == 0 && bedsBroken == 0) { + return null; + } + + String cacheKey = buildCacheKey(finalKills, bedsBroken, apiKey); + if (queryCache.containsFresh(cacheKey)) { + return parseResponse(queryCache.get(cacheKey)); + } + + String finalsKillsParam = finalKills == 0 ? "" : "&final_kills=" + finalKills; + String bedsBrokenParam = bedsBroken == 0 ? "" : "&beds_broken=" + bedsBroken; + String url = BASE_URL + "?key=" + apiKey + finalsKillsParam + bedsBrokenParam + "&rank=mvp%2B%2B"; + + Request request = new Request.Builder() + .url(url) + .header("User-Agent", "Mellow/4.1.0") + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + System.err.println("Frosty API request failed: " + response); + queryCache.put(cacheKey, null); + return null; + } + + ResponseBody body = response.body(); + if (body == null) { + queryCache.put(cacheKey, null); + return null; + } + + String payload = body.string(); + queryCache.put(cacheKey, payload); + return parseResponse(payload); + } + } + + public void clearCache() { + queryCache.clear(); + } + + private FrostyReponse parseResponse(String payload) { + return payload == null ? null : gson.fromJson(payload, FrostyReponse.class); + } + + private String buildCacheKey(int finalKills, int bedsBroken, String apiKey) { + return ( + finalKills + + "|" + + bedsBroken + + "|" + + safe(apiKey).toLowerCase(Locale.ROOT) + ); + } + + private String safe(String value) { + return value == null ? "" : value.trim(); + } +} + diff --git a/src/main/java/com/roxiun/mellow/api/frosty/FrostyReponse.java b/src/main/java/com/roxiun/mellow/api/frosty/FrostyReponse.java new file mode 100644 index 0000000..b0a10bb --- /dev/null +++ b/src/main/java/com/roxiun/mellow/api/frosty/FrostyReponse.java @@ -0,0 +1,101 @@ +package com.roxiun.mellow.api.frosty; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class FrostyReponse { + + @SerializedName("success") + public boolean success; + + @SerializedName("data") + public List data; + + public static class PlayerResult { + + @SerializedName("username") + public String username; + + @SerializedName("uuid") + public String uuid; + + @SerializedName("game_rank") + public String gameRank; + + @SerializedName("rank_color") + public String rankColor; + + @SerializedName("plus_color") + public String plusColor; + + @SerializedName("network_level") + public long networkLevel; + + @SerializedName("last_online") + public String lastOnline; + + @SerializedName("bedwars_index") + public long bedwarsIndex; + + @SerializedName("bedwars_level") + public long bedwarsLevel; + + @SerializedName("total_final_kills") + public long totalFinalKills; + + @SerializedName("total_final_deaths") + public long totalFinalDeaths; + + @SerializedName("total_beds_broken") + public long totalBedsBroken; + + @SerializedName("total_beds_lost") + public long totalBedsLost; + + @SerializedName("total_wins") + public long totalWins; + + @SerializedName("total_losses") + public long totalLosses; + + @SerializedName("island_topper") + public String islandTopper; + + @SerializedName("victory_dance") + public String victoryDance; + + @SerializedName("kill_effect") + public String killEffect; + + @SerializedName("projectile_trail") + public String projectileTrail; + + @SerializedName("kill_message") + public String killMessage; + + @SerializedName("npc_skin") + public String npcSkin; + + @SerializedName("death_cry") + public String deathCry; + + @SerializedName("spray") + public String spray; + + @SerializedName("bed_destroy") + public String bedDestroy; + + @SerializedName("glyph") + public String glyph; + + @SerializedName("wood_type") + public String woodType; + + @SerializedName("starting_weapon") + public String startingWeapon; + + @SerializedName("figurine") + public String figurine; + } +} + diff --git a/src/test/java/com/roxiun/mellow/api/frosty/FrostyApiTest.java b/src/test/java/com/roxiun/mellow/api/frosty/FrostyApiTest.java new file mode 100644 index 0000000..01833dc --- /dev/null +++ b/src/test/java/com/roxiun/mellow/api/frosty/FrostyApiTest.java @@ -0,0 +1,116 @@ +package com.roxiun.mellow.api.frosty; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Assert; +import org.junit.Test; + +public class FrostyApiTest { + + @Test + public void queryStatsCachesSuccessfulResponsesByRequestParams() throws IOException { + AtomicInteger requestCount = new AtomicInteger(); + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(chain -> { + Request request = chain.request(); + requestCount.incrementAndGet(); + Assert.assertEquals("mvp++", request.url().queryParameter("rank")); + Assert.assertEquals( + "19953", + request.url().queryParameter("final_kills") + ); + Assert.assertEquals( + "9183", + request.url().queryParameter("beds_broken") + ); + return response( + request, + 200, + "{" + + "\"success\":true," + + "\"data\":[{" + + "\"username\":\"bashbuta\"," + + "\"uuid\":\"5efb01316aef44219e2f67e36f607c0c\"," + + "\"game_rank\":\"MVP++\"," + + "\"rank_color\":\"GOLD\"," + + "\"plus_color\":\"DARK_AQUA\"," + + "\"network_level\":38201236," + + "\"last_online\":\"2026-04-17T22:22:15.000Z\"," + + "\"bedwars_index\":53343," + + "\"bedwars_level\":2441048," + + "\"total_final_kills\":19953," + + "\"total_final_deaths\":1940," + + "\"total_beds_broken\":9183," + + "\"total_beds_lost\":2608," + + "\"total_wins\":4533," + + "\"total_losses\":1940," + + "\"island_topper\":\"islandtopper_none\"," + + "\"victory_dance\":\"victorydance_toy_stick\"," + + "\"kill_effect\":\"killeffect_blood_explosion\"," + + "\"projectile_trail\":\"projectiletrail_meteorblaze\"," + + "\"kill_message\":\"killmessages_counter\"," + + "\"npc_skin\":\"npcskin_none\"," + + "\"death_cry\":\"deathcry_none\"," + + "\"spray\":\"sprays_carried\"," + + "\"bed_destroy\":\"beddestroy_pig_missile\"," + + "\"glyph\":\"glyph_none\"," + + "\"wood_type\":\"woodskin_dark_oak_log\"," + + "\"starting_weapon\":\"starting_weapon_wooden_sword\"," + + "\"figurine\":\"figurine_none\"" + + "}]" + + "}" + ); + }) + .build(); + + FrostyApi api = new FrostyApi(client); + + FrostyReponse first = api.queryCosmetics(19953, 9183, "frosty-key"); + FrostyReponse second = api.queryCosmetics(19953, 9183, "frosty-key"); + + Assert.assertNotNull(first); + Assert.assertNotNull(second); + Assert.assertTrue(first.success); + Assert.assertEquals(1, first.data.size()); + Assert.assertEquals("bashbuta", first.data.get(0).username); + Assert.assertEquals(38201236L, first.data.get(0).networkLevel); + Assert.assertEquals(1, requestCount.get()); + Assert.assertNotSame(first, second); + } + + @Test + public void queryStatsCachesFailedResponsesByRequestParams() throws IOException { + AtomicInteger requestCount = new AtomicInteger(); + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(chain -> { + Request request = chain.request(); + requestCount.incrementAndGet(); + return response(request, 500, "{}"); + }) + .build(); + + FrostyApi api = new FrostyApi(client); + + Assert.assertNull(api.queryCosmetics(19953, 9183, "frosty-key")); + Assert.assertNull(api.queryCosmetics(19953, 9183, "frosty-key")); + Assert.assertEquals(1, requestCount.get()); + } + + private static Response response(Request request, int code, String body) { + return new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(code) + .message(code == 200 ? "OK" : "Error") + .body(ResponseBody.create(body, MediaType.parse("application/json"))) + .build(); + } +} + + From 1408b4c193c60f643f4fb9f53982b27eadaff9bb Mon Sep 17 00:00:00 2001 From: Taken Date: Sat, 2 May 2026 23:08:57 +0200 Subject: [PATCH 3/7] Added frosty denick command --- src/main/java/com/roxiun/mellow/Mellow.java | 6 + .../mellow/commands/FrostyDenickCommand.java | 193 ++++++++++++++++++ .../roxiun/mellow/commands/MellowCommand.java | 5 + 3 files changed, 204 insertions(+) create mode 100644 src/main/java/com/roxiun/mellow/commands/FrostyDenickCommand.java diff --git a/src/main/java/com/roxiun/mellow/Mellow.java b/src/main/java/com/roxiun/mellow/Mellow.java index 5d5b695..0350a7e 100644 --- a/src/main/java/com/roxiun/mellow/Mellow.java +++ b/src/main/java/com/roxiun/mellow/Mellow.java @@ -4,6 +4,7 @@ import com.roxiun.mellow.api.aurora.AuroraApi; import com.roxiun.mellow.api.aurora.AuroraPingService; import com.roxiun.mellow.api.aurora.AuroraWinstreakService; +import com.roxiun.mellow.api.frosty.FrostyApi; import com.roxiun.mellow.api.hypixel.HypixelFeatures; import com.roxiun.mellow.api.luna.LunaPingService; import com.roxiun.mellow.api.mojang.MojangApi; @@ -69,6 +70,7 @@ public class Mellow { public static AuroraPingService auroraPingService; public static AuroraWinstreakService auroraWinstreakService; public static AuroraApi auroraApi; + public static FrostyApi frostyApi; public static LunaPingService lunaPingService; public static SeraphClientCacheService seraphClientCacheService; public static SeraphPingService seraphPingService; @@ -110,6 +112,7 @@ public void init(FMLInitializationEvent event) { seraphApi = new SeraphApi(mojangApi); seraphClientCacheService = new SeraphClientCacheService(seraphApi, config); auroraApi = new AuroraApi(); + frostyApi = new FrostyApi(); playerCache = new PlayerCache( mojangApi, @@ -244,6 +247,9 @@ public void run() { ClientCommandHandler.instance.registerCommand( new AuroraDenickCommand(config, auroraApi) ); + ClientCommandHandler.instance.registerCommand( + new FrostyDenickCommand(config, frostyApi) + ); ClientCommandHandler.instance.registerCommand( new SkinDenickCommand(playerCache) ); diff --git a/src/main/java/com/roxiun/mellow/commands/FrostyDenickCommand.java b/src/main/java/com/roxiun/mellow/commands/FrostyDenickCommand.java new file mode 100644 index 0000000..0ba2c8b --- /dev/null +++ b/src/main/java/com/roxiun/mellow/commands/FrostyDenickCommand.java @@ -0,0 +1,193 @@ +package com.roxiun.mellow.commands; + +import com.roxiun.mellow.api.frosty.FrostyApi; +import com.roxiun.mellow.api.frosty.FrostyReponse; +import com.roxiun.mellow.config.MellowOneConfig; +import com.roxiun.mellow.core.async.AsyncExecutor; +import com.roxiun.mellow.core.async.MainThreadDispatcher; +import com.roxiun.mellow.util.ChatUtils; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import net.minecraft.command.CommandBase; +import net.minecraft.command.ICommandSender; +import net.minecraft.util.BlockPos; + +public class FrostyDenickCommand extends CommandBase { + + private final MellowOneConfig config; + private final FrostyApi frostyApi; + + public FrostyDenickCommand(MellowOneConfig config, FrostyApi frostyApi) { + this.config = config; + this.frostyApi = frostyApi; + } + + @Override + public String getCommandName() { + return "frostydenick"; + } + + @Override + public String getCommandUsage(ICommandSender sender) { + return "/frostydenick "; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) { + if (args.length != 2) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid usage. Use: " + getCommandUsage(sender) + ); + return; + } + + if (config.frostyApiKey == null || config.frostyApiKey.trim().isEmpty()) { + ChatUtils.sendCommandMessage( + sender, + "§cMissing Frosty API key. Set it in OneConfig: API Keys > Frosty." + ); + return; + } + + int finalsCount; + int bedsCount; + try { + finalsCount = Integer.parseInt(args[0].replace(",", "")); + bedsCount = Integer.parseInt(args[1].replace(",", "")); + } catch (NumberFormatException e) { + ChatUtils.sendCommandMessage( + sender, + "§cBoth finals_count and beds_count must be valid numbers." + ); + return; + } + + if (finalsCount < 0 || bedsCount < 0) { + ChatUtils.sendCommandMessage( + sender, + "§cfinals_count and beds_count must be 0 or higher." + ); + return; + } + + if (finalsCount == 0 && bedsCount == 0) { + ChatUtils.sendCommandMessage( + sender, + "§cAt least one value must be above 0." + ); + return; + } + + ChatUtils.sendCommandMessage(sender, "§aSearching for players..."); + + AsyncExecutor.getInstance().command(() -> { + try { + FrostyReponse response = frostyApi.queryStats( + finalsCount, + bedsCount, + config.frostyApiKey + ); + + MainThreadDispatcher.run(() -> { + if (response == null || !response.success) { + ChatUtils.sendCommandMessage( + sender, + "§cError fetching data from Frosty API." + ); + return; + } + + if (response.data == null || response.data.isEmpty()) { + ChatUtils.sendCommandMessage(sender, "§cNo players found."); + return; + } + + String players = response.data + .stream() + .map( + p -> { + String finalsText = formatFinalsOutput( + p.totalFinalKills, + finalsCount + ); + String bedsText = formatBedsOutput( + p.totalBedsBroken, + bedsCount + ); + return ( + "§a" + + p.username + + " §7(" + + finalsText + + ", " + + bedsText + + ")" + ); + } + ) + .collect(Collectors.joining(", ")); + + ChatUtils.sendCommandMessage( + sender, + "§aFound players: " + players + ); + }); + } catch (IOException e) { + MainThreadDispatcher.run(() -> { + ChatUtils.sendCommandMessage( + sender, + "§cAn error occurred while fetching data." + ); + }); + e.printStackTrace(); + } + }); + } + + @Override + public List addTabCompletionOptions( + ICommandSender sender, + String[] args, + BlockPos pos + ) { + return null; + } + + @Override + public int getRequiredPermissionLevel() { + return 0; + } + + private String formatSignedDelta(long delta) { + return delta >= 0 ? "+" + delta : String.valueOf(delta); + } + + private String formatFinalsOutput(long totalFinalKills, int finalsCount) { + if (finalsCount == 0) { + return "FK: " + totalFinalKills; + } + return ( + "FK: " + + totalFinalKills + + " [" + + formatSignedDelta(totalFinalKills - finalsCount) + + "]" + ); + } + + private String formatBedsOutput(long totalBedsBroken, int bedsCount) { + if (bedsCount == 0) { + return "Beds: " + totalBedsBroken; + } + return ( + "Beds: " + + totalBedsBroken + + " [" + + formatSignedDelta(totalBedsBroken - bedsCount) + + "]" + ); + } +} + diff --git a/src/main/java/com/roxiun/mellow/commands/MellowCommand.java b/src/main/java/com/roxiun/mellow/commands/MellowCommand.java index b2c2b6b..4348df5 100644 --- a/src/main/java/com/roxiun/mellow/commands/MellowCommand.java +++ b/src/main/java/com/roxiun/mellow/commands/MellowCommand.java @@ -103,6 +103,11 @@ public void processCommand(ICommandSender sender, String[] args) { "§r§5/auroradenick :§d Manually denick a player based on finals or beds using Aurora.§r" ) ); + sender.addChatMessage( + new ChatComponentText( + "§r§5/frostydenick :§d Manually denick players using Frosty cosmetics lookup.§r" + ) + ); sender.addChatMessage( new ChatComponentText( "§r§5/skindenick :§d Manually denick a player based on their skin.§r" From 0b63ebbb6c91888868c837e269301162005263fa Mon Sep 17 00:00:00 2001 From: Taken Date: Sat, 2 May 2026 23:24:00 +0200 Subject: [PATCH 4/7] Added frosty denick to automatic resolution --- src/main/java/com/roxiun/mellow/Mellow.java | 3 +- .../mellow/feature/nicks/NumberDenicker.java | 223 ++++++++++++++++-- 2 files changed, 199 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/roxiun/mellow/Mellow.java b/src/main/java/com/roxiun/mellow/Mellow.java index 0350a7e..b31f665 100644 --- a/src/main/java/com/roxiun/mellow/Mellow.java +++ b/src/main/java/com/roxiun/mellow/Mellow.java @@ -138,7 +138,8 @@ public void init(FMLInitializationEvent event) { NumberDenicker numberDenicker = new NumberDenicker( config, nickUtils, - auroraApi + auroraApi, + frostyApi ); PregameStats pregameStats = new PregameStats( playerCache, diff --git a/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java b/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java index 6b81d66..4ac94f9 100644 --- a/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java +++ b/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java @@ -1,6 +1,8 @@ package com.roxiun.mellow.feature.nicks; import com.roxiun.mellow.api.aurora.AuroraApi; +import com.roxiun.mellow.api.frosty.FrostyApi; +import com.roxiun.mellow.api.frosty.FrostyReponse; import com.roxiun.mellow.config.MellowOneConfig; import com.roxiun.mellow.core.async.AsyncExecutor; import com.roxiun.mellow.util.ChatUtils; @@ -22,6 +24,7 @@ public class NumberDenicker { private final Minecraft mc = Minecraft.getMinecraft(); private final MellowOneConfig config; private final AuroraApi auroraApi; + private final FrostyApi frostyApi; private final NickUtils nickUtils; private boolean gameStarted = false; @@ -37,10 +40,12 @@ public class NumberDenicker { public NumberDenicker( MellowOneConfig config, NickUtils nickUtils, - AuroraApi auroraApi + AuroraApi auroraApi, + FrostyApi frostyApi ) { this.config = config; this.auroraApi = auroraApi; + this.frostyApi = frostyApi; this.nickUtils = nickUtils; } @@ -64,6 +69,8 @@ public void onChat(ClientChatReceivedEvent event) { if (!this.gameStarted) return; + boolean useFrosty = config.numberDenickerProvider == 1; + Matcher finalMatcher = FINAL_KILL_PATTERN.matcher(message); if (finalMatcher.find()) { String nickName = finalMatcher.group(2); @@ -78,20 +85,34 @@ public void onChat(ClientChatReceivedEvent event) { ); if ( isPlayerInGame(nickName) && - nickUtils.isNicked(nickName) && - (!player.finalsChecked || - player.fuzzy_finals_potentials == null) + nickUtils.isNicked(nickName) ) { - mc.addScheduledTask(() -> - ChatUtils.sendMessage( - "§aAttempting to denick " + - nickName + - " with " + - finalNumberStr + - " finals" - ) - ); - processNumbers("finals", nickName, finalNumberStr); + if (useFrosty) { + // For Frosty, accumulate until we have both stats + player.finalsNumber = finalNumber; + player.currentType = "finals"; + // Don't process yet if we don't have beds number + if (player.bedsNumber != -1) { + processNumbers("both", nickName, null); + // Reset for next detection + player.finalsNumber = -1; + player.bedsNumber = -1; + } + } else { + // Aurora API processes each stat independently + if (!player.finalsChecked) { + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§aAttempting to denick " + + nickName + + " with " + + finalNumberStr + + " finals" + ) + ); + processNumbers("finals", nickName, finalNumberStr); + } + } } } } catch (NumberFormatException e) { @@ -110,19 +131,34 @@ public void onChat(ClientChatReceivedEvent event) { ); if ( isPlayerInGame(nickName) && - nickUtils.isNicked(nickName) && - (!player.bedsChecked || player.fuzzy_beds_potentials == null) + nickUtils.isNicked(nickName) ) { - mc.addScheduledTask(() -> - ChatUtils.sendMessage( - "§aAttempting to denick " + - nickName + - " with " + - bedNumber + - " beds" - ) - ); - processNumbers("beds", nickName, bedNumber); + if (useFrosty) { + // For Frosty, accumulate until we have both stats + player.bedsNumber = Integer.parseInt(bedNumber); + player.currentType = "beds"; + // Process if we have finals number too + if (player.finalsNumber != -1) { + processNumbers("both", nickName, null); + // Reset for next detection + player.finalsNumber = -1; + player.bedsNumber = -1; + } + } else { + // Aurora API processes each stat independently + if (!player.bedsChecked) { + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§aAttempting to denick " + + nickName + + " with " + + bedNumber + + " beds" + ) + ); + processNumbers("beds", nickName, bedNumber); + } + } } } } @@ -131,6 +167,132 @@ private void processNumbers(String type, String nickName, String number) { PotentialNick player = nickToPotentials.get(nickName); if (player == null) return; + boolean useFrosty = config.numberDenickerProvider == 1; + + if (useFrosty) { + processFrostyNumbers(nickName); + } else { + processAuroraNumbers(type, nickName, number); + } + } + + private void processFrostyNumbers(String nickName) { + PotentialNick player = nickToPotentials.get(nickName); + if (player == null) return; + + try { + final int finalKills = Math.max(player.finalsNumber, 0); + final int bedsBroken = Math.max(player.bedsNumber, 0); + + if (finalKills == 0 && bedsBroken == 0) { + return; + } + + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§aAttempting to denick " + + nickName + + " with stats" + ) + ); + + AsyncExecutor.getInstance().profileIo(() -> { + try { + FrostyReponse response = frostyApi.queryStats( + finalKills, + bedsBroken, + config.frostyApiKey + ); + + if (response != null && response.success) { + if (response.data == null || response.data.isEmpty()) { + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§cNo fuzzy match found for " + nickName + ) + ); + return; + } + + List playerNames = response.data + .stream() + .map(p -> p.username) + .collect(Collectors.toList()); + + if (config.numberDenickerFuzzy) { + String fuzzyPlayers = response.data + .stream() + .map( + p -> { + String finalsText = finalKills > 0 + ? "FK: " + + p.totalFinalKills + + " [" + + formatSignedDelta( + p.totalFinalKills - finalKills + ) + + "]" + : "FK: " + p.totalFinalKills; + String bedsText = bedsBroken > 0 + ? "Beds: " + + p.totalBedsBroken + + " [" + + formatSignedDelta( + p.totalBedsBroken - bedsBroken + ) + + "]" + : "Beds: " + p.totalBedsBroken; + return ( + "§a" + + p.username + + " §7(" + + finalsText + + ", " + + bedsText + + ")" + ); + } + ) + .collect(Collectors.joining(", ")); + + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§aFound potential players: " + fuzzyPlayers + ) + ); + } + + if (!playerNames.isEmpty()) { + String realName = playerNames.get(0); + mc.addScheduledTask(() -> { + sendAlert(nickName, realName); + setNickDisplayName(nickName, realName + "?"); + }); + } + } else { + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§cError fetching data from Frosty API." + ) + ); + } + } catch (IOException e) { + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§cError fetching data from Frosty API." + ) + ); + } + }); + } catch (NumberFormatException e) { + // Ignore if the number is invalid + } + } + + private void processAuroraNumbers(String type, String nickName, String number) { + PotentialNick player = nickToPotentials.get(nickName); + if (player == null) return; + AsyncExecutor.getInstance().profileIo(() -> { try { int range = config.getAuroraDenickRange(type); @@ -262,6 +424,10 @@ private void processNumbers(String type, String nickName, String number) { }); } + private String formatSignedDelta(long delta) { + return delta >= 0 ? "+" + delta : String.valueOf(delta); + } + private void sendAlert(String playerName, String realName) { String alertMsg = "§6" + realName + "§7 might be nicked as " + playerName + "§7."; @@ -306,6 +472,11 @@ private static class PotentialNick { List fuzzy_finals_potentials = null; List fuzzy_beds_potentials = null; + // For Frosty API support + String currentType = null; + int finalsNumber = -1; + int bedsNumber = -1; + void setPotentials(List potentials) { this.potentials = potentials; } From a967e040ad6c4f724ba287db9c9ef36a821227ac Mon Sep 17 00:00:00 2001 From: Taken Date: Sun, 3 May 2026 00:02:41 +0200 Subject: [PATCH 5/7] Updated denick command --- README.md | 7 +- src/main/java/com/roxiun/mellow/Mellow.java | 3 + .../roxiun/mellow/commands/DenickCommand.java | 289 +++++++++++++++++- 3 files changed, 295 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index efab95c..e4d99a5 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,13 @@ You can **import** your own **tag ignore list** by doing, type `/tagignore impor To **skin denick** type `/skindenick ` -To use the **number denicker** add your Aurora API key +To use the **number denicker**, select your provider in **Settings > Number Denicker** and add the matching API key - You can obtain one [here](https://discord.com/oauth2/authorize?client_id=1244205279697174539) -- After setup, denicking happens automatically during games, but you can also manually run: `/denick ` +- After setup, denicking happens automatically during games, but you can also manually run `/denick`. +- `/denick` uses the provider selected in **Settings > Number Denicker**: + - **Aurora**: `/denick ` + - **Frosty**: `/denick ` ## Known issues diff --git a/src/main/java/com/roxiun/mellow/Mellow.java b/src/main/java/com/roxiun/mellow/Mellow.java index b31f665..4e52490 100644 --- a/src/main/java/com/roxiun/mellow/Mellow.java +++ b/src/main/java/com/roxiun/mellow/Mellow.java @@ -245,6 +245,9 @@ public void run() { ClientCommandHandler.instance.registerCommand( new RefreshCommand(inGameTabStatsSyncService) ); + ClientCommandHandler.instance.registerCommand( + new DenickCommand(config, auroraApi, frostyApi) + ); ClientCommandHandler.instance.registerCommand( new AuroraDenickCommand(config, auroraApi) ); diff --git a/src/main/java/com/roxiun/mellow/commands/DenickCommand.java b/src/main/java/com/roxiun/mellow/commands/DenickCommand.java index 7a24499..ea870e7 100644 --- a/src/main/java/com/roxiun/mellow/commands/DenickCommand.java +++ b/src/main/java/com/roxiun/mellow/commands/DenickCommand.java @@ -1,22 +1,307 @@ package com.roxiun.mellow.commands; +import com.roxiun.mellow.api.aurora.AuroraApi; +import com.roxiun.mellow.api.frosty.FrostyApi; +import com.roxiun.mellow.api.frosty.FrostyReponse; +import com.roxiun.mellow.config.MellowOneConfig; +import com.roxiun.mellow.core.async.AsyncExecutor; +import com.roxiun.mellow.core.async.MainThreadDispatcher; +import com.roxiun.mellow.util.ChatUtils; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; import net.minecraft.command.CommandBase; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; +import net.minecraft.util.BlockPos; public class DenickCommand extends CommandBase { + + private final MellowOneConfig config; + private final AuroraApi auroraApi; + private final FrostyApi frostyApi; + + public DenickCommand( + MellowOneConfig config, + AuroraApi auroraApi, + FrostyApi frostyApi + ) { + this.config = config; + this.auroraApi = auroraApi; + this.frostyApi = frostyApi; + } + @Override public String getCommandName() { - return ""; + return "denick"; } @Override public String getCommandUsage(ICommandSender sender) { - return ""; + return config != null && config.numberDenickerProvider == 1 + ? "/denick " + : "/denick "; } @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { + if (config != null && config.numberDenickerProvider == 1) { + processFrostyCommand(sender, args); + return; + } + + processAuroraCommand(sender, args); + } + + @Override + public List addTabCompletionOptions( + ICommandSender sender, + String[] args, + BlockPos pos + ) { + if (config != null && config.numberDenickerProvider == 1) { + return null; + } + + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, "finals", "beds"); + } + return null; + } + + @Override + public int getRequiredPermissionLevel() { + return 0; + } + + private void processAuroraCommand(ICommandSender sender, String[] args) { + if (args.length != 2) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid usage. Use: " + getCommandUsage(sender) + ); + return; + } + + String type = args[0]; + if ( + !type.equalsIgnoreCase("finals") && !type.equalsIgnoreCase("beds") + ) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid type. Use 'finals' or 'beds'." + ); + return; + } + + String numberStr = args[1]; + try { + Integer.parseInt(numberStr.replace(",", "")); + } catch (NumberFormatException e) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid number: " + numberStr + ); + return; + } + + ChatUtils.sendCommandMessage(sender, "§aSearching for players..."); + + AsyncExecutor.getInstance().command(() -> { + try { + int range = config.getAuroraDenickRange(type); + int max = config.getAuroraDenickMaxResults(); + + AuroraApi.AuroraResponse response = auroraApi.queryStats( + type, + numberStr, + range, + max, + config.auroraApiKey + ); + + MainThreadDispatcher.run(() -> { + if (response != null && response.success) { + if (response.data.isEmpty()) { + ChatUtils.sendCommandMessage( + sender, + "§cNo players found." + ); + } else { + String players = response.data + .stream() + .map( + p -> + "§a" + + p.name + + " §7(distance: " + + p.distance + + ")" + ) + .collect(Collectors.joining(", ")); + ChatUtils.sendCommandMessage( + sender, + "§aFound players: " + players + ); + } + } else { + ChatUtils.sendCommandMessage( + sender, + "§cError fetching data from Aurora API." + ); + } + }); + } catch (IOException e) { + MainThreadDispatcher.run(() -> { + ChatUtils.sendCommandMessage( + sender, + "§cAn error occurred while fetching data." + ); + }); + e.printStackTrace(); + } + }); + } + + private void processFrostyCommand(ICommandSender sender, String[] args) { + if (args.length != 2) { + ChatUtils.sendCommandMessage( + sender, + "§cInvalid usage. Use: " + getCommandUsage(sender) + ); + return; + } + + if (config.frostyApiKey == null || config.frostyApiKey.trim().isEmpty()) { + ChatUtils.sendCommandMessage( + sender, + "§cMissing Frosty API key. Set it in OneConfig: API Keys > Frosty." + ); + return; + } + + int finalsCount; + int bedsCount; + try { + finalsCount = Integer.parseInt(args[0].replace(",", "")); + bedsCount = Integer.parseInt(args[1].replace(",", "")); + } catch (NumberFormatException e) { + ChatUtils.sendCommandMessage( + sender, + "§cBoth finals_count and beds_count must be valid numbers." + ); + return; + } + + if (finalsCount < 0 || bedsCount < 0) { + ChatUtils.sendCommandMessage( + sender, + "§cfinals_count and beds_count must be 0 or higher." + ); + return; + } + + if (finalsCount == 0 && bedsCount == 0) { + ChatUtils.sendCommandMessage( + sender, + "§cAt least one value must be above 0." + ); + return; + } + + ChatUtils.sendCommandMessage(sender, "§aSearching for players..."); + + AsyncExecutor.getInstance().command(() -> { + try { + FrostyReponse response = frostyApi.queryStats( + finalsCount, + bedsCount, + config.frostyApiKey + ); + + MainThreadDispatcher.run(() -> { + if (response == null || !response.success) { + ChatUtils.sendCommandMessage( + sender, + "§cError fetching data from Frosty API." + ); + return; + } + + if (response.data == null || response.data.isEmpty()) { + ChatUtils.sendCommandMessage(sender, "§cNo players found."); + return; + } + + String players = response.data + .stream() + .map( + p -> { + String finalsText = formatFinalsOutput( + p.totalFinalKills, + finalsCount + ); + String bedsText = formatBedsOutput( + p.totalBedsBroken, + bedsCount + ); + return ( + "§a" + + p.username + + " §7(" + + finalsText + + ", " + + bedsText + + ")" + ); + } + ) + .collect(Collectors.joining(", ")); + + ChatUtils.sendCommandMessage( + sender, + "§aFound players: " + players + ); + }); + } catch (IOException e) { + MainThreadDispatcher.run(() -> { + ChatUtils.sendCommandMessage( + sender, + "§cAn error occurred while fetching data." + ); + }); + e.printStackTrace(); + } + }); + } + + private String formatSignedDelta(long delta) { + return delta >= 0 ? "+" + delta : String.valueOf(delta); + } + + private String formatFinalsOutput(long totalFinalKills, int finalsCount) { + if (finalsCount == 0) { + return "FK: " + totalFinalKills; + } + return ( + "FK: " + + totalFinalKills + + " [" + + formatSignedDelta(totalFinalKills - finalsCount) + + "]" + ); + } + private String formatBedsOutput(long totalBedsBroken, int bedsCount) { + if (bedsCount == 0) { + return "Beds: " + totalBedsBroken; + } + return ( + "Beds: " + + totalBedsBroken + + " [" + + formatSignedDelta(totalBedsBroken - bedsCount) + + "]" + ); } } From 282586567e616619e707be730241f061b5c88666 Mon Sep 17 00:00:00 2001 From: Taken Date: Sun, 3 May 2026 00:14:56 +0200 Subject: [PATCH 6/7] Updated config --- .../roxiun/mellow/config/MellowOneConfig.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java b/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java index e8055b3..7037eb8 100644 --- a/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java +++ b/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java @@ -1317,6 +1317,9 @@ public class MellowOneConfig extends Config { ) public static boolean ignoredNumberDenickerInfo; + @Switch(name = "Enable Number Denicker", category = "Number Denicker", subcategory = "General") + public boolean numberDenicker = false; + @Dropdown( name = "Provider", options = { "Aurora", "Frosty" }, @@ -1325,9 +1328,6 @@ public class MellowOneConfig extends Config { ) public int numberDenickerProvider = 0; // 0 = Aurora, 1 = Frosty - @Switch(name = "Enable Number Denicker", category = "Number Denicker", subcategory = "General") - public boolean numberDenicker = false; - @Number( name = "Minimum Finals to Check", category = "Number Denicker", @@ -1338,6 +1338,19 @@ public class MellowOneConfig extends Config { ) public int minFinalsForDenick = 15000; + @Button( + name = "Run /generate-key on the bot to get your key. You will need to be whitelisted for it.", + text = "Discord Server", + size = OptionSize.DUAL, + category = "Number Denicker", + subcategory = "Frosty" + ) + Runnable frostyLinkButton = () -> { + NetworkUtils.browseLink( + "https://discord.gg/JwvA3GeDtA" + ); + }; + @Button( name = "Run /api view on the bot to get your key", text = "Discord Bot", From bc56a8a3fe12bc50e9787dccb22daca5ddfb3482 Mon Sep 17 00:00:00 2001 From: Taken Date: Sun, 3 May 2026 00:32:04 +0200 Subject: [PATCH 7/7] Added minimum beds for checking --- .../roxiun/mellow/config/MellowOneConfig.java | 10 +++ .../mellow/feature/nicks/NumberDenicker.java | 74 ++++++++++--------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java b/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java index 7037eb8..e2d5590 100644 --- a/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java +++ b/src/main/java/com/roxiun/mellow/config/MellowOneConfig.java @@ -1338,6 +1338,16 @@ public class MellowOneConfig extends Config { ) public int minFinalsForDenick = 15000; + @Number( + name = "Minimum Beds to Check", + category = "Number Denicker", + subcategory = "General", + min = 0, + max = 500000, + step = 1000 + ) + public int minBedsForDenick = 5000; + @Button( name = "Run /generate-key on the bot to get your key. You will need to be whitelisted for it.", text = "Discord Server", diff --git a/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java b/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java index 4ac94f9..a1f0e8f 100644 --- a/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java +++ b/src/main/java/com/roxiun/mellow/feature/nicks/NumberDenicker.java @@ -124,41 +124,49 @@ public void onChat(ClientChatReceivedEvent event) { Matcher bedMatcher = BED_DESTRUCTION_PATTERN.matcher(message); if (bedMatcher.find()) { String nickName = bedMatcher.group(3); - String bedNumber = bedMatcher.group(2).replace(",", ""); - PotentialNick player = nickToPotentials.computeIfAbsent( - nickName, - k -> new PotentialNick() - ); - if ( - isPlayerInGame(nickName) && - nickUtils.isNicked(nickName) - ) { - if (useFrosty) { - // For Frosty, accumulate until we have both stats - player.bedsNumber = Integer.parseInt(bedNumber); - player.currentType = "beds"; - // Process if we have finals number too - if (player.finalsNumber != -1) { - processNumbers("both", nickName, null); - // Reset for next detection - player.finalsNumber = -1; - player.bedsNumber = -1; - } - } else { - // Aurora API processes each stat independently - if (!player.bedsChecked) { - mc.addScheduledTask(() -> - ChatUtils.sendMessage( - "§aAttempting to denick " + - nickName + - " with " + - bedNumber + - " beds" - ) - ); - processNumbers("beds", nickName, bedNumber); + String bedNumberStr = bedMatcher.group(2).replace(",", ""); + + try { + int bedNumber = Integer.parseInt(bedNumberStr); + if (bedNumber >= config.minBedsForDenick) { + PotentialNick player = nickToPotentials.computeIfAbsent( + nickName, + k -> new PotentialNick() + ); + if ( + isPlayerInGame(nickName) && + nickUtils.isNicked(nickName) + ) { + if (useFrosty) { + // For Frosty, accumulate until we have both stats + player.bedsNumber = bedNumber; + player.currentType = "beds"; + // Process if we have finals number too + if (player.finalsNumber != -1) { + processNumbers("both", nickName, null); + // Reset for next detection + player.finalsNumber = -1; + player.bedsNumber = -1; + } + } else { + // Aurora API processes each stat independently + if (!player.bedsChecked) { + mc.addScheduledTask(() -> + ChatUtils.sendMessage( + "§aAttempting to denick " + + nickName + + " with " + + bedNumberStr + + " beds" + ) + ); + processNumbers("beds", nickName, bedNumberStr); + } + } } } + } catch (NumberFormatException e) { + // Ignore if the number is invalid } } }