· 10 years ago · Sep 12, 2016, 09:14 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 Network::PacketParser; # import
24use base qw(Network::PacketParser);
25use utf8;
26use Carp::Assert;
27use Scalar::Util;
28use Socket qw(inet_aton inet_ntoa);
29
30use AI qw(ai_items_take);
31use Globals;
32#use Settings;
33use Log qw(message warning error debug);
34use FileParsers qw(updateMonsterLUT updateNPCLUT);
35use I18N qw(bytesToString stringToBytes);
36use Interface;
37use Network;
38use Network::MessageTokenizer;
39use Misc;
40use Plugins;
41use Utils;
42use Utils::Exceptions;
43use Utils::Crypton;
44use Translation;
45
46# object_type constants for &actor_display
47use constant {
48 PC_TYPE => 0x0,
49 NPC_TYPE => 0x1,
50 ITEM_TYPE => 0x2,
51 SKILL_TYPE => 0x3,
52 UNKNOWN_TYPE => 0x4,
53 NPC_MOB_TYPE => 0x5,
54 NPC_EVT_TYPE => 0x6,
55 NPC_PET_TYPE => 0x7,
56 NPC_HO_TYPE => 0x8,
57 NPC_MERSOL_TYPE => 0x9,
58 NPC_ELEMENTAL_TYPE => 0xa
59};
60
61######################################
62### CATEGORY: Class methods
63######################################
64
65# Just a wrapper for SUPER::parse.
66sub parse {
67 my $self = shift;
68 my $args = $self->SUPER::parse(@_);
69
70 if ($args && $config{debugPacket_received} == 3 &&
71 existsInList($config{'debugPacket_include'}, $args->{switch})) {
72 my $packet = $self->{packet_list}{$args->{switch}};
73 my ($name, $packString, $varNames) = @{$packet};
74
75 my @vars = ();
76 for my $varName (@{$varNames}) {
77 message "$varName = $args->{$varName}\n";
78 }
79 }
80
81 return $args;
82}
83
84##
85# Network::Receive->decrypt(r_msg, themsg)
86# r_msg: a reference to a scalar.
87# themsg: the message to decrypt.
88#
89# Decrypts the packets in $themsg and put the result in the scalar
90# referenced by $r_msg.
91#
92# This is an old method used back in the iRO beta 2 days when iRO had encrypted packets.
93# At the moment (December 20 2006) there are no servers that still use encrypted packets.
94#
95# Example:
96# } elsif ($switch eq "ABCD") {
97# my $level;
98# Network::Receive->decrypt(\$level, substr($msg, 0, 2));
99sub decrypt {
100 use bytes;
101 my ($self, $r_msg, $themsg) = @_;
102 my @mask;
103 my $i;
104 my ($temp, $msg_temp, $len_add, $len_total, $loopin, $len, $val);
105 if ($config{encrypt} == 1) {
106 undef $$r_msg;
107 undef $len_add;
108 undef $msg_temp;
109 for ($i = 0; $i < 13;$i++) {
110 $mask[$i] = 0;
111 }
112 $len = unpack("v1",substr($themsg,0,2));
113 $val = unpack("v1",substr($themsg,2,2));
114 {
115 use integer;
116 $temp = ($val * $val * 1391);
117 }
118 $temp = ~(~($temp));
119 $temp = $temp % 13;
120 $mask[$temp] = 1;
121 {
122 use integer;
123 $temp = $val * 1397;
124 }
125 $temp = ~(~($temp));
126 $temp = $temp % 13;
127 $mask[$temp] = 1;
128 for($loopin = 0; ($loopin + 4) < $len; $loopin++) {
129 if (!($mask[$loopin % 13])) {
130 $msg_temp .= substr($themsg,$loopin + 4,1);
131 }
132 }
133 if (($len - 4) % 8 != 0) {
134 $len_add = 8 - (($len - 4) % 8);
135 }
136 $len_total = $len + $len_add;
137 $$r_msg = $msg_temp.substr($themsg, $len_total, length($themsg) - $len_total);
138 } elsif ($config{encrypt} >= 2) {
139 undef $$r_msg;
140 undef $len_add;
141 undef $msg_temp;
142 for ($i = 0; $i < 17;$i++) {
143 $mask[$i] = 0;
144 }
145 $len = unpack("v1",substr($themsg,0,2));
146 $val = unpack("v1",substr($themsg,2,2));
147 {
148 use integer;
149 $temp = ($val * $val * 34953);
150 }
151 $temp = ~(~($temp));
152 $temp = $temp % 17;
153 $mask[$temp] = 1;
154 {
155 use integer;
156 $temp = $val * 2341;
157 }
158 $temp = ~(~($temp));
159 $temp = $temp % 17;
160 $mask[$temp] = 1;
161 for($loopin = 0; ($loopin + 4) < $len; $loopin++) {
162 if (!($mask[$loopin % 17])) {
163 $msg_temp .= substr($themsg,$loopin + 4,1);
164 }
165 }
166 if (($len - 4) % 8 != 0) {
167 $len_add = 8 - (($len - 4) % 8);
168 }
169 $len_total = $len + $len_add;
170 $$r_msg = $msg_temp.substr($themsg, $len_total, length($themsg) - $len_total);
171 } else {
172 $$r_msg = $themsg;
173 }
174}
175
176
177#######################################
178### CATEGORY: Private class methods
179#######################################
180
181##
182# int Network::Receive::queryLoginPinCode([String message])
183# Returns: login PIN code, or undef if cancelled
184# Ensures: length(result) in 4..8
185#
186# Request login PIN code from user.
187sub queryLoginPinCode {
188 my $message = $_[0] || T("You've never set a login PIN code before.\nPlease enter a new login PIN code:");
189 do {
190 my $input = $interface->query($message, isPassword => 1,);
191 if (!defined($input)) {
192 quit();
193 return;
194 } else {
195 if ($input !~ /^\d+$/) {
196 $interface->errorDialog(T("The PIN code may only contain digits."));
197 } elsif ((length($input) <= 3) || (length($input) >= 9)) {
198 $interface->errorDialog(T("The PIN code must be between 4 and 9 characters."));
199 } else {
200 return $input;
201 }
202 }
203 } while (1);
204}
205
206##
207# boolean Network::Receive->queryAndSaveLoginPinCode([String message])
208# Returns: true on success
209#
210# Request login PIN code from user and save it in config.
211sub queryAndSaveLoginPinCode {
212 my ($self, $message) = @_;
213 my $pin = queryLoginPinCode($message);
214 if (defined $pin) {
215 configModify('loginPinCode', $pin, silent => 1);
216 return 1;
217 } else {
218 return 0;
219 }
220}
221
222sub changeToInGameState {
223 if ($net->version() == 1) {
224 if ($accountID && UNIVERSAL::isa($char, 'Actor::You')) {
225 if ($net->getState() != Network::IN_GAME) {
226 $net->setState(Network::IN_GAME);
227 }
228 return 1;
229 } else {
230 if ($net->getState() != Network::IN_GAME_BUT_UNINITIALIZED) {
231 $net->setState(Network::IN_GAME_BUT_UNINITIALIZED);
232 if ($config{verbose} && $messageSender && !$sentWelcomeMessage) {
233 $messageSender->injectAdminMessage("Please relogin to enable X-${Settings::NAME}.");
234 $sentWelcomeMessage = 1;
235 }
236 }
237 return 0;
238 }
239 } else {
240 return 1;
241 }
242}
243
244### Packet inner struct handlers
245
246# The block size in the received_characters packet varies from server to server.
247# This method may be overrided in other ServerType handlers to return
248# the correct block size.
249sub received_characters_blockSize {
250 if ($masterServer && $masterServer->{charBlockSize}) {
251 return $masterServer->{charBlockSize};
252 } else {
253 return 106;
254 }
255}
256
257# The length must exactly match charBlockSize, as it's used to construct packets.
258sub received_characters_unpackString {
259 for ($masterServer && $masterServer->{charBlockSize}) {
260 # unknown purpose (0 = disabled, otherwise displays "Add-Ons" sidebar) (from rA)
261 # change $hairstyle
262 return 'a4 V9 v V2 v4 V v9 Z24 C8 v Z16 V x4 x4 x4 x1' if $_ == 147;
263 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4 x4' if $_ == 144;
264 # change slot feature
265 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4' if $_ == 140;
266 # robe
267 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4' if $_ == 136;
268 # delete date
269 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V' if $_ == 132;
270 return 'a4 V9 v V2 v14 Z24 C8 v Z16' if $_ == 128;
271 # bRO (bitfrost update)
272 return 'a4 V9 v V2 v14 Z24 C8 v Z12' if $_ == 124;
273 return 'a4 V9 v V2 v14 Z24 C6 v2 x4' if $_ == 116; # TODO: (missing 2 last bytes)
274 return 'a4 V9 v V2 v14 Z24 C6 v2' if $_ == 112;
275 return 'a4 V9 v17 Z24 C6 v2' if $_ == 108;
276 return 'a4 V9 v17 Z24 C6 v' if $_ == 106 || !$_;
277 die "Unknown charBlockSize: $_";
278 }
279}
280
281### Parse/reconstruct callbacks and packet handlers
282
283sub parse_account_server_info {
284 my ($self, $args) = @_;
285
286 if (length $args->{lastLoginIP} == 4 && $args->{lastLoginIP} ne "\0"x4) {
287 $args->{lastLoginIP} = inet_ntoa($args->{lastLoginIP});
288 } else {
289 delete $args->{lastLoginIP};
290 }
291
292 @{$args->{servers}} = map {
293 my %server;
294 @server{qw(ip port name users display)} = unpack 'a4 v Z20 v2 x2', $_;
295 if ($masterServer && $masterServer->{private}) {
296 $server{ip} = $masterServer->{ip};
297 } else {
298 $server{ip} = inet_ntoa($server{ip});
299 }
300 $server{name} = bytesToString($server{name});
301 \%server
302 } unpack '(a32)*', $args->{serverInfo};
303}
304
305sub reconstruct_account_server_info {
306 my ($self, $args) = @_;
307
308 $args->{lastLoginIP} = inet_aton($args->{lastLoginIP});
309
310 $args->{serverInfo} = pack '(a32)*', map { pack(
311 'a4 v Z20 v2 x2',
312 inet_aton($_->{ip}),
313 $_->{port},
314 stringToBytes($_->{name}),
315 @{$_}{qw(users display)},
316 ) } @{$args->{servers}};
317}
318
319sub account_server_info {
320 my ($self, $args) = @_;
321
322 $net->setState(2);
323 undef $conState_tries;
324 $sessionID = $args->{sessionID};
325 $accountID = $args->{accountID};
326 $sessionID2 = $args->{sessionID2};
327 # Account sex should only be 0 (female) or 1 (male)
328 # inRO gives female as 2 but expects 0 back
329 # do modulus of 2 here to fix?
330 # FIXME: we should check exactly what operation the client does to the number given
331 $accountSex = $args->{accountSex} % 2;
332 $accountSex2 = ($config{'sex'} ne "") ? $config{'sex'} : $accountSex;
333
334 # any servers with lastLoginIP lastLoginTime?
335 # message TF("Last login: %s from %s\n", @{$args}{qw(lastLoginTime lastLoginIP)}) if ...;
336
337 message
338 center(T(" Account Info "), 34, '-') ."\n" .
339 swrite(
340 T("Account ID: \@<<<<<<<<< \@<<<<<<<<<<\n" .
341 "Sex: \@<<<<<<<<<<<<<<<<<<<<<\n" .
342 "Session ID: \@<<<<<<<<< \@<<<<<<<<<<\n" .
343 " \@<<<<<<<<< \@<<<<<<<<<<\n"),
344 [unpack('V',$accountID), getHex($accountID), $sex_lut{$accountSex}, unpack('V',$sessionID), getHex($sessionID),
345 unpack('V',$sessionID2), getHex($sessionID2)]) .
346 ('-'x34) . "\n", 'connection';
347
348 @servers = @{$args->{servers}};
349
350 my $msg = center(T(" Servers "), 53, '-') ."\n" .
351 T("# Name Users IP Port\n");
352 for (my $num = 0; $num < @servers; $num++) {
353 $msg .= swrite(
354 "@<< @<<<<<<<<<<<<<<<<<<<< @<<<<< @<<<<<<<<<<<<<< @<<<<<",
355 [$num, $servers[$num]{name}, $servers[$num]{users}, $servers[$num]{ip}, $servers[$num]{port}]);
356 }
357 $msg .= ('-'x53) . "\n";
358 message $msg, "connection";
359
360 if ($net->version != 1) {
361 message T("Closing connection to Account Server\n"), 'connection';
362 $net->serverDisconnect();
363 if (!$masterServer->{charServer_ip} && $config{server} eq "") {
364 my @serverList;
365 foreach my $server (@servers) {
366 push @serverList, $server->{name};
367 }
368 my $ret = $interface->showMenu(
369 T("Please select your login server."),
370 \@serverList,
371 title => T("Select Login Server"));
372 if ($ret == -1) {
373 quit();
374 } else {
375 main::configModify('server', $ret, 1);
376 }
377
378 } elsif ($masterServer->{charServer_ip}) {
379 message TF("Forcing connect to char server %s: %s\n", $masterServer->{charServer_ip}, $masterServer->{charServer_port}), 'connection';
380
381 } else {
382 message TF("Selected server: %s\n", @servers[$config{server}]->{name}), 'connection';
383 }
384 }
385
386 # FIXME better support for multiple received_characters packets
387 undef @chars;
388 if ($config{'XKore'} eq '1') {
389 $incomingMessages->nextMessageMightBeAccountID();
390 }
391}
392
393sub connection_refused {
394 my ($self, $args) = @_;
395
396 error TF("The server has denied your connection (error: %d).\n", $args->{error}), 'connection';
397}
398
399*actor_exists = *actor_display_compatibility;
400*actor_connected = *actor_display_compatibility;
401*actor_moved = *actor_display_compatibility;
402*actor_spawned = *actor_display_compatibility;
403sub actor_display_compatibility {
404 my ($self, $args) = @_;
405 # compatibility; TODO do it in PacketParser->parse?
406 Plugins::callHook('packet_pre/actor_display', $args);
407 &actor_display unless $args->{return};
408 Plugins::callHook('packet/actor_display', $args);
409}
410
411# This function is a merge of actor_exists, actor_connected, actor_moved, etc...
412sub actor_display {
413 my ($self, $args) = @_;
414 return unless changeToInGameState();
415 my ($actor, $mustAdd);
416
417
418 #### Initialize ####
419
420 my $nameID = unpack("V", $args->{ID});
421
422 if ($args->{switch} eq "0086") {
423 # Message 0086 contains less information about the actor than other similar
424 # messages. So we use the existing actor information.
425 my $coordsArg = $args->{coords};
426 my $tickArg = $args->{tick};
427 $args = Actor::get($args->{ID})->deepCopy();
428 # Here we overwrite the $args data with the 0086 packet data.
429 $args->{switch} = "0086";
430 $args->{coords} = $coordsArg;
431 $args->{tick} = $tickArg; # lol tickcount what do we do with that? debug "tick: " . $tickArg/1000/3600/24 . "\n";
432 }
433
434 my (%coordsFrom, %coordsTo);
435 if (length $args->{coords} == 6) {
436 # Actor Moved
437 makeCoordsFromTo(\%coordsFrom, \%coordsTo, $args->{coords}); # body dir will be calculated using the vector
438 } else {
439 # Actor Spawned/Exists
440 makeCoordsDir(\%coordsTo, $args->{coords}, \$args->{body_dir});
441 %coordsFrom = %coordsTo;
442 }
443
444 # Remove actors that are located outside the map
445 # This may be caused by:
446 # - server sending us false actors
447 # - actor packets not being parsed correctly
448 if (defined $field && ($field->isOffMap($coordsFrom{x}, $coordsFrom{y}) || $field->isOffMap($coordsTo{x}, $coordsTo{y}))) {
449 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());
450 return;
451 }
452
453 # Remove actors with a distance greater than removeActorWithDistance. Useful for vending (so you don't spam
454 # too many packets in prontera and cause server lag). As a side effect, you won't be able to "see" actors
455 # beyond removeActorWithDistance.
456 if ($config{removeActorWithDistance}) {
457 if ((my $block_dist = blockDistance($char->{pos_to}, \%coordsTo)) > ($config{removeActorWithDistance})) {
458 my $nameIdTmp = unpack("V", $args->{ID});
459 debug "Removed out of sight actor $nameIdTmp at ($coordsTo{x}, $coordsTo{y}) (distance: $block_dist)\n";
460 return;
461 }
462 }
463=pod
464 # Zealotus bug
465 if ($args->{type} == 1200) {
466 open DUMP, ">> test_Zealotus.txt";
467 print DUMP "Zealotus: " . $nameID . "\n";
468 print DUMP Dumper($args);
469 close DUMP;
470 }
471=cut
472
473 #### Step 0: determine object type ####
474 my $object_class;
475 if (defined $args->{object_type}) {
476 if ($args->{type} == 45) { # portals use the same object_type as NPCs
477 $object_class = 'Actor::Portal';
478 } else {
479 $object_class = {
480 PC_TYPE, 'Actor::Player',
481 # NPC_TYPE? # not encountered, NPCs are NPC_EVT_TYPE
482 # SKILL_TYPE? # not encountered
483 # UNKNOWN_TYPE? # not encountered
484 NPC_MOB_TYPE, 'Actor::Monster',
485 NPC_EVT_TYPE, 'Actor::NPC', # both NPCs and portals
486 NPC_PET_TYPE, 'Actor::Pet',
487 NPC_HO_TYPE, 'Actor::Slave',
488 # NPC_MERSOL_TYPE? # not encountered
489 # NPC_ELEMENTAL_TYPE? # not encountered
490 }->{$args->{object_type}};
491 }
492
493 }
494
495 unless (defined $object_class) {
496 if ($jobs_lut{$args->{type}}) {
497 unless ($args->{type} > 6000) {
498 $object_class = 'Actor::Player';
499 } else {
500 $object_class = 'Actor::Slave';
501 }
502 } elsif ($args->{type} == 45) {
503 $object_class = 'Actor::Portal';
504
505 } elsif ($args->{type} >= 1000) {
506 if ($args->{hair_style} == 0x64) {
507 $object_class = 'Actor::Pet';
508 } else {
509 $object_class = 'Actor::Monster';
510 }
511 } else { # ($args->{type} < 1000 && $args->{type} != 45 && !$jobs_lut{$args->{type}})
512 $object_class = 'Actor::NPC';
513 }
514 }
515
516 #### Step 1: create the actor object ####
517
518 if ($object_class eq 'Actor::Player') {
519 # Actor is a player
520 $actor = $playersList->getByID($args->{ID});
521 if (!defined $actor) {
522 $actor = new Actor::Player();
523 $actor->{appear_time} = time;
524 # New actor_display packets include the player's name
525 if ($args->{switch} eq "0086") {
526 $actor->{name} = $args->{name};
527 } else {
528 $actor->{name} = bytesToString($args->{name}) if exists $args->{name};
529 }
530 $mustAdd = 1;
531 }
532 $actor->{nameID} = $nameID;
533 } elsif ($object_class eq 'Actor::Slave') {
534 # Actor is a homunculus or a mercenary
535 $actor = $slavesList->getByID($args->{ID});
536 if (!defined $actor) {
537 $actor = ($char->{slaves} && $char->{slaves}{$args->{ID}})
538 ? $char->{slaves}{$args->{ID}} : new Actor::Slave ($args->{type});
539
540 $actor->{appear_time} = time;
541 $actor->{name_given} = bytesToString($args->{name}) if exists $args->{name};
542 $actor->{jobId} = $args->{type} if exists $args->{type};
543 $mustAdd = 1;
544 }
545 $actor->{nameID} = $nameID;
546 } elsif ($object_class eq 'Actor::Portal') {
547 # Actor is a portal
548 $actor = $portalsList->getByID($args->{ID});
549 if (!defined $actor) {
550 $actor = new Actor::Portal();
551 $actor->{appear_time} = time;
552 my $exists = portalExists($field->baseName, \%coordsTo);
553 $actor->{source}{map} = $field->baseName;
554 if ($exists ne "") {
555 $actor->setName("$portals_lut{$exists}{source}{map} -> " . getPortalDestName($exists));
556 }
557 $mustAdd = 1;
558
559 # Strangely enough, portals (like all other actors) have names, too.
560 # We _could_ send a "actor_info_request" packet to find the names of each portal,
561 # however I see no gain from this. (And it might even provide another way of private
562 # servers to auto-ban bots.)
563 }
564 $actor->{nameID} = $nameID;
565 } elsif ($object_class eq 'Actor::Pet') {
566 # Actor is a pet
567 $actor = $petsList->getByID($args->{ID});
568 if (!defined $actor) {
569 $actor = new Actor::Pet();
570 $actor->{appear_time} = time;
571 $actor->{name} = $args->{name};
572# if ($monsters_lut{$args->{type}}) {
573# $actor->setName($monsters_lut{$args->{type}});
574# }
575 $actor->{name_given} = exists $args->{name} ? bytesToString($args->{name}) : T("Unknown");
576 $mustAdd = 1;
577
578 # Previously identified monsters could suddenly be identified as pets.
579 if ($monstersList->getByID($args->{ID})) {
580 $monstersList->removeByID($args->{ID});
581 }
582
583 # Why do monsters and pets use nameID as type?
584 $actor->{nameID} = $args->{type};
585
586 }
587 } elsif ($object_class eq 'Actor::Monster') {
588 $actor = $monstersList->getByID($args->{ID});
589 if (!defined $actor) {
590 $actor = new Actor::Monster();
591 $actor->{appear_time} = time;
592 if ($monsters_lut{$args->{type}}) {
593 $actor->setName($monsters_lut{$args->{type}});
594 }
595 #$actor->{name_given} = exists $args->{name} ? bytesToString($args->{name}) : "Unknown";
596 $actor->{name_given} = "Unknown";
597 $actor->{binType} = $args->{type};
598 $mustAdd = 1;
599
600 # Why do monsters and pets use nameID as type?
601 $actor->{nameID} = $args->{type};
602 }
603 } elsif ($object_class eq 'Actor::NPC') {
604 # Actor is an NPC
605 $actor = $npcsList->getByID($args->{ID});
606 if (!defined $actor) {
607 $actor = new Actor::NPC();
608 $actor->{appear_time} = time;
609 $actor->{name} = bytesToString($args->{name}) if exists $args->{name};
610 $mustAdd = 1;
611 }
612 $actor->{nameID} = $nameID;
613 }
614
615 #### Step 2: update actor information ####
616 $actor->{ID} = $args->{ID};
617 $actor->{jobID} = $args->{type};
618 $actor->{type} = $args->{type};
619 $actor->{lv} = $args->{lv};
620 $actor->{pos} = {%coordsFrom};
621 $actor->{pos_to} = {%coordsTo};
622 $actor->{walk_speed} = $args->{walk_speed} / 1000 if (exists $args->{walk_speed} && $args->{switch} ne "0086");
623 $actor->{time_move} = time;
624 $actor->{time_move_calc} = distance(\%coordsFrom, \%coordsTo) * $actor->{walk_speed};
625 # 0086 would need that?
626 $actor->{object_type} = $args->{object_type} if (defined $args->{object_type});
627
628 if (UNIVERSAL::isa($actor, "Actor::Player")) {
629 # None of this stuff should matter if the actor isn't a player... => does matter for a guildflag npc!
630
631 # Interesting note about emblemID. If it is 0 (or none), the Ragnarok
632 # client will display "Send (Player) a guild invitation" (assuming one has
633 # invitation priveledges), regardless of whether or not guildID is set.
634 # I bet that this is yet another brilliant "feature" by GRAVITY's good programmers.
635 $actor->{emblemID} = $args->{emblemID} if (exists $args->{emblemID});
636 $actor->{guildID} = $args->{guildID} if (exists $args->{guildID});
637
638 if (exists $args->{lowhead}) {
639 $actor->{headgear}{low} = $args->{lowhead};
640 $actor->{headgear}{mid} = $args->{midhead};
641 $actor->{headgear}{top} = $args->{tophead};
642 $actor->{weapon} = $args->{weapon};
643 $actor->{shield} = $args->{shield};
644 }
645
646 $actor->{sex} = $args->{sex};
647
648 if ($args->{act} == 1) {
649 $actor->{dead} = 1;
650 } elsif ($args->{act} == 2) {
651 $actor->{sitting} = 1;
652 }
653
654 # Monsters don't have hair colors or heads to look around...
655 $actor->{hair_color} = $args->{hair_color} if (exists $args->{hair_color});
656
657 } elsif (UNIVERSAL::isa($actor, "Actor::NPC") && $args->{type} == 722) { # guild flag has emblem
658 # odd fact: "this data can also be found in a strange place:
659 # (shield OR lowhead) + midhead = emblemID (either shield or lowhead depending on the packet)
660 # tophead = guildID
661 $actor->{emblemID} = $args->{emblemID};
662 $actor->{guildID} = $args->{guildID};
663 }
664
665 # But hair_style is used for pets, and their bodies can look different ways...
666 $actor->{hair_style} = $args->{hair_style} if (exists $args->{hair_style});
667 $actor->{look}{body} = $args->{body_dir} if (exists $args->{body_dir});
668 $actor->{look}{head} = $args->{head_dir} if (exists $args->{head_dir});
669
670 # When stance is non-zero, character is bobbing as if they had just got hit,
671 # but the cursor also turns to a sword when they are mouse-overed.
672 #$actor->{stance} = $args->{stance} if (exists $args->{stance});
673
674 # Visual effects are a set of flags (some of the packets don't have this argument)
675 $actor->{opt3} = $args->{opt3} if (exists $args->{opt3}); # stackable
676
677 # Known visual effects:
678 # 0x0001 = Yellow tint (eg, a quicken skill)
679 # 0x0002 = Red tint (eg, power-thrust)
680 # 0x0004 = Gray tint (eg, energy coat)
681 # 0x0008 = Slow lightning (eg, mental strength)
682 # 0x0010 = Fast lightning (eg, MVP fury)
683 # 0x0020 = Black non-moving statue (eg, stone curse)
684 # 0x0040 = Translucent weapon
685 # 0x0080 = Translucent red sprite (eg, marionette control?)
686 # 0x0100 = Spaztastic weapon image (eg, mystical amplification)
687 # 0x0200 = Gigantic glowy sphere-thing
688 # 0x0400 = Translucent pink sprite (eg, marionette control?)
689 # 0x0800 = Glowy sprite outline (eg, assumptio)
690 # 0x1000 = Bright red sprite, slowly moving red lightning (eg, MVP fury?)
691 # 0x2000 = Vortex-type effect
692
693 # Note that these are flags, and you can mix and match them
694 # Example: 0x000C (0x0008 & 0x0004) = gray tint with slow lightning
695
696=pod
697typedef enum <unnamed-tag> {
698 SHOW_EFST_NORMAL = 0x0,
699 SHOW_EFST_QUICKEN = 0x1,
700 SHOW_EFST_OVERTHRUST = 0x2,
701 SHOW_EFST_ENERGYCOAT = 0x4,
702 SHOW_EFST_EXPLOSIONSPIRITS = 0x8,
703 SHOW_EFST_STEELBODY = 0x10,
704 SHOW_EFST_BLADESTOP = 0x20,
705 SHOW_EFST_AURABLADE = 0x40,
706 SHOW_EFST_REDBODY = 0x80,
707 SHOW_EFST_LIGHTBLADE = 0x100,
708 SHOW_EFST_MOON = 0x200,
709 SHOW_EFST_PINKBODY = 0x400,
710 SHOW_EFST_ASSUMPTIO = 0x800,
711 SHOW_EFST_SUN_WARM = 0x1000,
712 SHOW_EFST_REFLECT = 0x2000,
713 SHOW_EFST_BUNSIN = 0x4000,
714 SHOW_EFST_SOULLINK = 0x8000,
715 SHOW_EFST_UNDEAD = 0x10000,
716 SHOW_EFST_CONTRACT = 0x20000,
717} <unnamed-tag>;
718=cut
719
720 # Save these parameters ...
721 $actor->{opt1} = $args->{opt1}; # nonstackable
722 $actor->{opt2} = $args->{opt2}; # stackable
723 $actor->{option} = $args->{option}; # stackable
724
725 # And use them to set status flags.
726 if (setStatus($actor, $args->{opt1}, $args->{opt2}, $args->{option})) {
727 $mustAdd = 0;
728 }
729
730
731 #### Step 3: Add actor to actor list ####
732 if ($mustAdd) {
733 if (UNIVERSAL::isa($actor, "Actor::Player")) {
734 $playersList->add($actor);
735 Plugins::callHook('add_player_list', $actor);
736
737 } elsif (UNIVERSAL::isa($actor, "Actor::Monster")) {
738 $monstersList->add($actor);
739 Plugins::callHook('add_monster_list', $actor);
740
741 } elsif (UNIVERSAL::isa($actor, "Actor::Pet")) {
742 $petsList->add($actor);
743 Plugins::callHook('add_pet_list', $actor);
744
745 } elsif (UNIVERSAL::isa($actor, "Actor::Portal")) {
746 $portalsList->add($actor);
747 Plugins::callHook('add_portal_list', $actor);
748
749 } elsif (UNIVERSAL::isa($actor, "Actor::NPC")) {
750 my $ID = $args->{ID};
751 my $location = $field->baseName . " $actor->{pos}{x} $actor->{pos}{y}";
752 if ($npcs_lut{$location}) {
753 $actor->setName($npcs_lut{$location});
754 }
755 $npcsList->add($actor);
756 Plugins::callHook('add_npc_list', $actor);
757
758 } elsif (UNIVERSAL::isa($actor, "Actor::Slave")) {
759 $slavesList->add($actor);
760 Plugins::callHook('add_slave_list', $actor);
761 }
762 }
763
764
765 #### Packet specific ####
766 if ($args->{switch} eq "0078" ||
767 $args->{switch} eq "01D8" ||
768 $args->{switch} eq "022A" ||
769 $args->{switch} eq "02EE" ||
770 $args->{switch} eq "07F9" ||
771 $args->{switch} eq "0857") {
772 # Actor Exists (standing)
773
774 if ($actor->isa('Actor::Player')) {
775 my $domain = existsInList($config{friendlyAID}, unpack("V", $actor->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
776 debug "Player Exists: " . $actor->name . " ($actor->{binID}) Level $actor->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsFrom{x}, $coordsFrom{y})\n", $domain;
777
778 Plugins::callHook('player', {player => $actor}); #backwards compatibility
779
780 Plugins::callHook('player_exist', {player => $actor});
781
782 } elsif ($actor->isa('Actor::NPC')) {
783 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;
784 Plugins::callHook('npc_exist', {npc => $actor});
785
786 } elsif ($actor->isa('Actor::Portal')) {
787 message TF("Portal Exists: %s (%s, %s) - (%s)\n", $actor->name, $actor->{pos_to}{x}, $actor->{pos_to}{y}, $actor->{binID}), "portals", 1;
788 Plugins::callHook('portal_exist', {portal => $actor});
789
790 } elsif ($actor->isa('Actor::Monster')) {
791 debug sprintf("Monster Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
792
793 } elsif ($actor->isa('Actor::Pet')) {
794 debug sprintf("Pet Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
795
796 } elsif ($actor->isa('Actor::Slave')) {
797 debug sprintf("Slave Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
798
799 } else {
800 debug sprintf("Unknown Actor Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
801 }
802
803 } elsif ($args->{switch} eq "0079" ||
804 $args->{switch} eq "01DB" ||
805 $args->{switch} eq "022B" ||
806 $args->{switch} eq "02ED" ||
807 $args->{switch} eq "01D9" ||
808 $args->{switch} eq "07F8" ||
809 $args->{switch} eq "0858") {
810 # Actor Connected (new)
811
812 if ($actor->isa('Actor::Player')) {
813 my $domain = existsInList($config{friendlyAID}, unpack("V", $args->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
814 debug "Player Connected: ".$actor->name." ($actor->{binID}) Level $args->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsTo{x}, $coordsTo{y})\n", $domain;
815
816 Plugins::callHook('player', {player => $actor}); #backwards compatibailty
817
818 Plugins::callHook('player_connected', {player => $actor});
819 } else {
820 debug "Unknown Connected: $args->{type} - \n", "parseMsg";
821 }
822
823 } elsif ($args->{switch} eq "007B" ||
824 $args->{switch} eq "0086" ||
825 $args->{switch} eq "01DA" ||
826 $args->{switch} eq "022C" ||
827 $args->{switch} eq "02EC" ||
828 $args->{switch} eq "07F7" ||
829 $args->{switch} eq "0856") {
830 # Actor Moved
831
832 # Correct the direction in which they're looking
833 my %vec;
834 getVector(\%vec, \%coordsTo, \%coordsFrom);
835 my $direction = int sprintf("%.0f", (360 - vectorToDegree(\%vec)) / 45);
836
837 $actor->{look}{body} = $direction;
838 $actor->{look}{head} = 0;
839
840 if ($actor->isa('Actor::Player')) {
841 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";
842 } elsif ($actor->isa('Actor::Monster')) {
843 debug "Monster Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
844 } elsif ($actor->isa('Actor::Pet')) {
845 debug "Pet Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
846 } elsif ($actor->isa('Actor::Slave')) {
847 debug "Slave Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
848 } elsif ($actor->isa('Actor::Portal')) {
849 # This can never happen of course.
850 debug "Portal Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
851 } elsif ($actor->isa('Actor::NPC')) {
852 # Neither can this.
853 debug "NPC Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
854 } else {
855 debug "Unknown Actor Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
856 }
857
858 } elsif ($args->{switch} eq "007C") {
859 # Actor Spawned
860 if ($actor->isa('Actor::Player')) {
861 debug "Player Spawned: " . $actor->nameIdx . " $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}}\n", "parseMsg";
862 } elsif ($actor->isa('Actor::Monster')) {
863 debug "Monster Spawned: " . $actor->nameIdx . "\n", "parseMsg";
864 } elsif ($actor->isa('Actor::Pet')) {
865 debug "Pet Spawned: " . $actor->nameIdx . "\n", "parseMsg";
866 } elsif ($actor->isa('Actor::Slave')) {
867 debug "Slave Spawned: " . $actor->nameIdx . " $jobs_lut{$actor->{jobID}}\n", "parseMsg";
868 } elsif ($actor->isa('Actor::Portal')) {
869 # Can this happen?
870 debug "Portal Spawned: " . $actor->nameIdx . "\n", "parseMsg";
871 } elsif ($actor->isa('NPC')) {
872 debug "NPC Spawned: " . $actor->nameIdx . "\n", "parseMsg";
873 } else {
874 debug "Unknown Spawned: " . $actor->nameIdx . "\n", "parseMsg";
875 }
876 }
877}
878
879sub actor_died_or_disappeared {
880 my ($self,$args) = @_;
881 return unless changeToInGameState();
882 my $ID = $args->{ID};
883 avoidList_ID($ID);
884
885 if ($ID eq $accountID) {
886 message T("You have died\n") if (!$char->{dead});
887 Plugins::callHook('self_died');
888 closeShop() unless !$shopstarted || $config{'dcOnDeath'} == -1 || $AI == AI::OFF;
889 $char->{deathCount}++;
890 $char->{dead} = 1;
891 $char->{dead_time} = time;
892 if ($char->{equipment}{arrow} && $char->{equipment}{arrow}{type} == 19) {
893 delete $char->{equipment}{arrow};
894 }
895
896 } elsif (defined $monstersList->getByID($ID)) {
897 my $monster = $monstersList->getByID($ID);
898 if ($args->{type} == 0) {
899 debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
900 $monster->{disappeared} = 1;
901
902 } elsif ($args->{type} == 1) {
903 debug "Monster Died: " . $monster->name . " ($monster->{binID})\n", "parseMsg_damage";
904 $monster->{dead} = 1;
905
906 if ((AI::action ne "attack" || AI::args(0)->{ID} eq $ID) &&
907 ($config{itemsTakeAuto_party} &&
908 ($monster->{dmgFromParty} > 0 ||
909 $monster->{dmgFromYou} > 0))) {
910 AI::clear("items_take");
911 ai_items_take($monster->{pos}{x}, $monster->{pos}{y},
912 $monster->{pos_to}{x}, $monster->{pos_to}{y});
913 }
914
915 } elsif ($args->{type} == 2) { # What's this?
916 debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
917 $monster->{disappeared} = 1;
918
919 } elsif ($args->{type} == 3) {
920 debug "Monster Teleported: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
921 $monster->{teleported} = 1;
922 }
923
924 $monster->{gone_time} = time;
925 $monsters_old{$ID} = $monster->deepCopy();
926 Plugins::callHook('monster_disappeared', {monster => $monster});
927 $monstersList->remove($monster);
928
929 } elsif (defined $playersList->getByID($ID)) {
930 my $player = $playersList->getByID($ID);
931 if ($args->{type} == 1) {
932 message TF("Player Died: %s (%d) %s %s\n", $player->name, $player->{binID}, $sex_lut{$player->{sex}}, $jobs_lut{$player->{jobID}});
933 $player->{dead} = 1;
934 $player->{dead_time} = time;
935 } else {
936 if ($args->{type} == 0) {
937 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";
938 $player->{disappeared} = 1;
939 } elsif ($args->{type} == 2) {
940 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";
941 $player->{disconnected} = 1;
942 } elsif ($args->{type} == 3) {
943 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";
944 $player->{teleported} = 1;
945 } else {
946 debug "Player Disappeared in an unknown way: ".$player->name." ($player->{binID}) $sex_lut{$player->{sex}} $jobs_lut{$player->{jobID}}\n", "parseMsg_presence";
947 $player->{disappeared} = 1;
948 }
949
950 if (grep { $ID eq $_ } @venderListsID) {
951 binRemove(\@venderListsID, $ID);
952 delete $venderLists{$ID};
953 }
954
955 $player->{gone_time} = time;
956 $players_old{$ID} = $player->deepCopy();
957 Plugins::callHook('player_disappeared', {player => $player});
958
959 $playersList->remove($player);
960 }
961
962 } elsif ($players_old{$ID}) {
963 if ($args->{type} == 2) {
964 debug "Player Disconnected: " . $players_old{$ID}->name . "\n", "parseMsg_presence";
965 $players_old{$ID}{disconnected} = 1;
966 } elsif ($args->{type} == 3) {
967 debug "Player Teleported: " . $players_old{$ID}->name . "\n", "parseMsg_presence";
968 $players_old{$ID}{teleported} = 1;
969 }
970
971 } elsif (defined $portalsList->getByID($ID)) {
972 my $portal = $portalsList->getByID($ID);
973 debug "Portal Disappeared: " . $portal->name . " ($portal->{binID})\n", "parseMsg";
974 $portal->{disappeared} = 1;
975 $portal->{gone_time} = time;
976 $portals_old{$ID} = $portal->deepCopy();
977 Plugins::callHook('portal_disappeared', {portal => $portal});
978 $portalsList->remove($portal);
979
980 } elsif (defined $npcsList->getByID($ID)) {
981 my $npc = $npcsList->getByID($ID);
982 debug "NPC Disappeared: " . $npc->name . " ($npc->{nameID})\n", "parseMsg";
983 $npc->{disappeared} = 1;
984 $npc->{gone_time} = time;
985 $npcs_old{$ID} = $npc->deepCopy();
986 Plugins::callHook('npc_disappeared', {npc => $npc});
987 $npcsList->remove($npc);
988
989 } elsif (defined $petsList->getByID($ID)) {
990 my $pet = $petsList->getByID($ID);
991 debug "Pet Disappeared: " . $pet->name . " ($pet->{binID})\n", "parseMsg";
992 $pet->{disappeared} = 1;
993 $pet->{gone_time} = time;
994 Plugins::callHook('pet_disappeared', {pet => $pet});
995 $petsList->remove($pet);
996
997 } elsif (defined $slavesList->getByID($ID)) {
998 my $slave = $slavesList->getByID($ID);
999 if ($args->{type} == 1) {
1000 message TF("Slave Died: %s (%d) %s\n", $slave->name, $slave->{binID}, $slave->{actorType});
1001 $slave->{state} = 4;
1002 } else {
1003 if ($args->{type} == 0) {
1004 debug "Slave Disappeared: " . $slave->name . " ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
1005 $slave->{disappeared} = 1;
1006 } elsif ($args->{type} == 2) {
1007 debug "Slave Disconnected: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
1008 $slave->{disconnected} = 1;
1009 } elsif ($args->{type} == 3) {
1010 debug "Slave Teleported: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
1011 $slave->{teleported} = 1;
1012 } else {
1013 debug "Slave Disappeared in an unknown way: ".$slave->name." ($slave->{binID}) $slave->{actorType}\n", "parseMsg_presence";
1014 $slave->{disappeared} = 1;
1015 }
1016
1017 $slave->{gone_time} = time;
1018 Plugins::callHook('slave_disappeared', {slave => $slave});
1019 }
1020
1021 $slavesList->remove($slave);
1022
1023 } else {
1024 debug "Unknown Disappeared: ".getHex($ID)."\n", "parseMsg";
1025 }
1026}
1027
1028sub actor_action {
1029 my ($self,$args) = @_;
1030 return unless changeToInGameState();
1031
1032 $args->{damage} = intToSignedShort($args->{damage});
1033 if ($args->{type} == ACTION_ITEMPICKUP) {
1034 # Take item
1035 my $source = Actor::get($args->{sourceID});
1036 my $verb = $source->verb('pick up', 'picks up');
1037 my $target = getActorName($args->{targetID});
1038 debug "$source $verb $target\n", 'parseMsg_presence';
1039
1040 my $item = $itemsList->getByID($args->{targetID});
1041 $item->{takenBy} = $args->{sourceID} if ($item);
1042
1043 } elsif ($args->{type} == ACTION_SIT) {
1044 # Sit
1045 my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
1046 if ($args->{sourceID} eq $accountID) {
1047 message T("You are sitting.\n") if (!$char->{sitting});
1048 $char->{sitting} = 1;
1049 AI::queue("sitAuto") unless (AI::inQueue("sitAuto")) || $ai_v{sitAuto_forcedBySitCommand};
1050 } else {
1051 message TF("%s is sitting.\n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
1052 my $player = $playersList->getByID($args->{sourceID});
1053 $player->{sitting} = 1 if ($player);
1054 }
1055 Misc::checkValidity("actor_action (take item)");
1056
1057 } elsif ($args->{type} == ACTION_STAND) {
1058 # Stand
1059 my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
1060 if ($args->{sourceID} eq $accountID) {
1061 message T("You are standing.\n") if ($char->{sitting});
1062 if ($config{sitAuto_idle}) {
1063 $timeout{ai_sit_idle}{time} = time;
1064 }
1065 $char->{sitting} = 0;
1066 } else {
1067 message TF("%s is standing.\n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
1068 my $player = $playersList->getByID($args->{sourceID});
1069 $player->{sitting} = 0 if ($player);
1070 }
1071 Misc::checkValidity("actor_action (stand)");
1072
1073 } else {
1074 # Attack
1075 my $dmgdisplay;
1076 my $totalDamage = $args->{damage} + $args->{dual_wield_damage};
1077 if ($totalDamage == 0) {
1078 $dmgdisplay = T("Miss!");
1079 $dmgdisplay .= "!" if ($args->{type} == ACTION_ATTACK_LUCKY); # lucky dodge
1080 } else {
1081 $dmgdisplay = $args->{div} > 1
1082 ? sprintf '%d*%d', $args->{damage} / $args->{div}, $args->{div}
1083 : $args->{damage}
1084 ;
1085 $dmgdisplay .= "!" if ($args->{type} == ACTION_ATTACK_CRITICAL); # critical hit
1086 $dmgdisplay .= " + $args->{dual_wield_damage}" if $args->{dual_wield_damage};
1087 }
1088
1089 Misc::checkValidity("actor_action (attack 1)");
1090
1091 updateDamageTables($args->{sourceID}, $args->{targetID}, $totalDamage);
1092
1093 Misc::checkValidity("actor_action (attack 2)");
1094
1095 my $source = Actor::get($args->{sourceID});
1096 my $target = Actor::get($args->{targetID});
1097 my $verb = $source->verb('attack', 'attacks');
1098
1099 $target->{sitting} = 0 unless $args->{type} == ACTION_ATTACK_NOMOTION || $args->{type} == ACTION_ATTACK_MULTIPLE_NOMOTION || $totalDamage == 0;
1100
1101 my $msg = attack_string($source, $target, $dmgdisplay, ($args->{src_speed}));
1102 Plugins::callHook('packet_attack', {sourceID => $args->{sourceID}, targetID => $args->{targetID}, msg => \$msg, dmg => $totalDamage, type => $args->{type}});
1103
1104 my $status = sprintf("[%3d/%3d]", percent_hp($char), percent_sp($char));
1105
1106 Misc::checkValidity("actor_action (attack 3)");
1107
1108 if ($args->{sourceID} eq $accountID) {
1109 message("$status $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
1110 if ($startedattack) {
1111 $monstarttime = time();
1112 $monkilltime = time();
1113 $startedattack = 0;
1114 }
1115 Misc::checkValidity("actor_action (attack 4)");
1116 calcStat($args->{damage});
1117 Misc::checkValidity("actor_action (attack 5)");
1118
1119 } elsif ($args->{targetID} eq $accountID) {
1120 message("$status $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
1121 if ($args->{damage} > 0) {
1122 $damageTaken{$source->{name}}{attack} += $args->{damage};
1123 }
1124
1125 } elsif ($char->{slaves} && $char->{slaves}{$args->{sourceID}}) {
1126 message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{sourceID}}{hpPercent}, $char->{slaves}{$args->{sourceID}}{spPercent}) . " $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
1127
1128 } elsif ($char->{slaves} && $char->{slaves}{$args->{targetID}}) {
1129 message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{targetID}}{hpPercent}, $char->{slaves}{$args->{targetID}}{spPercent}) . " $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
1130
1131 } elsif ($args->{sourceID} eq $args->{targetID}) {
1132 message("$status $msg");
1133
1134 } elsif ($config{showAllDamage}) {
1135 message("$status $msg");
1136
1137 } else {
1138 debug("$msg", 'parseMsg_damage');
1139 }
1140
1141 Misc::checkValidity("actor_action (attack 6)");
1142 }
1143}
1144
1145sub actor_info {
1146 my ($self, $args) = @_;
1147 return unless changeToInGameState();
1148
1149 debug "Received object info: $args->{name}\n", "parseMsg_presence/name", 2;
1150
1151 my $player = $playersList->getByID($args->{ID});
1152 if ($player) {
1153 # 0095: This packet tells us the names of players who aren't in a guild.
1154 # 0195: Receive names of players who are in a guild.
1155 # FIXME: There is more to this packet than just party name and guild name.
1156 # This packet is received when you leave a guild
1157 # (with cryptic party and guild name fields, at least for now)
1158 $player->setName(bytesToString($args->{name}));
1159 $player->{info} = 1;
1160
1161 $player->{party}{name} = bytesToString($args->{partyName}) if defined $args->{partyName};
1162 $player->{guild}{name} = bytesToString($args->{guildName}) if defined $args->{guildName};
1163 $player->{guild}{title} = bytesToString($args->{guildTitle}) if defined $args->{guildTitle};
1164
1165 message "Player Info: " . $player->nameIdx . "\n", "parseMsg_presence", 2;
1166 updatePlayerNameCache($player);
1167 Plugins::callHook('charNameUpdate', {player => $player});
1168 }
1169
1170 my $monster = $monstersList->getByID($args->{ID});
1171 if ($monster) {
1172 my $name = bytesToString($args->{name});
1173 $name =~ s/^\s+|\s+$//g;
1174 debug "Monster Info: $name ($monster->{binID})\n", "parseMsg", 2;
1175 $monster->{name_given} = $name;
1176 $monster->{info} = 1;
1177 if ($monsters_lut{$monster->{nameID}} eq "") {
1178 $monster->setName($name);
1179 $monsters_lut{$monster->{nameID}} = $name;
1180 updateMonsterLUT(Settings::getTableFilename("monsters.txt"), $monster->{nameID}, $name);
1181 }
1182 }
1183
1184 my $npc = $npcs{$args->{ID}};
1185 if ($npc) {
1186 $npc->setName(bytesToString($args->{name}));
1187 $npc->{info} = 1;
1188 if ($config{debug} >= 2) {
1189 my $binID = binFind(\@npcsID, $args->{ID});
1190 debug "NPC Info: $npc->{name} ($binID)\n", "parseMsg", 2;
1191 }
1192
1193 my $location = $field->baseName . " $npc->{pos}{x} $npc->{pos}{y}";
1194 if (!$npcs_lut{$location}) {
1195 $npcs_lut{$location} = $npc->{name};
1196 updateNPCLUT(Settings::getTableFilename("npcs.txt"), $location, $npc->{name});
1197 }
1198 }
1199
1200 my $pet = $pets{$args->{ID}};
1201 if ($pet) {
1202 my $name = bytesToString($args->{name});
1203 $pet->{name_given} = $name;
1204 $pet->setName($name);
1205 $pet->{info} = 1;
1206 if ($config{debug} >= 2) {
1207 my $binID = binFind(\@petsID, $args->{ID});
1208 debug "Pet Info: $pet->{name_given} ($binID)\n", "parseMsg", 2;
1209 }
1210 }
1211
1212 my $slave = $slavesList->getByID($args->{ID});
1213 if ($slave) {
1214 my $name = bytesToString($args->{name});
1215 $slave->{name_given} = $name;
1216 $slave->setName($name);
1217 $slave->{info} = 1;
1218 my $binID = binFind(\@slavesID, $args->{ID});
1219 debug "Slave Info: $name ($binID)\n", "parseMsg_presence", 2;
1220 updatePlayerNameCache($slave);
1221 }
1222
1223 # TODO: $args->{ID} eq $accountID
1224}
1225
1226use constant QTYPE => (
1227 0x0 => [0xff, 0xff, 0, 0],
1228 0x1 => [0xff, 0x80, 0, 0],
1229 0x2 => [0, 0xff, 0, 0],
1230 0x3 => [0x80, 0, 0x80, 0],
1231);
1232
1233sub parse_minimap_indicator {
1234 my ($self, $args) = @_;
1235
1236 $args->{actor} = Actor::get($args->{npcID});
1237 $args->{show} = $args->{type} != 2;
1238
1239 unless (defined $args->{red}) {
1240 @{$args}{qw(red green blue alpha)} = @{{QTYPE}->{$args->{qtype}} || [0xff, 0xff, 0xff, 0]};
1241 }
1242
1243 # FIXME: packet 0144: coordinates are missing now when clearing indicators; ID is used
1244 # Wx depends on coordinates there
1245}
1246
1247sub account_payment_info {
1248 my ($self, $args) = @_;
1249 my $D_minute = $args->{D_minute};
1250 my $H_minute = $args->{H_minute};
1251
1252 my $D_d = int($D_minute / 1440);
1253 my $D_h = int(($D_minute % 1440) / 60);
1254 my $D_m = int(($D_minute % 1440) % 60);
1255
1256 my $H_d = int($H_minute / 1440);
1257 my $H_h = int(($H_minute % 1440) / 60);
1258 my $H_m = int(($H_minute % 1440) % 60);
1259
1260 message T("============= Account payment information =============\n"), "info";
1261 message TF("Pay per day : %s day(s) %s hour(s) and %s minute(s)\n", $D_d, $D_h, $D_m), "info";
1262 message TF("Pay per hour : %s day(s) %s hour(s) and %s minute(s)\n", $H_d, $H_h, $H_m), "info";
1263 message "-------------------------------------------------------\n", "info";
1264}
1265
1266# TODO
1267sub reconstruct_minimap_indicator {
1268}
1269
1270use constant {
1271 HO_PRE_INIT => 0x0,
1272 HO_RELATIONSHIP_CHANGED => 0x1,
1273 HO_FULLNESS_CHANGED => 0x2,
1274 HO_ACCESSORY_CHANGED => 0x3,
1275 HO_HEADTYPE_CHANGED => 0x4,
1276};
1277
1278# 0230
1279# TODO: what is type?
1280sub homunculus_info {
1281 my ($self, $args) = @_;
1282 debug "homunculus_info type: $args->{type}\n", "homunculus";
1283 if ($args->{state} == HO_PRE_INIT) {
1284 my $state = $char->{homunculus}{state}
1285 if ($char->{homunculus} && $char->{homunculus}{ID} && $char->{homunculus}{ID} ne $args->{ID});
1286 $char->{homunculus} = Actor::get($args->{ID}) if ($char->{homunculus}{ID} ne $args->{ID});
1287 $char->{homunculus}{state} = $state if (defined $state);
1288 $char->{homunculus}{map} = $field->baseName;
1289 unless ($char->{slaves}{$char->{homunculus}{ID}}) {
1290 AI::SlaveManager::addSlave ($char->{homunculus});
1291 $char->{homunculus}{appear_time} = time;
1292 }
1293 } elsif ($args->{state} == HO_RELATIONSHIP_CHANGED) {
1294 $char->{homunculus}{intimacy} = $args->{val} if $char->{homunculus};
1295 } elsif ($args->{state} == HO_FULLNESS_CHANGED) {
1296 $char->{homunculus}{hunger} = $args->{val} if $char->{homunculus};
1297 } elsif ($args->{state} == HO_ACCESSORY_CHANGED) {
1298 $char->{homunculus}{accessory} = $args->{val} if $char->{homunculus};
1299 } elsif ($args->{state} == HO_HEADTYPE_CHANGED) {
1300 #
1301 }
1302}
1303
1304##
1305# minimap_indicator({bool show, Actor actor, int x, int y, int red, int green, int blue, int alpha [, int effect]})
1306# show: whether indicator is shown or cleared
1307# actor: @MODULE(Actor) who issued the indicator; or which Actor it's binded to
1308# x, y: indicator coordinates
1309# red, green, blue, alpha: indicator color
1310# effect: unknown, may be missing
1311#
1312# Minimap indicator.
1313sub minimap_indicator {
1314 my ($self, $args) = @_;
1315
1316 my $color_str = "[R:$args->{red}, G:$args->{green}, B:$args->{blue}, A:$args->{alpha}]";
1317 my $indicator = T("minimap indicator");
1318 if (defined $args->{type}) {
1319 unless ($args->{type} == 1 || $args->{type} == 2) {
1320 $indicator .= TF(" (unknown type %d)", $args->{type});
1321 }
1322 } elsif (defined $args->{effect}) {
1323 if ($args->{effect} == 1) {
1324 $indicator = T("*Quest!*");
1325 } elsif ($args->{effect}) { # 0 is no effect
1326 $indicator = TF("unknown effect %d", $args->{effect});
1327 }
1328 }
1329
1330 if ($args->{show}) {
1331 message TF("%s shown %s at location %d, %d " .
1332 "with the color %s\n", $args->{actor}, $indicator, @{$args}{qw(x y)}, $color_str),
1333 'effect';
1334 } else {
1335 message TF("%s cleared %s at location %d, %d " .
1336 "with the color %s\n", $args->{actor}, $indicator, @{$args}{qw(x y)}, $color_str),
1337 'effect';
1338 }
1339}
1340
1341sub local_broadcast {
1342 my ($self, $args) = @_;
1343 my $message = bytesToString($args->{message});
1344 my $color = uc(sprintf("%06x", $args->{color})); # hex code
1345 stripLanguageCode(\$message);
1346 chatLog("lb", "$message\n");# if ($config{logLocalBroadcast});
1347 message "$message\n", "schat";
1348 Plugins::callHook('packet_localBroadcast', {
1349 Msg => $message,
1350 color => $color
1351 });
1352}
1353
1354sub parse_sage_autospell {
1355 my ($self, $args) = @_;
1356
1357 $args->{skills} = [map { Skill->new(idn => $_) } sort { $a<=>$b } grep {$_}
1358 exists $args->{autoshadowspell_list}
1359 ? (unpack 'v*', $args->{autoshadowspell_list})
1360 : (unpack 'V*', $args->{autospell_list})
1361 ];
1362}
1363
1364sub reconstruct_sage_autospell {
1365 my ($self, $args) = @_;
1366
1367 my @skillIDs = map { $_->getIDN } $args->{skills};
1368 $args->{autoshadowspell_list} = pack 'v*', @skillIDs;
1369 $args->{autospell_list} = pack 'V*', @skillIDs;
1370}
1371
1372##
1373# sage_autospell({arrayref skills, int why})
1374# skills: list of @MODULE(Skill) instances
1375# why: unknown
1376#
1377# Skill list for Sage's Hindsight and Shadow Chaser's Auto Shadow Spell.
1378sub sage_autospell {
1379 my ($self, $args) = @_;
1380
1381 return unless $self->changeToInGameState;
1382
1383 my $msg = center(' ' . T('Auto Spell') . ' ', 40, '-') . "\n"
1384 . T(" # Skill\n")
1385 . (join '', map { swrite '@>>> @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<', [$_->getIDN, $_] } @{$args->{skills}})
1386 . ('-'x40) . "\n";
1387
1388 message $msg, 'list';
1389
1390 if ($config{autoSpell}) {
1391 my @autoSpells = split /\s*,\s*/, $config{autoSpell};
1392 for my $autoSpell (@autoSpells) {
1393 my $skill = new Skill(auto => $autoSpell);
1394 message 'Testing autoSpell ' . $autoSpell . "\n";
1395 if (!$config{autoSpell_safe} || List::Util::first { $_->getIDN == $skill->getIDN } @{$args->{skills}}) {
1396 if (defined $args->{why}) {
1397 $messageSender->sendSkillSelect($skill->getIDN, $args->{why});
1398 return;
1399 } else {
1400 $messageSender->sendAutoSpell($skill->getIDN);
1401 return;
1402 }
1403 }
1404 }
1405 error TF("Configured autoSpell (%s) not available.\n", $config{autoSpell});
1406 message T("Disable autoSpell_safe to use it anyway.\n"), 'hint';
1407 } else {
1408 message T("Configure autoSpell to automatically select skill for Auto Spell.\n"), 'hint';
1409 }
1410}
1411
1412sub show_eq {
1413 my ($self, $args) = @_;
1414
1415 my $jump = 26;
1416
1417 my $unpack_string = "v ";
1418 $unpack_string .= "v C2 v v C2 ";
1419 $unpack_string .= "a8 ";
1420 $unpack_string .= "a6"; #unimplemented in eA atm
1421
1422 if (exists $args->{robe}) { # check packet version
1423 $unpack_string .= "v "; # ??
1424 $jump += 2;
1425 }
1426
1427 for (my $i = 0; $i < length($args->{equips_info}); $i += $jump) {
1428 my ($index,
1429 $ID, $type, $identified, $type_equip, $equipped, $broken, $upgrade, # typical for nonstackables
1430 $cards,
1431 $expire) = unpack($unpack_string, substr($args->{equips_info}, $i));
1432
1433 my $item = {};
1434 $item->{index} = $index;
1435
1436 $item->{nameID} = $ID;
1437 $item->{type} = $type;
1438
1439 $item->{identified} = $identified;
1440 $item->{type_equip} = $type_equip;
1441 $item->{equipped} = $equipped;
1442 $item->{broken} = $broken;
1443 $item->{upgrade} = $upgrade;
1444
1445 $item->{cards} = $cards;
1446
1447 $item->{expire} = $expire;
1448
1449 message sprintf("%-20s: %s\n", $equipTypes_lut{$item->{equipped}}, itemName($item)), "list";
1450 debug "$index, $ID, $type, $identified, $type_equip, $equipped, $broken, $upgrade, $cards, $expire\n";
1451 }
1452}
1453
1454sub show_eq_msg_other {
1455 my ($self, $args) = @_;
1456 if ($args->{flag}) {
1457 message T("Allowed to view the other player's Equipment.\n");
1458 } else {
1459 message T("Not allowed to view the other player's Equipment.\n");
1460 }
1461}
1462
1463sub show_eq_msg_self {
1464 my ($self, $args) = @_;
1465 if ($args->{type}) {
1466 message T("Other players are allowed to view your Equipment.\n");
1467 } else {
1468 message T("Other players are not allowed to view your Equipment.\n");
1469 }
1470}
1471
1472# 043D
1473sub skill_post_delay {
1474 my ($self, $args) = @_;
1475
1476 my $skillName = (new Skill(idn => $args->{ID}))->getName;
1477 my $status = defined $statusName{'EFST_DELAY'} ? $statusName{'EFST_DELAY'} : 'Delay';
1478
1479 $char->setStatus($skillName." ".$status, 1, $args->{time});
1480}
1481
1482# TODO: known prefixes (chat domains): micc | ssss | blue | tool
1483# micc = micc<24 characters, this is the sender name. seems like it's null padded><hex color code><message>
1484# micc = Player Broadcast The struct: micc<23bytes player name+some hex><\x00><colour code><full message>
1485# The first player name is used for detecting the player name only according to the disassembled client.
1486# The full message contains the player name at the first 22 bytes
1487# TODO micc.* is currently unstricted, however .{24} and .{23} do not detect chinese with some reasons, please improve this regex if necessary
1488sub system_chat {
1489 my ($self, $args) = @_;
1490 my $message = bytesToString($args->{message});
1491 my $prefix;
1492 my $color;
1493 if ($message =~ s/^ssss//g) { # forces color yellow, or WoE indicator?
1494 $prefix = T('[WoE]');
1495 } elsif ($message =~ /^micc.*\0\0([0-9a-fA-F]{6})(.*)/) { #appears in twRO ## [micc][name][\x00\x00][unknown][\x00\x00][color][name][blablabla][message]
1496 ($color, $message) = $message =~ /^micc.*\0\0([0-9a-fA-F]{6})(.*)/;
1497 $prefix = T('[S]');
1498 } elsif ($message =~ /^micc.{12,24}([0-9a-fA-F]{6})(.*)/) {
1499 ($color, $message) = $message =~ /^micc.*([0-9a-fA-F]{6})(.*)/;
1500 $prefix = T('[S]');
1501 } elsif ($message =~ s/^blue//g) { # forces color blue
1502 $prefix = T('[S]');
1503 } elsif ($message =~ /^tool([0-9a-fA-F]{6})(.*)/) {
1504 ($color, $message) = $message =~ /^tool([0-9a-fA-F]{6})(.*)/;
1505 $prefix = T('[S]');
1506 } else {
1507 $prefix = T('[S]');
1508 }
1509 $message =~ s/\000//g; # remove null charachters
1510 $message =~ s/^ +//g; $message =~ s/ +$//g; # remove whitespace in the beginning and the end of $message
1511 stripLanguageCode(\$message);
1512 chatLog("s", "$message\n") if ($config{logSystemChat});
1513 # Translation Comment: System/GM chat
1514 message "$prefix $message\n", "schat";
1515 ChatQueue::add('gm', undef, undef, $message) if ($config{callSignGM});
1516
1517 Plugins::callHook('packet_sysMsg', {
1518 Msg => $message,
1519 MsgColor => $color,
1520 MsgUser => undef # TODO: implement this value, we can get this from "micc" messages by regex.
1521 });
1522}
1523
1524sub warp_portal_list {
1525 my ($self, $args) = @_;
1526
1527 # strip gat extension
1528 ($args->{memo1}) = $args->{memo1} =~ /^(.*)\.gat/;
1529 ($args->{memo2}) = $args->{memo2} =~ /^(.*)\.gat/;
1530 ($args->{memo3}) = $args->{memo3} =~ /^(.*)\.gat/;
1531 ($args->{memo4}) = $args->{memo4} =~ /^(.*)\.gat/;
1532 # Auto-detect saveMap
1533 if ($args->{type} == 26) {
1534 configModify('saveMap', $args->{memo2}) if ($args->{memo2} && $config{'saveMap'} ne $args->{memo2});
1535 } elsif ($args->{type} == 27) {
1536 configModify('saveMap', $args->{memo1}) if ($args->{memo1} && $config{'saveMap'} ne $args->{memo1});
1537 configModify( "memo$_", $args->{"memo$_"} ) foreach grep { $args->{"memo$_"} ne $config{"memo$_"} } 1 .. 4;
1538 }
1539
1540 $char->{warp}{type} = $args->{type};
1541 undef @{$char->{warp}{memo}};
1542 push @{$char->{warp}{memo}}, $args->{memo1} if $args->{memo1} ne "";
1543 push @{$char->{warp}{memo}}, $args->{memo2} if $args->{memo2} ne "";
1544 push @{$char->{warp}{memo}}, $args->{memo3} if $args->{memo3} ne "";
1545 push @{$char->{warp}{memo}}, $args->{memo4} if $args->{memo4} ne "";
1546
1547 my $msg = center(T(" Warp Portal "), 50, '-') ."\n".
1548 T("# Place Map\n");
1549 for (my $i = 0; $i < @{$char->{warp}{memo}}; $i++) {
1550 $msg .= swrite(
1551 "@< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<",
1552 [$i, $maps_lut{$char->{warp}{memo}[$i].'.rsw'}, $char->{warp}{memo}[$i]]);
1553 }
1554 $msg .= ('-'x50) . "\n";
1555 message $msg, "list";
1556
1557 if ($args->{type} == 26 && AI::inQueue('teleport')) {
1558 # We have already successfully used the Teleport skill.
1559 $messageSender->sendWarpTele(26, AI::args->{lv} == 2 ? "$config{saveMap}.gat" : "Random");
1560 AI::dequeue;
1561 }
1562}
1563
1564
1565# 0828,14
1566sub char_delete2_result {
1567 my ($self, $args) = @_;
1568 my $result = $args->{result};
1569 my $deleteDate = $args->{deleteDate};
1570
1571 if ($result && $deleteDate) {
1572 setCharDeleteDate($messageSender->{char_delete_slot}, $deleteDate);
1573 message TF("Your character will be delete, left %s\n", $chars[$messageSender->{char_delete_slot}]{deleteDate}), "connection";
1574 } elsif ($result == 0) {
1575 error T("That character already planned to be erased!\n");
1576 } elsif ($result == 3) {
1577 error T("Error in database of the server!\n");
1578 } elsif ($result == 4) {
1579 error T("To delete a character you must withdraw from the guild!\n");
1580 } elsif ($result == 5) {
1581 error T("To delete a character you must withdraw from the party!\n");
1582 } else {
1583 error TF("Unknown error when trying to delete the character! (Error number: %s)\n", $result);
1584 }
1585
1586 charSelectScreen;
1587}
1588
1589# 082A,10
1590sub char_delete2_accept_result {
1591 my ($self, $args) = @_;
1592 my $charID = $args->{charID};
1593 my $result = $args->{result};
1594
1595 if ($result == 1) { # Success
1596 if (defined $AI::temp::delIndex) {
1597 message TF("Character %s (%d) deleted.\n", $chars[$AI::temp::delIndex]{name}, $AI::temp::delIndex), "info";
1598 delete $chars[$AI::temp::delIndex];
1599 undef $AI::temp::delIndex;
1600 for (my $i = 0; $i < @chars; $i++) {
1601 delete $chars[$i] if ($chars[$i] && !scalar(keys %{$chars[$i]}))
1602 }
1603 } else {
1604 message T("Character deleted.\n"), "info";
1605 }
1606
1607 if (charSelectScreen() == 1) {
1608 $net->setState(3);
1609 $firstLoginMap = 1;
1610 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1611 $sentWelcomeMessage = 1;
1612 }
1613 return;
1614 } elsif ($result == 0) {
1615 error T("Enter your 6-digit birthday (YYMMDD) (e.g: 801122).\n");
1616 } elsif ($result == 2) {
1617 error T("Due to system settings, can not be deleted.\n");
1618 } elsif ($result == 3) {
1619 error T("A database error has occurred.\n");
1620 } elsif ($result == 4) {
1621 error T("You cannot delete this character at the moment.\n");
1622 } elsif ($result == 5) {
1623 error T("Your entered birthday does not match.\n");
1624 } elsif ($result == 7) {
1625 error T("Character Deletion has failed because you have entered an incorrect e-mail address.\n");
1626 } else {
1627 error TF("An unknown error has occurred. Error number %d\n", $result);
1628 }
1629
1630 undef $AI::temp::delIndex;
1631 if (charSelectScreen() == 1) {
1632 $net->setState(3);
1633 $firstLoginMap = 1;
1634 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1635 $sentWelcomeMessage = 1;
1636 }
1637}
1638
1639# 082C,14
1640sub char_delete2_cancel_result {
1641 my ($self, $args) = @_;
1642 my $result = $args->{result};
1643
1644 if ($result) {
1645 message T("Character is no longer scheduled to be deleted\n"), "connection";
1646 $chars[$messageSender->{char_delete_slot}]{deleteDate} = '';
1647 } elsif ($result == 2) {
1648 error T("Error in database of the server!\n");
1649 } else {
1650 error TF("Unknown error when trying to cancel the deletion of the character! (Error number: %s)\n", $result);
1651 }
1652
1653 charSelectScreen;
1654}
1655
1656# 013C
1657sub arrow_equipped {
1658 my ($self, $args) = @_;
1659 return unless changeToInGameState();
1660 return unless $args->{index};
1661 $char->{arrow} = $args->{index};
1662
1663 my $item = $char->inventory->getByServerIndex($args->{index});
1664 if ($item && $char->{equipment}{arrow} != $item) {
1665 $char->{equipment}{arrow} = $item;
1666 $item->{equipped} = 32768;
1667 $ai_v{temp}{waitForEquip}-- if $ai_v{temp}{waitForEquip};
1668 message TF("Arrow/Bullet equipped: %s (%d) x %s\n", $item->{name}, $item->{invIndex}, $item->{amount});
1669 }
1670}
1671
1672# 00AF, 07FA
1673sub inventory_item_removed {
1674 my ($self, $args) = @_;
1675 return unless changeToInGameState();
1676 my $item = $char->inventory->getByServerIndex($args->{index});
1677 my $reason = $args->{reason};
1678
1679 if ($reason) {
1680 if ($reason == 1) {
1681 debug TF("%s was used to cast the skill\n", $item->{name}), "inventory", 1;
1682 } elsif ($reason == 2) {
1683 debug TF("%s broke due to the refinement failed\n", $item->{name}), "inventory", 1;
1684 } elsif ($reason == 3) {
1685 debug TF("%s used in a chemical reaction\n", $item->{name}), "inventory", 1;
1686 } elsif ($reason == 4) {
1687 debug TF("%s was moved to the storage\n", $item->{name}), "inventory", 1;
1688 } elsif ($reason == 5) {
1689 debug TF("%s was moved to the cart\n", $item->{name}), "inventory", 1;
1690 } elsif ($reason == 6) {
1691 debug TF("%s was sold\n", $item->{name}), "inventory", 1;
1692 } elsif ($reason == 7) {
1693 debug TF("%s was consumed by Four Spirit Analysis skill\n", $item->{name}), "inventory", 1;
1694 } else {
1695 debug TF("%s was consumed by an unknown reason (reason number %s)\n", $item->{name}, $reason), "inventory", 1;
1696 }
1697 }
1698
1699 if ($item) {
1700 inventoryItemRemoved($item->{invIndex}, $args->{amount});
1701 Plugins::callHook('packet_item_removed', {index => $item->{invIndex}});
1702 }
1703}
1704
1705# 012B
1706sub cart_off {
1707 undef $cart{exists};
1708 message T("Cart released.\n"), "success";
1709}
1710
1711# 012D
1712sub shop_skill {
1713 my ($self, $args) = @_;
1714
1715 # Used the shop skill.
1716 my $number = $args->{number};
1717 message TF("You can sell %s items!\n", $number);
1718}
1719
1720# 01D0 (spirits), 01E1 (coins), 08CF (amulets)
1721sub revolving_entity {
1722 my ($self, $args) = @_;
1723
1724 # Monk Spirits or Gunslingers' coins or senior ninja
1725 my $sourceID = $args->{sourceID};
1726 my $entityNum = $args->{entity};
1727 my $entityElement = $elements_lut{$args->{type}} if ($args->{type} && $entityNum);
1728 my $entityType;
1729
1730 my $actor = Actor::get($sourceID);
1731 if ($args->{switch} eq '01D0') {
1732 # Translation Comment: Spirit sphere of the monks
1733 $entityType = T('spirit');
1734 } elsif ($args->{switch} eq '01E1') {
1735 # Translation Comment: Coin of the gunslinger
1736 $entityType = T('coin');
1737 } elsif ($args->{switch} eq '08CF') {
1738 # Translation Comment: Amulet of the warlock
1739 $entityType = T('amulet');
1740 } else {
1741 $entityType = T('entity unknown');
1742 }
1743
1744 if ($sourceID eq $accountID && $entityNum != $char->{spirits}) {
1745 $char->{spirits} = $entityNum;
1746 $char->{amuletType} = $entityElement;
1747 $entityElement ?
1748 # Translation Comment: Message displays following: quantity, the name of the entity and its element
1749 message TF("You have %s %s(s) of %s now\n", $entityNum, $entityType, $entityElement), "parseMsg_statuslook", 1 :
1750 # Translation Comment: Message displays following: quantity and the name of the entity
1751 message TF("You have %s %s(s) now\n", $entityNum, $entityType), "parseMsg_statuslook", 1;
1752 } elsif ($entityNum != $actor->{spirits}) {
1753 $actor->{spirits} = $entityNum;
1754 $actor->{amuletType} = $entityElement;
1755 $entityElement ?
1756 # Translation Comment: Message displays following: actor, quantity, the name of the entity and its element
1757 message TF("%s has %s %s(s) of %s now\n", $actor, $entityNum, $entityType, $entityElement), "parseMsg_statuslook", 1 :
1758 # Translation Comment: Message displays following: actor, quantity and the name of the entity
1759 message TF("%s has %s %s(s) now\n", $actor, $entityNum, $entityType), "parseMsg_statuslook", 1;
1760 }
1761}
1762
1763# 0977
1764sub monster_hp_info {
1765 my ($self, $args) = @_;
1766 my $monster = $monstersList->getByID($args->{ID});
1767 if ($monster) {
1768 $monster->{hp} = $args->{hp};
1769 $monster->{hp_max} = $args->{hp_max};
1770
1771 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";
1772 }
1773}
1774
1775##
1776# account_id({accountID})
1777#
1778# This is for what eA calls PacketVersion 9, they send the AID in a 'proper' packet
1779sub account_id {
1780 my ($self, $args) = @_;
1781 # the account ID is already unpacked into PLAIN TEXT when it gets to this function...
1782 # So lets not fuckup the $accountID since we need that later... someone will prolly have to fix this later on
1783 my $accountID = $args->{accountID};
1784 debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
1785}
1786
1787##
1788# marriage_partner_name({String name})
1789#
1790# Name of the partner character, sent to everyone around right before casting "I miss you".
1791sub marriage_partner_name {
1792 my ($self, $args) = @_;
1793
1794 message TF("Marriage partner name: %s\n", $args->{name});
1795}
1796
1797sub login_pin_code_request {
1798 # This is ten second-level password login for 2013/3/29 upgrading of twRO
1799 my ($self, $args) = @_;
1800 # flags:
1801 # 0 - correct
1802 # 1 - requested (already defined)
1803 # 2 - requested (not defined)
1804 # 3 - expired
1805 # 5 - invalid (official servers?)
1806 # 7 - disabled?
1807 # 8 - incorrect
1808 if ($args->{flag} == 0) { # removed check for seed 0, eA/rA/brA sends a normal seed.
1809 message T("PIN code is correct.\n"), "success";
1810 # call charSelectScreen
1811 if (charSelectScreen(1) == 1) {
1812 $firstLoginMap = 1;
1813 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1814 $sentWelcomeMessage = 1;
1815 }
1816 } elsif ($args->{flag} == 1) {
1817 # PIN code query request.
1818 $accountID = $args->{accountID};
1819 debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
1820
1821 message T("Server requested PIN password in order to select your character.\n"), "connection";
1822 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1823 $messageSender->sendLoginPinCode($args->{seed}, 0);
1824 } elsif ($args->{flag} == 2) {
1825 # PIN code has never been set before, so set it.
1826 warning T("PIN password is not set for this account.\n"), "connection";
1827 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1828
1829 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1830 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1831 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1832 }
1833 $messageSender->sendLoginPinCode($args->{seed}, 1);
1834 } elsif ($args->{flag} == 3) {
1835 # should we use the same one again? is it possible?
1836 warning T("PIN password expired.\n"), "connection";
1837 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1838
1839 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1840 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1841 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1842 }
1843 $messageSender->sendLoginPinCode($args->{seed}, 1);
1844 } elsif ($args->{flag} == 5) {
1845 # PIN code invalid.
1846 error T("PIN code is invalid, don't use sequences or repeated numbers.\n");
1847 # configModify('loginPinCode', '', 1);
1848 return if (!($self->queryAndSaveLoginPinCode(T("The login PIN code that you entered is invalid. Please re-enter your login PIN code."))));
1849 $messageSender->sendLoginPinCode($args->{seed}, 0);
1850 } elsif ($args->{flag} == 7) {
1851 # PIN code disabled.
1852 $accountID = $args->{accountID};
1853 debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
1854
1855 # call charSelectScreen
1856 $self->{lockCharScreen} = 0;
1857 if (charSelectScreen(1) == 1) {
1858 $firstLoginMap = 1;
1859 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1860 $sentWelcomeMessage = 1;
1861 }
1862 } elsif ($args->{flag} == 8) {
1863 # PIN code incorrect.
1864 error T("PIN code is incorrect.\n");
1865 #configModify('loginPinCode', '', 1);
1866 return if (!($self->queryAndSaveLoginPinCode(T("The login PIN code that you entered is incorrect. Please re-enter your login PIN code."))));
1867 $messageSender->sendLoginPinCode($args->{seed}, 0);
1868 } else {
1869 debug("login_pin_code_request: unknown flag $args->{flag}\n");
1870 }
1871
1872 $timeout{master}{time} = time;
1873}
1874
1875sub login_pin_new_code_result {
1876 my ($self, $args) = @_;
1877
1878 if ($args->{flag} == 2) {
1879 # PIN code invalid.
1880 error T("PIN code is invalid, don't use sequences or repeated numbers.\n");
1881 #configModify('loginPinCode', '', 1);
1882 return if (!($self->queryAndSaveLoginPinCode(T("PIN code is invalid, don't use sequences or repeated numbers.\n"))));
1883
1884 # there's a bug in bRO where you can use letters or symbols or even a string as your PIN code.
1885 # as a result this will render you unable to login again (forever?) using the official client
1886 # and this is detectable and can result in a permanent ban. we're using this code in order to
1887 # prevent this. - revok 17.12.2012
1888 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1889 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1890 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1891 }
1892
1893 $messageSender->sendLoginPinCode($args->{seed}, 0);
1894 }
1895}
1896
1897sub actor_status_active {
1898 my ($self, $args) = @_;
1899 return unless changeToInGameState();
1900 my ($type, $ID, $tick, $unknown1, $unknown2, $unknown3, $unknown4) = @{$args}{qw(type ID tick unknown1 unknown2 unknown3 unknown4)};
1901 my $flag = (exists $args->{flag}) ? $args->{flag} : 1;
1902 my $status = defined $statusHandle{$type} ? $statusHandle{$type} : "UNKNOWN_STATUS_$type";
1903 $cart{type} = $unknown1 if ($type == 673 && defined $unknown1 && ($ID eq $accountID)); # for Cart active
1904 $args->{skillName} = defined $statusName{$status} ? $statusName{$status} : $status;
1905# ($args->{actor} = Actor::get($ID))->setStatus($status, 1, $tick == 9999 ? undef : $tick, $args->{unknown1}); # need test for '08FF'
1906 ($args->{actor} = Actor::get($ID))->setStatus($status, $flag, $tick == 9999 ? undef : $tick);
1907 # Rolling Cutter counters.
1908 if ( $type == 0x153 && $char->{spirits} != $unknown1 ) {
1909 $char->{spirits} = $unknown1 || 0;
1910 if ( $ID eq $accountID ) {
1911 message TF( "You have %s %s(s) now\n", $char->{spirits}, 'counters' ), "parseMsg_statuslook", 1;
1912 } else {
1913 message TF( "%s has %s %s(s) now\n", $args->{actor}, $char->{spirits}, 'counters' ), "parseMsg_statuslook", 1;
1914 }
1915 }
1916}
1917
1918#099B
1919sub map_property3 {
1920 my ($self, $args) = @_;
1921
1922 if($config{'status_mapType'}){
1923 $char->setStatus(@$_) for map {[$_->[1], $args->{type} == $_->[0]]}
1924 grep { $args->{type} == $_->[0] || $char->{statuses}{$_->[1]} }
1925 map {[$_, defined $mapTypeHandle{$_} ? $mapTypeHandle{$_} : "UNKNOWN_MAPTYPE_$_"]}
1926 0 .. List::Util::max $args->{type}, keys %mapTypeHandle;
1927 }
1928
1929 $pvp = {6 => 1, 8 => 2, 19 => 3}->{$args->{type}};
1930 if ($pvp) {
1931 Plugins::callHook('pvp_mode', {
1932 pvp => $pvp # 1 PvP, 2 GvG, 3 Battleground
1933 });
1934 }
1935}
1936
1937#099F
1938sub area_spell_multiple2 {
1939 my ($self, $args) = @_;
1940
1941 # Area effect spells; including traps!
1942 my $len = $args->{len} - 4;
1943 my $spellInfo = $args->{spellInfo};
1944 my $msg = "";
1945 my $binID;
1946 my ($ID, $sourceID, $x, $y, $type, $range, $fail);
1947 for (my $i = 0; $i < $len; $i += 18) {
1948 $msg = substr($spellInfo, $i, 18);
1949 ($ID, $sourceID, $x, $y, $type, $range, $fail) = unpack('a4 a4 v3 X2 C2', $msg);
1950
1951 if ($spells{$ID} && $spells{$ID}{'sourceID'} eq $sourceID) {
1952 $binID = binFind(\@spellsID, $ID);
1953 $binID = binAdd(\@spellsID, $ID) if ($binID eq "");
1954 } else {
1955 $binID = binAdd(\@spellsID, $ID);
1956 }
1957
1958 $spells{$ID}{'sourceID'} = $sourceID;
1959 $spells{$ID}{'pos'}{'x'} = $x;
1960 $spells{$ID}{'pos'}{'y'} = $y;
1961 $spells{$ID}{'pos_to'}{'x'} = $x;
1962 $spells{$ID}{'pos_to'}{'y'} = $y;
1963 $spells{$ID}{'binID'} = $binID;
1964 $spells{$ID}{'type'} = $type;
1965 if ($type == 0x81) {
1966 message TF("%s opened Warp Portal on (%d, %d)\n", getActorName($sourceID), $x, $y), "skill";
1967 }
1968 debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)\n", "skill", 2;
1969 }
1970
1971 Plugins::callHook('packet_areaSpell', {
1972 fail => $fail,
1973 sourceID => $sourceID,
1974 type => $type,
1975 x => $x,
1976 y => $y
1977 });
1978}
1979
1980#09CA
1981sub area_spell_multiple3 {
1982 my ($self, $args) = @_;
1983
1984 # Area effect spells; including traps!
1985 my $len = $args->{len} - 4;
1986 my $spellInfo = $args->{spellInfo};
1987 my $msg = "";
1988 my $binID;
1989 my ($ID, $sourceID, $x, $y, $type, $range, $fail);
1990 for (my $i = 0; $i < $len; $i += 19) {
1991 $msg = substr($spellInfo, $i, 19);
1992 ($ID, $sourceID, $x, $y, $type, $range, $fail) = unpack('a4 a4 v3 X3 C2', $msg);
1993
1994 if ($spells{$ID} && $spells{$ID}{'sourceID'} eq $sourceID) {
1995 $binID = binFind(\@spellsID, $ID);
1996 $binID = binAdd(\@spellsID, $ID) if ($binID eq "");
1997 } else {
1998 $binID = binAdd(\@spellsID, $ID);
1999 }
2000
2001 $spells{$ID}{'sourceID'} = $sourceID;
2002 $spells{$ID}{'pos'}{'x'} = $x;
2003 $spells{$ID}{'pos'}{'y'} = $y;
2004 $spells{$ID}{'pos_to'}{'x'} = $x;
2005 $spells{$ID}{'pos_to'}{'y'} = $y;
2006 $spells{$ID}{'binID'} = $binID;
2007 $spells{$ID}{'type'} = $type;
2008 if ($type == 0x81) {
2009 message TF("%s opened Warp Portal on (%d, %d)\n", getActorName($sourceID), $x, $y), "skill";
2010 }
2011 debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)\n", "skill", 2;
2012 }
2013
2014 Plugins::callHook('packet_areaSpell', {
2015 fail => $fail,
2016 sourceID => $sourceID,
2017 type => $type,
2018 x => $x,
2019 y => $y
2020 });
2021}
2022
2023sub sync_request_ex {
2024 my ($self, $args) = @_;
2025
2026 # Debug Log
2027 # message "Received Sync Ex : 0x" . $args->{switch} . "\n";
2028
2029 # Computing Sync Ex - By Fr3DBr
2030 my $PacketID = $args->{switch};
2031
2032 # Getting Sync Ex Reply ID from Table
2033 my $SyncID = $self->{sync_ex_reply}->{$PacketID};
2034
2035 # Cleaning Leading Zeros
2036 $PacketID =~ s/^0+//;
2037
2038 # Cleaning Leading Zeros
2039 $SyncID =~ s/^0+//;
2040
2041 # Debug Log
2042 #error sprintf("Received Ex Packet ID : 0x%s => 0x%s\n", $PacketID, $SyncID);
2043
2044 # Converting ID to Hex Number
2045 $SyncID = hex($SyncID);
2046
2047 # Dispatching Sync Ex Reply
2048 $messageSender->sendReplySyncRequestEx($SyncID);
2049}
2050
2051sub cash_shop_list {
2052 my ($self, $args) = @_;
2053 my $tabcode = $args->{tabcode};
2054 my $jump = 6;
2055 my $unpack_string = "v V";
2056 # CASHSHOP_TAB_NEW => 0x0,
2057 # CASHSHOP_TAB_POPULAR => 0x1,
2058 # CASHSHOP_TAB_LIMITED => 0x2,
2059 # CASHSHOP_TAB_RENTAL => 0x3,
2060 # CASHSHOP_TAB_PERPETUITY => 0x4,
2061 # CASHSHOP_TAB_BUFF => 0x5,
2062 # CASHSHOP_TAB_RECOVERY => 0x6,
2063 # CASHSHOP_TAB_ETC => 0x7
2064 # CASHSHOP_TAB_MAX => 8
2065 my %cashitem_tab = (
2066 0 => 'New',
2067 1 => 'Popular',
2068 2 => 'Limited',
2069 3 => 'Rental',
2070 4 => 'Perpetuity',
2071 5 => 'Buff',
2072 6 => 'Recovery',
2073 7 => 'Etc',
2074 );
2075 debug TF("%s\n" .
2076 "# Name Price\n",
2077 center(' Tab: ' . $cashitem_tab{$tabcode} . ' ', 44, '-')), "list";
2078 for (my $i = 0; $i < length($args->{itemInfo}); $i += $jump) {
2079 my ($ID, $price) = unpack($unpack_string, substr($args->{itemInfo}, $i));
2080 my $name = itemNameSimple($ID);
2081 push(@{$cashShop{list}[$tabcode]}, {item_id => $ID, price => $price}); # add to cashshop
2082 debug(swrite(
2083 "@<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>C",
2084 [$i, $name, formatNumber($price)]),
2085 "list");
2086
2087 }
2088}
2089
2090sub cash_shop_open_result {
2091 my ($self, $args) = @_;
2092 #'0845' => ['cash_window_shop_open', 'v2', [qw(cash_points kafra_points)]],
2093 message TF("Cash Points: %sC - Kafra Points: %sC\n", formatNumber ($args->{cash_points}), formatNumber ($args->{kafra_points}));
2094 $cashShop{points} = {
2095 cash => $args->{cash_points},
2096 kafra => $args->{kafra_points}
2097 };
2098}
2099
2100sub cash_shop_buy_result {
2101 my ($self, $args) = @_;
2102 # TODO: implement result messages:
2103 # SUCCESS = 0x0,
2104 # WRONG_TAB? = 0x1, // we should take care with this, as it's detectable by the server
2105 # SHORTTAGE_CASH = 0x2,
2106 # UNKONWN_ITEM = 0x3,
2107 # INVENTORY_WEIGHT = 0x4,
2108 # INVENTORY_ITEMCNT = 0x5,
2109 # RUNE_OVERCOUNT = 0x9,
2110 # EACHITEM_OVERCOUNT = 0xa,
2111 # UNKNOWN = 0xb,
2112 if ($args->{result} > 0) {
2113 error TF("Error while buying %s from cash shop. Error code: %s\n", itemNameSimple($args->{item_id}), $args->{result});
2114 } else {
2115 message TF("Bought %s from cash shop. Current CASH: %s\n", itemNameSimple($args->{item_id}), formatNumber($args->{updated_points})), "success";
2116 $cashShop{points}->{cash} = $args->{updated_points};
2117 }
2118
2119 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}));
2120
2121
2122}
2123
2124sub player_equipment {
2125 my ($self, $args) = @_;
2126
2127 my ($sourceID, $type, $ID1, $ID2) = @{$args}{qw(sourceID type ID1 ID2)};
2128 my $player = ($sourceID ne $accountID)? $playersList->getByID($sourceID) : $char;
2129 return unless $player;
2130
2131 if ($type == 0) {
2132 # Player changed job
2133 $player->{jobID} = $ID1;
2134
2135 } elsif ($type == 2) {
2136 if ($ID1 ne $player->{weapon}) {
2137 message TF("%s changed Weapon to %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
2138 $player->{weapon} = $ID1;
2139 }
2140 if ($ID2 ne $player->{shield}) {
2141 message TF("%s changed Shield to %s\n", $player, itemName({nameID => $ID2})), "parseMsg_statuslook", 2;
2142 $player->{shield} = $ID2;
2143 }
2144 } elsif ($type == 3) {
2145 $player->{headgear}{low} = $ID1;
2146 } elsif ($type == 4) {
2147 $player->{headgear}{top} = $ID1;
2148 } elsif ($type == 5) {
2149 $player->{headgear}{mid} = $ID1;
2150 } elsif ($type == 9) {
2151 if ($player->{shoes} && $ID1 ne $player->{shoes}) {
2152 message TF("%s changed Shoes to: %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
2153 }
2154 $player->{shoes} = $ID1;
2155 }
2156}
2157
2158sub progress_bar {
2159 my($self, $args) = @_;
2160 message TF("Progress bar loading (time: %d).\n", $args->{time}), 'info';
2161 $char->{progress_bar} = 1;
2162 $taskManager->add(
2163 new Task::Chained(tasks => [new Task::Wait(seconds => $args->{time}),
2164 new Task::Function(function => sub {
2165 $messageSender->sendProgress();
2166 message TF("Progress bar finished.\n"), 'info';
2167 $char->{progress_bar} = 0;
2168 $_[0]->setDone;
2169 })]));
2170}
2171
2172sub progress_bar_stop {
2173 my($self, $args) = @_;
2174 message TF("Progress bar finished.\n", 'info');
2175}
2176
2177# 02B1
2178sub quest_all_list {
2179 my ($self, $args) = @_;
2180 $questList = {};
2181 for (my $i = 8; $i < $args->{amount}*5+8; $i += 5) {
2182 my ($questID, $active) = unpack('V C', substr($args->{RAW_MSG}, $i, 5));
2183 $questList->{$questID}->{active} = $active;
2184 debug "$questID $active\n", "info";
2185 }
2186}
2187
2188# 02B2
2189# note: this packet shows all quests + their missions and has variable length
2190sub quest_all_mission {
2191 my ($self, $args) = @_;
2192 debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
2193 for (my $i = 8; $i < $args->{amount}*104+8; $i += 104) {
2194 my ($questID, $time_start, $time, $mission_amount) = unpack('V3 v', substr($args->{RAW_MSG}, $i, 14));
2195 my $quest = \%{$questList->{$questID}};
2196 $quest->{time_start} = $time_start;
2197 $quest->{time} = $time;
2198 debug "$questID $time_start $time $mission_amount\n", "info";
2199 for (my $j = 0; $j < $mission_amount; $j++) {
2200 my ($mobID, $count, $mobName) = unpack('V v Z24', substr($args->{RAW_MSG}, 14+$i+$j*30, 30));
2201 my $mission = \%{$quest->{missions}->{$mobID}};
2202 $mission->{mobID} = $mobID;
2203 $mission->{count} = $count;
2204 $mission->{mobName} = bytesToString($mobName);
2205 debug "- $mobID $count $mobName\n", "info";
2206 }
2207 }
2208}
2209
2210# 02B3
2211# note: this packet shows all missions for 1 quest and has fixed length
2212sub quest_add {
2213 my ($self, $args) = @_;
2214 my $questID = $args->{questID};
2215 my $quest = \%{$questList->{$questID}};
2216
2217 unless (%$quest) {
2218 message TF("Quest: %s has been added.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
2219 }
2220
2221 $quest->{time_start} = $args->{time_start};
2222 $quest->{time} = $args->{time};
2223 $quest->{active} = $args->{active};
2224 debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
2225 for (my $i = 0; $i < $args->{amount}; $i++) {
2226 my ($mobID, $count, $mobName) = unpack('V v Z24', substr($args->{RAW_MSG}, 17+$i*30, 30));
2227 my $mission = \%{$quest->{missions}->{$mobID}};
2228 $mission->{mobID} = $mobID;
2229 $mission->{count} = $count;
2230 $mission->{mobName} = bytesToString($mobName);
2231 debug "- $mobID $count $mobName\n", "info";
2232 }
2233}
2234
2235# 02B4
2236sub quest_delete {
2237 my ($self, $args) = @_;
2238 my $questID = $args->{questID};
2239 message TF("Quest: %s has been deleted.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
2240 delete $questList->{$questID};
2241}
2242
2243sub parse_quest_update_mission_hunt {
2244 my ($self, $args) = @_;
2245 @{$args->{mobs}} = map {
2246 my %result; @result{qw(questID mobID count)} = unpack 'V2 v', $_; \%result
2247 } unpack '(a10)*', $args->{mobInfo};
2248}
2249
2250sub reconstruct_quest_update_mission_hunt {
2251 my ($self, $args) = @_;
2252 $args->{mobInfo} = pack '(a10)*', map { pack 'V2 v', @{$_}{qw(questID mobID count)} } @{$args->{mobs}};
2253}
2254
2255sub parse_quest_update_mission_hunt_v2 {
2256 my ($self, $args) = @_;
2257 @{$args->{mobs}} = map {
2258 my %result; @result{qw(questID mobID goal count)} = unpack 'V2 v2', $_; \%result
2259 } unpack '(a12)*', $args->{mobInfo};
2260}
2261
2262sub reconstruct_quest_update_mission_hunt_v2 {
2263 my ($self, $args) = @_;
2264 $args->{mobInfo} = pack '(a12)*', map { pack 'V2 v2', @{$_}{qw(questID mobID goal count)} } @{$args->{mobs}};
2265}
2266
2267# 02B5
2268sub quest_update_mission_hunt {
2269 my ($self, $args) = @_;
2270 my ($questID, $mobID, $goal, $count) = unpack('V2 v2', substr($args->{RAW_MSG}, 6));
2271 my $quest = \%{$questList->{$questID}};
2272 my $mission = \%{$quest->{missions}->{$mobID}};
2273 $mission->{goal} = $goal;
2274 $mission->{count} = $count;
2275 debug "- $questID $mobID $count $goal\n", "info";
2276}
2277
2278# 02B7
2279sub quest_active {
2280 my ($self, $args) = @_;
2281 my $questID = $args->{questID};
2282
2283 message $args->{active}
2284 ? TF("Quest %s is now active.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
2285 : TF("Quest %s is now inactive.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
2286 , "info";
2287
2288 $questList->{$args->{questID}}->{active} = $args->{active};
2289}
2290
2291sub forge_list {
2292 my ($self, $args) = @_;
2293
2294 message T("========Forge List========\n");
2295 for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 8) {
2296 my $viewID = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
2297 message "$viewID $items_lut{$viewID}\n";
2298 # always 0x0012
2299 #my $unknown = unpack("v1", substr($args->{RAW_MSG}, $i+2, 2));
2300 # ???
2301 #my $charID = substr($args->{RAW_MSG}, $i+4, 4);
2302 }
2303 message "=========================\n";
2304}
2305
23061;