· 9 years ago · May 05, 2017, 05:30 AM
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;
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
61use constant {
62 REFUSE_INVALID_ID => 0x0,
63 REFUSE_INVALID_PASSWD => 0x1,
64 REFUSE_ID_EXPIRED => 0x2,
65 ACCEPT_ID_PASSWD => 0x3,
66 REFUSE_NOT_CONFIRMED => 0x4,
67 REFUSE_INVALID_VERSION => 0x5,
68 REFUSE_BLOCK_TEMPORARY => 0x6,
69 REFUSE_BILLING_NOT_READY => 0x7,
70 REFUSE_NONSAKRAY_ID_BLOCKED => 0x8,
71 REFUSE_BAN_BY_DBA => 0x9,
72 REFUSE_EMAIL_NOT_CONFIRMED => 0xa,
73 REFUSE_BAN_BY_GM => 0xb,
74 REFUSE_TEMP_BAN_FOR_DBWORK => 0xc,
75 REFUSE_SELF_LOCK => 0xd,
76 REFUSE_NOT_PERMITTED_GROUP => 0xe,
77 REFUSE_WAIT_FOR_SAKRAY_ACTIVE => 0xf,
78 REFUSE_NOT_CHANGED_PASSWD => 0x10,
79 REFUSE_BLOCK_INVALID => 0x11,
80 REFUSE_WARNING => 0x12,
81 REFUSE_NOT_OTP_USER_INFO => 0x13,
82 REFUSE_OTP_AUTH_FAILED => 0x14,
83 REFUSE_SSO_AUTH_FAILED => 0x15,
84 REFUSE_NOT_ALLOWED_IP_ON_TESTING => 0x16,
85 REFUSE_OVER_BANDWIDTH => 0x17,
86 REFUSE_OVER_USERLIMIT => 0x18,
87 REFUSE_UNDER_RESTRICTION => 0x19,
88 REFUSE_BY_OUTER_SERVER => 0x1a,
89 REFUSE_BY_UNIQUESERVER_CONNECTION => 0x1b,
90 REFUSE_BY_AUTHSERVER_CONNECTION => 0x1c,
91 REFUSE_BY_BILLSERVER_CONNECTION => 0x1d,
92 REFUSE_BY_AUTH_WAITING => 0x1e,
93 REFUSE_DELETED_ACCOUNT => 0x63,
94 REFUSE_ALREADY_CONNECT => 0x64,
95 REFUSE_TEMP_BAN_HACKING_INVESTIGATION => 0x65,
96 REFUSE_TEMP_BAN_BUG_INVESTIGATION => 0x66,
97 REFUSE_TEMP_BAN_DELETING_CHAR => 0x67,
98 REFUSE_TEMP_BAN_DELETING_SPOUSE_CHAR => 0x68,
99 REFUSE_USER_PHONE_BLOCK => 0x69,
100 ACCEPT_LOGIN_USER_PHONE_BLOCK => 0x6a,
101 ACCEPT_LOGIN_CHILD => 0x6b,
102 REFUSE_IS_NOT_FREEUSER => 0x6c,
103 REFUSE_INVALID_ONETIMELIMIT => 0x6d,
104 REFUSE_CHANGE_PASSWD_FORCE => 0x6e,
105 REFUSE_OUTOFDATE_PASSWORD => 0x6f,
106 REFUSE_NOT_CHANGE_ACCOUNTID => 0xf0,
107 REFUSE_NOT_CHANGE_CHARACTERID => 0xf1,
108 REFUSE_SSO_AUTH_BLOCK_USER => 0x1394,
109 REFUSE_SSO_AUTH_GAME_APPLY => 0x1395,
110 REFUSE_SSO_AUTH_INVALID_GAMENUM => 0x1396,
111 REFUSE_SSO_AUTH_INVALID_USER => 0x1397,
112 REFUSE_SSO_AUTH_OTHERS => 0x1398,
113 REFUSE_SSO_AUTH_INVALID_AGE => 0x1399,
114 REFUSE_SSO_AUTH_INVALID_MACADDRESS => 0x139a,
115 REFUSE_SSO_AUTH_BLOCK_ETERNAL => 0x13c6,
116 REFUSE_SSO_AUTH_BLOCK_ACCOUNT_STEAL => 0x13c7,
117 REFUSE_SSO_AUTH_BLOCK_BUG_INVESTIGATION => 0x13c8,
118 REFUSE_SSO_NOT_PAY_USER => 0x13ba,
119 REFUSE_SSO_ALREADY_LOGIN_USER => 0x13bb,
120 REFUSE_SSO_CURRENT_USED_USER => 0x13bc,
121 REFUSE_SSO_OTHER_1 => 0x13bd,
122 REFUSE_SSO_DROP_USER => 0x13be,
123 REFUSE_SSO_NOTHING_USER => 0x13bf,
124 REFUSE_SSO_OTHER_2 => 0x13c0,
125 REFUSE_SSO_WRONG_RATETYPE_1 => 0x13c1,
126 REFUSE_SSO_EXTENSION_PCBANG_TIME => 0x13c2,
127 REFUSE_SSO_WRONG_RATETYPE_2 => 0x13c3,
128};
129
130######################################
131### CATEGORY: Class methods
132######################################
133
134# Just a wrapper for SUPER::parse.
135sub parse {
136 my $self = shift;
137 my $args = $self->SUPER::parse(@_);
138
139 if ($args && $config{debugPacket_received} == 3 &&
140 existsInList($config{'debugPacket_include'}, $args->{switch})) {
141 my $packet = $self->{packet_list}{$args->{switch}};
142 my ($name, $packString, $varNames) = @{$packet};
143
144 my @vars = ();
145 for my $varName (@{$varNames}) {
146 message "$varName = $args->{$varName}\n";
147 }
148 }
149
150 return $args;
151}
152
153#######################################
154### CATEGORY: Private class methods
155#######################################
156
157##
158# int Network::Receive::queryLoginPinCode([String message])
159# Returns: login PIN code, or undef if cancelled
160# Ensures: length(result) in 4..8
161#
162# Request login PIN code from user.
163sub queryLoginPinCode {
164 my $message = $_[0] || T("You've never set a login PIN code before.\nPlease enter a new login PIN code:");
165 do {
166 my $input = $interface->query($message, isPassword => 1,);
167 if (!defined($input)) {
168 quit();
169 return;
170 } else {
171 if ($input !~ /^\d+$/) {
172 $interface->errorDialog(T("The PIN code may only contain digits."));
173 } elsif ((length($input) <= 3) || (length($input) >= 9)) {
174 $interface->errorDialog(T("The PIN code must be between 4 and 9 characters."));
175 } else {
176 return $input;
177 }
178 }
179 } while (1);
180}
181
182##
183# boolean Network::Receive->queryAndSaveLoginPinCode([String message])
184# Returns: true on success
185#
186# Request login PIN code from user and save it in config.
187sub queryAndSaveLoginPinCode {
188 my ($self, $message) = @_;
189 my $pin = queryLoginPinCode($message);
190 if (defined $pin) {
191 configModify('loginPinCode', $pin, silent => 1);
192 return 1;
193 } else {
194 return 0;
195 }
196}
197
198sub changeToInGameState {
199 if ($net->version() == 1) {
200 if ($accountID && UNIVERSAL::isa($char, 'Actor::You')) {
201 if ($net->getState() != Network::IN_GAME) {
202 $net->setState(Network::IN_GAME);
203 }
204 return 1;
205 } else {
206 if ($net->getState() != Network::IN_GAME_BUT_UNINITIALIZED) {
207 $net->setState(Network::IN_GAME_BUT_UNINITIALIZED);
208 if ($config{verbose} && $messageSender && !$sentWelcomeMessage) {
209 $messageSender->injectAdminMessage("Please relogin to enable X-${Settings::NAME}.");
210 $sentWelcomeMessage = 1;
211 }
212 }
213 return 0;
214 }
215 } else {
216 return 1;
217 }
218}
219
220### Packet inner struct handlers
221
222# The block size in the received_characters packet varies from server to server.
223# This method may be overrided in other ServerType handlers to return
224# the correct block size.
225sub received_characters_blockSize {
226 if ($masterServer && $masterServer->{charBlockSize}) {
227 return $masterServer->{charBlockSize};
228 } else {
229 return 106;
230 }
231}
232
233# The length must exactly match charBlockSize, as it's used to construct packets.
234sub received_characters_unpackString {
235 for ($masterServer && $masterServer->{charBlockSize}) {
236 # unknown purpose (0 = disabled, otherwise displays "Add-Ons" sidebar) (from rA)
237 # change $hairstyle
238 return 'a4 V9 v V2 v4 V v9 Z24 C8 v Z16 V x4 x4 x4 x1' if $_ == 147;
239 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4 x4 C' if $_ == 145;
240 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4 x4' if $_ == 144;
241 # change slot feature
242 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4 x4' if $_ == 140;
243 # robe
244 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V x4' if $_ == 136;
245 # delete date
246 return 'a4 V9 v V2 v14 Z24 C8 v Z16 V' if $_ == 132;
247 return 'a4 V9 v V2 v14 Z24 C8 v Z16' if $_ == 128;
248 # bRO (bitfrost update)
249 return 'a4 V9 v V2 v14 Z24 C8 v Z12' if $_ == 124;
250 return 'a4 V9 v V2 v14 Z24 C6 v2 x4' if $_ == 116; # TODO: (missing 2 last bytes)
251 return 'a4 V9 v V2 v14 Z24 C6 v2' if $_ == 112;
252 return 'a4 V9 v17 Z24 C6 v2' if $_ == 108;
253 return 'a4 V9 v17 Z24 C6 v' if $_ == 106 || !$_;
254 die "Unknown charBlockSize: $_";
255 }
256}
257
258### Parse/reconstruct callbacks and packet handlers
259
260sub parse_account_server_info {
261 my ($self, $args) = @_;
262
263 if (length $args->{lastLoginIP} == 4 && $args->{lastLoginIP} ne "\0"x4) {
264 $args->{lastLoginIP} = inet_ntoa($args->{lastLoginIP});
265 } else {
266 delete $args->{lastLoginIP};
267 }
268
269 @{$args->{servers}} = map {
270 my %server;
271 @server{qw(ip port name users display)} = unpack 'a4 v Z20 v2 x2', $_;
272 if ($masterServer && $masterServer->{private}) {
273 $server{ip} = $masterServer->{ip};
274 } else {
275 $server{ip} = inet_ntoa($server{ip});
276 }
277 $server{name} = bytesToString($server{name});
278 \%server
279 } unpack '(a32)*', $args->{serverInfo};
280}
281
282sub reconstruct_account_server_info {
283 my ($self, $args) = @_;
284
285 $args->{lastLoginIP} = inet_aton($args->{lastLoginIP});
286
287 $args->{serverInfo} = pack '(a32)*', map { pack(
288 'a4 v Z20 v2 x2',
289 inet_aton($_->{ip}),
290 $_->{port},
291 stringToBytes($_->{name}),
292 @{$_}{qw(users display)},
293 ) } @{$args->{servers}};
294}
295
296sub account_server_info {
297 my ($self, $args) = @_;
298
299 $net->setState(2);
300 undef $conState_tries;
301 $sessionID = $args->{sessionID};
302 $accountID = $args->{accountID};
303 $sessionID2 = $args->{sessionID2};
304 # Account sex should only be 0 (female) or 1 (male)
305 # inRO gives female as 2 but expects 0 back
306 # do modulus of 2 here to fix?
307 # FIXME: we should check exactly what operation the client does to the number given
308 $accountSex = $args->{accountSex} % 2;
309 $accountSex2 = ($config{'sex'} ne "") ? $config{'sex'} : $accountSex;
310
311 # any servers with lastLoginIP lastLoginTime?
312 # message TF("Last login: %s from %s\n", @{$args}{qw(lastLoginTime lastLoginIP)}) if ...;
313
314 message
315 center(T(" Account Info "), 34, '-') ."\n" .
316 swrite(
317 T("Account ID: \@<<<<<<<<< \@<<<<<<<<<<\n" .
318 "Sex: \@<<<<<<<<<<<<<<<<<<<<<\n" .
319 "Session ID: \@<<<<<<<<< \@<<<<<<<<<<\n" .
320 " \@<<<<<<<<< \@<<<<<<<<<<\n"),
321 [unpack('V',$accountID), getHex($accountID), $sex_lut{$accountSex}, unpack('V',$sessionID), getHex($sessionID),
322 unpack('V',$sessionID2), getHex($sessionID2)]) .
323 ('-'x34) . "\n", 'connection';
324
325 @servers = @{$args->{servers}};
326
327 my $msg = center(T(" Servers "), 53, '-') ."\n" .
328 T("# Name Users IP Port\n");
329 for (my $num = 0; $num < @servers; $num++) {
330 $msg .= swrite(
331 "@<< @<<<<<<<<<<<<<<<<<<<< @<<<<< @<<<<<<<<<<<<<< @<<<<<",
332 [$num, $servers[$num]{name}, $servers[$num]{users}, $servers[$num]{ip}, $servers[$num]{port}]);
333 }
334 $msg .= ('-'x53) . "\n";
335 message $msg, "connection";
336
337 if ($net->version != 1) {
338 message T("Closing connection to Account Server\n"), 'connection';
339 $net->serverDisconnect();
340 if (!$masterServer->{charServer_ip} && $config{server} eq "") {
341 my @serverList;
342 foreach my $server (@servers) {
343 push @serverList, $server->{name};
344 }
345 my $ret = $interface->showMenu(
346 T("Please select your login server."),
347 \@serverList,
348 title => T("Select Login Server"));
349 if ($ret == -1) {
350 quit();
351 } else {
352 main::configModify('server', $ret, 1);
353 }
354
355 } elsif ($masterServer->{charServer_ip}) {
356 message TF("Forcing connect to char server %s: %s\n", $masterServer->{charServer_ip}, $masterServer->{charServer_port}), 'connection';
357 }
358 }
359
360 # FIXME better support for multiple received_characters packets
361 undef @chars;
362 if ($config{'XKore'} eq '1') {
363 $incomingMessages->nextMessageMightBeAccountID();
364 }
365}
366
367sub connection_refused {
368 my ($self, $args) = @_;
369
370 error TF("The server has denied your connection (error: %d).\n", $args->{error}), 'connection';
371}
372
373*actor_exists = *actor_display_compatibility;
374*actor_connected = *actor_display_compatibility;
375*actor_moved = *actor_display_compatibility;
376*actor_spawned = *actor_display_compatibility;
377sub actor_display_compatibility {
378 my ($self, $args) = @_;
379 # compatibility; TODO do it in PacketParser->parse?
380 Plugins::callHook('packet_pre/actor_display', $args);
381 &actor_display unless $args->{return};
382 Plugins::callHook('packet/actor_display', $args);
383}
384
385# This function is a merge of actor_exists, actor_connected, actor_moved, etc...
386sub actor_display {
387 my ($self, $args) = @_;
388 return unless changeToInGameState();
389 my ($actor, $mustAdd);
390
391
392 #### Initialize ####
393
394 my $nameID = unpack("V", $args->{ID});
395
396 if ($args->{switch} eq "0086") {
397 # Message 0086 contains less information about the actor than other similar
398 # messages. So we use the existing actor information.
399 my $coordsArg = $args->{coords};
400 my $tickArg = $args->{tick};
401 $args = Actor::get($args->{ID})->deepCopy();
402 # Here we overwrite the $args data with the 0086 packet data.
403 $args->{switch} = "0086";
404 $args->{coords} = $coordsArg;
405 $args->{tick} = $tickArg; # lol tickcount what do we do with that? debug "tick: " . $tickArg/1000/3600/24 . "\n";
406 }
407
408 my (%coordsFrom, %coordsTo);
409 if (length $args->{coords} == 6) {
410 # Actor Moved
411 makeCoordsFromTo(\%coordsFrom, \%coordsTo, $args->{coords}); # body dir will be calculated using the vector
412 } else {
413 # Actor Spawned/Exists
414 makeCoordsDir(\%coordsTo, $args->{coords}, \$args->{body_dir});
415 %coordsFrom = %coordsTo;
416 }
417
418 # Remove actors that are located outside the map
419 # This may be caused by:
420 # - server sending us false actors
421 # - actor packets not being parsed correctly
422 if (defined $field && ($field->isOffMap($coordsFrom{x}, $coordsFrom{y}) || $field->isOffMap($coordsTo{x}, $coordsTo{y}))) {
423 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());
424 return;
425 }
426
427 # Remove actors with a distance greater than removeActorWithDistance. Useful for vending (so you don't spam
428 # too many packets in prontera and cause server lag). As a side effect, you won't be able to "see" actors
429 # beyond removeActorWithDistance.
430 if ($config{removeActorWithDistance}) {
431 if ((my $block_dist = blockDistance($char->{pos_to}, \%coordsTo)) > ($config{removeActorWithDistance})) {
432 my $nameIdTmp = unpack("V", $args->{ID});
433 debug "Removed out of sight actor $nameIdTmp at ($coordsTo{x}, $coordsTo{y}) (distance: $block_dist)\n";
434 return;
435 }
436 }
437=pod
438 # Zealotus bug
439 if ($args->{type} == 1200) {
440 open DUMP, ">> test_Zealotus.txt";
441 print DUMP "Zealotus: " . $nameID . "\n";
442 print DUMP Dumper($args);
443 close DUMP;
444 }
445=cut
446
447 #### Step 0: determine object type ####
448 my $object_class;
449 if (defined $args->{object_type}) {
450 if ($args->{type} == 45) { # portals use the same object_type as NPCs
451 $object_class = 'Actor::Portal';
452 } else {
453 $object_class = {
454 PC_TYPE, 'Actor::Player',
455 # NPC_TYPE? # not encountered, NPCs are NPC_EVT_TYPE
456 # SKILL_TYPE? # not encountered
457 # UNKNOWN_TYPE? # not encountered
458 NPC_MOB_TYPE, 'Actor::Monster',
459 NPC_EVT_TYPE, 'Actor::NPC', # both NPCs and portals
460 NPC_PET_TYPE, 'Actor::Pet',
461 NPC_HO_TYPE, 'Actor::Slave',
462 # NPC_MERSOL_TYPE? # not encountered
463 # NPC_ELEMENTAL_TYPE? # not encountered
464 }->{$args->{object_type}};
465 }
466
467 }
468
469 unless (defined $object_class) {
470 if ($jobs_lut{$args->{type}}) {
471 unless ($args->{type} > 6000) {
472 $object_class = 'Actor::Player';
473 } else {
474 $object_class = 'Actor::Slave';
475 }
476 } elsif ($args->{type} == 45) {
477 $object_class = 'Actor::Portal';
478
479 } elsif ($args->{type} >= 1000) {
480 if ($args->{hair_style} == 0x64) {
481 $object_class = 'Actor::Pet';
482 } else {
483 $object_class = 'Actor::Monster';
484 }
485 } else { # ($args->{type} < 1000 && $args->{type} != 45 && !$jobs_lut{$args->{type}})
486 $object_class = 'Actor::NPC';
487 }
488 }
489
490 #### Step 1: create the actor object ####
491
492 if ($object_class eq 'Actor::Player') {
493 # Actor is a player
494 $actor = $playersList->getByID($args->{ID});
495 if (!defined $actor) {
496 $actor = new Actor::Player();
497 $actor->{appear_time} = time;
498 # New actor_display packets include the player's name
499 if ($args->{switch} eq "0086") {
500 $actor->{name} = $args->{name};
501 } else {
502 $actor->{name} = bytesToString($args->{name}) if exists $args->{name};
503 }
504 $mustAdd = 1;
505 }
506 $actor->{nameID} = $nameID;
507 } elsif ($object_class eq 'Actor::Slave') {
508 # Actor is a homunculus or a mercenary
509 $actor = $slavesList->getByID($args->{ID});
510 if (!defined $actor) {
511 $actor = ($char->{slaves} && $char->{slaves}{$args->{ID}})
512 ? $char->{slaves}{$args->{ID}} : new Actor::Slave ($args->{type});
513
514 $actor->{appear_time} = time;
515 $actor->{name_given} = bytesToString($args->{name}) if exists $args->{name};
516 $actor->{jobId} = $args->{type} if exists $args->{type};
517 $mustAdd = 1;
518 }
519 $actor->{nameID} = $nameID;
520 } elsif ($object_class eq 'Actor::Portal') {
521 # Actor is a portal
522 $actor = $portalsList->getByID($args->{ID});
523 if (!defined $actor) {
524 $actor = new Actor::Portal();
525 $actor->{appear_time} = time;
526 my $exists = portalExists($field->baseName, \%coordsTo);
527 $actor->{source}{map} = $field->baseName;
528 if ($exists ne "") {
529 $actor->setName("$portals_lut{$exists}{source}{map} -> " . getPortalDestName($exists));
530 }
531 $mustAdd = 1;
532
533 # Strangely enough, portals (like all other actors) have names, too.
534 # We _could_ send a "actor_info_request" packet to find the names of each portal,
535 # however I see no gain from this. (And it might even provide another way of private
536 # servers to auto-ban bots.)
537 }
538 $actor->{nameID} = $nameID;
539 } elsif ($object_class eq 'Actor::Pet') {
540 # Actor is a pet
541 $actor = $petsList->getByID($args->{ID});
542 if (!defined $actor) {
543 $actor = new Actor::Pet();
544 $actor->{appear_time} = time;
545 $actor->{name} = $args->{name};
546# if ($monsters_lut{$args->{type}}) {
547# $actor->setName($monsters_lut{$args->{type}});
548# }
549 $actor->{name_given} = exists $args->{name} ? bytesToString($args->{name}) : T("Unknown");
550 $mustAdd = 1;
551
552 # Previously identified monsters could suddenly be identified as pets.
553 if ($monstersList->getByID($args->{ID})) {
554 $monstersList->removeByID($args->{ID});
555 }
556
557 # Why do monsters and pets use nameID as type?
558 $actor->{nameID} = $args->{type};
559
560 }
561 } elsif ($object_class eq 'Actor::Monster') {
562 $actor = $monstersList->getByID($args->{ID});
563 if (!defined $actor) {
564 $actor = new Actor::Monster();
565 $actor->{appear_time} = time;
566 if ($monsters_lut{$args->{type}}) {
567 $actor->setName($monsters_lut{$args->{type}});
568 }
569 #$actor->{name_given} = exists $args->{name} ? bytesToString($args->{name}) : "Unknown";
570 $actor->{name_given} = "Unknown";
571 $actor->{binType} = $args->{type};
572 $mustAdd = 1;
573
574 # Why do monsters and pets use nameID as type?
575 $actor->{nameID} = $args->{type};
576 }
577 } elsif ($object_class eq 'Actor::NPC') {
578 # Actor is an NPC
579 $actor = $npcsList->getByID($args->{ID});
580 if (!defined $actor) {
581 $actor = new Actor::NPC();
582 $actor->{appear_time} = time;
583 $actor->{name} = bytesToString($args->{name}) if exists $args->{name};
584 $mustAdd = 1;
585 }
586 $actor->{nameID} = $nameID;
587 }
588
589 #### Step 2: update actor information ####
590 $actor->{ID} = $args->{ID};
591 $actor->{charID} = $args->{charID} if $args->{charID} && $args->{charID} ne "\0\0\0\0";
592 $actor->{jobID} = $args->{type};
593 $actor->{type} = $args->{type};
594 $actor->{lv} = $args->{lv};
595 $actor->{pos} = {%coordsFrom};
596 $actor->{pos_to} = {%coordsTo};
597 $actor->{walk_speed} = $args->{walk_speed} / 1000 if (exists $args->{walk_speed} && $args->{switch} ne "0086");
598 $actor->{time_move} = time;
599 $actor->{time_move_calc} = distance(\%coordsFrom, \%coordsTo) * $actor->{walk_speed};
600 # 0086 would need that?
601 $actor->{object_type} = $args->{object_type} if (defined $args->{object_type});
602
603 if (UNIVERSAL::isa($actor, "Actor::Player")) {
604 # None of this stuff should matter if the actor isn't a player... => does matter for a guildflag npc!
605
606 # Interesting note about emblemID. If it is 0 (or none), the Ragnarok
607 # client will display "Send (Player) a guild invitation" (assuming one has
608 # invitation priveledges), regardless of whether or not guildID is set.
609 # I bet that this is yet another brilliant "feature" by GRAVITY's good programmers.
610 $actor->{emblemID} = $args->{emblemID} if (exists $args->{emblemID});
611 $actor->{guildID} = $args->{guildID} if (exists $args->{guildID});
612
613 if (exists $args->{lowhead}) {
614 $actor->{headgear}{low} = $args->{lowhead};
615 $actor->{headgear}{mid} = $args->{midhead};
616 $actor->{headgear}{top} = $args->{tophead};
617 $actor->{weapon} = $args->{weapon};
618 $actor->{shield} = $args->{shield};
619 }
620
621 $actor->{sex} = $args->{sex};
622
623 if ($args->{act} == 1) {
624 $actor->{dead} = 1;
625 } elsif ($args->{act} == 2) {
626 $actor->{sitting} = 1;
627 }
628
629 # Monsters don't have hair colors or heads to look around...
630 $actor->{hair_color} = $args->{hair_color} if (exists $args->{hair_color});
631
632 } elsif (UNIVERSAL::isa($actor, "Actor::NPC") && $args->{type} == 722) { # guild flag has emblem
633 # odd fact: "this data can also be found in a strange place:
634 # (shield OR lowhead) + midhead = emblemID (either shield or lowhead depending on the packet)
635 # tophead = guildID
636 $actor->{emblemID} = $args->{emblemID};
637 $actor->{guildID} = $args->{guildID};
638 }
639
640 # But hair_style is used for pets, and their bodies can look different ways...
641 $actor->{hair_style} = $args->{hair_style} if (exists $args->{hair_style});
642 $actor->{look}{body} = $args->{body_dir} if (exists $args->{body_dir});
643 $actor->{look}{head} = $args->{head_dir} if (exists $args->{head_dir});
644
645 # When stance is non-zero, character is bobbing as if they had just got hit,
646 # but the cursor also turns to a sword when they are mouse-overed.
647 #$actor->{stance} = $args->{stance} if (exists $args->{stance});
648
649 # Visual effects are a set of flags (some of the packets don't have this argument)
650 $actor->{opt3} = $args->{opt3} if (exists $args->{opt3}); # stackable
651
652 # Known visual effects:
653 # 0x0001 = Yellow tint (eg, a quicken skill)
654 # 0x0002 = Red tint (eg, power-thrust)
655 # 0x0004 = Gray tint (eg, energy coat)
656 # 0x0008 = Slow lightning (eg, mental strength)
657 # 0x0010 = Fast lightning (eg, MVP fury)
658 # 0x0020 = Black non-moving statue (eg, stone curse)
659 # 0x0040 = Translucent weapon
660 # 0x0080 = Translucent red sprite (eg, marionette control?)
661 # 0x0100 = Spaztastic weapon image (eg, mystical amplification)
662 # 0x0200 = Gigantic glowy sphere-thing
663 # 0x0400 = Translucent pink sprite (eg, marionette control?)
664 # 0x0800 = Glowy sprite outline (eg, assumptio)
665 # 0x1000 = Bright red sprite, slowly moving red lightning (eg, MVP fury?)
666 # 0x2000 = Vortex-type effect
667
668 # Note that these are flags, and you can mix and match them
669 # Example: 0x000C (0x0008 & 0x0004) = gray tint with slow lightning
670
671=pod
672typedef enum <unnamed-tag> {
673 SHOW_EFST_NORMAL = 0x0,
674 SHOW_EFST_QUICKEN = 0x1,
675 SHOW_EFST_OVERTHRUST = 0x2,
676 SHOW_EFST_ENERGYCOAT = 0x4,
677 SHOW_EFST_EXPLOSIONSPIRITS = 0x8,
678 SHOW_EFST_STEELBODY = 0x10,
679 SHOW_EFST_BLADESTOP = 0x20,
680 SHOW_EFST_AURABLADE = 0x40,
681 SHOW_EFST_REDBODY = 0x80,
682 SHOW_EFST_LIGHTBLADE = 0x100,
683 SHOW_EFST_MOON = 0x200,
684 SHOW_EFST_PINKBODY = 0x400,
685 SHOW_EFST_ASSUMPTIO = 0x800,
686 SHOW_EFST_SUN_WARM = 0x1000,
687 SHOW_EFST_REFLECT = 0x2000,
688 SHOW_EFST_BUNSIN = 0x4000,
689 SHOW_EFST_SOULLINK = 0x8000,
690 SHOW_EFST_UNDEAD = 0x10000,
691 SHOW_EFST_CONTRACT = 0x20000,
692} <unnamed-tag>;
693=cut
694
695 # Save these parameters ...
696 $actor->{opt1} = $args->{opt1}; # nonstackable
697 $actor->{opt2} = $args->{opt2}; # stackable
698 $actor->{option} = $args->{option}; # stackable
699
700 # And use them to set status flags.
701 if (setStatus($actor, $args->{opt1}, $args->{opt2}, $args->{option})) {
702 $mustAdd = 0;
703 }
704
705
706 #### Step 3: Add actor to actor list ####
707 if ($mustAdd) {
708 if (UNIVERSAL::isa($actor, "Actor::Player")) {
709 $playersList->add($actor);
710 Plugins::callHook('add_player_list', $actor);
711
712 } elsif (UNIVERSAL::isa($actor, "Actor::Monster")) {
713 $monstersList->add($actor);
714 Plugins::callHook('add_monster_list', $actor);
715
716 } elsif (UNIVERSAL::isa($actor, "Actor::Pet")) {
717 $petsList->add($actor);
718 Plugins::callHook('add_pet_list', $actor);
719
720 } elsif (UNIVERSAL::isa($actor, "Actor::Portal")) {
721 $portalsList->add($actor);
722 Plugins::callHook('add_portal_list', $actor);
723
724 } elsif (UNIVERSAL::isa($actor, "Actor::NPC")) {
725 my $ID = $args->{ID};
726 my $location = $field->baseName . " $actor->{pos}{x} $actor->{pos}{y}";
727 if ($npcs_lut{$location}) {
728 $actor->setName($npcs_lut{$location});
729 }
730 $npcsList->add($actor);
731 Plugins::callHook('add_npc_list', $actor);
732
733 } elsif (UNIVERSAL::isa($actor, "Actor::Slave")) {
734 $slavesList->add($actor);
735 Plugins::callHook('add_slave_list', $actor);
736 }
737 }
738
739
740 #### Packet specific ####
741 if ($args->{switch} eq "0078" ||
742 $args->{switch} eq "01D8" ||
743 $args->{switch} eq "022A" ||
744 $args->{switch} eq "02EE" ||
745 $args->{switch} eq "07F9" ||
746 $args->{switch} eq "0857") {
747 # Actor Exists (standing)
748
749 if ($actor->isa('Actor::Player')) {
750 my $domain = existsInList($config{friendlyAID}, unpack("V", $actor->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
751 debug "Player Exists: " . $actor->name . " ($actor->{binID}) Level $actor->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsFrom{x}, $coordsFrom{y})\n", $domain;
752
753 Plugins::callHook('player', {player => $actor}); #backwards compatibility
754
755 Plugins::callHook('player_exist', {player => $actor});
756
757 } elsif ($actor->isa('Actor::NPC')) {
758 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;
759 Plugins::callHook('npc_exist', {npc => $actor});
760
761 } elsif ($actor->isa('Actor::Portal')) {
762 message TF("Portal Exists: %s (%s, %s) - (%s)\n", $actor->name, $actor->{pos_to}{x}, $actor->{pos_to}{y}, $actor->{binID}), "portals", 1;
763 Plugins::callHook('portal_exist', {portal => $actor});
764
765 } elsif ($actor->isa('Actor::Monster')) {
766 debug sprintf("Monster Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
767
768 } elsif ($actor->isa('Actor::Pet')) {
769 debug sprintf("Pet Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
770
771 } elsif ($actor->isa('Actor::Slave')) {
772 debug sprintf("Slave Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
773
774 } else {
775 debug sprintf("Unknown Actor Exists: %s (%d)\n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
776 }
777
778 } elsif ($args->{switch} eq "0079" ||
779 $args->{switch} eq "01DB" ||
780 $args->{switch} eq "022B" ||
781 $args->{switch} eq "02ED" ||
782 $args->{switch} eq "01D9" ||
783 $args->{switch} eq "07F8" ||
784 $args->{switch} eq "0858") {
785 # Actor Connected (new)
786
787 if ($actor->isa('Actor::Player')) {
788 my $domain = existsInList($config{friendlyAID}, unpack("V", $args->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
789 debug "Player Connected: ".$actor->name." ($actor->{binID}) Level $args->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsTo{x}, $coordsTo{y})\n", $domain;
790
791 Plugins::callHook('player', {player => $actor}); #backwards compatibailty
792
793 Plugins::callHook('player_connected', {player => $actor});
794 } else {
795 debug "Unknown Connected: $args->{type} - \n", "parseMsg";
796 }
797
798 } elsif ($args->{switch} eq "007B" ||
799 $args->{switch} eq "0086" ||
800 $args->{switch} eq "01DA" ||
801 $args->{switch} eq "022C" ||
802 $args->{switch} eq "02EC" ||
803 $args->{switch} eq "07F7" ||
804 $args->{switch} eq "0856") {
805 # Actor Moved
806
807 # Correct the direction in which they're looking
808 my %vec;
809 getVector(\%vec, \%coordsTo, \%coordsFrom);
810 my $direction = int sprintf("%.0f", (360 - vectorToDegree(\%vec)) / 45);
811
812 $actor->{look}{body} = $direction;
813 $actor->{look}{head} = 0;
814
815 if ($actor->isa('Actor::Player')) {
816 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";
817 Plugins::callHook('player_moved', $actor);
818 } elsif ($actor->isa('Actor::Monster')) {
819 debug "Monster Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
820 Plugins::callHook('monster_moved', $actor);
821 } elsif ($actor->isa('Actor::Pet')) {
822 debug "Pet Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
823 Plugins::callHook('pet_moved', $actor);
824 } elsif ($actor->isa('Actor::Slave')) {
825 debug "Slave Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
826 Plugins::callHook('slave_moved', $actor);
827 } elsif ($actor->isa('Actor::Portal')) {
828 # This can never happen of course.
829 debug "Portal Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
830 Plugins::callHook('portal_moved', $actor);
831 } elsif ($actor->isa('Actor::NPC')) {
832 # Neither can this.
833 debug "NPC Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
834 Plugins::callHook('npc_moved', $actor);
835 } else {
836 debug "Unknown Actor Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})\n", "parseMsg";
837 }
838
839 } elsif ($args->{switch} eq "007C") {
840 # Actor Spawned
841 if ($actor->isa('Actor::Player')) {
842 debug "Player Spawned: " . $actor->nameIdx . " $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}}\n", "parseMsg";
843 } elsif ($actor->isa('Actor::Monster')) {
844 debug "Monster Spawned: " . $actor->nameIdx . "\n", "parseMsg";
845 } elsif ($actor->isa('Actor::Pet')) {
846 debug "Pet Spawned: " . $actor->nameIdx . "\n", "parseMsg";
847 } elsif ($actor->isa('Actor::Slave')) {
848 debug "Slave Spawned: " . $actor->nameIdx . " $jobs_lut{$actor->{jobID}}\n", "parseMsg";
849 } elsif ($actor->isa('Actor::Portal')) {
850 # Can this happen?
851 debug "Portal Spawned: " . $actor->nameIdx . "\n", "parseMsg";
852 } elsif ($actor->isa('NPC')) {
853 debug "NPC Spawned: " . $actor->nameIdx . "\n", "parseMsg";
854 } else {
855 debug "Unknown Spawned: " . $actor->nameIdx . "\n", "parseMsg";
856 }
857 }
858}
859
860sub actor_died_or_disappeared {
861 my ($self,$args) = @_;
862 return unless changeToInGameState();
863 my $ID = $args->{ID};
864 avoidList_ID($ID);
865
866 if ($ID eq $accountID) {
867 message T("You have died\n") if (!$char->{dead});
868 Plugins::callHook('self_died');
869 closeShop() unless !$shopstarted || $config{'dcOnDeath'} == -1 || $AI == AI::OFF;
870 $char->{deathCount}++;
871 $char->{dead} = 1;
872 $char->{dead_time} = time;
873 if ($char->{equipment}{arrow} && $char->{equipment}{arrow}{type} == 19) {
874 delete $char->{equipment}{arrow};
875 }
876
877 } elsif (defined $monstersList->getByID($ID)) {
878 my $monster = $monstersList->getByID($ID);
879 if ($args->{type} == 0) {
880 debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
881 $monster->{disappeared} = 1;
882
883 } elsif ($args->{type} == 1) {
884 debug "Monster Died: " . $monster->name . " ($monster->{binID})\n", "parseMsg_damage";
885 $monster->{dead} = 1;
886
887 if ((AI::action ne "attack" || AI::args(0)->{ID} eq $ID) &&
888 ($config{itemsTakeAuto_party} &&
889 ($monster->{dmgFromParty} > 0 ||
890 $monster->{dmgFromYou} > 0))) {
891 AI::clear("items_take");
892 ai_items_take($monster->{pos}{x}, $monster->{pos}{y},
893 $monster->{pos_to}{x}, $monster->{pos_to}{y});
894 }
895
896 } elsif ($args->{type} == 2) { # What's this?
897 debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
898 $monster->{disappeared} = 1;
899
900 } elsif ($args->{type} == 3) {
901 debug "Monster Teleported: " . $monster->name . " ($monster->{binID})\n", "parseMsg_presence";
902 $monster->{teleported} = 1;
903 }
904
905 $monster->{gone_time} = time;
906 $monsters_old{$ID} = $monster->deepCopy();
907 Plugins::callHook('monster_disappeared', {monster => $monster});
908 $monstersList->remove($monster);
909
910 } elsif (defined $playersList->getByID($ID)) {
911 my $player = $playersList->getByID($ID);
912 if ($args->{type} == 1) {
913 message TF("Player Died: %s (%d) %s %s\n", $player->name, $player->{binID}, $sex_lut{$player->{sex}}, $jobs_lut{$player->{jobID}});
914 $player->{dead} = 1;
915 $player->{dead_time} = time;
916 } else {
917 if ($args->{type} == 0) {
918 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";
919 $player->{disappeared} = 1;
920 } elsif ($args->{type} == 2) {
921 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";
922 $player->{disconnected} = 1;
923 } elsif ($args->{type} == 3) {
924 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";
925 $player->{teleported} = 1;
926 } else {
927 debug "Player Disappeared in an unknown way: ".$player->name." ($player->{binID}) $sex_lut{$player->{sex}} $jobs_lut{$player->{jobID}}\n", "parseMsg_presence";
928 $player->{disappeared} = 1;
929 }
930
931 if (grep { $ID eq $_ } @venderListsID) {
932 binRemove(\@venderListsID, $ID);
933 delete $venderLists{$ID};
934 }
935
936 $player->{gone_time} = time;
937 $players_old{$ID} = $player->deepCopy();
938 Plugins::callHook('player_disappeared', {player => $player});
939
940 $playersList->remove($player);
941 }
942
943 } elsif ($players_old{$ID}) {
944 if ($args->{type} == 2) {
945 debug "Player Disconnected: " . $players_old{$ID}->name . "\n", "parseMsg_presence";
946 $players_old{$ID}{disconnected} = 1;
947 } elsif ($args->{type} == 3) {
948 debug "Player Teleported: " . $players_old{$ID}->name . "\n", "parseMsg_presence";
949 $players_old{$ID}{teleported} = 1;
950 }
951
952 } elsif (defined $portalsList->getByID($ID)) {
953 my $portal = $portalsList->getByID($ID);
954 debug "Portal Disappeared: " . $portal->name . " ($portal->{binID})\n", "parseMsg";
955 $portal->{disappeared} = 1;
956 $portal->{gone_time} = time;
957 $portals_old{$ID} = $portal->deepCopy();
958 Plugins::callHook('portal_disappeared', {portal => $portal});
959 $portalsList->remove($portal);
960
961 } elsif (defined $npcsList->getByID($ID)) {
962 my $npc = $npcsList->getByID($ID);
963 debug "NPC Disappeared: " . $npc->name . " ($npc->{nameID})\n", "parseMsg";
964 $npc->{disappeared} = 1;
965 $npc->{gone_time} = time;
966 $npcs_old{$ID} = $npc->deepCopy();
967 Plugins::callHook('npc_disappeared', {npc => $npc});
968 $npcsList->remove($npc);
969
970 } elsif (defined $petsList->getByID($ID)) {
971 my $pet = $petsList->getByID($ID);
972 debug "Pet Disappeared: " . $pet->name . " ($pet->{binID})\n", "parseMsg";
973 $pet->{disappeared} = 1;
974 $pet->{gone_time} = time;
975 Plugins::callHook('pet_disappeared', {pet => $pet});
976 $petsList->remove($pet);
977
978 } elsif (defined $slavesList->getByID($ID)) {
979 my $slave = $slavesList->getByID($ID);
980 if ($args->{type} == 1) {
981 message TF("Slave Died: %s (%d) %s\n", $slave->name, $slave->{binID}, $slave->{actorType});
982 $slave->{state} = 4;
983 } else {
984 if ($args->{type} == 0) {
985 debug "Slave Disappeared: " . $slave->name . " ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
986 $slave->{disappeared} = 1;
987 } elsif ($args->{type} == 2) {
988 debug "Slave Disconnected: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
989 $slave->{disconnected} = 1;
990 } elsif ($args->{type} == 3) {
991 debug "Slave Teleported: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})\n", "parseMsg_presence";
992 $slave->{teleported} = 1;
993 } else {
994 debug "Slave Disappeared in an unknown way: ".$slave->name." ($slave->{binID}) $slave->{actorType}\n", "parseMsg_presence";
995 $slave->{disappeared} = 1;
996 }
997
998 $slave->{gone_time} = time;
999 Plugins::callHook('slave_disappeared', {slave => $slave});
1000 }
1001
1002 $slavesList->remove($slave);
1003
1004 } else {
1005 debug "Unknown Disappeared: ".getHex($ID)."\n", "parseMsg";
1006 }
1007}
1008
1009sub actor_action {
1010 my ($self,$args) = @_;
1011 return unless changeToInGameState();
1012
1013 $args->{damage} = intToSignedShort($args->{damage});
1014 if ($args->{type} == ACTION_ITEMPICKUP) {
1015 # Take item
1016 my $source = Actor::get($args->{sourceID});
1017 my $verb = $source->verb('pick up', 'picks up');
1018 my $target = getActorName($args->{targetID});
1019 debug "$source $verb $target\n", 'parseMsg_presence';
1020
1021 my $item = $itemsList->getByID($args->{targetID});
1022 $item->{takenBy} = $args->{sourceID} if ($item);
1023
1024 } elsif ($args->{type} == ACTION_SIT) {
1025 # Sit
1026 my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
1027 if ($args->{sourceID} eq $accountID) {
1028 message T("You are sitting.\n") if (!$char->{sitting});
1029 $char->{sitting} = 1;
1030 AI::queue("sitAuto") unless (AI::inQueue("sitAuto")) || $ai_v{sitAuto_forcedBySitCommand};
1031 } else {
1032 message TF("%s is sitting.\n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
1033 my $player = $playersList->getByID($args->{sourceID});
1034 $player->{sitting} = 1 if ($player);
1035 }
1036 Misc::checkValidity("actor_action (take item)");
1037
1038 } elsif ($args->{type} == ACTION_STAND) {
1039 # Stand
1040 my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
1041 if ($args->{sourceID} eq $accountID) {
1042 message T("You are standing.\n") if ($char->{sitting});
1043 if ($config{sitAuto_idle}) {
1044 $timeout{ai_sit_idle}{time} = time;
1045 }
1046 $char->{sitting} = 0;
1047 } else {
1048 message TF("%s is standing.\n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
1049 my $player = $playersList->getByID($args->{sourceID});
1050 $player->{sitting} = 0 if ($player);
1051 }
1052 Misc::checkValidity("actor_action (stand)");
1053
1054 } else {
1055 # Attack
1056 my $dmgdisplay;
1057 my $totalDamage = $args->{damage} + $args->{dual_wield_damage};
1058 if ($totalDamage == 0) {
1059 $dmgdisplay = T("Miss!");
1060 $dmgdisplay .= "!" if ($args->{type} == ACTION_ATTACK_LUCKY); # lucky dodge
1061 } else {
1062 $dmgdisplay = $args->{div} > 1
1063 ? sprintf '%d*%d', $args->{damage} / $args->{div}, $args->{div}
1064 : $args->{damage}
1065 ;
1066 $dmgdisplay .= "!" if ($args->{type} == ACTION_ATTACK_CRITICAL); # critical hit
1067 $dmgdisplay .= " + $args->{dual_wield_damage}" if $args->{dual_wield_damage};
1068 }
1069
1070 Misc::checkValidity("actor_action (attack 1)");
1071
1072 updateDamageTables($args->{sourceID}, $args->{targetID}, $totalDamage);
1073
1074 Misc::checkValidity("actor_action (attack 2)");
1075
1076 my $source = Actor::get($args->{sourceID});
1077 my $target = Actor::get($args->{targetID});
1078 my $verb = $source->verb('attack', 'attacks');
1079
1080 $target->{sitting} = 0 unless $args->{type} == ACTION_ATTACK_NOMOTION || $args->{type} == ACTION_ATTACK_MULTIPLE_NOMOTION || $totalDamage == 0;
1081
1082 my $msg = attack_string($source, $target, $dmgdisplay, ($args->{src_speed}));
1083 Plugins::callHook('packet_attack', {sourceID => $args->{sourceID}, targetID => $args->{targetID}, msg => \$msg, dmg => $totalDamage, type => $args->{type}});
1084
1085 my $status = sprintf("[%3d/%3d]", percent_hp($char), percent_sp($char));
1086
1087 Misc::checkValidity("actor_action (attack 3)");
1088
1089 if ($args->{sourceID} eq $accountID) {
1090 message("$status $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
1091 if ($startedattack) {
1092 $monstarttime = time();
1093 $monkilltime = time();
1094 $startedattack = 0;
1095 }
1096 Misc::checkValidity("actor_action (attack 4)");
1097 calcStat($args->{damage});
1098 Misc::checkValidity("actor_action (attack 5)");
1099
1100 } elsif ($args->{targetID} eq $accountID) {
1101 message("$status $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
1102 if ($args->{damage} > 0) {
1103 $damageTaken{$source->{name}}{attack} += $args->{damage};
1104 }
1105
1106 } elsif ($char->{slaves} && $char->{slaves}{$args->{sourceID}}) {
1107 message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{sourceID}}{hpPercent}, $char->{slaves}{$args->{sourceID}}{spPercent}) . " $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
1108
1109 } elsif ($char->{slaves} && $char->{slaves}{$args->{targetID}}) {
1110 message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{targetID}}{hpPercent}, $char->{slaves}{$args->{targetID}}{spPercent}) . " $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
1111
1112 } elsif ($args->{sourceID} eq $args->{targetID}) {
1113 message("$status $msg");
1114
1115 } elsif ($config{showAllDamage}) {
1116 message("$status $msg");
1117
1118 } else {
1119 debug("$msg", 'parseMsg_damage');
1120 }
1121
1122 Misc::checkValidity("actor_action (attack 6)");
1123 }
1124}
1125
1126sub actor_info {
1127 my ($self, $args) = @_;
1128 return unless changeToInGameState();
1129
1130 debug "Received object info: $args->{name}\n", "parseMsg_presence/name", 2;
1131
1132 my $player = $playersList->getByID($args->{ID});
1133 if ($player) {
1134 # 0095: This packet tells us the names of players who aren't in a guild.
1135 # 0195: Receive names of players who are in a guild.
1136 # FIXME: There is more to this packet than just party name and guild name.
1137 # This packet is received when you leave a guild
1138 # (with cryptic party and guild name fields, at least for now)
1139 $player->setName(bytesToString($args->{name}));
1140 $player->{info} = 1;
1141
1142 $player->{party}{name} = bytesToString($args->{partyName}) if defined $args->{partyName};
1143 $player->{guild}{name} = bytesToString($args->{guildName}) if defined $args->{guildName};
1144 $player->{guild}{title} = bytesToString($args->{guildTitle}) if defined $args->{guildTitle};
1145
1146 message "Player Info: " . $player->nameIdx . "\n", "parseMsg_presence", 2;
1147 updatePlayerNameCache($player);
1148 Plugins::callHook('charNameUpdate', {player => $player});
1149 }
1150
1151 my $monster = $monstersList->getByID($args->{ID});
1152 if ($monster) {
1153 my $name = bytesToString($args->{name});
1154 $name =~ s/^\s+|\s+$//g;
1155 debug "Monster Info: $name ($monster->{binID})\n", "parseMsg", 2;
1156 $monster->{name_given} = $name;
1157 $monster->{info} = 1;
1158 if ($monsters_lut{$monster->{nameID}} eq "") {
1159 $monster->setName($name);
1160 $monsters_lut{$monster->{nameID}} = $name;
1161 updateMonsterLUT(Settings::getTableFilename("monsters.txt"), $monster->{nameID}, $name);
1162 }
1163 }
1164
1165 my $npc = $npcs{$args->{ID}};
1166 if ($npc) {
1167 $npc->setName(bytesToString($args->{name}));
1168 $npc->{info} = 1;
1169 if ($config{debug} >= 2) {
1170 my $binID = binFind(\@npcsID, $args->{ID});
1171 debug "NPC Info: $npc->{name} ($binID)\n", "parseMsg", 2;
1172 }
1173
1174 my $location = $field->baseName . " $npc->{pos}{x} $npc->{pos}{y}";
1175 if (!$npcs_lut{$location}) {
1176 $npcs_lut{$location} = $npc->{name};
1177 updateNPCLUT(Settings::getTableFilename("npcs.txt"), $location, $npc->{name});
1178 }
1179 }
1180
1181 my $pet = $pets{$args->{ID}};
1182 if ($pet) {
1183 my $name = bytesToString($args->{name});
1184 $pet->{name_given} = $name;
1185 $pet->setName($name);
1186 $pet->{info} = 1;
1187 if ($config{debug} >= 2) {
1188 my $binID = binFind(\@petsID, $args->{ID});
1189 debug "Pet Info: $pet->{name_given} ($binID)\n", "parseMsg", 2;
1190 }
1191 }
1192
1193 my $slave = $slavesList->getByID($args->{ID});
1194 if ($slave) {
1195 my $name = bytesToString($args->{name});
1196 $slave->{name_given} = $name;
1197 $slave->setName($name);
1198 $slave->{info} = 1;
1199 my $binID = binFind(\@slavesID, $args->{ID});
1200 debug "Slave Info: $name ($binID)\n", "parseMsg_presence", 2;
1201 updatePlayerNameCache($slave);
1202 }
1203
1204 # TODO: $args->{ID} eq $accountID
1205}
1206
1207use constant QTYPE => (
1208 0x0 => [0xff, 0xff, 0, 0],
1209 0x1 => [0xff, 0x80, 0, 0],
1210 0x2 => [0, 0xff, 0, 0],
1211 0x3 => [0x80, 0, 0x80, 0],
1212);
1213
1214sub parse_minimap_indicator {
1215 my ($self, $args) = @_;
1216
1217 $args->{actor} = Actor::get($args->{npcID});
1218 $args->{show} = $args->{type} != 2;
1219
1220 unless (defined $args->{red}) {
1221 @{$args}{qw(red green blue alpha)} = @{{QTYPE}->{$args->{qtype}} || [0xff, 0xff, 0xff, 0]};
1222 }
1223
1224 # FIXME: packet 0144: coordinates are missing now when clearing indicators; ID is used
1225 # Wx depends on coordinates there
1226}
1227
1228sub account_payment_info {
1229 my ($self, $args) = @_;
1230 my $D_minute = $args->{D_minute};
1231 my $H_minute = $args->{H_minute};
1232
1233 my $D_d = int($D_minute / 1440);
1234 my $D_h = int(($D_minute % 1440) / 60);
1235 my $D_m = int(($D_minute % 1440) % 60);
1236
1237 my $H_d = int($H_minute / 1440);
1238 my $H_h = int(($H_minute % 1440) / 60);
1239 my $H_m = int(($H_minute % 1440) % 60);
1240
1241 message T("============= Account payment information =============\n"), "info";
1242 message TF("Pay per day : %s day(s) %s hour(s) and %s minute(s)\n", $D_d, $D_h, $D_m), "info";
1243 message TF("Pay per hour : %s day(s) %s hour(s) and %s minute(s)\n", $H_d, $H_h, $H_m), "info";
1244 message "-------------------------------------------------------\n", "info";
1245}
1246
1247# TODO
1248sub reconstruct_minimap_indicator {
1249}
1250
1251use constant {
1252 HO_PRE_INIT => 0x0,
1253 HO_RELATIONSHIP_CHANGED => 0x1,
1254 HO_FULLNESS_CHANGED => 0x2,
1255 HO_ACCESSORY_CHANGED => 0x3,
1256 HO_HEADTYPE_CHANGED => 0x4,
1257};
1258
1259# 0230
1260# TODO: what is type?
1261sub homunculus_info {
1262 my ($self, $args) = @_;
1263 debug "homunculus_info type: $args->{type}\n", "homunculus";
1264 if ($args->{state} == HO_PRE_INIT) {
1265 my $state = $char->{homunculus}{state}
1266 if ($char->{homunculus} && $char->{homunculus}{ID} && $char->{homunculus}{ID} ne $args->{ID});
1267 $char->{homunculus} = Actor::get($args->{ID}) if ($char->{homunculus}{ID} ne $args->{ID});
1268 $char->{homunculus}{state} = $state if (defined $state);
1269 $char->{homunculus}{map} = $field->baseName;
1270 unless ($char->{slaves}{$char->{homunculus}{ID}}) {
1271 AI::SlaveManager::addSlave ($char->{homunculus});
1272 $char->{homunculus}{appear_time} = time;
1273 }
1274 } elsif ($args->{state} == HO_RELATIONSHIP_CHANGED) {
1275 $char->{homunculus}{intimacy} = $args->{val} if $char->{homunculus};
1276 } elsif ($args->{state} == HO_FULLNESS_CHANGED) {
1277 $char->{homunculus}{hunger} = $args->{val} if $char->{homunculus};
1278 } elsif ($args->{state} == HO_ACCESSORY_CHANGED) {
1279 $char->{homunculus}{accessory} = $args->{val} if $char->{homunculus};
1280 } elsif ($args->{state} == HO_HEADTYPE_CHANGED) {
1281 #
1282 }
1283}
1284
1285##
1286# minimap_indicator({bool show, Actor actor, int x, int y, int red, int green, int blue, int alpha [, int effect]})
1287# show: whether indicator is shown or cleared
1288# actor: @MODULE(Actor) who issued the indicator; or which Actor it's binded to
1289# x, y: indicator coordinates
1290# red, green, blue, alpha: indicator color
1291# effect: unknown, may be missing
1292#
1293# Minimap indicator.
1294sub minimap_indicator {
1295 my ($self, $args) = @_;
1296
1297 my $color_str = "[R:$args->{red}, G:$args->{green}, B:$args->{blue}, A:$args->{alpha}]";
1298 my $indicator = T("minimap indicator");
1299 if (defined $args->{type}) {
1300 unless ($args->{type} == 1 || $args->{type} == 2) {
1301 $indicator .= TF(" (unknown type %d)", $args->{type});
1302 }
1303 } elsif (defined $args->{effect}) {
1304 if ($args->{effect} == 1) {
1305 $indicator = T("*Quest!*");
1306 } elsif ($args->{effect}) { # 0 is no effect
1307 $indicator = TF("unknown effect %d", $args->{effect});
1308 }
1309 }
1310
1311 if ($args->{show}) {
1312 message TF("%s shown %s at location %d, %d " .
1313 "with the color %s\n", $args->{actor}, $indicator, @{$args}{qw(x y)}, $color_str),
1314 'effect';
1315 } else {
1316 message TF("%s cleared %s at location %d, %d " .
1317 "with the color %s\n", $args->{actor}, $indicator, @{$args}{qw(x y)}, $color_str),
1318 'effect';
1319 }
1320}
1321
1322sub local_broadcast {
1323 my ($self, $args) = @_;
1324 my $message = bytesToString($args->{message});
1325 my $color = uc(sprintf("%06x", $args->{color})); # hex code
1326 stripLanguageCode(\$message);
1327 chatLog("lb", "$message\n");# if ($config{logLocalBroadcast});
1328 message "$message\n", "schat";
1329 Plugins::callHook('packet_localBroadcast', {
1330 Msg => $message,
1331 color => $color
1332 });
1333}
1334
1335sub parse_sage_autospell {
1336 my ($self, $args) = @_;
1337
1338 $args->{skills} = [map { Skill->new(idn => $_) } sort { $a<=>$b } grep {$_}
1339 exists $args->{autoshadowspell_list}
1340 ? (unpack 'v*', $args->{autoshadowspell_list})
1341 : (unpack 'V*', $args->{autospell_list})
1342 ];
1343}
1344
1345sub reconstruct_sage_autospell {
1346 my ($self, $args) = @_;
1347
1348 my @skillIDs = map { $_->getIDN } $args->{skills};
1349 $args->{autoshadowspell_list} = pack 'v*', @skillIDs;
1350 $args->{autospell_list} = pack 'V*', @skillIDs;
1351}
1352
1353##
1354# sage_autospell({arrayref skills, int why})
1355# skills: list of @MODULE(Skill) instances
1356# why: unknown
1357#
1358# Skill list for Sage's Hindsight and Shadow Chaser's Auto Shadow Spell.
1359sub sage_autospell {
1360 my ($self, $args) = @_;
1361
1362 return unless $self->changeToInGameState;
1363
1364 my $msg = center(' ' . T('Auto Spell') . ' ', 40, '-') . "\n"
1365 . T(" # Skill\n")
1366 . (join '', map { swrite '@>>> @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<', [$_->getIDN, $_] } @{$args->{skills}})
1367 . ('-'x40) . "\n";
1368
1369 message $msg, 'list';
1370
1371 if ($config{autoSpell}) {
1372 my @autoSpells = split /\s*,\s*/, $config{autoSpell};
1373 for my $autoSpell (@autoSpells) {
1374 my $skill = new Skill(auto => $autoSpell);
1375 message 'Testing autoSpell ' . $autoSpell . "\n";
1376 if (!$config{autoSpell_safe} || List::Util::first { $_->getIDN == $skill->getIDN } @{$args->{skills}}) {
1377 if (defined $args->{why}) {
1378 $messageSender->sendSkillSelect($skill->getIDN, $args->{why});
1379 return;
1380 } else {
1381 $messageSender->sendAutoSpell($skill->getIDN);
1382 return;
1383 }
1384 }
1385 }
1386 error TF("Configured autoSpell (%s) not available.\n", $config{autoSpell});
1387 message T("Disable autoSpell_safe to use it anyway.\n"), 'hint';
1388 } else {
1389 message T("Configure autoSpell to automatically select skill for Auto Spell.\n"), 'hint';
1390 }
1391}
1392
1393sub show_eq {
1394 my ($self, $args) = @_;
1395
1396 my $jump = 26;
1397
1398 my $unpack_string = "v ";
1399 $unpack_string .= "v C2 v v C2 ";
1400 $unpack_string .= "a8 ";
1401 $unpack_string .= "a6"; #unimplemented in eA atm
1402
1403 if (exists $args->{robe}) { # check packet version
1404 $unpack_string .= "v "; # ??
1405 $jump += 2;
1406 }
1407
1408 for (my $i = 0; $i < length($args->{equips_info}); $i += $jump) {
1409 my ($index,
1410 $ID, $type, $identified, $type_equip, $equipped, $broken, $upgrade, # typical for nonstackables
1411 $cards,
1412 $expire) = unpack($unpack_string, substr($args->{equips_info}, $i));
1413
1414 my $item = {};
1415 $item->{index} = $index;
1416
1417 $item->{nameID} = $ID;
1418 $item->{type} = $type;
1419
1420 $item->{identified} = $identified;
1421 $item->{type_equip} = $type_equip;
1422 $item->{equipped} = $equipped;
1423 $item->{broken} = $broken;
1424 $item->{upgrade} = $upgrade;
1425
1426 $item->{cards} = $cards;
1427
1428 $item->{expire} = $expire;
1429
1430 message sprintf("%-20s: %s\n", $equipTypes_lut{$item->{equipped}}, itemName($item)), "list";
1431 debug "$index, $ID, $type, $identified, $type_equip, $equipped, $broken, $upgrade, $cards, $expire\n";
1432 }
1433}
1434
1435sub show_eq_msg_other {
1436 my ($self, $args) = @_;
1437 if ($args->{flag}) {
1438 message T("Allowed to view the other player's Equipment.\n");
1439 } else {
1440 message T("Not allowed to view the other player's Equipment.\n");
1441 }
1442}
1443
1444sub show_eq_msg_self {
1445 my ($self, $args) = @_;
1446 if ($args->{type}) {
1447 message T("Other players are allowed to view your Equipment.\n");
1448 } else {
1449 message T("Other players are not allowed to view your Equipment.\n");
1450 }
1451}
1452
1453# 043D
1454sub skill_post_delay {
1455 my ($self, $args) = @_;
1456
1457 my $skillName = (new Skill(idn => $args->{ID}))->getName;
1458 my $status = defined $statusName{'EFST_DELAY'} ? $statusName{'EFST_DELAY'} : 'Delay';
1459
1460 $char->setStatus($skillName." ".$status, 1, $args->{time});
1461}
1462
1463# TODO: known prefixes (chat domains): micc | ssss | blue | tool
1464# micc = micc<24 characters, this is the sender name. seems like it's null padded><hex color code><message>
1465# micc = Player Broadcast The struct: micc<23bytes player name+some hex><\x00><colour code><full message>
1466# The first player name is used for detecting the player name only according to the disassembled client.
1467# The full message contains the player name at the first 22 bytes
1468# TODO micc.* is currently unstricted, however .{24} and .{23} do not detect chinese with some reasons, please improve this regex if necessary
1469sub system_chat {
1470 my ($self, $args) = @_;
1471 my $message = bytesToString($args->{message});
1472 my $prefix;
1473 my $color;
1474 if ($message =~ s/^ssss//g) { # forces color yellow, or WoE indicator?
1475 $prefix = T('[WoE]');
1476 } elsif ($message =~ /^micc.*\0\0([0-9a-fA-F]{6})(.*)/) { #appears in twRO ## [micc][name][\x00\x00][unknown][\x00\x00][color][name][blablabla][message]
1477 ($color, $message) = $message =~ /^micc.*\0\0([0-9a-fA-F]{6})(.*)/;
1478 $prefix = T('[S]');
1479 } elsif ($message =~ /^micc.{12,24}([0-9a-fA-F]{6})(.*)/) {
1480 ($color, $message) = $message =~ /^micc.*([0-9a-fA-F]{6})(.*)/;
1481 $prefix = T('[S]');
1482 } elsif ($message =~ s/^blue//g) { # forces color blue
1483 $prefix = T('[S]');
1484 } elsif ($message =~ /^tool([0-9a-fA-F]{6})(.*)/) {
1485 ($color, $message) = $message =~ /^tool([0-9a-fA-F]{6})(.*)/;
1486 $prefix = T('[S]');
1487 } else {
1488 $prefix = T('[S]');
1489 }
1490 $message =~ s/\000//g; # remove null charachters
1491 $message =~ s/^ +//g; $message =~ s/ +$//g; # remove whitespace in the beginning and the end of $message
1492 stripLanguageCode(\$message);
1493 chatLog("s", "$message\n") if ($config{logSystemChat});
1494 # Translation Comment: System/GM chat
1495 message "$prefix $message\n", "schat";
1496 ChatQueue::add('gm', undef, undef, $message) if ($config{callSignGM});
1497
1498 Plugins::callHook('packet_sysMsg', {
1499 Msg => $message,
1500 MsgColor => $color,
1501 MsgUser => undef # TODO: implement this value, we can get this from "micc" messages by regex.
1502 });
1503}
1504
1505sub warp_portal_list {
1506 my ($self, $args) = @_;
1507
1508 # strip gat extension
1509 ($args->{memo1}) = $args->{memo1} =~ /^(.*)\.gat/;
1510 ($args->{memo2}) = $args->{memo2} =~ /^(.*)\.gat/;
1511 ($args->{memo3}) = $args->{memo3} =~ /^(.*)\.gat/;
1512 ($args->{memo4}) = $args->{memo4} =~ /^(.*)\.gat/;
1513 # Auto-detect saveMap
1514 if ($args->{type} == 26) {
1515 configModify('saveMap', $args->{memo2}) if ($args->{memo2} && $config{'saveMap'} ne $args->{memo2});
1516 } elsif ($args->{type} == 27) {
1517 configModify('saveMap', $args->{memo1}) if ($args->{memo1} && $config{'saveMap'} ne $args->{memo1});
1518 configModify( "memo$_", $args->{"memo$_"} ) foreach grep { $args->{"memo$_"} ne $config{"memo$_"} } 1 .. 4;
1519 }
1520
1521 $char->{warp}{type} = $args->{type};
1522 undef @{$char->{warp}{memo}};
1523 push @{$char->{warp}{memo}}, $args->{memo1} if $args->{memo1} ne "";
1524 push @{$char->{warp}{memo}}, $args->{memo2} if $args->{memo2} ne "";
1525 push @{$char->{warp}{memo}}, $args->{memo3} if $args->{memo3} ne "";
1526 push @{$char->{warp}{memo}}, $args->{memo4} if $args->{memo4} ne "";
1527
1528 my $msg = center(T(" Warp Portal "), 50, '-') ."\n".
1529 T("# Place Map\n");
1530 for (my $i = 0; $i < @{$char->{warp}{memo}}; $i++) {
1531 $msg .= swrite(
1532 "@< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<",
1533 [$i, $maps_lut{$char->{warp}{memo}[$i].'.rsw'}, $char->{warp}{memo}[$i]]);
1534 }
1535 $msg .= ('-'x50) . "\n";
1536 message $msg, "list";
1537
1538 if ($args->{type} == 26 && AI::inQueue('teleport')) {
1539 # We have already successfully used the Teleport skill.
1540 $messageSender->sendWarpTele(26, AI::args->{lv} == 2 ? "$config{saveMap}.gat" : "Random");
1541 AI::dequeue;
1542 }
1543}
1544
1545
1546# 0828,14
1547sub char_delete2_result {
1548 my ($self, $args) = @_;
1549 my $result = $args->{result};
1550 my $deleteDate = $args->{deleteDate};
1551
1552 if ($result && $deleteDate) {
1553 setCharDeleteDate($messageSender->{char_delete_slot}, $deleteDate);
1554 message TF("Your character will be delete, left %s\n", $chars[$messageSender->{char_delete_slot}]{deleteDate}), "connection";
1555 } elsif ($result == 0) {
1556 error T("That character already planned to be erased!\n");
1557 } elsif ($result == 3) {
1558 error T("Error in database of the server!\n");
1559 } elsif ($result == 4) {
1560 error T("To delete a character you must withdraw from the guild!\n");
1561 } elsif ($result == 5) {
1562 error T("To delete a character you must withdraw from the party!\n");
1563 } else {
1564 error TF("Unknown error when trying to delete the character! (Error number: %s)\n", $result);
1565 }
1566
1567 charSelectScreen;
1568}
1569
1570# 082A,10
1571sub char_delete2_accept_result {
1572 my ($self, $args) = @_;
1573 my $charID = $args->{charID};
1574 my $result = $args->{result};
1575
1576 if ($result == 1) { # Success
1577 if (defined $AI::temp::delIndex) {
1578 message TF("Character %s (%d) deleted.\n", $chars[$AI::temp::delIndex]{name}, $AI::temp::delIndex), "info";
1579 delete $chars[$AI::temp::delIndex];
1580 undef $AI::temp::delIndex;
1581 for (my $i = 0; $i < @chars; $i++) {
1582 delete $chars[$i] if ($chars[$i] && !scalar(keys %{$chars[$i]}))
1583 }
1584 } else {
1585 message T("Character deleted.\n"), "info";
1586 }
1587
1588 if (charSelectScreen() == 1) {
1589 $net->setState(3);
1590 $firstLoginMap = 1;
1591 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1592 $sentWelcomeMessage = 1;
1593 }
1594 return;
1595 } elsif ($result == 0) {
1596 error T("Enter your 6-digit birthday (YYMMDD) (e.g: 801122).\n");
1597 } elsif ($result == 2) {
1598 error T("Due to system settings, can not be deleted.\n");
1599 } elsif ($result == 3) {
1600 error T("A database error has occurred.\n");
1601 } elsif ($result == 4) {
1602 error T("You cannot delete this character at the moment.\n");
1603 } elsif ($result == 5) {
1604 error T("Your entered birthday does not match.\n");
1605 } elsif ($result == 7) {
1606 error T("Character Deletion has failed because you have entered an incorrect e-mail address.\n");
1607 } else {
1608 error TF("An unknown error has occurred. Error number %d\n", $result);
1609 }
1610
1611 undef $AI::temp::delIndex;
1612 if (charSelectScreen() == 1) {
1613 $net->setState(3);
1614 $firstLoginMap = 1;
1615 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1616 $sentWelcomeMessage = 1;
1617 }
1618}
1619
1620# 082C,14
1621sub char_delete2_cancel_result {
1622 my ($self, $args) = @_;
1623 my $result = $args->{result};
1624
1625 if ($result) {
1626 message T("Character is no longer scheduled to be deleted\n"), "connection";
1627 $chars[$messageSender->{char_delete_slot}]{deleteDate} = '';
1628 } elsif ($result == 2) {
1629 error T("Error in database of the server!\n");
1630 } else {
1631 error TF("Unknown error when trying to cancel the deletion of the character! (Error number: %s)\n", $result);
1632 }
1633
1634 charSelectScreen;
1635}
1636
1637# 013C
1638sub arrow_equipped {
1639 my ($self, $args) = @_;
1640 return unless changeToInGameState();
1641 return unless $args->{index};
1642 $char->{arrow} = $args->{index};
1643
1644 my $item = $char->inventory->getByServerIndex($args->{index});
1645 if ($item && $char->{equipment}{arrow} != $item) {
1646 $char->{equipment}{arrow} = $item;
1647 $item->{equipped} = 32768;
1648 $ai_v{temp}{waitForEquip}-- if $ai_v{temp}{waitForEquip};
1649 message TF("Arrow/Bullet equipped: %s (%d) x %s\n", $item->{name}, $item->{invIndex}, $item->{amount});
1650 Plugins::callHook('equipped_item', {slot => 'arrow', item => $item});
1651 }
1652}
1653
1654# 00AF, 07FA
1655sub inventory_item_removed {
1656 my ($self, $args) = @_;
1657 return unless changeToInGameState();
1658 my $item = $char->inventory->getByServerIndex($args->{index});
1659 my $reason = $args->{reason};
1660
1661 if ($reason) {
1662 if ($reason == 1) {
1663 debug TF("%s was used to cast the skill\n", $item->{name}), "inventory", 1;
1664 } elsif ($reason == 2) {
1665 debug TF("%s broke due to the refinement failed\n", $item->{name}), "inventory", 1;
1666 } elsif ($reason == 3) {
1667 debug TF("%s used in a chemical reaction\n", $item->{name}), "inventory", 1;
1668 } elsif ($reason == 4) {
1669 debug TF("%s was moved to the storage\n", $item->{name}), "inventory", 1;
1670 } elsif ($reason == 5) {
1671 debug TF("%s was moved to the cart\n", $item->{name}), "inventory", 1;
1672 } elsif ($reason == 6) {
1673 debug TF("%s was sold\n", $item->{name}), "inventory", 1;
1674 } elsif ($reason == 7) {
1675 debug TF("%s was consumed by Four Spirit Analysis skill\n", $item->{name}), "inventory", 1;
1676 } else {
1677 debug TF("%s was consumed by an unknown reason (reason number %s)\n", $item->{name}, $reason), "inventory", 1;
1678 }
1679 }
1680
1681 if ($item) {
1682 inventoryItemRemoved($item->{invIndex}, $args->{amount});
1683 Plugins::callHook('packet_item_removed', {index => $item->{invIndex}});
1684 }
1685}
1686
1687# 012B
1688sub cart_off {
1689 $char->cart->close;
1690 message T("Cart released.\n"), "success";
1691}
1692
1693# 012D
1694sub shop_skill {
1695 my ($self, $args) = @_;
1696
1697 # Used the shop skill.
1698 my $number = $args->{number};
1699 message TF("You can sell %s items!\n", $number);
1700}
1701
1702# 01D0 (spirits), 01E1 (coins), 08CF (amulets)
1703sub revolving_entity {
1704 my ($self, $args) = @_;
1705
1706 # Monk Spirits or Gunslingers' coins or senior ninja
1707 my $sourceID = $args->{sourceID};
1708 my $entityNum = $args->{entity};
1709 my $entityElement = $elements_lut{$args->{type}} if ($args->{type} && $entityNum);
1710 my $entityType;
1711
1712 my $actor = Actor::get($sourceID);
1713 if ($args->{switch} eq '01D0') {
1714 # Translation Comment: Spirit sphere of the monks
1715 $entityType = T('spirit');
1716 } elsif ($args->{switch} eq '01E1') {
1717 # Translation Comment: Coin of the gunslinger
1718 $entityType = T('coin');
1719 } elsif ($args->{switch} eq '08CF') {
1720 # Translation Comment: Amulet of the warlock
1721 $entityType = T('amulet');
1722 } else {
1723 $entityType = T('entity unknown');
1724 }
1725
1726 if ($sourceID eq $accountID && $entityNum != $char->{spirits}) {
1727 $char->{spirits} = $entityNum;
1728 $char->{amuletType} = $entityElement;
1729 $entityElement ?
1730 # Translation Comment: Message displays following: quantity, the name of the entity and its element
1731 message TF("You have %s %s(s) of %s now\n", $entityNum, $entityType, $entityElement), "parseMsg_statuslook", 1 :
1732 # Translation Comment: Message displays following: quantity and the name of the entity
1733 message TF("You have %s %s(s) now\n", $entityNum, $entityType), "parseMsg_statuslook", 1;
1734 } elsif ($entityNum != $actor->{spirits}) {
1735 $actor->{spirits} = $entityNum;
1736 $actor->{amuletType} = $entityElement;
1737 $entityElement ?
1738 # Translation Comment: Message displays following: actor, quantity, the name of the entity and its element
1739 message TF("%s has %s %s(s) of %s now\n", $actor, $entityNum, $entityType, $entityElement), "parseMsg_statuslook", 1 :
1740 # Translation Comment: Message displays following: actor, quantity and the name of the entity
1741 message TF("%s has %s %s(s) now\n", $actor, $entityNum, $entityType), "parseMsg_statuslook", 1;
1742 }
1743}
1744
1745# 0977
1746sub monster_hp_info {
1747 my ($self, $args) = @_;
1748 my $monster = $monstersList->getByID($args->{ID});
1749 if ($monster) {
1750 $monster->{hp} = $args->{hp};
1751 $monster->{hp_max} = $args->{hp_max};
1752
1753 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";
1754 }
1755}
1756
1757##
1758# account_id({accountID})
1759#
1760# This is for what eA calls PacketVersion 9, they send the AID in a 'proper' packet
1761sub account_id {
1762 my ($self, $args) = @_;
1763 # the account ID is already unpacked into PLAIN TEXT when it gets to this function...
1764 # So lets not fuckup the $accountID since we need that later... someone will prolly have to fix this later on
1765 my $accountID = $args->{accountID};
1766 debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
1767}
1768
1769##
1770# marriage_partner_name({String name})
1771#
1772# Name of the partner character, sent to everyone around right before casting "I miss you".
1773sub marriage_partner_name {
1774 my ($self, $args) = @_;
1775
1776 message TF("Marriage partner name: %s\n", $args->{name});
1777}
1778
1779sub login_pin_code_request {
1780 # This is ten second-level password login for 2013/3/29 upgrading of twRO
1781 my ($self, $args) = @_;
1782 # flags:
1783 # 0 - correct
1784 # 1 - requested (already defined)
1785 # 2 - requested (not defined)
1786 # 3 - expired
1787 # 5 - invalid (official servers?)
1788 # 7 - disabled?
1789 # 8 - incorrect
1790 if ($args->{flag} == 0) { # removed check for seed 0, eA/rA/brA sends a normal seed.
1791 message T("PIN code is correct.\n"), "success";
1792 # call charSelectScreen
1793 if (charSelectScreen(1) == 1) {
1794 $firstLoginMap = 1;
1795 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1796 $sentWelcomeMessage = 1;
1797 }
1798 } elsif ($args->{flag} == 1) {
1799 # PIN code query request.
1800 $accountID = $args->{accountID};
1801 debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
1802
1803 message T("Server requested PIN password in order to select your character.\n"), "connection";
1804 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1805 $messageSender->sendLoginPinCode($args->{seed}, 0);
1806 } elsif ($args->{flag} == 2) {
1807 # PIN code has never been set before, so set it.
1808 warning T("PIN password is not set for this account.\n"), "connection";
1809 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1810
1811 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1812 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1813 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1814 }
1815 $messageSender->sendLoginPinCode($args->{seed}, 1);
1816 } elsif ($args->{flag} == 3) {
1817 # should we use the same one again? is it possible?
1818 warning T("PIN password expired.\n"), "connection";
1819 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1820
1821 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1822 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1823 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1824 }
1825 $messageSender->sendLoginPinCode($args->{seed}, 1);
1826 } elsif ($args->{flag} == 5) {
1827 # PIN code invalid.
1828 error T("PIN code is invalid, don't use sequences or repeated numbers.\n");
1829 # configModify('loginPinCode', '', 1);
1830 return if (!($self->queryAndSaveLoginPinCode(T("The login PIN code that you entered is invalid. Please re-enter your login PIN code."))));
1831 $messageSender->sendLoginPinCode($args->{seed}, 0);
1832 } elsif ($args->{flag} == 7) {
1833 # PIN code disabled.
1834 $accountID = $args->{accountID};
1835 debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
1836
1837 # call charSelectScreen
1838 $self->{lockCharScreen} = 0;
1839 if (charSelectScreen(1) == 1) {
1840 $firstLoginMap = 1;
1841 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1842 $sentWelcomeMessage = 1;
1843 }
1844 } elsif ($args->{flag} == 8) {
1845 # PIN code incorrect.
1846 error T("PIN code is incorrect.\n");
1847 #configModify('loginPinCode', '', 1);
1848 return if (!($self->queryAndSaveLoginPinCode(T("The login PIN code that you entered is incorrect. Please re-enter your login PIN code."))));
1849 $messageSender->sendLoginPinCode($args->{seed}, 0);
1850 } else {
1851 debug("login_pin_code_request: unknown flag $args->{flag}\n");
1852 }
1853
1854 $timeout{master}{time} = time;
1855}
1856
1857sub login_pin_code_request2 {
1858 my ($self, $args) = @_;
1859 # flags:
1860 # 0 - correct - RMS
1861 # 1 - requested (already defined) - RMS
1862 # 3 - expired - RMS(?)
1863 # 4 - requested (not defined) - RMS
1864 # 7 - correct - RMS
1865 # 8 - incorrect - RMS
1866 if ($args->{flag} == 7) { # removed check for seed 0, eA/rA/brA sends a normal seed.
1867 message T("PIN code is correct.\n"), "success";
1868 # call charSelectScreen
1869 if (charSelectScreen(1) == 1) {
1870 $firstLoginMap = 1;
1871 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
1872 $sentWelcomeMessage = 1;
1873 }
1874 } elsif ($args->{flag} == 1) {
1875 # PIN code query request.
1876 $accountID = $args->{accountID};
1877 debug sprintf("Account ID: %s (%s)\n", unpack('V',$accountID), getHex($accountID));
1878
1879 message T("Server requested PIN password in order to select your character.\n"), "connection";
1880 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1881 $messageSender->sendLoginPinCode($args->{seed}, 0);
1882 } elsif ($args->{flag} == 4) {
1883 # PIN code has never been set before, so set it.
1884 warning T("PIN password is not set for this account.\n"), "connection";
1885 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1886
1887 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1888 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1889 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1890 }
1891 $messageSender->sendLoginPinCode($args->{seed}, 1);
1892 } elsif ($args->{flag} == 3) {
1893 # should we use the same one again? is it possible?
1894 warning T("PIN password expired.\n"), "connection";
1895 return if ($config{loginPinCode} eq '' && !($self->queryAndSaveLoginPinCode()));
1896
1897 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1898 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1899 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1900 }
1901 $messageSender->sendLoginPinCode($args->{seed}, 1);
1902 } elsif ($args->{flag} == 8) {
1903 # PIN code incorrect.
1904 error T("PIN code is incorrect.\n");
1905 #configModify('loginPinCode', '', 1);
1906 return if (!($self->queryAndSaveLoginPinCode(T("The login PIN code that you entered is incorrect. Please re-enter your login PIN code."))));
1907 $messageSender->sendLoginPinCode($args->{seed}, 0);
1908 } else {
1909 debug("login_pin_code_request: unknown flag $args->{flag}\n");
1910 }
1911
1912 $timeout{master}{time} = time;
1913}
1914
1915sub login_pin_new_code_result {
1916 my ($self, $args) = @_;
1917
1918 if ($args->{flag} == 2) {
1919 # PIN code invalid.
1920 error T("PIN code is invalid, don't use sequences or repeated numbers.\n");
1921 #configModify('loginPinCode', '', 1);
1922 return if (!($self->queryAndSaveLoginPinCode(T("PIN code is invalid, don't use sequences or repeated numbers.\n"))));
1923
1924 # there's a bug in bRO where you can use letters or symbols or even a string as your PIN code.
1925 # as a result this will render you unable to login again (forever?) using the official client
1926 # and this is detectable and can result in a permanent ban. we're using this code in order to
1927 # prevent this. - revok 17.12.2012
1928 while ((($config{loginPinCode} =~ /[^0-9]/) || (length($config{loginPinCode}) != 4)) &&
1929 !($self->queryAndSaveLoginPinCode("Your PIN should never contain anything but exactly 4 numbers.\n"))) {
1930 error T("Your PIN should never contain anything but exactly 4 numbers.\n");
1931 }
1932
1933 $messageSender->sendLoginPinCode($args->{seed}, 0);
1934 }
1935}
1936
1937sub actor_status_active {
1938 my ($self, $args) = @_;
1939 return unless changeToInGameState();
1940 my ($type, $ID, $tick, $unknown1, $unknown2, $unknown3, $unknown4) = @{$args}{qw(type ID tick unknown1 unknown2 unknown3 unknown4)};
1941 my $flag = (exists $args->{flag}) ? $args->{flag} : 1;
1942 my $status = defined $statusHandle{$type} ? $statusHandle{$type} : "UNKNOWN_STATUS_$type";
1943 $char->cart->changeType($unknown1) if ($type == 673 && defined $unknown1 && ($ID eq $accountID)); # for Cart active
1944 $args->{skillName} = defined $statusName{$status} ? $statusName{$status} : $status;
1945# ($args->{actor} = Actor::get($ID))->setStatus($status, 1, $tick == 9999 ? undef : $tick, $args->{unknown1}); # need test for '08FF'
1946 ($args->{actor} = Actor::get($ID))->setStatus($status, $flag, $tick == 9999 ? undef : $tick);
1947 # Rolling Cutter counters.
1948 if ( $type == 0x153 && $char->{spirits} != $unknown1 ) {
1949 $char->{spirits} = $unknown1 || 0;
1950 if ( $ID eq $accountID ) {
1951 message TF( "You have %s %s(s) now\n", $char->{spirits}, 'counters' ), "parseMsg_statuslook", 1;
1952 } else {
1953 message TF( "%s has %s %s(s) now\n", $args->{actor}, $char->{spirits}, 'counters' ), "parseMsg_statuslook", 1;
1954 }
1955 }
1956}
1957
1958#099B
1959sub map_property3 {
1960 my ($self, $args) = @_;
1961
1962 if($config{'status_mapType'}){
1963 $char->setStatus(@$_) for map {[$_->[1], $args->{type} == $_->[0]]}
1964 grep { $args->{type} == $_->[0] || $char->{statuses}{$_->[1]} }
1965 map {[$_, defined $mapTypeHandle{$_} ? $mapTypeHandle{$_} : "UNKNOWN_MAPTYPE_$_"]}
1966 0 .. List::Util::max $args->{type}, keys %mapTypeHandle;
1967
1968 if ($args->{info_table}) {
1969 my $info_table = unpack('V1',$args->{info_table});
1970 for (my $i = 0; $i < 16; $i++) {
1971 if ($info_table&(1<<$i)) {
1972 $char->setStatus(defined $mapPropertyInfoHandle{$i} ? $mapPropertyInfoHandle{$i} : "UNKNOWN_MAPPROPERTY_INFO_$i",1);
1973 }
1974 }
1975 }
1976 }
1977
1978 $pvp = {6 => 1, 8 => 2, 19 => 3}->{$args->{type}};
1979 if ($pvp) {
1980 Plugins::callHook('pvp_mode', {
1981 pvp => $pvp # 1 PvP, 2 GvG, 3 Battleground
1982 });
1983 }
1984}
1985
1986#099F
1987sub area_spell_multiple2 {
1988 my ($self, $args) = @_;
1989
1990 # Area effect spells; including traps!
1991 my $len = $args->{len} - 4;
1992 my $spellInfo = $args->{spellInfo};
1993 my $msg = "";
1994 my $binID;
1995 my ($ID, $sourceID, $x, $y, $type, $range, $fail);
1996 for (my $i = 0; $i < $len; $i += 18) {
1997 $msg = substr($spellInfo, $i, 18);
1998 ($ID, $sourceID, $x, $y, $type, $range, $fail) = unpack('a4 a4 v3 X2 C2', $msg);
1999
2000 if ($spells{$ID} && $spells{$ID}{'sourceID'} eq $sourceID) {
2001 $binID = binFind(\@spellsID, $ID);
2002 $binID = binAdd(\@spellsID, $ID) if ($binID eq "");
2003 } else {
2004 $binID = binAdd(\@spellsID, $ID);
2005 }
2006
2007 $spells{$ID}{'sourceID'} = $sourceID;
2008 $spells{$ID}{'pos'}{'x'} = $x;
2009 $spells{$ID}{'pos'}{'y'} = $y;
2010 $spells{$ID}{'pos_to'}{'x'} = $x;
2011 $spells{$ID}{'pos_to'}{'y'} = $y;
2012 $spells{$ID}{'binID'} = $binID;
2013 $spells{$ID}{'type'} = $type;
2014 if ($type == 0x81) {
2015 message TF("%s opened Warp Portal on (%d, %d)\n", getActorName($sourceID), $x, $y), "skill";
2016 }
2017 debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)\n", "skill", 2;
2018 }
2019
2020 Plugins::callHook('packet_areaSpell', {
2021 fail => $fail,
2022 sourceID => $sourceID,
2023 type => $type,
2024 x => $x,
2025 y => $y
2026 });
2027}
2028
2029#09CA
2030sub area_spell_multiple3 {
2031 my ($self, $args) = @_;
2032
2033 # Area effect spells; including traps!
2034 my $len = $args->{len} - 4;
2035 my $spellInfo = $args->{spellInfo};
2036 my $msg = "";
2037 my $binID;
2038 my ($ID, $sourceID, $x, $y, $type, $range, $fail);
2039 for (my $i = 0; $i < $len; $i += 19) {
2040 $msg = substr($spellInfo, $i, 19);
2041 ($ID, $sourceID, $x, $y, $type, $range, $fail) = unpack('a4 a4 v3 X3 C2', $msg);
2042
2043 if ($spells{$ID} && $spells{$ID}{'sourceID'} eq $sourceID) {
2044 $binID = binFind(\@spellsID, $ID);
2045 $binID = binAdd(\@spellsID, $ID) if ($binID eq "");
2046 } else {
2047 $binID = binAdd(\@spellsID, $ID);
2048 }
2049
2050 $spells{$ID}{'sourceID'} = $sourceID;
2051 $spells{$ID}{'pos'}{'x'} = $x;
2052 $spells{$ID}{'pos'}{'y'} = $y;
2053 $spells{$ID}{'pos_to'}{'x'} = $x;
2054 $spells{$ID}{'pos_to'}{'y'} = $y;
2055 $spells{$ID}{'binID'} = $binID;
2056 $spells{$ID}{'type'} = $type;
2057 if ($type == 0x81) {
2058 message TF("%s opened Warp Portal on (%d, %d)\n", getActorName($sourceID), $x, $y), "skill";
2059 }
2060 debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)\n", "skill", 2;
2061 }
2062
2063 Plugins::callHook('packet_areaSpell', {
2064 fail => $fail,
2065 sourceID => $sourceID,
2066 type => $type,
2067 x => $x,
2068 y => $y
2069 });
2070}
2071
2072sub sync_request_ex {
2073 my ($self, $args) = @_;
2074
2075 # Debug Log
2076 # message "Received Sync Ex : 0x" . $args->{switch} . "\n";
2077
2078 # Computing Sync Ex - By Fr3DBr
2079 my $PacketID = $args->{switch};
2080
2081 # Getting Sync Ex Reply ID from Table
2082 my $SyncID = $self->{sync_ex_reply}->{$PacketID};
2083
2084 # Cleaning Leading Zeros
2085 $PacketID =~ s/^0+//;
2086
2087 # Cleaning Leading Zeros
2088 $SyncID =~ s/^0+//;
2089
2090 # Debug Log
2091 #error sprintf("Received Ex Packet ID : 0x%s => 0x%s\n", $PacketID, $SyncID);
2092
2093 # Converting ID to Hex Number
2094 $SyncID = hex($SyncID);
2095
2096 # Dispatching Sync Ex Reply
2097 $messageSender->sendReplySyncRequestEx($SyncID);
2098}
2099
2100sub cash_shop_list {
2101 my ($self, $args) = @_;
2102 my $tabcode = $args->{tabcode};
2103 my $jump = 6;
2104 my $unpack_string = "v V";
2105 # CASHSHOP_TAB_NEW => 0x0,
2106 # CASHSHOP_TAB_POPULAR => 0x1,
2107 # CASHSHOP_TAB_LIMITED => 0x2,
2108 # CASHSHOP_TAB_RENTAL => 0x3,
2109 # CASHSHOP_TAB_PERPETUITY => 0x4,
2110 # CASHSHOP_TAB_BUFF => 0x5,
2111 # CASHSHOP_TAB_RECOVERY => 0x6,
2112 # CASHSHOP_TAB_ETC => 0x7
2113 # CASHSHOP_TAB_MAX => 8
2114 my %cashitem_tab = (
2115 0 => 'New',
2116 1 => 'Popular',
2117 2 => 'Limited',
2118 3 => 'Rental',
2119 4 => 'Perpetuity',
2120 5 => 'Buff',
2121 6 => 'Recovery',
2122 7 => 'Etc',
2123 );
2124 debug TF("%s\n" .
2125 "# Name Price\n",
2126 center(' Tab: ' . $cashitem_tab{$tabcode} . ' ', 44, '-')), "list";
2127 for (my $i = 0; $i < length($args->{itemInfo}); $i += $jump) {
2128 my ($ID, $price) = unpack($unpack_string, substr($args->{itemInfo}, $i));
2129 my $name = itemNameSimple($ID);
2130 push(@{$cashShop{list}[$tabcode]}, {item_id => $ID, price => $price}); # add to cashshop
2131 debug(swrite(
2132 "@<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>C",
2133 [$i, $name, formatNumber($price)]),
2134 "list");
2135
2136 }
2137}
2138
2139sub cash_shop_open_result {
2140 my ($self, $args) = @_;
2141 #'0845' => ['cash_window_shop_open', 'v2', [qw(cash_points kafra_points)]],
2142 message TF("Cash Points: %sC - Kafra Points: %sC\n", formatNumber ($args->{cash_points}), formatNumber ($args->{kafra_points}));
2143 $cashShop{points} = {
2144 cash => $args->{cash_points},
2145 kafra => $args->{kafra_points}
2146 };
2147}
2148
2149sub cash_shop_buy_result {
2150 my ($self, $args) = @_;
2151 # TODO: implement result messages:
2152 # SUCCESS = 0x0,
2153 # WRONG_TAB? = 0x1, // we should take care with this, as it's detectable by the server
2154 # SHORTTAGE_CASH = 0x2,
2155 # UNKONWN_ITEM = 0x3,
2156 # INVENTORY_WEIGHT = 0x4,
2157 # INVENTORY_ITEMCNT = 0x5,
2158 # RUNE_OVERCOUNT = 0x9,
2159 # EACHITEM_OVERCOUNT = 0xa,
2160 # UNKNOWN = 0xb,
2161 if ($args->{result} > 0) {
2162 error TF("Error while buying %s from cash shop. Error code: %s\n", itemNameSimple($args->{item_id}), $args->{result});
2163 } else {
2164 message TF("Bought %s from cash shop. Current CASH: %s\n", itemNameSimple($args->{item_id}), formatNumber($args->{updated_points})), "success";
2165 $cashShop{points}->{cash} = $args->{updated_points};
2166 }
2167
2168 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}));
2169
2170
2171}
2172
2173sub player_equipment {
2174 my ($self, $args) = @_;
2175
2176 my ($sourceID, $type, $ID1, $ID2) = @{$args}{qw(sourceID type ID1 ID2)};
2177 my $player = ($sourceID ne $accountID)? $playersList->getByID($sourceID) : $char;
2178 return unless $player;
2179
2180 if ($type == 0) {
2181 # Player changed job
2182 $player->{jobID} = $ID1;
2183
2184 } elsif ($type == 2) {
2185 if ($ID1 ne $player->{weapon}) {
2186 message TF("%s changed Weapon to %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
2187 $player->{weapon} = $ID1;
2188 }
2189 if ($ID2 ne $player->{shield}) {
2190 message TF("%s changed Shield to %s\n", $player, itemName({nameID => $ID2})), "parseMsg_statuslook", 2;
2191 $player->{shield} = $ID2;
2192 }
2193 } elsif ($type == 3) {
2194 $player->{headgear}{low} = $ID1;
2195 } elsif ($type == 4) {
2196 $player->{headgear}{top} = $ID1;
2197 } elsif ($type == 5) {
2198 $player->{headgear}{mid} = $ID1;
2199 } elsif ($type == 9) {
2200 if ($player->{shoes} && $ID1 ne $player->{shoes}) {
2201 message TF("%s changed Shoes to: %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
2202 }
2203 $player->{shoes} = $ID1;
2204 }
2205}
2206
2207sub progress_bar {
2208 my($self, $args) = @_;
2209 message TF("Progress bar loading (time: %d).\n", $args->{time}), 'info';
2210 $char->{progress_bar} = 1;
2211 $taskManager->add(
2212 new Task::Chained(tasks => [new Task::Wait(seconds => $args->{time}),
2213 new Task::Function(function => sub {
2214 $messageSender->sendProgress();
2215 message TF("Progress bar finished.\n"), 'info';
2216 $char->{progress_bar} = 0;
2217 $_[0]->setDone;
2218 })]));
2219}
2220
2221sub progress_bar_stop {
2222 my($self, $args) = @_;
2223 message TF("Progress bar finished.\n", 'info');
2224}
2225
2226# 02B1
2227sub quest_all_list {
2228 my ($self, $args) = @_;
2229 $questList = {};
2230 for (my $i = 8; $i < $args->{amount}*5+8; $i += 5) {
2231 my ($questID, $active) = unpack('V C', substr($args->{RAW_MSG}, $i, 5));
2232 $questList->{$questID}->{active} = $active;
2233 debug "$questID $active\n", "info";
2234 }
2235}
2236
2237# 02B2
2238# note: this packet shows all quests + their missions and has variable length
2239sub quest_all_mission {
2240 my ($self, $args) = @_;
2241 debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
2242 for (my $i = 8; $i < $args->{amount}*104+8; $i += 104) {
2243 my ($questID, $time_start, $time, $mission_amount) = unpack('V3 v', substr($args->{RAW_MSG}, $i, 14));
2244 my $quest = \%{$questList->{$questID}};
2245 $quest->{time_start} = $time_start;
2246 $quest->{time} = $time;
2247 debug "$questID $time_start $time $mission_amount\n", "info";
2248 for (my $j = 0; $j < $mission_amount; $j++) {
2249 my ($mobID, $count, $mobName) = unpack('V v Z24', substr($args->{RAW_MSG}, 14+$i+$j*30, 30));
2250 my $mission = \%{$quest->{missions}->{$mobID}};
2251 $mission->{mobID} = $mobID;
2252 $mission->{count} = $count;
2253 $mission->{mobName} = bytesToString($mobName);
2254 debug "- $mobID $count $mobName\n", "info";
2255 }
2256 }
2257}
2258
2259# 02B3
2260# note: this packet shows all missions for 1 quest and has fixed length
2261sub quest_add {
2262 my ($self, $args) = @_;
2263 my $questID = $args->{questID};
2264 my $quest = \%{$questList->{$questID}};
2265
2266 unless (%$quest) {
2267 message TF("Quest: %s has been added.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
2268 }
2269
2270 $quest->{time_start} = $args->{time_start};
2271 $quest->{time} = $args->{time};
2272 $quest->{active} = $args->{active};
2273 debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
2274 for (my $i = 0; $i < $args->{amount}; $i++) {
2275 my ($mobID, $count, $mobName) = unpack('V v Z24', substr($args->{RAW_MSG}, 17+$i*30, 30));
2276 my $mission = \%{$quest->{missions}->{$mobID}};
2277 $mission->{mobID} = $mobID;
2278 $mission->{count} = $count;
2279 $mission->{mobName} = bytesToString($mobName);
2280 debug "- $mobID $count $mobName\n", "info";
2281 }
2282}
2283
2284# 02B4
2285sub quest_delete {
2286 my ($self, $args) = @_;
2287 my $questID = $args->{questID};
2288 message TF("Quest: %s has been deleted.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
2289 delete $questList->{$questID};
2290}
2291
2292sub parse_quest_update_mission_hunt {
2293 my ($self, $args) = @_;
2294 @{$args->{mobs}} = map {
2295 my %result; @result{qw(questID mobID count)} = unpack 'V2 v', $_; \%result
2296 } unpack '(a10)*', $args->{mobInfo};
2297}
2298
2299sub reconstruct_quest_update_mission_hunt {
2300 my ($self, $args) = @_;
2301 $args->{mobInfo} = pack '(a10)*', map { pack 'V2 v', @{$_}{qw(questID mobID count)} } @{$args->{mobs}};
2302}
2303
2304sub parse_quest_update_mission_hunt_v2 {
2305 my ($self, $args) = @_;
2306 @{$args->{mobs}} = map {
2307 my %result; @result{qw(questID mobID goal count)} = unpack 'V2 v2', $_; \%result
2308 } unpack '(a12)*', $args->{mobInfo};
2309}
2310
2311sub reconstruct_quest_update_mission_hunt_v2 {
2312 my ($self, $args) = @_;
2313 $args->{mobInfo} = pack '(a12)*', map { pack 'V2 v2', @{$_}{qw(questID mobID goal count)} } @{$args->{mobs}};
2314}
2315
2316# 02B5
2317sub quest_update_mission_hunt {
2318 my ($self, $args) = @_;
2319 my ($questID, $mobID, $goal, $count) = unpack('V2 v2', substr($args->{RAW_MSG}, 6));
2320 my $quest = \%{$questList->{$questID}};
2321 my $mission = \%{$quest->{missions}->{$mobID}};
2322 $mission->{goal} = $goal;
2323 $mission->{count} = $count;
2324 debug "- $questID $mobID $count $goal\n", "info";
2325}
2326
2327# 02B7
2328sub quest_active {
2329 my ($self, $args) = @_;
2330 my $questID = $args->{questID};
2331
2332 message $args->{active}
2333 ? TF("Quest %s is now active.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
2334 : TF("Quest %s is now inactive.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
2335 , "info";
2336
2337 $questList->{$args->{questID}}->{active} = $args->{active};
2338}
2339
2340# 02C1
2341sub parse_npc_chat {
2342 my ($self, $args) = @_;
2343
2344 $args->{actor} = Actor::get($args->{ID});
2345}
2346
2347sub npc_chat {
2348 my ($self, $args) = @_;
2349
2350 # like public_chat, but also has color
2351
2352 my $actor = $args->{actor};
2353 my $message = $args->{message}; # needs bytesToString or not?
2354 my $position = sprintf("[%s %d, %d]",
2355 $field ? $field->baseName : T("Unknown field,"),
2356 @{$char->{pos_to}}{qw(x y)});
2357 my $dist;
2358
2359 if ($message =~ / : /) {
2360 ((my $name), $message) = split / : /, $message, 2;
2361 $dist = 'unknown';
2362 unless ($actor->isa('Actor::Unknown')) {
2363 $dist = distance($char->{pos_to}, $actor->{pos_to});
2364 $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
2365 }
2366 if ($actor->{name} eq $name) {
2367 $name = "$actor";
2368 } else {
2369 $name = sprintf "%s (%s)", $name, $actor->{binID};
2370 }
2371 $message = "$name: $message";
2372
2373 $position .= sprintf(" [%d, %d] [dist=%s] (%d)",
2374 @{$actor->{pos_to}}{qw(x y)},
2375 $dist, $actor->{nameID});
2376 $dist = "[dist=$dist] ";
2377 }
2378
2379 chatLog("npc", "$position $message\n") if ($config{logChat});
2380 message TF("%s%s\n", $dist, $message), "npcchat";
2381
2382 # TODO hook
2383}
2384
2385sub forge_list {
2386 my ($self, $args) = @_;
2387
2388 message T("========Forge List========\n");
2389 for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 8) {
2390 my $viewID = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
2391 message "$viewID $items_lut{$viewID}\n";
2392 # always 0x0012
2393 #my $unknown = unpack("v1", substr($args->{RAW_MSG}, $i+2, 2));
2394 # ???
2395 #my $charID = substr($args->{RAW_MSG}, $i+4, 4);
2396 }
2397 message "=========================\n";
2398}
2399
2400sub storage_opened {
2401 my ($self, $args) = @_;
2402 $char->storage->open($args);
2403}
2404
2405sub storage_closed {
2406 $char->storage->close();
2407 message T("Storage closed.\n"), "storage";;
2408
2409 # Storage log
2410 writeStorageLog(0);
2411
2412 if ($char->{dcOnEmptyItems} ne "") {
2413 message TF("Disconnecting on empty %s!\n", $char->{dcOnEmptyItems});
2414 chatLog("k", TF("Disconnecting on empty %s!\n", $char->{dcOnEmptyItems}));
2415 quit();
2416 }
2417}
2418
2419sub storage_items_stackable {
2420 my ($self, $args) = @_;
2421
2422 $self->_items_list({
2423 class => 'Actor::Item',
2424 hook => 'packet_storage',
2425 debug_str => 'Stackable Storage Item',
2426 items => [$self->parse_items_stackable($args)],
2427 getter => sub { $char->storage->getByServerIndex($_[0]{index}) },
2428 adder => sub { $char->storage->add($_[0]) },
2429 callback => sub {
2430 my ($local_item) = @_;
2431
2432 $local_item->{amount} = $local_item->{amount} & ~0x80000000;
2433 },
2434 });
2435
2436 $storageTitle = $args->{title} ? $args->{title} : undef;
2437}
2438
2439sub storage_items_nonstackable {
2440 my ($self, $args) = @_;
2441
2442 $self->_items_list({
2443 class => 'Actor::Item',
2444 hook => 'packet_storage',
2445 debug_str => 'Non-Stackable Storage Item',
2446 items => [$self->parse_items_nonstackable($args)],
2447 getter => sub { $char->storage->getByServerIndex($_[0]{index}) },
2448 adder => sub { $char->storage->add($_[0]) },
2449 });
2450
2451 $storageTitle = $args->{title} ? $args->{title} : undef;
2452}
2453
2454sub storage_item_added {
2455 my ($self, $args) = @_;
2456
2457 my $index = $args->{index};
2458 my $amount = $args->{amount};
2459
2460 my $item = $char->storage->getByServerIndex($index);
2461 if (!$item) {
2462 $item = new Actor::Item();
2463 $item->{nameID} = $args->{nameID};
2464 $item->{index} = $index;
2465 $item->{amount} = $amount;
2466 $item->{type} = $args->{type};
2467 $item->{identified} = $args->{identified};
2468 $item->{broken} = $args->{broken};
2469 $item->{upgrade} = $args->{upgrade};
2470 $item->{cards} = $args->{cards};
2471 $item->{options} = $args->{options};
2472 $item->{name} = itemName($item);
2473 $char->storage->add($item);
2474 } else {
2475 $item->{amount} += $amount;
2476 }
2477 my $disp = TF("Storage Item Added: %s (%d) x %d - %s",
2478 $item->{name}, $item->{invIndex}, $amount, $itemTypes_lut{$item->{type}});
2479 message "$disp\n", "drop";
2480
2481 $itemChange{$item->{name}} += $amount;
2482 $args->{item} = $item;
2483}
2484
2485sub storage_item_removed {
2486 my ($self, $args) = @_;
2487
2488 my ($index, $amount) = @{$args}{qw(index amount)};
2489
2490 my $item = $char->storage->getByServerIndex($index);
2491
2492 if ($item) {
2493 Misc::storageItemRemoved($item->{invIndex}, $amount);
2494 }
2495}
2496
2497sub cart_items_stackable {
2498 my ($self, $args) = @_;
2499
2500 $self->_items_list({
2501 class => 'Actor::Item',
2502 hook => 'packet_cart',
2503 debug_str => 'Stackable Cart Item',
2504 items => [$self->parse_items_stackable($args)],
2505 getter => sub { $char->cart->getByServerIndex($_[0]{index}) },
2506 adder => sub { $char->cart->add($_[0]) },
2507 });
2508}
2509
2510sub cart_items_nonstackable {
2511 my ($self, $args) = @_;
2512
2513 $self->_items_list({
2514 class => 'Actor::Item',
2515 hook => 'packet_cart',
2516 debug_str => 'Non-Stackable Cart Item',
2517 items => [$self->parse_items_nonstackable($args)],
2518 getter => sub { $char->cart->getByServerIndex($_[0]{index}) },
2519 adder => sub { $char->cart->add($_[0]) },
2520 });
2521}
2522
2523sub cart_item_added {
2524 my ($self, $args) = @_;
2525
2526 my $index = $args->{index};
2527 my $amount = $args->{amount};
2528
2529 my $item = $char->cart->getByServerIndex($index);
2530 if (!$item) {
2531 $item = new Actor::Item();
2532 $item->{index} = $args->{index};
2533 $item->{nameID} = $args->{nameID};
2534 $item->{amount} = $args->{amount};
2535 $item->{identified} = $args->{identified};
2536 $item->{broken} = $args->{broken};
2537 $item->{upgrade} = $args->{upgrade};
2538 $item->{cards} = $args->{cards};
2539 $item->{options} = $args->{options};
2540 $item->{type} = $args->{type} if (exists $args->{type});
2541 $item->{name} = itemName($item);
2542 $char->cart->add($item);
2543 } else {
2544 $item->{amount} += $args->{amount};
2545 }
2546 my $disp = TF("Cart Item Added: %s (%d) x %d - %s",
2547 $item->{name}, $item->{invIndex}, $amount, $itemTypes_lut{$item->{type}});
2548 message "$disp\n", "drop";
2549 $itemChange{$item->{name}} += $args->{amount};
2550 $args->{item} = $item;
2551}
2552
2553sub cart_item_removed {
2554 my ($self, $args) = @_;
2555
2556 my ($index, $amount) = @{$args}{qw(index amount)};
2557
2558 my $item = $char->cart->getByServerIndex($index);
2559
2560 if ($item) {
2561 Misc::cartItemRemoved($item->{invIndex}, $amount);
2562 }
2563}
2564
2565sub cart_info {
2566 my ($self, $args) = @_;
2567 $char->cart->info($args);
2568 debug "[cart_info] received.\n", "parseMsg";
2569}
2570
2571sub cart_add_failed {
2572 my ($self, $args) = @_;
2573
2574 my $reason;
2575 if ($args->{fail} == 0) {
2576 $reason = T('overweight');
2577 } elsif ($args->{fail} == 1) {
2578 $reason = T('too many items');
2579 } else {
2580 $reason = TF("Unknown code %s",$args->{fail});
2581 }
2582 error TF("Can't Add Cart Item (%s)\n", $reason);
2583}
2584
2585sub inventory_items_stackable {
2586 my ($self, $args) = @_;
2587 return unless changeToInGameState();
2588
2589 $self->_items_list({
2590 class => 'Actor::Item',
2591 hook => 'packet_inventory',
2592 debug_str => 'Stackable Inventory Item',
2593 items => [$self->parse_items_stackable($args)],
2594 getter => sub { $char->inventory->getByServerIndex($_[0]{index}) },
2595 adder => sub { $char->inventory->add($_[0]) },
2596 callback => sub {
2597 my ($local_item) = @_;
2598
2599 if (defined $char->{arrow} && $local_item->{index} == $char->{arrow}) {
2600 $local_item->{equipped} = 32768;
2601 $char->{equipment}{arrow} = $local_item;
2602 }
2603 }
2604 });
2605}
2606
2607sub login_error {
2608 my ($self, $args) = @_;
2609
2610 $net->serverDisconnect();
2611 if ($args->{type} == REFUSE_INVALID_ID) {
2612 error TF("Account name [%s] doesn't exist\n", $config{'username'}), "connection";
2613 if (!$net->clientAlive() && !$config{'ignoreInvalidLogin'} && !UNIVERSAL::isa($net, 'Network::XKoreProxy')) {
2614 my $username = $interface->query(T("Enter your Ragnarok Online username again."));
2615 if (defined($username)) {
2616 configModify('username', $username, 1);
2617 $timeout_ex{master}{time} = 0;
2618 $conState_tries = 0;
2619 } else {
2620 quit();
2621 return;
2622 }
2623 }
2624 } elsif ($args->{type} == REFUSE_INVALID_PASSWD) {
2625 error TF("Password Error for account [%s]\n", $config{'username'}), "connection";
2626 if (!$net->clientAlive() && !$config{'ignoreInvalidLogin'} && !UNIVERSAL::isa($net, 'Network::XKoreProxy')) {
2627 my $password = $interface->query(T("Enter your Ragnarok Online password again."), isPassword => 1);
2628 if (defined($password)) {
2629 configModify('password', $password, 1);
2630 $timeout_ex{master}{time} = 0;
2631 $conState_tries = 0;
2632 } else {
2633 quit();
2634 return;
2635 }
2636 }
2637 } elsif ($args->{type} == ACCEPT_ID_PASSWD) {
2638 error T("The server has denied your connection.\n"), "connection";
2639 } elsif ($args->{type} == REFUSE_NOT_CONFIRMED) {
2640 $interface->errorDialog(T("Critical Error: Your account has been blocked."));
2641 $quit = 1 unless ($net->clientAlive());
2642 } elsif ($args->{type} == REFUSE_INVALID_VERSION) {
2643 my $master = $masterServer;
2644 error TF("Connect failed, something is wrong with the login settings:\n" .
2645 "version: %s\n" .
2646 "master_version: %s\n" .
2647 "serverType: %s\n", $master->{version}, $master->{master_version}, $masterServer->{serverType}), "connection";
2648 relog(30);
2649 } elsif ($args->{type} == REFUSE_BLOCK_TEMPORARY) {
2650 error TF("The server is temporarily blocking your connection until %s\n", $args->{date}), "connection";
2651 } elsif ($args->{type} == REFUSE_USER_PHONE_BLOCK) { #Phone lock
2652 error T("Please dial to activate the login procedure.\n"), "connection";
2653 Plugins::callHook('dial');
2654 relog(10);
2655 } elsif ($args->{type} == ACCEPT_LOGIN_USER_PHONE_BLOCK) {
2656 error T("Mobile Authentication: Max number of simultaneous IP addresses reached.\n"), "connection";
2657 } else {
2658 error TF("The server has denied your connection for unknown reason (%d).\n", $args->{type}), 'connection';
2659 }
2660
2661 if ($args->{type} != REFUSE_INVALID_VERSION && $versionSearch) {
2662 $versionSearch = 0;
2663 writeSectionedFileIntact(Settings::getTableFilename("servers.txt"), \%masterServers);
2664 }
2665}
2666
2667sub login_error_game_login_server {
2668 error T("Error logging into Character Server (invalid character specified)...\n"), 'connection';
2669 $net->setState(1);
2670 undef $conState_tries;
2671 $timeout_ex{master}{time} = time;
2672 $timeout_ex{master}{timeout} = $timeout{'reconnect'}{'timeout'};
2673 $net->serverDisconnect();
2674}
2675
2676sub character_deletion_successful {
2677 if (defined $AI::temp::delIndex) {
2678 message TF("Character %s (%d) deleted.\n", $chars[$AI::temp::delIndex]{name}, $AI::temp::delIndex), "info";
2679 delete $chars[$AI::temp::delIndex];
2680 undef $AI::temp::delIndex;
2681 for (my $i = 0; $i < @chars; $i++) {
2682 delete $chars[$i] if ($chars[$i] && !scalar(keys %{$chars[$i]}))
2683 }
2684 } else {
2685 message T("Character deleted.\n"), "info";
2686 }
2687
2688 if (charSelectScreen() == 1) {
2689 $net->setState(3);
2690 $firstLoginMap = 1;
2691 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
2692 $sentWelcomeMessage = 1;
2693 }
2694}
2695
2696sub character_deletion_failed {
2697 error T("Character cannot be deleted. Your e-mail address was probably wrong.\n");
2698 undef $AI::temp::delIndex;
2699 if (charSelectScreen() == 1) {
2700 $net->setState(3);
2701 $firstLoginMap = 1;
2702 $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
2703 $sentWelcomeMessage = 1;
2704 }
2705}
2706
2707sub character_moves {
2708 my ($self, $args) = @_;
2709
2710 return unless changeToInGameState();
2711 makeCoordsFromTo($char->{pos}, $char->{pos_to}, $args->{coords});
2712 my $dist = sprintf("%.1f", distance($char->{pos}, $char->{pos_to}));
2713 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";
2714 $char->{time_move} = time;
2715 $char->{time_move_calc} = distance($char->{pos}, $char->{pos_to}) * ($char->{walk_speed} || 0.12);
2716
2717 # Correct the direction in which we're looking
2718 my (%vec, $degree);
2719 getVector(\%vec, $char->{pos_to}, $char->{pos});
2720 $degree = vectorToDegree(\%vec);
2721 if (defined $degree) {
2722 my $direction = int sprintf("%.0f", (360 - $degree) / 45);
2723 $char->{look}{body} = $direction & 0x07;
2724 $char->{look}{head} = 0;
2725 }
2726
2727 # Ugly; AI code in network subsystem! This must be fixed.
2728 if (AI::action eq "mapRoute" && $config{route_escape_reachedNoPortal} && $dist eq "0.0"){
2729 if (!$portalsID[0]) {
2730 if ($config{route_escape_shout} ne "" && !defined($timeout{ai_route_escape}{time})){
2731 sendMessage("c", $config{route_escape_shout});
2732 }
2733 $timeout{ai_route_escape}{time} = time;
2734 AI::queue("escape");
2735 }
2736 }
2737}
2738
2739sub character_name {
2740 my ($self, $args) = @_;
2741 my $name; # Type: String
2742
2743 $name = bytesToString($args->{name});
2744 debug "Character name received: $name\n";
2745}
2746
2747sub character_status {
2748 my ($self, $args) = @_;
2749
2750 my $actor = Actor::get($args->{ID});
2751
2752 if ($args->{switch} eq '028A') {
2753 $actor->{lv} = $args->{lv}; # TODO: test if it is ok to use this piece of information
2754 $actor->{opt3} = $args->{opt3};
2755 } elsif ($args->{switch} eq '0229' || $args->{switch} eq '0119') {
2756 $actor->{opt1} = $args->{opt1};
2757 $actor->{opt2} = $args->{opt2};
2758 }
2759
2760 $actor->{option} = $args->{option};
2761
2762 setStatus($actor, $args->{opt1}, $args->{opt2}, $args->{option});
2763}
2764
2765sub chat_created {
2766 my ($self, $args) = @_;
2767
2768 $currentChatRoom = $accountID;
2769 $chatRooms{$accountID} = {%createdChatRoom};
2770 binAdd(\@chatRoomsID, $accountID);
2771 binAdd(\@currentChatRoomUsers, $char->{name});
2772 message T("Chat Room Created\n");
2773}
2774
2775sub chat_info {
2776 my ($self, $args) = @_;
2777
2778 my $title = bytesToString($args->{title});
2779
2780 my $chat = $chatRooms{$args->{ID}};
2781 if (!$chat || !%{$chat}) {
2782 $chat = $chatRooms{$args->{ID}} = {};
2783 binAdd(\@chatRoomsID, $args->{ID});
2784 }
2785 $chat->{title} = $title;
2786 $chat->{ownerID} = $args->{ownerID};
2787 $chat->{limit} = $args->{limit};
2788 $chat->{public} = $args->{public};
2789 $chat->{num_users} = $args->{num_users};
2790
2791 Plugins::callHook('packet_chatinfo', {
2792 chatID => $args->{ID},
2793 ownerID => $args->{ownerID},
2794 title => $title,
2795 limit => $args->{limit},
2796 public => $args->{public},
2797 num_users => $args->{num_users}
2798 });
2799}
2800
2801sub chat_join_result {
2802 my ($self, $args) = @_;
2803
2804 if ($args->{type} == 1) {
2805 message T("Can't join Chat Room - Incorrect Password\n");
2806 } elsif ($args->{type} == 2) {
2807 message T("Can't join Chat Room - You're banned\n");
2808 }
2809}
2810
2811sub chat_modified {
2812 my ($self, $args) = @_;
2813
2814 my $title = bytesToString($args->{title});
2815
2816 my ($ownerID, $ID, $limit, $public, $num_users) = @{$args}{qw(ownerID ID limit public num_users)};
2817
2818 if ($ownerID eq $accountID) {
2819 $chatRooms{new}{title} = $title;
2820 $chatRooms{new}{ownerID} = $ownerID;
2821 $chatRooms{new}{limit} = $limit;
2822 $chatRooms{new}{public} = $public;
2823 $chatRooms{new}{num_users} = $num_users;
2824 } else {
2825 $chatRooms{$ID}{title} = $title;
2826 $chatRooms{$ID}{ownerID} = $ownerID;
2827 $chatRooms{$ID}{limit} = $limit;
2828 $chatRooms{$ID}{public} = $public;
2829 $chatRooms{$ID}{num_users} = $num_users;
2830 }
2831 message T("Chat Room Properties Modified\n");
2832}
2833
2834sub chat_newowner {
2835 my ($self, $args) = @_;
2836
2837 my $user = bytesToString($args->{user});
2838 if ($args->{type} == 0) {
2839 if ($user eq $char->{name}) {
2840 $chatRooms{$currentChatRoom}{ownerID} = $accountID;
2841 } else {
2842 my $players = $playersList->getItems();
2843 my $player;
2844 foreach my $p (@{$players}) {
2845 if ($p->{name} eq $user) {
2846 $player = $p;
2847 last;
2848 }
2849 }
2850
2851 if ($player) {
2852 my $key = $player->{ID};
2853 $chatRooms{$currentChatRoom}{ownerID} = $key;
2854 }
2855 }
2856 $chatRooms{$currentChatRoom}{users}{$user} = 2;
2857 } else {
2858 $chatRooms{$currentChatRoom}{users}{$user} = 1;
2859 }
2860}
2861
2862sub chat_user_join {
2863 my ($self, $args) = @_;
2864
2865 my $user = bytesToString($args->{user});
2866 if ($currentChatRoom ne "") {
2867 binAdd(\@currentChatRoomUsers, $user);
2868 $chatRooms{$currentChatRoom}{users}{$user} = 1;
2869 $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
2870 message TF("%s has joined the Chat Room\n", $user);
2871 }
2872}
2873
2874sub chat_user_leave {
2875 my ($self, $args) = @_;
2876
2877 my $user = bytesToString($args->{user});
2878 delete $chatRooms{$currentChatRoom}{users}{$user};
2879 binRemove(\@currentChatRoomUsers, $user);
2880 $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
2881 if ($user eq $char->{name}) {
2882 binRemove(\@chatRoomsID, $currentChatRoom);
2883 delete $chatRooms{$currentChatRoom};
2884 undef @currentChatRoomUsers;
2885 $currentChatRoom = "";
2886 message T("You left the Chat Room\n");
2887 } else {
2888 message TF("%s has left the Chat Room\n", $user);
2889 }
2890}
2891
2892sub chat_removed {
2893 my ($self, $args) = @_;
2894
2895 binRemove(\@chatRoomsID, $args->{ID});
2896 delete $chatRooms{ $args->{ID} };
2897}
2898
2899sub deal_add_other {
2900 my ($self, $args) = @_;
2901
2902 if ($args->{nameID} > 0) {
2903 my $item = $currentDeal{other}{ $args->{nameID} } ||= {};
2904 $item->{amount} += $args->{amount};
2905 $item->{nameID} = $args->{nameID};
2906 $item->{identified} = $args->{identified};
2907 $item->{broken} = $args->{broken};
2908 $item->{upgrade} = $args->{upgrade};
2909 $item->{cards} = $args->{cards};
2910 $item->{options} = $args->{options};
2911 $item->{name} = itemName($item);
2912 message TF("%s added Item to Deal: %s x %s\n", $currentDeal{name}, $item->{name}, $args->{amount}), "deal";
2913 } elsif ($args->{amount} > 0) {
2914 $currentDeal{other_zeny} += $args->{amount};
2915 my $amount = formatNumber($args->{amount});
2916 message TF("%s added %s z to Deal\n", $currentDeal{name}, $amount), "deal";
2917 }
2918}
2919
2920sub deal_begin {
2921 my ($self, $args) = @_;
2922
2923 if ($args->{type} == 0) {
2924 error T("That person is too far from you to trade.\n"), "deal";
2925 } elsif ($args->{type} == 2) {
2926 error T("That person is in another deal.\n"), "deal";
2927 } elsif ($args->{type} == 3) {
2928 if (%incomingDeal) {
2929 $currentDeal{name} = $incomingDeal{name};
2930 undef %incomingDeal;
2931 } else {
2932 my $ID = $outgoingDeal{ID};
2933 my $player;
2934 $player = $playersList->getByID($ID) if (defined $ID);
2935 $currentDeal{ID} = $ID;
2936 if ($player) {
2937 $currentDeal{name} = $player->{name};
2938 } else {
2939 $currentDeal{name} = T('Unknown #') . unpack("V", $ID);
2940 }
2941 undef %outgoingDeal;
2942 }
2943 message TF("Engaged Deal with %s\n", $currentDeal{name}), "deal";
2944 } elsif ($args->{type} == 5) {
2945 error T("That person is opening storage.\n"), "deal";
2946 } else {
2947 error TF("Deal request failed (unknown error %s).\n", $args->{type}), "deal";
2948 }
2949}
2950
2951sub deal_cancelled {
2952 undef %incomingDeal;
2953 undef %outgoingDeal;
2954 undef %currentDeal;
2955 message T("Deal Cancelled\n"), "deal";
2956}
2957
2958sub deal_complete {
2959 undef %outgoingDeal;
2960 undef %incomingDeal;
2961 undef %currentDeal;
2962 message T("Deal Complete\n"), "deal";
2963}
2964
2965sub deal_finalize {
2966 my ($self, $args) = @_;
2967 if ($args->{type} == 1) {
2968 $currentDeal{other_finalize} = 1;
2969 message TF("%s finalized the Deal\n", $currentDeal{name}), "deal";
2970
2971 } else {
2972 $currentDeal{you_finalize} = 1;
2973 # FIXME: shouldn't we do this when we actually complete the deal?
2974 $char->{zeny} -= $currentDeal{you_zeny};
2975 message T("You finalized the Deal\n"), "deal";
2976 }
2977}
2978
2979sub deal_request {
2980 my ($self, $args) = @_;
2981 my $level = $args->{level} || 'Unknown'; # TODO: store this info
2982 my $user = bytesToString($args->{user});
2983
2984 $incomingDeal{name} = $user;
2985 $timeout{ai_dealAutoCancel}{time} = time;
2986 message TF("%s (level %s) Requests a Deal\n", $user, $level), "deal";
2987 message T("Type 'deal' to start dealing, or 'deal no' to deny the deal.\n"), "deal";
2988}
2989
2990sub devotion {
2991 my ($self, $args) = @_;
2992 my $msg = '';
2993 my $source = Actor::get($args->{sourceID});
2994
2995 undef $devotionList->{$args->{sourceID}};
2996 for (my $i = 0; $i < 5; $i++) {
2997 my $ID = substr($args->{targetIDs}, $i*4, 4);
2998 last if unpack("V", $ID) == 0;
2999 $devotionList->{$args->{sourceID}}->{targetIDs}->{$ID} = $i;
3000 my $actor = Actor::get($ID);
3001 #FIXME: Need a better display
3002 $msg .= skillUseNoDamage_string($source, $actor, 0, 'devotion');
3003 }
3004 $devotionList->{$args->{sourceID}}->{range} = $args->{range};
3005
3006 message "$msg", "devotion";
3007}
3008
3009sub egg_list {
3010 my ($self, $args) = @_;
3011 my $msg = center(T(" Egg Hatch Candidates "), 38, '-') ."\n";
3012 for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 2) {
3013 my $index = unpack("v", substr($args->{RAW_MSG}, $i, 2));
3014 my $item = $char->inventory->getByServerIndex($index);
3015 $msg .= "$item->{invIndex} $item->{name}\n";
3016 }
3017 $msg .= ('-'x38) . "\n".
3018 T("Ready to use command 'pet [hatch|h] #'\n");
3019 message $msg, "list";
3020}
3021
3022sub emoticon {
3023 my ($self, $args) = @_;
3024 my $emotion = $emotions_lut{$args->{type}}{display} || "<emotion #$args->{type}>";
3025
3026 if ($args->{ID} eq $accountID) {
3027 message "$char->{name}: $emotion\n", "emotion";
3028 chatLog("e", "$char->{name}: $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
3029
3030 } elsif (my $player = $playersList->getByID($args->{ID})) {
3031 my $name = $player->name;
3032
3033 #my $dist = "unknown";
3034 my $dist = distance($char->{pos_to}, $player->{pos_to});
3035 $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
3036
3037 # Translation Comment: "[dist=$dist] $name ($player->{binID}): $emotion\n"
3038 message TF("[dist=%s] %s (%d): %s\n", $dist, $name, $player->{binID}, $emotion), "emotion";
3039 chatLog("e", "$name".": $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
3040
3041 my $index = AI::findAction("follow");
3042 if ($index ne "") {
3043 my $masterID = AI::args($index)->{ID};
3044 if ($config{'followEmotion'} && $masterID eq $args->{ID} &&
3045 distance($char->{pos_to}, $player->{pos_to}) <= $config{'followEmotion_distance'})
3046 {
3047 my %args = ();
3048 $args{timeout} = time + rand (1) + 0.75;
3049
3050 if ($args->{type} == 30) {
3051 $args{emotion} = 31;
3052 } elsif ($args->{type} == 31) {
3053 $args{emotion} = 30;
3054 } else {
3055 $args{emotion} = $args->{type};
3056 }
3057
3058 AI::queue("sendEmotion", \%args);
3059 }
3060 }
3061 } elsif (my $monster = $monstersList->getByID($args->{ID}) || $slavesList->getByID($args->{ID})) {
3062 my $dist = distance($char->{pos_to}, $monster->{pos_to});
3063 $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
3064
3065 # Translation Comment: "[dist=$dist] $monster->name ($monster->{binID}): $emotion\n"
3066 message TF("[dist=%s] %s %s (%d): %s\n", $dist, $monster->{actorType}, $monster->name, $monster->{binID}, $emotion), "emotion";
3067
3068 } else {
3069 my $actor = Actor::get($args->{ID});
3070 my $name = $actor->name;
3071
3072 my $dist = T("unknown");
3073 if (!$actor->isa('Actor::Unknown')) {
3074 $dist = distance($char->{pos_to}, $actor->{pos_to});
3075 $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
3076 }
3077
3078 message TF("[dist=%s] %s: %s\n", $dist, $actor->nameIdx, $emotion), "emotion";
3079 chatLog("e", "$name".": $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
3080 }
3081 Plugins::callHook('packet_emotion', {
3082 emotion => $emotion,
3083 ID => $args->{ID}
3084 });
3085}
3086
3087sub errors {
3088 my ($self, $args) = @_;
3089
3090 Plugins::callHook('disconnected') if ($net->getState() == Network::IN_GAME);
3091 if ($net->getState() == Network::IN_GAME &&
3092 ($config{dcOnDisconnect} > 1 ||
3093 ($config{dcOnDisconnect} &&
3094 $args->{type} != 3 &&
3095 $args->{type} != 10))) {
3096 error T("Auto disconnecting on Disconnect!\n");
3097 chatLog("k", T("*** You disconnected, auto disconnect! ***\n"));
3098 $quit = 1;
3099 }
3100
3101 $net->setState(1);
3102 undef $conState_tries;
3103
3104 $timeout_ex{'master'}{'time'} = time;
3105 $timeout_ex{'master'}{'timeout'} = $timeout{'reconnect'}{'timeout'};
3106 if (($args->{type} != 0)) {
3107 $net->serverDisconnect();
3108 }
3109 if ($args->{type} == 0) {
3110 # FIXME BAN_SERVER_SHUTDOWN is 0x1, 0x0 is BAN_UNFAIR
3111 if ($config{'dcOnServerShutDown'} == 1) {
3112 error T("Auto disconnecting on ServerShutDown!\n");
3113 chatLog("k", T("*** Server shutting down , auto disconnect! ***\n"));
3114 $quit = 1;
3115 } else {
3116 error T("Server shutting down\n"), "connection";
3117 }
3118 } elsif ($args->{type} == 1) {
3119 if($config{'dcOnServerClose'} == 1) {
3120 error T("Auto disconnecting on ServerClose!\n");
3121 chatLog("k", T("*** Server is closed , auto disconnect! ***\n"));
3122 $quit = 1;
3123 } else {
3124 error T("Error: Server is closed\n"), "connection";
3125 }
3126 } elsif ($args->{type} == 2) {
3127 if ($config{'dcOnDualLogin'} == 1) {
3128 error (TF("Critical Error: Dual login prohibited - Someone trying to login!\n\n" .
3129 "%s will now immediately disconnect.\n", $Settings::NAME));
3130 chatLog("k", T("*** DualLogin, auto disconnect! ***\n"));
3131 quit();
3132 } elsif ($config{'dcOnDualLogin'} >= 2) {
3133 error T("Critical Error: Dual login prohibited - Someone trying to login!\n");
3134 message TF("Reconnecting, wait %s seconds...\n", $config{'dcOnDualLogin'}), "connection";
3135 $timeout_ex{'master'}{'timeout'} = $config{'dcOnDualLogin'};
3136 } else {
3137 error T("Critical Error: Dual login prohibited - Someone trying to login!\n"), "connection";
3138 }
3139
3140 } elsif ($args->{type} == 3) {
3141 error T("Error: Out of sync with server\n"), "connection";
3142 } elsif ($args->{type} == 4) {
3143 # fRO: "Your account is not validated, please click on the validation link in your registration mail."
3144 error T("Error: Server is jammed due to over-population.\n"), "connection";
3145 } elsif ($args->{type} == 5) {
3146 error T("Error: You are underaged and cannot join this server.\n"), "connection";
3147 } elsif ($args->{type} == 6) {
3148 $interface->errorDialog(T("Critical Error: You must pay to play this account!\n"));
3149 $quit = 1 unless ($net->version == 1);
3150 } elsif ($args->{type} == 8) {
3151 error T("Error: The server still recognizes your last connection\n"), "connection";
3152 } elsif ($args->{type} == 9) {
3153 error T("Error: IP capacity of this Internet Cafe is full. Would you like to pay the personal base?\n"), "connection";
3154 } elsif ($args->{type} == 10) {
3155 error T("Error: You are out of available time paid for\n"), "connection";
3156 } elsif ($args->{type} == 15) {
3157 error T("Error: You have been forced to disconnect by a GM\n"), "connection";
3158 } elsif ($args->{type} == 101) {
3159 error T("Error: Your account has been suspended until the next maintenance period for possible use of 3rd party programs\n"), "connection";
3160 } elsif ($args->{type} == 102) {
3161 error T("Error: For an hour, more than 10 connections having same IP address, have made. Please check this matter.\n"), "connection";
3162 } else {
3163 error TF("Unknown error %s\n", $args->{type}), "connection";
3164 }
3165}
3166
3167sub friend_logon {
3168 my ($self, $args) = @_;
3169
3170 # Friend In/Out
3171 my $friendAccountID = $args->{friendAccountID};
3172 my $friendCharID = $args->{friendCharID};
3173 my $isNotOnline = $args->{isNotOnline};
3174
3175 for (my $i = 0; $i < @friendsID; $i++) {
3176 if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
3177 $friends{$i}{'online'} = 1 - $isNotOnline;
3178 if ($isNotOnline) {
3179 message TF("Friend %s has disconnected\n", $friends{$i}{name}), undef, 1;
3180 } else {
3181 message TF("Friend %s has connected\n", $friends{$i}{name}), undef, 1;
3182 }
3183 last;
3184 }
3185 }
3186}
3187
3188sub friend_request {
3189 my ($self, $args) = @_;
3190
3191 # Incoming friend request
3192 $incomingFriend{'accountID'} = $args->{accountID};
3193 $incomingFriend{'charID'} = $args->{charID};
3194 $incomingFriend{'name'} = bytesToString($args->{name});
3195 message TF("%s wants to be your friend\n", $incomingFriend{'name'});
3196 message TF("Type 'friend accept' to be friend with %s, otherwise type 'friend reject'\n", $incomingFriend{'name'});
3197}
3198
3199sub friend_removed {
3200 my ($self, $args) = @_;
3201
3202 # Friend removed
3203 my $friendAccountID = $args->{friendAccountID};
3204 my $friendCharID = $args->{friendCharID};
3205 for (my $i = 0; $i < @friendsID; $i++) {
3206 if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
3207 message TF("%s is no longer your friend\n", $friends{$i}{'name'});
3208 binRemove(\@friendsID, $i);
3209 delete $friends{$i};
3210 last;
3211 }
3212 }
3213}
3214
3215sub friend_response {
3216 my ($self, $args) = @_;
3217
3218 # Response to friend request
3219 my $type = $args->{type};
3220 my $name = bytesToString($args->{name});
3221 if ($type) {
3222 message TF("%s rejected to be your friend\n", $name);
3223 } else {
3224 my $ID = @friendsID;
3225 binAdd(\@friendsID, $ID);
3226 $friends{$ID}{accountID} = substr($args->{RAW_MSG}, 4, 4);
3227 $friends{$ID}{charID} = substr($args->{RAW_MSG}, 8, 4);
3228 $friends{$ID}{name} = $name;
3229 $friends{$ID}{online} = 1;
3230 message TF("%s is now your friend\n", $name);
3231 }
3232}
3233
3234sub homunculus_food {
3235 my ($self, $args) = @_;
3236 if ($args->{success}) {
3237 message TF("Fed homunculus with %s\n", itemNameSimple($args->{foodID})), "homunculus";
3238 } else {
3239 error TF("Failed to feed homunculus with %s: no food in inventory.\n", itemNameSimple($args->{foodID})), "homunculus";
3240 # auto-vaporize
3241 if ($char->{homunculus} && $char->{homunculus}{hunger} <= 11 && timeOut($char->{homunculus}{vaporize_time}, 5)) {
3242 $messageSender->sendSkillUse(244, 1, $accountID);
3243 $char->{homunculus}{vaporize_time} = time;
3244 error "Critical hunger level reached. Homunculus is put to rest.\n", "homunculus";
3245 }
3246 }
3247}
3248
3249# TODO: wouldn't it be better if we calculated these only at (first) request after a change in value, if requested at all?
3250sub slave_calcproperty_handler {
3251 my ($slave, $args) = @_;
3252 # so we don't devide by 0
3253 # wtf
3254=pod
3255 $slave->{hp_max} = ($args->{hp_max} > 0) ? $args->{hp_max} : $args->{hp};
3256 $slave->{sp_max} = ($args->{sp_max} > 0) ? $args->{sp_max} : $args->{sp};
3257=cut
3258
3259 $slave->{attack_speed} = int (200 - (($args->{aspd} < 10) ? 10 : ($args->{aspd} / 10)));
3260 $slave->{hpPercent} = $slave->{hp_max} ? ($slave->{hp} / $slave->{hp_max}) * 100 : undef;
3261 $slave->{spPercent} = $slave->{sp_max} ? ($slave->{sp} / $slave->{sp_max}) * 100 : undef;
3262 $slave->{expPercent} = ($args->{exp_max}) ? ($args->{exp} / $args->{exp_max}) * 100 : undef;
3263}
3264
3265sub gameguard_grant {
3266 my ($self, $args) = @_;
3267
3268 if ($args->{server} == 0) {
3269 error T("The server Denied the login because GameGuard packets where not replied " .
3270 "correctly or too many time has been spent to send the response.\n" .
3271 "Please verify the version of your poseidon server and try again\n"), "poseidon";
3272 return;
3273 } elsif ($args->{server} == 1) {
3274 message T("Server granted login request to account server\n"), "poseidon";
3275 } else {
3276 message T("Server granted login request to char/map server\n"), "poseidon";
3277 # FIXME
3278 change_to_constate25() if ($config{'gameGuard'} eq "2");
3279 }
3280 $net->setState(1.3) if ($net->getState() == 1.2);
3281}
3282
3283sub guild_allies_enemy_list {
3284 my ($self, $args) = @_;
3285
3286 # Guild Allies/Enemy List
3287 # <len>.w (<type>.l <guildID>.l <guild name>.24B).*
3288 # type=0 Ally
3289 # type=1 Enemy
3290
3291 # This is the length of the entire packet
3292 my $msg = $args->{RAW_MSG};
3293 my $len = unpack("v", substr($msg, 2, 2));
3294
3295 # clear $guild{enemy} and $guild{ally} otherwise bot will misremember alliances -zdivpsa
3296 $guild{enemy} = {}; $guild{ally} = {};
3297
3298 for (my $i = 4; $i < $len; $i += 32) {
3299 my ($type, $guildID, $guildName) = unpack('V2 Z24', substr($msg, $i, 32));
3300 $guildName = bytesToString($guildName);
3301 if ($type) {
3302 # Enemy guild
3303 $guild{enemy}{$guildID} = $guildName;
3304 } else {
3305 # Allied guild
3306 $guild{ally}{$guildID} = $guildName;
3307 }
3308 debug "Your guild is ".($type ? 'enemy' : 'ally')." with guild $guildID ($guildName)\n", "guild";
3309 }
3310}
3311
3312sub guild_ally_request {
3313 my ($self, $args) = @_;
3314
3315 my $ID = $args->{ID}; # is this a guild ID or account ID? Freya calls it an account ID
3316 my $name = bytesToString($args->{guildName}); # Type: String
3317
3318 message TF("Incoming Request to Ally Guild '%s'\n", $name);
3319 $incomingGuild{ID} = $ID;
3320 $incomingGuild{Type} = 2;
3321 $timeout{ai_guildAutoDeny}{time} = time;
3322}
3323
3324sub guild_broken {
3325 my ($self, $args) = @_;
3326 my $flag = $args->{flag};
3327
3328 if ($flag == 2) {
3329 error T("Guild can not be undone: there are still members in the guild\n");
3330 } elsif ($flag == 1) {
3331 error T("Guild can not be undone: invalid key\n");
3332 } elsif ($flag == 0) {
3333 message T("Guild broken.\n");
3334 undef %{$char->{guild}};
3335 undef $char->{guildID};
3336 undef %guild;
3337 } else {
3338 error TF("Guild can not be undone: unknown reason (flag: %s)\n", $flag);
3339 }
3340}
3341
3342sub guild_create_result {
3343 my ($self, $args) = @_;
3344 my $type = $args->{type};
3345
3346 my %types = (
3347 0 => T("Guild create successful.\n"),
3348 2 => T("Guild create failed: Guild name already exists.\n"),
3349 3 => T("Guild create failed: Emperium is needed.\n")
3350 );
3351 if ($types{$type}) {
3352 message $types{$type};
3353 } else {
3354 message TF("Guild create: Unknown error %s\n", $type);
3355 }
3356}
3357
3358sub guild_info {
3359 my ($self, $args) = @_;
3360 # Guild Info
3361 foreach (qw(ID lv conMember maxMember average exp exp_next tax tendency_left_right tendency_down_up name master castles_string)) {
3362 $guild{$_} = $args->{$_};
3363 }
3364 $guild{name} = bytesToString($args->{name});
3365 $guild{master} = bytesToString($args->{master});
3366 $guild{members}++; # count ourselves in the guild members count
3367}
3368
3369sub guild_invite_result {
3370 my ($self, $args) = @_;
3371
3372 my $type = $args->{type};
3373
3374 my %types = (
3375 0 => T('Target is already in a guild.'),
3376 1 => T('Target has denied.'),
3377 2 => T('Target has accepted.'),
3378 3 => T('Your guild is full.')
3379 );
3380 if ($types{$type}) {
3381 message TF("Guild join request: %s\n", $types{$type});
3382 } else {
3383 message TF("Guild join request: Unknown %s\n", $type);
3384 }
3385}
3386
3387sub guild_location {
3388 # FIXME: not implemented
3389 my ($self, $args) = @_;
3390 unless ($args->{x} > 0 && $args->{y} > 0) {
3391 # delete locator for ID
3392 } else {
3393 # add/replace locator for ID
3394 }
3395}
3396
3397sub guild_leave {
3398 my ($self, $args) = @_;
3399
3400 message TF("%s has left the guild.\n" .
3401 "Reason: %s\n", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
3402}
3403
3404sub guild_expulsion {
3405 my ($self, $args) = @_;
3406
3407 message TF("%s has been removed from the guild.\n" .
3408 "Reason: %s\n", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
3409}
3410
3411sub guild_member_online_status {
3412 my ($self, $args) = @_;
3413
3414 foreach my $guildmember (@{$guild{member}}) {
3415 if ($guildmember->{charID} eq $args->{charID}) {
3416 if ($guildmember->{online} = $args->{online}) {
3417 message TF("Guild member %s logged in.\n", $guildmember->{name}), "guildchat";
3418 } else {
3419 message TF("Guild member %s logged out.\n", $guildmember->{name}), "guildchat";
3420 }
3421 last;
3422 }
3423 }
3424}
3425
3426sub misc_effect {
3427 my ($self, $args) = @_;
3428
3429 my $actor = Actor::get($args->{ID});
3430 message sprintf(
3431 $actor->verb(T("%s use effect: %s\n"), T("%s uses effect: %s\n")),
3432 $actor, defined $effectName{$args->{effect}} ? $effectName{$args->{effect}} : T("Unknown #")."$args->{effect}"
3433 ), 'effect'
3434}
3435
3436sub guild_members_title_list {
3437 my ($self, $args) = @_;
3438
3439 my $msg = $args->{RAW_MSG};
3440 my $msg_size = $args->{RAW_MSG_SIZE};
3441
3442 my $gtIndex;
3443 for (my $i = 4; $i < $msg_size; $i+=28) {
3444 $gtIndex = unpack('V', substr($msg, $i, 4));
3445 $guild{positions}[$gtIndex]{title} = bytesToString(unpack('Z24', substr($msg, $i + 4, 24)));
3446 }
3447}
3448
3449sub guild_name {
3450 my ($self, $args) = @_;
3451
3452 my $guildID = $args->{guildID};
3453 my $emblemID = $args->{emblemID};
3454 my $mode = $args->{mode};
3455 my $guildName = bytesToString($args->{guildName});
3456 $char->{guild}{name} = $guildName;
3457 $char->{guildID} = $guildID;
3458 $char->{guild}{emblem} = $emblemID;
3459
3460 $messageSender->sendGuildMasterMemberCheck();
3461 $messageSender->sendGuildRequestInfo(0); #requests for guild info packet 01B6 and 014C
3462 $messageSender->sendGuildRequestInfo(1); #requests for guild member packet 0166 and 0154
3463 debug "guild name: $guildName\n";
3464}
3465
3466sub guild_request {
3467 my ($self, $args) = @_;
3468
3469 # Guild request
3470 my $ID = $args->{ID};
3471 my $name = bytesToString($args->{name});
3472 message TF("Incoming Request to join Guild '%s'\n", $name);
3473 $incomingGuild{'ID'} = $ID;
3474 $incomingGuild{'Type'} = 1;
3475 $timeout{'ai_guildAutoDeny'}{'time'} = time;
3476}
3477
3478sub identify {
3479 my ($self, $args) = @_;
3480 if ($args->{flag} == 0) {
3481 my $item = $char->inventory->getByServerIndex($args->{index});
3482 $item->{identified} = 1;
3483 $item->{type_equip} = $itemSlots_lut{$item->{nameID}};
3484 message TF("Item Identified: %s (%d)\n", $item->{name}, $item->{invIndex}), "info";
3485 } else {
3486 message T("Item Appraisal has failed.\n");
3487 }
3488 undef @identifyID;
3489}
3490
3491# TODO: store this state
3492sub ignore_all_result {
3493 my ($self, $args) = @_;
3494 if ($args->{type} == 0) {
3495 message T("All Players ignored\n");
3496 } elsif ($args->{type} == 1) {
3497 if ($args->{error} == 0) {
3498 message T("All players unignored\n");
3499 }
3500 }
3501}
3502
3503# TODO: store list of ignored players
3504sub ignore_player_result {
3505 my ($self, $args) = @_;
3506 if ($args->{type} == 0) {
3507 message T("Player ignored\n");
3508 } elsif ($args->{type} == 1) {
3509 if ($args->{error} == 0) {
3510 message T("Player unignored\n");
3511 }
3512 }
3513}
3514
3515sub item_used {
3516 my ($self, $args) = @_;
3517
3518 my ($index, $itemID, $ID, $remaining, $success) =
3519 @{$args}{qw(index itemID ID remaining success)};
3520 my %hook_args = (
3521 serverIndex => $index,
3522 itemID => $itemID,
3523 userID => $ID,
3524 remaining => $remaining,
3525 success => $success
3526 );
3527
3528 if ($ID eq $accountID) {
3529 my $item = $char->inventory->getByServerIndex($index);
3530 if ($item) {
3531 if ($success == 1) {
3532 my $amount = $item->{amount} - $remaining;
3533
3534 message TF("You used Item: %s (%d) x %d - %d left\n", $item->{name}, $item->{invIndex},
3535 $amount, $remaining), "useItem", 1;
3536
3537 inventoryItemRemoved($item->{invIndex}, $amount);
3538
3539 $hook_args{item} = $item;
3540 $hook_args{invIndex} = $item->{invIndex};
3541 $hook_args{name} => $item->{name};
3542 $hook_args{amount} = $amount;
3543
3544 } else {
3545 message TF("You failed to use item: %s (%d)\n", $item ? $item->{name} : "#$itemID", $remaining), "useItem", 1;
3546 }
3547 } else {
3548 if ($success == 1) {
3549 message TF("You used unknown item #%d - %d left\n", $itemID, $remaining), "useItem", 1;
3550 } else {
3551 message TF("You failed to use unknown item #%d - %d left\n", $itemID, $remaining), "useItem", 1;
3552 }
3553 }
3554 } else {
3555 my $actor = Actor::get($ID);
3556 my $itemDisplay = itemNameSimple($itemID);
3557 message TF("%s used Item: %s - %s left\n", $actor, $itemDisplay, $remaining), "useItem", 2;
3558 }
3559 Plugins::callHook('packet_useitem', \%hook_args);
3560}
3561
3562sub married {
3563 my ($self, $args) = @_;
3564
3565 my $actor = Actor::get($args->{ID});
3566 message TF("%s got married!\n", $actor);
3567}
3568
3569sub item_appeared {
3570 my ($self, $args) = @_;
3571 return unless changeToInGameState();
3572
3573 my $item = $itemsList->getByID($args->{ID});
3574 my $mustAdd;
3575 if (!$item) {
3576 $item = new Actor::Item();
3577 $item->{appear_time} = time;
3578 $item->{amount} = $args->{amount};
3579 $item->{nameID} = $args->{nameID};
3580 $item->{identified} = $args->{identified};
3581 $item->{name} = itemName($item);
3582 $item->{ID} = $args->{ID};
3583 $mustAdd = 1;
3584 }
3585 $item->{pos}{x} = $args->{x};
3586 $item->{pos}{y} = $args->{y};
3587 $item->{pos_to}{x} = $args->{x};
3588 $item->{pos_to}{y} = $args->{y};
3589 $itemsList->add($item) if ($mustAdd);
3590
3591 # Take item as fast as possible
3592 if ($AI == AI::AUTO && pickupitems(lc($item->{name})) == 2
3593 && ($config{'itemsTakeAuto'} || $config{'itemsGatherAuto'})
3594 && (percent_weight($char) < $config{'itemsMaxWeight'})
3595 && distance($item->{pos}, $char->{pos_to}) <= 5) {
3596 $messageSender->sendTake($args->{ID});
3597 }
3598
3599 message TF("Item Appeared: %s (%d) x %d (%d, %d)\n", $item->{name}, $item->{binID}, $item->{amount}, $args->{x}, $args->{y}), "drop", 1;
3600
3601}
3602
3603sub item_exists {
3604 my ($self, $args) = @_;
3605 return unless changeToInGameState();
3606
3607 my $item = $itemsList->getByID($args->{ID});
3608 my $mustAdd;
3609 if (!$item) {
3610 $item = new Actor::Item();
3611 $item->{appear_time} = time;
3612 $item->{amount} = $args->{amount};
3613 $item->{nameID} = $args->{nameID};
3614 $item->{ID} = $args->{ID};
3615 $item->{identified} = $args->{identified};
3616 $item->{name} = itemName($item);
3617 $mustAdd = 1;
3618 }
3619 $item->{pos}{x} = $args->{x};
3620 $item->{pos}{y} = $args->{y};
3621 $item->{pos_to}{x} = $args->{x};
3622 $item->{pos_to}{y} = $args->{y};
3623 $itemsList->add($item) if ($mustAdd);
3624
3625 message TF("Item Exists: %s (%d) x %d\n", $item->{name}, $item->{binID}, $item->{amount}), "drop", 1;
3626}
3627
3628sub item_disappeared {
3629 my ($self, $args) = @_;
3630 return unless changeToInGameState();
3631
3632 my $item = $itemsList->getByID($args->{ID});
3633 if ($item) {
3634 if ($config{attackLooters} && AI::action ne "sitAuto" && pickupitems(lc($item->{name})) > 0) {
3635 foreach my Actor::Monster $monster (@{$monstersList->getItems()}) { # attack looter code
3636 if (my $control = mon_control($monster->name,$monster->{nameID})) {
3637 next if ( ($control->{attack_auto} ne "" && $control->{attack_auto} == -1)
3638 || ($control->{attack_lvl} ne "" && $control->{attack_lvl} > $char->{lv})
3639 || ($control->{attack_jlvl} ne "" && $control->{attack_jlvl} > $char->{lv_job})
3640 || ($control->{attack_hp} ne "" && $control->{attack_hp} > $char->{hp})
3641 || ($control->{attack_sp} ne "" && $control->{attack_sp} > $char->{sp})
3642 );
3643 }
3644 if (distance($item->{pos}, $monster->{pos}) == 0) {
3645 attack($monster->{ID});
3646 message TF("Attack Looter: %s looted %s\n", $monster->nameIdx, $item->{name}), "looter";
3647 last;
3648 }
3649 }
3650 }
3651
3652 debug "Item Disappeared: $item->{name} ($item->{binID})\n", "parseMsg_presence";
3653 my $ID = $args->{ID};
3654 $items_old{$ID} = $item->deepCopy();
3655 $items_old{$ID}{disappeared} = 1;
3656 $items_old{$ID}{gone_time} = time;
3657 $itemsList->removeByID($ID);
3658 }
3659}
3660
3661sub item_upgrade {
3662 my ($self, $args) = @_;
3663 my ($type, $index, $upgrade) = @{$args}{qw(type index upgrade)};
3664
3665 my $item = $char->inventory->getByServerIndex($index);
3666 if ($item) {
3667 $item->{upgrade} = $upgrade;
3668 message TF("Item %s has been upgraded to +%s\n", $item->{name}, $upgrade), "parseMsg/upgrade";
3669 $item->setName(itemName($item));
3670 }
3671}
3672
3673sub job_equipment_hair_change {
3674 my ($self, $args) = @_;
3675 return unless changeToInGameState();
3676
3677 my $actor = Actor::get($args->{ID});
3678 assert(UNIVERSAL::isa($actor, "Actor")) if DEBUG;
3679
3680 if ($args->{part} == 0) {
3681 # Job change
3682 $actor->{jobID} = $args->{number};
3683 message TF("%s changed job to: %s\n", $actor, $jobs_lut{$args->{number}}), "parseMsg/job", ($actor->isa('Actor::You') ? 0 : 2);
3684
3685 } elsif ($args->{part} == 3) {
3686 # Bottom headgear change
3687 message TF("%s changed bottom headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
3688 $actor->{headgear}{low} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
3689
3690 } elsif ($args->{part} == 4) {
3691 # Top headgear change
3692 message TF("%s changed top headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
3693 $actor->{headgear}{top} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
3694
3695 } elsif ($args->{part} == 5) {
3696 # Middle headgear change
3697 message TF("%s changed middle headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
3698 $actor->{headgear}{mid} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
3699
3700 } elsif ($args->{part} == 6) {
3701 # Hair color change
3702 $actor->{hair_color} = $args->{number};
3703 message TF("%s changed hair color to: %s (%s)\n", $actor, $haircolors{$args->{number}}, $args->{number}), "parseMsg/hairColor", ($actor->isa('Actor::You') ? 0 : 2);
3704 }
3705
3706 #my %parts = (
3707 # 0 => 'Body',
3708 # 2 => 'Right Hand',
3709 # 3 => 'Low Head',
3710 # 4 => 'Top Head',
3711 # 5 => 'Middle Head',
3712 # 8 => 'Left Hand'
3713 #);
3714 #if ($part == 3) {
3715 # $part = 'low';
3716 #} elsif ($part == 4) {
3717 # $part = 'top';
3718 #} elsif ($part == 5) {
3719 # $part = 'mid';
3720 #}
3721 #
3722 #my $name = getActorName($ID);
3723 #if ($part == 3 || $part == 4 || $part == 5) {
3724 # my $actor = Actor::get($ID);
3725 # $actor->{headgear}{$part} = $items_lut{$number} if ($actor);
3726 # my $itemName = $items_lut{$itemID};
3727 # $itemName = 'nothing' if (!$itemName);
3728 # debug "$name changes $parts{$part} ($part) equipment to $itemName\n", "parseMsg";
3729 #} else {
3730 # debug "$name changes $parts{$part} ($part) equipment to item #$number\n", "parseMsg";
3731 #}
3732
3733}
3734
3735# Leap, Snap, Back Slide... Various knockback
3736sub high_jump {
3737 my ($self, $args) = @_;
3738 return unless changeToInGameState();
3739
3740 my $actor = Actor::get ($args->{ID});
3741 if (!defined $actor) {
3742 $actor = new Actor::Unknown;
3743 $actor->{appear_time} = time;
3744 $actor->{nameID} = unpack ('V', $args->{ID});
3745 } elsif ($actor->{pos_to}{x} == $args->{x} && $actor->{pos_to}{y} == $args->{y}) {
3746 message TF("%s failed to instantly move\n", $actor->nameString), 'skill';
3747 return;
3748 }
3749
3750 $actor->{pos} = {x => $args->{x}, y => $args->{y}};
3751 $actor->{pos_to} = {x => $args->{x}, y => $args->{y}};
3752
3753 message TF("%s instantly moved to %d, %d\n", $actor->nameString, $actor->{pos_to}{x}, $actor->{pos_to}{y}), 'skill', 2;
3754
3755 $actor->{time_move} = time;
3756 $actor->{time_move_calc} = 0;
3757}
3758
3759sub hp_sp_changed {
3760 my ($self, $args) = @_;
3761 return unless changeToInGameState();
3762
3763 my $type = $args->{type};
3764 my $amount = $args->{amount};
3765 if ($type == 5) {
3766 $char->{hp} += $amount;
3767 $char->{hp} = $char->{hp_max} if ($char->{hp} > $char->{hp_max});
3768 } elsif ($type == 7) {
3769 $char->{sp} += $amount;
3770 $char->{sp} = $char->{sp_max} if ($char->{sp} > $char->{sp_max});
3771 }
3772}
3773
3774# The difference between map_change and map_changed is that map_change
3775# represents a map change event on the current map server, while
3776# map_changed means that you've changed to a different map server.
3777# map_change also represents teleport events.
3778sub map_change {
3779 my ($self, $args) = @_;
3780 return unless changeToInGameState();
3781
3782 my $oldMap = $field ? $field->baseName : undef; # Get old Map name without InstanceID
3783 my ($map) = $args->{map} =~ /([\s\S]*)\./;
3784 my $map_noinstance;
3785 ($map_noinstance, undef) = Field::nameToBaseName(undef, $map); # Hack to clean up InstanceID
3786
3787 checkAllowedMap($map_noinstance);
3788 if (!$field || $map ne $field->name()) {
3789 eval {
3790 $field = new Field(name => $map);
3791 };
3792 if (my $e = caught('FileNotFoundException', 'IOException')) {
3793 error TF("Cannot load field %s: %s\n", $map_noinstance, $e);
3794 undef $field;
3795 } elsif ($@) {
3796 die $@;
3797 }
3798 }
3799
3800 if ($ai_v{temp}{clear_aiQueue}) {
3801 AI::clear;
3802 AI::SlaveManager::clear();
3803 }
3804
3805 main::initMapChangeVars();
3806 for (my $i = 0; $i < @ai_seq; $i++) {
3807 ai_setMapChanged($i);
3808 }
3809 AI::SlaveManager::setMapChanged ();
3810 if ($net->version == 0) {
3811 $ai_v{portalTrace_mapChanged} = time;
3812 }
3813
3814 my %coords = (
3815 x => $args->{x},
3816 y => $args->{y}
3817 );
3818 $char->{pos} = {%coords};
3819 $char->{pos_to} = {%coords};
3820 message TF("Map Change: %s (%s, %s)\n", $args->{map}, $char->{pos}{x}, $char->{pos}{y}), "connection";
3821 if ($net->version == 1) {
3822 ai_clientSuspend(0, 10);
3823 } else {
3824 $messageSender->sendMapLoaded();
3825 # $messageSender->sendSync(1);
3826 $timeout{ai}{time} = time;
3827 }
3828
3829 Plugins::callHook('Network::Receive::map_changed', {
3830 oldMap => $oldMap,
3831 });
3832
3833 $timeout{ai}{time} = time;
3834}
3835
3836# Parse 0A3B with structure
3837# '0A3B' => ['hat_effect', 'v a4 C a*', [qw(len ID flag effect)]],
3838# Unpack effect info into HatEFID
3839# @author [Cydh]
3840sub parse_hat_effect {
3841 my ($self, $args) = @_;
3842 @{$args->{effects}} = map {{ HatEFID => unpack('v', $_) }} unpack '(a2)*', $args->{effect};
3843 debug "Hat Effect. Flag: ".$args->{flag}." HatEFIDs: ".(join ', ', map {$_->{HatEFID}} @{$args->{effects}})."\n";
3844}
3845
3846# Display information for player's Hat Effects
3847# @author [Cydh]
3848sub hat_effect {
3849 my ($self, $args) = @_;
3850
3851 my $actor = Actor::get($args->{ID});
3852 my $hatName;
3853 my $i = 0;
3854
3855 #TODO: Stores the hat effect into actor for single player's information
3856 for my $hat (@{$args->{effects}}) {
3857 my $hatHandle;
3858 $hatName .= ", " if ($i);
3859 if (defined $hatEffectHandle{$hat->{HatEFID}}) {
3860 $hatHandle = $hatEffectHandle{$hat->{HatEFID}};
3861 $hatName .= defined $hatEffectName{$hatHandle} ? $hatEffectName{$hatHandle} : $hatHandle;
3862 } else {
3863 $hatName .= T("Unknown #").$hat->{HatEFID};
3864 }
3865 $i++;
3866 }
3867
3868 if ($args->{flag} == 1) {
3869 message sprintf(
3870 $actor->verb(T("%s use effect: %s\n"), T("%s uses effect: %s\n")),
3871 $actor, $hatName
3872 ), 'effect';
3873 } else {
3874 message sprintf(
3875 $actor->verb(T("%s are no longer: %s\n"), T("%s is no longer: %s\n")),
3876 $actor, $hatName
3877 ), 'effect';
3878 }
3879}
3880
38811;