Advertisement
Guest User

Receive.pm

a guest
May 5th, 2017
352
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 130.64 KB | None | 0 0
  1. #########################################################################
  2. # OpenKore - Server message parsing
  3. #
  4. # This software is open source, licensed under the GNU General Public
  5. # License, version 2.
  6. # Basically, this means that you're allowed to modify and distribute
  7. # this software. However, if you distribute modified versions, you MUST
  8. # also distribute the source code.
  9. # See http://www.gnu.org/licenses/gpl.html for the full license.
  10. #########################################################################
  11. ##
  12. # MODULE DESCRIPTION: Server message parsing
  13. #
  14. # This class is responsible for parsing messages that are sent by the RO
  15. # server to Kore. Information in the messages are stored in global variables
  16. # (in the module Globals).
  17. #
  18. # Please also read <a href="http://wiki.openkore.com/index.php/Network_subsystem">the
  19. # network subsystem overview.</a>
  20. package Network::Receive;
  21.  
  22. use strict;
  23. use Network::PacketParser; # import
  24. use base qw(Network::PacketParser);
  25. use utf8;
  26. use Carp::Assert;
  27. use Scalar::Util;
  28. use Socket qw(inet_aton inet_ntoa);
  29.  
  30. use AI;
  31. use Globals;
  32. #use Settings;
  33. use Log qw(message warning error debug);
  34. use FileParsers qw(updateMonsterLUT updateNPCLUT);
  35. use I18N qw(bytesToString stringToBytes);
  36. use Interface;
  37. use Network;
  38. use Network::MessageTokenizer;
  39. use Misc;
  40. use Plugins;
  41. use Utils;
  42. use Utils::Exceptions;
  43. use Utils::Crypton;
  44. use Translation;
  45.  
  46. # object_type constants for &actor_display
  47. use constant {
  48. PC_TYPE => 0x0,
  49. NPC_TYPE => 0x1,
  50. ITEM_TYPE => 0x2,
  51. SKILL_TYPE => 0x3,
  52. UNKNOWN_TYPE => 0x4,
  53. NPC_MOB_TYPE => 0x5,
  54. NPC_EVT_TYPE => 0x6,
  55. NPC_PET_TYPE => 0x7,
  56. NPC_HO_TYPE => 0x8,
  57. NPC_MERSOL_TYPE => 0x9,
  58. NPC_ELEMENTAL_TYPE => 0xa
  59. };
  60.  
  61. use 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.
  135. sub 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.
  163. sub 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.
  187. sub 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.  
  198. sub 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.
  225. sub 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.
  234. sub 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.  
  260. sub 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.  
  282. sub 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.  
  296. sub 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.  
  367. sub 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;
  377. sub 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...
  386. sub 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
  672. typedef 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.  
  860. sub 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.  
  1009. sub 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.  
  1126. sub 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.  
  1207. use 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.  
  1214. sub 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.  
  1228. sub 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
  1248. sub reconstruct_minimap_indicator {
  1249. }
  1250.  
  1251. use 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?
  1261. sub 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.
  1294. sub 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.  
  1322. sub 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.  
  1335. sub 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.  
  1345. sub 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.
  1359. sub 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.  
  1393. sub 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.  
  1435. sub 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.  
  1444. sub 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
  1454. sub 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
  1469. sub 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.  
  1505. sub 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
  1547. sub 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
  1571. sub 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
  1621. sub 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
  1638. sub 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
  1655. sub 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
  1688. sub cart_off {
  1689. $char->cart->close;
  1690. message T("Cart released.\n"), "success";
  1691. }
  1692.  
  1693. # 012D
  1694. sub 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)
  1703. sub 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
  1746. sub 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
  1761. sub 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".
  1773. sub marriage_partner_name {
  1774. my ($self, $args) = @_;
  1775.  
  1776. message TF("Marriage partner name: %s\n", $args->{name});
  1777. }
  1778.  
  1779. sub 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.  
  1857. sub 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.  
  1915. sub 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.  
  1937. sub 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
  1959. sub 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
  1987. sub 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
  2030. sub 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.  
  2072. sub 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.  
  2100. sub 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.  
  2139. sub 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.  
  2149. sub 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.  
  2173. sub 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.  
  2207. sub 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.  
  2221. sub progress_bar_stop {
  2222. my($self, $args) = @_;
  2223. message TF("Progress bar finished.\n", 'info');
  2224. }
  2225.  
  2226. # 02B1
  2227. sub 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
  2239. sub 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
  2261. sub 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
  2285. sub 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.  
  2292. sub 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.  
  2299. sub 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.  
  2304. sub 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.  
  2311. sub 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
  2317. sub 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
  2328. sub 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
  2341. sub parse_npc_chat {
  2342. my ($self, $args) = @_;
  2343.  
  2344. $args->{actor} = Actor::get($args->{ID});
  2345. }
  2346.  
  2347. sub 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.  
  2385. sub 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.  
  2400. sub storage_opened {
  2401. my ($self, $args) = @_;
  2402. $char->storage->open($args);
  2403. }
  2404.  
  2405. sub 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.  
  2419. sub 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.  
  2439. sub 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.  
  2454. sub 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.  
  2485. sub 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.  
  2497. sub 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.  
  2510. sub 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.  
  2523. sub 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.  
  2553. sub 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.  
  2565. sub cart_info {
  2566. my ($self, $args) = @_;
  2567. $char->cart->info($args);
  2568. debug "[cart_info] received.\n", "parseMsg";
  2569. }
  2570.  
  2571. sub 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.  
  2585. sub 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.  
  2607. sub 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.  
  2667. sub 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.  
  2676. sub 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.  
  2696. sub 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.  
  2707. sub 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.  
  2739. sub 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.  
  2747. sub 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.  
  2765. sub 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.  
  2775. sub 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.  
  2801. sub 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.  
  2811. sub 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.  
  2834. sub 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.  
  2862. sub 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.  
  2874. sub 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.  
  2892. sub chat_removed {
  2893. my ($self, $args) = @_;
  2894.  
  2895. binRemove(\@chatRoomsID, $args->{ID});
  2896. delete $chatRooms{ $args->{ID} };
  2897. }
  2898.  
  2899. sub 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.  
  2920. sub 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.  
  2951. sub deal_cancelled {
  2952. undef %incomingDeal;
  2953. undef %outgoingDeal;
  2954. undef %currentDeal;
  2955. message T("Deal Cancelled\n"), "deal";
  2956. }
  2957.  
  2958. sub deal_complete {
  2959. undef %outgoingDeal;
  2960. undef %incomingDeal;
  2961. undef %currentDeal;
  2962. message T("Deal Complete\n"), "deal";
  2963. }
  2964.  
  2965. sub 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.  
  2979. sub 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.  
  2990. sub 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.  
  3009. sub 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.  
  3022. sub 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.  
  3087. sub 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.  
  3167. sub 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.  
  3188. sub 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.  
  3199. sub 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.  
  3215. sub 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.  
  3234. sub 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?
  3250. sub 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.  
  3265. sub 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.  
  3283. sub 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.  
  3312. sub 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.  
  3324. sub 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.  
  3342. sub 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.  
  3358. sub 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.  
  3369. sub 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.  
  3387. sub 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.  
  3397. sub 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.  
  3404. sub 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.  
  3411. sub 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.  
  3426. sub 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.  
  3436. sub 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.  
  3449. sub 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.  
  3466. sub 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.  
  3478. sub 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
  3492. sub 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
  3504. sub 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.  
  3515. sub 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.  
  3562. sub married {
  3563. my ($self, $args) = @_;
  3564.  
  3565. my $actor = Actor::get($args->{ID});
  3566. message TF("%s got married!\n", $actor);
  3567. }
  3568.  
  3569. sub 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.  
  3603. sub 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.  
  3628. sub 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.  
  3661. sub 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.  
  3673. sub 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
  3736. sub 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.  
  3759. sub 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.
  3778. sub 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]
  3840. sub 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]
  3848. sub 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.  
  3881. 1;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement