diff --git a/gradle.properties b/gradle.properties index 9490e21..77b2493 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.configuration-cache=false minecraft_version=1.21.11 loader_version=0.18.4 # Mod Properties -mod_version=2.19.2 +mod_version=3.0.0-alpha.6 maven_group=net.legitimoose archives_base_name=Legitimoose-Bot # Dependencies diff --git a/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java index f0c53a9..ffdd00d 100644 --- a/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java +++ b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java @@ -142,7 +142,7 @@ private static void rejoin() { if ((now - lastJoinTimestamp) >= REJOIN_COOLDOWN_MS) { lastJoinTimestamp = now; LOGGER.info("Attempting to reconnect to server"); - ServerData info = new ServerData("Server", "legitimoose.com", ServerData.Type.OTHER); + ServerData info = new ServerData("Legitimoose", "legitimoose.com", ServerData.Type.OTHER); ConnectScreen.startConnecting( new JoinMultiplayerScreen(null), Minecraft.getInstance(), diff --git a/src/client/java/net/legitimoose/bot/http/endpoint/PlayerEndpoint.java b/src/client/java/net/legitimoose/bot/http/endpoint/PlayerEndpoint.java index cec9c25..f21ad0f 100644 --- a/src/client/java/net/legitimoose/bot/http/endpoint/PlayerEndpoint.java +++ b/src/client/java/net/legitimoose/bot/http/endpoint/PlayerEndpoint.java @@ -2,13 +2,13 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import com.mongodb.client.MongoCollection; import net.legitimoose.bot.scraper.Database; import net.legitimoose.bot.scraper.Player; import net.legitimoose.bot.scraper.Rank; -import net.legitimoose.bot.scraper.Scraper; import net.legitimoose.bot.util.McUtil; +import org.bson.Document; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,11 +32,19 @@ public JsonArray handleRequest() { } } - for (String user : usernames.keySet()) { + for (String username : usernames.keySet()) { + try { + if (Database.getPlayers().countDocuments(new Document("name", username)) == 0) { + new Player(McUtil.getUuid(username), username, Rank.Unknown, List.of(), new Player.Streak(1, false), Instant.EPOCH).write(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + for (Player dbPlayer : Database.getPlayers().find()) { JsonObject player = new JsonObject(); try { - String uuid = McUtil.getUuid(user); - Player dbPlayer = Database.getPlayers().find(eq("uuid", uuid)).first(); Rank rank; if (dbPlayer == null) { rank = Rank.Unknown; @@ -45,10 +53,18 @@ public JsonArray handleRequest() { rank = dbPlayer.rank(); } - player.addProperty("uuid", uuid); - player.addProperty("name", user); + boolean online = false; + String world = ""; + if (usernames.get(dbPlayer.name()) != null) { + online = true; + world = usernames.get(dbPlayer.name()); + } + + player.addProperty("uuid", dbPlayer.uuid()); + player.addProperty("name", dbPlayer.name()); player.addProperty("rank", rank.toString()); - player.addProperty("world", usernames.get(user)); + player.addProperty("online", online); + player.addProperty("world", world); response.add(player); } catch (Exception e) { throw new RuntimeException(e); @@ -70,28 +86,35 @@ public JsonObject handleRequest(String uuid) { } } - for (String user : usernames.keySet()) { + for (String username : usernames.keySet()) { try { - if (!McUtil.getUuid(user).equals(uuid)) { - continue; - } - Player dbPlayer = Database.getPlayers().find(eq("uuid", uuid)).first(); - Rank rank; - if (dbPlayer == null) { - rank = Rank.Unknown; - } else { - response.addProperty("streak", dbPlayer.streak().days()); - rank = dbPlayer.rank(); + if (Database.getPlayers().countDocuments(new Document("name", username)) == 0) { + new Player(McUtil.getUuid(username), username, Rank.Unknown, List.of(), new Player.Streak(1, false), Instant.EPOCH).write(); } - - response.addProperty("name", user); - response.addProperty("rank", rank.toString()); - response.addProperty("world", usernames.get(user)); } catch (Exception e) { throw new RuntimeException(e); } } + Player dbPlayer = Database.getPlayers().find(eq("uuid", uuid)).first(); + try { + boolean online = false; + String world = ""; + if (usernames.get(dbPlayer.name()) != null) { + online = true; + world = usernames.get(dbPlayer.name()); + } + + response.addProperty("uuid", dbPlayer.uuid()); + response.addProperty("name", dbPlayer.name()); + response.addProperty("rank", dbPlayer.rank().toString()); + response.addProperty("streak", dbPlayer.streak().days()); + response.addProperty("online", online); + response.addProperty("world", world); + } catch (Exception e) { + throw new RuntimeException(e); + } + return response; } } diff --git a/src/client/java/net/legitimoose/bot/scraper/Player.java b/src/client/java/net/legitimoose/bot/scraper/Player.java index c335234..d4f7bed 100644 --- a/src/client/java/net/legitimoose/bot/scraper/Player.java +++ b/src/client/java/net/legitimoose/bot/scraper/Player.java @@ -29,7 +29,7 @@ public void write() { Updates.set("uuid", this.uuid), Updates.set("name", this.name), Updates.set("rank", this.rank), - Updates.set("blocked", this.blocked), + Updates.setOnInsert("blocked", this.blocked), Updates.set("streak", this.streak), Updates.set("last_joined", new BsonDateTime(this.last_joined.toEpochMilli()))); Database.getPlayers().updateOne(eq("uuid", this.uuid), updates, new UpdateOptions().upsert(true)); diff --git a/src/client/java/net/legitimoose/bot/scraper/Scraper.java b/src/client/java/net/legitimoose/bot/scraper/Scraper.java index d27c09f..a91ba55 100644 --- a/src/client/java/net/legitimoose/bot/scraper/Scraper.java +++ b/src/client/java/net/legitimoose/bot/scraper/Scraper.java @@ -10,6 +10,7 @@ import net.legitimoose.bot.LegitimooseBotClient; import net.legitimoose.bot.util.DiscordUtil; import net.legitimoose.bot.util.DiscordWebhook; +import net.legitimoose.bot.util.Unicode; import net.minecraft.client.Minecraft; import net.minecraft.core.component.DataComponents; import net.minecraft.nbt.CompoundTag; @@ -25,6 +26,7 @@ import java.io.IOException; import java.net.URISyntaxException; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -46,7 +48,7 @@ public class Scraper { private final DiscordWebhook errorWebhook = new DiscordWebhook(CONFIG.getString("errorWebhook")); private final Pattern jamScorePattern = Pattern.compile("^CategoryScore\\(rank=(.*), score=(.*)\\)"); - private final Pattern ownerNamePattern = Pattern.compile("^by (?:[^|]+\\|\\s*)?(.+)"); + private final Pattern ownerNamePattern = Pattern.compile("^by (?:([^|]+) \\|\\s*)?(.+)"); private void waitSeconds(int time) { try { @@ -88,7 +90,7 @@ private void error(String message, Exception exception) throws IOException, URIS errorWebhook.execute(); } - public void scrape() { + public void scrape() throws IOException, URISyntaxException, InterruptedException { if (!CONFIG.getBoolean("scrape", true)) return; Minecraft client = Minecraft.getInstance(); MongoCollection stats = Database.getStats(); @@ -130,6 +132,57 @@ public void scrape() { client.player.closeContainer(); + String lobbyRawDescription = "["; + + + World lobby = new World( + "Nov 28, 2023, 4:57 PM", + 1701219420, + + false, + true, + + "5f4641cb-a718-4556-8c87-fbf153a8cc9a", + "Legitermoose", + Rank.Moose, + + Minecraft.getInstance().getConnection().getOnlinePlayers().size(), + + 100, + 100, + + "", + "lobby", + + // Change on moose/bot update + "1.21.10", + + 300000, + 300000, + + false, + + "Legitimoose Lobby", + Unicode.normalize("Legitimoose Lobby"), + Minecraft.getInstance().getCurrentServer().motd.getString(), + + "{\"text\":\"Legitimoose Lobby\",\"color\":\"white\",\"italic\":false}", + lobbyRawDescription + ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, Minecraft.getInstance().getCurrentServer().motd) + .result() + .get() + .toString() + "]", + + -1, + + new JsonObject(), + + "minecraft:grass_block", + + System.currentTimeMillis() / 1000L, + System.currentTimeMillis() + ); + bulkUpsert(List.of(lobby), List.of()); + client.player.connection.sendCommand("worlds"); waitSeconds(1); @@ -140,6 +193,7 @@ public void scrape() { LOGGER.info("Last page is: {}", max_pages); for (int i = 1; i <= max_pages; i++) { List worlds = new ArrayList<>(); + List players = new ArrayList<>(); Container inv = client.player.containerMenu.getSlot(0).container; for (int j = 0; j <= 26; j++) { @@ -159,14 +213,41 @@ public void scrape() { } String owner_name = ""; + Rank owner_rank = Rank.Unknown; int ownerLine = descriptionLines; while (!itemStack.get(DataComponents.LORE).lines().get(ownerLine).getString().startsWith("by")) { ownerLine++; } Matcher ownerNameMatcher = ownerNamePattern.matcher(itemStack.get(DataComponents.LORE).lines().get(ownerLine).getString()); if (ownerNameMatcher.find()) { - owner_name = ownerNameMatcher.group(1); + owner_name = ownerNameMatcher.group(2); + if (ownerNameMatcher.group(1) == null) { + owner_rank = Rank.Non; + } else { + owner_rank = Rank.getEnum(ownerNameMatcher.group(1)); + } + } + + String owner_uuid = getNbtString(publicBukkitValues, "owner"); + Player dbPlayer = Database.getPlayers().find(eq("uuid", owner_uuid)).first(); + int streak; + Instant last_joined; + if (dbPlayer == null) { + last_joined = Instant.EPOCH; + streak = 0; + } else { + if (dbPlayer.last_joined() != null) { + last_joined = dbPlayer.last_joined(); + } else { + last_joined = Instant.EPOCH; + } + if (dbPlayer.streak() == null) { + streak = 0; + } else { + streak = dbPlayer.streak().days(); + } } + players.add(new Player(owner_uuid, owner_name, owner_rank, List.of(), new Player.Streak(streak, false), last_joined)); StringBuilder description = new StringBuilder(); for (int k = 0; k < descriptionLines; k++) { @@ -211,6 +292,8 @@ public void scrape() { } } + String itemName = itemStack.get(DataComponents.CUSTOM_NAME).getString(); + World world = new World( getNbtString(publicBukkitValues, "creation_date"), getNbtInt(publicBukkitValues, "creation_date_unix_seconds"), @@ -218,8 +301,9 @@ public void scrape() { getNbtBoolean(publicBukkitValues, "enforce_whitelist"), getNbtBoolean(publicBukkitValues, "locked"), - getNbtString(publicBukkitValues, "owner"), + owner_uuid, owner_name, + owner_rank, getNbtInt(publicBukkitValues, "player_count"), getNbtInt(publicBukkitValues, "max_players"), @@ -234,7 +318,8 @@ public void scrape() { getNbtBoolean(publicBukkitValues, "whitelist_on_version_change"), - itemStack.get(DataComponents.CUSTOM_NAME).getString(), + itemName, + Unicode.normalize(itemName), description.toString(), ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, itemStack.get(DataComponents.CUSTOM_NAME)) @@ -245,19 +330,17 @@ public void scrape() { featured_instant, - jam_world, - jam_id, - jam, itemStack.toString().substring(2), System.currentTimeMillis() / 1000L, System.currentTimeMillis() ); + worlds.add(world); LOGGER.info("Scraped World {} {}: {}", j, world.world_uuid(), world.name()); } - bulkUpsert(worlds); + bulkUpsert(worlds, players); // finally, click on next page button LOGGER.info("Scraped page #{}", i); Minecraft.getInstance() @@ -271,8 +354,9 @@ public void scrape() { LOGGER.info("Finished Scraping"); } - private void bulkUpsert(List worlds) { + private void bulkUpsert(List worlds, List players) { List> operations = new ArrayList<>(); + List> playerOperations = new ArrayList<>(); LOGGER.info("writing world"); for (World world : worlds) { Bson updates = @@ -283,6 +367,7 @@ private void bulkUpsert(List worlds) { Updates.set("locked", world.locked()), Updates.set("owner_uuid", world.owner_uuid()), Updates.set("owner_name", world.owner_name()), + Updates.set("owner_rank", world.owner_rank()), Updates.set("player_count", world.player_count()), Updates.set("max_players", world.max_players()), Updates.set("max_datapack_size", world.max_datapack_size()), @@ -292,12 +377,11 @@ private void bulkUpsert(List worlds) { Updates.set("votes", world.votes()), Updates.set("whitelist_on_version_change", world.whitelist_on_version_change()), Updates.set("name", world.name()), + Updates.set("normalized_name", world.normalized_name()), Updates.set("description", world.description()), Updates.set("raw_name", Document.parse(world.raw_name())), Updates.set("raw_description", BsonArray.parse(world.raw_description())), Updates.set("featured_instant", world.featured_instant()), - Updates.set("jam_world", world.jam_world()), - Updates.set("jam_id", world.jam_id()), Updates.set("jam", Document.parse(world.jam().toString())), Updates.set("icon", world.icon()), Updates.set("last_scraped", world.last_scraped()), @@ -309,10 +393,32 @@ private void bulkUpsert(List worlds) { )); } + for (Player player : players) { + Bson playerUpdates = + Updates.combine( + Updates.set("uuid", player.uuid()), + Updates.set("name", player.name()), + Updates.set("rank", player.rank()), + Updates.set("streak", player.streak()), + Updates.set("last_joined", new BsonDateTime(player.last_joined().toEpochMilli())), + Updates.setOnInsert("blocked", player.blocked())); + + playerOperations.add(new UpdateOneModel<>( + eq("uuid", player.uuid()), + playerUpdates, + new UpdateOptions().upsert(true) + )); + } + if (!operations.isEmpty()) { Database.getWorlds().bulkWrite(operations); } + + if (!playerOperations.isEmpty()) { + Database.getPlayers().bulkWrite(playerOperations); + } LOGGER.info("Bulk wrote {} worlds", operations.size()); + LOGGER.info("Bulk wrote {} players", playerOperations.size()); } private String getNbtString(CompoundTag tag, String field) { diff --git a/src/client/java/net/legitimoose/bot/scraper/World.java b/src/client/java/net/legitimoose/bot/scraper/World.java index c34e2b4..e3ded5a 100644 --- a/src/client/java/net/legitimoose/bot/scraper/World.java +++ b/src/client/java/net/legitimoose/bot/scraper/World.java @@ -1,7 +1,6 @@ package net.legitimoose.bot.scraper; import com.google.gson.JsonObject; -import org.jetbrains.annotations.ApiStatus; public record World( String creation_date, @@ -10,6 +9,7 @@ public record World( boolean locked, String owner_uuid, String owner_name, + Rank owner_rank, int player_count, int max_players, int max_datapack_size, @@ -20,19 +20,12 @@ public record World( int votes, boolean whitelist_on_version_change, String name, + String normalized_name, String description, String raw_name, String raw_description, int featured_instant, - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "3.0.0") // API v4 - boolean jam_world, - - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "3.0.0") // API v4 - int jam_id, - JsonObject jam, String icon, diff --git a/src/client/java/net/legitimoose/bot/util/Unicode.java b/src/client/java/net/legitimoose/bot/util/Unicode.java new file mode 100644 index 0000000..38eb3c6 --- /dev/null +++ b/src/client/java/net/legitimoose/bot/util/Unicode.java @@ -0,0 +1,104 @@ +package net.legitimoose.bot.util; + +import java.util.Map; + +public class Unicode { + private static final Map CONFUSABLES = Map.ofEntries( + Map.entry(" ", " "), + Map.entry("0", "⓿"), + Map.entry("1", "11⓵➊⑴¹𝟏𝟙1𝟷𝟣⒈𝟭1➀₁①❶⥠"), + Map.entry("2", "⓶⒉⑵➋ƻ²ᒿ𝟚2𝟮𝟤ᒾ𝟸Ƨ𝟐②ᴤ₂➁❷ᘝƨ"), + Map.entry("3", "³ȝჳⳌꞫ𝟑ℨ𝟛𝟯𝟥Ꝫ➌ЗȜ⓷ӠƷ3𝟹⑶⒊ʒʓǯǮƺ𝕴ᶾзᦡ➂③₃ᶚᴣᴟ❸ҘҙӬӡӭӟӞ"), + Map.entry("4", "𝟰𝟺𝟦𝟒➍ҶᏎ𝟜ҷ⓸ҸҹӴӵᶣ4чㄩ⁴➃₄④❹Ӌ⑷⒋"), + Map.entry("5", "𝟱⓹➎Ƽ𝟓𝟻𝟝𝟧5➄₅⑤⁵❺ƽ⑸⒌"), + Map.entry("6", "Ⳓ🄇𝟼Ꮾ𝟲𝟞𝟨𝟔➏⓺Ϭϭ⁶б6ᧈ⑥➅₆❻⑹⒍"), + Map.entry("7", "𝟕𝟟𝟩𝟳𝟽🄈⓻𐓒➐7⁷⑦₇❼➆⑺⒎"), + Map.entry("8", "𐌚🄉➑⓼8𝟠𝟪৪⁸₈𝟴➇⑧❽𝟾𝟖⑻⒏"), + Map.entry("9", "൭Ꝯ𝝑𝞋𝟅🄊𝟡𝟵Ⳋ⓽➒੧৭୨9𝟫𝟿𝟗⁹₉Գ➈⑨❾⑼⒐"), + Map.entry("10", "⓾❿➉➓🔟⑩⑽⒑"), + Map.entry("11", "⑪⑾⒒⓫"), + Map.entry("12", "⑫⑿⒓⓬"), + Map.entry("13", "⑬⒀⒔⓭"), + Map.entry("14", "⑭⒁⒕⓮"), + Map.entry("15", "⑮⒂⒖⓯"), + Map.entry("16", "⑯⒃⒗⓰"), + Map.entry("17", "⑰⒄⒘⓱"), + Map.entry("18", "⑱⒅⒙⓲"), + Map.entry("19", "⑲⒆⒚⓳"), + Map.entry("20", "⑳⒇⒛⓴"), + Map.entry("ae", "æ"), + Map.entry("OE", "Œ"), + Map.entry("oe", "œ"), + Map.entry("pi", "ᒆ"), + Map.entry("Nj", "Nj"), + Map.entry("AE", "ᴁ"), + Map.entry("A", "𝑨𝔄ᗄ𝖠𝗔ꓯ𝞐🄐🄰Ꭿ𐊠𝕬𝜜𝐴ꓮᎪ𝚨ꭺ𝝖🅐Å∀🇦₳🅰𝒜𝘈𝐀𝔸дǺᗅⒶAΑᾋᗩĂÃÅǍȀȂĀȺĄʌΛλƛᴀᴬДАልÄₐᕱªǞӒΆẠẢẦẨẬẮẰẲẴẶᾸᾹᾺΆᾼᾈᾉᾊᾌᾍᾎᾏἈἉἊἋἌἍἎἏḀȦǠӐÀÁÂẤẪ𝛢𝓐𝙰𝘼ᗩ"), + Map.entry("a", "∂⍺ⓐձǟᵃᶏ⒜аɒaαȃȁคǎმäɑāɐąᾄẚạảǡầẵḁȧӑӓãåάὰάăẩằẳặᾀᾁᾂᾃᾅᾆᾰᾱᾲᾳᾴᶐᾶᾷἀἁἂἃἄἅἆἇᾇậắàáâấẫǻⱥ𝐚𝑎𝒂𝒶𝓪𝔞𝕒𝖆𝖺𝗮𝘢𝙖𝚊𝛂𝛼𝜶𝝰𝞪⍶"), + Map.entry("B", "🄑𝔙𝖁ꞵ𝛃𝛽𝜷𝝱𝞫Ᏸ𐌁𝑩𝕭🄱𐊡𝖡𝘽ꓐ𝗕𝘉𝜝𐊂𝚩𝐁𝛣𝝗𝐵𝙱𝔹Ᏼᏼ𝞑Ꞵ𝔅🅑฿𝓑ᗿᗾᗽ🅱ⒷBвϐᗷƁ乃ßცჩ๖βɮБՅ๒ᙖʙᴮᵇጌḄℬΒВẞḂḆɃദᗹᗸᵝᙞᙟᙝᛒᙗᙘᴃ🇧"), + Map.entry("b", "ꮟᏏ𝐛𝘣𝒷𝔟𝓫𝖇𝖻𝑏𝙗𝕓𝒃𝗯𝚋♭ᑳᒈbᖚᕹᕺⓑḃḅҍъḇƃɓƅᖯƄЬᑲþƂ⒝ЪᶀᑿᒀᒂᒁᑾьƀҌѢѣᔎ"), + Map.entry("C", "ꞆႠ℃🄒ᏟⲤ🄲ꓚ𐊢𐌂🅲𐐕🅒☾ČÇⒸCↃƇᑕㄈ¢८↻ĈϾՇȻᙅᶜ⒞ĆҀĊ©टƆℂℭϹС匚ḈҪʗᑖᑡᑢᑣᑤᑥⅭ𝐂𝐶𝑪𝒞𝓒𝕮𝖢𝗖𝘊𝘾ᔍ"), + Map.entry("c", "🝌cⅽ𝐜𝑐𝒄𝒸𝓬𝔠𝕔𝖈𝖼𝗰𝘤𝙘𝚌ᴄϲⲥсꮯ𐐽ⲥ𐐽ꮯĉcⓒćčċçҁƈḉȼↄсርᴄϲҫ꒝ςɽϛ𝙲ᑦ᧚𝐜𝑐𝒄𝒸𝓬𝔠𝕔𝖈𝖼𝗰𝘤𝙘𝚌₵🇨ᥴᒼⅽ"), + Map.entry("D", "🄓Ꭰ🄳𝔡𝖉𝔻𝗗𝘋𝙳𝐷𝓓𝐃𝑫𝕯𝖣𝔇𝘿ꭰⅅ𝒟ꓓ🅳🅓ⒹDƉᗪƊÐԺᴅᴰↁḊĐÞⅮᗞᑯĎḌḐḒḎᗫᗬᗟᗠᶛᴆ🇩"), + Map.entry("d", "Ꮷ𝔡𝖉ᑯꓒ𝓭ᵭ₫ԃⓓdḋďḍḑḓḏđƌɖɗᵈ⒟ԁⅾᶁԀᑺᑻᑼᑽᒄᑰᑱᶑ𝕕𝖽𝑑𝘥𝒅𝙙𝐝𝗱𝚍ⅆ𝒹ʠժ"), + Map.entry("E", "£ᙓ⋿∃ⴺꓱ𝐄𝐸𝔈𝕰𝖤𝘌𝙴𝛦𝜠ꭼ🄔🄴𝙀𝔼𐊆𝚬ꓰ𝝚𝞔𝓔𝑬𝗘🅴🅔ⒺΈEƎἝᕮƐモЄᴇᴱᵉÉ乇ЁɆꂅ€ÈℰΕЕⴹᎬĒĔĖĘĚÊËԐỀẾỄỂẼḔḖẺȄȆẸỆȨḜḘḚἘἙἚἛἜῈΈӖὲέЀϵ🇪"), + Map.entry("e", "əәⅇꬲꞓ⋴𝛆𝛜𝜀𝜖𝜺𝝐𝝴𝞊𝞮𝟄ⲉꮛ𐐩ꞒⲈ⍷𝑒𝓮𝕖𝖊𝘦𝗲𝚎𝙚𝒆𝔢𝖾𝐞Ҿҿⓔe⒠èᧉéᶒêɘἔềếễ૯ǝєεēҽɛểẽḕḗĕėëẻěȅȇẹệȩɇₑęḝḙḛ℮еԑѐӗᥱёἐἑἒἓἕℯ"), + Map.entry("F", "ᖵꘘꓞꟻᖷ𝐅𝐹𝑭𝔽𝕱𝖥𝗙𝙁𝙵𝟊℉🄕🄵𐊇𝔉𝘍𐊥ꓝꞘ🅵🅕𝓕ⒻFғҒᖴƑԲϝቻḞℱϜ₣🇫Ⅎ"), + Map.entry("f", "𝐟ᵮ𝑓𝒇𝒻𝓯𝔣𝕗𝖿𝗳𝙛𝚏ꬵꞙẝ𝖋ⓕfƒḟʃբᶠ⒡ſꊰʄ∱ᶂ𝘧"), + Map.entry("G", "𝗚𝘎🄖ꓖᏳ🄶Ꮐᏻ𝔾𝓖𝑮𝕲ꮐ𝒢𝙂𝖦𝙶𝔊𝐺𝐆🅶🅖ⒼGɢƓʛĢᘜᴳǴĠԌĜḠĞǦǤԍ₲🇬⅁"), + Map.entry("g", "ᶃᶢⓖgǵĝḡğġǧģց૭ǥɠﻭﻮᵍ⒢ℊɡᧁ𝐠𝑔𝒈𝓰𝔤𝕘𝖌𝗀𝗴𝘨𝙜𝚐"), + Map.entry("H", "Ἤ🄗𝆦🄷𝜢ꓧ𝘏𝐻𝝜𝖧𐋏𝗛ꮋℍᎻℌⲎ𝑯𝞖🅷🅗ዞǶԋⒽHĤᚺḢḦȞḤḨḪĦⱧҢңҤῊΉῌἨἩἪἫἭἮἯᾘᾙᾚᾛᾜᾝᾞᾟӉӈҥΉн卄♓𝓗ℋН𝐇𝙃𝙷ʜ𝛨Η𝚮ᕼӇᴴᵸ🇭"), + Map.entry("h", "ꞕ৸𝕳ꚕᏲℏӊԊꜧᏂҺ⒣ђⓗhĥḣḧȟḥḩḫẖħⱨհһከኩኪካɦℎ𝐡𝒉𝒽𝓱𝔥𝕙𝖍𝗁𝗵𝘩𝙝𝚑իʰᑋᗁɧんɥ"), + Map.entry("I", "ⲒἿ🄘🄸ЇꀤᏆ🅸🅘إﺇٳأﺃٲٵⒾI៸ÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗェエῘῙῚΊἸἹἺἻἼἽἾⅠΪΊɪᶦᑊᥣ𝛪𝐈𝙄𝙸𝓵𝐼ᴵ𝚰𝑰🇮"), + Map.entry("i", "⍳ℹⅈ𝑖𝒊𝒾ı𝚤ɩιιͺ𝛊𝜄𝜾𝞲ꙇӏꭵᎥⓘiìíîĩīĭïḯỉǐȉȋịḭῐῑῒΐῖῗἰἱἲⅰⅼ∣ⵏ│׀ا١۱ߊᛁἳἴἵɨіὶίᶖ𝔦𝚒𝝸𝗂𝐢𝕚𝖎𝗶𝘪𝙞ίⁱᵢ𝓲⒤"), + Map.entry("J", "𝐉𝐽𝑱𝒥𝓙𝔍𝕁𝕵𝖩𝗝𝘑𝙅𝙹ꞲͿꓙ🄙🄹🅹🅙ⒿJЈʝᒍנフĴʆวلյʖᴊᴶﻝጋɈⱼՂๅႱįᎫȷ丿ℐℑᒘᒙᒚᒛᒴᒵᒎᒏ🇯"), + Map.entry("j", "𝚥ꭻⅉⓙjϳʲ⒥ɉĵǰјڶᶨ𝒿𝘫𝗷𝑗𝙟𝔧𝒋𝗃𝓳𝕛𝚓𝖏𝐣"), + Map.entry("K", "𝐊ꝄꝀ𝐾𝑲𝓚𝕶𝖪𝙺𝚱𝝟🄚𝗞🄺𝜥𝘒ꓗ𝙆𝕂Ⲕ𝔎𝛫Ꮶ𝞙𝒦🅺🅚₭ⓀKĸḰќƘкҠκқҟӄʞҚКҡᴋᴷᵏ⒦ᛕЌጕḲΚKҜҝҞĶḴǨⱩϗӃ🇰"), + Map.entry("k", "ⓚꝁkḱǩḳķḵƙⱪᶄ𝐤𝘬𝗄𝕜𝜅𝜘𝜿𝝒𝝹𝞌𝞳𝙠𝚔𝑘𝒌ϰ𝛋𝛞𝟆𝗸𝓴𝓀"), + Map.entry("L", "𝐋𝐿𝔏𝕃𝕷𝖫𝗟𝘓𝙇ﴼ🄛🄻𐐛Ⳑ𝑳𝙻𐑃𝓛ⳑꮮᏞꓡ🅻🅛ﺈ└ⓁւLĿᒪ乚ՆʟꓶιԼᴸˡĹረḶₗΓլĻᄂⅬℒⱢᥧᥨᒻᒶᒷᶫﺎᒺᒹᒸᒫ⎳ㄥŁⱠﺄȽ🇱"), + Map.entry("l", "𝙡ⓛlŀĺľḷḹļӀℓḽḻłレɭƚɫⱡ|Ɩ⒧ʅǀוןΙІ|ᶩӏ𝓘𝕀𝖨𝗜𝘐𝐥𝑙𝒍𝓁𝔩𝕝𝖑𝗅𝗹𝘭𝚕𝜤𝝞ı𝚤ɩι𝛊𝜄𝜾𝞲"), + Map.entry("M", "ꮇ🄜🄼𐌑𐊰ꓟⲘᎷ🅼🅜ⓂMмṂ൱ᗰ州ᘻო๓♏ʍᙏᴍᴹᵐ⒨ḾМṀ௱ⅯℳΜϺᛖӍӎ𝐌𝑀𝑴𝓜𝔐𝕄𝕸𝖬𝗠𝘔𝙈𝙼𝚳𝛭𝜧𝝡𝞛🇲"), + Map.entry("m", "₥ᵯ𝖒𝐦𝗆𝔪𝕞𝓂ⓜmനᙢ൩ḿṁⅿϻṃጠɱ៳ᶆ𝙢𝓶𝚖𝑚𝗺᧕᧗"), + Map.entry("N", "𝇙𝇚𝇜🄝𝆧𝙉🄽ℕꓠ𝛮𝝢𝙽𝚴𝑵𝑁Ⲛ𝐍𝒩𝞜𝗡𝘕𝜨𝓝𝖭🅽₦🅝ЙЍⓃҋ៷NᴎɴƝᑎ几иՈռИהЛπᴺᶰŃ刀ክṄⁿÑПΝᴨոϖǸŇṆŅṊṈทŊӢӣӤӥћѝйᥢҊᴻ🇳"), + Map.entry("n", "ոռח𝒏𝓷𝙣𝑛𝖓𝔫𝗇𝚗𝗻ᥒⓝήnǹᴒńñᾗηṅňṇɲņṋṉղຖՌƞŋ⒩ภกɳпʼnлԉȠἠἡῃդᾐᾑᾒᾓᾔᾕᾖῄῆῇῂἢἣἤἥἦἧὴήበቡቢባቤብቦȵ𝛈𝜂𝜼𝝶𝞰𝕟𝘯𝐧𝓃ᶇᵰᥥ∩"), + Map.entry("O", "𝜽⭘🔿ꭴ⭕⏺🄁🄀Ꭴ𝚯𝚹𝛩𝛳𝜣𝜭𝝝𝝧𝞗𝞡ⴱᎾᏫ⍬𝞱𝝷𝛉𝟎𝜃θ𝟘𝑂𝑶𝓞𝔒𝕆𝕺𝗢𝘖𝙊𝛰㈇ꄲ🄞🔾🄾𐊒𝟬ꓳⲞ𐐄𐊫𐓂𝞞🅞⍥◯ⵁ⊖0⊝𝝤Ѳϴ𝚶𝜪ѺӦӨӪΌʘ𝐎ǑÒŎÓÔÕȌȎㇿ❍ⓄOὋロ❤૦⊕ØФԾΘƠᴼᵒ⒪ŐÖₒ¤◊Φ〇ΟОՕଠഠ௦סỒỐỖỔṌȬṎŌṐṒȮȰȪỎỜỚỠỞỢỌỘǪǬǾƟⵔ߀៰⍜⎔⎕⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃ὈὉὊὌὍ"), + Map.entry("o", "ంಂംං૦௦۵ℴ𝑜𝒐𝖔ꬽ𝝄𝛔𝜎𝝈𝞂ჿ𝚘০୦ዐ𝛐𝗈𝞼ဝⲟ𝙤၀𐐬𝔬𐓪𝓸🇴⍤○ϙ🅾𝒪𝖮𝟢𝟶𝙾𝘰𝗼𝕠𝜊𝐨𝝾𝞸ᐤⓞѳ᧐ᥲðoఠᦞՓòөӧóºōôǒȏŏồốȍỗổõσṍȭṏὄṑṓȯȫ๏ᴏőöѻоዐǭȱ০୦٥౦೦൦๐໐οօᴑ०੦ỏơờớỡởợọộǫøǿɵծὀὁόὸόὂὃὅ"), + Map.entry("P", "🄟🄿ꓑ𝚸𝙿𝞠𝙋ꮲⲢ𝒫𝝦𝑃𝑷𝗣𝐏𐊕𝜬𝘗𝓟𝖯𝛲Ꮲ🅟Ҏ🅿ⓅPƤᑭ尸Ṗրφքᴘᴾᵖ⒫ṔアקРየᴩⱣℙΡῬᑸᑶᑷᑹᑬᑮ🇵₱"), + Map.entry("p", "ⲣҏ℗ⓟpṕṗƥᵽῥρрƿǷῤ⍴𝓹𝓅𝐩𝑝𝒑𝔭𝕡𝖕𝗉𝗽𝘱𝙥𝚙𝛒𝝆𝞺𝜌𝞀"), + Map.entry("Q", "🅀🄠Ꝗ🆀🅠ⓆQℚⵕԚ𝐐𝑄𝑸𝒬𝓠𝚀𝘘𝙌𝖰𝕼𝔔𝗤🇶"), + Map.entry("q", "𝓆ꝗ𝗾ⓠqգ⒬۹զᑫɋɊԛ𝗊𝑞𝘲𝕢𝚚𝒒𝖖𝐪𝔮𝓺𝙦"), + Map.entry("R", "℞🄡℟ꭱᏒ𐒴ꮢᎡꓣ🆁🅡ⓇRᴙȒʀᖇя尺ŔЯરƦᴿዪṚɌʁℛℜℝṘŘȐṜŖṞⱤ𝐑𝑅𝑹𝓡𝕽𝖱𝗥𝘙𝙍𝚁ᚱ🇷ᴚ"), + Map.entry("r", "𝚛ꭇᣴℾ𝚪𝛤𝜞𝝘𝞒ⲄГᎱᒥꭈⲅꮁⓡrŕṙřȑȓṛṝŗгՐɾᥬṟɍʳ⒭ɼѓᴦᶉ𝐫𝑟𝒓𝓇𝓻𝔯𝕣𝖗𝗋𝗿𝘳𝙧ᵲґᵣ"), + Map.entry("S", "🅂🄪🄢ꇙ𝓢𝗦Ꮪ𝒮Ꮥ𝚂𝐒ꓢ𝖲𝔖𝙎𐊖𝕾𐐠𝘚𝕊𝑆𝑺🆂🅢ⓈSṨŞֆՏȘˢ⒮ЅṠŠŚṤŜṦṢടᔕᔖᔢᔡᔣᔤ"), + Map.entry("s", "ᣵⓢꜱ𐑈ꮪsśṥŝṡšṧʂṣṩѕşșȿᶊక𝐬𝑠𝒔𝓈𝓼𝔰𝕤𝖘𝗌𝘀𝘴𝙨𝚜ގ🇸"), + Map.entry("T", "🅃🄣七ፒ𝜯🆃𐌕𝚻𝛵𝕋𝕿𝑻𐊱𐊗𝖳𝙏🝨𝝩𝞣𝚃𝘛𝑇ꓔ⟙𝐓Ⲧ𝗧⊤𝔗Ꭲꭲ𝒯🅣⏇⏉ⓉTтҬҭƬイŦԵτᴛᵀイፕϮŤ⊥ƮΤТ下ṪṬȚŢṰṮ丅丁ᐪ𝛕𝜏𝝉𝞃𝞽𝓣ㄒ🇹ጥ"), + Map.entry("t", "ⓣtṫẗťṭțȶ੮էʇ†ţṱṯƭŧᵗ⒯ʈեƫ𝐭𝑡𝒕𝓉𝓽𝔱𝕥𝖙𝗍𝘁𝘵𝙩𝚝ナ"), + Map.entry("U", "🅄Џ🄤ሀꓴ𐓎꒤🆄🅤ŨŬŮᑗᑘǓǕǗǙⓊUȖᑌ凵ƱմԱꓵЦŪՄƲᙀᵁᵘ⒰ŰપÜՍÙÚÛṸṺǛỦȔƯỪỨỮỬỰỤṲŲṶṴɄᥩᑧ∪ᘮ⋃𝐔𝑈𝑼𝒰𝓤𝔘𝕌𝖀𝖴𝗨𝘜𝙐𝚄🇺"), + Map.entry("u", "𝘂𝘶𝙪𝚞ꞟꭎꭒ𝛖𝜐𝝊𝞄𝞾𐓶ὺύⓤuùũūừṷṹŭǖữᥙǚǜὗυΰนսʊǘǔúůᴜűųยûṻцሁüᵾᵤµʋủȕȗưứửựụṳṵʉῠῡῢΰῦῧὐὑϋύὒὓὔὕὖᥔ𝐮𝑢𝒖𝓊𝓾𝔲𝕦𝖚𝗎ᶙ"), + Map.entry("V", "𝑉𝒱𝕍𝗩🄥🅅ꓦ𝑽𝖵𝘝Ꮩ𝚅𝙑𝐕🆅🅥ⓋVᐯѴᵛ⒱۷ṾⅴⅤṼ٧ⴸѶᐺᐻ🇻𝓥"), + Map.entry("v", "∨⌄⋁ⅴ𝐯𝑣𝒗𝓋𝔳𝕧𝖛𝗏ꮩሀⓥv𝜐𝝊ṽṿ౮งѵעᴠνטᵥѷ៴ᘁ𝙫𝚟𝛎𝜈𝝂𝝼𝞶𝘷𝘃𝓿"), + Map.entry("W", "𝐖𝑊𝓦𝔚𝕎𝖂𝖶𝗪𝙒𝚆🄦🅆ᏔᎳ𝑾ꓪ𝒲𝘞🆆Ⓦ🅦wWẂᾧᗯᥕ山ѠຟచաЩШώщฬшᙎᵂʷ⒲ฝሠẄԜẀŴẆẈധᘺѿᙡƜ₩🇼"), + Map.entry("w", "𝐰ꝡ𝑤𝒘𝓌𝔀𝔴𝕨𝖜𝗐𝘄𝘸𝙬𝚠աẁꮃẃⓦ⍵ŵẇẅẘẉⱳὼὠὡὢὣωὤὥὦὧῲῳῴῶῷⱲѡԝᴡώᾠᾡᾢᾣᾤᾥᾦɯ𝝕𝟉𝞏"), + Map.entry("X", "ꭓꭕ𝛘𝜒𝝌𝞆𝟀ⲭ🞨𝑿𝛸🄧🞩🞪🅇🞫🞬𐌗Ⲭꓫ𝖃𝞦𝘟𐊐𝚾𝝬𝜲Ꭓ𐌢𝖷𝑋𝕏𝔛𐊴𝗫🆇🅧❌Ⓧ𝓧XẊ᙭χㄨ𝒳ӾჯӼҳЖΧҲᵡˣ⒳אሸẌꊼⅩХ╳᙮ᕁᕽⅹᚷⵝ𝙓𝚇乂𝐗🇽"), + Map.entry("x", "᙮ⅹ𝑥𝒙𝓍𝔵𝕩𝖝𝗑𝘅ᕁᕽⓧxхẋ×ₓ⤫⤬⨯ẍᶍ𝙭ӽ𝘹𝐱𝚡⨰メ𝔁"), + Map.entry("Y", "𝒴🄨𝓨𝔜𝖄𝖸𝘠𝙔𝚼𝛶𝝪𝞤УᎩᎽⲨ𝚈𝑌𝗬𝐘ꓬ𝒀𝜰𐊲🆈🅨ⓎYὛƳㄚʏ⅄ϔ¥¥ՎϓγץӲЧЎሃŸɎϤΥϒҮỲÝŶỸȲẎỶỴῨῩῪΎὙὝὟΫΎӮӰҰұ𝕐🇾"), + Map.entry("y", "𝐲𝑦𝒚𝓎𝔂𝔶𝕪𝖞𝗒𝘆𝘺𝙮𝚢ʏỿꭚγℽ𝛄𝛾𝜸𝝲𝞬🅈ᎽᎩⓨyỳýŷỹȳẏÿỷуყẙỵƴɏᵞɣʸᶌү⒴ӳӱӯўУʎ"), + Map.entry("Z", "🄩🅉ꓜ𝗭𝐙☡Ꮓ𝘡🆉🅩ⓏZẔƵ乙ẐȤᶻ⒵ŹℤΖŻŽẒⱫ🇿"), + Map.entry("z", "𝑍𝒁𝒵𝓩𝖹𝙕𝚉𝚭𝛧𝜡𝝛𝞕ᵶꮓ𝐳𝑧𝒛𝓏𝔃𝔷𝕫𝖟𝗓𝘇𝘻𝙯𝚣ⓩzźẑżžẓẕƶȥɀᴢጊʐⱬᶎʑᙆ") + ); + + public static String normalize(String orig) { + StringBuilder normalized = new StringBuilder(); + for (char c : orig.toCharArray()) { + String cStr = Character.toString(c); + normalized.append( + CONFUSABLES + .entrySet() + .stream() + .filter(e -> e.getValue().contains(cStr)) + .map(Map.Entry::getKey) + .findFirst().orElse(cStr) + ); + } + return normalized.toString(); + } +}