Merge branch 'master' into netty

This commit is contained in:
P0nk
2021-07-11 15:20:00 +02:00
1103 changed files with 4100 additions and 163889 deletions

View File

@@ -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();

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
* <p>
* 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.
* <p>
* 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<Integer, Pair<Integer, Integer>> mobRange = new HashMap<>();
private static PrintWriter printWriter;
private static Map<Integer, MapleMonsterStats> mobStats;
private static Pair<Integer, Integer> 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<Integer, MapleMonsterStats> mobStat : mobStats.entrySet()) {
MapleMonsterStats mms = mobStat.getValue();
Pair<Integer, Integer> 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<int[]> getArrowEntryValues(Map<Integer, List<Integer>> existingEntries) {
List<int[]> entryValues = new ArrayList<>(200);
List<Entry<Integer, List<Integer>>> listEntries = new ArrayList<>(existingEntries.entrySet());
listEntries.sort((o1, o2) -> {
int val1 = o1.getKey();
int val2 = o2.getKey();
return Integer.compare(val1, val2);
});
for (Entry<Integer, List<Integer>> ee : listEntries) {
int mobid = ee.getKey();
Pair<Integer, Integer> 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<Integer, List<Integer>> 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<Integer> em = existingEntries.get(mobid);
if (em == null) {
em = new ArrayList<>(2);
existingEntries.put(mobid, em);
}
em.add(itemid);
}
}
if (!existingEntries.isEmpty()) {
List<int[]> 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();
}
}

View File

@@ -0,0 +1,190 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @author RonanLana
* <p>
* This application parses the Mob.wz file inputted and generates a report showing
* all cases where a mob has a boss HP bar and doesn't have a "boss" label.
* <p>
* Running it should generate a report file under "lib" folder with the search results.
*/
public class BossHpBarFetcher {
private static final String OUTPUT_FILE_NAME = "boss_hp_bar_report.txt";
private static final int INITIAL_STRING_LENGTH = 50;
private static final List<Integer> missingBosses = new ArrayList<>();
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int curBoss;
private static int curHpTag;
private static int curMobId;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void readMobLabel(String token) {
String name = getName(token);
String value = getValue(token);
switch (name) {
case "boss" -> curBoss = Integer.parseInt(value);
case "hpTagColor" -> curHpTag = Integer.parseInt(value);
}
}
private static void processMobData() {
if (curHpTag != Integer.MAX_VALUE && curBoss == Integer.MAX_VALUE) {
missingBosses.add(curMobId);
}
}
private static void translateToken(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
if (status == 1) {
processMobData();
}
} else if (token.contains("imgdir")) {
if (status == 0) {
String mobText = getName(token);
curMobId = Integer.parseInt(mobText.substring(0, mobText.lastIndexOf('.')));
} else if (status == 1) { //getting info
d = getName(token);
if (!d.contentEquals("info")) {
forwardCursor(status);
} else {
curBoss = Integer.MAX_VALUE;
curHpTag = Integer.MAX_VALUE;
}
} else if (status == 2) {
forwardCursor(status);
}
status += 1;
} else {
if (status == 2) { //info tags
readMobLabel(token);
}
}
}
private static void readBossHpBarData() throws IOException {
String line;
final File mobDirectory = WZFiles.MOB.getFile();
for (File file : mobDirectory.listFiles()) {
if (file.isFile()) {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
bufferedReader.close();
fileReader.close();
}
}
}
private static void printReportFileHeader(PrintWriter writer) {
writer.println(" # Report File autogenerated from the MapleBossHpBarFetcher feature by Ronan Lana.");
writer.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
writer.println();
}
private static void printReportFileResults(PrintWriter writer) {
for (int mobId : missingBosses) {
writer.println("Missing 'isBoss' on id: " + mobId);
}
}
private static void reportBossHpBarData() {
// This will reference one line at a time
try {
System.out.println("Reading WZs...");
readBossHpBarData();
System.out.println("Reporting results...");
final PrintWriter printWriter = new PrintWriter(ToolConstants.getOutputFile(OUTPUT_FILE_NAME), StandardCharsets.UTF_8);
printReportFileHeader(printWriter);
printReportFileResults(printWriter);
printWriter.close();
System.out.println("Done!");
} catch (FileNotFoundException ex) {
System.out.println("Unable to open mob file.");
} catch (IOException ex) {
System.out.println("Error reading mob file.");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
reportBossHpBarData();
}
}

View File

@@ -0,0 +1,680 @@
package tools.mapletools;
import provider.wz.WZFiles;
import tools.Pair;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application parses the cosmetic recipes defined within "lib/care" folder, loads
* every present cosmetic itemid from the XML data, then checks the scripts for missed
* cosmetics within the stylist/surgeon. Results from the search are reported in a report
* file.
* <p>
* Note: to best make use of this feature, set IGNORE_CURRENT_SCRIPT_COSMETICS = true. This
* way, every available cosmetic present on the recipes will be listed on the report.
* <p>
* Estimated parse time: 1 minute
*/
public class CashCosmeticsChecker {
private static final String INPUT_DIRECTORY_PATH = ToolConstants.getInputFile("care").getPath();
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("cash_cosmetics_result.txt");
private static final boolean IGNORE_CURRENT_SCRIPT_COSMETICS = false; // Toggle to preference
private static final int INITIAL_STRING_LENGTH = 50;
private static final Map<Integer, Set<Integer>> scriptCosmetics = new HashMap<>();
private static final Map<Integer, String> scriptEntries = new HashMap<>(500);
private static final Set<Integer> allCosmetics = new HashSet<>();
private static final Set<Integer> unusedCosmetics = new HashSet<>();
private static final Map<Integer, List<Integer>> usedCosmetics = new HashMap<>();
private static final Map<Integer, String> couponNames = new HashMap<>();
private static final Map<Integer, Integer> cosmeticNpcs = new HashMap<>(); // expected only 1 NPC per cosmetic coupon (town care/salon)
private static final Map<List<String>, Integer> cosmeticNpcids = new HashMap<>();
private static final Set<String> missingCosmeticNames = new HashSet<>();
private static final Map<String, Integer> cosmeticNameIds = new HashMap<>();
private static final Map<Integer, String> cosmeticIdNames = new HashMap<>();
private static final Map<Pair<Integer, String>, Set<Integer>> missingCosmeticsNpcTypes = new HashMap<>();
private static PrintWriter printWriter = null;
private static InputStreamReader fileReader = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void translateToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
if (status == 3) {
String d = getName(token);
if (!(d.contentEquals("Face") || d.contentEquals("Hair"))) {
forwardCursor(status);
}
} else if (status == 4) {
String d = getName(token);
int itemid = Integer.parseInt(d);
int cosmeticid;
if (itemid >= 30000) {
cosmeticid = (itemid / 10) * 10;
} else {
cosmeticid = itemid - ((itemid / 100) % 10) * 100;
}
allCosmetics.add(cosmeticid);
forwardCursor(status);
}
}
}
private static void readEqpStringData(String eqpStringDirectory) throws IOException {
String line;
fileReader = new InputStreamReader(new FileInputStream(eqpStringDirectory), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
bufferedReader.close();
fileReader.close();
}
private static void loadCosmeticWzData() throws IOException {
System.out.println("Reading String.wz ...");
readEqpStringData(WZFiles.STRING.getFilePath() + "/Eqp.img.xml");
}
private static void setCosmeticUsage(List<Integer> usedByNpcids, int cosmeticid) {
if (!usedByNpcids.isEmpty()) {
usedCosmetics.put(cosmeticid, usedByNpcids);
} else {
unusedCosmetics.add(cosmeticid);
}
}
private static void listFiles(String directoryName, ArrayList<File> files) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
files.add(file);
} else if (file.isDirectory()) {
listFiles(file.getAbsolutePath(), files);
}
}
}
private static int getNpcIdFromFilename(String name) {
try {
return Integer.parseInt(name.substring(0, name.indexOf('.')));
} catch (Exception e) {
return -1;
}
}
private static List<Integer> findCosmeticDataNpcids(int itemid) {
List<Integer> npcids = new LinkedList<>();
for (Map.Entry<Integer, Set<Integer>> sc : scriptCosmetics.entrySet()) {
if (sc.getValue().contains(itemid)) {
npcids.add(itemid);
}
}
return npcids;
}
private static void loadScripts() throws IOException {
ArrayList<File> files = new ArrayList<>();
listFiles(ToolConstants.SCRIPTS_PATH + "/npc", files);
for (File f : files) {
Integer npcid = getNpcIdFromFilename(f.getName());
//System.out.println("Parsing " + f.getAbsolutePath());
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line;
StringBuilder stringBuffer = new StringBuilder();
boolean cosmeticNpc = false;
Set<Integer> cosmeticids = new HashSet<>();
while ((line = bufferedReader.readLine()) != null) {
String[] s = line.split("hair_. = Array\\(", 2);
if (s.length > 1) {
cosmeticNpc = true;
s = s[1].split("\\)", 2);
s = s[0].split(", ");
for (String st : s) {
if (!st.isEmpty()) {
int itemid = Integer.parseInt(st);
cosmeticids.add(itemid);
}
}
} else {
s = line.split("face_. = Array\\(", 2);
if (s.length > 1) {
cosmeticNpc = true;
s = s[1].split("\\)", 2);
s = s[0].split(", ");
for (String st : s) {
if (!st.isEmpty()) {
int itemid = Integer.parseInt(st);
cosmeticids.add(itemid);
}
}
}
}
stringBuffer.append(line).append("\n");
}
scriptEntries.put(npcid, stringBuffer.toString());
if (cosmeticNpc) {
scriptCosmetics.put(npcid, cosmeticids);
}
bufferedReader.close();
fileReader.close();
}
}
private static void processCosmeticScriptData() throws IOException {
System.out.println("Reading script files ...");
loadScripts();
if (IGNORE_CURRENT_SCRIPT_COSMETICS) {
for (Set<Integer> npcCosmetics : scriptCosmetics.values()) {
npcCosmetics.clear();
}
}
for (Integer itemid : allCosmetics) {
List<Integer> npcids = findCosmeticDataNpcids(itemid);
setCosmeticUsage(npcids, itemid);
}
}
private static List<Integer> loadCosmeticCouponids() throws IOException {
List<Integer> couponItemids = new LinkedList<>();
fileReader = new InputStreamReader(new FileInputStream(getHandbookFileName("/Cash.txt")), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
String[] s = line.split(" - ", 3);
int itemid = Integer.parseInt(s[0]);
if (itemid >= 5150000 && itemid < 5160000) {
couponItemids.add(itemid);
couponNames.put(itemid, s[1]);
}
}
bufferedReader.close();
fileReader.close();
return couponItemids;
}
private static List<Integer> findItemidOnScript(int itemid) {
List<Integer> files = new LinkedList<>();
String t = String.valueOf(itemid);
for (Map.Entry<Integer, String> text : scriptEntries.entrySet()) {
if (text.getValue().contains(t)) {
files.add(text.getKey());
}
}
return files;
}
private static void loadCosmeticCouponNpcs() throws IOException {
System.out.println("Locating cosmetic NPCs ...");
for (Integer itemid : loadCosmeticCouponids()) {
List<Integer> npcids = findItemidOnScript(itemid);
if (!npcids.isEmpty()) {
cosmeticNpcs.put(itemid, npcids.get(0));
}
}
}
private enum CosmeticType {
HAIRSTYLE,
HAIRCOLOR,
DIRTYHAIR,
FACE_SURGERY,
EYE_COLOR,
SKIN_CARE
}
private static Pair<Integer, CosmeticType> parseCosmeticCoupon(String[] tokens) {
for (int i = 0; i < tokens.length; i++) {
String s = tokens[i];
if (s.startsWith("Hair")) {
if (s.contentEquals("Hairstyle")) {
return new Pair<>(i, CosmeticType.HAIRSTYLE);
} else {
if (i - 1 >= 0 && tokens[i - 1].contentEquals("Dirty")) {
return new Pair<>(i - 1, CosmeticType.DIRTYHAIR);
} else if (i + 1 < tokens.length && tokens[i + 1].contentEquals("Color")) {
return new Pair<>(i, CosmeticType.HAIRCOLOR);
} else {
return new Pair<>(i, CosmeticType.HAIRSTYLE);
}
}
} else if (s.startsWith("Face")) {
return new Pair<>(i, CosmeticType.FACE_SURGERY);
} else if (s.startsWith("Cosmetic")) {
return new Pair<>(i, CosmeticType.EYE_COLOR);
} else if (s.startsWith("Plastic")) {
return new Pair<>(i, CosmeticType.FACE_SURGERY);
} else if (s.startsWith("Skin")) {
return new Pair<>(i, CosmeticType.SKIN_CARE);
}
}
return null;
}
private static List<String> getCosmeticCouponData(String town, String type, String subtype) {
List<String> ret = new ArrayList<>(3);
ret.add(town);
ret.add(type);
ret.add(subtype);
return ret;
}
private static List<String> parseCosmeticCoupon(String couponName) {
String town, type, subtype = "EXP";
String[] s = couponName.split(" Coupon ", 2);
if (s.length > 1) {
subtype = s[1].substring(1, s[1].length() - 1);
}
String[] tokens = s[0].split(" ");
Pair<Integer, CosmeticType> cosmeticData = parseCosmeticCoupon(tokens);
if (cosmeticData == null) {
return null;
}
town = "";
for (int i = 0; i < cosmeticData.left; i++) {
town += (tokens[i] + "_");
}
town = town.substring(0, town.length() - 1).toLowerCase();
switch (cosmeticData.right) {
case HAIRSTYLE:
type = "hair";
break;
case FACE_SURGERY:
type = "face";
break;
default:
return null;
}
return getCosmeticCouponData(town, type, subtype);
}
private static void generateCosmeticPlaceNpcs() {
for (Map.Entry<Integer, String> e : couponNames.entrySet()) {
Integer npcid = cosmeticNpcs.get(e.getKey());
if (npcid == null) {
continue;
}
String couponName = e.getValue();
List<String> couponData = parseCosmeticCoupon(couponName);
if (couponData == null) {
continue;
}
cosmeticNpcids.put(couponData, npcid);
}
}
private static Integer getCosmeticNpcid(String townName, String typeCosmetic, String typeCoupon) {
return cosmeticNpcids.get(getCosmeticCouponData(townName, typeCosmetic, typeCoupon));
}
private static String getCosmeticName(String name, boolean gender) {
final String genderString = gender ? "F" : "M";
return String.format("%s (%s)", name, genderString);
}
private static void loadCosmeticNames(String cosmeticPath) throws IOException {
fileReader = new InputStreamReader(new FileInputStream(cosmeticPath), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
String[] s = line.split(" - ", 3);
int itemid = Integer.parseInt(s[0]);
String name;
if (itemid < 30000) {
itemid = itemid - ((itemid / 100) % 10) * 100;
int idx = s[1].lastIndexOf(" ");
if (idx > -1) {
name = s[1].substring(0, idx);
} else {
name = s[1];
}
} else {
itemid = (Integer.valueOf(s[0]) / 10) * 10;
int idx = s[1].indexOf(" ");
if (idx > -1) {
name = s[1].substring(idx + 1);
} else {
name = s[1];
}
}
name = name.trim();
String cname = getCosmeticName(name, (((itemid / 1000) % 10) % 3) != 0);
/*
if (cosmeticNameIds.containsKey(cname) && Math.abs(cosmeticNameIds.get(cname) - itemid) > 50) {
System.out.println("Clashing '" + name + "' " + itemid + "/" + cosmeticNameIds.get(cname));
}
*/
cosmeticNameIds.put(cname, itemid);
cosmeticIdNames.put(itemid, name);
}
bufferedReader.close();
fileReader.close();
}
private static void loadCosmeticNames() throws IOException {
System.out.println("Reading cosmetics from handbook ...");
loadCosmeticNames(getHandbookFileName("/Equip/Face.txt"));
loadCosmeticNames(getHandbookFileName("/Equip/Hair.txt"));
}
private static String getHandbookFileName(String fileName) {
return ToolConstants.HANDBOOK_PATH + fileName;
}
private static List<Integer> fetchExpectedCosmetics(String[] cosmeticList, boolean gender) {
List<Integer> list = new LinkedList<>();
for (String cosmetic : cosmeticList) {
String cname = getCosmeticName(cosmetic, gender);
Integer itemid = cosmeticNameIds.get(cname);
if (itemid != null) {
list.add(itemid);
} else {
missingCosmeticNames.add(cosmetic);
}
}
return list;
}
private static void verifyCosmeticExpectedFile(File f) throws IOException {
String townName = f.getParent().substring(f.getParent().lastIndexOf("\\") + 1);
String typeCosmetic = f.getName().substring(0, f.getName().indexOf("."));
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
String[] s = line.split(": ", 2);
String[] t = s[0].split("ale ");
String typeCoupon = t[1];
boolean gender = !t[0].contentEquals("M");
Integer npcid = getCosmeticNpcid(townName, typeCosmetic, typeCoupon);
if (npcid != null) {
String[] cosmetics = s[1].split(", ");
List<Integer> cosmeticItemids = fetchExpectedCosmetics(cosmetics, gender);
Set<Integer> npcCosmetics = scriptCosmetics.get(npcid);
Set<Integer> missingCosmetics = new HashSet<>();
for (Integer itemid : cosmeticItemids) {
if (!npcCosmetics.contains(itemid)) {
missingCosmetics.add(itemid);
}
}
if (!missingCosmetics.isEmpty()) {
Pair<Integer, String> key = new Pair<>(npcid, typeCoupon);
Set<Integer> list = missingCosmeticsNpcTypes.get(key);
if (list == null) {
missingCosmeticsNpcTypes.put(key, missingCosmetics);
} else {
list.addAll(missingCosmetics);
}
}
}
}
bufferedReader.close();
fileReader.close();
}
private static void verifyCosmeticExpectedData() throws IOException {
System.out.println("Analyzing cosmetic NPC scripts ...");
ArrayList<File> cosmeticRecipes = new ArrayList<>();
listFiles(INPUT_DIRECTORY_PATH, cosmeticRecipes);
for (File f : cosmeticRecipes) {
verifyCosmeticExpectedFile(f);
}
}
private static List<Pair<Pair<Integer, String>, List<Integer>>> getSortedMapEntries(Map<Pair<Integer, String>, Set<Integer>> map) {
List<Pair<Pair<Integer, String>, List<Integer>>> list = new ArrayList<>(map.size());
for (Map.Entry<Pair<Integer, String>, Set<Integer>> e : map.entrySet()) {
List<Integer> il = new ArrayList<>(2);
il.addAll(e.getValue());
il.sort((o1, o2) -> o1 - o2);
list.add(new Pair<>(e.getKey(), il));
}
list.sort((o1, o2) -> {
int cmp = o1.getLeft().getLeft() - o2.getLeft().getLeft();
if (cmp == 0) {
return o1.getLeft().getRight().compareTo(o2.getLeft().getRight());
} else {
return cmp;
}
});
return list;
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleCashCosmeticsChecker feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server source files and the server-side WZ.xmls.");
printWriter.println();
}
private static Pair<List<Integer>, List<Integer>> getCosmeticReport(List<Integer> itemids) {
List<Integer> maleItemids = new LinkedList<>();
List<Integer> femaleItemids = new LinkedList<>();
for (Integer i : itemids) {
if ((((i / 1000) % 10) % 3) == 0) {
maleItemids.add(i);
} else {
femaleItemids.add(i);
}
}
return new Pair<>(maleItemids, femaleItemids);
}
private static void reportNpcCosmetics(List<Integer> itemids) {
if (!itemids.isEmpty()) {
String res = " ";
for (Integer i : itemids) {
res += (i + ", ");
unusedCosmetics.remove(i);
}
printWriter.println(res.substring(0, res.length() - 2));
}
}
private static void reportCosmeticResults() throws IOException {
System.out.println("Reporting results ...");
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
if (!missingCosmeticsNpcTypes.isEmpty()) {
printWriter.println("Found " + missingCosmeticsNpcTypes.size() + " entries with missing cosmetic entries.");
for (Pair<Pair<Integer, String>, List<Integer>> mcn : getSortedMapEntries(missingCosmeticsNpcTypes)) {
printWriter.println(" NPC " + mcn.getLeft());
Pair<List<Integer>, List<Integer>> genderItemids = getCosmeticReport(mcn.getRight());
reportNpcCosmetics(genderItemids.getLeft());
reportNpcCosmetics(genderItemids.getRight());
printWriter.println();
}
}
if (!unusedCosmetics.isEmpty()) {
printWriter.println("Unused cosmetics: " + unusedCosmetics.size());
List<Integer> list = new ArrayList<>(unusedCosmetics);
Collections.sort(list);
for (Integer i : list) {
printWriter.println(i + " " + cosmeticIdNames.get(i));
}
printWriter.println();
}
if (!missingCosmeticNames.isEmpty()) {
printWriter.println("Missing cosmetic itemids: " + missingCosmeticNames.size());
List<String> listString = new ArrayList<>(missingCosmeticNames);
Collections.sort(listString);
for (String c : listString) {
printWriter.println(c);
}
printWriter.println();
}
printWriter.close();
}
public static void main(String[] args) {
try {
loadCosmeticWzData();
processCosmeticScriptData();
loadCosmeticCouponNpcs();
generateCosmeticPlaceNpcs();
loadCosmeticNames();
verifyCosmeticExpectedData();
reportCosmeticResults();
System.out.println("Done!");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

View File

@@ -0,0 +1,115 @@
package tools.mapletools;
import server.MapleItemInformationProvider;
import tools.DatabaseConnection;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
*
* @author RonanLana
This application gathers info from the WZ.XML files, fetching all cosmetic coupons and tickets from there, and then
searches the NPC script files, identifying the stylish NPCs that supposedly uses them. It will reports all NPCs that
uses up a card, as well as report those currently unused.
Estimated parse time: 10 seconds
*/
public class CashCosmeticsFetcher {
private static final Map<Integer, String> scriptEntries = new HashMap<>(500);
private static void listFiles(String directoryName, ArrayList<File> files) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
files.add(file);
} else if (file.isDirectory()) {
listFiles(file.getAbsolutePath(), files);
}
}
}
private static int getNpcIdFromFilename(String name) {
try {
return Integer.parseInt(name.substring(0, name.indexOf('.')));
} catch(Exception e) {
return -1;
}
}
private static void loadScripts() throws Exception {
ArrayList<File> files = new ArrayList<>();
listFiles(ToolConstants.SCRIPTS_PATH + "/npc", files);
for(File f : files) {
Integer npcid = getNpcIdFromFilename(f.getName());
//System.out.println("Parsing " + f.getAbsolutePath());
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(fileReader);
StringBuilder stringBuffer = new StringBuilder();
String line;
while((line = bufferedReader.readLine())!=null){
stringBuffer.append(line).append("\n");
}
scriptEntries.put(npcid, stringBuffer.toString());
bufferedReader.close();
fileReader.close();
}
}
private static List<Integer> findItemidOnScript(int itemid) {
List<Integer> files = new LinkedList<>();
String t = String.valueOf(itemid);
for (Map.Entry<Integer, String> text : scriptEntries.entrySet()) {
if (text.getValue().contains(t)) {
files.add(text.getKey());
}
}
return files;
}
private static void reportCosmeticCouponResults() {
final MapleItemInformationProvider ii = MapleItemInformationProvider.getInstance();
for (int itemid = 5150000; itemid <= 5154000; itemid++) {
String itemName = ii.getName(itemid);
if (itemName != null) {
List<Integer> npcids = findItemidOnScript(itemid);
if (!npcids.isEmpty()) {
System.out.println("Itemid " + itemid + " found on " + npcids + ". (" + itemName + ")");
} else {
System.out.println("NOT FOUND ITEMID " + itemid + " (" + itemName + ")");
}
}
}
}
public static void main(String[] args) {
DatabaseConnection.initializeConnectionPool(); // MapleItemInformationProvider loads unrelated stuff from the db
try {
loadScripts();
System.out.println("Loaded scripts");
reportCosmeticCouponResults();
} catch(Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,341 @@
package tools.mapletools;
import provider.wz.WZFiles;
import tools.Pair;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application gets info from the WZ.XML files regarding cash itemids then searches the drop data on the DB
* after any NX (cash item) drops and reports them.
* <p>
* Estimated parse time: 2 minutes
*/
public class CashDropFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("cash_drop_report.txt");
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static final int INITIAL_STRING_LENGTH = 50;
private static final int ITEM_FILE_NAME_SIZE = 13;
private static final Set<Integer> nxItems = new HashSet<>();
private static final Set<Integer> nxDrops = new HashSet<>();
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int currentItemid = 0;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
if (j < i) {
return "0"; //node value containing 'name' in it's scope, cheap fix since we don't deal with strings anyway
}
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void inspectEquipWzEntry() {
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
translateEquipToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateEquipToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) {
if (!getName(token).equals("info")) {
forwardCursor(status);
}
}
status += 1;
} else {
if (status == 2) {
String d = getName(token);
if (d.equals("cash")) {
if (!getValue(token).equals("0")) {
nxItems.add(currentItemid);
}
forwardCursor(status);
}
}
}
}
private static void inspectItemWzEntry() {
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
translateItemToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateItemToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) {
currentItemid = Integer.parseInt(getName(token));
} else if (status == 2) {
if (!getName(token).equals("info")) {
forwardCursor(status);
}
}
status += 1;
} else {
if (status == 3) {
String d = getName(token);
if (d.equals("cash")) {
if (!getValue(token).equals("0")) {
nxItems.add(currentItemid);
}
forwardCursor(status);
}
}
}
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleCashDropFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the underlying DB and the server-side WZ.xmls.");
printWriter.println();
}
private static void listFiles(String directoryName, ArrayList<File> files) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
files.add(file);
} else if (file.isDirectory()) {
listFiles(file.getAbsolutePath(), files);
}
}
}
private static int getItemIdFromFilename(String name) {
try {
return Integer.parseInt(name.substring(0, name.indexOf('.')));
} catch (Exception e) {
return -1;
}
}
private static String getDropTableName(boolean dropdata) {
return (dropdata ? "drop_data" : "reactordrops");
}
private static String getDropElementName(boolean dropdata) {
return (dropdata ? "dropperid" : "reactorid");
}
private static void filterNxDropsOnDB(boolean dropdata) throws SQLException {
nxDrops.clear();
PreparedStatement ps = con.prepareStatement("SELECT DISTINCT itemid FROM " + getDropTableName(dropdata));
ResultSet rs = ps.executeQuery();
while (rs.next()) {
int itemid = rs.getInt("itemid");
if (nxItems.contains(itemid)) {
nxDrops.add(itemid);
}
}
rs.close();
ps.close();
}
private static List<Pair<Integer, Integer>> getNxDropsEntries(boolean dropdata) throws SQLException {
List<Pair<Integer, Integer>> entries = new ArrayList<>();
List<Integer> sortedNxDrops = new ArrayList<>(nxDrops);
Collections.sort(sortedNxDrops);
for (Integer nx : sortedNxDrops) {
PreparedStatement ps = con.prepareStatement("SELECT " + getDropElementName(dropdata) + " FROM " + getDropTableName(dropdata) + " WHERE itemid = ?");
ps.setInt(1, nx);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
entries.add(new Pair<>(nx, rs.getInt(getDropElementName(dropdata))));
}
rs.close();
ps.close();
}
return entries;
}
private static void reportNxDropResults(boolean dropdata) throws SQLException {
filterNxDropsOnDB(dropdata);
if (!nxDrops.isEmpty()) {
List<Pair<Integer, Integer>> nxEntries = getNxDropsEntries(dropdata);
printWriter.println("NX DROPS ON " + getDropTableName(dropdata));
for (Pair<Integer, Integer> nx : nxEntries) {
printWriter.println(nx.left + " : " + nx.right);
}
printWriter.println("\n\n\n");
}
}
private static void reportNxDropData() {
try {
System.out.println("Reading Character.wz ...");
ArrayList<File> files = new ArrayList<>();
listFiles(WZFiles.CHARACTER.getFilePath(), files);
InputStreamReader fileReader = null;
for (File f : files) {
//System.out.println("Parsing " + f.getAbsolutePath());
int itemid = getItemIdFromFilename(f.getName());
if (itemid < 0) {
continue;
}
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
currentItemid = itemid;
inspectEquipWzEntry();
bufferedReader.close();
fileReader.close();
}
System.out.println("Reading Item.wz ...");
files = new ArrayList<>();
listFiles(WZFiles.ITEM.getFilePath(), files);
for (File f : files) {
//System.out.println("Parsing " + f.getAbsolutePath());
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
if (f.getName().length() <= ITEM_FILE_NAME_SIZE) {
inspectItemWzEntry();
} else { // pet file structure is similar to equips, maybe there are other item-types following this behaviour?
int itemid = getItemIdFromFilename(f.getName());
if (itemid < 0) {
continue;
}
currentItemid = itemid;
inspectEquipWzEntry();
}
bufferedReader.close();
fileReader.close();
}
System.out.println("Reporting results...");
// report suspects of missing quest drop data, as well as those drop data that may have incorrect questids.
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
reportNxDropResults(true);
reportNxDropResults(false);
/*
printWriter.println("NX LIST"); // list of all cash items found
for(Integer nx : nxItems) {
printWriter.println(nx);
}
*/
con.close();
printWriter.close();
System.out.println("Done!");
} catch (SQLException e) {
System.out.println("Warning: Could not establish connection to database to report quest data.");
System.out.println(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
reportNxDropData();
}
}

View File

@@ -0,0 +1,188 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
/**
*
* @author RonanLana
*
This application main objective is to read Vega-related information from
the item's description report back missing nodes for these items.
Estimated parse time: 10 seconds
*/
public class CashVegaChecker {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("vega_checker_report.txt");
private static final int INITIAL_STRING_LENGTH = 1000;
private static final Set<Integer> vegaItems = new HashSet<>();
private static PrintWriter printWriter = null;
private static InputStreamReader fileReader = null;
private static BufferedReader bufferedReader = null;
private static int currentItem;
private static byte status = 0;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return(d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value=");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return(d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while(status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
}
catch(Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if(token.contains("/imgdir")) {
status -= 1;
}
else if(token.contains("imgdir")) {
status += 1;
}
}
private static void translateItemToken(String token) {
if(token.contains("/imgdir")) {
status -= 1;
}
else if(token.contains("imgdir")) {
status += 1;
if (status == 2) {
currentItem = Integer.parseInt(getName(token));
}
} else {
if (status == 2) {
if (getValue(token).endsWith("Vega&apos;s Spell.")) {
vegaItems.add(currentItem);
}
}
}
}
private static void translateVegaToken(String token) {
if(token.contains("/imgdir")) {
status -= 1;
}
else if(token.contains("imgdir")) {
status += 1;
} else {
if (status == 2) {
if (getName(token).contentEquals("item")) {
vegaItems.remove(Integer.valueOf(getValue(token)));
}
}
}
}
private static void readItemDescriptionFile(File f) {
System.out.print("Reading String.wz... ");
try {
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line;
while((line = bufferedReader.readLine())!=null){
translateItemToken(line);
}
bufferedReader.close();
fileReader.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
System.out.println(vegaItems.size() + " Vega Scroll items found");
}
private static void readVegaDescriptionFile(File f) {
System.out.println("Reading Etc.wz...");
try {
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line;
while((line = bufferedReader.readLine())!=null){
translateVegaToken(line);
}
bufferedReader.close();
fileReader.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleCashVegaChecker feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static void reportMissingVegaItems() {
System.out.println("Reporting results ...");
try {
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
for (Integer itemid : vegaItems) {
printWriter.println(" " + itemid);
}
printWriter.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
public static void main(String[] args) {
readItemDescriptionFile(new File(WZFiles.STRING.getFilePath() + "/Consume.img.xml"));
readVegaDescriptionFile(new File(WZFiles.ETC.getFilePath() + "/VegaSpell.img.xml"));
reportMissingVegaItems();
}
}

View File

@@ -0,0 +1,356 @@
package tools.mapletools;
import tools.Pair;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author RonanLana
* <p>
* This application parses the coupon descriptor XML file and automatically generates
* code entries on the DB reflecting the descriptions found. Parse time relies on the
* sum of coupon codes created and amount of current codes on DB.
* <p>
* Estimated parse time: 2 minutes (for 100 code entries)
*/
public class CodeCouponGenerator {
private static final File INPUT_FILE = ToolConstants.getInputFile("CouponCodes.img.xml");
private static final int INITIAL_STRING_LENGTH = 250;
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static final List<CodeCouponDescriptor> activeCoupons = new ArrayList<>();
private static final Set<String> usedCodes = new HashSet<>();
private static final List<Pair<Integer, Integer>> itemList = new ArrayList<>();
private static BufferedReader bufferedReader = null;
private static long currentTime;
private static String name;
private static boolean active;
private static int quantity;
private static int duration;
private static int maplePoint;
private static int nxCredit;
private static int nxPrepaid;
private static Pair<Integer, Integer> item;
private static List<Integer> generatedKeys;
private static byte status;
private static void resetCouponPackage() {
name = null;
active = false;
quantity = 1;
duration = 7;
maplePoint = 0;
nxCredit = 0;
nxPrepaid = 0;
itemList.clear();
}
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
try {
token.getChars(i, j, dest, 0);
} catch (StringIndexOutOfBoundsException e) {
// do nothing
return "";
} catch (Exception e) {
System.out.println("error in: " + token + "");
e.printStackTrace();
try {
Thread.sleep(100000000);
} catch (Exception ex) {
}
}
return new String(dest).trim();
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void translateToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
if (status == 1) {
if (active) {
activeCoupons.add(new CodeCouponDescriptor(name, quantity, duration, maplePoint, nxCredit, nxPrepaid, itemList));
}
resetCouponPackage();
} else if (status == 3) {
itemList.add(item);
}
} else if (token.contains("imgdir")) {
status += 1;
if (status == 4) {
item = new Pair<>(-1, -1);
} else if (status == 2) {
String d = getName(token);
System.out.println(" Reading coupon '" + d + "'");
name = d;
}
} else {
String d = getName(token);
if (status == 2) {
switch (d) {
case "active":
if (Integer.parseInt(getValue(token)) == 0) {
forwardCursor(status);
resetCouponPackage();
} else {
active = true;
}
break;
case "quantity":
quantity = Integer.parseInt(getValue(token));
break;
case "duration":
duration = Integer.parseInt(getValue(token));
break;
case "maplePoint":
maplePoint = Integer.parseInt(getValue(token));
break;
case "nxCredit":
nxCredit = Integer.parseInt(getValue(token));
break;
case "nxPrepaid":
nxPrepaid = Integer.parseInt(getValue(token));
break;
}
} else if (status == 4) {
switch (d) {
case "count":
item.right = Integer.valueOf(getValue(token));
break;
case "id":
item.left = Integer.valueOf(getValue(token));
break;
}
}
}
}
private static class CodeCouponDescriptor {
protected String name;
protected int quantity, duration;
protected int nxCredit, maplePoint, nxPrepaid;
protected List<Pair<Integer, Integer>> itemList;
protected CodeCouponDescriptor(String name, int quantity, int duration, int maplePoint, int nxCredit, int nxPrepaid, List<Pair<Integer, Integer>> itemList) {
this.name = name;
this.quantity = quantity;
this.duration = duration;
this.maplePoint = maplePoint;
this.nxCredit = nxCredit;
this.nxPrepaid = nxPrepaid;
this.itemList = new ArrayList<>(itemList);
}
}
private static String randomizeCouponCode() {
return Long.toHexString(Double.doubleToLongBits(Math.random())).substring(0, 15);
}
private static String generateCouponCode() {
String newCode;
do {
newCode = randomizeCouponCode();
} while (usedCodes.contains(newCode));
usedCodes.add(newCode);
return newCode;
}
private static List<Integer> getGeneratedKeys(PreparedStatement ps) throws SQLException {
if (generatedKeys == null) {
generatedKeys = new ArrayList<>();
ResultSet rs = ps.getGeneratedKeys();
while (rs.next()) {
generatedKeys.add(rs.getInt(1));
}
rs.close();
}
return generatedKeys;
}
private static void commitCodeCouponDescription(CodeCouponDescriptor recipe) throws SQLException {
if (recipe.quantity < 1) {
return;
}
System.out.println(" Generating coupon '" + recipe.name + "'");
generatedKeys = null;
PreparedStatement ps = con.prepareStatement("INSERT IGNORE INTO `nxcode` (`code`, `expiration`) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS);
ps.setLong(2, currentTime + ((long) recipe.duration * 60 * 60 * 1000));
for (int i = 0; i < recipe.quantity; i++) {
ps.setString(1, generateCouponCode());
ps.addBatch();
}
ps.executeBatch();
PreparedStatement ps2 = con.prepareStatement("INSERT IGNORE INTO `nxcode_items` (`codeid`, `type`, `item`, `quantity`) VALUES (?, ?, ?, ?)");
if (!recipe.itemList.isEmpty()) {
ps2.setInt(2, 5);
List<Integer> keys = getGeneratedKeys(ps);
for (Pair<Integer, Integer> p : recipe.itemList) {
ps2.setInt(3, p.getLeft());
ps2.setInt(4, p.getRight());
for (Integer codeid : keys) {
ps2.setInt(1, codeid);
ps2.addBatch();
}
}
}
ps2.setInt(3, 0);
if (recipe.nxCredit > 0) {
ps2.setInt(2, 0);
ps2.setInt(4, recipe.nxCredit);
List<Integer> keys = getGeneratedKeys(ps);
for (Integer codeid : keys) {
ps2.setInt(1, codeid);
ps2.addBatch();
}
}
if (recipe.maplePoint > 0) {
ps2.setInt(2, 1);
ps2.setInt(4, recipe.maplePoint);
List<Integer> keys = getGeneratedKeys(ps);
for (Integer codeid : keys) {
ps2.setInt(1, codeid);
ps2.addBatch();
}
}
if (recipe.nxPrepaid > 0) {
ps2.setInt(2, 2);
ps2.setInt(4, recipe.nxPrepaid);
List<Integer> keys = getGeneratedKeys(ps);
for (Integer codeid : keys) {
ps2.setInt(1, codeid);
ps2.addBatch();
}
}
ps2.executeBatch();
ps2.close();
ps.close();
}
private static void loadUsedCouponCodes() throws SQLException {
PreparedStatement ps = con.prepareStatement("SELECT code FROM nxcode", Statement.RETURN_GENERATED_KEYS);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
usedCodes.add(rs.getString("code"));
}
rs.close();
ps.close();
}
private static void generateCodeCoupons(File file) throws IOException {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
resetCouponPackage();
status = 0;
System.out.println("Reading XML coupon information...");
String line;
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
bufferedReader.close();
fileReader.close();
System.out.println();
try {
System.out.println("Loading DB coupon codes...");
loadUsedCouponCodes();
System.out.println();
System.out.println("Saving generated coupons...");
currentTime = System.currentTimeMillis();
for (CodeCouponDescriptor ccd : activeCoupons) {
commitCodeCouponDescription(ccd);
}
System.out.println();
con.close();
System.out.println("Done.");
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
generateCodeCoupons(INPUT_FILE);
} catch (IOException ex) {
System.out.println("Error reading file '" + INPUT_FILE.getAbsolutePath() + "'");
}
}
}

View File

@@ -0,0 +1,255 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* @author RonanLana
* <p>
* This application gathers information about the Cash Shop's EXP & DROP coupons,
* such as applied rates, active times of day and days of week and dumps them in
* a SQL table that the server will make use.
*/
public class CouponInstaller {
private static final File COUPON_INPUT_FILE_1 = new File(WZFiles.ITEM.getFilePath(), "/Cash/0521.img.xml");
private static final File COUPON_INPUT_FILE_2 = new File(WZFiles.ITEM.getFilePath(), "/Cash/0536.img.xml");
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int itemId = -1;
private static int itemMultiplier = 1;
private static int startHour = -1;
private static int endHour = -1;
private static int activeDay = 0;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
if (i < 0) {
return "";
}
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[8];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d);
}
private static String getNodeValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value=");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
if (j - i < 1) {
return "";
}
dest = new char[j - i];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d);
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static int getDayOfWeek(String day) {
return switch (day) {
case "SUN" -> 1;
case "MON" -> 2;
case "TUE" -> 3;
case "WED" -> 4;
case "THU" -> 5;
case "FRI" -> 6;
case "SAT" -> 7;
default -> 0;
};
}
private static void processHourTimeString(String time) {
startHour = Integer.parseInt(time.substring(4, 6));
endHour = Integer.parseInt(time.substring(7, 9));
}
private static void processDayTimeString(String time) {
String day = time.substring(0, 3);
int d = getDayOfWeek(day);
activeDay |= (1 << d);
}
private static void loadTimeFromCoupon(int st) {
System.out.println("Loading coupon id " + itemId + ". Rate: " + itemMultiplier + "x.");
String line = null;
try {
startHour = -1;
endHour = -1;
activeDay = 0;
String time = null;
while ((line = bufferedReader.readLine()) != null) {
simpleToken(line);
if (status < st) {
break;
}
time = getNodeValue(line);
processDayTimeString(time);
simpleToken(line);
}
if (time != null) {
processHourTimeString(time);
PreparedStatement ps = con.prepareStatement("INSERT INTO nxcoupons (couponid, rate, activeday, starthour, endhour) VALUES (?, ?, ?, ?, ?)");
ps.setInt(1, itemId);
ps.setInt(2, itemMultiplier);
ps.setInt(3, activeDay);
ps.setInt(4, startHour);
ps.setInt(5, endHour);
ps.execute();
ps.close();
}
} catch (SQLException | IOException e) {
e.printStackTrace();
}
}
private static void translateToken(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting ItemId
d = getName(token);
itemId = Integer.parseInt(d);
} else if (status == 2) {
d = getName(token);
if (!d.contains("info")) {
forwardCursor(status);
}
} else if (status == 3) {
d = getName(token);
if (!d.contains("time")) {
forwardCursor(status);
} else {
loadTimeFromCoupon(status);
}
}
status += 1;
} else {
if (status == 3) {
d = getName(token);
if (d.contains("rate")) {
String r = getNodeValue(token);
double db = Double.parseDouble(r);
itemMultiplier = (int) db;
}
}
}
}
private static void installRateCoupons(File file) {
// This will reference one line at a time
String line = null;
try {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
bufferedReader.close();
fileReader.close();
} catch (FileNotFoundException ex) {
System.out.println("Unable to open file '" + file + "'");
} catch (IOException ex) {
System.out.println("Error reading file '" + file + "'");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void installCouponsTable() {
try {
PreparedStatement ps = con.prepareStatement("DROP TABLE IF EXISTS `nxcoupons`;");
ps.execute();
ps.close();
ps = con.prepareStatement(
"""
CREATE TABLE IF NOT EXISTS `nxcoupons` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`couponid` int(11) NOT NULL DEFAULT '0',
`rate` int(11) NOT NULL DEFAULT '0',
`activeday` int(11) NOT NULL DEFAULT '0',
`starthour` int(11) NOT NULL DEFAULT '0',
`endhour` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;"""
);
ps.execute();
ps.close();
installRateCoupons(COUPON_INPUT_FILE_1);
installRateCoupons(COUPON_INPUT_FILE_2);
con.close();
} catch (SQLException e) {
System.out.println("Warning: Could not establish connection to database to change card chance rate.");
System.out.println(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
installCouponsTable();
}
}

View File

@@ -0,0 +1,182 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @author RonanLana
* <p>
* This application parses the Character.wz folder inputted and adds/updates the "info/level"
* node on every known equipment id. This addition enables client-side view of the equipment
* level attribute on every equipment in the game, given proper item visibility, be it from
* own equipments or from other players.
* <p>
* Estimated parse time: 10 seconds
*/
public class DojoUpdate {
private static final File INPUT_DIRECTORY = new File(WZFiles.MAP.getFile(), "/Map/Map9");
private static final File OUTPUT_DIRECTORY = ToolConstants.getOutputFile("dojo-maps");
private static final int DOJO_MIN_MAP_ID = 925_020_100;
private static final int DOJO_MAX_MAP_ID = 925_033_804;
private static final int INITIAL_STRING_LENGTH = 250;
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static boolean isDojoMapid;
private static byte status;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
try {
token.getChars(i, j, dest, 0);
} catch (StringIndexOutOfBoundsException e) {
// do nothing
return "";
} catch (Exception e) {
System.out.println("error in: " + token + "");
e.printStackTrace();
try {
Thread.sleep(100000000);
} catch (Exception ex) {
}
}
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
printWriter.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void translateToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
printWriter.println(token);
} else if (token.contains("imgdir")) {
printWriter.println(token);
status += 1;
if (status == 2) {
String d = getName(token);
if (!d.contentEquals("info")) {
forwardCursor(status);
}
} else if (status > 2) {
forwardCursor(status);
}
} else {
if (status == 2 && isDojoMapid) {
String item = getName(token);
if (item.contentEquals("onFirstUserEnter")) {
printWriter.println(" <string name=\"onFirstUserEnter\" value=\"dojang_1st\"/>");
} else if (item.contentEquals("onUserEnter")) {
printWriter.println(" <string name=\"onUserEnter\" value=\"dojang_Eff\"/>");
} else {
printWriter.println(token);
}
} else {
printWriter.println(token);
}
}
}
private static int getMapId(String fileName) {
return Integer.parseInt(fileName.substring(0, 9));
}
private static void parseDojoData(File file, String curPath) throws IOException {
int mapId = getMapId(file.getName());
isDojoMapid = isDojoMapId(mapId);
if (!isDojoMapid) {
return;
}
printWriter = new PrintWriter(OUTPUT_DIRECTORY.getPath() + "/" + curPath + file.getName(), StandardCharsets.UTF_8);
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
status = 0;
String line;
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
bufferedReader.close();
fileReader.close();
printFileFooter();
printWriter.close();
}
private static boolean isDojoMapId(int mapId) {
return mapId >= DOJO_MIN_MAP_ID && mapId <= DOJO_MAX_MAP_ID;
}
private static void printFileFooter() {
printWriter.println("<!--");
printWriter.println(" # WZ XML File updated by the MapleDojoUpdate feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account info from the server-side WZ.xmls.");
printWriter.println("-->");
}
private static void parseDirectoryDojoData(String curPath) {
File folder = new File(OUTPUT_DIRECTORY, curPath);
if (!folder.exists()) {
folder.mkdir();
}
System.out.println("Parsing directory '" + curPath + "'");
folder = new File(INPUT_DIRECTORY, curPath);
for (File file : folder.listFiles()) {
if (file.isFile()) {
try {
parseDojoData(file, curPath);
} catch (FileNotFoundException ex) {
System.out.println("Unable to open dojo file " + file.getAbsolutePath() + ".");
} catch (IOException ex) {
System.out.println("Error reading dojo file " + file.getAbsolutePath() + ".");
} catch (Exception e) {
e.printStackTrace();
}
} else {
parseDirectoryDojoData(curPath + file.getName() + "/");
}
}
}
public static void main(String[] args) {
parseDirectoryDojoData("");
}
}

View File

@@ -0,0 +1,437 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application has two objectives: it reports in a detailed file all itemids which is
* currently missing either a name entry in the String.wz or an item entry in the Item.wz;
* And it removes from the String.wz XMLs all entries which misses properties on Item.wz.
*/
public class EmptyItemWzChecker {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("empty_item_wz_report.txt");
private static final String OUTPUT_PATH = ToolConstants.OUTPUT_DIRECTORY.getPath();
private static final int INITIAL_STRING_LENGTH = 50;
private static final int ITEM_FILE_NAME_SIZE = 13;
private static final Stack<String> currentPath = new Stack<>();
private static final Map<Integer, String> stringWzItems = new HashMap<>();
private static final Map<Integer, String> contentWzItems = new HashMap<>();
private static final Set<Integer> handbookItems = new HashSet<>();
private static PrintWriter printWriter = null;
private static InputStreamReader fileReader = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int currentItemid = 0;
private static int currentDepth = 0;
private static String currentFile;
private static Set<Integer> nonPropItems;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void listFiles(String directoryName, ArrayList<File> files) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
files.add(file);
} else if (file.isDirectory()) {
listFiles(file.getAbsolutePath(), files);
}
}
}
private static int getItemIdFromFilename(String name) {
try {
return Integer.parseInt(name.substring(0, name.indexOf('.')));
} catch (Exception e) {
return -1;
}
}
private static void inspectItemWzEntry() {
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
translateItemToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static String currentItemPath() {
String s = currentFile + " -> ";
for (String p : currentPath) {
s += (p + "\\");
}
return s;
}
private static void translateItemToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
currentPath.pop();
} else if (token.contains("imgdir")) {
status += 1;
String d = getName(token);
if (status == 2) {
currentItemid = Integer.parseInt(d);
contentWzItems.put(currentItemid, currentItemPath());
forwardCursor(status);
} else {
currentPath.push(d);
}
}
}
private static void inspectStringWzEntry() {
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
translateStringToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateStringToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
currentPath.pop();
} else if (token.contains("imgdir")) {
status += 1;
String d = getName(token);
if (status == currentDepth) {
currentItemid = Integer.parseInt(d);
stringWzItems.put(currentItemid, currentItemPath());
//if (currentItemid >= 4000000) System.out.println(" " + currentItemid);
forwardCursor(status);
} else {
currentPath.push(d);
}
}
}
private static void loadStringWzFile(String filePath, int depth) throws IOException {
fileReader = new InputStreamReader(new FileInputStream(filePath), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
currentFile = filePath;
currentDepth = 2 + depth;
//System.out.println(filePath + " depth " + depth);
inspectStringWzEntry();
bufferedReader.close();
fileReader.close();
}
private static void loadStringWz() throws IOException {
System.out.println("Reading String.wz ...");
String[][] stringWzFiles = {{"Cash", "Consume", "Ins", "Pet"}, {"Etc"}, {"Eqp"}};
for (int i = 0; i < stringWzFiles.length; i++) {
for (String dirFile : stringWzFiles[i]) {
final String fileName = "/" + dirFile + ".img.xml";
loadStringWzFile(WZFiles.STRING.getFilePath() + fileName, i);
}
}
}
private static void loadItemWz() throws IOException {
System.out.println("Reading Item.wz ...");
ArrayList<File> files = new ArrayList<>();
listFiles(WZFiles.ITEM.getFilePath(), files);
for (File f : files) {
if (f.getParentFile().getName().contentEquals("Special")) {
continue;
}
//System.out.println("Parsing " + f.getAbsolutePath());
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
currentFile = f.getCanonicalPath();
if (f.getName().length() <= ITEM_FILE_NAME_SIZE) {
inspectItemWzEntry();
} else { // pet file structure is similar to equips, maybe there are other item-types following this behaviour?
int itemid = getItemIdFromFilename(f.getName());
if (itemid < 0) {
continue;
}
currentItemid = itemid;
contentWzItems.put(currentItemid, currentItemPath());
}
bufferedReader.close();
fileReader.close();
}
}
private static void loadCharacterWz() throws IOException {
System.out.println("Reading Character.wz ...");
ArrayList<File> files = new ArrayList<>();
listFiles(WZFiles.CHARACTER.getFilePath(), files);
for (File f : files) {
if (f.getParentFile().getName().contentEquals("Character.wz")) {
continue;
}
int itemid = getItemIdFromFilename(f.getName());
if (itemid < 0) {
continue;
}
currentFile = f.getCanonicalPath();
currentItemid = itemid;
contentWzItems.put(currentItemid, currentItemPath());
}
}
private static void calculateItemNameDiff(Set<Integer> emptyItemNames, Set<Integer> emptyNameItems) {
for (Integer i : contentWzItems.keySet()) {
if (!stringWzItems.containsKey(i)) {
emptyNameItems.add(i);
}
}
for (Integer i : stringWzItems.keySet()) {
if (!contentWzItems.containsKey(i)) {
emptyItemNames.add(i);
}
}
}
private static void readHandbookItems() throws IOException {
System.out.println("Reading handbook ...");
String[] handbookPaths = {"Equip", "Cash.txt", "Etc.txt", "Pet.txt", "Setup.txt", "Use.txt"};
for (String path : handbookPaths) {
readHandbookPath(ToolConstants.HANDBOOK_PATH + "/" + path);
}
}
private static void readHandbookPath(String filePath) throws IOException {
ArrayList<File> files = new ArrayList<>();
File testFile = new File(filePath);
if (testFile.isDirectory()) {
listFiles(filePath, files);
} else {
files.add(testFile);
}
for (File f : files) {
fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
String[] tokens = line.split(" - ");
if (tokens[0].length() > 0) {
int itemid = Integer.parseInt(tokens[0]);
handbookItems.add(itemid);
}
}
} catch (Exception e) {
e.printStackTrace();
}
bufferedReader.close();
fileReader.close();
}
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleEmptyItemWzChecker feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static List<Integer> getSortedItems(Set<Integer> items) {
List<Integer> sortedItems = new ArrayList<>(items);
Collections.sort(sortedItems);
return sortedItems;
}
private static void printReportFileResults(Set<Integer> emptyItemNames, Set<Integer> emptyNameItems) {
if (!emptyItemNames.isEmpty()) {
printWriter.println("String.wz NAMES with no Item.wz node, " + emptyItemNames.size() + " entries:");
for (Integer itemid : getSortedItems(emptyItemNames)) {
printWriter.println(" " + itemid + " " + stringWzItems.get(itemid) + (handbookItems.contains(itemid) ? "" : " NOT FOUND"));
}
printWriter.println();
}
if (!emptyNameItems.isEmpty()) {
printWriter.println("Item.wz ITEMS with no String.wz node, " + emptyNameItems.size() + " entries:");
for (Integer itemid : getSortedItems(emptyNameItems)) {
printWriter.println(" " + itemid + " " + contentWzItems.get(itemid) + (handbookItems.contains(itemid) ? "" : " NOT FOUND"));
}
printWriter.println();
}
}
private static void reportItemNameDiff(Set<Integer> emptyItemNames, Set<Integer> emptyNameItems) throws IOException {
System.out.println("Reporting results...");
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
printReportFileResults(emptyItemNames, emptyNameItems);
printWriter.close();
}
private static void locateItemStringWzDiff() throws IOException {
Set<Integer> emptyItemNames = new HashSet<>(), emptyNameItems = new HashSet<>();
calculateItemNameDiff(emptyItemNames, emptyNameItems);
reportItemNameDiff(emptyItemNames, emptyNameItems);
nonPropItems = emptyItemNames;
}
private static void runEmptyItemWzChecker() throws IOException {
readHandbookItems();
loadCharacterWz();
loadItemWz();
loadStringWz();
locateItemStringWzDiff();
}
private static void generateStringWzEntry() {
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
updateStringToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void updateStringToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
if (status == currentDepth && nonPropItems.contains(Integer.valueOf(getName(token)))) {
forwardCursor(status);
return;
}
}
printWriter.println(token);
}
private static void generateStringWzFile(String filePath, int depth) throws IOException {
fileReader = new InputStreamReader(new FileInputStream(WZFiles.DIRECTORY + filePath), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
printWriter = new PrintWriter(OUTPUT_PATH + filePath, StandardCharsets.UTF_8);
currentDepth = 2 + depth;
//System.out.println(filePath + " depth " + depth);
generateStringWzEntry();
printWriter.close();
bufferedReader.close();
fileReader.close();
}
private static void generateStringWz() throws IOException {
System.out.println("Generating clean String.wz ...");
String[][] stringWzFiles = {{"Cash", "Consume", "Ins", "Pet"}, {"Etc"}, {"Eqp"}};
String stringWzPath = "/String.wz/";
File folder = new File(OUTPUT_PATH + "/String.wz/");
if (!folder.exists()) {
folder.mkdirs();
}
for (int i = 0; i < stringWzFiles.length; i++) {
for (String dirFile : stringWzFiles[i]) {
generateStringWzFile(stringWzPath + dirFile + ".img.xml", i);
}
}
}
public static void main(String[] args) {
try {
runEmptyItemWzChecker();
generateStringWz();
System.out.println("Done!");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

View File

@@ -0,0 +1,443 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @author RonanLana
* <p>
* This application parses the Character.wz folder inputted and adds/updates the "info/level"
* node on every known equipment id. This addition enables client-side view of the equipment
* level attribute on every equipment in the game, given proper item visibility, be it from
* own equipments or from other players.
* <p>
* Estimated parse time: 7 minutes
*/
public class EquipmentOmniLeveller {
private static final File INPUT_DIRECTORY = WZFiles.CHARACTER.getFile();
private static final File OUTPUT_DIRECTORY = ToolConstants.getOutputFile("equips-with-levels");
private static final int INITIAL_STRING_LENGTH = 250;
private static final int FIXED_EXP = 10000;
private static final int MAX_EQP_LEVEL = 30;
private static PrintWriter printWriter = null;
private static InputStreamReader fileReader = null;
private static BufferedReader bufferedReader = null;
private static int infoTagState = -1;
private static int infoTagExpState = -1;
private static boolean infoTagLevel;
private static boolean infoTagLevelExp;
private static boolean infoTagLevelInfo;
private static int parsedLevels = 0;
private static byte status;
private static boolean upgradeable;
private static boolean cash;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
try {
token.getChars(i, j, dest, 0);
} catch (StringIndexOutOfBoundsException e) {
// do nothing
return "";
} catch (Exception e) {
System.out.println("error in: " + token + "");
e.printStackTrace();
try {
Thread.sleep(100000000);
} catch (Exception ex) {
}
}
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
printWriter.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateLevelCursor(int st) {
String line = null;
try {
infoTagLevelInfo = false;
while (status >= st && (line = bufferedReader.readLine()) != null) {
translateLevelToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateInfoTag(int st) {
infoTagLevel = false;
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) { // skipping directory & canvas definition
translateInfoToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
if (!upgradeable || cash) {
throw new RuntimeException();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void printUpdatedLevelExp() {
printWriter.println(" <int name=\"exp\" value=\"" + FIXED_EXP + "\"/>");
}
private static void printDefaultLevel(int level) {
printWriter.println(" <imgdir name=\"" + level + "\">");
printUpdatedLevelExp();
printWriter.println(" </imgdir>");
}
private static void printDefaultLevelInfoTag() {
printWriter.println(" <imgdir name=\"info\">");
for (int i = 1; i <= MAX_EQP_LEVEL; i++) {
printDefaultLevel(i);
}
printWriter.println(" </imgdir>");
}
private static void printDefaultLevelTag() {
printWriter.println(" <imgdir name=\"level\">");
printDefaultLevelInfoTag();
printWriter.println(" </imgdir>");
}
private static void processLevelInfoTag(int st) {
String line;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
translateLevelExpToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void processLevelInfoSet(int st) {
parsedLevels = (1 << MAX_EQP_LEVEL) - 1;
String line;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
translateLevelInfoSetToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateLevelToken(String token) {
if (token.contains("/imgdir")) {
if (status == 3) {
if (!infoTagLevelInfo) {
printDefaultLevelInfoTag();
}
}
printWriter.println(token);
status -= 1;
} else if (token.contains("imgdir")) {
printWriter.println(token);
status += 1;
if (status == 4) {
String d = getName(token);
if (d.contentEquals("info")) {
infoTagLevelInfo = true;
processLevelInfoSet(status);
} else {
forwardCursor(status);
}
}
} else {
printWriter.println(token);
}
}
private static void translateLevelInfoSetToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
if (status == 3) {
if (parsedLevels != 0) {
for (int i = 0; i < MAX_EQP_LEVEL; i++) {
if ((parsedLevels >> i) % 2 != 0) {
int level = i + 1;
printDefaultLevel(level);
}
}
}
}
printWriter.println(token);
} else if (token.contains("imgdir")) {
printWriter.println(token);
status += 1;
if (status == 5) {
int level = Integer.parseInt(getName(token)) - 1;
parsedLevels ^= (1 << level);
infoTagLevelExp = false;
infoTagExpState = status; // status: 5
processLevelInfoTag(status);
infoTagExpState = -1;
}
} else {
printWriter.println(token);
}
}
private static void translateLevelExpToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
if (status < infoTagExpState) {
if (!infoTagLevelExp) {
printUpdatedLevelExp();
}
}
printWriter.println(token);
} else if (token.contains("imgdir")) {
printWriter.println(token);
status += 1;
forwardCursor(status);
} else {
String name = getName(token);
if (name.contentEquals("exp")) {
infoTagLevelExp = true;
printUpdatedLevelExp();
} else {
printWriter.println(token);
}
}
}
private static void translateInfoToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
if (status < infoTagState) {
if (!infoTagLevel) {
printDefaultLevelTag();
}
}
printWriter.println(token);
} else if (token.contains("imgdir")) {
status += 1;
printWriter.println(token);
String d = getName(token);
if (d.contentEquals("level")) {
infoTagLevel = true;
translateLevelCursor(status);
} else {
forwardCursor(status);
}
} else {
String name = getName(token);
switch (name) {
case "cash":
if (!getValue(token).contentEquals("0")) {
cash = true;
}
break;
case "tuc":
case "incPAD":
case "incMAD":
case "incPDD":
case "incMDD":
case "incACC":
case "incEVA":
case "incSpeed":
case "incJump":
case "incMHP":
case "incMMP":
case "incSTR":
case "incDEX":
case "incINT":
case "incLUK":
if (!getValue(token).contentEquals("0")) {
upgradeable = true;
}
break;
}
printWriter.println(token);
}
}
private static boolean translateToken(String token) {
boolean accessInfoTag = false;
if (token.contains("/imgdir")) {
status -= 1;
printWriter.println(token);
} else if (token.contains("imgdir")) {
printWriter.println(token);
status += 1;
if (status == 2) {
String d = getName(token);
if (!d.contentEquals("info")) {
forwardCursor(status);
} else {
accessInfoTag = true;
}
} else if (status > 2) {
forwardCursor(status);
}
} else {
printWriter.println(token);
}
return accessInfoTag;
}
private static void copyCashItemData(File file, String curPath) throws IOException {
printWriter = new PrintWriter(new File(OUTPUT_DIRECTORY, curPath + file.getName()), StandardCharsets.UTF_8);
fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
printWriter.println(line);
}
bufferedReader.close();
fileReader.close();
printWriter.close();
}
private static void parseEquipData(File file, String curPath) throws IOException {
printWriter = new PrintWriter(new File(OUTPUT_DIRECTORY, curPath + file.getName()), StandardCharsets.UTF_8);
fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
try {
status = 0;
upgradeable = false;
cash = false;
String line;
while ((line = bufferedReader.readLine()) != null) {
if (translateToken(line)) {
infoTagState = status; // status: 2
translateInfoTag(status);
infoTagState = -1;
}
}
bufferedReader.close();
fileReader.close();
printFileFooter();
printWriter.close();
} catch (RuntimeException e) {
bufferedReader.close();
fileReader.close();
printWriter.close();
copyCashItemData(file, curPath);
}
}
private static void printFileFooter() {
printWriter.println("<!--");
printWriter.println(" # WZ XML File parsed by the MapleEquipmentOmnilever feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account info from the server-side WZ.xmls.");
printWriter.println("-->");
}
private static void parseDirectoryEquipData(String curPath) {
File folder = new File(OUTPUT_DIRECTORY, curPath);
if (!folder.exists()) {
folder.mkdir();
}
System.out.println("Parsing directory '" + curPath + "'");
folder = new File(INPUT_DIRECTORY, curPath);
for (File file : folder.listFiles()) {
if (file.isFile()) {
try {
parseEquipData(file, curPath);
} catch (FileNotFoundException ex) {
System.out.println("Unable to open equip file " + file.getAbsolutePath() + ".");
} catch (IOException ex) {
System.out.println("Error reading equip file " + file.getAbsolutePath() + ".");
} catch (Exception e) {
e.printStackTrace();
}
} else {
parseDirectoryEquipData(curPath + file.getName() + "/");
}
}
}
public static void main(String[] args) {
parseDirectoryEquipData("");
}
}

View File

@@ -0,0 +1,102 @@
package tools.mapletools;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author RonanLana
* <p>
* This application objective is to read all scripts from the event folder
* and fill empty functions for every function name not yet present in the
* script.
* <p>
* Estimated parse time: 10 seconds
*/
public class EventMethodFiller {
private static boolean foundMatchingDataOnFile(String fileContent, Pattern pattern) {
Matcher matcher = pattern.matcher(fileContent);
return matcher.find();
}
private static void fileSearchMatchingData(File file, Map<Pattern, String> functions) {
try {
String fileContent = FileUtils.readFileToString(file, "UTF-8");
List<String> fillFunctions = new LinkedList<>();
for (Map.Entry<Pattern, String> f : functions.entrySet()) {
if (!foundMatchingDataOnFile(fileContent, f.getKey())) {
fillFunctions.add(f.getValue());
}
}
if (!fillFunctions.isEmpty()) {
System.out.println("Filling out " + file.getName() + "...");
FileWriter fileWriter = new FileWriter(file, true);
PrintWriter printWriter = new PrintWriter(fileWriter);
printWriter.println();
printWriter.println();
printWriter.println("// ---------- FILLER FUNCTIONS ----------");
printWriter.println();
for (String s : fillFunctions) {
printWriter.println(s);
printWriter.println();
}
printWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void filterDirectorySearchMatchingData(String directoryPath, Map<Pattern, String> functions) {
Iterator iter = FileUtils.iterateFiles(new File(directoryPath), new String[]{"sql", "js", "txt", "java"}, true);
while (iter.hasNext()) {
File file = (File) iter.next();
fileSearchMatchingData(file, functions);
}
}
private static Pattern compileJsFunctionPattern(String function) {
String jsFunction = "function(\\s)+";
return Pattern.compile(jsFunction + function);
}
private static Map<Pattern, String> getFunctions() {
Map<Pattern, String> functions = new HashMap<>();
functions.put(compileJsFunctionPattern("playerEntry"), "function playerEntry(eim, player) {}");
functions.put(compileJsFunctionPattern("playerExit"), "function playerExit(eim, player) {}");
functions.put(compileJsFunctionPattern("scheduledTimeout"), "function scheduledTimeout(eim) {}");
functions.put(compileJsFunctionPattern("playerUnregistered"), "function playerUnregistered(eim, player) {}");
functions.put(compileJsFunctionPattern("changedLeader"), "function changedLeader(eim, leader) {}");
functions.put(compileJsFunctionPattern("monsterKilled"), "function monsterKilled(mob, eim) {}");
functions.put(compileJsFunctionPattern("allMonstersDead"), "function allMonstersDead(eim) {}");
functions.put(compileJsFunctionPattern("playerDisconnected"), "function playerDisconnected(eim, player) {}");
functions.put(compileJsFunctionPattern("monsterValue"), "function monsterValue(eim, mobid) {return 0;}");
functions.put(compileJsFunctionPattern("dispose"), "function dispose() {}");
functions.put(compileJsFunctionPattern("leftParty"), "function leftParty(eim, player) {}");
functions.put(compileJsFunctionPattern("disbandParty"), "function disbandParty(eim, player) {}");
functions.put(compileJsFunctionPattern("clearPQ"), "function clearPQ(eim) {}");
functions.put(compileJsFunctionPattern("afterSetup"), "function afterSetup(eim) {}");
functions.put(compileJsFunctionPattern("cancelSchedule"), "function cancelSchedule() {}");
functions.put(compileJsFunctionPattern("setup"), "function setup(eim, leaderid) {}");
//put(compileJsFunctionPattern("getEligibleParty"), "function getEligibleParty(party) {}"); not really needed
return functions;
}
public static void main(String[] args) {
filterDirectorySearchMatchingData(ToolConstants.SCRIPTS_PATH + "/event", getFunctions());
}
}

View File

@@ -0,0 +1,330 @@
package tools.mapletools;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author RonanLana
* <p>
* This application reads metadata for the gachapons found on the "gachapon_items.txt"
* recipe file, then checks up the Handbook DB (installed through MapleIdRetriever)
* and translates the item names from the recipe file into their respective itemids.
* The translated itemids are then stored in specific gachapon files inside the
* "lib/gachapons" folder.
* <p>
* Estimated parse time: 1 minute
*/
public class GachaponItemIdRetriever {
private static final File INPUT_FILE = ToolConstants.getInputFile("gachapon_items.txt");
private static final File OUTPUT_DIRECTORY = ToolConstants.getOutputFile("gachapons");
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static final Pattern pattern = Pattern.compile("(\\d*)%");
private static final int[] scrollsChances = new int[]{10, 15, 30, 60, 65, 70, 100};
private static final Map<GachaponScroll, List<Integer>> scrollItemids = new HashMap<>();
private static PrintWriter printWriter = null;
private static void insertGachaponScrollItemid(Integer id, String name, String description, boolean both) {
GachaponScroll gachaScroll = getGachaponScroll(name, description, both);
List<Integer> list = scrollItemids.get(gachaScroll);
if (list == null) {
list = new LinkedList<>();
scrollItemids.put(gachaScroll, list);
}
list.add(id);
}
private static void loadHandbookUseNames() throws SQLException {
PreparedStatement ps = con.prepareStatement("SELECT * FROM `handbook` WHERE `id` >= 2040000 AND `id` < 2050000 ORDER BY `id` ASC;");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Integer id = rs.getInt("id");
String name = rs.getString("name");
if (isUpgradeScroll(name)) {
String description = rs.getString("description");
insertGachaponScrollItemid(id, name, description, false);
insertGachaponScrollItemid(id, name, description, true);
}
}
rs.close();
ps.close();
/*
for (Entry<GachaponScroll, List<Integer>> e : scrollItemids.entrySet()) {
System.out.println(e);
}
System.out.println("------------");
*/
}
private static class GachaponScroll {
private String header;
private String target;
private String buff;
private int prop;
private GachaponScroll(GachaponScroll from, int prop) {
this.header = from.header;
this.target = from.target;
this.buff = from.buff;
this.prop = prop;
}
private GachaponScroll(String name, String description, boolean both) {
String[] params = name.split(" for ");
if (params.length < 3) {
return;
}
String header = both ? "scroll" : " " + params[0];
String target = params[1];
int prop = 0;
String buff = params[2];
Matcher m = pattern.matcher(buff);
if (m.find()) {
prop = Integer.parseInt(m.group(1));
buff = buff.substring(0, m.start() - 1).trim();
} else {
m = pattern.matcher(description);
if (m.find()) {
prop = Integer.parseInt(m.group(1));
}
}
int idx = buff.indexOf(" ("); // remove percentage & dots from name checking
if (idx > -1) {
buff = buff.substring(0, idx);
}
buff = buff.replace(".", "");
this.header = header;
this.target = target;
this.buff = buff;
this.prop = prop;
}
@Override
public int hashCode() {
int result = prop ^ (prop >>> 32);
result = 31 * result + (header != null ? header.hashCode() : 0);
result = 31 * result + (target != null ? target.hashCode() : 0);
result = 31 * result + (buff != null ? buff.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GachaponScroll sc = (GachaponScroll) o;
if (header != null ? !header.equals(sc.header) : sc.header != null) {
return false;
}
if (target != null ? !target.equals(sc.target) : sc.target != null) {
return false;
}
if (buff != null ? !buff.equals(sc.buff) : sc.buff != null) {
return false;
}
return prop == sc.prop;
}
@Override
public String toString() {
return header + " for " + target + " for " + buff + " - " + prop + "%";
}
}
private static String getGachaponScrollResults(String line, boolean both) {
String str = "";
List<GachaponScroll> gachaScrollList;
GachaponScroll gachaScroll = getGachaponScroll(line, "", both);
if (gachaScroll.prop != 0) {
gachaScrollList = Collections.singletonList(gachaScroll);
} else {
gachaScrollList = new ArrayList<>(scrollsChances.length);
for (int prop : scrollsChances) {
gachaScrollList.add(new GachaponScroll(gachaScroll, prop));
}
}
for (GachaponScroll gs : gachaScrollList) {
List<Integer> gachaItemids = scrollItemids.get(gs);
if (gachaItemids != null) {
String listStr = "";
for (Integer id : gachaItemids) {
listStr += id.toString();
listStr += " ";
}
if (gachaItemids.size() > 1) {
str += "[" + listStr + "]";
} else {
str += listStr;
}
}
}
return str;
}
private static GachaponScroll getGachaponScroll(String name, String description, boolean both) {
name = name.toLowerCase();
name = name.replace("for acc ", "for accuracy ");
name = name.replace("blunt weapon", "bw");
name = name.replace("eye eqp.", "eye accessory");
name = name.replace("face eqp.", "face accessory");
name = name.replace("for attack", "for att");
name = name.replace("1-handed", "one-handed");
name = name.replace("2-handed", "two-handed");
return new GachaponScroll(name, description, both);
}
private static boolean isUpgradeScroll(String name) {
return name.matches("^(([D|d]ark )?[S|s]croll for).*");
}
private static void fetchLineOnMapleHandbook(String line, String rarity) throws SQLException {
String str = "";
if (!isUpgradeScroll(line)) {
PreparedStatement ps = con.prepareStatement("SELECT `id` FROM `handbook` WHERE `name` LIKE ? ORDER BY `id` ASC;");
ps.setString(1, line);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
int id = rs.getInt("id");
str += Integer.toString(id);
str += " ";
}
rs.close();
ps.close();
} else {
str += getGachaponScrollResults(line, false);
if (str.isEmpty()) {
str += getGachaponScrollResults(line, true);
if (str.isEmpty()) {
System.out.println("NONE for '" + line + "' : " + getGachaponScroll(line, "", false));
}
}
}
if (str.isEmpty()) {
str += line;
}
if (rarity != null) {
str += ("- " + rarity);
}
printWriter.println(str);
}
private static void fetchDataOnMapleHandbook() throws SQLException {
String line;
try {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(fileReader);
int skip = 0;
boolean lineHeader = false;
while ((line = bufferedReader.readLine()) != null) {
if (skip > 0) {
skip--;
if (lineHeader) {
if (!line.isEmpty()) {
lineHeader = false;
printWriter.println();
printWriter.println(line + ":");
}
}
} else if (line.isEmpty()) {
printWriter.println("");
} else if (line.startsWith("Gachapon ")) {
String[] s = line.split("<EFBFBD> ");
String gachaponName = s[s.length - 1];
gachaponName = gachaponName.replace(" ", "_");
gachaponName = gachaponName.toLowerCase();
if (printWriter != null) {
printWriter.close();
}
File outputFile = new File(OUTPUT_DIRECTORY, gachaponName + ".txt");
setupDirectories(outputFile);
printWriter = new PrintWriter(outputFile, StandardCharsets.UTF_8);
skip = 2;
lineHeader = true;
} else if (line.startsWith(".")) {
skip = 1;
lineHeader = true;
} else {
line = line.replace("<EFBFBD>", "'");
for (String item : line.split("\\s\\|\\s")) {
item = item.trim();
if (!item.contentEquals("n/a")) {
String[] itemInfo = item.split(" - ");
fetchLineOnMapleHandbook(itemInfo[0], itemInfo.length > 1 ? itemInfo[1] : null);
}
}
}
}
if (printWriter != null) {
printWriter.close();
}
bufferedReader.close();
fileReader.close();
} catch (IOException ex) {
System.out.println(ex.getMessage());
ex.printStackTrace();
}
}
private static void setupDirectories(File file) {
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
}
public static void main(String[] args) {
try {
loadHandbookUseNames();
fetchDataOnMapleHandbook();
con.close();
} catch (SQLException e) {
System.out.println("Error: invalid SQL syntax");
System.out.println(e.getMessage());
}
}
}

View File

@@ -0,0 +1,192 @@
package tools.mapletools;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
/**
* @author RonanLana
* <p>
* This application acts two-way: first section sets up a table on the SQL Server with all the names used within MapleStory,
* and the second queries all the names placed inside "fetch.txt", returning in the same line order the ids of the elements.
* In case of multiple entries with the same name, multiple ids will be returned in the same line split by a simple space
* in ascending order. An empty line means that no entry with the given name in a line has been found.
* <p>
* IMPORTANT: this will fail for fetching MAP ID (you shouldn't be using this program for these, just checking them up in the
* handbook is enough anyway).
* <p>
* Set whether you are first installing the handbook on the SQL Server (TRUE) or just fetching whatever is on your "fetch_ids.txt"
* file (FALSE) on the INSTALL_SQLTABLE property and build the project. With all done, run the Java executable.
* <p>
* Expected installing time: 30 minutes
*/
public class IdRetriever {
private static final boolean INSTALL_SQLTABLE = true;
private static final File INPUT_FILE = ToolConstants.getInputFile("fetch_ids.txt");
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("fetched_ids.txt");
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static InputStreamReader fileReader = null;
private static BufferedReader bufferedReader = null;
private static void listFiles(String directoryName, ArrayList<File> files) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
files.add(file);
} else if (file.isDirectory()) {
listFiles(file.getAbsolutePath(), files);
}
}
}
private static void parseMapleHandbookLine(String line) throws SQLException {
String[] tokens = line.split(" - ", 3);
if (tokens.length > 1) {
PreparedStatement ps = con.prepareStatement("INSERT INTO `handbook` (`id`, `name`, `description`) VALUES (?, ?, ?)");
try {
ps.setInt(1, Integer.parseInt(tokens[0]));
} catch (NumberFormatException npe) { // odd...
String num = tokens[0].substring(1);
ps.setInt(1, Integer.parseInt(num));
}
ps.setString(2, tokens[1]);
ps.setString(3, tokens.length > 2 ? tokens[2] : "");
ps.execute();
ps.close();
}
}
private static void parseMapleHandbookFile(File fileObj) throws SQLException {
if (shouldSkipParsingFile(fileObj.getName())) {
return;
}
String line;
try {
fileReader = new InputStreamReader(new FileInputStream(fileObj), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
System.out.println("Parsing file '" + fileObj.getCanonicalPath() + "'.");
while ((line = bufferedReader.readLine()) != null) {
try {
parseMapleHandbookLine(line);
} catch (SQLException e) {
System.err.println("Failed to parse line: " + line);
throw e;
}
}
bufferedReader.close();
fileReader.close();
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
// Quest.txt has different formatting: id is last token on the line, instead of the first
private static boolean shouldSkipParsingFile(String fileName) {
return "Quest.txt".equals(fileName);
}
private static void setupSqlTable() throws SQLException {
PreparedStatement ps = con.prepareStatement("DROP TABLE IF EXISTS `handbook`;");
ps.execute();
ps.close();
ps = con.prepareStatement("CREATE TABLE `handbook` ("
+ "`key` int(10) unsigned NOT NULL AUTO_INCREMENT,"
+ "`id` int(10) DEFAULT NULL,"
+ "`name` varchar(200) DEFAULT NULL,"
+ "`description` varchar(1000) DEFAULT '',"
+ "PRIMARY KEY (`key`)"
+ ");");
ps.execute();
ps.close();
}
private static void parseMapleHandbook() throws SQLException {
ArrayList<File> files = new ArrayList<>();
listFiles(ToolConstants.HANDBOOK_PATH, files);
if (files.isEmpty()) {
return;
}
setupSqlTable();
for (File f : files) {
parseMapleHandbookFile(f);
}
}
private static void fetchDataOnMapleHandbook() throws SQLException {
String line;
try {
fileReader = new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
PrintWriter printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
while ((line = bufferedReader.readLine()) != null) {
if (line.isEmpty()) {
printWriter.println("");
continue;
}
PreparedStatement ps = con.prepareStatement("SELECT `id` FROM `handbook` WHERE `name` LIKE ? ORDER BY `id` ASC;");
ps.setString(1, line);
ResultSet rs = ps.executeQuery();
String str = "";
while (rs.next()) {
int id = rs.getInt("id");
str += Integer.toString(id);
str += " ";
}
rs.close();
ps.close();
printWriter.println(str);
}
printWriter.close();
bufferedReader.close();
fileReader.close();
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
public static void main(String[] args) {
try {
if (INSTALL_SQLTABLE) {
parseMapleHandbook();
} else {
fetchDataOnMapleHandbook();
}
con.close();
} catch (SQLException e) {
System.out.println("Error: invalid SQL syntax");
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,64 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package tools.mapletools;
import java.util.List;
/**
*
* @author RonanLana
*/
public class MakerItemEntry {
public int id = -1;
public int itemid = -1;
public int reqLevel = -1;
public int reqMakerLevel = -1;
public int reqItem = -1;
public int reqMeso = -1;
public int reqEquip = -1;
public int catalyst = -1;
public int quantity = -1;
public int tuc = -1;
public int recipeCount = -1;
public int recipeItem = -1;
public List<int[]> recipeList = null;
public List<int[]> randomList = null;
MakerItemEntry(int id, int itemid, int reqLevel, int reqMakerLevel, int reqItem, int reqMeso, int reqEquip, int catalyst, int quantity, int tuc, int recipeCount, int recipeItem, List<int[]> recipeList, List<int[]> randomList) {
this.id = id;
this.itemid = itemid;
this.reqLevel = reqLevel;
this.reqMakerLevel = reqMakerLevel;
this.reqItem = reqItem;
this.reqMeso = reqMeso;
this.reqEquip = reqEquip;
this.catalyst = catalyst;
this.quantity = quantity;
this.tuc = tuc;
this.recipeCount = recipeCount;
this.recipeItem = recipeItem;
this.recipeList = recipeList;
this.randomList = randomList;
}
}

View File

@@ -0,0 +1,160 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
/**
* @author RonanLana
* <p>
* This application seeks from the XMLs all mapid entries that holds the specified
* fieldLimit.
*/
public class MapFieldLimitChecker {
private static final int INITIAL_STRING_LENGTH = 50;
private static final int FIELD_LIMIT = 0x400000;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int mapid = 0;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void listFiles(String directoryName, ArrayList<File> files) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
files.add(file);
} else if (file.isDirectory()) {
listFiles(file.getAbsolutePath(), files);
}
}
}
private static int getMapIdFromFilename(String name) {
try {
return Integer.parseInt(name.substring(0, name.indexOf('.')));
} catch (Exception e) {
return -1;
}
}
private static void translateToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
if (status == 2) {
String d = getName(token);
if (!d.contentEquals("info")) {
forwardCursor(status);
}
}
} else {
if (status == 2) {
String d = getName(token);
if (d.contentEquals("fieldLimit")) {
int value = Integer.parseInt(getValue(token));
if ((value & FIELD_LIMIT) == FIELD_LIMIT) {
System.out.println(mapid + " " + value);
}
}
}
}
}
private static void inspectMapEntry() {
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void loadMapWz() throws IOException {
System.out.println("Reading Map.wz ...");
ArrayList<File> files = new ArrayList<>();
listFiles(WZFiles.MAP.getFilePath() + "/Map", files);
for (File f : files) {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
mapid = getMapIdFromFilename(f.getName());
inspectMapEntry();
bufferedReader.close();
fileReader.close();
}
}
public static void main(String[] args) {
try {
loadMapWz();
System.out.println("Done!");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

View File

@@ -0,0 +1,157 @@
package tools.mapletools;
import org.apache.commons.io.FileUtils;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author RonanLana
* <p>
* The main objective of this tool is to locate all mapids that doesn't have
* the "info" node in their WZ node tree.
*/
public class MapInfoRetriever {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("map_info_report.txt");
private static final List<Integer> missingInfo = new ArrayList<>();
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static boolean hasInfo;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[50];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static boolean translateToken(String token) {
String d;
int temp;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) {
d = getName(token);
if (d.contains("info")) {
hasInfo = true;
return true;
}
temp = status;
forwardCursor(temp);
}
status += 1;
}
return false;
}
private static void searchMapDirectory(int mapArea) {
final File mapDirectory = new File(WZFiles.MAP.getFilePath() + "/Map/Map" + mapArea);
try {
Iterator<File> iter = FileUtils.iterateFiles(mapDirectory, new String[]{"xml"}, true);
System.out.println("Parsing map area " + mapArea);
while (iter.hasNext()) {
File file = iter.next();
searchMapFile(file);
}
} catch (UncheckedIOException e) {
System.err.println("Directory " + mapDirectory.getPath() + " does not exist");
}
}
private static void searchMapFile(File file) {
// This will reference one line at a time
String line = null;
try {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
hasInfo = false;
status = 0;
while ((line = bufferedReader.readLine()) != null) {
if (translateToken(line)) {
break;
}
}
if (!hasInfo) {
missingInfo.add(Integer.valueOf(file.getName().split(".img.xml")[0]));
}
bufferedReader.close();
fileReader.close();
} catch (IOException ex) {
System.out.println("Error reading file '" + file.getName() + "'");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void writeReport() {
try {
PrintWriter printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
if (!missingInfo.isEmpty()) {
for (Integer i : missingInfo) {
printWriter.println(i);
}
} else {
printWriter.println("All map files contain 'info' node.");
}
printWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i <= 9; i++) {
searchMapDirectory(i);
}
writeReport();
}
}

View File

@@ -0,0 +1,184 @@
package tools.mapletools;
import server.life.MapleMonsterStats;
import tools.Pair;
import java.io.File;
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;
/**
* @author RonanLana
* <p>
* This application traces missing meso drop data 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.
* <p>
* 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.
*/
public class MesoFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("meso_drop_data.sql");
private static final boolean PERMIT_MESOS_ON_DOJO_BOSSES = false;
private static final int MESO_ID = 0;
private static final int MIN_ITEMS = 4;
private static final int CHANCE = 400000;
private static final Map<Integer, Pair<Integer, Integer>> mobRange = new HashMap<>();
private static PrintWriter printWriter;
private static Map<Integer, MapleMonsterStats> mobStats;
private static Pair<Integer, Integer> calcMesoRange90(int level, boolean boss) {
int minRange, maxRange;
// MIN range
minRange = (int) (72.70814714 * Math.exp(0.02284640619 * level));
// MAX range
maxRange = (int) (133.8194881 * Math.exp(0.02059225059 * level));
// boss perks
if (boss) {
minRange *= 3;
maxRange *= 10;
}
return new Pair<>(minRange, maxRange);
}
private static Pair<Integer, Integer> calcMesoRange(int level, boolean boss) {
int minRange, maxRange;
// MIN range
minRange = (int) (30.32032228 * Math.exp(0.03281144930 * level));
// MAX range
maxRange = (int) (44.45878459 * Math.exp(0.03289611686 * level));
// boss perks
if (boss) {
minRange *= 3;
maxRange *= 10;
}
return new Pair<>(minRange, maxRange);
}
private static void calcAllMobsMesoRange() {
System.out.print("Calculating range... ");
for (Map.Entry<Integer, MapleMonsterStats> mobStat : mobStats.entrySet()) {
MapleMonsterStats mms = mobStat.getValue();
Pair<Integer, Integer> mesoRange;
if (mms.getLevel() < 90) {
mesoRange = calcMesoRange(mms.getLevel(), mms.isBoss());
} else {
mesoRange = calcMesoRange90(mms.getLevel(), mms.isBoss());
}
mobRange.put(mobStat.getKey(), mesoRange);
}
System.out.println("done!");
}
private static void printSqlHeader() {
printWriter.println(" # SQL File autogenerated from the MapleMesoFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account mob stats such as level and boss for the meso ranges.");
printWriter.println(" # Only mobs with " + MIN_ITEMS + " or more items with no meso entry on the DB it was compiled are presented here.");
printWriter.println();
printWriter.println(" INSERT IGNORE INTO drop_data (`dropperid`, `itemid`, `minimum_quantity`, `maximum_quantity`, `questid`, `chance`) VALUES");
}
private static void printSqlExceptions() {
if (!PERMIT_MESOS_ON_DOJO_BOSSES) {
printWriter.println("\r\n DELETE FROM drop_data WHERE dropperid >= 9300184 AND dropperid <= 9300215 AND itemid = " + MESO_ID + ";");
}
}
private static void printSqlMobMesoRange(int mobid) {
Pair<Integer, Integer> mobmeso = mobRange.get(mobid);
printWriter.println("(" + mobid + ", " + MESO_ID + ", " + mobmeso.left + ", " + mobmeso.right + ", 0, " + CHANCE + "),");
}
private static void printSqlMobMesoRangeFinal(int mobid) {
Pair<Integer, Integer> mobmeso = mobRange.get(mobid);
printWriter.println("(" + mobid + ", " + MESO_ID + ", " + mobmeso.left + ", " + mobmeso.right + ", 0, " + CHANCE + ");");
}
private static void generateMissingMobsMesoRange() {
System.out.print("Generating missing ranges... ");
Connection con = SimpleDatabaseConnection.getConnection();
List<Integer> existingMobs = new ArrayList<>(200);
try {
// select all mobs which doesn't drop mesos and have a fair amount of items dropping (meaning they are not an event mob)
PreparedStatement ps = con.prepareStatement("SELECT dropperid FROM drop_data WHERE dropperid NOT IN (SELECT DISTINCT dropperid FROM drop_data WHERE itemid = 0) GROUP BY dropperid HAVING count(*) >= " + MIN_ITEMS + ";");
ResultSet rs = ps.executeQuery();
if (rs.isBeforeFirst()) {
while (rs.next()) {
int mobid = rs.getInt(1);
if (mobRange.containsKey(mobid)) {
existingMobs.add(mobid);
}
}
if (!existingMobs.isEmpty()) {
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printSqlHeader();
for (int i = 0; i < existingMobs.size() - 1; i++) {
printSqlMobMesoRange(existingMobs.get(i));
}
printSqlMobMesoRangeFinal(existingMobs.get(existingMobs.size() - 1));
printSqlExceptions();
printWriter.close();
} else {
throw new Exception("ALREADY UPDATED");
}
} else {
throw new Exception("ALREADY UPDATED");
}
rs.close();
ps.close();
con.close();
System.out.println("done!");
} catch (Exception e) {
if (e.getMessage() != null && e.getMessage().equals("ALREADY UPDATED")) {
System.out.println("done! The DB is already up-to-date, no file generated.");
} else {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// load mob stats from WZ
mobStats = MonsterStatFetcher.getAllMonsterStats();
calcAllMobsMesoRange();
generateMissingMobsMesoRange();
}
}

View File

@@ -0,0 +1,169 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author RonanLana
* <p>
* This application simply gets from the MonsterBook.img.xml all mobid's and
* puts them on a SQL table with the correspondent mob cardid.
*/
public class MobBookIndexer {
private static final File INPUT_FILE = new File(WZFiles.STRING.getFile(), "MonsterBook.img.xml");
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int mobId = -1;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
if (j - i < 7) {
dest = new char[6];
} else {
dest = new char[7];
}
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d);
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static boolean isCard(int itemId) {
return itemId / 10000 == 238;
}
private static void loadPairFromMob() {
System.out.println("Loading mob id " + mobId);
try {
PreparedStatement ps, ps2;
ResultSet rs;
ps = con.prepareStatement("SELECT itemid FROM drop_data WHERE (dropperid = ? AND itemid > 0) GROUP BY itemid;");
ps.setInt(1, mobId);
rs = ps.executeQuery();
while (rs.next()) {
int itemId = rs.getInt("itemid");
if (isCard(itemId)) {
ps2 = con.prepareStatement("INSERT INTO `monstercardwz` (`cardid`, `mobid`) VALUES (?, ?)");
ps2.setInt(1, itemId);
ps2.setInt(2, mobId);
ps2.executeUpdate();
}
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void translateToken(String token) {
String d;
int temp;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting MobId
d = getName(token);
mobId = Integer.parseInt(d);
} else if (status == 2) {
d = getName(token);
if (d.contains("reward")) {
temp = status;
loadPairFromMob();
forwardCursor(temp);
}
}
status += 1;
}
}
private static void indexFromDropData() {
// This will reference one line at a time
String line = null;
try {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
PreparedStatement ps = con.prepareStatement("DROP TABLE IF EXISTS monstercardwz;");
ps.execute();
ps = con.prepareStatement("CREATE TABLE `monstercardwz` ("
+ "`id` int(10) unsigned NOT NULL AUTO_INCREMENT,"
+ "`cardid` int(10) NOT NULL DEFAULT '-1',"
+ "`mobid` int(10) NOT NULL DEFAULT '-1',"
+ "PRIMARY KEY (`id`)"
+ ");");
ps.execute();
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
bufferedReader.close();
fileReader.close();
con.close();
} catch (FileNotFoundException ex) {
System.out.println("Unable to open file '" + INPUT_FILE + "'");
} catch (IOException ex) {
System.out.println("Error reading file '" + INPUT_FILE + "'");
} catch (SQLException e) {
System.out.println("Warning: Could not establish connection to database to change card chance rate.");
System.out.println(e.getMessage());
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
indexFromDropData();
}
}

View File

@@ -0,0 +1,179 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author RonanLana
* <p>
* This application updates the Monster Book drop data with the actual underlying drop data from
* the Maplestory database specified in the URL below.
* <p>
* In other words all items drops from monsters listed inside the Mob Book feature will be patched to match exactly like the item
* drop list specified in the URL's Maplestory database.
* <p>
* The original file "MonsterBook.img.xml" from String.wz must be copied to the directory of this application and only then
* executed. This program will generate another file that must replace the original server file to make the effects take place
* to on your server.
* <p>
* After replacing on server, this XML must be updated on the client via WZ Editor (HaRepack for instance). Once inside the repack,
* remove the property 'MonsterBook.img' inside 'string.wz' and choose to import the xml generated with this software.
*/
public class MobBookUpdate {
private static final File INPUT_FILE = new File(WZFiles.STRING.getFile(), "MonsterBook.img.xml");
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("MonsterBook_updated.img.xml");
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int mobId = -1;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
if (j - i < 7) {
dest = new char[6];
} else {
dest = new char[7];
}
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d);
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
if (line != null) {
printWriter.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void loadDropsFromMob() {
System.out.println("Loading mob id " + mobId);
try {
String toPrint;
int itemId, cont = 0;
PreparedStatement ps = con.prepareStatement("SELECT itemid FROM drop_data WHERE (dropperid = ? AND itemid > 0) GROUP BY itemid;");
ps.setInt(1, mobId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
toPrint = "";
for (int k = 0; k <= status; k++) {
toPrint += " ";
}
toPrint += "<int name=\"";
toPrint += cont;
toPrint += "\" value=\"";
itemId = rs.getInt("itemid");
toPrint += itemId;
toPrint += "\" />";
printWriter.println(toPrint);
cont++;
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void translateToken(String token) {
String d;
int temp;
printWriter.println(token);
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting MobId
d = getName(token);
mobId = Integer.parseInt(d);
} else if (status == 2) {
d = getName(token);
if (d.contains("reward")) {
temp = status;
loadDropsFromMob();
forwardCursor(temp);
}
}
status += 1;
}
}
private static void updateFromDropData() {
// This will reference one line at a time
String line = null;
try {
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
printWriter.close();
bufferedReader.close();
fileReader.close();
con.close();
} catch (FileNotFoundException ex) {
System.out.println("Unable to open file '" + INPUT_FILE + "'");
} catch (IOException ex) {
System.out.println("Error reading file '" + INPUT_FILE + "'");
} catch (SQLException e) {
System.out.println("Warning: Could not establish connection to database to change card chance rate.");
System.out.println(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
updateFromDropData();
}
}

View File

@@ -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<Integer, MapleMonsterStats> monsterStats = new HashMap<>();
static Map<Integer, MapleMonsterStats> 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<Integer> 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<Pair<Integer, Integer>> 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)))));
}
}
}

View File

@@ -0,0 +1,216 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application finds inexistent itemids within the drop data from
* the Maplestory database specified in the URL below. This program
* assumes all itemids uses 7 digits.
* <p>
* A file is generated listing all the inexistent ids.
*/
public class NoItemIdFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("no_item_id_report.txt");
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static final Set<Integer> existingIds = new HashSet<>();
private static final Set<Integer> nonExistingIds = new HashSet<>();
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int itemId = -1;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[100];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d);
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void translateToken(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting ItemId
d = getName(token);
itemId = Integer.parseInt(d.substring(1, 8));
existingIds.add(itemId);
forwardCursor(status);
}
status += 1;
}
}
private static void readItemDataFile(File file) {
// This will reference one line at a time
String line = null;
try {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
status = 0;
try {
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
} catch (NumberFormatException npe) {
// second criteria, itemid is on the name of the file
try {
itemId = Integer.parseInt(file.getName().substring(0, 7));
existingIds.add(itemId);
} catch (NumberFormatException npe2) {
}
}
bufferedReader.close();
fileReader.close();
} catch (FileNotFoundException ex) {
System.out.println("Unable to open file '" + file.getName() + "'");
} catch (IOException ex) {
System.out.println("Error reading file '" + file.getName() + "'");
}
}
private static void readEquipDataDirectory(String dirPath) {
File[] folders = new File(dirPath).listFiles();
//If this pathname does not denote a directory, then listFiles() returns null.
for (File folder : folders) { // enter all subfolders
if (folder.isDirectory()) {
System.out.println("Reading '" + dirPath + "/" + folder.getName() + "'...");
try {
File[] files = folder.listFiles();
for (File file : files) { // enter all XML files under subfolders
if (file.isFile()) {
itemId = Integer.parseInt(file.getName().substring(0, 8));
existingIds.add(itemId);
}
}
} catch (NumberFormatException nfe) {
}
}
}
}
private static void readItemDataDirectory(String dirPath) {
File[] folders = new File(dirPath).listFiles();
//If this pathname does not denote a directory, then listFiles() returns null.
for (File folder : folders) { // enter all subfolders
if (folder.isDirectory()) {
System.out.println("Reading '" + dirPath + "/" + folder.getName() + "'...");
File[] files = folder.listFiles();
for (File file : files) { // enter all XML files under subfolders
if (file.isFile()) {
readItemDataFile(file);
}
}
}
}
}
private static void evaluateDropsFromTable(String table) throws SQLException {
PreparedStatement ps = con.prepareStatement("SELECT DISTINCT itemid FROM " + table + ";");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
if (!existingIds.contains(rs.getInt(1))) {
nonExistingIds.add(rs.getInt(1));
}
}
rs.close();
ps.close();
}
private static void evaluateDropsFromDb() {
try {
System.out.println("Evaluating item data on DB...");
evaluateDropsFromTable("drop_data");
evaluateDropsFromTable("reactordrops");
if (!nonExistingIds.isEmpty()) {
List<Integer> list = new ArrayList<>(nonExistingIds);
Collections.sort(list);
for (Integer i : list) {
printWriter.println(i);
}
}
System.out.println("Inexistent itemid count: " + nonExistingIds.size());
System.out.println("Total itemid count: " + existingIds.size());
con.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
existingIds.add(0); // meso itemid
readEquipDataDirectory(WZFiles.CHARACTER.getFilePath());
readItemDataDirectory(WZFiles.ITEM.getFilePath());
evaluateDropsFromDb();
printWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,473 @@
package tools.mapletools;
import provider.*;
import provider.wz.WZFiles;
import java.io.File;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application finds itemids with inexistent name and description from
* within the server-side XMLs, then identify them on a report file along
* with a XML excerpt to be appended on the String.wz xml nodes. This program
* assumes all equipids are depicted using 8 digits and item using 7 digits.
* <p>
* Estimated parse time: 2 minutes
*/
public class NoItemNameFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("no_item_name_result.txt");
private static final File OUTPUT_XML_FILE = ToolConstants.getOutputFile("no_item_name_xml.txt");
private static final Map<Integer, String> itemsWzPath = new HashMap<>();
private static final Map<Integer, EquipType> equipTypes = new HashMap<>();
private static final Map<Integer, ItemType> itemsWithNoNameProperty = new HashMap<>();
private static final Set<Integer> equipsWithNoCashProperty = new HashSet<>();
private static final Map<Integer, String> nameContentCache = new HashMap<>();
private static final Map<Integer, String> descContentCache = new HashMap<>();
private static PrintWriter printWriter = null;
private static ItemType curType = ItemType.UNDEF;
private enum ItemType {
UNDEF, CASH, CONSUME, EQP, ETC, INS, PET
}
private enum EquipType {
UNDEF, ACCESSORY, CAP, CAPE, COAT, FACE, GLOVE, HAIR, LONGCOAT, PANTS, PETEQUIP, RING, SHIELD, SHOES, TAMING, WEAPON
}
private static void processStringSubdirectoryData(MapleData subdirData, String subdirPath) {
for (MapleData md : subdirData.getChildren()) {
try {
MapleData nameData = md.getChildByPath("name");
MapleData descData = md.getChildByPath("desc");
int itemId = Integer.parseInt(md.getName());
if (nameData != null && descData != null) {
itemsWithNoNameProperty.remove(itemId);
} else {
if (nameData != null) {
nameContentCache.put(itemId, MapleDataTool.getString("name", md));
} else if (descData != null) {
descContentCache.put(itemId, MapleDataTool.getString("desc", md));
}
System.out.println("Found itemid on String.wz with no full property: " + subdirPath + subdirData.getName() + "/" + md.getName());
}
} catch (NumberFormatException nfe) {
System.out.println("Error reading string image: " + subdirPath + subdirData.getName() + "/" + md.getName());
}
}
}
private static void readStringSubdirectoryData(MapleData subdirData, int depth, String subdirPath) {
if (depth > 0) {
for (MapleData mDir : subdirData.getChildren()) {
readStringSubdirectoryData(mDir, depth - 1, subdirPath + mDir.getName() + "/");
}
} else {
processStringSubdirectoryData(subdirData, subdirPath);
}
}
private static void readStringSubdirectoryData(MapleData subdirData, int depth) {
readStringSubdirectoryData(subdirData, depth, "");
}
private static void readStringWZData() {
System.out.println("Parsing String.wz...");
MapleDataProvider stringData = MapleDataProviderFactory.getDataProvider(WZFiles.STRING);
MapleData cashStringData = stringData.getData("Cash.img");
readStringSubdirectoryData(cashStringData, 0);
MapleData consumeStringData = stringData.getData("Consume.img");
readStringSubdirectoryData(consumeStringData, 0);
MapleData eqpStringData = stringData.getData("Eqp.img");
readStringSubdirectoryData(eqpStringData, 2);
MapleData etcStringData = stringData.getData("Etc.img");
readStringSubdirectoryData(etcStringData, 1);
MapleData insStringData = stringData.getData("Ins.img");
readStringSubdirectoryData(insStringData, 0);
MapleData petStringData = stringData.getData("Pet.img");
readStringSubdirectoryData(petStringData, 0);
}
private static boolean isTamingMob(int itemId) {
int itemType = itemId / 1000;
return itemType == 1902 || itemType == 1912;
}
private static boolean isAccessory(int itemId) {
return itemId >= 1110000 && itemId < 1140000;
}
private static ItemType getItemTypeFromDirectoryName(String dirName) {
return switch (dirName) {
case "Cash" -> ItemType.CASH;
case "Consume" -> ItemType.CONSUME;
case "Etc" -> ItemType.ETC;
case "Install" -> ItemType.INS;
case "Pet" -> ItemType.PET;
default -> ItemType.UNDEF;
};
}
private static EquipType getEquipTypeFromDirectoryName(String dirName) {
return switch (dirName) {
case "Accessory" -> EquipType.ACCESSORY;
case "Cap" -> EquipType.CAP;
case "Cape" -> EquipType.CAPE;
case "Coat" -> EquipType.COAT;
case "Face" -> EquipType.FACE;
case "Glove" -> EquipType.GLOVE;
case "Hair" -> EquipType.HAIR;
case "Longcoat" -> EquipType.LONGCOAT;
case "Pants" -> EquipType.PANTS;
case "PetEquip" -> EquipType.PETEQUIP;
case "Ring" -> EquipType.RING;
case "Shield" -> EquipType.SHIELD;
case "Shoes" -> EquipType.SHOES;
case "TamingMob" -> EquipType.TAMING;
case "Weapon" -> EquipType.WEAPON;
default -> EquipType.UNDEF;
};
}
private static String getStringDirectoryNameFromEquipType(EquipType eType) {
return switch (eType) {
case ACCESSORY -> "Accessory";
case CAP -> "Cap";
case CAPE -> "Cape";
case COAT -> "Coat";
case FACE -> "Face";
case GLOVE -> "Glove";
case HAIR -> "Hair";
case LONGCOAT -> "Longcoat";
case PANTS -> "Pants";
case PETEQUIP -> "PetEquip";
case RING -> "Ring";
case SHIELD -> "Shield";
case SHOES -> "Shoes";
case TAMING -> "Taming";
case WEAPON -> "Weapon";
default -> "Undefined";
};
}
private static void readEquipNodeData(MapleDataProvider data, MapleDataDirectoryEntry mDir, String wzFileName, String dirName) {
EquipType eqType = getEquipTypeFromDirectoryName(dirName);
for (MapleDataFileEntry mFile : mDir.getFiles()) {
String fileName = mFile.getName();
try {
int itemId = Integer.parseInt(fileName.substring(0, 8));
itemsWithNoNameProperty.put(itemId, curType);
equipTypes.put(itemId, eqType);
itemsWzPath.put(itemId, wzFileName + "/" + dirName + "/" + fileName);
if (!isAccessory(itemId) && !isTamingMob(itemId)) {
try {
MapleData fileData = data.getData(dirName + "/" + fileName);
MapleData mdinfo = fileData.getChildByPath("info");
if (mdinfo.getChildByPath("cash") == null) {
equipsWithNoCashProperty.add(itemId);
}
} catch (NullPointerException npe) {
System.out.println("[SEVERE] " + mFile.getName() + " failed to load. Issue: " + npe.getMessage() + "\n\n");
}
}
} catch (Exception e) {
}
}
}
private static void readEquipWZData() {
String wzFileName = "Character.wz";
MapleDataProvider data = MapleDataProviderFactory.getDataProvider(WZFiles.CHARACTER);
MapleDataDirectoryEntry root = data.getRoot();
System.out.println("Parsing " + wzFileName + "...");
for (MapleDataDirectoryEntry mDir : root.getSubdirectories()) {
String dirName = mDir.getName();
if (dirName.contentEquals("Dragon")) {
continue;
}
readEquipNodeData(data, mDir, wzFileName, dirName);
}
}
private static void readItemWZData() {
String wzFileName = "Item.wz";
MapleDataProvider data = MapleDataProviderFactory.getDataProvider(WZFiles.ITEM);
MapleDataDirectoryEntry root = data.getRoot();
System.out.println("Parsing " + wzFileName + "...");
for (MapleDataDirectoryEntry mDir : root.getSubdirectories()) {
String dirName = mDir.getName();
if (dirName.contentEquals("Special")) {
continue;
}
curType = getItemTypeFromDirectoryName(dirName);
if (!dirName.contentEquals("Pet")) {
for (MapleDataFileEntry mFile : mDir.getFiles()) {
String fileName = mFile.getName();
MapleData fileData = data.getData(dirName + "/" + fileName);
for (MapleData mData : fileData.getChildren()) {
try {
int itemId = Integer.parseInt(mData.getName());
itemsWithNoNameProperty.put(itemId, curType);
itemsWzPath.put(itemId, wzFileName + "/" + dirName + "/" + fileName);
} catch (Exception e) {
System.out.println("EXCEPTION on '" + mData.getName() + "' " + wzFileName + "/" + dirName + "/" + fileName);
}
}
}
} else {
readEquipNodeData(data, mDir, wzFileName, dirName);
}
}
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleInvalidItemWithNoNameFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static void printReportFileResults() {
if (!itemsWithNoNameProperty.isEmpty()) {
printWriter.println("Itemids with missing 'name' property: ");
List<Integer> itemids = new ArrayList<>(itemsWithNoNameProperty.keySet());
Collections.sort(itemids);
for (Integer itemid : itemids) {
printWriter.println(" " + itemid + " " + itemsWzPath.get(itemid));
}
printWriter.println();
}
if (!equipsWithNoCashProperty.isEmpty()) {
printWriter.println("Equipids with missing 'cash' property: ");
List<Integer> itemids = new ArrayList<>(equipsWithNoCashProperty);
Collections.sort(itemids);
for (Integer itemid : itemids) {
printWriter.println(" " + itemid + " " + itemsWzPath.get(itemid));
}
}
}
private static Map<String, List<Integer>> filterMissingItemNames() {
List<Integer> cashList = new ArrayList<>(20);
List<Integer> consList = new ArrayList<>(20);
List<Integer> eqpList = new ArrayList<>(20);
List<Integer> etcList = new ArrayList<>(20);
List<Integer> insList = new ArrayList<>(20);
List<Integer> petList = new ArrayList<>(20);
for (Map.Entry<Integer, ItemType> ids : itemsWithNoNameProperty.entrySet()) {
switch (ids.getValue()) {
case CASH -> cashList.add(ids.getKey());
case CONSUME -> consList.add(ids.getKey());
case EQP -> eqpList.add(ids.getKey());
case ETC -> etcList.add(ids.getKey());
case INS -> insList.add(ids.getKey());
case PET -> petList.add(ids.getKey());
}
}
Map<String, List<Integer>> nameTags = new HashMap<>();
nameTags.put("Cash.img", cashList);
nameTags.put("Consume.img", consList);
nameTags.put("Eqp.img", eqpList);
nameTags.put("Etc.img", etcList);
nameTags.put("Ins.img", insList);
nameTags.put("Pet.img", petList);
return nameTags;
}
private static void printOutputFileHeader() {
printWriter.println(" # XML File autogenerated from the MapleInvalidItemWithNoNameFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static String getMissingEquipName(int itemid) {
String s = nameContentCache.get(itemid);
if (s == null) {
s = "MISSING NAME " + itemid;
}
return s;
}
private static String getMissingEquipDesc(int itemid) {
String s = descContentCache.get(itemid);
if (s == null && itemid >= 2000000) { // thanks Halcyon for noticing "missing info" on equips
s = "MISSING INFO " + itemid;
}
return s;
}
private static void writeMissingEquipInfo(Integer itemid) {
printWriter.println(" <imgdir name=\"" + itemid + "\">");
String s;
s = getMissingEquipName(itemid);
printWriter.println(" <string name=\"name\" value=\"" + s + "\"/>");
s = getMissingEquipDesc(itemid);
printWriter.println(" <string name=\"desc\" value=\"" + s + "\"/>");
printWriter.println(" </imgdir>");
}
private static void writeEquipSubdirectoryHeader(EquipType eType) {
printWriter.println(" <imgdir name=\"" + getStringDirectoryNameFromEquipType(eType) + "\">");
}
private static void writeEquipSubdirectoryFooter() {
printWriter.println(" </imgdir>");
}
private static void writeEquipXMLHeader() {
printWriter.println(" <imgdir name=\"Eqp\">");
}
private static void writeEquipXMLFooter() {
printWriter.println(" </imgdir>");
}
private static void writeMissingItemInfo(Integer itemid) {
printWriter.println(" <imgdir name=\"" + itemid + "\">");
printWriter.println(" <string name=\"name\" value=\"MISSING NAME\"/>");
printWriter.println(" <string name=\"desc\" value=\"MISSING INFO\"/>");
printWriter.println(" </imgdir>");
}
private static void writeXMLHeader(String fileName) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
printWriter.println("<imgdir name=\"" + fileName + "\">");
}
private static void writeXMLFooter() {
printWriter.println("</imgdir>");
}
private static void writeMissingEquipWZNode(EquipType eType, List<Integer> missingNames) {
if (!missingNames.isEmpty()) {
Collections.sort(missingNames);
writeEquipSubdirectoryHeader(eType);
for (Integer equipid : missingNames) {
writeMissingEquipInfo(equipid);
}
writeEquipSubdirectoryFooter();
}
}
private static void writeMissingStringWZNode(String nodePath, List<Integer> missingNames, boolean isEquip) {
if (!missingNames.isEmpty()) {
if (!isEquip) {
Collections.sort(missingNames);
printWriter.println(nodePath + ":");
printWriter.println();
writeXMLHeader(nodePath);
for (Integer i : missingNames) {
writeMissingItemInfo(i);
}
writeXMLFooter();
printWriter.println();
} else {
int arraySize = EquipType.values().length;
List<Integer>[] equips = new List[arraySize];
for (int i = 0; i < arraySize; i++) {
equips[i] = new ArrayList<>(42);
}
for (Integer itemid : missingNames) {
equips[equipTypes.get(itemid).ordinal()].add(itemid);
}
printWriter.println(nodePath + ":");
printWriter.println();
writeXMLHeader(nodePath);
writeEquipXMLHeader();
for (EquipType eType : EquipType.values()) {
writeMissingEquipWZNode(eType, equips[eType.ordinal()]);
}
writeEquipXMLFooter();
writeXMLFooter();
printWriter.println();
}
}
}
private static void writeMissingStringWZNames(Map<String, List<Integer>> missingNames) throws Exception {
System.out.println("Writing remaining 'String.wz' names...");
printWriter = new PrintWriter(OUTPUT_XML_FILE, StandardCharsets.UTF_8);
printOutputFileHeader();
String[] nodePaths = {"Cash.img", "Consume.img", "Eqp.img", "Etc.img", "Ins.img", "Pet.img"};
for (int i = 0; i < nodePaths.length; i++) {
writeMissingStringWZNode(nodePaths[i], missingNames.get(nodePaths[i]), i == 2);
}
printWriter.close();
}
public static void main(String[] args) {
try {
curType = ItemType.EQP;
readEquipWZData();
curType = ItemType.UNDEF;
readItemWZData();
readStringWZData(); // calculates the diff and effectively holds all items with no name property on the WZ
System.out.println("Reporting results...");
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
printReportFileResults();
printWriter.close();
Map<String, List<Integer>> missingNames = filterMissingItemNames();
writeMissingStringWZNames(missingNames);
System.out.println("Done!");
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,268 @@
package tools.mapletools;
import provider.wz.WZFiles;
import tools.Pair;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application parses the Quest.wz file inputted and generates a report showing
* all cases where a quest requires an item, but doesn't take them, which may happen
* because the node representing the item doesn't have a "count" clause.
* <p>
* Running it should generate a report file under "output" folder with the search results.
*/
public class QuestItemCountFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("quest_item_count_report.txt");
private static final String ACT_NAME = WZFiles.QUEST.getFilePath() + "/Act.img.xml";
private static final String CHECK_NAME = WZFiles.QUEST.getFilePath() + "/Check.img.xml";
private static final int INITIAL_STRING_LENGTH = 50;
private static final Map<Integer, Map<Integer, Integer>> checkItems = new HashMap<>();
private static final Map<Integer, Map<Integer, Integer>> actItems = new HashMap<>();
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int questId = -1;
private static int isCompleteState = 0;
private static int curItemId;
private static int curItemCount;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void readItemLabel(String token) {
String name = getName(token);
String value = getValue(token);
switch (name) {
case "id" -> curItemId = Integer.parseInt(value);
case "count" -> curItemCount = Integer.parseInt(value);
}
}
private static void commitQuestItemPair(Map<Integer, Map<Integer, Integer>> map) {
Map<Integer, Integer> list = map.get(questId);
if (list == null) {
list = new LinkedHashMap<>();
map.put(questId, list);
}
list.put(curItemId, curItemCount);
}
private static void translateTokenAct(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
if (status == 4) {
if (curItemCount == Integer.MAX_VALUE && isCompleteState == 1) {
commitQuestItemPair(actItems);
}
}
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
if (!token.contains("item")) {
forwardCursor(status);
}
} else if (status == 4) {
curItemId = Integer.MAX_VALUE;
curItemCount = Integer.MAX_VALUE;
}
status += 1;
} else {
if (status == 5) {
readItemLabel(token);
}
}
}
private static void translateTokenCheck(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
if (status == 4) {
Map<Integer, Integer> missedItems = actItems.get(questId);
if (missedItems != null && missedItems.containsKey(curItemId) && isCompleteState == 1) {
commitQuestItemPair(checkItems);
}
}
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
if (!token.contains("item")) {
forwardCursor(status);
}
} else if (status == 4) {
curItemId = Integer.MAX_VALUE;
curItemCount = Integer.MAX_VALUE;
}
status += 1;
} else {
if (status == 5) {
readItemLabel(token);
}
}
}
private static void readQuestItemCountData() throws IOException {
String line;
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(ACT_NAME), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateTokenAct(line);
}
bufferedReader.close();
fileReader.close();
fileReader = new InputStreamReader(new FileInputStream(CHECK_NAME), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateTokenCheck(line);
}
bufferedReader.close();
fileReader.close();
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleQuestItemCountFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static void printReportFileResults() {
List<Pair<Integer, Pair<Integer, Integer>>> reports = new ArrayList<>();
List<Pair<Integer, Integer>> notChecked = new ArrayList<>();
for (Map.Entry<Integer, Map<Integer, Integer>> actItem : actItems.entrySet()) {
int questid = actItem.getKey();
for (Map.Entry<Integer, Integer> actData : actItem.getValue().entrySet()) {
int itemid = actData.getKey();
Map<Integer, Integer> checkData = checkItems.get(questid);
if (checkData != null) {
Integer count = checkData.get(itemid);
if (count != null) {
reports.add(new Pair<>(questid, new Pair<>(itemid, -count)));
}
} else {
notChecked.add(new Pair<>(questid, itemid));
}
}
}
for (Pair<Integer, Pair<Integer, Integer>> r : reports) {
printWriter.println("Questid " + r.left + " : Itemid " + r.right.left + " should have qty " + r.right.right);
}
for (Pair<Integer, Integer> r : notChecked) {
printWriter.println("Questid " + r.left + " : Itemid " + r.right + " is unchecked");
}
}
private static void reportQuestItemCountData() {
// This will reference one line at a time
try {
System.out.println("Reading WZs...");
readQuestItemCountData();
System.out.println("Reporting results...");
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
printReportFileResults();
printWriter.close();
System.out.println("Done!");
} catch (FileNotFoundException ex) {
System.out.println("Unable to open quest file.");
} catch (IOException ex) {
System.out.println("Error reading quest file.");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
reportQuestItemCountData();
}
}

View File

@@ -0,0 +1,529 @@
package tools.mapletools;
import org.apache.commons.io.FileUtils;
import provider.wz.WZFiles;
import server.MapleItemInformationProvider;
import tools.DatabaseConnection;
import tools.Pair;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application haves 2 objectives: fetch missing drop data relevant to quests,
* and update the questid from items that are labeled as "Quest Item" on the DB.
* <p>
* Running it should generate a report file under "output" folder with the search results.
* <p>
* Estimated parse time: 1 minute
*/
public class QuestItemFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("quest_report.txt");
private static final int INITIAL_STRING_LENGTH = 50;
private static final int INITIAL_LENGTH = 200;
private static final boolean DISPLAY_EXTRA_INFO = true; // display items with zero quantity over the quest act WZ
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static final Map<Integer, Set<Integer>> startQuestItems = new HashMap<>(INITIAL_LENGTH);
private static final Map<Integer, Set<Integer>> completeQuestItems = new HashMap<>(INITIAL_LENGTH);
private static final Map<Integer, Set<Integer>> zeroedStartQuestItems = new HashMap<>();
private static final Map<Integer, Set<Integer>> zeroedCompleteQuestItems = new HashMap<>();
private static final Map<Integer, int[]> mixedQuestidItems = new HashMap<>();
private static final Set<Integer> limitedQuestids = new HashSet<>();
private static MapleItemInformationProvider ii;
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int questId = -1;
private static int isCompleteState = 0;
private static int currentItemid = 0;
private static int currentCount = 0;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
if (j < i) {
return "0"; //node value containing 'name' in it's scope, cheap fix since we don't deal with strings anyway
}
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void inspectQuestItemList(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
readItemToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void processCurrentItem() {
try {
if (ii.isQuestItem(currentItemid)) {
if (currentCount != 0) {
if (isCompleteState == 1) {
if (currentCount < 0) {
Set<Integer> qi = completeQuestItems.get(questId);
if (qi == null) {
Set<Integer> newSet = new HashSet<>();
newSet.add(currentItemid);
completeQuestItems.put(questId, newSet);
} else {
qi.add(currentItemid);
}
}
} else {
if (currentCount > 0) {
Set<Integer> qi = startQuestItems.get(questId);
if (qi == null) {
Set<Integer> newSet = new HashSet<>();
newSet.add(currentItemid);
startQuestItems.put(questId, newSet);
} else {
qi.add(currentItemid);
}
}
}
} else {
if (isCompleteState == 1) {
Set<Integer> qi = zeroedCompleteQuestItems.get(questId);
if (qi == null) {
Set<Integer> newSet = new HashSet<>();
newSet.add(currentItemid);
zeroedCompleteQuestItems.put(questId, newSet);
} else {
qi.add(currentItemid);
}
} else {
Set<Integer> qi = zeroedStartQuestItems.get(questId);
if (qi == null) {
Set<Integer> newSet = new HashSet<>();
newSet.add(currentItemid);
zeroedStartQuestItems.put(questId, newSet);
} else {
qi.add(currentItemid);
}
}
}
}
} catch (Exception e) {
}
}
private static void readItemToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
processCurrentItem();
currentItemid = 0;
currentCount = 0;
} else if (token.contains("imgdir")) {
status += 1;
} else {
String d = getName(token);
if (d.equals("id")) {
currentItemid = Integer.parseInt(getValue(token));
} else if (d.equals("count")) {
currentCount = Integer.parseInt(getValue(token));
}
}
}
private static void translateActToken(String token) {
String d;
int temp;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
d = getName(token);
if (d.contains("item")) {
temp = status;
inspectQuestItemList(temp);
} else {
forwardCursor(status);
}
}
status += 1;
} else {
if (status == 3) {
d = getName(token);
if (d.equals("end")) {
limitedQuestids.add(questId);
}
}
}
}
private static void translateCheckToken(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
forwardCursor(status);
}
status += 1;
} else {
if (status == 3) {
d = getName(token);
if (d.equals("end")) {
limitedQuestids.add(questId);
}
}
}
}
private static void calculateQuestItemDiff() {
// This will remove started quest items from the "to complete" item set.
for (Map.Entry<Integer, Set<Integer>> qd : startQuestItems.entrySet()) {
for (Integer qi : qd.getValue()) {
Set<Integer> questSet = completeQuestItems.get(qd.getKey());
if (questSet != null) {
if (questSet.remove(qi)) {
if (completeQuestItems.isEmpty()) {
completeQuestItems.remove(qd.getKey());
}
}
}
}
}
}
private static List<Pair<Integer, Integer>> getPairsQuestItem() { // quest items not gained at WZ's quest start
List<Pair<Integer, Integer>> list = new ArrayList<>(INITIAL_LENGTH);
for (Map.Entry<Integer, Set<Integer>> qd : completeQuestItems.entrySet()) {
for (Integer qi : qd.getValue()) {
list.add(new Pair<>(qi, qd.getKey()));
}
}
return list;
}
private static String getTableName(boolean dropdata) {
return dropdata ? "drop_data" : "reactordrops";
}
private static void filterQuestDropsOnTable(Pair<Integer, Integer> iq, List<Pair<Integer, Integer>> itemsWithQuest, boolean dropdata) throws SQLException {
PreparedStatement ps = con.prepareStatement("SELECT questid FROM " + getTableName(dropdata) + " WHERE itemid = ?;");
ps.setInt(1, iq.getLeft());
ResultSet rs = ps.executeQuery();
if (rs.isBeforeFirst()) {
while (rs.next()) {
int curQuest = rs.getInt(1);
if (curQuest != iq.getRight()) {
Set<Integer> sqSet = startQuestItems.get(curQuest);
if (sqSet != null && sqSet.contains(iq.getLeft())) {
continue;
}
int[] mixed = new int[3];
mixed[0] = iq.getLeft();
mixed[1] = curQuest;
mixed[2] = iq.getRight();
mixedQuestidItems.put(iq.getLeft(), mixed);
}
}
itemsWithQuest.remove(iq);
}
rs.close();
ps.close();
}
private static void filterQuestDropsOnDB(List<Pair<Integer, Integer>> itemsWithQuest) throws SQLException {
List<Pair<Integer, Integer>> copyItemsWithQuest = new ArrayList<>(itemsWithQuest);
try {
for (Pair<Integer, Integer> iq : copyItemsWithQuest) {
filterQuestDropsOnTable(iq, itemsWithQuest, true);
filterQuestDropsOnTable(iq, itemsWithQuest, false);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void filterDirectorySearchMatchingData(String path, List<Pair<Integer, Integer>> itemsWithQuest) {
Iterator<File> iter = FileUtils.iterateFiles(new File(path), new String[]{"sql", "js", "txt", "java"}, true);
while (iter.hasNext()) {
File file = iter.next();
fileSearchMatchingData(file, itemsWithQuest);
}
}
private static boolean foundMatchingDataOnFile(String fileContent, String searchStr) {
return fileContent.contains(searchStr);
}
private static void fileSearchMatchingData(File file, List<Pair<Integer, Integer>> itemsWithQuest) {
try {
String fileContent = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
List<Pair<Integer, Integer>> copyItemsWithQuest = new ArrayList<>(itemsWithQuest);
for (Pair<Integer, Integer> iq : copyItemsWithQuest) {
if (foundMatchingDataOnFile(fileContent, String.valueOf(iq.getLeft()))) {
itemsWithQuest.remove(iq);
}
}
} catch (IOException ioe) {
System.out.println("Failed to read file: " + file.getAbsolutePath());
ioe.printStackTrace();
}
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleQuestItemFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the underlying DB, server source files and the server-side WZ.xmls.");
printWriter.println();
}
private static List<Map.Entry<Integer, Integer>> getSortedMapEntries0(Map<Integer, Integer> map) {
List<Map.Entry<Integer, Integer>> list = new ArrayList<>(map.size());
list.addAll(map.entrySet());
list.sort((o1, o2) -> o1.getKey() - o2.getKey());
return list;
}
private static List<Map.Entry<Integer, int[]>> getSortedMapEntries1(Map<Integer, int[]> map) {
List<Map.Entry<Integer, int[]>> list = new ArrayList<>(map.size());
list.addAll(map.entrySet());
list.sort((o1, o2) -> o1.getKey() - o2.getKey());
return list;
}
private static List<Pair<Integer, List<Integer>>> getSortedMapEntries2(Map<Integer, Set<Integer>> map) {
List<Pair<Integer, List<Integer>>> list = new ArrayList<>(map.size());
for (Map.Entry<Integer, Set<Integer>> e : map.entrySet()) {
List<Integer> il = new ArrayList<>(2);
il.addAll(e.getValue());
il.sort((o1, o2) -> o1 - o2);
list.add(new Pair<>(e.getKey(), il));
}
list.sort((o1, o2) -> o1.getLeft() - o2.getLeft());
return list;
}
private static String getExpiredStringLabel(int questid) {
return (!limitedQuestids.contains(questid) ? "" : " EXPIRED");
}
private static void reportQuestItemData() {
// This will reference one line at a time
String line = null;
String fileName = null;
try {
System.out.println("Reading WZs...");
fileName = WZFiles.QUEST.getFilePath() + "/Check.img.xml";
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(fileName), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateCheckToken(line); // fetch expired quests through here as well
}
bufferedReader.close();
fileReader.close();
fileName = WZFiles.QUEST.getFilePath() + "/Act.img.xml";
fileReader = new InputStreamReader(new FileInputStream(fileName), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateActToken(line);
}
bufferedReader.close();
fileReader.close();
System.out.println("Calculating table diffs...");
calculateQuestItemDiff();
System.out.println("Filtering drops on DB...");
List<Pair<Integer, Integer>> itemsWithQuest = getPairsQuestItem();
filterQuestDropsOnDB(itemsWithQuest);
con.close();
System.out.println("Filtering drops on project files...");
// finally, filter whether this item is mentioned on the source code or not.
filterDirectorySearchMatchingData("scripts", itemsWithQuest);
filterDirectorySearchMatchingData("sql", itemsWithQuest);
filterDirectorySearchMatchingData("src", itemsWithQuest);
System.out.println("Reporting results...");
// report suspects of missing quest drop data, as well as those drop data that may have incorrect questids.
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
if (!mixedQuestidItems.isEmpty()) {
printWriter.println("INCORRECT QUESTIDS ON DB");
for (Map.Entry<Integer, int[]> emqi : getSortedMapEntries1(mixedQuestidItems)) {
int[] mqi = emqi.getValue();
printWriter.println(mqi[0] + " : " + mqi[1] + " -> " + mqi[2] + getExpiredStringLabel(mqi[2]));
}
printWriter.println("\n\n\n\n\n");
}
if (!itemsWithQuest.isEmpty()) {
Map<Integer, Integer> mapIwq = new HashMap<>(itemsWithQuest.size());
for (Pair<Integer, Integer> iwq : itemsWithQuest) {
mapIwq.put(iwq.getLeft(), iwq.getRight());
}
printWriter.println("ITEMS WITH NO QUEST DROP DATA ON DB");
for (Map.Entry<Integer, Integer> iwq : getSortedMapEntries0(mapIwq)) {
printWriter.println(iwq.getKey() + " - " + iwq.getValue() + getExpiredStringLabel(iwq.getValue()));
}
printWriter.println("\n\n\n\n\n");
}
if (DISPLAY_EXTRA_INFO) {
if (!zeroedStartQuestItems.isEmpty()) {
printWriter.println("START QUEST ITEMS WITH ZERO QUANTITY");
for (Pair<Integer, List<Integer>> iwq : getSortedMapEntries2(zeroedStartQuestItems)) {
printWriter.println(iwq.getLeft() + getExpiredStringLabel(iwq.getLeft()) + ":");
for (Integer i : iwq.getRight()) {
printWriter.println(" " + i);
}
printWriter.println();
}
printWriter.println("\n\n\n\n\n");
}
if (!zeroedCompleteQuestItems.isEmpty()) {
printWriter.println("COMPLETE QUEST ITEMS WITH ZERO QUANTITY");
for (Pair<Integer, List<Integer>> iwq : getSortedMapEntries2(zeroedCompleteQuestItems)) {
printWriter.println(iwq.getLeft() + getExpiredStringLabel(iwq.getLeft()) + ":");
for (Integer i : iwq.getRight()) {
printWriter.println(" " + i);
}
printWriter.println();
}
printWriter.println("\n\n\n\n\n");
}
}
printWriter.close();
System.out.println("Done!");
} catch (FileNotFoundException ex) {
System.out.println("Unable to open file '" + fileName + "'");
} catch (IOException ex) {
System.out.println("Error reading file '" + fileName + "'");
} catch (SQLException e) {
System.out.println("Warning: Could not establish connection to database to report quest data.");
System.out.println(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
DatabaseConnection.initializeConnectionPool(); // MapleItemInformationProvider loads some unrelated db data
ii = MapleItemInformationProvider.getInstance();
reportQuestItemData();
}
}

View File

@@ -0,0 +1,263 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application parses the Quest.wz file inputted and generates a report showing
* all cases where a quest takes a meso fee to complete a quest, but it doesn't
* properly checks the player for the needed amount before completing it.
* <p>
* Running it should generate a report file under "output" folder with the search results.
*/
public class QuestMesoFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("quest_meso_report.txt");
private static final boolean PRINT_FEES = true; // print missing values as additional info report
private static final int INITIAL_STRING_LENGTH = 50;
private static final Map<Integer, Integer> checkedMesoQuests = new HashMap<>();
private static final Map<Integer, Integer> appliedMesoQuests = new HashMap<>();
private static final Set<Integer> checkedEndscriptQuests = new HashSet<>();
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int questId = -1;
private static int isCompleteState = 0;
private static int currentMeso = 0;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void translateTokenAct(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
forwardCursor(status);
}
status += 1;
} else {
if (token.contains("money")) {
if (isCompleteState != 0) {
d = getValue(token);
currentMeso = -1 * Integer.parseInt(d);
if (currentMeso > 0) {
appliedMesoQuests.put(questId, currentMeso);
}
}
}
}
}
private static void translateTokenCheck(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
forwardCursor(status);
}
status += 1;
} else {
if (token.contains("endmeso")) {
d = getValue(token);
currentMeso = Integer.parseInt(d);
checkedMesoQuests.put(questId, currentMeso);
} else if (token.contains("endscript")) {
checkedEndscriptQuests.add(questId);
}
}
}
private static void readQuestMesoData() throws IOException {
String line;
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(WZFiles.QUEST.getFilePath() + "/Act.img.xml"), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateTokenAct(line);
}
bufferedReader.close();
fileReader.close();
fileReader = new InputStreamReader(new FileInputStream(WZFiles.QUEST.getFilePath() + "/Check.img.xml"), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateTokenCheck(line);
}
bufferedReader.close();
fileReader.close();
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleQuestMesoFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static void printReportFileResults(Map<Integer, Integer> target, Map<Integer, Integer> base, boolean testingCheck) {
List<Integer> result = new ArrayList<>();
List<Integer> error = new ArrayList<>();
Map<Integer, Integer> questFee = new HashMap<>();
for (Map.Entry<Integer, Integer> e : base.entrySet()) {
Integer v = target.get(e.getKey());
if (v == null) {
if (testingCheck || !checkedEndscriptQuests.contains(e.getKey())) {
result.add(e.getKey());
questFee.put(e.getKey(), e.getValue());
}
} else if (v.intValue() != e.getValue().intValue()) {
error.add(e.getKey());
}
}
if (!result.isEmpty() || !error.isEmpty()) {
printWriter.println("MISMATCH INFORMATION ON '" + (testingCheck ? "check" : "act") + "':");
if (!result.isEmpty()) {
result.sort((o1, o2) -> o1 - o2);
printWriter.println("# MISSING");
if (!PRINT_FEES) {
for (Integer i : result) {
printWriter.println(i);
}
} else {
for (Integer i : result) {
printWriter.println(i + " " + questFee.get(i));
}
}
printWriter.println();
}
if (!error.isEmpty() && testingCheck) {
error.sort((o1, o2) -> o1 - o2);
printWriter.println("# WRONG VALUE");
for (Integer i : error) {
printWriter.println(i);
}
printWriter.println();
}
printWriter.println("\r\n");
}
}
private static void reportQuestMesoData() {
// This will reference one line at a time
try {
System.out.println("Reading WZs...");
readQuestMesoData();
System.out.println("Reporting results...");
// report missing meso checks on quest completes
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
printReportFileResults(checkedMesoQuests, appliedMesoQuests, true);
printReportFileResults(appliedMesoQuests, checkedMesoQuests, false);
printWriter.close();
System.out.println("Done!");
} catch (FileNotFoundException ex) {
System.out.println("Unable to open quest file.");
} catch (IOException ex) {
System.out.println("Error reading quest file.");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
reportQuestMesoData();
}
}

View File

@@ -0,0 +1,362 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application parses the Quest.wz file inputted and generates a report showing
* all cases where quest script files have not been found for quests that requires a
* script file.
* As an extension, it highlights missing script files for questlines that hand over
* skills as rewards.
* <p>
* Running it should generate a report file under "output" folder with the search results.
*/
public class QuestlineFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("questline_report.txt");
private static final String ACT_NAME = WZFiles.QUEST.getFilePath() + "/Act.img.xml";
private static final String CHECK_NAME = WZFiles.QUEST.getFilePath() + "/Check.img.xml";
private static final int INITIAL_STRING_LENGTH = 50;
private static final Stack<Integer> skillObtainableQuests = new Stack<>();
private static final Set<Integer> scriptedQuestFiles = new HashSet<>();
private static final Set<Integer> expiredQuests = new HashSet<>();
private static final Map<Integer, List<Integer>> questDependencies = new HashMap<>();
private static final Set<Integer> nonScriptedQuests = new HashSet<>();
private static final Set<Integer> skillObtainableNonScriptedQuests = new HashSet<>();
private static PrintWriter printWriter = null;
private static InputStreamReader fileReader = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int questId = -1;
private static int isCompleteState = 0;
private static boolean isScriptedQuest;
private static boolean isExpiredQuest;
private static List<Integer> questDependencyList;
private static int curQuestId;
private static int curQuestState;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void translateTokenCheck(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
if (status == 1) {
evaluateCurrentQuest();
} else if (status == 4) {
evaluateCurrentQuestDependency();
}
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
isScriptedQuest = false;
isExpiredQuest = false;
questDependencyList = new LinkedList<>();
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
if (isCompleteState == 1 || !token.contains("quest")) {
forwardCursor(status);
}
}
status += 1;
} else {
if (status == 3) {
d = getName(token);
if (d.contains("script")) {
isScriptedQuest = true;
} else if (d.contains("end")) {
isExpiredQuest = true;
}
} else if (status == 5) {
readQuestLabel(token);
}
}
}
private static void translateTokenAct(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting QuestId
d = getName(token);
questId = Integer.parseInt(d);
} else if (status == 2) { //start/complete
d = getName(token);
isCompleteState = Integer.parseInt(d);
} else if (status == 3) {
if (isCompleteState == 1 && token.contains("skill")) {
skillObtainableQuests.add(questId);
}
forwardCursor(status);
}
status += 1;
}
}
private static void readQuestLabel(String token) {
String name = getName(token);
String value = getValue(token);
switch (name) {
case "id" -> curQuestId = Integer.parseInt(value);
case "state" -> curQuestState = Integer.parseInt(value);
}
}
private static void evaluateCurrentQuestDependency() {
if (curQuestState == 2) {
questDependencyList.add(curQuestId);
}
}
private static void evaluateCurrentQuest() {
if (isScriptedQuest && !scriptedQuestFiles.contains(questId)) {
nonScriptedQuests.add(questId);
}
if (isExpiredQuest) {
expiredQuests.add(questId);
}
questDependencies.put(questId, questDependencyList);
}
private static void instantiateQuestScriptFiles(String directoryName) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
String fname = file.getName();
try {
Integer questid = Integer.parseInt(fname.substring(0, fname.indexOf('.')));
scriptedQuestFiles.add(questid);
} catch (NumberFormatException nfe) {
}
}
}
}
private static void readQuestsWithMissingScripts() throws IOException {
String line;
fileReader = new InputStreamReader(new FileInputStream(CHECK_NAME), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateTokenCheck(line);
}
bufferedReader.close();
fileReader.close();
}
private static void readQuestsWithSkillReward() throws IOException {
String line;
fileReader = new InputStreamReader(new FileInputStream(ACT_NAME), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateTokenAct(line);
}
bufferedReader.close();
fileReader.close();
}
private static void calculateSkillRelatedMissingQuestScripts() {
Stack<Integer> frontierQuests = skillObtainableQuests;
Set<Integer> solvedQuests = new HashSet<>();
while (!frontierQuests.isEmpty()) {
Integer questid = frontierQuests.pop();
solvedQuests.add(questid);
if (nonScriptedQuests.contains(questid)) {
skillObtainableNonScriptedQuests.add(questid);
nonScriptedQuests.remove(questid);
}
List<Integer> questDependency = questDependencies.get(questid);
for (Integer i : questDependency) {
if (!solvedQuests.contains(i)) {
frontierQuests.add(i);
}
}
}
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleQuestlineFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static List<Integer> getSortedListEntries(Set<Integer> set) {
List<Integer> list = new ArrayList<>(set);
Collections.sort(list);
return list;
}
private static void printReportFileResults() {
if (!skillObtainableNonScriptedQuests.isEmpty()) {
printWriter.println("SKILL-RELATED NON-SCRIPTED QUESTS");
for (Integer nsq : getSortedListEntries(skillObtainableNonScriptedQuests)) {
printWriter.println(" " + nsq + (expiredQuests.contains(nsq) ? " EXPIRED" : ""));
}
printWriter.println();
}
printWriter.println("\nCOMMON NON-SCRIPTED QUESTS");
for (Integer nsq : getSortedListEntries(nonScriptedQuests)) {
printWriter.println(" " + nsq + (expiredQuests.contains(nsq) ? " EXPIRED" : ""));
}
}
private static void reportQuestlineData() {
// This will reference one line at a time
try {
System.out.println("Reading quest scripts...");
instantiateQuestScriptFiles(ToolConstants.SCRIPTS_PATH + "/quest");
System.out.println("Reading WZs...");
readQuestsWithSkillReward();
readQuestsWithMissingScripts();
System.out.println("Calculating skill related quests...");
calculateSkillRelatedMissingQuestScripts();
System.out.println("Reporting results...");
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
printReportFileResults();
printWriter.close();
System.out.println("Done!");
} catch (FileNotFoundException ex) {
System.out.println("Unable to open quest file.");
} catch (IOException ex) {
System.out.println("Error reading quest file.");
} catch (Exception e) {
e.printStackTrace();
}
}
/*
private static List<Pair<Integer, List<Integer>>> getSortedMapEntries(Map<Integer, List<Integer>> map) {
List<Pair<Integer, List<Integer>>> list = new ArrayList<>(map.size());
for(Map.Entry<Integer, List<Integer>> e : map.entrySet()) {
List<Integer> il = new ArrayList<>(2);
for(Integer i : e.getValue()) {
il.add(i);
}
Collections.sort(il, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
list.add(new Pair<>(e.getKey(), il));
}
Collections.sort(list, new Comparator<Pair<Integer, List<Integer>>>() {
@Override
public int compare(Pair<Integer, List<Integer>> o1, Pair<Integer, List<Integer>> o2) {
return o1.getLeft() - o2.getLeft();
}
});
return list;
}
private static void DumpQuestlineData() {
for(Pair<Integer, List<Integer>> questDependency : getSortedMapEntries(questDependencies)) {
if(!questDependency.right.isEmpty()) {
System.out.println(questDependency);
}
}
}
*/
public static void main(String[] args) {
reportQuestlineData();
}
}

View File

@@ -0,0 +1,112 @@
package tools.mapletools;
import java.io.File;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application reports in reactor ids that have drops on the SQL table but are
* not yet coded.
*/
public class ReactorDropFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("reactor_drop_report.txt");
private static final String REACTOR_SCRIPT_PATH = ToolConstants.SCRIPTS_PATH + "/reactor";
private static final Connection con = SimpleDatabaseConnection.getConnection();
private static PrintWriter printWriter = null;
private static final Set<Integer> reactors = new HashSet<>();
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleReactorDropFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the underlying DB and the server-side files.");
printWriter.println();
}
private static int getReactorIdFromFilename(String name) {
try {
return Integer.parseInt(name.substring(0, name.indexOf('.')));
} catch (Exception e) {
return -1;
}
}
private static void removeScriptedReactorids(String directoryName) {
File directory = new File(directoryName);
// get all the files from a directory
File[] fList = directory.listFiles();
for (File file : fList) {
if (file.isFile()) {
reactors.remove(getReactorIdFromFilename(file.getName()));
}
}
}
private static void loadReactoridsOnDB() throws SQLException {
PreparedStatement ps = con.prepareStatement("SELECT DISTINCT reactorid FROM reactordrops;");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
reactors.add(rs.getInt("reactorid"));
}
rs.close();
ps.close();
}
private static List<Integer> getSortedReactorids() {
List<Integer> sortedReactors = new ArrayList<>(reactors);
Collections.sort(sortedReactors);
return sortedReactors;
}
private static void fetchMissingReactorDrops() throws SQLException {
loadReactoridsOnDB();
removeScriptedReactorids(REACTOR_SCRIPT_PATH);
}
private static void reportMissingReactorDrops() throws SQLException {
if (!reactors.isEmpty()) {
printWriter.println("MISSING REACTOR DROP SCRIPTS");
for (Integer reactorid : getSortedReactorids()) {
printWriter.println(" " + reactorid);
}
printWriter.println("\n");
}
}
private static void reportMissingReactors() {
try {
System.out.println("Fetching reactors from DB...");
fetchMissingReactorDrops();
con.close();
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
// report suspects of missing quest drop data, as well as those drop data that may have incorrect questids.
System.out.println("Reporting results...");
printReportFileHeader();
reportMissingReactorDrops();
printWriter.close();
System.out.println("Done!");
} catch (SQLException e) {
System.out.println("Warning: Could not establish connection to database to report quest data.");
System.out.println(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
reportMissingReactors();
}
}

View File

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

View File

@@ -0,0 +1,336 @@
package tools.mapletools;
import provider.wz.WZFiles;
import server.MapleItemInformationProvider;
import tools.DatabaseConnection;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author RonanLana
* <p>
* The objective of this program is to uncover all maker data from the
* ItemMaker.wz.xml files and generate a SQL file with every data info
* for the Maker DB tables.
*/
public class SkillMakerFetcher {
private static final File INPUT_FILE = new File(WZFiles.ETC.getFile(), "ItemMake.img.xml");
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("maker-data.sql");
private static final int INITIAL_STRING_LENGTH = 50;
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static byte state = 0;
// maker data fields
private static int id = -1;
private static int itemid = -1;
private static int reqLevel = -1;
private static int reqMakerLevel = -1;
private static int reqItem = -1;
private static int reqMeso = -1;
private static int reqEquip = -1;
private static int catalyst = -1;
private static int quantity = -1;
private static int tuc = -1;
private static int recipePos = -1;
private static int recipeProb = -1;
private static int recipeCount = -1;
private static int recipeItem = -1;
static List<int[]> recipeList = null;
static List<int[]> randomList = null;
static List<MakerItemEntry> makerList = new ArrayList<>(100);
private static void resetMakerDataFields() {
reqLevel = 0;
reqMakerLevel = 0;
reqItem = 0;
reqMeso = 0;
reqEquip = 0;
catalyst = 0;
quantity = 0;
tuc = 0;
recipePos = 0;
recipeProb = 0;
recipeCount = 0;
recipeItem = 0;
recipeList = null;
randomList = null;
}
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
String s = d.trim();
s.replaceFirst("^0+(?!$)", "");
return (s);
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
String s = d.trim();
s.replaceFirst("^0+(?!$)", "");
return (s);
}
private static int[] generateRecipeItem() {
int[] pair = new int[2];
pair[0] = recipeItem;
pair[1] = recipeCount;
return pair;
}
private static int[] generateRandomItem() {
int[] tuple = new int[3];
tuple[0] = recipeItem;
tuple[1] = recipeCount;
tuple[2] = recipeProb;
return tuple;
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateToken(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
if (status == 2) { //close item maker data
generateUpdatedItemFee(); // for equipments, this will try to update reqMeso to be conformant with the client.
makerList.add(new MakerItemEntry(id, itemid, reqLevel, reqMakerLevel, reqItem, reqMeso, reqEquip, catalyst, quantity, tuc, recipeCount, recipeItem, recipeList, randomList));
resetMakerDataFields();
} else if (status == 4) { //close recipe/random item
if (state == 0) {
recipeList.add(generateRecipeItem());
} else if (state == 1) {
randomList.add(generateRandomItem());
}
}
} else if (token.contains("imgdir")) {
if (status == 1) { //getting id
d = getName(token);
id = Integer.parseInt(d);
System.out.println("Parsing maker id " + id);
} else if (status == 2) { //getting target item id
d = getName(token);
itemid = Integer.parseInt(d);
} else if (status == 3) {
d = getName(token);
switch (d) {
case "recipe" -> {
recipeList = new LinkedList<>();
state = 0;
}
case "randomReward" -> {
randomList = new LinkedList<>();
state = 1;
}
default -> forwardCursor(3); // unused content, read until end of block
}
} else if (status == 4) { // inside recipe/random
d = getName(token);
recipePos = Integer.parseInt(d);
}
status += 1;
} else {
if (status == 3) {
d = getName(token);
switch (d) {
case "itemNum" -> quantity = Integer.parseInt(getValue(token));
case "meso" -> reqMeso = Integer.parseInt(getValue(token));
case "reqItem" -> reqItem = Integer.parseInt(getValue(token));
case "reqLevel" -> reqLevel = Integer.parseInt(getValue(token));
case "reqSkillLevel" -> reqMakerLevel = Integer.parseInt(getValue(token));
case "tuc" -> tuc = Integer.parseInt(getValue(token));
case "catalyst" -> catalyst = Integer.parseInt(getValue(token));
case "reqEquip" -> reqEquip = Integer.parseInt(getValue(token));
default -> {
System.out.println("Unhandled case: '" + d + "'");
state = 2;
}
}
} else if (status == 5) { // inside recipe/random item
d = getName(token);
if (d.equals("item")) {
recipeItem = Integer.parseInt(getValue(token));
} else {
if (state == 0) {
recipeCount = Integer.parseInt(getValue(token));
} else {
if (d.equals("itemNum")) {
recipeCount = Integer.parseInt(getValue(token));
} else {
recipeProb = Integer.parseInt(getValue(token));
}
}
}
}
}
}
private static void generateUpdatedItemFee() {
MapleItemInformationProvider ii = MapleItemInformationProvider.getInstance();
float adjPrice = reqMeso;
if (itemid < 2000000) {
Map<String, Integer> stats = ii.getEquipStats(itemid);
if (stats != null) {
int val = itemid / 100000;
if (val == 13 || val == 14) { // is weapon-type
adjPrice /= 10;
adjPrice += reqMeso;
adjPrice /= 1000;
reqMeso = 1000 * (int) Math.floor(adjPrice);
} else {
adjPrice /= ((stats.get("reqLevel") >= 108) ? 10 : 11);
adjPrice += reqMeso;
adjPrice /= 1000;
reqMeso = 1000 * (int) Math.ceil(adjPrice);
}
} else {
System.out.println("null stats for itemid " + itemid);
}
} else {
adjPrice /= 10;
adjPrice += reqMeso;
adjPrice /= 1000;
reqMeso = 1000 * (int) Math.ceil(adjPrice);
}
}
private static void WriteMakerTableFile() {
printWriter.println(" # SQL File autogenerated from the MapleSkillMakerFetcher feature by Ronan Lana.");
printWriter.println(" # Generated data is conformant with the ItemMake.img.xml file used to compile this.");
printWriter.println();
StringBuilder sb_create = new StringBuilder("INSERT IGNORE INTO `makercreatedata` (`id`, `itemid`, `req_level`, `req_maker_level`, `req_meso`, `req_item`, `req_equip`, `catalyst`, `quantity`, `tuc`) VALUES\r\n");
StringBuilder sb_recipe = new StringBuilder("INSERT IGNORE INTO `makerrecipedata` (`itemid`, `req_item`, `count`) VALUES\r\n");
StringBuilder sb_reward = new StringBuilder("INSERT IGNORE INTO `makerrewarddata` (`itemid`, `rewardid`, `quantity`, `prob`) VALUES\r\n");
for (MakerItemEntry it : makerList) {
sb_create.append(" (" + it.id + ", " + it.itemid + ", " + it.reqLevel + ", " + it.reqMakerLevel + ", " + it.reqMeso + ", " + it.reqItem + ", " + it.reqEquip + ", " + it.catalyst + ", " + it.quantity + ", " + it.tuc + "),\r\n");
if (it.recipeList != null) {
for (int[] rit : it.recipeList) {
sb_recipe.append(" (" + it.itemid + ", " + rit[0] + ", " + rit[1] + "),\r\n");
}
}
if (it.randomList != null) {
for (int[] rit : it.randomList) {
sb_reward.append(" (" + it.itemid + ", " + rit[0] + ", " + rit[1] + ", " + rit[2] + "),\r\n");
}
}
}
sb_create.setLength(sb_create.length() - 3);
sb_create.append(";\r\n");
sb_recipe.setLength(sb_recipe.length() - 3);
sb_recipe.append(";\r\n");
sb_reward.setLength(sb_reward.length() - 3);
sb_reward.append(";");
printWriter.println(sb_create);
printWriter.println(sb_recipe);
printWriter.println(sb_reward);
}
private static void writeMakerTableData() {
// This will reference one line at a time
String line = null;
try {
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
resetMakerDataFields();
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
WriteMakerTableFile();
printWriter.close();
bufferedReader.close();
fileReader.close();
} catch (FileNotFoundException ex) {
System.out.println("Unable to open file '" + INPUT_FILE + "'");
} catch (IOException ex) {
System.out.println("Error reading file '" + INPUT_FILE + "'");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
DatabaseConnection.initializeConnectionPool(); // Using MapleItemInformationProvider which loads som unrelated things from the db
writeMakerTableData();
}
}

View File

@@ -0,0 +1,182 @@
package tools.mapletools;
import provider.wz.WZFiles;
import tools.Pair;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @author RonanLana
* <p>
* The main objective of this program is to index relevant reagent data
* from the Item.wz folder and generate a SQL table with them, to be used
* by the server source.
*/
public class SkillMakerReagentIndexer {
private static final File INPUT_FILE = new File(WZFiles.ITEM.getFile(), "Etc/0425.img.xml");
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("maker-reagent-data.sql");
private static final int INITIAL_STRING_LENGTH = 50;
private static final List<Pair<Integer, Pair<String, Integer>>> reagentList = new ArrayList<>();
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static byte status = 0;
private static int id = -1;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void translateToken(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
if (status == 1) { //getting id
d = getName(token);
id = Integer.parseInt(d);
System.out.println("Parsing maker reagent id " + id);
} else if (status == 2) {
d = getName(token);
if (!d.equals("info")) {
System.out.println("not info");
forwardCursor(status);
}
}
status += 1;
} else {
if (status == 3) {
if (token.contains("int")) {
d = getName(token);
if (d.contains("inc") || d.contains("rand")) {
Integer v = Integer.valueOf(getValue(token));
Pair<String, Integer> reagBuff = new Pair<>(d, v);
Pair<Integer, Pair<String, Integer>> reagItem = new Pair<>(id, reagBuff);
reagentList.add(reagItem);
}
} else {
if (token.contains("canvas")) {
forwardCursor(status + 1);
}
}
}
}
}
private static void SortReagentList() {
reagentList.sort((p1, p2) -> p1.getLeft().compareTo(p2.getLeft()));
}
private static void WriteMakerReagentTableFile() {
printWriter.println(" # SQL File autogenerated from the MapleSkillMakerReagentIndexer feature by Ronan Lana.");
printWriter.println(" # Generated data is conformant with the Item.wz folder used to compile this.");
printWriter.println();
printWriter.println("CREATE TABLE IF NOT EXISTS `makerreagentdata` (");
printWriter.println(" `itemid` int(11) NOT NULL,");
printWriter.println(" `stat` varchar(20) NOT NULL,");
printWriter.println(" `value` smallint(6) NOT NULL,");
printWriter.println(" PRIMARY KEY (`itemid`)");
printWriter.println(");");
printWriter.println();
StringBuilder sb = new StringBuilder("INSERT IGNORE INTO `makerreagentdata` (`itemid`, `stat`, `value`) VALUES\r\n");
for (Pair<Integer, Pair<String, Integer>> it : reagentList) {
sb.append(" (" + it.left + ", \"" + it.right.left + "\", " + it.right.right + "),\r\n");
}
sb.setLength(sb.length() - 3);
sb.append(";");
printWriter.println(sb);
}
private static void writeMakerReagentTableData() {
// This will reference one line at a time
String line = null;
try {
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
bufferedReader.close();
fileReader.close();
SortReagentList();
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
WriteMakerReagentTableFile();
printWriter.close();
} catch (FileNotFoundException ex) {
System.out.println("Unable to open file '" + OUTPUT_FILE + "'");
} catch (IOException ex) {
System.out.println("Error reading file '" + OUTPUT_FILE + "'");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
writeMakerReagentTableData();
}
}

View File

@@ -0,0 +1,128 @@
package tools.mapletools;
import server.life.MapleMonsterStats;
import tools.Pair;
import java.io.File;
import java.io.IOException;
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;
/**
* @author RonanLana
* <p>
* This application traces missing meso drop data 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.
* <p>
* 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.
*/
public class SkillbookChanceFetcher {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("skillbook_drop_data.sql");
private static final Map<Pair<Integer, Integer>, Integer> skillbookChances = new HashMap<>();
private static PrintWriter printWriter;
private static Map<Integer, MapleMonsterStats> mobStats;
private static List<Map.Entry<Pair<Integer, Integer>, Integer>> sortedSkillbookChances() {
List<Map.Entry<Pair<Integer, Integer>, Integer>> skillbookChancesList = new ArrayList<>(skillbookChances.entrySet());
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);
}
return (o1.getKey().getLeft() < o2.getKey().getLeft()) ? -1 : 1;
});
return skillbookChancesList;
}
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
}
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() {
try {
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printSkillbookChanceUpdateSqlHeader();
List<Map.Entry<Pair<Integer, Integer>, Integer>> skillbookChancesList = sortedSkillbookChances();
for (Map.Entry<Pair<Integer, Integer>, Integer> e : skillbookChancesList) {
printWriter.println("(" + e.getKey().getLeft() + ", " + e.getKey().getRight() + ", 1, 1, 0, " + e.getValue() + "),");
}
printWriter.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
public static void main(String[] args) {
// load mob stats from WZ
mobStats = MonsterStatFetcher.getAllMonsterStats();
fetchSkillbookDropChances();
generateSkillbookChanceUpdateFile();
}
}

View File

@@ -0,0 +1,157 @@
package tools.mapletools;
import provider.wz.WZFiles;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @author RonanLana
* <p>
* This application parses skillbook XMLs, filling up stack amount of those
* items to 100 (eliminating limitations on held skillbooks, now using
* default stack quantity expected from USE items).
* <p>
* Estimated parse time: 10 seconds
*/
public class SkillbookStackUpdate {
private static final File INPUT_DIRECTORY = new File(WZFiles.ITEM.getFile(), "Consume");
private static final File OUTPUT_DIRECTORY = ToolConstants.getOutputFile("skillbook-update");
private static final int INITIAL_STRING_LENGTH = 50;
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static int status = 0;
private static boolean isSkillMasteryBook(int itemid) {
return itemid >= 2280000 && itemid < 2300000;
}
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
if (j < i) {
return "0"; //node value containing 'name' in it's scope, cheap fix since we don't deal with strings anyway
}
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
printWriter.println(token);
}
private static void translateItemToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
if (status == 2) { //itemid
int itemid = Integer.parseInt(getName(token));
if (!isSkillMasteryBook(itemid)) {
printWriter.println(token);
forwardCursor(status);
return;
}
}
} else {
if (status == 3) {
if (getName(token).contentEquals("slotMax")) {
printWriter.println(" <int name=\"slotMax\" value=\"100\"/>");
return;
}
}
}
printWriter.println(token);
}
private static void parseItemFile(File file, File outputFile) {
setupDirectories(outputFile);
// This will reference one line at a time
String line = null;
try {
printWriter = new PrintWriter(outputFile);
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
translateItemToken(line);
}
bufferedReader.close();
fileReader.close();
printWriter.close();
} catch (IOException ex) {
System.out.println("Error reading file '" + file.getName() + "'");
ex.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void setupDirectories(File file) {
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
}
private static void parseItemDirectory(File inputDirectory, File outputDirectory) {
for (File f : inputDirectory.listFiles()) {
parseItemFile(f, new File(outputDirectory, f.getName()));
}
}
public static void main(String[] args) {
System.out.println("Reading item files...");
parseItemDirectory(INPUT_DIRECTORY, OUTPUT_DIRECTORY);
System.out.println("Done!");
}
}

View File

@@ -0,0 +1,18 @@
package tools.mapletools;
import java.io.File;
class ToolConstants {
static final File INPUT_DIRECTORY = new File("tools/input");
static final File OUTPUT_DIRECTORY = new File("tools/output");
static final String SCRIPTS_PATH = "scripts";
static final String HANDBOOK_PATH = "handbook";
static File getInputFile(String fileName) {
return new File(INPUT_DIRECTORY, fileName);
}
static File getOutputFile(String fileName) {
return new File(OUTPUT_DIRECTORY, fileName);
}
}

View File

@@ -0,0 +1,256 @@
package tools.mapletools;
import provider.wz.WZFiles;
import tools.Pair;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author RonanLana
* <p>
* This application parses the Map.wz file inputted and reports areas (mapids) that are supposed to be referenced
* throughout the map tree (area map -> continent map -> world map) but are currently missing.
*/
public class WorldmapChecker {
private static final File OUTPUT_FILE = ToolConstants.getOutputFile("worldmap_report.txt");
private static final int INITIAL_STRING_LENGTH = 50;
private static final Map<String, Set<Integer>> worldMapids = new HashMap<>();
private static final Map<String, String> parentWorldmaps = new HashMap<>();
private static final Set<String> rootWorldmaps = new HashSet<>();
private static PrintWriter printWriter = null;
private static BufferedReader bufferedReader = null;
private static Set<Integer> currentWorldMapids;
private static String currentParent;
private static byte status = 0;
private static boolean isInfo;
private static String getName(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("name");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static String getValue(String token) {
int i, j;
char[] dest;
String d;
i = token.lastIndexOf("value");
i = token.indexOf("\"", i) + 1; //lower bound of the string
j = token.indexOf("\"", i); //upper bound
dest = new char[INITIAL_STRING_LENGTH];
token.getChars(i, j, dest, 0);
d = new String(dest);
return (d.trim());
}
private static void forwardCursor(int st) {
String line = null;
try {
while (status >= st && (line = bufferedReader.readLine()) != null) {
simpleToken(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void simpleToken(String token) {
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
}
}
private static void translateToken(String token) {
String d;
if (token.contains("/imgdir")) {
status -= 1;
} else if (token.contains("imgdir")) {
status += 1;
if (status == 2) {
d = getName(token);
switch (d) {
case "MapList" -> isInfo = false;
case "info" -> isInfo = true;
default -> forwardCursor(status);
}
} else if (status == 4) {
d = getName(token);
if (!d.contentEquals("mapNo")) {
forwardCursor(status);
}
}
} else {
if (status == 4) {
currentWorldMapids.add(Integer.valueOf(getValue(token)));
} else if (status == 2 && isInfo) {
try {
d = getName(token);
if (d.contentEquals("parentMap")) {
currentParent = (getValue(token) + ".img.xml");
} else {
forwardCursor(status);
}
} catch (Exception e) {
System.out.println("failed '" + token + "'");
}
}
}
}
private static void parseWorldmapFile(File worldmapFile) throws IOException {
String line;
InputStreamReader fileReader = new InputStreamReader(new FileInputStream(worldmapFile), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(fileReader);
currentParent = "";
status = 0;
currentWorldMapids = new HashSet<>();
while ((line = bufferedReader.readLine()) != null) {
translateToken(line);
}
String worldmapName = worldmapFile.getName();
worldMapids.put(worldmapName, currentWorldMapids);
if (!currentParent.isEmpty()) {
parentWorldmaps.put(worldmapName, currentParent);
} else {
rootWorldmaps.add(worldmapName);
}
bufferedReader.close();
fileReader.close();
}
private static void parseWorldmapDirectory() {
File folder = new File(WZFiles.MAP.getFilePath(), "WorldMap");
System.out.println("Parsing directory '" + folder.getPath() + "'");
for (File file : folder.listFiles()) {
if (file.isFile()) {
try {
parseWorldmapFile(file);
} catch (FileNotFoundException ex) {
System.out.println("Unable to open worldmap file " + file.getAbsolutePath() + ".");
} catch (IOException ex) {
System.out.println("Error reading worldmap file " + file.getAbsolutePath() + ".");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static void printReportFileHeader() {
printWriter.println(" # Report File autogenerated from the MapleWorldmapChecker feature by Ronan Lana.");
printWriter.println(" # Generated data takes into account several data info from the server-side WZ.xmls.");
printWriter.println();
}
private static void printReportFileResults(List<Pair<String, List<Pair<Integer, String>>>> results) {
printWriter.println("Missing mapid references in top hierarchy:\n");
for (Pair<String, List<Pair<Integer, String>>> res : results) {
printWriter.println("'" + res.getLeft() + "':");
for (Pair<Integer, String> i : res.getRight()) {
printWriter.println(" " + i);
}
printWriter.println("\n");
}
}
private static void verifyWorldmapTreeMapids() {
try {
printWriter = new PrintWriter(OUTPUT_FILE, StandardCharsets.UTF_8);
printReportFileHeader();
if (rootWorldmaps.size() > 1) {
printWriter.println("[WARNING] Detected several root worldmaps: " + rootWorldmaps + "\n");
}
Set<String> worldmaps = new HashSet<>(parentWorldmaps.keySet());
worldmaps.addAll(rootWorldmaps);
Map<String, Set<Integer>> tempMapids = new HashMap<>(worldMapids.size());
for (Map.Entry<String, Set<Integer>> e : worldMapids.entrySet()) {
tempMapids.put(e.getKey(), new HashSet<>(e.getValue()));
}
Map<String, List<Pair<Integer, String>>> unreferencedMapids = new HashMap<>();
for (String s : worldmaps) {
List<Pair<Integer, String>> currentUnreferencedMapids = new ArrayList<>();
for (Integer i : tempMapids.get(s)) {
String parent = parentWorldmaps.get(s);
while (parent != null) {
Set<Integer> mapids = worldMapids.get(parent);
if (!mapids.contains(i)) {
currentUnreferencedMapids.add(new Pair<>(i, parent));
break;
} else {
tempMapids.get(parent).remove(i);
}
parent = parentWorldmaps.get(parent);
}
}
if (!currentUnreferencedMapids.isEmpty()) {
unreferencedMapids.put(s, currentUnreferencedMapids);
}
}
if (!unreferencedMapids.isEmpty()) {
List<Pair<String, List<Pair<Integer, String>>>> unreferencedEntries = new ArrayList<>(20);
for (Map.Entry<String, List<Pair<Integer, String>>> e : unreferencedMapids.entrySet()) {
List<Pair<Integer, String>> list = new ArrayList<>(e.getValue());
list.sort((o1, o2) -> o1.getLeft().compareTo(o2.getLeft()));
unreferencedEntries.add(new Pair<>(e.getKey(), list));
}
unreferencedEntries.sort((o1, o2) -> o1.getLeft().compareTo(o2.getLeft()));
printReportFileResults(unreferencedEntries);
}
printWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
parseWorldmapDirectory();
verifyWorldmapTreeMapids();
}
}