diff --git a/src/main/java/tools/mapletools/SkillbookChanceFetcher.java b/src/main/java/tools/mapletools/SkillbookChanceFetcher.java index 9d9d065f68..1e88f7e638 100644 --- a/src/main/java/tools/mapletools/SkillbookChanceFetcher.java +++ b/src/main/java/tools/mapletools/SkillbookChanceFetcher.java @@ -1,7 +1,6 @@ package tools.mapletools; import server.life.MonsterStats; -import tools.Pair; import java.io.IOException; import java.io.PrintWriter; @@ -10,119 +9,135 @@ import java.nio.file.Path; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; /** - * @author RonanLana *

- * This application traces missing meso drop data on the underlying DB (that must be + * This application normalizes skillbook drop chances on the underlying DB (that must be * defined on the DatabaseConnection file of this project) and generates a * SQL file that proposes missing drop entries for the drop_data table. *

- * The meso range is calculated accordingly with the target mob stats, such as level - * and if it's a boss or not, similarly as how it has been done for the actual meso - * drops. + * The drop chances are calculated based on the mob level. Bosses get a higher drop chance. + * Legend skillbooks gain a boost in drop chance (for some reason). + * + * @author RonanLana + * @author Ponk */ public class SkillbookChanceFetcher { private static final Path OUTPUT_FILE = ToolConstants.getOutputFile("skillbook_drop_data.sql"); - private static final Map, Integer> skillbookChances = new HashMap<>(); - private static PrintWriter printWriter; - private static Map mobStats; + private record DropIdentifier(int mobId, int itemId) {} - private static List, Integer>> sortedSkillbookChances() { - List, Integer>> skillbookChancesList = new ArrayList<>(skillbookChances.entrySet()); + public static void main(String[] args) { + Instant start = Instant.now(); - skillbookChancesList.sort((o1, o2) -> { - if (o1.getKey().getLeft().equals(o2.getKey().getLeft())) { - return o1.getKey().getRight() < o2.getKey().getRight() ? -1 : (o1.getKey().getRight().equals(o2.getKey().getRight()) ? 0 : 1); + System.out.println("Fetching all skillbook drops from the database..."); + List skillbookDrops = fetchAllSkillbookDrops(); + System.out.printf("Found %d skillbook drops%n", skillbookDrops.size()); + + // load mob stats from WZ + Map mobStats = MonsterStatFetcher.getAllMonsterStats(); + + System.out.println("Calculating drop chances..."); + Map skillbookChances = calculateSkillbookDropChances(skillbookDrops, mobStats); + + System.out.println("Writing output file..."); + generateSkillbookChanceUpdateFile(skillbookChances); + + Duration totalDuration = Duration.between(start, Instant.now()); + System.out.printf("Done! Total elapsed time: %d ms", totalDuration.toMillis()); + } + + private static List fetchAllSkillbookDrops() { + String sql = """ + SELECT dropperid, itemid + FROM drop_data + WHERE itemid >= 2280000 + AND itemid < 2300000"""; + try (Connection con = SimpleDatabaseConnection.getConnection(); + PreparedStatement ps = con.prepareStatement(sql); + ResultSet rs = ps.executeQuery()) { + List skillbookDrops = new ArrayList<>(); + while (rs.next()) { + int mobId = rs.getInt("dropperid"); + int itemId = rs.getInt("itemid"); + skillbookDrops.add(new DropIdentifier(mobId, itemId)); + } + return skillbookDrops; + } catch (Exception e) { + throw new RuntimeException("Failed to fetch all skillbook drops", e); + } + } + + private static Map calculateSkillbookDropChances(List skillbookDrops, + Map mobStats) { + Map skillbookChances = new HashMap<>(); + for (DropIdentifier drop : skillbookDrops) { + int expectedChance = 250; + + if (mobStats.get(drop.mobId) != null) { + int level = mobStats.get(drop.mobId).getLevel(); + expectedChance *= Math.max(2, (level - 80) / 15); + + if (mobStats.get(drop.mobId).isBoss()) { + expectedChance *= 20; + } + } else { + expectedChance = 1287; } - return (o1.getKey().getLeft() < o2.getKey().getLeft()) ? -1 : 1; - }); + if (isLegendSkillUpgradeBook(drop.itemId)) { // drop rate of Legends seems to be higher than explorers, in retrospect from values in DB + expectedChance *= 3; + } - return skillbookChancesList; + skillbookChances.put(drop, expectedChance); + } + + return skillbookChances; } private static boolean isLegendSkillUpgradeBook(int itemid) { int itemidBranch = itemid / 10000; - return (itemidBranch == 228 && itemid >= 2280013 || itemidBranch == 229 && itemid >= 2290126); // drop rate of Legends are higher + return (itemidBranch == 228 && itemid >= 2280013 || itemidBranch == 229 && itemid >= 2290126); // drop rate of Legends are higher } - private static void fetchSkillbookDropChances() { - Connection con = SimpleDatabaseConnection.getConnection(); - - try { - PreparedStatement ps = con.prepareStatement("SELECT dropperid, itemid FROM drop_data WHERE itemid >= 2280000 AND itemid < 2300000"); - ResultSet rs = ps.executeQuery(); - - while (rs.next()) { - int mobid = rs.getInt("dropperid"); - int itemid = rs.getInt("itemid"); - - int expectedChance = 250; - - if (mobStats.get(mobid) != null) { - int level = mobStats.get(mobid).getLevel(); - expectedChance *= Math.max(2, (level - 80) / 15); - - if (mobStats.get(mobid).isBoss()) { - expectedChance *= 20; - } else { - expectedChance *= 1; - } - } else { - expectedChance = 1287; - } - - if (isLegendSkillUpgradeBook(itemid)) { // drop rate of Legends seems to be higher than explorers, in retrospect from values in DB - expectedChance *= 3; - } - - skillbookChances.put(new Pair<>(mobid, itemid), expectedChance); - } - - rs.close(); - ps.close(); - con.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private static void printSkillbookChanceUpdateSqlHeader() { - printWriter.println(" # SQL File autogenerated from the MapleSkillbookChanceFetcher feature by Ronan Lana."); - printWriter.println(" # Generated data takes into account mob stats such as level and boss for the chance rates."); - printWriter.println(); - - printWriter.println(" REPLACE INTO drop_data (`dropperid`, `itemid`, `minimum_quantity`, `maximum_quantity`, `questid`, `chance`) VALUES"); - } - - private static void generateSkillbookChanceUpdateFile() { + private static void generateSkillbookChanceUpdateFile(Map skillbookChances) { try (PrintWriter pw = new PrintWriter(Files.newOutputStream(OUTPUT_FILE))) { - printWriter = pw; + printSkillbookChanceUpdateSqlHeader(pw); - printSkillbookChanceUpdateSqlHeader(); - - List, Integer>> skillbookChancesList = sortedSkillbookChances(); - for (Map.Entry, Integer> e : skillbookChancesList) { - printWriter.println("(" + e.getKey().getLeft() + ", " + e.getKey().getRight() + ", 1, 1, 0, " - + e.getValue() + "),"); + List> skillbookChancesList = sortedSkillbookChances(skillbookChances); + for (Map.Entry e : skillbookChancesList) { + pw.println("(%d, %d, 1, 1, 0, %d),".formatted(e.getKey().mobId, e.getKey().itemId, e.getValue())); } - } catch (IOException ioe) { - ioe.printStackTrace(); + } catch (IOException e) { + throw new RuntimeException("Failed to write output file", e); } } - public static void main(String[] args) { - // load mob stats from WZ - mobStats = MonsterStatFetcher.getAllMonsterStats(); + private static void printSkillbookChanceUpdateSqlHeader(PrintWriter pw) { + pw.println(" # SQL File autogenerated from the SkillbookChanceFetcher feature by Ronan Lana."); + pw.println(" # Generated data takes into account mob stats such as level and boss for the chance rates."); + pw.println(); - fetchSkillbookDropChances(); - generateSkillbookChanceUpdateFile(); + pw.println(" REPLACE INTO drop_data (`dropperid`, `itemid`, `minimum_quantity`, `maximum_quantity`, `questid`, `chance`) VALUES"); + } + + private static List> sortedSkillbookChances( + Map skillbookChances) { + List> skillbookChancesList = new ArrayList<>(skillbookChances.entrySet()); + skillbookChancesList.sort(mobIdAscThenItemIdAscComparator()); + return skillbookChancesList; + } + + private static Comparator> mobIdAscThenItemIdAscComparator() { + return Comparator.comparing((Map.Entry entry) -> entry.getKey().mobId) + .thenComparing(entry -> entry.getKey().itemId); } }