diff --git a/src/main/java/server/life/MapleLifeFactory.java b/src/main/java/server/life/MapleLifeFactory.java
index d8a8914341..556a8953c9 100644
--- a/src/main/java/server/life/MapleLifeFactory.java
+++ b/src/main/java/server/life/MapleLifeFactory.java
@@ -322,7 +322,7 @@ public class MapleLifeFactory {
private int id;
private byte chance, x;
- private loseItem(int id, byte chance, byte x) {
+ public loseItem(int id, byte chance, byte x) {
this.id = id;
this.chance = chance;
this.x = x;
@@ -347,7 +347,7 @@ public class MapleLifeFactory {
private int removeAfter;
private int hp;
- private selfDestruction(byte action, int removeAfter, int hp) {
+ public selfDestruction(byte action, int removeAfter, int hp) {
this.action = action;
this.removeAfter = removeAfter;
this.hp = hp;
diff --git a/src/main/java/tools/DatabaseConnection.java b/src/main/java/tools/DatabaseConnection.java
index ba74ee0f18..0246152ea3 100644
--- a/src/main/java/tools/DatabaseConnection.java
+++ b/src/main/java/tools/DatabaseConnection.java
@@ -63,6 +63,10 @@ public class DatabaseConnection {
* @return true if connection to the database initiated successfully, false if not successful
*/
public static boolean initializeConnectionPool() {
+ if (dataSource != null) {
+ return true;
+ }
+
log.info("Initializing connection pool...");
final HikariConfig config = getConfig();
Instant initStart = Instant.now();
diff --git a/src/main/java/tools/mapletools/ArrowFetcher.java b/src/main/java/tools/mapletools/ArrowFetcher.java
new file mode 100644
index 0000000000..d492414a1a
--- /dev/null
+++ b/src/main/java/tools/mapletools/ArrowFetcher.java
@@ -0,0 +1,220 @@
+/*
+ This file is part of the HeavenMS MapleStory Server
+ Copyleft (L) 2016 - 2019 RonanLana
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation version 3 as published by
+ the Free Software Foundation. You may not use, modify or distribute
+ this program under any other version of the GNU Affero General Public
+ License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+package tools.mapletools;
+
+import server.life.MapleMonsterStats;
+import tools.Pair;
+
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * @author RonanLana
+ *
+ * This application traces arrow drop data on the underlying DB (that must be
+ * defined on the DatabaseConnection file of this project) and generates a SQL file
+ * that proposes updated arrow quantitty on drop entries for the drop_data table.
+ *
+ * The arrow quantity range is calculated accordingly with the target mob stats, such
+ * as level and if it's a boss or not.
+ */
+
+public class ArrowFetcher {
+ private static final String OUTPUT_FILE_NAME = "arrow_drop_data.sql";
+ private static final int MIN_ARROW_ID = 2060000;
+ private static final int MAX_ARROW_ID = 2061004;
+ private static final float CORRECTION_FACTOR = 2.2f;
+
+ private static final Map> mobRange = new HashMap<>();
+ private static PrintWriter printWriter;
+ private static Map mobStats;
+
+ private static Pair calcArrowRange(int level, boolean boss) {
+ int minRange, maxRange;
+
+ // MIN range
+ minRange = (int) Math.ceil(((2.870503597 * level) - 1.870503597) * (boss ? 1.4 : 1.0) / CORRECTION_FACTOR);
+
+ // MAX range
+ maxRange = (int) Math.ceil(1.25 * minRange);
+
+ return new Pair<>(minRange, maxRange);
+ }
+
+ private static void calcAllMobsArrowRange() {
+ System.out.print("Calculating range... ");
+
+ for (Entry mobStat : mobStats.entrySet()) {
+ MapleMonsterStats mms = mobStat.getValue();
+ Pair arrowRange;
+
+ arrowRange = calcArrowRange(mms.getLevel(), mms.isBoss());
+ mobRange.put(mobStat.getKey(), arrowRange);
+ }
+
+ System.out.println("done!");
+ }
+
+ private static void printSqlHeader() {
+ printWriter.println(" # SQL File autogenerated from the MapleArrowFetcher feature by Ronan Lana.");
+ printWriter.println(" # Generated data takes into account mob stats such as level and boss for the raw arrow ranges.");
+ printWriter.println(" # Only current arrows entries on the DB it was compiled are being updated here.");
+ printWriter.println();
+
+ printWriter.println("UPDATE drop_data");
+ printWriter.println("SET minimum_quantity = CASE");
+ }
+
+ private static void printSqlMiddle() {
+ printWriter.println(" ELSE minimum_quantity END,");
+ printWriter.println(" maximum_quantity = CASE");
+ }
+
+ private static void printSqlFooter() {
+ printWriter.println(" ELSE maximum_quantity END");
+ printWriter.println(";");
+ }
+
+ private static void updateSqlMobArrowMinEntry(int[] entry) {
+ printWriter.println(" WHEN dropperid = " + entry[0] + " AND itemid = " + entry[1] + " THEN " + entry[2]);
+ }
+
+ private static void updateSqlMobArrowMaxEntry(int[] entry) {
+ printWriter.println(" WHEN dropperid = " + entry[0] + " AND itemid = " + entry[1] + " THEN " + entry[3]);
+ }
+
+ private static List getArrowEntryValues(Map> existingEntries) {
+ List entryValues = new ArrayList<>(200);
+
+ List>> listEntries = new ArrayList<>(existingEntries.entrySet());
+
+ listEntries.sort((o1, o2) -> {
+ int val1 = o1.getKey();
+ int val2 = o2.getKey();
+ return Integer.compare(val1, val2);
+ });
+
+ for (Entry> ee : listEntries) {
+ int mobid = ee.getKey();
+ Pair mr = mobRange.get(mobid);
+
+ for (Integer itemid : ee.getValue()) {
+ int itemWeight = (itemid % 10) + 1;
+
+ int[] values = new int[4];
+ values[0] = mobid;
+ values[1] = itemid;
+
+ values[2] = (int) Math.ceil(mr.getLeft() / itemWeight); // weighted min quantity
+ values[3] = (int) Math.ceil(mr.getRight() / itemWeight); // weighted max quantity
+
+ entryValues.add(values);
+ }
+ }
+
+ return entryValues;
+ }
+
+ private static void updateMobsArrowRange() {
+ System.out.print("Generating updated ranges... ");
+ final Connection con = SimpleDatabaseConnection.getConnection();
+
+ Map> existingEntries = new HashMap<>(200);
+
+ try {
+ // select all arrow drop entries on the DB, to update their values
+ PreparedStatement ps = con.prepareStatement("SELECT dropperid, itemid FROM drop_data WHERE itemid >= " + MIN_ARROW_ID + " AND itemid <= " + MAX_ARROW_ID + " ORDER BY itemid;");
+ ResultSet rs = ps.executeQuery();
+
+ if (rs.isBeforeFirst()) {
+ while (rs.next()) {
+ int mobid = rs.getInt(1);
+ int itemid = rs.getInt(2);
+
+ if (mobRange.containsKey(mobid)) {
+ List em = existingEntries.get(mobid);
+
+ if (em == null) {
+ em = new ArrayList<>(2);
+ existingEntries.put(mobid, em);
+ }
+
+ em.add(itemid);
+ }
+ }
+
+ if (!existingEntries.isEmpty()) {
+ List entryValues = getArrowEntryValues(existingEntries);
+
+ printWriter = new PrintWriter(ToolConstants.getOutputFile(OUTPUT_FILE_NAME), StandardCharsets.UTF_8);
+ printSqlHeader();
+
+ for (int[] arrowEntry : entryValues) {
+ updateSqlMobArrowMinEntry(arrowEntry);
+ }
+
+ printSqlMiddle();
+
+ for (int[] arrowEntry : entryValues) {
+ updateSqlMobArrowMaxEntry(arrowEntry);
+ }
+
+ printSqlFooter();
+
+ printWriter.close();
+ } else {
+ throw new Exception("NO DATA");
+ }
+
+ } else {
+ throw new Exception("NO DATA");
+ }
+
+ rs.close();
+ ps.close();
+ con.close();
+
+ System.out.println("done!");
+
+ } catch (Exception e) {
+ if (e.getMessage() != null && e.getMessage().equals("NO DATA")) {
+ System.out.println("failed! The DB has no arrow entry to be updated.");
+ } else {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ // load mob stats from WZ
+ mobStats = MonsterStatFetcher.getAllMonsterStats();
+
+ calcAllMobsArrowRange();
+ updateMobsArrowRange();
+ }
+}
diff --git a/src/main/java/tools/mapletools/MonsterStatFetcher.java b/src/main/java/tools/mapletools/MonsterStatFetcher.java
new file mode 100644
index 0000000000..61cbe5a2cc
--- /dev/null
+++ b/src/main/java/tools/mapletools/MonsterStatFetcher.java
@@ -0,0 +1,143 @@
+package tools.mapletools;
+
+import provider.*;
+import provider.wz.MapleDataType;
+import provider.wz.WZFiles;
+import server.life.Element;
+import server.life.ElementalEffectiveness;
+import server.life.MapleLifeFactory.BanishInfo;
+import server.life.MapleLifeFactory.loseItem;
+import server.life.MapleLifeFactory.selfDestruction;
+import server.life.MapleMonsterStats;
+import tools.Pair;
+
+import java.util.*;
+
+public class MonsterStatFetcher {
+ private static final MapleDataProvider data = MapleDataProviderFactory.getDataProvider(WZFiles.MOB);
+ private static final MapleDataProvider stringDataWZ = MapleDataProviderFactory.getDataProvider(WZFiles.STRING);
+ private static final MapleData mobStringData = stringDataWZ.getData("Mob.img");
+ private static final Map monsterStats = new HashMap<>();
+
+ static Map getAllMonsterStats() {
+ MapleDataDirectoryEntry root = data.getRoot();
+
+ System.out.print("Parsing mob stats... ");
+ for (MapleDataFileEntry mFile : root.getFiles()) {
+ try {
+ String fileName = mFile.getName();
+
+ //System.out.println("Parsing '" + fileName + "'");
+ MapleData monsterData = data.getData(fileName);
+ if (monsterData == null) {
+ continue;
+ }
+
+ Integer mid = getMonsterId(fileName);
+
+ MapleData monsterInfoData = monsterData.getChildByPath("info");
+ MapleMonsterStats stats = new MapleMonsterStats();
+ stats.setHp(MapleDataTool.getIntConvert("maxHP", monsterInfoData));
+ stats.setFriendly(MapleDataTool.getIntConvert("damagedByMob", monsterInfoData, 0) == 1);
+ stats.setPADamage(MapleDataTool.getIntConvert("PADamage", monsterInfoData));
+ stats.setPDDamage(MapleDataTool.getIntConvert("PDDamage", monsterInfoData));
+ stats.setMADamage(MapleDataTool.getIntConvert("MADamage", monsterInfoData));
+ stats.setMDDamage(MapleDataTool.getIntConvert("MDDamage", monsterInfoData));
+ stats.setMp(MapleDataTool.getIntConvert("maxMP", monsterInfoData, 0));
+ stats.setExp(MapleDataTool.getIntConvert("exp", monsterInfoData, 0));
+ stats.setLevel(MapleDataTool.getIntConvert("level", monsterInfoData));
+ stats.setRemoveAfter(MapleDataTool.getIntConvert("removeAfter", monsterInfoData, 0));
+ stats.setBoss(MapleDataTool.getIntConvert("boss", monsterInfoData, 0) > 0);
+ stats.setExplosiveReward(MapleDataTool.getIntConvert("explosiveReward", monsterInfoData, 0) > 0);
+ stats.setFfaLoot(MapleDataTool.getIntConvert("publicReward", monsterInfoData, 0) > 0);
+ stats.setUndead(MapleDataTool.getIntConvert("undead", monsterInfoData, 0) > 0);
+ stats.setName(MapleDataTool.getString(mid + "/name", mobStringData, "MISSINGNO"));
+ stats.setBuffToGive(MapleDataTool.getIntConvert("buff", monsterInfoData, -1));
+ stats.setCP(MapleDataTool.getIntConvert("getCP", monsterInfoData, 0));
+ stats.setRemoveOnMiss(MapleDataTool.getIntConvert("removeOnMiss", monsterInfoData, 0) > 0);
+
+ MapleData special = monsterInfoData.getChildByPath("coolDamage");
+ if (special != null) {
+ int coolDmg = MapleDataTool.getIntConvert("coolDamage", monsterInfoData);
+ int coolProb = MapleDataTool.getIntConvert("coolDamageProb", monsterInfoData, 0);
+ stats.setCool(new Pair<>(coolDmg, coolProb));
+ }
+ special = monsterInfoData.getChildByPath("loseItem");
+ if (special != null) {
+ for (MapleData liData : special.getChildren()) {
+ stats.addLoseItem(new loseItem(MapleDataTool.getInt(liData.getChildByPath("id")), (byte) MapleDataTool.getInt(liData.getChildByPath("prop")), (byte) MapleDataTool.getInt(liData.getChildByPath("x"))));
+ }
+ }
+ special = monsterInfoData.getChildByPath("selfDestruction");
+ if (special != null) {
+ stats.setSelfDestruction(new selfDestruction((byte) MapleDataTool.getInt(special.getChildByPath("action")), MapleDataTool.getIntConvert("removeAfter", special, -1), MapleDataTool.getIntConvert("hp", special, -1)));
+ }
+ MapleData firstAttackData = monsterInfoData.getChildByPath("firstAttack");
+ int firstAttack = 0;
+ if (firstAttackData != null) {
+ if (firstAttackData.getType() == MapleDataType.FLOAT) {
+ firstAttack = Math.round(MapleDataTool.getFloat(firstAttackData));
+ } else {
+ firstAttack = MapleDataTool.getInt(firstAttackData);
+ }
+ }
+ stats.setFirstAttack(firstAttack > 0);
+ stats.setDropPeriod(MapleDataTool.getIntConvert("dropItemPeriod", monsterInfoData, 0) * 10000);
+
+ stats.setTagColor(MapleDataTool.getIntConvert("hpTagColor", monsterInfoData, 0));
+ stats.setTagBgColor(MapleDataTool.getIntConvert("hpTagBgcolor", monsterInfoData, 0));
+
+ for (MapleData idata : monsterData) {
+ if (!idata.getName().equals("info")) {
+ int delay = 0;
+ for (MapleData pic : idata.getChildren()) {
+ delay += MapleDataTool.getIntConvert("delay", pic, 0);
+ }
+ stats.setAnimationTime(idata.getName(), delay);
+ }
+ }
+ MapleData reviveInfo = monsterInfoData.getChildByPath("revive");
+ if (reviveInfo != null) {
+ List revives = new LinkedList<>();
+ for (MapleData data_ : reviveInfo) {
+ revives.add(MapleDataTool.getInt(data_));
+ }
+ stats.setRevives(revives);
+ }
+ decodeElementalString(stats, MapleDataTool.getString("elemAttr", monsterInfoData, ""));
+ MapleData monsterSkillData = monsterInfoData.getChildByPath("skill");
+ if (monsterSkillData != null) {
+ int i = 0;
+ List> skills = new ArrayList<>();
+ while (monsterSkillData.getChildByPath(Integer.toString(i)) != null) {
+ skills.add(new Pair<>(MapleDataTool.getInt(i + "/skill", monsterSkillData, 0), MapleDataTool.getInt(i + "/level", monsterSkillData, 0)));
+ i++;
+ }
+ stats.setSkills(skills);
+ }
+ MapleData banishData = monsterInfoData.getChildByPath("ban");
+ if (banishData != null) {
+ stats.setBanishInfo(new BanishInfo(MapleDataTool.getString("banMsg", banishData), MapleDataTool.getInt("banMap/0/field", banishData, -1), MapleDataTool.getString("banMap/0/portal", banishData, "sp")));
+ }
+
+ monsterStats.put(mid, stats);
+ } catch(NullPointerException npe) {
+ //System.out.println("[SEVERE] " + mFile.getName() + " failed to load. Issue: " + npe.getMessage() + "\n\n");
+ }
+ }
+
+ System.out.println("Done parsing mob stats!");
+ return monsterStats;
+ }
+
+ private static int getMonsterId(String fileName) {
+ return Integer.parseInt(fileName.substring(0, 7));
+ }
+
+ private static void decodeElementalString(MapleMonsterStats stats, String elemAttr) {
+ for (int i = 0; i < elemAttr.length(); i += 2) {
+ stats.setEffectiveness(Element.getFromChar(elemAttr.charAt(i)), ElementalEffectiveness.getByNumber(Integer.valueOf(String.valueOf(elemAttr.charAt(i + 1)))));
+ }
+ }
+
+}
diff --git a/src/main/java/tools/mapletools/SimpleDatabaseConnection.java b/src/main/java/tools/mapletools/SimpleDatabaseConnection.java
new file mode 100644
index 0000000000..80f5f1bf88
--- /dev/null
+++ b/src/main/java/tools/mapletools/SimpleDatabaseConnection.java
@@ -0,0 +1,30 @@
+package tools.mapletools;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.config.Configurator;
+import tools.DatabaseConnection;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+final class SimpleDatabaseConnection {
+ private SimpleDatabaseConnection() {}
+
+ static Connection getConnection() {
+ muffleLogging();
+ DatabaseConnection.initializeConnectionPool();
+
+ try {
+ return DatabaseConnection.getConnection();
+ } catch (SQLException e) {
+ throw new IllegalStateException("Failed to get database connection", e);
+ }
+ }
+
+ private static void muffleLogging() {
+ final Level minimumVisibleLevel = Level.WARN;
+ Configurator.setLevel(LogManager.getLogger(com.zaxxer.hikari.HikariDataSource.class).getName(), minimumVisibleLevel);
+ Configurator.setRootLevel(minimumVisibleLevel);
+ }
+}
diff --git a/src/main/java/tools/mapletools/ToolConstants.java b/src/main/java/tools/mapletools/ToolConstants.java
new file mode 100644
index 0000000000..aa097172cf
--- /dev/null
+++ b/src/main/java/tools/mapletools/ToolConstants.java
@@ -0,0 +1,12 @@
+package tools.mapletools;
+
+import java.io.File;
+
+public class ToolConstants {
+ public static final File INPUT_DIRECTORY = new File("tools/input");
+ public static final File OUTPUT_DIRECTORY = new File("tools/output");
+
+ public static File getOutputFile(String fileName) {
+ return new File(OUTPUT_DIRECTORY, fileName);
+ }
+}
diff --git a/tools/input/.gitignore b/tools/input/.gitignore
new file mode 100644
index 0000000000..c96a04f008
--- /dev/null
+++ b/tools/input/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/tools/output/.gitignore b/tools/output/.gitignore
new file mode 100644
index 0000000000..c96a04f008
--- /dev/null
+++ b/tools/output/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file