· 8 years ago · Jun 13, 2018, 01:50 PM
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>
20package Network::Receive;
21
22use strict;
23use Exporter;
24use Network::PacketParser; # import
25use base qw(Network::PacketParser);
26use utf8;
27use Carp::Assert;
28use Scalar::Util;
29use Socket qw(inet_aton inet_ntoa);
30
31use AI;
32use Globals;
33use Field;
34#use Settings;
35use Log qw(message warning error debug);
36use FileParsers qw(updateMonsterLUT updateNPCLUT);
37use I18N qw(bytesToString stringToBytes);
38use Interface;
39use Network;
40use Network::MessageTokenizer;
41use Misc;
42use Plugins;
43use Utils;
44use Utils::Exceptions;
45use Utils::Crypton;
46use Translation;
47
48our %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
73our @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
81use 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
95use 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
165use 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
178use 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.
188sub 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.
216sub 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.
240sub 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
251sub 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.
278sub 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.
287sub 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
314sub 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
366sub 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
394sub 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
465sub 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;
475sub 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...
484sub 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
771typedef 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
967sub 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
1116sub 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
1233sub 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
1317use 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
1324sub 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
1338sub 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
1358sub reconstruct_minimap_indicator {
1359}
1360
1361use 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?
1371sub 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.
1404sub 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
1433sub parse_npc_image {
1434 my ($self, $args) = @_;
1435
1436 $args->{npc_image} = bytesToString($args->{npc_image});
1437}
1438
1439sub reconstruct_npc_image {
1440 my ($self, $args) = @_;
1441
1442 $args->{npc_image} = stringToBytes($args->{npc_image});
1443}
1444
1445sub 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
1463sub 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
1476sub 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
1486sub 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.
1500sub 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
1534sub 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
1596sub 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
1605sub 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
1615sub 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
1630sub 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
1666sub 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
1708sub 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
1732sub 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
1782sub 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
1799sub 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
1816sub 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
1849sub cart_off {
1850 $char->cart->close;
1851 message T("Cart released.\n"), "success";
1852}
1853
1854# 012D
1855sub 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#
1865sub 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
1912sub 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)
1965sub 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
2010sub 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
2025sub 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".
2037sub marriage_partner_name {
2038 my ($self, $args) = @_;
2039
2040 message TF("Marriage partner name: %s\n", $args->{name});
2041}
2042
2043sub 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
2127sub 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
2149sub 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
2171sub 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
2199sub 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
2242sub 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
2284sub 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
2314sub 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
2353sub 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
2363sub 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
2387sub 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
2421sub 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
2435sub progress_bar_stop {
2436 my($self, $args) = @_;
2437 message TF("Progress bar finished.\n", 'info');
2438}
2439
2440# 02B1
2441sub 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
2453sub 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
2482sub 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
2517sub 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
2524sub 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
2533sub 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
2538sub 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
2545sub 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
2552sub 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
2571sub 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
2584sub parse_npc_chat {
2585 my ($self, $args) = @_;
2586
2587 $args->{actor} = Actor::get($args->{ID});
2588}
2589
2590sub 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
2628sub 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
2643sub storage_opened {
2644 my ($self, $args) = @_;
2645 $char->storage->open($args);
2646}
2647
2648sub 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
2662sub 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
2684sub 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
2699sub 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
2730sub 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
2742sub 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
2755sub 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
2768sub 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
2798sub 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
2810sub cart_info {
2811 my ($self, $args) = @_;
2812 $char->cart->info($args);
2813 debug "[cart_info] received.\n", "parseMsg";
2814}
2815
2816sub 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
2830sub 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
2852sub 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
2912sub 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
2921sub 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
2941sub 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
2952sub 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
2984sub 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
2992sub 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
3010sub 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
3024sub 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
3051sub 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
3061sub 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
3092sub 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
3119sub 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
3131sub 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
3150sub 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
3162sub 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
3183sub 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
3219sub 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
3227sub 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
3235sub 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
3250sub 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
3262sub 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
3281sub 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
3294sub 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
3359sub 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
3439sub 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
3460sub 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
3471sub 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
3487sub 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
3506sub 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?
3522sub 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
3537sub 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
3555sub 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
3584sub 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
3596sub 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
3614sub 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
3630sub 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
3641sub 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
3659sub 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
3669sub 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
3676sub 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
3683sub 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
3698sub 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
3708sub 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
3721sub 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
3738sub 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
3750sub 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
3764sub 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
3776sub 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
3787sub 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
3834sub married {
3835 my ($self, $args) = @_;
3836
3837 my $actor = Actor::get($args->{ID});
3838 message TF("%s got married!\n", $actor);
3839}
3840
3841sub 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
3875sub 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
3900sub 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
3933sub 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
3945sub 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
4008sub 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
4031sub 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.
4050sub 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]
4113sub 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]
4121sub 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
4154sub 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
4168sub 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
4177sub 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
4187sub 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
4245sub 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
4255sub 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
4265sub 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
4291sub 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
4321sub 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]
4342sub 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]
4352sub 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
4395sub character_ban_list {
4396 my ($self, $args) = @_;
4397 # Header + Len + CharList[character_name(size:24)]
4398}
4399
4400sub flag {
4401 my ($self, $args) = @_;
4402}
4403
4404sub 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
4413sub 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
4422sub 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
4457sub 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
4480sub 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
4485sub 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
4495sub 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
4518sub 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
4561sub received_sync {
4562 return unless changeToInGameState();
4563 debug "Received Sync\n", 'parseMsg', 2;
4564 $timeout{'play'}{'time'} = time;
4565}
4566
4567sub 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
4577sub 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
4599sub 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
4607sub 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
4662sub 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
4672sub 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
4694sub 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
4724sub 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
4737sub 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
4747sub 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
4755sub 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
4779sub 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
4802sub 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}
4814sub 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
4830sub 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
4846sub 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
4899sub 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
4960sub 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
5030sub unread_rodex {
5031 my ( $self, $args ) = @_;
5032 message "You have new unread rodex mails.\n";
5033}
5034
5035sub 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
5055sub 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
5088sub rodex_open_write {
5089 my ( $self, $args ) = @_;
5090
5091 $rodexWrite = {};
5092
5093 $rodexWrite->{items} = new InventoryList;
5094
5095}
5096
5097sub 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
5115sub 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
5127sub 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
5140sub 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
5153sub 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
5164sub 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
5178sub 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
5200sub 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
5214sub 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
5221sub 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
5228sub booking_delete {
5229 my ($self, $args) = @_;
5230
5231 message TF("Deleted reserve group index %s\n", $args->{ID});
5232}
5233
5234
5235sub 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
5244sub 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
5275sub 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
5295sub 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
5305sub 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
5312sub 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
53291;