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);
}
}