Advertisement
Guest User

Untitled

a guest
Jun 13th, 2018
149
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 174.84 KB | None | 0 0
  1. #########################################################################
  2. #  OpenKore - Server message parsing
  3. #
  4. #  This software is open source, licensed under the GNU General Public
  5. #  License, version 2.
  6. #  Basically, this means that you're allowed to modify and distribute
  7. #  this software. However, if you distribute modified versions, you MUST
  8. #  also distribute the source code.
  9. #  See http://www.gnu.org/licenses/gpl.html for the full license.
  10. #########################################################################
  11. ##
  12. # MODULE DESCRIPTION: Server message parsing
  13. #
  14. # This class is responsible for parsing messages that are sent by the RO
  15. # server to Kore. Information in the messages are stored in global variables
  16. # (in the module Globals).
  17. #
  18. # Please also read <a href="http://wiki.openkore.com/index.php/Network_subsystem">the
  19. # network subsystem overview.</a>
  20. package Network::Receive;
  21.  
  22. use strict;
  23. use Exporter;
  24. use Network::PacketParser; # import
  25. use base qw(Network::PacketParser);
  26. use utf8;
  27. use Carp::Assert;
  28. use Scalar::Util;
  29. use Socket qw(inet_aton inet_ntoa);
  30.  
  31. use AI;
  32. use Globals;
  33. use Field;
  34. #use Settings;
  35. use Log qw(message warning error debug);
  36. use FileParsers qw(updateMonsterLUT updateNPCLUT);
  37. use I18N qw(bytesToString stringToBytes);
  38. use Interface;
  39. use Network;
  40. use Network::MessageTokenizer;
  41. use Misc;
  42. use Plugins;
  43. use Utils;
  44. use Utils::Exceptions;
  45. use Utils::Crypton;
  46. use Translation;
  47.  
  48. our %EXPORT_TAGS = (
  49.     actor_type => [qw(PC_TYPE NPC_TYPE ITEM_TYPE SKILL_TYPE UNKNOWN_TYPE NPC_MOB_TYPE NPC_EVT_TYPE NPC_PET_TYPE NPC_HO_TYPE NPC_MERSOL_TYPE
  50.                         NPC_ELEMENTAL_TYPE)],
  51.     connection => [qw(REFUSE_INVALID_ID REFUSE_INVALID_PASSWD REFUSE_ID_EXPIRED ACCEPT_ID_PASSWD REFUSE_NOT_CONFIRMED REFUSE_INVALID_VERSION
  52.                         REFUSE_BLOCK_TEMPORARY REFUSE_BILLING_NOT_READY REFUSE_NONSAKRAY_ID_BLOCKED REFUSE_BAN_BY_DBA
  53.                         REFUSE_EMAIL_NOT_CONFIRMED REFUSE_BAN_BY_GM REFUSE_TEMP_BAN_FOR_DBWORK REFUSE_SELF_LOCK REFUSE_NOT_PERMITTED_GROUP
  54.                         REFUSE_WAIT_FOR_SAKRAY_ACTIVE REFUSE_NOT_CHANGED_PASSWD REFUSE_BLOCK_INVALID REFUSE_WARNING REFUSE_NOT_OTP_USER_INFO
  55.                         REFUSE_OTP_AUTH_FAILED REFUSE_SSO_AUTH_FAILED REFUSE_NOT_ALLOWED_IP_ON_TESTING REFUSE_OVER_BANDWIDTH
  56.                         REFUSE_OVER_USERLIMIT REFUSE_UNDER_RESTRICTION REFUSE_BY_OUTER_SERVER REFUSE_BY_UNIQUESERVER_CONNECTION
  57.                         REFUSE_BY_AUTHSERVER_CONNECTION REFUSE_BY_BILLSERVER_CONNECTION REFUSE_BY_AUTH_WAITING REFUSE_DELETED_ACCOUNT
  58.                         REFUSE_ALREADY_CONNECT REFUSE_TEMP_BAN_HACKING_INVESTIGATION REFUSE_TEMP_BAN_BUG_INVESTIGATION
  59.                         REFUSE_TEMP_BAN_DELETING_CHAR REFUSE_TEMP_BAN_DELETING_SPOUSE_CHAR REFUSE_USER_PHONE_BLOCK
  60.                         ACCEPT_LOGIN_USER_PHONE_BLOCK ACCEPT_LOGIN_CHILD REFUSE_IS_NOT_FREEUSER REFUSE_INVALID_ONETIMELIMIT
  61.                         REFUSE_CHANGE_PASSWD_FORCE REFUSE_OUTOFDATE_PASSWORD REFUSE_NOT_CHANGE_ACCOUNTID REFUSE_NOT_CHANGE_CHARACTERID
  62.                         REFUSE_SSO_AUTH_BLOCK_USER REFUSE_SSO_AUTH_GAME_APPLY REFUSE_SSO_AUTH_INVALID_GAMENUM REFUSE_SSO_AUTH_INVALID_USER
  63.                         REFUSE_SSO_AUTH_OTHERS REFUSE_SSO_AUTH_INVALID_AGE REFUSE_SSO_AUTH_INVALID_MACADDRESS REFUSE_SSO_AUTH_BLOCK_ETERNAL
  64.                         REFUSE_SSO_AUTH_BLOCK_ACCOUNT_STEAL REFUSE_SSO_AUTH_BLOCK_BUG_INVESTIGATION REFUSE_SSO_NOT_PAY_USER
  65.                         REFUSE_SSO_ALREADY_LOGIN_USER REFUSE_SSO_CURRENT_USED_USER REFUSE_SSO_OTHER_1 REFUSE_SSO_DROP_USER
  66.                         REFUSE_SSO_NOTHING_USER REFUSE_SSO_OTHER_2 REFUSE_SSO_WRONG_RATETYPE_1 REFUSE_SSO_EXTENSION_PCBANG_TIME
  67.                         REFUSE_SSO_WRONG_RATETYPE_2)],
  68.     party_invite => [qw(ANSWER_ALREADY_OTHERGROUPM ANSWER_JOIN_REFUSE ANSWER_JOIN_ACCEPT ANSWER_MEMBER_OVERSIZE ANSWER_DUPLICATE
  69.                         ANSWER_JOINMSG_REFUSE ANSWER_UNKNOWN_ERROR ANSWER_UNKNOWN_CHARACTER ANSWER_INVALID_MAPPROPERTY)],
  70.     party_leave => [qw(GROUPMEMBER_DELETE_LEAVE GROUPMEMBER_DELETE_EXPEL)],
  71. );
  72.  
  73. our @EXPORT = (
  74.     @{$EXPORT_TAGS{actor_type}},
  75.     @{$EXPORT_TAGS{connection}},
  76.     @{$EXPORT_TAGS{party_invite}},
  77.     @{$EXPORT_TAGS{party_leave}},
  78. );
  79.  
  80. # object_type constants for &actor_display
  81. use constant {
  82.     PC_TYPE => 0x0,
  83.     NPC_TYPE => 0x1,
  84.     ITEM_TYPE => 0x2,
  85.     SKILL_TYPE => 0x3,
  86.     UNKNOWN_TYPE => 0x4,
  87.     NPC_MOB_TYPE => 0x5,
  88.     NPC_EVT_TYPE => 0x6,
  89.     NPC_PET_TYPE => 0x7,
  90.     NPC_HO_TYPE => 0x8,
  91.     NPC_MERSOL_TYPE => 0x9,
  92.     NPC_ELEMENTAL_TYPE => 0xa
  93. };
  94.  
  95. use constant {
  96.     REFUSE_INVALID_ID => 0x0,
  97.     REFUSE_INVALID_PASSWD => 0x1,
  98.     REFUSE_ID_EXPIRED => 0x2,
  99.     ACCEPT_ID_PASSWD => 0x3,
  100.     REFUSE_NOT_CONFIRMED => 0x4,
  101.     REFUSE_INVALID_VERSION => 0x5,
  102.     REFUSE_BLOCK_TEMPORARY => 0x6,
  103.     REFUSE_BILLING_NOT_READY => 0x7,
  104.     REFUSE_NONSAKRAY_ID_BLOCKED => 0x8,
  105.     REFUSE_BAN_BY_DBA => 0x9,
  106.     REFUSE_EMAIL_NOT_CONFIRMED => 0xa,
  107.     REFUSE_BAN_BY_GM => 0xb,
  108.     REFUSE_TEMP_BAN_FOR_DBWORK => 0xc,
  109.     REFUSE_SELF_LOCK => 0xd,
  110.     REFUSE_NOT_PERMITTED_GROUP => 0xe,
  111.     REFUSE_WAIT_FOR_SAKRAY_ACTIVE => 0xf,
  112.     REFUSE_NOT_CHANGED_PASSWD => 0x10,
  113.     REFUSE_BLOCK_INVALID => 0x11,
  114.     REFUSE_WARNING => 0x12,
  115.     REFUSE_NOT_OTP_USER_INFO => 0x13,
  116.     REFUSE_OTP_AUTH_FAILED => 0x14,
  117.     REFUSE_SSO_AUTH_FAILED => 0x15,
  118.     REFUSE_NOT_ALLOWED_IP_ON_TESTING => 0x16,
  119.     REFUSE_OVER_BANDWIDTH => 0x17,
  120.     REFUSE_OVER_USERLIMIT => 0x18,
  121.     REFUSE_UNDER_RESTRICTION => 0x19,
  122.     REFUSE_BY_OUTER_SERVER => 0x1a,
  123.     REFUSE_BY_UNIQUESERVER_CONNECTION => 0x1b,
  124.     REFUSE_BY_AUTHSERVER_CONNECTION => 0x1c,
  125.     REFUSE_BY_BILLSERVER_CONNECTION => 0x1d,
  126.     REFUSE_BY_AUTH_WAITING => 0x1e,
  127.     REFUSE_DELETED_ACCOUNT => 0x63,
  128.     REFUSE_ALREADY_CONNECT => 0x64,
  129.     REFUSE_TEMP_BAN_HACKING_INVESTIGATION => 0x65,
  130.     REFUSE_TEMP_BAN_BUG_INVESTIGATION => 0x66,
  131.     REFUSE_TEMP_BAN_DELETING_CHAR => 0x67,
  132.     REFUSE_TEMP_BAN_DELETING_SPOUSE_CHAR => 0x68,
  133.     REFUSE_USER_PHONE_BLOCK => 0x69,
  134.     ACCEPT_LOGIN_USER_PHONE_BLOCK => 0x6a,
  135.     ACCEPT_LOGIN_CHILD => 0x6b,
  136.     REFUSE_IS_NOT_FREEUSER => 0x6c,
  137.     REFUSE_INVALID_ONETIMELIMIT => 0x6d,
  138.     REFUSE_CHANGE_PASSWD_FORCE => 0x6e,
  139.     REFUSE_OUTOFDATE_PASSWORD => 0x6f,
  140.     REFUSE_NOT_CHANGE_ACCOUNTID => 0xf0,
  141.     REFUSE_NOT_CHANGE_CHARACTERID => 0xf1,
  142.     REFUSE_SSO_AUTH_BLOCK_USER => 0x1394,
  143.     REFUSE_SSO_AUTH_GAME_APPLY => 0x1395,
  144.     REFUSE_SSO_AUTH_INVALID_GAMENUM => 0x1396,
  145.     REFUSE_SSO_AUTH_INVALID_USER => 0x1397,
  146.     REFUSE_SSO_AUTH_OTHERS => 0x1398,
  147.     REFUSE_SSO_AUTH_INVALID_AGE => 0x1399,
  148.     REFUSE_SSO_AUTH_INVALID_MACADDRESS => 0x139a,
  149.     REFUSE_SSO_AUTH_BLOCK_ETERNAL => 0x13c6,
  150.     REFUSE_SSO_AUTH_BLOCK_ACCOUNT_STEAL => 0x13c7,
  151.     REFUSE_SSO_AUTH_BLOCK_BUG_INVESTIGATION => 0x13c8,
  152.     REFUSE_SSO_NOT_PAY_USER => 0x13ba,
  153.     REFUSE_SSO_ALREADY_LOGIN_USER => 0x13bb,
  154.     REFUSE_SSO_CURRENT_USED_USER => 0x13bc,
  155.     REFUSE_SSO_OTHER_1 => 0x13bd,
  156.     REFUSE_SSO_DROP_USER => 0x13be,
  157.     REFUSE_SSO_NOTHING_USER => 0x13bf,
  158.     REFUSE_SSO_OTHER_2 => 0x13c0,
  159.     REFUSE_SSO_WRONG_RATETYPE_1 => 0x13c1,
  160.     REFUSE_SSO_EXTENSION_PCBANG_TIME => 0x13c2,
  161.     REFUSE_SSO_WRONG_RATETYPE_2 => 0x13c3,
  162. };
  163.  
  164. # party invite result
  165. use constant {
  166.     ANSWER_ALREADY_OTHERGROUPM => 0x0,
  167.     ANSWER_JOIN_REFUSE => 0x1,
  168.     ANSWER_JOIN_ACCEPT => 0x2,
  169.     ANSWER_MEMBER_OVERSIZE => 0x3,
  170.     ANSWER_DUPLICATE => 0x4,
  171.     ANSWER_JOINMSG_REFUSE => 0x5,
  172.     ANSWER_UNKNOWN_ERROR => 0x6,
  173.     ANSWER_UNKNOWN_CHARACTER => 0x7,
  174.     ANSWER_INVALID_MAPPROPERTY => 0x8,
  175. };
  176.  
  177. # party leave result
  178. use constant {
  179.     GROUPMEMBER_DELETE_LEAVE => 0x0,
  180.     GROUPMEMBER_DELETE_EXPEL => 0x1,
  181. };
  182.  
  183. ######################################
  184. ### CATEGORY: Class methods
  185. ######################################
  186.  
  187. # Just a wrapper for SUPER::parse.
  188. sub parse {
  189.     my $self = shift;
  190.     my $args = $self->SUPER::parse(@_);
  191.  
  192.     if ($args && $config{debugPacket_received} == 3 &&
  193.             existsInList($config{'debugPacket_include'}, $args->{switch})) {
  194.         my $packet = $self->{packet_list}{$args->{switch}};
  195.         my ($name, $packString, $varNames) = @{$packet};
  196.  
  197.         my @vars = ();
  198.         for my $varName (@{$varNames}) {
  199.             message "$varName = $args->{$varName}\n";
  200.         }
  201.     }
  202.  
  203.     return $args;
  204. }
  205.  
  206. #######################################
  207. ### CATEGORY: Private class methods
  208. #######################################
  209.  
  210. ##
  211. # int Network::Receive::queryLoginPinCode([String message])
  212. # Returns: login PIN code, or undef if cancelled
  213. # Ensures: length(result) in 4..8
  214. #
  215. # Request login PIN code from user.
  216. sub queryLoginPinCode {
  217.     my $message = $_[0] || T("You've never set a login PIN code before.\nPlease enter a new login PIN code:");
  218.     do {
  219.         my $input = $interface->query($message, isPassword => 1,);
  220.         if (!defined($input)) {
  221.             quit();
  222.             return;
  223.         } else {
  224.             if ($input !~ /^\d+$/) {
  225.                 $interface->errorDialog(T("The PIN code may only contain digits."));
  226.             } elsif ((length($input) <= 3) || (length($input) >= 9)) {
  227.                 $interface->errorDialog(T("The PIN code must be between 4 and 9 characters."));
  228.             } else {
  229.                 return $input;
  230.             }
  231.         }
  232.     } while (1);
  233. }
  234.  
  235. ##
  236. # boolean Network::Receive->queryAndSaveLoginPinCode([String message])
  237. # Returns: true on success
  238. #
  239. # Request login PIN code from user and save it in config.
  240. sub queryAndSaveLoginPinCode {
  241.     my ($self, $message) = @_;
  242.     my $pin = queryLoginPinCode($message);
  243.     if (defined $pin) {
  244.         configModify('loginPinCode', $pin, silent => 1);
  245.         return 1;
  246.     } else {
  247.         return 0;
  248.     }
  249. }
  250.  
  251. sub changeToInGameState {
  252.     if ($net->version() == 1) {
  253.         if ($accountID && UNIVERSAL::isa($char, 'Actor::You')) {
  254.             if ($net->getState() != Network::IN_GAME) {
  255.                 $net->setState(Network::IN_GAME);
  256.             }
  257.             return 1;
  258.         } else {
  259.             if ($net->getState() != Network::IN_GAME_BUT_UNINITIALIZED) {
  260.                 $net->setState(Network::IN_GAME_BUT_UNINITIALIZED);
  261.                 if ($config{verbose} && $messageSender && !$sentWelcomeMessage) {
  262.                     $messageSender->injectAdminMessage("Please relogin to enable X-${Settings::NAME}.");
  263.                     $sentWelcomeMessage = 1;
  264.                 }
  265.             }
  266.             return 0;
  267.         }
  268.     } else {
  269.         return 1;
  270.     }
  271. }
  272.  
  273. ### Packet inner struct handlers
  274.  
  275. # The block size in the received_characters packet varies from server to server.
  276. # This method may be overrided in other ServerType handlers to return
  277. # the correct block size.
  278. sub received_characters_blockSize {
  279.     if ($masterServer && $masterServer->{charBlockSize}) {
  280.         return $masterServer->{charBlockSize};
  281.     } else {
  282.         return 106;
  283.     }
  284. }
  285.  
  286. # The length must exactly match charBlockSize, as it's used to construct packets.
  287. sub received_characters_unpackString {
  288.     for ($masterServer && $masterServer->{charBlockSize}) {
  289.         # unknown purpose (0 = disabled, otherwise displays "Add-Ons" sidebar) (from rA)
  290.         # change $hairstyle
  291.         return 'a4 Z8 V Z8 V6 v V2 v4 V v9 Z24 C8 v a16 Z16 C' if $_ == 155;
  292.         return 'a4 V9 v V2 v4 V v9 Z24 C8 v a16 Z16 C' if $_ == 147;
  293.         return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4 x4 C' if $_ == 145;
  294.         return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4 x4' if $_ == 144;
  295.         # change slot feature
  296.         return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4' if $_ == 140;
  297.         # robe
  298.         return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4' if $_ == 136;
  299.         # delete date
  300.         return 'a4 V9 v V2 v14 Z24 C8 v Z16 V' if $_ == 132;
  301.         return 'a4 V9 v V2 v14 Z24 C8 v Z16' if $_ == 128;
  302.         # bRO (bitfrost update)
  303.         return 'a4 V9 v V2 v14 Z24 C8 v Z12' if $_ == 124;
  304.         return 'a4 V9 v V2 v14 Z24 C6 v2 x4' if $_ == 116; # TODO: (missing 2 last bytes)
  305.         return 'a4 V9 v V2 v14 Z24 C6 v2' if $_ == 112;
  306.         return 'a4 V9 v17 Z24 C6 v2' if $_ == 108;
  307.         return 'a4 V9 v17 Z24 C6 v' if $_ == 106 || !$_;
  308.         die "Unknown charBlockSize: $_";
  309.     }
  310. }
  311.  
  312. ### Parse/reconstruct callbacks and packet handlers
  313.  
  314. sub parse_account_server_info {
  315.     my ($self, $args) = @_;
  316.     my $server_info;
  317.  
  318.     if ($args->{switch} eq '0069') {  # DEFAULT PACKET
  319.         $server_info = {
  320.             len => 32,
  321.             types => 'a4 v Z20 v2 x2',
  322.             keys => [qw(ip port name users display)],
  323.         };
  324.  
  325.     } elsif ($args->{switch} eq '0AC4') { # kRO Zero 2017, kRO ST 201703+
  326.         $server_info = {
  327.             len => 160,
  328.             types => 'a4 v Z20 v3 a128',
  329.             keys => [qw(ip port name users state property unknown)],
  330.         };
  331.        
  332.     } elsif ($args->{switch} eq '0AC9') { # cRO 2017
  333.         $server_info = {
  334.             len => 154,
  335.             types => 'a20 V a2 a126',
  336.             keys => [qw(name users unknown ip_port)],
  337.         };
  338.        
  339.     } else { # this can't happen
  340.         return;
  341.     }
  342.  
  343.     @{$args->{servers}} = map {
  344.         my %server;
  345.         @server{@{$server_info->{keys}}} = unpack($server_info->{types}, $_);
  346.         if ($masterServer && $masterServer->{private}) {
  347.             $server{ip} = $masterServer->{ip};
  348.         } elsif ($args->{switch} eq '0AC9') {
  349.             @server{qw(ip port)} = split (/\:/, $server{ip_port});
  350.             $server{ip} =~ s/^\s+|\s+$//g;
  351.             $server{port} =~ tr/0-9//cd;
  352.         } else {
  353.             $server{ip} = inet_ntoa($server{ip});
  354.         }
  355.         $server{name} = bytesToString($server{name});
  356.         \%server
  357.     } unpack '(a'.$server_info->{len}.')*', $args->{serverInfo};
  358.  
  359.     if (length $args->{lastLoginIP} == 4 && $args->{lastLoginIP} ne "\0"x4) {
  360.         $args->{lastLoginIP} = inet_ntoa($args->{lastLoginIP});
  361.     } else {
  362.         delete $args->{lastLoginIP};
  363.     }
  364. }
  365.  
  366. sub reconstruct_account_server_info {
  367.     my ($self, $args) = @_;
  368.     $args->{lastLoginIP} = inet_aton($args->{lastLoginIP});
  369.  
  370.     if($args->{'switch'} eq "0AC4") {
  371.         $args->{serverInfo} = pack '(a160)*', map { pack(
  372.             'a4 v Z20 v3 a128',
  373.             inet_aton($_->{ip}),
  374.             $_->{port},
  375.             stringToBytes($_->{name}),
  376.             @{$_}{qw(users state property unknown)},
  377.         ) } @{$args->{servers}};
  378.     } elsif($args->{'switch'} eq "0AC9") {
  379.         $args->{serverInfo} = pack '(a154)*', map { pack(
  380.             'a20 V a2 a126',
  381.             @{$_}{qw(name users unknown ip_port)},
  382.         ) } @{$args->{servers}};
  383.     } else {
  384.         $args->{serverInfo} = pack '(a32)*', map { pack(
  385.             'a4 v Z20 v2 x2',
  386.             inet_aton($_->{ip}),
  387.             $_->{port},
  388.             stringToBytes($_->{name}),
  389.             @{$_}{qw(users display)},
  390.         ) } @{$args->{servers}};
  391.     }
  392. }
  393.  
  394. sub account_server_info {
  395.     my ($self, $args) = @_;
  396.  
  397.     $net->setState(2);
  398.     undef $conState_tries;
  399.     $sessionID = $args->{sessionID};
  400.     $accountID = $args->{accountID};
  401.     $sessionID2 = $args->{sessionID2};
  402.     # Account sex should only be 0 (female) or 1 (male)
  403.     # inRO gives female as 2 but expects 0 back
  404.     # do modulus of 2 here to fix?
  405.     # FIXME: we should check exactly what operation the client does to the number given
  406.     $accountSex = $args->{accountSex} % 2;
  407.     $accountSex2 = ($config{'sex'} ne "") ? $config{'sex'} : $accountSex;
  408.  
  409.     # any servers with lastLoginIP lastLoginTime?
  410.     # message TF("Last login: %s from %s\n", @{$args}{qw(lastLoginTime lastLoginIP)}) if ...;
  411.  
  412.     message
  413.         center(T(" Account Info "), 34, '-') ."\n" .
  414.         swrite(
  415.         T("Account ID: \@<<<<<<<<< \@<<<<<<<<<<\n" .
  416.         "Sex:        \@<<<<<<<<<<<<<<<<<<<<<\n" .
  417.         "Session ID: \@<<<<<<<<< \@<<<<<<<<<<\n" .
  418.         "            \@<<<<<<<<< \@<<<<<<<<<<\n"),
  419.         [unpack('V',$accountID), getHex($accountID), $sex_lut{$accountSex}, unpack('V',$sessionID), getHex($sessionID),
  420.         unpack('V',$sessionID2), getHex($sessionID2)]) .
  421.         ('-'x34) . "\n", 'connection';
  422.  
  423.     @servers = @{$args->{servers}};
  424.  
  425.     my $msg = center(T(" Servers "), 53, '-') ."\n" .
  426.             T("#   Name                  Users  IP              Port\n");
  427.     for (my $num = 0; $num < @servers; $num++) {
  428.         $msg .= swrite(
  429.             "@<< @<<<<<<<<<<<<<<<<<<<< @<<<<< @<<<<<<<<<<<<<< @<<<<<",
  430.             [$num, $servers[$num]{name}, $servers[$num]{users}, $servers[$num]{ip}, $servers[$num]{port}]);
  431.     }
  432.     $msg .= ('-'x53) . "\n";
  433.     message $msg, "connection";
  434.  
  435.     if ($net->version != 1) {
  436.         message T("Closing connection to Account Server\n"), 'connection';
  437.         $net->serverDisconnect();
  438.         if (!$masterServer->{charServer_ip} && $config{server} eq "") {
  439.             my @serverList;
  440.             foreach my $server (@servers) {
  441.                 push @serverList, $server->{name};
  442.             }
  443.             my $ret = $interface->showMenu(
  444.                     T("Please select your login server."),
  445.                     \@serverList,
  446.                     title => T("Select Login Server"));
  447.             if ($ret == -1) {
  448.                 quit();
  449.             } else {
  450.                 main::configModify('server', $ret, 1);
  451.             }
  452.  
  453.         } elsif ($masterServer->{charServer_ip}) {
  454.             message TF("Forcing connect to char server %s: %s\n", $masterServer->{charServer_ip}, $masterServer->{charServer_port}), 'connection';
  455.         }
  456.     }
  457.  
  458.     # FIXME better support for multiple received_characters packets
  459.     undef @chars;
  460.     if ($config{'XKore'} eq '1') {
  461.         $incomingMessages->nextMessageMightBeAccountID();
  462.     }
  463. }
  464.  
  465. sub connection_refused {
  466.     my ($self, $args) = @_;
  467.  
  468.     error TF("The server has denied your connection (error: %d).\n", $args->{error}), 'connection';
  469. }
  470.  
  471. *actor_exists = *actor_display_compatibility;
  472. *actor_connected = *actor_display_compatibility;
  473. *actor_moved = *actor_display_compatibility;
  474. *actor_spawned = *actor_display_compatibility;
  475. sub actor_display_compatibility {
  476.     my ($self, $args) = @_;
  477.     # compatibility; TODO do it in PacketParser->parse?
  478.     Plugins::callHook('packet_pre/actor_display', $args);
  479.     &actor_display unless $args->{return};
  480.     Plugins::callHook('packet/actor_display', $args);
  481. }
  482.  
  483. # This function is a merge of actor_exists, actor_connected, actor_moved, etc...
  484. sub actor_display {
  485.     my ($self, $args) = @_;
  486.     return unless changeToInGameState();
  487.     my ($actor, $mustAdd);
  488.  
  489.  
  490.     #### Initialize ####
  491.  
  492.     my $nameID = unpack("V", $args->{ID});
  493.  
  494.     if ($args->{switch} eq "0086") {
  495.         # Message 0086 contains less information about the actor than other similar
  496.         # messages. So we use the existing actor information.
  497.         my $coordsArg = $args->{coords};
  498.         my $tickArg = $args->{tick};
  499.         $args = Actor::get($args->{ID})->deepCopy();
  500.         # Here we overwrite the $args data with the 0086 packet data.
  501.         $args->{switch} = "0086";
  502.         $args->{coords} = $coordsArg;
  503.         $args->{tick} = $tickArg; # lol tickcount what do we do with that? debug "tick: " . $tickArg/1000/3600/24 . "\n";
  504.     }
  505.  
  506.     my (%coordsFrom, %coordsTo);
  507.     if (length $args->{coords} == 6) {
  508.         # Actor Moved
  509.         makeCoordsFromTo(\%coordsFrom, \%coordsTo, $args->{coords}); # body dir will be calculated using the vector
  510.     } else {
  511.         # Actor Spawned/Exists
  512.         makeCoordsDir(\%coordsTo, $args->{coords}, \$args->{body_dir});
  513.         %coordsFrom = %coordsTo;
  514.     }
  515.  
  516.     # Remove actors that are located outside the map
  517.     # This may be caused by:
  518.     #  - server sending us false actors
  519.     #  - actor packets not being parsed correctly
  520.     if (defined $field && ($field->isOffMap($coordsFrom{x}, $coordsFrom{y}) || $field->isOffMap($coordsTo{x}, $coordsTo{y}))) {
  521.         warning TF("Removed actor with off map coordinates: (%d,%d)->(%d,%d), field max: (%d,%d)\n",$coordsFrom{x},$coordsFrom{y},$coordsTo{x},$coordsTo{y},$field->width(),$field->height());
  522.         return;
  523.     }
  524.  
  525.     # Remove actors with a distance greater than removeActorWithDistance. Useful for vending (so you don't spam
  526.     # too many packets in prontera and cause server lag). As a side effect, you won't be able to "see" actors
  527.     # beyond removeActorWithDistance.
  528.     if ($config{removeActorWithDistance}) {
  529.         if ((my $block_dist = blockDistance($char->{pos_to}, \%coordsTo)) > ($config{removeActorWithDistance})) {
  530.             my $nameIdTmp = unpack("V", $args->{ID});
  531.             debug "Removed out of sight actor $nameIdTmp at ($coordsTo{x}, $coordsTo{y}) (distance: $block_dist)\n";
  532.             return;
  533.         }
  534.     }
  535. =pod
  536.     # Zealotus bug
  537.     if ($args->{type} == 1200) {
  538.         open DUMP, ">> test_Zealotus.txt";
  539.         print DUMP "Zealotus: " . $nameID . "\n";
  540.         print DUMP Dumper($args);
  541.         close DUMP;
  542.     }
  543. =cut
  544.  
  545.     #### Step 0: determine object type ####
  546.     my $object_class;
  547.     if (defined $args->{object_type}) {
  548.         if ($args->{type} == 45) { # portals use the same object_type as NPCs
  549.             $object_class = 'Actor::Portal';
  550.         } else {
  551.             $object_class = {
  552.                 PC_TYPE, 'Actor::Player',
  553.                 # NPC_TYPE? # not encountered, NPCs are NPC_EVT_TYPE
  554.                 # SKILL_TYPE? # not encountered
  555.                 # UNKNOWN_TYPE? # not encountered
  556.                 NPC_MOB_TYPE, 'Actor::Monster',
  557.                 NPC_EVT_TYPE, 'Actor::NPC', # both NPCs and portals
  558.                 NPC_PET_TYPE, 'Actor::Pet',
  559.                 NPC_HO_TYPE, 'Actor::Slave',
  560.                 NPC_MERSOL_TYPE, 'Actor::Slave',
  561.                 # NPC_ELEMENTAL_TYPE? # not encountered
  562.             }->{$args->{object_type}};
  563.         }
  564.  
  565.     }
  566.  
  567.     unless (defined $object_class) {
  568.         if ($jobs_lut{$args->{type}}) {
  569.             unless ($args->{type} > 6000) {
  570.                 $object_class = 'Actor::Player';
  571.             } else {
  572.                 $object_class = 'Actor::Slave';
  573.             }
  574.         } elsif ($args->{type} == 45) {
  575.             $object_class = 'Actor::Portal';
  576.  
  577.         } elsif ($args->{type} >= 1000) {
  578.             if ($args->{hair_style} == 0x64) {
  579.                 $object_class = 'Actor::Pet';
  580.             } else {
  581.                 $object_class = 'Actor::Monster';
  582.             }
  583.         } else {   # ($args->{type} < 1000 && $args->{type} != 45 && !$jobs_lut{$args->{type}})
  584.             $object_class = 'Actor::NPC';
  585.         }
  586.     }
  587.  
  588.     #### Step 1: create the actor object ####
  589.  
  590.     if ($object_class eq 'Actor::Player') {
  591.         # Actor is a player
  592.         $actor = $playersList->getByID($args->{ID});
  593.         if (!defined $actor) {
  594.             $actor = new Actor::Player();
  595.             $actor->{appear_time} = time;
  596.             # New actor_display packets include the player's name
  597.             if ($args->{switch} eq "0086") {
  598.                 $actor->{name} = $args->{name};
  599.             } else {
  600.                 $actor->{name} = bytesToString($args->{name}) if exists $args->{name};
  601.             }
  602.             $mustAdd = 1;
  603.         }
  604.         $actor->{nameID} = $nameID;
  605.     } elsif ($object_class eq 'Actor::Slave') {
  606.         # Actor is a homunculus or a mercenary
  607.         $actor = $slavesList->getByID($args->{ID});
  608.         if (!defined $actor) {
  609.             $actor = ($char->{slaves} && $char->{slaves}{$args->{ID}})
  610.             ? $char->{slaves}{$args->{ID}} : new Actor::Slave ($args->{type});
  611.  
  612.             $actor->{appear_time} = time;
  613.             $actor->{name_given} = bytesToString($args->{name}) if exists $args->{name};
  614.             $actor->{jobId} = $args->{type} if exists $args->{type};
  615.             $mustAdd = 1;
  616.         }
  617.         $actor->{nameID} = $nameID;
  618.     } elsif ($object_class eq 'Actor::Portal') {
  619.         # Actor is a portal
  620.         $actor = $portalsList->getByID($args->{ID});
  621.         if (!defined $actor) {
  622.             $actor = new Actor::Portal();
  623.             $actor->{appear_time} = time;
  624.             my $exists = portalExists($field->baseName, \%coordsTo);
  625.             $actor->{source}{map} = $field->baseName;
  626.             if ($exists ne "") {
  627.                 $actor->setName("$portals_lut{$exists}{source}{map} -> " . getPortalDestName($exists));
  628.             }
  629.             $mustAdd = 1;
  630.  
  631.             # Strangely enough, portals (like all other actors) have names, too.
  632.             # We _could_ send a "actor_info_request" packet to find the names of each portal,
  633.             # however I see no gain from this. (And it might even provide another way of private
  634.             # servers to auto-ban bots.)
  635.         }
  636.         $actor->{nameID} = $nameID;
  637.     } elsif ($object_class eq 'Actor::Pet') {
  638.         # Actor is a pet
  639.         $actor = $petsList->getByID($args->{ID});
  640.         if (!defined $actor) {
  641.             $actor = new Actor::Pet();
  642.             $actor->{appear_time} = time;
  643.             $actor->{name} = $args->{name};
  644. #           if ($monsters_lut{$args->{type}}) {
  645. #               $actor->setName($monsters_lut{$args->{type}});
  646. #           }
  647.             $actor->{name_given} = exists $args->{name} ? bytesToString($args->{name}) : T("Unknown");
  648.             $mustAdd = 1;
  649.  
  650.             # Previously identified monsters could suddenly be identified as pets.
  651.             if ($monstersList->getByID($args->{ID})) {
  652.                 $monstersList->removeByID($args->{ID});
  653.             }
  654.  
  655.             # Why do monsters and pets use nameID as type?
  656.             $actor->{nameID} = $args->{type};
  657.  
  658.         }
  659.     } elsif ($object_class eq 'Actor::Monster') {
  660.         $actor = $monstersList->getByID($args->{ID});
  661.         if (!defined $actor) {
  662.             $actor = new Actor::Monster();
  663.             $actor->{appear_time} = time;
  664.             if ($monsters_lut{$args->{type}}) {
  665.                 $actor->setName($monsters_lut{$args->{type}});
  666.             }
  667.             #$actor->{name_given} = exists $args->{name} ? bytesToString($args->{name}) : "Unknown";
  668.             $actor->{name_given} = "Unknown";
  669.             $actor->{binType} = $args->{type};
  670.             $mustAdd = 1;
  671.  
  672.             # Why do monsters and pets use nameID as type?
  673.             $actor->{nameID} = $args->{type};
  674.         }
  675.     } elsif ($object_class eq 'Actor::NPC') {
  676.         # Actor is an NPC
  677.         $actor = $npcsList->getByID($args->{ID});
  678.         if (!defined $actor) {
  679.             $actor = new Actor::NPC();
  680.             $actor->{appear_time} = time;
  681.             $actor->{name} = bytesToString($args->{name}) if exists $args->{name};
  682.             $mustAdd = 1;
  683.         }
  684.         $actor->{nameID} = $nameID;
  685.     }
  686.  
  687.     #### Step 2: update actor information ####
  688.     $actor->{ID} = $args->{ID};
  689.     $actor->{charID} = $args->{charID} if $args->{charID} && $args->{charID} ne "\0\0\0\0";
  690.     $actor->{jobID} = $args->{type};
  691.     $actor->{type} = $args->{type};
  692.     $actor->{lv} = $args->{lv};
  693.     $actor->{pos} = {%coordsFrom};
  694.     $actor->{pos_to} = {%coordsTo};
  695.     $actor->{walk_speed} = $args->{walk_speed} / 1000 if (exists $args->{walk_speed} && $args->{switch} ne "0086");
  696.     $actor->{time_move} = time;
  697.     $actor->{time_move_calc} = distance(\%coordsFrom, \%coordsTo) * $actor->{walk_speed};
  698.     $actor->{len} = $args->{len} if $args->{len};
  699.     # 0086 would need that?
  700.     $actor->{object_type} = $args->{object_type} if (defined $args->{object_type});
  701.  
  702.     if (UNIVERSAL::isa($actor, "Actor::Player")) {
  703.         # None of this stuff should matter if the actor isn't a player... => does matter for a guildflag npc!
  704.  
  705.         # Interesting note about emblemID. If it is 0 (or none), the Ragnarok
  706.         # client will display "Send (Player) a guild invitation" (assuming one has
  707.         # invitation priveledges), regardless of whether or not guildID is set.
  708.         # I bet that this is yet another brilliant "feature" by GRAVITY's good programmers.
  709.         $actor->{emblemID} = $args->{emblemID} if (exists $args->{emblemID});
  710.         $actor->{guildID} = $args->{guildID} if (exists $args->{guildID});
  711.  
  712.         if (exists $args->{lowhead}) {
  713.             $actor->{headgear}{low} = $args->{lowhead};
  714.             $actor->{headgear}{mid} = $args->{midhead};
  715.             $actor->{headgear}{top} = $args->{tophead};
  716.             $actor->{weapon} = $args->{weapon};
  717.             $actor->{shield} = $args->{shield};
  718.         }
  719.  
  720.         $actor->{sex} = $args->{sex};
  721.  
  722.         if ($args->{act} == 1) {
  723.             $actor->{dead} = 1;
  724.         } elsif ($args->{act} == 2) {
  725.             $actor->{sitting} = 1;
  726.         }
  727.  
  728.         # Monsters don't have hair colors or heads to look around...
  729.         $actor->{hair_color} = $args->{hair_color} if (exists $args->{hair_color});
  730.  
  731.     } elsif (UNIVERSAL::isa($actor, "Actor::NPC") && $args->{type} == 722) { # guild flag has emblem
  732.         # odd fact: "this data can also be found in a strange place:
  733.         # (shield OR lowhead) + midhead = emblemID      (either shield or lowhead depending on the packet)
  734.         # tophead = guildID
  735.         $actor->{emblemID} = $args->{emblemID};
  736.         $actor->{guildID} = $args->{guildID};
  737.     }
  738.  
  739.     # But hair_style is used for pets, and their bodies can look different ways...
  740.     $actor->{hair_style} = $args->{hair_style} if (exists $args->{hair_style});
  741.     $actor->{look}{body} = $args->{body_dir} if (exists $args->{body_dir});
  742.     $actor->{look}{head} = $args->{head_dir} if (exists $args->{head_dir});
  743.  
  744.     # When stance is non-zero, character is bobbing as if they had just got hit,
  745.     # but the cursor also turns to a sword when they are mouse-overed.
  746.     #$actor->{stance} = $args->{stance} if (exists $args->{stance});
  747.  
  748.     # Visual effects are a set of flags (some of the packets don't have this argument)
  749.     $actor->{opt3} = $args->{opt3} if (exists $args->{opt3}); # stackable
  750.  
  751.     # Known visual effects:
  752.     # 0x0001 = Yellow tint (eg, a quicken skill)
  753.     # 0x0002 = Red tint (eg, power-thrust)
  754.     # 0x0004 = Gray tint (eg, energy coat)
  755.     # 0x0008 = Slow lightning (eg, mental strength)
  756.     # 0x0010 = Fast lightning (eg, MVP fury)
  757.     # 0x0020 = Black non-moving statue (eg, stone curse)
  758.     # 0x0040 = Translucent weapon
  759.     # 0x0080 = Translucent red sprite (eg, marionette control?)
  760.     # 0x0100 = Spaztastic weapon image (eg, mystical amplification)
  761.     # 0x0200 = Gigantic glowy sphere-thing
  762.     # 0x0400 = Translucent pink sprite (eg, marionette control?)
  763.     # 0x0800 = Glowy sprite outline (eg, assumptio)
  764.     # 0x1000 = Bright red sprite, slowly moving red lightning (eg, MVP fury?)
  765.     # 0x2000 = Vortex-type effect
  766.  
  767.     # Note that these are flags, and you can mix and match them
  768.     # Example: 0x000C (0x0008 & 0x0004) = gray tint with slow lightning
  769.  
  770. =pod
  771. typedef enum <unnamed-tag> {
  772.   SHOW_EFST_NORMAL =  0x0,
  773.   SHOW_EFST_QUICKEN =  0x1,
  774.   SHOW_EFST_OVERTHRUST =  0x2,
  775.   SHOW_EFST_ENERGYCOAT =  0x4,
  776.   SHOW_EFST_EXPLOSIONSPIRITS =  0x8,
  777.   SHOW_EFST_STEELBODY =  0x10,
  778.   SHOW_EFST_BLADESTOP =  0x20,
  779.   SHOW_EFST_AURABLADE =  0x40,
  780.   SHOW_EFST_REDBODY =  0x80,
  781.   SHOW_EFST_LIGHTBLADE =  0x100,
  782.   SHOW_EFST_MOON =  0x200,
  783.   SHOW_EFST_PINKBODY =  0x400,
  784.   SHOW_EFST_ASSUMPTIO =  0x800,
  785.   SHOW_EFST_SUN_WARM =  0x1000,
  786.   SHOW_EFST_REFLECT =  0x2000,
  787.   SHOW_EFST_BUNSIN =  0x4000,
  788.   SHOW_EFST_SOULLINK =  0x8000,
  789.   SHOW_EFST_UNDEAD =  0x10000,
  790.   SHOW_EFST_CONTRACT =  0x20000,
  791. } <unnamed-tag>;
  792. =cut
  793.  
  794.     # Save these parameters ...
  795.     $actor->{opt1} = $args->{opt1}; # nonstackable
  796.     $actor->{opt2} = $args->{opt2}; # stackable
  797.     $actor->{option} = $args->{option}; # stackable
  798.  
  799.     # And use them to set status flags.
  800.     if (setStatus($actor, $args->{opt1}, $args->{opt2}, $args->{option})) {
  801.         $mustAdd = 0;
  802.     }
  803.  
  804.  
  805.     #### Step 3: Add actor to actor list ####
  806.     if ($mustAdd) {
  807.         if (UNIVERSAL::isa($actor, "Actor::Player")) {
  808.             $playersList->add($actor);
  809.             Plugins::callHook('add_player_list', $actor);
  810.  
  811.         } elsif (UNIVERSAL::isa($actor, "Actor::Monster")) {
  812.             $monstersList->add($actor);
  813.             Plugins::callHook('add_monster_list', $actor);
  814.  
  815.         } elsif (UNIVERSAL::isa($actor, "Actor::Pet")) {
  816.             $petsList->add($actor);
  817.             Plugins::callHook('add_pet_list', $actor);
  818.  
  819.         } elsif (UNIVERSAL::isa($actor, "Actor::Portal")) {
  820.             $portalsList->add($actor);
  821.             Plugins::callHook('add_portal_list', $actor);
  822.  
  823.         } elsif (UNIVERSAL::isa($actor, "Actor::NPC")) {
  824.             my $ID = $args->{ID};
  825.             my $location = $field->baseName . " $actor->{pos}{x} $actor->{pos}{y}";
  826.             if ($npcs_lut{$location}) {
  827.                 $actor->setName($npcs_lut{$location});
  828.             }
  829.             $npcsList->add($actor);
  830.             Plugins::callHook('add_npc_list', $actor);
  831.  
  832.         } elsif (UNIVERSAL::isa($actor, "Actor::Slave")) {
  833.             $slavesList->add($actor);
  834.             Plugins::callHook('add_slave_list', $actor);
  835.         }
  836.     }
  837.  
  838.  
  839.     #### Packet specific ####
  840.     if ($args->{switch} eq "0078" ||
  841.         $args->{switch} eq "01D8" ||
  842.         $args->{switch} eq "022A" ||
  843.         $args->{switch} eq "02EE" ||
  844.         $args->{switch} eq "07F9" ||
  845.         $args->{switch} eq "0915" ||
  846.         $args->{switch} eq "09DD" ||
  847.         $args->{switch} eq "09FF") {
  848.         # Actor Exists (standing)
  849.  
  850.         if ($actor->isa('Actor::Player')) {
  851.             my $domain = existsInList($config{friendlyAID}, unpack("V", $actor->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
  852.             debug "Player Exists: " . $actor->name . " ($actor->{binID}) Level $actor->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsFrom{x}, $coordsFrom{y})\n", $domain;
  853.  
  854.             Plugins::callHook('player', {player => $actor});  #backwards compatibility
  855.  
  856.             Plugins::callHook('player_exist', {player => $actor});
  857.  
  858.         } elsif ($actor->isa('Actor::NPC')) {
  859.             message TF("NPC Exists: %s (%d, %d) (ID %d) - (%d)\n", $actor->name, $actor->{pos_to}{x}, $actor->{pos_to}{y}, $actor->{nameID}, $actor->{binID}), ($config{showDomain_NPC}?$config{showDomain_NPC}:"parseMsg_presence"), 1;
  860.             Plugins::callHook('npc_exist', {npc => $actor});
  861.  
  862.         } elsif ($actor->isa('Actor::Portal')) {
  863.             message TF("Portal Exists: %s (%s, %s) - (%s)\n", $actor->name, $actor->{pos_to}{x}, $actor->{pos_to}{y}, $actor->{binID}), "portals", 1;
  864.             Plugins::callHook('portal_exist', {portal => $actor});
  865.            
  866.         } elsif ($actor->isa('Actor::Monster')) {
  867.             debug sprintf("Monster Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  868.  
  869.         } elsif ($actor->isa('Actor::Pet')) {
  870.             debug sprintf("Pet Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  871.  
  872.         } elsif ($actor->isa('Actor::Slave')) {
  873.             debug sprintf("Slave Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  874.  
  875.         } else {
  876.             debug sprintf("Unknown Actor Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  877.         }
  878.  
  879.     } elsif ($args->{switch} eq "0079" ||
  880.         $args->{switch} eq "01DB" ||
  881.         $args->{switch} eq "022B" ||
  882.         $args->{switch} eq "02ED" ||
  883.         $args->{switch} eq "01D9" ||
  884.         $args->{switch} eq "07F8" ||
  885.         $args->{switch} eq "0858" ||
  886.         $args->{switch} eq "090F" ||
  887.         $args->{switch} eq "09DC" ||
  888.         $args->{switch} eq "09FE") {
  889.         # Actor Connected (new)
  890.  
  891.         if ($actor->isa('Actor::Player')) {
  892.             my $domain = existsInList($config{friendlyAID}, unpack("V", $args->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
  893.             debug "Player Connected: ".$actor->name." ($actor->{binID}) Level $args->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsTo{x}, $coordsTo{y})\n", $domain;
  894.  
  895.             Plugins::callHook('player', {player => $actor});  #backwards compatibailty
  896.  
  897.             Plugins::callHook('player_connected', {player => $actor});
  898.         } else {
  899.             debug "Unknown Connected: $args->{type} - \n", "parseMsg";
  900.         }
  901.  
  902.     } elsif ($args->{switch} eq "007B" ||
  903.         $args->{switch} eq "0086" ||
  904.         $args->{switch} eq "01DA" ||
  905.         $args->{switch} eq "022C" ||
  906.         $args->{switch} eq "02EC" ||
  907.         $args->{switch} eq "07F7" ||
  908.         $args->{switch} eq "0856" ||
  909.         $args->{switch} eq "0914" ||
  910.         $args->{switch} eq "09DB" ||
  911.         $args->{switch} eq "09FD") {
  912.         # Actor Moved
  913.  
  914.         # Correct the direction in which they're looking
  915.         my %vec;
  916.         getVector(\%vec, \%coordsTo, \%coordsFrom);
  917.         my $direction = int sprintf("%.0f", (360 - vectorToDegree(\%vec)) / 45);
  918.  
  919.         $actor->{look}{body} = $direction;
  920.         $actor->{look}{head} = 0;
  921.  
  922.         if ($actor->isa('Actor::Player')) {
  923.             debug "Player Moved: " . $actor->name . " ($actor->{binID}) Level $actor->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
  924.                 Plugins::callHook('player_moved', $actor);
  925.         } elsif ($actor->isa('Actor::Monster')) {
  926.             debug "Monster Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
  927.                 Plugins::callHook('monster_moved', $actor);
  928.         } elsif ($actor->isa('Actor::Pet')) {
  929.             debug "Pet Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
  930.                 Plugins::callHook('pet_moved', $actor);
  931.         } elsif ($actor->isa('Actor::Slave')) {
  932.             debug "Slave Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
  933.                 Plugins::callHook('slave_moved', $actor);
  934.         } elsif ($actor->isa('Actor::Portal')) {
  935.             # This can never happen of course.
  936.             debug "Portal Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
  937.                 Plugins::callHook('portal_moved', $actor);
  938.         } elsif ($actor->isa('Actor::NPC')) {
  939.             # Neither can this.
  940.             debug "NPC Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
  941.                 Plugins::callHook('npc_moved', $actor);
  942.         } else {
  943.             debug "Unknown Actor Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
  944.         }
  945.  
  946.     } elsif ($args->{switch} eq "007C") {
  947.         # Actor Spawned
  948.         if ($actor->isa('Actor::Player')) {
  949.             debug "Player Spawned: " . $actor->nameIdx . " $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}}\n", "parseMsg";
  950.         } elsif ($actor->isa('Actor::Monster')) {
  951.             debug "Monster Spawned: " . $actor->nameIdx . "\n", "parseMsg";
  952.         } elsif ($actor->isa('Actor::Pet')) {
  953.             debug "Pet Spawned: " . $actor->nameIdx . "\n", "parseMsg";
  954.         } elsif ($actor->isa('Actor::Slave')) {
  955.             debug "Slave Spawned: " . $actor->nameIdx . " $jobs_lut{$actor->{jobID}}\n", "parseMsg";
  956.         } elsif ($actor->isa('Actor::Portal')) {
  957.             # Can this happen?
  958.             debug "Portal Spawned: " . $actor->nameIdx . "\n", "parseMsg";
  959.         } elsif ($actor->isa('NPC')) {
  960.             debug "NPC Spawned: " . $actor->nameIdx . "\n", "parseMsg";
  961.         } else {
  962.             debug "Unknown Spawned: " . $actor->nameIdx . "\n", "parseMsg";
  963.         }
  964.     }
  965. }
  966.  
  967. sub actor_died_or_disappeared {
  968.     my ($self,$args) = @_;
  969.     return unless changeToInGameState();
  970.     my $ID = $args->{ID};
  971.     avoidList_ID($ID);
  972.  
  973.     if ($ID eq $accountID) {
  974.         message T("You have died\n") if (!$char->{dead});
  975.         Plugins::callHook('self_died');
  976.         closeShop() unless !$shopstarted || $config{'dcOnDeath'} == -1 || AI::state == AI::OFF;
  977.         $char->{deathCount}++;
  978.         $char->{dead} = 1;
  979.         $char->{dead_time} = time;
  980.         if ($char->{equipment}{arrow} && $char->{equipment}{arrow}{type} == 19) {
  981.             delete $char->{equipment}{arrow};
  982.         }
  983.  
  984.     } elsif (defined $monstersList->getByID($ID)) {
  985.         my $monster = $monstersList->getByID($ID);
  986.         if ($args->{type} == 0) {
  987.             debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
  988.             $monster->{disappeared} = 1;
  989.  
  990.         } elsif ($args->{type} == 1) {
  991.             debug "Monster Died: " . $monster->name . " ($monster->{binID})\n", "parseMsg_damage";
  992.             $monster->{dead} = 1;
  993.  
  994.             if ((AI::action ne "attack" || AI::args(0)->{ID} eq $ID) &&
  995.                 ($config{itemsTakeAuto_party} &&
  996.                 ($monster->{dmgFromParty} > 0 ||
  997.                  $monster->{dmgFromYou} > 0))) {
  998.                 AI::clear("items_take");
  999.                 ai_items_take($monster->{pos}{x}, $monster->{pos}{y},
  1000.                     $monster->{pos_to}{x}, $monster->{pos_to}{y});
  1001.             }
  1002.  
  1003.         } elsif ($args->{type} == 2) { # What's this?
  1004.             debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
  1005.             $monster->{disappeared} = 1;
  1006.  
  1007.         } elsif ($args->{type} == 3) {
  1008.             debug "Monster Teleported: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
  1009.             $monster->{teleported} = 1;
  1010.         }
  1011.  
  1012.         $monster->{gone_time} = time;
  1013.         $monsters_old{$ID} = $monster->deepCopy();
  1014.         Plugins::callHook('monster_disappeared', {monster => $monster});
  1015.         $monstersList->remove($monster);
  1016.  
  1017.     } elsif (defined $playersList->getByID($ID)) {
  1018.         my $player = $playersList->getByID($ID);
  1019.         if ($args->{type} == 1) {
  1020.             message TF("Player Died: %s (%d) %s %s\n", $player->name, $player->{binID}, $sex_lut{$player->{sex}}, $jobs_lut{$player->{jobID}});
  1021.             $player->{dead} = 1;
  1022.             $player->{dead_time} = time;
  1023.         } else {
  1024.             if ($args->{type} == 0) {
  1025.                 debug "Player Disappeared: " . $player->name . " ($player->{binID}) $sex_lut{$player->{sex}} $jobs_lut{$player->{jobID}} ($player->{pos_to}{x}, $player->{pos_to}{y})\n", "parseMsg_presence";
  1026.                 $player->{disappeared} = 1;
  1027.             } elsif ($args->{type} == 2) {
  1028.                 debug "Player Disconnected: ".$player->name." ($player->{binID}) $sex_lut{$player->{sex}} $jobs_lut{$player->{jobID}} ($player->{pos_to}{x}, $player->{pos_to}{y})\n", "parseMsg_presence";
  1029.                 $player->{disconnected} = 1;
  1030.             } elsif ($args->{type} == 3) {
  1031.                 debug "Player Teleported: ".$player->name." ($player->{binID}) $sex_lut{$player->{sex}} $jobs_lut{$player->{jobID}} ($player->{pos_to}{x}, $player->{pos_to}{y})\n", "parseMsg_presence";
  1032.                 $player->{teleported} = 1;
  1033.             } else {
  1034.                 debug "Player Disappeared in an unknown way: ".$player->name." ($player->{binID}) $sex_lut{$player->{sex}} $jobs_lut{$player->{jobID}}\n", "parseMsg_presence";
  1035.                 $player->{disappeared} = 1;
  1036.             }
  1037.  
  1038.             if (grep { $ID eq $_ } @venderListsID) {
  1039.                 binRemove(\@venderListsID, $ID);
  1040.                 delete $venderLists{$ID};
  1041.             }
  1042.  
  1043.             $player->{gone_time} = time;
  1044.             $players_old{$ID} = $player->deepCopy();
  1045.             Plugins::callHook('player_disappeared', {player => $player});
  1046.  
  1047.             $playersList->remove($player);
  1048.         }
  1049.  
  1050.     } elsif ($players_old{$ID}) {
  1051.         if ($args->{type} == 2) {
  1052.             debug "Player Disconnected: " . $players_old{$ID}->name . "\n", "parseMsg_presence";
  1053.             $players_old{$ID}{disconnected} = 1;
  1054.         } elsif ($args->{type} == 3) {
  1055.             debug "Player Teleported: " . $players_old{$ID}->name . "\n", "parseMsg_presence";
  1056.             $players_old{$ID}{teleported} = 1;
  1057.         }
  1058.  
  1059.     } elsif (defined $portalsList->getByID($ID)) {
  1060.         my $portal = $portalsList->getByID($ID);
  1061.         debug "Portal Disappeared: " . $portal->name . " ($portal->{binID})\n", "parseMsg";
  1062.         $portal->{disappeared} = 1;
  1063.         $portal->{gone_time} = time;
  1064.         $portals_old{$ID} = $portal->deepCopy();
  1065.         Plugins::callHook('portal_disappeared', {portal => $portal});
  1066.         $portalsList->remove($portal);
  1067.  
  1068.     } elsif (defined $npcsList->getByID($ID)) {
  1069.         my $npc = $npcsList->getByID($ID);
  1070.         debug "NPC Disappeared: " . $npc->name . " ($npc->{nameID})\n", "parseMsg";
  1071.         $npc->{disappeared} = 1;
  1072.         $npc->{gone_time} = time;
  1073.         $npcs_old{$ID} = $npc->deepCopy();
  1074.         Plugins::callHook('npc_disappeared', {npc => $npc});
  1075.         $npcsList->remove($npc);
  1076.  
  1077.     } elsif (defined $petsList->getByID($ID)) {
  1078.         my $pet = $petsList->getByID($ID);
  1079.         debug "Pet Disappeared: " . $pet->name . " ($pet->{binID})\n", "parseMsg";
  1080.         $pet->{disappeared} = 1;
  1081.         $pet->{gone_time} = time;
  1082.         Plugins::callHook('pet_disappeared', {pet => $pet});
  1083.         $petsList->remove($pet);
  1084.  
  1085.     } elsif (defined $slavesList->getByID($ID)) {
  1086.         my $slave = $slavesList->getByID($ID);
  1087.         if ($args->{type} == 1) {
  1088.             message TF("Slave Died: %s (%d) %s\n", $slave->name, $slave->{binID}, $slave->{actorType});
  1089.             $slave->{state} = 4;
  1090.         } else {
  1091.             if ($args->{type} == 0) {
  1092.                 debug "Slave Disappeared: " . $slave->name . " ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
  1093.                 $slave->{disappeared} = 1;
  1094.             } elsif ($args->{type} == 2) {
  1095.                 debug "Slave Disconnected: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
  1096.                 $slave->{disconnected} = 1;
  1097.             } elsif ($args->{type} == 3) {
  1098.                 debug "Slave Teleported: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
  1099.                 $slave->{teleported} = 1;
  1100.             } else {
  1101.                 debug "Slave Disappeared in an unknown way: ".$slave->name." ($slave->{binID}) $slave->{actorType}\n", "parseMsg_presence";
  1102.                 $slave->{disappeared} = 1;
  1103.             }
  1104.  
  1105.             $slave->{gone_time} = time;
  1106.             Plugins::callHook('slave_disappeared', {slave => $slave});
  1107.         }
  1108.  
  1109.         $slavesList->remove($slave);
  1110.  
  1111.     } else {
  1112.         debug "Unknown Disappeared: ".getHex($ID)."\n", "parseMsg";
  1113.     }
  1114. }
  1115.  
  1116. sub actor_action {
  1117.     my ($self,$args) = @_;
  1118.     return unless changeToInGameState();
  1119.  
  1120.     $args->{damage} = intToSignedShort($args->{damage});
  1121.     if ($args->{type} == ACTION_ITEMPICKUP) {
  1122.         # Take item
  1123.         my $source = Actor::get($args->{sourceID});
  1124.         my $verb = $source->verb('pick up', 'picks up');
  1125.         my $target = getActorName($args->{targetID});
  1126.         debug "$source $verb $target\n", 'parseMsg_presence';
  1127.  
  1128.         my $item = $itemsList->getByID($args->{targetID});
  1129.         $item->{takenBy} = $args->{sourceID} if ($item);
  1130.  
  1131.     } elsif ($args->{type} == ACTION_SIT) {
  1132.         # Sit
  1133.         my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
  1134.         if ($args->{sourceID} eq $accountID) {
  1135.             message T("You are sitting.\n") if (!$char->{sitting});
  1136.             $char->{sitting} = 1;
  1137.             AI::queue("sitAuto") unless (AI::inQueue("sitAuto")) || $ai_v{sitAuto_forcedBySitCommand};
  1138.         } else {
  1139.             message TF("%s is sitting.\n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
  1140.             my $player = $playersList->getByID($args->{sourceID});
  1141.             $player->{sitting} = 1 if ($player);
  1142.         }
  1143.         Misc::checkValidity("actor_action (take item)");
  1144.  
  1145.     } elsif ($args->{type} == ACTION_STAND) {
  1146.         # Stand
  1147.         my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
  1148.         if ($args->{sourceID} eq $accountID) {
  1149.             message T("You are standing.\n") if ($char->{sitting});
  1150.             if ($config{sitAuto_idle}) {
  1151.                 $timeout{ai_sit_idle}{time} = time;
  1152.             }
  1153.             $char->{sitting} = 0;
  1154.         } else {
  1155.             message TF("%s is standing.\n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
  1156.             my $player = $playersList->getByID($args->{sourceID});
  1157.             $player->{sitting} = 0 if ($player);
  1158.         }
  1159.         Misc::checkValidity("actor_action (stand)");
  1160.  
  1161.     } else {
  1162.         # Attack
  1163.         my $dmgdisplay;
  1164.         my $totalDamage = $args->{damage} + $args->{dual_wield_damage};
  1165.         if ($totalDamage == 0) {
  1166.             $dmgdisplay = T("Miss!");
  1167.             $dmgdisplay .= "!" if ($args->{type} == ACTION_ATTACK_LUCKY); # lucky dodge
  1168.         } else {
  1169.             $dmgdisplay = $args->{div} > 1
  1170.                 ? sprintf '%d*%d', $args->{damage} / $args->{div}, $args->{div}
  1171.                 : $args->{damage}
  1172.             ;
  1173.             $dmgdisplay .= "!" if ($args->{type} == ACTION_ATTACK_CRITICAL); # critical hit
  1174.             $dmgdisplay .= " + $args->{dual_wield_damage}" if $args->{dual_wield_damage};
  1175.         }
  1176.  
  1177.         Misc::checkValidity("actor_action (attack 1)");
  1178.  
  1179.         updateDamageTables($args->{sourceID}, $args->{targetID}, $totalDamage);
  1180.  
  1181.         Misc::checkValidity("actor_action (attack 2)");
  1182.  
  1183.         my $source = Actor::get($args->{sourceID});
  1184.         my $target = Actor::get($args->{targetID});
  1185.         my $verb = $source->verb('attack', 'attacks');
  1186.  
  1187.         $target->{sitting} = 0 unless $args->{type} == ACTION_ATTACK_NOMOTION || $args->{type} == ACTION_ATTACK_MULTIPLE_NOMOTION || $totalDamage == 0;
  1188.  
  1189.         my $msg = attack_string($source, $target, $dmgdisplay, ($args->{src_speed}));
  1190.         Plugins::callHook('packet_attack', {sourceID => $args->{sourceID}, targetID => $args->{targetID}, msg => \$msg, dmg => $totalDamage, type => $args->{type}});
  1191.  
  1192.         my $status = sprintf("[%3d/%3d]", percent_hp($char), percent_sp($char));
  1193.  
  1194.         Misc::checkValidity("actor_action (attack 3)");
  1195.  
  1196.         if ($args->{sourceID} eq $accountID) {
  1197.             message("$status $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
  1198.             if ($startedattack) {
  1199.                 $monstarttime = time();
  1200.                 $monkilltime = time();
  1201.                 $startedattack = 0;
  1202.             }
  1203.             Misc::checkValidity("actor_action (attack 4)");
  1204.             calcStat($args->{damage});
  1205.             Misc::checkValidity("actor_action (attack 5)");
  1206.  
  1207.         } elsif ($args->{targetID} eq $accountID) {
  1208.             message("$status $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
  1209.             if ($args->{damage} > 0) {
  1210.                 $damageTaken{$source->{name}}{attack} += $args->{damage};
  1211.             }
  1212.  
  1213.         } elsif ($char->{slaves} && $char->{slaves}{$args->{sourceID}}) {
  1214.             message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{sourceID}}{hpPercent}, $char->{slaves}{$args->{sourceID}}{spPercent}) . " $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
  1215.  
  1216.         } elsif ($char->{slaves} && $char->{slaves}{$args->{targetID}}) {
  1217.             message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{targetID}}{hpPercent}, $char->{slaves}{$args->{targetID}}{spPercent}) . " $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
  1218.  
  1219.         } elsif ($args->{sourceID} eq $args->{targetID}) {
  1220.             message("$status $msg");
  1221.  
  1222.         } elsif ($config{showAllDamage}) {
  1223.             message("$status $msg");
  1224.  
  1225.         } else {
  1226.             debug("$msg", 'parseMsg_damage');
  1227.         }
  1228.  
  1229.         Misc::checkValidity("actor_action (attack 6)");
  1230.     }
  1231. }
  1232.  
  1233. sub actor_info {
  1234.     my ($self, $args) = @_;
  1235.     return unless changeToInGameState();
  1236.  
  1237.     debug "Received object info: $args->{name}\n", "parseMsg_presence/name", 2;
  1238.     my $player = $playersList->getByID($args->{ID});
  1239.     if ($player) {
  1240.         # 0095: This packet tells us the names of players who aren't in a guild.
  1241.         # 0195: Receive names of players who are in a guild.
  1242.         # FIXME: There is more to this packet than just party name and guild name.
  1243.         # This packet is received when you leave a guild
  1244.         # (with cryptic party and guild name fields, at least for now)
  1245.         $player->setName(bytesToString($args->{name}));
  1246.         $player->{info} = 1;
  1247.  
  1248.         $player->{party}{name} = bytesToString($args->{partyName}) if defined $args->{partyName};
  1249.         $player->{guild}{name} = bytesToString($args->{guildName}) if defined $args->{guildName};
  1250.         $player->{guild}{title} = bytesToString($args->{guildTitle}) if defined $args->{guildTitle};
  1251.         $player->{title}{ID} = $args->{titleID} if defined $args->{titleID};
  1252.         message "Player Info: " . $player->nameIdx . "\n", "parseMsg_presence", 2;
  1253.         updatePlayerNameCache($player);
  1254.         Plugins::callHook('charNameUpdate', {player => $player});
  1255.     }
  1256.  
  1257.     my $monster = $monstersList->getByID($args->{ID});
  1258.     if ($monster) {
  1259.         my $name = bytesToString($args->{name});
  1260.         $name =~ s/^\s+|\s+$//g;
  1261.         debug "Monster Info: $name ($monster->{binID})\n", "parseMsg", 2;
  1262.         $monster->{name_given} = $name;
  1263.         $monster->{info} = 1;
  1264.         if ($monsters_lut{$monster->{nameID}} eq "") {
  1265.             $monster->setName($name);
  1266.             $monsters_lut{$monster->{nameID}} = $name;
  1267.             updateMonsterLUT(Settings::getTableFilename("monsters.txt"), $monster->{nameID}, $name);
  1268.             Plugins::callHook('mobNameUpdate', {monster => $monster});
  1269.         }
  1270.     }
  1271.  
  1272.     my $npc = $npcs{$args->{ID}};
  1273.     if ($npc) {
  1274.         $npc->setName(bytesToString($args->{name}));
  1275.         $npc->{info} = 1;
  1276.         if ($config{debug} >= 2) {
  1277.             my $binID = binFind(\@npcsID, $args->{ID});
  1278.             debug "NPC Info: $npc->{name} ($binID)\n", "parseMsg", 2;
  1279.         }
  1280.  
  1281.         my $location = $field->baseName . " $npc->{pos}{x} $npc->{pos}{y}";
  1282.         if (!$npcs_lut{$location}) {
  1283.             $npcs_lut{$location} = $npc->{name};
  1284.             updateNPCLUT(Settings::getTableFilename("npcs.txt"), $location, $npc->{name});
  1285.         }
  1286.         Plugins::callHook('npcNameUpdate', {npc => $npc});
  1287.     }
  1288.  
  1289.     my $pet = $pets{$args->{ID}};
  1290.     if ($pet) {
  1291.         my $name = bytesToString($args->{name});
  1292.         $pet->{name_given} = $name;
  1293.         $pet->setName($name);
  1294.         $pet->{info} = 1;
  1295.         if ($config{debug} >= 2) {
  1296.             my $binID = binFind(\@petsID, $args->{ID});
  1297.             debug "Pet Info: $pet->{name_given} ($binID)\n", "parseMsg", 2;
  1298.         }
  1299.         Plugins::callHook('petNameUpdate', {pet => $pet});
  1300.     }
  1301.  
  1302.     my $slave = $slavesList->getByID($args->{ID});
  1303.     if ($slave) {
  1304.         my $name = bytesToString($args->{name});
  1305.         $slave->{name_given} = $name;
  1306.         $slave->setName($name);
  1307.         $slave->{info} = 1;
  1308.         my $binID = binFind(\@slavesID, $args->{ID});
  1309.         debug "Slave Info: $name ($binID)\n", "parseMsg_presence", 2;
  1310.         updatePlayerNameCache($slave);
  1311.         Plugins::callHook('slaveNameUpdate', {slave => $slave});
  1312.     }
  1313.  
  1314.     # TODO: $args->{ID} eq $accountID
  1315. }
  1316.  
  1317. use constant QTYPE => (
  1318.     0x0 => [0xff, 0xff, 0, 0],
  1319.     0x1 => [0xff, 0x80, 0, 0],
  1320.     0x2 => [0, 0xff, 0, 0],
  1321.     0x3 => [0x80, 0, 0x80, 0],
  1322. );
  1323.  
  1324. sub parse_minimap_indicator {
  1325.     my ($self, $args) = @_;
  1326.  
  1327.     $args->{actor} = Actor::get($args->{npcID});
  1328.     $args->{show} = $args->{type} != 2;
  1329.  
  1330.     unless (defined $args->{red}) {
  1331.         @{$args}{qw(red green blue alpha)} = @{{QTYPE}->{$args->{qtype}} || [0xff, 0xff, 0xff, 0]};
  1332.     }
  1333.  
  1334.     # FIXME: packet 0144: coordinates are missing now when clearing indicators; ID is used
  1335.     # Wx depends on coordinates there
  1336. }
  1337.  
  1338. sub account_payment_info {
  1339.     my ($self, $args) = @_;
  1340.     my $D_minute = $args->{D_minute};
  1341.     my $H_minute = $args->{H_minute};
  1342.  
  1343.     my $D_d = int($D_minute / 1440);
  1344.     my $D_h = int(($D_minute % 1440) / 60);
  1345.     my $D_m = int(($D_minute % 1440) % 60);
  1346.  
  1347.     my $H_d = int($H_minute / 1440);
  1348.     my $H_h = int(($H_minute % 1440) / 60);
  1349.     my $H_m = int(($H_minute % 1440) % 60);
  1350.  
  1351.     message  T("============= Account payment information =============\n"), "info";
  1352.     message TF("Pay per day  : %s day(s) %s hour(s) and %s minute(s)\n", $D_d, $D_h, $D_m), "info";
  1353.     message TF("Pay per hour : %s day(s) %s hour(s) and %s minute(s)\n", $H_d, $H_h, $H_m), "info";
  1354.     message  "-------------------------------------------------------\n", "info";
  1355. }
  1356.  
  1357. # TODO
  1358. sub reconstruct_minimap_indicator {
  1359. }
  1360.  
  1361. use constant {
  1362.     HO_PRE_INIT => 0x0,
  1363.     HO_RELATIONSHIP_CHANGED => 0x1,
  1364.     HO_FULLNESS_CHANGED => 0x2,
  1365.     HO_ACCESSORY_CHANGED => 0x3,
  1366.     HO_HEADTYPE_CHANGED => 0x4,
  1367. };
  1368.  
  1369. # 0230
  1370. # TODO: what is type?
  1371. sub homunculus_info {
  1372.     my ($self, $args) = @_;
  1373.     debug "homunculus_info type: $args->{type}\n", "homunculus";
  1374.     if ($args->{state} == HO_PRE_INIT) {
  1375.         my $state = $char->{homunculus}{state}
  1376.             if ($char->{homunculus} && $char->{homunculus}{ID} && $char->{homunculus}{ID} ne $args->{ID});
  1377.         $char->{homunculus} = Actor::get($args->{ID}) if ($char->{homunculus}{ID} ne $args->{ID});
  1378.         $char->{homunculus}{state} = $state if (defined $state);
  1379.         $char->{homunculus}{map} = $field->baseName;
  1380.         unless ($char->{slaves}{$char->{homunculus}{ID}}) {
  1381.             AI::SlaveManager::addSlave ($char->{homunculus});
  1382.             $char->{homunculus}{appear_time} = time;
  1383.         }
  1384.     } elsif ($args->{state} == HO_RELATIONSHIP_CHANGED) {
  1385.         $char->{homunculus}{intimacy} = $args->{val} if $char->{homunculus};
  1386.     } elsif ($args->{state} == HO_FULLNESS_CHANGED) {
  1387.         $char->{homunculus}{hunger} = $args->{val} if $char->{homunculus};
  1388.     } elsif ($args->{state} == HO_ACCESSORY_CHANGED) {
  1389.         $char->{homunculus}{accessory} = $args->{val} if $char->{homunculus};
  1390.     } elsif ($args->{state} == HO_HEADTYPE_CHANGED) {
  1391.         #
  1392.     }
  1393. }
  1394.  
  1395. ##
  1396. # minimap_indicator({bool show, Actor actor, int x, int y, int red, int green, int blue, int alpha [, int effect]})
  1397. # show: whether indicator is shown or cleared
  1398. # actor: @MODULE(Actor) who issued the indicator; or which Actor it's binded to
  1399. # x, y: indicator coordinates
  1400. # red, green, blue, alpha: indicator color
  1401. # effect: unknown, may be missing
  1402. #
  1403. # Minimap indicator.
  1404. sub minimap_indicator {
  1405.     my ($self, $args) = @_;
  1406.  
  1407.     my $color_str = "[R:$args->{red}, G:$args->{green}, B:$args->{blue}, A:$args->{alpha}]";
  1408.     my $indicator = T("minimap indicator");
  1409.     if (defined $args->{type}) {
  1410.         unless ($args->{type} == 1 || $args->{type} == 2) {
  1411.             $indicator .= TF(" (unknown type %d)", $args->{type});
  1412.         }
  1413.     } elsif (defined $args->{effect}) {
  1414.         if ($args->{effect} == 1) {
  1415.             $indicator = T("*Quest!*");
  1416.         } elsif ($args->{effect}) { # 0 is no effect
  1417.             $indicator = TF("unknown effect %d", $args->{effect});
  1418.         }
  1419.     }
  1420.  
  1421.     if ($args->{show}) {
  1422.         message TF("%s shown %s at location %d, %d " .
  1423.         "with the color %s\n", $args->{actor}, $indicator, @{$args}{qw(x y)}, $color_str),
  1424.         'effect';
  1425.     } else {
  1426.         message TF("%s cleared %s at location %d, %d " .
  1427.         "with the color %s\n", $args->{actor}, $indicator, @{$args}{qw(x y)}, $color_str),
  1428.         'effect';
  1429.     }
  1430. }
  1431.  
  1432. # 0x01B3
  1433. sub parse_npc_image {
  1434.     my ($self, $args) = @_;
  1435.  
  1436.     $args->{npc_image} = bytesToString($args->{npc_image});
  1437. }
  1438.  
  1439. sub reconstruct_npc_image {
  1440.     my ($self, $args) = @_;
  1441.  
  1442.     $args->{npc_image} = stringToBytes($args->{npc_image});
  1443. }
  1444.  
  1445. sub npc_image {
  1446.     my ($self, $args) = @_;
  1447.  
  1448.     if ($args->{type} == 2) {
  1449.         message TF("NPC image: %s\n", $args->{npc_image}), 'npc';
  1450.     } elsif ($args->{type} == 255) {
  1451.         debug "Hide NPC image: $args->{npc_image}\n", "parseMsg";
  1452.     } else {
  1453.         message TF("NPC image: %s (unknown type %s)\n", $args->{npc_image}, $args->{type}), 'npc';
  1454.     }
  1455.  
  1456.     unless ($args->{type} == 255) {
  1457.         $talk{image} = $args->{npc_image};
  1458.     } else {
  1459.         delete $talk{image};
  1460.     }
  1461. }
  1462.  
  1463. sub local_broadcast {
  1464.     my ($self, $args) = @_;
  1465.     my $message = bytesToString($args->{message});
  1466.     my $color = uc(sprintf("%06x", $args->{color})); # hex code
  1467.     stripLanguageCode(\$message);
  1468.     chatLog("lb", "$message\n");# if ($config{logLocalBroadcast});
  1469.     message "$message\n", "schat";
  1470.     Plugins::callHook('packet_localBroadcast', {
  1471.         Msg => $message,
  1472.         color => $color
  1473.     });
  1474. }
  1475.  
  1476. sub parse_sage_autospell {
  1477.     my ($self, $args) = @_;
  1478.  
  1479.     $args->{skills} = [map { Skill->new(idn => $_) } sort { $a<=>$b } grep {$_}
  1480.         exists $args->{autoshadowspell_list}
  1481.         ? (unpack 'v*', $args->{autoshadowspell_list})
  1482.         : (unpack 'V*', $args->{autospell_list})
  1483.     ];
  1484. }
  1485.  
  1486. sub reconstruct_sage_autospell {
  1487.     my ($self, $args) = @_;
  1488.  
  1489.     my @skillIDs = map { $_->getIDN } $args->{skills};
  1490.     $args->{autoshadowspell_list} = pack 'v*', @skillIDs;
  1491.     $args->{autospell_list} = pack 'V*', @skillIDs;
  1492. }
  1493.  
  1494. ##
  1495. # sage_autospell({arrayref skills, int why})
  1496. # skills: list of @MODULE(Skill) instances
  1497. # why: unknown
  1498. #
  1499. # Skill list for Sage's Hindsight and Shadow Chaser's Auto Shadow Spell.
  1500. sub sage_autospell {
  1501.     my ($self, $args) = @_;
  1502.  
  1503.     return unless $self->changeToInGameState;
  1504.  
  1505.     my $msg = center(' ' . T('Auto Spell') . ' ', 40, '-') . "\n"
  1506.     . T("   # Skill\n")
  1507.     . (join '', map { swrite '@>>> @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<', [$_->getIDN, $_] } @{$args->{skills}})
  1508.     . ('-'x40) . "\n";
  1509.  
  1510.     message $msg, 'list';
  1511.  
  1512.     if ($config{autoSpell}) {
  1513.         my @autoSpells = split /\s*,\s*/, $config{autoSpell};
  1514.         for my $autoSpell (@autoSpells) {
  1515.             my $skill = new Skill(auto => $autoSpell);
  1516.             message 'Testing autoSpell ' . $autoSpell . "\n";
  1517.             if (!$config{autoSpell_safe} || List::Util::first { $_->getIDN == $skill->getIDN } @{$args->{skills}}) {
  1518.                 if (defined $args->{why}) {
  1519.                     $messageSender->sendSkillSelect($skill->getIDN, $args->{why});
  1520.                     return;
  1521.                 } else {
  1522.                     $messageSender->sendAutoSpell($skill->getIDN);
  1523.                     return;
  1524.                 }
  1525.             }
  1526.         }
  1527.         error TF("Configured autoSpell (%s) not available.\n", $config{autoSpell});
  1528.         message T("Disable autoSpell_safe to use it anyway.\n"), 'hint';
  1529.     } else {
  1530.         message T("Configure autoSpell to automatically select skill for Auto Spell.\n"), 'hint';
  1531.     }
  1532. }
  1533.  
  1534. sub show_eq {
  1535.     my ($self, $args) = @_;
  1536.     my $item_info;
  1537.     my @item;
  1538.    
  1539.     if ($args->{switch} eq '02D7') {  # PACKETVER DEFAULT  
  1540.         $item_info = {
  1541.             len => 26,
  1542.             types => 'a2 v C2 v2 C2 a8 l v',
  1543.             keys => [qw(ID nameID type identified type_equip equipped broken upgrade cards expire bindOnEquipType)],
  1544.         };
  1545.        
  1546.         if (exists $args->{robe}) {  # PACKETVER >= 20100629
  1547.             $item_info->{type} .= 'v';
  1548.             $item_info->{len} += 2;
  1549.         }
  1550.        
  1551.     } elsif ($args->{switch} eq '0906') {  # PACKETVER >= ?? NOT IMPLEMENTED ON EATHENA BASED EMULATOR 
  1552.         $item_info = {
  1553.             len => 27,
  1554.             types => 'v2 C v2 C a8 l v2 C',
  1555.             keys => [qw(ID nameID type type_equip equipped upgrade cards expire bindOnEquipType sprite_id identified)],
  1556.         };
  1557.  
  1558.     } elsif ($args->{switch} eq '0859') { # PACKETVER >= 20101124  
  1559.         $item_info = {
  1560.             len => 28,
  1561.             types => 'a2 v C2 v2 C2 a8 l v2',
  1562.             keys => [qw(ID nameID type identified type_equip equipped broken upgrade cards expire bindOnEquipType sprite_id)],
  1563.         };
  1564.        
  1565.     } elsif ($args->{switch} eq '0997') { # PACKETVER >= 20120925
  1566.         $item_info = {
  1567.             len => 31,
  1568.             types => 'a2 v C V2 C a8 l v2 C',
  1569.             keys => [qw(ID nameID type type_equip equipped upgrade cards expire bindOnEquipType sprite_id identified)],
  1570.         };
  1571.        
  1572.     } elsif ($args->{switch} eq '0A2D') { # PACKETVER >= 20150226
  1573.         $item_info = {
  1574.             len => 57,
  1575.             types => 'a2 v C V2 C a8 l v2 C a25 C',
  1576.             keys => [qw(ID nameID type type_equip equipped upgrade cards expire bindOnEquipType sprite_id num_options options identified)],
  1577.         };
  1578.     } else { # this can't happen
  1579.         return;
  1580.     }
  1581.    
  1582.     message "--- $args->{name} Equip Info --- \n";
  1583.  
  1584.     for (my $i = 0; $i < length($args->{equips_info}); $i += $item_info->{len}) {
  1585.         my $item;      
  1586.         @{$item}{@{$item_info->{keys}}} = unpack($item_info->{types}, substr($args->{equips_info}, $i, $item_info->{len}));        
  1587.         $item->{broken} = 0;
  1588.         $item->{identified} = 1;       
  1589.         message sprintf("%-20s: %s\n", $equipTypes_lut{$item->{equipped}}, itemName($item)), "list";
  1590.     }
  1591.    
  1592.     message "----------------- \n";
  1593.    
  1594. }
  1595.  
  1596. sub show_eq_msg_other {
  1597.     my ($self, $args) = @_;
  1598.     if ($args->{flag}) {
  1599.         message T("Allowed to view the other player's Equipment.\n");
  1600.     } else {
  1601.         message T("Not allowed to view the other player's Equipment.\n");
  1602.     }
  1603. }
  1604.  
  1605. sub show_eq_msg_self {
  1606.     my ($self, $args) = @_;
  1607.     if ($args->{type}) {
  1608.         message T("Other players are allowed to view your Equipment.\n");
  1609.     } else {
  1610.         message T("Other players are not allowed to view your Equipment.\n");
  1611.     }
  1612. }
  1613.  
  1614. # 043D
  1615. sub skill_post_delay {
  1616.     my ($self, $args) = @_;
  1617.  
  1618.     my $skillName = (new Skill(idn => $args->{ID}))->getName;
  1619.     my $status = defined $statusName{'EFST_DELAY'} ? $statusName{'EFST_DELAY'} : 'Delay';
  1620.  
  1621.     $char->setStatus($skillName." ".$status, 1, $args->{time});
  1622. }
  1623.  
  1624. # TODO: known prefixes (chat domains): micc | ssss | blue | tool
  1625. # micc = micc<24 characters, this is the sender name. seems like it's null padded><hex color code><message>
  1626. # micc = Player Broadcast   The struct: micc<23bytes player name+some hex><\x00><colour code><full message>
  1627. # The first player name is used for detecting the player name only according to the disassembled client.
  1628. # The full message contains the player name at the first 22 bytes
  1629. # TODO micc.* is currently unstricted, however .{24} and .{23} do not detect chinese with some reasons, please improve this regex if necessary
  1630. sub system_chat {
  1631.     my ($self, $args) = @_;
  1632.     my $message = bytesToString($args->{message});
  1633.     my $prefix;
  1634.     my $color;
  1635.     if ($message =~ s/^ssss//g) {  # forces color yellow, or WoE indicator?
  1636.         $prefix = T('[WoE]');
  1637.     } elsif ($message =~ /^micc.*\0\0([0-9a-fA-F]{6})(.*)/) { #appears in twRO   ## [micc][name][\x00\x00][unknown][\x00\x00][color][name][blablabla][message]
  1638.         ($color, $message) = $message =~ /^micc.*\0\0([0-9a-fA-F]{6})(.*)/;
  1639.         $prefix = T('[S]');
  1640.     } elsif ($message =~ /^micc.{12,24}([0-9a-fA-F]{6})(.*)/) {
  1641.         ($color, $message) = $message =~ /^micc.*([0-9a-fA-F]{6})(.*)/;
  1642.         $prefix = T('[S]');
  1643.     } elsif ($message =~ s/^blue//g) {  # forces color blue
  1644.         $prefix = T('[S]');
  1645.     } elsif ($message =~ /^tool([0-9a-fA-F]{6})(.*)/) {
  1646.         ($color, $message) = $message =~ /^tool([0-9a-fA-F]{6})(.*)/;
  1647.         $prefix = T('[S]');
  1648.     } else {
  1649.         $prefix = T('[S]');
  1650.     }
  1651.     $message =~ s/\000//g; # remove null charachters
  1652.     $message =~ s/^ +//g; $message =~ s/ +$//g; # remove whitespace in the beginning and the end of $message
  1653.     stripLanguageCode(\$message);
  1654.     chatLog("s", "$message\n") if ($config{logSystemChat});
  1655.     # Translation Comment: System/GM chat
  1656.     message "$prefix $message\n", "schat";
  1657.     ChatQueue::add('gm', undef, undef, $message) if ($config{callSignGM});
  1658.  
  1659.     Plugins::callHook('packet_sysMsg', {
  1660.         Msg => $message,
  1661.         MsgColor => $color,
  1662.         MsgUser => undef # TODO: implement this value, we can get this from "micc" messages by regex.
  1663.     });
  1664. }
  1665.  
  1666. sub warp_portal_list {
  1667.     my ($self, $args) = @_;
  1668.  
  1669.     # strip gat extension
  1670.     ($args->{memo1}) = $args->{memo1} =~ /^(.*)\.gat/;
  1671.     ($args->{memo2}) = $args->{memo2} =~ /^(.*)\.gat/;
  1672.     ($args->{memo3}) = $args->{memo3} =~ /^(.*)\.gat/;
  1673.     ($args->{memo4}) = $args->{memo4} =~ /^(.*)\.gat/;
  1674.     # Auto-detect saveMap
  1675.     if ($args->{type} == 26) {
  1676.         configModify('saveMap', $args->{memo2}) if ($args->{memo2} && $config{'saveMap'} ne $args->{memo2});
  1677.     } elsif ($args->{type} == 27) {
  1678.         configModify('saveMap', $args->{memo1}) if ($args->{memo1} && $config{'saveMap'} ne $args->{memo1});
  1679.         configModify( "memo$_", $args->{"memo$_"} ) foreach grep { $args->{"memo$_"} ne $config{"memo$_"} } 1 .. 4;
  1680.     }
  1681.  
  1682.     $char->{warp}{type} = $args->{type};
  1683.     undef @{$char->{warp}{memo}};
  1684.     push @{$char->{warp}{memo}}, $args->{memo1} if $args->{memo1} ne "";
  1685.     push @{$char->{warp}{memo}}, $args->{memo2} if $args->{memo2} ne "";
  1686.     push @{$char->{warp}{memo}}, $args->{memo3} if $args->{memo3} ne "";
  1687.     push @{$char->{warp}{memo}}, $args->{memo4} if $args->{memo4} ne "";
  1688.  
  1689.     my $msg = center(T(" Warp Portal "), 50, '-') ."\n".
  1690.         T("#  Place                           Map\n");
  1691.     for (my $i = 0; $i < @{$char->{warp}{memo}}; $i++) {
  1692.         $msg .= swrite(
  1693.             "@< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<",
  1694.             [$i, $maps_lut{$char->{warp}{memo}[$i].'.rsw'}, $char->{warp}{memo}[$i]]);
  1695.     }
  1696.     $msg .= ('-'x50) . "\n";
  1697.     message $msg, "list";
  1698.    
  1699.     if ($args->{type} == 26 && AI::inQueue('teleport')) {
  1700.         # We have already successfully used the Teleport skill.
  1701.         $messageSender->sendWarpTele(26, AI::args->{lv} == 2 ? "$config{saveMap}.gat" : "Random");
  1702.         AI::dequeue;
  1703.     }
  1704. }
  1705.  
  1706.  
  1707. # 0828,14
  1708. sub char_delete2_result {
  1709.     my ($self, $args) = @_;
  1710.     my $result = $args->{result};
  1711.     my $deleteDate = $args->{deleteDate};
  1712.  
  1713.     if ($result && $deleteDate) {
  1714.         setCharDeleteDate($messageSender->{char_delete_slot}, $deleteDate);
  1715.         message TF("Your character will be delete, left %s\n", $chars[$messageSender->{char_delete_slot}]{deleteDate}), "connection";
  1716.     } elsif ($result == 0) {
  1717.         error T("That character already planned to be erased!\n");
  1718.     } elsif ($result == 3) {
  1719.         error T("Error in database of the server!\n");
  1720.     } elsif ($result == 4) {
  1721.         error T("To delete a character you must withdraw from the guild!\n");
  1722.     } elsif ($result == 5) {
  1723.         error T("To delete a character you must withdraw from the party!\n");
  1724.     } else {
  1725.         error TF("Unknown error when trying to delete the character! (Error number: %s)\n", $result);
  1726.     }
  1727.  
  1728.     charSelectScreen;
  1729. }
  1730.  
  1731. # 082A,10
  1732. sub char_delete2_accept_result {
  1733.     my ($self, $args) = @_;
  1734.     my $charID = $args->{charID};
  1735.     my $result = $args->{result};
  1736.  
  1737.     if ($result == 1) { # Success
  1738.         if (defined $AI::temp::delIndex) {
  1739.             message TF("Character %s (%d) deleted.\n", $chars[$AI::temp::delIndex]{name}, $AI::temp::delIndex), "info";
  1740.             delete $chars[$AI::temp::delIndex];
  1741.             undef $AI::temp::delIndex;
  1742.             for (my $i = 0; $i < @chars; $i++) {
  1743.                 delete $chars[$i] if ($chars[$i] && !scalar(keys %{$chars[$i]}))
  1744.             }
  1745.         } else {
  1746.             message T("Character deleted.\n"), "info";
  1747.         }
  1748.  
  1749.         if (charSelectScreen() == 1) {
  1750.             $net->setState(3);
  1751.             $firstLoginMap = 1;
  1752.             $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  1753.             $sentWelcomeMessage = 1;
  1754.         }
  1755.         return;
  1756.     } elsif ($result == 0) {
  1757.         error T("Enter your 6-digit birthday (YYMMDD) (e.g: 801122).\n");
  1758.     } elsif ($result == 2) {
  1759.         error T("Due to system settings, can not be deleted.\n");
  1760.     } elsif ($result == 3) {
  1761.         error T("A database error has occurred.\n");
  1762.     } elsif ($result == 4) {
  1763.         error T("You cannot delete this character at the moment.\n");
  1764.     } elsif ($result == 5) {
  1765.         error T("Your entered birthday does not match.\n");
  1766.     } elsif ($result == 7) {
  1767.         error T("Character Deletion has failed because you have entered an incorrect e-mail address.\n");
  1768.     } else {
  1769.         error TF("An unknown error has occurred. Error number %d\n", $result);
  1770.     }
  1771.  
  1772.     undef $AI::temp::delIndex;
  1773.     if (charSelectScreen() == 1) {
  1774.         $net->setState(3);
  1775.         $firstLoginMap = 1;
  1776.         $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  1777.         $sentWelcomeMessage = 1;
  1778.     }
  1779. }
  1780.  
  1781. # 082C,14
  1782. sub char_delete2_cancel_result {
  1783.     my ($self, $args) = @_;
  1784.     my $result = $args->{result};
  1785.  
  1786.     if ($result) {
  1787.         message T("Character is no longer scheduled to be deleted\n"), "connection";
  1788.         $chars[$messageSender->{char_delete_slot}]{deleteDate} = '';
  1789.     } elsif ($result == 2) {
  1790.         error T("Error in database of the server!\n");
  1791.     } else {
  1792.         error TF("Unknown error when trying to cancel the deletion of the character! (Error number: %s)\n", $result);
  1793.     }
  1794.  
  1795.     charSelectScreen;
  1796. }
  1797.  
  1798. # 013C
  1799. sub arrow_equipped {
  1800.     my ($self, $args) = @_;
  1801.     return unless changeToInGameState();
  1802.     return unless $args->{ID};
  1803.     $char->{arrow} = $args->{ID};
  1804.  
  1805.     my $item = $char->inventory->getByID($args->{ID});
  1806.     if ($item && $char->{equipment}{arrow} != $item) {
  1807.         $char->{equipment}{arrow} = $item;
  1808.         $item->{equipped} = 32768;
  1809.         $ai_v{temp}{waitForEquip}-- if $ai_v{temp}{waitForEquip};
  1810.         message TF("Arrow/Bullet equipped: %s (%d) x %s\n", $item->{name}, $item->{binID}, $item->{amount});
  1811.         Plugins::callHook('equipped_item', {slot => 'arrow', item => $item});
  1812.     }
  1813. }
  1814.  
  1815. # 00AF, 07FA
  1816. sub inventory_item_removed {
  1817.     my ($self, $args) = @_;
  1818.     return unless changeToInGameState();
  1819.     my $item = $char->inventory->getByID($args->{ID});
  1820.     my $reason = $args->{reason};
  1821.  
  1822.     if ($reason) {
  1823.         if ($reason == 1) {
  1824.             debug TF("%s was used to cast the skill\n", $item->{name}), "inventory", 1;
  1825.         } elsif ($reason == 2) {
  1826.             debug TF("%s broke due to the refinement failed\n", $item->{name}), "inventory", 1;
  1827.         } elsif ($reason == 3) {
  1828.             debug TF("%s used in a chemical reaction\n", $item->{name}), "inventory", 1;
  1829.         } elsif ($reason == 4) {
  1830.             debug TF("%s was moved to the storage\n", $item->{name}), "inventory", 1;
  1831.         } elsif ($reason == 5) {
  1832.             debug TF("%s was moved to the cart\n", $item->{name}), "inventory", 1;
  1833.         } elsif ($reason == 6) {
  1834.             debug TF("%s was sold\n", $item->{name}), "inventory", 1;
  1835.         } elsif ($reason == 7) {
  1836.             debug TF("%s was consumed by Four Spirit Analysis skill\n", $item->{name}), "inventory", 1;
  1837.         } else {
  1838.             debug TF("%s was consumed by an unknown reason (reason number %s)\n", $item->{name}, $reason), "inventory", 1;
  1839.         }
  1840.     }
  1841.  
  1842.     if ($item) {
  1843.         inventoryItemRemoved($item->{binID}, $args->{amount});
  1844.         Plugins::callHook('packet_item_removed', {index => $item->{binID}});
  1845.     }
  1846. }
  1847.  
  1848. # 012B
  1849. sub cart_off {
  1850.     $char->cart->close;
  1851.     message T("Cart released.\n"), "success";
  1852. }
  1853.  
  1854. # 012D
  1855. sub shop_skill {
  1856.     my ($self, $args) = @_;
  1857.  
  1858.     # Used the shop skill.
  1859.     my $number = $args->{number};
  1860.     message TF("You can sell %s items!\n", $number);
  1861. }
  1862.  
  1863. # Your shop has sold an item -- one packet sent per item sold.
  1864. #
  1865. sub shop_sold {
  1866.     my ($self, $args) = @_;
  1867.  
  1868.     # sold something
  1869.     my $number = $args->{number};
  1870.     my $amount = $args->{amount};
  1871.  
  1872.     $articles[$number]{sold} += $amount;
  1873.     my $earned = $amount * $articles[$number]{price};
  1874.     $shopEarned += $earned;
  1875.     $articles[$number]{quantity} -= $amount;
  1876.     my $msg = TF("Sold: %s x %s - %sz\n", $articles[$number]{name}, $amount, $earned);
  1877.     shopLog($msg) if $config{logShop};
  1878.     message($msg, "sold");
  1879.  
  1880.     # Call hook before we possibly remove $articles[$number] or
  1881.     # $articles itself as a result of the sale.
  1882.     Plugins::callHook(
  1883.         'vending_item_sold',
  1884.         {
  1885.             'vendShopIndex' => $number,
  1886.             'amount' => $amount,
  1887.             'vendArticle' => $articles[$number], #This is a hash
  1888.             'zenyEarned' => $earned,
  1889.             'packetType' => "short",
  1890.         }
  1891.     );
  1892.  
  1893.     # Adjust the shop's articles for sale, and notify if the sold
  1894.     # item and/or the whole shop has been sold out.
  1895.     if ($articles[$number]{quantity} < 1) {
  1896.         message TF("Sold out: %s\n", $articles[$number]{name}), "sold";
  1897.         Plugins::callHook(
  1898.             'vending_item_sold_out',
  1899.             {
  1900.                 'vendShopIndex' => $number,
  1901.                 'vendArticle' => $articles[$number],
  1902.             }
  1903.         );
  1904.         #$articles[$number] = "";
  1905.         if (!--$articles){
  1906.             message T("Items have been sold out.\n"), "sold";
  1907.             closeShop();
  1908.         }
  1909.     }
  1910. }
  1911.  
  1912. sub shop_sold_long {
  1913.     my ($self, $args) = @_;
  1914.  
  1915.     # sold something
  1916.     my $number = $args->{number};
  1917.     my $amount = $args->{amount};
  1918.     my $earned = $args->{zeny};
  1919.     my $charID = getHex($args->{charID});
  1920.     my $when = $args->{time};
  1921.  
  1922.     $articles[$number]{sold} += $amount;
  1923.     $shopEarned += $earned;
  1924.     $articles[$number]{quantity} -= $amount;
  1925.    
  1926.     my $msg = TF("Sold: %s x %s - %sz (Buyer charID: %s)\n", $articles[$number]{name}, $amount, $earned, $charID);
  1927.     shopLog($msg) if $config{logShop};
  1928.     message("[" . getFormattedDate($when) . "] " . $msg, "sold");
  1929.  
  1930.     # Call hook before we possibly remove $articles[$number] or
  1931.     # $articles itself as a result of the sale.
  1932.     Plugins::callHook(
  1933.         'vending_item_sold',
  1934.         {
  1935.             'vendShopIndex' => $number,
  1936.             'amount' => $amount,
  1937.             'vendArticle' => $articles[$number], #This is a hash
  1938.             'buyerCharID' => $args->{charID},
  1939.             'zenyEarned' => $earned,
  1940.             'time' => $when,
  1941.             'packetType' => "long",
  1942.         }
  1943.     );
  1944.  
  1945.     # Adjust the shop's articles for sale, and notify if the sold
  1946.     # item and/or the whole shop has been sold out.
  1947.     if ($articles[$number]{quantity} < 1) {
  1948.         message TF("Sold out: %s\n", $articles[$number]{name}), "sold";
  1949.         Plugins::callHook(
  1950.             'vending_item_sold_out',
  1951.             {
  1952.                 'vendShopIndex' => $number,
  1953.                 'vendArticle' => $articles[$number],
  1954.             }
  1955.         );
  1956.         #$articles[$number] = "";
  1957.         if (!--$articles){
  1958.             message T("Items have been sold out.\n"), "sold";
  1959.             closeShop();
  1960.         }
  1961.     }
  1962. }
  1963.  
  1964. # 01D0 (spirits), 01E1 (coins), 08CF (amulets)
  1965. sub revolving_entity {
  1966.     my ($self, $args) = @_;
  1967.  
  1968.     # Monk Spirits or Gunslingers' coins or senior ninja
  1969.     my $sourceID = $args->{sourceID};
  1970.     my $entityNum = $args->{entity};
  1971.     my $entityElement = $elements_lut{$args->{type}} if ($args->{type} && $entityNum);
  1972.     my $entityType;
  1973.  
  1974.     my $actor = Actor::get($sourceID);
  1975.     if ($args->{switch} eq '01D0') {
  1976.         # Translation Comment: Spirit sphere of the monks
  1977.         $entityType = T('spirit');
  1978.     } elsif ($args->{switch} eq '01E1') {
  1979.         # Translation Comment: Coin of the gunslinger
  1980.         $entityType = T('coin');
  1981.     } elsif ($args->{switch} eq '08CF') {
  1982.         # Translation Comment: Amulet of the warlock
  1983.         $entityType = T('amulet');
  1984.     } else {
  1985.         $entityType = T('entity unknown');
  1986.     }
  1987.  
  1988.     if ($sourceID eq $accountID && $entityNum != $char->{spirits}) {
  1989.         $char->{spirits} = $entityNum;
  1990.         $char->{amuletType} = $entityElement;
  1991.         $char->{spiritsType} = $entityType;
  1992.         $entityElement ?
  1993.             # Translation Comment: Message displays following: quantity, the name of the entity and its element
  1994.             message TF("You have %s %s(s) of %s now\n", $entityNum, $entityType, $entityElement), "parseMsg_statuslook", 1 :
  1995.             # Translation Comment: Message displays following: quantity and the name of the entity
  1996.             message TF("You have %s %s(s) now\n", $entityNum, $entityType), "parseMsg_statuslook", 1;
  1997.     } elsif ($entityNum != $actor->{spirits}) {
  1998.         $actor->{spirits} = $entityNum;
  1999.         $actor->{amuletType} = $entityElement;
  2000.         $actor->{spiritsType} = $entityType;
  2001.         $entityElement ?
  2002.             # Translation Comment: Message displays following: actor, quantity, the name of the entity and its element
  2003.             message TF("%s has %s %s(s) of %s now\n", $actor, $entityNum, $entityType, $entityElement), "parseMsg_statuslook", 1 :
  2004.             # Translation Comment: Message displays following: actor, quantity and the name of the entity
  2005.             message TF("%s has %s %s(s) now\n", $actor, $entityNum, $entityType), "parseMsg_statuslook", 1;
  2006.     }
  2007. }
  2008.  
  2009. # 0977
  2010. sub monster_hp_info {
  2011.     my ($self, $args) = @_;
  2012.     my $monster = $monstersList->getByID($args->{ID});
  2013.     if ($monster) {
  2014.         $monster->{hp} = $args->{hp};
  2015.         $monster->{hp_max} = $args->{hp_max};
  2016.  
  2017.         debug TF("Monster %s has hp %s/%s (%s%)\n", $monster->name, $monster->{hp}, $monster->{hp_max}, $monster->{hp} * 100 / $monster->{hp_max}), "parseMsg_damage";
  2018.     }
  2019. }
  2020.  
  2021. ##
  2022. # account_id({accountID})
  2023. #
  2024. # This is for what eA calls PacketVersion 9, they send the AID in a 'proper' packet
  2025. sub account_id {
  2026.     my ($self, $args) = @_;
  2027.     # the account ID is already unpacked into PLAIN TEXT when it gets to this function...
  2028.     # So lets not fuckup the $accountID since we need that later... someone will prolly have to fix this later on
  2029.     my $accountID = $args->{accountID};
  2030.     debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
  2031. }
  2032.  
  2033. ##
  2034. # marriage_partner_name({String name})
  2035. #
  2036. # Name of the partner character, sent to everyone around right before casting "I miss you".
  2037. sub marriage_partner_name {
  2038.     my ($self, $args) = @_;
  2039.  
  2040.     message TF("Marriage partner name: %s\n", $args->{name});
  2041. }
  2042.  
  2043. sub login_pin_code_request {
  2044.     # This is ten second-level password login for 2013/3/29 upgrading of twRO
  2045.     my ($self, $args) = @_;
  2046.  
  2047.     if($args->{flag} ne 0 && ($config{XKore} eq "1" || $config{XKore} eq "3")) {
  2048.         $timeout{master}{time} = time;
  2049.         return;
  2050.     }
  2051.  
  2052.     # flags:
  2053.     # 0 - correct
  2054.     # 1 - requested (already defined)
  2055.     # 2 - requested (not defined)
  2056.     # 3 - expired
  2057.     # 5 - invalid (official servers?)
  2058.     # 7 - disabled?
  2059.     # 8 - incorrect
  2060.     if ($args->{flag} == 0) { # removed check for seed 0, eA/rA/brA sends a normal seed.
  2061.         message T("PIN code is correct.\n"), "success";
  2062.         # call charSelectScreen
  2063.         if (charSelectScreen(1) == 1) {
  2064.             $firstLoginMap = 1;
  2065.             $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  2066.             $sentWelcomeMessage = 1;
  2067.         }
  2068.     } elsif ($args->{flag} == 1) {
  2069.         # PIN code query request.
  2070.         $accountID = $args->{accountID};
  2071.         debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
  2072.  
  2073.         message T("Server requested PIN password in order to select your character.\n"), "connection";
  2074.         return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
  2075.         $messageSender->sendLoginPinCode($args->{seed}, 0);
  2076.     } elsif ($args->{flag} == 2) {
  2077.         # PIN code has never been set before, so set it.
  2078.         warning T("PIN password is not set for this account.\n"), "connection";
  2079.         return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
  2080.  
  2081.         while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
  2082.           !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
  2083.             error T("Your PIN should never contain anything but exactly 4 numbers.\n");
  2084.         }
  2085.         $messageSender->sendLoginPinCode($args->{seed}, 1);
  2086.     } elsif ($args->{flag} == 3) {
  2087.         # should we use the same one again? is it possible?
  2088.         warning T("PIN password expired.\n"), "connection";
  2089.         return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
  2090.  
  2091.         while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
  2092.           !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
  2093.             error T("Your PIN should never contain anything but exactly 4 numbers.\n");
  2094.         }
  2095.         $messageSender->sendLoginPinCode($args->{seed}, 1);
  2096.     } elsif ($args->{flag} == 5) {
  2097.         # PIN code invalid.
  2098.         error T("PIN code is invalid, don't use sequences or repeated numbers.\n");
  2099.         # configModify('loginPinCode', '', 1);
  2100.         return if (!($self->queryAndSaveLoginPinCode(T("The login PIN code that you entered is invalid. Please re-enter your login PIN code."))));
  2101.         $messageSender->sendLoginPinCode($args->{seed}, 0);
  2102.     } elsif ($args->{flag} == 7) {
  2103.         # PIN code disabled.
  2104.         $accountID = $args->{accountID};
  2105.         debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
  2106.  
  2107.         # call charSelectScreen
  2108.         $self->{lockCharScreen} = 0;
  2109.         if (charSelectScreen(1) == 1) {
  2110.             $firstLoginMap = 1;
  2111.             $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  2112.             $sentWelcomeMessage = 1;
  2113.         }
  2114.     } elsif ($args->{flag} == 8) {
  2115.         # PIN code incorrect.
  2116.         error T("PIN code is incorrect.\n");
  2117.         #configModify('loginPinCode', '', 1);
  2118.         return if (!($self->queryAndSaveLoginPinCode(T("The login PIN code that you entered is incorrect. Please re-enter your login PIN code."))));
  2119.         $messageSender->sendLoginPinCode($args->{seed}, 0);
  2120.     } else {
  2121.         debug("login_pin_code_request: unknown flag $args->{flag}\n");
  2122.     }
  2123.  
  2124.     $timeout{master}{time} = time;
  2125. }
  2126.  
  2127. sub login_pin_new_code_result {
  2128.     my ($self, $args) = @_;
  2129.  
  2130.     if ($args->{flag} == 2) {
  2131.         # PIN code invalid.
  2132.         error T("PIN code is invalid, don't use sequences or repeated numbers.\n");
  2133.         #configModify('loginPinCode', '', 1);
  2134.         return if (!($self->queryAndSaveLoginPinCode(T("PIN code is invalid, don't use sequences or repeated numbers.\n"))));
  2135.  
  2136.         # there's a bug in bRO where you can use letters or symbols or even a string as your PIN code.
  2137.         # as a result this will render you unable to login again (forever?) using the official client
  2138.         # and this is detectable and can result in a permanent ban. we're using this code in order to
  2139.         # prevent this. - revok 17.12.2012
  2140.         while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
  2141.             !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
  2142.             error T("Your PIN should never contain anything but exactly 4 numbers.\n");
  2143.         }
  2144.  
  2145.         $messageSender->sendLoginPinCode($args->{seed}, 0);
  2146.     }
  2147. }
  2148.  
  2149. sub actor_status_active {
  2150.     my ($self, $args) = @_;
  2151.     return unless changeToInGameState();
  2152.     my ($type, $ID, $tick, $unknown1, $unknown2, $unknown3, $unknown4) = @{$args}{qw(type ID tick unknown1 unknown2 unknown3 unknown4)};
  2153.     my $flag = (exists $args->{flag}) ? $args->{flag} : 1;
  2154.     my $status = defined $statusHandle{$type} ? $statusHandle{$type} : "UNKNOWN_STATUS_$type";
  2155.     $char->cart->changeType($unknown1) if ($type == 673 && defined $unknown1 && ($ID eq $accountID)); # for Cart active
  2156.     $args->{skillName} = defined $statusName{$status} ? $statusName{$status} : $status;
  2157. #   ($args->{actor} = Actor::get($ID))->setStatus($status, 1, $tick == 9999 ? undef : $tick, $args->{unknown1}); # need test for '08FF'
  2158.     ($args->{actor} = Actor::get($ID))->setStatus($status, $flag, $tick == 9999 ? undef : $tick);
  2159.     # Rolling Cutter counters.
  2160.     if ( $type == 0x153 && $char->{spirits} != $unknown1 ) {
  2161.         $char->{spirits} = $unknown1 || 0;
  2162.         if ( $ID eq $accountID ) {
  2163.             message TF( "You have %s %s(s) now\n", $char->{spirits}, 'counters' ), "parseMsg_statuslook", 1;
  2164.         } else {
  2165.             message TF( "%s has %s %s(s) now\n", $args->{actor}, $char->{spirits}, 'counters' ), "parseMsg_statuslook", 1;
  2166.         }
  2167.     }
  2168. }
  2169.  
  2170. #099B
  2171. sub map_property3 {
  2172.     my ($self, $args) = @_;
  2173.  
  2174.     if($config{'status_mapType'}){
  2175.         $char->setStatus(@$_) for map {[$_->[1], $args->{type} == $_->[0]]}
  2176.         grep { $args->{type} == $_->[0] || $char->{statuses}{$_->[1]} }
  2177.         map {[$_, defined $mapTypeHandle{$_} ? $mapTypeHandle{$_} : "UNKNOWN_MAPTYPE_$_"]}
  2178.         0 .. List::Util::max $args->{type}, keys %mapTypeHandle;
  2179.  
  2180.         if ($args->{info_table}) {
  2181.             my $info_table = unpack('V1',$args->{info_table});
  2182.             for (my $i = 0; $i < 16; $i++) {
  2183.                 if ($info_table&(1<<$i)) {
  2184.                     $char->setStatus(defined $mapPropertyInfoHandle{$i} ? $mapPropertyInfoHandle{$i} : "UNKNOWN_MAPPROPERTY_INFO_$i",1);
  2185.                 }
  2186.             }
  2187.         }
  2188.     }
  2189.  
  2190.     $pvp = {6 => 1, 8 => 2, 19 => 3}->{$args->{type}};
  2191.     if ($pvp) {
  2192.         Plugins::callHook('pvp_mode', {
  2193.             pvp => $pvp # 1 PvP, 2 GvG, 3 Battleground
  2194.         });
  2195.     }
  2196. }
  2197.  
  2198. #099F
  2199. sub area_spell_multiple2 {
  2200.     my ($self, $args) = @_;
  2201.  
  2202.     # Area effect spells; including traps!
  2203.     my $len = $args->{len} - 4;
  2204.     my $spellInfo = $args->{spellInfo};
  2205.     my $msg = "";
  2206.     my $binID;
  2207.     my ($ID, $sourceID, $x, $y, $type, $range, $fail);
  2208.     for (my $i = 0; $i < $len; $i += 18) {
  2209.         $msg = substr($spellInfo, $i, 18);
  2210.         ($ID, $sourceID, $x, $y, $type, $range, $fail) = unpack('a4 a4 v3 X2 C2', $msg);
  2211.  
  2212.         if ($spells{$ID} && $spells{$ID}{'sourceID'} eq $sourceID) {
  2213.             $binID = binFind(\@spellsID, $ID);
  2214.             $binID = binAdd(\@spellsID, $ID) if ($binID eq "");
  2215.         } else {
  2216.             $binID = binAdd(\@spellsID, $ID);
  2217.         }
  2218.    
  2219.         $spells{$ID}{'sourceID'} = $sourceID;
  2220.         $spells{$ID}{'pos'}{'x'} = $x;
  2221.         $spells{$ID}{'pos'}{'y'} = $y;
  2222.         $spells{$ID}{'pos_to'}{'x'} = $x;
  2223.         $spells{$ID}{'pos_to'}{'y'} = $y;
  2224.         $spells{$ID}{'binID'} = $binID;
  2225.         $spells{$ID}{'type'} = $type;
  2226.         if ($type == 0x81) {
  2227.             message TF("%s opened Warp Portal on (%d, %d)\n", getActorName($sourceID), $x, $y), "skill";
  2228.         }
  2229.         debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)\n", "skill", 2;
  2230.     }
  2231.  
  2232.     Plugins::callHook('packet_areaSpell', {
  2233.         fail => $fail,
  2234.         sourceID => $sourceID,
  2235.         type => $type,
  2236.         x => $x,
  2237.         y => $y
  2238.     });
  2239. }
  2240.  
  2241. #09CA
  2242. sub area_spell_multiple3 {
  2243.     my ($self, $args) = @_;
  2244.  
  2245.     # Area effect spells; including traps!
  2246.     my $len = $args->{len} - 4;
  2247.     my $spellInfo = $args->{spellInfo};
  2248.     my $msg = "";
  2249.     my $binID;
  2250.     my ($ID, $sourceID, $x, $y, $type, $range, $fail);
  2251.     for (my $i = 0; $i < $len; $i += 19) {
  2252.         $msg = substr($spellInfo, $i, 19);
  2253.         ($ID, $sourceID, $x, $y, $type, $range, $fail) = unpack('a4 a4 v3 X3 C2', $msg);
  2254.  
  2255.         if ($spells{$ID} && $spells{$ID}{'sourceID'} eq $sourceID) {
  2256.             $binID = binFind(\@spellsID, $ID);
  2257.             $binID = binAdd(\@spellsID, $ID) if ($binID eq "");
  2258.         } else {
  2259.             $binID = binAdd(\@spellsID, $ID);
  2260.         }
  2261.    
  2262.         $spells{$ID}{'sourceID'} = $sourceID;
  2263.         $spells{$ID}{'pos'}{'x'} = $x;
  2264.         $spells{$ID}{'pos'}{'y'} = $y;
  2265.         $spells{$ID}{'pos_to'}{'x'} = $x;
  2266.         $spells{$ID}{'pos_to'}{'y'} = $y;
  2267.         $spells{$ID}{'binID'} = $binID;
  2268.         $spells{$ID}{'type'} = $type;
  2269.         if ($type == 0x81) {
  2270.             message TF("%s opened Warp Portal on (%d, %d)\n", getActorName($sourceID), $x, $y), "skill";
  2271.         }
  2272.         debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)\n", "skill", 2;
  2273.     }
  2274.  
  2275.     Plugins::callHook('packet_areaSpell', {
  2276.         fail => $fail,
  2277.         sourceID => $sourceID,
  2278.         type => $type,
  2279.         x => $x,
  2280.         y => $y
  2281.     });
  2282. }
  2283.  
  2284. sub sync_request_ex {
  2285.     my ($self, $args) = @_;
  2286.    
  2287.     return if($config{XKore} eq 1 || $config{XKore} eq 3); # let the clien hanle this
  2288.    
  2289.     # Debug Log
  2290.     # message "Received Sync Ex : 0x" . $args->{switch} . "\n";
  2291.    
  2292.     # Computing Sync Ex - By Fr3DBr
  2293.     my $PacketID = $args->{switch};
  2294.    
  2295.     # Getting Sync Ex Reply ID from Table
  2296.     my $SyncID = $self->{sync_ex_reply}->{$PacketID};
  2297.    
  2298.     # Cleaning Leading Zeros
  2299.     $PacketID =~ s/^0+//;  
  2300.    
  2301.     # Cleaning Leading Zeros   
  2302.     $SyncID =~ s/^0+//;
  2303.    
  2304.     # Debug Log
  2305.     #error sprintf("Received Ex Packet ID : 0x%s => 0x%s\n", $PacketID, $SyncID);
  2306.  
  2307.     # Converting ID to Hex Number
  2308.     $SyncID = hex($SyncID);
  2309.  
  2310.     # Dispatching Sync Ex Reply
  2311.     $messageSender->sendReplySyncRequestEx($SyncID);
  2312. }
  2313.  
  2314. sub cash_shop_list {
  2315.     my ($self, $args) = @_;
  2316.     my $tabcode = $args->{tabcode};
  2317.     my $jump = 6;
  2318.     my $unpack_string  = "v V";
  2319.     # CASHSHOP_TAB_NEW => 0x0,
  2320.     # CASHSHOP_TAB_POPULAR => 0x1,
  2321.     # CASHSHOP_TAB_LIMITED => 0x2,
  2322.     # CASHSHOP_TAB_RENTAL => 0x3,
  2323.     # CASHSHOP_TAB_PERPETUITY => 0x4,
  2324.     # CASHSHOP_TAB_BUFF => 0x5,
  2325.     # CASHSHOP_TAB_RECOVERY => 0x6,
  2326.     # CASHSHOP_TAB_ETC => 0x7
  2327.     # CASHSHOP_TAB_MAX => 8
  2328.     my %cashitem_tab = (
  2329.         0 => 'New',
  2330.         1 => 'Popular',
  2331.         2 => 'Limited',
  2332.         3 => 'Rental',
  2333.         4 => 'Perpetuity',
  2334.         5 => 'Buff',
  2335.         6 => 'Recovery',
  2336.         7 => 'Etc',
  2337.     );
  2338.     debug TF("%s\n" .
  2339.         "#   Name                               Price\n",
  2340.         center(' Tab: ' . $cashitem_tab{$tabcode} . ' ', 44, '-')), "list";
  2341.     for (my $i = 0; $i < length($args->{itemInfo}); $i += $jump) {
  2342.         my ($ID, $price) = unpack($unpack_string, substr($args->{itemInfo}, $i));
  2343.         my $name = itemNameSimple($ID);
  2344.         push(@{$cashShop{list}[$tabcode]}, {item_id => $ID, price => $price}); # add to cashshop
  2345.         debug(swrite(
  2346.             "@<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>C",
  2347.             [$i, $name, formatNumber($price)]),
  2348.             "list");
  2349.  
  2350.         }
  2351. }
  2352.  
  2353. sub cash_shop_open_result {
  2354.     my ($self, $args) = @_;
  2355.     #'0845' => ['cash_window_shop_open', 'v2', [qw(cash_points kafra_points)]],
  2356.     message TF("Cash Points: %sC - Kafra Points: %sC\n", formatNumber ($args->{cash_points}), formatNumber ($args->{kafra_points}));
  2357.     $cashShop{points} = {
  2358.                             cash => $args->{cash_points},
  2359.                             kafra => $args->{kafra_points}
  2360.                         };
  2361. }
  2362.  
  2363. sub cash_shop_buy_result {
  2364.     my ($self, $args) = @_;
  2365.         # TODO: implement result messages:
  2366.         # SUCCESS           = 0x0,
  2367.         # WRONG_TAB?        = 0x1, // we should take care with this, as it's detectable by the server
  2368.         # SHORTTAGE_CASH        = 0x2,
  2369.         # UNKONWN_ITEM      = 0x3,
  2370.         # INVENTORY_WEIGHT      = 0x4,
  2371.         # INVENTORY_ITEMCNT     = 0x5,
  2372.         # RUNE_OVERCOUNT        = 0x9,
  2373.         # EACHITEM_OVERCOUNT        = 0xa,
  2374.         # UNKNOWN           = 0xb,
  2375.     if ($args->{result} > 0) {
  2376.         error TF("Error while buying %s from cash shop. Error code: %s\n", itemNameSimple($args->{item_id}), $args->{result});
  2377.     } else {
  2378.         message TF("Bought %s from cash shop. Current CASH: %s\n", itemNameSimple($args->{item_id}), formatNumber($args->{updated_points})), "success";
  2379.         $cashShop{points}->{cash} = $args->{updated_points};
  2380.     }
  2381.    
  2382.     debug sprintf("Got result ID [%s] while buying %s from CASH Shop. Current CASH: %s \n", $args->{result}, itemNameSimple($args->{item_id}), formatNumber($args->{updated_points}));
  2383.  
  2384.    
  2385. }
  2386.  
  2387. sub player_equipment {
  2388.     my ($self, $args) = @_;
  2389.  
  2390.     my ($sourceID, $type, $ID1, $ID2) = @{$args}{qw(sourceID type ID1 ID2)};
  2391.     my $player = ($sourceID ne $accountID)? $playersList->getByID($sourceID) : $char;
  2392.     return unless $player;
  2393.  
  2394.     if ($type == 0) {
  2395.         # Player changed job
  2396.         $player->{jobID} = $ID1;
  2397.  
  2398.     } elsif ($type == 2) {
  2399.         if ($ID1 ne $player->{weapon}) {
  2400.             message TF("%s changed Weapon to %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
  2401.             $player->{weapon} = $ID1;
  2402.         }
  2403.         if ($ID2 ne $player->{shield}) {
  2404.             message TF("%s changed Shield to %s\n", $player, itemName({nameID => $ID2})), "parseMsg_statuslook", 2;
  2405.             $player->{shield} = $ID2;
  2406.         }
  2407.     } elsif ($type == 3) {
  2408.         $player->{headgear}{low} = $ID1;
  2409.     } elsif ($type == 4) {
  2410.         $player->{headgear}{top} = $ID1;
  2411.     } elsif ($type == 5) {
  2412.         $player->{headgear}{mid} = $ID1;
  2413.     } elsif ($type == 9) {
  2414.         if ($player->{shoes} && $ID1 ne $player->{shoes}) {
  2415.             message TF("%s changed Shoes to: %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
  2416.         }
  2417.         $player->{shoes} = $ID1;
  2418.     }
  2419. }
  2420.  
  2421. sub progress_bar {
  2422.     my($self, $args) = @_;
  2423.     message TF("Progress bar loading (time: %d).\n", $args->{time}), 'info';
  2424.     $char->{progress_bar} = 1;
  2425.     $taskManager->add(
  2426.         new Task::Chained(tasks => [new Task::Wait(seconds => $args->{time}),
  2427.         new Task::Function(function => sub {
  2428.              $messageSender->sendProgress();
  2429.              message TF("Progress bar finished.\n"), 'info';
  2430.              $char->{progress_bar} = 0;
  2431.              $_[0]->setDone;
  2432.         })]));
  2433. }
  2434.  
  2435. sub progress_bar_stop {
  2436.     my($self, $args) = @_;
  2437.     message TF("Progress bar finished.\n", 'info');
  2438. }
  2439.  
  2440. # 02B1
  2441. sub quest_all_list {
  2442.     my ($self, $args) = @_;
  2443.     $questList = {};
  2444.     for (my $i = 8; $i < $args->{amount}*5+8; $i += 5) {
  2445.         my ($questID, $active) = unpack('V C', substr($args->{RAW_MSG}, $i, 5));
  2446.         $questList->{$questID}->{active} = $active;
  2447.         debug "$questID $active\n", "info";
  2448.     }
  2449. }
  2450.  
  2451. # 02B2
  2452. # note: this packet shows all quests + their missions and has variable length
  2453. sub quest_all_mission {
  2454.     my ($self, $args) = @_;
  2455.     debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
  2456.     for (my $i = 8; $i < $args->{amount}*104+8; $i += 104) {
  2457.         my ($questID, $time_start, $time, $mission_amount) = unpack('V3 v', substr($args->{RAW_MSG}, $i, 14));
  2458.         my $quest = \%{$questList->{$questID}};
  2459.         $quest->{time_start} = $time_start;
  2460.         $quest->{time} = $time;
  2461.         debug "$questID $time_start $time $mission_amount\n", "info";
  2462.         for (my $j = 0; $j < $mission_amount; $j++) {
  2463.             my ($mobID, $count, $mobName) = unpack('V v Z24', substr($args->{RAW_MSG}, 14+$i+$j*30, 30));
  2464.             my $mission = \%{$quest->{missions}->{$mobID}};
  2465.             $mission->{mobID} = $mobID;
  2466.             $mission->{count} = $count;
  2467.             $mission->{mobName} = bytesToString($mobName);
  2468.             Plugins::callHook('quest_mission_added', {
  2469.                 questID => $questID,
  2470.                 mobID => $mobID,
  2471.                 count => $count
  2472.                
  2473.             });
  2474.             debug "- $mobID $count $mobName\n", "info";
  2475.         }
  2476.     }
  2477. }
  2478.  
  2479. # 02B3
  2480. # 09F9
  2481. # note: this packet shows all missions for 1 quest and has fixed length
  2482. sub quest_add {
  2483.     my ($self, $args) = @_;
  2484.     my $questID = $args->{questID};
  2485.     my $quest = \%{$questList->{$questID}};
  2486.  
  2487.     unless (%$quest) {
  2488.         message TF("Quest: %s has been added.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
  2489.     }
  2490.  
  2491.     my $pack = 'a0 V v Z24';
  2492.     $pack = 'V x4 V x4 v Z24' if $args->{switch} eq '09F9';
  2493.     my $pack_len = length pack $pack, ( 0 ) x 7;
  2494.  
  2495.     $quest->{time_start} = $args->{time_start};
  2496.     $quest->{time} = $args->{time};
  2497.     $quest->{active} = $args->{active};
  2498.     debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
  2499.     my $o = 17;
  2500.     for (my $i = 0; $i < $args->{amount}; $i++) {
  2501.         my ( $conditionID, $mobID, $count, $mobName ) = unpack $pack, substr $args->{RAW_MSG}, $o + $i * $pack_len, $pack_len;
  2502.         my $mission = \%{$quest->{missions}->{$conditionID || $mobID}};
  2503.         $mission->{mobID} = $mobID;
  2504.         $mission->{conditionID} = $conditionID;
  2505.         $mission->{count} = $count;
  2506.         $mission->{mobName} = bytesToString($mobName);
  2507.         Plugins::callHook('quest_mission_added', {
  2508.                 questID => $questID,
  2509.                 mobID => $mobID,
  2510.                 count => $count
  2511.         });
  2512.         debug "- $mobID $count $mobName\n", "info";
  2513.     }
  2514. }
  2515.  
  2516. # 02B4
  2517. sub quest_delete {
  2518.     my ($self, $args) = @_;
  2519.     my $questID = $args->{questID};
  2520.     message TF("Quest: %s has been deleted.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
  2521.     delete $questList->{$questID};
  2522. }
  2523.  
  2524. sub parse_quest_update_mission_hunt {
  2525.     my ( $self, $args ) = @_;
  2526.     if ( $args->{switch} eq '09FA' ) {
  2527.         @{ $args->{mobs} } = map { my %result; @result{qw(questID mobID goal count)} = unpack 'V2 v2', $_; \%result } unpack '(a12)*', $args->{mobInfo};
  2528.     } else {
  2529.         @{ $args->{mobs} } = map { my %result; @result{qw(questID mobID count)} = unpack 'V2 v', $_; \%result } unpack '(a10)*', $args->{mobInfo};
  2530.     }
  2531. }
  2532.  
  2533. sub reconstruct_quest_update_mission_hunt {
  2534.     my ($self, $args) = @_;
  2535.     $args->{mobInfo} = pack '(a10)*', map { pack 'V2 v', @{$_}{qw(questID mobID count)} } @{$args->{mobs}};
  2536. }
  2537.  
  2538. sub parse_quest_update_mission_hunt_v2 {
  2539.     my ($self, $args) = @_;
  2540.     @{$args->{mobs}} = map {
  2541.         my %result; @result{qw(questID mobID goal count)} = unpack 'V2 v2', $_; \%result
  2542.     } unpack '(a12)*', $args->{mobInfo};
  2543. }
  2544.  
  2545. sub reconstruct_quest_update_mission_hunt_v2 {
  2546.     my ($self, $args) = @_;
  2547.     $args->{mobInfo} = pack '(a12)*', map { pack 'V2 v2', @{$_}{qw(questID mobID goal count)} } @{$args->{mobs}};
  2548. }
  2549.  
  2550. # 02B5
  2551. # 09FA
  2552. sub quest_update_mission_hunt {
  2553.     my ($self, $args) = @_;
  2554.     my ($questID, $mobID, $goal, $count) = unpack('V2 v2', substr($args->{RAW_MSG}, 6));
  2555.     debug "- $questID $mobID $count $goal\n", "info";
  2556.     if ($questID) {
  2557.         my $quest = \%{$questList->{$questID}};
  2558.         my $mission = \%{$quest->{missions}->{$mobID}};
  2559.         $mission->{goal} = $goal;
  2560.         $mission->{count} = $count;
  2561.         Plugins::callHook('quest_mission_updated', {
  2562.                 questID => $questID,
  2563.                 mobID => $mobID,
  2564.                 count => $count,
  2565.                 goal => $goal
  2566.         });
  2567.     }
  2568. }
  2569.  
  2570. # 02B7
  2571. sub quest_active {
  2572.     my ($self, $args) = @_;
  2573.     my $questID = $args->{questID};
  2574.  
  2575.     message $args->{active}
  2576.         ? TF("Quest %s is now active.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
  2577.         : TF("Quest %s is now inactive.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
  2578.     , "info";
  2579.  
  2580.     $questList->{$args->{questID}}->{active} = $args->{active};
  2581. }
  2582.  
  2583. # 02C1
  2584. sub parse_npc_chat {
  2585.     my ($self, $args) = @_;
  2586.  
  2587.     $args->{actor} = Actor::get($args->{ID});
  2588. }
  2589.  
  2590. sub npc_chat {
  2591.     my ($self, $args) = @_;
  2592.  
  2593.     # like public_chat, but also has color
  2594.  
  2595.     my $actor = $args->{actor};
  2596.     my $message = $args->{message}; # needs bytesToString or not?
  2597.     my $position = sprintf("[%s %d, %d]",
  2598.         $field ? $field->baseName : T("Unknown field,"),
  2599.         @{$char->{pos_to}}{qw(x y)});
  2600.     my $dist;
  2601.  
  2602.     if ($message =~ / : /) {
  2603.         ((my $name), $message) = split / : /, $message, 2;
  2604.         $dist = 'unknown';
  2605.         unless ($actor->isa('Actor::Unknown')) {
  2606.             $dist = distance($char->{pos_to}, $actor->{pos_to});
  2607.             $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  2608.         }
  2609.         if ($actor->{name} eq $name) {
  2610.             $name = "$actor";
  2611.         } else {
  2612.             $name = sprintf "%s (%s)", $name, $actor->{binID};
  2613.         }
  2614.         $message = "$name: $message";
  2615.  
  2616.         $position .= sprintf(" [%d, %d] [dist=%s] (%d)",
  2617.             @{$actor->{pos_to}}{qw(x y)},
  2618.             $dist, $actor->{nameID});
  2619.         $dist = "[dist=$dist] ";
  2620.     }
  2621.  
  2622.     chatLog("npc", "$position $message\n") if ($config{logChat});
  2623.     message TF("%s%s\n", $dist, $message), "npcchat";
  2624.  
  2625.     # TODO hook
  2626. }
  2627.  
  2628. sub forge_list {
  2629.     my ($self, $args) = @_;
  2630.  
  2631.     message T("========Forge List========\n");
  2632.     for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 8) {
  2633.         my $viewID = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
  2634.         message "$viewID $items_lut{$viewID}\n";
  2635.         # always 0x0012
  2636.         #my $unknown = unpack("v1", substr($args->{RAW_MSG}, $i+2, 2));
  2637.         # ???
  2638.         #my $charID = substr($args->{RAW_MSG}, $i+4, 4);
  2639.     }
  2640.     message "=========================\n";
  2641. }
  2642.  
  2643. sub storage_opened {
  2644.     my ($self, $args) = @_;
  2645.     $char->storage->open($args);
  2646. }
  2647.  
  2648. sub storage_closed {
  2649.     $char->storage->close();
  2650.     message T("Storage closed.\n"), "storage";;
  2651.  
  2652.     # Storage log
  2653.     writeStorageLog(0);
  2654.  
  2655.     if ($char->{dcOnEmptyItems} ne "") {
  2656.         message TF("Disconnecting on empty %s!\n", $char->{dcOnEmptyItems});
  2657.         chatLog("k", TF("Disconnecting on empty %s!\n", $char->{dcOnEmptyItems}));
  2658.         quit();
  2659.     }
  2660. }
  2661.  
  2662. sub storage_items_stackable {
  2663.     my ($self, $args) = @_;
  2664.  
  2665.     $char->storage->clear;
  2666.  
  2667.     $self->_items_list({
  2668.         class => 'Actor::Item',
  2669.         hook => 'packet_storage',
  2670.         debug_str => 'Stackable Storage Item',
  2671.         items => [$self->parse_items_stackable($args)],
  2672.         getter => sub { $char->storage->getByID($_[0]{ID}) },
  2673.         adder => sub { $char->storage->add($_[0]) },
  2674.         callback => sub {
  2675.             my ($local_item) = @_;
  2676.  
  2677.             $local_item->{amount} = $local_item->{amount} & ~0x80000000;
  2678.         },
  2679.     });
  2680.  
  2681.     $storageTitle = $args->{title} ? $args->{title} : undef;
  2682. }
  2683.  
  2684. sub storage_items_nonstackable {
  2685.     my ($self, $args) = @_;
  2686.  
  2687.     $self->_items_list({
  2688.         class => 'Actor::Item',
  2689.         hook => 'packet_storage',
  2690.         debug_str => 'Non-Stackable Storage Item',
  2691.         items => [$self->parse_items_nonstackable($args)],
  2692.         getter => sub { $char->storage->getByID($_[0]{ID}) },
  2693.         adder => sub { $char->storage->add($_[0]) },
  2694.     });
  2695.  
  2696.     $storageTitle = $args->{title} ? $args->{title} : undef;
  2697. }
  2698.  
  2699. sub storage_item_added {
  2700.     my ($self, $args) = @_;
  2701.  
  2702.     my $index = $args->{ID};
  2703.     my $amount = $args->{amount};
  2704.  
  2705.     my $item = $char->storage->getByID($index);
  2706.     if (!$item) {
  2707.         $item = new Actor::Item();
  2708.         $item->{nameID} = $args->{nameID};
  2709.         $item->{ID} = $index;
  2710.         $item->{amount} = $amount;
  2711.         $item->{type} = $args->{type};
  2712.         $item->{identified} = $args->{identified};
  2713.         $item->{broken} = $args->{broken};
  2714.         $item->{upgrade} = $args->{upgrade};
  2715.         $item->{cards} = $args->{cards};
  2716.         $item->{options} = $args->{options};
  2717.         $item->{name} = itemName($item);
  2718.         $char->storage->add($item);
  2719.     } else {
  2720.         $item->{amount} += $amount;
  2721.     }
  2722.     my $disp = TF("Storage Item Added: %s (%d) x %d - %s",
  2723.             $item->{name}, $item->{binID}, $amount, $itemTypes_lut{$item->{type}});
  2724.     message "$disp\n", "drop";
  2725.    
  2726.     $itemChange{$item->{name}} += $amount;
  2727.     $args->{item} = $item;
  2728. }
  2729.  
  2730. sub storage_item_removed {
  2731.     my ($self, $args) = @_;
  2732.  
  2733.     my ($index, $amount) = @{$args}{qw(ID amount)};
  2734.  
  2735.     my $item = $char->storage->getByID($index);
  2736.    
  2737.     if ($item) {
  2738.         Misc::storageItemRemoved($item->{binID}, $amount);
  2739.     }
  2740. }
  2741.  
  2742. sub cart_items_stackable {
  2743.     my ($self, $args) = @_;
  2744.  
  2745.     $self->_items_list({
  2746.         class => 'Actor::Item',
  2747.         hook => 'packet_cart',
  2748.         debug_str => 'Stackable Cart Item',
  2749.         items => [$self->parse_items_stackable($args)],
  2750.         getter => sub { $char->cart->getByID($_[0]{ID}) },
  2751.         adder => sub { $char->cart->add($_[0]) },
  2752.     });
  2753. }
  2754.  
  2755. sub cart_items_nonstackable {
  2756.     my ($self, $args) = @_;
  2757.  
  2758.     $self->_items_list({
  2759.         class => 'Actor::Item',
  2760.         hook => 'packet_cart',
  2761.         debug_str => 'Non-Stackable Cart Item',
  2762.         items => [$self->parse_items_nonstackable($args)],
  2763.         getter => sub { $char->cart->getByID($_[0]{ID}) },
  2764.         adder => sub { $char->cart->add($_[0]) },
  2765.     });
  2766. }
  2767.  
  2768. sub cart_item_added {
  2769.     my ($self, $args) = @_;
  2770.    
  2771.     my $index = $args->{ID};
  2772.     my $amount = $args->{amount};
  2773.  
  2774.     my $item = $char->cart->getByID($index);
  2775.     if (!$item) {
  2776.         $item = new Actor::Item();
  2777.         $item->{ID} = $args->{ID};
  2778.         $item->{nameID} = $args->{nameID};
  2779.         $item->{amount} = $args->{amount};
  2780.         $item->{identified} = $args->{identified};
  2781.         $item->{broken} = $args->{broken};
  2782.         $item->{upgrade} = $args->{upgrade};
  2783.         $item->{cards} = $args->{cards};
  2784.         $item->{options} = $args->{options};
  2785.         $item->{type} = $args->{type} if (exists $args->{type});
  2786.         $item->{name} = itemName($item);
  2787.         $char->cart->add($item);
  2788.     } else {
  2789.         $item->{amount} += $args->{amount};
  2790.     }
  2791.     my $disp = TF("Cart Item Added: %s (%d) x %d - %s",
  2792.             $item->{name}, $item->{binID}, $amount, $itemTypes_lut{$item->{type}});
  2793.     message "$disp\n", "drop";
  2794.     $itemChange{$item->{name}} += $args->{amount};
  2795.     $args->{item} = $item;
  2796. }
  2797.  
  2798. sub cart_item_removed {
  2799.     my ($self, $args) = @_;
  2800.  
  2801.     my ($index, $amount) = @{$args}{qw(ID amount)};
  2802.  
  2803.     my $item = $char->cart->getByID($index);
  2804.    
  2805.     if ($item) {
  2806.         Misc::cartItemRemoved($item->{binID}, $amount);
  2807.     }
  2808. }
  2809.  
  2810. sub cart_info {
  2811.     my ($self, $args) = @_;
  2812.     $char->cart->info($args);
  2813.     debug "[cart_info] received.\n", "parseMsg";
  2814. }
  2815.  
  2816. sub cart_add_failed {
  2817.     my ($self, $args) = @_;
  2818.  
  2819.     my $reason;
  2820.     if ($args->{fail} == 0) {
  2821.         $reason = T('overweight');
  2822.     } elsif ($args->{fail} == 1) {
  2823.         $reason = T('too many items');
  2824.     } else {
  2825.         $reason = TF("Unknown code %s",$args->{fail});
  2826.     }
  2827.     error TF("Can't Add Cart Item (%s)\n", $reason);
  2828. }
  2829.  
  2830. sub inventory_items_stackable {
  2831.     my ($self, $args) = @_;
  2832.     return unless changeToInGameState();
  2833.  
  2834.     $self->_items_list({
  2835.         class => 'Actor::Item',
  2836.         hook => 'packet_inventory',
  2837.         debug_str => 'Stackable Inventory Item',
  2838.         items => [$self->parse_items_stackable($args)],
  2839.         getter => sub { $char->inventory->getByID($_[0]{ID}) },
  2840.         adder => sub { $char->inventory->add($_[0]) },
  2841.         callback => sub {
  2842.             my ($local_item) = @_;
  2843.  
  2844.             if (defined $char->{arrow} && $local_item->{ID} eq $char->{arrow}) {
  2845.                 $local_item->{equipped} = 32768;
  2846.                 $char->{equipment}{arrow} = $local_item;
  2847.             }
  2848.         }
  2849.     });
  2850. }
  2851.  
  2852. sub login_error {
  2853.     my ($self, $args) = @_;
  2854.  
  2855.     $net->serverDisconnect();
  2856.     if ($args->{type} == REFUSE_INVALID_ID) {
  2857.         error TF("Account name [%s] doesn't exist\n", $config{'username'}), "connection";
  2858.         if (!$net->clientAlive() && !$config{'ignoreInvalidLogin'} && !UNIVERSAL::isa($net, 'Network::XKoreProxy')) {
  2859.             my $username = $interface->query(T("Enter your Ragnarok Online username again."));
  2860.             if (defined($username)) {
  2861.                 configModify('username', $username, 1);
  2862.                 $timeout_ex{master}{time} = 0;
  2863.                 $conState_tries = 0;
  2864.             } else {
  2865.                 quit();
  2866.                 return;
  2867.             }
  2868.         }
  2869.     } elsif ($args->{type} == REFUSE_INVALID_PASSWD) {
  2870.         error TF("Password Error for account [%s]\n", $config{'username'}), "connection";
  2871.         if (!$net->clientAlive() && !$config{'ignoreInvalidLogin'} && !UNIVERSAL::isa($net, 'Network::XKoreProxy')) {
  2872.             my $password = $interface->query(T("Enter your Ragnarok Online password again."), isPassword => 1);
  2873.             if (defined($password)) {
  2874.                 configModify('password', $password, 1);
  2875.                 $timeout_ex{master}{time} = 0;
  2876.                 $conState_tries = 0;
  2877.             } else {
  2878.                 quit();
  2879.                 return;
  2880.             }
  2881.         }
  2882.     } elsif ($args->{type} == ACCEPT_ID_PASSWD) {
  2883.         error T("The server has denied your connection.\n"), "connection";
  2884.     } elsif ($args->{type} == REFUSE_NOT_CONFIRMED) {
  2885.         $interface->errorDialog(T("Critical Error: Your account has been blocked."));
  2886.         $quit = 1 unless ($net->clientAlive());
  2887.     } elsif ($args->{type} == REFUSE_INVALID_VERSION) {
  2888.         my $master = $masterServer;
  2889.         error TF("Connect failed, something is wrong with the login settings:\n" .
  2890.             "version: %s\n" .
  2891.             "master_version: %s\n" .
  2892.             "serverType: %s\n", $master->{version}, $master->{master_version}, $masterServer->{serverType}), "connection";
  2893.         relog(30);
  2894.     } elsif ($args->{type} == REFUSE_BLOCK_TEMPORARY) {
  2895.         error TF("The server is temporarily blocking your connection until %s\n", $args->{date}), "connection";
  2896.     } elsif ($args->{type} == REFUSE_USER_PHONE_BLOCK) { #Phone lock
  2897.         error T("Please dial to activate the login procedure.\n"), "connection";
  2898.         Plugins::callHook('dial');
  2899.         relog(10);
  2900.     } elsif ($args->{type} == ACCEPT_LOGIN_USER_PHONE_BLOCK) {
  2901.         error T("Mobile Authentication: Max number of simultaneous IP addresses reached.\n"), "connection";
  2902.     } else {
  2903.         error TF("The server has denied your connection for unknown reason (%d).\n", $args->{type}), 'connection';
  2904.     }
  2905.  
  2906.     if ($args->{type} != REFUSE_INVALID_VERSION && $versionSearch) {
  2907.         $versionSearch = 0;
  2908.         writeSectionedFileIntact(Settings::getTableFilename("servers.txt"), \%masterServers);
  2909.     }
  2910. }
  2911.  
  2912. sub login_error_game_login_server {
  2913.     error T("Error logging into Character Server (invalid character specified)...\n"), 'connection';
  2914.     $net->setState(1);
  2915.     undef $conState_tries;
  2916.     $timeout_ex{master}{time} = time;
  2917.     $timeout_ex{master}{timeout} = $timeout{'reconnect'}{'timeout'};
  2918.     $net->serverDisconnect();
  2919. }
  2920.  
  2921. sub character_deletion_successful {
  2922.     if (defined $AI::temp::delIndex) {
  2923.         message TF("Character %s (%d) deleted.\n", $chars[$AI::temp::delIndex]{name}, $AI::temp::delIndex), "info";
  2924.         delete $chars[$AI::temp::delIndex];
  2925.         undef $AI::temp::delIndex;
  2926.         for (my $i = 0; $i < @chars; $i++) {
  2927.             delete $chars[$i] if ($chars[$i] && !scalar(keys %{$chars[$i]}))
  2928.         }
  2929.     } else {
  2930.         message T("Character deleted.\n"), "info";
  2931.     }
  2932.  
  2933.     if (charSelectScreen() == 1) {
  2934.         $net->setState(3);
  2935.         $firstLoginMap = 1;
  2936.         $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  2937.         $sentWelcomeMessage = 1;
  2938.     }
  2939. }
  2940.  
  2941. sub character_deletion_failed {
  2942.     error T("Character cannot be deleted. Your e-mail address was probably wrong.\n");
  2943.     undef $AI::temp::delIndex;
  2944.     if (charSelectScreen() == 1) {
  2945.         $net->setState(3);
  2946.         $firstLoginMap = 1;
  2947.         $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  2948.         $sentWelcomeMessage = 1;
  2949.     }
  2950. }
  2951.  
  2952. sub character_moves {
  2953.     my ($self, $args) = @_;
  2954.  
  2955.     return unless changeToInGameState();
  2956.     makeCoordsFromTo($char->{pos}, $char->{pos_to}, $args->{coords});
  2957.     my $dist = sprintf("%.1f", distance($char->{pos}, $char->{pos_to}));
  2958.     debug "You're moving from ($char->{pos}{x}, $char->{pos}{y}) to ($char->{pos_to}{x}, $char->{pos_to}{y}) - distance $dist\n", "parseMsg_move";
  2959.     $char->{time_move} = time;
  2960.     $char->{time_move_calc} = distance($char->{pos}, $char->{pos_to}) * ($char->{walk_speed} || 0.12);
  2961.  
  2962.     # Correct the direction in which we're looking
  2963.     my (%vec, $degree);
  2964.     getVector(\%vec, $char->{pos_to}, $char->{pos});
  2965.     $degree = vectorToDegree(\%vec);
  2966.     if (defined $degree) {
  2967.         my $direction = int sprintf("%.0f", (360 - $degree) / 45);
  2968.         $char->{look}{body} = $direction & 0x07;
  2969.         $char->{look}{head} = 0;
  2970.     }
  2971.  
  2972.     # Ugly; AI code in network subsystem! This must be fixed.
  2973.     if (AI::action eq "mapRoute" && $config{route_escape_reachedNoPortal} && $dist eq "0.0"){
  2974.        if (!$portalsID[0]) {
  2975.         if ($config{route_escape_shout} ne "" && !defined($timeout{ai_route_escape}{time})){
  2976.             sendMessage("c", $config{route_escape_shout});
  2977.         }
  2978.          $timeout{ai_route_escape}{time} = time;
  2979.          AI::queue("escape");
  2980.        }
  2981.     }
  2982. }
  2983.  
  2984. sub character_name {
  2985.     my ($self, $args) = @_;
  2986.     my $name; # Type: String
  2987.  
  2988.     $name = bytesToString($args->{name});
  2989.     debug "Character name received: $name\n";
  2990. }
  2991.  
  2992. sub character_status {
  2993.     my ($self, $args) = @_;
  2994.  
  2995.     my $actor = Actor::get($args->{ID});
  2996.  
  2997.     if ($args->{switch} eq '028A') {
  2998.         $actor->{lv} = $args->{lv}; # TODO: test if it is ok to use this piece of information
  2999.         $actor->{opt3} = $args->{opt3};
  3000.     } elsif ($args->{switch} eq '0229' || $args->{switch} eq '0119') {
  3001.         $actor->{opt1} = $args->{opt1};
  3002.         $actor->{opt2} = $args->{opt2};
  3003.     }
  3004.  
  3005.     $actor->{option} = $args->{option};
  3006.  
  3007.     setStatus($actor, $args->{opt1}, $args->{opt2}, $args->{option});
  3008. }
  3009.  
  3010. sub chat_created {
  3011.     my ($self, $args) = @_;
  3012.  
  3013.     $currentChatRoom = $accountID;
  3014.     $chatRooms{$accountID} = {%createdChatRoom};
  3015.     binAdd(\@chatRoomsID, $accountID);
  3016.     binAdd(\@currentChatRoomUsers, $char->{name});
  3017.     message T("Chat Room Created\n");
  3018.    
  3019.     Plugins::callHook('chat_created', {
  3020.         chat => $chatRooms{$accountID},
  3021.     });
  3022. }
  3023.  
  3024. sub chat_info {
  3025.     my ($self, $args) = @_;
  3026.  
  3027.     my $title = bytesToString($args->{title});
  3028.  
  3029.     my $chat = $chatRooms{$args->{ID}};
  3030.     if (!$chat || !%{$chat}) {
  3031.         $chat = $chatRooms{$args->{ID}} = {};
  3032.         binAdd(\@chatRoomsID, $args->{ID});
  3033.     }
  3034.     $chat->{len} = $args->{len};
  3035.     $chat->{title} = $title;
  3036.     $chat->{ownerID} = $args->{ownerID};
  3037.     $chat->{limit} = $args->{limit};
  3038.     $chat->{public} = $args->{public};
  3039.     $chat->{num_users} = $args->{num_users};
  3040.  
  3041.     Plugins::callHook('packet_chatinfo', {
  3042.       chatID => $args->{ID},
  3043.       ownerID => $args->{ownerID},
  3044.       title => $title,
  3045.       limit => $args->{limit},
  3046.       public => $args->{public},
  3047.       num_users => $args->{num_users}
  3048.     });
  3049. }
  3050.  
  3051. sub chat_join_result {
  3052.     my ($self, $args) = @_;
  3053.  
  3054.     if ($args->{type} == 1) {
  3055.         message T("Can't join Chat Room - Incorrect Password\n");
  3056.     } elsif ($args->{type} == 2) {
  3057.         message T("Can't join Chat Room - You're banned\n");
  3058.     }
  3059. }
  3060.  
  3061. sub chat_modified {
  3062.     my ($self, $args) = @_;
  3063.  
  3064.     my $title = bytesToString($args->{title});
  3065.  
  3066.     my ($ownerID, $chat_ID, $limit, $public, $num_users) = @{$args}{qw(ownerID ID limit public num_users)};
  3067.     my $ID;
  3068.     if ($ownerID eq $accountID) {
  3069.         $ID = $accountID;
  3070.     } else {
  3071.         $ID = $chat_ID;
  3072.     }
  3073.    
  3074.     my %chat = ();
  3075.     $chat{title} = $title;
  3076.     $chat{ownerID} = $ownerID;
  3077.     $chat{limit} = $limit;
  3078.     $chat{public} = $public;
  3079.     $chat{num_users} = $num_users;
  3080.    
  3081.     Plugins::callHook('chat_modified', {
  3082.         ID => $ID,
  3083.         old => $chatRooms{$ID},
  3084.         new => \%chat,
  3085.     });
  3086.    
  3087.     $chatRooms{$ID} = {%chat};
  3088.    
  3089.     message T("Chat Room Properties Modified\n");
  3090. }
  3091.  
  3092. sub chat_newowner {
  3093.     my ($self, $args) = @_;
  3094.  
  3095.     my $user = bytesToString($args->{user});
  3096.     if ($args->{type} == 0) {
  3097.         if ($user eq $char->{name}) {
  3098.             $chatRooms{$currentChatRoom}{ownerID} = $accountID;
  3099.         } else {
  3100.             my $player;
  3101.             for my $p (@$playersList) {
  3102.                 if ($p->{name} eq $user) {
  3103.                     $player = $p;
  3104.                     last;
  3105.                 }
  3106.             }
  3107.  
  3108.             if ($player) {
  3109.                 my $key = $player->{ID};
  3110.                 $chatRooms{$currentChatRoom}{ownerID} = $key;
  3111.             }
  3112.         }
  3113.         $chatRooms{$currentChatRoom}{users}{$user} = 2;
  3114.     } else {
  3115.         $chatRooms{$currentChatRoom}{users}{$user} = 1;
  3116.     }
  3117. }
  3118.  
  3119. sub chat_user_join {
  3120.     my ($self, $args) = @_;
  3121.  
  3122.     my $user = bytesToString($args->{user});
  3123.     if ($currentChatRoom ne "") {
  3124.         binAdd(\@currentChatRoomUsers, $user);
  3125.         $chatRooms{$currentChatRoom}{users}{$user} = 1;
  3126.         $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
  3127.         message TF("%s has joined the Chat Room\n", $user);
  3128.     }
  3129. }
  3130.  
  3131. sub chat_user_leave {
  3132.     my ($self, $args) = @_;
  3133.  
  3134.     my $user = bytesToString($args->{user});
  3135.     delete $chatRooms{$currentChatRoom}{users}{$user};
  3136.     binRemove(\@currentChatRoomUsers, $user);
  3137.     $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
  3138.     if ($user eq $char->{name}) {
  3139.         binRemove(\@chatRoomsID, $currentChatRoom);
  3140.         delete $chatRooms{$currentChatRoom};
  3141.         undef @currentChatRoomUsers;
  3142.         $currentChatRoom = "";
  3143.         message T("You left the Chat Room\n");
  3144.         Plugins::callHook('chat_leave');
  3145.     } else {
  3146.         message TF("%s has left the Chat Room\n", $user);
  3147.     }
  3148. }
  3149.  
  3150. sub chat_removed {
  3151.     my ($self, $args) = @_;
  3152.  
  3153.     binRemove(\@chatRoomsID, $args->{ID});
  3154.     my $chat = delete $chatRooms{ $args->{ID} };
  3155.    
  3156.     Plugins::callHook('chat_removed', {
  3157.         ID => $args->{ID},
  3158.         chat => $chat,
  3159.     });
  3160. }
  3161.  
  3162. sub deal_add_other {
  3163.     my ($self, $args) = @_;
  3164.  
  3165.     if ($args->{nameID} > 0) {
  3166.         my $item = $currentDeal{other}{ $args->{nameID} } ||= {};
  3167.         $item->{amount} += $args->{amount};
  3168.         $item->{nameID} = $args->{nameID};
  3169.         $item->{identified} = $args->{identified};
  3170.         $item->{broken} = $args->{broken};
  3171.         $item->{upgrade} = $args->{upgrade};
  3172.         $item->{cards} = $args->{cards};
  3173.         $item->{options} = $args->{options};
  3174.         $item->{name} = itemName($item);
  3175.         message TF("%s added Item to Deal: %s x %s\n", $currentDeal{name}, $item->{name}, $args->{amount}), "deal";
  3176.     } elsif ($args->{amount} > 0) {
  3177.         $currentDeal{other_zeny} += $args->{amount};
  3178.         my $amount = formatNumber($args->{amount});
  3179.         message TF("%s added %s z to Deal\n", $currentDeal{name}, $amount), "deal";
  3180.     }
  3181. }
  3182.  
  3183. sub deal_begin {
  3184.     my ($self, $args) = @_;
  3185.  
  3186.     if ($args->{type} == 0) {
  3187.         error T("That person is too far from you to trade.\n"), "deal";
  3188.         Plugins::callHook("error_deal", { type =>$args->{type}} );
  3189.     } elsif ($args->{type} == 2) {
  3190.         error T("That person is in another deal.\n"), "deal";
  3191.         Plugins::callHook("error_deal", { type =>$args->{type}} );
  3192.     } elsif ($args->{type} == 3) {
  3193.         if (%incomingDeal) {
  3194.             $currentDeal{name} = $incomingDeal{name};
  3195.             undef %incomingDeal;
  3196.         } else {
  3197.             my $ID = $outgoingDeal{ID};
  3198.             my $player;
  3199.             $player = $playersList->getByID($ID) if (defined $ID);
  3200.             $currentDeal{ID} = $ID;
  3201.             if ($player) {
  3202.                 $currentDeal{name} = $player->{name};
  3203.             } else {
  3204.                 $currentDeal{name} = T('Unknown #') . unpack("V", $ID);
  3205.             }
  3206.             undef %outgoingDeal;
  3207.         }
  3208.         message TF("Engaged Deal with %s\n", $currentDeal{name}), "deal";
  3209.         Plugins::callHook("engaged_deal", {name => $currentDeal{name}});
  3210.     } elsif ($args->{type} == 5) {
  3211.         error T("That person is opening storage.\n"), "deal";
  3212.         Plugins::callHook("error_deal", { type =>$args->{type}} );
  3213.     } else {
  3214.         error TF("Deal request failed (unknown error %s).\n", $args->{type}), "deal";
  3215.         Plugins::callHook("error_deal", { type =>$args->{type}} );
  3216.     }
  3217. }
  3218.  
  3219. sub deal_cancelled {
  3220.     undef %incomingDeal;
  3221.     undef %outgoingDeal;
  3222.     undef %currentDeal;
  3223.     message T("Deal Cancelled\n"), "deal";
  3224.     Plugins::callHook("cancelled_deal");
  3225. }
  3226.  
  3227. sub deal_complete {
  3228.     undef %outgoingDeal;
  3229.     undef %incomingDeal;
  3230.     undef %currentDeal;
  3231.     message T("Deal Complete\n"), "deal";
  3232.     Plugins::callHook("complete_deal");
  3233. }
  3234.  
  3235. sub deal_finalize {
  3236.     my ($self, $args) = @_;
  3237.     if ($args->{type} == 1) {
  3238.         $currentDeal{other_finalize} = 1;
  3239.         message TF("%s finalized the Deal\n", $currentDeal{name}), "deal";
  3240.         Plugins::callHook("finalized_deal", {name => $currentDeal{name}});
  3241.  
  3242.     } else {
  3243.         $currentDeal{you_finalize} = 1;
  3244.         # FIXME: shouldn't we do this when we actually complete the deal?
  3245.         $char->{zeny} -= $currentDeal{you_zeny};
  3246.         message T("You finalized the Deal\n"), "deal";
  3247.     }
  3248. }
  3249.  
  3250. sub deal_request {
  3251.     my ($self, $args) = @_;
  3252.     my $level = $args->{level} || 'Unknown'; # TODO: store this info
  3253.     my $user = bytesToString($args->{user});
  3254.  
  3255.     $incomingDeal{name} = $user;
  3256.     $timeout{ai_dealAutoCancel}{time} = time;
  3257.     message TF("%s (level %s) Requests a Deal\n", $user, $level), "deal";
  3258.     message T("Type 'deal' to start dealing, or 'deal no' to deny the deal.\n"), "deal";
  3259.     Plugins::callHook("incoming_deal", {name => $user});
  3260. }
  3261.  
  3262. sub devotion {
  3263.     my ($self, $args) = @_;
  3264.     my $msg = '';
  3265.     my $source = Actor::get($args->{sourceID});
  3266.  
  3267.     undef $devotionList->{$args->{sourceID}};
  3268.     for (my $i = 0; $i < 5; $i++) {
  3269.         my $ID = substr($args->{targetIDs}, $i*4, 4);
  3270.         last if unpack("V", $ID) == 0;
  3271.         $devotionList->{$args->{sourceID}}->{targetIDs}->{$ID} = $i;
  3272.         my $actor = Actor::get($ID);
  3273.         #FIXME: Need a better display
  3274.         $msg .= skillUseNoDamage_string($source, $actor, 0, 'devotion');
  3275.     }
  3276.     $devotionList->{$args->{sourceID}}->{range} = $args->{range};
  3277.  
  3278.     message "$msg", "devotion";
  3279. }
  3280.  
  3281. sub egg_list {
  3282.     my ($self, $args) = @_;
  3283.     my $msg = center(T(" Egg Hatch Candidates "), 38, '-') ."\n";
  3284.     for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 2) {
  3285.         my $index = unpack("a2", substr($args->{RAW_MSG}, $i, 2));
  3286.         my $item = $char->inventory->getByID($index);
  3287.         $msg .=  "$item->{binID} $item->{name}\n";
  3288.     }
  3289.     $msg .= ('-'x38) . "\n".
  3290.             T("Ready to use command 'pet [hatch|h] #'\n");
  3291.     message $msg, "list";
  3292. }
  3293.  
  3294. sub emoticon {
  3295.     my ($self, $args) = @_;
  3296.     my $emotion = $emotions_lut{$args->{type}}{display} || "<emotion #$args->{type}>";
  3297.  
  3298.     if ($args->{ID} eq $accountID) {
  3299.         message "$char->{name}: $emotion\n", "emotion";
  3300.         chatLog("e", "$char->{name}: $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  3301.  
  3302.     } elsif (my $player = $playersList->getByID($args->{ID})) {
  3303.         my $name = $player->name;
  3304.  
  3305.         #my $dist = "unknown";
  3306.         my $dist = distance($char->{pos_to}, $player->{pos_to});
  3307.         $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  3308.  
  3309.         # Translation Comment: "[dist=$dist] $name ($player->{binID}): $emotion\n"
  3310.         message TF("[dist=%s] %s (%d): %s\n", $dist, $name, $player->{binID}, $emotion), "emotion";
  3311.         chatLog("e", "$name".": $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  3312.  
  3313.         my $index = AI::findAction("follow");
  3314.         if ($index ne "") {
  3315.             my $masterID = AI::args($index)->{ID};
  3316.             if ($config{'followEmotion'} && $masterID eq $args->{ID} &&
  3317.                    distance($char->{pos_to}, $player->{pos_to}) <= $config{'followEmotion_distance'})
  3318.             {
  3319.                 my %args = ();
  3320.                 $args{timeout} = time + rand (1) + 0.75;
  3321.  
  3322.                 if ($args->{type} == 30) {
  3323.                     $args{emotion} = 31;
  3324.                 } elsif ($args->{type} == 31) {
  3325.                     $args{emotion} = 30;
  3326.                 } else {
  3327.                     $args{emotion} = $args->{type};
  3328.                 }
  3329.  
  3330.                 AI::queue("sendEmotion", \%args);
  3331.             }
  3332.         }
  3333.     } elsif (my $monster = $monstersList->getByID($args->{ID}) || $slavesList->getByID($args->{ID})) {
  3334.         my $dist = distance($char->{pos_to}, $monster->{pos_to});
  3335.         $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  3336.  
  3337.         # Translation Comment: "[dist=$dist] $monster->name ($monster->{binID}): $emotion\n"
  3338.         message TF("[dist=%s] %s %s (%d): %s\n", $dist, $monster->{actorType}, $monster->name, $monster->{binID}, $emotion), "emotion";
  3339.  
  3340.     } else {
  3341.         my $actor = Actor::get($args->{ID});
  3342.         my $name = $actor->name;
  3343.  
  3344.         my $dist = T("unknown");
  3345.         if (!$actor->isa('Actor::Unknown')) {
  3346.             $dist = distance($char->{pos_to}, $actor->{pos_to});
  3347.             $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  3348.         }
  3349.  
  3350.         message TF("[dist=%s] %s: %s\n", $dist, $actor->nameIdx, $emotion), "emotion";
  3351.         chatLog("e", "$name".": $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  3352.     }
  3353.     Plugins::callHook('packet_emotion', {
  3354.         emotion => $emotion,
  3355.         ID => $args->{ID}
  3356.     });
  3357. }
  3358.  
  3359. sub errors {
  3360.     my ($self, $args) = @_;
  3361.  
  3362.     Plugins::callHook('disconnected') if ($net->getState() == Network::IN_GAME);
  3363.     if ($net->getState() == Network::IN_GAME &&
  3364.         ($config{dcOnDisconnect} > 1 ||
  3365.         ($config{dcOnDisconnect} &&
  3366.         $args->{type} != 3 &&
  3367.         $args->{type} != 10))) {
  3368.         error T("Auto disconnecting on Disconnect!\n");
  3369.         chatLog("k", T("*** You disconnected, auto disconnect! ***\n"));
  3370.         $quit = 1;
  3371.     }
  3372.  
  3373.     $net->setState(1);
  3374.     undef $conState_tries;
  3375.  
  3376.     $timeout_ex{'master'}{'time'} = time;
  3377.     $timeout_ex{'master'}{'timeout'} = $timeout{'reconnect'}{'timeout'};
  3378.     if (($args->{type} != 0)) {
  3379.         $net->serverDisconnect();
  3380.     }
  3381.     if ($args->{type} == 0) {
  3382.         # FIXME BAN_SERVER_SHUTDOWN is 0x1, 0x0 is BAN_UNFAIR
  3383.         if ($config{'dcOnServerShutDown'} == 1) {
  3384.             error T("Auto disconnecting on ServerShutDown!\n");
  3385.             chatLog("k", T("*** Server shutting down , auto disconnect! ***\n"));
  3386.             $quit = 1;
  3387.         } else {
  3388.             error T("Server shutting down\n"), "connection";
  3389.         }
  3390.     } elsif ($args->{type} == 1) {
  3391.         if($config{'dcOnServerClose'} == 1) {
  3392.             error T("Auto disconnecting on ServerClose!\n");
  3393.             chatLog("k", T("*** Server is closed , auto disconnect! ***\n"));
  3394.             $quit = 1;
  3395.         } else {
  3396.             error T("Error: Server is closed\n"), "connection";
  3397.         }
  3398.     } elsif ($args->{type} == 2) {
  3399.         if ($config{'dcOnDualLogin'} == 1) {
  3400.             error (TF("Critical Error: Dual login prohibited - Someone trying to login!\n\n" .
  3401.                 "%s will now immediately    disconnect.\n", $Settings::NAME));
  3402.             chatLog("k", T("*** DualLogin, auto disconnect! ***\n"));
  3403.             quit();
  3404.         } elsif ($config{'dcOnDualLogin'} >= 2) {
  3405.             error T("Critical Error: Dual login prohibited - Someone trying to login!\n");
  3406.             message TF("Reconnecting, wait %s seconds...\n", $config{'dcOnDualLogin'}), "connection";
  3407.             $timeout_ex{'master'}{'timeout'} = $config{'dcOnDualLogin'};
  3408.         } else {
  3409.             error T("Critical Error: Dual login prohibited - Someone trying to login!\n"), "connection";
  3410.         }
  3411.  
  3412.     } elsif ($args->{type} == 3) {
  3413.         error T("Error: Out of sync with server\n"), "connection";
  3414.     } elsif ($args->{type} == 4) {
  3415.         # fRO: "Your account is not validated, please click on the validation link in your registration mail."
  3416.         error T("Error: Server is jammed due to over-population.\n"), "connection";
  3417.     } elsif ($args->{type} == 5) {
  3418.         error T("Error: You are underaged and cannot join this server.\n"), "connection";
  3419.     } elsif ($args->{type} == 6) {
  3420.         $interface->errorDialog(T("Critical Error: You must pay to play this account!\n"));
  3421.         $quit = 1 unless ($net->version == 1);
  3422.     } elsif ($args->{type} == 8) {
  3423.         error T("Error: The server still recognizes your last connection\n"), "connection";
  3424.     } elsif ($args->{type} == 9) {
  3425.         error T("Error: IP capacity of this Internet Cafe is full. Would you like to pay the personal base?\n"), "connection";
  3426.     } elsif ($args->{type} == 10) {
  3427.         error T("Error: You are out of available time paid for\n"), "connection";
  3428.     } elsif ($args->{type} == 15) {
  3429.         error T("Error: You have been forced to disconnect by a GM\n"), "connection";
  3430.     } elsif ($args->{type} == 101) {
  3431.         error T("Error: Your account has been suspended until the next maintenance period for possible use of 3rd party programs\n"), "connection";
  3432.     } elsif ($args->{type} == 102) {
  3433.         error T("Error: For an hour, more than 10 connections having same IP address, have made. Please check this matter.\n"), "connection";
  3434.     } else {
  3435.         error TF("Unknown error %s\n", $args->{type}), "connection";
  3436.     }
  3437. }
  3438.  
  3439. sub friend_logon {
  3440.     my ($self, $args) = @_;
  3441.  
  3442.     # Friend In/Out
  3443.     my $friendAccountID = $args->{friendAccountID};
  3444.     my $friendCharID = $args->{friendCharID};
  3445.     my $isNotOnline = $args->{isNotOnline};
  3446.  
  3447.     for (my $i = 0; $i < @friendsID; $i++) {
  3448.         if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
  3449.             $friends{$i}{'online'} = 1 - $isNotOnline;
  3450.             if ($isNotOnline) {
  3451.                 message TF("Friend %s has disconnected\n", $friends{$i}{name}), undef, 1;
  3452.             } else {
  3453.                 message TF("Friend %s has connected\n", $friends{$i}{name}), undef, 1;
  3454.             }
  3455.             last;
  3456.         }
  3457.     }
  3458. }
  3459.  
  3460. sub friend_request {
  3461.     my ($self, $args) = @_;
  3462.  
  3463.     # Incoming friend request
  3464.     $incomingFriend{'accountID'} = $args->{accountID};
  3465.     $incomingFriend{'charID'} = $args->{charID};
  3466.     $incomingFriend{'name'} = bytesToString($args->{name});
  3467.     message TF("%s wants to be your friend\n", $incomingFriend{'name'});
  3468.     message TF("Type 'friend accept' to be friend with %s, otherwise type 'friend reject'\n", $incomingFriend{'name'});
  3469. }
  3470.  
  3471. sub friend_removed {
  3472.     my ($self, $args) = @_;
  3473.  
  3474.     # Friend removed
  3475.     my $friendAccountID =  $args->{friendAccountID};
  3476.     my $friendCharID =  $args->{friendCharID};
  3477.     for (my $i = 0; $i < @friendsID; $i++) {
  3478.         if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
  3479.             message TF("%s is no longer your friend\n", $friends{$i}{'name'});
  3480.             binRemove(\@friendsID, $i);
  3481.             delete $friends{$i};
  3482.             last;
  3483.         }
  3484.     }
  3485. }
  3486.  
  3487. sub friend_response {
  3488.     my ($self, $args) = @_;
  3489.  
  3490.     # Response to friend request
  3491.     my $type = $args->{type};
  3492.     my $name = bytesToString($args->{name});
  3493.     if ($type) {
  3494.         message TF("%s rejected to be your friend\n", $name);
  3495.     } else {
  3496.         my $ID = @friendsID;
  3497.         binAdd(\@friendsID, $ID);
  3498.         $friends{$ID}{accountID} = substr($args->{RAW_MSG}, 4, 4);
  3499.         $friends{$ID}{charID} = substr($args->{RAW_MSG}, 8, 4);
  3500.         $friends{$ID}{name} = $name;
  3501.         $friends{$ID}{online} = 1;
  3502.         message TF("%s is now your friend\n", $name);
  3503.     }
  3504. }
  3505.  
  3506. sub homunculus_food {
  3507.     my ($self, $args) = @_;
  3508.     if ($args->{success}) {
  3509.         message TF("Fed homunculus with %s\n", itemNameSimple($args->{foodID})), "homunculus";
  3510.     } else {
  3511.         error TF("Failed to feed homunculus with %s: no food in inventory.\n", itemNameSimple($args->{foodID})), "homunculus";
  3512.         # auto-vaporize
  3513.         if ($char->{homunculus} && $char->{homunculus}{hunger} <= 11 && timeOut($char->{homunculus}{vaporize_time}, 5)) {
  3514.             $messageSender->sendSkillUse(244, 1, $accountID);
  3515.             $char->{homunculus}{vaporize_time} = time;
  3516.             error "Critical hunger level reached. Homunculus is put to rest.\n", "homunculus";
  3517.         }
  3518.     }
  3519. }
  3520.  
  3521. # TODO: wouldn't it be better if we calculated these only at (first) request after a change in value, if requested at all?
  3522. sub slave_calcproperty_handler {
  3523.     my ($slave, $args) = @_;
  3524.     # so we don't devide by 0
  3525.     # wtf
  3526. =pod
  3527.     $slave->{hp_max}       = ($args->{hp_max} > 0) ? $args->{hp_max} : $args->{hp};
  3528.     $slave->{sp_max}       = ($args->{sp_max} > 0) ? $args->{sp_max} : $args->{sp};
  3529. =cut
  3530.  
  3531.     $slave->{attack_speed}     = int (200 - (($args->{aspd} < 10) ? 10 : ($args->{aspd} / 10)));
  3532.     $slave->{hpPercent}    = $slave->{hp_max} ? ($slave->{hp} / $slave->{hp_max}) * 100 : undef;
  3533.     $slave->{spPercent}    = $slave->{sp_max} ? ($slave->{sp} / $slave->{sp_max}) * 100 : undef;
  3534.     $slave->{expPercent}   = ($args->{exp_max}) ? ($args->{exp} / $args->{exp_max}) * 100 : undef;
  3535. }
  3536.  
  3537. sub gameguard_grant {
  3538.     my ($self, $args) = @_;
  3539.  
  3540.     if ($args->{server} == 0) {
  3541.         error T("The server Denied the login because GameGuard packets where not replied " .
  3542.             "correctly or too many time has been spent to send the response.\n" .
  3543.             "Please verify the version of your poseidon server and try again\n"), "poseidon";
  3544.         return;
  3545.     } elsif ($args->{server} == 1) {
  3546.         message T("Server granted login request to account server\n"), "poseidon";
  3547.     } else {
  3548.         message T("Server granted login request to char/map server\n"), "poseidon";
  3549.         # FIXME
  3550.         change_to_constate25() if ($config{'gameGuard'} eq "2");
  3551.     }
  3552.     $net->setState(1.3) if ($net->getState() == 1.2);
  3553. }
  3554.  
  3555. sub guild_allies_enemy_list {
  3556.     my ($self, $args) = @_;
  3557.  
  3558.     # Guild Allies/Enemy List
  3559.     # <len>.w (<type>.l <guildID>.l <guild name>.24B).*
  3560.     # type=0 Ally
  3561.     # type=1 Enemy
  3562.  
  3563.     # This is the length of the entire packet
  3564.     my $msg = $args->{RAW_MSG};
  3565.     my $len = unpack("v", substr($msg, 2, 2));
  3566.  
  3567.     # clear $guild{enemy} and $guild{ally} otherwise bot will misremember alliances -zdivpsa
  3568.     $guild{enemy} = {}; $guild{ally} = {};
  3569.  
  3570.     for (my $i = 4; $i < $len; $i += 32) {
  3571.         my ($type, $guildID, $guildName) = unpack('V2 Z24', substr($msg, $i, 32));
  3572.         $guildName = bytesToString($guildName);
  3573.         if ($type) {
  3574.             # Enemy guild
  3575.             $guild{enemy}{$guildID} = $guildName;
  3576.         } else {
  3577.             # Allied guild
  3578.             $guild{ally}{$guildID} = $guildName;
  3579.         }
  3580.         debug "Your guild is ".($type ? 'enemy' : 'ally')." with guild $guildID ($guildName)\n", "guild";
  3581.     }
  3582. }
  3583.  
  3584. sub guild_ally_request {
  3585.     my ($self, $args) = @_;
  3586.  
  3587.     my $ID = $args->{ID}; # is this a guild ID or account ID? Freya calls it an account ID
  3588.     my $name = bytesToString($args->{guildName}); # Type: String
  3589.  
  3590.     message TF("Incoming Request to Ally Guild '%s'\n", $name);
  3591.     $incomingGuild{ID} = $ID;
  3592.     $incomingGuild{Type} = 2;
  3593.     $timeout{ai_guildAutoDeny}{time} = time;
  3594. }
  3595.  
  3596. sub guild_broken {
  3597.     my ($self, $args) = @_;
  3598.     my $flag = $args->{flag};
  3599.  
  3600.     if ($flag == 2) {
  3601.         error T("Guild can not be undone: there are still members in the guild\n");
  3602.     } elsif ($flag == 1) {
  3603.         error T("Guild can not be undone: invalid key\n");
  3604.     } elsif ($flag == 0) {
  3605.         message T("Guild broken.\n");
  3606.         undef %{$char->{guild}};
  3607.         undef $char->{guildID};
  3608.         undef %guild;
  3609.     } else {
  3610.         error TF("Guild can not be undone: unknown reason (flag: %s)\n", $flag);
  3611.     }
  3612. }
  3613.  
  3614. sub guild_create_result {
  3615.     my ($self, $args) = @_;
  3616.     my $type = $args->{type};
  3617.  
  3618.     my %types = (
  3619.         0 => T("Guild create successful.\n"),
  3620.         2 => T("Guild create failed: Guild name already exists.\n"),
  3621.         3 => T("Guild create failed: Emperium is needed.\n")
  3622.     );
  3623.     if ($types{$type}) {
  3624.         message $types{$type};
  3625.     } else {
  3626.         message TF("Guild create: Unknown error %s\n", $type);
  3627.     }
  3628. }
  3629.  
  3630. sub guild_info {
  3631.     my ($self, $args) = @_;
  3632.     # Guild Info
  3633.     foreach (qw(ID lv conMember maxMember average exp exp_next tax tendency_left_right tendency_down_up name master castles_string)) {
  3634.         $guild{$_} = $args->{$_};
  3635.     }
  3636.     $guild{name} = bytesToString($args->{name});
  3637.     $guild{master} = bytesToString($args->{master});
  3638.     $guild{members}++; # count ourselves in the guild members count
  3639. }
  3640.  
  3641. sub guild_invite_result {
  3642.     my ($self, $args) = @_;
  3643.  
  3644.     my $type = $args->{type};
  3645.  
  3646.     my %types = (
  3647.         0 => T('Target is already in a guild.'),
  3648.         1 => T('Target has denied.'),
  3649.         2 => T('Target has accepted.'),
  3650.         3 => T('Your guild is full.')
  3651.     );
  3652.     if ($types{$type}) {
  3653.         message TF("Guild join request: %s\n", $types{$type});
  3654.     } else {
  3655.         message TF("Guild join request: Unknown %s\n", $type);
  3656.     }
  3657. }
  3658.  
  3659. sub guild_location {
  3660.     # FIXME: not implemented
  3661.     my ($self, $args) = @_;
  3662.     unless ($args->{x} > 0 && $args->{y} > 0) {
  3663.         # delete locator for ID
  3664.     } else {
  3665.         # add/replace locator for ID
  3666.     }
  3667. }
  3668.  
  3669. sub guild_leave {
  3670.     my ($self, $args) = @_;
  3671.  
  3672.     message TF("%s has left the guild.\n" .
  3673.         "Reason: %s\n", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
  3674. }
  3675.  
  3676. sub guild_expulsion {
  3677.     my ($self, $args) = @_;
  3678.  
  3679.     message TF("%s has been removed from the guild.\n" .
  3680.         "Reason: %s\n", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
  3681. }
  3682.  
  3683. sub guild_member_online_status {
  3684.     my ($self, $args) = @_;
  3685.  
  3686.     foreach my $guildmember (@{$guild{member}}) {
  3687.         if ($guildmember->{charID} eq $args->{charID}) {
  3688.             if ($guildmember->{online} = $args->{online}) {
  3689.                 message TF("Guild member %s logged in.\n", $guildmember->{name}), "guildchat";
  3690.             } else {
  3691.                 message TF("Guild member %s logged out.\n", $guildmember->{name}), "guildchat";
  3692.             }
  3693.             last;
  3694.         }
  3695.     }
  3696. }
  3697.  
  3698. sub misc_effect {
  3699.     my ($self, $args) = @_;
  3700.  
  3701.     my $actor = Actor::get($args->{ID});
  3702.     message sprintf(
  3703.         $actor->verb(T("%s use effect: %s\n"), T("%s uses effect: %s\n")),
  3704.         $actor, defined $effectName{$args->{effect}} ? $effectName{$args->{effect}} : T("Unknown #")."$args->{effect}"
  3705.     ), 'effect'
  3706. }
  3707.  
  3708. sub guild_members_title_list {
  3709.     my ($self, $args) = @_;
  3710.  
  3711.     my $msg = $args->{RAW_MSG};
  3712.     my $msg_size = $args->{RAW_MSG_SIZE};
  3713.  
  3714.     my $gtIndex;
  3715.     for (my $i = 4; $i < $msg_size; $i+=28) {
  3716.         $gtIndex = unpack('V', substr($msg, $i, 4));
  3717.         $guild{positions}[$gtIndex]{title} = bytesToString(unpack('Z24', substr($msg, $i + 4, 24)));
  3718.     }
  3719. }
  3720.  
  3721. sub guild_name {
  3722.     my ($self, $args) = @_;
  3723.  
  3724.     my $guildID = $args->{guildID};
  3725.     my $emblemID = $args->{emblemID};
  3726.     my $mode = $args->{mode};
  3727.     my $guildName = bytesToString($args->{guildName});
  3728.     $char->{guild}{name} = $guildName;
  3729.     $char->{guildID} = $guildID;
  3730.     $char->{guild}{emblem} = $emblemID;
  3731.  
  3732.     $messageSender->sendGuildMasterMemberCheck();
  3733.     $messageSender->sendGuildRequestInfo(0);    #requests for guild info packet 01B6 and 014C
  3734.     $messageSender->sendGuildRequestInfo(1);    #requests for guild member packet 0166 and 0154
  3735.     debug "guild name: $guildName\n";
  3736. }
  3737.  
  3738. sub guild_request {
  3739.     my ($self, $args) = @_;
  3740.  
  3741.     # Guild request
  3742.     my $ID = $args->{ID};
  3743.     my $name = bytesToString($args->{name});
  3744.     message TF("Incoming Request to join Guild '%s'\n", $name);
  3745.     $incomingGuild{'ID'} = $ID;
  3746.     $incomingGuild{'Type'} = 1;
  3747.     $timeout{'ai_guildAutoDeny'}{'time'} = time;
  3748. }
  3749.  
  3750. sub identify {
  3751.     my ($self, $args) = @_;
  3752.     if ($args->{flag} == 0) {
  3753.         my $item = $char->inventory->getByID($args->{ID});
  3754.         $item->{identified} = 1;
  3755.         $item->{type_equip} = $itemSlots_lut{$item->{nameID}};
  3756.         message TF("Item Identified: %s (%d)\n", $item->{name}, $item->{binID}), "info";
  3757.     } else {
  3758.         message T("Item Appraisal has failed.\n");
  3759.     }
  3760.     undef @identifyID;
  3761. }
  3762.  
  3763. # TODO: store this state
  3764. sub ignore_all_result {
  3765.     my ($self, $args) = @_;
  3766.     if ($args->{type} == 0) {
  3767.         message T("All Players ignored\n");
  3768.     } elsif ($args->{type} == 1) {
  3769.         if ($args->{error} == 0) {
  3770.             message T("All players unignored\n");
  3771.         }
  3772.     }
  3773. }
  3774.  
  3775. # TODO: store list of ignored players
  3776. sub ignore_player_result {
  3777.     my ($self, $args) = @_;
  3778.     if ($args->{type} == 0) {
  3779.         message T("Player ignored\n");
  3780.     } elsif ($args->{type} == 1) {
  3781.         if ($args->{error} == 0) {
  3782.             message T("Player unignored\n");
  3783.         }
  3784.     }
  3785. }
  3786.  
  3787. sub item_used {
  3788.     my ($self, $args) = @_;
  3789.  
  3790.     my ($index, $itemID, $ID, $remaining, $success) =
  3791.         @{$args}{qw(ID itemID actorID remaining success)};
  3792.     my %hook_args = (
  3793.         serverIndex => $index,
  3794.         itemID => $itemID,
  3795.         userID => $ID,
  3796.         remaining => $remaining,
  3797.         success => $success
  3798.     );
  3799.  
  3800.     if ($ID eq $accountID) {
  3801.         my $item = $char->inventory->getByID($index);
  3802.         if ($item) {
  3803.             if ($success == 1) {
  3804.                 my $amount = $item->{amount} - $remaining;
  3805.  
  3806.                 message TF("You used Item: %s (%d) x %d - %d left\n", $item->{name}, $item->{binID},
  3807.                     $amount, $remaining), "useItem", 1;
  3808.                
  3809.                 inventoryItemRemoved($item->{binID}, $amount);
  3810.  
  3811.                 $hook_args{item} = $item;
  3812.                 $hook_args{binID} = $item->{binID};
  3813.                 $hook_args{name} => $item->{name};
  3814.                 $hook_args{amount} = $amount;
  3815.  
  3816.             } else {
  3817.                 message TF("You failed to use item: %s (%d)\n", $item ? $item->{name} : "#$itemID", $remaining), "useItem", 1;
  3818.             }
  3819.         } else {
  3820.             if ($success == 1) {
  3821.                 message TF("You used unknown item #%d - %d left\n", $itemID, $remaining), "useItem", 1;
  3822.             } else {
  3823.                 message TF("You failed to use unknown item #%d - %d left\n", $itemID, $remaining), "useItem", 1;
  3824.             }
  3825.         }
  3826.     } else {
  3827.         my $actor = Actor::get($ID);
  3828.         my $itemDisplay = itemNameSimple($itemID);
  3829.         message TF("%s used Item: %s - %s left\n", $actor, $itemDisplay, $remaining), "useItem", 2;
  3830.     }
  3831.     Plugins::callHook('packet_useitem', \%hook_args);
  3832. }
  3833.  
  3834. sub married {
  3835.     my ($self, $args) = @_;
  3836.  
  3837.     my $actor = Actor::get($args->{ID});
  3838.     message TF("%s got married!\n", $actor);
  3839. }
  3840.  
  3841. sub item_appeared {
  3842.     my ($self, $args) = @_;
  3843.     return unless changeToInGameState();
  3844.  
  3845.     my $item = $itemsList->getByID($args->{ID});
  3846.     my $mustAdd;
  3847.     if (!$item) {
  3848.         $item = new Actor::Item();
  3849.         $item->{appear_time} = time;
  3850.         $item->{amount} = $args->{amount};
  3851.         $item->{nameID} = $args->{nameID};
  3852.         $item->{identified} = $args->{identified};
  3853.         $item->{name} = itemName($item);
  3854.         $item->{ID} = $args->{ID};
  3855.         $mustAdd = 1;
  3856.     }
  3857.     $item->{pos}{x} = $args->{x};
  3858.     $item->{pos}{y} = $args->{y};
  3859.     $item->{pos_to}{x} = $args->{x};
  3860.     $item->{pos_to}{y} = $args->{y};
  3861.     $itemsList->add($item) if ($mustAdd);
  3862.  
  3863.     # Take item as fast as possible
  3864.     if (AI::state == AI::AUTO && pickupitems($item->{name}, $item->{nameID}) == 2
  3865.      && ($config{'itemsTakeAuto'} || $config{'itemsGatherAuto'})
  3866.      && (percent_weight($char) < $config{'itemsMaxWeight'})
  3867.      && distance($item->{pos}, $char->{pos_to}) <= 5) {
  3868.         $messageSender->sendTake($args->{ID});
  3869.     }
  3870.  
  3871.     message TF("Item Appeared: %s (%d) x %d (%d, %d)\n", $item->{name}, $item->{binID}, $item->{amount}, $args->{x}, $args->{y}), "drop", 1;
  3872.  
  3873. }
  3874.  
  3875. sub item_exists {
  3876.     my ($self, $args) = @_;
  3877.     return unless changeToInGameState();
  3878.  
  3879.     my $item = $itemsList->getByID($args->{ID});
  3880.     my $mustAdd;
  3881.     if (!$item) {
  3882.         $item = new Actor::Item();
  3883.         $item->{appear_time} = time;
  3884.         $item->{amount} = $args->{amount};
  3885.         $item->{nameID} = $args->{nameID};
  3886.         $item->{ID} = $args->{ID};
  3887.         $item->{identified} = $args->{identified};
  3888.         $item->{name} = itemName($item);
  3889.         $mustAdd = 1;
  3890.     }
  3891.     $item->{pos}{x} = $args->{x};
  3892.     $item->{pos}{y} = $args->{y};
  3893.     $item->{pos_to}{x} = $args->{x};
  3894.     $item->{pos_to}{y} = $args->{y};
  3895.     $itemsList->add($item) if ($mustAdd);
  3896.  
  3897.     message TF("Item Exists: %s (%d) x %d\n", $item->{name}, $item->{binID}, $item->{amount}), "drop", 1;
  3898. }
  3899.  
  3900. sub item_disappeared {
  3901.     my ($self, $args) = @_;
  3902.     return unless changeToInGameState();
  3903.  
  3904.     my $item = $itemsList->getByID($args->{ID});
  3905.     if ($item) {
  3906.         if ($config{attackLooters} && AI::action ne "sitAuto" && pickupitems($item->{name}, $item->{nameID}) > 0) {
  3907.             for my Actor::Monster $monster (@$monstersList) { # attack looter code
  3908.                 if (my $control = mon_control($monster->name,$monster->{nameID})) {
  3909.                     next if ( ($control->{attack_auto}  ne "" && $control->{attack_auto} == -1)
  3910.                         || ($control->{attack_lvl}  ne "" && $control->{attack_lvl} > $char->{lv})
  3911.                         || ($control->{attack_jlvl} ne "" && $control->{attack_jlvl} > $char->{lv_job})
  3912.                         || ($control->{attack_hp}   ne "" && $control->{attack_hp} > $char->{hp})
  3913.                         || ($control->{attack_sp}   ne "" && $control->{attack_sp} > $char->{sp})
  3914.                         );
  3915.                 }
  3916.                 if (distance($item->{pos}, $monster->{pos}) == 0) {
  3917.                     attack($monster->{ID});
  3918.                     message TF("Attack Looter: %s looted %s\n", $monster->nameIdx, $item->{name}), "looter";
  3919.                     last;
  3920.                 }
  3921.             }
  3922.         }
  3923.  
  3924.         debug "Item Disappeared: $item->{name} ($item->{binID})\n", "parseMsg_presence";
  3925.         my $ID = $args->{ID};
  3926.         $items_old{$ID} = $item->deepCopy();
  3927.         $items_old{$ID}{disappeared} = 1;
  3928.         $items_old{$ID}{gone_time} = time;
  3929.         $itemsList->removeByID($ID);
  3930.     }
  3931. }
  3932.  
  3933. sub item_upgrade {
  3934.     my ($self, $args) = @_;
  3935.     my ($type, $index, $upgrade) = @{$args}{qw(type ID upgrade)};
  3936.  
  3937.     my $item = $char->inventory->getByID($index);
  3938.     if ($item) {
  3939.         $item->{upgrade} = $upgrade;
  3940.         message TF("Item %s has been upgraded to +%s\n", $item->{name}, $upgrade), "parseMsg/upgrade";
  3941.         $item->setName(itemName($item));
  3942.     }
  3943. }
  3944.  
  3945. sub job_equipment_hair_change {
  3946.     my ($self, $args) = @_;
  3947.     return unless changeToInGameState();
  3948.  
  3949.     my $actor = Actor::get($args->{ID});
  3950.     assert(UNIVERSAL::isa($actor, "Actor")) if DEBUG;
  3951.  
  3952.     if ($args->{part} == 0) {
  3953.         # Job change
  3954.         $actor->{jobID} = $args->{number};
  3955.         message TF("%s changed job to: %s\n", $actor, $jobs_lut{$args->{number}}), "parseMsg/job", ($actor->isa('Actor::You') ? 0 : 2);
  3956.  
  3957.     } elsif ($args->{part} == 3) {
  3958.         # Bottom headgear change
  3959.         message TF("%s changed bottom headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
  3960.         $actor->{headgear}{low} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
  3961.  
  3962.     } elsif ($args->{part} == 4) {
  3963.         # Top headgear change
  3964.         message TF("%s changed top headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
  3965.         $actor->{headgear}{top} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
  3966.  
  3967.     } elsif ($args->{part} == 5) {
  3968.         # Middle headgear change
  3969.         message TF("%s changed middle headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
  3970.         $actor->{headgear}{mid} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
  3971.  
  3972.     } elsif ($args->{part} == 6) {
  3973.         # Hair color change
  3974.         $actor->{hair_color} = $args->{number};
  3975.         message TF("%s changed hair color to: %s (%s)\n", $actor, $haircolors{$args->{number}}, $args->{number}), "parseMsg/hairColor", ($actor->isa('Actor::You') ? 0 : 2);
  3976.     }
  3977.  
  3978.     #my %parts = (
  3979.     #   0 => 'Body',
  3980.     #   2 => 'Right Hand',
  3981.     #   3 => 'Low Head',
  3982.     #   4 => 'Top Head',
  3983.     #   5 => 'Middle Head',
  3984.     #   8 => 'Left Hand'
  3985.     #);
  3986.     #if ($part == 3) {
  3987.     #   $part = 'low';
  3988.     #} elsif ($part == 4) {
  3989.     #   $part = 'top';
  3990.     #} elsif ($part == 5) {
  3991.     #   $part = 'mid';
  3992.     #}
  3993.     #
  3994.     #my $name = getActorName($ID);
  3995.     #if ($part == 3 || $part == 4 || $part == 5) {
  3996.     #   my $actor = Actor::get($ID);
  3997.     #   $actor->{headgear}{$part} = $items_lut{$number} if ($actor);
  3998.     #   my $itemName = $items_lut{$itemID};
  3999.     #   $itemName = 'nothing' if (!$itemName);
  4000.     #   debug "$name changes $parts{$part} ($part) equipment to $itemName\n", "parseMsg";
  4001.     #} else {
  4002.     #   debug "$name changes $parts{$part} ($part) equipment to item #$number\n", "parseMsg";
  4003.     #}
  4004.  
  4005. }
  4006.  
  4007. # Leap, Snap, Back Slide... Various knockback
  4008. sub high_jump {
  4009.     my ($self, $args) = @_;
  4010.     return unless changeToInGameState();
  4011.  
  4012.     my $actor = Actor::get ($args->{ID});
  4013.     if (!defined $actor) {
  4014.         $actor = new Actor::Unknown;
  4015.         $actor->{appear_time} = time;
  4016.         $actor->{nameID} = unpack ('V', $args->{ID});
  4017.     } elsif ($actor->{pos_to}{x} == $args->{x} && $actor->{pos_to}{y} == $args->{y}) {
  4018.         message TF("%s failed to instantly move\n", $actor->nameString), 'skill';
  4019.         return;
  4020.     }
  4021.  
  4022.     $actor->{pos} = {x => $args->{x}, y => $args->{y}};
  4023.     $actor->{pos_to} = {x => $args->{x}, y => $args->{y}};
  4024.  
  4025.     message TF("%s instantly moved to %d, %d\n", $actor->nameString, $actor->{pos_to}{x}, $actor->{pos_to}{y}), 'skill', 2;
  4026.  
  4027.     $actor->{time_move} = time;
  4028.     $actor->{time_move_calc} = 0;
  4029. }
  4030.  
  4031. sub hp_sp_changed {
  4032.     my ($self, $args) = @_;
  4033.     return unless changeToInGameState();
  4034.  
  4035.     my $type = $args->{type};
  4036.     my $amount = $args->{amount};
  4037.     if ($type == 5) {
  4038.         $char->{hp} += $amount;
  4039.         $char->{hp} = $char->{hp_max} if ($char->{hp} > $char->{hp_max});
  4040.     } elsif ($type == 7) {
  4041.         $char->{sp} += $amount;
  4042.         $char->{sp} = $char->{sp_max} if ($char->{sp} > $char->{sp_max});
  4043.     }
  4044. }
  4045.  
  4046. # The difference between map_change and map_changed is that map_change
  4047. # represents a map change event on the current map server, while
  4048. # map_changed means that you've changed to a different map server.
  4049. # map_change also represents teleport events.
  4050. sub map_change {
  4051.     my ($self, $args) = @_;
  4052.     return unless changeToInGameState();
  4053.  
  4054.     my $oldMap = $field ? $field->baseName : undef; # Get old Map name without InstanceID
  4055.     my ($map) = $args->{map} =~ /([\s\S]*)\./;
  4056.     my $map_noinstance;
  4057.     ($map_noinstance, undef) = Field::nameToBaseName(undef, $map); # Hack to clean up InstanceID
  4058.  
  4059.     checkAllowedMap($map_noinstance);
  4060.     if (!$field || $map ne $field->name()) {
  4061.         eval {
  4062.             $field = new Field(name => $map);
  4063.         };
  4064.         if (my $e = caught('FileNotFoundException', 'IOException')) {
  4065.             error TF("Cannot load field %s: %s\n", $map_noinstance, $e);
  4066.             undef $field;
  4067.         } elsif ($@) {
  4068.             die $@;
  4069.         }
  4070.     }
  4071.  
  4072.     if ($ai_v{temp}{clear_aiQueue}) {
  4073.         AI::clear;
  4074.         AI::SlaveManager::clear();
  4075.     }
  4076.  
  4077.     main::initMapChangeVars();
  4078.     for (my $i = 0; $i < @ai_seq; $i++) {
  4079.         ai_setMapChanged($i);
  4080.     }
  4081.     AI::SlaveManager::setMapChanged ();
  4082.     if ($net->version == 0) {
  4083.         $ai_v{portalTrace_mapChanged} = time;
  4084.     }
  4085.  
  4086.     my %coords = (
  4087.         x => $args->{x},
  4088.         y => $args->{y}
  4089.     );
  4090.     $char->{pos} = {%coords};
  4091.     $char->{pos_to} = {%coords};
  4092.     message TF("Map Change: %s (%s, %s)\n", $args->{map}, $char->{pos}{x}, $char->{pos}{y}), "connection";
  4093.     if ($net->version == 1) {
  4094.         ai_clientSuspend(0, 10);
  4095.     } else {
  4096.         $messageSender->sendMapLoaded();
  4097.         $messageSender->sendBlockingPlayerCancel() if(grep { $masterServer->{serverType} eq $_ } qw( Zero idRO_Renewal cRO)); # request to unfreeze char alisonrag
  4098.         # $messageSender->sendSync(1);
  4099.         $timeout{ai}{time} = time;
  4100.     }
  4101.  
  4102.     Plugins::callHook('Network::Receive::map_changed', {
  4103.         oldMap => $oldMap,
  4104.     });
  4105.  
  4106.     $timeout{ai}{time} = time;
  4107. }
  4108.  
  4109. # Parse 0A3B with structure
  4110. # '0A3B' => ['hat_effect', 'v a4 C a*', [qw(len ID flag effect)]],
  4111. # Unpack effect info into HatEFID
  4112. # @author [Cydh]
  4113. sub parse_hat_effect {
  4114.     my ($self, $args) = @_;
  4115.     @{$args->{effects}} = map {{ HatEFID => unpack('v', $_) }} unpack '(a2)*', $args->{effect};
  4116.     debug "Hat Effect. Flag: ".$args->{flag}." HatEFIDs: ".(join ', ', map {$_->{HatEFID}} @{$args->{effects}})."\n";
  4117. }
  4118.  
  4119. # Display information for player's Hat Effects
  4120. # @author [Cydh]
  4121. sub hat_effect {
  4122.     my ($self, $args) = @_;
  4123.  
  4124.     my $actor = Actor::get($args->{ID});
  4125.     my $hatName;
  4126.     my $i = 0;
  4127.  
  4128.     #TODO: Stores the hat effect into actor for single player's information
  4129.     for my $hat (@{$args->{effects}}) {
  4130.         my $hatHandle;
  4131.         $hatName .= ", " if ($i);
  4132.         if (defined $hatEffectHandle{$hat->{HatEFID}}) {
  4133.             $hatHandle = $hatEffectHandle{$hat->{HatEFID}};
  4134.             $hatName .= defined $hatEffectName{$hatHandle} ? $hatEffectName{$hatHandle} : $hatHandle;
  4135.         } else {
  4136.             $hatName .= T("Unknown #").$hat->{HatEFID};
  4137.         }
  4138.         $i++;
  4139.     }
  4140.  
  4141.     if ($args->{flag} == 1) {
  4142.         message sprintf(
  4143.             $actor->verb(T("%s use effect: %s\n"), T("%s uses effect: %s\n")),
  4144.             $actor, $hatName
  4145.         ), 'effect';
  4146.     } else {
  4147.         message sprintf(
  4148.             $actor->verb(T("%s are no longer: %s\n"), T("%s is no longer: %s\n")),
  4149.             $actor, $hatName
  4150.         ), 'effect';
  4151.     }
  4152. }
  4153.  
  4154. sub npc_talk_close {
  4155.     my ($self, $args) = @_;
  4156.     # 00b6: long ID
  4157.     # "Close" icon appreared on the NPC message dialog
  4158.     my $ID = $args->{ID};
  4159.     my $name = getNPCName($ID);
  4160.  
  4161.     $ai_v{'npc_talk'}{'talk'} = 'close';
  4162.     $ai_v{'npc_talk'}{'time'} = time;
  4163.     undef %talk;
  4164.  
  4165.     Plugins::callHook('npc_talk_done', {ID => $ID});
  4166. }
  4167.  
  4168. sub npc_talk_continue {
  4169.     my ($self, $args) = @_;
  4170.     my $ID = substr($args->{RAW_MSG}, 2, 4);
  4171.     my $name = getNPCName($ID);
  4172.  
  4173.     $ai_v{'npc_talk'}{'talk'} = 'next';
  4174.     $ai_v{'npc_talk'}{'time'} = time;
  4175. }
  4176.  
  4177. sub npc_talk_number {
  4178.     my ($self, $args) = @_;
  4179.  
  4180.     my $ID = $args->{ID};
  4181.  
  4182.     my $name = getNPCName($ID);
  4183.     $ai_v{'npc_talk'}{'talk'} = 'number';
  4184.     $ai_v{'npc_talk'}{'time'} = time;
  4185. }
  4186.  
  4187. sub npc_talk_responses {
  4188.     my ($self, $args) = @_;
  4189.    
  4190.     # 00b7: word len, long ID, string str
  4191.     # A list of selections appeared on the NPC message dialog.
  4192.     # Each item is divided with ':'
  4193.     my $msg = $args->{RAW_MSG};
  4194.  
  4195.     my $ID = substr($msg, 4, 4);
  4196.     my $nameID = unpack 'V', $ID;
  4197.    
  4198.     # Auto-create Task::TalkNPC if not active
  4199.     if (!AI::is("NPC") && !(AI::is("route") && $char->args->getSubtask && UNIVERSAL::isa($char->args->getSubtask, 'Task::TalkNPC'))) {
  4200.         debug "An unexpected npc conversation has started, auto-creating a TalkNPC Task\n";
  4201.         my $task = Task::TalkNPC->new(type => 'autotalk', nameID => $nameID, ID => $ID);
  4202.         AI::queue("NPC", $task);
  4203.         # TODO: The following npc_talk hook is only added on activation.
  4204.         # Make the task module or AI listen to the hook instead
  4205.         # and wrap up all the logic.
  4206.         $task->activate;
  4207.         Plugins::callHook('npc_autotalk', {
  4208.             task => $task
  4209.         });
  4210.     }
  4211.    
  4212.     $talk{ID} = $ID;
  4213.     $talk{nameID} = $nameID;
  4214.     my $talk = unpack("Z*", substr($msg, 8));
  4215.     $talk = substr($msg, 8) if (!defined $talk);
  4216.     $talk = bytesToString($talk);
  4217.  
  4218.     my @preTalkResponses = split /:/, $talk;
  4219.     $talk{responses} = [];
  4220.     foreach my $response (@preTalkResponses) {
  4221.         # Remove RO color codes
  4222.         $response =~ s/\^[a-fA-F0-9]{6}//g;
  4223.         if ($response =~ /^\^nItemID\^(\d+)$/) {
  4224.             $response = itemNameSimple($1);
  4225.         }
  4226.  
  4227.         push @{$talk{responses}}, $response if ($response ne "");
  4228.     }
  4229.  
  4230.     $talk{responses}[@{$talk{responses}}] = T("Cancel Chat");
  4231.  
  4232.     $ai_v{'npc_talk'}{'talk'} = 'select';
  4233.     $ai_v{'npc_talk'}{'time'} = time;
  4234.  
  4235.     Commands::run('talk resp');
  4236.  
  4237.     my $name = getNPCName($ID);
  4238.     Plugins::callHook('npc_talk_responses', {
  4239.                         ID => $ID,
  4240.                         name => $name,
  4241.                         responses => $talk{responses},
  4242.                         });
  4243. }
  4244.  
  4245. sub npc_talk_text {
  4246.     my ($self, $args) = @_;
  4247.  
  4248.     my $ID = $args->{ID};
  4249.  
  4250.     my $name = getNPCName($ID);
  4251.     $ai_v{'npc_talk'}{'talk'} = 'text';
  4252.     $ai_v{'npc_talk'}{'time'} = time;
  4253. }
  4254.  
  4255. sub npc_store_begin {
  4256.     my ($self, $args) = @_;
  4257.     undef %talk;
  4258.     $talk{ID} = $args->{ID};
  4259.     $ai_v{'npc_talk'}{'talk'} = 'buy_or_sell';
  4260.     $ai_v{'npc_talk'}{'time'} = time;
  4261.  
  4262.     $storeList->{npcName} = getNPCName($args->{ID}) || T('Unknown');
  4263. }
  4264.  
  4265. sub npc_store_info {
  4266.     my ($self, $args) = @_;
  4267.     my $msg = $args->{RAW_MSG};
  4268.     my $pack = 'V V C v';
  4269.     my $len = length pack $pack;
  4270.     $storeList->clear;
  4271.     undef %talk;
  4272.     for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += $len) {
  4273.         my $item = Actor::Item->new;
  4274.         @$item{qw( price _ type nameID )} = unpack $pack, substr $msg, $i, $len;
  4275.         $item->{ID} = $item->{nameID};
  4276.         $item->{name} = itemName($item);
  4277.         $storeList->add($item);
  4278.  
  4279.         debug "Item added to Store: $item->{name} - $item->{price}z\n", "parseMsg", 2;
  4280.     }
  4281.  
  4282.     $ai_v{npc_talk}{talk} = 'store';
  4283.     # continue talk sequence now
  4284.     $ai_v{'npc_talk'}{'time'} = time;
  4285.  
  4286.     if (AI::action ne 'buyAuto') {
  4287.         Commands::run('store');
  4288.     }
  4289. }
  4290.  
  4291. sub deal_add_you {
  4292.     my ($self, $args) = @_;
  4293.  
  4294.     if ($args->{fail} == 1) {
  4295.         error T("That person is overweight; you cannot trade.\n"), "deal";
  4296.         return;
  4297.     } elsif ($args->{fail} == 2) {
  4298.         error T("This item cannot be traded.\n"), "deal";
  4299.         return;
  4300.     } elsif ($args->{fail}) {
  4301.         error TF("You cannot trade (fail code %s).\n", $args->{fail}), "deal";
  4302.         return;
  4303.     }
  4304.  
  4305.     my $id = unpack('v',$args->{ID});
  4306.    
  4307.     return unless ($id > 0);
  4308.  
  4309.     my $item = $char->inventory->getByID($args->{ID});
  4310.     $args->{item} = $item;
  4311.     # FIXME: quickly add two items => lastItemAmount is lost => inventory corruption; see also Misc::dealAddItem
  4312.     # FIXME: what will be in case of two items with the same nameID?
  4313.     # TODO: no info about items is stored
  4314.     $currentDeal{you_items}++;
  4315.     $currentDeal{you}{$item->{nameID}}{amount} += $currentDeal{lastItemAmount};
  4316.     $currentDeal{you}{$item->{nameID}}{nameID} = $item->{nameID};
  4317.     message TF("You added Item to Deal: %s x %s\n", $item->{name}, $currentDeal{lastItemAmount}), "deal";
  4318.     inventoryItemRemoved($item->{binID}, $currentDeal{lastItemAmount});
  4319. }
  4320.  
  4321. sub skill_exchange_item {
  4322.     my ($self, $args) = @_;
  4323.     if ($args->{type} == 0) {
  4324.         message T("Change Material is ready. Use command 'cm' to continue.\n"), "info";
  4325.     } else {
  4326.         message T("Four Spirit Analysis is ready. Use command 'analysis' to continue.\n"), "info";
  4327.     }
  4328.     ##
  4329.     # $args->{type} : Type
  4330.     #                 0: Change Material         -> 1
  4331.     #                 1: Elemental Analysis Lv 1 -> 2
  4332.     #                 2: Elemental Analysis Lv 2 -> 3
  4333.     #                 This value will be added +1 for simple check later
  4334.     # $args->{val} : ????
  4335.     ##
  4336.     $skillExchangeItem = $args->{type} + 1;
  4337. }
  4338.  
  4339. # Allowed to RefineUI by server
  4340. # '0AA0' => ['refineui_opened', '' ,[qw()]],
  4341. # @author [Cydh]
  4342. sub refineui_opened {
  4343.     my ($self, $args) = @_;
  4344.     message TF("RefineUI is opened. Type 'i' to check equipment and its index. To continue: refineui select [ItemIdx]\n"), "info";
  4345.     $refineUI->{open} = 1;
  4346. }
  4347.  
  4348. # Received refine info for selected item
  4349. # '0AA2' => ['refineui_info', 'v v C a*' ,[qw(index bless materials)]],
  4350. # @param args Packet data
  4351. # @author [Cydh]
  4352. sub refineui_info {
  4353.     my ($self, $args) = @_;
  4354.  
  4355.     if ($args->{len} > 7) {
  4356.         $refineUI->{itemIndex} = $args->{index};
  4357.         $refineUI->{bless} = $args->{bless};
  4358.  
  4359.         my $item = $char->inventory->[$refineUI->{invIndex}];
  4360.         my $bless = $char->inventory->getByNameID($Blacksmith_Blessing);
  4361.  
  4362.         message T("========= RefineUI Info =========\n"), "info";
  4363.         message TF("Target Equip:\n".
  4364.                 "- Index: %d\n".
  4365.                 "- Name: %s\n",
  4366.                 $refineUI->{invIndex}, $item ? itemName($item) : "Unknown."),
  4367.                 "info";
  4368.  
  4369.         message TF("%s:\n".
  4370.                 "- Needed: %d\n".
  4371.                 "- Owned: %d\n",
  4372.                 #itemNameSimple($Blacksmith_Blessing)
  4373.                 "Blacksmith Blessing", $refineUI->{bless}, $bless ? $bless->{amount} : 0),
  4374.                 "info";
  4375.  
  4376.         @{$refineUI->{materials}} = map { my %r; @r{qw(nameid chance zeny)} = unpack 'v C V', $_; \%r} unpack '(a7)*', $args->{materials};
  4377.  
  4378.         my $msg = center(T(" Possible Materials "), 53, '-') ."\n" .
  4379.                 T("Mat_ID      %           Zeny        Material                        \n");
  4380.         foreach my $mat (@{$refineUI->{materials}}) {
  4381.             my $myMat = $char->inventory->getByNameID($mat->{nameid});
  4382.             my $myMatCount = sprintf("%d ea %s", $myMat ? $myMat->{amount} : 0, itemNameSimple($mat->{nameid}));
  4383.             $msg .= swrite(
  4384.                 "@>>>>>>>> @>>>>> @>>>>>>>>>>>>   @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<",
  4385.                 [$mat->{nameid}, $mat->{chance}, $mat->{zeny}, $myMatCount]);
  4386.         }
  4387.         $msg .= ('-'x53) . "\n";
  4388.         message $msg, "info";
  4389.         message TF("Continue: refineui refine %d [Mat_ID] [catalyst_toggle] to continue.\n", $refineUI->{invIndex}), "info";
  4390.     } else {
  4391.         error T("Equip cannot be refined, try different equipment. Type 'i' to check equipment and its index.\n");
  4392.     }
  4393. }
  4394.  
  4395. sub character_ban_list {
  4396.     my ($self, $args) = @_;
  4397.     # Header + Len + CharList[character_name(size:24)]
  4398. }
  4399.  
  4400. sub flag {
  4401.     my ($self, $args) = @_;
  4402. }
  4403.  
  4404. sub parse_stat_info {
  4405.     my ($self, $args) = @_;
  4406.     if($args->{switch} eq "0ACB") {
  4407.         $args->{val} = getHex($args->{val});
  4408.         $args->{val} = join '', reverse split / /, $args->{val};
  4409.         $args->{val} = hex $args->{val};
  4410.     }
  4411. }
  4412.  
  4413. sub parse_exp {
  4414.     my ($self, $args) = @_;
  4415.     if($args->{switch} eq "0ACC") {
  4416.         $args->{val} = getHex($args->{val});
  4417.         $args->{val} = join '', reverse split / /, $args->{val};
  4418.         $args->{val} = hex $args->{val};
  4419.     }
  4420. }
  4421.  
  4422. sub clone_vender_found {
  4423.     my ($self, $args) = @_;
  4424.     my $ID = unpack("V", $args->{ID});
  4425.     if (!$venderLists{$ID} || !%{$venderLists{$ID}}) {
  4426.         binAdd(\@venderListsID, $ID);
  4427.         Plugins::callHook('packet_vender', {ID => $ID, title => bytesToString($args->{title})});
  4428.     }
  4429.     $venderLists{$ID}{title} = bytesToString($args->{title});
  4430.     $venderLists{$ID}{id} = $ID;
  4431.  
  4432.     my $actor = $playersList->getByID($args->{ID});
  4433.     if (!defined $actor) {
  4434.         $actor = new Actor::Player();
  4435.         $actor->{ID} = $args->{ID};
  4436.         $actor->{nameID} = $ID;
  4437.         $actor->{appear_time} = time;
  4438.         $actor->{jobID} = $args->{jobID};
  4439.         $actor->{pos_to}{x} = $args->{coord_x};
  4440.         $actor->{pos_to}{y} = $args->{coord_y};
  4441.         $actor->{walk_speed} = 1; #hack
  4442.         $actor->{robe} = $args->{robe};
  4443.         $actor->{clothes_color} = $args->{clothes_color};
  4444.         $actor->{headgear}{low} = $args->{lowhead};
  4445.         $actor->{headgear}{mid} = $args->{midhead};
  4446.         $actor->{headgear}{top} = $args->{tophead};
  4447.         $actor->{weapon} = $args->{weapon};
  4448.         $actor->{shield} = $args->{shield};
  4449.         $actor->{sex} = $args->{sex};
  4450.         $actor->{hair_color} = $args->{hair_color} if (exists $args->{hair_color});
  4451.  
  4452.         $playersList->add($actor);
  4453.         Plugins::callHook('add_player_list', $actor);
  4454.     }
  4455. }
  4456.  
  4457. sub clone_vender_lost {
  4458.     my ($self, $args) = @_;
  4459.  
  4460.     my $ID = unpack("V", $args->{ID});
  4461.     binRemove(\@venderListsID, $ID);
  4462.     delete $venderLists{$ID};
  4463.  
  4464.     if (defined $playersList->getByID($args->{ID})) {
  4465.         my $player = $playersList->getByID($args->{ID});
  4466.  
  4467.         if (grep { $ID eq $_ } @venderListsID) {
  4468.             binRemove(\@venderListsID, $ID);
  4469.             delete $venderLists{$ID};
  4470.         }
  4471.  
  4472.         $player->{gone_time} = time;
  4473.         $players_old{$ID} = $player->deepCopy();
  4474.         Plugins::callHook('player_disappeared', {player => $player});
  4475.  
  4476.         $playersList->removeByID($args->{ID});
  4477.     }
  4478. }
  4479.  
  4480. sub remain_time_info {
  4481.     my ($self, $args) = @_;
  4482.     debug TF("Remain Time - Result: %s - Expiration Date: %s - Time: %s\n", $args->{result}, $args->{expiration_date}, $args->{remain_time}), "console", 1;
  4483. }
  4484.  
  4485. sub received_login_token {
  4486.     my ($self, $args) = @_;
  4487.  
  4488.     my $master = $masterServers{$config{master}};
  4489.  
  4490.     $messageSender->sendTokenToServer($config{username}, $config{password}, $master->{master_version}, $master->{version}, $args->{login_token}, $args->{len}, $master->{OTT_ip}, $master->{OTT_port});
  4491. }
  4492.  
  4493.  
  4494. # this info will be sent to xkore 2 clients
  4495. sub hotkeys {
  4496.     my ($self, $args) = @_;
  4497.     undef $hotkeyList;
  4498.     my $msg;
  4499.  
  4500.     # TODO: implement this: $hotkeyList->{rotate} = $args->{rotate} if $args->{rotate};
  4501.     $msg .= center(" " . T("Hotkeys") . " ", 79, '-') . "\n";
  4502.     $msg .= swrite(sprintf("\@%s \@%s \@%s \@%s", ('>'x3), ('<'x30), ('<'x5), ('>'x3)),
  4503.             ["#", T("Name"), T("Type"), T("Lv")]);
  4504.     $msg .= sprintf("%s\n", ('-'x79));
  4505.     my $j = 0;
  4506.     for (my $i = 0; $i < length($args->{hotkeys}); $i += 7) {
  4507.         @{$hotkeyList->[$j]}{qw(type ID lv)} = unpack('C V v', substr($args->{hotkeys}, $i, 7));
  4508.         $msg .= swrite(TF("\@%s \@%s \@%s \@%s", ('>'x3), ('<'x30), ('<'x5), ('>'x3)),
  4509.             [$j, $hotkeyList->[$j]->{type} ? Skill->new(idn => $hotkeyList->[$j]->{ID})->getName() : itemNameSimple($hotkeyList->[$j]->{ID}),
  4510.             $hotkeyList->[$j]->{type} ? T("skill") : T("item"),
  4511.             $hotkeyList->[$j]->{lv}]);
  4512.         $j++;
  4513.     }
  4514.     $msg .= sprintf("%s\n", ('-'x79));
  4515.     debug($msg, "list");
  4516. }
  4517.  
  4518. sub received_character_ID_and_Map {
  4519.     my ($self, $args) = @_;
  4520.     message T("Received character ID and Map IP from Character Server\n"), "connection";
  4521.     $net->setState(4);
  4522.     undef $conState_tries;
  4523.     $charID = $args->{charID};
  4524.  
  4525.     if ($net->version == 1) {
  4526.         undef $masterServer;
  4527.         $masterServer = $masterServers{$config{master}} if ($config{master} ne "");
  4528.     }
  4529.  
  4530.     my ($map) = $args->{mapName} =~ /([\s\S]*)\./; # cut off .gat
  4531.     my $map_noinstance;
  4532.     ($map_noinstance, undef) = Field::nameToBaseName(undef, $map); # Hack to clean up InstanceID
  4533.     if (!$field || $map ne $field->name()) {
  4534.         eval {
  4535.             $field = new Field(name => $map);
  4536.         };
  4537.         if (my $e = caught('FileNotFoundException', 'IOException')) {
  4538.             error TF("Cannot load field %s: %s\n", $map_noinstance, $e);
  4539.             undef $field;
  4540.         } elsif ($@) {
  4541.             die $@;
  4542.         }
  4543.     }
  4544.  
  4545.     $map_ip = makeIP($args->{mapIP});
  4546.     $map_ip = $masterServer->{ip} if ($masterServer && $masterServer->{private});
  4547.     $map_port = $args->{mapPort};
  4548.     message TF("----------Game Info----------\n" .
  4549.         "Char ID: %s (%s)\n" .
  4550.         "MAP Name: %s\n" .
  4551.         "MAP IP: %s\n" .
  4552.         "MAP Port: %s\n" .
  4553.         "-----------------------------\n", getHex($charID), unpack("V1", $charID),
  4554.         $args->{mapName}, $map_ip, $map_port), "connection";
  4555.     checkAllowedMap($map_noinstance);
  4556.     message(T("Closing connection to Character Server\n"), "connection") unless ($net->version == 1);
  4557.     $net->serverDisconnect(1);
  4558.     main::initStatVars();
  4559. }
  4560.  
  4561. sub received_sync {
  4562.     return unless changeToInGameState();
  4563.     debug "Received Sync\n", 'parseMsg', 2;
  4564.     $timeout{'play'}{'time'} = time;
  4565. }
  4566.  
  4567. sub actor_look_at {
  4568.     my ($self, $args) = @_;
  4569.     return unless changeToInGameState();
  4570.  
  4571.     my $actor = Actor::get($args->{ID});
  4572.     $actor->{look}{head} = $args->{head};
  4573.     $actor->{look}{body} = $args->{body};
  4574.     debug $actor->nameString . " looks at $args->{body}, $args->{head}\n", "parseMsg";
  4575. }
  4576.  
  4577. sub actor_movement_interrupted {
  4578.     my ($self, $args) = @_;
  4579.     return unless changeToInGameState();
  4580.     my %coords;
  4581.     $coords{x} = $args->{x};
  4582.     $coords{y} = $args->{y};
  4583.  
  4584.     my $actor = Actor::get($args->{ID});
  4585.     $actor->{pos} = {%coords};
  4586.     $actor->{pos_to} = {%coords};
  4587.     if ($actor->isa('Actor::You') || $actor->isa('Actor::Player')) {
  4588.         $actor->{sitting} = 0;
  4589.     }
  4590.     if ($actor->isa('Actor::You')) {
  4591.         debug "Movement interrupted, your coordinates: $coords{x}, $coords{y}\n", "parseMsg_move";
  4592.         AI::clear("move");
  4593.     }
  4594.     if ($char->{homunculus} && $char->{homunculus}{ID} eq $actor->{ID}) {
  4595.         AI::clear("move");
  4596.     }
  4597. }
  4598.  
  4599. sub actor_trapped {
  4600.     my ($self, $args) = @_;
  4601.     # original comment was that ID is not a valid ID
  4602.     # but it seems to be, at least on eAthena/Freya
  4603.     my $actor = Actor::get($args->{ID});
  4604.     debug "$actor->nameString() is trapped.\n";
  4605. }
  4606.  
  4607. sub party_join {
  4608.     my ($self, $args) = @_;
  4609.     return unless changeToInGameState();
  4610.     my $keys;
  4611.     my $info;
  4612.     if ($args->{switch} eq '0104') {  # DEFAULT OLD PACKET
  4613.         $keys = [qw(ID role x y type name user map)];
  4614.     } elsif ($args->{switch} eq '01E9') { # PACKETVER >= 2015
  4615.         $keys = [qw(ID role x y type name user map lv item_pickup item_share)];
  4616.  
  4617.     } elsif ($args->{switch} eq '0A43') { #  PACKETVER >= 2016
  4618.         $keys = [qw(ID role jobID lv x y type name user map item_pickup item_share)];
  4619.  
  4620.     } elsif ($args->{switch} eq '0AE4') { #  PACKETVER >= 2017
  4621.         $keys = [qw(ID charID role jobID lv x y type name user map item_pickup item_share)];
  4622.  
  4623.     } else { # this can't happen
  4624.         return;
  4625.     }
  4626.    
  4627.     @{$info}{@{$keys}} = @{$args}{@{$keys}};
  4628.  
  4629.     if (!$char->{party}{joined} || !$char->{party}{users}{$info->{ID}} || !%{$char->{party}{users}{$info->{ID}}}) {
  4630.         binAdd(\@partyUsersID, $info->{ID}) if (binFind(\@partyUsersID, $info->{ID}) eq "");
  4631.         if ($info->{ID} eq $accountID) {
  4632.             message TF("You joined party '%s'\n", $info->{name}), undef, 1;
  4633.             # Some servers receive party_users_info before party_join when logging in
  4634.             # This is to prevent clearing info already in $char->{party}
  4635.             $char->{party} = {} unless ref($char->{party}) eq "HASH";
  4636.             $char->{party}{joined} = 1;
  4637.             Plugins::callHook('packet_partyJoin', { partyName => $info->{name} });
  4638.         } else {
  4639.             message TF("%s joined your party '%s'\n", $info->{user}, $info->{name}), undef, 1;
  4640.         }
  4641.     }
  4642.  
  4643.     my $actor = $char->{party}{users}{$info->{ID}} && %{$char->{party}{users}{$info->{ID}}} ? $char->{party}{users}{$info->{ID}} : new Actor::Party;
  4644.  
  4645.     $actor->{admin} = !$info->{'role'};
  4646.     delete $actor->{statuses} unless $actor->{'online'} = !$info-{'type'};
  4647.     $actor->{pos}{x} = $info->{'x'};
  4648.     $actor->{pos}{y} = $info->{'y'};
  4649.     $actor->{map} = $info->{'map'};
  4650.     $actor->{name} = $info->{'user'};
  4651.     $actor->{ID} = $info->{'ID'};
  4652.     $actor->{lv} = $info->{'lv'} if $info->{'lv'};
  4653.     $actor->{jobID} = $info->{'jobID'} if $info->{'jobID'};
  4654.     $actor->{charID} = $info->{'charID'} if $info->{'charID'}; # why now use charID?
  4655.     $char->{party}{users}{$info->{'ID'}} = $actor;
  4656.     $char->{party}{name} = $info->{'name'};
  4657.     $char->{party}{itemPickup} = $info->{'item_pickup'};
  4658.     $char->{party}{itemDivision} = $info->{'item_share'};
  4659. }
  4660.  
  4661. # TODO: store this state
  4662. sub party_allow_invite {
  4663.    my ($self, $args) = @_;
  4664.  
  4665.    if ($args->{type}) {
  4666.       message T("Not allowed other player invite to Party\n"), "party", 1;
  4667.    } else {
  4668.       message T("Allowed other player invite to Party\n"), "party", 1;
  4669.    }
  4670. }
  4671.  
  4672. sub party_chat {
  4673.     my ($self, $args) = @_;
  4674.     my $msg = $args->{message};
  4675.  
  4676.     # Type: String
  4677.     my ($chatMsgUser, $chatMsg) = $msg =~ /(.*?) : (.*)/;
  4678.     $chatMsgUser =~ s/ $//;
  4679.  
  4680.     stripLanguageCode(\$chatMsg);
  4681.     # Type: String
  4682.     my $chat = "$chatMsgUser : $chatMsg";
  4683.     message TF("[Party] %s\n", $chat), "partychat";
  4684.  
  4685.     chatLog("p", "$chat\n") if ($config{'logPartyChat'});
  4686.     ChatQueue::add('p', $args->{ID}, $chatMsgUser, $chatMsg);
  4687.  
  4688.     Plugins::callHook('packet_partyMsg', {
  4689.         MsgUser => $chatMsgUser,
  4690.         Msg => $chatMsg
  4691.     });
  4692. }
  4693.  
  4694. sub party_exp {
  4695.     my ($self, $args) = @_;
  4696.     $char->{party}{share} = $args->{type}; # Always will be there, in 0101 also in 07D8
  4697.     if ($args->{type} == 0) {
  4698.         message T("Party EXP set to Individual Take\n"), "party", 1;
  4699.     } elsif ($args->{type} == 1) {
  4700.         message T("Party EXP set to Even Share\n"), "party", 1;
  4701.     } else {
  4702.         error T("Error setting party option\n");
  4703.     }
  4704.     if(exists($args->{itemPickup}) || exists($args->{itemDivision})) {
  4705.         $char->{party}{itemPickup} = $args->{itemPickup};
  4706.         $char->{party}{itemDivision} = $args->{itemDivision};
  4707.         if ($args->{itemPickup} == 0) {
  4708.             message T("Party item set to Individual Take\n"), "party", 1;
  4709.         } elsif ($args->{itemPickup} == 1) {
  4710.             message T("Party item set to Even Share\n"), "party", 1;
  4711.         } else {
  4712.             error T("Error setting party option\n");
  4713.         }
  4714.         if ($args->{itemDivision} == 0) {
  4715.             message T("Party item division set to Individual Take\n"), "party", 1;
  4716.         } elsif ($args->{itemDivision} == 1) {
  4717.             message T("Party item division set to Even Share\n"), "party", 1;
  4718.         } else {
  4719.             error T("Error setting party option\n");
  4720.         }
  4721.     }
  4722. }
  4723.  
  4724. sub party_leader {
  4725.     my ($self, $args) = @_;
  4726.     for (my $i = 0; $i < @partyUsersID; $i++) {
  4727.         if (unpack("V",$partyUsersID[$i]) eq $args->{new}) {
  4728.             $char->{party}{users}{$partyUsersID[$i]}{admin} = 1;
  4729.             message TF("New party leader: %s\n", $char->{party}{users}{$partyUsersID[$i]}{name}), "party", 1;
  4730.         }
  4731.         if (unpack("V",$partyUsersID[$i]) eq $args->{old}) {
  4732.             $char->{party}{users}{$partyUsersID[$i]}{admin} = '';
  4733.         }
  4734.     }
  4735. }
  4736.  
  4737. sub party_hp_info {
  4738.     my ($self, $args) = @_;
  4739.     my $ID = $args->{ID};
  4740.  
  4741.     if ($char->{party}{users}{$ID}) {
  4742.         $char->{party}{users}{$ID}{hp} = $args->{hp};
  4743.         $char->{party}{users}{$ID}{hp_max} = $args->{hp_max};
  4744.     }
  4745. }
  4746.  
  4747. sub party_invite {
  4748.     my ($self, $args) = @_;
  4749.     message TF("Incoming Request to join party '%s'\n", bytesToString($args->{name}));
  4750.     $incomingParty{ID} = $args->{ID};
  4751.     $incomingParty{ACK} = $args->{switch} eq '02C6' ? '02C7' : '00FF';
  4752.     $timeout{ai_partyAutoDeny}{time} = time;
  4753. }
  4754.  
  4755. sub party_invite_result {
  4756.     my ($self, $args) = @_;
  4757.     my $name = bytesToString($args->{name});
  4758.     if ($args->{type} == ANSWER_ALREADY_OTHERGROUPM) {
  4759.         warning TF("Join request failed: %s is already in a party\n", $name);
  4760.     } elsif ($args->{type} == ANSWER_JOIN_REFUSE) {
  4761.         warning TF("Join request failed: %s denied request\n", $name);
  4762.     } elsif ($args->{type} == ANSWER_JOIN_ACCEPT) {
  4763.         message TF("%s accepted your request\n", $name), "info";
  4764.     } elsif ($args->{type} == ANSWER_MEMBER_OVERSIZE) {
  4765.         message T("Join request failed: Party is full.\n"), "info";
  4766.     } elsif ($args->{type} == ANSWER_DUPLICATE) {
  4767.         message TF("Join request failed: same account of %s allready joined the party.\n", $name), "info";
  4768.     } elsif ($args->{type} == ANSWER_JOINMSG_REFUSE) {
  4769.         message TF("Join request failed: ANSWER_JOINMSG_REFUSE.\n", $name), "info";
  4770.     } elsif ($args->{type} == ANSWER_UNKNOWN_ERROR) {
  4771.         message TF("Join request failed: unknown error.\n", $name), "info";
  4772.     } elsif ($args->{type} == ANSWER_UNKNOWN_CHARACTER) {
  4773.         message TF("Join request failed: the character is not currently online or does not exist.\n", $name), "info";
  4774.     } elsif ($args->{type} == ANSWER_INVALID_MAPPROPERTY) {
  4775.         message TF("Join request failed: ANSWER_INVALID_MAPPROPERTY.\n", $name), "info";
  4776.     }
  4777. }
  4778.  
  4779. sub party_leave {
  4780.     my ($self, $args) = @_;
  4781.  
  4782.     my $ID = $args->{ID};
  4783.     my $actor = $char->{party}{users}{$ID}; # bytesToString($args->{name})
  4784.     delete $char->{party}{users}{$ID};
  4785.     binRemove(\@partyUsersID, $ID);
  4786.     if ($ID eq $accountID) {
  4787.         $actor = $char;
  4788.         delete $char->{party};
  4789.         undef @partyUsersID;
  4790.         $char->{party}{joined} = 0;
  4791.     }
  4792.  
  4793.     if ($args->{result} == GROUPMEMBER_DELETE_LEAVE) {
  4794.         message TF("%s left the party\n", $actor);
  4795.     } elsif ($args->{result} == GROUPMEMBER_DELETE_EXPEL) {
  4796.         message TF("%s left the party (kicked)\n", $actor);
  4797.     } else {
  4798.         message TF("%s left the party (unknown reason: %d)\n", $actor, $args->{result});
  4799.     }
  4800. }
  4801.  
  4802. sub party_location {
  4803.     my ($self, $args) = @_;
  4804.  
  4805.     my $ID = $args->{ID};
  4806.  
  4807.     if ($char->{party}{users}{$ID}) {
  4808.         $char->{party}{users}{$ID}{pos}{x} = $args->{x};
  4809.         $char->{party}{users}{$ID}{pos}{y} = $args->{y};
  4810.         $char->{party}{users}{$ID}{online} = 1;
  4811.         debug "Party member location: $char->{party}{users}{$ID}{name} - $args->{x}, $args->{y}\n", "parseMsg";
  4812.     }
  4813. }
  4814. sub party_organize_result {
  4815.     my ($self, $args) = @_;
  4816.  
  4817.     unless ($args->{fail}) {
  4818.         $char->{party}{users}{$accountID}{admin} = 1 if $char->{party}{users}{$accountID};
  4819.     } elsif ($args->{fail} == 1) {
  4820.         warning T("Can't organize party - party name exists\n");
  4821.     } elsif ($args->{fail} == 2) {
  4822.         warning T("Can't organize party - you are already in a party\n");
  4823.     } elsif ($args->{fail} == 3) {
  4824.         warning T("Can't organize party - not allowed in current map\n");
  4825.     } else {
  4826.         warning TF("Can't organize party - unknown (%d)\n", $args->{fail});
  4827.     }
  4828. }
  4829.  
  4830. sub party_show_picker {
  4831.     my ($self, $args) = @_;
  4832.  
  4833.     # wtf the server sends this packet for your own character? (rRo)
  4834.     return if $args->{sourceID} eq $accountID;
  4835.  
  4836.     my $string = ($char->{party}{users}{$args->{sourceID}} && %{$char->{party}{users}{$args->{sourceID}}}) ? $char->{party}{users}{$args->{sourceID}}->name() : $args->{sourceID};
  4837.     my $item = {};
  4838.     $item->{nameID} = $args->{nameID};
  4839.     $item->{identified} = $args->{identified};
  4840.     $item->{upgrade} = $args->{upgrade};
  4841.     $item->{cards} = $args->{cards};
  4842.     $item->{broken} = $args->{broken};
  4843.     message TF("Party member %s has picked up item %s.\n", $string, itemName($item)), "info";
  4844. }
  4845.  
  4846. sub party_users_info {
  4847.     my ($self, $args) = @_;
  4848.     return unless changeToInGameState();
  4849.  
  4850.     my $player_info;
  4851.  
  4852.     if ($args->{switch} eq '00FB') {  # DEFAULT OLD PACKET
  4853.         $player_info = {
  4854.             len => 46,
  4855.             types => 'V Z24 Z16 C2',
  4856.             keys => [qw(ID name map admin online)],
  4857.         };
  4858.  
  4859.     } elsif ($args->{switch} eq '0A44') { # PACKETVER >= 20151007
  4860.         $player_info = {
  4861.             len => 50,
  4862.             types => 'V Z24 Z16 C2 v2',
  4863.             keys => [qw(ID name map admin online jobID lv)],
  4864.         };
  4865.  
  4866.     } elsif ($args->{switch} eq '0AE5') { #  PACKETVER >= 20171207
  4867.         $player_info = {
  4868.             len => 54,
  4869.             types => 'V V Z24 Z16 C2 v2',
  4870.             keys => [qw(ID GID name map admin online jobID lv)],
  4871.         };
  4872.  
  4873.     } else { # this can't happen
  4874.         return;
  4875.     }
  4876.  
  4877.     $char->{party}{name} = bytesToString($args->{party_name});
  4878.  
  4879.     for (my $i = 0; $i < length($args->{playerInfo}); $i += $player_info->{len}) {
  4880.         # in 0a43 lasts bytes: { <item pickup rule>.B <item share rule>.B <unknown>.L }
  4881.         return if(length($args->{playerInfo}) - $i == 6);
  4882.  
  4883.         my $ID = substr($args->{playerInfo}, $i, 4);
  4884.  
  4885.         if (binFind(\@partyUsersID, $ID) eq "") {
  4886.             binAdd(\@partyUsersID, $ID);
  4887.         }
  4888.  
  4889.         $char->{party}{users}{$ID} = new Actor::Party();
  4890.         @{$char->{party}{users}{$ID}}{@{$player_info->{keys}}} = unpack($player_info->{types}, substr($args->{playerInfo}, $i, $player_info->{len}));
  4891.         $char->{party}{users}{$ID}{name} = bytesToString($char->{party}{users}{$ID}{name});
  4892.         $char->{party}{users}{$ID}{admin} = !$char->{party}{users}{$ID}{admin};
  4893.         $char->{party}{users}{$ID}{online} = !$char->{party}{users}{$ID}{online};
  4894.  
  4895.         debug TF("Party Member: %s (%s)\n", $char->{party}{users}{$ID}{name}, $char->{party}{users}{$ID}{map}), "party", 1;
  4896.     }
  4897. }
  4898.  
  4899. sub rodex_mail_list {
  4900.     my ( $self, $args ) = @_;
  4901.    
  4902.     my $msg = $args->{RAW_MSG};
  4903.     my $msg_size = $args->{RAW_MSG_SIZE};
  4904.     my $header_pack = 'v C C C';
  4905.     my $header_len = ((length pack $header_pack) + 2);
  4906.    
  4907.     my $mail_pack = 'V2 C C Z24 V V v';
  4908.     my $base_mail_len = length pack $mail_pack;
  4909.    
  4910.     if ($args->{switch} eq '0A7D') {
  4911.         $rodexList->{current_page} = 0;
  4912.         $rodexList = {};
  4913.         $rodexList->{mails} = {};
  4914.     } else {
  4915.         $rodexList->{current_page}++;
  4916.     }
  4917.    
  4918.     if ($args->{isEnd} == 1) {
  4919.         $rodexList->{last_page} = $rodexList->{current_page};
  4920.     } else {
  4921.         $rodexList->{mails_per_page} = $args->{amount};
  4922.     }
  4923.    
  4924.     my $mail_len;
  4925.    
  4926.     my $print_msg = center(" " . "Rodex Mail Page ". $rodexList->{current_page} . " ", 79, '-') . "\n";
  4927.    
  4928.     my $index = 0;
  4929.     for (my $i = $header_len; $i < $args->{RAW_MSG_SIZE}; $i+=$mail_len) {
  4930.         my $mail;
  4931.  
  4932.         ($mail->{mailID1},
  4933.         $mail->{mailID2},
  4934.         $mail->{isRead},
  4935.         $mail->{type},
  4936.         $mail->{sender},
  4937.         $mail->{regDateTime},
  4938.         $mail->{expireDateTime},
  4939.         $mail->{Titlelength}) = unpack($mail_pack, substr($msg, $i, $base_mail_len));
  4940.        
  4941.         $mail->{title} = substr($msg, ($i+$base_mail_len), $mail->{Titlelength});
  4942.        
  4943.         $mail->{page} = $rodexList->{current_page};
  4944.         $mail->{page_index} = $index;
  4945.        
  4946.         $mail_len = $base_mail_len + $mail->{Titlelength};
  4947.        
  4948.         $rodexList->{mails}{$mail->{mailID1}} = $mail;
  4949.        
  4950.         $rodexList->{current_page_last_mailID} = $mail->{mailID1};
  4951.        
  4952.         $print_msg .= swrite("@<<< @<<<<< @<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<< @<<< @<<< @<<<<<<<< @<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<", [$index, "From:", $mail->{sender}, "Read:", $mail->{isRead} ? "Yes" : "No", "ID:", $mail->{mailID1}, "Title:", $mail->{title}]);
  4953.        
  4954.         $index++;
  4955.     }
  4956.     $print_msg .= sprintf("%s\n", ('-'x79));
  4957.     message $print_msg, "list";
  4958. }
  4959.  
  4960. sub rodex_read_mail {
  4961.     my ( $self, $args ) = @_;
  4962.    
  4963.     my $msg = $args->{RAW_MSG};
  4964.     my $msg_size = $args->{RAW_MSG_SIZE};
  4965.     my $header_pack = 'v C V2 v V2 C';
  4966.     my $header_len = ((length pack $header_pack) + 2);
  4967.    
  4968.     my $mail = {};
  4969.    
  4970.     $mail->{body} = substr($msg, $header_len, $args->{text_len});
  4971.     $mail->{zeny1} = $args->{zeny1};
  4972.     $mail->{zeny2} = $args->{zeny2};
  4973.    
  4974.     my $item_pack = 'v2 C3 a8 a4 C a4 a25';
  4975.     my $item_len = length pack $item_pack;
  4976.    
  4977.     my $mail_len;
  4978.    
  4979.     $mail->{items} = [];
  4980.    
  4981.     my $print_msg = center(" " . "Mail ".$args->{mailID1} . " ", 79, '-') . "\n";
  4982.    
  4983.     my @message_parts = unpack("(A51)*", $mail->{body});
  4984.    
  4985.     $print_msg .= swrite("@<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", ["Message:", $message_parts[0]]);
  4986.    
  4987.     foreach my $part (@message_parts[1..$#message_parts]) {
  4988.         $print_msg .= swrite("@<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", ["", $part]);
  4989.     }
  4990.    
  4991.     $print_msg .= swrite("@<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", ["Item count:", $args->{itemCount}]);
  4992.    
  4993.     $print_msg .= swrite("@<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", ["Zeny:", $args->{zeny1}]);
  4994.  
  4995.     my $index = 0;
  4996.     for (my $i = ($header_len + $args->{text_len}); $i < $args->{RAW_MSG_SIZE}; $i += $item_len) {
  4997.         my $item;
  4998.         ($item->{amount},
  4999.         $item->{nameID},
  5000.         $item->{identified},
  5001.         $item->{broken},
  5002.         $item->{upgrade},
  5003.         $item->{cards},
  5004.         $item->{unknow1},
  5005.         $item->{type},
  5006.         $item->{unknow2},
  5007.         $item->{options}) = unpack($item_pack, substr($msg, $i, $item_len));
  5008.        
  5009.         $item->{name} = itemName($item);
  5010.        
  5011.         my $display = $item->{name};
  5012.         $display .= " x $item->{amount}";
  5013.        
  5014.         $print_msg .= swrite("@<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", [$index, $display]);
  5015.        
  5016.         push(@{$mail->{items}}, $item);
  5017.         $index++;
  5018.     }
  5019.    
  5020.     $print_msg .= sprintf("%s\n", ('-'x79));
  5021.     message $print_msg, "list";
  5022.    
  5023.     @{$rodexList->{mails}{$args->{mailID1}}}{qw(body items zeny1 zeny2)} = @{$mail}{qw(body items zeny1 zeny2)};
  5024.    
  5025.     $rodexList->{mails}{$args->{mailID1}}{isRead} = 1;
  5026.    
  5027.     $rodexList->{current_read} = $args->{mailID1};
  5028. }
  5029.  
  5030. sub unread_rodex {
  5031.     my ( $self, $args ) = @_;
  5032.     message "You have new unread rodex mails.\n";
  5033. }
  5034.  
  5035. sub rodex_remove_item {
  5036.     my ( $self, $args ) = @_;
  5037.    
  5038.     if (!$args->{result}) {
  5039.         error "You failed to remove an item from rodex mail.\n";
  5040.         return;
  5041.     }
  5042.    
  5043.     my $rodex_item = $rodexWrite->{items}->getByID($args->{ID});
  5044.    
  5045.     my $disp = TF("Item removed from rodex mail message: %s (%d) x %d - %s",
  5046.             $rodex_item->{name}, $rodex_item->{binID}, $args->{amount}, $itemTypes_lut{$rodex_item->{type}});
  5047.     message "$disp\n", "drop";
  5048.    
  5049.     $rodex_item->{amount} -= $args->{amount};
  5050.     if ($rodex_item->{amount} <= 0) {
  5051.         $rodexWrite->{items}->remove($rodex_item);
  5052.     }
  5053. }
  5054.  
  5055. sub rodex_add_item {
  5056.     my ( $self, $args ) = @_;
  5057.    
  5058.     if ($args->{fail}) {
  5059.         error "You failed to add an item to rodex mail.\n";
  5060.         return;
  5061.     }
  5062.    
  5063.     my $rodex_item = $rodexWrite->{items}->getByID($args->{ID});
  5064.    
  5065.     if ($rodex_item) {
  5066.         $rodex_item->{amount} += $args->{amount};
  5067.     } else {
  5068.         $rodex_item = new Actor::Item();
  5069.         $rodex_item->{ID} = $args->{ID};
  5070.         $rodex_item->{nameID} = $args->{nameID};
  5071.         $rodex_item->{type} = $args->{type};
  5072.         $rodex_item->{amount} = $args->{amount};
  5073.         $rodex_item->{identified} = $args->{identified};
  5074.         $rodex_item->{broken} = $args->{broken};
  5075.         $rodex_item->{upgrade} = $args->{upgrade};
  5076.         $rodex_item->{cards} = $args->{cards};
  5077.         $rodex_item->{options} = $args->{options};
  5078.         $rodex_item->{name} = itemName($rodex_item);
  5079.  
  5080.         $rodexWrite->{items}->add($rodex_item);
  5081.     }
  5082.    
  5083.     my $disp = TF("Item added to rodex mail message: %s (%d) x %d - %s",
  5084.             $rodex_item->{name}, $rodex_item->{binID}, $args->{amount}, $itemTypes_lut{$rodex_item->{type}});
  5085.     message "$disp\n", "drop";
  5086. }
  5087.  
  5088. sub rodex_open_write {
  5089.     my ( $self, $args ) = @_;
  5090.    
  5091.     $rodexWrite = {};
  5092.    
  5093.     $rodexWrite->{items} = new InventoryList;
  5094.    
  5095. }
  5096.  
  5097. sub rodex_check_player {
  5098.     my ( $self, $args ) = @_;
  5099.    
  5100.     if (!$args->{char_id}) {
  5101.         error "Could not find player with name '".$args->{name}."'.";
  5102.         return;
  5103.     }
  5104.    
  5105.     my $print_msg = center(" " . "Rodex Mail Target" . " ", 79, '-') . "\n";
  5106.    
  5107.     $print_msg .= swrite("@<<<<< @<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<< @<<< @<<<<<< @<<<<<<<<<<<<<<< @<<<<<<<< @<<<<<<<<<", ["Name:", $args->{name}, "Base Level:", $args->{base_level}, "Class:", $args->{class}, "Char ID:", $args->{char_id}]);
  5108.    
  5109.     $print_msg .= sprintf("%s\n", ('-'x79));
  5110.     message $print_msg, "list";
  5111.    
  5112.     @{$rodexWrite->{target}}{qw(name base_level class char_id)} = @{$args}{qw(name base_level class char_id)};
  5113. }
  5114.  
  5115. sub rodex_write_result {
  5116.     my ( $self, $args ) = @_;
  5117.    
  5118.     if ($args->{fail}) {
  5119.         error "You failed to send the rodex mail.\n";
  5120.         return;
  5121.     }
  5122.    
  5123.     message "Your rodex mail was sent with success.\n";
  5124.     undef $rodexWrite;
  5125. }
  5126.  
  5127. sub rodex_get_zeny {
  5128.     my ( $self, $args ) = @_;
  5129.    
  5130.     if ($args->{fail}) {
  5131.         error "You failed to get the zeny of the rodex mail.\n";
  5132.         return;
  5133.     }
  5134.    
  5135.     message "The zeny of the rodex mail was requested with success.\n";
  5136.    
  5137.     $rodexList->{mails}{$args->{mailID1}}{zeny1} = 0;
  5138. }
  5139.  
  5140. sub rodex_get_item {
  5141.     my ( $self, $args ) = @_;
  5142.    
  5143.     if ($args->{fail}) {
  5144.         error "You failed to get the items of the rodex mail.\n";
  5145.         return;
  5146.     }
  5147.    
  5148.     message "The items of the rodex mail were requested with success.\n";
  5149.    
  5150.     $rodexList->{mails}{$args->{mailID1}}{items} = [];
  5151. }
  5152.  
  5153. sub rodex_delete {
  5154.     my ( $self, $args ) = @_;
  5155.    
  5156.     return unless (exists $rodexList->{mails}{$args->{mailID1}});
  5157.    
  5158.     message "You have deleted the mail of ID ".$args->{mailID1}.".\n";
  5159.    
  5160.     delete $rodexList->{mails}{$args->{mailID1}};
  5161. }
  5162.  
  5163. # 0x803
  5164. sub booking_register_request {
  5165.     my ($self, $args) = @_;
  5166.     my $result = $args->{result};
  5167.  
  5168.     if ($result == 0) {
  5169.         message T("Booking successfully created!\n"), "booking";
  5170.     } elsif ($result == 2) {
  5171.         error T("You already got a reservation group active!\n"), "booking";
  5172.     } else {
  5173.         error TF("Unknown error in creating the group booking (Error %s)\n", $result), "booking";
  5174.     }
  5175. }
  5176.  
  5177. # 0x805
  5178. sub booking_search_request {
  5179.     my ($self, $args) = @_;
  5180.  
  5181.     if (length($args->{innerData}) == 0) {
  5182.         error T("Without results!\n"), "booking";
  5183.         return;
  5184.     }
  5185.  
  5186.     message T("-------------- Booking Search ---------------\n");
  5187.     for (my $offset = 0; $offset < length($args->{innerData}); $offset += 48) {
  5188.         my ($index, $charName, $expireTime, $level, $mapID, @job) = unpack("V Z24 V s8", substr($args->{innerData}, $offset, 48));
  5189.         message swrite(
  5190.             T("Name: \@<<<<<<<<<<<<<<<<<<<<<<<< Index: \@>>>>\n" .
  5191.             "Created: \@<<<<<<<<<<<<<<<<<<<<<   Level: \@>>>\n" .
  5192.             "MapID: \@<<<<<\n".
  5193.             "Job: \@<<<< \@<<<< \@<<<< \@<<<< \@<<<<\n" .
  5194.             "---------------------------------------------"),
  5195.             [bytesToString($charName), $index, getFormattedDate($expireTime), $level, $mapID, @job]), "booking";
  5196.     }
  5197. }
  5198.  
  5199. # 0x807
  5200. sub booking_delete_request {
  5201.     my ($self, $args) = @_;
  5202.     my $result = $args->{result};
  5203.  
  5204.     if ($result == 0) {
  5205.         message T("Reserve deleted successfully!\n"), "booking";
  5206.     } elsif ($result == 3) {
  5207.         error T("You're not with a group booking active!\n"), "booking";
  5208.     } else {
  5209.         error TF("Unknown error in deletion of group booking (Error %s)\n", $result), "booking";
  5210.     }
  5211. }
  5212.  
  5213. # 0x809
  5214. sub booking_insert {
  5215.     my ($self, $args) = @_;
  5216.  
  5217.     message TF("%s has created a new group booking (index: %s)\n", bytesToString($args->{name}), $args->{ID});
  5218. }
  5219.  
  5220. # 0x80A
  5221. sub booking_update {
  5222.     my ($self, $args) = @_;
  5223.  
  5224.     message TF("Reserve index of %s has changed its settings\n", $args->{ID});
  5225. }
  5226.  
  5227. # 0x80B
  5228. sub booking_delete {
  5229.     my ($self, $args) = @_;
  5230.  
  5231.     message TF("Deleted reserve group index %s\n", $args->{ID});
  5232. }
  5233.  
  5234.  
  5235. sub clan_user {
  5236.     my ($self, $args) = @_;
  5237.     foreach (qw(onlineuser totalmembers)) {
  5238.         $clan{$_} = $args->{$_};
  5239.     }  
  5240.     $clan{onlineuser} = $args->{onlineuser};
  5241.     $clan{totalmembers} = $args->{totalmembers};
  5242. }
  5243.  
  5244. sub clan_info {
  5245.     my ($self, $args) = @_;
  5246.     foreach (qw(clan_ID clan_name clan_master clan_map alliance_count antagonist_count)) {
  5247.         $clan{$_} = $args->{$_};
  5248.     }
  5249.  
  5250.     $clan{clan_name} = bytesToString($args->{clan_name});
  5251.     $clan{clan_master} = bytesToString($args->{clan_master});
  5252.     $clan{clan_map} = bytesToString($args->{clan_map});
  5253.    
  5254.     my $i = 0;
  5255.     my $count = 0;
  5256.     $clan{ally_names} = "";
  5257.     $clan{antagonist_names} = "";
  5258.  
  5259.     if($args->{alliance_count} > 0) {
  5260.         for ($count; $count < $args->{alliance_count}; $count++) {
  5261.             $clan{ally_names} .= bytesToString(unpack("Z24", substr($args->{ally_antagonist_names}, $i, 24))).", ";
  5262.             $i += 24;
  5263.         }
  5264.     }
  5265.  
  5266.     $count = 0;
  5267.     if($args->{antagonist_count} > 0) {
  5268.         for ($count; $count < $args->{antagonist_count}; $count++) {
  5269.             $clan{antagonist_names} .= bytesToString(unpack("Z24", substr($args->{ally_antagonist_names}, $i, 24))).", ";
  5270.             $i += 24;
  5271.         }
  5272.     }
  5273. }
  5274.  
  5275. sub clan_chat {
  5276.     my ($self, $args) = @_;
  5277.     my ($chatMsgUser, $chatMsg); # Type: String
  5278.  
  5279.     return unless changeToInGameState();
  5280.     $chatMsgUser = bytesToString($args->{charname});
  5281.     $chatMsg = bytesToString($args->{message});
  5282.  
  5283.     chatLog("clan", "$chatMsgUser : $chatMsg\n") if ($config{'logClanChat'});
  5284.     # Translation Comment: Guild Chat
  5285.     message TF("[Clan]%s %s\n", $chatMsgUser, $chatMsg), "clanchat";
  5286.     # Only queue this if it's a real chat message
  5287.     ChatQueue::add('clan', 0, $chatMsgUser, $chatMsg) if ($chatMsgUser);
  5288.  
  5289.     Plugins::callHook('packet_clanMsg', {
  5290.         MsgUser => $chatMsgUser,
  5291.         Msg => $chatMsg
  5292.     });
  5293. }
  5294.  
  5295. sub clan_leave {
  5296.     my ($self, $args) = @_;
  5297.    
  5298.     if($clan{clan_name}) {
  5299.         message TF("[Clan] You leaved $clan{clan_name}");
  5300.         undef %clan;
  5301.     }
  5302. }
  5303.  
  5304.  
  5305. sub change_title {
  5306.     my ($self, $args) = @_;
  5307.     #TODO : <result>.B
  5308.     message TF("You changed Title_ID :  %s.\n", $args->{title_id}), "info";
  5309. }
  5310.  
  5311.  
  5312. sub pet_evolution_result {
  5313.     my ($self, $args) = @_;
  5314.     if ($args->{result} == 0x0) {
  5315.         error TF("Pet evolution error.\n");
  5316.     #PET_EVOL_NO_CALLPET = 0x1,
  5317.     #PET_EVOL_NO_PETEGG = 0x2,
  5318.     } elsif ($args->{result} == 0x3) {
  5319.         error TF("Unequip pet accessories first to start evolution.\n");
  5320.     } elsif ($args->{result} == 0x4) {
  5321.         error TF("Insufficient materials for evolution.\n");
  5322.     } elsif ($args->{result} == 0x5) { 
  5323.         error TF("Loyal Intimacy is required to evolve.\n");
  5324.     } elsif ($args->{result} == 0x6) {
  5325.         message TF("Pet evolution success.\n"), "success";
  5326.     }
  5327. }
  5328.  
  5329. 1;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement