From 6a63f9d95e68f54a30144acb2e99924dc3f2ce24 Mon Sep 17 00:00:00 2001 From: ronancpl Date: Sat, 14 Apr 2018 13:38:14 -0300 Subject: [PATCH] Quest & Command tweak + MapleCashDropFetcher + Cash drop tidyup Solved a possible exploit on starting/completing non-scripted quests. Added missing drop data for Aran's puppeteer questline. Moved GM tier level of some commands. Applied proper synchronization for BuddyList modules. Issued commands now requires "@" heading for normal players and donators (GM level < 2) and "!" for Jr. GM and above (GM level >= 2). Added custom feature: a message will be sent to acquaintances of a player (friends, family, guild, spouse) when they change/upgrade jobs. Removed cash drop entries from the DB. New tool: MapleCashDropFetcher. Reports on a text file all cash-type drop data on DB. --- .gitignore | 4 + README.md | 10 +- docs/feature_list.md | 6 + docs/mychanges_ptbr.txt | 9 +- scripts/npc/PupeteerPassword.js | 9 +- scripts/npc/commands.js | 16 +- scripts/quest/21728.js | 49 + scripts/quest/21729.js | 44 + scripts/quest/2214.js | 9 + scripts/quest/2215.js | 9 + sql/db_database.sql | 3 - sql/db_drops.sql | 8 +- src/client/BuddyList.java | 58 +- src/client/MapleCharacter.java | 35 +- src/client/command/Commands.java | 292 ++-- src/constants/GameConstants.java | 61 +- src/constants/ServerConstants.java | 6 +- .../channel/handlers/GeneralChatHandler.java | 10 +- .../channel/handlers/QuestActionHandler.java | 18 +- src/scripting/event/EventManager.java | 16 +- src/scripting/quest/QuestScriptManager.java | 3 +- src/server/life/MapleMonster.java | 3 +- tools/MapleCashDropFetcher/build.xml | 73 + .../lib/CashDropReport.txt | 11 + tools/MapleCashDropFetcher/manifest.mf | 3 + .../nbproject/build-impl.xml | 1448 +++++++++++++++++ .../nbproject/genfiles.properties | 8 + .../nbproject/project.properties | 77 + .../nbproject/project.xml | 16 + .../MapleCashDropFetcher.java | 406 +++++ .../src/tools/DatabaseConnection.java | 51 + .../MapleCashDropFetcher/src/tools/Pair.java | 121 ++ .../nbproject/private/private.xml | 4 +- .../dist/MapleQuestItemFetcher.jar | Bin 143574 -> 145072 bytes .../MapleQuestItemFetcher/lib/QuestReport.txt | 453 ++---- .../MapleQuestItemFetcher.java | 165 +- wz/Quest.wz/Check.img.xml | 7 +- wz/String.wz/Consume.img.xml | 1008 ++++++------ 38 files changed, 3472 insertions(+), 1057 deletions(-) create mode 100644 scripts/quest/21728.js create mode 100644 scripts/quest/21729.js create mode 100644 tools/MapleCashDropFetcher/build.xml create mode 100644 tools/MapleCashDropFetcher/lib/CashDropReport.txt create mode 100644 tools/MapleCashDropFetcher/manifest.mf create mode 100644 tools/MapleCashDropFetcher/nbproject/build-impl.xml create mode 100644 tools/MapleCashDropFetcher/nbproject/genfiles.properties create mode 100644 tools/MapleCashDropFetcher/nbproject/project.properties create mode 100644 tools/MapleCashDropFetcher/nbproject/project.xml create mode 100644 tools/MapleCashDropFetcher/src/maplecashdropfetcher/MapleCashDropFetcher.java create mode 100644 tools/MapleCashDropFetcher/src/tools/DatabaseConnection.java create mode 100644 tools/MapleCashDropFetcher/src/tools/Pair.java diff --git a/.gitignore b/.gitignore index c477dc6a51..232f549d1e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ /tools/MapleBossHpBarFetcher/build/ /tools/MapleBossHpBarFetcher/dist/ +/tools/MapleCashDropFetcher/nbproject/private/ +/tools/MapleCashDropFetcher/build/ +/tools/MapleCashDropFetcher/dist/ + /tools/MapleCouponInstaller/build/ /tools/MapleCouponInstaller/dist/ /tools/MapleCouponInstaller/nbproject/private/ diff --git a/README.md b/README.md index 2dbaad7385..7e3b4d68ad 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,15 @@ Server files: https://github.com/ronancpl/HeavenMS Client files & general tools: https://drive.google.com/drive/folders/0BzDsHSr-0V4MYVJ0TWIxd05hYUk -Recommended localhost: https://hostr.co/r5QDmhlxpp8M +Recommended localhost: https://hostr.co/fuzm4X9j7TWh -* MapleSilver's starting on window-mode, with some string fixes. This is a variation of Fraysa's https://hostr.co/gJbLZITRVHmv +* Current revision: 'n' problem fixed and removed caps for WATK, WDEF, MDEF, ACC, AVOID. + + * 'n' problem fixed https://hostr.co/r5QDmhlxpp8M + + * Fraysa's https://hostr.co/gJbLZITRVHmv + + * MapleSilver's starting on window-mode --- ### Support us diff --git a/docs/feature_list.md b/docs/feature_list.md index 749dd01f3c..39984d3ac4 100644 --- a/docs/feature_list.md +++ b/docs/feature_list.md @@ -118,6 +118,7 @@ External tools: * MapleArrowFetcher - Updates min/max quantity dropped on all arrows drop data, calculations based on mob level and whether it's a boss or not. * MapleBossHpBarFetcher - Searches the quest WZ files and reports in all relevant data regarding mobs that has a boss HP bar whilst not having a proper "boss" label. +* MapleCashDropFetcher - Searches the DB for any CASH drop data entry and lists them on a report file. * MapleCouponInstaller - Retrieves coupon info from the WZ and makes a SQL table with it. The server will use that table to gather info regarding rates and intervals. * MapleIdRetriever - Two behaviors: generates a SQL table with relation (id, name) of the handbook given as input. Given a file with names, outputs a file with ids. * MapleInvalidItemIdFetcher - Generates a file listing all inexistent itemid's currently laying on the DB. @@ -140,4 +141,9 @@ Project: * Heavily reviewed future task management inside the project. Way less trivial schedules are spawned now, relieving task overload on the TimerManager. * ThreadTracker: embedded auditing tool for run-time deadlock scanning throughout the server source (relies heavily on memory usage, designed only for debugging purposes). +Localhost: + +* Removed the 'n' problem within NPC dialog. +* Removed caps for MATK, WDEF, MDEF, ACC and AVOID. + --------------------------- \ No newline at end of file diff --git a/docs/mychanges_ptbr.txt b/docs/mychanges_ptbr.txt index 22dc2cf5fe..82be832739 100644 --- a/docs/mychanges_ptbr.txt +++ b/docs/mychanges_ptbr.txt @@ -859,4 +859,11 @@ Corrigido NPC de guild tirando mesos do jogador sem efetuar a ação alguma caso 11 - 12 Março 2018, Localhost melhorado: retirado caps de Matk, Mdef, Wdef, Acc e Avoid. Balanceado Ninja Ambush, agora dando uma quantidade de dano justificável. -Implementado questline do Dyle. \ No newline at end of file +Implementado questline do Dyle. +Corrigido possível exploit com sistema de quests, onde jogador podia começar e completar quaisquer quests livremente. +Nova ferramenta: MapleCashDropFetcher. Aplicação busca por drop data de cash na DB e reporta. + +13 Merço 2018, +Adicionado feature de anúncio de mudança de classe. +Adicionado drops faltando da questline Puppeteer de Aran. +Movimentação de GM rank para alguns comandos. \ No newline at end of file diff --git a/scripts/npc/PupeteerPassword.js b/scripts/npc/PupeteerPassword.js index 78e414d5d5..462f8157fb 100644 --- a/scripts/npc/PupeteerPassword.js +++ b/scripts/npc/PupeteerPassword.js @@ -18,10 +18,17 @@ function action(mode, type, selection){ if(status == 0){ + if(cm.isQuestStarted(21728)) { + cm.sendOk("You search for any hints of the Puppeteer, but it seems a powerful force blocks the path... Better return to #b#p1061019##k."); + cm.setQuestProgress(21728, 0, 1); + cm.dispose(); + return; + } + cm.sendGetText("A suspicious voice pierces through the silence. #bPassword#k!"); } else if(status == 1){ - if(cm.getText() == "Francis is a genius Puppeteer!"){ + if(cm.getText() == "Francis is a genius Puppeteer!"){ if(cm.isQuestStarted(20730) && cm.getQuestProgress(20730, 9300285) == 0) cm.warp(910510001, 1); else if(cm.isQuestStarted(21731) && cm.getQuestProgress(21731, 9300346) == 0) diff --git a/scripts/npc/commands.js b/scripts/npc/commands.js index 20f6e5bb40..a355847ffb 100644 --- a/scripts/npc/commands.js +++ b/scripts/npc/commands.js @@ -152,6 +152,7 @@ function writeSolaxiaCommandsLv2() { //JrGM comm_cursor = comm_lv2; desc_cursor = desc_lv2; + addCommand("whereami", ""); addCommand("hide", ""); addCommand("unhide", ""); addCommand("sp", ""); @@ -188,10 +189,13 @@ function writeSolaxiaCommandsLv1() { //Donator comm_cursor = comm_lv1; desc_cursor = desc_lv1; + addCommand("bosshp", ""); + addCommand("mobhp", ""); + addCommand("whatdropsfrom", ""); + addCommand("whodrops", ""); addCommand("buffme", ""); addCommand("goto", ""); addCommand("recharge", ""); - addCommand("whereami", ""); } function writeSolaxiaCommandsLv0() { //Common @@ -204,21 +208,21 @@ function writeSolaxiaCommandsLv0() { //Common addCommand("credits", ""); addCommand("uptime", ""); addCommand("gacha", ""); - addCommand("whatdropsfrom", ""); - addCommand("whodrops", ""); addCommand("dispose", ""); addCommand("equiplv", ""); addCommand("showrates", ""); addCommand("rates", ""); addCommand("online", ""); addCommand("gm", ""); - addCommand("bug", ""); + addCommand("reportbug", ""); //addCommand("points", ""); addCommand("joinevent", ""); addCommand("leaveevent", ""); - addCommand("bosshp", ""); - addCommand("mobhp", ""); addCommand("ranks", ""); + addCommand("str", ""); + addCommand("int", ""); + addCommand("luk", ""); + addCommand("dex", ""); } function writeSolaxiaCommands() { diff --git a/scripts/quest/21728.js b/scripts/quest/21728.js new file mode 100644 index 0000000000..88bb7ba1fe --- /dev/null +++ b/scripts/quest/21728.js @@ -0,0 +1,49 @@ +/* + This file is part of the HeavenMS (MapleSolaxiaV2) MapleStory Server + Copyleft (L) 2017 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 . +*/ + +var status = -1; + +function end(mode, type, selection) { + if (mode == -1) { + qm.dispose(); + } else { + if(mode == 0 && type > 0) { + qm.dispose(); + return; + } + + if (mode == 1) + status++; + else + status--; + + if (status == 0) { + if(qm.getQuestProgress(21728, 0) == 0) { + qm.sendNext("You haven't found the #rPuppeteer's cave#k yet, did you?"); + } else { + qm.sendNext("Hm, so the entrance is blocked by a powerful force? I see, gimme a time to think now..."); + qm.gainExp(200 * qm.getPlayer().getExpRate()); + qm.forceCompleteQuest(); + } + + qm.dispose(); + } + } +} \ No newline at end of file diff --git a/scripts/quest/21729.js b/scripts/quest/21729.js new file mode 100644 index 0000000000..f56e2b7172 --- /dev/null +++ b/scripts/quest/21729.js @@ -0,0 +1,44 @@ +/* + This file is part of the HeavenMS (MapleSolaxiaV2) MapleStory Server + Copyleft (L) 2017 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 . +*/ + +var status = -1; + +function start(mode, type, selection) { + if (mode == -1) { + qm.dispose(); + } else { + if(mode == 0 && type > 0) { + qm.dispose(); + return; + } + + if (mode == 1) + status++; + else + status--; + + if (status == 0) { + qm.sendNext("Okay, you should not return to #bTru#k for further details on your next steps. ... Oh wait!! I remembered something. See the #rMysterious Statue#k over there? That statue has it's origins unknwown, and there's something scribbled onto it that resembles something big, it probably is the password for the cave? #rGet the password there#k, it may help you on your journey."); + qm.forceStartQuest(); + + qm.dispose(); + } + } +} diff --git a/scripts/quest/2214.js b/scripts/quest/2214.js index b1f2f33075..10ade811f9 100644 --- a/scripts/quest/2214.js +++ b/scripts/quest/2214.js @@ -25,6 +25,8 @@ Quest ID: 2214 */ +importPackage(java.util); + var status = -1; function end(mode, type, selection) { @@ -42,6 +44,13 @@ function end(mode, type, selection) { status--; if (status == 0) { + var hourDay = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + if(!(hourDay >= 17 && hourDay < 20)) { + qm.sendNext("(Hmm, I'm searching the trash can but can't find the #t4031894# JM was talking about, maybe it's not time yet...)"); + qm.dispose(); + return; + } + if(!qm.canHold(4031894, 1)) { qm.sendNext("(Eh, I can't hold the #t4031894# right now, I need an ETC slot available.)"); qm.dispose(); diff --git a/scripts/quest/2215.js b/scripts/quest/2215.js index edb50c6a13..bea586d10e 100644 --- a/scripts/quest/2215.js +++ b/scripts/quest/2215.js @@ -25,6 +25,8 @@ Quest ID: 2215 */ +importPackage(java.util); + var status = -1; function end(mode, type, selection) { @@ -42,6 +44,13 @@ function end(mode, type, selection) { status--; if (status == 0) { + var hourDay = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + if(!(hourDay >= 17 && hourDay < 20)) { + qm.sendNext("(Hmm, I'm searching the trash can but can't find the #t4031894# JM was talking about, maybe it's not time yet...)"); + qm.dispose(); + return; + } + if(qm.getMeso() < 2000) { qm.sendNext("(Oh, I don't have the combined fee amount yet.)"); qm.dispose(); diff --git a/sql/db_database.sql b/sql/db_database.sql index ddfc68660b..b4775a491b 100644 --- a/sql/db_database.sql +++ b/sql/db_database.sql @@ -10830,9 +10830,6 @@ INSERT IGNORE INTO `temp_data` (`id`, `dropperid`, `itemid`, `minimum_quantity`, (10608, 9302010, 2022524, 1, 1, 0, 100000), (10609, 9400256, 4032192, 1, 1, 0, 50000), (10610, 9400257, 4032192, 1, 1, 0, 50000), -(10611, 9410066, 5490001, 1, 1, 0, 700000), -(10612, 9410066, 5490001, 1, 1, 0, 700000), -(10613, 9410066, 5490000, 1, 1, 0, 300000), (10614, 9410066, 4000306, 1, 1, 0, 700000), (10615, 9410066, 4000306, 1, 1, 0, 700000), (10616, 9410066, 4000306, 1, 1, 0, 700000), diff --git a/sql/db_drops.sql b/sql/db_drops.sql index 27e0fadcca..2a83061591 100644 --- a/sql/db_drops.sql +++ b/sql/db_drops.sql @@ -1107,7 +1107,6 @@ USE `heavenms`; (9300063, 1052101, 1, 1, 0, 700), (9300082, 1052101, 1, 1, 0, 700), (9400503, 1052101, 1, 1, 0, 40000), -(2100100, 5240005, 1, 1, 0, 7000), (2100100, 4003004, 1, 1, 0, 7000), (2100100, 2000001, 1, 1, 0, 40000), (2100100, 2000003, 1, 1, 0, 40000), @@ -10022,7 +10021,6 @@ USE `heavenms`; (9500326, 1372000, 1, 1, 0, 40000), (9500345, 1372000, 1, 1, 0, 40000), (9303004, 1372000, 1, 1, 0, 700), -(6130103, 1702131, 1, 1, 0, 700), (6130103, 2000006, 1, 1, 0, 40000), (6130103, 2000004, 1, 1, 0, 40000), (6130103, 2040401, 1, 1, 0, 750), @@ -20183,7 +20181,11 @@ USE `heavenms`; (9400114, 2022063, 10, 30, 0, 200000), (9400114, 2022064, 10, 30, 0, 200000), (9400120, 4000094, 1, 1, 0, 400000), -(9400122, 4000094, 1, 1, 0, 400000); +(9400122, 4000094, 1, 1, 0, 400000), +(1110130, 4032317, 1, 1, 21717, 40000), +(1110130, 4032318, 1, 1, 21718, 40000), +(1140130, 4032319, 1, 1, 21723, 100000), +(2230131, 4032321, 1, 1, 21727, 20000); # (dropperid, itemid, minqty, maxqty, questid, chance) diff --git a/src/client/BuddyList.java b/src/client/BuddyList.java index c2e3dc0a11..1a5d77b626 100644 --- a/src/client/BuddyList.java +++ b/src/client/BuddyList.java @@ -26,10 +26,12 @@ import java.sql.ResultSet; import java.sql.Connection; import java.sql.SQLException; import java.util.Collection; +import java.util.Collections; import java.util.Deque; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Map; +import net.server.PlayerStorage; import tools.DatabaseConnection; import tools.MaplePacketCreator; @@ -50,15 +52,22 @@ public class BuddyList { } public boolean contains(int characterId) { - return buddies.containsKey(Integer.valueOf(characterId)); + synchronized(buddies) { + return buddies.containsKey(Integer.valueOf(characterId)); + } } public boolean containsVisible(int characterId) { - BuddylistEntry ble = buddies.get(characterId); + BuddylistEntry ble; + synchronized(buddies) { + ble = buddies.get(characterId); + } + if (ble == null) { return false; } return ble.isVisible(); + } public int getCapacity() { @@ -70,42 +79,65 @@ public class BuddyList { } public BuddylistEntry get(int characterId) { - return buddies.get(Integer.valueOf(characterId)); + synchronized(buddies) { + return buddies.get(Integer.valueOf(characterId)); + } } public BuddylistEntry get(String characterName) { String lowerCaseName = characterName.toLowerCase(); - for (BuddylistEntry ble : buddies.values()) { + for (BuddylistEntry ble : getBuddies()) { if (ble.getName().toLowerCase().equals(lowerCaseName)) { return ble; } } + return null; } public void put(BuddylistEntry entry) { - buddies.put(Integer.valueOf(entry.getCharacterId()), entry); + synchronized(buddies) { + buddies.put(Integer.valueOf(entry.getCharacterId()), entry); + } } public void remove(int characterId) { - buddies.remove(Integer.valueOf(characterId)); + synchronized(buddies) { + buddies.remove(Integer.valueOf(characterId)); + } } public Collection getBuddies() { - return buddies.values(); + synchronized(buddies) { + return Collections.unmodifiableCollection(buddies.values()); + } } public boolean isFull() { - return buddies.size() >= capacity; + synchronized(buddies) { + return buddies.size() >= capacity; + } } public int[] getBuddyIds() { - int buddyIds[] = new int[buddies.size()]; - int i = 0; - for (BuddylistEntry ble : buddies.values()) { - buddyIds[i++] = ble.getCharacterId(); + synchronized(buddies) { + int buddyIds[] = new int[buddies.size()]; + int i = 0; + for (BuddylistEntry ble : buddies.values()) { + buddyIds[i++] = ble.getCharacterId(); + } + return buddyIds; + } + } + + public void broadcast(byte[] packet, PlayerStorage pstorage) { + for(int bid : getBuddyIds()) { + MapleCharacter chr = pstorage.getCharacterById(bid); + + if(chr != null && chr.isLoggedin() && !chr.isAwayFromWorld()) { + chr.announce(packet); + } } - return buddyIds; } public void loadFromDb(int characterId) { diff --git a/src/client/MapleCharacter.java b/src/client/MapleCharacter.java index 25f7109243..1904fc7e83 100644 --- a/src/client/MapleCharacter.java +++ b/src/client/MapleCharacter.java @@ -199,7 +199,6 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject { private int expRate = 1, mesoRate = 1, dropRate = 1, expCoupon = 1, mesoCoupon = 1, dropCoupon = 1; private int omokwins, omokties, omoklosses, matchcardwins, matchcardties, matchcardlosses; private int owlSearch; - private int married; private long lastfametime, lastUsedCashItem, lastHealed, lastMesoDrop = -1, jailExpiration = -1; private transient int localmaxhp, localmaxmp, localstr, localdex, localluk, localint_, magic, watk; private boolean hidden, canDoor = true, berserk, hasMerchant, whiteChat = false; @@ -1093,6 +1092,36 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject { } createDragon(); } + + if(ServerConstants.USE_ANNOUNCE_CHANGEJOB) { + if(gmLevel > 1) { + broadcastAcquaintances(6, "[" + GameConstants.ordinal(GameConstants.getJobBranch(newJob)) + " Job] " + name + " has just become a " + newJob.name() + "."); + } + } + } + + public void broadcastAcquaintances(int type, String message) { + broadcastAcquaintances(MaplePacketCreator.serverNotice(type, message)); + } + + public void broadcastAcquaintances(byte[] packet) { + buddylist.broadcast(packet, client.getWorldServer().getPlayerStorage()); + + if(family != null) { + //family.broadcast(packet, id); not yet implemented + } + + MapleGuild guild = getGuild(); + if(guild != null) { + guild.broadcast(packet, id); + } + + /* + if(partnerid > 0) { + partner.announce(packet); not yet implemented + } + */ + announce(packet); } public void changeKeybinding(int key, MapleKeyBinding keybinding) { @@ -3955,10 +3984,6 @@ public class MapleCharacter extends AbstractAnimatedMapleMapObject { return marriageRing; } - public int getMarried() { - return married; - } - public int getMasterLevel(Skill skill) { if (skills.get(skill) == null) { return 0; diff --git a/src/client/command/Commands.java b/src/client/command/Commands.java index b3fcea7ad7..ed77335191 100644 --- a/src/client/command/Commands.java +++ b/src/client/command/Commands.java @@ -351,7 +351,7 @@ public class Commands { case "playercommands": c.getAbstractPlayerInteraction().openNpc(9201143, "commands"); break; - + case "droplimit": int dropCount = c.getPlayer().getMap().getDroppedItemCount(); if(((float) dropCount) / ServerConstants.ITEM_LIMIT_ON_MAP < 0.75f) { @@ -402,90 +402,14 @@ public class Commands { } break; } - String output = "The #b" + gachaName + "#k Gachapon contains the following items.\r\n\r\n"; + String talkStr = "The #b" + gachaName + "#k Gachapon contains the following items.\r\n\r\n"; for (int i = 0; i < 2; i++){ for (int id : gacha.getItems(i)){ - output += "-" + MapleItemInformationProvider.getInstance().getName(id) + "\r\n"; + talkStr += "-" + MapleItemInformationProvider.getInstance().getName(id) + "\r\n"; } } - output += "\r\nPlease keep in mind that there are items that are in all gachapons and are not listed here."; - c.announce(MaplePacketCreator.getNPCTalk(9010000, (byte) 0, output, "00 00", (byte) 0)); - break; - - case "whatdropsfrom": - if (sub.length < 2) { - player.dropMessage(5, "Please do @whatdropsfrom "); - break; - } - String monsterName = joinStringFrom(sub, 1); - output = ""; - int limit = 3; - Iterator> listIterator = MapleMonsterInformationProvider.getMobsIDsFromName(monsterName).iterator(); - for (int i = 0; i < limit; i++) { - if(listIterator.hasNext()) { - Pair data = listIterator.next(); - int mobId = data.getLeft(); - String mobName = data.getRight(); - output += mobName + " drops the following items:\r\n\r\n"; - for (MonsterDropEntry drop : MapleMonsterInformationProvider.getInstance().retrieveDrop(mobId)){ - try { - String name = MapleItemInformationProvider.getInstance().getName(drop.itemId); - if (name.equals("null") || drop.chance == 0){ - continue; - } - float chance = 1000000 / drop.chance / player.getDropRate(); - output += "- " + name + " (1/" + (int) chance + ")\r\n"; - } catch (Exception ex){ - ex.printStackTrace(); - continue; - } - } - output += "\r\n"; - } - } - c.announce(MaplePacketCreator.getNPCTalk(9010000, (byte) 0, output, "00 00", (byte) 0)); - break; - - case "whodrops": - if (sub.length < 2) { - player.dropMessage(5, "Please do @whodrops "); - break; - } - String searchString = joinStringFrom(sub, 1); - output = ""; - listIterator = MapleItemInformationProvider.getInstance().getItemDataByName(searchString).iterator(); - if(listIterator.hasNext()) { - int count = 1; - while(listIterator.hasNext() && count <= 3) { - Pair data = listIterator.next(); - output += "#b" + data.getRight() + "#k is dropped by:\r\n"; - try { - Connection con = DatabaseConnection.getConnection(); - PreparedStatement ps = con.prepareStatement("SELECT dropperid FROM drop_data WHERE itemid = ? LIMIT 50"); - ps.setInt(1, data.getLeft()); - ResultSet rs = ps.executeQuery(); - while(rs.next()) { - String resultName = MapleMonsterInformationProvider.getMobNameFromID(rs.getInt("dropperid")); - if (resultName != null) { - output += resultName + ", "; - } - } - rs.close(); - ps.close(); - con.close(); - } catch (Exception e) { - player.dropMessage(6, "There was a problem retrieving the required data. Please try again."); - e.printStackTrace(); - break; - } - output += "\r\n\r\n"; - count++; - } - } else { - player.dropMessage(5, "The item you searched for doesn't exist."); - break; - } - c.announce(MaplePacketCreator.getNPCTalk(9010000, (byte) 0, output, "00 00", (byte) 0)); + talkStr += "\r\nPlease keep in mind that there are items that are in all gachapons and are not listed here."; + c.announce(MaplePacketCreator.getNPCTalk(9010000, (byte) 0, talkStr, "00 00", (byte) 0)); break; case "dispose": @@ -560,7 +484,7 @@ public class Commands { player.dropMessage(5, tips[Randomizer.nextInt(tips.length)]); break; - case "bug": + case "reportbug": if (sub.length < 2) { player.dropMessage(5, "Message too short and not sent. Please do @bug "); @@ -636,30 +560,6 @@ public class Commands { } break; - case "bosshp": - for(MapleMonster monster : player.getMap().getMonsters()) { - if(monster != null && monster.isBoss() && monster.getHp() > 0) { - long percent = monster.getHp() * 100L / monster.getMaxHp(); - String bar = "["; - for (int i = 0; i < 100; i++){ - bar += i < percent ? "|" : "."; - } - bar += "]"; - player.yellowMessage(monster.getName() + " (" + monster.getId() + ") has " + percent + "% HP left."); - player.yellowMessage("HP: " + bar); - } - } - break; - - case "mobhp": - for(MapleMonster monster : player.getMap().getMonsters()) { - if(monster != null && monster.getHp() > 0) { - player.yellowMessage(monster.getName() + " (" + monster.getId() + ") has " + monster.getHp() + " / " + monster.getMaxHp() + " HP."); - - } - } - break; - case "ranks": PreparedStatement ps = null; ResultSet rs = null; @@ -691,6 +591,47 @@ public class Commands { } } break; + + // stat autoassigning command credited to HeliosMS dev team + case "str": + case "int": + case "luk": + case "dex": + int amount = (sub.length > 1) ? Integer.parseInt(sub[1]) : player.getRemainingAp(); + boolean str = sub[0].equalsIgnoreCase("str"); + boolean Int = sub[0].equalsIgnoreCase("int"); + boolean luk = sub[0].equalsIgnoreCase("luk"); + boolean dex = sub[0].equalsIgnoreCase("dex"); + + if (amount > 0 && amount <= player.getRemainingAp() && amount <= 32763 || amount < 0 && amount >= -32763 && Math.abs(amount) + player.getRemainingAp() <= 32767) { + if (str && amount + player.getStr() <= 32767 && amount + player.getStr() >= 4) { + player.setStr(player.getStr() + amount); + player.updateSingleStat(MapleStat.STR, player.getStr()); + player.setRemainingAp(player.getRemainingAp() - amount); + player.updateSingleStat(MapleStat.AVAILABLEAP, player.getRemainingAp()); + } else if (Int && amount + player.getInt() <= 32767 && amount + player.getInt() >= 4) { + player.setInt(player.getInt() + amount); + player.updateSingleStat(MapleStat.INT, player.getInt()); + player.setRemainingAp(player.getRemainingAp() - amount); + player.updateSingleStat(MapleStat.AVAILABLEAP, player.getRemainingAp()); + } else if (luk && amount + player.getLuk() <= 32767 && amount + player.getLuk() >= 4) { + player.setLuk(player.getLuk() + amount); + player.updateSingleStat(MapleStat.LUK, player.getLuk()); + player.setRemainingAp(player.getRemainingAp() - amount); + player.updateSingleStat(MapleStat.AVAILABLEAP, player.getRemainingAp()); + } else if (dex && amount + player.getDex() <= 32767 && amount + player.getDex() >= 4) { + player.setDex(player.getDex() + amount); + player.updateSingleStat(MapleStat.DEX, player.getDex()); + player.setRemainingAp(player.getRemainingAp() - amount); + player.updateSingleStat(MapleStat.AVAILABLEAP, player.getRemainingAp()); + } else { + player.dropMessage("Please make sure the stat you are trying to raise is not over 32,767 or under 4."); + } + } else { + player.dropMessage("Please make sure your AP is not over 32,767 and you have enough to distribute."); + } + + break; default: return false; @@ -702,8 +643,113 @@ public class Commands { public static boolean executeHeavenMsCommandLv1(Channel cserv, Server srv, MapleClient c, String[] sub) { //Donator MapleCharacter player = c.getPlayer(); - switch(sub[0]) { + switch(sub[0]) { + case "bosshp": + for(MapleMonster monster : player.getMap().getMonsters()) { + if(monster != null && monster.isBoss() && monster.getHp() > 0) { + long percent = monster.getHp() * 100L / monster.getMaxHp(); + String bar = "["; + for (int i = 0; i < 100; i++){ + bar += i < percent ? "|" : "."; + } + bar += "]"; + player.yellowMessage(monster.getName() + " (" + monster.getId() + ") has " + percent + "% HP left."); + player.yellowMessage("HP: " + bar); + } + } + break; + + case "mobhp": + for(MapleMonster monster : player.getMap().getMonsters()) { + if(monster != null && monster.getHp() > 0) { + player.yellowMessage(monster.getName() + " (" + monster.getId() + ") has " + monster.getHp() + " / " + monster.getMaxHp() + " HP."); + + } + } + break; + + case "whatdropsfrom": + if (sub.length < 2) { + player.dropMessage(5, "Please do @whatdropsfrom "); + break; + } + String monsterName = joinStringFrom(sub, 1); + String output = ""; + int limit = 3; + Iterator> listIterator = MapleMonsterInformationProvider.getMobsIDsFromName(monsterName).iterator(); + for (int i = 0; i < limit; i++) { + if(listIterator.hasNext()) { + Pair data = listIterator.next(); + int mobId = data.getLeft(); + String mobName = data.getRight(); + output += mobName + " drops the following items:\r\n\r\n"; + for (MonsterDropEntry drop : MapleMonsterInformationProvider.getInstance().retrieveDrop(mobId)){ + try { + String name = MapleItemInformationProvider.getInstance().getName(drop.itemId); + if (name.equals("null") || drop.chance == 0){ + continue; + } + float chance = 1000000 / drop.chance / player.getDropRate(); + output += "- " + name + " (1/" + (int) chance + ")\r\n"; + } catch (Exception ex){ + ex.printStackTrace(); + continue; + } + } + output += "\r\n"; + } + } + c.announce(MaplePacketCreator.getNPCTalk(9010000, (byte) 0, output, "00 00", (byte) 0)); + break; + + case "whodrops": + if (sub.length < 2) { + player.dropMessage(5, "Please do @whodrops "); + break; + } + String searchString = joinStringFrom(sub, 1); + output = ""; + listIterator = MapleItemInformationProvider.getInstance().getItemDataByName(searchString).iterator(); + if(listIterator.hasNext()) { + int count = 1; + while(listIterator.hasNext() && count <= 3) { + Pair data = listIterator.next(); + output += "#b" + data.getRight() + "#k is dropped by:\r\n"; + try { + Connection con = DatabaseConnection.getConnection(); + PreparedStatement ps = con.prepareStatement("SELECT dropperid FROM drop_data WHERE itemid = ? LIMIT 50"); + ps.setInt(1, data.getLeft()); + ResultSet rs = ps.executeQuery(); + while(rs.next()) { + String resultName = MapleMonsterInformationProvider.getMobNameFromID(rs.getInt("dropperid")); + if (resultName != null) { + output += resultName + ", "; + } + } + rs.close(); + ps.close(); + con.close(); + } catch (Exception e) { + player.dropMessage(6, "There was a problem retrieving the required data. Please try again."); + e.printStackTrace(); + break; + } + output += "\r\n\r\n"; + count++; + } + } else { + player.dropMessage(5, "The item you searched for doesn't exist."); + break; + } + c.announce(MaplePacketCreator.getNPCTalk(9010000, (byte) 0, output, "00 00", (byte) 0)); + break; + case "buffme": + if(!player.isGM()) { + player.dropMessage(5, "You are already dead."); + break; + } + //GM Skills : Haste(Super) - Holy Symbol - Bless - Hyper Body - Echo of Hero SkillFactory.getSkill(4101004).getEffect(SkillFactory.getSkill(4101004).getMaxLevel()).applyTo(player); SkillFactory.getSkill(2311003).getEffect(SkillFactory.getSkill(2311003).getMaxLevel()).applyTo(player); @@ -753,7 +799,20 @@ public class Commands { } player.dropMessage(5, "USE Recharged."); break; - + + default: + return false; + } + + return true; + } + + public static boolean executeHeavenMsCommandLv2(Channel cserv, Server srv, MapleClient c, String[] sub) { //JrGM + MapleCharacter player = c.getPlayer(); + MapleCharacter victim; + Skill skill; + + switch(sub[0]) { case "whereami": player.yellowMessage("Map ID: " + player.getMap().getId()); player.yellowMessage("Players on this map:"); @@ -777,21 +836,8 @@ public class Commands { } } } - break; - - default: - return false; - } - - return true; - } - - public static boolean executeHeavenMsCommandLv2(Channel cserv, Server srv, MapleClient c, String[] sub) { //JrGM - MapleCharacter player = c.getPlayer(); - MapleCharacter victim; - Skill skill; - - switch(sub[0]) { + break; + case "hide": SkillFactory.getSkill(9101004).getEffect(SkillFactory.getSkill(9101004).getMaxLevel()).applyTo(player); break; diff --git a/src/constants/GameConstants.java b/src/constants/GameConstants.java index 2f4269c4c7..5899106a36 100644 --- a/src/constants/GameConstants.java +++ b/src/constants/GameConstants.java @@ -62,26 +62,36 @@ public class GameConstants { 330000, 340000, 350000, 360000, 370000, 380000, 390000, 400000, 410000, 420000, 430000, 440000, 450000, 460000, 470000, 480000, 490000, 500000, 510000, 520000, 530000, 550000, 570000, 590000, 610000, 630000, 650000, 670000, 690000, 710000, 730000, 750000, 770000, 790000, 810000, 830000, 850000, 870000, 890000, 910000}; - public static int getJobMaxLevel(MapleJob job) { - if(job.getId() % 1000 == 0) { // beginner - return 10; - - } else if(job.getId() % 100 == 0) { // 1st job - return 30; - + public static int getJobBranch(MapleJob job) { + int jobid = job.getId(); + + if(jobid % 1000 == 0) { + return 0; + } else if(jobid % 100 == 0) { + return 1; } else { - int jobBranch = job.getId() % 10; - - switch(jobBranch) { - case 0: - return 70; // 2nd job - - case 1: - return 120; // 3rd job - - default: - return (job.getId() / 1000 == 1) ? 120 : 200; // 4th job: cygnus is 120, rest is 200 - } + return 2 + (jobid % 10); + } + } + + public static int getJobMaxLevel(MapleJob job) { + int jobBranch = getJobBranch(job); + + switch(jobBranch) { + case 0: + return 10; // beginner + + case 1: + return 30; // 1st job + + case 2: + return 70; // 2nd job + + case 3: + return 120; // 3rd job + + default: + return (job.getId() / 1000 == 1) ? 120 : 200; // 4th job: cygnus is 120, rest is 200 } } @@ -225,4 +235,17 @@ public class GameConstants { } return mobHpVal[level]; } + + public static String ordinal(int i) { + String[] sufixes = new String[] { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" }; + switch (i % 100) { + case 11: + case 12: + case 13: + return i + "th"; + + default: + return i + sufixes[i % 10]; + } + } } diff --git a/src/constants/ServerConstants.java b/src/constants/ServerConstants.java index d0efb470a5..8e466a79c5 100644 --- a/src/constants/ServerConstants.java +++ b/src/constants/ServerConstants.java @@ -73,6 +73,10 @@ public class ServerConstants { public static final boolean USE_QUEST_RATE = false; //Exp/Meso gained by quests uses fixed server exp/meso rate times quest rate as multiplier, instead of player rates. public static final boolean USE_MULTIPLE_SAME_EQUIP_DROP = true;//Enables multiple drops by mobs of the same equipment, number of possible drops based on the quantities provided at the drop data. + + //Announcement Configuration + public static final boolean USE_ANNOUNCE_CHANGEJOB = true; //Automatic message sent to acquantainces when changing jobs. + //Server Rates And Experience public static final int EXP_RATE = 10; public static final int MESO_RATE = 10; @@ -158,7 +162,7 @@ public class ServerConstants { public static final boolean USE_DEADLY_DOJO = false; //Should bosses really use 1HP,1MP attacks in dojo? public static final int DOJO_ENERGY_ATK = 100; //Dojo energy gain when deal attack public static final int DOJO_ENERGY_DMG = 20; //Dojo energy gain when recv attack - + //Event End Timestamp public static final long EVENT_END_TIMESTAMP = 1428897600000L; diff --git a/src/net/server/channel/handlers/GeneralChatHandler.java b/src/net/server/channel/handlers/GeneralChatHandler.java index e304e2dae4..8583404248 100644 --- a/src/net/server/channel/handlers/GeneralChatHandler.java +++ b/src/net/server/channel/handlers/GeneralChatHandler.java @@ -32,6 +32,14 @@ import java.text.SimpleDateFormat; import java.util.Calendar; public final class GeneralChatHandler extends net.AbstractMaplePacketHandler { + private static boolean isCommandIssue(char heading, MapleCharacter chr) { + if(chr.gmLevel() > 1 && heading == '!') { + return true; + } else { + return heading == '@'; + } + } + @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { String s = slea.readMapleAsciiString(); @@ -47,7 +55,7 @@ public final class GeneralChatHandler extends net.AbstractMaplePacketHandler { return; } char heading = s.charAt(0); - if (heading == '!' || heading == '@') { + if (isCommandIssue(heading, chr)) { String[] sp = s.split(" "); sp[0] = sp[0].toLowerCase().substring(1); diff --git a/src/net/server/channel/handlers/QuestActionHandler.java b/src/net/server/channel/handlers/QuestActionHandler.java index 69eb70f4d0..f57490edba 100644 --- a/src/net/server/channel/handlers/QuestActionHandler.java +++ b/src/net/server/channel/handlers/QuestActionHandler.java @@ -44,15 +44,21 @@ public final class QuestActionHandler extends AbstractMaplePacketHandler { if (slea.available() >= 4) { slea.readInt(); } - quest.start(player, npc); + + if(quest.canStart(player, npc)) { + quest.start(player, npc); + } } else if (action == 2) { // Complete Quest int npc = slea.readInt(); slea.readInt(); - if (slea.available() >= 2) { - int selection = slea.readShort(); - quest.complete(player, npc, selection); - } else { - quest.complete(player, npc); + + if(quest.canComplete(player, npc)) { + if (slea.available() >= 2) { + int selection = slea.readShort(); + quest.complete(player, npc, selection); + } else { + quest.complete(player, npc); + } } } else if (action == 3) {// forfeit quest quest.forfeit(player); diff --git a/src/scripting/event/EventManager.java b/src/scripting/event/EventManager.java index cd7a4be21d..def406db73 100644 --- a/src/scripting/event/EventManager.java +++ b/src/scripting/event/EventManager.java @@ -34,6 +34,7 @@ import javax.script.Invocable; import javax.script.ScriptException; import constants.ServerConstants; +import constants.GameConstants; import client.MapleCharacter; import net.server.Server; import net.server.world.World; @@ -499,19 +500,6 @@ public class EventManager { return(MapleLifeFactory.getMonster(mid)); } - private static String ordinal(int i) { - String[] sufixes = new String[] { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" }; - switch (i % 100) { - case 11: - case 12: - case 13: - return i + "th"; - - default: - return i + sufixes[i % 10]; - } - } - private void exportReadyGuild(Integer guildId) { MapleGuild mg = server.getGuild(guildId); String callout = "[Guild Quest] Your guild has been registered to attend to the Sharenian Guild Quest at channel " + this.getChannelServer().getId() @@ -524,7 +512,7 @@ public class EventManager { private void exportMovedQueueToGuild(Integer guildId, int place) { MapleGuild mg = server.getGuild(guildId); String callout = "[Guild Quest] Your guild has been registered to attend to the Sharenian Guild Quest at channel " + this.getChannelServer().getId() - + " and is currently on the " + ordinal(place) + " place on the waiting queue."; + + " and is currently on the " + GameConstants.ordinal(place) + " place on the waiting queue."; mg.dropMessage(6, callout); } diff --git a/src/scripting/quest/QuestScriptManager.java b/src/scripting/quest/QuestScriptManager.java index 22782c71d0..5f647213c5 100644 --- a/src/scripting/quest/QuestScriptManager.java +++ b/src/scripting/quest/QuestScriptManager.java @@ -61,7 +61,7 @@ public class QuestScriptManager extends AbstractScriptManager { qms.put(c, qm); Invocable iv = getInvocable("quest/" + questid + ".js", c); if (iv == null) { - FilePrinter.printError(FilePrinter.QUEST_UNCODED, "Quest " + questid + " is uncoded.\r\n"); + FilePrinter.printError(FilePrinter.QUEST_UNCODED, "START Quest " + questid + " is uncoded.\r\n"); } if (iv == null || QuestScriptManager.getInstance() == null) { qm.dispose(); @@ -112,6 +112,7 @@ public class QuestScriptManager extends AbstractScriptManager { qms.put(c, qm); Invocable iv = getInvocable("quest/" + questid + ".js", c); if (iv == null) { + FilePrinter.printError(FilePrinter.QUEST_UNCODED, "END Quest " + questid + " is uncoded.\r\n"); qm.dispose(); return; } diff --git a/src/server/life/MapleMonster.java b/src/server/life/MapleMonster.java index d07e56e318..15d67082eb 100644 --- a/src/server/life/MapleMonster.java +++ b/src/server/life/MapleMonster.java @@ -535,8 +535,7 @@ public class MapleMonster extends AbstractLoadedMapleLife { } }, getAnimationTime("die1")); } - } - else { // is this even necessary? + } else { // is this even necessary? System.out.println("[CRITICAL LOSS] toSpawn is null for " + this.getName()); } diff --git a/tools/MapleCashDropFetcher/build.xml b/tools/MapleCashDropFetcher/build.xml new file mode 100644 index 0000000000..a34b8b10a7 --- /dev/null +++ b/tools/MapleCashDropFetcher/build.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + Builds, tests, and runs the project MapleCashDropFetcher. + + + diff --git a/tools/MapleCashDropFetcher/lib/CashDropReport.txt b/tools/MapleCashDropFetcher/lib/CashDropReport.txt new file mode 100644 index 0000000000..b60c817722 --- /dev/null +++ b/tools/MapleCashDropFetcher/lib/CashDropReport.txt @@ -0,0 +1,11 @@ + # Report File autogenerated from the MapleCashDropFetcher feature by Ronan Lana. + # Generated data takes into account several data info from the underlying DB and the server-side WZ.xmls. + +NX DROPS ON drop_data +5240005 : 2100100 +5490000 : 9410066 +5490001 : 9410066 + + + + diff --git a/tools/MapleCashDropFetcher/manifest.mf b/tools/MapleCashDropFetcher/manifest.mf new file mode 100644 index 0000000000..328e8e5bc3 --- /dev/null +++ b/tools/MapleCashDropFetcher/manifest.mf @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +X-COMMENT: Main-Class will be added automatically by build + diff --git a/tools/MapleCashDropFetcher/nbproject/build-impl.xml b/tools/MapleCashDropFetcher/nbproject/build-impl.xml new file mode 100644 index 0000000000..a6ae1f8964 --- /dev/null +++ b/tools/MapleCashDropFetcher/nbproject/build-impl.xml @@ -0,0 +1,1448 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set platform.home + Must set platform.bootcp + Must set platform.java + Must set platform.javac + + The J2SE Platform is not correctly set up. + Your active platform is: ${platform.active}, but the corresponding property "platforms.${platform.active}.home" is not found in the project's properties files. + Either open the project in the IDE and setup the Platform with the same name or add it manually. + For example like this: + ant -Duser.properties.file=<path_to_property_file> jar (where you put the property "platforms.${platform.active}.home" in a .properties file) + or ant -Dplatforms.${platform.active}.home=<path_to_JDK_home> jar (where no properties file is used) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + ${platform.java} -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/MapleCashDropFetcher/nbproject/genfiles.properties b/tools/MapleCashDropFetcher/nbproject/genfiles.properties new file mode 100644 index 0000000000..f63836edd2 --- /dev/null +++ b/tools/MapleCashDropFetcher/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=928d5bb0 +build.xml.script.CRC32=27a7b03c +build.xml.stylesheet.CRC32=8064a381@1.75.2.48 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=928d5bb0 +nbproject/build-impl.xml.script.CRC32=34e6e8ba +nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48 diff --git a/tools/MapleCashDropFetcher/nbproject/project.properties b/tools/MapleCashDropFetcher/nbproject/project.properties new file mode 100644 index 0000000000..a6bd84f5da --- /dev/null +++ b/tools/MapleCashDropFetcher/nbproject/project.properties @@ -0,0 +1,77 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +application.title=MapleCashDropFetcher +application.vendor=USER +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# Os arquivos em build.classes.dir que devem ser exclu\u00eddos do jar de distribui\u00e7\u00e3o +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/MapleCashDropFetcher.jar +dist.javadoc.dir=${dist.dir}/javadoc +endorsed.classpath= +excludes= +file.reference.mysql-connector-java-bin.jar=../../cores/mysql-connector-java-bin.jar +includes=** +jar.compress=false +javac.classpath=\ + ${file.reference.mysql-connector-java-bin.jar} +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.7 +javac.target=1.7 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +main.class=maplecashdropfetcher.MapleCashDropFetcher +manifest.file=manifest.mf +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=false +platform.active=JDK_1.7 +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/tools/MapleCashDropFetcher/nbproject/project.xml b/tools/MapleCashDropFetcher/nbproject/project.xml new file mode 100644 index 0000000000..ea33fc5939 --- /dev/null +++ b/tools/MapleCashDropFetcher/nbproject/project.xml @@ -0,0 +1,16 @@ + + + org.netbeans.modules.java.j2seproject + + + MapleCashDropFetcher + + + + + + + + + + diff --git a/tools/MapleCashDropFetcher/src/maplecashdropfetcher/MapleCashDropFetcher.java b/tools/MapleCashDropFetcher/src/maplecashdropfetcher/MapleCashDropFetcher.java new file mode 100644 index 0000000000..3c9509f89d --- /dev/null +++ b/tools/MapleCashDropFetcher/src/maplecashdropfetcher/MapleCashDropFetcher.java @@ -0,0 +1,406 @@ +/* + This file is part of the HeavenMS (MapleSolaxiaV2) MapleStory Server + Copyleft (L) 2017 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 maplecashdropfetcher; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.HashSet; +import java.util.Set; + +import java.io.File; + +import tools.Pair; + +/** + * + * @author RonanLana + + 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. + + Estimated parse time: 2 minutes + */ +public class MapleCashDropFetcher { + static String host = "jdbc:mysql://localhost:3306/heavenms"; + static String driver = "com.mysql.jdbc.Driver"; + static String username = "root"; + static String password = ""; + + static String wzPath = "../../wz"; + + static String directoryName = "../.."; + static String newFile = "lib/CashDropReport.txt"; + + static Connection con = null; + static PrintWriter printWriter = null; + static InputStreamReader fileReader = null; + static BufferedReader bufferedReader = null; + + static int initialLength = 200; + static int initialStringLength = 50; + static int itemFileNameSize = 13; + + static Set nxItems = new HashSet<>(); + static Set nxDrops = new HashSet<>(); + + static byte status = 0; + 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[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 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.valueOf(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 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.valueOf(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> getNxDropsEntries(boolean dropdata) throws SQLException { + List> entries = new ArrayList<>(); + + List 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> nxEntries = getNxDropsEntries(dropdata); + + printWriter.println("NX DROPS ON " + getDropTableName(dropdata)); + for(Pair nx : nxEntries) { + printWriter.println(nx.left + " : " + nx.right); + } + printWriter.println("\n\n\n"); + } + } + + private static void ReportNxDropData() { + try { + Class.forName(driver).newInstance(); + + System.out.println("Reading Character.wz ..."); + ArrayList files = new ArrayList<>(); + listFiles(wzPath + "/Character.wz", files); + + 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), "UTF-8"); + bufferedReader = new BufferedReader(fileReader); + + currentItemid = itemid; + inspectEquipWzEntry(); + + bufferedReader.close(); + fileReader.close(); + } + + System.out.println("Reading Item.wz ..."); + files = new ArrayList<>(); + listFiles(wzPath + "/Item.wz", files); + + for(File f : files) { + //System.out.println("Parsing " + f.getAbsolutePath()); + fileReader = new InputStreamReader(new FileInputStream(f), "UTF-8"); + bufferedReader = new BufferedReader(fileReader); + + if(f.getName().length() <= itemFileNameSize) { + 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..."); + + // filter drop data on DB + con = DriverManager.getConnection(host, username, password); + + // report suspects of missing quest drop data, as well as those drop data that may have incorrect questids. + printWriter = new PrintWriter(newFile, "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(ClassNotFoundException e) { + System.out.println("Error: could not find class"); + System.out.println(e.getMessage()); + } + + catch(InstantiationException e) { + System.out.println("Error: instantiation failure"); + System.out.println(e.getMessage()); + } + + catch(Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + ReportNxDropData(); + } + +} diff --git a/tools/MapleCashDropFetcher/src/tools/DatabaseConnection.java b/tools/MapleCashDropFetcher/src/tools/DatabaseConnection.java new file mode 100644 index 0000000000..9dcd4e6545 --- /dev/null +++ b/tools/MapleCashDropFetcher/src/tools/DatabaseConnection.java @@ -0,0 +1,51 @@ +package tools; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * @author Frz (Big Daddy) + * @author The Real Spookster (some modifications to this beautiful code) + */ +public class DatabaseConnection { + private static String DB_URL = "jdbc:mysql://localhost:3306/heavenms"; + private static String DB_USER = "root"; + private static String DB_PASS = ""; + + public static final int RETURN_GENERATED_KEYS = 1; + + private static ThreadLocal con = new ThreadLocalConnection(); + + public static Connection getConnection() { + Connection c = con.get(); + try { + c.getMetaData(); + } catch (SQLException e) { // connection is dead, therefore discard old object 5ever + con.remove(); + c = con.get(); + } + return c; + } + + private static class ThreadLocalConnection extends ThreadLocal { + + @Override + protected Connection initialValue() { + try { + Class.forName("com.mysql.jdbc.Driver"); // touch the mysql driver + } catch (ClassNotFoundException e) { + System.out.println("[SEVERE] SQL Driver Not Found. Consider death by clams."); + e.printStackTrace(); + return null; + } + try { + return DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); + } catch (SQLException e) { + System.out.println("[SEVERE] Unable to make database connection."); + e.printStackTrace(); + return null; + } + } + } +} \ No newline at end of file diff --git a/tools/MapleCashDropFetcher/src/tools/Pair.java b/tools/MapleCashDropFetcher/src/tools/Pair.java new file mode 100644 index 0000000000..f88718cbe3 --- /dev/null +++ b/tools/MapleCashDropFetcher/src/tools/Pair.java @@ -0,0 +1,121 @@ +/* +This file is part of the OdinMS Maple Story Server +Copyright (C) 2008 ~ 2010 Patrick Huy +Matthias Butz +Jan Christian Meyer + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License version 3 +as published by the Free Software Foundation. You may not use, modify +or distribute this program under any other version of the +GNU Affero General Public License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + */ +package tools; + +/** + * Represents a pair of values. + * + * @author Frz + * @since Revision 333 + * @version 1.0 + * + * @param The type of the left value. + * @param The type of the right value. + */ +public class Pair { + + public E left; + public F right; + + /** + * Class constructor - pairs two objects together. + * + * @param left The left object. + * @param right The right object. + */ + public Pair(E left, F right) { + this.left = left; + this.right = right; + } + + /** + * Gets the left value. + * + * @return The left value. + */ + public E getLeft() { + return left; + } + + /** + * Gets the right value. + * + * @return The right value. + */ + public F getRight() { + return right; + } + + /** + * Turns the pair into a string. + * + * @return Each value of the pair as a string joined by a colon. + */ + @Override + public String toString() { + return left.toString() + ":" + right.toString(); + } + + /** + * Gets the hash code of this pair. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((left == null) ? 0 : left.hashCode()); + result = prime * result + ((right == null) ? 0 : right.hashCode()); + return result; + } + + /** + * Checks to see if two pairs are equal. + */ + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Pair other = (Pair) obj; + if (left == null) { + if (other.left != null) { + return false; + } + } else if (!left.equals(other.left)) { + return false; + } + if (right == null) { + if (other.right != null) { + return false; + } + } else if (!right.equals(other.right)) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/tools/MapleMesoFetcher/nbproject/private/private.xml b/tools/MapleMesoFetcher/nbproject/private/private.xml index f02aee0624..6807a2ba19 100644 --- a/tools/MapleMesoFetcher/nbproject/private/private.xml +++ b/tools/MapleMesoFetcher/nbproject/private/private.xml @@ -2,8 +2,6 @@ - - file:/C:/Nexon/MapleSolaxia/MapleSolaxiaV2/tools/MapleMesoFetcher/src/maplemesofetcher/MapleMesoFetcher.java - + diff --git a/tools/MapleQuestItemFetcher/dist/MapleQuestItemFetcher.jar b/tools/MapleQuestItemFetcher/dist/MapleQuestItemFetcher.jar index eecb39f117b1348fc8034a4f2c7f0d8ee3d07d60..a81e6a6878f94b4cf9775f3968ea6efa8a4de691 100644 GIT binary patch delta 13557 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 Zy1GS$80^h7hr@3RrF&c-e9uAje*h)(t&0Ev delta 12138 zcmb_i34D}Avajly$xJ3+0vU3Xz(7KP+yN3G$fbaCgd?0m1tJ6tM}UM|bU0K56~zHR z0Yy+W-a^Jrg19T5c)@z?ii#|FyX$(OqJm)FzxtaQh;R43x4+*DeBJd`S66pcS65Z{ zH+#Nzz59_XX7q5I-A&{odg-QHhi{7MY=05;-rl3wE(rfETwnnQINbui;7kjMRbC4y zQk^VdipsWtTUCxrx*wYzvo!FW+H2><{?zC(yv$!6y%O+O(KB@Ws(-eL3OwPOXXhJq z`%Sw225FC#{$G9iOU-E;kv!e~3zPB!8)Mqo*!15QJCL{eKaHIPxUc65%`9$PB-1crW?>*PTuOKSl%Y+?NZ$B=6iUDK=(?q zQ;JdUV>`878U;Mb$5i60Wo8v;Ee1czLFIEw$wZwkR%GK06H_#OU> z6n~B4cln4E?@95#kbEGWj!Jh7_AY-YbRRnTn9oh)c$<^|=H%mUn#k*g?u0-ex%gwL zKXKCp{?x^vx%hJzf8pXU-TZg{he-OBn`ZH!r1+<3<7+2>BQM`d@tt5!3f1>c{z16@ z=;Bju+Q~n;_-Cnq5rKWDoqWbkwY=WVzw%ippL5d^Uhm}J+_X&UW;dlHGs-3*IcPXz+b#SVVLYEGaREFR? zxm2dqot@kV3RGQOs;f(72`byAa%2r&a;jXZPrH5eIKS#rdD5CM)CDe8DEzv)RChu4 zaH%4vDt4)!F4arah8gsBsS=m!<5U;9R9~0shc#6Fooawf4Rq5B0$%J=m$>O=)!(HC z3Ae#+sA&kcgt}CiUglCmU22$14Nr8b5iV8gQX^ezlpB*-?^L6mYK(uOEzVz)x**au zvubYT$clNDffK3oZ1Hhb)wK&Mr!N|_xUzQ9&_$K=MpV@<@?Y*Pv~R)|^dIu}i+zwO zx~8&XhO~~GKc}+Vf1)@sc>+_+qMC~8+PM{rDm9wP4vGc-(hi61W!M+~_8m8JvHyvV zrT(Wga{V>w$^Jd*{XME`A(Js;(fs*yYx74}RMqq`AD_&rtzxRsyLMvLqO0{>{LLBh zHl`l_imXolJ@!^<*gQ@()~Uw9%9-4Qmrbu+u&8Q&buH7-F8(oDMS;9(B*P-AajEM!JM*8JNl5l8X7Uw1T~R~I~r<| zDhE(t(6{uBp(d-#{T&Xp$u!umt}xV&{2aGedAOWhPp~kH`oS#%qZw5`oUll_A^u^rfH~|YL@@`+)Urqs>)Ea z)f|Jq2Mzrq#n1EulQX|+-mDo_HHMn2<{6wOGmi+OIEukeRn0I|HTrWL8-|)M@;Ii? zUtGNiGKzu?j^_j>TV?eOgMOv6hFYK&8mdOsI#rpW7OBOCQ!Pi5 zzLOJt%hXasEtk2i02QEFAevpVq#}RuqN=(118Zt3mW%Zm^tA|SmvP+Zmsyl|jT9>l z`iwqj${#y;#NbQDc^8W5Rn71YDH~Plol!G?!PFTQiz>VmFB@Do*ei2JTVHSgf!H6{ z8tOXbGt?@X%4)R+24SeR>Uu-npl*a|7;2qNZHW{&sr821075zq1*)6b;op?s+3e%z zl=%jILdOhsi@Fu7oNuU&vUGM%hhl0M&V?Fh!>VkHmWkFxLZ`aTP`9g12AA_>L)`&# z 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 28175 +2022055 : -1 -> 9330 EXPIRED +2022056 : -1 -> 9330 EXPIRED +4001352 : 28205 -> 28206 +4001366 : 28194 -> 28195 4001367 : 28257 -> 28262 4001368 : 28258 -> 28262 4001369 : 28259 -> 28262 4001370 : 28260 -> 28262 4001371 : 28261 -> 28262 +4001372 : 28344 -> 28282 +4031107 : -1 -> 3409 +4031116 : -1 -> 3419 4031130 : 0 -> 3238 4031164 : 0 -> 2084 -4031171 : 0 -> 7101 -4031172 : 7103 -> 7106 4031189 : 0 -> 3448 4031218 : 0 -> 3071 4031223 : 3607 -> 3608 -4031343 : 6904 -> 6905 -4031344 : 6904 -> 6905 -4031405 : 0 -> 4207 +4031405 : 0 -> 4207 EXPIRED 4031511 : 6904 -> 6914 -4031512 : 6914 -> 6915 -4031514 : 6924 -> 6925 -4031515 : 6924 -> 6925 -4031517 : 6934 -> 6935 -4031518 : 6934 -> 6935 4031856 : 0 -> 2191 4031857 : 0 -> 2192 -4031860 : 6944 -> 6945 -4031861 : 6944 -> 6945 -4031871 : 6350 -> 28344 +4032319 : -1 -> 21723 4032324 : 21736 -> 21737 4032339 : 0 -> 21303 @@ -38,283 +33,195 @@ INCORRECT QUESTIDS ON DB ITEMS WITH NO QUEST DROP DATA ON DB -1002436 - 2075 -1102057 - 7103 -1102061 - 3066 1302014 - 2048 -2022053 - 9330 -2022054 - 9330 -2022055 - 9330 -2022056 - 9330 -2022057 - 9332 -2022281 - 8569 -2100016 - 3223 -2100017 - 3419 -2100018 - 3236 -2100019 - 3238 -3994139 - 10360 -4001118 - 3814 -4001340 - 28167 -4001347 - 28229 -4001348 - 28231 -4001349 - 28235 -4001350 - 28235 -4001351 - 28237 -4001352 - 28206 +2022053 - 9330 EXPIRED +2022054 - 9330 EXPIRED +2022057 - 9332 EXPIRED +3994139 - 10360 EXPIRED +4000142 - 1018 4001353 - 28227 -4001366 - 28195 4031014 - 2020 -4031015 - 2022 -4031019 - 9411 +4031015 - 2020 4031020 - 2050 +4031025 - 2052 +4031026 - 2053 +4031028 - 2054 4031032 - 2051 4031039 - 2055 4031040 - 2056 4031041 - 2057 4031042 - 2035 -4031063 - 9260 +4031063 - 9260 EXPIRED 4031064 - 8012 -4031107 - 3409 -4031116 - 3419 4031117 - 3421 -4031122 - 9340 -4031124 - 9340 -4031134 - 3443 -4031136 - 3439 -4031141 - 3407 -4031142 - 3407 -4031143 - 3407 +4031122 - 9340 EXPIRED +4031124 - 9340 EXPIRED 4031144 - 2047 -4031150 - 2067 -4031157 - 2074 -4031158 - 2074 -4031165 - 2086 -4031167 - 9052 -4031168 - 9055 -4031169 - 9058 +4031167 - 9052 EXPIRED +4031168 - 9055 EXPIRED +4031169 - 9058 EXPIRED 4031180 - 8020 -4031181 - 9140 -4031182 - 9140 -4031183 - 9140 -4031184 - 9150 -4031185 - 9150 -4031186 - 9150 -4031190 - 3054 +4031181 - 9140 EXPIRED +4031182 - 9140 EXPIRED +4031183 - 9140 EXPIRED +4031184 - 9150 EXPIRED +4031185 - 9150 EXPIRED +4031186 - 9150 EXPIRED +4031190 - 3055 4031191 - 3063 -4031192 - 8700 -4031198 - 3043 -4031199 - 3046 -4031200 - 3069 +4031192 - 8700 EXPIRED +4031199 - 3045 4031201 - 3048 4031202 - 3050 4031207 - 3443 -4031220 - 9210 -4031225 - 3606 -4031226 - 9321 +4031220 - 9210 EXPIRED +4031226 - 9321 EXPIRED 4031227 - 4103 EXPIRED 4031230 - 3619 -4031231 - 3620 -4031235 - 3607 +4031235 - 3615 4031236 - 3616 -4031237 - 3605 -4031238 - 3611 +4031237 - 3617 +4031238 - 3618 4031243 - 3443 -4031257 - 9350 -4031258 - 9351 -4031270 - 3629 -4031271 - 9351 -4031272 - 9352 -4031274 - 3083 -4031275 - 3083 -4031276 - 3083 -4031277 - 3083 -4031278 - 3083 -4031280 - 3632 -4031290 - 4106 -4031291 - 4006 -4031292 - 4009 -4031293 - 4010 -4031296 - 4010 -4031297 - 9386 -4031298 - 3636 -4031301 - 9391 -4031302 - 9503 -4031303 - 4008 -4031304 - 9392 -4031321 - 9504 -4031352 - 4005 -4031354 - 4013 -4031388 - 4218 -4031418 - 8823 -4031419 - 8823 -4031420 - 8823 -4031421 - 8823 -4031425 - 8822 +4031257 - 9350 EXPIRED +4031270 - 3630 +4031271 - 9351 EXPIRED +4031272 - 9352 EXPIRED +4031280 - 3633 +4031290 - 4104 EXPIRED +4031291 - 4006 EXPIRED +4031292 - 4006 EXPIRED +4031293 - 4006 EXPIRED +4031296 - 4010 EXPIRED +4031297 - 9386 EXPIRED +4031298 - 3639 +4031301 - 9391 EXPIRED +4031302 - 9503 EXPIRED +4031303 - 4007 EXPIRED +4031304 - 9392 EXPIRED +4031321 - 9504 EXPIRED +4031352 - 4005 EXPIRED +4031354 - 4013 EXPIRED +4031388 - 4218 EXPIRED +4031418 - 8823 EXPIRED +4031419 - 8823 EXPIRED +4031420 - 8823 EXPIRED +4031421 - 8823 EXPIRED 4031448 - 6134 -4031450 - 6263 -4031452 - 6201 -4031454 - 6281 4031455 - 6280 4031456 - 6230 4031462 - 6210 4031468 - 6222 -4031471 - 6153 4031478 - 6210 4031488 - 6312 -4031495 - 6192 -4031504 - 9640 -4031505 - 9641 -4031506 - 9642 -4031507 - 6002 -4031508 - 6002 +4031504 - 9640 EXPIRED +4031505 - 9641 EXPIRED +4031506 - 9642 EXPIRED 4031554 - 3821 -4031557 - 9710 -4031558 - 9711 -4031559 - 9712 -4031560 - 9713 -4031561 - 9714 -4031563 - 8850 -4031564 - 8851 -4031565 - 8852 -4031566 - 8853 -4031567 - 8854 -4031568 - 3911 -4031570 - 3939 -4031571 - 3941 -4031574 - 3935 +4031557 - 9710 EXPIRED +4031558 - 9711 EXPIRED +4031559 - 9712 EXPIRED +4031560 - 9713 EXPIRED +4031561 - 9714 EXPIRED +4031563 - 8855 +4031564 - 8856 +4031565 - 8857 +4031566 - 8858 +4031567 - 8859 +4031570 - 3938 +4031571 - 3940 4031578 - 3923 -4031581 - 3937 -4031582 - 3901 -4031584 - 9731 -4031585 - 9732 -4031586 - 9740 -4031587 - 9741 -4031588 - 9742 -4031590 - 8881 -4031608 - 9803 -4031611 - 9804 -4031612 - 9805 +4031582 - 3949 +4031584 - 9731 EXPIRED +4031585 - 9732 EXPIRED +4031586 - 9740 EXPIRED +4031587 - 9741 EXPIRED +4031588 - 9742 EXPIRED +4031590 - 8881 EXPIRED +4031608 - 9803 EXPIRED +4031611 - 9804 EXPIRED +4031612 - 9805 EXPIRED 4031625 - 9820 -4031661 - 9861 -4031662 - 9866 -4031667 - 9863 -4031683 - 1115 -4031684 - 1116 -4031685 - 1117 -4031686 - 1118 -4031687 - 1119 -4031688 - 1120 -4031689 - 1121 -4031690 - 1122 -4031691 - 1123 -4031692 - 1124 -4031695 - 3335 +4031661 - 9861 EXPIRED +4031662 - 9866 EXPIRED +4031667 - 9863 EXPIRED 4031696 - 3334 4031697 - 3322 -4031703 - 3302 4031708 - 3309 -4031709 - 3310 -4031737 - 3343 -4031764 - 4949 -4031766 - 4959 -4031767 - 4947 -4031768 - 4953 -4031769 - 4946 -4031770 - 4946 -4031771 - 4944 +4031764 - 4949 EXPIRED +4031766 - 4959 EXPIRED +4031767 - 4947 EXPIRED +4031769 - 4946 EXPIRED +4031770 - 4946 EXPIRED 4031772 - 4942 4031774 - 3361 4031785 - 3376 -4031789 - 3844 4031796 - 3362 4031797 - 3367 -4031798 - 3366 -4031801 - 1040 -4031806 - 3380 -4031812 - 4950 -4031833 - 9946 -4031837 - 9945 +4031806 - 3379 +4031812 - 4950 EXPIRED +4031837 - 9942 EXPIRED 4031839 - 2162 -4031881 - 4484 -4031894 - 2214 +4031850 - 2180 +4031881 - 4484 EXPIRED 4031921 - 4646 -4031927 - 3454 -4031928 - 3454 -4031945 - 9987 -4032037 - 9154 -4032038 - 9154 -4032039 - 9154 -4032055 - 4675 -4032087 - 10081 -4032092 - 28003 -4032119 - 28109 +4032037 - 9154 EXPIRED +4032038 - 9154 EXPIRED +4032039 - 9154 EXPIRED +4032087 - 10081 EXPIRED +4032119 - 28109 EXPIRED 4032136 - 20710 -4032138 - 20713 +4032138 - 20712 4032142 - 20716 -4032143 - 20717 4032196 - 20528 4032197 - 20528 4032198 - 20528 -4032233 - 8298 -4032234 - 8299 -4032235 - 8299 -4032236 - 8299 -4032237 - 8299 -4032238 - 8299 -4032239 - 8299 -4032247 - 28103 -4032248 - 28108 -4032264 - 10240 -4032265 - 10241 -4032266 - 10240 -4032270 - 10241 -4032271 - 10260 -4032272 - 10268 -4032273 - 10268 -4032275 - 10261 -4032276 - 10262 -4032277 - 10263 -4032278 - 10270 -4032279 - 10271 -4032280 - 10272 -4032281 - 10270 -4032282 - 10271 -4032283 - 10272 -4032284 - 10264 -4032285 - 10265 -4032286 - 10266 -4032287 - 10267 -4032307 - 28121 -4032308 - 28122 -4032317 - 21717 -4032318 - 21718 -4032319 - 21723 -4032321 - 21727 -4032325 - 21752 +4032233 - 8298 EXPIRED +4032234 - 8299 EXPIRED +4032235 - 8299 EXPIRED +4032236 - 8299 EXPIRED +4032237 - 8299 EXPIRED +4032238 - 8299 EXPIRED +4032239 - 8298 EXPIRED +4032248 - 28108 EXPIRED +4032264 - 10240 EXPIRED +4032265 - 10241 EXPIRED +4032266 - 10240 EXPIRED +4032270 - 10241 EXPIRED +4032271 - 10260 EXPIRED +4032272 - 10268 EXPIRED +4032273 - 10268 EXPIRED +4032275 - 10261 EXPIRED +4032276 - 10262 EXPIRED +4032277 - 10263 EXPIRED +4032278 - 10270 EXPIRED +4032279 - 10271 EXPIRED +4032280 - 10272 EXPIRED +4032281 - 10270 EXPIRED +4032282 - 10271 EXPIRED +4032283 - 10272 EXPIRED +4032284 - 10264 EXPIRED +4032285 - 10265 EXPIRED +4032286 - 10266 EXPIRED +4032287 - 10267 EXPIRED +4032306 - 28120 EXPIRED +4032307 - 28121 EXPIRED +4032308 - 28122 EXPIRED +4032325 - 21751 4032326 - 21752 4032331 - 21601 4032333 - 21608 -4032335 - 21617 -4032342 - 21743 -4032348 - 10300 -4032349 - 10301 -4032350 - 10302 -4032374 - 2405 -4032376 - 2406 -4032377 - 2407 -4032378 - 2408 -4032379 - 2409 +4032348 - 10300 EXPIRED +4032349 - 10301 EXPIRED +4032350 - 10302 EXPIRED 4032401 - 2261 4032402 - 2263 -4032404 - 28128 +4032404 - 28128 EXPIRED 4032423 - 21767 -4032435 - 28307 -4032436 - 28314 -4032437 - 28321 -4032443 - 28317 -4032496 - 28238 -4032512 - 3720 -4161000 - 9322 +4032435 - 28307 EXPIRED +4032436 - 28314 EXPIRED +4032437 - 28321 EXPIRED +4161000 - 9322 EXPIRED @@ -322,87 +229,27 @@ ITEMS WITH NO QUEST DROP DATA ON DB COMPLETE QUEST ITEMS WITH ZERO QUANTITY -1018: - 4000142 - -2052: - 4031025 - -2053: - 4031026 - -2054: - 4031028 - -2167: - 4031841 - -2168: - 4031842 - -2169: - 4031843 - -2173: - 4031846 - -2180: - 4031850 - -2183: - 4031851 - -2185: - 4031852 - -3010: - 4031050 - -6340: - 4031872 - -6350: - 4031871 - -6360: - 4031869 - -6361: - 4031870 - -6380: - 4031873 - -6390: - 4031874 - 8142: 4000300 4000301 -8218: +8218 EXPIRED: 4031664 4031665 4031666 -8886: +8886 EXPIRED: 4031659 -8887: +8887 EXPIRED: 4031658 -8888: +8888 EXPIRED: 4031660 -10430: - 4220152 - 28104: 4032247 -28120: - 4032306 - diff --git a/tools/MapleQuestItemFetcher/src/maplequestitemfetcher/MapleQuestItemFetcher.java b/tools/MapleQuestItemFetcher/src/maplequestitemfetcher/MapleQuestItemFetcher.java index 8231c38585..e6879b655a 100644 --- a/tools/MapleQuestItemFetcher/src/maplequestitemfetcher/MapleQuestItemFetcher.java +++ b/tools/MapleQuestItemFetcher/src/maplequestitemfetcher/MapleQuestItemFetcher.java @@ -72,7 +72,6 @@ public class MapleQuestItemFetcher { static String password = ""; static String wzPath = "../../wz"; - static String fileName = "../../wz/Quest.wz/Act.img.xml"; static String directoryName = "../.."; static String newFile = "lib/QuestReport.txt"; @@ -134,6 +133,28 @@ public class MapleQuestItemFetcher { 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; @@ -152,24 +173,28 @@ public class MapleQuestItemFetcher { if(ii.isQuestItem(currentItemid)) { if(currentCount != 0) { if(isCompleteState == 1) { - Set qi = completeQuestItems.get(questId); - if(qi == null) { - Set newSet = new HashSet<>(); - newSet.add(currentItemid); + if(currentCount < 0) { + Set qi = completeQuestItems.get(questId); + if(qi == null) { + Set newSet = new HashSet<>(); + newSet.add(currentItemid); - completeQuestItems.put(questId, newSet); - } else { - qi.add(currentItemid); + completeQuestItems.put(questId, newSet); + } else { + qi.add(currentItemid); + } } } else { - Set qi = startQuestItems.get(questId); - if(qi == null) { - Set newSet = new HashSet<>(); - newSet.add(currentItemid); + if(currentCount > 0) { + Set qi = startQuestItems.get(questId); + if(qi == null) { + Set newSet = new HashSet<>(); + newSet.add(currentItemid); - startQuestItems.put(questId, newSet); - } else { - qi.add(currentItemid); + startQuestItems.put(questId, newSet); + } else { + qi.add(currentItemid); + } } } } else { @@ -222,7 +247,7 @@ public class MapleQuestItemFetcher { } } - private static void translateToken(String token) { + private static void translateActToken(String token) { String d; int temp; @@ -244,6 +269,8 @@ public class MapleQuestItemFetcher { if(d.contains("item")) { temp = status; inspectQuestItemList(temp); + } else { + forwardCursor(status); } } @@ -258,6 +285,37 @@ public class MapleQuestItemFetcher { } } } + + 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. @@ -289,32 +347,46 @@ public class MapleQuestItemFetcher { return list; } + private static String getTableName(boolean dropdata) { + return dropdata ? "drop_data" : "reactordrops"; + } + + private static void filterQuestDropsOnTable(Pair iq, List> 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 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> itemsWithQuest) throws SQLException { List> copyItemsWithQuest = new ArrayList<>(itemsWithQuest); try { for(Pair iq : copyItemsWithQuest) { - PreparedStatement ps = con.prepareStatement("SELECT questid FROM drop_data 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()) { - 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(); + filterQuestDropsOnTable(iq, itemsWithQuest, true); + filterQuestDropsOnTable(iq, itemsWithQuest, false); } } catch(SQLException e) { @@ -431,17 +503,30 @@ public class MapleQuestItemFetcher { private static void ReportQuestItemData() { // This will reference one line at a time String line = null; + String fileName = null; try { Class.forName(driver).newInstance(); System.out.println("Reading WZs..."); - + + fileName = wzPath + "/Quest.wz/Check.img.xml"; fileReader = new InputStreamReader(new FileInputStream(fileName), "UTF-8"); bufferedReader = new BufferedReader(fileReader); while((line = bufferedReader.readLine()) != null) { - translateToken(line); + translateCheckToken(line); // fetch expired quests through here as well + } + + bufferedReader.close(); + fileReader.close(); + + fileName = wzPath + "/Quest.wz/Act.img.xml"; + fileReader = new InputStreamReader(new FileInputStream(fileName), "UTF-8"); + bufferedReader = new BufferedReader(fileReader); + + while((line = bufferedReader.readLine()) != null) { + translateActToken(line); } bufferedReader.close(); diff --git a/wz/Quest.wz/Check.img.xml b/wz/Quest.wz/Check.img.xml index 98a6cc7275..149571986a 100644 --- a/wz/Quest.wz/Check.img.xml +++ b/wz/Quest.wz/Check.img.xml @@ -22139,12 +22139,7 @@ - - - - - - + diff --git a/wz/String.wz/Consume.img.xml b/wz/String.wz/Consume.img.xml index 0cc87d7222..1c3100271c 100644 --- a/wz/String.wz/Consume.img.xml +++ b/wz/String.wz/Consume.img.xml @@ -4743,510 +4743,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -9204,4 +8700,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +