From 9538c415e1c70eee5da68289d82e12e6abd8a856 Mon Sep 17 00:00:00 2001 From: ronancpl Date: Sun, 10 Mar 2019 01:30:22 -0300 Subject: [PATCH] Trade results + Map-Ownership & Fishing + P. EXP distribution rework Fixed an issue where players could lose priority over recently dropped items after switching maps. Adjusted EXP bonus buffs to also cover party bonus gains. Fixed items taken back from merchants not properly checking for stacking opportunities in player inventory. Fixed some merchant references in visitors' player object not being properly cleared when owner closes shop. Adjusted merchants to automatically close as soon as the merchant owner finishes maintenance process with it having no items in store. Added trade result opcodes. Trade results now should work almost as intended originally. Implemented server-side check for portal distance when deploying player shops and merchants. Implemented server-side check for whether local or remote IP is being used when logging in a local/remote server (this should mitigate a few of the issues people may find when trying to log in game world). Implemented commands designed for management of opened IO sessions. Fixed chalkboard not showing up for owner player when changing maps. Added "time left" functionality for merchant owners managing the opened store. Fixed skillbooks not showing properly for other players in the map. Fixed commands using lowercased-version of content inputted by player. Implemented the Fredrick expected fee on using the Store Bank service. Implemented "exclusive invitation management" in the system. Inviters are notified the invited players are already managing an invite, should it be visually "in-progress" for that one. Implemented "map ownership". Non-map owners are unable to farm in an area if they are not party members with the owner or until the ownership rescinds. Adjusted inventory sort feature, now sorting projectile items in such a fashion that commonly stronger versions comes before the basic ones. Added a visual effect that shows up when obtaining Aran skills. Revised party EXP gain system. Party bonuses now accounts a fraction of the accumulated EXP gained by members when defeating a mob, and raw EXP gained by a player is kept the same regardless of him/her being in a party or not (thus a bonus being REALLY a bonus). Implemented a custom fishing system in the source, on which during "seasonal" times (that gets arbitrarily defined by both day-of-year and time-of-day) fishes are more likely to be hooked. Such likelihood also improved depending on the amount of mesos spent as lure. --- docs/mychanges_ptbr.txt | 53 +- launchtest.bat | 61 ++ scripts/npc/2010009.js | 4 +- scripts/npc/9201128.js | 8 +- scripts/npc/9201129.js | 8 +- scripts/npc/9201130.js | 8 +- scripts/npc/9201131.js | 8 +- scripts/npc/9201132.js | 8 +- scripts/npc/9977777.js | 10 +- sql/db_database.sql | 9 + src/client/MapleCharacter.java | 149 ++++- src/client/MapleClient.java | 26 +- src/client/command/CommandsExecutor.java | 20 +- .../command/commands/gm0/GachaCommand.java | 3 +- .../command/commands/gm0/GmCommand.java | 2 +- .../commands/gm0/MapOwnerClaimCommand.java | 62 ++ .../commands/gm0/ReportBugCommand.java | 2 +- .../commands/gm1/WhatDropsFromCommand.java | 2 +- .../command/commands/gm1/WhoDropsCommand.java | 2 +- .../command/commands/gm3/GiveNxCommand.java | 36 +- .../command/commands/gm3/MusicCommand.java | 2 +- .../command/commands/gm3/NoticeCommand.java | 2 +- .../commands/gm4/FishingRateCommand.java | 48 ++ .../commands/gm4/ServerMessageCommand.java | 4 +- .../command/commands/gm5/DebugCommand.java | 2 +- .../command/commands/gm5/IpListCommand.java | 60 ++ .../commands/gm5/ShowSessionsCommand.java | 39 ++ src/client/inventory/ItemFactory.java | 16 +- src/client/inventory/MapleInventory.java | 4 +- src/client/processor/FredrickProcessor.java | 234 ++++++- src/constants/GameConstants.java | 4 + src/constants/ItemConstants.java | 4 + src/constants/ServerConstants.java | 10 +- src/net/PacketProcessor.java | 1 + src/net/opcodes/RecvOpcode.java | 1 + src/net/server/Server.java | 7 +- src/net/server/channel/Channel.java | 57 +- .../handlers/AbstractDealDamageHandler.java | 4 + .../channel/handlers/AdminCommandHandler.java | 1 - .../handlers/AllianceOperationHandler.java | 34 +- .../handlers/CashOperationHandler.java | 616 +++++++++--------- .../channel/handlers/ChangeMapHandler.java | 116 ++-- .../handlers/ChangeMapSpecialHandler.java | 3 +- .../handlers/CloseChalkboardHandler.java | 2 + .../handlers/DenyAllianceRequestHandler.java | 47 ++ .../handlers/DenyGuildRequestHandler.java | 8 +- .../handlers/DenyPartyRequestHandler.java | 9 +- .../handlers/EnterCashShopHandler.java | 1 + .../channel/handlers/EnterMTSHandler.java | 1 + .../handlers/GuildOperationHandler.java | 78 +-- .../handlers/InventorySortHandler.java | 78 ++- .../channel/handlers/MesoDropHandler.java | 7 +- .../channel/handlers/MessengerHandler.java | 140 ++-- .../handlers/PartyOperationHandler.java | 48 +- .../handlers/PlayerInteractionHandler.java | 50 +- .../channel/handlers/SkillBookHandler.java | 5 +- .../channel/handlers/SummonDamageHandler.java | 3 + .../channel/handlers/UseCashItemHandler.java | 7 + .../coordinator/MapleInviteCoordinator.java | 149 +++++ .../coordinator/MapleSessionCoordinator.java | 116 +++- src/net/server/guild/MapleAlliance.java | 51 ++ src/net/server/guild/MapleGuild.java | 39 +- src/net/server/guild/MapleGuildResponse.java | 14 +- .../handlers/login/CreateCharHandler.java | 19 +- .../handlers/login/LoginPasswordHandler.java | 13 +- src/net/server/worker/FishingWorker.java | 37 ++ src/net/server/worker/FredrickWorker.java | 33 + src/net/server/worker/InvitationWorker.java | 33 + src/net/server/worker/MapOwnershipWorker.java | 40 ++ src/net/server/world/World.java | 117 +++- src/scripting/AbstractPlayerInteraction.java | 18 +- src/scripting/event/EventManager.java | 1 + src/scripting/npc/NPCConversationManager.java | 2 + src/server/MapleTrade.java | 194 ++++-- src/server/life/MapleMonster.java | 178 +++-- src/server/maps/MapleGenericPortal.java | 19 +- src/server/maps/MapleHiredMerchant.java | 58 +- src/server/maps/MapleMap.java | 111 +++- src/server/maps/MaplePlayerShop.java | 3 +- src/tools/FilePrinter.java | 1 + src/tools/MaplePacketCreator.java | 99 ++- .../EventInstanceInProgressException.java | 2 +- src/tools/packets/Fishing.java | 199 ++++++ wz/Effect.wz/BasicEff.img.xml | 25 +- wz/Quest.wz/Act.img.xml | 44 ++ wz/Quest.wz/Check.img.xml | 30 + 86 files changed, 2992 insertions(+), 887 deletions(-) create mode 100644 launchtest.bat create mode 100644 src/client/command/commands/gm0/MapOwnerClaimCommand.java create mode 100644 src/client/command/commands/gm4/FishingRateCommand.java create mode 100644 src/client/command/commands/gm5/IpListCommand.java create mode 100644 src/client/command/commands/gm5/ShowSessionsCommand.java create mode 100644 src/net/server/channel/handlers/DenyAllianceRequestHandler.java create mode 100644 src/net/server/coordinator/MapleInviteCoordinator.java create mode 100644 src/net/server/worker/FishingWorker.java create mode 100644 src/net/server/worker/FredrickWorker.java create mode 100644 src/net/server/worker/InvitationWorker.java create mode 100644 src/net/server/worker/MapOwnershipWorker.java rename src/{scripting/event => tools/exceptions}/EventInstanceInProgressException.java (97%) create mode 100644 src/tools/packets/Fishing.java diff --git a/docs/mychanges_ptbr.txt b/docs/mychanges_ptbr.txt index 9f5614badc..e9613c79d5 100644 --- a/docs/mychanges_ptbr.txt +++ b/docs/mychanges_ptbr.txt @@ -1660,4 +1660,55 @@ Incrementado mensagem custom de venda de produtos pelo mercante. 13 - 14 Fevereiro 2019, Corrigido limites na função isWeapon, que não contabilizaria certos itens corretamente. -Resolvido comportamento de puppets usando o novo sistema de aggro. \ No newline at end of file +Resolvido comportamento de puppets usando o novo sistema de aggro. + +18 Fevereiro 2019, +Corrigido um caso onde jogador em party poderia perder prioridade sobre loots recentes no chão ao mudar de mapas. +Ajustado contabilização de buffs no ganho de exp, agora bonus de party também contabiliza ganho pelos buffs. +Corrigido mercantes não checando estoque de itens apropriadamente ao pegar de volta itens colocados nos mercantes. +Corrigido referências a lojas de jogadores não sendo devidamente liberadas para visitantes quando dono fecha a loja, levando a problemas de update visuais no mapa ao realizar certas ações. +Ajustado mercantes para automaticamente fechar a loja assim que dono tenta sair da loja deixando a mesma vazia. +Adicionado opcodes de resultado de trades entre jogadores. +Incrementado resultados de trades, agora utilizando um conjunto de respostas mais parecido com o do GMS. + +19 - 20 Fevereiro 2019, +Implementado checagem de distância a portais no mapa em criação de loja/mercante de jogador. +Implementado checagem de IP localhost ao tentar logar em server com IP de produção. +Ajustado quests de door bosses, agora entregando emblemas a jogadores no início e retirando os mesmos ao terminar a quest. O item emblema é um dos requerimentos para acessar o boss da área. +Corrigido problema no sistema de login utilizando flag de detecção de múltiplas sessões abertas não tratando hashcodes negativos de IP. +Adicionado comandos de desenvolvedor que permitem visualizar situações de conexão de jogadores (avaliação de stack de IP abertos no servidor e sessões logadas). + +21 Fevereiro 2019, +Corrigido chalkboard não aparecendo corretamente para o usuário do item ao trocar mapas. +Revisado chalkboard agora removendo unidade do inventário ao usar. +Implementado checagem por chalkboard em salas do FM. +Adicionado opcodes de mensagens genéricas de guild. + +22 Fevereiro 2019, +Corrigido líder de aliança de guilds não sendo permitido convidar guilds logo após aumentar a capacidade da aliança. +Corrigido impossibilidade de abrir novos mercantes logo assim que último mercante expirou. +Implementado visualização do "tempo de sobra" ao reentrar no mercante (somente sendo dono da loja). +Ajustado resultado de skillbooks não aparecendo para todos jogadores no mapa. +Corrigido alguns comandos utilizando letras minúsculas em conteúdos de mensagens. + +23 Fevereiro 2019, +Adicionado funcionalidade de colocar tipo de cash a adicionar no comando GiveNx. +Implementado taxas de manutenção de mesos/itens no Fredrick. Após 100 dias, itens guardados são removidos conforme descrição de utilização das lojas. +Incrementado comportamento do NPC Fredrick. Como na descrição do serviço (ao abrir mercantes), itens guardados agora podem ser removidos após 100 dias, e há a perda de 1% da valia do montante vendido a cada dia que se passa sem os itens serem recuperados pelo jogador. + +25 - 28 Fevereiro 2019, +Adicionado número de locks disponíveis para seção de login e controle de item factory. +Incrementado quantidade de locks de atividades disponíveis por canal, visando uma carga relevante de pessoas online. +Corrigido abertura de inventário (comportamento do trade) sendo feita mesmo em cenários onde não é possível nem convidar o outro jogador. +Implementado sistema de gerenciamento de convites para as diversas modalidades no jogo (party, guild, messenger, etc). +Ajustado mercantes, agora salvando dados do jogador dono da loja a todo movimento de itens na loja não-aberta (quando o mesmo ainda está gerenciando itens na loja). +Implementado funcionalidade de arrendamento de mapas. Jogadores podem se tornar donos de mapa e evitar outros de farmar nos mesmos. +Ajustado ordenador de inventários, agora deixando itens projéteis (flechas, shurikens e balas) em ordem confortável para uso do jogador. + +01 Março 2019, +Adicionado efeito visual em ganho de Aran skills. +Implementado mecânica de pescaria no código-fonte. + +09 Março 2019, +Corrigido membros de party não recebendo bonus devidamente após membros sairem do mapa/party em alguns casos. +Revisado sistema de experiência em party. Ganhos de bonus agora levam em conta valores-base que membros de party ganham ao derrotar um mob para definir o ganho geral da equipe. Contabilização de ganhos remodelado, buscando por um modo de distribuição mais coerente. \ No newline at end of file diff --git a/launchtest.bat b/launchtest.bat new file mode 100644 index 0000000000..a44a45e8fb --- /dev/null +++ b/launchtest.bat @@ -0,0 +1,61 @@ +REM 'launchtest.bat' Author: Tochi +@echo off +set a=0 +title HeavenMS: Offline +color 1b +:clear +cls +echo HeavenMS Server Launcher +echo. +echo Commands: +echo ------------------------------------------------------------- +echo start - Start HeavenMS server +echo shutdown - Shut down HeavenMS server and close Launcher File +echo restart - Restart HeavenMS Launcher File +echo clear - Clear this window +echo ------------------------------------------------------------- +echo. + +:command +set /p s="Enter command: " +if "%s%"=="start" goto :start +if "%s%"=="shutdown" goto :shutdown +if "%s%"=="restart" goto :restart +if "%s%"=="clear" goto :clear +echo Wrong Command. +echo. +goto :command + +:start +if "%a%"=="1" ( +echo HeavenMS is already active! +echo. +goto :command +) +color 4c +echo This might take a while.... +echo. +title HeavenMS: activating +echo Server Launching... +start /b launch.bat +color 2a +title HeavenMS: Online +set a=1 +ping localhost -w 10000 >nul +echo. +goto :command + +:shutdown +color 4c +title HeavenMS: Shutting Down... +echo The Server Launcher will be close in a few seconds. +ping localhost -w 100000 >nul +taskkill /im cmd.exe + +:restart +color 4c +title HeavenMS: Restarting... +echo Please type 'start' in command box after bat file have been restarted. +ping localhost -w 100000 >nul +start launch.bat +taskkill /im cmd.exe \ No newline at end of file diff --git a/scripts/npc/2010009.js b/scripts/npc/2010009.js index 03f1b62737..3e01a7ae29 100644 --- a/scripts/npc/2010009.js +++ b/scripts/npc/2010009.js @@ -56,7 +56,7 @@ function action(mode, type, selection) { cm.sendNext("Guild Union is just as it says, a union of a number of guilds to form a super group. I am in charge of managing these Guild Unions."); cm.dispose(); } else if (selection == 1) { - cm.sendNext("To make a Guild Union, two and only two Guild Masters need to be in a party and both must be present on this room on the same channel. The leader of this party will be assigned as the Guild Union Master."); + cm.sendNext("To make a Guild Union, two and only #btwo Guild Masters need to be in a party#k and #bboth must be present on this room#k on the same channel. The leader of this party will be assigned as the Guild Union Master.\r\n\r\nInitially, #bonly two guilds#k can make part of the new Union, but over the time you can #rexpand#k the Union capacity by talking to me when the time comes and investing in an estipulated fee."); cm.dispose(); } else if(selection == 2) { if(!cm.isLeader()) { @@ -80,7 +80,7 @@ function action(mode, type, selection) { var rank = cm.getPlayer().getMGC().getAllianceRank(); if (rank == 1) - cm.sendYesNo("Do you want to increase your Alliance by one guild slot? The fee for this procedure is #b" + increaseCost + " mesos#k."); + cm.sendYesNo("Do you want to increase your Alliance by #rone guild#k slot? The fee for this procedure is #b" + increaseCost + " mesos#k."); else { cm.sendNext("Only the Guild Union Master can expand the number of guilds in the Union."); cm.dispose(); diff --git a/scripts/npc/9201128.js b/scripts/npc/9201128.js index 64b0c24817..6fea1725b9 100644 --- a/scripts/npc/9201128.js +++ b/scripts/npc/9201128.js @@ -1,5 +1,6 @@ var map = 677000004; var quest = 28179; +var questItem = 4032491; var status = -1; function start(mode, type, selection) { @@ -15,7 +16,12 @@ function action(mode, type, selection) { } if (status == 0) { if (cm.isQuestStarted(quest)) { - cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + if (cm.haveItem(questItem)) { + cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + } else { + cm.sendOk("The entrance is blocked by a force that can only be lifted by those holding an emblem."); + cm.dispose(); + } } else { cm.sendOk("The entrance is blocked by a strange force."); cm.dispose(); diff --git a/scripts/npc/9201129.js b/scripts/npc/9201129.js index 58d4230c2f..44362c3756 100644 --- a/scripts/npc/9201129.js +++ b/scripts/npc/9201129.js @@ -1,5 +1,6 @@ var map = 677000000; var quest = 28198; +var questItem = 4032495; var status = -1; function start(mode, type, selection) { @@ -15,7 +16,12 @@ function action(mode, type, selection) { } if (status == 0) { if (cm.isQuestStarted(quest)) { - cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + if (cm.haveItem(questItem)) { + cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + } else { + cm.sendOk("The entrance is blocked by a force that can only be lifted by those holding an emblem."); + cm.dispose(); + } } else { cm.sendOk("The entrance is blocked by a strange force."); cm.dispose(); diff --git a/scripts/npc/9201130.js b/scripts/npc/9201130.js index 797499a0c8..795bcde1d5 100644 --- a/scripts/npc/9201130.js +++ b/scripts/npc/9201130.js @@ -1,5 +1,6 @@ var map = 677000008; var quest = 28219; +var questItem = 4032493; var status = -1; function start(mode, type, selection) { @@ -15,7 +16,12 @@ function action(mode, type, selection) { } if (status == 0) { if (cm.isQuestStarted(quest)) { - cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + if (cm.haveItem(questItem)) { + cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + } else { + cm.sendOk("The entrance is blocked by a force that can only be lifted by those holding an emblem."); + cm.dispose(); + } } else { cm.sendOk("The entrance is blocked by a strange force."); cm.dispose(); diff --git a/scripts/npc/9201131.js b/scripts/npc/9201131.js index 1a1b8efb69..ab545d7784 100644 --- a/scripts/npc/9201131.js +++ b/scripts/npc/9201131.js @@ -1,5 +1,6 @@ var map = 677000002; var quest = 28238; +var questItem = 4032492; var status = -1; function start(mode, type, selection) { @@ -15,7 +16,12 @@ function action(mode, type, selection) { } if (status == 0) { if (cm.isQuestStarted(quest)) { - cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + if (cm.haveItem(questItem)) { + cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + } else { + cm.sendOk("The entrance is blocked by a force that can only be lifted by those holding an emblem."); + cm.dispose(); + } } else { cm.sendOk("The entrance is blocked by a strange force."); cm.dispose(); diff --git a/scripts/npc/9201132.js b/scripts/npc/9201132.js index 4b69ee3774..a790f832f6 100644 --- a/scripts/npc/9201132.js +++ b/scripts/npc/9201132.js @@ -1,5 +1,6 @@ var map = 677000006; var quest = 28256; +var questItem = 4032494; var status = -1; function start(mode, type, selection) { @@ -15,7 +16,12 @@ function action(mode, type, selection) { } if (status == 0) { if (cm.isQuestStarted(quest)) { - cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + if (cm.haveItem(questItem)) { + cm.sendYesNo("Would you like to move to #b#m" + map + "##k?"); + } else { + cm.sendOk("The entrance is blocked by a force that can only be lifted by those holding an emblem."); + cm.dispose(); + } } else { cm.sendOk("The entrance is blocked by a strange force."); cm.dispose(); diff --git a/scripts/npc/9977777.js b/scripts/npc/9977777.js index 9591063262..6e3a3079d6 100644 --- a/scripts/npc/9977777.js +++ b/scripts/npc/9977777.js @@ -87,6 +87,8 @@ function writeFeatureTab_PlayerSocialNetwork() { addFeature("Engagement & Wedding system with ring effects."); addFeature("Equipments displays to everyone it's level & EXP info."); addFeature("Further improved the existent minigame mechanics."); + addFeature("Trade complete using handshake synchronization."); + addFeature("GMS-like trade results displaying after transactions."); } function writeFeatureTab_CashItems() { @@ -123,11 +125,11 @@ 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("Enhanced aggro system, with real-time DPS detection."); + addFeature("Puppets keep targeted mobs nearby on new aggro."); 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."); - addFeature("Puppets holds targeted mobs nearby on new aggro feat."); addFeature("Properly applying MP cost on non-skill mob moves."); addFeature("Limited underling mob spawns."); addFeature("Implemented mob banish by touch & skill move."); @@ -173,6 +175,7 @@ function writeFeatureTab_Playerpotentials() { addFeature("Reviewed keybinding system."); addFeature("Character slots per world/server-wide."); addFeature("Optional cash shop inventory separated by classes."); + addFeature("Players manage 'same-typed' invites exclusively."); } function writeFeatureTab_Serverpotentials() { @@ -188,6 +191,7 @@ function writeFeatureTab_Serverpotentials() { addFeature("Players-appointed bestsellers for Owl and Cash Shop."); addFeature("Tweaked pet/mount hunger to a balanced growth rate."); addFeature("Consistent experience and meso gain system."); + addFeature("Fredrick Store Bank warn/delete unclaimed items."); addFeature("NPC crafters won't take items freely anymore."); addFeature("Duey: pkg rcvd popup and many delivery mechanics."); addFeature("Pet pickup gives preference to player attacks."); @@ -198,6 +202,8 @@ function writeFeatureTab_Serverpotentials() { addFeature("M. book announcer displays info based on demand."); addFeature("Custom jail system."); addFeature("Custom buyback system, uses mesos / NX, via MTS."); + addFeature("Custom fishing system having 'seasonal' catch times."); + addFeature("Custom map leasing system."); addFeature("Delete Character."); addFeature("Smooth view-all-char, now showing all account chars."); addFeature("Centralized servertime, boosting handler performance."); diff --git a/sql/db_database.sql b/sql/db_database.sql index 645d8bada3..aaaf8c9f6b 100644 --- a/sql/db_database.sql +++ b/sql/db_database.sql @@ -12866,6 +12866,15 @@ CREATE TABLE IF NOT EXISTS `family_character` ( INDEX (cid, familyid) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; +CREATE TABLE IF NOT EXISTS `fredstorage` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `cid` int(10) unsigned NOT NULL, + `daynotes` int(4) unsigned NOT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY `cid_2` (`cid`), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; + CREATE TABLE IF NOT EXISTS `gifts` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `to` int(11) NOT NULL, diff --git a/src/client/MapleCharacter.java b/src/client/MapleCharacter.java index a481d7c72a..ccc2792c6b 100644 --- a/src/client/MapleCharacter.java +++ b/src/client/MapleCharacter.java @@ -54,6 +54,7 @@ import java.util.regex.Pattern; import net.server.PlayerBuffValueHolder; import net.server.PlayerCoolDownValueHolder; import net.server.Server; +import net.server.coordinator.MapleInviteCoordinator; import net.server.guild.MapleAlliance; import net.server.guild.MapleGuild; import net.server.guild.MapleGuildCharacter; @@ -125,6 +126,7 @@ import client.inventory.PetDataFactory; import client.inventory.manipulator.MapleCashidGenerator; import client.inventory.manipulator.MapleInventoryManipulator; import client.newyear.NewYearCardRecord; +import client.processor.FredrickProcessor; import constants.ExpTable; import constants.GameConstants; import constants.ItemConstants; @@ -167,7 +169,7 @@ import net.server.audit.locks.factory.MonitoredReentrantLockFactory; public class MapleCharacter extends AbstractMapleCharacterObject { private static final MapleItemInformationProvider ii = MapleItemInformationProvider.getInstance(); private static final String LEVEL_200 = "[Congrats] %s has reached Level %d! Congratulate %s on such an amazing achievement!"; - private static final String[] BLOCKED_NAMES = {"admin", "owner", "moderator", "intern", "donor", "administrator", "help", "helper", "alert", "notice", "maplestory", "fuck", "wizet", "fucking", "negro", "fuk", "fuc", "penis", "pussy", "asshole", "gay", + private static final String[] BLOCKED_NAMES = {"admin", "owner", "moderator", "intern", "donor", "administrator", "FREDRICK", "help", "helper", "alert", "notice", "maplestory", "fuck", "wizet", "fucking", "negro", "fuk", "fuc", "penis", "pussy", "asshole", "gay", "nigger", "homo", "suck", "cum", "shit", "shitty", "condom", "security", "official", "rape", "nigga", "sex", "tit", "boner", "orgy", "clit", "asshole", "fatass", "bitch", "support", "gamemaster", "cock", "gaay", "gm", "operate", "master", "sysop", "party", "GameMaster", "community", "message", "event", "test", "meso", "Scania", "yata", "AsiaSoft", "henesys"}; @@ -208,6 +210,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { private boolean usedStorage = false; private String name; private String chalktext; + private String commandtext; private String dataString; private String search = null; private AtomicBoolean mapTransitioning = new AtomicBoolean(true); // player client is currently trying to change maps or log in the game map @@ -313,7 +316,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { private short extraRecInterval; private int targetHpBarHash = 0; private long targetHpBarTime = 0; - private long nextUnderlevelTime = 0; + private long nextWarningTime = 0; private int banishMap = -1; private int banishSp = -1; private long banishTime = 0; @@ -1055,9 +1058,12 @@ public class MapleCharacter extends AbstractMapleCharacterObject { gainSp(spGain, GameConstants.getSkillBook(newJob.getId()), true); } - if (newJob.getId() % 10 > 1) { + // thanks xinyifly for finding out job advancements awarding APs + /* + if (newJob.getId() % 10 >= 1) { gainAp(5, true); } + */ if (!isGM()) { for (byte i = 1; i < 5; i++) { @@ -1578,6 +1584,10 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } } } + + public void removeIncomingInvites() { + MapleInviteCoordinator.removePlayerIncomingInvites(id); + } private void changeMapInternal(final MapleMap to, final Point pos, final byte[] warpPacket) { if(!canWarpMap) return; @@ -1586,6 +1596,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { this.unregisterChairBuff(); this.clearBanishPlayerData(); + MapleTrade.cancelTrade(this, MapleTrade.TradeResult.UNSUCCESSFUL_ANOTHER_MAP); this.closePlayerInteractions(); client.announce(warpPacket); @@ -1947,6 +1958,16 @@ public class MapleCharacter extends AbstractMapleCharacterObject { public boolean canHold(int itemid, int quantity) { return client.getAbstractPlayerInteraction().canHold(itemid, quantity); } + + public boolean canHoldUniques(List itemids) { + for (Integer itemid : itemids) { + if (ii.isPickupRestricted(itemid) && this.haveItem(itemid)) { + return false; + } + } + + return true; + } public boolean isRidingBattleship() { Integer bv = getBuffedValue(MapleBuffStat.MONSTER_RIDING); @@ -4131,15 +4152,15 @@ public class MapleCharacter extends AbstractMapleCharacterObject { public int getChair() { return chair.get(); } - + public String getChalkboard() { return this.chalktext; } - + public MapleClient getClient() { return client; } - + public final List getCompletedQuests() { synchronized (quests) { List ret = new LinkedList<>(); @@ -4738,6 +4759,32 @@ public class MapleCharacter extends AbstractMapleCharacterObject { public int getMerchantMeso() { return merchantmeso; } + + public int getMerchantNetMeso() { + int elapsedDays = 0; + + try { + Connection con = DatabaseConnection.getConnection(); + + try (PreparedStatement ps = con.prepareStatement("SELECT `timestamp` FROM `fredstorage` WHERE `cid` = ?")) { + ps.setInt(1, id); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + elapsedDays = FredrickProcessor.timestampElapsedDays(rs.getTimestamp(1), System.currentTimeMillis()); + } + } + } + + con.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + if (elapsedDays > 100) elapsedDays = 100; + + int netMeso = (merchantmeso * (100 - elapsedDays)) / 100; + return netMeso; + } public int getMesosTraded() { return mesosTraded; @@ -4980,7 +5027,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } public void closeTrade() { - MapleTrade.cancelTrade(this); + MapleTrade.cancelTrade(this, MapleTrade.TradeResult.PARTNER_CANCEL); } public void closePlayerShop() { @@ -5024,9 +5071,13 @@ public class MapleCharacter extends AbstractMapleCharacterObject { MapleHiredMerchant merchant = this.getHiredMerchant(); if(merchant == null) return; - if(closeMerchant) { - merchant.removeVisitor(this); - this.setHiredMerchant(null); + if (closeMerchant) { + if (merchant.isOwner(this) && merchant.getItems().isEmpty()) { + merchant.forceClose(); + } else { + merchant.removeVisitor(this); + this.setHiredMerchant(null); + } } else { if (merchant.isOwner(this)) { merchant.setOpen(true); @@ -5674,6 +5725,10 @@ public class MapleCharacter extends AbstractMapleCharacterObject { return guildid > 0 && guildRank < 3; } + public boolean attemptCatchFish(int baitLevel) { + return GameConstants.isFishingArea(mapid) && this.getPosition().getY() > 0 && ItemConstants.isFishingChair(chair.get()) && this.getWorldServer().registerFisherPlayer(this, baitLevel); + } + public void leaveMap() { releaseControlledMonsters(); visibleMapObjects.clear(); @@ -5681,6 +5736,10 @@ public class MapleCharacter extends AbstractMapleCharacterObject { if (hpDecreaseTask != null) { hpDecreaseTask.cancel(false); } + + if (map.unclaimOwnership(this)) { + map.dropMessage(5, "This lawn is now free real estate."); + } } private int getChangedJobSp(MapleJob newJob) { @@ -6950,7 +7009,12 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } private void unsitChairInternal() { - if (chair.get() != 0) { + int chairid = chair.get(); + if (chairid != 0) { + if (ItemConstants.isFishingChair(chairid)) { + this.getWorldServer().unregisterFisherPlayer(this); + } + setChair(0); if (unregisterChairBuff()) { getMap().broadcastMessage(this, MaplePacketCreator.cancelForeignChairSkillEffect(this.getId()), false); @@ -8085,10 +8149,14 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } public void sendNote(String to, String msg, byte fame) throws SQLException { + sendNote(to, this.getName(), msg, fame); + } + + public static void sendNote(String to, String from, String msg, byte fame) throws SQLException { Connection con = DatabaseConnection.getConnection(); try (PreparedStatement ps = con.prepareStatement("INSERT INTO notes (`to`, `from`, `message`, `timestamp`, `fame`) VALUES (?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, to); - ps.setString(2, this.getName()); + ps.setString(2, from); ps.setString(3, msg); ps.setLong(4, Server.getInstance().getCurrentTime()); ps.setByte(5, fame); @@ -8262,7 +8330,7 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } public synchronized void withdrawMerchantMesos() { - int merchantMeso = this.getMerchantMeso(); + int merchantMeso = this.getMerchantNetMeso(); if (merchantMeso > 0) { int possible = Integer.MAX_VALUE - merchantMeso; @@ -8275,6 +8343,17 @@ public class MapleCharacter extends AbstractMapleCharacterObject { this.setMerchantMeso(0); } } + } else { + int playerMeso = this.getMeso(); + int nextMeso = playerMeso + merchantMeso; + + if (nextMeso < 0) { + this.gainMeso(-playerMeso, false); + this.setMerchantMeso(merchantMeso + playerMeso); + } else { + this.gainMeso(merchantMeso, false); + this.setMerchantMeso(0); + } } } @@ -8461,6 +8540,8 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } public void changeName(String name) { + FredrickProcessor.removeFredrickReminders(this.getId()); + this.name = name; try { Connection con = DatabaseConnection.getConnection(); @@ -8830,16 +8911,32 @@ public class MapleCharacter extends AbstractMapleCharacterObject { } public void showUnderleveledInfo(MapleMonster mob) { - chrLock.lock(); - try { - long curTime = Server.getInstance().getCurrentTime(); - if(nextUnderlevelTime < curTime) { - nextUnderlevelTime = curTime + (60 * 1000); // show underlevel info again after 1 minute - - showHint("You have gained #rno experience#k from defeating #e#b" + mob.getName() + "#k#n (lv. #b" + mob.getLevel() + "#k)! Take note you must have around the same level as the mob to start earning EXP from it."); + long curTime = Server.getInstance().getCurrentTime(); + if(nextWarningTime < curTime) { + nextWarningTime = curTime + (60 * 1000); // show underlevel info again after 1 minute + + showHint("You have gained #rno experience#k from defeating #e#b" + mob.getName() + "#k#n (lv. #b" + mob.getLevel() + "#k)! Take note you must have around the same level as the mob to start earning EXP from it."); + } + } + + public void showMapOwnershipInfo(MapleCharacter mapOwner) { + long curTime = Server.getInstance().getCurrentTime(); + if(nextWarningTime < curTime) { + nextWarningTime = curTime + (60 * 1000); // show underlevel info again after 1 minute + + String medal = ""; + Item medalItem = mapOwner.getInventory(MapleInventoryType.EQUIPPED).getItem((short) -49); + if (medalItem != null) { + medal = "<" + ii.getName(medalItem.getItemId()) + "> "; } - } finally { - chrLock.unlock(); + + List strLines = new LinkedList<>(); + strLines.add(""); + strLines.add(""); + strLines.add(""); + strLines.add(this.getClient().getChannelServer().getServerMessage().isEmpty() ? 0 : 1, "Get off my lawn!!"); + + this.announce(MaplePacketCreator.getAvatarMega(mapOwner, medal, this.getClient().getChannel(), 5390006, strLines, true)); } } @@ -9844,6 +9941,14 @@ public class MapleCharacter extends AbstractMapleCharacterObject { public void removeJailExpirationTime() { jailExpiration = 0; } + + public String getLastCommandMessage() { + return this.commandtext; + } + + public void setLastCommandMessage(String text) { + this.commandtext = text; + } public int getRewardPoints() { Connection con = null; diff --git a/src/client/MapleClient.java b/src/client/MapleClient.java index 55089dd751..7cd65524ed 100644 --- a/src/client/MapleClient.java +++ b/src/client/MapleClient.java @@ -118,12 +118,18 @@ public class MapleClient { private final Semaphore actionsSemaphore = new Semaphore(7); private final Lock lock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CLIENT, true); private final Lock encoderLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CLIENT_ENCODER, true); - private static final Lock loginLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CLIENT_LOGIN, true); + private static final Lock loginLocks[] = new Lock[200]; // thanks Masterrulax & try2hack for pointing out a bottleneck issue here private int votePoints; private int voteTime = -1; private int visibleWorlds; private long lastNpcClick; private long sessionId; + + static { + for (int i = 0; i < 200; i++) { + loginLocks[i] = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CLIENT_LOGIN, true); + } + } public MapleClient(MapleAESOFB send, MapleAESOFB receive, IoSession session) { this.send = send; @@ -434,6 +440,7 @@ public class MapleClient { } public int finishLogin() { + Lock loginLock = loginLocks[this.getAccID() % 200]; loginLock.lock(); try { if (getLoginState() > LOGIN_NOTLOGGEDIN) { // 0 = LOGIN_NOTLOGGEDIN, 1= LOGIN_SERVER_TRANSITION, 2 = LOGIN_LOGGEDIN @@ -535,8 +542,15 @@ public class MapleClient { ps.setString(1, login); rs = ps.executeQuery(); if (rs.next()) { - boolean banned = (rs.getByte("banned") == 1); accId = rs.getInt("id"); + if (accId == 0) { + // odd case where accId is actually attributed as 0 (further on this leads to getLoginState ACCID = 0, an absurd), thanks Thora for finding this issue + return 15; + } else if (accId < 0) { + FilePrinter.printError(FilePrinter.LOGIN_EXCEPTION, "Tried to login with accid " + accId); + } + + boolean banned = (rs.getByte("banned") == 1); gmlevel = 0; pin = rs.getString("pin"); pic = rs.getString("pic"); @@ -861,6 +875,7 @@ public class MapleClient { try { player.setDisconnectedFromChannelWorld(); player.notifyMapTransferToPartner(-1); + player.removeIncomingInvites(); player.cancelAllBuffs(true); player.closePlayerInteractions(); @@ -1000,8 +1015,8 @@ public class MapleClient { MapleSessionCoordinator.getInstance().closeSession(session, false); session.removeAttribute(MapleClient.CLIENT_KEY); } - - engines.clear(); + + engines = null; // thanks Tochi for pointing out a NPE here } } @@ -1413,7 +1428,7 @@ public class MapleClient { } if (player.getTrade() != null) { - MapleTrade.cancelTrade(getPlayer()); + MapleTrade.cancelTrade(getPlayer(), MapleTrade.TradeResult.PARTNER_CANCEL); } MapleHiredMerchant merchant = player.getHiredMerchant(); @@ -1429,6 +1444,7 @@ public class MapleClient { server.getPlayerBuffStorage().addDiseasesToStorage(player.getId(), player.getAllDiseases()); player.setDisconnectedFromChannelWorld(); player.notifyMapTransferToPartner(-1); + player.removeIncomingInvites(); player.cancelAllBuffs(true); player.cancelAllDebuffs(); player.cancelBuffExpireTask(); diff --git a/src/client/command/CommandsExecutor.java b/src/client/command/CommandsExecutor.java index 012a784ef0..3a899c2231 100644 --- a/src/client/command/CommandsExecutor.java +++ b/src/client/command/CommandsExecutor.java @@ -93,8 +93,16 @@ public class CommandsExecutor { } private void handleInternal(MapleClient client, String message){ - final String[] spitedMessage = message.toLowerCase().substring(1).split("[ ]"); - final String commandName = spitedMessage[0]; + final String splitRegex = "[ ]"; + String[] splitedMessage = message.substring(1).split(splitRegex, 2); + if (splitedMessage.length < 2) { + splitedMessage = new String[]{splitedMessage[0], ""}; + } + + client.getPlayer().setLastCommandMessage(splitedMessage[1]); // thanks Tochi & Nulliphite for noticing string messages being marshalled lowercase + final String commandName = splitedMessage[0].toLowerCase(); + final String[] lowercaseParams = splitedMessage[1].toLowerCase().split(splitRegex); + final RegisteredCommand command = registeredCommands.get(commandName); if (command == null){ client.getPlayer().yellowMessage("Command '" + commandName + "' is not available. See @commands for a list of available commands."); @@ -105,8 +113,8 @@ public class CommandsExecutor { return; } String[] params; - if (spitedMessage.length > 1) { - params = Arrays.copyOfRange(spitedMessage, 1, spitedMessage.length); + if (lowercaseParams.length > 0) { + params = Arrays.copyOfRange(lowercaseParams, 0, lowercaseParams.length); } else { params = new String[]{}; } @@ -194,6 +202,7 @@ public class CommandsExecutor { addCommand("luk", StatLukCommand.class); addCommand("enableauth", EnableAuthCommand.class); addCommand("toggleexp", ToggleExpCommand.class); + addCommand("mylawn", MapOwnerClaimCommand.class); commandsNameDesc.add(levelCommandsCursor); } @@ -331,6 +340,7 @@ public class CommandsExecutor { addCommand("droprate", 4, DropRateCommand.class); addCommand("questrate", 4, QuestRateCommand.class); addCommand("travelrate", 4, TravelRateCommand.class); + addCommand("fishrate", 4, FishingRateCommand.class); addCommand("itemvac", 4, ItemVacCommand.class); addCommand("forcevac", 4, ForceVacCommand.class); addCommand("zakum", 4, ZakumCommand.class); @@ -356,6 +366,8 @@ public class CommandsExecutor { addCommand("set", 5, SetCommand.class); addCommand("showpackets", 5, ShowPacketsCommand.class); addCommand("showmovelife", 5, ShowMoveLifeCommand.class); + addCommand("showsessions", 5, ShowSessionsCommand.class); + addCommand("iplist", 5, IpListCommand.class); commandsNameDesc.add(levelCommandsCursor); } diff --git a/src/client/command/commands/gm0/GachaCommand.java b/src/client/command/commands/gm0/GachaCommand.java index 44762e9a43..a5f604c14f 100644 --- a/src/client/command/commands/gm0/GachaCommand.java +++ b/src/client/command/commands/gm0/GachaCommand.java @@ -27,7 +27,6 @@ import client.command.Command; import client.MapleClient; import server.MapleItemInformationProvider; import server.gachapon.MapleGachapon; -import tools.MaplePacketCreator; public class GachaCommand extends Command { { @@ -37,7 +36,7 @@ public class GachaCommand extends Command { @Override public void execute(MapleClient c, String[] params) { MapleGachapon.Gachapon gacha = null; - String search = joinStringFrom(params,0); + String search = c.getPlayer().getLastCommandMessage(); String gachaName = ""; String [] names = {"Henesys", "Ellinia", "Perion", "Kerning City", "Sleepywood", "Mushroom Shrine", "Showa Spa Male", "Showa Spa Female", "New Leaf City", "Nautilus Harbor"}; int [] ids = {9100100, 9100101, 9100102, 9100103, 9100104, 9100105, 9100106, 9100107, 9100109, 9100117}; diff --git a/src/client/command/commands/gm0/GmCommand.java b/src/client/command/commands/gm0/GmCommand.java index c625022789..b9f2a553f9 100644 --- a/src/client/command/commands/gm0/GmCommand.java +++ b/src/client/command/commands/gm0/GmCommand.java @@ -50,7 +50,7 @@ public class GmCommand extends Command { player.dropMessage(5, "Your message was too short. Please provide as much detail as possible."); return; } - String message = joinStringFrom(params, 0); + String message = player.getLastCommandMessage(); Server.getInstance().broadcastGMMessage(c.getWorld(), MaplePacketCreator.sendYellowTip("[GM MESSAGE]:" + MapleCharacter.makeMapleReadable(player.getName()) + ": " + message)); Server.getInstance().broadcastGMMessage(c.getWorld(), MaplePacketCreator.serverNotice(1, message)); FilePrinter.printError(FilePrinter.COMMAND_GM, MapleCharacter.makeMapleReadable(player.getName()) + ": " + message); diff --git a/src/client/command/commands/gm0/MapOwnerClaimCommand.java b/src/client/command/commands/gm0/MapOwnerClaimCommand.java new file mode 100644 index 0000000000..eac30717e6 --- /dev/null +++ b/src/client/command/commands/gm0/MapOwnerClaimCommand.java @@ -0,0 +1,62 @@ +/* + This file is part of the HeavenMS MapleStory Server, commands OdinMS-based + 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 . +*/ + +/* + @Author: Ronan +*/ +package client.command.commands.gm0; + +import client.command.Command; +import client.MapleCharacter; +import client.MapleClient; +import constants.ServerConstants; + +public class MapOwnerClaimCommand extends Command { + { + setDescription(""); + } + + @Override + public void execute(MapleClient c, String[] params) { + if (c.tryacquireClient()) { + try { + MapleCharacter chr = c.getPlayer(); + + if (ServerConstants.USE_MAP_OWNERSHIP_SYSTEM) { + if (chr.getEventInstance() == null) { + if (chr.getMap().unclaimOwnership(chr)) { + chr.dropMessage(5, "This lawn is now free real estate."); + } else if (chr.getMap().claimOwnership(chr)) { + chr.dropMessage(5, "You have leased this lawn for a while, until you leave here or after 1 minute of inactivity."); + } else { + chr.dropMessage(5, "This lawn has already been leased by another player."); + } + } else { + chr.dropMessage(5, "This lawn cannot be leased."); + } + } else { + chr.dropMessage(5, "Feature unavailable."); + } + } finally { + c.releaseClient(); + } + } + } +} diff --git a/src/client/command/commands/gm0/ReportBugCommand.java b/src/client/command/commands/gm0/ReportBugCommand.java index 06dfff78b6..27bdbd102f 100644 --- a/src/client/command/commands/gm0/ReportBugCommand.java +++ b/src/client/command/commands/gm0/ReportBugCommand.java @@ -43,7 +43,7 @@ public class ReportBugCommand extends Command { player.dropMessage(5, "Message too short and not sent. Please do @bug "); return; } - String message = joinStringFrom(params, 0); + String message = player.getLastCommandMessage(); Server.getInstance().broadcastGMMessage(c.getWorld(), MaplePacketCreator.sendYellowTip("[BUG]:" + MapleCharacter.makeMapleReadable(player.getName()) + ": " + message)); Server.getInstance().broadcastGMMessage(c.getWorld(), MaplePacketCreator.serverNotice(1, message)); FilePrinter.printError(FilePrinter.COMMAND_BUG, MapleCharacter.makeMapleReadable(player.getName()) + ": " + message); diff --git a/src/client/command/commands/gm1/WhatDropsFromCommand.java b/src/client/command/commands/gm1/WhatDropsFromCommand.java index cd1eecee99..e13c9c6339 100644 --- a/src/client/command/commands/gm1/WhatDropsFromCommand.java +++ b/src/client/command/commands/gm1/WhatDropsFromCommand.java @@ -46,7 +46,7 @@ public class WhatDropsFromCommand extends Command { player.dropMessage(5, "Please do @whatdropsfrom "); return; } - String monsterName = joinStringFrom(params, 0); + String monsterName = player.getLastCommandMessage(); String output = ""; int limit = 3; Iterator> listIterator = MapleMonsterInformationProvider.getMobsIDsFromName(monsterName).iterator(); diff --git a/src/client/command/commands/gm1/WhoDropsCommand.java b/src/client/command/commands/gm1/WhoDropsCommand.java index 8da3801911..955c1a1b83 100644 --- a/src/client/command/commands/gm1/WhoDropsCommand.java +++ b/src/client/command/commands/gm1/WhoDropsCommand.java @@ -51,7 +51,7 @@ public class WhoDropsCommand extends Command { if (c.tryacquireClient()) { try { - String searchString = joinStringFrom(params, 0); + String searchString = player.getLastCommandMessage(); String output = ""; Iterator> listIterator = MapleItemInformationProvider.getInstance().getItemDataByName(searchString).iterator(); if(listIterator.hasNext()) { diff --git a/src/client/command/commands/gm3/GiveNxCommand.java b/src/client/command/commands/gm3/GiveNxCommand.java index f3d4dd12c0..cae5b4bf0e 100644 --- a/src/client/command/commands/gm3/GiveNxCommand.java +++ b/src/client/command/commands/gm3/GiveNxCommand.java @@ -36,15 +36,37 @@ public class GiveNxCommand extends Command { public void execute(MapleClient c, String[] params) { MapleCharacter player = c.getPlayer(); if (params.length < 1) { - player.yellowMessage("Syntax: !givenx [] "); + player.yellowMessage("Syntax: !givenx [nx, mp, np] [] "); return; } - String recv; - int value; + String recv, typeStr = "nx"; + int value, type = 1; if (params.length > 1) { - recv = params[0]; - value = Integer.parseInt(params[1]); + if (params[0].length() == 2) { + switch (params[0]) { + case "mp": // maplePoint + type = 2; + break; + case "np": // nxPrepaid + type = 4; + break; + default: + type = 1; + } + typeStr = params[0]; + + if (params.length > 2) { + recv = params[1]; + value = Integer.parseInt(params[2]); + } else { + recv = c.getPlayer().getName(); + value = Integer.parseInt(params[1]); + } + } else { + recv = params[0]; + value = Integer.parseInt(params[1]); + } } else { recv = c.getPlayer().getName(); value = Integer.parseInt(params[0]); @@ -52,8 +74,8 @@ public class GiveNxCommand extends Command { MapleCharacter victim = c.getWorldServer().getPlayerStorage().getCharacterByName(recv); if (victim != null) { - victim.getCashShop().gainCash(1, value); - player.message("NX given."); + victim.getCashShop().gainCash(type, value); + player.message(typeStr.toUpperCase() + " given."); } else { player.message("Player '" + recv + "' could not be found."); } diff --git a/src/client/command/commands/gm3/MusicCommand.java b/src/client/command/commands/gm3/MusicCommand.java index 9f383d781f..988a3574e9 100644 --- a/src/client/command/commands/gm3/MusicCommand.java +++ b/src/client/command/commands/gm3/MusicCommand.java @@ -57,7 +57,7 @@ public class MusicCommand extends Command { return; } - String song = joinStringFrom(params, 0); + String song = player.getLastCommandMessage(); for (String s : GameConstants.GAME_SONGS) { if (s.equalsIgnoreCase(song)) { // thanks Masterrulax for finding an issue here player.getMap().broadcastMessage(MaplePacketCreator.musicChange(s)); diff --git a/src/client/command/commands/gm3/NoticeCommand.java b/src/client/command/commands/gm3/NoticeCommand.java index 9ee0011f2f..8726b33cb5 100644 --- a/src/client/command/commands/gm3/NoticeCommand.java +++ b/src/client/command/commands/gm3/NoticeCommand.java @@ -37,6 +37,6 @@ public class NoticeCommand extends Command { @Override public void execute(MapleClient c, String[] params) { MapleCharacter player = c.getPlayer(); - Server.getInstance().broadcastMessage(c.getWorld(), MaplePacketCreator.serverNotice(6, "[Notice] " + joinStringFrom(params, 0))); + Server.getInstance().broadcastMessage(c.getWorld(), MaplePacketCreator.serverNotice(6, "[Notice] " + player.getLastCommandMessage())); } } diff --git a/src/client/command/commands/gm4/FishingRateCommand.java b/src/client/command/commands/gm4/FishingRateCommand.java new file mode 100644 index 0000000000..9fb325113c --- /dev/null +++ b/src/client/command/commands/gm4/FishingRateCommand.java @@ -0,0 +1,48 @@ +/* + This file is part of the HeavenMS MapleStory Server, commands OdinMS-based + 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 . +*/ + +/* + @Author: Ronan +*/ +package client.command.commands.gm4; + +import client.command.Command; +import client.MapleClient; +import client.MapleCharacter; +import tools.MaplePacketCreator; + +public class FishingRateCommand extends Command { + { + setDescription(""); + } + + @Override + public void execute(MapleClient c, String[] params) { + MapleCharacter player = c.getPlayer(); + if (params.length < 1) { + player.yellowMessage("Syntax: !fishrate "); + return; + } + + int fishrate = Math.max(Integer.parseInt(params[0]), 1); + c.getWorldServer().setFishingRate(fishrate); + c.getWorldServer().broadcastPacket(MaplePacketCreator.serverNotice(6, "[Rate] Fishing Rate has been changed to " + fishrate + "x.")); + } +} diff --git a/src/client/command/commands/gm4/ServerMessageCommand.java b/src/client/command/commands/gm4/ServerMessageCommand.java index 428133eded..ee2a657055 100644 --- a/src/client/command/commands/gm4/ServerMessageCommand.java +++ b/src/client/command/commands/gm4/ServerMessageCommand.java @@ -34,7 +34,7 @@ public class ServerMessageCommand extends Command { @Override public void execute(MapleClient c, String[] params) { - //MapleCharacter player = c.getPlayer(); - c.getWorldServer().setServerMessage(joinStringFrom(params, 0)); + MapleCharacter player = c.getPlayer(); + c.getWorldServer().setServerMessage(player.getLastCommandMessage()); } } diff --git a/src/client/command/commands/gm5/DebugCommand.java b/src/client/command/commands/gm5/DebugCommand.java index 7346f522cb..517ca62525 100644 --- a/src/client/command/commands/gm5/DebugCommand.java +++ b/src/client/command/commands/gm5/DebugCommand.java @@ -77,7 +77,7 @@ public class DebugCommand extends Command { break; case "packet": - player.getMap().broadcastMessage(MaplePacketCreator.customPacket(joinStringFrom(params, 1))); + //player.getMap().broadcastMessage(MaplePacketCreator.customPacket(joinStringFrom(params, 1))); break; case "portal": diff --git a/src/client/command/commands/gm5/IpListCommand.java b/src/client/command/commands/gm5/IpListCommand.java new file mode 100644 index 0000000000..8442aac894 --- /dev/null +++ b/src/client/command/commands/gm5/IpListCommand.java @@ -0,0 +1,60 @@ +/* + 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 client.command.commands.gm5; + +import java.util.Collection; +import client.MapleClient; +import client.MapleCharacter; +import client.command.Command; +import constants.GameConstants; +import net.server.Server; +import net.server.world.World; + +/** + * + * @author Mist + * @author Blood (Tochi) + * @author Ronan + */ +public class IpListCommand extends Command { + { + setDescription(""); + } + + @Override + public void execute(MapleClient c, String[] params) { + String str = "Player-IP relation:"; + + for (World w : Server.getInstance().getWorlds()) { + Collection chars = w.getPlayerStorage().getAllCharacters(); + + if (!chars.isEmpty()) { + str += "\r\n" + GameConstants.WORLD_NAMES[w.getId()] + "\r\n"; + + for (MapleCharacter chr : chars) { + str += " " + chr.getName() + " - " + chr.getClient().getSession().getRemoteAddress() + "\r\n"; + } + } + } + + c.getAbstractPlayerInteraction().npcTalk(22000, str); + } + +} \ No newline at end of file diff --git a/src/client/command/commands/gm5/ShowSessionsCommand.java b/src/client/command/commands/gm5/ShowSessionsCommand.java new file mode 100644 index 0000000000..f48d0cfd84 --- /dev/null +++ b/src/client/command/commands/gm5/ShowSessionsCommand.java @@ -0,0 +1,39 @@ +/* + 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 client.command.commands.gm5; + +import client.MapleClient; +import client.command.Command; +import net.server.coordinator.MapleSessionCoordinator; + +/** + * + * @author Ronan + */ +public class ShowSessionsCommand extends Command { + { + setDescription(""); + } + + @Override + public void execute(MapleClient c, String[] params) { + MapleSessionCoordinator.getInstance().printSessionTrace(c); + } +} diff --git a/src/client/inventory/ItemFactory.java b/src/client/inventory/ItemFactory.java index db943ba385..49fa62af2e 100644 --- a/src/client/inventory/ItemFactory.java +++ b/src/client/inventory/ItemFactory.java @@ -28,10 +28,10 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Lock; -import tools.DatabaseConnection; -import tools.Pair; import net.server.audit.locks.MonitoredLockType; import net.server.audit.locks.factory.MonitoredReentrantLockFactory; +import tools.DatabaseConnection; +import tools.Pair; /** * @@ -48,8 +48,14 @@ public enum ItemFactory { CASH_OVERALL(7, true); private final int value; private final boolean account; - private static final Lock lock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.ITEM, true); - + private static final Lock locks[] = new Lock[200]; // thanks Masterrulax for pointing out a bottleneck issue here + + static { + for (int i = 0; i < 200; i++) { + locks[i] = MonitoredReentrantLockFactory.createLock(MonitoredLockType.ITEM, true); + } + } + private ItemFactory(int value, boolean account) { this.value = value; this.account = account; @@ -192,6 +198,7 @@ public enum ItemFactory { PreparedStatement pse = null; ResultSet rs = null; + Lock lock = locks[id % 200]; lock.lock(); try { StringBuilder query = new StringBuilder(); @@ -357,6 +364,7 @@ public enum ItemFactory { PreparedStatement pse = null; ResultSet rs = null; + Lock lock = locks[id % 200]; lock.lock(); try { ps = con.prepareStatement("DELETE FROM `inventorymerchant` WHERE `characterid` = ?"); diff --git a/src/client/inventory/MapleInventory.java b/src/client/inventory/MapleInventory.java index 3ebb08e710..104dfc9b13 100644 --- a/src/client/inventory/MapleInventory.java +++ b/src/client/inventory/MapleInventory.java @@ -435,8 +435,8 @@ public class MapleInventory implements Iterable { } } - public static boolean checkSpot(MapleCharacter chr, Item item) { - return !chr.getInventory(item.getInventoryType()).isFull(); + public static boolean checkSpot(MapleCharacter chr, Item item) { // thanks Vcoc for noticing pshops not checking item stacks when taking item back + return checkSpotsAndOwnership(chr, Collections.singletonList(new Pair<>(item, item.getInventoryType()))); } public static boolean checkSpots(MapleCharacter chr, List> items) { diff --git a/src/client/processor/FredrickProcessor.java b/src/client/processor/FredrickProcessor.java index 49311315a0..429000e075 100644 --- a/src/client/processor/FredrickProcessor.java +++ b/src/client/processor/FredrickProcessor.java @@ -31,9 +31,16 @@ import client.inventory.MapleInventory; import client.inventory.MapleInventoryType; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.LinkedList; import java.util.List; import client.inventory.manipulator.MapleInventoryManipulator; +import constants.ServerConstants; +import java.util.Collections; +import net.server.Server; +import net.server.world.World; import server.MapleItemInformationProvider; import server.maps.MapleHiredMerchant; import tools.DatabaseConnection; @@ -43,14 +50,220 @@ import tools.Pair; /** * - * @author RonanLana - synchronization of Fredrick modules + * @author RonanLana - synchronization of Fredrick modules & operation results */ public class FredrickProcessor { - private static boolean canRetrieveFromFredrick(MapleCharacter chr, List> items) { - if (!chr.canHoldMeso(chr.getMerchantMeso())) { - return false; + + private static int[] dailyReminders = new int[]{2, 5, 10, 15, 30, 60, 90, Integer.MAX_VALUE}; + + private static byte canRetrieveFromFredrick(MapleCharacter chr, List> items) { + if (!MapleInventory.checkSpotsAndOwnership(chr, items)) { + List itemids = new LinkedList<>(); + for (Pair it : items) { + itemids.add(it.getLeft().getItemId()); + } + + if (chr.canHoldUniques(itemids)) { + return 0x22; + } else { + return 0x20; + } + } + + int netMeso = chr.getMerchantNetMeso(); + if (netMeso > 0) { + if (!chr.canHoldMeso(netMeso)) { + return 0x1F; + } + } else { + if (chr.getMeso() < -1 * netMeso) { + return 0x21; + } + } + + return 0x0; + } + + public static int timestampElapsedDays(Timestamp then, long timeNow) { + return (int) ((timeNow - then.getTime()) / (1000 * 60 * 60 * 24)); + } + + private static String fredrickReminderMessage(int daynotes) { + String msg; + + if (daynotes < 4) { + msg = "Hi customer! I am Fredrick, the Union Chief of the Hired Merchant Union. A reminder that " + dailyReminders[daynotes] + " days have passed since you used our service. Please reclaim your stored goods at FM Entrance."; + } else { + msg = "Hi customer! I am Fredrick, the Union Chief of the Hired Merchant Union. " + dailyReminders[daynotes] + " days have passed since you used our service. Consider claiming back the items before we move them away for refund."; + } + + return msg; + } + + private static void removeFredrickLog(int cid) { + try { + Connection con = DatabaseConnection.getConnection(); + removeFredrickLog(con, cid); + con.close(); + } catch (SQLException sqle) { + sqle.printStackTrace(); + } + } + + private static void removeFredrickLog(Connection con, int cid) throws SQLException { + try (PreparedStatement ps = con.prepareStatement("DELETE FROM `fredstorage` WHERE `cid` = ?")) { + ps.setInt(1, cid); + ps.execute(); + } + } + + public static void insertFredrickLog(int cid) { + try { + Connection con = DatabaseConnection.getConnection(); + + removeFredrickLog(con, cid); + try (PreparedStatement ps = con.prepareStatement("INSERT INTO `fredstorage` (`cid`, `daynotes`, `timestamp`) VALUES (?, 0, ?)")) { + ps.setInt(1, cid); + ps.setTimestamp(2, new Timestamp(System.currentTimeMillis())); + ps.execute(); + } + + con.close(); + } catch (SQLException sqle) { + sqle.printStackTrace(); + } + } + + public static void removeFredrickReminders(int cid) { + removeFredrickReminders(Collections.singletonList(new Pair<>(cid, 0))); + } + + private static void removeFredrickReminders(List> expiredCids) { + List expiredCnames = new LinkedList<>(); + for (Pair id : expiredCids) { + String name = MapleCharacter.getNameById(id.getLeft()); + if (name != null) { + expiredCnames.add(name); + } + } + + try { + Connection con = DatabaseConnection.getConnection(); + try (PreparedStatement ps = con.prepareStatement("DELETE FROM `notes` WHERE `from` LIKE ? AND `to` LIKE ?")) { + ps.setString(1, "FREDRICK"); + + for (String cname : expiredCnames) { + ps.setString(2, cname); + ps.executeBatch(); + } + } + con.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static void runFredrickSchedule() { + try { + Connection con = DatabaseConnection.getConnection(); + + List> expiredCids = new LinkedList<>(); + List, Integer>> notifCids = new LinkedList<>(); + try (PreparedStatement ps = con.prepareStatement("SELECT * FROM fredstorage f LEFT JOIN (SELECT id, name, world, lastLogoutTime FROM characters) AS c ON c.id = f.cid")) { + try (ResultSet rs = ps.executeQuery()) { + long curTime = System.currentTimeMillis(); + + while (rs.next()) { + int cid = rs.getInt("cid"); + int world = rs.getInt("world"); + Timestamp ts = rs.getTimestamp("timestamp"); + int daynotes = Math.min(dailyReminders.length - 1, rs.getInt("daynotes")); + + int elapsedDays = timestampElapsedDays(ts, curTime); + if (elapsedDays > 100) { + expiredCids.add(new Pair<>(cid, world)); + } else { + int notifDay = dailyReminders[daynotes]; + + if (elapsedDays >= notifDay) { + do { + daynotes++; + notifDay = dailyReminders[daynotes]; + } while (elapsedDays >= notifDay); + + Timestamp logoutTs = rs.getTimestamp("lastLogoutTime"); + int inactivityDays = timestampElapsedDays(logoutTs, curTime); + + if (inactivityDays < 7 || daynotes >= dailyReminders.length - 1) { // don't spam inactive players + String name = rs.getString("name"); + notifCids.add(new Pair<>(new Pair<>(cid, name), daynotes)); + } + } + } + } + } + } + + if (!expiredCids.isEmpty()) { + try (PreparedStatement ps = con.prepareStatement("DELETE FROM `inventoryitems` WHERE `type` = ? AND `characterid` = ?")) { + ps.setInt(1, ItemFactory.MERCHANT.getValue()); + + for (Pair cid : expiredCids) { + ps.setInt(2, cid.getLeft()); + ps.addBatch(); + } + + ps.executeBatch(); + } + + try (PreparedStatement ps = con.prepareStatement("UPDATE `characters` SET `MerchantMesos` = 0 WHERE `id` = ?")) { + for (Pair cid : expiredCids) { + ps.setInt(1, cid.getLeft()); + ps.addBatch(); + + World wserv = Server.getInstance().getWorld(cid.getRight()); + if (wserv != null) { + MapleCharacter chr = wserv.getPlayerStorage().getCharacterById(cid.getLeft()); + if (chr != null) { + chr.setMerchantMeso(0); + } + } + } + + ps.executeBatch(); + } + + removeFredrickReminders(expiredCids); + + try (PreparedStatement ps = con.prepareStatement("DELETE FROM `fredstorage` WHERE `cid` = ?")) { + for (Pair cid : expiredCids) { + ps.setInt(1, cid.getLeft()); + ps.addBatch(); + } + + ps.executeBatch(); + } + } + + if (!notifCids.isEmpty()) { + try (PreparedStatement ps = con.prepareStatement("UPDATE `fredstorage` SET `daynotes` = ? WHERE `cid` = ?")) { + for (Pair, Integer> cid : notifCids) { + ps.setInt(1, cid.getRight()); + ps.setInt(2, cid.getLeft().getLeft()); + ps.addBatch(); + + String msg = fredrickReminderMessage(cid.getRight() - 1); + MapleCharacter.sendNote(cid.getLeft().getRight(), "FREDRICK", msg, (byte) 0); + } + + ps.executeBatch(); + } + } + + con.close(); + } catch (SQLException e) { + e.printStackTrace(); } - return MapleInventory.checkSpotsAndOwnership(chr, items); } private static boolean deleteFredrickItems(int cid) { @@ -77,13 +290,15 @@ public class FredrickProcessor { List> items; try { items = ItemFactory.MERCHANT.loadItems(chr.getId(), false); - if (!canRetrieveFromFredrick(chr, items)) { - chr.announce(MaplePacketCreator.fredrickMessage((byte) 0x21)); + + byte response = canRetrieveFromFredrick(chr, items); + if (response != 0) { + chr.announce(MaplePacketCreator.fredrickMessage(response)); return; } - + chr.withdrawMerchantMesos(); - + if (deleteFredrickItems(chr.getId())) { MapleHiredMerchant merchant = chr.getHiredMerchant(); @@ -98,6 +313,7 @@ public class FredrickProcessor { } chr.announce(MaplePacketCreator.fredrickMessage((byte) 0x1E)); + removeFredrickLog(chr.getId()); } else { chr.message("An unknown error has occured."); } diff --git a/src/constants/GameConstants.java b/src/constants/GameConstants.java index d8cfc98e9f..b77cfe6423 100644 --- a/src/constants/GameConstants.java +++ b/src/constants/GameConstants.java @@ -592,6 +592,10 @@ public class GameConstants { return isDojo(mapid) || isPyramid(mapid); } + public static boolean isFishingArea(int mapid) { + return mapid == 120010000 || mapid == 251000100 || mapid == 541010110; + } + public static boolean isFinisherSkill(int skillId) { return skillId > 1111002 && skillId < 1111007 || skillId == 11111002 || skillId == 11111003; } diff --git a/src/constants/ItemConstants.java b/src/constants/ItemConstants.java index d1624daf66..fbbbfc900b 100644 --- a/src/constants/ItemConstants.java +++ b/src/constants/ItemConstants.java @@ -224,6 +224,10 @@ public final class ItemConstants { return itemId < 2000000 && itemId != 0; } + public static boolean isFishingChair(int itemId) { + return itemId == 3011000; + } + public static boolean isMedal(int itemId) { return itemId >= 1140000 && itemId < 1143000; } diff --git a/src/constants/ServerConstants.java b/src/constants/ServerConstants.java index 554dc7f76b..cd07a535c8 100644 --- a/src/constants/ServerConstants.java +++ b/src/constants/ServerConstants.java @@ -20,6 +20,7 @@ public class ServerConstants { public static final int WLDLIST_SIZE = 21; //Max possible worlds on the server. public static final int CHANNEL_SIZE = 20; //Max possible channels per world (which is 20, based on the channel list on login phase). public static final int CHANNEL_LOAD = 100; //Max players per channel (limit actually used to calculate the World server capacity). + public static final int CHANNEL_LOCKS = 20; //Total number of structure management locks each channel has. public static final long RESPAWN_INTERVAL = 10 * 1000; //10 seconds, 10000. public static final long PURGING_INTERVAL = 5 * 60 * 1000; @@ -92,6 +93,7 @@ public class ServerConstants { public static final boolean USE_ENFORCE_ITEM_SUGGESTION = false;//Forces the Owl of Minerva and the Cash Shop to always display the defined item array instead of those featured by the players. public static final boolean USE_ENFORCE_UNMERCHABLE_CASH = true;//Forces players to not sell CASH items via merchants. public static final boolean USE_ENFORCE_UNMERCHABLE_PET = true; //Forces players to not sell pets via merchants. (since non-named pets gets dirty name and other possible DB-related issues) + public static final boolean USE_ENFORCE_MERCHANT_SAVE = true; //Forces automatic DB save on merchant owners, at every item movement on shop. public static final boolean USE_ENFORCE_MDOOR_POSITION = false; //Forces mystic door to be spawned near spawnpoints. public static final boolean USE_SPAWN_LOOT_ON_ANIMATION = false;//Makes loot appear some time after the mob has been killed (following the mob death animation, instead of instantly). public static final boolean USE_SPAWN_RELEVANT_LOOT = true; //Forces to only spawn loots that are collectable by the player or any of their party members. @@ -103,8 +105,9 @@ public class ServerConstants { public static final boolean USE_MULTIPLE_SAME_EQUIP_DROP = true;//Enables multiple drops by mobs of the same equipment, number of possible drops based on the quantities provided at the drop data. public static final boolean USE_BANISHABLE_TOWN_SCROLL = true; //Enables town scrolls to act as if it's a "player banish", rendering the antibanish scroll effect available. public static final boolean USE_ENABLE_FULL_RESPAWN = true; //At respawn task, always respawn missing mobs when they're available. Spawn count doesn't depend on how many players are currently there. - public static final boolean USE_ENABLE_CHAT_LOG = true; //Write in-game chat to log - public static final boolean USE_REBIRTH_SYSTEM = false; //Flag to enable/disable rebirth system + public static final boolean USE_ENABLE_CHAT_LOG = false; //Write in-game chat to log + public static final boolean USE_REBIRTH_SYSTEM = false; //Flag to enable/disable rebirth system + public static final boolean USE_MAP_OWNERSHIP_SYSTEM = true; //Flag to enable/disable map ownership system //Events/PQs Configuration public static final boolean USE_OLD_GMS_STYLED_PQ_NPCS = true; //Enables PQ NPCs with similar behaviour to old GMS style, that skips info about the PQs and immediately tries to register the party in. @@ -137,10 +140,11 @@ public class ServerConstants { public static final int DROP_RATE = 10; public static final int BOSS_DROP_RATE = 10; //NOTE: Boss drop rate OVERRIDES common drop rate, for bosses-only. public static final int QUEST_RATE = 5; //Multiplier for Exp & Meso gains when completing a quest. Only available when USE_QUEST_RATE is true. Stacks with server Exp & Meso rates. + public static final int FISHING_RATE = 10; //Multiplier for success likelihood on meso thrown during fishing. public static final int TRAVEL_RATE = 10; //Means of transportation rides/departs using 1/N of the default time. public static final double EQUIP_EXP_RATE = 1.0; //Rate for equipment exp gain, grows linearly. Set 1.0 for default (about 100~200 same-level range mobs killed to pass equip from level 1 to 2). - public static final double PARTY_BONUS_EXP_RATE = 1.0; //Rate for the party exp reward. + public static final float PARTY_BONUS_EXP_RATE = 1.0f; //Rate for the party exp bonus reward. public static final double PQ_BONUS_EXP_RATE = 0.5; //Rate for the PQ exp reward. public static final int PARTY_EXPERIENCE_MOD = 1; //Change for event stuff. diff --git a/src/net/PacketProcessor.java b/src/net/PacketProcessor.java index ae8ec01675..2b8d264305 100644 --- a/src/net/PacketProcessor.java +++ b/src/net/PacketProcessor.java @@ -231,6 +231,7 @@ public final class PacketProcessor { registerHandler(RecvOpcode.ADMIN_COMMAND, new AdminCommandHandler()); registerHandler(RecvOpcode.ADMIN_LOG, new AdminLogHandler()); registerHandler(RecvOpcode.ALLIANCE_OPERATION, new AllianceOperationHandler()); + registerHandler(RecvOpcode.DENY_ALLIANCE_REQUEST, new DenyAllianceRequestHandler()); registerHandler(RecvOpcode.USE_SOLOMON_ITEM, new UseSolomonHandler()); registerHandler(RecvOpcode.USE_GACHA_EXP, new UseGachaExpHandler()); registerHandler(RecvOpcode.NEW_YEAR_CARD_REQUEST, new NewYearCardHandler()); diff --git a/src/net/opcodes/RecvOpcode.java b/src/net/opcodes/RecvOpcode.java index c6b5ba1517..15a2278227 100644 --- a/src/net/opcodes/RecvOpcode.java +++ b/src/net/opcodes/RecvOpcode.java @@ -144,6 +144,7 @@ public enum RecvOpcode { WEDDING_TALK(0x8B), WEDDING_TALK_MORE(0x8B), ALLIANCE_OPERATION(0x8F), + DENY_ALLIANCE_REQUEST(0x90), OPEN_FAMILY(0x92), ADD_FAMILY(0x93), ACCEPT_FAMILY(0x96), diff --git a/src/net/server/Server.java b/src/net/server/Server.java index ca0bdbbc9c..3632e139d0 100644 --- a/src/net/server/Server.java +++ b/src/net/server/Server.java @@ -60,6 +60,8 @@ import net.server.guild.MapleGuildCharacter; import net.server.worker.CharacterDiseaseWorker; import net.server.worker.CouponWorker; import net.server.worker.EventRecallCoordinatorWorker; +import net.server.worker.FredrickWorker; +import net.server.worker.InvitationWorker; import net.server.worker.LoginCoordinatorWorker; import net.server.worker.LoginStorageWorker; import net.server.worker.RankingCommandWorker; @@ -398,11 +400,12 @@ public class Server { int bossdroprate = getWorldProperty(p, "bossdroprate", i, ServerConstants.BOSS_DROP_RATE); int questrate = getWorldProperty(p, "questrate", i, ServerConstants.QUEST_RATE); int travelrate = getWorldProperty(p, "travelrate", i, ServerConstants.TRAVEL_RATE); + int fishingrate = getWorldProperty(p, "fishrate", i, ServerConstants.FISHING_RATE); World world = new World(i, Integer.parseInt(p.getProperty("flag" + i)), p.getProperty("eventmessage" + i), - exprate, droprate, bossdroprate, mesorate, questrate, travelrate); + exprate, droprate, bossdroprate, mesorate, questrate, travelrate, fishingrate); worldRecommendedList.add(new Pair<>(i, p.getProperty("whyamirecommended" + i))); worlds.add(world); @@ -915,6 +918,8 @@ public class Server { tMan.register(new LoginCoordinatorWorker(), 60 * 60 * 1000, timeLeft); tMan.register(new EventRecallCoordinatorWorker(), 60 * 60 * 1000, timeLeft); tMan.register(new LoginStorageWorker(), 2 * 60 * 1000, 2 * 60 * 1000); + tMan.register(new FredrickWorker(), 60 * 60 * 1000, 60 * 60 * 1000); + tMan.register(new InvitationWorker(), 30 * 1000, 30 * 1000); long timeToTake = System.currentTimeMillis(); SkillFactory.loadAllSkills(); diff --git a/src/net/server/channel/Channel.java b/src/net/server/channel/Channel.java index 2a84b75d1a..08b9aab87f 100644 --- a/src/net/server/channel/Channel.java +++ b/src/net/server/channel/Channel.java @@ -33,6 +33,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.WeakHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; @@ -89,18 +90,19 @@ public final class Channel { private String ip, serverMessage; private MapleMapFactory mapFactory; private EventScriptManager eventSM; - private MobStatusScheduler mobStatusSchedulers[] = new MobStatusScheduler[4]; - private MobAnimationScheduler mobAnimationSchedulers[] = new MobAnimationScheduler[4]; - private MobClearSkillScheduler mobClearSkillSchedulers[] = new MobClearSkillScheduler[4]; - private MobMistScheduler mobMistSchedulers[] = new MobMistScheduler[4]; - private FaceExpressionScheduler faceExpressionSchedulers[] = new FaceExpressionScheduler[4]; - private EventScheduler eventSchedulers[] = new EventScheduler[4]; - private OverallScheduler channelSchedulers[] = new OverallScheduler[4]; + private MobStatusScheduler mobStatusSchedulers[] = new MobStatusScheduler[ServerConstants.CHANNEL_LOCKS]; + private MobAnimationScheduler mobAnimationSchedulers[] = new MobAnimationScheduler[ServerConstants.CHANNEL_LOCKS]; + private MobClearSkillScheduler mobClearSkillSchedulers[] = new MobClearSkillScheduler[ServerConstants.CHANNEL_LOCKS]; + private MobMistScheduler mobMistSchedulers[] = new MobMistScheduler[ServerConstants.CHANNEL_LOCKS]; + private FaceExpressionScheduler faceExpressionSchedulers[] = new FaceExpressionScheduler[ServerConstants.CHANNEL_LOCKS]; + private EventScheduler eventSchedulers[] = new EventScheduler[ServerConstants.CHANNEL_LOCKS]; + private OverallScheduler channelSchedulers[] = new OverallScheduler[ServerConstants.CHANNEL_LOCKS]; private Map hiredMerchants = new HashMap<>(); private final Map storedVars = new HashMap<>(); private Set playersAway = new HashSet<>(); private List expeditions = new ArrayList<>(); private List expedType = new ArrayList<>(); + private Set ownedMaps = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap())); private MapleEvent event; private boolean finishedShutdown = false; private int usedDojo = 0; @@ -130,7 +132,7 @@ public final class Channel { private ReadLock merchRlock = merchantLock.readLock(); private WriteLock merchWlock = merchantLock.writeLock(); - private MonitoredReentrantLock faceLock[] = new MonitoredReentrantLock[4]; + private MonitoredReentrantLock faceLock[] = new MonitoredReentrantLock[ServerConstants.CHANNEL_LOCKS]; private MonitoredReentrantLock lock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHANNEL, true); @@ -168,7 +170,7 @@ public final class Channel { dojoTask[i] = null; } - for(int i = 0; i < 4; i++) { + for(int i = 0; i < ServerConstants.CHANNEL_LOCKS; i++) { faceLock[i] = MonitoredReentrantLockFactory.createLock(MonitoredLockType.CHANNEL_FACEEXPRS, true); mobStatusSchedulers[i] = new MobStatusScheduler(); @@ -235,7 +237,7 @@ public final class Channel { } } - for(int i = 0; i < 4; i++) { + for(int i = 0; i < ServerConstants.CHANNEL_LOCKS; i++) { if(mobStatusSchedulers[i] != null) { mobStatusSchedulers[i].dispose(); mobStatusSchedulers[i] = null; @@ -285,7 +287,7 @@ public final class Channel { } private void emptyLocks() { - for(int i = 0; i < 4; i++) { + for(int i = 0; i < ServerConstants.CHANNEL_LOCKS; i++) { faceLock[i] = faceLock[i].dispose(); } @@ -968,22 +970,33 @@ public final class Channel { } } - private static int getChannelSchedulerIndex(int mapid) { - if(mapid >= 250000000) { - if(mapid >= 900000000) { - return 3; - } else { - return 2; + public void registerOwnedMap(MapleMap map) { + ownedMaps.add(map); + } + + public void unregisterOwnedMap(MapleMap map) { + ownedMaps.remove(map); + } + + public void runCheckOwnedMapsSchedule() { + if (!ownedMaps.isEmpty()) { + List ownedMapsList; + + synchronized (ownedMaps) { + ownedMapsList = new ArrayList<>(ownedMaps); } - } else { - if(mapid >= 200000000) { - return 1; - } else { - return 0; + + for (MapleMap map : ownedMapsList) { + map.checkMapOwnerActivity(); } } } + private static int getChannelSchedulerIndex(int mapid) { + int section = 1000000000 / ServerConstants.CHANNEL_LOCKS; + return mapid / section; + } + public void registerMobStatus(int mapid, MonsterStatusEffect mse, Runnable cancelAction, long duration) { registerMobStatus(mapid, mse, cancelAction, duration, null, -1); } diff --git a/src/net/server/channel/handlers/AbstractDealDamageHandler.java b/src/net/server/channel/handlers/AbstractDealDamageHandler.java index 47aa1e9681..854b6f0979 100644 --- a/src/net/server/channel/handlers/AbstractDealDamageHandler.java +++ b/src/net/server/channel/handlers/AbstractDealDamageHandler.java @@ -136,6 +136,10 @@ public abstract class AbstractDealDamageHandler extends AbstractMaplePacketHandl } protected synchronized void applyAttack(AttackInfo attack, final MapleCharacter player, int attackCount) { + if (player.getMap().isOwnershipRestricted(player)) { + return; + } + Skill theSkill = null; MapleStatEffect attackEffect = null; final int job = player.getJob().getId(); diff --git a/src/net/server/channel/handlers/AdminCommandHandler.java b/src/net/server/channel/handlers/AdminCommandHandler.java index 2da805ecb6..8e440a44a4 100644 --- a/src/net/server/channel/handlers/AdminCommandHandler.java +++ b/src/net/server/channel/handlers/AdminCommandHandler.java @@ -132,7 +132,6 @@ public final class AdminCommandHandler extends AbstractMaplePacketHandler { MapleMonster monster = (MapleMonster) monsterx.get(x); if (monster.getId() == mobToKill) { c.getPlayer().getMap().killMonster(monster, c.getPlayer(), true); - //monster.giveExpToCharacter(c.getPlayer(), monster.getExp(), true, 1); already being done } } break; diff --git a/src/net/server/channel/handlers/AllianceOperationHandler.java b/src/net/server/channel/handlers/AllianceOperationHandler.java index d514b5d3fe..0c74c76142 100644 --- a/src/net/server/channel/handlers/AllianceOperationHandler.java +++ b/src/net/server/channel/handlers/AllianceOperationHandler.java @@ -61,7 +61,7 @@ public final class AllianceOperationHandler extends AbstractMaplePacketHandler { } } else { if (b == 4) { - chr.dropMessage("Your guild is already registered on a Guild Alliance."); + chr.dropMessage(5, "Your guild is already registered on a guild alliance."); c.announce(MaplePacketCreator.enableActions()); return; } @@ -88,27 +88,16 @@ public final class AllianceOperationHandler extends AbstractMaplePacketHandler { case 0x03: // Send Invite String guildName = slea.readMapleAsciiString(); - if(alliance.getGuilds().size() == alliance.getCapacity()) { - chr.dropMessage("Your alliance cannot comport any more guilds at the moment."); + if (alliance.getGuilds().size() == alliance.getCapacity()) { + chr.dropMessage(5, "Your alliance cannot comport any more guilds at the moment."); } else { - MapleGuild mg = Server.getInstance().getGuildByName(guildName); - if(mg == null) { - chr.dropMessage("The entered guild does not exist."); - } - else { - MapleCharacter victim = mg.getMGC(mg.getLeaderId()).getCharacter(); - - if (victim == null) { - chr.dropMessage("The master of the guild that you offered an invitation is currently not online."); - } else { - victim.getClient().announce(MaplePacketCreator.sendAllianceInvitation(alliance.getId(), chr)); - } - } + MapleAlliance.sendInvitation(c, guildName, alliance.getId()); } break; case 0x04: { // Accept Invite - if (chr.getGuild().getAllianceId() != 0 || chr.getGuildRank() != 1 || chr.getGuildId() < 1) { + MapleGuild guild = chr.getGuild(); + if (guild.getAllianceId() != 0 || chr.getGuildRank() != 1 || chr.getGuildId() < 1) { return; } @@ -120,6 +109,15 @@ public final class AllianceOperationHandler extends AbstractMaplePacketHandler { return; } + if (!MapleAlliance.answerInvitation(c.getPlayer().getId(), guild.getName(), alliance.getId(), true)) { + return; + } + + if (alliance.getGuilds().size() == alliance.getCapacity()) { + chr.dropMessage(5, "Your alliance cannot comport any more guilds at the moment."); + return; + } + int guildid = chr.getGuildId(); Server.getInstance().addGuildtoAlliance(alliance.getId(), guildid); @@ -132,7 +130,7 @@ public final class AllianceOperationHandler extends AbstractMaplePacketHandler { Server.getInstance().allianceMessage(alliance.getId(), MaplePacketCreator.addGuildToAlliance(alliance, guildid, c), -1, -1); Server.getInstance().allianceMessage(alliance.getId(), MaplePacketCreator.updateAllianceInfo(alliance, c.getWorld()), -1, -1); Server.getInstance().allianceMessage(alliance.getId(), MaplePacketCreator.allianceNotice(alliance.getId(), alliance.getNotice()), -1, -1); - chr.getGuild().dropMessage("Your guild has joined the [" + alliance.getName() + "] union."); + guild.dropMessage("Your guild has joined the [" + alliance.getName() + "] union."); break; } diff --git a/src/net/server/channel/handlers/CashOperationHandler.java b/src/net/server/channel/handlers/CashOperationHandler.java index 34fbcc7ecb..9ebb36ccb6 100644 --- a/src/net/server/channel/handlers/CashOperationHandler.java +++ b/src/net/server/channel/handlers/CashOperationHandler.java @@ -57,317 +57,325 @@ public final class CashOperationHandler extends AbstractMaplePacketHandler { return; } - final int action = slea.readByte(); - if (action == 0x03 || action == 0x1E) { - slea.readByte(); - final int useNX = slea.readInt(); - final int snCS = slea.readInt(); - CashItem cItem = CashItemFactory.getItem(snCS); - if (!canBuy(chr, cItem, cs.getCash(useNX))) { - FilePrinter.printError(FilePrinter.ITEM, "Denied to sell cash item with SN " + snCS); // preventing NPE here thanks to MedicOP - c.enableCSActions(); - return; - } - - if (action == 0x03) { // Item - if (ItemConstants.isCashStore(cItem.getItemId()) && chr.getLevel() < 16) { - c.enableCSActions(); - return; - } else if (ItemConstants.isRateCoupon(cItem.getItemId()) && !ServerConstants.USE_SUPPLY_RATE_COUPONS) { - chr.dropMessage(1, "Rate coupons are currently unavailable to purchase."); - c.enableCSActions(); - return; - } else if (ItemConstants.isMapleLife(cItem.getItemId()) && chr.getLevel() < 30) { - c.enableCSActions(); - return; - } - - Item item = cItem.toItem(); - cs.addToInventory(item); - c.announce(MaplePacketCreator.showBoughtCashItem(item, c.getAccID())); - } else { // Package - List cashPackage = CashItemFactory.getPackage(cItem.getItemId()); - for (Item item : cashPackage) { - cs.addToInventory(item); - } - c.announce(MaplePacketCreator.showBoughtCashPackage(cashPackage, c.getAccID())); - } - cs.gainCash(useNX, cItem, chr.getWorld()); - c.announce(MaplePacketCreator.showCash(chr)); - } else if (action == 0x04) {//TODO check for gender - int birthday = slea.readInt(); - CashItem cItem = CashItemFactory.getItem(slea.readInt()); - Map recipient = MapleCharacter.getCharacterFromDatabase(slea.readMapleAsciiString()); - String message = slea.readMapleAsciiString(); - if (!canBuy(chr, cItem, cs.getCash(4)) || message.length() < 1 || message.length() > 73) { - c.enableCSActions(); - return; - } - if (!checkBirthday(c, birthday)) { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC4)); - return; - } else if (recipient == null) { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xA9)); - return; - } else if (recipient.get("accountid").equals(String.valueOf(c.getAccID()))) { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xA8)); - return; - } - cs.gift(Integer.parseInt(recipient.get("id")), chr.getName(), message, cItem.getSN()); - c.announce(MaplePacketCreator.showGiftSucceed(recipient.get("name"), cItem)); - cs.gainCash(4, cItem, chr.getWorld()); - c.announce(MaplePacketCreator.showCash(chr)); + if (c.tryacquireClient()) { // thanks Thora for finding out an exploit within cash operations try { - chr.sendNote(recipient.get("name"), chr.getName() + " has sent you a gift! Go check out the Cash Shop.", (byte) 0); //fame or not - } catch (SQLException ex) { - ex.printStackTrace(); - } - MapleCharacter receiver = c.getChannelServer().getPlayerStorage().getCharacterByName(recipient.get("name")); - if (receiver != null) receiver.showNote(); - } else if (action == 0x05) { // Modify wish list - cs.clearWishList(); - for (byte i = 0; i < 10; i++) { - int sn = slea.readInt(); - CashItem cItem = CashItemFactory.getItem(sn); - if (cItem != null && cItem.isOnSale() && sn != 0) { - cs.addToWishList(sn); - } - } - c.announce(MaplePacketCreator.showWishList(chr, true)); - } else if (action == 0x06) { // Increase Inventory Slots - slea.skip(1); - int cash = slea.readInt(); - byte mode = slea.readByte(); - if (mode == 0) { - byte type = slea.readByte(); - if (cs.getCash(cash) < 4000) { - c.enableCSActions(); - return; - } - if (chr.gainSlots(type, 4, false)) { - c.announce(MaplePacketCreator.showBoughtInventorySlots(type, chr.getSlots(type))); - cs.gainCash(cash, -4000); - c.announce(MaplePacketCreator.showCash(chr)); - } - } else { - CashItem cItem = CashItemFactory.getItem(slea.readInt()); - int type = (cItem.getItemId() - 9110000) / 1000; - if (!canBuy(chr, cItem, cs.getCash(cash))) { - c.enableCSActions(); - return; - } - if (chr.gainSlots(type, 8, false)) { - c.announce(MaplePacketCreator.showBoughtInventorySlots(type, chr.getSlots(type))); - cs.gainCash(cash, cItem, chr.getWorld()); - c.announce(MaplePacketCreator.showCash(chr)); - } - } - } else if (action == 0x07) { // Increase Storage Slots - slea.skip(1); - int cash = slea.readInt(); - byte mode = slea.readByte(); - if (mode == 0) { - if (cs.getCash(cash) < 4000) { - c.enableCSActions(); - return; - } - if (chr.getStorage().gainSlots(4)) { - FilePrinter.print(FilePrinter.STORAGE + c.getAccountName() + ".txt", c.getPlayer().getName() + " bought 4 slots to their account storage."); - chr.setUsedStorage(); - - c.announce(MaplePacketCreator.showBoughtStorageSlots(chr.getStorage().getSlots())); - cs.gainCash(cash, -4000); - c.announce(MaplePacketCreator.showCash(chr)); - } - } else { - CashItem cItem = CashItemFactory.getItem(slea.readInt()); - - if (!canBuy(chr, cItem, cs.getCash(cash))) { - c.enableCSActions(); - return; - } - if (chr.getStorage().gainSlots(8)) { // thanks ABaldParrot & Thora for detecting storage issues here - FilePrinter.print(FilePrinter.STORAGE + c.getAccountName() + ".txt", c.getPlayer().getName() + " bought 8 slots to their account storage."); - chr.setUsedStorage(); - - c.announce(MaplePacketCreator.showBoughtStorageSlots(chr.getStorage().getSlots())); - cs.gainCash(cash, cItem, chr.getWorld()); - c.announce(MaplePacketCreator.showCash(chr)); - } - } - } else if (action == 0x08) { // Increase Character Slots - slea.skip(1); - int cash = slea.readInt(); - CashItem cItem = CashItemFactory.getItem(slea.readInt()); - - if (!canBuy(chr, cItem, cs.getCash(cash))) { - c.enableCSActions(); - return; - } - - if (c.gainCharacterSlot()) { - c.announce(MaplePacketCreator.showBoughtCharacterSlot(c.getCharacterSlots())); - cs.gainCash(cash, cItem, chr.getWorld()); - c.announce(MaplePacketCreator.showCash(chr)); - } else { - chr.dropMessage(1, "You have already used up all 12 extra character slots."); - c.enableCSActions(); - return; - } - } else if (action == 0x0D) { // Take from Cash Inventory - Item item = cs.findByCashId(slea.readInt()); - if (item == null) { - c.enableCSActions(); - return; - } - if (chr.getInventory(item.getInventoryType()).addItem(item) != -1) { - cs.removeFromInventory(item); - c.announce(MaplePacketCreator.takeFromCashInventory(item)); - - if(item instanceof Equip) { - Equip equip = (Equip) item; - if(equip.getRingId() >= 0) { - MapleRing ring = MapleRing.loadFromDb(equip.getRingId()); - chr.addPlayerRing(ring); - } - } - } - } else if (action == 0x0E) { // Put into Cash Inventory - int cashId = slea.readInt(); - slea.skip(4); - - byte invType = slea.readByte(); - if (invType < 1 || invType > 5) { - c.disconnect(false, false); - return; - } - - MapleInventory mi = chr.getInventory(MapleInventoryType.getByType(invType)); - Item item = mi.findByCashId(cashId); - if (item == null) { - c.enableCSActions(); - return; - } else if (c.getPlayer().getPetIndex(item.getPetId()) > -1) { - chr.getClient().announce(MaplePacketCreator.serverNotice(1, "You cannot put the pet you currently equip into the Cash Shop inventory.")); - c.enableCSActions(); - return; - } else if (ItemConstants.isWeddingRing(item.getItemId()) || ItemConstants.isWeddingToken(item.getItemId())) { - chr.getClient().announce(MaplePacketCreator.serverNotice(1, "You cannot put relationship items into the Cash Shop inventory.")); - c.enableCSActions(); - return; - } - cs.addToInventory(item); - mi.removeSlot(item.getPosition()); - c.announce(MaplePacketCreator.putIntoCashInventory(item, c.getAccID())); - } else if (action == 0x1D) { //crush ring (action 28) - int birthday = slea.readInt(); - if (checkBirthday(c, birthday)) { - int toCharge = slea.readInt(); - int SN = slea.readInt(); - String recipientName = slea.readMapleAsciiString(); - String text = slea.readMapleAsciiString(); - CashItem itemRing = CashItemFactory.getItem(SN); - MapleCharacter partner = c.getChannelServer().getPlayerStorage().getCharacterByName(recipientName); - if (partner == null) { - chr.getClient().announce(MaplePacketCreator.serverNotice(1, "The partner you specified cannot be found.\r\nPlease make sure your partner is online and in the same channel.")); - } else { - - /* if (partner.getGender() == chr.getGender()) { - chr.dropMessage("You and your partner are the same gender, please buy a friendship ring."); + final int action = slea.readByte(); + if (action == 0x03 || action == 0x1E) { + slea.readByte(); + final int useNX = slea.readInt(); + final int snCS = slea.readInt(); + CashItem cItem = CashItemFactory.getItem(snCS); + if (!canBuy(chr, cItem, cs.getCash(useNX))) { + FilePrinter.printError(FilePrinter.ITEM, "Denied to sell cash item with SN " + snCS); // preventing NPE here thanks to MedicOP c.enableCSActions(); return; - }*/ //Gotta let them faggots marry too, hence why this is commented out <3 - - if(itemRing.toItem() instanceof Equip) { - Equip eqp = (Equip) itemRing.toItem(); - Pair rings = MapleRing.createRing(itemRing.getItemId(), chr, partner); - eqp.setRingId(rings.getLeft()); - cs.addToInventory(eqp); - c.announce(MaplePacketCreator.showBoughtCashItem(eqp, c.getAccID())); - cs.gift(partner.getId(), chr.getName(), text, eqp.getSN(), rings.getRight()); - cs.gainCash(toCharge, itemRing, chr.getWorld()); - chr.addCrushRing(MapleRing.loadFromDb(rings.getLeft())); - try { - chr.sendNote(partner.getName(), text, (byte) 1); - } catch (SQLException ex) { - ex.printStackTrace(); - } - partner.showNote(); - } - } - } else { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC4)); - } - - c.announce(MaplePacketCreator.showCash(c.getPlayer())); - } else if (action == 0x20) { - int serialNumber = slea.readInt(); // thanks GabrielSin for detecting a potential exploit with 1 meso cash items. - if (serialNumber / 10000000 != 8) { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC0)); - return; - } - - CashItem item = CashItemFactory.getItem(serialNumber); - if (item == null || !item.isOnSale()) { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC0)); - return; - } - - int itemId = item.getItemId(); - int itemPrice = item.getPrice(); - if (itemPrice <= 0) { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC0)); - return; - } - - if (chr.getMeso() >= itemPrice) { - if (chr.canHold(itemId)) { - chr.gainMeso(-itemPrice, false); - MapleInventoryManipulator.addById(c, itemId, (short) 1, "", -1); - c.announce(MaplePacketCreator.showBoughtQuestItem(itemId)); - } - } - c.announce(MaplePacketCreator.showCash(c.getPlayer())); - } else if (action == 0x23) { //Friendship :3 - int birthday = slea.readInt(); - if (checkBirthday(c, birthday)) { - int payment = slea.readByte(); - slea.skip(3); //0s - int snID = slea.readInt(); - CashItem itemRing = CashItemFactory.getItem(snID); - String sentTo = slea.readMapleAsciiString(); - int available = slea.readShort() - 1; - String text = slea.readAsciiString(available); - slea.readByte(); - MapleCharacter partner = c.getChannelServer().getPlayerStorage().getCharacterByName(sentTo); - if (partner == null) { - chr.dropMessage("The partner you specified cannot be found.\r\nPlease make sure your partner is online and in the same channel."); - } else { - // Need to check to make sure its actually an equip and the right SN... - if(itemRing.toItem() instanceof Equip) { - Equip eqp = (Equip) itemRing.toItem(); - Pair rings = MapleRing.createRing(itemRing.getItemId(), chr, partner); - eqp.setRingId(rings.getLeft()); - cs.addToInventory(eqp); - c.announce(MaplePacketCreator.showBoughtCashItem(eqp, c.getAccID())); - cs.gift(partner.getId(), chr.getName(), text, eqp.getSN(), rings.getRight()); - cs.gainCash(payment, -itemRing.getPrice()); - chr.addFriendshipRing(MapleRing.loadFromDb(rings.getLeft())); - try { - chr.sendNote(partner.getName(), text, (byte) 1); - } catch (SQLException ex) { - ex.printStackTrace(); - } - partner.showNote(); } + + if (action == 0x03) { // Item + if (ItemConstants.isCashStore(cItem.getItemId()) && chr.getLevel() < 16) { + c.enableCSActions(); + return; + } else if (ItemConstants.isRateCoupon(cItem.getItemId()) && !ServerConstants.USE_SUPPLY_RATE_COUPONS) { + chr.dropMessage(1, "Rate coupons are currently unavailable to purchase."); + c.enableCSActions(); + return; + } else if (ItemConstants.isMapleLife(cItem.getItemId()) && chr.getLevel() < 30) { + c.enableCSActions(); + return; + } + + Item item = cItem.toItem(); + cs.addToInventory(item); + c.announce(MaplePacketCreator.showBoughtCashItem(item, c.getAccID())); + } else { // Package + List cashPackage = CashItemFactory.getPackage(cItem.getItemId()); + for (Item item : cashPackage) { + cs.addToInventory(item); + } + c.announce(MaplePacketCreator.showBoughtCashPackage(cashPackage, c.getAccID())); + } + cs.gainCash(useNX, cItem, chr.getWorld()); + c.announce(MaplePacketCreator.showCash(chr)); + } else if (action == 0x04) {//TODO check for gender + int birthday = slea.readInt(); + CashItem cItem = CashItemFactory.getItem(slea.readInt()); + Map recipient = MapleCharacter.getCharacterFromDatabase(slea.readMapleAsciiString()); + String message = slea.readMapleAsciiString(); + if (!canBuy(chr, cItem, cs.getCash(4)) || message.length() < 1 || message.length() > 73) { + c.enableCSActions(); + return; + } + if (!checkBirthday(c, birthday)) { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC4)); + return; + } else if (recipient == null) { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xA9)); + return; + } else if (recipient.get("accountid").equals(String.valueOf(c.getAccID()))) { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xA8)); + return; + } + cs.gift(Integer.parseInt(recipient.get("id")), chr.getName(), message, cItem.getSN()); + c.announce(MaplePacketCreator.showGiftSucceed(recipient.get("name"), cItem)); + cs.gainCash(4, cItem, chr.getWorld()); + c.announce(MaplePacketCreator.showCash(chr)); + try { + chr.sendNote(recipient.get("name"), chr.getName() + " has sent you a gift! Go check out the Cash Shop.", (byte) 0); //fame or not + } catch (SQLException ex) { + ex.printStackTrace(); + } + MapleCharacter receiver = c.getChannelServer().getPlayerStorage().getCharacterByName(recipient.get("name")); + if (receiver != null) receiver.showNote(); + } else if (action == 0x05) { // Modify wish list + cs.clearWishList(); + for (byte i = 0; i < 10; i++) { + int sn = slea.readInt(); + CashItem cItem = CashItemFactory.getItem(sn); + if (cItem != null && cItem.isOnSale() && sn != 0) { + cs.addToWishList(sn); + } + } + c.announce(MaplePacketCreator.showWishList(chr, true)); + } else if (action == 0x06) { // Increase Inventory Slots + slea.skip(1); + int cash = slea.readInt(); + byte mode = slea.readByte(); + if (mode == 0) { + byte type = slea.readByte(); + if (cs.getCash(cash) < 4000) { + c.enableCSActions(); + return; + } + if (chr.gainSlots(type, 4, false)) { + c.announce(MaplePacketCreator.showBoughtInventorySlots(type, chr.getSlots(type))); + cs.gainCash(cash, -4000); + c.announce(MaplePacketCreator.showCash(chr)); + } + } else { + CashItem cItem = CashItemFactory.getItem(slea.readInt()); + int type = (cItem.getItemId() - 9110000) / 1000; + if (!canBuy(chr, cItem, cs.getCash(cash))) { + c.enableCSActions(); + return; + } + if (chr.gainSlots(type, 8, false)) { + c.announce(MaplePacketCreator.showBoughtInventorySlots(type, chr.getSlots(type))); + cs.gainCash(cash, cItem, chr.getWorld()); + c.announce(MaplePacketCreator.showCash(chr)); + } + } + } else if (action == 0x07) { // Increase Storage Slots + slea.skip(1); + int cash = slea.readInt(); + byte mode = slea.readByte(); + if (mode == 0) { + if (cs.getCash(cash) < 4000) { + c.enableCSActions(); + return; + } + if (chr.getStorage().gainSlots(4)) { + FilePrinter.print(FilePrinter.STORAGE + c.getAccountName() + ".txt", c.getPlayer().getName() + " bought 4 slots to their account storage."); + chr.setUsedStorage(); + + c.announce(MaplePacketCreator.showBoughtStorageSlots(chr.getStorage().getSlots())); + cs.gainCash(cash, -4000); + c.announce(MaplePacketCreator.showCash(chr)); + } + } else { + CashItem cItem = CashItemFactory.getItem(slea.readInt()); + + if (!canBuy(chr, cItem, cs.getCash(cash))) { + c.enableCSActions(); + return; + } + if (chr.getStorage().gainSlots(8)) { // thanks ABaldParrot & Thora for detecting storage issues here + FilePrinter.print(FilePrinter.STORAGE + c.getAccountName() + ".txt", c.getPlayer().getName() + " bought 8 slots to their account storage."); + chr.setUsedStorage(); + + c.announce(MaplePacketCreator.showBoughtStorageSlots(chr.getStorage().getSlots())); + cs.gainCash(cash, cItem, chr.getWorld()); + c.announce(MaplePacketCreator.showCash(chr)); + } + } + } else if (action == 0x08) { // Increase Character Slots + slea.skip(1); + int cash = slea.readInt(); + CashItem cItem = CashItemFactory.getItem(slea.readInt()); + + if (!canBuy(chr, cItem, cs.getCash(cash))) { + c.enableCSActions(); + return; + } + + if (c.gainCharacterSlot()) { + c.announce(MaplePacketCreator.showBoughtCharacterSlot(c.getCharacterSlots())); + cs.gainCash(cash, cItem, chr.getWorld()); + c.announce(MaplePacketCreator.showCash(chr)); + } else { + chr.dropMessage(1, "You have already used up all 12 extra character slots."); + c.enableCSActions(); + return; + } + } else if (action == 0x0D) { // Take from Cash Inventory + Item item = cs.findByCashId(slea.readInt()); + if (item == null) { + c.enableCSActions(); + return; + } + if (chr.getInventory(item.getInventoryType()).addItem(item) != -1) { + cs.removeFromInventory(item); + c.announce(MaplePacketCreator.takeFromCashInventory(item)); + + if(item instanceof Equip) { + Equip equip = (Equip) item; + if(equip.getRingId() >= 0) { + MapleRing ring = MapleRing.loadFromDb(equip.getRingId()); + chr.addPlayerRing(ring); + } + } + } + } else if (action == 0x0E) { // Put into Cash Inventory + int cashId = slea.readInt(); + slea.skip(4); + + byte invType = slea.readByte(); + if (invType < 1 || invType > 5) { + c.disconnect(false, false); + return; + } + + MapleInventory mi = chr.getInventory(MapleInventoryType.getByType(invType)); + Item item = mi.findByCashId(cashId); + if (item == null) { + c.enableCSActions(); + return; + } else if (c.getPlayer().getPetIndex(item.getPetId()) > -1) { + chr.getClient().announce(MaplePacketCreator.serverNotice(1, "You cannot put the pet you currently equip into the Cash Shop inventory.")); + c.enableCSActions(); + return; + } else if (ItemConstants.isWeddingRing(item.getItemId()) || ItemConstants.isWeddingToken(item.getItemId())) { + chr.getClient().announce(MaplePacketCreator.serverNotice(1, "You cannot put relationship items into the Cash Shop inventory.")); + c.enableCSActions(); + return; + } + cs.addToInventory(item); + mi.removeSlot(item.getPosition()); + c.announce(MaplePacketCreator.putIntoCashInventory(item, c.getAccID())); + } else if (action == 0x1D) { //crush ring (action 28) + int birthday = slea.readInt(); + if (checkBirthday(c, birthday)) { + int toCharge = slea.readInt(); + int SN = slea.readInt(); + String recipientName = slea.readMapleAsciiString(); + String text = slea.readMapleAsciiString(); + CashItem itemRing = CashItemFactory.getItem(SN); + MapleCharacter partner = c.getChannelServer().getPlayerStorage().getCharacterByName(recipientName); + if (partner == null) { + chr.getClient().announce(MaplePacketCreator.serverNotice(1, "The partner you specified cannot be found.\r\nPlease make sure your partner is online and in the same channel.")); + } else { + + /* if (partner.getGender() == chr.getGender()) { + chr.dropMessage(5, "You and your partner are the same gender, please buy a friendship ring."); + c.enableCSActions(); + return; + }*/ //Gotta let them faggots marry too, hence why this is commented out <3 + + if(itemRing.toItem() instanceof Equip) { + Equip eqp = (Equip) itemRing.toItem(); + Pair rings = MapleRing.createRing(itemRing.getItemId(), chr, partner); + eqp.setRingId(rings.getLeft()); + cs.addToInventory(eqp); + c.announce(MaplePacketCreator.showBoughtCashItem(eqp, c.getAccID())); + cs.gift(partner.getId(), chr.getName(), text, eqp.getSN(), rings.getRight()); + cs.gainCash(toCharge, itemRing, chr.getWorld()); + chr.addCrushRing(MapleRing.loadFromDb(rings.getLeft())); + try { + chr.sendNote(partner.getName(), text, (byte) 1); + } catch (SQLException ex) { + ex.printStackTrace(); + } + partner.showNote(); + } + } + } else { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC4)); + } + + c.announce(MaplePacketCreator.showCash(c.getPlayer())); + } else if (action == 0x20) { + int serialNumber = slea.readInt(); // thanks GabrielSin for detecting a potential exploit with 1 meso cash items. + if (serialNumber / 10000000 != 8) { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC0)); + return; + } + + CashItem item = CashItemFactory.getItem(serialNumber); + if (item == null || !item.isOnSale()) { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC0)); + return; + } + + int itemId = item.getItemId(); + int itemPrice = item.getPrice(); + if (itemPrice <= 0) { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC0)); + return; + } + + if (chr.getMeso() >= itemPrice) { + if (chr.canHold(itemId)) { + chr.gainMeso(-itemPrice, false); + MapleInventoryManipulator.addById(c, itemId, (short) 1, "", -1); + c.announce(MaplePacketCreator.showBoughtQuestItem(itemId)); + } + } + c.announce(MaplePacketCreator.showCash(c.getPlayer())); + } else if (action == 0x23) { //Friendship :3 + int birthday = slea.readInt(); + if (checkBirthday(c, birthday)) { + int payment = slea.readByte(); + slea.skip(3); //0s + int snID = slea.readInt(); + CashItem itemRing = CashItemFactory.getItem(snID); + String sentTo = slea.readMapleAsciiString(); + int available = slea.readShort() - 1; + String text = slea.readAsciiString(available); + slea.readByte(); + MapleCharacter partner = c.getChannelServer().getPlayerStorage().getCharacterByName(sentTo); + if (partner == null) { + chr.dropMessage(5, "The partner you specified cannot be found. Please make sure your partner is online and in the same channel."); + } else { + // Need to check to make sure its actually an equip and the right SN... + if(itemRing.toItem() instanceof Equip) { + Equip eqp = (Equip) itemRing.toItem(); + Pair rings = MapleRing.createRing(itemRing.getItemId(), chr, partner); + eqp.setRingId(rings.getLeft()); + cs.addToInventory(eqp); + c.announce(MaplePacketCreator.showBoughtCashItem(eqp, c.getAccID())); + cs.gift(partner.getId(), chr.getName(), text, eqp.getSN(), rings.getRight()); + cs.gainCash(payment, -itemRing.getPrice()); + chr.addFriendshipRing(MapleRing.loadFromDb(rings.getLeft())); + try { + chr.sendNote(partner.getName(), text, (byte) 1); + } catch (SQLException ex) { + ex.printStackTrace(); + } + partner.showNote(); + } + } + } else { + c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC4)); + } + + c.announce(MaplePacketCreator.showCash(c.getPlayer())); + } else { + System.out.println("Unhandled action: " + action + "\n" + slea); } - } else { - c.announce(MaplePacketCreator.showCashShopMessage((byte) 0xC4)); + } finally { + c.releaseClient(); } - - c.announce(MaplePacketCreator.showCash(c.getPlayer())); } else { - System.out.println("Unhandled action: " + action + "\n" + slea); + c.announce(MaplePacketCreator.enableActions()); } } diff --git a/src/net/server/channel/handlers/ChangeMapHandler.java b/src/net/server/channel/handlers/ChangeMapHandler.java index 746716040a..8f2bd4b4d3 100644 --- a/src/net/server/channel/handlers/ChangeMapHandler.java +++ b/src/net/server/channel/handlers/ChangeMapHandler.java @@ -52,7 +52,7 @@ public final class ChangeMapHandler extends AbstractMaplePacketHandler { return; } if (chr.getTrade() != null) { - MapleTrade.cancelTrade(chr); + MapleTrade.cancelTrade(chr, MapleTrade.TradeResult.UNSUCCESSFUL_ANOTHER_MAP); } if (slea.available() == 0) { //Cash Shop :) if(!chr.getCashShop().isOpened()) { @@ -79,66 +79,70 @@ public final class ChangeMapHandler extends AbstractMaplePacketHandler { MaplePortal portal = chr.getMap().getPortal(startwp); slea.readByte(); boolean wheel = slea.readShort() > 0; - if (targetid != -1 && !chr.isAlive()) { - MapleMap map = chr.getMap(); - if (wheel && chr.haveItemWithId(5510000, false)) { - // thanks lucasziron for showing revivePlayer() also being triggered by Wheel - - MapleInventoryManipulator.removeById(c, MapleInventoryType.CASH, 5510000, 1, true, false); - chr.announce(MaplePacketCreator.showWheelsLeft(chr.getItemQuantity(5510000, false))); + + if (targetid != -1) { + if (!chr.isAlive()) { + MapleMap map = chr.getMap(); + if (wheel && chr.haveItemWithId(5510000, false)) { + // thanks lucasziron for showing revivePlayer() also being triggered by Wheel - chr.updateHp(50); - chr.changeMap(map, map.findClosestPlayerSpawnpoint(chr.getPosition())); + MapleInventoryManipulator.removeById(c, MapleInventoryType.CASH, 5510000, 1, true, false); + chr.announce(MaplePacketCreator.showWheelsLeft(chr.getItemQuantity(5510000, false))); + + chr.updateHp(50); + chr.changeMap(map, map.findClosestPlayerSpawnpoint(chr.getPosition())); + } else { + boolean executeStandardPath = true; + if (chr.getEventInstance() != null) { + executeStandardPath = chr.getEventInstance().revivePlayer(chr); + } + if (executeStandardPath) { + chr.respawn(map.getReturnMapId()); + } + } } else { - boolean executeStandardPath = true; - if (chr.getEventInstance() != null) { - executeStandardPath = chr.getEventInstance().revivePlayer(chr); - } - if (executeStandardPath) { - chr.respawn(map.getReturnMapId()); - } - } - } else if (targetid != -1) { - if(chr.isGM()) { - MapleMap to = chr.getWarpMap(targetid); - chr.changeMap(to, to.getPortal(0)); - } - else { - final int divi = chr.getMapId() / 100; - boolean warp = false; - if (divi == 0) { - if (targetid == 10000) { - warp = true; - } - } else if (divi == 20100) { - if (targetid == 104000000) { - c.announce(MaplePacketCreator.lockUI(false)); - c.announce(MaplePacketCreator.disableUI(false)); - warp = true; - } - } else if (divi == 9130401) { // Only allow warp if player is already in Intro map, or else = hack - if (targetid == 130000000 || targetid / 100 == 9130401) { // Cygnus introduction - warp = true; - } - } else if (divi == 9140900) { // Aran Introduction - if (targetid == 914090011 || targetid == 914090012 || targetid == 914090013 || targetid == 140090000) { - warp = true; - } - } else if (divi / 10 == 1020) { // Adventurer movie clip Intro - if (targetid == 1020000) { - warp = true; - } - } else if(divi / 10 >= 980040 && divi / 10 <= 980045) { - if(targetid == 980040000) { - warp = true; - } - } - if (warp) { - final MapleMap to = chr.getWarpMap(targetid); + if(chr.isGM()) { + MapleMap to = chr.getWarpMap(targetid); chr.changeMap(to, to.getPortal(0)); } + else { + final int divi = chr.getMapId() / 100; + boolean warp = false; + if (divi == 0) { + if (targetid == 10000) { + warp = true; + } + } else if (divi == 20100) { + if (targetid == 104000000) { + c.announce(MaplePacketCreator.lockUI(false)); + c.announce(MaplePacketCreator.disableUI(false)); + warp = true; + } + } else if (divi == 9130401) { // Only allow warp if player is already in Intro map, or else = hack + if (targetid == 130000000 || targetid / 100 == 9130401) { // Cygnus introduction + warp = true; + } + } else if (divi == 9140900) { // Aran Introduction + if (targetid == 914090011 || targetid == 914090012 || targetid == 914090013 || targetid == 140090000) { + warp = true; + } + } else if (divi / 10 == 1020) { // Adventurer movie clip Intro + if (targetid == 1020000) { + warp = true; + } + } else if(divi / 10 >= 980040 && divi / 10 <= 980045) { + if(targetid == 980040000) { + warp = true; + } + } + if (warp) { + final MapleMap to = chr.getWarpMap(targetid); + chr.changeMap(to, to.getPortal(0)); + } + } } - } + } + if (portal != null && !portal.getPortalStatus()) { c.announce(MaplePacketCreator.blockedMessage(1)); c.announce(MaplePacketCreator.enableActions()); diff --git a/src/net/server/channel/handlers/ChangeMapSpecialHandler.java b/src/net/server/channel/handlers/ChangeMapSpecialHandler.java index b376e7867e..fdafc0d709 100644 --- a/src/net/server/channel/handlers/ChangeMapSpecialHandler.java +++ b/src/net/server/channel/handlers/ChangeMapSpecialHandler.java @@ -25,6 +25,7 @@ import client.MapleClient; import net.AbstractMaplePacketHandler; import server.MaplePortal; import server.MapleTrade; +import server.MapleTrade.TradeResult; import tools.MaplePacketCreator; import tools.data.input.SeekableLittleEndianAccessor; @@ -44,7 +45,7 @@ public final class ChangeMapSpecialHandler extends AbstractMaplePacketHandler { return; } if (c.getPlayer().getTrade() != null) { - MapleTrade.cancelTrade(c.getPlayer()); + MapleTrade.cancelTrade(c.getPlayer(), TradeResult.UNSUCCESSFUL_ANOTHER_MAP); } portal.enterPortal(c); } diff --git a/src/net/server/channel/handlers/CloseChalkboardHandler.java b/src/net/server/channel/handlers/CloseChalkboardHandler.java index 8489d44316..497660d247 100644 --- a/src/net/server/channel/handlers/CloseChalkboardHandler.java +++ b/src/net/server/channel/handlers/CloseChalkboardHandler.java @@ -31,6 +31,8 @@ import tools.data.input.SeekableLittleEndianAccessor; * @author Xterminator */ public final class CloseChalkboardHandler extends AbstractMaplePacketHandler { + + @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { c.getPlayer().setChalkboard(null); c.getPlayer().getMap().broadcastMessage(MaplePacketCreator.useChalkboard(c.getPlayer(), true)); diff --git a/src/net/server/channel/handlers/DenyAllianceRequestHandler.java b/src/net/server/channel/handlers/DenyAllianceRequestHandler.java new file mode 100644 index 0000000000..bf5c63d771 --- /dev/null +++ b/src/net/server/channel/handlers/DenyAllianceRequestHandler.java @@ -0,0 +1,47 @@ +/* + 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.channel.handlers; + +import client.MapleClient; +import client.MapleCharacter; +import net.AbstractMaplePacketHandler; +import net.server.guild.MapleAlliance; +import tools.data.input.SeekableLittleEndianAccessor; + +/** + * @author Ronan + */ +public final class DenyAllianceRequestHandler extends AbstractMaplePacketHandler { + + @Override + public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { + slea.readByte(); + String inviterName = slea.readMapleAsciiString(); + String guildName = slea.readMapleAsciiString(); + + MapleCharacter chr = c.getWorldServer().getPlayerStorage().getCharacterByName(inviterName); + if (chr != null) { + MapleAlliance alliance = chr.getAlliance(); + if (alliance != null) { + MapleAlliance.answerInvitation(c.getPlayer().getId(), guildName, alliance.getId(), false); + } + } + } +} \ No newline at end of file diff --git a/src/net/server/channel/handlers/DenyGuildRequestHandler.java b/src/net/server/channel/handlers/DenyGuildRequestHandler.java index f37ad4829e..e68e116c15 100644 --- a/src/net/server/channel/handlers/DenyGuildRequestHandler.java +++ b/src/net/server/channel/handlers/DenyGuildRequestHandler.java @@ -24,7 +24,7 @@ package net.server.channel.handlers; import client.MapleCharacter; import client.MapleClient; import net.AbstractMaplePacketHandler; -import tools.MaplePacketCreator; +import net.server.guild.MapleGuild; import tools.data.input.SeekableLittleEndianAccessor; /** @@ -32,11 +32,13 @@ import tools.data.input.SeekableLittleEndianAccessor; * @author Xterminator */ public final class DenyGuildRequestHandler extends AbstractMaplePacketHandler { + + @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { slea.readByte(); - MapleCharacter cfrom = c.getChannelServer().getPlayerStorage().getCharacterByName(slea.readMapleAsciiString()); + MapleCharacter cfrom = c.getWorldServer().getPlayerStorage().getCharacterByName(slea.readMapleAsciiString()); if (cfrom != null) { - cfrom.getClient().announce(MaplePacketCreator.denyGuildInvitation(c.getPlayer().getName())); + MapleGuild.answerInvitation(c.getPlayer().getId(), c.getPlayer().getName(), cfrom.getGuildId(), false); } } } diff --git a/src/net/server/channel/handlers/DenyPartyRequestHandler.java b/src/net/server/channel/handlers/DenyPartyRequestHandler.java index b5135c873a..19b18d84a3 100644 --- a/src/net/server/channel/handlers/DenyPartyRequestHandler.java +++ b/src/net/server/channel/handlers/DenyPartyRequestHandler.java @@ -24,15 +24,22 @@ package net.server.channel.handlers; import client.MapleCharacter; import client.MapleClient; import net.AbstractMaplePacketHandler; +import net.server.coordinator.MapleInviteCoordinator; +import net.server.coordinator.MapleInviteCoordinator.InviteResult; +import net.server.coordinator.MapleInviteCoordinator.InviteType; import tools.MaplePacketCreator; import tools.data.input.SeekableLittleEndianAccessor; public final class DenyPartyRequestHandler extends AbstractMaplePacketHandler { + + @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { slea.readByte(); MapleCharacter cfrom = c.getChannelServer().getPlayerStorage().getCharacterByName(slea.readMapleAsciiString()); if (cfrom != null) { - cfrom.getClient().announce(MaplePacketCreator.partyStatusMessage(23, c.getPlayer().getName())); + if (MapleInviteCoordinator.answerInvite(InviteType.PARTY, c.getPlayer().getId(), cfrom.getPartyId(), false).getLeft() == InviteResult.DENIED) { + cfrom.getClient().announce(MaplePacketCreator.partyStatusMessage(23, c.getPlayer().getName())); + } } } } diff --git a/src/net/server/channel/handlers/EnterCashShopHandler.java b/src/net/server/channel/handlers/EnterCashShopHandler.java index 5898f96d68..f58b1a0d4a 100644 --- a/src/net/server/channel/handlers/EnterCashShopHandler.java +++ b/src/net/server/channel/handlers/EnterCashShopHandler.java @@ -68,6 +68,7 @@ public class EnterCashShopHandler extends AbstractMaplePacketHandler { Server.getInstance().getPlayerBuffStorage().addDiseasesToStorage(mc.getId(), mc.getAllDiseases()); mc.setAwayFromChannelWorld(); mc.notifyMapTransferToPartner(-1); + mc.removeIncomingInvites(); mc.cancelAllBuffs(true); mc.cancelAllDebuffs(); mc.cancelBuffExpireTask(); diff --git a/src/net/server/channel/handlers/EnterMTSHandler.java b/src/net/server/channel/handlers/EnterMTSHandler.java index 56a2cbeff5..4bab7b410f 100644 --- a/src/net/server/channel/handlers/EnterMTSHandler.java +++ b/src/net/server/channel/handlers/EnterMTSHandler.java @@ -93,6 +93,7 @@ public final class EnterMTSHandler extends AbstractMaplePacketHandler { Server.getInstance().getPlayerBuffStorage().addDiseasesToStorage(chr.getId(), chr.getAllDiseases()); chr.setAwayFromChannelWorld(); chr.notifyMapTransferToPartner(-1); + chr.removeIncomingInvites(); chr.cancelAllBuffs(true); chr.cancelAllDebuffs(); chr.cancelBuffExpireTask(); diff --git a/src/net/server/channel/handlers/GuildOperationHandler.java b/src/net/server/channel/handlers/GuildOperationHandler.java index 92483e8c51..20bce7afb5 100644 --- a/src/net/server/channel/handlers/GuildOperationHandler.java +++ b/src/net/server/channel/handlers/GuildOperationHandler.java @@ -28,7 +28,6 @@ import constants.ServerConstants; import client.MapleClient; import net.AbstractMaplePacketHandler; import tools.data.input.SeekableLittleEndianAccessor; -import java.util.Iterator; import tools.MaplePacketCreator; import client.MapleCharacter; import net.server.Server; @@ -47,50 +46,8 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { return true; } - private class Invited { - public String name; - public int gid; - public long expiration; - - public Invited(String n, int id) { - name = n.toLowerCase(); - gid = id; - expiration = currentServerTime() + 60 * 60 * 1000; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Invited)) { - return false; - } - Invited oth = (Invited) other; - return (gid == oth.gid && name.equals(oth)); - } - - @Override - public int hashCode() { - int hash = 3; - hash = 83 * hash + (this.name != null ? this.name.hashCode() : 0); - hash = 83 * hash + this.gid; - return hash; - } - } - private java.util.List invited = new java.util.LinkedList(); - private long nextPruneTime = currentServerTime() + 20 * 60 * 1000; - @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { - if (currentServerTime() >= nextPruneTime) { - Iterator itr = invited.iterator(); - Invited inv; - while (itr.hasNext()) { - inv = itr.next(); - if (currentServerTime() >= inv.expiration) { - itr.remove(); - } - } - nextPruneTime = currentServerTime() + 20 * 60 * 1000; - } MapleCharacter mc = c.getPlayer(); byte type = slea.readByte(); int allianceId = -1; @@ -134,16 +91,13 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { if (mc.getGuildId() <= 0 || mc.getGuildRank() > 2) { return; } - String name = slea.readMapleAsciiString(); - MapleGuildResponse mgr = MapleGuild.sendInvite(c, name); + + String targetName = slea.readMapleAsciiString(); + MapleGuildResponse mgr = MapleGuild.sendInvitation(c, targetName); if (mgr != null) { - c.announce(mgr.getPacket()); - } else { - Invited inv = new Invited(name, mc.getGuildId()); - if (!invited.contains(inv)) { - invited.add(inv); - } - } + c.announce(mgr.getPacket(targetName)); + } else {} // already sent invitation, do nothing + break; case 0x06: if (mc.getGuildId() > 0) { @@ -156,28 +110,18 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { System.out.println("[hax] " + mc.getName() + " attempted to join a guild with a different character id."); return; } - name = mc.getName().toLowerCase(); - Iterator itr = invited.iterator(); - boolean bOnList = false; - while (itr.hasNext()) { - Invited inv = itr.next(); - if (gid == inv.gid && name.equals(inv.name)) { - bOnList = true; - itr.remove(); - break; - } - } - if (!bOnList) { - System.out.println("[hax] " + mc.getName() + " is trying to join a guild that never invited him/her (or that the invitation has expired)"); + + if (!MapleGuild.answerInvitation(cid, mc.getName(), gid, true)) { return; } + mc.getMGC().setGuildId(gid); // joins the guild mc.getMGC().setGuildRank(5); // start at lowest rank mc.getMGC().setAllianceRank(5); int s = Server.getInstance().addGuildMember(mc.getMGC(), mc); if (s == 0) { - c.getPlayer().dropMessage(1, "The Guild you are trying to join is already full."); + c.getPlayer().dropMessage(1, "The guild you are trying to join is already full."); mc.getMGC().setGuildId(0); return; } @@ -193,7 +137,7 @@ public final class GuildOperationHandler extends AbstractMaplePacketHandler { break; case 0x07: cid = slea.readInt(); - name = slea.readMapleAsciiString(); + String name = slea.readMapleAsciiString(); if (cid != mc.getId() || !name.equals(mc.getName()) || mc.getGuildId() <= 0) { System.out.println("[hax] " + mc.getName() + " tried to quit guild under the name \"" + name + "\" and current guild id of " + mc.getGuildId() + "."); return; diff --git a/src/net/server/channel/handlers/InventorySortHandler.java b/src/net/server/channel/handlers/InventorySortHandler.java index 137cfcd0c7..c48a8d9390 100644 --- a/src/net/server/channel/handlers/InventorySortHandler.java +++ b/src/net/server/channel/handlers/InventorySortHandler.java @@ -72,6 +72,28 @@ class PairedQuicksort { } while (i <= j); } + private void PartitionByItemIdReverse(int Esq, int Dir, ArrayList A) { + Item x, w; + + i = Esq; + j = Dir; + + x = A.get((i + j) / 2); + do { + while (x.getItemId() < A.get(i).getItemId()) i++; + while (x.getItemId() > A.get(j).getItemId()) j--; + + if (i <= j) { + w = A.get(i); + A.set(i, A.get(j)); + A.set(j, w); + + i++; + j--; + } + } while (i <= j); + } + private void PartitionByName(int Esq, int Dir, ArrayList A) { Item x, w; @@ -163,10 +185,64 @@ class PairedQuicksort { if (i < Dir) MapleQuicksort(i, Dir, A, sort); } + private static int getItemSubtype(Item it) { + return it.getItemId() / 10000; + } + + private int[] BinarySearchElement(ArrayList A, int rangeId) { + int st = 0, en = A.size() - 1; + + int mid = -1, idx = -1; + while (en >= st) { + idx = (st + en) / 2; + mid = getItemSubtype(A.get(idx)); + + if (mid == rangeId) { + break; + } else if (mid < rangeId) { + st = idx + 1; + } else { + en = idx - 1; + } + } + + if (en < st) { + return null; + } + + st = idx - 1; + en = idx + 1; + while (st >= 0 && getItemSubtype(A.get(st)) == rangeId) { + st -= 1; + } + st += 1; + + while (en < A.size() && getItemSubtype(A.get(en)) == rangeId) { + en += 1; + } + en -= 1; + + return new int[]{st, en}; + } + + public void reverseSortSublist(ArrayList A, int[] range) { + if (range != null) { + PartitionByItemIdReverse(range[0], range[1], A); + } + } + public PairedQuicksort(ArrayList A, int primarySort, int secondarySort) { intersect = new ArrayList<>(); - if(A.size() > 0) MapleQuicksort(0, A.size() - 1, A, primarySort); + if(A.size() > 0) { + MapleQuicksort(0, A.size() - 1, A, primarySort); + + if (A.get(0).getInventoryType().equals(MapleInventoryType.USE)) { // thanks KDA & Vcoc for suggesting stronger projectiles coming before weaker ones + reverseSortSublist(A, BinarySearchElement(A, 206)); // arrows + reverseSortSublist(A, BinarySearchElement(A, 207)); // stars + reverseSortSublist(A, BinarySearchElement(A, 233)); // bullets + } + } intersect.add(0); for(int ind = 1; ind < A.size(); ind++) { diff --git a/src/net/server/channel/handlers/MesoDropHandler.java b/src/net/server/channel/handlers/MesoDropHandler.java index 878303ccdc..bed3e23e7b 100644 --- a/src/net/server/channel/handlers/MesoDropHandler.java +++ b/src/net/server/channel/handlers/MesoDropHandler.java @@ -47,7 +47,12 @@ public final class MesoDropHandler extends AbstractMaplePacketHandler { int meso = slea.readInt(); if (meso <= player.getMeso() && meso > 9 && meso < 50001) { player.gainMeso(-meso, false, true, false); - player.getMap().spawnMesoDrop(meso, player.getPosition(), player, player, true, (byte) 2); + + if (player.attemptCatchFish(meso)) { + player.getMap().disappearingMesoDrop(meso, player, player, player.getPosition()); + } else { + player.getMap().spawnMesoDrop(meso, player.getPosition(), player, player, true, (byte) 2); + } } } } \ No newline at end of file diff --git a/src/net/server/channel/handlers/MessengerHandler.java b/src/net/server/channel/handlers/MessengerHandler.java index 941b72f445..1d652a77a0 100644 --- a/src/net/server/channel/handlers/MessengerHandler.java +++ b/src/net/server/channel/handlers/MessengerHandler.java @@ -24,84 +24,104 @@ package net.server.channel.handlers; import client.MapleCharacter; import client.MapleClient; import net.AbstractMaplePacketHandler; +import net.server.coordinator.MapleInviteCoordinator; +import net.server.coordinator.MapleInviteCoordinator.InviteResult; +import net.server.coordinator.MapleInviteCoordinator.InviteType; import net.server.world.MapleMessenger; import net.server.world.MapleMessengerCharacter; import net.server.world.World; import tools.MaplePacketCreator; +import tools.Pair; import tools.data.input.SeekableLittleEndianAccessor; public final class MessengerHandler extends AbstractMaplePacketHandler { @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { - String input; - byte mode = slea.readByte(); - MapleCharacter player = c.getPlayer(); - World world = c.getWorldServer(); - MapleMessenger messenger = player.getMessenger(); - switch (mode) { - case 0x00: - if (messenger == null) { + c.tryacquireClient(); + try { + String input; + byte mode = slea.readByte(); + MapleCharacter player = c.getPlayer(); + World world = c.getWorldServer(); + MapleMessenger messenger = player.getMessenger(); + switch (mode) { + case 0x00: int messengerid = slea.readInt(); - if (messengerid == 0) { - MapleMessengerCharacter messengerplayer = new MapleMessengerCharacter(player, 0); - messenger = world.createMessenger(messengerplayer); - player.setMessenger(messenger); - player.setMessengerPosition(0); - } else { - messenger = world.getMessenger(messengerid); - int position = messenger.getLowestPosition(); - MapleMessengerCharacter messengerplayer = new MapleMessengerCharacter(player, position); - if (messenger.getMembers().size() < 3) { + if (messenger == null) { + if (messengerid == 0) { + MapleInviteCoordinator.removeInvite(InviteType.MESSENGER, player.getId()); + + MapleMessengerCharacter messengerplayer = new MapleMessengerCharacter(player, 0); + messenger = world.createMessenger(messengerplayer); player.setMessenger(messenger); - player.setMessengerPosition(position); - world.joinMessenger(messenger.getId(), messengerplayer, player.getName(), messengerplayer.getChannel()); - } - } - } - break; - case 0x02: - player.closePlayerMessenger(); - break; - case 0x03: - if (messenger.getMembers().size() < 3) { - input = slea.readMapleAsciiString(); - MapleCharacter target = c.getChannelServer().getPlayerStorage().getCharacterByName(input); - if (target != null) { - if (target.getMessenger() == null) { - target.getClient().announce(MaplePacketCreator.messengerInvite(c.getPlayer().getName(), messenger.getId())); - c.announce(MaplePacketCreator.messengerNote(input, 4, 1)); + player.setMessengerPosition(0); } else { - c.announce(MaplePacketCreator.messengerChat(player.getName() + " : " + input + " is already using Maple Messenger")); + messenger = world.getMessenger(messengerid); + if (messenger != null) { + Pair inviteRes = MapleInviteCoordinator.answerInvite(InviteType.MESSENGER, player.getId(), messengerid, true); + InviteResult res = inviteRes.getLeft(); + if (res == InviteResult.ACCEPTED) { + int position = messenger.getLowestPosition(); + MapleMessengerCharacter messengerplayer = new MapleMessengerCharacter(player, position); + if (messenger.getMembers().size() < 3) { + player.setMessenger(messenger); + player.setMessengerPosition(position); + world.joinMessenger(messenger.getId(), messengerplayer, player.getName(), messengerplayer.getChannel()); + } + } else { + player.message("Could not verify your Maple Messenger accept since the invitation rescinded."); + } + } } } else { - if (world.find(input) > -1) { - world.messengerInvite(c.getPlayer().getName(), messenger.getId(), input, c.getChannel()); + MapleInviteCoordinator.answerInvite(InviteType.MESSENGER, player.getId(), messengerid, false); + } + break; + case 0x02: + player.closePlayerMessenger(); + break; + case 0x03: + if (messenger == null) { + c.announce(MaplePacketCreator.messengerChat(player.getName() + " : This Maple Messenger is currently unavailable. Please quit this chat.")); + } else if (messenger.getMembers().size() < 3) { + input = slea.readMapleAsciiString(); + MapleCharacter target = c.getChannelServer().getPlayerStorage().getCharacterByName(input); + if (target != null) { + if (target.getMessenger() == null) { + if (MapleInviteCoordinator.createInvite(InviteType.MESSENGER, c.getPlayer(), messenger.getId(), target.getId())) { + target.getClient().announce(MaplePacketCreator.messengerInvite(c.getPlayer().getName(), messenger.getId())); + c.announce(MaplePacketCreator.messengerNote(input, 4, 1)); + } else { + c.announce(MaplePacketCreator.messengerChat(player.getName() + " : " + input + " is already managing a Maple Messenger invitation")); + } + } else { + c.announce(MaplePacketCreator.messengerChat(player.getName() + " : " + input + " is already using Maple Messenger")); + } } else { - c.announce(MaplePacketCreator.messengerNote(input, 4, 0)); + if (world.find(input) > -1) { + world.messengerInvite(c.getPlayer().getName(), messenger.getId(), input, c.getChannel()); + } else { + c.announce(MaplePacketCreator.messengerNote(input, 4, 0)); + } } + } else { + c.announce(MaplePacketCreator.messengerChat(player.getName() + " : You cannot have more than 3 people in the Maple Messenger")); } - } else { - c.announce(MaplePacketCreator.messengerChat(player.getName() + " : You cannot have more than 3 people in the Maple Messenger")); - } - break; - case 0x05: - String targeted = slea.readMapleAsciiString(); - MapleCharacter target = c.getChannelServer().getPlayerStorage().getCharacterByName(targeted); - if (target != null) { - if (target.getMessenger() != null) { - target.getClient().announce(MaplePacketCreator.messengerNote(player.getName(), 5, 0)); + break; + case 0x05: + String targeted = slea.readMapleAsciiString(); + world.declineChat(targeted, player); + break; + case 0x06: + if (messenger != null) { + MapleMessengerCharacter messengerplayer = new MapleMessengerCharacter(player, player.getMessengerPosition()); + input = slea.readMapleAsciiString(); + world.messengerChat(messenger, input, messengerplayer.getName()); } - } else { - world.declineChat(targeted, player.getName()); - } - break; - case 0x06: - if (messenger != null) { - MapleMessengerCharacter messengerplayer = new MapleMessengerCharacter(player, player.getMessengerPosition()); - input = slea.readMapleAsciiString(); - world.messengerChat(messenger, input, messengerplayer.getName()); - } - break; + break; + } + } finally { + c.releaseClient(); } } } diff --git a/src/net/server/channel/handlers/PartyOperationHandler.java b/src/net/server/channel/handlers/PartyOperationHandler.java index 95727de6c3..cf7530c9b9 100644 --- a/src/net/server/channel/handlers/PartyOperationHandler.java +++ b/src/net/server/channel/handlers/PartyOperationHandler.java @@ -31,9 +31,14 @@ import tools.data.input.SeekableLittleEndianAccessor; import client.MapleCharacter; import client.MapleClient; import constants.ServerConstants; -import java.util.List; +import net.server.coordinator.MapleInviteCoordinator; +import net.server.coordinator.MapleInviteCoordinator.InviteResult; +import net.server.coordinator.MapleInviteCoordinator.InviteType; import scripting.event.EventInstanceManager; import server.maps.MapleMap; +import tools.Pair; + +import java.util.List; public final class PartyOperationHandler extends AbstractMaplePacketHandler { @@ -101,26 +106,33 @@ public final class PartyOperationHandler extends AbstractMaplePacketHandler { } case 3: { // join int partyid = slea.readInt(); - if (party == null) { - party = world.getParty(partyid); - if (party != null) { - if (party.getMembers().size() < 6) { - partyplayer = new MaplePartyCharacter(player); - player.getMap().addPartyMember(player); + + Pair inviteRes = MapleInviteCoordinator.answerInvite(InviteType.PARTY, player.getId(), partyid, true); + InviteResult res = inviteRes.getLeft(); + if (res == InviteResult.ACCEPTED) { + if (party == null) { + party = world.getParty(partyid); + if (party != null) { + if (party.getMembers().size() < 6) { + partyplayer = new MaplePartyCharacter(player); + player.getMap().addPartyMember(player); - world.updateParty(party.getId(), PartyOperation.JOIN, partyplayer); - player.receivePartyMemberHP(); - player.updatePartyMemberHP(); - - player.partyOperationUpdate(party, null); + world.updateParty(party.getId(), PartyOperation.JOIN, partyplayer); + player.receivePartyMemberHP(); + player.updatePartyMemberHP(); + + player.partyOperationUpdate(party, null); + } else { + c.announce(MaplePacketCreator.partyStatusMessage(17)); + } } else { - c.announce(MaplePacketCreator.partyStatusMessage(17)); + c.announce(MaplePacketCreator.serverNotice(5, "The person you have invited to the party is already in one.")); } } else { - c.announce(MaplePacketCreator.serverNotice(5, "The person you have invited to the party is already in one.")); + c.announce(MaplePacketCreator.serverNotice(5, "You can't join the party as you are already in one.")); } } else { - c.announce(MaplePacketCreator.serverNotice(5, "You can't join the party as you are already in one.")); + c.announce(MaplePacketCreator.serverNotice(5, "You couldn't join the party due to an expired invitation request.")); } break; } @@ -154,7 +166,11 @@ public final class PartyOperationHandler extends AbstractMaplePacketHandler { c.announce(MaplePacketCreator.partyCreated(party, partyplayer.getId())); } if (party.getMembers().size() < 6) { - invited.getClient().announce(MaplePacketCreator.partyInvite(player)); + if (MapleInviteCoordinator.createInvite(InviteType.PARTY, player, party.getId(), invited.getId())) { + invited.getClient().announce(MaplePacketCreator.partyInvite(player)); + } else { + c.announce(MaplePacketCreator.partyStatusMessage(22, invited.getName())); + } } else { c.announce(MaplePacketCreator.partyStatusMessage(17)); } diff --git a/src/net/server/channel/handlers/PlayerInteractionHandler.java b/src/net/server/channel/handlers/PlayerInteractionHandler.java index 0f74a86b60..32a13008f0 100644 --- a/src/net/server/channel/handlers/PlayerInteractionHandler.java +++ b/src/net/server/channel/handlers/PlayerInteractionHandler.java @@ -36,7 +36,6 @@ 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; @@ -48,6 +47,9 @@ import tools.FilePrinter; import tools.MaplePacketCreator; import tools.data.input.SeekableLittleEndianAccessor; +import java.awt.Point; +import java.sql.SQLException; + /** * * @author Matze @@ -146,7 +148,7 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { } byte createType = slea.readByte(); - if (createType == 3) {// trade + if (createType == 3) { // trade MapleTrade.startTrade(chr); } else if (createType == 1) { // omok mini game int status = establishMiniroomStatus(chr, true); @@ -234,7 +236,15 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { chr.getClient().announce(MaplePacketCreator.getMiniRoomError(status)); return; } - + + try { + Point cpos = chr.getPosition(); + if (chr.getMap().findClosestWarpPortal(cpos).getPosition().distance(cpos) < 120.0) { + chr.getClient().announce(MaplePacketCreator.getMiniRoomError(10)); + return; + } + } catch (NullPointerException npe) {} + String desc = slea.readMapleAsciiString(); slea.skip(3); int itemId = slea.readInt(); @@ -259,20 +269,22 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { } } } else if (mode == Action.INVITE.getCode()) { - int otherPlayer = slea.readInt(); - if (chr.getId() == chr.getMap().getCharacterById(otherPlayer).getId()) { + int otherCid = slea.readInt(); + MapleCharacter other = chr.getMap().getCharacterById(otherCid); + if (other == null || chr.getId() == other.getId()) { return; } - MapleTrade.inviteTrade(chr, chr.getMap().getCharacterById(otherPlayer)); + + MapleTrade.inviteTrade(chr, other); } else if (mode == Action.DECLINE.getCode()) { MapleTrade.declineTrade(chr); } else if (mode == Action.VISIT.getCode()) { if (chr.getTrade() != null && chr.getTrade().getPartner() != null) { if (!chr.getTrade().isFullTrade() && !chr.getTrade().getPartner().isFullTrade()) { - MapleTrade.visitTrade(chr, chr.getTrade().getPartner().getChr()); + MapleTrade.visitTrade(chr, chr.getTrade().getPartner().getChr()); } else { - chr.getClient().announce(MaplePacketCreator.getMiniRoomError(2)); - return; + chr.getClient().announce(MaplePacketCreator.getMiniRoomError(2)); + return; } } else { if (isTradeOpen(chr)) return; @@ -329,7 +341,7 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { } } else if (mode == Action.EXIT.getCode()) { if (chr.getTrade() != null) { - MapleTrade.cancelTrade(chr); + MapleTrade.cancelTrade(chr, MapleTrade.TradeResult.PARTNER_CANCEL); } else { chr.closePlayerShop(); chr.closeMiniGame(); @@ -627,6 +639,10 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { c.announce(MaplePacketCreator.updateHiredMerchant(merchant, chr)); + if (ServerConstants.USE_ENFORCE_MERCHANT_SAVE) { + chr.saveCharToDB(false); + } + try { merchant.saveItems(false); // thanks Masterrulax for realizing yet another dupe with merchants/Fredrick } catch (SQLException ex) { @@ -726,14 +742,14 @@ public final class PlayerInteractionHandler extends AbstractMaplePacketHandler { MapleHiredMerchant merchant = chr.getHiredMerchant(); if(merchant != null) { - if (merchant.getItems().isEmpty() && merchant.isOwner(chr)) { - merchant.closeShop(c, false); - chr.setHasMerchant(false); - } if (merchant.isOwner(chr)) { - merchant.clearMessages(); - merchant.setOpen(true); - merchant.getMap().broadcastMessage(MaplePacketCreator.updateHiredMerchantBox(merchant)); + if (merchant.getItems().isEmpty()) { + merchant.closeOwnerMerchant(chr); + } else { + merchant.clearMessages(); + merchant.setOpen(true); + merchant.getMap().broadcastMessage(MaplePacketCreator.updateHiredMerchantBox(merchant)); + } } } diff --git a/src/net/server/channel/handlers/SkillBookHandler.java b/src/net/server/channel/handlers/SkillBookHandler.java index 84318aad0a..919d9c5fa8 100644 --- a/src/net/server/channel/handlers/SkillBookHandler.java +++ b/src/net/server/channel/handlers/SkillBookHandler.java @@ -32,7 +32,6 @@ import net.AbstractMaplePacketHandler; import client.inventory.manipulator.MapleInventoryManipulator; import server.MapleItemInformationProvider; import tools.MaplePacketCreator; -import tools.Randomizer; import tools.data.input.SeekableLittleEndianAccessor; public final class SkillBookHandler extends AbstractMaplePacketHandler { @@ -75,7 +74,9 @@ public final class SkillBookHandler extends AbstractMaplePacketHandler { } else { canuse = false; } - player.getClient().announce(MaplePacketCreator.skillBookSuccess(player, skill, maxlevel, canuse, success)); + + // thanks Vcoc for noting skill book result not showing for all in area + player.getMap().broadcastMessage(MaplePacketCreator.skillBookResult(player, skill, maxlevel, canuse, success)); } } } diff --git a/src/net/server/channel/handlers/SummonDamageHandler.java b/src/net/server/channel/handlers/SummonDamageHandler.java index 9c5c8ebc4b..5604eee16a 100644 --- a/src/net/server/channel/handlers/SummonDamageHandler.java +++ b/src/net/server/channel/handlers/SummonDamageHandler.java @@ -84,6 +84,9 @@ public final class SummonDamageHandler extends AbstractDealDamageHandler { allDamage.add(new SummonAttackEntry(monsterOid, damage)); } player.getMap().broadcastMessage(player, MaplePacketCreator.summonAttack(player.getId(), summon.getObjectId(), direction, allDamage), summon.getPosition()); + if (player.getMap().isOwnershipRestricted(player)) { + return; + } for (SummonAttackEntry attackEntry : allDamage) { int damage = attackEntry.getDamage(); MapleMonster target = player.getMap().getMonsterByOid(attackEntry.getMonsterOid()); diff --git a/src/net/server/channel/handlers/UseCashItemHandler.java b/src/net/server/channel/handlers/UseCashItemHandler.java index 5b5cb13540..00bd0c5b20 100644 --- a/src/net/server/channel/handlers/UseCashItemHandler.java +++ b/src/net/server/channel/handlers/UseCashItemHandler.java @@ -400,9 +400,16 @@ public final class UseCashItemHandler extends AbstractMaplePacketHandler { } else if (itemType == 533) { NPCScriptManager.getInstance().start(c, 9010009, null); } else if (itemType == 537) { + if (GameConstants.isFreeMarketRoom(player.getMapId())) { + player.dropMessage(5, "You cannot use the chalkboard here."); + player.getClient().announce(MaplePacketCreator.enableActions()); + return; + } + player.setChalkboard(slea.readMapleAsciiString()); player.getMap().broadcastMessage(MaplePacketCreator.useChalkboard(player, false)); player.getClient().announce(MaplePacketCreator.enableActions()); + remove(c, position, itemId); } else if (itemType == 539) { List strLines = new LinkedList<>(); for (int i = 0; i < 4; i++) { diff --git a/src/net/server/coordinator/MapleInviteCoordinator.java b/src/net/server/coordinator/MapleInviteCoordinator.java new file mode 100644 index 0000000000..ac5bf18946 --- /dev/null +++ b/src/net/server/coordinator/MapleInviteCoordinator.java @@ -0,0 +1,149 @@ +/* + 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 client.MapleCharacter; +import tools.Pair; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; + +/** + * + * @author Ronan + */ +public class MapleInviteCoordinator { + + public enum InviteResult { + ACCEPTED, + DENIED, + NOT_FOUND; + } + + public enum InviteType { + //BUDDY, (not needed) + //FAMILY, (not implemented) + MESSENGER, + TRADE, + PARTY, + GUILD, + ALLIANCE; + + final ConcurrentHashMap invites; + final ConcurrentHashMap inviteFrom; + final ConcurrentHashMap inviteTimeouts; + + private InviteType() { + invites = new ConcurrentHashMap<>(); + inviteTimeouts = new ConcurrentHashMap<>(); + inviteFrom = new ConcurrentHashMap<>(); + } + + private Map getRequestsTable() { + return invites; + } + + private Map getRequestsTimeoutTable() { + return inviteTimeouts; + } + + private MapleCharacter removeRequest(Integer target) { + invites.remove(target); + MapleCharacter from = inviteFrom.remove(target); + inviteTimeouts.remove(target); + + return from; + } + + private boolean addRequest(MapleCharacter from, Object referenceFrom, int targetCid) { + Object v = invites.putIfAbsent(targetCid, referenceFrom); + if (v != null) { // there was already an entry + return false; + } + + inviteFrom.put(targetCid, from); + inviteTimeouts.put(targetCid, 0); + + return true; + } + + private boolean hasRequest(int targetCid) { + return invites.containsKey(targetCid); + } + } + + // note: referenceFrom is a specific value that represents the "common association" created between the sender/recver parties + public static boolean createInvite(InviteType type, MapleCharacter from, Object referenceFrom, int targetCid) { + return type.addRequest(from, referenceFrom, targetCid); + } + + public static boolean hasInvite(InviteType type, int targetCid) { + return type.hasRequest(targetCid); + } + + public static Pair answerInvite(InviteType type, int targetCid, Object referenceFrom, boolean answer) { + Map table = type.getRequestsTable(); + + MapleCharacter from = null; + InviteResult result = InviteResult.NOT_FOUND; + + Object reference = table.get(targetCid); + if (referenceFrom.equals(reference)) { + from = type.removeRequest(targetCid); + if (from != null && !from.isLoggedinWorld()) from = null; + + result = answer ? InviteResult.ACCEPTED : InviteResult.DENIED; + } + + return new Pair<>(result, from); + } + + public static void removeInvite(InviteType type, int targetCid) { + type.removeRequest(targetCid); + } + + public static void removePlayerIncomingInvites(int cid) { + for (InviteType it : InviteType.values()) { + it.removeRequest(cid); + } + } + + public static void runTimeoutSchedule() { + for (InviteType it : InviteType.values()) { + Map timeoutTable = it.getRequestsTimeoutTable(); + + if (!timeoutTable.isEmpty()) { + Set> entrySet = new HashSet<>(timeoutTable.entrySet()); + for (Entry e : entrySet) { + int eVal = e.getValue(); + + if (eVal > 5) { // 3min to expire + it.removeRequest(e.getKey()); + } else { + timeoutTable.put(e.getKey(), eVal + 1); + } + } + } + } + } +} diff --git a/src/net/server/coordinator/MapleSessionCoordinator.java b/src/net/server/coordinator/MapleSessionCoordinator.java index cc14b57c7d..234f684512 100644 --- a/src/net/server/coordinator/MapleSessionCoordinator.java +++ b/src/net/server/coordinator/MapleSessionCoordinator.java @@ -34,6 +34,8 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.HashMap; @@ -215,7 +217,7 @@ public class MapleSessionCoordinator { } private Lock getCoodinatorLock(String remoteHost) { - return poolLock.get(remoteHost.hashCode() % 100); + return poolLock.get(Math.abs(remoteHost.hashCode()) % 100); } private static String getRemoteIp(IoSession session) { @@ -318,7 +320,12 @@ public class MapleSessionCoordinator { MapleClient client = getSessionClient(session); if (client != null) { - onlineClients.remove(client.getAccID()); + MapleClient loggedClient = onlineClients.get(client.getAccID()); + + // do not remove an online game session here, only login session + if (loggedClient != null && loggedClient.getSessionId() == client.getSessionId()) { + onlineClients.remove(client.getAccID()); + } } } } @@ -466,15 +473,24 @@ public class MapleSessionCoordinator { } public void closeSession(IoSession session, Boolean immediately) { - String hwid = (String) session.removeAttribute(MapleClient.CLIENT_HWID); + String hwid = (String) session.removeAttribute(MapleClient.CLIENT_NIBBLEHWID); // making sure to clean up calls to this function on login phase onlineRemoteHwids.remove(hwid); - hwid = (String) session.removeAttribute(MapleClient.CLIENT_NIBBLEHWID); // making sure to clean up calls to this function on login phase + hwid = (String) session.removeAttribute(MapleClient.CLIENT_HWID); onlineRemoteHwids.remove(hwid); MapleClient client = getSessionClient(session); if (client != null) { - onlineClients.remove(client.getAccID()); + if (hwid != null) { // is a game session + onlineClients.remove(client.getAccID()); + } else { + MapleClient loggedClient = onlineClients.get(client.getAccID()); + + // do not remove an online game session here, only login session + if (loggedClient != null && loggedClient.getSessionId() == client.getSessionId()) { + onlineClients.remove(client.getAccID()); + } + } } if (immediately != null) { @@ -523,4 +539,94 @@ public class MapleSessionCoordinator { public void runUpdateLoginHistory() { loginStorage.updateLoginHistory(); } + + public void printSessionTrace() { + if (!onlineClients.isEmpty()) { + List> elist = new ArrayList<>(onlineClients.entrySet()); + Collections.sort(elist, new Comparator>() { + @Override + public int compare(Entry e1, Entry e2) { + return e1.getKey().compareTo(e2.getKey()); + } + }); + + System.out.println("Current online clients: "); + for (Entry e : elist) { + System.out.println(" " + e.getKey()); + } + } + + if (!onlineRemoteHwids.isEmpty()) { + List slist = new ArrayList<>(onlineRemoteHwids); + Collections.sort(slist); + + System.out.println("Current online HWIDs: "); + for (String s : slist) { + System.out.println(" " + s); + } + } + + if (!loginRemoteHosts.isEmpty()) { + List>> elist = new ArrayList<>(loginRemoteHosts.entrySet()); + + Collections.sort(elist, new Comparator>>() { + @Override + public int compare(Entry> e1, Entry> e2) { + return e1.getKey().compareTo(e2.getKey()); + } + }); + + System.out.println("Current login sessions: "); + for (Entry> e : elist) { + System.out.println(" " + e.getKey() + ", size: " + e.getValue().size()); + } + } + } + + public void printSessionTrace(MapleClient c) { + String str = "Opened server sessions:\r\n\r\n"; + + if (!onlineClients.isEmpty()) { + List> elist = new ArrayList<>(onlineClients.entrySet()); + Collections.sort(elist, new Comparator>() { + @Override + public int compare(Entry e1, Entry e2) { + return e1.getKey().compareTo(e2.getKey()); + } + }); + + str += ("Current online clients:\r\n"); + for (Entry e : elist) { + str += (" " + e.getKey() + "\r\n"); + } + } + + if (!onlineRemoteHwids.isEmpty()) { + List slist = new ArrayList<>(onlineRemoteHwids); + Collections.sort(slist); + + str += ("Current online HWIDs:\r\n"); + for (String s : slist) { + str += (" " + s + "\r\n"); + } + } + + if (!loginRemoteHosts.isEmpty()) { + List>> elist = new ArrayList<>(loginRemoteHosts.entrySet()); + + Collections.sort(elist, new Comparator>>() { + @Override + public int compare(Entry> e1, Entry> e2) { + return e1.getKey().compareTo(e2.getKey()); + } + }); + + str += ("Current login sessions:\r\n"); + for (Entry> e : elist) { + str += (" " + e.getKey() + ", IP: " + e.getValue() + "\r\n"); + } + } + + c.getAbstractPlayerInteraction().npcTalk(2140000, str); + } } diff --git a/src/net/server/guild/MapleAlliance.java b/src/net/server/guild/MapleAlliance.java index 3320aa22cd..91c4a78517 100644 --- a/src/net/server/guild/MapleAlliance.java +++ b/src/net/server/guild/MapleAlliance.java @@ -29,11 +29,16 @@ import java.util.LinkedList; import java.util.List; import client.MapleCharacter; +import client.MapleClient; import net.server.Server; +import net.server.coordinator.MapleInviteCoordinator; +import net.server.coordinator.MapleInviteCoordinator.InviteResult; +import net.server.coordinator.MapleInviteCoordinator.InviteType; import net.server.world.MapleParty; import net.server.world.MaplePartyCharacter; import tools.DatabaseConnection; import tools.MaplePacketCreator; +import tools.Pair; /** * @@ -465,4 +470,50 @@ public class MapleAlliance { public void broadcastMessage(byte[] packet) { Server.getInstance().allianceMessage(allianceId, packet, -1, -1); } + + public static void sendInvitation(MapleClient c, String targetGuildName, int allianceId) { + MapleGuild mg = Server.getInstance().getGuildByName(targetGuildName); + if(mg == null) { + c.getPlayer().dropMessage(5, "The entered guild does not exist."); + } else { + if (mg.getAllianceId() > 0) { + c.getPlayer().dropMessage(5, "The entered guild is already registered on a guild alliance."); + } else { + MapleCharacter victim = mg.getMGC(mg.getLeaderId()).getCharacter(); + if (victim == null) { + c.getPlayer().dropMessage(5, "The master of the guild that you offered an invitation is currently not online."); + } else { + if (MapleInviteCoordinator.createInvite(InviteType.ALLIANCE, c.getPlayer(), allianceId, victim.getId())) { + victim.getClient().announce(MaplePacketCreator.allianceInvite(allianceId, c.getPlayer())); + } else { + c.getPlayer().dropMessage(5, "The master of the guild that you offered an invitation is currently managing another invite."); + } + } + } + } + } + + public static boolean answerInvitation(int targetId, String targetGuildName, int allianceId, boolean answer) { + Pair res = MapleInviteCoordinator.answerInvite(InviteType.ALLIANCE, targetId, allianceId, answer); + + String msg; + MapleCharacter sender = res.getRight(); + switch (res.getLeft()) { + case ACCEPTED: + return true; + + case DENIED: + msg = "[" + targetGuildName + "] guild has denied your guild alliance invitation."; + break; + + default: + msg = "The guild alliance request has not been accepted, since the invitation expired."; + } + + if (sender != null) { + sender.dropMessage(5, msg); + } + + return false; + } } diff --git a/src/net/server/guild/MapleGuild.java b/src/net/server/guild/MapleGuild.java index 2969cb7ce4..8fccd15f41 100644 --- a/src/net/server/guild/MapleGuild.java +++ b/src/net/server/guild/MapleGuild.java @@ -44,7 +44,11 @@ import net.server.Server; import net.server.channel.Channel; import tools.DatabaseConnection; import tools.MaplePacketCreator; +import tools.Pair; import net.server.audit.locks.MonitoredLockType; +import net.server.coordinator.MapleInviteCoordinator; +import net.server.coordinator.MapleInviteCoordinator.InviteType; +import net.server.coordinator.MapleInviteCoordinator.InviteResult; public class MapleGuild { @@ -702,7 +706,7 @@ public class MapleGuild { this.guildMessage(MaplePacketCreator.updateGP(this.id, this.gp)); } - public static MapleGuildResponse sendInvite(MapleClient c, String targetName) { + public static MapleGuildResponse sendInvitation(MapleClient c, String targetName) { MapleCharacter mc = c.getChannelServer().getPlayerStorage().getCharacterByName(targetName); if (mc == null) { return MapleGuildResponse.NOT_IN_CHANNEL; @@ -710,8 +714,37 @@ public class MapleGuild { if (mc.getGuildId() > 0) { return MapleGuildResponse.ALREADY_IN_GUILD; } - mc.getClient().announce(MaplePacketCreator.guildInvite(c.getPlayer().getGuildId(), c.getPlayer().getName())); - return null; + + MapleCharacter sender = c.getPlayer(); + if (MapleInviteCoordinator.createInvite(InviteType.GUILD, sender, sender.getGuildId(), mc.getId())) { + mc.getClient().announce(MaplePacketCreator.guildInvite(sender.getGuildId(), sender.getName())); + return null; + } else { + return MapleGuildResponse.MANAGING_INVITE; + } + } + + public static boolean answerInvitation(int targetId, String targetName, int guildId, boolean answer) { + Pair res = MapleInviteCoordinator.answerInvite(InviteType.GUILD, targetId, guildId, answer); + + MapleGuildResponse mgr; + MapleCharacter sender = res.getRight(); + switch (res.getLeft()) { + case ACCEPTED: + return true; + + case DENIED: + mgr = MapleGuildResponse.DENIED_INVITE; + break; + + default: + mgr = MapleGuildResponse.NOT_FOUND_INVITE; + } + + if (mgr != null && sender != null) { + sender.announce(mgr.getPacket(targetName)); + } + return false; } public static void displayGuildRanks(MapleClient c, int npcid) { diff --git a/src/net/server/guild/MapleGuildResponse.java b/src/net/server/guild/MapleGuildResponse.java index 9ed3b6090d..b8c04e1832 100644 --- a/src/net/server/guild/MapleGuildResponse.java +++ b/src/net/server/guild/MapleGuildResponse.java @@ -26,14 +26,22 @@ import tools.MaplePacketCreator; public enum MapleGuildResponse { NOT_IN_CHANNEL(0x2a), ALREADY_IN_GUILD(0x28), - NOT_IN_GUILD(0x2d); + NOT_IN_GUILD(0x2d), + NOT_FOUND_INVITE(0x2e), + MANAGING_INVITE(0x36), + DENIED_INVITE(0x37); + private int value; private MapleGuildResponse(int val) { value = val; } - public final byte[] getPacket() { - return MaplePacketCreator.genericGuildMessage((byte) value); + public final byte[] getPacket(String targetName) { + if (value >= MANAGING_INVITE.value) { + return MaplePacketCreator.responseGuildMessage((byte) value, targetName); + } else { + return MaplePacketCreator.genericGuildMessage((byte) value); + } } } diff --git a/src/net/server/handlers/login/CreateCharHandler.java b/src/net/server/handlers/login/CreateCharHandler.java index a6024c0dc0..3c6b4f7226 100644 --- a/src/net/server/handlers/login/CreateCharHandler.java +++ b/src/net/server/handlers/login/CreateCharHandler.java @@ -28,9 +28,13 @@ import tools.FilePrinter; import tools.MaplePacketCreator; import tools.data.input.SeekableLittleEndianAccessor; -public final class CreateCharHandler extends AbstractMaplePacketHandler { +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; - private static int[] IDs = { +public final class CreateCharHandler extends AbstractMaplePacketHandler { + + private final static Set IDs = new HashSet<>(Arrays.asList(new Integer[]{ 1302000, 1312004, 1322005, 1442079,// weapons 1040002, 1040006, 1040010, 1041002, 1041006, 1041010, 1041011, 1042167,// bottom 1060002, 1060006, 1061002, 1061008, 1062115, // top @@ -38,15 +42,10 @@ public final class CreateCharHandler extends AbstractMaplePacketHandler { 30000, 30010,30020, 30030, 31000, 31040, 31050,// hair 20000, 20001, 20002, 21000, 21001, 21002, 21201, 20401, 20402, 21700, 20100 //face //#NeverTrustStevenCode - }; + })); - private static boolean isLegal(int toCompare) { - for (int i = 0; i < IDs.length; i++) { - if (IDs[i] == toCompare) { - return true; - } - } - return false; + private static boolean isLegal(Integer toCompare) { + return IDs.contains(toCompare); } diff --git a/src/net/server/handlers/login/LoginPasswordHandler.java b/src/net/server/handlers/login/LoginPasswordHandler.java index 6940088b71..edfb2b65e9 100644 --- a/src/net/server/handlers/login/LoginPasswordHandler.java +++ b/src/net/server/handlers/login/LoginPasswordHandler.java @@ -38,8 +38,10 @@ import client.MapleClient; import java.sql.ResultSet; import java.sql.Statement; import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import org.apache.mina.core.session.IoSession; public final class LoginPasswordHandler implements MaplePacketHandler { @@ -54,9 +56,18 @@ public final class LoginPasswordHandler implements MaplePacketHandler { return HexTool.toString(digester.digest()).replace(" ", "").toLowerCase(); } + private static String getRemoteIp(IoSession session) { + return ((InetSocketAddress) session.getRemoteAddress()).getAddress().getHostAddress(); + } + @Override public final void handlePacket(SeekableLittleEndianAccessor slea, MapleClient c) { - + String remoteHost = getRemoteIp(c.getSession()); + if (remoteHost.startsWith("127.") && !ServerConstants.HOST.startsWith("127.")) { + c.announce(MaplePacketCreator.getLoginFailed(13)); // cannot login as localhost if it's not a test server + return; + } + String login = slea.readMapleAsciiString(); String pwd = slea.readMapleAsciiString(); c.setAccountName(login); diff --git a/src/net/server/worker/FishingWorker.java b/src/net/server/worker/FishingWorker.java new file mode 100644 index 0000000000..56eeb5fe46 --- /dev/null +++ b/src/net/server/worker/FishingWorker.java @@ -0,0 +1,37 @@ +/* + 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.worker; + +import net.server.world.World; + +/** + * @author Ronan + */ +public class FishingWorker extends BaseWorker implements Runnable { + + @Override + public void run() { + wserv.runCheckFishingSchedule(); + } + + public FishingWorker(World world) { + super(world); + } +} diff --git a/src/net/server/worker/FredrickWorker.java b/src/net/server/worker/FredrickWorker.java new file mode 100644 index 0000000000..2845fb80ab --- /dev/null +++ b/src/net/server/worker/FredrickWorker.java @@ -0,0 +1,33 @@ +/* + 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.worker; + +import client.processor.FredrickProcessor; + +/** + * @author Ronan + */ +public class FredrickWorker implements Runnable { + + @Override + public void run() { + FredrickProcessor.runFredrickSchedule(); + } +} diff --git a/src/net/server/worker/InvitationWorker.java b/src/net/server/worker/InvitationWorker.java new file mode 100644 index 0000000000..87a171bef4 --- /dev/null +++ b/src/net/server/worker/InvitationWorker.java @@ -0,0 +1,33 @@ +/* + 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.worker; + +import net.server.coordinator.MapleInviteCoordinator; + +/** + * @author Ronan + */ +public class InvitationWorker implements Runnable { + + @Override + public void run() { + MapleInviteCoordinator.runTimeoutSchedule(); + } +} diff --git a/src/net/server/worker/MapOwnershipWorker.java b/src/net/server/worker/MapOwnershipWorker.java new file mode 100644 index 0000000000..adc201de26 --- /dev/null +++ b/src/net/server/worker/MapOwnershipWorker.java @@ -0,0 +1,40 @@ +/* + 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.worker; + +import net.server.world.World; +import net.server.channel.Channel; + +/** + * @author Ronan + */ +public class MapOwnershipWorker extends BaseWorker implements Runnable { + + @Override + public void run() { + for (Channel ch : wserv.getChannels()) { + ch.runCheckOwnedMapsSchedule(); + } + } + + public MapOwnershipWorker(World world) { + super(world); + } +} diff --git a/src/net/server/world/World.java b/src/net/server/world/World.java index 3e73d53f47..062282a6a1 100644 --- a/src/net/server/world/World.java +++ b/src/net/server/world/World.java @@ -55,6 +55,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.Set; import java.util.HashSet; import java.util.PriorityQueue; +import java.util.WeakHashMap; import java.util.concurrent.ScheduledFuture; import scripting.event.EventInstanceManager; @@ -65,7 +66,9 @@ import server.maps.MaplePlayerShop; import server.maps.MaplePlayerShopItem; import server.maps.AbstractMapleMapObject; import net.server.worker.CharacterAutosaverWorker; +import net.server.worker.FishingWorker; import net.server.worker.HiredMerchantWorker; +import net.server.worker.MapOwnershipWorker; import net.server.worker.MountTirednessWorker; import net.server.worker.PetFullnessWorker; import net.server.worker.ServerMessageWorker; @@ -86,6 +89,10 @@ import net.server.audit.locks.MonitoredLockType; import net.server.audit.locks.MonitoredReentrantLock; import net.server.audit.locks.MonitoredReentrantReadWriteLock; import net.server.audit.locks.factory.MonitoredReentrantLockFactory; +import net.server.coordinator.MapleInviteCoordinator; +import net.server.coordinator.MapleInviteCoordinator.InviteResult; +import net.server.coordinator.MapleInviteCoordinator.InviteType; +import tools.packets.Fishing; /** * @@ -94,7 +101,7 @@ import net.server.audit.locks.factory.MonitoredReentrantLockFactory; */ public class World { - private int id, flag, exprate, droprate, bossdroprate, mesorate, questrate, travelrate; + private int id, flag, exprate, droprate, bossdroprate, mesorate, questrate, travelrate, fishingrate; private String eventmsg; private List channels = new ArrayList<>(); private Map pnpcStep = new HashMap<>(); @@ -155,10 +162,14 @@ public class World { private ScheduledFuture timedMapObjectsSchedule; private MonitoredReentrantLock timedMapObjectLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.WORLD_MAPOBJS, true); + private Map fishingAttempters = Collections.synchronizedMap(new WeakHashMap()); + private ScheduledFuture charactersSchedule; private ScheduledFuture marriagesSchedule; + private ScheduledFuture mapOwnershipSchedule; + private ScheduledFuture fishingSchedule; - public World(int world, int flag, String eventmsg, int exprate, int droprate, int bossdroprate, int mesorate, int questrate, int travelrate) { + public World(int world, int flag, String eventmsg, int exprate, int droprate, int bossdroprate, int mesorate, int questrate, int travelrate, int fishingrate) { this.id = world; this.flag = flag; this.eventmsg = eventmsg; @@ -168,7 +179,8 @@ public class World { this.mesorate = mesorate; this.questrate = questrate; this.travelrate = travelrate; - runningPartyId.set(1); + this.fishingrate = fishingrate; + runningPartyId.set(1000000001); // partyid must not clash with charid to solve update item looting issues, found thanks to Vcoc runningMessengerId.set(1); petUpdate = Server.getInstance().getCurrentTime(); @@ -186,6 +198,8 @@ public class World { timedMapObjectsSchedule = tman.register(new TimedMapObjectWorker(this), 60 * 1000, 60 * 1000); charactersSchedule = tman.register(new CharacterAutosaverWorker(this), 60 * 60 * 1000, 60 * 60 * 1000); marriagesSchedule = tman.register(new WeddingReservationWorker(this), ServerConstants.WEDDING_RESERVATION_INTERVAL * 60 * 1000, ServerConstants.WEDDING_RESERVATION_INTERVAL * 60 * 1000); + mapOwnershipSchedule = tman.register(new MapOwnershipWorker(this), 20 * 1000, 20 * 1000); + fishingSchedule = tman.register(new FishingWorker(this), 10 * 1000, 10 * 1000); } @@ -362,14 +376,22 @@ public class World { return travelrate; } - public void setTravelRate(int quest) { - this.travelrate = quest; + public void setTravelRate(int travel) { + this.travelrate = travel; } public int getTransportationTime(int travelTime) { return (int) Math.ceil(travelTime / travelrate); } + public int getFishingRate() { + return fishingrate; + } + + public void setFishingRate(int quest) { + this.fishingrate = quest; + } + public void loadAccountCharactersView(Integer accountId, List chars) { SortedMap charsMap = new TreeMap<>(); for(MapleCharacter chr : chars) { @@ -985,14 +1007,23 @@ public class World { public void messengerInvite(String sender, int messengerid, String target, int fromchannel) { if (isConnected(target)) { - MapleMessenger messenger = getPlayerStorage().getCharacterByName(target).getMessenger(); - if (messenger == null) { - getPlayerStorage().getCharacterByName(target).getClient().announce(MaplePacketCreator.messengerInvite(sender, messengerid)); - MapleCharacter from = getChannel(fromchannel).getPlayerStorage().getCharacterByName(sender); - from.getClient().announce(MaplePacketCreator.messengerNote(target, 4, 1)); - } else { - MapleCharacter from = getChannel(fromchannel).getPlayerStorage().getCharacterByName(sender); - from.getClient().announce(MaplePacketCreator.messengerChat(sender + " : " + target + " is already using Maple Messenger")); + MapleCharacter targetChr = getPlayerStorage().getCharacterByName(target); + if (targetChr != null) { + MapleMessenger messenger = targetChr.getMessenger(); + if (messenger == null) { + MapleCharacter from = getChannel(fromchannel).getPlayerStorage().getCharacterByName(sender); + if (from != null) { + if (MapleInviteCoordinator.createInvite(InviteType.MESSENGER, from, messengerid, targetChr.getId())) { + targetChr.getClient().announce(MaplePacketCreator.messengerInvite(sender, messengerid)); + from.getClient().announce(MaplePacketCreator.messengerNote(target, 4, 1)); + } else { + from.announce(MaplePacketCreator.messengerChat(sender + " : " + target + " is already managing a Maple Messenger invitation")); + } + } + } else { + MapleCharacter from = getChannel(fromchannel).getPlayerStorage().getCharacterByName(sender); + from.getClient().announce(MaplePacketCreator.messengerChat(sender + " : " + target + " is already using Maple Messenger")); + } } } } @@ -1043,11 +1074,13 @@ public class World { } } - public void declineChat(String target, String namefrom) { - if (isConnected(target)) { - MapleCharacter chr = getPlayerStorage().getCharacterByName(target); - if (chr != null && chr.getMessenger() != null) { - chr.getClient().announce(MaplePacketCreator.messengerNote(namefrom, 5, 0)); + public void declineChat(String sender, MapleCharacter player) { + if (isConnected(sender)) { + MapleCharacter senderChr = getPlayerStorage().getCharacterByName(sender); + if (senderChr != null && senderChr.getMessenger() != null) { + if (MapleInviteCoordinator.answerInvite(InviteType.MESSENGER, player.getId(), senderChr.getMessenger().getId(), false).getLeft() == InviteResult.DENIED) { + senderChr.getClient().announce(MaplePacketCreator.messengerNote(player.getName(), 5, 0)); + } } } } @@ -1890,6 +1923,44 @@ public class World { } } + public boolean registerFisherPlayer(MapleCharacter chr, int baitLevel) { + synchronized (fishingAttempters) { + if (fishingAttempters.containsKey(chr)) { + return false; + } + + fishingAttempters.put(chr, baitLevel); + return true; + } + } + + public int unregisterFisherPlayer(MapleCharacter chr) { + Integer baitLevel = fishingAttempters.remove(chr); + if (baitLevel != null) { + return baitLevel; + } else { + return 0; + } + } + + public void runCheckFishingSchedule() { + double[] fishingLikelihoods = Fishing.fetchFishingLikelihood(); + double yearLikelihood = fishingLikelihoods[0], timeLikelihood = fishingLikelihoods[1]; + + if (!fishingAttempters.isEmpty()) { + List fishingAttemptersList; + + synchronized (fishingAttempters) { + fishingAttemptersList = new ArrayList<>(fishingAttempters.keySet()); + } + + for (MapleCharacter chr : fishingAttemptersList) { + int baitLevel = unregisterFisherPlayer(chr); + Fishing.doFishing(chr, baitLevel, yearLikelihood, timeLikelihood); + } + } + } + private void clearWorldData() { List pList; partyLock.lock(); @@ -1966,6 +2037,16 @@ public class World { marriagesSchedule = null; } + if(mapOwnershipSchedule != null) { + mapOwnershipSchedule.cancel(false); + mapOwnershipSchedule = null; + } + + if(fishingSchedule != null) { + fishingSchedule.cancel(false); + fishingSchedule = null; + } + players.disconnectAll(); players = null; diff --git a/src/scripting/AbstractPlayerInteraction.java b/src/scripting/AbstractPlayerInteraction.java index da8d4d6b31..e314184218 100644 --- a/src/scripting/AbstractPlayerInteraction.java +++ b/src/scripting/AbstractPlayerInteraction.java @@ -848,18 +848,18 @@ public class AbstractPlayerInteraction { } public void teachSkill(int skillid, byte level, byte masterLevel, long expiration, boolean force) { - Skill skill = SkillFactory.getSkill(skillid); - - if (!force && level > -1) { + Skill skill = SkillFactory.getSkill(skillid); MapleCharacter.SkillEntry skillEntry = getPlayer().getSkills().get(skill); - if (skillEntry != null) { - getPlayer().changeSkillLevel(skill, (byte) Math.max(skillEntry.skillevel, level), Math.max(skillEntry.masterlevel, masterLevel), expiration == -1 ? -1 : Math.max(skillEntry.expiration, expiration)); - return; + if (!force && level > -1) { + getPlayer().changeSkillLevel(skill, (byte) Math.max(skillEntry.skillevel, level), Math.max(skillEntry.masterlevel, masterLevel), expiration == -1 ? -1 : Math.max(skillEntry.expiration, expiration)); + return; + } + } else if (GameConstants.isAranSkills(skillid)) { + c.announce(MaplePacketCreator.showInfo("Effect/BasicEff.img/AranGetSkill")); } - } - - getPlayer().changeSkillLevel(skill, level, masterLevel, expiration); + + getPlayer().changeSkillLevel(skill, level, masterLevel, expiration); } public void removeEquipFromSlot(short slot) { diff --git a/src/scripting/event/EventManager.java b/src/scripting/event/EventManager.java index d051f2586d..b856f461d1 100644 --- a/src/scripting/event/EventManager.java +++ b/src/scripting/event/EventManager.java @@ -21,6 +21,7 @@ */ package scripting.event; +import tools.exceptions.EventInstanceInProgressException; import java.util.Collection; import java.util.HashMap; import java.util.Map; diff --git a/src/scripting/npc/NPCConversationManager.java b/src/scripting/npc/NPCConversationManager.java index e5c7a41bf9..906e33c2fa 100644 --- a/src/scripting/npc/NPCConversationManager.java +++ b/src/scripting/npc/NPCConversationManager.java @@ -435,6 +435,8 @@ public class NPCConversationManager extends AbstractPlayerInteraction { Server.getInstance().allianceMessage(alliance.getId(), MaplePacketCreator.getGuildAlliances(alliance, c.getWorld()), -1, -1); Server.getInstance().allianceMessage(alliance.getId(), MaplePacketCreator.allianceNotice(alliance.getId(), alliance.getNotice()), -1, -1); + + c.announce(MaplePacketCreator.updateAllianceInfo(alliance, c.getWorld())); // thanks Vcoc for finding an alliance update to leader issue } public void disbandAlliance(MapleClient c, int allianceId) { diff --git a/src/server/MapleTrade.java b/src/server/MapleTrade.java index a477144ac2..13a096f8b2 100644 --- a/src/server/MapleTrade.java +++ b/src/server/MapleTrade.java @@ -36,14 +36,38 @@ import client.inventory.manipulator.MapleInventoryManipulator; import client.inventory.manipulator.MapleKarmaManipulator; import constants.GameConstants; import constants.ServerConstants; +import net.server.coordinator.MapleInviteCoordinator; +import net.server.coordinator.MapleInviteCoordinator.InviteResult; +import net.server.coordinator.MapleInviteCoordinator.InviteType; import tools.Pair; /** * * @author Matze - * @author Ronan - concurrency safety & check available slots + * @author Ronan - concurrency safety & check available slots & trade results */ public class MapleTrade { + + public enum TradeResult { + NO_RESPONSE(1), + PARTNER_CANCEL(2), + SUCCESSFUL(7), + UNSUCCESSFUL(8), + UNSUCCESSFUL_UNIQUE_ITEM_LIMIT(9), + UNSUCCESSFUL_ANOTHER_MAP(12), + UNSUCCESSFUL_DAMAGED_FILES(13); + + private final int res; + + private TradeResult(int res) { + this.res = res; + } + + private byte getValue() { + return (byte) res; + } + } + private MapleTrade partner = null; private List items = new ArrayList<>(); private List exchangeItems; @@ -88,6 +112,7 @@ public class MapleTrade { } private void completeTrade() { + byte result; boolean show = ServerConstants.USE_DEBUG; items.clear(); meso = 0; @@ -106,18 +131,21 @@ public class MapleTrade { } else { chr.dropMessage(1, "Transaction completed. You received " + GameConstants.numberWithCommas(exchangeMeso) + " mesos."); } + + result = TradeResult.NO_RESPONSE.getValue(); } else { - chr.dropMessage(1, "Transaction completed."); + result = TradeResult.SUCCESSFUL.getValue(); } exchangeMeso = 0; if (exchangeItems != null) { exchangeItems.clear(); } - chr.getClient().announce(MaplePacketCreator.getTradeCompletion(number)); + + chr.getClient().announce(MaplePacketCreator.getTradeResult(number, result)); } - private void cancel() { + private void cancel(byte result) { boolean show = ServerConstants.USE_DEBUG; for (Item item : items) { @@ -134,7 +162,8 @@ public class MapleTrade { if (exchangeItems != null) { exchangeItems.clear(); } - chr.getClient().announce(MaplePacketCreator.getTradeCancel(number)); + + chr.getClient().announce(MaplePacketCreator.getTradeResult(number, result)); } private boolean isLocked() { @@ -224,6 +253,15 @@ public class MapleTrade { return MapleInventory.checkSpotsAndOwnership(chr, tradeItems); } + private boolean fitsUniquesInInventory() { + List exchangeItemids = new LinkedList<>(); + for (Item item : exchangeItems) { + exchangeItemids.add(item.getItemId()); + } + + return chr.canHoldUniques(exchangeItemids); + } + private synchronized boolean checkTradeCompleteHandshake(boolean updateSelf) { MapleTrade self, other; @@ -259,32 +297,42 @@ public class MapleTrade { partner.fetchExchangedItems(); if (!local.fitsMeso()) { - cancelTrade(c); + cancelTrade(local.getChr(), TradeResult.UNSUCCESSFUL); 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); + cancelTrade(partner.getChr(), TradeResult.UNSUCCESSFUL); 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."); - partner.getChr().message("Partner does not have enough inventory space to complete the trade."); + if (local.fitsUniquesInInventory()) { + cancelTrade(local.getChr(), TradeResult.UNSUCCESSFUL); + c.message("There is not enough inventory space to complete the trade."); + partner.getChr().message("Partner does not have enough inventory space to complete the trade."); + } else { + cancelTrade(local.getChr(), TradeResult.UNSUCCESSFUL_UNIQUE_ITEM_LIMIT); + partner.getChr().message("Partner cannot hold more than one one-of-a-kind item at a time."); + } return; } else if (!partner.fitsInInventory()) { - cancelTrade(c); - c.message("Partner does not have enough inventory space to complete the trade."); - partner.getChr().message("There is not enough inventory space to complete the trade."); + if (partner.fitsUniquesInInventory()) { + cancelTrade(partner.getChr(), TradeResult.UNSUCCESSFUL); + c.message("Partner does not have enough inventory space to complete the trade."); + partner.getChr().message("There is not enough inventory space to complete the trade."); + } else { + cancelTrade(partner.getChr(), TradeResult.UNSUCCESSFUL_UNIQUE_ITEM_LIMIT); + c.message("Partner cannot hold more than one one-of-a-kind item at a time."); + } return; } if (local.getChr().getLevel() < 15) { if (local.getChr().getMesosTraded() + local.exchangeMeso > 1000000) { - cancelTrade(c); + cancelTrade(local.getChr(), TradeResult.NO_RESPONSE); local.getChr().getClient().announce(MaplePacketCreator.serverNotice(1, "Characters under level 15 may not trade more than 1 million mesos per day.")); return; } else { @@ -292,7 +340,7 @@ public class MapleTrade { } } else if (partner.getChr().getLevel() < 15) { if (partner.getChr().getMesosTraded() + partner.exchangeMeso > 1000000) { - cancelTrade(c); + cancelTrade(partner.getChr(), TradeResult.NO_RESPONSE); partner.getChr().getClient().announce(MaplePacketCreator.serverNotice(1, "Characters under level 15 may not trade more than 1 million mesos per day.")); return; } else { @@ -309,74 +357,122 @@ public class MapleTrade { } } - private static void cancelTradeInternal(MapleCharacter chr) { + private static void cancelTradeInternal(MapleCharacter chr, byte selfResult, byte partnerResult) { MapleTrade trade = chr.getTrade(); if(trade == null) return; - trade.cancel(); + trade.cancel(selfResult); if (trade.getPartner() != null) { - trade.getPartner().cancel(); + trade.getPartner().cancel(partnerResult); trade.getPartner().getChr().setTrade(null); + + MapleInviteCoordinator.answerInvite(InviteType.TRADE, trade.getChr().getId(), trade.getPartner().getChr().getId(), false); + MapleInviteCoordinator.answerInvite(InviteType.TRADE, trade.getPartner().getChr().getId(), trade.getChr().getId(), false); } chr.setTrade(null); } - private synchronized void tradeCancelHandshake(boolean updateSelf) { + private synchronized void tradeCancelHandshake(boolean updateSelf, byte result) { + byte selfResult, partnerResult; MapleTrade self; - + + partnerResult = result; + selfResult = (result == TradeResult.PARTNER_CANCEL.getValue() ? TradeResult.NO_RESPONSE.getValue() : result); + if (updateSelf) { self = this; } else { self = this.getPartner(); } - cancelTradeInternal(self.getChr()); + cancelTradeInternal(self.getChr(), selfResult, partnerResult); } - private void cancelHandshake() { // handshake checkout thanks to Ronan - if (this.getChr().getId() < this.getPartner().getChr().getId()) { - this.tradeCancelHandshake(true); + private void cancelHandshake(byte result) { // handshake checkout thanks to Ronan + MapleTrade partner = this.getPartner(); + if (partner == null || this.getChr().getId() < partner.getChr().getId()) { + this.tradeCancelHandshake(true, result); } else { - this.getPartner().tradeCancelHandshake(false); + partner.tradeCancelHandshake(false, result); } } - public static void cancelTrade(MapleCharacter chr) { + public static void cancelTrade(MapleCharacter chr, TradeResult result) { MapleTrade trade = chr.getTrade(); if(trade == null) return; - trade.cancelHandshake(); + trade.cancelHandshake(result.getValue()); } public static void startTrade(MapleCharacter c) { if (c.getTrade() == null) { c.setTrade(new MapleTrade((byte) 0, c)); - c.getClient().announce(MaplePacketCreator.getTradeStart(c.getClient(), c.getTrade(), (byte) 0)); - } else { - c.message("You are already in a trade."); } } - + + private static boolean hasTradeInviteBack(MapleCharacter c1, MapleCharacter c2) { + MapleTrade other = c2.getTrade(); + if (other != null) { + MapleTrade otherPartner = other.getPartner(); + if (otherPartner != null) { + if (otherPartner.getChr().getId() == c1.getId()) { + return true; + } + } + } + + return false; + } + public static void inviteTrade(MapleCharacter c1, MapleCharacter c2) { - if (c2.getTrade() == null) { - c2.setTrade(new MapleTrade((byte) 1, c2)); - c2.getTrade().setPartner(c1.getTrade()); - c1.getTrade().setPartner(c2.getTrade()); - c2.getClient().announce(MaplePacketCreator.getTradeInvite(c1)); + if (MapleInviteCoordinator.hasInvite(InviteType.TRADE, c1.getId())) { + if (hasTradeInviteBack(c1, c2)) { + c1.message("You are already managing this player's trade invitation."); + } else { + c1.message("You are already managing someone's trade invitation."); + } + + return; + } else if (c1.getTrade().isFullTrade()) { + c1.message("You are already in a trade."); + return; + } + + if (MapleInviteCoordinator.createInvite(InviteType.TRADE, c1, c1.getId(), c2.getId())) { + if (c2.getTrade() == null) { + c2.setTrade(new MapleTrade((byte) 1, c2)); + c2.getTrade().setPartner(c1.getTrade()); + c1.getTrade().setPartner(c2.getTrade()); + + c1.getClient().announce(MaplePacketCreator.getTradeStart(c1.getClient(), c1.getTrade(), (byte) 0)); + c2.getClient().announce(MaplePacketCreator.tradeInvite(c1)); + } else { + c1.message("The other player is already trading with someone else."); + cancelTrade(c1, TradeResult.NO_RESPONSE); + MapleInviteCoordinator.answerInvite(InviteType.TRADE, c2.getId(), c1.getId(), false); + } } else { - c1.message("The other player is already trading with someone else."); - cancelTrade(c1); + c1.message("The other player is already managing someone else's trade invitation."); + cancelTrade(c1, TradeResult.NO_RESPONSE); } } public static void visitTrade(MapleCharacter c1, MapleCharacter c2) { - if (c1.getTrade() != null && c1.getTrade().getPartner() == c2.getTrade() && c2.getTrade() != null && c2.getTrade().getPartner() == c1.getTrade()) { - c2.getClient().announce(MaplePacketCreator.getTradePartnerAdd(c1)); - c1.getClient().announce(MaplePacketCreator.getTradeStart(c1.getClient(), c1.getTrade(), (byte) 1)); - c1.getTrade().setFullTrade(true); - c2.getTrade().setFullTrade(true); + Pair inviteRes = MapleInviteCoordinator.answerInvite(InviteType.TRADE, c1.getId(), c2.getId(), true); + + InviteResult res = inviteRes.getLeft(); + if (res == InviteResult.ACCEPTED) { + if (c1.getTrade() != null && c1.getTrade().getPartner() == c2.getTrade() && c2.getTrade() != null && c2.getTrade().getPartner() == c1.getTrade()) { + c2.getClient().announce(MaplePacketCreator.getTradePartnerAdd(c1)); + c1.getClient().announce(MaplePacketCreator.getTradeStart(c1.getClient(), c1.getTrade(), (byte) 1)); + c1.getTrade().setFullTrade(true); + c2.getTrade().setFullTrade(true); + } else { + c1.message("The other player has already closed the trade."); + } } else { - c1.message("The other player has already closed the trade."); + c1.message("This trade invitation already rescinded."); + cancelTrade(c1, TradeResult.NO_RESPONSE); } } @@ -385,11 +481,15 @@ public class MapleTrade { if (trade != null) { if (trade.getPartner() != null) { MapleCharacter other = trade.getPartner().getChr(); - other.getTrade().cancel(); + if (MapleInviteCoordinator.answerInvite(InviteType.TRADE, c.getId(), other.getId(), false).getLeft() == InviteResult.DENIED) { + other.message(c.getName() + " has declined your trade request."); + } + + other.getTrade().cancel(TradeResult.PARTNER_CANCEL.getValue()); other.setTrade(null); - other.message(c.getName() + " has declined your trade request."); + } - trade.cancel(); + trade.cancel(TradeResult.NO_RESPONSE.getValue()); c.setTrade(null); } } diff --git a/src/server/life/MapleMonster.java b/src/server/life/MapleMonster.java index 6ea511e450..b4e4f29a00 100644 --- a/src/server/life/MapleMonster.java +++ b/src/server/life/MapleMonster.java @@ -460,47 +460,50 @@ public class MapleMonster extends AbstractLoadedMapleLife { public boolean isAttackedBy(MapleCharacter chr) { return takenDamage.containsKey(chr.getId()); } - - private void distributeExperienceToParty(int pid, float exp, int killer, int killerLevel, Set underleveled, int minThresholdLevel) { - List members = new LinkedList<>(); - MapleCharacter pchar = getMap().getAnyCharacterFromParty(pid); + + private void distributeExperienceToParty(int pid, float exp, int mostDamageCid, int minThresholdLevel, int killerLevel, Set underleveled, Map partyExpReward) { + MapleCharacter pchar = getMap().getAnyCharacterFromParty(pid); // thanks G h o s t, Alfred, Vcoc, BHB for poiting out a bug in detecting party members after membership transactions in a party took place + + List members; if(pchar != null) { - for(MapleCharacter chr : pchar.getPartyMembersOnSameMap()) { - members.add(chr); - } + members = pchar.getPartyMembersOnSameMap(); } else { - MapleCharacter chr = getMap().getCharacterById(killer); - if(chr == null) return; - - members.add(chr); + members = new LinkedList<>(); } List expSharers = new LinkedList<>(); - int expSharersLevel = 0; + int expSharersMaxLevel = 1; + boolean hasMostDamageCid = false; for (MapleCharacter mc : members) { + if (mc.getId() == mostDamageCid) { + hasMostDamageCid = true; + } + if (mc.getLevel() >= minThresholdLevel) { //NO EXP WILL BE GIVEN for those who are underleveled! if (Math.abs(killerLevel - mc.getLevel()) < ServerConstants.MIN_RANGELEVEL_TO_EXP_LEECH) { // thanks Thora for pointing out leech level limitation - expSharersLevel += mc.getLevel(); + if (expSharersMaxLevel < mc.getLevel()) { + expSharersMaxLevel = mc.getLevel(); + } expSharers.add(mc); } } else { underleveled.add(mc); } } - - final int mostDamageCid = getHighestDamagerId(); - + + int numExpSharers = expSharers.size(); + + // PARTY BONUS: 2p -> +2% , 3p -> +4% , 4p -> +6% , 5p -> +8% , 6p -> +10% + // MOST DAMAGE BONUS: 1.5x bonus + final float partyModifier = numExpSharers <= 1 ? 0.0f : 0.02f * (numExpSharers - 1); + final float mostDamageModifier = hasMostDamageCid ? 1.5f : 1.0f; + final float partyExp = exp * partyModifier * mostDamageModifier; + for (MapleCharacter mc : expSharers) { - int id = mc.getId(); - boolean isKiller = killer == id; - boolean mostDamage = mostDamageCid == id; - float xp = ((0.80f * exp * mc.getLevel()) / expSharersLevel); - if (mostDamage) { - xp += (0.20f * exp); - } - giveExpToCharacter(mc, xp, isKiller, expSharers.size()); + float levelPenaltyModifier = (float) Math.sqrt(((float) mc.getLevel()) / expSharersMaxLevel); + partyExpReward.put(mc, partyExp * levelPenaltyModifier); } } @@ -518,11 +521,26 @@ public class MapleMonster extends AbstractLoadedMapleLife { } } + private void propagateExperienceGains(Map personalExpReward, Map partyExpReward) { + Set expRewardPlayers = new HashSet<>(personalExpReward.keySet()); + expRewardPlayers.addAll(partyExpReward.keySet()); + + for (MapleCharacter chr : expRewardPlayers) { + Float personalExp = personalExpReward.get(chr); + Float partyExp = partyExpReward.get(chr); + + this.giveExpToCharacter(chr, personalExp, partyExp); + } + } + private void distributeExperience(int killerId) { if (isAlive()) { return; } + Map personalExpReward = new HashMap<>(); + Map partyExpReward = new HashMap<>(); + EventInstanceManager eim = getMap().getEventInstance(); int minThresholdLevel = calcThresholdLevel(eim != null), killerLevel = Integer.MAX_VALUE; int exp = getExp(); @@ -553,18 +571,18 @@ public class MapleMonster extends AbstractLoadedMapleLife { xp += exp2; } - MapleParty p = mc.getParty(); - if (p != null) { - int pID = p.getId(); - float pXP = xp + (partyExp.containsKey(pID) ? partyExp.get(pID) : 0); - partyExp.put(pID, pXP); - } else { - if(mc.getLevel() >= minThresholdLevel) { - //NO EXP WILL BE GIVEN for those who are underleveled! - giveExpToCharacter(mc, xp, isKiller, 1); - } else { - underleveled.add(mc); + if(mc.getLevel() >= minThresholdLevel) { + //NO EXP WILL BE GIVEN for those who are underleveled! + personalExpReward.put(mc, xp); + + MapleParty p = mc.getParty(); + if (p != null) { // for party bonus exp + int pID = p.getId(); + float pXP = xp + (partyExp.containsKey(pID) ? partyExp.get(pID) : 0); + partyExp.put(pID, pXP); } + } else { + underleveled.add(mc); } } } @@ -587,52 +605,78 @@ public class MapleMonster extends AbstractLoadedMapleLife { } } + int mostDamageCid = this.getHighestDamagerId(); for (Entry party : partyExp.entrySet()) { - distributeExperienceToParty(party.getKey(), party.getValue(), killerId, killerLevel, underleveled, minThresholdLevel); + distributeExperienceToParty(party.getKey(), party.getValue(), mostDamageCid, minThresholdLevel, killerLevel, underleveled, partyExpReward); } for(MapleCharacter mc : underleveled) { mc.showUnderleveledInfo(this); } - } - - private void giveExpToCharacter(MapleCharacter attacker, float exp, boolean isKiller, int numExpSharers) { - //PARTY BONUS: 2p -> +2% , 3p -> +4% , 4p -> +6% , 5p -> +8% , 6p -> +10% - final float partyModifier = numExpSharers <= 1 ? 0.0f : 0.02f * (numExpSharers - 1); - int partyExp = 0; - if (attacker.getHp() > 0) { - exp *= attacker.getExpRate(); + propagateExperienceGains(personalExpReward, partyExpReward); + } + + private float getStatusExpMultiplier(MapleCharacter attacker) { + float multiplier = 1.0f; + + // thanks Prophecy & Aika for finding out Holy Symbol not being applied on party bonuses + Integer holySymbol = attacker.getBuffedValue(MapleBuffStat.HOLY_SYMBOL); + if (holySymbol != null) { + multiplier *= (1.0 + (holySymbol.doubleValue() / 100.0)); + } + + statiLock.lock(); + try { + MonsterStatusEffect mse = stati.get(MonsterStatus.SHOWDOWN); + if (mse != null) { + multiplier *= (1.0 + (mse.getStati().get(MonsterStatus.SHOWDOWN).doubleValue() / 100.0)); + } + } finally { + statiLock.unlock(); + } + + return multiplier; + } + + private static int expValueToInteger(double exp) { + if (exp > Integer.MAX_VALUE) { + exp = Integer.MAX_VALUE; + } else if (exp < Integer.MIN_VALUE) { + exp = Integer.MIN_VALUE; + } + + return (int) exp; + } + + private void giveExpToCharacter(MapleCharacter attacker, Float personalExp, Float partyExp) { + if (attacker.isAlive()) { + if (personalExp != null) { + personalExp *= getStatusExpMultiplier(attacker); + personalExp *= attacker.getExpRate(); + } else { + personalExp = 0.0f; + } Integer expBonus = attacker.getBuffedValue(MapleBuffStat.EXP_INCREASE); - if (expBonus != null) { // exp increase buff found thanks to HighKey21 - exp += expBonus; + if (expBonus != null) { // exp increase player buff found thanks to HighKey21 + personalExp += expBonus; } - - int personalExp = (int) exp; - if (exp <= Integer.MAX_VALUE) { // assuming no negative xp here - if (partyModifier > 0.0f) { - partyExp = (int) (personalExp * partyModifier * ServerConstants.PARTY_BONUS_EXP_RATE); - } - Integer holySymbol = attacker.getBuffedValue(MapleBuffStat.HOLY_SYMBOL); - if (holySymbol != null) { - personalExp *= 1.0 + (holySymbol.doubleValue() / 100.0); - } - statiLock.lock(); - try { - if (stati.containsKey(MonsterStatus.SHOWDOWN)) { - personalExp *= (stati.get(MonsterStatus.SHOWDOWN).getStati().get(MonsterStatus.SHOWDOWN).doubleValue() / 100.0 + 1.0); - } - } finally { - statiLock.unlock(); - } + int _personalExp = expValueToInteger(personalExp); // assuming no negative xp here + + if (partyExp != null) { + partyExp *= getStatusExpMultiplier(attacker); + partyExp *= attacker.getExpRate(); + partyExp *= ServerConstants.PARTY_BONUS_EXP_RATE; } else { - personalExp = Integer.MAX_VALUE; + partyExp = 0.0f; } - attacker.gainExp(personalExp, partyExp, true, false, isKiller); - attacker.increaseEquipExp(personalExp); + int _partyExp = expValueToInteger(partyExp); + + attacker.gainExp(_personalExp, _partyExp, true, false, false); + attacker.increaseEquipExp(_personalExp); attacker.updateQuestMobCount(getId()); } } diff --git a/src/server/maps/MapleGenericPortal.java b/src/server/maps/MapleGenericPortal.java index 7ba6fe6b9d..be8b169c87 100644 --- a/src/server/maps/MapleGenericPortal.java +++ b/src/server/maps/MapleGenericPortal.java @@ -22,6 +22,8 @@ along with this program. If not, see . package server.maps; import client.MapleClient; +import client.MapleCharacter; +import constants.GameConstants; import java.awt.Point; import scripting.portal.PortalScriptManager; import server.MaplePortal; @@ -140,13 +142,18 @@ public class MapleGenericPortal implements MaplePortal { npe.printStackTrace(); } } else if (getTargetMapId() != 999999999) { - MapleMap to = c.getPlayer().getEventInstance() == null ? c.getChannelServer().getMapFactory().getMap(getTargetMapId()) : c.getPlayer().getEventInstance().getMapInstance(getTargetMapId()); - MaplePortal pto = to.getPortal(getTarget()); - if (pto == null) {// fallback for missing portals - no real life case anymore - interesting for not implemented areas - pto = to.getPortal(0); + MapleCharacter chr = c.getPlayer(); + if (!(chr.getChalkboard() != null && GameConstants.isFreeMarketRoom(getTargetMapId()))) { + MapleMap to = chr.getEventInstance() == null ? c.getChannelServer().getMapFactory().getMap(getTargetMapId()) : chr.getEventInstance().getMapInstance(getTargetMapId()); + MaplePortal pto = to.getPortal(getTarget()); + if (pto == null) {// fallback for missing portals - no real life case anymore - interesting for not implemented areas + pto = to.getPortal(0); + } + chr.changeMap(to, pto); //late resolving makes this harder but prevents us from loading the whole world at once + changed = true; + } else { + chr.dropMessage(5, "You cannot enter this map with the chalkboard opened."); } - c.getPlayer().changeMap(to, pto); //late resolving makes this harder but prevents us from loading the whole world at once - changed = true; } if (!changed) { c.announce(MaplePacketCreator.enableActions()); diff --git a/src/server/maps/MapleHiredMerchant.java b/src/server/maps/MapleHiredMerchant.java index 0f16f494e7..0fca0338c7 100644 --- a/src/server/maps/MapleHiredMerchant.java +++ b/src/server/maps/MapleHiredMerchant.java @@ -29,6 +29,7 @@ import client.inventory.MapleInventory; import client.inventory.MapleInventoryType; import client.inventory.manipulator.MapleInventoryManipulator; import client.inventory.manipulator.MapleKarmaManipulator; +import client.processor.FredrickProcessor; import com.mysql.jdbc.Statement; import constants.ServerConstants; import java.sql.Connection; @@ -191,6 +192,14 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { visitorLock.unlock(); } } + + private void removeOwner(MapleCharacter owner) { + if (owner.getHiredMerchant() == this) { + owner.announce(MaplePacketCreator.hiredMerchantOwnerLeave()); + owner.announce(MaplePacketCreator.leaveHiredMerchant(0x00, 0x03)); + owner.setHiredMerchant(null); + } + } public void withdrawMesos(MapleCharacter chr) { if (isOwner(chr)) { @@ -220,6 +229,10 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { removeFromSlot(slot); chr.announce(MaplePacketCreator.updateHiredMerchant(this, chr)); } + + if (ServerConstants.USE_ENFORCE_MERCHANT_SAVE) { + chr.saveCharToDB(false); + } } } @@ -228,15 +241,17 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { } private int getQuantityLeft(int itemid) { - int count = 0; - - for (MaplePlayerShopItem mpsi : items) { - if (mpsi.getItem().getItemId() == itemid) { - count += (mpsi.getBundles() * mpsi.getItem().getQuantity()); + synchronized (items) { + int count = 0; + + for (MaplePlayerShopItem mpsi : items) { + if (mpsi.getItem().getItemId() == itemid) { + count += (mpsi.getBundles() * mpsi.getItem().getQuantity()); + } } + + return count; } - - return count; } public void buy(MapleClient c, int item, short quantity) { @@ -291,10 +306,14 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { } } } else { - c.getPlayer().dropMessage(1, "Your inventory is full. Please clean a slot before buying this item."); + c.getPlayer().dropMessage(1, "Your inventory is full. Please clear a slot before buying this item."); + c.announce(MaplePacketCreator.enableActions()); + return; } } else { c.getPlayer().dropMessage(1, "You don't have enough mesos to purchase this item."); + c.announce(MaplePacketCreator.enableActions()); + return; } try { this.saveItems(false); @@ -365,17 +384,18 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { public void closeOwnerMerchant(MapleCharacter chr) { if(this.isOwner(chr)) { - chr.announce(MaplePacketCreator.hiredMerchantOwnerLeave()); - chr.announce(MaplePacketCreator.leaveHiredMerchant(0x00, 0x03)); this.closeShop(chr.getClient(), false); chr.setHasMerchant(false); } } - public void closeShop(MapleClient c, boolean timeout) { + private void closeShop(MapleClient c, boolean timeout) { map.removeMapObject(this); map.broadcastMessage(MaplePacketCreator.removeHiredMerchantBox(ownerId)); c.getChannelServer().removeHiredMerchant(ownerId); + + this.removeAllVisitors(); + this.removeOwner(c.getPlayer()); try { MapleCharacter player = c.getWorldServer().getPlayerStorage().getCharacterById(ownerId); @@ -412,6 +432,10 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { } catch (Exception e) { e.printStackTrace(); } + + if (ServerConstants.USE_ENFORCE_MERCHANT_SAVE) { + c.getPlayer().saveCharToDB(false); + } synchronized (items) { items.clear(); @@ -419,7 +443,7 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { } catch (Exception e) { e.printStackTrace(); } - + Server.getInstance().getWorld(world).unregisterHiredMerchant(this); } @@ -611,6 +635,8 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { Connection con = DatabaseConnection.getConnection(); ItemFactory.MERCHANT.saveItems(itemsWithType, bundles, this.ownerId, con); con.close(); + + FredrickProcessor.insertFredrickLog(this.ownerId); } private static boolean check(MapleCharacter chr, List items) { @@ -629,8 +655,12 @@ public class MapleHiredMerchant extends AbstractMapleMapObject { return channel; } - public int getTimeLeft() { - return (int) ((System.currentTimeMillis() - start) / 1000); + public int getTimeOpen() { + double openTime = (System.currentTimeMillis() - start) / 60000; + openTime /= 1440; // heuristics since engineered method to count time here is unknown + openTime *= 1318; + + return (int) Math.ceil(openTime); } public void clearMessages() { diff --git a/src/server/maps/MapleMap.java b/src/server/maps/MapleMap.java index c0e6908dea..2f2e7026b1 100644 --- a/src/server/maps/MapleMap.java +++ b/src/server/maps/MapleMap.java @@ -31,6 +31,7 @@ import client.inventory.MapleInventoryType; import client.inventory.MaplePet; import client.status.MonsterStatus; import client.status.MonsterStatusEffect; +import constants.GameConstants; import constants.ItemConstants; import constants.ServerConstants; import java.awt.Point; @@ -150,6 +151,8 @@ public class MapleMap { private Pair timeMob = null; private short mobInterval = 5000; private boolean allowSummons = true; // All maps should have this true at the beginning + private MapleCharacter mapOwner = null; + private long mapOwnerLastActivityTime = Long.MAX_VALUE; // HPQ private int riceCakes = 0; @@ -1088,6 +1091,18 @@ public class MapleMap { mdrop.unlockItem(); } } + + public final void disappearingMesoDrop(final int meso, final MapleMapObject dropper, final MapleCharacter owner, final Point pos) { + final Point droppos = calcDropPos(pos, pos); + final MapleMapItem mdrop = new MapleMapItem(meso, droppos, dropper, owner, owner.getClient(), (byte) 1, false); + + mdrop.lockItem(); + try { + broadcastItemDropMessage(mdrop, dropper.getPosition(), droppos, (byte) 3, mdrop.getPosition()); + } finally { + mdrop.unlockItem(); + } + } public MapleMonster getMonsterById(int id) { objectRLock.lock(); @@ -2254,23 +2269,13 @@ public class MapleMap { } public MapleCharacter getAnyCharacterFromParty(int partyid) { - chrRLock.lock(); - try { - Set list = mapParty.get(partyid); - if(list == null) return null; - - for(Integer cid : list) { - for (MapleCharacter c : this.characters) { - if (c.getId() == cid) { - return c; - } - } + for (MapleCharacter chr : this.getAllPlayers()) { + if (chr.getPartyId() == partyid) { + return chr; } - - return null; - } finally { - chrRLock.unlock(); } + + return null; } private void addPartyMemberInternal(MapleCharacter chr) { @@ -2451,6 +2456,14 @@ public class MapleMap { chr.removeSandboxItems(); + if (chr.getChalkboard() != null) { + if (!GameConstants.isFreeMarketRoom(mapid)) { + chr.announce(MaplePacketCreator.useChalkboard(chr, false)); // update player's chalkboard when changing maps found thanks to Vcoc + } else { + chr.setChalkboard(null); + } + } + if (chr.isHidden()) { broadcastGMSpawnPlayerMapObjectMessage(chr, chr, true); chr.announce(MaplePacketCreator.getGMEffect(0x10, (byte) 1)); @@ -2479,10 +2492,11 @@ public class MapleMap { } finally { objectWLock.unlock(); } + if (chr.getPlayerShop() != null) { addMapObject(chr.getPlayerShop()); } - + final MapleDragon dragon = chr.getDragon(); if (dragon != null) { dragon.setPosition(chr.getPosition()); @@ -2557,6 +2571,19 @@ public class MapleMap { return portal != null ? portal : getPortal(0); } + public MaplePortal findClosestWarpPortal(Point from) { + MaplePortal closest = null; + double shortestDistance = Double.POSITIVE_INFINITY; + for (MaplePortal portal : portals.values()) { + double distance = portal.getPosition().distanceSq(from); + if (portal.getType() == MaplePortal.MAP_PORTAL && distance < shortestDistance && portal.getTargetMapId() == 999999999) { + closest = portal; + shortestDistance = distance; + } + } + return closest; + } + public MaplePortal findClosestPlayerSpawnpoint(Point from) { MaplePortal closest = null; double shortestDistance = Double.POSITIVE_INFINITY; @@ -4021,6 +4048,58 @@ public class MapleMap { } } + public boolean claimOwnership(MapleCharacter chr) { + if (mapOwner == null) { + mapOwner = chr; + mapOwnerLastActivityTime = Server.getInstance().getCurrentTime(); + + getChannelServer().registerOwnedMap(this); + return true; + } else { + return chr == mapOwner; + } + } + + public boolean unclaimOwnership(MapleCharacter chr) { + if (mapOwner == chr) { + mapOwner = null; + mapOwnerLastActivityTime = Long.MAX_VALUE; + + getChannelServer().unregisterOwnedMap(this); + return true; + } else { + return false; + } + } + + private void refreshOwnership() { + mapOwnerLastActivityTime = Server.getInstance().getCurrentTime(); + } + + public boolean isOwnershipRestricted(MapleCharacter chr) { + MapleCharacter owner = mapOwner; + + if (owner != null) { + if (owner != chr && !owner.isPartyMember(chr)) { // thanks Vcoc & BHB for suggesting the map ownership feature + chr.showMapOwnershipInfo(owner); + return true; + } else { + this.refreshOwnership(); + } + } + + return false; + } + + public void checkMapOwnerActivity() { + long timeNow = Server.getInstance().getCurrentTime(); + if (timeNow - mapOwnerLastActivityTime > 60000) { + if (unclaimOwnership(mapOwner)) { + this.dropMessage(5, "This lawn is now free real estate."); + } + } + } + public void dispose() { for(MapleMonster mm : this.getMonsters()) { mm.dispose(); diff --git a/src/server/maps/MaplePlayerShop.java b/src/server/maps/MaplePlayerShop.java index 965f413651..43ba5746f9 100644 --- a/src/server/maps/MaplePlayerShop.java +++ b/src/server/maps/MaplePlayerShop.java @@ -146,6 +146,7 @@ public class MaplePlayerShop extends AbstractMapleMapObject { try { for (int i = 0; i < 3; i++) { if (visitors[i] != null && visitors[i].getId() == visitor.getId()) { + visitors[i].setPlayerShop(null); visitors[i] = null; visitor.setSlot(-1); @@ -298,7 +299,7 @@ public class MaplePlayerShop extends AbstractMapleMapObject { } } } else { - c.getPlayer().dropMessage(1, "Your inventory is full. Please clean a slot before buying this item."); + c.getPlayer().dropMessage(1, "Your inventory is full. Please clear a slot before buying this item."); c.announce(MaplePacketCreator.enableActions()); return false; } diff --git a/src/tools/FilePrinter.java b/src/tools/FilePrinter.java index f9e5feee6a..a6c40a9e88 100644 --- a/src/tools/FilePrinter.java +++ b/src/tools/FilePrinter.java @@ -28,6 +28,7 @@ public class FilePrinter { PACKET_LOG = "game/Log.txt", CASHITEM_BOUGHT = "interactions/CashLog.txt", EXCEPTION = "game/Exceptions.txt", + LOGIN_EXCEPTION = "game/LoginExceptions.txt", TRADE_EXCEPTION = "game/TradeExceptions.txt", SQL_EXCEPTION = "game/SqlExceptions.txt", PACKET_HANDLER = "game/packethandler/", diff --git a/src/tools/MaplePacketCreator.java b/src/tools/MaplePacketCreator.java index e26d6370d6..b0bd968959 100644 --- a/src/tools/MaplePacketCreator.java +++ b/src/tools/MaplePacketCreator.java @@ -1065,7 +1065,7 @@ public class MaplePacketCreator { mplew.writeShort(chr.getHp()); mplew.writeBool(false); mplew.writeLong(getTime(Server.getInstance().getCurrentTime())); - mplew.skip(14); + mplew.skip(18); return mplew.getPacket(); } @@ -1082,7 +1082,7 @@ public class MaplePacketCreator { mplew.writeInt(spawnPosition.x); // spawn position placement thanks to Arnah (Vertisy) mplew.writeInt(spawnPosition.y); mplew.writeLong(getTime(Server.getInstance().getCurrentTime())); - mplew.skip(14); + mplew.skip(18); return mplew.getPacket(); } @@ -3175,7 +3175,7 @@ public class MaplePacketCreator { return mplew.getPacket(); } - public static byte[] getTradeInvite(MapleCharacter c) { + public static byte[] tradeInvite(MapleCharacter c) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); mplew.writeShort(SendOpcode.PLAYER_INTERACTION.getValue()); mplew.write(PlayerInteractionHandler.Action.INVITE.getCode()); @@ -3309,21 +3309,22 @@ public class MaplePacketCreator { return mplew.getPacket(); } - public static byte[] getTradeCompletion(byte number) { + /** + * Possible values for operation:
2: Trade cancelled by the + * other character
7: Trade successful
8: Trade unsuccessful
+ * 9: Cannot carry more one-of-a-kind items
12: Cannot trade on different maps
+ * 13: Cannot trade, game files damaged
+ * + * @param number + * @param operation + * @return + */ + public static byte[] getTradeResult(byte number, byte operation) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(5); mplew.writeShort(SendOpcode.PLAYER_INTERACTION.getValue()); mplew.write(PlayerInteractionHandler.Action.EXIT.getCode()); mplew.write(number); - mplew.write(6); - return mplew.getPacket(); - } - - public static byte[] getTradeCancel(byte number) { - final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(5); - mplew.writeShort(SendOpcode.PLAYER_INTERACTION.getValue()); - mplew.write(PlayerInteractionHandler.Action.EXIT.getCode()); - mplew.write(number); - mplew.write(2); + mplew.write(operation); return mplew.getPacket(); } @@ -3783,11 +3784,9 @@ public class MaplePacketCreator { * party. 13: You have yet to join a party. * 16: Already have joined a party. 17: The party you're trying to join is * already in full capacity. 19: Unable to find the requested character in - * this channel. 21: Player is blocking any party invitations. 22: Player - * is taking care of another invitation. 23: Player denied request. - * 25: Cannot kick another user in this map. 28/29: Leadership can only be - * given to a party member in the vicinity. 30: Change leadership only on - * same channel. + * this channel. 25: Cannot kick another user in this map. 28/29: Leadership + * can only be given to a party member in the vicinity. 30: Change leadership + * only on same channel. * * @param message * @return @@ -3800,7 +3799,8 @@ public class MaplePacketCreator { } /** - * 23: 'Char' have denied request to the party. + * 21: Player is blocking any party invitations, 22: Player is taking care of + * another invitation, 23: Player have denied request to the party. * * @param message * @param charname @@ -4372,25 +4372,43 @@ public class MaplePacketCreator { } /** - * 'Char' has denied your guild invitation. + * Gets a Heracle/guild message packet. * - * @param charname - * @return + * Possible values for code:
28: guild name already in use
+ * 31: problem in locating players during agreement
33/40: already joined a guild
+ * 35: Cannot make guild
36: problem in player agreement
38: problem during forming guild
+ * 41: max number of players in joining guild
42: character can't be found this channel
+ * 45/48: character not in guild
52: problem in disbanding guild
56: admin cannot make guild
+ * 57: problem in increasing guild size
+ * + * + * @param code The response code. + * @return The guild message packet. */ - public static byte[] denyGuildInvitation(String charname) { - final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); - mplew.writeShort(SendOpcode.GUILD_OPERATION.getValue()); - mplew.write(0x37); - mplew.writeMapleAsciiString(charname); - return mplew.getPacket(); - } - public static byte[] genericGuildMessage(byte code) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); mplew.writeShort(SendOpcode.GUILD_OPERATION.getValue()); mplew.write(code); return mplew.getPacket(); } + + /** + * Gets a guild message packet appended with target name. + * + * 53: player not accepting guild invites
+ * 54: player already managing an invite
55: player denied an invite
+ * + * @param code The response code. + * @param targetName The initial player target of the invitation. + * @return The guild message packet. + */ + public static byte[] responseGuildMessage(byte code, String targetName) { + final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); + mplew.writeShort(SendOpcode.GUILD_OPERATION.getValue()); + mplew.write(code); + mplew.writeMapleAsciiString(targetName); + return mplew.getPacket(); + } public static byte[] newGuildMember(MapleGuildCharacter mgc) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); @@ -4937,7 +4955,7 @@ public class MaplePacketCreator { return mplew.getPacket(); } - public static byte[] skillBookSuccess(MapleCharacter chr, int skillid, int maxlevel, boolean canuse, boolean success) { + public static byte[] skillBookResult(MapleCharacter chr, int skillid, int maxlevel, boolean canuse, boolean success) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); mplew.writeShort(SendOpcode.SKILL_LEARN_ITEM_RESULT.getValue()); mplew.writeInt(chr.getId()); @@ -5378,7 +5396,7 @@ public class MaplePacketCreator { mplew.writeInt(9030000); // Fredrick mplew.writeInt(32272); //id mplew.skip(5); - mplew.writeInt(chr.getMerchantMeso()); + mplew.writeInt(chr.getMerchantNetMeso()); mplew.write(0); try { List> items = ItemFactory.MERCHANT.loadItems(chr.getId(), false); @@ -5578,7 +5596,8 @@ public class MaplePacketCreator { } mplew.writeMapleAsciiString(hm.getOwner()); if (hm.isOwner(chr)) { - mplew.writeInt(hm.getTimeLeft()); + mplew.writeShort(0); + mplew.writeShort(hm.getTimeOpen()); mplew.write(firstTime ? 1 : 0); List sold = hm.getSold(); mplew.write(sold.size()); @@ -6348,6 +6367,16 @@ public class MaplePacketCreator { mplew.writeInt(1); return mplew.getPacket(); } + + public static byte[] showForeignInfo(int cid, String path) { + final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); + mplew.writeShort(SendOpcode.SHOW_FOREIGN_EFFECT.getValue()); + mplew.writeInt(cid); + mplew.write(0x17); + mplew.writeMapleAsciiString(path); + mplew.writeInt(1); + return mplew.getPacket(); + } /** * Sends a UI utility. 0x01 - Equipment Inventory. 0x02 - Stat Window. 0x03 @@ -6875,7 +6904,7 @@ public class MaplePacketCreator { return mplew.getPacket(); } - public static byte[] sendAllianceInvitation(int allianceid, MapleCharacter chr) { + public static byte[] allianceInvite(int allianceid, MapleCharacter chr) { final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter(); mplew.writeShort(SendOpcode.ALLIANCE_OPERATION.getValue()); mplew.write(0x03); diff --git a/src/scripting/event/EventInstanceInProgressException.java b/src/tools/exceptions/EventInstanceInProgressException.java similarity index 97% rename from src/scripting/event/EventInstanceInProgressException.java rename to src/tools/exceptions/EventInstanceInProgressException.java index 128429562d..3a6d750908 100644 --- a/src/scripting/event/EventInstanceInProgressException.java +++ b/src/tools/exceptions/EventInstanceInProgressException.java @@ -17,7 +17,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package scripting.event; +package tools.exceptions; /** diff --git a/src/tools/packets/Fishing.java b/src/tools/packets/Fishing.java new file mode 100644 index 0000000000..b7d1c7c90a --- /dev/null +++ b/src/tools/packets/Fishing.java @@ -0,0 +1,199 @@ +/* + 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 tools.packets; + +import client.MapleCharacter; +import constants.GameConstants; +import constants.ItemConstants; +import constants.ServerConstants; +import server.MapleItemInformationProvider; +import tools.MaplePacketCreator; + +import java.util.Calendar; + +/** + * + * @author FateJiki (RaGeZONE) + * @author Ronan - timing pattern + */ +public class Fishing { + + private static double getFishingLikelihood(int x) { + return 50.0 + 7.0 * (7.0 * Math.sin(x)) * (Math.cos(Math.pow(x, 0.777))); + } + + public static double[] fetchFishingLikelihood() { + Calendar calendar = Calendar.getInstance(); + int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR); + + int hours = calendar.get(Calendar.HOUR); + int minutes = calendar.get(Calendar.MINUTE); + int seconds = calendar.get(Calendar.SECOND); + + double yearLikelihood = getFishingLikelihood(dayOfYear); + double timeLikelihood = getFishingLikelihood(hours + minutes + seconds); + + return new double[]{yearLikelihood, timeLikelihood}; + } + + private static boolean hitFishingTime(MapleCharacter chr, int baitLevel, double yearLikelihood, double timeLikelihood) { + double baitLikelihood = 0.0002 * chr.getWorldServer().getFishingRate() * baitLevel; // can improve 10.0 at "max level 50000" on rate 1x + + if (ServerConstants.USE_DEBUG) { + chr.dropMessage(5, "----- FISHING RESULT -----"); + chr.dropMessage(5, "Likelihoods - Year: " + yearLikelihood + " Time: " + timeLikelihood + " Meso: " + baitLikelihood); + chr.dropMessage(5, "Score rolls - Year: " + (0.23 * yearLikelihood) + " Time: " + (0.77 * timeLikelihood) + " Meso: " + baitLikelihood); + } + + return (0.23 * yearLikelihood) + (0.77 * timeLikelihood) + (baitLikelihood) > 57.777; + } + + public static void doFishing(MapleCharacter chr, int baitLevel, double yearLikelihood, double timeLikelihood){ + // thanks Fadi, Vcoc for suggesting a custom fishing system + + if (!chr.isLoggedinWorld() || !chr.isAlive()) { + return; + } + + if (!GameConstants.isFishingArea(chr.getMapId())) { + chr.dropMessage("You are not in a fishing area!"); + return; + } + + if (chr.getLevel() < 30) { + chr.dropMessage(5, "You must be above level 30 to fish!"); + return; + } + + String fishingEffect; + if (!hitFishingTime(chr, baitLevel, yearLikelihood, timeLikelihood)) { + fishingEffect = "Effect/BasicEff.img/Catch/Fail"; + } else { + String rewardStr = ""; + fishingEffect = "Effect/BasicEff.img/Catch/Success"; + + int rand = (int)(3.0 * Math.random()); + switch(rand){ + case 0: + int mesoAward = (int)(1400.0 * Math.random() + 1201) * chr.getMesoRate() + (15 * chr.getLevel() / 5); + chr.gainMeso(mesoAward, true, true, true); + + rewardStr = mesoAward + " mesos."; + break; + case 1: + int expAward = (int)(645.0 * Math.random() + 620.0) * chr.getExpRate() + (15 * chr.getLevel() / 4); + chr.gainExp(expAward, true, true); + + rewardStr = expAward + " EXP."; + break; + case 2: + int itemid = getRandomItem(); + rewardStr = "a(n) " + MapleItemInformationProvider.getInstance().getName(itemid) + "."; + + if (chr.canHold(itemid)) { + chr.getClient().getAbstractPlayerInteraction().gainItem(itemid, true); + } else { + chr.showHint("Couldn't catch a(n) #r" + MapleItemInformationProvider.getInstance().getName(itemid) + "#k due to #e#b" + ItemConstants.getInventoryType(itemid) + "#k#n inventory limit."); + rewardStr += ".. but has goofed up due to full inventory."; + } + break; + } + + chr.getMap().dropMessage(6, chr.getName() + " found " + rewardStr); + } + + chr.announce(MaplePacketCreator.showInfo(fishingEffect)); + chr.getMap().broadcastMessage(chr, MaplePacketCreator.showForeignInfo(chr.getId(), fishingEffect), false); + } + + public static int getRandomItem(){ + int rand = (int)(100.0 * Math.random()); + int[] commons = {1002851, 2002020, 2002020, 2000006, 2000018, 2002018, 2002024, 2002027, 2002027, 2000018, 2000018, 2000018 , 2000018, 2002030, 2002018, 2000016}; // filler' up + int[] uncommons = {1000025, 1002662, 1002812, 1002850, 1002881, 1002880, 1012072, 4020009, 2043220, 2043022, 2040543, 2044420, 2040943, 2043713, 2044220, 2044120, 2040429, 2043220, 2040943}; // filler' uptoo + int[] rares = {1002859, 1002553, 01002762, 01002763, 01002764, 01002765, 01002766, 01002663, 1002788, 1002949, 2049100, 2340000, 2040822,2040822,2040822,2040822,2040822,2040822,2040822,2040822}; // filler' uplast + + if(rand >= 25){ + return commons[(int)(commons.length * Math.random())]; + } else if(rand <= 7 && rand >= 4){ + return uncommons[(int)(uncommons.length * Math.random())]; + } else { + return rares[(int)(rares.length * Math.random())]; + } + } + + private static void debugFishingLikelihood() { + long a[] = new long[365], b[] = new long[365]; + long hits = 0, hits10 = 0, total = 0; + + for (int i = 0; i < 365; i++) { + double yearLikelihood = getFishingLikelihood(i); + + int dayHits = 0, dayHits10 = 0; + for (int k = 0; k < 24; k++) { + for (int l = 0; l < 60; l++) { + for (int m = 0; m < 60; m++) { + double timeLikelihood = getFishingLikelihood(k + l + m); + + if ((0.23 * yearLikelihood) + (0.77 * timeLikelihood) > 57.777) { + hits++; + dayHits++; + } + + if ((0.23 * yearLikelihood) + (0.77 * timeLikelihood) + 10.0 > 57.777) { + hits10++; + dayHits10++; + } + + total++; + } + } + } + + a[i] = dayHits; + b[i] = dayHits10; + } + + long maxhit = 0, minhit = Long.MAX_VALUE; + for (int i = 0; i < 365; i++) { + if (maxhit < a[i]) { + maxhit = a[i]; + } + + if (minhit > a[i]) { + minhit = a[i]; + } + } + + long maxhit10 = 0, minhit10 = Long.MAX_VALUE; + for (int i = 0; i < 365; i++) { + if (maxhit10 < b[i]) { + maxhit10 = b[i]; + } + + if (minhit10 > b[i]) { + minhit10 = b[i]; + } + } + + System.out.println("Diary min " + minhit + " max " + maxhit); + System.out.println("Diary10 min " + minhit10 + " max " + maxhit10); + System.out.println("Hits: " + hits + "Hits10: " + hits10 + " Total: " + total + " -- %1000: " + (hits * 1000 / total) + ", +10 %1000: " + (hits10 * 1000 / total)); + } +} diff --git a/wz/Effect.wz/BasicEff.img.xml b/wz/Effect.wz/BasicEff.img.xml index 035e4ba7f2..5e0137afbd 100644 --- a/wz/Effect.wz/BasicEff.img.xml +++ b/wz/Effect.wz/BasicEff.img.xml @@ -247,9 +247,28 @@ - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/wz/Quest.wz/Act.img.xml b/wz/Quest.wz/Act.img.xml index 565a5f4226..b257813231 100644 --- a/wz/Quest.wz/Act.img.xml +++ b/wz/Quest.wz/Act.img.xml @@ -15075,6 +15075,10 @@ + + + + @@ -15288,6 +15292,10 @@ + + + + @@ -15501,6 +15509,12 @@ + + + + + + @@ -15513,6 +15527,10 @@ + + + + @@ -15736,6 +15754,12 @@ + + + + + + @@ -15748,6 +15772,10 @@ + + + + @@ -15934,10 +15962,22 @@ + + + + + + + + + + + + @@ -15950,6 +15990,10 @@ + + + + diff --git a/wz/Quest.wz/Check.img.xml b/wz/Quest.wz/Check.img.xml index 64365fca39..30c363625a 100644 --- a/wz/Quest.wz/Check.img.xml +++ b/wz/Quest.wz/Check.img.xml @@ -29056,6 +29056,12 @@ + + + + + + @@ -29715,6 +29721,12 @@ + + + + + + @@ -30367,6 +30379,12 @@ + + + + + + @@ -30968,6 +30986,12 @@ + + + + + + @@ -31542,6 +31566,12 @@ + + + + + +