/* This file is part of the HeavenMS MapleStory Server Copyleft (L) 2016 - 2019 RonanLana This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation version 3 as published by the Free Software Foundation. You may not use, modify or distribute this program under any other version of the GNU Affero General Public License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package maplecashcosmeticschecker; import java.io.*; import java.util.*; import java.util.Map.Entry; /** * * @author RonanLana 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. Note: to best make use of this feature, set ignoreCurrentScriptCosmetics = true. This way, every available cosmetic present on the recipes will be listed on the report. Estimated parse time: 1 minute */ public class MapleCashCosmeticsChecker { static String libPath = "lib"; static String handbookPath = "../../handbook"; static String wzPath = "../../wz"; static String scriptPath = "../../scripts"; static PrintWriter printWriter = null; static InputStreamReader fileReader = null; static BufferedReader bufferedReader = null; static boolean ignoreCurrentScriptCosmetics = false; static int initialStringLength = 50; static byte status = 0; static Map> scriptCosmetics = new HashMap<>(); static Map scriptEntries = new HashMap<>(500); static Set allCosmetics = new HashSet<>(); static Set unusedCosmetics = new HashSet<>(); static Map> usedCosmetics = new HashMap<>(); static Map couponNames = new HashMap<>(); static Map cosmeticNpcs = new HashMap<>(); // expected only 1 NPC per cosmetic coupon (town care/salon) static Map, Integer> cosmeticNpcids = new HashMap<>(); static Set missingCosmeticNames = new HashSet<>(); static Map cosmeticNameIds = new HashMap<>(); static Map cosmeticIdNames = new HashMap<>(); static Map, Set> missingCosmeticsNpcTypes = new HashMap<>(); 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[initialStringLength]; 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[initialStringLength]; 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.valueOf(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), "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(wzPath + "/String.wz/Eqp.img.xml"); } private static void setCosmeticUsage(List usedByNpcids, int cosmeticid) { if (!usedByNpcids.isEmpty()) { usedCosmetics.put(cosmeticid, usedByNpcids); } else { unusedCosmetics.add(cosmeticid); } } private static void listFiles(String directoryName, ArrayList 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.valueOf(name.substring(0, name.indexOf('.'))); } catch(Exception e) { return -1; } } private static List findCosmeticDataNpcids(int itemid) { List npcids = new LinkedList<>(); for (Entry> sc : scriptCosmetics.entrySet()) { if (sc.getValue().contains(itemid)) { npcids.add(itemid); } } return npcids; } private static void loadScripts() throws IOException { ArrayList files = new ArrayList<>(); listFiles(scriptPath + "/npc", files); for(File f : files) { Integer npcid = getNpcIdFromFilename(f.getName()); //System.out.println("Parsing " + f.getAbsolutePath()); fileReader = new InputStreamReader(new FileInputStream(f), "UTF-8"); bufferedReader = new BufferedReader(fileReader); String line; StringBuilder stringBuffer = new StringBuilder(); boolean cosmeticNpc = false; Set 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.valueOf(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.valueOf(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 (ignoreCurrentScriptCosmetics) { for (Set npcCosmetics : scriptCosmetics.values()) { npcCosmetics.clear(); } } for (Integer itemid : allCosmetics) { List npcids = findCosmeticDataNpcids(itemid); setCosmeticUsage(npcids, itemid); } } private static List loadCosmeticCouponids() throws IOException { List couponItemids = new LinkedList<>(); fileReader = new InputStreamReader(new FileInputStream(handbookPath + "/Cash.txt"), "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.valueOf(s[0]); if (itemid >= 5150000 && itemid < 5160000) { couponItemids.add(itemid); couponNames.put(itemid, s[1]); } } bufferedReader.close(); fileReader.close(); return couponItemids; } private static List findItemidOnScript(int itemid) { List files = new LinkedList<>(); String t = String.valueOf(itemid); for (Entry 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 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 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 getCosmeticCouponData(String town, String type, String subtype) { List ret = new ArrayList<>(3); ret.add(town); ret.add(type); ret.add(subtype); return ret; } private static List 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 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 (Entry e : couponNames.entrySet()) { Integer npcid = cosmeticNpcs.get(e.getKey()); if (npcid == null) continue; String couponName = e.getValue(); List 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) { String ret = name + " (" + (gender ? "F" : "M") + ")"; return ret; } private static void loadCosmeticNames(String cosmeticPath) throws IOException { fileReader = new InputStreamReader(new FileInputStream(cosmeticPath), "UTF-8"); bufferedReader = new BufferedReader(fileReader); String line; while((line = bufferedReader.readLine()) != null) { String[] s = line.split(" - ", 3); int itemid = Integer.valueOf(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(handbookPath + "/Equip/Face.txt"); loadCosmeticNames(handbookPath + "/Equip/Hair.txt"); } private static List fetchExpectedCosmetics(String[] cosmeticList, boolean gender) { List 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), "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 cosmeticItemids = fetchExpectedCosmetics(cosmetics, gender); Set npcCosmetics = scriptCosmetics.get(npcid); Set missingCosmetics = new HashSet<>(); for (Integer itemid : cosmeticItemids) { if (!npcCosmetics.contains(itemid)) { missingCosmetics.add(itemid); } } if (!missingCosmetics.isEmpty()) { Pair key = new Pair<>(npcid, typeCoupon); Set 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 cosmeticRecipes = new ArrayList<>(); listFiles(libPath + "/care", cosmeticRecipes); for (File f : cosmeticRecipes) { verifyCosmeticExpectedFile(f); } } private static List, List>> getSortedMapEntries(Map, Set> map) { List, List>> list = new ArrayList<>(map.size()); for(Entry, Set> e : map.entrySet()) { List il = new ArrayList<>(2); il.addAll(e.getValue()); Collections.sort(il, (o1, o2) -> o1 - o2); list.add(new Pair<>(e.getKey(), il)); } Collections.sort(list, (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> getCosmeticReport(List itemids) { List maleItemids = new LinkedList<>(); List 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 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("lib/result.txt", "UTF-8"); printReportFileHeader(); if (!missingCosmeticsNpcTypes.isEmpty()) { printWriter.println("Found " + missingCosmeticsNpcTypes.size() + " entries with missing cosmetic entries."); for (Pair, List> mcn : getSortedMapEntries(missingCosmeticsNpcTypes)) { printWriter.println(" NPC " + mcn.getLeft()); Pair, List> genderItemids = getCosmeticReport(mcn.getRight()); reportNpcCosmetics(genderItemids.getLeft()); reportNpcCosmetics(genderItemids.getRight()); printWriter.println(); } } if (!unusedCosmetics.isEmpty()) { printWriter.println("Unused cosmetics: " + unusedCosmetics.size()); List 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 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(); } } }