diff --git a/README.md b/README.md index f82f4bf60a..df258ac230 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Besides myself for maintaining this repository, credits are to be given to Wizet Regarding distributability and usage of the code presented here: like it was before, this MapleStory server is open-source. By that, it is meant that anyone is **free to install, use, modify and redistribute the contents**, as long as there is **no kind of commercial trading involved** and the **credits to the original creators are maintained** within the codes. -This is a NetBeans 8.0.2 Project, that MUST be built and run under JDK/JRE 7 (1.7.0_79+) in order to run properly. This means that it's easier to install the project via opening the server project folder inside NetBeans' IDE. Once installed, build this project on your machine and run the server using the "launch.bat" application. +This is a NetBeans 8.0.2 Project, that MUST be built and run on Java 7 (JDK/JRE 1.7.0_79+) in order to run properly. This means that it's easier to install the project via opening the server project folder inside NetBeans' IDE. Once installed, build this project on your machine and run the server using the "launch.bat" application. In this project, many gameplay-wise issues generated from either the original WZ files and the server source have been partially or completely solved. Considering the use of the provided edited WZ's and server-side wz.xml files should be of the greatest importance when dealing with this instance of server source, in order to perceive it at it's full potential. My opinion, though! @@ -19,6 +19,8 @@ Server files: https://github.com/ronancpl/HeavenMS Client files & general tools: https://drive.google.com/drive/folders/0BzDsHSr-0V4MYVJ0TWIxd05hYUk +Java7 SDK: https://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html + **Important note about localhosts**: these executables are red-flagged by antivirus tools as __potentially malicious softwares__, this happens due to the reverse engineering methods that were applied onto these software artifacts. Those depicted here have been put to use for years already and posed no harm so far, so they are soundly assumed to be safe. Latest localhost: https://hostr.co/m2bVtnizCtmD @@ -81,6 +83,7 @@ It's never enough to tell this, thanks to everyone that have been contributing s Our Discord channel is still available on: https://discord.gg/Q7wKxHX + ### Donation If you REALLY liked what you have seen on the project, please feel free to donate a little something as a helping hand for my contributions towards Maple development. Also remember to **support Nexon**! diff --git a/cores/HikariCP-java7-2.4.12.jar b/cores/HikariCP-java7-2.4.12.jar deleted file mode 100644 index 792b690564..0000000000 Binary files a/cores/HikariCP-java7-2.4.12.jar and /dev/null differ diff --git a/cores/HikariCP-java7-2.4.13.jar b/cores/HikariCP-java7-2.4.13.jar new file mode 100644 index 0000000000..6506cbac92 Binary files /dev/null and b/cores/HikariCP-java7-2.4.13.jar differ diff --git a/cores/slf4j-api-1.6.6.jar b/cores/slf4j-api-1.6.6.jar deleted file mode 100644 index 4c03fa6bb2..0000000000 Binary files a/cores/slf4j-api-1.6.6.jar and /dev/null differ diff --git a/cores/slf4j-api-1.7.21.jar b/cores/slf4j-api-1.7.21.jar new file mode 100644 index 0000000000..2a5c33ec55 Binary files /dev/null and b/cores/slf4j-api-1.7.21.jar differ diff --git a/docs/mychanges_ptbr.txt b/docs/mychanges_ptbr.txt index 3f09112e5e..25788d0a90 100644 --- a/docs/mychanges_ptbr.txt +++ b/docs/mychanges_ptbr.txt @@ -1573,4 +1573,50 @@ Protegido concorrentemente sistema de comandos, assim evitando processamento em 16 Janeiro 2019, Revisado login handler, trazendo métodos de desconexão para antes de checar LoginState, e não permitindo reatribuição de objeto-cliente para jogador já logado em canal. -Revisado disconnect no login handler, agora devidamente finalizando sessões recém-criadas que falham em conectar jogador ao mundo. \ No newline at end of file +Revisado disconnect no login handler, agora devidamente finalizando sessões recém-criadas que falham em conectar jogador ao mundo. +Revisado skill Monster Magnet, agora devidamente mostrando efeitos de skill. +Mais uma tentativa de proteção contra acesso concorrente no login handler, buscando evitar múltiplas requisições concorrente por um accountId durante processo e múltiplas requisições de mesmo objeto de cliente. +Revisado objeto das expedições, não mais permitindo armazenar objetos de jogadores (que já podem ter sido desativados). +Corrigido expedições enviando pacotes para jogadores que estão fora de jogo. +Normalizado mensagens de 1o job de NPCs explorers, com afirmação de mínimo de stats antes de checar requerimentos. + +18 Janeiro 2019, +Corrigido dispose de interação com NPCs não devolvendo controle de certas ações ao jogador, mais notável quando a interação ocorreu automaticamente (jogador caminhou pra área de atuação). +Removido pigs de Maple Island, já que o quiz de 3rd job sugere não há os mesmos lá. Cynical Orange Mushrooms passam a ocupar posições dos mesmos. +Editado "launch.bat" para somente usar Java7, assim permitindo usuários a não terem que especificar preferência de uso de Java7 nas variáveis de ambiente PATH. + +20 Janeiro 2019, +Corrigido necessidade de repetir aprovação dos mestres para realizar expedições de Zakum. +Revisado comando goto. Jogadores tem acesso somente a mapas de cidade, GMs tem acesso a areas também. +Implementado EXP rate específico para novatos de nível menor que 11. +Revisado petid sendo setado fora de área de atuação (até então não compete setar isso após criado objeto de Item). + +21 - 24 Janeiro 2019, +Corrigido sistema de guilds não atualizando corretamente tooltips de informação de guild de jogadores no mapa, para ampla quantidade de eventos com guilds (adicionar player na guild, trocar emblema, remover player, etc). +Corrigido sistema de eventos permitindo entrada duplicada de eventos com mesmo nome nos registros do gerenciador de eventos, potencialmente levando a nulos em EIMs. +Corrigido skill Hyper Body cancelando efeito do buff anterior ao reutilizar a skill, levando valores suficientemente grandes de HP corrente do jogador ao MaxHP base (MaxHP antes de utilizar a skill). +Refatorado uso de evtLock para métodos que gerenciam expiração de quests, possívelmente resolvendo caso de deadlock no método de desistência de quests expiráveis. +Revisado MoveLifeHandler, não mais propagando pacotes de movimentação de mobs vindas de jogadores que não são controladores. +Refatorado completamente o sistema de aggro de mobs. Agora, métodos que atuam na atualização de aggros de mobs usam funções comuns. + +25 - 26 Janeiro 2019, +Corrigido problema de acesso a ponteiro nulo ao realizar dispose no sistema de mapas das instâncias de eventos. Referência ao gerenciador de eventos já era nulo ao atingir tal método. +Implementado retorno de estado de aggro para "sem aggro" se não houveram atividades de aggro num mob por algum tempo. +Implementado checagem de aniversário ao adicionar itens de cash às lojas de player. +Corrigido Hired Merchants não fechando devidamente ao abrir loja com itens de cash. +Corrigido tooltip de Hired Merchant não atualizando para "indisponível" quando a loja está em manutenção. +Corrigido tooltip de Player Shops não fechando corretamente se ainda havia visitantes na loja. +Mais alguns ajustes no novo sistema de aggros. Nesta implementação aparentemente somente controladores processam ataques do tipo "areaWarning" de mobs. +Corrigido função forceChangeMap não levando devidamente jogadores para o mapa dado como parâmetro, quando tal mapa é de instância de evento. +Revisado comando de summon de jogadores, agora utilizando função padrão forceChangeMap, que lida com registro de jogadores em instâncias e coloca jogador no mapa alvo. +Adicionado, no comando de summon de jogadores, habilidade de colocar jogadores em outros canais dentro do mapa de evento. + +27 - 29 Janeiro 2019, +Corrigido custom AP autoassigner não utilizando devidamente os caps definidos no escopo do handler. +Revisado custom AP autoassigner. Classes que usam DEX como atributo secundário para STR foram rebalanceados, de forma que o aumento no uso do atributo secundário permita melhores condições de ataque no jogo. +Adicionado checagem de mesos em transações entre jogadores, lojas de jogador, ao receber itens de Duey e Fredrick. +Adicionado checagem em quantidade de itens nas lojas e mercantes de jogadores, assim evitando bugs quando há mais itens a serem vendidos do que o esperado. +Corrigido um exploit com mercantes, envolvendo chamada à DB antes de retirar item (que está sendo inserido na loja) do inventário do jogador. + +30 Janeiro 2019, +Corrigido chamadas irregulares a "qm/cm" em scripts. \ No newline at end of file diff --git a/launch.bat b/launch.bat index 9f87e29e73..076a28d634 100644 --- a/launch.bat +++ b/launch.bat @@ -1,5 +1,6 @@ @echo off @title HeavenMS +set PATH=C:\Program Files\Java\jdk1.7.0_79\bin;%PATH% set CLASSPATH=.;dist\* java -Xmx2048m -Dwzpath=wz\ net.server.Server pause \ No newline at end of file diff --git a/nbproject/project.properties b/nbproject/project.properties index 62e82d2465..3c19c318ab 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -28,21 +28,21 @@ dist.jar=${dist.dir}/HeavenMS.jar dist.javadoc.dir=${dist.dir}/javadoc endorsed.classpath= excludes= -file.reference.HikariCP-java7-2.4.12.jar=cores/HikariCP-java7-2.4.12.jar +file.reference.HikariCP-java7-2.4.13.jar=cores/HikariCP-java7-2.4.13.jar file.reference.MapleSolaxia-src=src file.reference.mina-core-2.0.7.jar=cores/mina-core-2.0.7.jar file.reference.mysql-connector-java-bin.jar=cores/mysql-connector-java-bin.jar -file.reference.slf4j-api-1.6.6.jar=cores/slf4j-api-1.6.6.jar +file.reference.slf4j-api-1.7.21.jar=cores/slf4j-api-1.7.21.jar file.reference.slf4j-jdk14-1.7.5.jar=cores/slf4j-jdk14-1.7.5.jar includes=** jar.archive.disabled=${jnlp.enabled} jar.compress=true jar.index=${jnlp.enabled} javac.classpath=\ - ${file.reference.HikariCP-java7-2.4.12.jar}:\ ${file.reference.mina-core-2.0.7.jar}:\ + ${file.reference.slf4j-api-1.7.21.jar}:\ + ${file.reference.HikariCP-java7-2.4.13.jar}:\ ${file.reference.mysql-connector-java-bin.jar}:\ - ${file.reference.slf4j-api-1.6.6.jar}:\ ${file.reference.slf4j-jdk14-1.7.5.jar} # Space-separated list of extra javac options javac.compilerargs= diff --git a/scripts/npc/1012100.js b/scripts/npc/1012100.js index a132cfce17..abebcb671a 100644 --- a/scripts/npc/1012100.js +++ b/scripts/npc/1012100.js @@ -46,12 +46,7 @@ function start() { } else { if (cm.getJobId() == 0) { actionx["1stJob"] = true; - if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) - cm.sendNext("So you decided to become a #rBowman#k?"); - else { - cm.sendOk("Train a bit more until you reach #blevel 10, " + cm.getFirstJobStatRequirement(jobType) + "#k and I can show you the way of the #rBowman#k."); - cm.dispose(); - } + cm.sendNext("So you decided to become a #rbowman#k? There are some standards to meet, y'know... #bYour level should be at least 10, with at least " + cm.getFirstJobStatRequirement(jobType) + "#k. Let's see."); // thanks Vcoc for noticing a need to state and check requirements on first job adv starting message } else if (cm.getLevel() >= 30 && cm.getJobId() == 300) { actionx["2ndJob"] = true; if (cm.haveItem(4031012)) @@ -79,8 +74,13 @@ function start() { function action(mode, type, selection) { status++; - if (mode == 0 && type != 1) + if (mode == -1 && selection == -1) { + cm.dispose(); + return; + } else if (mode == 0 && type != 1) { status -= 2; + } + if (status == -1){ start(); return; @@ -116,10 +116,15 @@ function action(mode, type, selection) { } if (actionx["1stJob"]){ - if (status == 0) - cm.sendNextPrev("It is an important and final choice. You will not be able to turn back."); - else if (status == 1){ - if (cm.canHold(1452051) && cm.canHold(2060000)){ + if (status == 0) { + if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) { + cm.sendNextPrev("It is an important and final choice. You will not be able to turn back."); + } else { + cm.sendOk("Train a bit more until you reach the base requirements and I can show you the way of the #rBowman#k."); + cm.dispose(); + } + } else if (status == 1){ + if (cm.canHold(1452051) && cm.canHold(2070000)){ if (cm.getJobId() == 0){ cm.changeJobById(300); cm.gainItem(1452051, 1); diff --git a/scripts/npc/1013001.js b/scripts/npc/1013001.js index 89c035317c..ba4813df37 100644 --- a/scripts/npc/1013001.js +++ b/scripts/npc/1013001.js @@ -8,7 +8,7 @@ function action(mode, type, selection) { if (mode == 0 && type == 0) { status--; } else if (mode == -1) { - qm.dispose(); + cm.dispose(); return; } else { status++; diff --git a/scripts/npc/1022000.js b/scripts/npc/1022000.js index a112e93b0c..4d4aaeae59 100644 --- a/scripts/npc/1022000.js +++ b/scripts/npc/1022000.js @@ -47,12 +47,7 @@ function start() { } else { if (cm.getJobId() == 0) { actionx["1stJob"] = true; - if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) - cm.sendNext("Do you want to become a Warrior? You need to meet some criteria in order to do so.#b You should be at least in level 10, with at least 35 in STR#k. Let's see..."); - else { - cm.sendOk("Train a bit more until you reach #blevel 10, " + cm.getFirstJobStatRequirement(jobType) + "#k and I can show you the way of the #rWarrior#k."); - cm.dispose(); - } + cm.sendNext("Do you want to become a #rwarrior#k? You need to meet some criteria in order to do so.#b You should be at least in level 10, and at least " + cm.getFirstJobStatRequirement(jobType) + "#k. Let's see..."); // thanks Vcoc for noticing a need to state and check requirements on first job adv starting message } else if (cm.getLevel() >= 30 && cm.getJobId() == 100) { actionx["2ndJob"] = true; if (cm.haveItem(4031012)) @@ -80,8 +75,13 @@ function start() { function action(mode, type, selection) { status++; - if (mode == 0 && type != 1) + if (mode == -1 && selection == -1) { + cm.dispose(); + return; + } else if (mode == 0 && type != 1) { status -= 2; + } + if (status == -1){ start(); return; @@ -117,9 +117,14 @@ function action(mode, type, selection) { } if (actionx["1stJob"]){ - if (status == 0) - cm.sendNextPrev("It is an important and final choice. You will not be able to turn back."); - else if (status == 1){ + if (status == 0) { + if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) { + cm.sendNextPrev("It is an important and final choice. You will not be able to turn back."); + } else { + cm.sendOk("Train a bit more until you reach the base requirements and I can show you the way of the #rWarrior#k."); + cm.dispose(); + } + } else if (status == 1){ if (cm.canHold(1302077)){ if (cm.getJobId() == 0){ cm.changeJobById(100); diff --git a/scripts/npc/1032001.js b/scripts/npc/1032001.js index 1d501ebf8c..98bb35d84a 100644 --- a/scripts/npc/1032001.js +++ b/scripts/npc/1032001.js @@ -47,12 +47,7 @@ function start() { } else { if (cm.getJobId() == 0) { actionx["1stJob"] = true; - if (cm.getLevel() >= 8 && cm.canGetFirstJob(jobType)) - cm.sendNext("Want to be a magician? There are some standards to meet. because we can't just accept EVERYONE in... #bYour level should be at least 8#k, with getting INT as your top priority. Let's see."); - else { - cm.sendOk("Train a bit more until you reach #blevel 8, " + cm.getFirstJobStatRequirement(jobType) + "#k and I can show you the way of the #rMagician#k."); - cm.dispose(); - } + cm.sendNext("Want to be a #rmagician#k? There are some standards to meet. because we can't just accept EVERYONE in... #bYour level should be at least 8#k, with getting " + cm.getFirstJobStatRequirement(jobType) + " as your top priority. Let's see."); // thanks Vcoc for noticing a need to state and check requirements on first job adv starting message } else if (cm.getLevel() >= 30 && cm.getJobId() == 200) { actionx["2ndJob"] = true; if (cm.haveItem(4031012)) @@ -80,8 +75,13 @@ function start() { function action(mode, type, selection) { status++; - if (mode == 0 && type == 0) + if (mode == -1 && selection == -1) { + cm.dispose(); + return; + } else if (mode == 0 && type == 0) { status -= 2; + } + if (status == -1){ start(); return; @@ -117,9 +117,14 @@ function action(mode, type, selection) { } if (actionx["1stJob"]){ - if (status == 0) - cm.sendYesNo("Oh...! You look like someone that can definitely be a part of us... all you need is a little sinister mind, and... yeah... so, what do you think? Wanna be the Magician?"); - else if (status == 1){ + if (status == 0) { + if (cm.getLevel() >= 8 && cm.canGetFirstJob(jobType)) { + cm.sendYesNo("Oh...! You look like someone that can definitely be a part of us... all you need is a little sinister mind, and... yeah... so, what do you think? Wanna be the Magician?"); + } else { + cm.sendOk("Train a bit more until you reach the base requirements and I can show you the way of the #rMagician#k."); + cm.dispose(); + } + } else if (status == 1){ if (cm.canHold(1372043)){ if (cm.getJobId() == 0){ cm.changeJobById(200); diff --git a/scripts/npc/1052001.js b/scripts/npc/1052001.js index 3f76c7fb14..a9ae74c161 100644 --- a/scripts/npc/1052001.js +++ b/scripts/npc/1052001.js @@ -46,12 +46,7 @@ function start() { } else { if (cm.getJobId() == 0) { actionx["1stJob"] = true; - if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) - cm.sendNext("Want to be a thief? There are some standards to meet. because we can't just accept EVERYONE in... #bYour level should be at least 10, with your DEX over 25#k. Let's see."); - else { - cm.sendOk("Train a bit more until you reach #blevel 10, " + cm.getFirstJobStatRequirement(jobType) + "#k and I can show you the way of the #rThief#k."); - cm.dispose(); - } + cm.sendNext("Want to be a #rthief#k? There are some standards to meet. because we can't just accept EVERYONE in... #bYour level should be at least 10, with at least your " + cm.getFirstJobStatRequirement(jobType) + "#k. Let's see."); // thanks Vcoc for noticing a need to state and check requirements on first job adv starting message } else if (cm.getLevel() >= 30 && cm.getJobId() == 400) { actionx["2ndJob"] = true; if (cm.haveItem(4031012)) @@ -81,8 +76,13 @@ function start() { function action(mode, type, selection) { status++; - if (mode == 0 && type != 1) + if (mode == -1 && selection == -1) { + cm.dispose(); + return; + } else if (mode == 0 && type != 1) { status -= 2; + } + if (status == -1){ start(); return; @@ -118,14 +118,20 @@ function action(mode, type, selection) { } if (actionx["1stJob"]){ - if (status == 0) - cm.sendYesNo("Oh...! You look like someone that can definitely be a part of us... all you need is a little sinister mind, and... yeah... so, what do you think? Wanna be the Rogue?"); - else if (status == 1){ - if (cm.canHold(2070000) && cm.canHold(1472061)){ + if (status == 0) { + if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) + cm.sendYesNo("Oh...! You look like someone that can definitely be a part of us... all you need is a little sinister mind, and... yeah... so, what do you think? Wanna be the Rogue?"); + else { + cm.sendOk("Train a bit more until you reach the base requirements and I can show you the way of the #rThief#k."); + cm.dispose(); + } + } else if (status == 1){ + if (cm.canHold(2070000) && cm.canHoldAll([1472061, 1332063])){ if (cm.getJobId() == 0){ cm.changeJobById(400); - cm.gainItem(2070000, 500); + cm.gainItem(2070015, 500); cm.gainItem(1472061, 1); + cm.gainItem(1332063, 1); cm.resetStats(); } cm.sendNext("Alright, from here out, you are a part of us! You'll be living the life of a wanderer at ..., but just be patient as soon, you'll be living the high life. Alright, it ain't much, but I'll give you some of my abilities... HAAAHHH!!!"); diff --git a/scripts/npc/1061014.js b/scripts/npc/1061014.js index 8d6e5b4932..b7c51c4891 100644 --- a/scripts/npc/1061014.js +++ b/scripts/npc/1061014.js @@ -29,6 +29,7 @@ importPackage(Packages.scripting.event); var status = 0; var expedition; +var expedMembers; var player; var em; var exped = MapleExpeditionType.BALROG_NORMAL; @@ -68,7 +69,7 @@ function action(mode, type, selection) { status = 2; } else if (expedition.isRegistering()) { //If the expedition is registering if (expedition.contains(player)) { //If you're in it but it hasn't started, be patient - cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin the expedition."); + cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin it."); cm.dispose(); } else { //If you aren't in it, you're going to get added cm.sendOk(expedition.addMember(cm.getPlayer())); @@ -120,7 +121,8 @@ function action(mode, type, selection) { cm.dispose(); return; } - var size = expedition.getMembers().size(); + expedMembers = expedition.getMemberList(); + var size = expedMembers.size(); if (size == 1) { cm.sendOk("You are the only member of the expedition."); cm.dispose(); @@ -129,13 +131,13 @@ function action(mode, type, selection) { var text = "The following members make up your expedition (Click on them to expel them):\r\n"; text += "\r\n\t\t1." + expedition.getLeader().getName(); for (var i = 1; i < size; i++) { - text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedition.getMembers().get(i).getName() + "#l\n"; + text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedMembers.get(i).getValue() + "#l\n"; } cm.sendSimple(text); status = 6; } else if (selection == 2) { var min = exped.getMinSize(); - var size = expedition.getMembers().size(); + var size = expedition.getMemberList().size(); if (size < min) { cm.sendOk("You need at least " + min + " players registered in your expedition."); cm.dispose(); @@ -170,7 +172,7 @@ function action(mode, type, selection) { return; } else if (status == 6) { if (selection > 0) { - var banned = expedition.getMembers().get(selection - 1); + var banned = expedMembers.get(selection - 1); expedition.ban(banned); cm.sendOk("You have banned " + banned.getName() + " from the expedition."); cm.dispose(); diff --git a/scripts/npc/1090000.js b/scripts/npc/1090000.js index 8595f29c3f..60b573219d 100644 --- a/scripts/npc/1090000.js +++ b/scripts/npc/1090000.js @@ -77,12 +77,7 @@ function start() { } else { if (cm.getJobId() == 0) { actionx["1stJob"] = true; - if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) - cm.sendNext("Want to be a pirate? There are some standards to meet. because we can't just accept EVERYONE in... #bYour level should be at least 10#k. Let's see."); - else { - cm.sendOk("Train a bit more until you reach #blevel 10, " + cm.getFirstJobStatRequirement(jobType) + "#k and I can show you the way of the #rPirate#k."); - cm.dispose(); - } + cm.sendNext("Want to be a #rpirate#k? There are some standards to meet. because we can't just accept EVERYONE in... #bYour level should be at least 10, with " + cm.getFirstJobStatRequirement(jobType) + " minimum#k. Let's see."); // thanks Vcoc for noticing a need to state and check requirements on first job adv starting message } else if (cm.getLevel() >= 30 && cm.getJobId() == 500) { actionx["2ndJob"] = true; if (cm.isQuestCompleted(2191) || cm.isQuestCompleted(2192)) @@ -107,8 +102,13 @@ function start() { function action(mode, type, selection) { status++; - if (mode == 0 && type != 1) + if (mode == -1 && selection == -1) { + cm.dispose(); + return; + } else if (mode == 0 && type != 1) { status -= 2; + } + if (status == -1){ start(); return; @@ -161,10 +161,15 @@ function action(mode, type, selection) { } if (actionx["1stJob"]){ - if (status == 0) - cm.sendYesNo("Oh...! You look like someone that can definitely be a part of us... all you need is a little slang, and... yeah... so, what do you think? Wanna be the Pirate?"); - else if (status == 1){ - if (cm.canHold(2070000) && cm.canHold(1472061)){ + if (status == 0) { + if (cm.getLevel() >= 10 && cm.canGetFirstJob(jobType)) { + cm.sendYesNo("Oh...! You look like someone that can definitely be a part of us... all you need is a little slang, and... yeah... so, what do you think? Wanna be the Pirate?"); + } else { + cm.sendOk("Train a bit more until you reach the base requirements and I can show you the way of the #rPirate#k."); + cm.dispose(); + } + } else if (status == 1){ + if (cm.canHold(2070000) && cm.canHoldAll([1482000, 1492000])){ if (cm.getJobId() == 0){ cm.changeJobById(500); cm.gainItem(1492000, 1); diff --git a/scripts/npc/2020008.js b/scripts/npc/2020008.js index 2ce3f079ad..bfe4b52f82 100644 --- a/scripts/npc/2020008.js +++ b/scripts/npc/2020008.js @@ -141,7 +141,7 @@ function action(mode, type, selection){ } else { if (cm.getPlayer().getLevel() >= 50){ cm.sendNext("Ok, go."); - if(!cm.isQuestStarted(100200)) cm.startQuest(100200); + if(!(cm.isQuestStarted(100200) || cm.isQuestCompleted(100200))) cm.startQuest(100200); if(Packages.constants.ServerConstants.USE_ENABLE_SOLO_EXPEDITIONS && !cm.isQuestCompleted(100201)) cm.completeQuest(100201); }else cm.sendNext("You're weak."); diff --git a/scripts/npc/2020009.js b/scripts/npc/2020009.js index 39ab767c6c..a44bdcf957 100644 --- a/scripts/npc/2020009.js +++ b/scripts/npc/2020009.js @@ -113,7 +113,7 @@ function action(mode, type, selection){ } else { if (cm.getPlayer().getLevel() >= 50){ cm.sendNext("Ok, go."); - if(!cm.isQuestStarted(100200)) cm.startQuest(100200); + if(!(cm.isQuestStarted(100200) || cm.isQuestCompleted(100200))) cm.startQuest(100200); if(Packages.constants.ServerConstants.USE_ENABLE_SOLO_EXPEDITIONS && !cm.isQuestCompleted(100201)) cm.completeQuest(100201); }else cm.sendNext("You're weak."); diff --git a/scripts/npc/2020010.js b/scripts/npc/2020010.js index cab227ff6f..6c51b76405 100644 --- a/scripts/npc/2020010.js +++ b/scripts/npc/2020010.js @@ -114,7 +114,7 @@ function action(mode, type, selection){ } else { if (cm.getPlayer().getLevel() >= 50){ cm.sendNext("Ok, go."); - if(!cm.isQuestStarted(100200)) cm.startQuest(100200); + if(!(cm.isQuestStarted(100200) || cm.isQuestCompleted(100200))) cm.startQuest(100200); if(Packages.constants.ServerConstants.USE_ENABLE_SOLO_EXPEDITIONS && !cm.isQuestCompleted(100201)) cm.completeQuest(100201); }else cm.sendNext("You're weak."); diff --git a/scripts/npc/2020011.js b/scripts/npc/2020011.js index a85730f09d..850b295c62 100644 --- a/scripts/npc/2020011.js +++ b/scripts/npc/2020011.js @@ -113,7 +113,7 @@ function action(mode, type, selection){ } else { if (cm.getPlayer().getLevel() >= 50){ cm.sendNext("Ok, go."); - if(!cm.isQuestStarted(100200)) cm.startQuest(100200); + if(!(cm.isQuestStarted(100200) || cm.isQuestCompleted(100200))) cm.startQuest(100200); if(Packages.constants.ServerConstants.USE_ENABLE_SOLO_EXPEDITIONS && !cm.isQuestCompleted(100201)) cm.completeQuest(100201); }else cm.sendNext("You're weak."); diff --git a/scripts/npc/2020013.js b/scripts/npc/2020013.js index edec8efb1a..1521740211 100644 --- a/scripts/npc/2020013.js +++ b/scripts/npc/2020013.js @@ -112,7 +112,7 @@ function action(mode, type, selection){ } else { if (cm.getPlayer().getLevel() >= 50){ cm.sendNext("Ok, go."); - if(!cm.isQuestStarted(100200)) cm.startQuest(100200); + if(!(cm.isQuestStarted(100200) || cm.isQuestCompleted(100200))) cm.startQuest(100200); if(Packages.constants.ServerConstants.USE_ENABLE_SOLO_EXPEDITIONS && !cm.isQuestCompleted(100201)) cm.completeQuest(100201); }else cm.sendNext("You're weak."); diff --git a/scripts/npc/2030006.js b/scripts/npc/2030006.js index 6cc117e1e1..f05dfb3945 100644 --- a/scripts/npc/2030006.js +++ b/scripts/npc/2030006.js @@ -32,14 +32,14 @@ var questionTree = [ //Questions Related to ITEMS ["Which of following monsters got CORRECT item corresponding to the monster?", ["Royal cactus - Needle", "Wild Boar - Boar fang", "Lazy Buffy - Buffy hat", "Chipmunk - Nut", "Stirge - Stirge's wing"], 4], ["Which of following monsters got WRONG item corresponding to the monster?", ["Greatest Oldies - Greatest oldies", "Nependeath - Nependeath's leaf", "Ghost stump - Seedling", "Sparker - Seal tooth", "Miner Zombie - Zombie's lost tooth"], 1], - ["In GM Event, how many FRUIT CAKE you can get as reward?", ["20", "200", "5", "25", "100"], 2], + //["In GM Event, how many FRUIT CAKE you can get as reward?", ["20", "200", "5", "25", "100"], 2], ["Which of following potions got CORRECT info.?", ["Warrior Elixir - Attack +5 for 3 minutes", "Pure Water - Recover 700 MP", "Cake - Recover 150 HP & MP", "Salad - Recover 300 MP", "Pizza - Recover 400 HP"], 4], ["Which of following potions got WRONG info.?", ["Mana Elixir - Recover 300 MP", "Tonic - Cures state of weakness", "Apple - Recover 30 HP", "Sunrise Dew - Recover 3000 MP", "Ramen - Recover 1000 HP"], 3], //Questions Related to MONSTERS ["Green Mushroom, Tree Stump, Bubbling, Axe Stump, Octopus, which is highest level of all?", ["Tree Stump", "Bubbling", "Axe Stump", "Octopus", "Green Mushroom"], 2], ["Which monster will be seen during the ship trip to Orbis/Ellinia?", ["Werewolf", "Slime", "Crimson Balrog", "Zakum", "Star Pixie"], 2], - ["Maple Island doesn't have which following monsters?", ["Green Mushroom", "Blue Snail", "Orange Mushroom", "Red Snail", "Pig"], 0], + ["Maple Island doesn't have which following monsters?", ["Shroom", "Blue Snail", "Slime", "Red Snail", "Pig"], 4], // to get conformant with website answers, thanks to Vcoc ["Which monster is not at Victoria Island and Sleepywood?", ["Evil Eye", "Sentinel", "Jr. Balrog", "Ghost Stump", "Snail"], 1], ["El Nath doesn't have which following monsters?", ["Dark Yeti", "Dark Ligator", "Yeti & Pepe", "Bain", "Coolie Zombie"], 1], ["Which of following monsters can fly?", ["Malady", "Ligator", "Cold Eye", "Meerkat", "Alishar"], 0], diff --git a/scripts/npc/2030008.js b/scripts/npc/2030008.js index 31a3afa6f4..c836e0234d 100644 --- a/scripts/npc/2030008.js +++ b/scripts/npc/2030008.js @@ -58,7 +58,7 @@ function action(mode, type, selection) { return; } - if(!cm.isQuestStarted(100200)) { + if(!(cm.isQuestStarted(100200) || cm.isQuestCompleted(100200))) { // thanks Vcoc for finding out a need of reapproval from the masters for Zakum expeditions cm.sendOk("Beware, for the power of olde has not been forgotten... "); cm.dispose(); return; diff --git a/scripts/npc/2030013.js b/scripts/npc/2030013.js index 77d0586a59..ef61b1d339 100644 --- a/scripts/npc/2030013.js +++ b/scripts/npc/2030013.js @@ -29,6 +29,7 @@ importPackage(Packages.scripting.event); var status = 0; var expedition; +var expedMembers; var player; var em; var exped = MapleExpeditionType.ZAKUM; @@ -69,7 +70,7 @@ function action(mode, type, selection) { status = 2; } else if (expedition.isRegistering()) { //If the expedition is registering if (expedition.contains(player)) { //If you're in it but it hasn't started, be patient - cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin the expedition."); + cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin it."); cm.dispose(); } else { //If you aren't in it, you're going to get added cm.sendOk(expedition.addMember(cm.getPlayer())); @@ -121,7 +122,8 @@ function action(mode, type, selection) { cm.dispose(); return; } - var size = expedition.getMembers().size(); + expedMembers = expedition.getMemberList(); + var size = expedMembers.size(); if (size == 1) { cm.sendOk("You are the only member of the expedition."); cm.dispose(); @@ -130,14 +132,14 @@ function action(mode, type, selection) { var text = "The following members make up your expedition (Click on them to expel them):\r\n"; text += "\r\n\t\t1." + expedition.getLeader().getName(); for (var i = 1; i < size; i++) { - text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedition.getMembers().get(i).getName() + "#l\n"; + text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedMembers.get(i).getValue() + "#l\n"; } cm.sendSimple(text); status = 6; } else if (selection == 2) { var min = exped.getMinSize(); - var size = expedition.getMembers().size(); + var size = expedition.getMemberList().size(); if (size < min) { cm.sendOk("You need at least " + min + " players registered in your expedition."); cm.dispose(); @@ -172,7 +174,7 @@ function action(mode, type, selection) { return; } else if (status == 6) { if (selection > 0) { - var banned = expedition.getMembers().get(selection - 1); + var banned = expedMembers.get(selection - 1); expedition.ban(banned); cm.sendOk("You have banned " + banned.getName() + " from the expedition."); cm.dispose(); diff --git a/scripts/npc/2083004.js b/scripts/npc/2083004.js index 6194b60125..17520857e5 100644 --- a/scripts/npc/2083004.js +++ b/scripts/npc/2083004.js @@ -29,6 +29,7 @@ importPackage(Packages.scripting.event); var status = 0; var expedition; +var expedMembers; var player; var em; var exped = MapleExpeditionType.HORNTAIL; @@ -67,7 +68,7 @@ function action(mode, type, selection) { status = 2; } else if (expedition.isRegistering()) { //If the expedition is registering if (expedition.contains(player)) { //If you're in it but it hasn't started, be patient - cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin the expedition."); + cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin it."); cm.dispose(); } else { //If you aren't in it, you're going to get added cm.sendOk(expedition.addMember(cm.getPlayer())); @@ -113,7 +114,8 @@ function action(mode, type, selection) { cm.dispose(); return; } - var size = expedition.getMembers().size(); + expedMembers = expedition.getMemberList(); + var size = expedMembers.size(); if (size == 1) { cm.sendOk("You are the only member of the expedition."); cm.dispose(); @@ -122,14 +124,14 @@ function action(mode, type, selection) { var text = "The following members make up your expedition (Click on them to expel them):\r\n"; text += "\r\n\t\t1." + expedition.getLeader().getName(); for (var i = 1; i < size; i++) { - text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedition.getMembers().get(i).getName() + "#l\n"; + text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedMembers.get(i).getValue() + "#l\n"; } cm.sendSimple(text); status = 6; } else if (selection == 2) { var min = exped.getMinSize(); - var size = expedition.getMembers().size(); + var size = expedition.getMemberList().size(); if (size < min) { cm.sendOk("You need at least " + min + " players registered in your expedition."); cm.dispose(); @@ -164,7 +166,7 @@ function action(mode, type, selection) { return; } else if (status == 6) { if (selection > 0) { - var banned = expedition.getMembers().get(selection - 1); + var banned = expedMembers.get(selection - 1); expedition.ban(banned); cm.sendOk("You have banned " + banned.getName() + " from the expedition."); cm.dispose(); diff --git a/scripts/npc/2141001.js b/scripts/npc/2141001.js index 210faf5fa7..9772b05adc 100644 --- a/scripts/npc/2141001.js +++ b/scripts/npc/2141001.js @@ -31,6 +31,7 @@ importPackage(Packages.scripting.event); var status = 0; var expedition; +var expedMembers; var player; var em; var exped = MapleExpeditionType.PINKBEAN; @@ -70,7 +71,7 @@ function action(mode, type, selection) { status = 2; } else if (expedition.isRegistering()) { //If the expedition is registering if (expedition.contains(player)) { //If you're in it but it hasn't started, be patient - cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin the expedition."); + cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin it."); cm.dispose(); } else { //If you aren't in it, you're going to get added cm.sendOk(expedition.addMember(cm.getPlayer())); @@ -116,7 +117,8 @@ function action(mode, type, selection) { cm.dispose(); return; } - var size = expedition.getMembers().size(); + expedMembers = expedition.getMemberList(); + var size = expedMembers.size(); if (size == 1) { cm.sendOk("You are the only member of the expedition."); cm.dispose(); @@ -125,14 +127,14 @@ function action(mode, type, selection) { var text = "The following members make up your expedition (Click on them to expel them):\r\n"; text += "\r\n\t\t1." + expedition.getLeader().getName(); for (var i = 1; i < size; i++) { - text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedition.getMembers().get(i).getName() + "#l\n"; + text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedMembers.get(i).getValue() + "#l\n"; } cm.sendSimple(text); status = 6; } else if (selection == 2) { var min = exped.getMinSize(); - var size = expedition.getMembers().size(); + var size = expedition.getMemberList().size(); if (size < min) { cm.sendOk("You need at least " + min + " players registered in your expedition."); cm.dispose(); @@ -167,7 +169,7 @@ function action(mode, type, selection) { return; } else if (status == 6) { if (selection > 0) { - var banned = expedition.getMembers().get(selection - 1); + var banned = expedMembers.get(selection - 1); expedition.ban(banned); cm.sendOk("You have banned " + banned.getName() + " from the expedition."); cm.dispose(); diff --git a/scripts/npc/9120201.js b/scripts/npc/9120201.js index 09e41bf0db..4f3b233776 100644 --- a/scripts/npc/9120201.js +++ b/scripts/npc/9120201.js @@ -28,6 +28,7 @@ importPackage(Packages.scripting.event); var status = 0; var expedition; +var expedMembers; var player; var em; var exped = MapleExpeditionType.SHOWA; @@ -68,7 +69,7 @@ function action(mode, type, selection) { status = 2; } else if (expedition.isRegistering()) { //If the expedition is registering if (expedition.contains(player)) { //If you're in it but it hasn't started, be patient - cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin the expedition."); + cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin it."); cm.dispose(); } else { //If you aren't in it, you're going to get added cm.sendOk(expedition.addMember(cm.getPlayer())); @@ -120,7 +121,8 @@ function action(mode, type, selection) { cm.dispose(); return; } - var size = expedition.getMembers().size(); + expedMembers = expedition.getMemberList(); + var size = expedMembers.size(); if (size == 1) { cm.sendOk("You are the only member of the expedition."); cm.dispose(); @@ -129,14 +131,14 @@ function action(mode, type, selection) { var text = "The following members make up your expedition (Click on them to expel them):\r\n"; text += "\r\n\t\t1." + expedition.getLeader().getName(); for (var i = 1; i < size; i++) { - text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedition.getMembers().get(i).getName() + "#l\n"; + text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedMembers.get(i).getValue() + "#l\n"; } cm.sendSimple(text); status = 6; } else if (selection == 2) { var min = exped.getMinSize(); - var size = expedition.getMembers().size(); + var size = expedition.getMemberList().size(); if (size < min) { cm.sendOk("You need at least " + min + " players registered in your expedition."); cm.dispose(); @@ -171,7 +173,7 @@ function action(mode, type, selection) { return; } else if (status == 6) { if (selection > 0) { - var banned = expedition.getMembers().get(selection - 1); + var banned = expedMembers.get(selection - 1); expedition.ban(banned); cm.sendOk("You have banned " + banned.getName() + " from the expedition."); cm.dispose(); diff --git a/scripts/npc/9201014.js b/scripts/npc/9201014.js index 6713b15fcf..9fd36866af 100644 --- a/scripts/npc/9201014.js +++ b/scripts/npc/9201014.js @@ -61,7 +61,7 @@ function action(mode, type, selection) { } else if (status == 1) { if (selection == 0) { if (cm.haveItem(4031424)) { - if (cm.isMarried()) { + if (cm.getPlayer().isMarried()) { // thanks MedicOP for solving an issue here if(cm.getInventory(2).getNextFreeSlot() >= 0) { var rand = Math.floor(Math.random() * bgPrizes.length); cm.gainItem(bgPrizes[rand][0], bgPrizes[rand][1]); diff --git a/scripts/npc/9201113.js b/scripts/npc/9201113.js index d513e4e4fb..4cd1baca90 100644 --- a/scripts/npc/9201113.js +++ b/scripts/npc/9201113.js @@ -29,6 +29,7 @@ importPackage(Packages.scripting.event); var status = 0; var expedition; +var expedMembers; var player; var em; var cwkpq = MapleExpeditionType.CWKPQ; @@ -64,7 +65,7 @@ function action(mode, type, selection) { status = 2; } else if (expedition.isRegistering()) { //If the expedition is registering if (expedition.contains(player)) { //If you're in it but it hasn't started, be patient - cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin the expedition."); + cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin it."); cm.dispose(); } else { //If you aren't in it, you're going to get added cm.sendOk(expedition.addMember(cm.getPlayer())); @@ -104,7 +105,8 @@ function action(mode, type, selection) { cm.dispose(); return; } - var size = expedition.getMembers().size(); + expedMembers = expedition.getMemberList(); + var size = expedMembers.size(); if (size == 1) { cm.sendOk("You are the only member of the expedition."); cm.dispose(); @@ -113,13 +115,13 @@ function action(mode, type, selection) { var text = "The following members make up your expedition (Click on them to expel them):\r\n"; text += "\r\n\t\t1." + expedition.getLeader().getName(); for (var i = 1; i < size; i++) { - text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedition.getMembers().get(i).getName() + "#l\n"; + text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedMembers.get(i).getValue() + "#l\n"; } cm.sendSimple(text); status = 6; } else if (selection == 2) { var min = cwkpq.getMinSize(); - var size = expedition.getMembers().size(); + var size = expedition.getMemberList().size(); if (size < min) { cm.sendOk("You need at least " + min + " players registered in your expedition."); cm.dispose(); @@ -154,7 +156,7 @@ function action(mode, type, selection) { return; } else if (status == 6) { if (selection > 0) { - var banned = expedition.getMembers().get(selection - 1); + var banned = expedMembers.get(selection - 1); expedition.ban(banned); cm.sendOk("You have banned " + banned.getName() + " from the expedition."); cm.dispose(); diff --git a/scripts/npc/9270047.js b/scripts/npc/9270047.js index a84ced3482..4d7288d24e 100644 --- a/scripts/npc/9270047.js +++ b/scripts/npc/9270047.js @@ -29,6 +29,7 @@ importPackage(Packages.scripting.event); var status = 0; var expedition; +var expedMembers; var player; var em; var exped = MapleExpeditionType.SCARGA; @@ -69,7 +70,7 @@ function action(mode, type, selection) { status = 2; } else if (expedition.isRegistering()) { //If the expedition is registering if (expedition.contains(player)) { //If you're in it but it hasn't started, be patient - cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin the expedition."); + cm.sendOk("You have already registered for the expedition. Please wait for #r" + expedition.getLeader().getName() + "#k to begin it."); cm.dispose(); } else { //If you aren't in it, you're going to get added cm.sendOk(expedition.addMember(cm.getPlayer())); @@ -121,7 +122,8 @@ function action(mode, type, selection) { cm.dispose(); return; } - var size = expedition.getMembers().size(); + expedMembers = expedition.getMemberList(); + var size = expedMembers.size(); if (size == 1) { cm.sendOk("You are the only member of the expedition."); cm.dispose(); @@ -130,13 +132,13 @@ function action(mode, type, selection) { var text = "The following members make up your expedition (Click on them to expel them):\r\n"; text += "\r\n\t\t1." + expedition.getLeader().getName(); for (var i = 1; i < size; i++) { - text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedition.getMembers().get(i).getName() + "#l\n"; + text += "\r\n#b#L" + (i + 1) + "#" + (i + 1) + ". " + expedMembers.get(i).getValue() + "#l\n"; } cm.sendSimple(text); status = 6; } else if (selection == 2) { var min = exped.getMinSize(); - var size = expedition.getMembers().size(); + var size = expedition.getMemberList().size(); if (size < min) { cm.sendOk("You need at least " + min + " players registered in your expedition."); cm.dispose(); @@ -171,7 +173,7 @@ function action(mode, type, selection) { return; } else if (status == 6) { if (selection > 0) { - var banned = expedition.getMembers().get(selection - 1); + var banned = expedMembers.get(selection - 1); expedition.ban(banned); cm.sendOk("You have banned " + banned.getName() + " from the expedition."); cm.dispose(); diff --git a/scripts/npc/9977777.js b/scripts/npc/9977777.js index d0136b8e9c..e3c07313a7 100644 --- a/scripts/npc/9977777.js +++ b/scripts/npc/9977777.js @@ -77,6 +77,7 @@ function writeFeatureTab_PlayerSocialNetwork() { addFeature("P. members' HPBar accounts HP gain on equips."); addFeature("Thoroughly reviewed P. Shops and H. Merchants."); addFeature("Transactions on Merchs instantly announced to owner."); + addFeature("Proper meso space check on player transactions."); addFeature("Game minirooms with functional pw system."); addFeature("Proper item pickup cooldown on non-owned items."); addFeature("Improved ranking system, with daily movement."); @@ -121,6 +122,7 @@ function writeFeatureTab_MonstersMapsReactors() { addFeature("Added meso drop data for many missing mobs."); addFeature("Monsterbook displays updated drop data info."); addFeature("Every skill/mastery book is now obtainable."); + addFeature("Enhanced aggro system: real-time DPS aggro detection."); addFeature("Mobs now can drop more than one of the same equip."); addFeature("Mobs only drop items collectable by the player/party."); addFeature("Mobs shouldn't fall from foothold too often now."); @@ -146,6 +148,7 @@ function writeFeatureTab_MonstersMapsReactors() { addFeature("Added world maps for M. Castle, W. Tour & Ellin areas."); addFeature("Added W. Tour & Masteria continents in the world map."); addFeature("Reviewed several issues with W. Map tooltips & links."); + addFeature("Continent separated global drops."); addFeature("Giant Cake boss drops s. bags and Maple items."); } diff --git a/scripts/portal/Zakum05.js b/scripts/portal/Zakum05.js index 1afbeb322c..7f0f674aab 100644 --- a/scripts/portal/Zakum05.js +++ b/scripts/portal/Zakum05.js @@ -24,7 +24,7 @@ */ function enter(pi) { - if (!pi.isQuestStarted(100200)) { + if (!(pi.isQuestStarted(100200) || pi.isQuestCompleted(100200))) { pi.getPlayer().dropMessage(5,"You need approval from the masters to battle. You may not attempt the boss right now."); return false; } diff --git a/scripts/quest/20101.js b/scripts/quest/20101.js index f6d2107782..9af9d25a2f 100644 --- a/scripts/quest/20101.js +++ b/scripts/quest/20101.js @@ -27,6 +27,12 @@ function end(mode, type, selection) { return; } + if (!(qm.canHoldAll([1302077, 1142066]))) { + qm.sendOk("Make some room in your inventory and talk back to me."); + qm.dispose(); + return; + } + qm.sendNext("I have just molded your body to make it perfect for a Dawn Warrior. If you wish to become more powerful, use Stat Window (S) to raise the appropriate stats. If you aren't sure what to raise, just click on #bAuto#k."); if (qm.getPlayer().getJob().getId() != 1100) { qm.gainItem(1302077, 1); diff --git a/scripts/quest/20102.js b/scripts/quest/20102.js index f4bd37ddcb..cbdf5aee82 100644 --- a/scripts/quest/20102.js +++ b/scripts/quest/20102.js @@ -27,6 +27,12 @@ function end(mode, type, selection) { return; } + if (!(qm.canHoldAll([1372043, 1142066]))) { + qm.sendOk("Make some room in your inventory and talk back to me."); + qm.dispose(); + return; + } + qm.sendNext("I have just molded your body to make it perfect for a Blaze Wizard. If you wish to become more powerful, use Stat Window (S) to raise the appropriate stats. If you aren't sure what to raise, just click on #bAuto#k."); if (qm.getPlayer().getJob().getId() != 1200) { qm.gainItem(1372043, 1); diff --git a/scripts/quest/20103.js b/scripts/quest/20103.js index 03d54ab4e8..9d603bf429 100644 --- a/scripts/quest/20103.js +++ b/scripts/quest/20103.js @@ -27,6 +27,12 @@ function end(mode, type, selection) { return; } + if (!(qm.canHoldAll([1452051, 1142066]) && qm.canHold(2070000))) { + qm.sendOk("Make some room in your inventory and talk back to me."); + qm.dispose(); + return; + } + qm.sendNext("I have just molded your body to make it perfect for a Wind Archer. If you wish to become more powerful, use Stat Window (S) to raise the appropriate stats. If you aren't sure what to raise, just click on #bAuto#k."); if (qm.getPlayer().getJob().getId() != 1300) { qm.gainItem(2060000, 2000); diff --git a/scripts/quest/20104.js b/scripts/quest/20104.js index a0fc3311a0..232a97ad5d 100644 --- a/scripts/quest/20104.js +++ b/scripts/quest/20104.js @@ -27,11 +27,16 @@ function end(mode, type, selection) { return; } + if (!(qm.canHoldAll([1472061, 1142066]) && qm.canHold(2070000))) { + qm.sendOk("Make some room in your inventory and talk back to me."); + qm.dispose(); + return; + } + qm.sendNext("I have just molded your body to make it perfect for a Night Walker. If you wish to become more powerful, use Stat Window (S) to raise the appropriate stats. If you aren't sure what to raise, just click on #bAuto#k."); if (qm.getPlayer().getJob().getId() != 1400) { qm.gainItem(1472061, 1); - qm.gainItem(2070015, 800); - qm.gainItem(2070015, 800); + qm.gainItem(2070000, 800); qm.gainItem(1142066, 1); qm.changeJob(MapleJob.NIGHTWALKER1); qm.getPlayer().resetStats(); diff --git a/scripts/quest/20105.js b/scripts/quest/20105.js index 5de72b98f4..6580c083c4 100644 --- a/scripts/quest/20105.js +++ b/scripts/quest/20105.js @@ -27,6 +27,12 @@ function end(mode, type, selection) { return; } + if (!(qm.canHoldAll([1482014, 1142066]))) { + qm.sendOk("Make some room in your inventory and talk back to me."); + qm.dispose(); + return; + } + qm.sendNext("I have just molded your body to make it perfect for a Thunder Breaker. If you wish to become more powerful, use Stat Window (S) to raise the appropriate stats. If you aren't sure what to raise, just click on #bAuto#k."); if (qm.getPlayer().getJob().getId() != 1500) { qm.gainItem(1482014, 1); diff --git a/scripts/quest/21101.js b/scripts/quest/21101.js index 74815877db..e59c0bb9a6 100644 --- a/scripts/quest/21101.js +++ b/scripts/quest/21101.js @@ -39,7 +39,7 @@ function start(mode, type, selection) { } else if (status == 1) { if (qm.getPlayer().getJob().getId() == 2000) { if(!qm.canHold(1142129)) { - cm.sendOk("Wow, your #bequip#k inventory is full. You need to make at least 1 empty slot to complete this quest."); + qm.sendOk("Wow, your #bequip#k inventory is full. You need to make at least 1 empty slot to complete this quest."); qm.dispose(); return; } diff --git a/scripts/quest/21201.js b/scripts/quest/21201.js index da8713a1cd..4d8f0f8d06 100644 --- a/scripts/quest/21201.js +++ b/scripts/quest/21201.js @@ -59,7 +59,7 @@ function end(mode, type, selection) { else if (status == 8) { if(!qm.isQuestCompleted(21201)) { if(!qm.canHold(1142130)) { - cm.sendOk("Wow, your #bequip#k inventory is full. I need you to make at least 1 empty slot to complete this quest."); + qm.sendOk("Wow, your #bequip#k inventory is full. I need you to make at least 1 empty slot to complete this quest."); // thanks MedicOP for finding an issue here qm.dispose(); return; } diff --git a/scripts/quest/21302.js b/scripts/quest/21302.js index 0d3e60695c..35556f9587 100644 --- a/scripts/quest/21302.js +++ b/scripts/quest/21302.js @@ -39,7 +39,7 @@ function end(mode, type, selection) { } else if (status == 2) { if(!qm.isQuestCompleted(21302)) { if(!qm.canHold(1142131)) { - cm.sendOk("Wow, your #bequip#k inventory is full. I need you to make at least 1 empty slot to complete this quest."); + qm.sendOk("Wow, your #bequip#k inventory is full. I need you to make at least 1 empty slot to complete this quest."); qm.dispose(); return; } diff --git a/sql/db_database.sql b/sql/db_database.sql index fc3c325ec5..aae28570c9 100644 --- a/sql/db_database.sql +++ b/sql/db_database.sql @@ -12776,8 +12776,7 @@ INSERT IGNORE INTO `temp_data` (`id`, `dropperid`, `itemid`, `minimum_quantity`, CREATE TABLE IF NOT EXISTS `drop_data_global` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, - `continent` int(11) NOT NULL, - `dropType` tinyint(1) NOT NULL DEFAULT '0', + `continent` tinyint(1) NOT NULL DEFAULT '-1', `itemid` int(11) NOT NULL DEFAULT '0', `minimum_quantity` int(11) NOT NULL DEFAULT '1', `maximum_quantity` int(11) NOT NULL DEFAULT '1', @@ -12788,12 +12787,12 @@ CREATE TABLE IF NOT EXISTS `drop_data_global` ( KEY `mobid` (`continent`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=DYNAMIC AUTO_INCREMENT=5 ; -INSERT INTO `drop_data_global` (`id`, `continent`, `dropType`, `itemid`, `minimum_quantity`, `maximum_quantity`, `questid`, `chance`, `comments`) VALUES -(1, 0, 0, 4031865, 1, 1, 0, 35000, 'NX Card 100 PTS'), -(2, 0, 0, 4031866, 1, 1, 0, 20000, 'NX Card 250 PTS'), -(3, 0, 0, 4001126, 1, 2, 0, 8000, 'Maple Leaves'), -(4, 0, 0, 2049100, 1, 1, 0, 1200, 'Chaos Scroll 60%'), -(5, 0, 0, 4001006, 1, 1, 0, 10000, 'Flaming Feather'); +INSERT INTO `drop_data_global` (`id`, `continent`, `itemid`, `minimum_quantity`, `maximum_quantity`, `questid`, `chance`, `comments`) VALUES +(1, -1, 4031865, 1, 1, 0, 35000, 'NX Card 100 PTS'), +(2, -1, 4031866, 1, 1, 0, 20000, 'NX Card 250 PTS'), +(3, -1, 4001126, 1, 2, 0, 8000, 'Maple Leaves'), +(4, -1, 2049100, 1, 1, 0, 1200, 'Chaos Scroll 60%'), +(5, -1, 4001006, 1, 1, 0, 10000, 'Flaming Feather'); CREATE TABLE IF NOT EXISTS `dueyitems` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, diff --git a/sql/db_drops.sql b/sql/db_drops.sql index 6b906b2ba7..23e0864ebe 100644 --- a/sql/db_drops.sql +++ b/sql/db_drops.sql @@ -6719,8 +6719,8 @@ USE `heavenms`; (4220000, 4010002, 1, 1, 0, 7000), (4220001, 4010002, 1, 1, 0, 7000), (9303014, 4010002, 1, 1, 0, 7000), -(4220000, 1442018, 1, 1, 0, 40000), -(4220001, 1442018, 1, 1, 0, 40000), +(4220000, 1442018, 1, 1, 0, 25000), +(4220001, 1442018, 1, 1, 0, 25000), (9303014, 1442018, 1, 1, 0, 700), (4220000, 1302010, 1, 1, 0, 40000), (4220001, 1302010, 1, 1, 0, 40000), diff --git a/src/client/MapleCharacter.java b/src/client/MapleCharacter.java index d32325605e..1c077d6024 100644 --- a/src/client/MapleCharacter.java +++ b/src/client/MapleCharacter.java @@ -273,7 +273,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { private ScheduledFuture pendantOfSpirit = null; //1122017 private Lock chrLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHARACTER_CHR, true); private Lock evtLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHARACTER_EVT, true); - private Lock petLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHARACTER_PET, true); // for meso & quest tasks as well + private Lock petLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHARACTER_PET, true); private Lock prtLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHARACTER_PRT); private Lock cpnLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHARACTER_CPN); private Map> excluded = new LinkedHashMap<>(); @@ -852,17 +852,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } List> ldsstat = Collections.singletonList(new Pair(MapleBuffStat.DARKSIGHT, 0)); getMap().broadcastGMMessage(this, MaplePacketCreator.giveForeignBuff(id, ldsstat), false); - for (MapleMonster mon : this.getControlledMonsters()) { - mon.lockMonster(); - try { - mon.setController(null); - mon.setControllerHasAggro(false); - mon.setControllerKnowsAboutAggro(false); - mon.getMap().updateMonsterController(mon); - } finally { - mon.unlockMonster(); - } - } + this.releaseControlledMonsters(); } announce(MaplePacketCreator.enableActions()); } @@ -1356,7 +1346,8 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } } - mapEim.registerPlayer(this); + // thanks Thora for finding an issue with players not being actually warped into the target event map (rather sent to the event starting map) + mapEim.registerPlayer(this, false); } MapleMap to = target; // warps directly to the target intead of the target's map id, this allows GMs to patrol players inside instances. @@ -1708,29 +1699,48 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } } - public void checkMonsterAggro(MapleMonster monster) { - monster.lockMonster(); - try { - if (!monster.isControllerHasAggro()) { - if (monster.getController() == this) { - monster.setControllerHasAggro(true); - } else { - monster.switchController(this, true); - } + public void controlMonster(MapleMonster monster) { + if (cpnLock.tryLock()) { + try { + controlled.add(monster); + } finally { + cpnLock.unlock(); } - } finally { - monster.unlockMonster(); } } - - public void controlMonster(MapleMonster monster, boolean aggro) { - monster.lockMonster(); + + public void stopControllingMonster(MapleMonster monster) { + if (cpnLock.tryLock()) { + try { + controlled.remove(monster); + } finally { + cpnLock.unlock(); + } + } + } + + public int getNumControlledMonsters() { + cpnLock.lock(); try { - monster.setController(this); - controlled.add(monster); - client.announce(MaplePacketCreator.controlMonster(monster, false, aggro)); + return controlled.size(); } finally { - monster.unlockMonster(); + cpnLock.unlock(); + } + } + + public void releaseControlledMonsters() { + Collection controlledMonsters; + + cpnLock.lock(); + try { + controlledMonsters = new ArrayList<>(controlled); + controlled.clear(); + } finally { + cpnLock.unlock(); + } + + for (MapleMonster monster : controlledMonsters) { + monster.aggroRedirectController(); } } @@ -2967,6 +2977,11 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } } + public boolean canHoldMeso(int gain) { // thanks lucasziron found pointing out a need to check space availability for mesos on player transactions + long nextMeso = (long) meso.get() + gain; + return nextMeso <= Integer.MAX_VALUE; + } + public void gainMeso(int gain) { gainMeso(gain, true, false, true); } @@ -4124,11 +4139,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { return Collections.unmodifiableList(ret); } } - - public Collection getControlledMonsters() { - return Collections.unmodifiableCollection(controlled); - } - + public List getCrushRings() { Collections.sort(crushRings); return crushRings; @@ -4343,7 +4354,15 @@ public class MapleCharacter extends AbstractMapleCharacterObject { return gachaexp.get(); } + public boolean hasNoviceExpRate() { + return ServerConstants.USE_ENFORCE_NOVICE_EXPRATE && isBeginnerJob() && level < 11; + } + public int getExpRate() { + if (hasNoviceExpRate()) { // base exp rate 1x for early levels idea thanks to Vcoc + return 1; + } + return expRate; } @@ -4383,7 +4402,17 @@ public class MapleCharacter extends AbstractMapleCharacterObject { public int getRawMesoRate() { return mesoRate / (mesoCoupon * getWorldServer().getMesoRate()); } - + + public int getQuestExpRate() { + World w = getWorldServer(); + return w.getExpRate() * w.getQuestRate(); + } + + public int getQuestMesoRate() { + World w = getWorldServer(); + return w.getMesoRate() * w.getQuestRate(); + } + public int getFace() { return face; } @@ -4829,10 +4858,6 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } } - public int getNumControlledMonsters() { - return controlled.size(); - } - public MapleParty getParty() { prtLock.lock(); try { @@ -5635,9 +5660,9 @@ public class MapleCharacter extends AbstractMapleCharacterObject { public boolean isGuildLeader() { // true on guild master or jr. master return guildid > 0 && guildRank < 3; } - + public void leaveMap() { - controlled.clear(); + releaseControlledMonsters(); visibleMapObjects.clear(); setChair(0); if (hpDecreaseTask != null) { @@ -7772,17 +7797,22 @@ public class MapleCharacter extends AbstractMapleCharacterObject { throw new RuntimeException("Character not in database (" + id + ")"); } + List petList = new LinkedList<>(); petLock.lock(); try { for (int i = 0; i < 3; i++) { if (pets[i] != null) { - pets[i].saveToDb(); + petList.add(pets[i]); } } } finally { petLock.unlock(); } + for (MaplePet pet : petList) { + pet.saveToDb(); + } + for(Entry> es: getExcluded().entrySet()) { // this set is already protected try (PreparedStatement ps2 = con.prepareStatement("DELETE FROM petignores WHERE petid=?")) { ps2.setInt(1, es.getKey()); @@ -8912,11 +8942,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } }, duration); } - - public void stopControllingMonster(MapleMonster monster) { - controlled.remove(monster); - } - + public void unequipAllPets() { for (int i = 0; i < 3; i++) { MaplePet pet = getPet(i); @@ -9046,19 +9072,19 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } public void cancelQuestExpirationTask() { - petLock.lock(); + evtLock.lock(); try { if (questExpireTask != null) { questExpireTask.cancel(false); questExpireTask = null; } } finally { - petLock.unlock(); + evtLock.unlock(); } } public void forfeitExpirableQuests() { - petLock.lock(); + evtLock.lock(); try { for(MapleQuest quest : questExpirations.keySet()) { quest.forfeit(this); @@ -9066,12 +9092,12 @@ public class MapleCharacter extends AbstractMapleCharacterObject { questExpirations.clear(); } finally { - petLock.unlock(); + evtLock.unlock(); } } public void questExpirationTask() { - petLock.lock(); + evtLock.lock(); try { if(!questExpirations.isEmpty()) { if(questExpireTask == null) { @@ -9084,12 +9110,12 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } } } finally { - petLock.unlock(); + evtLock.unlock(); } } private void runQuestExpireTask() { - petLock.lock(); + evtLock.lock(); try { long timeNow = Server.getInstance().getCurrentTime(); List expireList = new LinkedList<>(); @@ -9110,12 +9136,12 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } } } finally { - petLock.unlock(); + evtLock.unlock(); } } private void registerQuestExpire(MapleQuest quest, long time) { - petLock.lock(); + evtLock.lock(); try { if(questExpireTask == null) { questExpireTask = TimerManager.getInstance().register(new Runnable() { @@ -9128,7 +9154,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { questExpirations.put(quest, Server.getInstance().getCurrentTime() + time); } finally { - petLock.unlock(); + evtLock.unlock(); } } @@ -9671,7 +9697,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { if (pendantOfSpirit != null) { pendantOfSpirit.cancel(true); } pendantOfSpirit = null; - petLock.lock(); + evtLock.lock(); try { if (questExpireTask != null) { questExpireTask.cancel(false); @@ -9681,7 +9707,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { questExpirations = null; } } finally { - petLock.unlock(); + evtLock.unlock(); } if (maplemount != null) { diff --git a/src/client/MapleClient.java b/src/client/MapleClient.java index 50f77e86aa..4e5acbe135 100644 --- a/src/client/MapleClient.java +++ b/src/client/MapleClient.java @@ -175,22 +175,22 @@ public class MapleClient { return chars; } - public List loadCharacterNames(int serverId) { + public List loadCharacterNames(int worldId) { List chars = new ArrayList<>(15); - for (CharNameAndId cni : loadCharactersInternal(serverId)) { + for (CharNameAndId cni : loadCharactersInternal(worldId)) { chars.add(cni.name); } return chars; } - private List loadCharactersInternal(int serverId) { + private List loadCharactersInternal(int worldId) { PreparedStatement ps; List chars = new ArrayList<>(15); try { Connection con = DatabaseConnection.getConnection(); ps = con.prepareStatement("SELECT id, name FROM characters WHERE accountid = ? AND world = ?"); ps.setInt(1, this.getAccID()); - ps.setInt(2, serverId); + ps.setInt(2, worldId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { chars.add(new CharNameAndId(rs.getString("name"), rs.getInt("id"))); @@ -882,7 +882,7 @@ public class MapleClient { } public final void disconnect(final boolean shutdown, final boolean cashshop) { - if (isDisconnecting()) { + if (canDisconnect()) { ThreadManager.getInstance().newTask(new Runnable() { @Override public void run() { @@ -893,12 +893,12 @@ public class MapleClient { } public final void forceDisconnect() { - if (isDisconnecting()) { + if (canDisconnect()) { disconnectInternal(true, false); } } - private synchronized boolean isDisconnecting() { + private synchronized boolean canDisconnect() { if (disconnecting) { return false; } diff --git a/src/client/command/commands/gm0/RatesCommand.java b/src/client/command/commands/gm0/RatesCommand.java index 4ae23dc779..378923e632 100644 --- a/src/client/command/commands/gm0/RatesCommand.java +++ b/src/client/command/commands/gm0/RatesCommand.java @@ -39,7 +39,7 @@ public class RatesCommand extends Command { // travel rates not applicable since it's intrinsically a server/environment rate rather than a character rate String showMsg_ = "#eCHARACTER RATES#n" + "\r\n\r\n"; - showMsg_ += "EXP Rate: #e#b" + player.getExpRate() + "x#k#n" + "\r\n"; + showMsg_ += "EXP Rate: #e#b" + player.getExpRate() + "x#k#n" + (player.hasNoviceExpRate() ? " - novice rate" : "") + "\r\n"; showMsg_ += "MESO Rate: #e#b" + player.getMesoRate() + "x#k#n" + "\r\n"; showMsg_ += "DROP Rate: #e#b" + player.getDropRate() + "x#k#n" + "\r\n"; showMsg_ += "BOSS DROP Rate: #e#b" + player.getBossDropRate() + "x#k#n" + "\r\n"; diff --git a/src/client/command/commands/gm0/ShowRatesCommand.java b/src/client/command/commands/gm0/ShowRatesCommand.java index e1bf006477..0db9088efd 100644 --- a/src/client/command/commands/gm0/ShowRatesCommand.java +++ b/src/client/command/commands/gm0/ShowRatesCommand.java @@ -40,7 +40,7 @@ public class ShowRatesCommand extends Command { showMsg += "World EXP Rate: #k" + c.getWorldServer().getExpRate() + "x#k" + "\r\n"; showMsg += "Player EXP Rate: #k" + player.getRawExpRate() + "x#k" + "\r\n"; if(player.getCouponExpRate() != 1) showMsg += "Coupon EXP Rate: #k" + player.getCouponExpRate() + "x#k" + "\r\n"; - showMsg += "EXP Rate: #e#b" + player.getExpRate() + "x#k#n" + "\r\n"; + showMsg += "EXP Rate: #e#b" + player.getExpRate() + "x#k#n" + (player.hasNoviceExpRate() ? " - novice rate" : "") + "\r\n"; showMsg += "\r\n" + "#eMESO RATE#n" + "\r\n"; showMsg += "World MESO Rate: #k" + c.getWorldServer().getMesoRate() + "x#k" + "\r\n"; @@ -66,7 +66,7 @@ public class ShowRatesCommand extends Command { } showMsg += "\r\n"; - showMsg += "World TRAVEL Rate: #e#b" + c.getWorldServer().getTravelRate() + "x#k#n" + "\r\nServer\r\nPlayer"; + showMsg += "World TRAVEL Rate: #e#b" + c.getWorldServer().getTravelRate() + "x#k#n" + "\r\n"; player.showHint(showMsg, 300); } diff --git a/src/client/command/commands/gm1/GotoCommand.java b/src/client/command/commands/gm1/GotoCommand.java index f39df86498..3e1a816449 100644 --- a/src/client/command/commands/gm1/GotoCommand.java +++ b/src/client/command/commands/gm1/GotoCommand.java @@ -41,8 +41,6 @@ public class GotoCommand extends Command { @Override public void execute(MapleClient c, String[] params) { - final HashMap gotomaps = GameConstants.GOTO_MAPS; - MapleCharacter player = c.getPlayer(); if (params.length < 1){ player.yellowMessage("Syntax: @goto "); @@ -54,6 +52,13 @@ public class GotoCommand extends Command { return; } + HashMap gotomaps; + if (player.isGM()) { + gotomaps = new HashMap<>(GameConstants.GOTO_AREAS); // distinct map registry for GM/users suggested thanks to Vcoc + } else { + gotomaps = new HashMap<>(GameConstants.GOTO_TOWNS); + } + if (gotomaps.containsKey(params[0])) { MapleMap target = c.getChannelServer().getMapFactory().getMap(gotomaps.get(params[0])); diff --git a/src/client/command/commands/gm2/SummonCommand.java b/src/client/command/commands/gm2/SummonCommand.java index f46bafe726..1e413b392e 100644 --- a/src/client/command/commands/gm2/SummonCommand.java +++ b/src/client/command/commands/gm2/SummonCommand.java @@ -26,6 +26,7 @@ package client.command.commands.gm2; import client.command.Command; import client.MapleClient; import client.MapleCharacter; +import server.maps.MapleMap; import net.server.Server; import net.server.channel.Channel; @@ -54,33 +55,26 @@ public class SummonCommand extends Command { } } if (victim != null) { - boolean changingEvent = true; - - if (victim.getEventInstance() != null) { - if (player.getEventInstance() != null && victim.getEventInstance().getLeaderId() == player.getEventInstance().getLeaderId()) { - changingEvent = false; - } else { - victim.getEventInstance().unregisterPlayer(victim); - } + if (!victim.isLoggedinWorld()) { + player.dropMessage(6, "Player currently not logged in or unreachable."); + return; } - //Attempt to join the warpers instance. - if (player.getEventInstance() != null && changingEvent) { - if (player.getClient().getChannel() == victim.getClient().getChannel()) { - player.getEventInstance().registerPlayer(victim); - victim.saveLocationOnWarp(); - victim.changeMap(player.getEventInstance().getMapInstance(player.getMapId()), player.getMap().findClosestPortal(player.getPosition())); - } else { - player.dropMessage("Target isn't on your channel, not able to warp into event instance."); - } - } else {//If victim isn't in an event instance or is in the same event instance as the one the caller is, just warp them. - victim.saveLocationOnWarp(); - victim.changeMap(player.getMapId(), player.getMap().findClosestPortal(player.getPosition())); - } if (player.getClient().getChannel() != victim.getClient().getChannel()) {//And then change channel if needed. victim.dropMessage("Changing channel, please wait a moment."); victim.getClient().changeChannel(player.getClient().getChannel()); } + + try { + for (int i = 0; i < 7; i++) { // poll for a while until the player reconnects + if (victim.isLoggedinWorld()) break; + Thread.sleep(1777); + } + } catch (InterruptedException e) {} + + MapleMap map = player.getMap(); + victim.saveLocationOnWarp(); + victim.forceChangeMap(map, map.findClosestPortal(player.getPosition())); } else { player.dropMessage(6, "Unknown player."); } diff --git a/src/client/command/commands/gm2/WarpAreaCommand.java b/src/client/command/commands/gm2/WarpAreaCommand.java index b531215e24..1ec42d97b8 100644 --- a/src/client/command/commands/gm2/WarpAreaCommand.java +++ b/src/client/command/commands/gm2/WarpAreaCommand.java @@ -29,7 +29,6 @@ import client.command.Command; import server.maps.MapleMap; import java.awt.*; -import java.util.ArrayList; import java.util.Collection; public class WarpAreaCommand extends Command { @@ -54,7 +53,7 @@ public class WarpAreaCommand extends Command { Point pos = player.getPosition(); - Collection characters = new ArrayList<>(player.getMap().getCharacters()); + Collection characters = player.getMap().getAllPlayers(); for (MapleCharacter victim : characters) { if (victim.getPosition().distanceSq(pos) <= 50000) { diff --git a/src/client/command/commands/gm2/WarpMapCommand.java b/src/client/command/commands/gm2/WarpMapCommand.java index 238890d767..d4d1760ae9 100644 --- a/src/client/command/commands/gm2/WarpMapCommand.java +++ b/src/client/command/commands/gm2/WarpMapCommand.java @@ -28,7 +28,6 @@ import client.MapleClient; import client.command.Command; import server.maps.MapleMap; -import java.util.ArrayList; import java.util.Collection; public class WarpMapCommand extends Command { @@ -51,7 +50,7 @@ public class WarpMapCommand extends Command { return; } - Collection characters = new ArrayList<>(player.getMap().getCharacters()); + Collection characters = player.getMap().getAllPlayers(); for (MapleCharacter victim : characters) { victim.saveLocationOnWarp(); diff --git a/src/client/command/commands/gm3/ExpedsCommand.java b/src/client/command/commands/gm3/ExpedsCommand.java index c9acc0c431..f01e12f38e 100644 --- a/src/client/command/commands/gm3/ExpedsCommand.java +++ b/src/client/command/commands/gm3/ExpedsCommand.java @@ -30,6 +30,8 @@ import net.server.Server; import net.server.channel.Channel; import server.expeditions.MapleExpedition; +import java.util.Map.Entry; + public class ExpedsCommand extends Command { { setDescription(""); @@ -53,11 +55,11 @@ public class ExpedsCommand extends Command { player.yellowMessage(">> Size: " + exped.getMembers().size()); player.yellowMessage(">> Leader: " + exped.getLeader().getName()); int memId = 2; - for (MapleCharacter member : exped.getMembers()) { - if (exped.isLeader(member)) { + for (Entry e : exped.getMembers().entrySet()) { + if (exped.isLeader(e.getKey())) { continue; } - player.yellowMessage(">>> Member " + memId + ": " + member.getName()); + player.yellowMessage(">>> Member " + memId + ": " + e.getValue()); memId++; } } diff --git a/src/client/command/commands/gm3/ReloadMapCommand.java b/src/client/command/commands/gm3/ReloadMapCommand.java index 12a0c31c5b..589eab85d6 100644 --- a/src/client/command/commands/gm3/ReloadMapCommand.java +++ b/src/client/command/commands/gm3/ReloadMapCommand.java @@ -28,7 +28,6 @@ import client.MapleClient; import client.MapleCharacter; import server.maps.MapleMap; -import java.util.ArrayList; import java.util.Collection; public class ReloadMapCommand extends Command { @@ -42,7 +41,7 @@ public class ReloadMapCommand extends Command { MapleMap newMap = c.getChannelServer().getMapFactory().resetMap(player.getMapId()); int callerid = c.getPlayer().getId(); - Collection characters = new ArrayList<>(player.getMap().getCharacters()); + Collection characters = player.getMap().getAllPlayers(); for (MapleCharacter chr : characters) { chr.saveLocationOnWarp(); diff --git a/src/client/command/commands/gm3/WarpSnowBallCommand.java b/src/client/command/commands/gm3/WarpSnowBallCommand.java index 4018010e2e..afa0945df1 100644 --- a/src/client/command/commands/gm3/WarpSnowBallCommand.java +++ b/src/client/command/commands/gm3/WarpSnowBallCommand.java @@ -23,12 +23,11 @@ */ package client.command.commands.gm3; - import client.command.Command; - import client.MapleClient; - import client.MapleCharacter; +import client.command.Command; +import client.MapleClient; +import client.MapleCharacter; - import java.util.ArrayList; - import java.util.List; +import java.util.List; public class WarpSnowBallCommand extends Command { { @@ -38,7 +37,7 @@ public class WarpSnowBallCommand extends Command { @Override public void execute(MapleClient c, String[] params) { MapleCharacter player = c.getPlayer(); - List chars = new ArrayList<>(player.getMap().getCharacters()); + List chars = player.getMap().getAllPlayers(); for (MapleCharacter chr : chars) { chr.saveLocationOnWarp(); chr.changeMap(109060000, chr.getTeam()); diff --git a/src/client/command/commands/gm5/DebugCommand.java b/src/client/command/commands/gm5/DebugCommand.java index 9d1296867b..7346f522cb 100644 --- a/src/client/command/commands/gm5/DebugCommand.java +++ b/src/client/command/commands/gm5/DebugCommand.java @@ -72,7 +72,7 @@ public class DebugCommand extends Command { for (MapleMapObject monstermo : monsters) { MapleMonster monster = (MapleMonster) monstermo; MapleCharacter controller = monster.getController(); - player.message("Monster ID: " + monster.getId() + " Aggro target: " + ((controller != null) ? controller.getName() : "")); + player.message("Monster ID: " + monster.getId() + " Aggro target: " + ((controller != null) ? controller.getName() + " Has aggro: " + monster.isControllerHasAggro() + " Knowns aggro: " + monster.isControllerKnowsAboutAggro() : "")); } break; diff --git a/src/client/inventory/Item.java b/src/client/inventory/Item.java index 8173dd5b1d..74ca97932e 100644 --- a/src/client/inventory/Item.java +++ b/src/client/inventory/Item.java @@ -56,8 +56,13 @@ public class Item implements Comparable { this.id = id; this.position = position; this.quantity = quantity; + if (petid > -1) { // issue with null "pet" having petid > -1 found thanks to MedicOP + this.pet = MaplePet.loadFromDb(id, position, petid); + if (this.pet == null) { + petid = -1; + } + } this.petid = petid; - if (petid > -1) this.pet = MaplePet.loadFromDb(id, position, petid); this.flag = 0; this.log = new LinkedList<>(); } @@ -121,10 +126,6 @@ public class Item implements Comparable { public int getPetId() { return petid; } - - public void setPetId(int id) { - this.petid = id; - } @Override public int compareTo(Item other) { diff --git a/src/client/processor/AssignAPProcessor.java b/src/client/processor/AssignAPProcessor.java index b9a879ad67..fd9e2d8aa6 100644 --- a/src/client/processor/AssignAPProcessor.java +++ b/src/client/processor/AssignAPProcessor.java @@ -134,10 +134,10 @@ public class AssignAPProcessor { luk = scStat; str = 0; dex = 0; - if(luk + chr.getLuk() > CAP) { + if(ServerConstants.USE_AUTOASSIGN_SECONDARY_CAP && luk + chr.getLuk() > CAP) { temp = luk + chr.getLuk() - CAP; - luk -= temp; - int_ += temp; + scStat -= temp; + prStat += temp; } primary = MapleStat.INT; @@ -160,10 +160,10 @@ public class AssignAPProcessor { str = scStat; int_ = 0; luk = 0; - if(str + chr.getStr() > CAP) { + if(ServerConstants.USE_AUTOASSIGN_SECONDARY_CAP && str + chr.getStr() > CAP) { temp = str + chr.getStr() - CAP; - str -= temp; - dex += temp; + scStat -= temp; + prStat += temp; } primary = MapleStat.DEX; @@ -186,10 +186,10 @@ public class AssignAPProcessor { str = scStat; int_ = 0; luk = 0; - if(str + chr.getStr() > CAP) { + if(ServerConstants.USE_AUTOASSIGN_SECONDARY_CAP && str + chr.getStr() > CAP) { temp = str + chr.getStr() - CAP; - str -= temp; - dex += temp; + scStat -= temp; + prStat += temp; } primary = MapleStat.DEX; @@ -240,15 +240,15 @@ public class AssignAPProcessor { str = trStat; int_ = 0; - if(dex + chr.getDex() > CAP) { + if(ServerConstants.USE_AUTOASSIGN_SECONDARY_CAP && dex + chr.getDex() > CAP) { temp = dex + chr.getDex() - CAP; - dex -= temp; - luk += temp; + scStat -= temp; + prStat += temp; } - if(str + chr.getStr() > CAP) { + if(ServerConstants.USE_AUTOASSIGN_SECONDARY_CAP && str + chr.getStr() > CAP) { temp = str + chr.getStr() - CAP; - str -= temp; - luk += temp; + trStat -= temp; + prStat += temp; } primary = MapleStat.LUK; @@ -258,50 +258,64 @@ public class AssignAPProcessor { break; case BRAWLER: - CAP = 120; - - scStat = chr.getLevel() - (chr.getDex() + dex - eqpDex); - if(scStat < 0) scStat = 0; - scStat = Math.min(scStat, tempAp); - - if(tempAp > scStat) tempAp -= scStat; - else tempAp = 0; - - prStat = tempAp; - str = prStat; - dex = scStat; - int_ = 0; luk = 0; - - if(dex + chr.getDex() > CAP) { - temp = dex + chr.getDex() - CAP; - dex -= temp; - str += temp; - } - - primary = MapleStat.STR; - secondary = MapleStat.DEX; - - break; - default: //warrior, beginner, ... - CAP = 80; + CAP = 300; + + boolean highDex = false; // thanks lucasziron & Vcoc for finding out DEX autoassigning poorly for STR-based characters + if (chr.getLevel() < 40) { + if (chr.getDex() >= (2 * chr.getLevel()) + 2) { + highDex = true; + } + } else { + if (chr.getDex() >= chr.getLevel() + 42) { + highDex = true; + } + } + + // other classes will start favoring more DEX only if a level-based threshold is reached. + if(!highDex) { + scStat = 0; + if(chr.getDex() < 80) { + scStat = (2 * chr.getLevel()) - (chr.getDex() + dex - eqpDex); + if(scStat < 0) scStat = 0; - scStat = ((2 * chr.getLevel()) / 3) - (chr.getDex() + dex - eqpDex); - if(scStat < 0) scStat = 0; - scStat = Math.min(scStat, tempAp); + scStat = Math.min(80 - chr.getDex(), scStat); + scStat = Math.min(tempAp, scStat); + tempAp -= scStat; + } - if(tempAp > scStat) tempAp -= scStat; - else tempAp = 0; + temp = (chr.getLevel() + 40) - Math.max(80, scStat + chr.getDex() + dex - eqpDex); + if(temp < 0) temp = 0; + temp = Math.min(tempAp, temp); + scStat += temp; + tempAp -= temp; + } else { + scStat = 0; + if(chr.getDex() < 96) { + scStat = (int)(2.4 * chr.getLevel()) - (chr.getDex() + dex - eqpDex); + if(scStat < 0) scStat = 0; + scStat = Math.min(96 - chr.getDex(), scStat); + scStat = Math.min(tempAp, scStat); + tempAp -= scStat; + } + + temp = 96 + (int)(1.2 * (chr.getLevel() - 40)) - Math.max(96, scStat + chr.getDex() + dex - eqpDex); + if(temp < 0) temp = 0; + temp = Math.min(tempAp, temp); + scStat += temp; + tempAp -= temp; + } + prStat = tempAp; str = prStat; dex = scStat; int_ = 0; luk = 0; - if(dex + chr.getDex() > CAP) { + if(ServerConstants.USE_AUTOASSIGN_SECONDARY_CAP && dex + chr.getDex() > CAP) { temp = dex + chr.getDex() - CAP; - dex -= temp; - str += temp; + scStat -= temp; + prStat += temp; } primary = MapleStat.STR; diff --git a/src/client/processor/DueyProcessor.java b/src/client/processor/DueyProcessor.java index b06ffdd3d9..d4a8cc6674 100644 --- a/src/client/processor/DueyProcessor.java +++ b/src/client/processor/DueyProcessor.java @@ -469,6 +469,11 @@ public class DueyProcessor { } if (dp.getItem() != null) { + if (!c.getPlayer().canHoldMeso(dp.getMesos())) { + c.announce(MaplePacketCreator.sendDueyMSG(Actions.TOCLIENT_RECV_UNKNOWN_ERROR.getCode())); + return; + } + if (!MapleInventoryManipulator.checkSpace(c, dp.getItem().getItemId(), dp.getItem().getQuantity(), dp.getItem().getOwner())) { int itemid = dp.getItem().getItemId(); if(MapleItemInformationProvider.getInstance().isPickupRestricted(itemid) && c.getPlayer().getInventory(ItemConstants.getInventoryType(itemid)).findById(itemid) != null) { @@ -483,17 +488,7 @@ public class DueyProcessor { } } - long gainmesos; - long totalmesos = (long) dp.getMesos() + c.getPlayer().getMeso(); - - if (totalmesos < 0 || dp.getMesos() < 0) { - gainmesos = 0; - } else { - totalmesos = Math.min(totalmesos, Integer.MAX_VALUE); - gainmesos = totalmesos - c.getPlayer().getMeso(); - } - - c.getPlayer().gainMeso((int)gainmesos, false); + c.getPlayer().gainMeso(dp.getMesos(), false); removeItemFromDB(packageid); c.announce(MaplePacketCreator.removeItemFromDuey(false, packageid)); diff --git a/src/client/processor/FredrickProcessor.java b/src/client/processor/FredrickProcessor.java index 50cd97f912..49311315a0 100644 --- a/src/client/processor/FredrickProcessor.java +++ b/src/client/processor/FredrickProcessor.java @@ -47,7 +47,7 @@ import tools.Pair; */ public class FredrickProcessor { private static boolean canRetrieveFromFredrick(MapleCharacter chr, List> items) { - if (chr.getMeso() + chr.getMerchantMeso() < 0) { + if (!chr.canHoldMeso(chr.getMerchantMeso())) { return false; } return MapleInventory.checkSpotsAndOwnership(chr, items); diff --git a/src/constants/GameConstants.java b/src/constants/GameConstants.java index 1c5b8c7dd7..d8cfc98e9f 100644 --- a/src/constants/GameConstants.java +++ b/src/constants/GameConstants.java @@ -45,9 +45,8 @@ public class GameConstants { return(EXP_RATE_GAIN[slot]); } - // used by the "goto" command - public static final HashMap GOTO_MAPS = new HashMap() {{ - put("gmmap", 180000000); + // used by the "goto" command for players + public static final HashMap GOTO_TOWNS = new HashMap() {{ put("southperry", 60000); put("amherst", 1000000); put("henesys", 100000000); @@ -72,6 +71,25 @@ public class GameConstants { put("korean", 222000000); put("ellin", 300000000); put("nlc", 600000000); + put("showa", 801000000); + put("shrine", 800000000); + put("ariant", 260000000); + put("magatia", 261000000); + put("singapore", 540000000); + put("quay", 541000000); + put("kampung", 551000000); + put("amoria", 680000000); + put("temple", 270000100); + put("square", 103040000); + put("neo", 240070000); + put("mushking", 106020000); + }}; + + // used by the "goto" command for only-GMs + public static final HashMap GOTO_AREAS = new HashMap() {{ + putAll(GOTO_TOWNS); + + put("gmmap", 180000000); put("excavation", 990000000); put("mushmom", 100000005); put("griffey", 240020101); @@ -80,25 +98,13 @@ public class GameConstants { put("balrog", 105090900); put("zakum", 211042300); put("papu", 220080001); - put("showa", 801000000); put("guild", 200000301); - put("shrine", 800000000); put("skelegon", 240040511); put("hpq", 100000200); put("pianus", 230040420); put("horntail", 240050400); put("pinkbean", 270050000); - put("ariant", 260000000); - put("magatia", 261000000); - put("singapore", 540000000); - put("quay", 541000000); - put("kampung", 551000000); put("keep", 610020006); - put("amoria", 680000000); - put("temple", 270000100); - put("square", 103040000); - put("neo", 240070000); - put("mushking", 106020000); put("dojo", 925020001); put("bosspq", 970030000); put("fm", 910000000); diff --git a/src/constants/ServerConstants.java b/src/constants/ServerConstants.java index 26a8ac20d7..90217e1d8a 100644 --- a/src/constants/ServerConstants.java +++ b/src/constants/ServerConstants.java @@ -76,13 +76,15 @@ public class ServerConstants { public static final boolean USE_ITEM_SORT_BY_NAME = false; //Item sorting based on name rather than id. public static final boolean USE_PARTY_SEARCH = false; public static final boolean USE_PARTY_FOR_STARTERS = true; //Players level 10 or below can create/invite other players on the given level range. - public static final boolean USE_AUTOASSIGN_STARTERS_AP = false; //Beginners level 10 or below have their AP autoassigned (they can't choose to levelup a stat). Set true if the localhost doesn't support AP assigning for beginners level 10 or below. + public static final boolean USE_AUTOASSIGN_STARTERS_AP = false; //Beginners level 10 or below have their AP autoassigned (they can't choose to levelup a stat). Set true ONLY if the localhost doesn't support AP assigning for beginners level 10 or below. + public static final boolean USE_AUTOASSIGN_SECONDARY_CAP = true;//Prevents AP autoassign from spending on secondary stats after the player class' cap (defined on the autoassign handler) has been reached. public static final boolean USE_AUTOBAN = false; //Commands the server to detect infractors automatically. public static final boolean USE_AUTOBAN_LOG = true; //Log autoban related messages. Still logs even with USE_AUTOBAN disabled. public static final boolean USE_AUTOSAVE = true; //Enables server autosaving feature (saves characters to DB each 1 hour). public static final boolean USE_SERVER_AUTOASSIGNER = true; //HeavenMS-builtin autoassigner, uses algorithm based on distributing AP accordingly with required secondary stat on equipments. public static final boolean USE_REFRESH_RANK_MOVE = true; public static final boolean USE_ENFORCE_ADMIN_ACCOUNT = false; //Forces accounts having GM characters to be treated as a "GM account" by the client (localhost). Some of the GM account perks is the ability to FLY, but unable to TRADE. + public static final boolean USE_ENFORCE_NOVICE_EXPRATE = false; //Hardsets experience rate 1x for beginners level 10 or under. Ideal for roaming on novice areas without caring too much about losing some stats. public static final boolean USE_ENFORCE_HPMP_SWAP = false; //Forces players to reuse stats (via AP Resetting) located on HP/MP pool only inside the HP/MP stats. public static final boolean USE_ENFORCE_MOB_LEVEL_RANGE = true; //Players N levels below the killed mob will gain no experience from defeating it. public static final boolean USE_ENFORCE_JOB_LEVEL_RANGE = false;//Caps the player level on the minimum required to advance their current jobs. @@ -166,11 +168,13 @@ public class ServerConstants { public static final int ITEM_EXPIRE_CHECK = 10 * 1000; //Interval between item expiring tasks on maps, which checks and makes disappear expired items. public static final int ITEM_LIMIT_ON_MAP = 200; //Max number of items allowed on a map. public static final int MAP_VISITED_SIZE = 5; //Max length for last mapids visited by a player. This is used to recover and update drops on these maps accordingly with player actions. - public static final int MAP_DAMAGE_OVERTIME_INTERVAL = 5000;//Interval in seconds between map environment damage (e.g. El Nath and Aqua Road surrondings). + public static final int MAP_DAMAGE_OVERTIME_INTERVAL = 5000;//Interval in milliseconds between map environment damage (e.g. El Nath and Aqua Road surrondings). //Channel Mob Disease Monitor Configuration - public static final int MOB_STATUS_MONITOR_PROC = 200; //Frequency in milliseconds between each proc on the mob disease monitor schedule. - public static final int MOB_STATUS_MONITOR_LIFE = 84; //Idle proc count the mob disease monitor is allowed to be there before closing it due to inactivity. + public static final int MOB_STATUS_MONITOR_PROC = 200; //Frequency in milliseconds between each proc on the mob disease monitor schedule. + public static final int MOB_STATUS_MONITOR_LIFE = 84; //Idle proc count the mob disease monitor is allowed to be there before closing it due to inactivity. + public static final int MOB_STATUS_AGGRO_PERSISTENCE = 2; //Idle proc count on aggro update for a mob to keep following the current controller, given him/her is the leading damage dealer. + public static final int MOB_STATUS_AGGRO_INTERVAL = 5000; //Interval in milliseconds between aggro logistics update. //Some Gameplay Enhancing Configurations //Scroll Configuration diff --git a/src/net/opcodes/SendOpcode.java b/src/net/opcodes/SendOpcode.java index 55110e68c6..7a48ee7c45 100644 --- a/src/net/opcodes/SendOpcode.java +++ b/src/net/opcodes/SendOpcode.java @@ -228,6 +228,8 @@ public enum SendOpcode { GIVE_FOREIGN_BUFF(0xC7), CANCEL_FOREIGN_BUFF(0xC8), UPDATE_PARTYMEMBER_HP(0xC9), + GUILD_NAME_CHANGED(0xCA), + GUILD_MARK_CHANGED(0xCB), THROW_GRENADE(0xCC), CANCEL_CHAIR(0xCD), SHOW_ITEM_GAIN_INCHAT(0xCE), @@ -257,8 +259,8 @@ public enum SendOpcode { DAMAGE_MONSTER(0xF6), ARIANT_THING(0xF9), SHOW_MONSTER_HP(0xFA), - SHOW_DRAGGED(0xFB),//CATCH - CATCH_MONSTER(0xFC), + CATCH_MONSTER(0xFB), + CATCH_MONSTER_WITH_ITEM(0xFC), SHOW_MAGNET(0xFD), SPAWN_NPC(0x101), REMOVE_NPC(0x102), diff --git a/src/net/server/audit/ThreadTracker.java b/src/net/server/audit/ThreadTracker.java index 319f1bd144..6a62633029 100644 --- a/src/net/server/audit/ThreadTracker.java +++ b/src/net/server/audit/ThreadTracker.java @@ -35,7 +35,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import constants.ServerConstants; import net.server.audit.locks.MonitoredLockType; import server.TimerManager; import tools.FilePrinter; diff --git a/src/net/server/audit/locks/MonitoredLockType.java b/src/net/server/audit/locks/MonitoredLockType.java index bcf84b12de..a477e4ab74 100644 --- a/src/net/server/audit/locks/MonitoredLockType.java +++ b/src/net/server/audit/locks/MonitoredLockType.java @@ -84,6 +84,7 @@ public enum MonitoredLockType { VISITOR_PSHOP, STORAGE, MOB, + MOB_AGGRO, MOB_ANI, MOB_EXT, MOB_STATI, @@ -96,6 +97,8 @@ public enum MonitoredLockType { MAP_ITEM, MAP_LOOT, MAP_BOUNDS, + MAP_AGGRO, + MAP_AGGRO_IDLE, MINIDUNGEON, REACTOR, REACTOR_HIT; diff --git a/src/net/server/channel/handlers/AbstractDealDamageHandler.java b/src/net/server/channel/handlers/AbstractDealDamageHandler.java index 27db3c4ee8..3299f3d5ba 100644 --- a/src/net/server/channel/handlers/AbstractDealDamageHandler.java +++ b/src/net/server/channel/handlers/AbstractDealDamageHandler.java @@ -274,7 +274,7 @@ public abstract class AbstractDealDamageHandler extends AbstractMaplePacketHandl totDamageToOneMonster += eachd; } totDamage += totDamageToOneMonster; - player.checkMonsterAggro(monster); + monster.aggroMonsterDamage(player, totDamageToOneMonster); if (player.getBuffedValue(MapleBuffStat.PICKPOCKET) != null && (attack.skill == 0 || attack.skill == Rogue.DOUBLE_STAB || attack.skill == Bandit.SAVAGE_BLOW || attack.skill == ChiefBandit.ASSAULTER || attack.skill == ChiefBandit.BAND_OF_THIEVES || attack.skill == Shadower.ASSASSINATE || attack.skill == Shadower.TAUNT || attack.skill == Shadower.BOOMERANG_STEP)) { Skill pickpocket = SkillFactory.getSkill(ChiefBandit.PICKPOCKET); int picklv = (player.isGM()) ? pickpocket.getMaxLevel() : player.getSkillLevel(pickpocket); diff --git a/src/net/server/channel/handlers/AbstractMovementPacketHandler.java b/src/net/server/channel/handlers/AbstractMovementPacketHandler.java index 405bf21ece..47f2852c60 100644 --- a/src/net/server/channel/handlers/AbstractMovementPacketHandler.java +++ b/src/net/server/channel/handlers/AbstractMovementPacketHandler.java @@ -51,11 +51,11 @@ public abstract class AbstractMovementPacketHandler extends AbstractMaplePacketH short ypos = lea.readShort(); short xwobble = lea.readShort(); short ywobble = lea.readShort(); - short unk = lea.readShort(); + short fh = lea.readShort(); byte newstate = lea.readByte(); short duration = lea.readShort(); AbsoluteLifeMovement alm = new AbsoluteLifeMovement(command, new Point(xpos, ypos), duration, newstate); - alm.setUnk(unk); + alm.setFh(fh); alm.setPixelsPerSecond(new Point(xwobble, ywobble)); res.add(alm); break; @@ -105,11 +105,11 @@ public abstract class AbstractMovementPacketHandler extends AbstractMaplePacketH /*case 11: { // Chair short xpos = lea.readShort(); short ypos = lea.readShort(); - short unk = lea.readShort(); + short fh = lea.readShort(); byte newstate = lea.readByte(); short duration = lea.readShort(); ChairMovement cm = new ChairMovement(command, new Point(xpos, ypos), duration, newstate); - cm.setUnk(unk); + cm.setFh(fh); res.add(cm); break; }*/ @@ -118,14 +118,14 @@ public abstract class AbstractMovementPacketHandler extends AbstractMaplePacketH short ypos = lea.readShort(); short xwobble = lea.readShort(); short ywobble = lea.readShort(); - short unk = lea.readShort(); short fh = lea.readShort(); + short ofh = lea.readShort(); byte newstate = lea.readByte(); short duration = lea.readShort(); JumpDownMovement jdm = new JumpDownMovement(command, new Point(xpos, ypos), duration, newstate); - jdm.setUnk(unk); + jdm.setFh(fh); jdm.setPixelsPerSecond(new Point(xwobble, ywobble)); - jdm.setFH(fh); + jdm.setOriginFh(ofh); res.add(jdm); break; } diff --git a/src/net/server/channel/handlers/AutoAggroHandler.java b/src/net/server/channel/handlers/AutoAggroHandler.java index ffb1124803..448f2adb5e 100644 --- a/src/net/server/channel/handlers/AutoAggroHandler.java +++ b/src/net/server/channel/handlers/AutoAggroHandler.java @@ -32,32 +32,15 @@ public final class AutoAggroHandler extends AbstractMaplePacketHandler { @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { - if (c.getPlayer().isHidden()) return; // Don't auto aggro GM's in hide... + MapleCharacter player = c.getPlayer(); + if (player.isHidden()) return; // Don't auto aggro GM's in hide... - MapleMap map = c.getPlayer().getMap(); + MapleMap map = player.getMap(); int oid = slea.readInt(); MapleMonster monster = map.getMonsterByOid(oid); if (monster != null) { - MapleCharacter currentController = monster.getController(); - monster.lockMonster(); - try { - if (currentController != null) { - if (!monster.isControllerHasAggro()) { - if (map.getCharacterById(currentController.getId()) == null) { - monster.switchController(c.getPlayer(), true); - } else { - monster.switchController(currentController, true); - } - } else if (map.getCharacterById(currentController.getId()) == null) { - monster.switchController(c.getPlayer(), true); - } - } else { - monster.switchController(c.getPlayer(), true); - } - } finally { - monster.unlockMonster(); - } + monster.aggroAutoAggroUpdate(player); } } } diff --git a/src/net/server/channel/handlers/GuildOperationHandler.java b/src/net/server/channel/handlers/GuildOperationHandler.java index dbfbf39572..92483e8c51 100644 --- a/src/net/server/channel/handlers/GuildOperationHandler.java +++ b/src/net/server/channel/handlers/GuildOperationHandler.java @@ -47,10 +47,6 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { return true; } - private void restancePlayer(MapleCharacter mc) { - mc.broadcastStance(); - } - private class Invited { public String name; public int gid; @@ -131,7 +127,8 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { c.announce(MaplePacketCreator.showGuildInfo(mc)); c.getPlayer().dropMessage(1, "You have successfully created a Guild."); - restancePlayer(mc); + mc.getGuild().broadcastNameChanged(); + mc.getGuild().broadcastEmblemChanged(); break; case 0x05: if (mc.getGuildId() <= 0 || mc.getGuildRank() > 2) { @@ -191,7 +188,8 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { if(allianceId > 0) Server.getInstance().getAlliance(allianceId).updateAlliancePackets(mc); mc.saveGuildStatus(); // update database - restancePlayer(mc); + mc.getMap().broadcastMessage(mc, MaplePacketCreator.guildNameChanged(mc.getId(), mc.getGuild().getName())); // thanks Vcoc for pointing out an issue with updating guild tooltip to players in the map + mc.getMap().broadcastMessage(mc, MaplePacketCreator.guildMarkChanged(mc.getId(), mc.getGuild())); break; case 0x07: cid = slea.readInt(); @@ -212,7 +210,7 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { mc.getMGC().setGuildId(0); mc.getMGC().setGuildRank(5); mc.saveGuildStatus(); - restancePlayer(mc); + mc.getMap().broadcastMessage(mc, MaplePacketCreator.guildNameChanged(mc.getId(), "")); break; case 0x08: allianceId = mc.getGuild().getAllianceId(); @@ -272,7 +270,8 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { } mc.gainMeso(-ServerConstants.CHANGE_EMBLEM_COST, true, false, true); - restancePlayer(mc); + mc.getGuild().broadcastNameChanged(); + mc.getGuild().broadcastEmblemChanged(); break; case 0x10: if (mc.getGuildId() <= 0 || mc.getGuildRank() > 2) { diff --git a/src/net/server/channel/handlers/MoveLifeHandler.java b/src/net/server/channel/handlers/MoveLifeHandler.java index 4ee2f0a88b..93c2c3035d 100644 --- a/src/net/server/channel/handlers/MoveLifeHandler.java +++ b/src/net/server/channel/handlers/MoveLifeHandler.java @@ -77,8 +77,6 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { boolean isAttack = inRangeInclusive(rawActivity, 24, 41); boolean isSkill = inRangeInclusive(rawActivity, 42, 59); - - boolean currentController = (monster.getController() == player); MobSkill toUse = null; int useSkillId = 0, useSkillLevel = 0; @@ -112,10 +110,6 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { int atkStatus = monster.canUseAttack(castPos, isSkill); if (atkStatus < 1) { - if (!currentController) { - return; - } - rawActivity = -1; pOption = 0; } @@ -147,24 +141,8 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { Point startPos = new Point(start_x, start_y - 2); List res = parseMovement(slea); - boolean aggro; - monster.lockMonster(); - try { - if (!currentController) { - if (monster.isAttackedBy(player)) { - monster.switchController(player, true); - } else { - return; - } - } - - aggro = monster.isControllerHasAggro(); - if (aggro) { - monster.setControllerKnowsAboutAggro(true); - } - } finally { - monster.unlockMonster(); - } + Boolean aggro = monster.aggroMoveLifeUpdate(player); + if (aggro == null) return; if (nextUse != null) { c.announce(MaplePacketCreator.moveMonsterResponse(objectid, moveid, mobMp, aggro, nextSkillId, nextSkillLevel)); @@ -174,7 +152,7 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { if (res != null) { if (ServerConstants.USE_DEBUG_SHOW_RCVD_MVLIFE) { - System.out.println((isSkill ? "SKILL " : (isAttack ? "ATTCK " : " ")) + "castPos: " + castPos + " rawAct: " + rawActivity + " opt: " + pOption + " skillID: " + useSkillId + " skillLV: " + useSkillLevel + " " + "allowSkill: " + nextMovementCouldBeSkill); + System.out.println((isSkill ? "SKILL " : (isAttack ? "ATTCK " : " ")) + "castPos: " + castPos + " rawAct: " + rawActivity + " opt: " + pOption + " skillID: " + useSkillId + " skillLV: " + useSkillLevel + " " + "allowSkill: " + nextMovementCouldBeSkill + " mobMp: " + mobMp); } map.broadcastMessage(player, MaplePacketCreator.moveMonster(objectid, nextMovementCouldBeSkill, rawActivity, useSkillId, useSkillLevel, pOption, startPos, res), monster.getPosition()); diff --git a/src/net/server/channel/handlers/PlayerInteractionHandler.java b/src/net/server/channel/handlers/PlayerInteractionHandler.java index d674c85ac0..8e06663fea 100644 --- a/src/net/server/channel/handlers/PlayerInteractionHandler.java +++ b/src/net/server/channel/handlers/PlayerInteractionHandler.java @@ -35,6 +35,7 @@ import net.AbstractMaplePacketHandler; import server.MapleItemInformationProvider; import server.MapleTrade; import constants.GameConstants; +import java.sql.SQLException; import server.maps.FieldLimit; import server.maps.MapleHiredMerchant; import server.maps.MapleMapObject; @@ -61,8 +62,8 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { CHAT(6), CHAT_THING(8), EXIT(0xA), - OPEN(0xB), - TRADE_BIRTHDAY(0x0E), + OPEN_STORE(0xB), + OPEN_CASH(0xE), SET_ITEMS(0xF), SET_MESO(0x10), CONFIRM(0x11), @@ -74,7 +75,7 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { REMOVE_ITEM(0x1B), BAN_PLAYER(0x1C), MERCHANT_THING(0x1D), - OPEN_STORE(0x1E), + OPEN_THING(0x1E), PUT_ITEM(0x21), MERCHANT_BUY(0x22), TAKE_ITEM_BACK(0x26), @@ -136,7 +137,7 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { try { byte mode = slea.readByte(); final MapleCharacter chr = c.getPlayer(); - + if (mode == Action.CREATE.getCode()) { if(!chr.isAlive()) { // thanks GabrielSin for pointing this chr.getClient().announce(MaplePacketCreator.getMiniRoomError(4)); @@ -333,14 +334,25 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { chr.closeMiniGame(); chr.closeHiredMerchant(true); } - } else if (mode == Action.OPEN.getCode()) { + } else if (mode == Action.OPEN_STORE.getCode() || mode == Action.OPEN_CASH.getCode()) { if (isTradeOpen(chr)) return; + + if (mode == Action.OPEN_STORE.getCode()) { + slea.readByte(); //01 + } else { + slea.readShort(); + int birthday = slea.readInt(); + if (!CashOperationHandler.checkBirthday(c, birthday)) { // birthday check here found thanks to lucasziron + c.announce(MaplePacketCreator.serverNotice(1, "Please check again the birthday date.")); + return; + } + + c.announce(MaplePacketCreator.hiredMerchantOwnerMaintenanceLeave()); + } MaplePlayerShop shop = chr.getPlayerShop(); MapleHiredMerchant merchant = chr.getHiredMerchant(); if (shop != null && shop.isOwner(chr)) { - slea.readByte();//01 - if(ServerConstants.USE_ERASE_PERMIT_ON_OPENSHOP) { try { MapleInventoryManipulator.removeById(c, MapleInventoryType.CASH, shop.getItemId(), 1, true, false); @@ -355,7 +367,6 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { chr.getMap().addMapObject(merchant); chr.setHiredMerchant(null); chr.getMap().broadcastMessage(MaplePacketCreator.spawnHiredMerchantBox(merchant)); - slea.readByte(); } } else if (mode == Action.READY.getCode()) { MapleMiniGame game = chr.getMiniGame(); @@ -528,7 +539,7 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { c.announce(MaplePacketCreator.serverNotice(1, "Cash items are not allowed to be sold on the Player Store.")); return; } - + if (ServerConstants.USE_ENFORCE_UNMERCHABLE_PET && ItemConstants.isPet(ivItem.getItemId())) { c.announce(MaplePacketCreator.serverNotice(1, "Pets are not allowed to be sold on the Player Store.")); return; @@ -543,26 +554,44 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { MaplePlayerShop shop = chr.getPlayerShop(); MapleHiredMerchant merchant = chr.getHiredMerchant(); if (shop != null && shop.isOwner(chr)) { - if (shop.isOpen()) { + if (shop.isOpen() || !shop.addItem(shopItem)) { // thanks Vcoc for pointing an exploit with unlimited shop slots c.announce(MaplePacketCreator.serverNotice(1, "You can't sell it anymore.")); return; } - - shop.addItem(shopItem); + + if (ItemConstants.isRechargeable(ivItem.getItemId())) { + MapleInventoryManipulator.removeFromSlot(c, ivType, slot, ivItem.getQuantity(), true); + } else { + MapleInventoryManipulator.removeFromSlot(c, ivType, slot, (short) (bundles * perBundle), true); + } + c.announce(MaplePacketCreator.getPlayerShopItemUpdate(shop)); } else if (merchant != null && merchant.isOwner(chr)) { - if (merchant.isOpen()) { + if (ivType.equals(MapleInventoryType.CASH) && merchant.isPublished()) { + c.announce(MaplePacketCreator.serverNotice(1, "Cash items are only allowed to be sold when first opening the store.")); + return; + } + + if (merchant.isOpen() || !merchant.addItem(shopItem)) { // thanks Vcoc for pointing an exploit with unlimited shop slots c.announce(MaplePacketCreator.serverNotice(1, "You can't sell it anymore.")); return; } - - merchant.addItem(shopItem); + + if (ItemConstants.isRechargeable(ivItem.getItemId())) { + MapleInventoryManipulator.removeFromSlot(c, ivType, slot, ivItem.getQuantity(), true); + } else { + MapleInventoryManipulator.removeFromSlot(c, ivType, slot, (short) (bundles * perBundle), true); + } + c.announce(MaplePacketCreator.updateHiredMerchant(merchant, chr)); - } - if (ItemConstants.isRechargeable(ivItem.getItemId())) { - MapleInventoryManipulator.removeFromSlot(c, ivType, slot, ivItem.getQuantity(), true); + + try { + merchant.saveItems(false); // thanks Masterrulax for realizing yet another dupe with merchants/Fredrick + } catch (SQLException ex) { + ex.printStackTrace(); + } } else { - MapleInventoryManipulator.removeFromSlot(c, ivType, slot, (short) (bundles * perBundle), true); + c.announce(MaplePacketCreator.serverNotice(1, "You can't sell without owning a shop.")); } } else if (mode == Action.REMOVE_ITEM.getCode()) { if (isTradeOpen(chr)) return; @@ -616,8 +645,9 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { MaplePlayerShop shop = chr.getPlayerShop(); MapleHiredMerchant merchant = chr.getHiredMerchant(); if (shop != null && shop.isVisitor(chr)) { - shop.buy(c, itemid, quantity); - shop.broadcast(MaplePacketCreator.getPlayerShopItemUpdate(shop)); + if (shop.buy(c, itemid, quantity)) { + shop.broadcast(MaplePacketCreator.getPlayerShopItemUpdate(shop)); + } } else if (merchant != null && !merchant.isOwner(chr)) { merchant.buy(c, itemid, quantity); merchant.broadcastToVisitorsThreadsafe(MaplePacketCreator.updateHiredMerchant(merchant, chr)); @@ -661,6 +691,7 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { if (merchant.isOwner(chr)) { merchant.clearMessages(); merchant.setOpen(true); + merchant.getMap().broadcastMessage(MaplePacketCreator.updateHiredMerchantBox(merchant)); } } diff --git a/src/net/server/channel/handlers/PlayerLoggedinHandler.java b/src/net/server/channel/handlers/PlayerLoggedinHandler.java index 527d9581c6..cb56de4fe2 100644 --- a/src/net/server/channel/handlers/PlayerLoggedinHandler.java +++ b/src/net/server/channel/handlers/PlayerLoggedinHandler.java @@ -62,7 +62,9 @@ import constants.ServerConstants; import java.net.InetSocketAddress; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import net.server.coordinator.MapleEventRecallCoordinator; import net.server.coordinator.MapleSessionCoordinator; import org.apache.mina.core.session.IoSession; @@ -72,6 +74,25 @@ import tools.packets.Wedding; public final class PlayerLoggedinHandler extends AbstractMaplePacketHandler { + private static Set attemptingLoginAccounts = new HashSet<>(); + + private boolean tryAcquireAccount(int accId) { + synchronized (attemptingLoginAccounts) { + if (attemptingLoginAccounts.contains(accId)) { + return false; + } + + attemptingLoginAccounts.add(accId); + return true; + } + } + + private void releaseAccount(int accId) { + synchronized (attemptingLoginAccounts) { + attemptingLoginAccounts.remove(accId); + } + } + @Override public final boolean validateState(MapleClient c) { return !c.isLoggedIn(); @@ -82,298 +103,313 @@ public final class PlayerLoggedinHandler extends AbstractMaplePacketHandler { final int cid = slea.readInt(); final Server server = Server.getInstance(); - World wserv = server.getWorld(c.getWorld()); - if(wserv == null) { - c.disconnect(true, false); - return; - } - - Channel cserv = wserv.getChannel(c.getChannel()); - if(cserv == null) { - c.setChannel(1); - cserv = wserv.getChannel(c.getChannel()); - - if(cserv == null) { - c.disconnect(true, false); - return; - } - } - - MapleCharacter player = wserv.getPlayerStorage().getCharacterById(cid); - boolean newcomer = false; - - IoSession session = c.getSession(); - String remoteHwid; - if (player == null) { - if (!server.validateCharacteridInTransition((InetSocketAddress) session.getRemoteAddress(), cid)) { - c.disconnect(true, false); - return; - } - - remoteHwid = MapleSessionCoordinator.getInstance().getGameSessionHwid(session); - if (remoteHwid == null) { - c.disconnect(true, false); - return; - } - + if (c.tryacquireClient()) { // thanks MedicOP for assisting on concurrency protection here try { - player = MapleCharacter.loadCharFromDB(cid, c, true); - newcomer = true; - } catch (SQLException e) { - e.printStackTrace(); - } - } else { - remoteHwid = player.getClient().getHWID(); - } - - if (player == null) { //If you are still getting null here then please just uninstall the game >.>, we dont need you fucking with the logs - c.disconnect(true, false); - return; - } - - c.setPlayer(player); - c.setAccID(player.getAccountID()); - - boolean allowLogin = true; - - /* is this check really necessary? - if (state == MapleClient.LOGIN_SERVER_TRANSITION || state == MapleClient.LOGIN_NOTLOGGEDIN) { - List charNames = c.loadCharacterNames(c.getWorld()); - if(!newcomer) { - charNames.remove(player.getName()); - } - - for (String charName : charNames) { - if(wserv.getPlayerStorage().getCharacterByName(charName) != null) { - allowLogin = false; - break; - } - } - } - */ - - c.lockClient(); // Sync this to prevent wrong login state for double channel changes - try { - int state = c.getLoginState(); - if (state != MapleClient.LOGIN_SERVER_TRANSITION || !allowLogin) { - c.setPlayer(null); - c.setAccID(0); - - if (state == MapleClient.LOGIN_LOGGEDIN) { + World wserv = server.getWorld(c.getWorld()); + if(wserv == null) { c.disconnect(true, false); - } else { - c.announce(MaplePacketCreator.getAfterLoginError(7)); + return; } - - return; - } - c.updateLoginState(MapleClient.LOGIN_LOGGEDIN); - } finally { - c.unlockClient(); - } - - if (!newcomer) { - c.setCharacterSlots((byte) player.getClient().getCharacterSlots()); - player.newClient(c); - } - - int hwidLen = remoteHwid.length(); - session.setAttribute(MapleClient.CLIENT_HWID, remoteHwid); - session.setAttribute(MapleClient.CLIENT_NIBBLEHWID, remoteHwid.substring(hwidLen - 8, hwidLen)); - c.setHWID(remoteHwid); - - cserv.addPlayer(player); - wserv.addPlayer(player); - player.setEnteredChannelWorld(); - - List buffs = server.getPlayerBuffStorage().getBuffsFromStorage(cid); - if (buffs != null) { - List> timedBuffs = getLocalStartTimes(buffs); - player.silentGiveBuffs(timedBuffs); - } - - Map> diseases = server.getPlayerBuffStorage().getDiseasesFromStorage(cid); - if (diseases != null) { - player.silentApplyDiseases(diseases); - } - - c.announce(MaplePacketCreator.getCharInfo(player)); - if (!player.isHidden()) { - if(player.isGM() && ServerConstants.USE_AUTOHIDE_GM) { - player.toggleHide(true); - } - } - player.sendKeymap(); - player.sendMacros(); - - // pot bindings being passed through other characters on the account detected thanks to Croosade dev team - MapleKeyBinding autohpPot = player.getKeymap().get(91); - player.announce(MaplePacketCreator.sendAutoHpPot(autohpPot != null ? autohpPot.getAction() : 0)); - - MapleKeyBinding autompPot = player.getKeymap().get(92); - player.announce(MaplePacketCreator.sendAutoMpPot(autompPot != null ? autompPot.getAction() : 0)); - - player.getMap().addPlayer(player); - player.visitMap(player.getMap()); - - BuddyList bl = player.getBuddylist(); - int buddyIds[] = bl.getBuddyIds(); - wserv.loggedOn(player.getName(), player.getId(), c.getChannel(), buddyIds); - for (CharacterIdChannelPair onlineBuddy : wserv.multiBuddyFind(player.getId(), buddyIds)) { - BuddylistEntry ble = bl.get(onlineBuddy.getCharacterId()); - ble.setChannel(onlineBuddy.getChannel()); - bl.put(ble); - } - c.announce(MaplePacketCreator.updateBuddylist(bl.getBuddies())); - - c.announce(MaplePacketCreator.loadFamily(player)); - if (player.getFamilyId() > 0) { - MapleFamily f = wserv.getFamily(player.getFamilyId()); - if (f == null) { - f = new MapleFamily(player.getId()); - wserv.addFamily(player.getFamilyId(), f); - } - player.setFamily(f); - c.announce(MaplePacketCreator.getFamilyInfo(f.getMember(player.getId()))); - } - if (player.getGuildId() > 0) { - MapleGuild playerGuild = server.getGuild(player.getGuildId(), player.getWorld(), player); - if (playerGuild == null) { - player.deleteGuild(player.getGuildId()); - player.getMGC().setGuildId(0); - player.getMGC().setGuildRank(5); - } else { - playerGuild.getMGC(player.getId()).setCharacter(player); - player.setMGC(playerGuild.getMGC(player.getId())); - server.setGuildMemberOnline(player, true, c.getChannel()); - c.announce(MaplePacketCreator.showGuildInfo(player)); - int allianceId = player.getGuild().getAllianceId(); - if (allianceId > 0) { - MapleAlliance newAlliance = server.getAlliance(allianceId); - if (newAlliance == null) { - newAlliance = MapleAlliance.loadAlliance(allianceId); - if (newAlliance != null) { - server.addAlliance(allianceId, newAlliance); - } else { - player.getGuild().setAllianceId(0); - } - } - if (newAlliance != null) { - c.announce(MaplePacketCreator.updateAllianceInfo(newAlliance, c)); - c.announce(MaplePacketCreator.allianceNotice(newAlliance.getId(), newAlliance.getNotice())); - - if (newcomer) { - server.allianceMessage(allianceId, MaplePacketCreator.allianceMemberOnline(player, true), player.getId(), -1); - } - } - } - } - } - player.showNote(); - if (player.getParty() != null) { - MaplePartyCharacter pchar = player.getMPC(); - - //Use this in case of enabling party HPbar HUD when logging in, however "you created a party" will appear on chat. - //c.announce(MaplePacketCreator.partyCreated(pchar)); - - pchar.setChannel(c.getChannel()); - pchar.setMapId(player.getMapId()); - pchar.setOnline(true); - wserv.updateParty(player.getParty().getId(), PartyOperation.LOG_ONOFF, pchar); - player.updatePartyMemberHP(); - } - - MapleInventory eqpInv = player.getInventory(MapleInventoryType.EQUIPPED); - eqpInv.lockInventory(); - try { - for(Item it : eqpInv.list()) { - player.equippedItem((Equip) it); - } - } finally { - eqpInv.unlockInventory(); - } - - c.announce(MaplePacketCreator.updateBuddylist(player.getBuddylist().getBuddies())); - - CharacterNameAndId pendingBuddyRequest = c.getPlayer().getBuddylist().pollPendingRequest(); - if (pendingBuddyRequest != null) { - c.announce(MaplePacketCreator.requestBuddylistAdd(pendingBuddyRequest.getId(), c.getPlayer().getId(), pendingBuddyRequest.getName())); - } - - c.announce(MaplePacketCreator.updateGender(player)); - player.checkMessenger(); - c.announce(MaplePacketCreator.enableReport()); - player.changeSkillLevel(SkillFactory.getSkill(10000000 * player.getJobType() + 12), (byte) (player.getLinkedLevel() / 10), 20, -1); - player.checkBerserk(player.isHidden()); - - if (newcomer) { - for(MaplePet pet : player.getPets()) { - if(pet != null) - wserv.registerPetHunger(player, player.getPetIndex(pet)); - } - - player.reloadQuestExpirations(); - - /* - if (!c.hasVotedAlready()){ - player.announce(MaplePacketCreator.earnTitleMessage("You can vote now! Vote and earn a vote point!")); - } - */ - if (player.isGM()){ - Server.getInstance().broadcastGMMessage(c.getWorld(), MaplePacketCreator.earnTitleMessage((player.gmLevel() < 6 ? "GM " : "Admin ") + player.getName() + " has logged in")); - } - - if(diseases != null) { - for(Entry> e : diseases.entrySet()) { - final List> debuff = Collections.singletonList(new Pair<>(e.getKey(), Integer.valueOf(e.getValue().getRight().getX()))); - c.announce(MaplePacketCreator.giveDebuff(debuff, e.getValue().getRight())); + Channel cserv = wserv.getChannel(c.getChannel()); + if(cserv == null) { + c.setChannel(1); + cserv = wserv.getChannel(c.getChannel()); + + if(cserv == null) { + c.disconnect(true, false); + return; + } + } + + MapleCharacter player = wserv.getPlayerStorage().getCharacterById(cid); + boolean newcomer = false; + + IoSession session = c.getSession(); + String remoteHwid; + if (player == null) { + if (!server.validateCharacteridInTransition((InetSocketAddress) session.getRemoteAddress(), cid)) { + c.disconnect(true, false); + return; + } + + remoteHwid = MapleSessionCoordinator.getInstance().getGameSessionHwid(session); + if (remoteHwid == null) { + c.disconnect(true, false); + return; + } + + try { + player = MapleCharacter.loadCharFromDB(cid, c, true); + newcomer = true; + } catch (SQLException e) { + e.printStackTrace(); + } + } else { + remoteHwid = player.getClient().getHWID(); + } + + if (player == null) { //If you are still getting null here then please just uninstall the game >.>, we dont need you fucking with the logs + c.disconnect(true, false); + return; + } + + c.setPlayer(player); + c.setAccID(player.getAccountID()); + + boolean allowLogin = true; + + /* is this check really necessary? + if (state == MapleClient.LOGIN_SERVER_TRANSITION || state == MapleClient.LOGIN_NOTLOGGEDIN) { + List charNames = c.loadCharacterNames(c.getWorld()); + if(!newcomer) { + charNames.remove(player.getName()); + } + + for (String charName : charNames) { + if(wserv.getPlayerStorage().getCharacterByName(charName) != null) { + allowLogin = false; + break; + } + } + } + */ + + int accId = c.getAccID(); + if (tryAcquireAccount(accId)) { // Sync this to prevent wrong login state for double loggedin handling + try { + int state = c.getLoginState(); + if (state != MapleClient.LOGIN_SERVER_TRANSITION || !allowLogin) { + c.setPlayer(null); + c.setAccID(0); + + if (state == MapleClient.LOGIN_LOGGEDIN) { + c.disconnect(true, false); + } else { + c.announce(MaplePacketCreator.getAfterLoginError(7)); + } + + return; + } + c.updateLoginState(MapleClient.LOGIN_LOGGEDIN); + } finally { + releaseAccount(accId); + } + } else { + c.setPlayer(null); + c.setAccID(0); + c.announce(MaplePacketCreator.getAfterLoginError(10)); + return; } - player.announceDiseases(); + if (!newcomer) { + c.setCharacterSlots((byte) player.getClient().getCharacterSlots()); + player.newClient(c); + } + + int hwidLen = remoteHwid.length(); + session.setAttribute(MapleClient.CLIENT_HWID, remoteHwid); + session.setAttribute(MapleClient.CLIENT_NIBBLEHWID, remoteHwid.substring(hwidLen - 8, hwidLen)); + c.setHWID(remoteHwid); + + cserv.addPlayer(player); + wserv.addPlayer(player); + player.setEnteredChannelWorld(); + + List buffs = server.getPlayerBuffStorage().getBuffsFromStorage(cid); + if (buffs != null) { + List> timedBuffs = getLocalStartTimes(buffs); + player.silentGiveBuffs(timedBuffs); + } + + Map> diseases = server.getPlayerBuffStorage().getDiseasesFromStorage(cid); + if (diseases != null) { + player.silentApplyDiseases(diseases); + } + + c.announce(MaplePacketCreator.getCharInfo(player)); + if (!player.isHidden()) { + if(player.isGM() && ServerConstants.USE_AUTOHIDE_GM) { + player.toggleHide(true); + } + } + player.sendKeymap(); + player.sendMacros(); + + // pot bindings being passed through other characters on the account detected thanks to Croosade dev team + MapleKeyBinding autohpPot = player.getKeymap().get(91); + player.announce(MaplePacketCreator.sendAutoHpPot(autohpPot != null ? autohpPot.getAction() : 0)); + + MapleKeyBinding autompPot = player.getKeymap().get(92); + player.announce(MaplePacketCreator.sendAutoMpPot(autompPot != null ? autompPot.getAction() : 0)); + + player.getMap().addPlayer(player); + player.visitMap(player.getMap()); + + BuddyList bl = player.getBuddylist(); + int buddyIds[] = bl.getBuddyIds(); + wserv.loggedOn(player.getName(), player.getId(), c.getChannel(), buddyIds); + for (CharacterIdChannelPair onlineBuddy : wserv.multiBuddyFind(player.getId(), buddyIds)) { + BuddylistEntry ble = bl.get(onlineBuddy.getCharacterId()); + ble.setChannel(onlineBuddy.getChannel()); + bl.put(ble); + } + c.announce(MaplePacketCreator.updateBuddylist(bl.getBuddies())); + + c.announce(MaplePacketCreator.loadFamily(player)); + if (player.getFamilyId() > 0) { + MapleFamily f = wserv.getFamily(player.getFamilyId()); + if (f == null) { + f = new MapleFamily(player.getId()); + wserv.addFamily(player.getFamilyId(), f); + } + player.setFamily(f); + c.announce(MaplePacketCreator.getFamilyInfo(f.getMember(player.getId()))); + } + if (player.getGuildId() > 0) { + MapleGuild playerGuild = server.getGuild(player.getGuildId(), player.getWorld(), player); + if (playerGuild == null) { + player.deleteGuild(player.getGuildId()); + player.getMGC().setGuildId(0); + player.getMGC().setGuildRank(5); + } else { + playerGuild.getMGC(player.getId()).setCharacter(player); + player.setMGC(playerGuild.getMGC(player.getId())); + server.setGuildMemberOnline(player, true, c.getChannel()); + c.announce(MaplePacketCreator.showGuildInfo(player)); + int allianceId = player.getGuild().getAllianceId(); + if (allianceId > 0) { + MapleAlliance newAlliance = server.getAlliance(allianceId); + if (newAlliance == null) { + newAlliance = MapleAlliance.loadAlliance(allianceId); + if (newAlliance != null) { + server.addAlliance(allianceId, newAlliance); + } else { + player.getGuild().setAllianceId(0); + } + } + if (newAlliance != null) { + c.announce(MaplePacketCreator.updateAllianceInfo(newAlliance, c)); + c.announce(MaplePacketCreator.allianceNotice(newAlliance.getId(), newAlliance.getNotice())); + + if (newcomer) { + server.allianceMessage(allianceId, MaplePacketCreator.allianceMemberOnline(player, true), player.getId(), -1); + } + } + } + } + } + + player.showNote(); + if (player.getParty() != null) { + MaplePartyCharacter pchar = player.getMPC(); + + //Use this in case of enabling party HPbar HUD when logging in, however "you created a party" will appear on chat. + //c.announce(MaplePacketCreator.partyCreated(pchar)); + + pchar.setChannel(c.getChannel()); + pchar.setMapId(player.getMapId()); + pchar.setOnline(true); + wserv.updateParty(player.getParty().getId(), PartyOperation.LOG_ONOFF, pchar); + player.updatePartyMemberHP(); + } + + MapleInventory eqpInv = player.getInventory(MapleInventoryType.EQUIPPED); + eqpInv.lockInventory(); + try { + for(Item it : eqpInv.list()) { + player.equippedItem((Equip) it); + } + } finally { + eqpInv.unlockInventory(); + } + + c.announce(MaplePacketCreator.updateBuddylist(player.getBuddylist().getBuddies())); + + CharacterNameAndId pendingBuddyRequest = c.getPlayer().getBuddylist().pollPendingRequest(); + if (pendingBuddyRequest != null) { + c.announce(MaplePacketCreator.requestBuddylistAdd(pendingBuddyRequest.getId(), c.getPlayer().getId(), pendingBuddyRequest.getName())); + } + + c.announce(MaplePacketCreator.updateGender(player)); + player.checkMessenger(); + c.announce(MaplePacketCreator.enableReport()); + player.changeSkillLevel(SkillFactory.getSkill(10000000 * player.getJobType() + 12), (byte) (player.getLinkedLevel() / 10), 20, -1); + player.checkBerserk(player.isHidden()); + + if (newcomer) { + for(MaplePet pet : player.getPets()) { + if(pet != null) + wserv.registerPetHunger(player, player.getPetIndex(pet)); + } + + player.reloadQuestExpirations(); + + /* + if (!c.hasVotedAlready()){ + player.announce(MaplePacketCreator.earnTitleMessage("You can vote now! Vote and earn a vote point!")); + } + */ + if (player.isGM()){ + Server.getInstance().broadcastGMMessage(c.getWorld(), MaplePacketCreator.earnTitleMessage((player.gmLevel() < 6 ? "GM " : "Admin ") + player.getName() + " has logged in")); + } + + if(diseases != null) { + for(Entry> e : diseases.entrySet()) { + final List> debuff = Collections.singletonList(new Pair<>(e.getKey(), Integer.valueOf(e.getValue().getRight().getX()))); + c.announce(MaplePacketCreator.giveDebuff(debuff, e.getValue().getRight())); + } + + player.announceDiseases(); + } + } else { + if(player.isRidingBattleship()) { + player.announceBattleshipHp(); + } + } + + player.buffExpireTask(); + player.diseaseExpireTask(); + player.skillCooldownTask(); + player.expirationTask(); + player.questExpirationTask(); + if (GameConstants.hasSPTable(player.getJob()) && player.getJob().getId() != 2001) { + player.createDragon(); + } + + player.commitExcludedItems(); + showDueyNotification(c, player); + + if (player.getMap().getHPDec() > 0) player.resetHpDecreaseTask(); + + player.resetPlayerRates(); + if(ServerConstants.USE_ADD_RATES_BY_LEVEL == true) player.setPlayerRates(); + player.setWorldRates(); + player.updateCouponRates(); + + player.receivePartyMemberHP(); + + if(player.getPartnerId() > 0) { + int partnerId = player.getPartnerId(); + final MapleCharacter partner = wserv.getPlayerStorage().getCharacterById(partnerId); + + if(partner != null && !partner.isAwayFromWorld()) { + player.announce(Wedding.OnNotifyWeddingPartnerTransfer(partnerId, partner.getMapId())); + partner.announce(Wedding.OnNotifyWeddingPartnerTransfer(player.getId(), player.getMapId())); + } + } + + if (newcomer) { + EventInstanceManager eim = MapleEventRecallCoordinator.getInstance().recallEventInstance(cid); + if (eim != null) { + eim.registerPlayer(player); + } + } + } finally { + c.releaseClient(); } } else { - if(player.isRidingBattleship()) { - player.announceBattleshipHp(); - } - } - - player.buffExpireTask(); - player.diseaseExpireTask(); - player.skillCooldownTask(); - player.expirationTask(); - player.questExpirationTask(); - if (GameConstants.hasSPTable(player.getJob()) && player.getJob().getId() != 2001) { - player.createDragon(); - } - - player.commitExcludedItems(); - showDueyNotification(c, player); - - if (player.getMap().getHPDec() > 0) player.resetHpDecreaseTask(); - - player.resetPlayerRates(); - if(ServerConstants.USE_ADD_RATES_BY_LEVEL == true) player.setPlayerRates(); - player.setWorldRates(); - player.updateCouponRates(); - - player.receivePartyMemberHP(); - - if(player.getPartnerId() > 0) { - int partnerId = player.getPartnerId(); - final MapleCharacter partner = wserv.getPlayerStorage().getCharacterById(partnerId); - - if(partner != null && !partner.isAwayFromWorld()) { - player.announce(Wedding.OnNotifyWeddingPartnerTransfer(partnerId, partner.getMapId())); - partner.announce(Wedding.OnNotifyWeddingPartnerTransfer(player.getId(), player.getMapId())); - } - } - - if (newcomer) { - EventInstanceManager eim = MapleEventRecallCoordinator.getInstance().recallEventInstance(cid); - if (eim != null) { - eim.registerPlayer(player); - } + c.announce(MaplePacketCreator.getAfterLoginError(10)); } } diff --git a/src/net/server/channel/handlers/SpecialMoveHandler.java b/src/net/server/channel/handlers/SpecialMoveHandler.java index 1fb868d340..b40b5ab874 100644 --- a/src/net/server/channel/handlers/SpecialMoveHandler.java +++ b/src/net/server/channel/handlers/SpecialMoveHandler.java @@ -91,26 +91,22 @@ public final class SpecialMoveHandler extends AbstractMaplePacketHandler { } if (skillid == Hero.MONSTER_MAGNET || skillid == Paladin.MONSTER_MAGNET || skillid == DarkKnight.MONSTER_MAGNET) { // Monster Magnet int num = slea.readInt(); - int mobId; - byte success; for (int i = 0; i < num; i++) { - mobId = slea.readInt(); - success = slea.readByte(); - chr.getMap().broadcastMessage(chr, MaplePacketCreator.showMagnet(mobId, success), false); - MapleMonster monster = chr.getMap().getMonsterByOid(mobId); + int mobOid = slea.readInt(); + byte success = slea.readByte(); + chr.getMap().broadcastMessage(chr, MaplePacketCreator.catchMonster(mobOid, success), false); + MapleMonster monster = chr.getMap().getMonsterByOid(mobOid); if (monster != null) { if (!monster.isBoss()) { - monster.lockMonster(); - try { - monster.switchController(chr, monster.isControllerHasAggro()); - } finally { - monster.unlockMonster(); - } + monster.aggroClearDamages(); + monster.aggroMonsterDamage(chr, 1); + + monster.aggroSwitchController(chr, true); } } } - byte direction = slea.readByte(); - chr.getMap().broadcastMessage(chr, MaplePacketCreator.showBuffeffect(chr.getId(), skillid, chr.getSkillLevel(skillid), direction), false); + byte direction = slea.readByte(); // thanks MedicOP for pointing some 3rd-party related issues with Magnet + chr.getMap().broadcastMessage(chr, MaplePacketCreator.showBuffeffect(chr.getId(), skillid, chr.getSkillLevel(skillid), 1, direction), false); c.announce(MaplePacketCreator.enableActions()); return; } else if (skillid == Brawler.MP_RECOVERY) {// MP Recovery diff --git a/src/net/server/channel/handlers/TakeDamageHandler.java b/src/net/server/channel/handlers/TakeDamageHandler.java index b38e04a225..9a67b895a2 100644 --- a/src/net/server/channel/handlers/TakeDamageHandler.java +++ b/src/net/server/channel/handlers/TakeDamageHandler.java @@ -191,7 +191,7 @@ public final class TakeDamageHandler extends AbstractMaplePacketHandler { damage -= bouncedamage; map.damageMonster(chr, attacker, bouncedamage); map.broadcastMessage(chr, MaplePacketCreator.damageMonster(oid, bouncedamage), false, true); - chr.checkMonsterAggro(attacker); + attacker.aggroMonsterDamage(chr, bouncedamage); } if (attacker != null && damagefrom == -1 && chr.getBuffedValue(MapleBuffStat.BODY_PRESSURE) != null) { Skill skill = SkillFactory.getSkill(Aran.BODY_PRESSURE); diff --git a/src/net/server/coordinator/MapleMonsterAggroCoordinator.java b/src/net/server/coordinator/MapleMonsterAggroCoordinator.java new file mode 100644 index 0000000000..c2f2224afd --- /dev/null +++ b/src/net/server/coordinator/MapleMonsterAggroCoordinator.java @@ -0,0 +1,371 @@ +/* + This file is part of the HeavenMS MapleStory Server + Copyleft (L) 2016 - 2018 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 net.server.coordinator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +import constants.ServerConstants; +import client.MapleCharacter; +import java.util.concurrent.ScheduledFuture; +import net.server.Server; +import net.server.audit.LockCollector; +import server.life.MapleMonster; +import server.maps.MapleMap; +import net.server.audit.locks.MonitoredLockType; +import net.server.audit.locks.MonitoredReentrantLock; +import net.server.audit.locks.factory.MonitoredReentrantLockFactory; +import server.TimerManager; +import tools.Pair; + +/** + * + * @author Ronan + */ +public class MapleMonsterAggroCoordinator { + + private MonitoredReentrantLock lock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MAP_AGGRO); + private MonitoredReentrantLock idleLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MAP_AGGRO_IDLE, true); + private long lastStopTime = Server.getInstance().getCurrentTime(); + + private ScheduledFuture aggroMonitor = null; + + private Map> mobAggroEntries = new HashMap<>(); + private Map> mobSortedAggros = new HashMap<>(); + + private class PlayerAggroEntry { + protected int cid; + protected int averageDamage = 0; + protected int currentDamageInstances = 0; + protected long accumulatedDamage = 0; + + protected int expireStreak = 0; + protected int updateStreak = 0; + protected int toNextUpdate = 0; + protected int entryRank = -1; + + protected PlayerAggroEntry(int cid) { + this.cid = cid; + } + } + + public void stopAggroCoordinator() { + idleLock.lock(); + try { + if (aggroMonitor == null) return; + + aggroMonitor.cancel(false); + aggroMonitor = null; + } finally { + idleLock.unlock(); + } + + lastStopTime = Server.getInstance().getCurrentTime(); + } + + public void startAggroCoordinator() { + idleLock.lock(); + try { + if (aggroMonitor != null) return; + + aggroMonitor = TimerManager.getInstance().register(new Runnable() { + @Override + public void run() { + runAggroUpdate(1); + runSortLeadingCharactersAggro(); + } + }, ServerConstants.MOB_STATUS_AGGRO_INTERVAL, ServerConstants.MOB_STATUS_AGGRO_INTERVAL); + } finally { + idleLock.unlock(); + } + + int timeDelta = (int) Math.ceil((Server.getInstance().getCurrentTime() - lastStopTime) / ServerConstants.MOB_STATUS_AGGRO_INTERVAL); + if (timeDelta > 0) { + runAggroUpdate(timeDelta); + } + } + + private static void updateEntryExpiration(PlayerAggroEntry pae) { + pae.toNextUpdate = (int) Math.ceil((120000L / ServerConstants.MOB_STATUS_AGGRO_INTERVAL) / Math.pow(2, pae.expireStreak + pae.currentDamageInstances)); + } + + private static void insertEntryDamage(PlayerAggroEntry pae, int damage) { + synchronized (pae) { + long totalDamage = pae.averageDamage; + totalDamage *= pae.currentDamageInstances; + totalDamage += damage; + + pae.expireStreak = 0; + pae.updateStreak = 0; + updateEntryExpiration(pae); + + pae.currentDamageInstances += 1; + pae.averageDamage = (int)(totalDamage / pae.currentDamageInstances); + pae.accumulatedDamage = totalDamage; + } + } + + private static boolean expiredAfterUpdateEntryDamage(PlayerAggroEntry pae, int deltaTime) { + synchronized (pae) { + pae.updateStreak += 1; + pae.toNextUpdate -= deltaTime; + + if (pae.toNextUpdate <= 0) { // reached dmg instance expire time + pae.expireStreak += 1; + updateEntryExpiration(pae); + + pae.currentDamageInstances -= 1; + if (pae.currentDamageInstances < 1) { // expired aggro for this player + return true; + } + pae.accumulatedDamage = pae.averageDamage * pae.currentDamageInstances; + } + + return false; + } + } + + public void addAggroDamage(MapleMonster mob, int cid, int damage) { // assumption: should not trigger after dispose() + if (!mob.isAlive()) return; + + List sortedAggro = mobSortedAggros.get(mob); + Map mobAggro = mobAggroEntries.get(mob); + if (mobAggro == null) { + if (lock.tryLock()) { // can run unreliably, as fast as possible... try lock that is! + try { + mobAggro = mobAggroEntries.get(mob); + if (mobAggro == null) { + mobAggro = new HashMap<>(); + mobAggroEntries.put(mob, mobAggro); + + sortedAggro = new LinkedList<>(); + mobSortedAggros.put(mob, sortedAggro); + } else { + sortedAggro = mobSortedAggros.get(mob); + } + } finally { + lock.unlock(); + } + } else { + return; + } + } + + PlayerAggroEntry aggroEntry = mobAggro.get(cid); + if (aggroEntry == null) { + aggroEntry = new PlayerAggroEntry(cid); + + synchronized (mobAggro) { + synchronized (sortedAggro) { + PlayerAggroEntry mappedEntry = mobAggro.get(cid); + + if (mappedEntry == null) { + mobAggro.put(aggroEntry.cid, aggroEntry); + sortedAggro.add(aggroEntry); + } else { + aggroEntry = mappedEntry; + } + } + } + } else if (damage < 1) { + return; + } + + insertEntryDamage(aggroEntry, damage); + } + + private void runAggroUpdate(int deltaTime) { + List>> aggroMobs = new LinkedList<>(); + lock.lock(); + try { + for (Entry> e : mobAggroEntries.entrySet()) { + aggroMobs.add(new Pair<>(e.getKey(), e.getValue())); + } + } finally { + lock.unlock(); + } + + for (Pair> am : aggroMobs) { + Map mobAggro = am.getRight(); + List sortedAggro = mobSortedAggros.get(am.getLeft()); + + if (sortedAggro != null) { + List toRemove = new LinkedList<>(); + List toRemoveIdx = new ArrayList<>(mobAggro.size()); + List toRemoveByFetch = new LinkedList<>(); + + synchronized (mobAggro) { + synchronized (sortedAggro) { + for (PlayerAggroEntry pae : mobAggro.values()) { + if (expiredAfterUpdateEntryDamage(pae, deltaTime)) { + toRemove.add(pae.cid); + if (pae.entryRank > -1) toRemoveIdx.add(pae.entryRank); + else toRemoveByFetch.add(pae.cid); + } + } + + if (!toRemove.isEmpty()) { + for (Integer cid : toRemove) { + mobAggro.remove(cid); + } + + if (mobAggro.isEmpty()) { // all aggro on this mob expired + am.getLeft().aggroResetAggro(); + } + } + + if (!toRemoveIdx.isEmpty()) { + Collections.sort(toRemoveIdx, new Comparator() { // last to first indexes + @Override + public int compare(Integer p1, Integer p2) { + return p1 < p2 ? 1 : p1.equals(p2) ? 0 : -1; + } + }); + + for (int idx : toRemoveIdx) { + sortedAggro.remove(idx); + } + } + + if (!toRemoveByFetch.isEmpty()) { + for (Integer cid : toRemoveByFetch) { + for (int i = 0; i < sortedAggro.size(); i++) { + if (cid.equals(sortedAggro.get(i).cid)) { + sortedAggro.remove(i); + break; + } + } + } + } + } + } + } + } + } + + private static void insertionSortAggroList(List paeList) { + for (int i = 1; i < paeList.size(); i++) { + PlayerAggroEntry pae = paeList.get(i); + long curAccDmg = pae.accumulatedDamage; + + int j = i - 1; + while (j >= 0 && curAccDmg > paeList.get(j).accumulatedDamage) { + j -= 1; + } + + j += 1; + if (j != i) { + paeList.remove(i); + paeList.add(j, pae); + } + } + + int i = 0; + for (PlayerAggroEntry pae : paeList) { + pae.entryRank = i; + i += 1; + } + } + + public boolean isLeadingCharacterAggro(MapleMonster mob, MapleCharacter player) { + // by assuming the quasi-sorted nature of "mobAggroList", this method + // returns whether the player given as parameter can be elected as next aggro leader + + List mobAggroList = mobSortedAggros.get(mob); + if (mobAggroList != null) { + synchronized (mobAggroList) { + mobAggroList = new ArrayList<>(mobAggroList.subList(0, Math.min(mobAggroList.size(), 5))); + } + + MapleMap map = mob.getMap(); + for (PlayerAggroEntry pae : mobAggroList) { + MapleCharacter chr = map.getCharacterById(pae.cid); + if (chr != null) { + if (player.getId() == pae.cid) { + return true; + } else if (pae.updateStreak < ServerConstants.MOB_STATUS_AGGRO_PERSISTENCE && chr.isAlive()) { // verifies currently leading players activity + return false; + } + } + } + } + + return false; + } + + public void runSortLeadingCharactersAggro() { + List> aggroList; + lock.lock(); + try { + aggroList = new ArrayList<>(mobSortedAggros.values()); + } finally { + lock.unlock(); + } + + for (List mobAggroList : aggroList) { + synchronized (mobAggroList) { + insertionSortAggroList(mobAggroList); + } + } + } + + public void removeAggroEntries(MapleMonster mob) { + lock.lock(); + try { + mobAggroEntries.remove(mob); + mobSortedAggros.remove(mob); + } finally { + lock.unlock(); + } + } + + public void dispose() { + stopAggroCoordinator(); + + lock.lock(); + try { + mobAggroEntries.clear(); + mobSortedAggros.clear(); + } finally { + lock.unlock(); + } + + disposeLocks(); + } + + private void disposeLocks() { + LockCollector.getInstance().registerDisposeAction(new Runnable() { + @Override + public void run() { + emptyLocks(); + } + }); + } + + private void emptyLocks() { + lock = lock.dispose(); + } +} diff --git a/src/net/server/guild/MapleGuild.java b/src/net/server/guild/MapleGuild.java index 8d50395f9a..2969cb7ce4 100644 --- a/src/net/server/guild/MapleGuild.java +++ b/src/net/server/guild/MapleGuild.java @@ -39,6 +39,7 @@ import java.util.Set; import java.util.concurrent.locks.Lock; import net.server.audit.locks.factory.MonitoredReentrantLockFactory; +import net.server.PlayerStorage; import net.server.Server; import net.server.channel.Channel; import tools.DatabaseConnection; @@ -109,11 +110,11 @@ public class MapleGuild { con.close(); } catch (SQLException se) { se.printStackTrace(); - System.out.println("Unable to read guild information from sql" + se); + System.out.println("Unable to read guild information from sql: " + se); } } - public void buildNotifications() { + private void buildNotifications() { if (!bDirty) { return; } @@ -260,10 +261,10 @@ public class MapleGuild { return name; } - public java.util.Collection getMembers() { + public List getMembers() { membersLock.lock(); try { - return java.util.Collections.unmodifiableCollection(members); + return new ArrayList<>(members); } finally { membersLock.unlock(); } @@ -277,6 +278,30 @@ public class MapleGuild { return signature; } + public void broadcastNameChanged() { + PlayerStorage ps = Server.getInstance().getWorld(world).getPlayerStorage(); + + for (MapleGuildCharacter mgc : getMembers()) { + MapleCharacter chr = ps.getCharacterById(mgc.getId()); + if (chr == null || !chr.isLoggedinWorld()) continue; + + byte[] packet = MaplePacketCreator.guildNameChanged(chr.getId(), this.getName()); + chr.getMap().broadcastMessage(chr, packet); + } + } + + public void broadcastEmblemChanged() { + PlayerStorage ps = Server.getInstance().getWorld(world).getPlayerStorage(); + + for (MapleGuildCharacter mgc : getMembers()) { + MapleCharacter chr = ps.getCharacterById(mgc.getId()); + if (chr == null || !chr.isLoggedinWorld()) continue; + + byte[] packet = MaplePacketCreator.guildMarkChanged(chr.getId(), this); + chr.getMap().broadcastMessage(chr, packet); + } + } + public void broadcast(final byte[] packet) { broadcast(packet, -1, BCOp.NONE); } diff --git a/src/net/server/world/World.java b/src/net/server/world/World.java index 8a73a67336..df5a70799f 100644 --- a/src/net/server/world/World.java +++ b/src/net/server/world/World.java @@ -612,7 +612,15 @@ public class World { mc.saveGuildStatus(); } if (bDifferentGuild) { - mc.broadcastStance(); + if (mc.isLoggedinWorld()) { + MapleGuild guild = Server.getInstance().getGuild(guildid); + if (guild != null) { + mc.getMap().broadcastMessage(mc, MaplePacketCreator.guildNameChanged(cid, guild.getName())); + mc.getMap().broadcastMessage(mc, MaplePacketCreator.guildMarkChanged(cid, guild)); + } else { + mc.getMap().broadcastMessage(mc, MaplePacketCreator.guildNameChanged(cid, "")); + } + } } } diff --git a/src/scripting/AbstractPlayerInteraction.java b/src/scripting/AbstractPlayerInteraction.java index 6b3363ef8a..10984129e1 100644 --- a/src/scripting/AbstractPlayerInteraction.java +++ b/src/scripting/AbstractPlayerInteraction.java @@ -237,6 +237,15 @@ public class AbstractPlayerInteraction { return intList; } + public boolean canHoldAll(List itemids) { + List quantity = new LinkedList<>(); + for (int i = 0; i < itemids.size(); i++) { + quantity.add(1.0); + } + + return canHoldAll(itemids, quantity); + } + public boolean canHoldAll(List itemids, List quantity) { return canHoldAll(convertToIntegerArray(itemids), convertToIntegerArray(quantity), true); } @@ -554,8 +563,6 @@ public class AbstractPlayerInteraction { if(expires >= 0) item.setExpiration(System.currentTimeMillis() + expires); - - item.setPetId(petId); if (!MapleInventoryManipulator.checkSpace(c, id, quantity, "")) { c.getPlayer().dropMessage(1, "Your inventory is full. Please remove an item from your " + ItemConstants.getInventoryType(id).name() + " inventory."); diff --git a/src/scripting/event/EventInstanceManager.java b/src/scripting/event/EventInstanceManager.java index 09929317b8..10ffc8070c 100644 --- a/src/scripting/event/EventInstanceManager.java +++ b/src/scripting/event/EventInstanceManager.java @@ -229,7 +229,11 @@ public class EventInstanceManager { } - public synchronized void registerPlayer(final MapleCharacter chr) { + public synchronized void registerPlayer(final MapleCharacter chr) { + registerPlayer(chr, true); + } + + public synchronized void registerPlayer(final MapleCharacter chr, boolean runEntryScript) { if (chr == null || !chr.isLoggedinWorld() || disposed) { return; } @@ -246,10 +250,12 @@ public class EventInstanceManager { wL.unlock(); } - try { - em.getIv().invokeFunction("playerEntry", EventInstanceManager.this, chr); - } catch (ScriptException | NoSuchMethodException ex) { - ex.printStackTrace(); + if (runEntryScript) { + try { + em.getIv().invokeFunction("playerEntry", EventInstanceManager.this, chr); + } catch (ScriptException | NoSuchMethodException ex) { + ex.printStackTrace(); + } } } @@ -372,7 +378,7 @@ public class EventInstanceManager { private void registerExpeditionTeam(MapleExpedition exped, int recruitMap) { expedition = exped; - for (MapleCharacter chr: exped.getMembers()) { + for (MapleCharacter chr: exped.getActiveMembers()) { if (chr.getMapId() == recruitMap) { registerPlayer(chr); } @@ -628,7 +634,6 @@ public class EventInstanceManager { sL.lock(); try { if(!eventCleared) em.disposeInstance(name); - em = null; } finally { sL.unlock(); } @@ -636,10 +641,11 @@ public class EventInstanceManager { TimerManager.getInstance().schedule(new Runnable() { @Override public void run() { - mapFactory.dispose(); // reactors issue on dispose event maps found thanks to MedicOP + mapFactory.dispose(); // issues from instantly disposing some event objects found thanks to MedicOP wL.lock(); try { mapFactory = null; + em = null; } finally { wL.unlock(); } @@ -675,7 +681,6 @@ public class EventInstanceManager { @Override public void run() { try { - if(em == null) return; em.getIv().invokeFunction(methodName, EventInstanceManager.this); } catch (ScriptException | NoSuchMethodException ex) { ex.printStackTrace(); diff --git a/src/scripting/event/EventManager.java b/src/scripting/event/EventManager.java index ce87183b7f..b91ce46206 100644 --- a/src/scripting/event/EventManager.java +++ b/src/scripting/event/EventManager.java @@ -22,7 +22,6 @@ package scripting.event; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; @@ -116,13 +115,16 @@ public class EventManager { ex.printStackTrace(); } + Collection eimList; synchronized(instances) { - for(EventInstanceManager eim : instances.values()) { - eim.dispose(true); - } + eimList = getInstances(); instances.clear(); } + for(EventInstanceManager eim : eimList) { + eim.dispose(true); + } + List readyEims; queueLock.lock(); try { @@ -253,7 +255,9 @@ public class EventManager { } public Collection getInstances() { - return Collections.unmodifiableCollection(instances.values()); + synchronized (instances) { + return new LinkedList<>(instances.values()); + } } public EventInstanceManager newInstance(String name) { @@ -265,7 +269,13 @@ public class EventManager { ret.setName(name); } - instances.put(name, ret); + synchronized (instances) { + if (instances.containsKey(name)) { + return null; + } + + instances.put(name, ret); + } return ret; } @@ -274,7 +284,10 @@ public class EventManager { @Override public void run() { freeLobbyInstance(name); - instances.remove(name); + + synchronized (instances) { + instances.remove(name); + } } }, ServerConstants.EVENT_LOBBY_DELAY * 1000); } @@ -379,14 +392,17 @@ public class EventManager { if(!startLobbyInstance(lobbyId)) return false; } - EventInstanceManager eim = (EventInstanceManager) (iv.invokeFunction("setup", leader.getClient().getChannel())); - if(eim == null) { + EventInstanceManager eim; + try { + eim = (EventInstanceManager) (iv.invokeFunction("setup", leader.getClient().getChannel())); + instanceLocks.put(eim.getName(), lobbyId); + } catch (NullPointerException npe) { if(lobbyId > -1) { setLockLobby(lobbyId, false); } return false; } - instanceLocks.put(eim.getName(), lobbyId); + eim.setLeader(leader); exped.start(); @@ -442,14 +458,16 @@ public class EventManager { } } - EventInstanceManager eim = (EventInstanceManager) (iv.invokeFunction("setup", difficulty, (lobbyId > -1) ? lobbyId : leader.getId())); - if(eim == null) { + EventInstanceManager eim; + try { + eim = (EventInstanceManager) (iv.invokeFunction("setup", difficulty, (lobbyId > -1) ? lobbyId : leader.getId())); + instanceLocks.put(eim.getName(), lobbyId); + } catch (NullPointerException npe) { if(lobbyId > -1) { setLockLobby(lobbyId, false); } return false; } - instanceLocks.put(eim.getName(), lobbyId); eim.setLeader(leader); if(chr != null) eim.registerPlayer(chr); @@ -500,14 +518,17 @@ public class EventManager { if(!startLobbyInstance(lobbyId)) return false; } - EventInstanceManager eim = (EventInstanceManager) (iv.invokeFunction("setup", (Object) null)); - if(eim == null) { + EventInstanceManager eim; + try { + eim = (EventInstanceManager) (iv.invokeFunction("setup", (Object) null)); + instanceLocks.put(eim.getName(), lobbyId); + } catch (NullPointerException npe) { if(lobbyId > -1) { setLockLobby(lobbyId, false); } return false; } - instanceLocks.put(eim.getName(), lobbyId); + eim.setLeader(leader); eim.registerParty(party, map); @@ -559,14 +580,17 @@ public class EventManager { if(!startLobbyInstance(lobbyId)) return false; } - EventInstanceManager eim = (EventInstanceManager) (iv.invokeFunction("setup", difficulty, (lobbyId > -1) ? lobbyId : party.getLeaderId())); - if(eim == null) { + EventInstanceManager eim; + try { + eim = (EventInstanceManager) (iv.invokeFunction("setup", difficulty, (lobbyId > -1) ? lobbyId : party.getLeaderId())); + instanceLocks.put(eim.getName(), lobbyId); + } catch (NullPointerException npe) { if(lobbyId > -1) { setLockLobby(lobbyId, false); } return false; } - instanceLocks.put(eim.getName(), lobbyId); + eim.setLeader(leader); eim.registerParty(party, map); diff --git a/src/scripting/npc/NPCConversationManager.java b/src/scripting/npc/NPCConversationManager.java index 8c205358ba..0a8746270c 100644 --- a/src/scripting/npc/NPCConversationManager.java +++ b/src/scripting/npc/NPCConversationManager.java @@ -100,6 +100,7 @@ public class NPCConversationManager extends AbstractPlayerInteraction { public void dispose() { NPCScriptManager.getInstance().dispose(this); + getClient().announce(MaplePacketCreator.enableActions()); } public void sendNext(String text) { diff --git a/src/server/MapleTrade.java b/src/server/MapleTrade.java index b6a9ec82e3..f2110b31db 100644 --- a/src/server/MapleTrade.java +++ b/src/server/MapleTrade.java @@ -202,6 +202,10 @@ public class MapleTrade { return exchangeMeso; } + private boolean fitsMeso() { + return chr.canHoldMeso(exchangeMeso - getFee(exchangeMeso)); + } + private boolean fitsInInventory() { List> tradeItems = new LinkedList<>(); for (Item item : exchangeItems) { @@ -218,6 +222,18 @@ public class MapleTrade { if (partner.isLocked()) { local.complete1(); partner.complete1(); + if (!local.fitsMeso()) { + cancelTrade(c); + c.message("There is not enough meso inventory space to complete the trade."); + partner.getChr().message("Partner does not have enough meso inventory space to complete the trade."); + return; + } + else if (!partner.fitsMeso()) { + cancelTrade(c); + c.message("Partner does not have enough meso inventory space to complete the trade."); + partner.getChr().message("There is not enough meso inventory space to complete the trade."); + return; + } if (!local.fitsInInventory()) { cancelTrade(c); c.message("There is not enough inventory space to complete the trade."); diff --git a/src/server/expeditions/MapleExpedition.java b/src/server/expeditions/MapleExpedition.java index 3aefc4378a..341d6f649a 100644 --- a/src/server/expeditions/MapleExpedition.java +++ b/src/server/expeditions/MapleExpedition.java @@ -23,11 +23,17 @@ package server.expeditions; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; +import net.server.PlayerStorage; import net.server.Server; import server.TimerManager; import server.life.MapleMonster; @@ -78,18 +84,18 @@ public class MapleExpedition { private MapleExpeditionType type; private boolean registering; private MapleMap startMap; - private ArrayList bossLogs; + private List bossLogs; private ScheduledFuture schedule; - private List members = new ArrayList<>(); - private List banned = new ArrayList<>(); + private Map members = new ConcurrentHashMap<>(); + private List banned = new CopyOnWriteArrayList<>(); private long startTime; public MapleExpedition(MapleCharacter player, MapleExpeditionType met) { leader = player; - members.add(leader); + members.put(player.getId(), player.getName()); startMap = player.getMap(); type = met; - bossLogs = new ArrayList(); + bossLogs = new CopyOnWriteArrayList<>(); beginRegistration(); } @@ -109,7 +115,7 @@ public class MapleExpedition { @Override public void run() { if (registering){ - leader.getClient().getChannelServer().getExpeditions().remove(exped); + startMap.getChannelServer().getExpeditions().remove(exped); startMap.broadcastMessage(MaplePacketCreator.serverNotice(6, "[Expedition] The time limit has been reached. Expedition has been disbanded.")); dispose(false); @@ -134,7 +140,7 @@ public class MapleExpedition { broadcastExped(MaplePacketCreator.removeClock()); broadcastExped(MaplePacketCreator.serverNotice(6, "[Expedition] The expedition has started! Good luck, brave heroes!")); startTime = System.currentTimeMillis(); - Server.getInstance().broadcastGMMessage(leader.getWorld(), MaplePacketCreator.serverNotice(6, "[Expedition] " + type.toString() + " Expedition started with leader: " + leader.getName())); + Server.getInstance().broadcastGMMessage(startMap.getWorld(), MaplePacketCreator.serverNotice(6, "[Expedition] " + type.toString() + " Expedition started with leader: " + leader.getName())); } public String addMember(MapleCharacter player) { @@ -147,22 +153,21 @@ public class MapleExpedition { if (members.size() >= type.getMaxSize()){ //Would be a miracle if anybody ever saw this return "Sorry, this expedition is full!"; } - if (members.add(player)){ - player.announce(MaplePacketCreator.getClock((int)(startTime - System.currentTimeMillis()) / 1000)); - broadcastExped(MaplePacketCreator.serverNotice(6, "[Expedition] " + player.getName() + " has joined the expedition!")); - return "You have registered for the expedition successfully!"; - } - return "Sorry, something went really wrong. Report this on the forum with a screenshot!"; + + members.put(player.getId(), player.getName()); + player.announce(MaplePacketCreator.getClock((int)(startTime - System.currentTimeMillis()) / 1000)); + broadcastExped(MaplePacketCreator.serverNotice(6, "[Expedition] " + player.getName() + " has joined the expedition!")); + return "You have registered for the expedition successfully!"; } - private void broadcastExped(byte [] data){ - for (MapleCharacter member : members){ - member.getClient().announce(data); + private void broadcastExped(byte[] packet){ + for (MapleCharacter chr : getActiveMembers()){ + chr.announce(packet); } } public boolean removeMember(MapleCharacter chr) { - if(members.remove(chr)) { + if(members.remove(chr.getId()) != null) { chr.announce(MaplePacketCreator.removeClock()); broadcastExped(MaplePacketCreator.serverNotice(6, "[Expedition] " + chr.getName() + " has left the expedition.")); chr.dropMessage(6, "[Expedition] You have left this expedition."); @@ -175,9 +180,42 @@ public class MapleExpedition { public MapleExpeditionType getType() { return type; } - - public List getMembers() { - return members; + + public List getActiveMembers() { // thanks MedicOP for figuring out an issue with broadcasting packets to offline members + PlayerStorage ps = startMap.getWorldServer().getPlayerStorage(); + + List activeMembers = new LinkedList<>(); + for (Integer chrid : getMembers().keySet()){ + MapleCharacter chr = ps.getCharacterById(chrid); + if (chr != null && chr.isLoggedinWorld()) { + activeMembers.add(chr); + } + } + + return activeMembers; + } + + public Map getMembers() { + return new HashMap<>(members); + } + + public List> getMemberList() { + List> memberList = new LinkedList<>(); + Entry leaderEntry = null; + + for (Entry e : getMembers().entrySet()) { + if (!isLeader(e.getKey())) { + memberList.add(e); + } else { + leaderEntry = e; + } + } + + if (leaderEntry != null) { + memberList.add(0, leaderEntry); + } + + return memberList; } public MapleCharacter getLeader(){ @@ -189,16 +227,15 @@ public class MapleExpedition { } public boolean contains(MapleCharacter player) { - for (MapleCharacter member : members){ - if (member.getId() == player.getId()){ - return true; - } - } - return false; + return members.containsKey(player.getId()); } public boolean isLeader(MapleCharacter player) { - return leader.equals(player); + return isLeader(player.getId()); + } + + public boolean isLeader(int playerid) { + return leader.getId() == playerid; } public boolean isRegistering(){ @@ -209,15 +246,20 @@ public class MapleExpedition { return !registering; } - public void ban(MapleCharacter player) { - if (!banned.contains(player.getId())) { - banned.add(player.getId()); - members.remove(player); + public void ban(Entry chr) { + int cid = chr.getKey(); + + if (!banned.contains(cid)) { + banned.add(cid); + members.remove(cid); - broadcastExped(MaplePacketCreator.serverNotice(6, "[Expedition] " + player.getName() + " has been banned from the expedition.")); + broadcastExped(MaplePacketCreator.serverNotice(6, "[Expedition] " + chr.getValue() + " has been banned from the expedition.")); - player.announce(MaplePacketCreator.removeClock()); - player.dropMessage(6, "[Expedition] You have been banned from this expedition."); + MapleCharacter player = startMap.getWorldServer().getPlayerStorage().getCharacterById(cid); + if (player != null && player.isLoggedinWorld()) { + player.announce(MaplePacketCreator.removeClock()); + player.dropMessage(6, "[Expedition] You have been banned from this expedition."); + } } } @@ -225,7 +267,7 @@ public class MapleExpedition { return startTime; } - public ArrayList getBossLogs(){ + public List getBossLogs(){ return bossLogs; } diff --git a/src/server/life/MapleMonster.java b/src/server/life/MapleMonster.java index df73a292f2..92d0b141ef 100644 --- a/src/server/life/MapleMonster.java +++ b/src/server/life/MapleMonster.java @@ -55,7 +55,6 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import net.server.audit.locks.MonitoredReentrantLock; -import net.server.Server; import net.server.channel.Channel; import net.server.world.World; import net.server.world.MapleParty; @@ -72,6 +71,7 @@ import tools.Randomizer; import net.server.audit.LockCollector; import net.server.audit.locks.MonitoredLockType; import net.server.audit.locks.factory.MonitoredReentrantLockFactory; +import net.server.coordinator.MapleMonsterAggroCoordinator; public class MapleMonster extends AbstractLoadedMapleLife { private ChangeableStats ostats = null; //unused, v83 WZs offers no support for changeable stats. @@ -100,6 +100,7 @@ public class MapleMonster extends AbstractLoadedMapleLife { private MonitoredReentrantLock monsterLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB, true); private MonitoredReentrantLock statiLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB_STATI); private MonitoredReentrantLock animationLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB_ANI); + private MonitoredReentrantLock aggroUpdateLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB_AGGRO); public MapleMonster(int id, MapleMonsterStats stats) { super(id); @@ -578,31 +579,10 @@ public class MapleMonster extends AbstractLoadedMapleLife { } } - private Pair removeController() { - this.lockMonster(); - try { - MapleCharacter chrController = getController(); - boolean hadAggro = isControllerHasAggro(); - - if (chrController != null) { // this can/should only happen when a hidden gm attacks the monster - chrController.announce(MaplePacketCreator.stopControllingMonster(this.getObjectId())); - chrController.stopControllingMonster(this); - } - - setController(null); - setControllerHasAggro(false); - setControllerKnowsAboutAggro(false); - - return new Pair<>(chrController, hadAggro); - } finally { - this.unlockMonster(); - } - } - public MapleCharacter killBy(final MapleCharacter killer) { distributeExperience(killer != null ? killer.getId() : 0); - final Pair lastController = removeController(); + final Pair lastController = aggroRemoveController(); final List toSpawn = this.getRevives(); if (toSpawn != null) { final MapleMap reviveMap = map; @@ -675,7 +655,7 @@ public class MapleMonster extends AbstractLoadedMapleLife { reviveMap.killMonster(reviveMap.getMonsterById(i), killer, true); } } else if (controller != null) { - mob.switchController(controller, aggro); + mob.aggroSwitchController(controller, aggro); } if(eim != null) { @@ -725,10 +705,12 @@ public class MapleMonster extends AbstractLoadedMapleLife { } private synchronized void processMonsterKilled(boolean hasKiller) { - if(!hasKiller) { + if(!hasKiller) { // players won't gain EXP from a mob that has no killer, but a quest count they should dispatchUpdateQuestMobCount(); } + this.aggroClearDamages(); + MonsterListener[] listenersList; statiLock.lock(); try { @@ -794,46 +776,7 @@ public class MapleMonster extends AbstractLoadedMapleLife { public boolean isAlive() { return this.hp.get() > 0; } - - public MapleCharacter getController() { - monsterLock.lock(); - try { - return controller.get(); - } finally { - monsterLock.unlock(); - } - } - - public void setController(MapleCharacter controller) { - monsterLock.lock(); - try { - this.controller = new WeakReference<>(controller); - } finally { - monsterLock.unlock(); - } - } - - public void switchController(MapleCharacter newController, boolean immediateAggro) { - this.lockMonster(); - try { - MapleCharacter controllers = getController(); - if (controllers == newController) { - return; - } - - removeController(); - - newController.controlMonster(this, immediateAggro); - setController(newController); - if (immediateAggro) { - setControllerHasAggro(true); - } - setControllerKnowsAboutAggro(false); - } finally { - this.unlockMonster(); - } - } - + public void addListener(MonsterListener listener) { statiLock.lock(); try { @@ -843,46 +786,28 @@ public class MapleMonster extends AbstractLoadedMapleLife { } } - public boolean isControllerHasAggro() { - monsterLock.lock(); - try { - return fake ? false : controllerHasAggro; - } finally { - monsterLock.unlock(); - } + public MapleCharacter getController() { + return controller.get(); } - public void setControllerHasAggro(boolean controllerHasAggro) { - monsterLock.lock(); - try { - if (fake) { - return; - } - this.controllerHasAggro = controllerHasAggro; - } finally { - monsterLock.unlock(); - } + private void setController(MapleCharacter controller) { + this.controller = new WeakReference<>(controller); + } + + public boolean isControllerHasAggro() { + return fake ? false : controllerHasAggro; + } + + private void setControllerHasAggro(boolean controllerHasAggro) { + if (!fake) this.controllerHasAggro = controllerHasAggro; } public boolean isControllerKnowsAboutAggro() { - monsterLock.lock(); - try { - return fake ? false : controllerKnowsAboutAggro; - } finally { - monsterLock.unlock(); - } + return fake ? false : controllerKnowsAboutAggro; } - public void setControllerKnowsAboutAggro(boolean controllerKnowsAboutAggro) { - monsterLock.lock(); - try { - if (fake) { - return; - } - this.controllerKnowsAboutAggro = controllerKnowsAboutAggro; - } finally { - monsterLock.unlock(); - } + private void setControllerKnowsAboutAggro(boolean controllerKnowsAboutAggro) { + if (!fake) this.controllerKnowsAboutAggro = controllerKnowsAboutAggro; } public byte[] makeBossHPBarPacket() { @@ -956,15 +881,29 @@ public class MapleMonster extends AbstractLoadedMapleLife { } } + private MapleCharacter getActiveController() { + MapleCharacter chr = getController(); + + if (chr != null && chr.isLoggedinWorld() && chr.getMap() == this.getMap()) { + return chr; + } else { + return null; + } + } + + private void broadcastMonsterStatusMessage(byte[] packet) { + map.broadcastMessage(packet, getPosition()); + + MapleCharacter chrController = getActiveController(); + if (chrController != null && !chrController.isMapObjectVisible(MapleMonster.this)) { + chrController.announce(packet); + } + } + private int broadcastStatusEffect(final MonsterStatusEffect status) { int animationTime = status.getSkill().getAnimationTime(); byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), status, null); - map.broadcastMessage(packet, getPosition()); - - MapleCharacter chrController = getController(); - if (chrController != null && !chrController.isMapObjectVisible(this)) { - chrController.getClient().announce(packet); - } + broadcastMonsterStatusMessage(packet); return animationTime; } @@ -1041,12 +980,7 @@ public class MapleMonster extends AbstractLoadedMapleLife { public void run() { if (isAlive()) { byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), status.getStati()); - map.broadcastMessage(packet, getPosition()); - - MapleCharacter controller = getController(); - if (controller != null && !controller.isMapObjectVisible(MapleMonster.this)) { - controller.getClient().announce(packet); - } + broadcastMonsterStatusMessage(packet); } statiLock.lock(); @@ -1148,12 +1082,7 @@ public class MapleMonster extends AbstractLoadedMapleLife { public void run() { if (isAlive()) { byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), stats); - map.broadcastMessage(packet, getPosition()); - - MapleCharacter controller = getController(); - if (controller != null && !controller.isMapObjectVisible(MapleMonster.this)) { - controller.getClient().announce(packet); - } + broadcastMonsterStatusMessage(packet); statiLock.lock(); try { @@ -1168,7 +1097,7 @@ public class MapleMonster extends AbstractLoadedMapleLife { }; final MonsterStatusEffect effect = new MonsterStatusEffect(stats, null, skill, true); byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), effect, reflection); - map.broadcastMessage(packet, getPosition()); + broadcastMonsterStatusMessage(packet); statiLock.lock(); try { @@ -1180,11 +1109,6 @@ public class MapleMonster extends AbstractLoadedMapleLife { statiLock.unlock(); } - MapleCharacter controller = getController(); - if (controller != null && !controller.isMapObjectVisible(this)) { - controller.getClient().announce(packet); - } - map.getChannelServer().registerMobStatus(map.getId(), effect, cancelTask, duration); } @@ -1193,30 +1117,28 @@ public class MapleMonster extends AbstractLoadedMapleLife { } public void resetMobPosition(Point newPoint) { - removeController(); + aggroRemoveController(); + setPosition(newPoint); map.broadcastMessage(MaplePacketCreator.moveMonster(this.getObjectId(), false, -1, 0, 0, 0, this.getPosition(), this.getIdleMovement())); map.moveMonster(this, this.getPosition()); - map.updateMonsterController(this); + + aggroUpdateController(); } private void debuffMobStat(MonsterStatus stat) { + MonsterStatusEffect oldEffect; statiLock.lock(); try { - if (isBuffed(stat)) { - final MonsterStatusEffect oldEffect = stati.get(stat); - byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), oldEffect.getStati()); - map.broadcastMessage(packet, getPosition()); - - MapleCharacter chrController = getController(); - if (chrController != null && !chrController.isMapObjectVisible(MapleMonster.this)) { - chrController.getClient().announce(packet); - } - stati.remove(stat); - } + oldEffect = stati.remove(stat); } finally { statiLock.unlock(); } + + if (oldEffect != null) { + byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), oldEffect.getStati()); + broadcastMonsterStatusMessage(packet); + } } public void debuffMob(int skillid) { @@ -1280,7 +1202,11 @@ public class MapleMonster extends AbstractLoadedMapleLife { public MapleMap getMap() { return map; } - + + public MapleMonsterAggroCoordinator getMapAggroCoordinator() { + return map.getAggroCoordinator(); + } + public List> getSkills() { return stats.getSkills(); } @@ -1679,6 +1605,206 @@ public class MapleMonster extends AbstractLoadedMapleLife { changeLevelByDifficulty(difficulty, pqMob); } + private MapleCharacter getNextControllerCandidate() { + int mincontrolled = Integer.MAX_VALUE; + MapleCharacter newController = null; + + int mincontrolleddead = Integer.MAX_VALUE; + MapleCharacter newControllerDead = null; + + for (MapleCharacter chr : getMap().getAllPlayers()) { + if (!chr.isHidden()) { + int ctrlMonsSize = chr.getNumControlledMonsters(); + + if (chr.isAlive()) { + if (ctrlMonsSize < mincontrolled) { + mincontrolled = ctrlMonsSize; + newController = chr; + } + } else { + if (ctrlMonsSize < mincontrolleddead) { + mincontrolleddead = ctrlMonsSize; + newControllerDead = chr; + } + } + } + } + + return newController != null ? newController : newControllerDead; + } + + /** + * Removes controllability status from the current controller of this mob. + * + */ + private Pair aggroRemoveController() { + MapleCharacter chrController; + boolean hadAggro; + + aggroUpdateLock.lock(); + try { + chrController = getActiveController(); + hadAggro = isControllerHasAggro(); + + this.setController(null); + this.setControllerHasAggro(false); + this.setControllerKnowsAboutAggro(false); + } finally { + aggroUpdateLock.unlock(); + } + + if (chrController != null) { // this can/should only happen when a hidden gm attacks the monster + chrController.announce(MaplePacketCreator.stopControllingMonster(this.getObjectId())); + chrController.stopControllingMonster(this); + } + + return new Pair<>(chrController, hadAggro); + } + + /** + * Pass over the mob controllability and updates aggro status on the new + * player controller. + * + */ + public void aggroSwitchController(MapleCharacter newController, boolean immediateAggro) { + if (aggroUpdateLock.tryLock()) { + try { + MapleCharacter prevController = getController(); + if (prevController == newController) { + return; + } + + aggroRemoveController(); + if (!(newController != null && newController.isLoggedinWorld() && newController.getMap() == this.getMap())) { + return; + } + + this.setController(newController); + this.setControllerHasAggro(immediateAggro); + this.setControllerKnowsAboutAggro(false); + } finally { + aggroUpdateLock.unlock(); + } + + newController.announce(MaplePacketCreator.controlMonster(this, false, immediateAggro)); + newController.controlMonster(this); + } + } + + /** + * Automagically finds a new controller for the given monster from the chars + * on the map it is from... + * + */ + public void aggroUpdateController() { + MapleCharacter chrController = this.getActiveController(); + if (chrController != null && chrController.isAlive()) { + return; + } + + MapleCharacter newController = getNextControllerCandidate(); + if (newController == null) { // was a new controller found? (if not no one is on the map) + return; + } + + this.aggroSwitchController(newController, false); + } + + /** + * Ensures controllability removal of the current player controller, and + * fetches for any player on the map to start controlling in place. + * + */ + public void aggroRedirectController() { + this.aggroRemoveController(); // don't care if new controller not found, at least remove current controller + this.aggroUpdateController(); + } + + /** + * Returns the current aggro status on the specified player, or null if the + * specified player is currently not this mob's controller. + * + */ + public Boolean aggroMoveLifeUpdate(MapleCharacter player) { + MapleCharacter chrController = getController(); + if (chrController != null && player.getId() == chrController.getId()) { + boolean aggro = this.isControllerHasAggro(); + if (aggro) { + this.setControllerKnowsAboutAggro(true); + } + + return aggro; + } else { + return null; + } + } + + /** + * Refreshes auto aggro for the player passed as parameter, does nothing if + * there is already an active controller for this mob. + * + */ + public void aggroAutoAggroUpdate(MapleCharacter player) { + MapleCharacter chrController = this.getActiveController(); + + if (chrController == null) { + this.aggroSwitchController(player, true); + } else if (chrController.getId() == player.getId()) { + this.setControllerHasAggro(true); + } + } + + /** + * Applied damage input for this mob, enough damage taken implies + * an aggro target update for the attacker shortly. + * + */ + public void aggroMonsterDamage(MapleCharacter attacker, int damage) { + MapleMonsterAggroCoordinator mmac = this.getMapAggroCoordinator(); + mmac.addAggroDamage(this, attacker.getId(), damage); + + MapleCharacter chrController = this.getController(); // aggro based on DPS rather than first-come-first-served, now live after suggestions thanks to MedicOP, Thora, Vcoc + if (chrController != attacker) { + if (this.getMapAggroCoordinator().isLeadingCharacterAggro(this, attacker)) { + this.aggroSwitchController(attacker, true); + } + + /* + For some reason, some mobs loses aggro on controllers if other players also attacks them. + Maybe it was intended by Nexon to interchange controllers at every attack... + + else if (chrController != null) { + chrController.announce(MaplePacketCreator.stopControllingMonster(this.getObjectId())); + chrController.announce(MaplePacketCreator.controlMonster(this, false, true)); + } + */ + } else { + this.setControllerHasAggro(true); + } + } + + /** + * Clears all applied damage input for this mob, doesn't refresh target aggro. + * + */ + public void aggroClearDamages() { + this.getMapAggroCoordinator().removeAggroEntries(this); + } + + /** + * Clears this mob aggro on the current controller. + * + */ + public void aggroResetAggro() { + aggroUpdateLock.lock(); + try { + this.setControllerHasAggro(false); + this.setControllerKnowsAboutAggro(false); + } finally { + aggroUpdateLock.unlock(); + } + } + public final void disposeLocks() { LockCollector.getInstance().registerDisposeAction(new Runnable() { @Override diff --git a/src/server/life/MapleMonsterInformationProvider.java b/src/server/life/MapleMonsterInformationProvider.java index e8c92fa190..2a44456481 100644 --- a/src/server/life/MapleMonsterInformationProvider.java +++ b/src/server/life/MapleMonsterInformationProvider.java @@ -52,8 +52,9 @@ public class MapleMonsterInformationProvider { return instance; } - private final Map> drops = new HashMap<>(); + private final Map> drops = new HashMap<>(); private final List globaldrops = new ArrayList<>(); + private final Map> continentdrops = new HashMap<>(); private final Map> dropsChancePool = new HashMap<>(); // thanks to ronan private final Set hasNoMultiEquipDrops = new HashSet<>(); @@ -70,9 +71,24 @@ public class MapleMonsterInformationProvider { protected MapleMonsterInformationProvider() { retrieveGlobal(); } - - public final List getGlobalDrop() { - return globaldrops; + + public final List getRelevantGlobalDrops(int mapid) { + int continentid = mapid / 100000000; + + List contiItems = continentdrops.get(continentid); + if (contiItems == null) { // continent separated global drops found thanks to marcuswoon + contiItems = new ArrayList<>(); + + for (MonsterGlobalDropEntry e : globaldrops) { + if (e.continentid < 0 || e.continentid == continentid) { + contiItems.add(e); + } + } + + continentdrops.put(continentid, contiItems); + } + + return contiItems; } private void retrieveGlobal() { @@ -90,8 +106,7 @@ public class MapleMonsterInformationProvider { new MonsterGlobalDropEntry( rs.getInt("itemid"), rs.getInt("chance"), - rs.getInt("continent"), - rs.getByte("dropType"), + rs.getByte("continent"), rs.getInt("minimum_quantity"), rs.getInt("maximum_quantity"), rs.getShort("questid"))); diff --git a/src/server/life/MonsterGlobalDropEntry.java b/src/server/life/MonsterGlobalDropEntry.java index 1ae3ef3a04..136f219736 100644 --- a/src/server/life/MonsterGlobalDropEntry.java +++ b/src/server/life/MonsterGlobalDropEntry.java @@ -25,15 +25,14 @@ package server.life; * @author LightPepsi */ public class MonsterGlobalDropEntry { - public MonsterGlobalDropEntry(int itemId, int chance, int continent, byte dropType, int Minimum, int Maximum, short questid) { + public MonsterGlobalDropEntry(int itemId, int chance, int continent, int Minimum, int Maximum, short questid) { this.itemId = itemId; this.chance = chance; - this.dropType = dropType; this.questid = questid; + this.continentid = continent; this.Minimum = Minimum; this.Maximum = Maximum; } - public byte dropType; - public int itemId, chance, Minimum, Maximum; + public int itemId, chance, Minimum, Maximum, continentid; public short questid; } diff --git a/src/server/maps/MapleHiredMerchant.java b/src/server/maps/MapleHiredMerchant.java index 0c1e8ce759..e97a7ebe27 100644 --- a/src/server/maps/MapleHiredMerchant.java +++ b/src/server/maps/MapleHiredMerchant.java @@ -64,6 +64,7 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { private List> messages = new LinkedList<>(); private List sold = new LinkedList<>(); private AtomicBoolean open = new AtomicBoolean(); + private boolean published = false; private MapleMap map; private Lock visitorLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.VISITOR_MERCH, true); @@ -100,10 +101,14 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { visitorLock.lock(); try { byte count = 0; - for (MapleCharacter visitor : visitors) { - if (visitor != null) { - count++; + if (this.isOpen()) { + for (MapleCharacter visitor : visitors) { + if (visitor != null) { + count++; + } } + } else { + count = (byte) (visitors.length + 1); } return new byte[]{count, (byte) (visitors.length + 1)}; @@ -471,15 +476,12 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { return false; } - public void addItem(MaplePlayerShopItem item) { + public boolean addItem(MaplePlayerShopItem item) { synchronized (items) { + if (items.size() >= 16) return false; + items.add(item); - } - - try { - this.saveItems(false); - } catch (SQLException ex) { - ex.printStackTrace(); + return true; } } @@ -521,13 +523,18 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { public void setDescription(String description) { this.description = description; } - + + public boolean isPublished() { + return published; + } + public boolean isOpen() { return open.get(); } public void setOpen(boolean set) { open.getAndSet(set); + published = true; } public int getItemId() { diff --git a/src/server/maps/MapleMap.java b/src/server/maps/MapleMap.java index 0d9839b1f0..b9f7198316 100644 --- a/src/server/maps/MapleMap.java +++ b/src/server/maps/MapleMap.java @@ -62,6 +62,7 @@ import net.server.audit.locks.MonitoredReentrantReadWriteLock; import net.server.audit.locks.factory.MonitoredReentrantLockFactory; import java.lang.ref.WeakReference; import net.server.Server; +import net.server.coordinator.MapleMonsterAggroCoordinator; import net.server.channel.Channel; import net.server.world.World; import scripting.map.MapScriptManager; @@ -140,6 +141,7 @@ public class MapleMap { private int fieldType; private int fieldLimit = 0; private int mobCapacity = -1; + private MapleMonsterAggroCoordinator aggroMonitor = null; // aggroMonitor activity in sync with itemMonitor private ScheduledFuture mapMonitor = null; private ScheduledFuture itemMonitor = null; private ScheduledFuture expireItemsTask = null; @@ -185,6 +187,8 @@ public class MapleMap { final ReentrantReadWriteLock objectLock = new MonitoredReentrantReadWriteLock(MonitoredLockType.MAP_OBJS, true); objectRLock = objectLock.readLock(); objectWLock = objectLock.writeLock(); + + aggroMonitor = new MapleMonsterAggroCoordinator(); } public void setEventInstance(EventInstanceManager eim) { @@ -709,7 +713,7 @@ public class MapleMap { if(useBaseRate) chRate = 1; final MapleMonsterInformationProvider mi = MapleMonsterInformationProvider.getInstance(); - final List globalEntry = mi.getGlobalDrop(); + final List globalEntry = mi.getRelevantGlobalDrops(this.getId()); final List dropEntry = new ArrayList<>(); final List visibleQuestEntry = new ArrayList<>(); @@ -777,6 +781,7 @@ public class MapleMap { if(itemMonitorTimeout == 0) { if(itemMonitor != null) { stopItemMonitor(); + aggroMonitor.stopAggroCoordinator(); } return; @@ -1144,12 +1149,10 @@ public class MapleMap { } public List getAllPlayers() { - List character = new LinkedList<>(); + List character; chrRLock.lock(); try { - for (MapleCharacter a : characters) { - character.add(a); - } + character = new ArrayList<>(characters); } finally { chrRLock.unlock(); } @@ -1316,7 +1319,7 @@ public class MapleMap { if (mons != null) { if (mons.getId() == 8800000) { makeMonsterReal(mons); - updateMonsterController(mons); + mons.aggroUpdateController(); break; } } @@ -1552,88 +1555,6 @@ public class MapleMap { } } - private MapleCharacter getNextControllerCandidate() { - int mincontrolled = Integer.MAX_VALUE; - MapleCharacter newController = null; - - int mincontrolleddead = Integer.MAX_VALUE; - MapleCharacter newControllerDead = null; - - chrRLock.lock(); - try { - for (MapleCharacter chr : characters) { - if (!chr.isHidden()) { - int ctrlMonsSize = chr.getControlledMonsters().size(); - - if (chr.isAlive()) { - if (ctrlMonsSize < mincontrolled) { - mincontrolled = ctrlMonsSize; - newController = chr; - } - } else { - if (ctrlMonsSize < mincontrolleddead) { - mincontrolleddead = ctrlMonsSize; - newControllerDead = chr; - } - } - } - } - } finally { - chrRLock.unlock(); - } - - return newController != null ? newController : newControllerDead; - } - - /** - * Automagically finds a new controller for the given monster from the chars - * on the map... - * - * @param monster - */ - public void updateMonsterController(MapleMonster monster) { - monster.lockMonster(); - try { - if (!monster.isAlive()) { - return; - } - - MapleCharacter newController; - MapleCharacter chrController = monster.getController(); - if (chrController != null) { - if (chrController.getMap() != this) { - chrController.stopControllingMonster(monster); - newController = getNextControllerCandidate(); - } else { - if (chrController.isAlive()) { - return; - } - - newController = getNextControllerCandidate(); - if (newController == null || !newController.isAlive()) { - return; - } - - chrController.stopControllingMonster(monster); - } - } else { - newController = getNextControllerCandidate(); - } - - if (newController != null) { // was a new controller found? (if not no one is on the map) - if (monster.isFirstAttack()) { - newController.controlMonster(monster, true); - monster.setControllerHasAggro(true); - monster.setControllerKnowsAboutAggro(true); - } else { - newController.controlMonster(monster, false); - } - } - } finally { - monster.unlockMonster(); - } - } - private Map getCopyMapObjects() { objectRLock.lock(); try { @@ -1862,7 +1783,7 @@ public class MapleMap { } }); - updateMonsterController(monster); + monster.aggroUpdateController(); if (monster.hasBossHPBar()) { broadcastBossHpMessage(monster, monster.hashCode(), monster.makeBossHPBarPacket(), monster.getPosition()); @@ -1948,7 +1869,7 @@ public class MapleMap { } }, null); - updateMonsterController(monster); + monster.aggroUpdateController(); if (monster.hasBossHPBar()) { broadcastBossHpMessage(monster, monster.hashCode(), monster.makeBossHPBarPacket(), monster.getPosition()); @@ -1993,7 +1914,8 @@ public class MapleMap { c.announce(MaplePacketCreator.spawnMonster(monster, true, effect)); } }); - updateMonsterController(monster); + + monster.aggroUpdateController(); if (monster.hasBossHPBar()) { broadcastBossHpMessage(monster, monster.hashCode(), monster.makeBossHPBarPacket(), monster.getPosition()); @@ -2019,7 +1941,8 @@ public class MapleMap { public void makeMonsterReal(final MapleMonster monster) { monster.setFake(false); broadcastMessage(MaplePacketCreator.makeMonsterReal(monster)); - updateMonsterController(monster); + + monster.aggroUpdateController(); } public void spawnReactor(final MapleReactor reactor) { @@ -2403,7 +2326,10 @@ public class MapleMap { chr.setMapId(mapid); if (chrSize == 1) { - if(!hasItemMonitor()) startItemMonitor(); + if(!hasItemMonitor()) { + startItemMonitor(); + aggroMonitor.startAggroCoordinator(); + } if (onFirstUserEnter.length() != 0 && !chr.hasEntered(onFirstUserEnter, mapid) && MapScriptManager.getInstance().scriptExists(onFirstUserEnter, true)) { chr.enteredScript(onFirstUserEnter, mapid); @@ -2688,19 +2614,8 @@ public class MapleMap { broadcastGMMessage(MaplePacketCreator.removePlayerFromMap(chr.getId())); } - for (MapleMonster monster : chr.getControlledMonsters()) { - monster.lockMonster(); - try { - monster.setController(null); - monster.setControllerHasAggro(false); - monster.setControllerKnowsAboutAggro(false); - updateMonsterController(monster); - } finally { - monster.unlockMonster(); - } - } - chr.leaveMap(); + for (MapleSummon summon : new ArrayList<>(chr.getSummonsValues())) { if (summon.isStationary()) { chr.cancelEffectFromBuffStat(MapleBuffStat.PUPPET); @@ -2926,7 +2841,7 @@ public class MapleMap { if (isNonRangedType(o.getType())) { o.sendSpawnData(mapleClient); } else if (o.getType() == MapleMapObjectType.MONSTER) { - updateMonsterController((MapleMonster) o); + ((MapleMonster) o).aggroUpdateController(); } else if (o.getType() == MapleMapObjectType.SUMMON) { MapleSummon summon = (MapleSummon) o; if (summon.getOwner() == chr) { @@ -3038,6 +2953,10 @@ public class MapleMap { mapArea.setBounds(vrLeft, vrTop, vrRight - vrLeft, vrBottom - vrTop); } + public MapleMonsterAggroCoordinator getAggroCoordinator() { + return aggroMonitor; + } + /** * it's threadsafe, gtfo :D * @@ -4091,6 +4010,9 @@ public class MapleMap { chrWLock.lock(); try { + aggroMonitor.dispose(); + aggroMonitor = null; + if(itemMonitor != null) { itemMonitor.cancel(false); itemMonitor = null; diff --git a/src/server/maps/MaplePlayerShop.java b/src/server/maps/MaplePlayerShop.java index 1233fce37d..965f413651 100644 --- a/src/server/maps/MaplePlayerShop.java +++ b/src/server/maps/MaplePlayerShop.java @@ -103,11 +103,15 @@ public class MaplePlayerShop extends AbstractMapleMapObject { visitorLock.lock(); try { byte count = 0; - for (MapleCharacter visitor : visitors) { - if (visitor != null) { - count++; + //if (this.isOpen()) { + for (MapleCharacter visitor : visitors) { + if (visitor != null) { + count++; + } } - } + //} else { shouldn't happen since there isn't a "closed" state for player shops. + // count = (byte) (visitors.length + 1); + //} return new byte[]{count, (byte) visitors.length}; } finally { @@ -198,9 +202,12 @@ public class MaplePlayerShop extends AbstractMapleMapObject { } } - public void addItem(MaplePlayerShopItem item) { + public boolean addItem(MaplePlayerShopItem item) { synchronized (items) { + if (items.size() >= 16) return false; + items.add(item); + return true; } } @@ -241,7 +248,7 @@ public class MaplePlayerShop extends AbstractMapleMapObject { * @param item * @param quantity */ - public void buy(MapleClient c, int item, short quantity) { + public boolean buy(MapleClient c, int item, short quantity) { synchronized (items) { if (isVisitor(c.getPlayer())) { MaplePlayerShopItem pItem = items.get(item); @@ -250,10 +257,10 @@ public class MaplePlayerShop extends AbstractMapleMapObject { newItem.setQuantity((short) ((pItem.getItem().getQuantity() * quantity))); if (quantity < 1 || !pItem.isExist() || pItem.getBundles() < quantity) { c.announce(MaplePacketCreator.enableActions()); - return; + return false; } else if (newItem.getInventoryType().equals(MapleInventoryType.EQUIP) && newItem.getQuantity() > 1) { c.announce(MaplePacketCreator.enableActions()); - return; + return false; } MapleKarmaManipulator.toggleKarmaFlagToUntradeable(newItem); @@ -264,6 +271,12 @@ public class MaplePlayerShop extends AbstractMapleMapObject { if (c.getPlayer().getMeso() >= price) { if (canBuy(c, newItem)) { + if (!owner.canHoldMeso(price)) { + owner.dropMessage(1, "Transaction failed since the shop owner can't hold any more mesos."); + c.announce(MaplePacketCreator.enableActions()); + return false; + } + c.getPlayer().gainMeso(-price, false); owner.gainMeso(price, true); @@ -286,13 +299,21 @@ public class MaplePlayerShop extends AbstractMapleMapObject { } } else { c.getPlayer().dropMessage(1, "Your inventory is full. Please clean a slot before buying this item."); + c.announce(MaplePacketCreator.enableActions()); + return false; } } else { c.getPlayer().dropMessage(1, "You don't have enough mesos to purchase this item."); + c.announce(MaplePacketCreator.enableActions()); + return false; } + + return true; } finally { visitorLock.unlock(); } + } else { + return false; } } } @@ -418,9 +439,9 @@ public class MaplePlayerShop extends AbstractMapleMapObject { } public void closeShop() { - owner.getMap().broadcastMessage(MaplePacketCreator.removePlayerShopBox(this)); clearChatLog(); removeVisitors(); + owner.getMap().broadcastMessage(MaplePacketCreator.removePlayerShopBox(this)); } public void sendShop(MapleClient c) { diff --git a/src/server/movement/AbsoluteLifeMovement.java b/src/server/movement/AbsoluteLifeMovement.java index 452ef2435e..ba4273e8b8 100644 --- a/src/server/movement/AbsoluteLifeMovement.java +++ b/src/server/movement/AbsoluteLifeMovement.java @@ -26,7 +26,7 @@ import tools.data.output.LittleEndianWriter; public class AbsoluteLifeMovement extends AbstractLifeMovement { private Point pixelsPerSecond; - private int unk; + private int fh; public AbsoluteLifeMovement(int type, Point position, int duration, int newstate) { super(type, position, duration, newstate); @@ -40,12 +40,12 @@ public class AbsoluteLifeMovement extends AbstractLifeMovement { this.pixelsPerSecond = wobble; } - public int getUnk() { - return unk; + public int getFh() { // unk -> fh, thanks Spoon for pointing this out + return fh; } - public void setUnk(int unk) { - this.unk = unk; + public void setFh(int fh) { + this.fh = fh; } @Override @@ -55,7 +55,7 @@ public class AbsoluteLifeMovement extends AbstractLifeMovement { lew.writeShort(getPosition().y); lew.writeShort(pixelsPerSecond.x); lew.writeShort(pixelsPerSecond.y); - lew.writeShort(unk); + lew.writeShort(fh); lew.write(getNewstate()); lew.writeShort(getDuration()); } diff --git a/src/server/movement/ChairMovement.java b/src/server/movement/ChairMovement.java index 71f0161c7a..3e8eb1b053 100644 --- a/src/server/movement/ChairMovement.java +++ b/src/server/movement/ChairMovement.java @@ -25,18 +25,18 @@ import java.awt.Point; import tools.data.output.LittleEndianWriter; public class ChairMovement extends AbstractLifeMovement { - private int unk; + private int fh; public ChairMovement(int type, Point position, int duration, int newstate) { super(type, position, duration, newstate); } - public int getUnk() { - return unk; + public int getFh() { + return fh; } - public void setUnk(int unk) { - this.unk = unk; + public void setFh(int fh) { + this.fh = fh; } @Override @@ -44,7 +44,7 @@ public class ChairMovement extends AbstractLifeMovement { lew.write(getType()); lew.writeShort(getPosition().x); lew.writeShort(getPosition().y); - lew.writeShort(unk); + lew.writeShort(fh); lew.write(getNewstate()); lew.writeShort(getDuration()); } diff --git a/src/server/movement/JumpDownMovement.java b/src/server/movement/JumpDownMovement.java index aedfb6b4dd..b60f6c19d4 100644 --- a/src/server/movement/JumpDownMovement.java +++ b/src/server/movement/JumpDownMovement.java @@ -26,8 +26,8 @@ import tools.data.output.LittleEndianWriter; public class JumpDownMovement extends AbstractLifeMovement { private Point pixelsPerSecond; - private int unk; private int fh; + private int originFh; public JumpDownMovement(int type, Point position, int duration, int newstate) { super(type, position, duration, newstate); @@ -41,22 +41,22 @@ public class JumpDownMovement extends AbstractLifeMovement { this.pixelsPerSecond = wobble; } - public int getUnk() { - return unk; - } - - public void setUnk(int unk) { - this.unk = unk; - } - - public int getFH() { + public int getFh() { return fh; } - public void setFH(int fh) { + public void setFh(int fh) { this.fh = fh; } + public int getOriginFh() { + return originFh; + } + + public void setOriginFh(int fh) { // fh actually originFh, thanks Spoon for pointing this out + this.originFh = fh; + } + @Override public void serialize(LittleEndianWriter lew) { lew.write(getType()); @@ -64,8 +64,8 @@ public class JumpDownMovement extends AbstractLifeMovement { lew.writeShort(getPosition().y); lew.writeShort(pixelsPerSecond.x); lew.writeShort(pixelsPerSecond.y); - lew.writeShort(unk); lew.writeShort(fh); + lew.writeShort(originFh); lew.write(getNewstate()); lew.writeShort(getDuration()); } diff --git a/src/server/quest/actions/ExpAction.java b/src/server/quest/actions/ExpAction.java index 73e9b8c27a..2ad65b3928 100644 --- a/src/server/quest/actions/ExpAction.java +++ b/src/server/quest/actions/ExpAction.java @@ -23,7 +23,6 @@ package server.quest.actions; import client.MapleCharacter; import constants.ServerConstants; -import net.server.world.World; import provider.MapleData; import provider.MapleDataTool; import server.quest.MapleQuest; @@ -53,15 +52,10 @@ public class ExpAction extends MapleQuestAction { } public static void runAction(MapleCharacter chr, int gain) { - if (chr.isBeginnerJob() && chr.getLevel() < 10) { - chr.gainExp(gain, true, true); - } else { - if(!ServerConstants.USE_QUEST_RATE) { - chr.gainExp(gain * chr.getExpRate(), true, true); - } else { - World w = chr.getClient().getWorldServer(); - chr.gainExp(gain * w.getExpRate() * w.getQuestRate(), true, true); - } - } + if (!ServerConstants.USE_QUEST_RATE) { + chr.gainExp(gain * chr.getExpRate(), true, true); + } else { + chr.gainExp(gain * chr.getQuestExpRate(), true, true); + } } } diff --git a/src/server/quest/actions/MesoAction.java b/src/server/quest/actions/MesoAction.java index 1fb79be4f9..8f84d6d299 100644 --- a/src/server/quest/actions/MesoAction.java +++ b/src/server/quest/actions/MesoAction.java @@ -23,7 +23,6 @@ package server.quest.actions; import client.MapleCharacter; import constants.ServerConstants; -import net.server.world.World; import provider.MapleData; import provider.MapleDataTool; import server.quest.MapleQuest; @@ -54,14 +53,13 @@ public class MesoAction extends MapleQuestAction { } public static void runAction(MapleCharacter chr, int gain) { - if(gain < 0) { + if (gain < 0) { chr.gainMeso(gain, true, false, true); } else { - if(!ServerConstants.USE_QUEST_RATE) { + if (!ServerConstants.USE_QUEST_RATE) { chr.gainMeso(gain * chr.getMesoRate(), true, false, true); } else { - World w = chr.getClient().getWorldServer(); - chr.gainMeso(gain * w.getMesoRate() * w.getQuestRate(), true, false, true); + chr.gainMeso(gain * chr.getQuestMesoRate(), true, false, true); } } } diff --git a/src/tools/LogHelper.java b/src/tools/LogHelper.java index f34d6422b6..7d37d78a8c 100644 --- a/src/tools/LogHelper.java +++ b/src/tools/LogHelper.java @@ -40,8 +40,8 @@ public class LogHelper { String log = expedition.getType().toString() + " EXPEDITION\r\n"; log += getTimeString(expedition.getStartTime()) + "\r\n"; - for (MapleCharacter member : expedition.getMembers()){ - log += ">>" + member.getName() + "\r\n"; + for (String memberName : expedition.getMembers().values()){ + log += ">>" + memberName + "\r\n"; } log += "BOSS KILLS\r\n"; for (String message: expedition.getBossLogs()){ diff --git a/src/tools/MaplePacketCreator.java b/src/tools/MaplePacketCreator.java index 28b73c9f7b..b999ae5567 100644 --- a/src/tools/MaplePacketCreator.java +++ b/src/tools/MaplePacketCreator.java @@ -1841,6 +1841,30 @@ public class MaplePacketCreator { return mplew.getPacket(); } + /** + * Guild Name & Mark update packet, thanks to Arnah (Vertisy) + * + * @param guildName The Guild name, blank for nothing. + */ + public static byte[] guildNameChanged(int chrid, String guildName){ + MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); + mplew.writeShort(SendOpcode.GUILD_NAME_CHANGED.getValue()); + mplew.writeInt(chrid); + mplew.writeMapleAsciiString(guildName); + return mplew.getPacket(); + } + + public static byte[] guildMarkChanged(int chrid, MapleGuild guild){ + MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); + mplew.writeShort(SendOpcode.GUILD_MARK_CHANGED.getValue()); + mplew.writeInt(chrid); + mplew.writeShort(guild.getLogoBG()); + mplew.write(guild.getLogoBGColor()); + mplew.writeShort(guild.getLogo()); + mplew.write(guild.getLogoColor()); + return mplew.getPacket(); + } + /** * Gets a packet spawning a player as a mapobject to other clients. * @@ -3384,7 +3408,7 @@ public class MaplePacketCreator { public static byte[] showBuffeffect(int cid, int skillid, int effectid) { return showBuffeffect(cid, skillid, effectid, (byte) 3); } - + public static byte[] showBuffeffect(int cid, int skillid, int effectid, byte direction) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); mplew.writeShort(SendOpcode.SHOW_FOREIGN_EFFECT.getValue()); @@ -3396,6 +3420,19 @@ public class MaplePacketCreator { mplew.writeLong(0); return mplew.getPacket(); } + + public static byte[] showBuffeffect(int cid, int skillid, int skilllv, int effectid, byte direction) { // updated packet structure found thanks to Rien dev team + final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); + mplew.writeShort(SendOpcode.SHOW_FOREIGN_EFFECT.getValue()); + mplew.writeInt(cid); + mplew.write(effectid); + mplew.writeInt(skillid); + mplew.write(0); + mplew.write(skilllv); + mplew.write(direction); + + return mplew.getPacket(); + } public static byte[] showOwnBuffEffect(int skillid, int effectid) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); @@ -4597,13 +4634,21 @@ public class MaplePacketCreator { mplew.writeInt(skillId); return mplew.getPacket(); } - - public static byte[] showMagnet(int mobid, byte success) { // Monster Magnet + + public static byte[] catchMonster(int mobOid, byte success) { // updated packet structure found thanks to Rien dev team final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); - mplew.writeShort(SendOpcode.SHOW_MAGNET.getValue()); - mplew.writeInt(mobid); + mplew.writeShort(SendOpcode.CATCH_MONSTER.getValue()); + mplew.writeInt(mobOid); + mplew.write(success); + return mplew.getPacket(); + } + + public static byte[] catchMonster(int mobOid, int itemid, byte success) { + final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); + mplew.writeShort(SendOpcode.CATCH_MONSTER_WITH_ITEM.getValue()); + mplew.writeInt(mobOid); + mplew.writeInt(itemid); mplew.write(success); - mplew.skip(10); //Mmmk return mplew.getPacket(); } @@ -4932,15 +4977,6 @@ public class MaplePacketCreator { return mplew.getPacket(); } - public static byte[] catchMonster(int monsobid, int itemid, byte success) { - final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); - mplew.writeShort(SendOpcode.CATCH_MONSTER.getValue()); - mplew.writeInt(monsobid); - mplew.writeInt(itemid); - mplew.write(success); - return mplew.getPacket(); - } - public static byte[] catchMessage(int message) { // not done, I guess final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); mplew.writeShort(SendOpcode.BRIDLE_MOB_CATCH_FAIL.getValue()); @@ -5599,7 +5635,7 @@ public class MaplePacketCreator { } return mplew.getPacket(); } - + public static byte[] hiredMerchantOwnerLeave() { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); mplew.writeShort(SendOpcode.PLAYER_INTERACTION.getValue()); @@ -5607,6 +5643,14 @@ public class MaplePacketCreator { mplew.write(0); return mplew.getPacket(); } + + public static byte[] hiredMerchantOwnerMaintenanceLeave() { + final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); + mplew.writeShort(SendOpcode.PLAYER_INTERACTION.getValue()); + mplew.write(PlayerInteractionHandler.Action.REAL_CLOSE_MERCHANT.getCode()); + mplew.write(5); + return mplew.getPacket(); + } public static byte[] hiredMerchantMaintenanceMessage() { MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(5); diff --git a/tools/MapleEventMethodFiller/src/mapleeventmethodfiller/MapleEventMethodFiller.java b/tools/MapleEventMethodFiller/src/mapleeventmethodfiller/MapleEventMethodFiller.java index 31e2a3650e..5fecba78ff 100644 --- a/tools/MapleEventMethodFiller/src/mapleeventmethodfiller/MapleEventMethodFiller.java +++ b/tools/MapleEventMethodFiller/src/mapleeventmethodfiller/MapleEventMethodFiller.java @@ -41,7 +41,7 @@ import org.apache.commons.io.FileUtils; and fill empty functions for every function name not yet present in the script. - Estimated parse time: + Estimated parse time: 10 seconds */ public class MapleEventMethodFiller { diff --git a/wz/Map.wz/Map/Map0/001000006.img.xml b/wz/Map.wz/Map/Map0/001000006.img.xml index dc5208b54d..faac7466cb 100644 --- a/wz/Map.wz/Map/Map0/001000006.img.xml +++ b/wz/Map.wz/Map/Map0/001000006.img.xml @@ -99,7 +99,7 @@ - + @@ -112,7 +112,7 @@ - + @@ -125,7 +125,7 @@ - + @@ -171,7 +171,7 @@ - + @@ -184,7 +184,7 @@ - + @@ -210,7 +210,7 @@ - + @@ -223,7 +223,7 @@ - + @@ -236,7 +236,7 @@ - + @@ -249,7 +249,7 @@ - + @@ -262,7 +262,7 @@ - + @@ -275,7 +275,7 @@ - + @@ -288,7 +288,7 @@ - + @@ -301,7 +301,7 @@ - + @@ -384,7 +384,7 @@ - + @@ -397,7 +397,7 @@ - + diff --git a/wz/Map.wz/Map/Map0/001010400.img.xml b/wz/Map.wz/Map/Map0/001010400.img.xml index d4ea1a7acc..dbe653609d 100644 --- a/wz/Map.wz/Map/Map0/001010400.img.xml +++ b/wz/Map.wz/Map/Map0/001010400.img.xml @@ -103,7 +103,7 @@ - + @@ -116,7 +116,7 @@ - + @@ -129,7 +129,7 @@ - + @@ -142,7 +142,7 @@ - + @@ -155,7 +155,7 @@ - + @@ -168,7 +168,7 @@ - + @@ -181,7 +181,7 @@ - + @@ -194,7 +194,7 @@ - + @@ -207,7 +207,7 @@ - + @@ -220,7 +220,7 @@ - + @@ -233,7 +233,7 @@ - + @@ -246,7 +246,7 @@ - + diff --git a/wz/Map.wz/Map/Map1/105090310.img.xml b/wz/Map.wz/Map/Map1/105090310.img.xml index 4cad11c7fe..4b7d0881e0 100644 --- a/wz/Map.wz/Map/Map1/105090310.img.xml +++ b/wz/Map.wz/Map/Map1/105090310.img.xml @@ -1,15294 +1,23 @@ - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -15299,71 +28,15299 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wz/Quest.wz/Check.img.xml b/wz/Quest.wz/Check.img.xml index ebc2afd886..6d03217b48 100644 --- a/wz/Quest.wz/Check.img.xml +++ b/wz/Quest.wz/Check.img.xml @@ -8339,7 +8339,7 @@ - + diff --git a/wz/Quest.wz/QuestInfo.img.xml b/wz/Quest.wz/QuestInfo.img.xml index d794b9c56d..a66bf5124e 100644 --- a/wz/Quest.wz/QuestInfo.img.xml +++ b/wz/Quest.wz/QuestInfo.img.xml @@ -2541,8 +2541,8 @@ - - + + diff --git a/wz/Quest.wz/Say.img.xml b/wz/Quest.wz/Say.img.xml index acfbca680f..63c54d3fb5 100644 --- a/wz/Quest.wz/Say.img.xml +++ b/wz/Quest.wz/Say.img.xml @@ -4503,21 +4503,22 @@ - + + - + - + diff --git a/wz/Skill.wz/MobSkill.img.xml b/wz/Skill.wz/MobSkill.img.xml index 23abbc7158..49011b15d9 100644 --- a/wz/Skill.wz/MobSkill.img.xml +++ b/wz/Skill.wz/MobSkill.img.xml @@ -14477,7 +14477,7 @@ - +