Receive.pm
上传用户:market2
上传日期:2018-11-18
资源大小:18786k
文件大小:226k
源码类别:

外挂编程

开发平台:

Windows_Unix

  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://www.openkore.com/wiki/index.php/Network_subsystem">the
  19. # network subsystem overview.</a>
  20. package Network::Receive;
  21. use strict;
  22. use Time::HiRes qw(time usleep);
  23. use encoding 'utf8';
  24. use Carp::Assert;
  25. use Scalar::Util;
  26. use Exception::Class ('Network::Receive::InvalidServerType', 'Network::Receive::CreationError');
  27. use Globals;
  28. use Actor;
  29. use Actor::You;
  30. use Actor::Player;
  31. use Actor::Monster;
  32. use Actor::Party;
  33. use Actor::Item;
  34. use Actor::Unknown;
  35. use Settings;
  36. use Log qw(message warning error debug);
  37. use FileParsers;
  38. use Interface;
  39. use Network;
  40. use Network::MessageTokenizer;
  41. use Network::Send ();
  42. use Misc;
  43. use Plugins;
  44. use Utils;
  45. use Skill;
  46. use AI;
  47. use Utils::Exceptions;
  48. use Utils::Crypton;
  49. use Translation;
  50. use I18N qw(bytesToString);
  51. ######################################
  52. ### Public methods
  53. ######################################
  54. # Do not call this directly. Use create() instead.
  55. sub new {
  56. my ($class) = @_;
  57. my %self;
  58. # If you are wondering about those funny strings like 'x2 v1' read http://perldoc.perl.org/functions/pack.html
  59. # and http://perldoc.perl.org/perlpacktut.html
  60. # Defines a list of Packet Handlers and decoding information
  61. # 'packetSwitch' => ['handler function','unpack string',[qw(argument names)]]
  62. $self{packet_list} = {
  63. '0069' => ['account_server_info', 'x2 a4 a4 a4 x30 C1 a*', [qw(sessionID accountID sessionID2 accountSex serverInfo)]],
  64. '006A' => ['login_error', 'C1', [qw(type)]],
  65. '006B' => ['received_characters'],
  66. '0072' => ['received_characters'],
  67. '006C' => ['login_error_game_login_server'],
  68. '006D' => ['character_creation_successful', 'a4 x4 V1 x62 Z24 C1 C1 C1 C1 C1 C1 C1', [qw(ID zenny name str agi vit int dex luk slot)]],
  69. '006E' => ['character_creation_failed'],
  70. '006F' => ['character_deletion_successful'],
  71. '0070' => ['character_deletion_failed'],
  72. '0071' => ['received_character_ID_and_Map', 'a4 Z16 a4 v1', [qw(charID mapName mapIP mapPort)]],
  73. '0073' => ['map_loaded', 'V a3', [qw(syncMapSync coords)]],
  74. '0075' => ['changeToInGameState'],
  75. '0077' => ['changeToInGameState'],
  76. '0078' => ['actor_display', 'a4 v14 a4 x7 C1 a3 x2 C1 v1', [qw(ID walk_speed param1 param2 param3 type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID sex coords act lv)]],
  77. '0079' => ['actor_display', 'a4 v14 a4 x7 C1 a3 x2 v1',    [qw(ID walk_speed param1 param2 param3 type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID sex coords lv)]],
  78. '007A' => ['changeToInGameState'],
  79. '007B' => ['actor_display', 'a4 v8 x4 v6 a4 x7 C1 a5 x3 v1',     [qw(ID walk_speed param1 param2 param3 type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID sex coords lv)]],
  80. #OLD '007C' => ['actor_display', 'a4 v1 v1 v1 v1 x6 v1 C1 x12 C1 a3', [qw(ID walk_speed param1 param2 param3 type pet sex coords)]],
  81. '007C' => ($rpackets{'007C'} == 41 # or 42
  82. ? ['actor_display', 'x1 a4 v13 C2 a3 C1', [qw(ID walk_speed param1 param2 param3 hair_style weapon lowhead type shield tophead midhead hair_color clothes_color head_dir karma sex coords unknown1)]]
  83. : ['actor_display', 'x1 a4 v13 C2 a3 C2', [qw(ID walk_speed param1 param2 param3 hair_style weapon lowhead type shield tophead midhead hair_color clothes_color head_dir karma sex coords unknown1 unknown2)]]
  84. ),
  85. '007F' => ['received_sync', 'V1', [qw(time)]],
  86. '0080' => ['actor_died_or_disappeared', 'a4 C1', [qw(ID type)]],
  87. '0081' => ['errors', 'C1', [qw(type)]],
  88. '0086' => ['actor_display', 'a4 a5', [qw(ID coords)]],
  89. '0087' => ['character_moves', 'x4 a5 C1', [qw(coords unknown)]],
  90. '0088' => ['actor_movement_interrupted', 'a4 v1 v1', [qw(ID x y)]],
  91. '008A' => ['actor_action', 'a4 a4 a4 V2 v1 v1 C1 v1', [qw(sourceID targetID tick src_speed dst_speed damage param2 type param3)]],
  92. '008D' => ['public_chat', 'v1 a4 Z*', [qw(len ID message)]],
  93. '008E' => ['self_chat', 'x2 Z*', [qw(message)]],
  94. '0091' => ['map_change', 'Z16 v1 v1', [qw(map x y)]],
  95. '0092' => ['map_changed', 'Z16 x4 a4 v1', [qw(map IP port)]],
  96. '0095' => ['actor_info', 'a4 Z24', [qw(ID name)]],
  97. '0097' => ['private_message', 'v1 Z24 Z*', [qw(len privMsgUser privMsg)]],
  98. '0098' => ['private_message_sent', 'C1', [qw(type)]],
  99. '009A' => ['system_chat', 'x2 Z*', [qw(message)]], #maybe use a* instead and $message =~ /00$//; if there are problems
  100. '009C' => ['actor_look_at', 'a4 C1 x1 C1', [qw(ID head body)]],
  101. '009D' => ['item_exists', 'a4 v1 x1 v3', [qw(ID type x y amount)]],
  102. '009E' => ['item_appeared', 'a4 v1 x1 v1 v1 x2 v1', [qw(ID type x y amount)]],
  103. '00A0' => ['inventory_item_added', 'v1 v1 v1 C1 C1 C1 a8 v1 C1 C1', [qw(index amount nameID identified broken upgrade cards type_equip type fail)]],
  104. '00A1' => ['item_disappeared', 'a4', [qw(ID)]],
  105. '00A3' => ['inventory_items_stackable'],
  106. '00A4' => ['inventory_items_nonstackable'],
  107. '00A5' => ['storage_items_stackable'],
  108. '00A6' => ['storage_items_nonstackable'],
  109. '00A8' => ['use_item', 'v1 x2 C1', [qw(index amount)]],
  110. '00AA' => ['equip_item', 'v1 v1 C1', [qw(index type success)]],
  111. '00AC' => ['unequip_item', 'v1 v1', [qw(index type)]],
  112. '00AF' => ['inventory_item_removed', 'v1 v1', [qw(index amount)]],
  113. '00B0' => ['stat_info', 'v1 V1', [qw(type val)]],
  114. '00B1' => ['exp_zeny_info', 'v1 V1', [qw(type val)]],
  115. '00B3' => ['switch_character'],
  116. '00B4' => ['npc_talk'],
  117. '00B5' => ['npc_talk_continue'],
  118. '00B6' => ['npc_talk_close', 'a4', [qw(ID)]],
  119. '00B7' => ['npc_talk_responses'],
  120. '00BC' => ['stats_added', 'v1 x1 C1', [qw(type val)]],
  121. '00BD' => ['stats_info', 'v1 C1 C1 C1 C1 C1 C1 C1 C1 C1 C1 C1 C1 v1 v1 v1 v1 v1 v1 v1 v1 v1 v1 v1 v1', [qw(points_free str points_str agi points_agi vit points_vit int points_int dex points_dex luk points_luk attack attack_bonus attack_magic_min attack_magic_max def def_bonus def_magic def_magic_bonus hit flee flee_bonus critical)]],
  122. '00BE' => ['stats_points_needed', 'v1 C1', [qw(type val)]],
  123. '00C0' => ['emoticon', 'a4 C1', [qw(ID type)]],
  124. '00CA' => ['buy_result', 'C1', [qw(fail)]],
  125. '00C2' => ['users_online', 'V1', [qw(users)]],
  126. '00C3' => ['job_equipment_hair_change', 'a4 C1 C1', [qw(ID part number)]],
  127. '00C4' => ['npc_store_begin', 'a4', [qw(ID)]],
  128. '00C6' => ['npc_store_info'],
  129. '00C7' => ['npc_sell_list'],
  130. '00D1' => ['ignore_player_result', 'C1 C1', [qw(type error)]],
  131. '00D2' => ['ignore_all_result', 'C1 C1', [qw(type error)]],
  132. '00D6' => ['chat_created'],
  133. '00D7' => ['chat_info', 'x2 a4 a4 v1 v1 C1 a*', [qw(ownerID ID limit num_users public title)]],
  134. '00DA' => ['chat_join_result', 'C1', [qw(type)]],
  135. '00D8' => ['chat_removed', 'a4', [qw(ID)]],
  136. '00DB' => ['chat_users'],
  137. '00DC' => ['chat_user_join', 'v1 Z24', [qw(num_users user)]],
  138. '00DD' => ['chat_user_leave', 'v1 Z24', [qw(num_users user)]],
  139. '00DF' => ['chat_modified', 'x2 a4 a4 v1 v1 C1 a*', [qw(ownerID ID limit num_users public title)]],
  140. '00E1' => ['chat_newowner', 'C1 x3 Z24', [qw(type user)]],
  141. '00E5' => ['deal_request', 'Z24', [qw(user)]],
  142. '00E7' => ['deal_begin', 'C1', [qw(type)]],
  143. '00E9' => ['deal_add_other', 'V1 v1 C1 C1 C1 a8', [qw(amount nameID identified broken upgrade cards)]],
  144. '00EA' => ['deal_add_you', 'v1 C1', [qw(index fail)]],
  145. '00EC' => ['deal_finalize', 'C1', [qw(type)]],
  146. '00EE' => ['deal_cancelled'],
  147. '00F0' => ['deal_complete'],
  148. '00F2' => ['storage_opened', 'v1 v1', [qw(items items_max)]],
  149. '00F4' => ['storage_item_added', 'v1 V1 v1 C1 C1 C1 a8', [qw(index amount ID identified broken upgrade cards)]],
  150. '00F6' => ['storage_item_removed', 'v1 V1', [qw(index amount)]],
  151. '00F8' => ['storage_closed'],
  152. '00FA' => ['party_organize_result', 'C1', [qw(fail)]],
  153. '00FB' => ['party_users_info', 'x2 Z24', [qw(party_name)]],
  154. '00FD' => ['party_invite_result', 'Z24 C1', [qw(name type)]],
  155. '00FE' => ['party_invite', 'a4 Z24', [qw(ID name)]],
  156. '0101' => ['party_exp', 'C1', [qw(type)]],
  157. '0104' => ['party_join', 'a4 x4 v1 v1 C1 Z24 Z24 Z16', [qw(ID x y type name user map)]],
  158. '0105' => ['party_leave', 'a4 Z24', [qw(ID name)]],
  159. '0106' => ['party_hp_info', 'a4 v1 v1', [qw(ID hp hp_max)]],
  160. '0107' => ['party_location', 'a4 v1 v1', [qw(ID x y)]],
  161. '0108' => ['item_upgrade', 'v1 v1 v1', [qw(type index upgrade)]],
  162. '0109' => ['party_chat', 'x2 a4 Z*', [qw(ID message)]],
  163. '0110' => ['skill_use_failed', 'v1 v1 v1 C1 C1', [qw(skillID btype unknown fail type)]],
  164. '010A' => ['mvp_item', 'v1', [qw(itemID)]],
  165. '010B' => ['mvp_you', 'V1', [qw(expAmount)]],
  166. '010C' => ['mvp_other', 'a4', [qw(ID)]],
  167. '010E' => ['skill_update', 'v1 v1 v1 v1 C1', [qw(skillID lv sp range up)]], # range = skill range, up = this skill can be leveled up further
  168. '010F' => ['skills_list'],
  169. '0111' => ['linker_skill', 'v2 x2 v3 Z24', [qw(skillID target lv sp range name)]],
  170. '0114' => ['skill_use', 'v1 a4 a4 V1 V1 V1 v1 v1 v1 C1', [qw(skillID sourceID targetID tick src_speed dst_speed damage level param3 type)]],
  171. '0117' => ['skill_use_location', 'v1 a4 v1 v1 v1', [qw(skillID sourceID lv x y)]],
  172. '0119' => ['character_status', 'a4 v3 x', [qw(ID param1 param2 param3)]],
  173. '011A' => ['skill_used_no_damage', 'v1 v1 a4 a4 C1', [qw(skillID amount targetID sourceID fail)]],
  174. '011C' => ['warp_portal_list', 'v1 Z16 Z16 Z16 Z16', [qw(type memo1 memo2 memo3 memo4)]],
  175. '011E' => ['memo_success', 'C1', [qw(fail)]],
  176. '011F' => ['area_spell', 'a4 a4 v2 C2', [qw(ID sourceID x y type fail)]],
  177. '0120' => ['area_spell_disappears', 'a4', [qw(ID)]],
  178. '0121' => ['cart_info', 'v1 v1 V1 V1', [qw(items items_max weight weight_max)]],
  179. '0122' => ['cart_equip_list'],
  180. '0123' => ['cart_items_list'],
  181. '0124' => ['cart_item_added', 'v1 V1 v1 x C1 C1 C1 a8', [qw(index amount ID identified broken upgrade cards)]],
  182. '0125' => ['cart_item_removed', 'v1 V1', [qw(index amount)]],
  183. '012C' => ['cart_add_failed', 'C1', [qw(fail)]],
  184. '012D' => ['shop_skill', 'v1', [qw(number)]],
  185. '0131' => ['vender_found', 'a4 A30', [qw(ID title)]],
  186. '0132' => ['vender_lost', 'a4', [qw(ID)]],
  187. '0133' => ['vender_items_list'],
  188. '0135' => ['vender_buy_fail', 'v1 v1 C1', [qw(index amount fail)]],
  189. '0136' => ['vending_start'],
  190. '0137' => ['shop_sold', 'v1 v1', [qw(number amount)]],
  191. '0139' => ['monster_ranged_attack', 'a4 v1 v1 v1 v1 C1', [qw(ID sourceX sourceY targetX targetY type)]],
  192. '013A' => ['attack_range', 'v1', [qw(type)]],
  193. '013B' => ['arrow_none', 'v1', [qw(type)]],
  194. '013D' => ['hp_sp_changed', 'v1 v1', [qw(type amount)]],
  195. '013E' => ['skill_cast', 'a4 a4 v1 v1 v1 v1 v1 V1', [qw(sourceID targetID x y skillID unknown type wait)]],
  196. '013C' => ['arrow_equipped', 'v1', [qw(index)]],
  197. '0141' => ['stat_info2', 'v1 x2 v1 x2 v1', [qw(type val val2)]],
  198. '0142' => ['npc_talk_number', 'a4', [qw(ID)]],
  199. '0144' => ['minimap_indicator', 'a4 V3 C5', [qw(npcID type x y ID blue green red alpha)]],
  200. '0147' => ['item_skill', 'v1 v1 v1 v1 v1 v1 A*', [qw(skillID targetType unknown skillLv sp unknown2 skillName)]],
  201. '0148' => ['resurrection', 'a4 v1', [qw(targetID type)]],
  202. '014C' => ['guild_allies_enemy_list'],
  203. '0154' => ['guild_members_list'],
  204. '015A' => ['guild_leave', 'Z24 Z40', [qw(name message)]],
  205. '015C' => ['guild_expulsion', 'Z24 Z40 Z24', [qw(name message unknown)]],
  206. '015E' => ['guild_broken', 'V1', [qw(flag)]], # clif_guild_broken
  207. '0160' => ['guild_member_setting_list'],
  208. '0162' => ['guild_skills_list'],
  209. '0163' => ['guild_expulsionlist'],
  210. '0166' => ['guild_members_title_list'],
  211. '0167' => ['guild_create_result', 'C1', [qw(type)]],
  212. '0169' => ['guild_invite_result', 'C1', [qw(type)]],
  213. '016A' => ['guild_request', 'a4 Z24', [qw(ID name)]],
  214. '016C' => ['guild_name', 'a4, V2 x5 Z24', [qw(guildID emblemID mode guildName)]],
  215. '016D' => ['guild_member_online_status', 'a4 a4 V1', [qw(ID charID online)]],
  216. '016F' => ['guild_notice'],
  217. '0171' => ['guild_ally_request', 'a4 Z24', [qw(ID name)]],
  218. #'0173' => ['guild_alliance', 'V1', [qw(flag)]],
  219. '0177' => ['identify_list'],
  220. '0179' => ['identify', 'v*', [qw(index)]],
  221. '017B' => ['card_merge_list'],
  222. '017D' => ['card_merge_status', 'v1 v1 C1', [qw(item_index card_index fail)]],
  223. '017F' => ['guild_chat', 'x2 Z*', [qw(message)]],
  224. #'0181' => ['guild_opposition_result', 'C1', [qw(flag)]], # clif_guild_oppositionack
  225. #'0184' => ['guild_unally', 'a4 V1', [qw(guildID flag)]], # clif_guild_delalliance
  226. '0187' => ['sync_request', 'a4', [qw(ID)]],
  227. '0188' => ['item_upgrade', 'v1 v1 v1', [qw(type index upgrade)]],
  228. '0189' => ['no_teleport', 'v1', [qw(fail)]],
  229. '018C' => ['sense_result', 'v1 v1 v1 V1 v1 v1 v1 v1 C1 C1 C1 C1 C1 C1 C1 C1 C1', [qw(nameID level size hp def race mdef element ice earth fire wind poison holy dark spirit undead)]],
  230. '018D' => ['forge_list'],
  231. '018F' => ['refine_result', 'v1 v1', [qw(fail nameID)]],
  232. #'0191' => ['talkie_box', 'a4 Z80', [qw(ID message)]], # talkie box message
  233. '0194' => ['character_name', 'a4 Z24', [qw(ID name)]],
  234. '0195' => ['actor_name_received', 'a4 Z24 Z24 Z24 Z24', [qw(ID name partyName guildName guildTitle)]],
  235. '0196' => ['actor_status_active', 'v1 a4 C1', [qw(type ID flag)]],
  236. '0199' => ['pvp_mode1', 'v1', [qw(type)]],
  237. '019A' => ['pvp_rank', 'x2 V1 V1 V1', [qw(ID rank num)]],
  238. '019B' => ['unit_levelup', 'a4 V1', [qw(ID type)]],
  239. '01A0' => ['pet_capture_result', 'C1', [qw(type)]],
  240. '01A2' => ($rpackets{'01A2'} == 35 # or 37
  241. ? ['pet_info', 'Z24 C1 v4', [qw(name nameflag level hungry friendly accessory)]]
  242. : ['pet_info', 'Z24 C1 v5', [qw(name nameflag level hungry friendly accessory type)]]
  243. ),
  244. '01A3' => ['pet_food', 'C1 v1', [qw(success foodID)]],
  245. '01A4' => ['pet_info2', 'C a4 V', [qw(type ID value)]],
  246. '01A6' => ['egg_list'],
  247. '01AA' => ['pet_emotion', 'a4 V1', [qw(ID type)]],
  248. '01AB' => ['actor_muted', 'x2 a4 x2 L1', [qw(ID duration)]],
  249. '01AC' => ['actor_trapped', 'a4', [qw(ID)]],
  250. '01AD' => ['arrowcraft_list'],
  251. '01B0' => ['monster_typechange', 'a4 a1 V1', [qw(ID unknown type)]],
  252. '01B3' => ['npc_image', 'Z63 C1', [qw(npc_image type)]],
  253. '01B5' => ['account_payment_info', 'V1 V1', [qw(D_minute H_minute)]],
  254. '01B6' => ['guild_info', 'a4 V1 V1 V1 V1 V1 V1 x12 V1 Z24 Z24', [qw(ID lvl conMember maxMember average exp next_exp members name master)]],
  255. '01B9' => ['cast_cancelled', 'a4', [qw(ID)]],
  256. '01C3' => ['local_broadcast', 'x2 a3 x9 Z*', [qw(color message)]],
  257. '01C4' => ['storage_item_added', 'v1 V1 v1 C1 C1 C1 C1 a8', [qw(index amount ID type identified broken upgrade cards)]],
  258. '01C5' => ['cart_item_added', 'v1 V1 v1 x C1 C1 C1 a8', [qw(index amount ID identified broken upgrade cards)]],
  259. '01C8' => ['item_used', 'v1 v1 a4 v1 C1', [qw(index itemID ID remaining success)]],
  260. '01C9' => ['area_spell', 'a4 a4 v2 C2 C Z80', [qw(ID sourceID x y type fail scribbleLen scribbleMsg)]],
  261. '01CD' => ['sage_autospell'],
  262. '01CF' => ['devotion', 'a4 a20', [qw(sourceID data)]],
  263. '01D0' => ['revolving_entity', 'a4 v', [qw(sourceID entity)]],
  264. '01D2' => ['combo_delay', 'a4 V1', [qw(ID delay)]],
  265. '01D4' => ['npc_talk_text', 'a4', [qw(ID)]],
  266. '01D7' => ['player_equipment', 'a4 C1 v2', [qw(sourceID type ID1 ID2)]],
  267. '01D8' => ['actor_display', 'a4 v14 a4 x4 v1 x1 C1 a3 x2 C1 v1', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID skillstatus sex coords act lv)]],
  268. '01D9' => ['actor_display', 'a4 v14 a4 x4 v1 x1 C1 a3 x2 v1',    [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID skillstatus sex coords lv)]],
  269. '01DA' => ['actor_display', 'a4 v5 C1 x1 v3 x4 v5 a4 x4 v1 x1 C1 a5 x3 v1', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID skillstatus sex coords lv)]],
  270. '01DC' => ['secure_login_key', 'x2 a*', [qw(secure_key)]],
  271. '01D6' => ['pvp_mode2', 'v1', [qw(type)]],
  272. '01DE' => ['skill_use', 'v1 a4 a4 V1 V1 V1 V1 v1 v1 C1', [qw(skillID sourceID targetID tick src_speed dst_speed damage level param3 type)]],
  273. '01E1' => ['revolving_entity', 'a4 v1', [qw(sourceID entity)]],
  274. #'01E2' => ['marriage_unknown'], clif_parse_ReqMarriage
  275. #'01E4' => ['marriage_unknown'], clif_marriage_process
  276. ##
  277. #01E6 26 Some Player Name.
  278. '01E9' => ['party_join', 'a4 x4 v1 v1 C1 Z24 Z24 Z16 v C2', [qw(ID x y type name user map lv item_pickup item_share)]],
  279. '01EB' => ['guild_location', 'a4 v1 v1', [qw(ID x y)]],
  280. '01EA' => ['married', 'a4', [qw(ID)]],
  281. '01EE' => ['inventory_items_stackable'],
  282. '01EF' => ['cart_items_list'],
  283. '01F2' => ['guild_member_online_status', 'a4 a4 V1 v3', [qw(ID charID online sex hair_style hair_color)]],
  284. # weather/misceffect2 packet
  285. '01F3' => ['npc_effect', 'a4 a4', [qw(ID effect)]],
  286. '01F4' => ['deal_request', 'Z24 x4 v1', [qw(user level)]],
  287. '01F5' => ['deal_begin', 'C1 a4 v1', [qw(type targetID level)]],
  288. #'01F6' => ['adopt_unknown'], # clif_parse_ReqAdopt
  289. #'01F8' => ['adopt_unknown'], # clif_adopt_process
  290. '01F0' => ['storage_items_stackable'],
  291. '01FC' => ['repair_list'],
  292. '01FE' => ['repair_result', 'v1 C1', [qw(nameID flag)]],
  293. '0201' => ['friend_list'],
  294. #'0205' => ['divorce_unknown', 'Z24', [qw(name)]], # clif_divorced
  295. '0206' => ['friend_logon', 'a4 a4 C1', [qw(friendAccountID friendCharID isNotOnline)]],
  296. '0207' => ['friend_request', 'a4 a4 Z24', [qw(accountID charID name)]],
  297. '0209' => ['friend_response', 'C1 Z24', [qw(type name)]],
  298. '020A' => ['friend_removed', 'a4 a4', [qw(friendAccountID friendCharID)]],
  299. '020E' => ['taekwon_mission_receive', 'Z24 a4 c1', [qw(monName ID value)]],
  300. '0215' => ['gospel_buff_aligned', 'a4', [qw(ID)]],
  301. '0219' => ['top10_blacksmith_rank'],
  302. '021A' => ['top10_alchemist_rank'],
  303. '021B' => ['blacksmith_points', 'V1 V1', [qw(points total)]],
  304. '021C' => ['alchemist_point', 'V1 V1', [qw(points total)]],
  305. '0224' => ['taekwon_rank', 'c1 x3 c1', [qw(type rank)]],
  306. '0226' => ['top10_taekwon_rank'],
  307. '0227' => ['gameguard_request'],
  308. '0229' => ['character_status', 'a4 v1 v1 v1', [qw(ID param1 param2 param3)]],
  309. '022A' => ['actor_display', 'a4 v4 x2 v8 x2 v a4 a4 v x2 C2 a3 x2 C v', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead tophead midhead hair_color head_dir guildID guildEmblem visual_effects stance sex coords act lv)]],
  310. '022B' => ['actor_display', 'a4 v4 x2 v8 x2 v a4 a4 v x2 C2 a3 x2 v', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead tophead midhead hair_color head_dir guildID guildEmblem visual_effects stance sex coords lv)]],
  311. '022C' => ['actor_display', 'a4 v4 x2 v5 V1 v3 x4 a4 a4 v x2 C2 a5 x3 v', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead timestamp tophead midhead hair_color guildID guildEmblem visual_effects stance sex coords lv)]],
  312. '022E' => ['homunculus_stats', 'Z24 C v16 V2 v2', [qw(name state lvl hunger intimacy accessory atk matk hit critical def mdef flee aspd hp hp_max sp sp_max exp exp_max points_skill unknown)]],
  313. '022F' => ['homunculus_food', 'C1 v1', [qw(success foodID)]],
  314. '0230' => ['homunculus_info', 'x1 C1 a4 V1',[qw(type ID val)]],
  315. '0235' => ['skills_list'], # homunculus skills
  316. '0238' => ['top10_pk_rank'],
  317. # homunculus skill update
  318. '0239' => ['skill_update', 'v1 v1 v1 v1 C1', [qw(skillID lv sp range up)]], # range = skill range, up = this skill can be leveled up further
  319. '023A' => ['storage_password_request', 'v1', [qw(flag)]],
  320. '023C' => ['storage_password_result', 'v1 v1', [qw(type val)]],
  321. '023E' => ['storage_password_request', 'v1', [qw(flag)]],
  322. '0240' => ['mail_refreshinbox', 'v1 V1', [qw(size  count)]],
  323. '0242' => ['mail_read', 'v1 V1 Z40 Z24 x4 V2 v1 C1 x1 C3 a8 x1 Z*', [qw(lenght mailID title sender zeny amount nameID type identified broken upgrade cards message)]],
  324. '0245' => ['mail_getattachment', 'C1', [qw(fail)]],
  325. '0249' => ['mail_send', 'C1', [qw(fail)]],
  326. '024A' => ['mail_new', 'V1 Z24 Z24', [qw(mailID sender title)]],
  327. '0250' => ['auction_result', 'C1', [qw(flag)]],
  328. '0252' => ['auction_item_request_search', 'v1 V1 V1', [qw(size pages count)]],
  329. '0255' => ['mail_setattachment', 'v1 C1', [qw(index fail)]],
  330. '0256' => ['auction_add_item', 'v1 C1', [qw(index fail)]],
  331. '0257' => ['mail_delete', 'V1 v1', [qw(mailID fail)]],
  332. '0259' => ['gameguard_grant', 'C1', [qw(server)]],
  333. '025D' => ['auction_my_sell_stop', 'V1', [qw(flag)]],
  334. '025F' => ['auction_windows', 'V1 C1 C1 C1 C1 v1', [qw(flag unknown1 unknown2 unknown3 unknown4 unknown5)]],
  335. '0260' => ['mail_window', 'v1', [qw(flag)]],
  336. '0274' => ['mail_return', 'V1 v1', [qw(mailID fail)]],
  337. # mail_return packet: '0274' => ['account_server_info', 'x2 a4 a4 a4 x30 C1 x4 a*', [qw(sessionID accountID sessionID2 accountSex serverInfo)]],
  338. # tRO new packets, need some work on them
  339. '0283' => ['account_id', 'V1', [qw(accountID)]],
  340. '0287' => ['cash_dealer'],
  341. '0291' => ['message_string', 'v1', [qw(msg_id)]],
  342. '0295' => ['inventory_items_nonstackable'],
  343. '0296' => ['storage_items_nonstackable'],
  344. '0297' => ['cart_equip_list'],
  345. '029A' => ['inventory_item_added', 'v1 v1 v1 C1 C1 C1 a8 v1 C1 C1 a4', [qw(index amount nameID identified broken upgrade cards type_equip type fail cards_ext)]],
  346. # mercenaries
  347. '029B' => ($rpackets{'029B'} == 72 # or 80 # mercenary stats
  348. ? ['homunculus_stats', 'a4 v8 Z24 v5 V1 v2', [qw(ID atk matk hit critical def mdef flee aspd name lvl hp hp_max sp sp_max contract_end faith summons)]]
  349. : ['homunculus_stats', 'a4 v8 Z24 v1 V5 v1 V2 v1', [qw(ID atk matk hit critical def mdef flee aspd name lvl hp hp_max sp sp_max contract_end faith summons kills range)]]
  350. ),
  351. # TODO: test on officials: '029B' => ['homunculus_stats', 'a4 v8 Z24 v1 V5 v1 V2 v1', [qw(ID atk matk hit critical def mdef flee aspd name lvl hp hp_max sp sp_max contract_end faith summons killcount range)]], # mercenary stats
  352. '029D' => ['skills_list'], # mercenary skills
  353. '02A2' => ['mercenary_param_change', 'v1 V1', [qw(type param)]],
  354. # tRO HShield packet challenge. 
  355. # Borrow sub gameguard_request because it use the same mechanic.
  356. '02A6' => ['gameguard_request'],
  357. # mRO PIN code Check
  358. '02AD' => ['login_pin_code_request', 'v1 V', [qw(flag key)]],
  359. # Packet Prefix encryption Support
  360. '02AE' => ['initialize_message_id_encryption', 'V1 V1', [qw(param1 param2)]],
  361. # tRO new packets (2008-09-16Ragexe12_Th)
  362. '02B1' => ['quest_list'],
  363. '02B2' => ['objective_info'],
  364. '02B9' => ['hotkeys'],
  365. '02C5' => ['party_invite_result', 'Z24 V1', [qw(name type)]],
  366. '02C6' => ['party_invite', 'a4 Z24', [qw(ID name)]],
  367. '02C9' => ['party_allow_invite', 'C1', [qw(type)]],
  368. '02D0' => ['inventory_items_nonstackable'],
  369. '02D1' => ['storage_items_nonstackable'],
  370. '02D2' => ['cart_equip_list'],
  371. '02D4' => ['inventory_item_added', 'v1 v1 v1 C1 C1 C1 a8 v1 C1 C1 a6', [qw(index amount nameID identified broken upgrade cards type_equip type fail unknown)]],
  372. '02DA' => ['show_equipment', 'C1', [qw(type)]],
  373. # 02E1 packet unsure of param3 needs more testing
  374. '02E1' => ['actor_action', 'a4 a4 a4 V2 v1 x2 v1 x2 C1 v1', [qw(sourceID targetID tick src_speed dst_speed damage param2 type param3)]],
  375. '02E8' => ['inventory_items_stackable'],
  376. '02E9' => ['cart_items_list'],
  377. '02EA' => ['storage_items_stackable'],
  378. '02EB' => ['map_loaded', 'V1 a3 x2 v1', [qw(syncMapSync coords unknown)]],
  379. '02EC' => ['actor_display', 'x1 a4 v3 V1 v5 V1 v5 a4 a4 V1 C2 a5 x3 v2', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead timestamp tophead midhead hair_color clothes_color head_dir guildID guildEmblem visual_effects stance sex coords lv unknown)]],
  380. '02ED' => ['actor_display', 'a4 v3 V1 v10 a4 a4 V1 C2 a3 v v2', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID guildEmblem visual_effects stance sex coords act lv unknown)]],
  381. '02EE' => ['actor_display', 'a4 v3 V1 v10 a4 a4 V1 C2 a3 x1 v v2', [qw(ID walk_speed param1 param2 param3 type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID guildEmblem visual_effects stance sex coords act lv unknown)]],
  382. # status timers (eA has 12 unknown bytes)
  383. '043F' => ['actor_status_active', 'v1 a4 C1 V4', [qw(type ID flag tick unknown1 unknown2 unknown3)]],
  384. # HackShield alarm
  385. '0449' => ['hack_shield_alarm'],
  386. };
  387. return bless %self, $class;
  388. }
  389. ##
  390. # Network::Receive->create(String serverType)
  391. #
  392. # Create a new server message parsing object for the specified server type.
  393. #
  394. # Throws Network::Receive::InvalidServerType if the specified server type does
  395. # not exist.
  396. # Throws Network::Receive::CreationError if some other error occured.
  397. sub create {
  398. my ($self, $type) = @_;
  399. ($type) = $type =~ /([0-9_]+)/;
  400. $type = 0 if $type eq '';
  401. my $class = "Network::Receive::ServerType" . $type;
  402. undef $@;
  403. eval "use $class;";
  404. if ($@ =~ /^Can't locate /s) {
  405. Network::Receive::InvalidServerType->throw(
  406. TF("Cannot load server message parser for server type '%s'.", $type)
  407. );
  408. } elsif ($@) {
  409. Network::Receive::CreationError->throw(
  410. TF("An error occured while loading the server message parser for server type '%s':n%s",
  411. $type, $@)
  412. );
  413. } else {
  414. return $class->new();
  415. }
  416. }
  417. # $packetParser->reconstruct($args)
  418. #
  419. # Reconstructs a raw packet from $args using $self->{packet_list}.
  420. sub reconstruct {
  421. my ($self, $args) = @_;
  422. my $switch = $args->{switch};
  423. my $packet = $self->{packet_list}{$switch};
  424. my ($name, $packString, $varNames) = @{$packet};
  425. my @vars = ();
  426. for my $varName (@{$varNames}) {
  427. push(@vars, $args->{$varName});
  428. }
  429. my $packet = pack("H2 H2 $packString", substr($switch, 2, 2), substr($switch, 0, 2), @vars);
  430. return $packet;
  431. }
  432. sub parse {
  433. my ($self, $msg) = @_;
  434. $bytesReceived += length($msg);
  435. my $switch = Network::MessageTokenizer::getMessageID($msg);
  436. my $handler = $self->{packet_list}{$switch};
  437. return undef unless $handler;
  438. debug "Received packet: $switch Handler: $handler->[0]n", "packetParser", 2;
  439. # RAW_MSG is the entire message, including packet switch
  440. my %args = (
  441. switch => $switch,
  442. RAW_MSG => $msg,
  443. RAW_MSG_SIZE => length($msg)
  444. );
  445. if ($handler->[1]) {
  446. my @unpacked_data = unpack("x2 $handler->[1]", $msg);
  447. my $keys = $handler->[2];
  448. foreach my $key (@{$keys}) {
  449. $args{$key} = shift @unpacked_data;
  450. }
  451. }
  452. my $callback = $self->can($handler->[0]);
  453. if ($callback) {
  454. Plugins::callHook("packet_pre/$handler->[0]", %args);
  455. Misc::checkValidity("Packet: " . $handler->[0] . " (pre)");
  456. $self->$callback(%args);
  457. Misc::checkValidity("Packet: " . $handler->[0]);
  458. } else {
  459. debug "Packet Parser: Unhandled Packet: $switch Handler: $handler->[0]n", "packetParser", 2;
  460. }
  461. Plugins::callHook("packet/$handler->[0]", %args);
  462. return %args;
  463. }
  464. ##
  465. # boolean $packetParser->willMangle(Bytes messageID)
  466. # messageID: a message ID, such as "008A".
  467. #
  468. # Check whether the message with the specified message ID will be mangled.
  469. # If the bot is running in X-Kore mode, then messages that will be mangled will not
  470. # be sent to the RO client.
  471. #
  472. # By default, a message will never be mangled. Plugins can register mangling procedures
  473. # though. This is done by using the following hooks:
  474. # `l
  475. # - "Network::Receive/willMangle" - This hook has arguments 'messageID' (Bytes) and 'name' (String).
  476. #          'name' is a human-readable description of the message, and may be undef. Plugins
  477. #          should set the 'return' argument to 1 if they want willMangle() to return 1.
  478. # - "Network::Receive/mangle" - This hook has arguments 'messageArgs' and 'messageName' (the latter may be undef).
  479. # `l`
  480. # The following example demonstrates how this is done:
  481. # <pre class="example">
  482. # Plugins::addHook("Network::Receive/willMangle", &willMangle);
  483. # Plugins::addHook("Network::Receive/mangle", &mangle);
  484. #
  485. # sub willMangle {
  486. #     my (undef, $args) = @_;
  487. #     if ($args->{messageID} eq '008A') {
  488. #         $args->{willMangle} = 1;
  489. #     }
  490. # }
  491. #
  492. # sub mangle {
  493. #     my (undef, $args) = @_;
  494. #     my $message_args = $args->{messageArgs};
  495. #     if ($message_args->{switch} eq '008A') {
  496. #         ...Modify $message_args as necessary....
  497. #     }
  498. # }
  499. # </pre>
  500. sub willMangle {
  501. if (Plugins::hasHook("Network::Receive/willMangle")) {
  502. my ($self, $messageID) = @_;
  503. my $packet = $self->{packet_list}{$messageID};
  504. my $name;
  505. $name = $packet->[0] if ($packet);
  506. my %args = (
  507. messageID => $messageID,
  508. name => $name
  509. );
  510. Plugins::callHook("Network::Receive/willMangle", %args);
  511. return $args{return};
  512. } else {
  513. return undef;
  514. }
  515. }
  516. # boolean $packetParser->mangle(Array* args)
  517. #
  518. # Calls the appropriate plugin function to mangle the packet, which
  519. # destructively modifies $args.
  520. # Returns false if the packet should be suppressed.
  521. sub mangle {
  522. my ($self, $args) = @_;
  523. my %hook_args = (messageArgs => $args);
  524. my $entry = $self->{packet_list}{$args->{switch}};
  525. if ($entry) {
  526. $hook_args{messageName} = $entry->[0];
  527. }
  528. Plugins::callHook("Network::Receive/mangle", %hook_args);
  529. return $hook_args{return};
  530. }
  531. ##
  532. # Network::Receive->decrypt(r_msg, themsg)
  533. # r_msg: a reference to a scalar.
  534. # themsg: the message to decrypt.
  535. #
  536. # Decrypts the packets in $themsg and put the result in the scalar
  537. # referenced by $r_msg.
  538. #
  539. # This is an old method used back in the iRO beta 2 days when iRO had encrypted packets.
  540. # At the moment (December 20 2006) there are no servers that still use encrypted packets.
  541. #
  542. # Example:
  543. # } elsif ($switch eq "ABCD") {
  544. #  my $level;
  545. #  Network::Receive->decrypt($level, substr($msg, 0, 2));
  546. sub decrypt {
  547. use bytes;
  548. my ($self, $r_msg, $themsg) = @_;
  549. my @mask;
  550. my $i;
  551. my ($temp, $msg_temp, $len_add, $len_total, $loopin, $len, $val);
  552. if ($config{encrypt} == 1) {
  553. undef $$r_msg;
  554. undef $len_add;
  555. undef $msg_temp;
  556. for ($i = 0; $i < 13;$i++) {
  557. $mask[$i] = 0;
  558. }
  559. $len = unpack("v1",substr($themsg,0,2));
  560. $val = unpack("v1",substr($themsg,2,2));
  561. {
  562. use integer;
  563. $temp = ($val * $val * 1391);
  564. }
  565. $temp = ~(~($temp));
  566. $temp = $temp % 13;
  567. $mask[$temp] = 1;
  568. {
  569. use integer;
  570. $temp = $val * 1397;
  571. }
  572. $temp = ~(~($temp));
  573. $temp = $temp % 13;
  574. $mask[$temp] = 1;
  575. for($loopin = 0; ($loopin + 4) < $len; $loopin++) {
  576.   if (!($mask[$loopin % 13])) {
  577.    $msg_temp .= substr($themsg,$loopin + 4,1);
  578. }
  579. }
  580. if (($len - 4) % 8 != 0) {
  581. $len_add = 8 - (($len - 4) % 8);
  582. }
  583. $len_total = $len + $len_add;
  584. $$r_msg = $msg_temp.substr($themsg, $len_total, length($themsg) - $len_total);
  585. } elsif ($config{encrypt} >= 2) {
  586. undef $$r_msg;
  587. undef $len_add;
  588. undef $msg_temp;
  589. for ($i = 0; $i < 17;$i++) {
  590. $mask[$i] = 0;
  591. }
  592. $len = unpack("v1",substr($themsg,0,2));
  593. $val = unpack("v1",substr($themsg,2,2));
  594. {
  595. use integer;
  596. $temp = ($val * $val * 34953);
  597. }
  598. $temp = ~(~($temp));
  599. $temp = $temp % 17;
  600. $mask[$temp] = 1;
  601. {
  602. use integer;
  603. $temp = $val * 2341;
  604. }
  605. $temp = ~(~($temp));
  606. $temp = $temp % 17;
  607. $mask[$temp] = 1;
  608. for($loopin = 0; ($loopin + 4) < $len; $loopin++) {
  609.   if (!($mask[$loopin % 17])) {
  610.    $msg_temp .= substr($themsg,$loopin + 4,1);
  611. }
  612. }
  613. if (($len - 4) % 8 != 0) {
  614. $len_add = 8 - (($len - 4) % 8);
  615. }
  616. $len_total = $len + $len_add;
  617. $$r_msg = $msg_temp.substr($themsg, $len_total, length($themsg) - $len_total);
  618. } else {
  619. $$r_msg = $themsg;
  620. }
  621. }
  622. #######################################
  623. ###### Private methods
  624. #######################################
  625. sub queryLoginPinCode {
  626. my $message = $_[0] || T("You've never set a login PIN code before.nPlease enter a new login PIN code:");
  627. do {
  628. my $input = $interface->query($message, isPassword => 1,);
  629. if (!defined($input)) {
  630. quit();
  631. return;
  632. } else {
  633. if ($input !~ /^d+$/) {
  634. $interface->errorDialog(T("The PIN code may only contain digits."));
  635. } elsif ((length($input) <= 3) || (length($input) >= 9)) {
  636. $interface->errorDialog(T("The PIN code must be between 4 and 9 characters."));
  637. } else {
  638. return $input;
  639. }
  640. }
  641. } while (1);
  642. }
  643. sub queryAndSaveLoginPinCode {
  644. my ($message) = @_;
  645. my $pin = queryLoginPinCode($message);
  646. if (defined $pin) {
  647. configModify('loginPinCode', $pin, silent => 1);
  648. return 1;
  649. } else {
  650. return 0;
  651. }
  652. }
  653. #######################################
  654. ###### Packet handling callbacks ######
  655. #######################################
  656. # This is for what eA calls PacketVersion 9, they send the AID in a 'proper' packet
  657. sub account_id {
  658. my ($self, $args) = @_;
  659. # the account ID is already unpacked into PLAIN TEXT when it gets to this function...
  660. # So lets not fuckup the $accountID since we need that later... someone will prolly have to fix this later on
  661. #$accountID = $args->{accountID};
  662. }
  663. sub account_payment_info {
  664. my ($self, $args) = @_;
  665. my $D_minute = $args->{D_minute};
  666. my $H_minute = $args->{H_minute};
  667. my $D_d = int($D_minute / 1440);
  668. my $D_h = int(($D_minute % 1440) / 60);
  669. my $D_m = int(($D_minute % 1440) % 60);
  670. my $H_d = int($H_minute / 1440);
  671. my $H_h = int(($H_minute % 1440) / 60);
  672. my $H_m = int(($H_minute % 1440) % 60);
  673. message  T("============= Account payment information =============n"), "info";
  674. message TF("Pay per day  : %s day(s) %s hour(s) and %s minute(s)n", $D_d, $D_h, $D_m), "info";
  675. message TF("Pay per hour : %s day(s) %s hour(s) and %s minute(s)n", $H_d, $H_h, $H_m), "info";
  676. message  T("-------------------------------------------------------n"), "info";
  677. }
  678. sub account_server_info {
  679. my ($self, $args) = @_;
  680. my $msg = $args->{serverInfo};
  681. my $msg_size = length($msg);
  682. $net->setState(2);
  683. undef $conState_tries;
  684. $sessionID = $args->{sessionID};
  685. $accountID = $args->{accountID};
  686. $sessionID2 = $args->{sessionID2};
  687. # Account sex should only be 0 (female) or 1 (male)
  688. # inRO gives female as 2 but expects 0 back
  689. # do modulus of 2 here to fix?
  690. # FIXME: we should check exactly what operation the client does to the number given
  691. $accountSex = $args->{accountSex} % 2;
  692. $accountSex2 = ($config{'sex'} ne "") ? $config{'sex'} : $accountSex;
  693. message swrite(
  694. T("-----------Account Info------------n" .
  695. "Account ID: @<<<<<<<<< @<<<<<<<<<<n" .
  696. "Sex:        @<<<<<<<<<<<<<<<<<<<<<n" .
  697. "Session ID: @<<<<<<<<< @<<<<<<<<<<n" .
  698. "            @<<<<<<<<< @<<<<<<<<<<n" .
  699. "-----------------------------------"),
  700. [unpack("V1",$accountID), getHex($accountID), $sex_lut{$accountSex}, unpack("V1",$sessionID), getHex($sessionID),
  701. unpack("V1",$sessionID2), getHex($sessionID2)]), 'connection';
  702. my $num = 0;
  703. undef @servers;
  704. for (my $i = 0; $i < $msg_size; $i+=32) {
  705. $servers[$num]{ip} = makeIP(substr($msg, $i, 4));
  706. $servers[$num]{ip} = $masterServer->{ip} if ($masterServer && $masterServer->{private});
  707. $servers[$num]{port} = unpack("v1", substr($msg, $i+4, 2));
  708. ($servers[$num]{name}) = bytesToString(unpack("Z*", substr($msg, $i + 6, 20)));
  709. $servers[$num]{users} = unpack("V",substr($msg, $i + 26, 4));
  710. $num++;
  711. }
  712. message T("--------- Servers ----------n" .
  713. "#   Name                  Users  IP              Portn"), 'connection';
  714. for (my $num = 0; $num < @servers; $num++) {
  715. message(swrite(
  716. "@<< @<<<<<<<<<<<<<<<<<<<< @<<<<< @<<<<<<<<<<<<<< @<<<<<",
  717. [$num, $servers[$num]{name}, $servers[$num]{users}, $servers[$num]{ip}, $servers[$num]{port}]
  718. ), 'connection');
  719. }
  720. message("-------------------------------n", 'connection');
  721. if ($net->version != 1) {
  722. message T("Closing connection to Account Servern"), 'connection';
  723. $net->serverDisconnect();
  724. if (!$masterServer->{charServer_ip} && $config{server} eq "") {
  725. my @serverList;
  726. foreach my $server (@servers) {
  727. push @serverList, $server->{name};
  728. }
  729. my $ret = $interface->showMenu(
  730. T("Please select your login server."),
  731. @serverList,
  732. title => T("Select Login Server"));
  733. if ($ret == -1) {
  734. quit();
  735. } else {
  736. main::configModify('server', $ret, 1);
  737. }
  738. } elsif ($masterServer->{charServer_ip}) {
  739. message TF("Forcing connect to char server %s: %sn", $masterServer->{charServer_ip}, $masterServer->{charServer_port}), 'connection';
  740. } else {
  741. message TF("Server %s selectedn",$config{server}), 'connection';
  742. }
  743. }
  744. }
  745. sub actor_action {
  746. my ($self,$args) = @_;
  747. return unless changeToInGameState();
  748. $args->{damage} = intToSignedShort($args->{damage});
  749. if ($args->{type} == 1) {
  750. # Take item
  751. my $source = Actor::get($args->{sourceID});
  752. my $verb = $source->verb('pick up', 'picks up');
  753. my $target = getActorName($args->{targetID});
  754. debug "$source $verb $targetn", 'parseMsg_presence';
  755. my $item = $itemsList->getByID($args->{targetID});
  756. $item->{takenBy} = $args->{sourceID} if ($item);
  757. } elsif ($args->{type} == 2) {
  758. # Sit
  759. my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
  760. if ($args->{sourceID} eq $accountID) {
  761. message T("You are sitting.n") if (!$char->{sitting});
  762. $char->{sitting} = 1;
  763. AI::queue("sitAuto") unless (AI::inQueue("sitAuto")) || $ai_v{sitAuto_forcedBySitCommand};
  764. } else {
  765. message TF("%s is sitting.n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
  766. my $player = $playersList->getByID($args->{sourceID});
  767. $player->{sitting} = 1 if ($player);
  768. }
  769. Misc::checkValidity("actor_action (take item)");
  770. } elsif ($args->{type} == 3) {
  771. # Stand
  772. my ($source, $verb) = getActorNames($args->{sourceID}, 0, 'are', 'is');
  773. if ($args->{sourceID} eq $accountID) {
  774. message T("You are standing.n") if ($char->{sitting});
  775. if ($config{sitAuto_idle}) {
  776. $timeout{ai_sit_idle}{time} = time;
  777. }
  778. $char->{sitting} = 0;
  779. } else {
  780. message TF("%s is standing.n", getActorName($args->{sourceID})), 'parseMsg_statuslook', 2;
  781. my $player = $playersList->getByID($args->{sourceID});
  782. $player->{sitting} = 0 if ($player);
  783. }
  784. Misc::checkValidity("actor_action (stand)");
  785. } else {
  786. # Attack
  787. my $dmgdisplay;
  788. my $totalDamage = $args->{damage} + $args->{param3};
  789. if ($totalDamage == 0) {
  790. $dmgdisplay = "Miss!";
  791. $dmgdisplay .= "!" if ($args->{type} == 11);
  792. } else {
  793. $dmgdisplay = $args->{damage};
  794. $dmgdisplay .= "!" if ($args->{type} == 10);
  795. $dmgdisplay .= " + $args->{param3}" if $args->{param3};
  796. }
  797. Misc::checkValidity("actor_action (attack 1)");
  798. updateDamageTables($args->{sourceID}, $args->{targetID}, $totalDamage);
  799. Misc::checkValidity("actor_action (attack 2)");
  800. my $source = Actor::get($args->{sourceID});
  801. my $target = Actor::get($args->{targetID});
  802. my $verb = $source->verb('attack', 'attacks');
  803. $target->{sitting} = 0 unless $args->{type} == 4 || $args->{type} == 9 || $totalDamage == 0;
  804. my $msg = attack_string($source, $target, $dmgdisplay, ($args->{src_speed}/10));
  805. Plugins::callHook('packet_attack', {sourceID => $args->{sourceID}, targetID => $args->{targetID}, msg => $msg, dmg => $totalDamage, type => $args->{type}});
  806. my $status = sprintf("[%3d/%3d]", percent_hp($char), percent_sp($char));
  807. Misc::checkValidity("actor_action (attack 3)");
  808. if ($args->{sourceID} eq $accountID) {
  809. message("$status $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
  810. if ($startedattack) {
  811. $monstarttime = time();
  812. $monkilltime = time();
  813. $startedattack = 0;
  814. }
  815. Misc::checkValidity("actor_action (attack 4)");
  816. calcStat($args->{damage});
  817. Misc::checkValidity("actor_action (attack 5)");
  818. } elsif ($args->{targetID} eq $accountID) {
  819. message("$status $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
  820. if ($args->{damage} > 0) {
  821. $damageTaken{$source->{name}}{attack} += $args->{damage};
  822. }
  823. } elsif ($char->{slaves} && $char->{slaves}{$args->{sourceID}}) {
  824. message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{sourceID}}{hpPercent}, $char->{slaves}{$args->{sourceID}}{spPercent}) . " $msg", $totalDamage > 0 ? "attackMon" : "attackMonMiss");
  825. } elsif ($char->{slaves} && $char->{slaves}{$args->{targetID}}) {
  826. message(sprintf("[%3d/%3d]", $char->{slaves}{$args->{targetID}}{hpPercent}, $char->{slaves}{$args->{targetID}}{spPercent}) . " $msg", $args->{damage} > 0 ? "attacked" : "attackedMiss");
  827. } else {
  828. debug("$msg", 'parseMsg_damage');
  829. }
  830. Misc::checkValidity("actor_action (attack 6)");
  831. }
  832. }
  833. sub actor_died_or_disappeared {
  834. my ($self,$args) = @_;
  835. return unless changeToInGameState();
  836. my $ID = $args->{ID};
  837. avoidList_ID($ID);
  838. if ($ID eq $accountID) {
  839. message T("You have diedn") if (!$char->{dead});
  840. Plugins::callHook('self_died');
  841. closeShop() unless !$shopstarted || $config{'dcOnDeath'} == -1 || !$AI;
  842. $char->{deathCount}++;
  843. $char->{dead} = 1;
  844. $char->{dead_time} = time;
  845. } elsif (defined $monstersList->getByID($ID)) {
  846. my $monster = $monstersList->getByID($ID);
  847. if ($args->{type} == 0) {
  848. debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})n", "parseMsg_presence";
  849. $monster->{disappeared} = 1;
  850. } elsif ($args->{type} == 1) {
  851. debug "Monster Died: " . $monster->name . " ($monster->{binID})n", "parseMsg_damage";
  852. $monster->{dead} = 1;
  853. if ((AI::action ne "attack" || AI::args(0)->{ID} ne $ID) &&
  854.     ($config{itemsTakeAuto_party} &&
  855.     ($monster->{dmgFromParty} > 0 ||
  856.      $monster->{dmgFromYou} > 0))) {
  857. AI::clear("items_take");
  858. ai_items_take($monster->{pos}{x}, $monster->{pos}{y},
  859. $monster->{pos_to}{x}, $monster->{pos_to}{y});
  860. }
  861. } elsif ($args->{type} == 2) { # What's this?
  862. debug "Monster Disappeared: " . $monster->name . " ($monster->{binID})n", "parseMsg_presence";
  863. $monster->{disappeared} = 1;
  864. } elsif ($args->{type} == 3) {
  865. debug "Monster Teleported: " . $monster->name . " ($monster->{binID})n", "parseMsg_presence";
  866. $monster->{teleported} = 1;
  867. }
  868. $monster->{gone_time} = time;
  869. $monsters_old{$ID} = $monster->deepCopy();
  870. Plugins::callHook('monster_disappeared', {monster => $monster});
  871. $monstersList->remove($monster);
  872. } elsif (defined $playersList->getByID($ID)) {
  873. my $player = $playersList->getByID($ID);
  874. if ($args->{type} == 1) {
  875. message TF("Player Died: %s (%d) %s %sn", $player->name, $player->{binID}, $sex_lut{$player->{sex}}, $jobs_lut{$player->{jobID}});
  876. if ($char->{homunculus} && $char->{homunculus}{ID} eq $player->{ID}) {
  877. $playersList->remove($player);
  878. } else {
  879. $player->{dead} = 1;
  880. $player->{dead_time} = time;
  881. }
  882. } else {
  883. if ($args->{type} == 0) {
  884. 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";
  885. $player->{disappeared} = 1;
  886. } elsif ($args->{type} == 2) {
  887. 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";
  888. $player->{disconnected} = 1;
  889. } elsif ($args->{type} == 3) {
  890. 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";
  891. $player->{teleported} = 1;
  892. } else {
  893. debug "Player Disappeared in an unknown way: ".$player->name." ($player->{binID}) $sex_lut{$player->{sex}} $jobs_lut{$player->{jobID}}n", "parseMsg_presence";
  894. $player->{disappeared} = 1;
  895. }
  896. $player->{gone_time} = time;
  897. $players_old{$ID} = $player->deepCopy();
  898. Plugins::callHook('player_disappeared', {player => $player});
  899. $playersList->remove($player);
  900. }
  901. } elsif ($players_old{$ID}) {
  902. if ($args->{type} == 2) {
  903. debug "Player Disconnected: " . $players_old{$ID}->name . "n", "parseMsg_presence";
  904. $players_old{$ID}{disconnected} = 1;
  905. } elsif ($args->{type} == 3) {
  906. debug "Player Teleported: " . $players_old{$ID}->name . "n", "parseMsg_presence";
  907. $players_old{$ID}{teleported} = 1;
  908. }
  909. } elsif (defined $portalsList->getByID($ID)) {
  910. my $portal = $portalsList->getByID($ID);
  911. debug "Portal Disappeared: " . $portal->name . " ($portal->{binID})n", "parseMsg";
  912. $portal->{disappeared} = 1;
  913. $portal->{gone_time} = time;
  914. $portals_old{$ID} = $portal->deepCopy();
  915. $portalsList->remove($portal);
  916. } elsif (defined $npcsList->getByID($ID)) {
  917. my $npc = $npcsList->getByID($ID);
  918. debug "NPC Disappeared: " . $npc->name . " ($npc->{nameID})n", "parseMsg";
  919. $npc->{disappeared} = 1;
  920. $npc->{gone_time} = time;
  921. $npcs_old{$ID} = $npc->deepCopy();
  922. $npcsList->remove($npc);
  923. } elsif (defined $petsList->getByID($ID)) {
  924. my $pet = $petsList->getByID($ID);
  925. debug "Pet Disappeared: " . $pet->name . " ($pet->{binID})n", "parseMsg";
  926. $pet->{disappeared} = 1;
  927. $pet->{gone_time} = time;
  928. $petsList->remove($pet);
  929. } elsif (defined $slavesList->getByID($ID)) {
  930. my $slave = $slavesList->getByID($ID);
  931. if ($args->{type} == 1) {
  932. message TF("Slave Died: %s (%d) %sn", $slave->name, $slave->{binID}, $slave->{actorType});
  933. } else {
  934. if ($args->{type} == 0) {
  935. debug "Slave Disappeared: " . $slave->name . " ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})n", "parseMsg_presence";
  936. $slave->{disappeared} = 1;
  937. } elsif ($args->{type} == 2) {
  938. debug "Slave Disconnected: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})n", "parseMsg_presence";
  939. $slave->{disconnected} = 1;
  940. } elsif ($args->{type} == 3) {
  941. debug "Slave Teleported: ".$slave->name." ($slave->{binID}) $slave->{actorType} ($slave->{pos_to}{x}, $slave->{pos_to}{y})n", "parseMsg_presence";
  942. $slave->{teleported} = 1;
  943. } else {
  944. debug "Slave Disappeared in an unknown way: ".$slave->name." ($slave->{binID}) $slave->{actorType}n", "parseMsg_presence";
  945. $slave->{disappeared} = 1;
  946. }
  947. $slave->{gone_time} = time;
  948. Plugins::callHook('slave_disappeared', {slave => $slave});
  949. }
  950. $slavesList->remove($slave);
  951. } else {
  952. debug "Unknown Disappeared: ".getHex($ID)."n", "parseMsg";
  953. }
  954. }
  955. # This function is a merge of actor_exists, actor_connected, actor_moved, etc...
  956. sub actor_display {
  957. my ($self, $args) = @_;
  958. return unless changeToInGameState();
  959. my ($actor, $mustAdd);
  960. #### Initialize ####
  961. my $nameID = unpack("V1", $args->{ID});
  962. my (%coordsFrom, %coordsTo);
  963. if ($args->{switch} eq "007C") {
  964. makeCoords(%coordsTo, $args->{coords});
  965. %coordsFrom = %coordsTo;
  966. } elsif ($args->{switch} eq "01DA") {
  967. makeCoords(%coordsFrom, substr($args->{RAW_MSG}, 50, 3));
  968. makeCoords2(%coordsTo, substr($args->{RAW_MSG}, 52, 3));
  969. } elsif (length($args->{coords}) >= 5) {
  970. my $coordsArg = $args->{coords};
  971. unShiftPack($coordsArg, $coordsTo{y}, 10);
  972. unShiftPack($coordsArg, $coordsTo{x}, 10);
  973. unShiftPack($coordsArg, $coordsFrom{y}, 10);
  974. unShiftPack($coordsArg, $coordsFrom{x}, 10);
  975. } else {
  976. my $coordsArg = $args->{coords};
  977. unShiftPack($coordsArg, $args->{body_dir}, 4);
  978. unShiftPack($coordsArg, $coordsTo{y}, 10);
  979. unShiftPack($coordsArg, $coordsTo{x}, 10);
  980. %coordsFrom = %coordsTo;
  981. }
  982. if ($args->{switch} eq "0086") {
  983. # Message 0086 contains less information about the actor than other similar
  984. # messages. So we use the existing actor information.
  985. $args = Actor::get($args->{ID})->deepCopy();
  986. $args->{switch} = "0086";
  987. }
  988. # Remove actors with a distance greater than removeActorWithDistance. Useful for vending (so you don't spam
  989. # too many packets in prontera and cause server lag). As a side effect, you won't be able to "see" actors
  990. # beyond removeActorWithDistance.
  991. if ($config{removeActorWithDistance}) {
  992. if ((my $block_dist = blockDistance($char->{pos_to}, %coordsTo)) > ($config{removeActorWithDistance})) {
  993. my $nameIdTmp = unpack("V1", $args->{ID});
  994. debug "Removed out of sight actor $nameIdTmp at ($coordsTo{x}, $coordsTo{y}) (distance: $block_dist)n";
  995. return;
  996. }
  997. }
  998. #### Step 1: create/get the correct actor object ####
  999. if ($jobs_lut{$args->{type}}) {
  1000. unless ($args->{type} > 6000) {
  1001. # Actor is a player
  1002. $actor = $playersList->getByID($args->{ID});
  1003. if (!defined $actor) {
  1004. $actor = new Actor::Player($args->{type});
  1005. $actor->{appear_time} = time;
  1006. $mustAdd = 1;
  1007. }
  1008. $actor->{nameID} = $nameID;
  1009. } else {
  1010. # Actor is a homunculus or a mercenary
  1011. $actor = $slavesList->getByID($args->{ID});
  1012. if (!defined $actor) {
  1013. $actor = $char->{slaves} && $char->{slaves}{$args->{ID}}
  1014. ? $char->{slaves}{$args->{ID}} : new Actor::Slave ($args->{type});
  1015. $actor->{appear_time} = time;
  1016. $mustAdd = 1;
  1017. }
  1018. $actor->{nameID} = $nameID;
  1019. }
  1020. } elsif ($args->{type} == 45) {
  1021. # Actor is a portal
  1022. $actor = $portalsList->getByID($args->{ID});
  1023. if (!defined $actor) {
  1024. $actor = new Actor::Portal();
  1025. $actor->{appear_time} = time;
  1026. my $exists = portalExists($field{name}, %coordsTo);
  1027. $actor->{source}{map} = $field{name};
  1028. if ($exists ne "") {
  1029. $actor->setName("$portals_lut{$exists}{source}{map} -> " . getPortalDestName($exists));
  1030. }
  1031. $mustAdd = 1;
  1032. # Strangely enough, portals (like all other actors) have names, too.
  1033. # We _could_ send a "actor_info_request" packet to find the names of each portal,
  1034. # however I see no gain from this. (And it might even provide another way of private
  1035. # servers to auto-ban bots.)
  1036. }
  1037. $actor->{nameID} = $nameID;
  1038. } elsif ($args->{type} >= 1000) {
  1039. # Actor might be a monster
  1040. if ($args->{hair_style} == 0x64) {
  1041. # Actor is a pet
  1042. $actor = $petsList->getByID($args->{ID});
  1043. if (!defined $actor) {
  1044. $actor = new Actor::Pet();
  1045. $actor->{appear_time} = time;
  1046. if ($monsters_lut{$args->{type}}) {
  1047. $actor->setName($monsters_lut{$args->{type}});
  1048. }
  1049. $actor->{name_given} = "Unknown";
  1050. $mustAdd = 1;
  1051. # Previously identified monsters could suddenly be identified as pets.
  1052. if ($monstersList->getByID($args->{ID})) {
  1053. $monstersList->removeByID($args->{ID});
  1054. }
  1055. }
  1056. } else {
  1057. # Actor really is a monster
  1058. $actor = $monstersList->getByID($args->{ID});
  1059. if (!defined $actor) {
  1060. $actor = new Actor::Monster();
  1061. $actor->{appear_time} = time;
  1062. if ($monsters_lut{$args->{type}}) {
  1063. $actor->setName($monsters_lut{$args->{type}});
  1064. }
  1065. $actor->{name_given} = "Unknown";
  1066. $actor->{binType} = $args->{type};
  1067. $mustAdd = 1;
  1068. }
  1069. }
  1070. # Why do monsters and pets use nameID as type?
  1071. $actor->{nameID} = $args->{type};
  1072. } else { # ($args->{type} < 1000 && $args->{type} != 45 && !$jobs_lut{$args->{type}})
  1073. # Actor is an NPC
  1074. $actor = $npcsList->getByID($args->{ID});
  1075. if (!defined $actor) {
  1076. $actor = new Actor::NPC();
  1077. $actor->{appear_time} = time;
  1078. $mustAdd = 1;
  1079. }
  1080. $actor->{nameID} = $nameID;
  1081. }
  1082. #### Step 2: update actor information ####
  1083. $actor->{ID} = $args->{ID};
  1084. $actor->{jobID} = $args->{type};
  1085. $actor->{type} = $args->{type};
  1086. $actor->{lv} = $args->{lv};
  1087. $actor->{pos} = {%coordsFrom};
  1088. $actor->{pos_to} = {%coordsTo};
  1089. $actor->{walk_speed} = $args->{walk_speed} / 1000 if (exists $args->{walk_speed});
  1090. $actor->{time_move} = time;
  1091. $actor->{time_move_calc} = distance(%coordsFrom, %coordsTo) * $actor->{walk_speed};
  1092. if (UNIVERSAL::isa($actor, "Actor::Player")) {
  1093. # None of this stuff should matter if the actor isn't a player...
  1094. # Interesting note about guildEmblem. If it is 0 (or none), the Ragnarok
  1095. # client will display "Send (Player) a guild invitation" (assuming one has
  1096. # invitation priveledges), regardless of whether or not guildID is set.
  1097. # I bet that this is yet another brilliant "feature" by GRAVITY's good programmers.
  1098. $actor->{guildEmblem} = $args->{guildEmblem} if (exists $args->{guildEmblem});
  1099. $actor->{guildID} = $args->{guildID} if (exists $args->{guildID});
  1100. if (exists $args->{lowhead}) {
  1101. $actor->{headgear}{low} = $args->{lowhead};
  1102. $actor->{headgear}{mid} = $args->{midhead};
  1103. $actor->{headgear}{top} = $args->{tophead};
  1104. $actor->{weapon} = $args->{weapon};
  1105. $actor->{shield} = $args->{shield};
  1106. }
  1107. $actor->{sex} = $args->{sex};
  1108. if ($args->{act} == 1) {
  1109. $actor->{dead} = 1;
  1110. } elsif ($args->{act} == 2) {
  1111. $actor->{sitting} = 1;
  1112. }
  1113. # Monsters don't have hair colors or heads to look around...
  1114. $actor->{hair_color} = $args->{hair_color} if (exists $args->{hair_color});
  1115. }
  1116. # But hair_style is used for pets, and their bodies can look different ways...
  1117. $actor->{hair_style} = $args->{hair_style} if (exists $args->{hair_style});
  1118. $actor->{look}{body} = $args->{body_dir} if (exists $args->{body_dir});
  1119. $actor->{look}{head} = $args->{head_dir} if (exists $args->{head_dir});
  1120. # When stance is non-zero, character is bobbing as if they had just got hit,
  1121. # but the cursor also turns to a sword when they are mouse-overed.
  1122. $actor->{stance} = $args->{stance} if (exists $args->{stance});
  1123. # Visual effects are a set of flags
  1124. $actor->{visual_effects} = $args->{visual_effects} if (exists $args->{visual_effects});
  1125. # Known visual effects:
  1126. # 0x0001 = Yellow tint (eg, a quicken skill)
  1127. # 0x0002 = Red tint (eg, power-thrust)
  1128. # 0x0004 = Gray tint (eg, energy coat)
  1129. # 0x0008 = Slow lightning (eg, mental strength)
  1130. # 0x0010 = Fast lightning (eg, MVP fury)
  1131. # 0x0020 = Black non-moving statue (eg, stone curse)
  1132. # 0x0040 = Translucent weapon
  1133. # 0x0080 = Translucent red sprite (eg, marionette control?)
  1134. # 0x0100 = Spaztastic weapon image (eg, mystical amplification)
  1135. # 0x0200 = Gigantic glowy sphere-thing
  1136. # 0x0400 = Translucent pink sprite (eg, marionette control?)
  1137. # 0x0800 = Glowy sprite outline (eg, assumptio)
  1138. # 0x1000 = Bright red sprite, slowly moving red lightning (eg, MVP fury?)
  1139. # 0x2000 = Vortex-type effect
  1140. # Note that these are flags, and you can mix and match them
  1141. # Example: 0x000C (0x0008 & 0x0004) = gray tint with slow lightning
  1142. # Save these parameters ...
  1143. $actor->{param1} = $args->{param1};
  1144. $actor->{param2} = $args->{param2};
  1145. $actor->{param3} = $args->{param3};
  1146. # And use them to set status flags.
  1147. if (setStatus($actor, $args->{param1}, $args->{param2}, $args->{param3})) {
  1148. $mustAdd = 0;
  1149. }
  1150. #### Step 3: Add actor to actor list ####
  1151. if ($mustAdd) {
  1152. if (UNIVERSAL::isa($actor, "Actor::Player")) {
  1153. $playersList->add($actor);
  1154. } elsif (UNIVERSAL::isa($actor, "Actor::Monster")) {
  1155. $monstersList->add($actor);
  1156. } elsif (UNIVERSAL::isa($actor, "Actor::Pet")) {
  1157. $petsList->add($actor);
  1158. } elsif (UNIVERSAL::isa($actor, "Actor::Portal")) {
  1159. $portalsList->add($actor);
  1160. } elsif (UNIVERSAL::isa($actor, "Actor::NPC")) {
  1161. my $ID = $args->{ID};
  1162. my $location = "$field{name} $actor->{pos}{x} $actor->{pos}{y}";
  1163. if ($npcs_lut{$location}) {
  1164. $actor->setName($npcs_lut{$location});
  1165. }
  1166. $npcsList->add($actor);
  1167. } elsif (UNIVERSAL::isa($actor, "Actor::Slave")) {
  1168. $slavesList->add($actor);
  1169. }
  1170. }
  1171. #### Packet specific ####
  1172. if ($args->{switch} eq "0078" ||
  1173. $args->{switch} eq "01D8" ||
  1174. $args->{switch} eq "022A" ||
  1175. $args->{switch} eq "02EE") {
  1176. # Actor Exists
  1177. if ($actor->isa('Actor::Player')) {
  1178. my $domain = existsInList($config{friendlyAID}, unpack("V1", $actor->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
  1179. debug "Player Exists: " . $actor->name . " ($actor->{binID}) Level $actor->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsFrom{x}, $coordsFrom{y})n", $domain;
  1180. Plugins::callHook('player', {player => $actor});  #backwards compatibailty
  1181. Plugins::callHook('player_exist', {player => $actor});
  1182. } elsif ($actor->isa('Actor::NPC')) {
  1183. 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}), "parseMsg_presence", 1;
  1184. } elsif ($actor->isa('Actor::Portal')) {
  1185. message TF("Portal Exists: %s (%s, %s) - (%s)n", $actor->name, $actor->{pos_to}{x}, $actor->{pos_to}{y}, $actor->{binID}), "portals", 1;
  1186. } elsif ($actor->isa('Actor::Monster')) {
  1187. debug sprintf("Monster Exists: %s (%d)n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  1188. } elsif ($actor->isa('Actor::Pet')) {
  1189. debug sprintf("Pet Exists: %s (%d)n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  1190. } elsif ($actor->isa('Actor::Slave')) {
  1191. debug sprintf("Slave Exists: %s (%d)n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  1192. } else {
  1193. debug sprintf("Unknown Actor Exists: %s (%d)n", $actor->name, $actor->{binID}), "parseMsg_presence", 1;
  1194. }
  1195. } elsif ($args->{switch} eq "0079" ||
  1196. $args->{switch} eq "01DB" ||
  1197. $args->{switch} eq "022B" ||
  1198. $args->{switch} eq "02ED" ||
  1199. $args->{switch} eq "01D9") {
  1200. # Actor Connected
  1201. if ($actor->isa('Actor::Player')) {
  1202. my $domain = existsInList($config{friendlyAID}, unpack("V1", $args->{ID})) ? 'parseMsg_presence' : 'parseMsg_presence/player';
  1203. debug "Player Connected: ".$actor->name." ($actor->{binID}) Level $args->{lv} $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}} ($coordsTo{x}, $coordsTo{y})n", $domain;
  1204. Plugins::callHook('player', {player => $actor});  #backwards compatibailty
  1205. Plugins::callHook('player_connected', {player => $actor});
  1206. } else {
  1207. debug "Unknown Connected: $args->{type} - ", "parseMsg";
  1208. }
  1209. } elsif ($args->{switch} eq "007B" ||
  1210. $args->{switch} eq "01DA" ||
  1211. $args->{switch} eq "022C" ||
  1212. $args->{switch} eq "02EC" ||
  1213. $args->{switch} eq "0086") {
  1214. # Actor Moved
  1215. # Correct the direction in which they're looking
  1216. my %vec;
  1217. getVector(%vec, %coordsTo, %coordsFrom);
  1218. my $direction = int sprintf("%.0f", (360 - vectorToDegree(%vec)) / 45);
  1219. $actor->{look}{body} = $direction;
  1220. $actor->{look}{head} = 0;
  1221. if ($actor->isa('Actor::Player')) {
  1222. 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";
  1223. } elsif ($actor->isa('Actor::Monster')) {
  1224. debug "Monster Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})n", "parseMsg";
  1225. } elsif ($actor->isa('Actor::Pet')) {
  1226. debug "Pet Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})n", "parseMsg";
  1227. } elsif ($actor->isa('Actor::Slave')) {
  1228. debug "Slave Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})n", "parseMsg";
  1229. } elsif ($actor->isa('Actor::Portal')) {
  1230. # This can never happen of course.
  1231. debug "Portal Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})n", "parseMsg";
  1232. } elsif ($actor->isa('Actor::NPC')) {
  1233. # Neither can this.
  1234. debug "Monster Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})n", "parseMsg";
  1235. } else {
  1236. debug "Unknown Actor Moved: " . $actor->nameIdx . " - ($coordsFrom{x}, $coordsFrom{y}) -> ($coordsTo{x}, $coordsTo{y})n", "parseMsg";
  1237. }
  1238. } elsif ($args->{switch} eq "007C") {
  1239. # Actor Spawned
  1240. if ($actor->isa('Actor::Player')) {
  1241. debug "Player Spawned: " . $actor->nameIdx . " $sex_lut{$actor->{sex}} $jobs_lut{$actor->{jobID}}n", "parseMsg";
  1242. } elsif ($actor->isa('Actor::Monster')) {
  1243. debug "Monster Spawned: " . $actor->nameIdx . "n", "parseMsg";
  1244. } elsif ($actor->isa('Actor::Slave')) {
  1245. debug "Slave Spawned: " . $actor->nameIdx . "n", "parseMsg";
  1246. } elsif ($actor->isa('NPC')) {
  1247. debug "NPC Spawned: " . $actor->nameIdx . "n", "parseMsg";
  1248. } else {
  1249. debug "Unknown Spawned: " . $actor->nameIdx . "n", "parseMsg";
  1250. }
  1251. }
  1252. }
  1253. sub actor_info {
  1254. my ($self, $args) = @_;
  1255. return unless changeToInGameState();
  1256. debug "Received object info: $args->{name}n", "parseMsg_presence/name", 2;
  1257. my $player = $playersList->getByID($args->{ID});
  1258. if ($player) {
  1259. # This packet tells us the names of players who aren't in a guild, as opposed to 0195.
  1260. $player->setName(bytesToString($args->{name}));
  1261. message "Player Info: " . $player->nameIdx . "n", "parseMsg_presence", 2;
  1262. updatePlayerNameCache($player);
  1263. Plugins::callHook('charNameUpdate', $player);
  1264. }
  1265. my $monster = $monstersList->getByID($args->{ID});
  1266. if ($monster) {
  1267. my $name = bytesToString($args->{name});
  1268. debug "Monster Info: $name ($monster->{binID})n", "parseMsg", 2;
  1269. $monster->{name_given} = $name;
  1270. if ($monsters_lut{$monster->{nameID}} eq "") {
  1271. $monster->setName($name);
  1272. $monsters_lut{$monster->{nameID}} = $name;
  1273. updateMonsterLUT(Settings::getTableFilename("monsters.txt"), $monster->{nameID}, $name);
  1274. }
  1275. }
  1276. my $npc = $npcs{$args->{ID}};
  1277. if ($npc) {
  1278. $npc->setName(bytesToString($args->{name}));
  1279. if ($config{debug} >= 2) {
  1280. my $binID = binFind(@npcsID, $args->{ID});
  1281. debug "NPC Info: $npc->{name} ($binID)n", "parseMsg", 2;
  1282. }
  1283. my $location = "$field{name} $npc->{pos}{x} $npc->{pos}{y}";
  1284. if (!$npcs_lut{$location}) {
  1285. $npcs_lut{$location} = $npc->{name};
  1286. updateNPCLUT(Settings::getTableFilename("npcs.txt"), $location, $npc->{name});
  1287. }
  1288. }
  1289. my $pet = $pets{$args->{ID}};
  1290. if ($pet) {
  1291. my $name = bytesToString($args->{name});
  1292. $pet->{name_given} = $name;
  1293. $pet->setName($name);
  1294. if ($config{debug} >= 2) {
  1295. my $binID = binFind(@petsID, $args->{ID});
  1296. debug "Pet Info: $pet->{name_given} ($binID)n", "parseMsg", 2;
  1297. }
  1298. }
  1299. my $slave = $slavesList->getByID($args->{ID});
  1300. if ($slave) {
  1301. my $name = bytesToString($args->{name});
  1302. #$slave->{name_given} = $name;
  1303. $slave->setName($name);
  1304. my $binID = binFind(@slavesID, $args->{ID});
  1305. debug "Slave Info: $name ($binID)n", "parseMsg_presence", 2;
  1306. updatePlayerNameCache($slave);
  1307. }
  1308. }
  1309. sub actor_look_at {
  1310. my ($self, $args) = @_;
  1311. return unless changeToInGameState();
  1312. my $actor = Actor::get($args->{ID});
  1313. $actor->{look}{head} = $args->{head};
  1314. $actor->{look}{body} = $args->{body};
  1315. debug $actor->nameString . " looks at $args->{body}, $args->{head}n", "parseMsg";
  1316. }
  1317. sub actor_movement_interrupted {
  1318. my ($self, $args) = @_;
  1319. return unless changeToInGameState();
  1320. my %coords;
  1321. $coords{x} = $args->{x};
  1322. $coords{y} = $args->{y};
  1323. my $actor = Actor::get($args->{ID});
  1324. $actor->{pos} = {%coords};
  1325. $actor->{pos_to} = {%coords};
  1326. if ($actor->isa('Actor::You') || $actor->isa('Actor::Player')) {
  1327. $actor->{sitting} = 0;
  1328. }
  1329. if ($actor->isa('Actor::You')) {
  1330. debug "Movement interrupted, your coordinates: $coords{x}, $coords{y}n", "parseMsg_move";
  1331. AI::clear("move");
  1332. }
  1333. if ($char->{homunculus} && $char->{homunculus}{ID} eq $actor->{ID}) {
  1334. AI::clear("move");
  1335. }
  1336. }
  1337. sub actor_muted {
  1338. my ($self, $args) = @_;
  1339. my $ID = $args->{ID};
  1340. my $duration = $args->{duration};
  1341. if ($duration > 0) {
  1342. $duration = 0xFFFFFFFF - $duration + 1;
  1343. message TF("%s is muted for %d minutesn", getActorName($ID), $duration), "parseMsg_statuslook", 2;
  1344. } else {
  1345. message TF("%s is no longer mutedn", getActorName($ID)), "parseMsg_statuslook", 2;
  1346. }
  1347. }
  1348. sub actor_name_received {
  1349. my ($self, $args) = @_;
  1350. # FIXME: There is more to this packet than just party name and guild name.
  1351. # This packet is received when you leave a guild
  1352. # (with cryptic party and guild name fields, at least for now)
  1353. my $player = $playersList->getByID($args->{ID});
  1354. if (defined $player) {
  1355. # Receive names of players who are in a guild.
  1356. $player->setName(bytesToString($args->{name}));
  1357. $player->{party}{name} = bytesToString($args->{partyName});
  1358. $player->{guild}{name} = bytesToString($args->{guildName});
  1359. $player->{guild}{title} = bytesToString($args->{guildTitle});
  1360. updatePlayerNameCache($player);
  1361. debug "Player Info: $player->{name} ($player->{binID})n", "parseMsg_presence", 2;
  1362. Plugins::callHook('charNameUpdate', $player);
  1363. } else {
  1364. debug "Player Info for " . unpack("V", $args->{ID}) .
  1365. " (not on screen): " . bytesToString($args->{name}) . "n",
  1366. "parseMsg_presence/remote", 2;
  1367. }
  1368. my $monster = $monstersList->getByID($args->{ID});
  1369. if ($monster) {
  1370. my $name = bytesToString($args->{name});
  1371. debug "Monster Info 2: $name ($monster->{binID})n", "parseMsg", 2;
  1372. $monster->{name_given} = $name;
  1373. if ($monsters_lut{$monster->{nameID}} eq "") {
  1374. $monster->setName($name);
  1375. $monsters_lut{$monster->{nameID}} = $name;
  1376. updateMonsterLUT(Settings::getTableFilename("monsters.txt"), $monster->{nameID}, $name);
  1377. }
  1378. }
  1379. }
  1380. sub actor_status_active {
  1381. my ($self, $args) = @_;
  1382. return unless changeToInGameState();
  1383. my ($type, $ID, $flag) = @{$args}{qw(type ID flag)};
  1384. my $tick = 0;
  1385. $tick = $args->{tick} if ($args->{switch} eq "043F");
  1386. my $skillName = (defined($skillsStatus{$type})) ? $skillsStatus{$type} : "Unknown $type";
  1387. $args->{skillName} = $skillName;
  1388. my $actor = Actor::get($ID);
  1389. $args->{actor} = $actor;
  1390. my ($name, $is) = getActorNames($ID, 0, 'are', 'is');
  1391. if ($flag) {
  1392. # Skill activated
  1393. my $again = 'now';
  1394. if ($actor) {
  1395. $again = 'again' if $actor->{statuses}{$skillName};
  1396. $actor->{statuses}{$skillName} = 1;
  1397. }
  1398. if ($char->{party}{users}{$ID}{name}) {
  1399. $again = 'again' if $char->{party}{users}{$ID}{statuses}{$skillName};
  1400. $char->{party}{users}{$ID}{statuses}{$skillName} = 1;
  1401. }
  1402. my $disp = status_string($actor, $skillName, $again);
  1403. if ($tick > 0) {
  1404. $disp = status_string($actor, $skillName, $again, $tick/1000);
  1405. };
  1406. message $disp, "parseMsg_statuslook", ($ID eq $accountID or $char->{slaves} && $char->{slaves}{$ID}) ? 1 : 2;
  1407. } else {
  1408. # Skill de-activated (expired)
  1409. delete $actor->{statuses}{$skillName} if $actor;
  1410. delete $char->{party}{users}{$ID}{statuses}{$skillName} if ($char->{party}{users}{$ID}{name});
  1411. my $disp = status_string($actor, $skillName, 'no longer');
  1412. message $disp, "parseMsg_statuslook", ($ID eq $accountID or $char->{slaves} && $char->{slaves}{$ID}) ? 1 : 2;
  1413. }
  1414. }
  1415. sub actor_trapped {
  1416. my ($self, $args) = @_;
  1417. # original comment was that ID is not a valid ID
  1418. # but it seems to be, at least on eAthena/Freya
  1419. my $actor = Actor::get($args->{ID});
  1420. debug "$actor is trapped.n";
  1421. }
  1422. sub area_spell {
  1423. my ($self, $args) = @_;
  1424. # Area effect spell; including traps!
  1425. my $ID = $args->{ID};
  1426. my $sourceID = $args->{sourceID};
  1427. my $x = $args->{x};
  1428. my $y = $args->{y};
  1429. my $type = $args->{type};
  1430. my $fail = $args->{fail};
  1431. # graffiti message, might only be for one of these switches
  1432. #my $message = unpack("Z80", substr($msg, 17, 80));
  1433. $spells{$ID}{'sourceID'} = $sourceID;
  1434. $spells{$ID}{'pos'}{'x'} = $x;
  1435. $spells{$ID}{'pos'}{'y'} = $y;
  1436. $spells{$ID}{'pos_to'}{'x'} = $x;
  1437. $spells{$ID}{'pos_to'}{'y'} = $y;
  1438. my $binID = binAdd(@spellsID, $ID);
  1439. $spells{$ID}{'binID'} = $binID;
  1440. $spells{$ID}{'type'} = $type;
  1441. if ($type == 0x81) {
  1442. message TF("%s opened Warp Portal on (%d, %d)n", getActorName($sourceID), $x, $y), "skill";
  1443. }
  1444. debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)n", "skill", 2;
  1445. Plugins::callHook('packet_areaSpell', {
  1446. fail => $fail,
  1447. sourceID => $sourceID,
  1448. type => $type,
  1449. x => $x,
  1450. y => $y
  1451. });
  1452. }
  1453. sub area_spell_disappears {
  1454. my ($self, $args) = @_;
  1455. # The area effect spell with ID dissappears
  1456. my $ID = $args->{ID};
  1457. my $spell = $spells{$ID};
  1458. debug "Area effect ".getSpellName($spell->{type})." ($spell->{binID}) from ".getActorName($spell->{sourceID})." disappeared from ($spell->{pos}{x}, $spell->{pos}{y})n", "skill", 2;
  1459. delete $spells{$ID};
  1460. binRemove(@spellsID, $ID);
  1461. }
  1462. sub arrow_equipped {
  1463. my ($self, $args) = @_;
  1464. return unless changeToInGameState();
  1465. return unless $args->{index};
  1466. $char->{arrow} = $args->{index};
  1467. my $item = $char->inventory->getByServerIndex($args->{index});
  1468. if ($item && $char->{equipment}{arrow} != $item) {
  1469. $char->{equipment}{arrow} = $item;
  1470. $item->{equipped} = 32768;
  1471. $ai_v{temp}{waitForEquip}-- if $ai_v{temp}{waitForEquip};
  1472. message TF("Arrow/Bullet equipped: %s (%d)n", $item->{name}, $item->{invIndex});
  1473. }
  1474. }
  1475. sub arrow_none {
  1476. my ($self, $args) = @_;
  1477. my $type = $args->{type};
  1478. if ($type == 0) {
  1479. delete $char->{'arrow'};
  1480. if ($config{'dcOnEmptyArrow'}) {
  1481. $interface->errorDialog(T("Please equip arrow first."));
  1482. quit();
  1483. } else {
  1484. error T("Please equip arrow first.n");
  1485. }
  1486. } elsif ($type == 3) {
  1487. debug "Arrow equippedn";
  1488. }
  1489. }
  1490. sub arrowcraft_list {
  1491. my ($self, $args) = @_;
  1492. my $newmsg;
  1493. my $msg = $args->{RAW_MSG};
  1494. my $msg_size = $args->{RAW_MSG_SIZE};
  1495. $self->decrypt($newmsg, substr($msg, 4));
  1496. $msg = substr($msg, 0, 4).$newmsg;
  1497. undef @arrowCraftID;
  1498. for (my $i = 4; $i < $msg_size; $i += 2) {
  1499. my $ID = unpack("v1", substr($msg, $i, 2));
  1500. my $item = $char->inventory->getByNameID($ID);
  1501. binAdd(@arrowCraftID, $item->{invIndex});
  1502. }
  1503. message T("Received Possible Arrow Craft List - type 'arrowcraft'n");
  1504. }
  1505. sub attack_range {
  1506. my ($self, $args) = @_;
  1507. my $type = $args->{type};
  1508. debug "Your attack range is: $typen";
  1509. return unless changeToInGameState();
  1510. $char->{attack_range} = $type;
  1511. if ($config{attackDistanceAuto} && $config{attackDistance} != $type) {
  1512. message TF("Autodetected attackDistance = %sn", $type), "success";
  1513. configModify('attackDistance', $type, 1);
  1514. configModify('attackMaxDistance', $type, 1);
  1515. }
  1516. }
  1517. sub buy_result {
  1518. my ($self, $args) = @_;
  1519. if ($args->{fail} == 0) {
  1520. message T("Buy completed.n"), "success";
  1521. } elsif ($args->{fail} == 1) {
  1522. error T("Buy failed (insufficient zeny).n");
  1523. } elsif ($args->{fail} == 2) {
  1524. error T("Buy failed (insufficient weight capacity).n");
  1525. } elsif ($args->{fail} == 3) {
  1526. error T("Buy failed (too many different inventory items).n");
  1527. } else {
  1528. error TF("Buy failed (failure code %s).n", $args->{fail});
  1529. }
  1530. }
  1531. sub card_merge_list {
  1532. my ($self, $args) = @_;
  1533. # You just requested a list of possible items to merge a card into
  1534. # The RO client does this when you double click a card
  1535. my $newmsg;
  1536. my $msg = $args->{RAW_MSG};
  1537. $self->decrypt($newmsg, substr($msg, 4));
  1538. $msg = substr($msg, 0, 4).$newmsg;
  1539. my ($len) = unpack("x2 v1", $msg);
  1540. my $display;
  1541. $display .= T("-----Card Merge Candidates-----n");
  1542. my $index;
  1543. for (my $i = 4; $i < $len; $i += 2) {
  1544. $index = unpack("v1", substr($msg, $i, 2));
  1545. my $item = $char->inventory->getByServerIndex($index);
  1546. binAdd(@cardMergeItemsID, $item->{invIndex});
  1547. $display .= "$item->{invIndex} $item->{name}n";
  1548. }
  1549. $display .= "-------------------------------n";
  1550. message $display, "list";
  1551. }
  1552. sub card_merge_status {
  1553. my ($self, $args) = @_;
  1554. # something about successful compound?
  1555. my $item_index = $args->{item_index};
  1556. my $card_index = $args->{card_index};
  1557. my $fail = $args->{fail};
  1558. if ($fail) {
  1559. message T("Card merging failedn");
  1560. } else {
  1561. my $item = $char->inventory->getByServerIndex($item_index);
  1562. my $card = $char->inventory->getByServerIndex($card_index);
  1563. message TF("%s has been successfully merged into %sn",
  1564. $card->{name}, $item->{name}), "success";
  1565. # Remove one of the card
  1566. $card->{amount} -= 1;
  1567. if ($card->{amount} <= 0) {
  1568. $char->inventory->remove($card);
  1569. }
  1570. # Rename the slotted item now
  1571. # FIXME: this is unoptimized
  1572. use bytes;
  1573. no encoding 'utf8';
  1574. my $newcards = '';
  1575. my $addedcard;
  1576. for (my $i = 0; $i < 4; $i++) {
  1577. my $cardData = substr($item->{cards}, $i * 2, 2);
  1578. if (unpack("v", $cardData)) {
  1579. $newcards .= $cardData;
  1580. } elsif (!$addedcard) {
  1581. $newcards .= pack("v", $card->{nameID});
  1582. $addedcard = 1;
  1583. } else {
  1584. $newcards .= pack("v", 0);
  1585. }
  1586. }
  1587. $item->{cards} = $newcards;
  1588. $item->setName(itemName($item));
  1589. }
  1590. undef @cardMergeItemsID;
  1591. undef $cardMergeIndex;
  1592. }
  1593. sub cart_info {
  1594. my ($self, $args) = @_;
  1595. $cart{items} = $args->{items};
  1596. $cart{items_max} = $args->{items_max};
  1597. $cart{weight} = int($args->{weight} / 10);
  1598. $cart{weight_max} = int($args->{weight_max} / 10);
  1599. $cart{exists} = 1;
  1600. debug "[cart_info] received.n", "parseMsg";
  1601. }
  1602. sub cart_add_failed {
  1603. my ($self, $args) = @_;
  1604. my $reason;
  1605. if ($args->{fail} == 0) {
  1606. $reason = 'overweight';
  1607. } elsif ($args->{fail} == 1) {
  1608. $reason = 'too many items';
  1609. } else {
  1610. $reason = "Unknown code $args->{fail}";
  1611. }
  1612. error TF("Can't Add Cart Item (%s)n", $reason);
  1613. }
  1614. sub cart_equip_list {
  1615. my ($self, $args) = @_;
  1616. # "0122" sends non-stackable item info
  1617. # "0123" sends stackable item info
  1618. my ($newmsg, $psize);
  1619. my $msg = $args->{RAW_MSG};
  1620. my $msg_size = $args->{RAW_MSG_SIZE};
  1621. $self->decrypt($newmsg, substr($msg, 4));
  1622. $msg = substr($msg, 0, 4).$newmsg;
  1623. if ($args->{switch} eq '0297') {
  1624.    $psize = 24;
  1625. } elsif ($args->{switch} eq '02D2') {
  1626.    $psize = 26;
  1627. } else {
  1628.    $psize = 20;
  1629. }
  1630. for (my $i = 4; $i < $msg_size; $i += $psize) {
  1631. my $index = unpack("v1", substr($msg, $i, 2));
  1632. my $ID = unpack("v1", substr($msg, $i+2, 2));
  1633. my $type = unpack("C1",substr($msg, $i+4, 1));
  1634. my $item = $cart{inventory}[$index] = {};
  1635. $item->{nameID} = $ID;
  1636. $item->{amount} = 1;
  1637. $item->{index} = $index;
  1638. $item->{identified} = unpack("C1", substr($msg, $i+5, 1));
  1639. $item->{type_equip} = unpack("v1", substr($msg, $i+6, 2));
  1640. $item->{broken} = unpack("C1", substr($msg, $i+10, 1));
  1641. $item->{upgrade} = unpack("C1", substr($msg, $i+11, 1));
  1642. $item->{cards} = ($args->{switch} eq '0297') ? substr($msg, $i+12, 12) : substr($msg, $i+12, 8);
  1643. $item->{name} = itemName($item);
  1644. debug "Non-Stackable Cart Item: $item->{name} ($index) x 1n", "parseMsg";
  1645. Plugins::callHook('packet_cart', {index => $index});
  1646. }
  1647. $ai_v{'inventory_time'} = time + 1;
  1648. $ai_v{'cart_time'} = time + 1;
  1649. }
  1650. sub cart_item_added {
  1651. my ($self, $args) = @_;
  1652. my $item = $cart{inventory}[$args->{index}] ||= {};
  1653. if ($item->{amount}) {
  1654. $item->{amount} += $args->{amount};
  1655. } else {
  1656. $item->{index} = $args->{index};
  1657. $item->{nameID} = $args->{ID};
  1658. $item->{amount} = $args->{amount};
  1659. $item->{identified} = $args->{identified};
  1660. $item->{broken} = $args->{broken};
  1661. $item->{upgrade} = $args->{upgrade};
  1662. $item->{cards} = $args->{cards};
  1663. $item->{name} = itemName($item);
  1664. }
  1665. message TF("Cart Item Added: %s (%d) x %sn", $item->{name}, $args->{index}, $args->{amount});
  1666. $itemChange{$item->{name}} += $args->{amount};
  1667. $args->{item} = $item;
  1668. }
  1669. sub cart_items_list {
  1670. my ($self, $args) = @_;
  1671. my ($newmsg, $psize);
  1672. my $msg = $args->{RAW_MSG};
  1673. my $msg_size = $args->{RAW_MSG_SIZE};
  1674. my $switch = $args->{switch};
  1675. $self->decrypt($newmsg, substr($msg, 4));
  1676. $msg = substr($msg, 0, 4).$newmsg;
  1677. if ($switch eq '0123') {
  1678.    $psize = 10;
  1679. } elsif ($switch eq '02E9') {
  1680.    $psize = 22;
  1681. } else {
  1682.    $psize = 18;
  1683. }
  1684. for (my $i = 4; $i < $msg_size; $i += $psize) {
  1685. my $index = unpack("v1", substr($msg, $i, 2));
  1686. my $ID = unpack("v1", substr($msg, $i+2, 2));
  1687. my $amount = unpack("v1", substr($msg, $i+6, 2));
  1688. my $item = $cart{inventory}[$index] ||= {};
  1689. if ($item->{amount}) {
  1690. $item->{amount} += $amount;
  1691. } else {
  1692. $item->{index} = $index;
  1693. $item->{nameID} = $ID;
  1694. $item->{amount} = $amount;
  1695. $item->{cards} = substr($msg, $i + 10, 8) if ($psize == 18);
  1696. $item->{name} = itemName($item);
  1697. $item->{identified} = 1;
  1698. }
  1699. debug "Stackable Cart Item: $item->{name} ($index) x $amountn", "parseMsg";
  1700. Plugins::callHook('packet_cart', {index => $index});
  1701. }
  1702. $ai_v{'inventory_time'} = time + 1;
  1703. $ai_v{'cart_time'} = time + 1;
  1704. }
  1705. sub cash_dealer {
  1706. my ($self, $args) = @_;
  1707. undef @cashList;
  1708. my $cashList = 0;
  1709. $char->{cashpoint} = unpack("x4 V", $args->{RAW_MSG});
  1710. for (my $i = 8; $i < $args->{RAW_MSG_SIZE}; $i += 11) {
  1711. my ($price, $dcprice, $type, $ID) = unpack("V2 C v", substr($args->{RAW_MSG}, $i, 11));
  1712. my $store = $cashList[$cashList] = {};
  1713. my $display = ($items_lut{$ID} ne "") ? $items_lut{$ID} : "Unknown $ID";
  1714. $store->{name} = $display;
  1715. $store->{nameID} = $ID;
  1716. $store->{type} = $type;
  1717. $store->{price} = $dcprice;
  1718. $cashList++;
  1719. }
  1720. $ai_v{npc_talk}{talk} = 'cash';
  1721. # continue talk sequence now
  1722. $ai_v{npc_talk}{time} = time;
  1723. message TF("-----------CashList (Cash Point: %-5d)------------n" .
  1724. "#  Name                    Type               Pricen", $char->{cashpoint}), "list";
  1725. my $display;
  1726. for (my $i = 0; $i < @cashList; $i++) {
  1727. $display = $cashList[$i]{name};
  1728. message(swrite(
  1729. "@< @<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<< @>>>>>>>p",
  1730. [$i, $display, $itemTypes_lut{$cashList[$i]{type}}, $cashList[$i]{price}]),
  1731. "list");
  1732. }
  1733. message("---------------------------------------------------n", "list");
  1734. }
  1735. sub combo_delay {
  1736. my ($self, $args) = @_;
  1737. $char->{combo_packet} = ($args->{delay}); #* 15) / 100000;
  1738. # How was the above formula derived? I think it's better that the manipulation be
  1739. # done in functions.pl (or whatever sub that handles this) instead of here.
  1740. $args->{actor} = Actor::get($args->{ID});
  1741. my $verb = $args->{actor}->verb('have', 'has');
  1742. debug "$args->{actor} $verb combo delay $args->{delay}n", "parseMsg_comboDelay";
  1743. }
  1744. sub cart_item_removed {
  1745. my ($self, $args) = @_;
  1746. my ($index, $amount) = @{$args}{qw(index amount)};
  1747. my $item = $cart{inventory}[$index];
  1748. $item->{amount} -= $amount;
  1749. message TF("Cart Item Removed: %s (%d) x %sn", $item->{name}, $index, $amount);
  1750. $itemChange{$item->{name}} -= $amount;
  1751. if ($item->{amount} <= 0) {
  1752. $cart{'inventory'}[$index] = undef;
  1753. }
  1754. $args->{item} = $item;
  1755. }
  1756. sub change_to_constate25 {
  1757. $net->setState(2.5);
  1758. undef $accountID;
  1759. }
  1760. sub changeToInGameState {
  1761. if ($net->version() == 1) {
  1762. if ($accountID && UNIVERSAL::isa($char, 'Actor::You')) {
  1763. if ($net->getState() != Network::IN_GAME) {
  1764. $net->setState(Network::IN_GAME);
  1765. }
  1766. return 1;
  1767. } else {
  1768. if ($net->getState() != Network::IN_GAME_BUT_UNINITIALIZED) {
  1769. $net->setState(Network::IN_GAME_BUT_UNINITIALIZED);
  1770. if ($config{verbose} && $messageSender && !$sentWelcomeMessage) {
  1771. $messageSender->injectAdminMessage("Please relogin to enable X-${Settings::NAME}.");
  1772. $sentWelcomeMessage = 1;
  1773. }
  1774. }
  1775. return 0;
  1776. }
  1777. } else {
  1778. return 1;
  1779. }
  1780. }
  1781. sub character_creation_failed {
  1782. message T("Character creation failed. " .
  1783. "If you didn't make any mistake, then the name you chose already exists.n"), "info";
  1784. if (charSelectScreen() == 1) {
  1785. $net->setState(3);
  1786. $firstLoginMap = 1;
  1787. $startingZenny = $chars[$config{'char'}]{'zenny'} unless defined $startingZenny;
  1788. $sentWelcomeMessage = 1;
  1789. }
  1790. }
  1791. sub character_creation_successful {
  1792. my ($self, $args) = @_;
  1793. my $char = new Actor::You;
  1794. $char->{ID} = $args->{ID};
  1795. $char->{name} = bytesToString($args->{name});
  1796. $char->{zenny} = $args->{zenny};
  1797. $char->{jobID} = 0;
  1798. $char->{str} = $args->{str};
  1799. $char->{agi} = $args->{agi};
  1800. $char->{vit} = $args->{vit};
  1801. $char->{int} = $args->{int};
  1802. $char->{dex} = $args->{dex};
  1803. $char->{luk} = $args->{luk};
  1804. my $slot = $args->{slot};
  1805. $char->{lv} = 1;
  1806. $char->{lv_job} = 1;
  1807. $char->{sex} = $accountSex2;
  1808. $chars[$slot] = $char;
  1809. $net->setState(3);
  1810. message TF("Character %s (%d) created.n", $char->{name}, $slot), "info";
  1811. if (charSelectScreen() == 1) {
  1812. $firstLoginMap = 1;
  1813. $startingZenny = $chars[$config{'char'}]{'zenny'} unless defined $startingZenny;
  1814. $sentWelcomeMessage = 1;
  1815. }
  1816. }
  1817. sub character_deletion_successful {
  1818. if (defined $AI::temp::delIndex) {
  1819. message TF("Character %s (%d) deleted.n", $chars[$AI::temp::delIndex]{name}, $AI::temp::delIndex), "info";
  1820. delete $chars[$AI::temp::delIndex];
  1821. undef $AI::temp::delIndex;
  1822. for (my $i = 0; $i < @chars; $i++) {
  1823. delete $chars[$i] if ($chars[$i] && !scalar(keys %{$chars[$i]}))
  1824. }
  1825. } else {
  1826. message T("Character deleted.n"), "info";
  1827. }
  1828. if (charSelectScreen() == 1) {
  1829. $net->setState(3);
  1830. $firstLoginMap = 1;
  1831. $startingZenny = $chars[$config{'char'}]{'zenny'} unless defined $startingZenny;
  1832. $sentWelcomeMessage = 1;
  1833. }
  1834. }
  1835. sub character_deletion_failed {
  1836. error T("Character cannot be deleted. Your e-mail address was probably wrong.n");
  1837. undef $AI::temp::delIndex;
  1838. if (charSelectScreen() == 1) {
  1839. $net->setState(3);
  1840. $firstLoginMap = 1;
  1841. $startingZenny = $chars[$config{'char'}]{'zenny'} unless defined $startingZenny;
  1842. $sentWelcomeMessage = 1;
  1843. }
  1844. }
  1845. sub character_moves {
  1846. my ($self, $args) = @_;
  1847. return unless changeToInGameState();
  1848. makeCoords($char->{pos}, substr($args->{RAW_MSG}, 6, 3));
  1849. makeCoords2($char->{pos_to}, substr($args->{RAW_MSG}, 8, 3));
  1850. my $dist = sprintf("%.1f", distance($char->{pos}, $char->{pos_to}));
  1851. debug "You're moving from ($char->{pos}{x}, $char->{pos}{y}) to ($char->{pos_to}{x}, $char->{pos_to}{y}) - distance $dist, unknown $args->{unknown}n", "parseMsg_move";
  1852. $char->{time_move} = time;
  1853. $char->{time_move_calc} = distance($char->{pos}, $char->{pos_to}) * ($char->{walk_speed} || 0.12);
  1854. # Correct the direction in which we're looking
  1855. my (%vec, $degree);
  1856. getVector(%vec, $char->{pos_to}, $char->{pos});
  1857. $degree = vectorToDegree(%vec);
  1858. if (defined $degree) {
  1859. my $direction = int sprintf("%.0f", (360 - $degree) / 45);
  1860. $char->{look}{body} = $direction & 0x07;
  1861. $char->{look}{head} = 0;
  1862. }
  1863. # Ugly; AI code in network subsystem! This must be fixed.
  1864. if (AI::action eq "mapRoute" && $config{route_escape_reachedNoPortal} && $dist eq "0.0"){
  1865.    if (!$portalsID[0]) {
  1866. if ($config{route_escape_shout} ne "" && !defined($timeout{ai_route_escape}{time})){
  1867. sendMessage("c", $config{route_escape_shout});
  1868. }
  1869.        $timeout{ai_route_escape}{time} = time;
  1870.      AI::queue("escape");
  1871.    }
  1872. }
  1873. }
  1874. sub character_name {
  1875. my ($self, $args) = @_;
  1876. my $name; # Type: String
  1877. $name = bytesToString($args->{name});
  1878. debug "Character name received: $namen";
  1879. }
  1880. sub character_status {
  1881. my ($self, $args) = @_;
  1882. if ($args->{ID} eq $accountID) {
  1883. $char->{param1} = $args->{param1};
  1884. $char->{param2} = $args->{param2};
  1885. $char->{param3} = $args->{param3};
  1886. }
  1887. setStatus(Actor::get($args->{ID}), $args->{param1}, $args->{param2}, $args->{param3});
  1888. }
  1889. sub chat_created {
  1890. my ($self, $args) = @_;
  1891. $currentChatRoom = $accountID;
  1892. $chatRooms{$accountID} = {%createdChatRoom};
  1893. binAdd(@chatRoomsID, $accountID);
  1894. binAdd(@currentChatRoomUsers, $char->{name});
  1895. message T("Chat Room Createdn");
  1896. }
  1897. sub chat_info {
  1898. my ($self, $args) = @_;
  1899. my $title;
  1900. $self->decrypt($title, $args->{title});
  1901. $title = bytesToString($title);
  1902. my $chat = $chatRooms{$args->{ID}};
  1903. if (!$chat || !%{$chat}) {
  1904. $chat = $chatRooms{$args->{ID}} = {};
  1905. binAdd(@chatRoomsID, $args->{ID});
  1906. }
  1907. $chat->{title} = $title;
  1908. $chat->{ownerID} = $args->{ownerID};
  1909. $chat->{limit} = $args->{limit};
  1910. $chat->{public} = $args->{public};
  1911. $chat->{num_users} = $args->{num_users};
  1912. }
  1913. sub chat_join_result {
  1914. my ($self, $args) = @_;
  1915. if ($args->{type} == 1) {
  1916. message T("Can't join Chat Room - Incorrect Passwordn");
  1917. } elsif ($args->{type} == 2) {
  1918. message T("Can't join Chat Room - You're bannedn");
  1919. }
  1920. }
  1921. sub chat_modified {
  1922. my ($self, $args) = @_;
  1923. my $title;
  1924. $self->decrypt($title, $args->{title});
  1925. $title = bytesToString($title);
  1926. my ($ownerID, $ID, $limit, $public, $num_users) = @{$args}{qw(ownerID ID limit public num_users)};
  1927. if ($ownerID eq $accountID) {
  1928. $chatRooms{new}{title} = $title;
  1929. $chatRooms{new}{ownerID} = $ownerID;
  1930. $chatRooms{new}{limit} = $limit;
  1931. $chatRooms{new}{public} = $public;
  1932. $chatRooms{new}{num_users} = $num_users;
  1933. } else {
  1934. $chatRooms{$ID}{title} = $title;
  1935. $chatRooms{$ID}{ownerID} = $ownerID;
  1936. $chatRooms{$ID}{limit} = $limit;
  1937. $chatRooms{$ID}{public} = $public;
  1938. $chatRooms{$ID}{num_users} = $num_users;
  1939. }
  1940. message T("Chat Room Properties Modifiedn");
  1941. }
  1942. sub chat_newowner {
  1943. my ($self, $args) = @_;
  1944. my $user = bytesToString($args->{user});
  1945. if ($args->{type} == 0) {
  1946. if ($user eq $char->{name}) {
  1947. $chatRooms{$currentChatRoom}{ownerID} = $accountID;
  1948. } else {
  1949. my $players = $playersList->getItems();
  1950. my $player;
  1951. foreach my $p (@{$players}) {
  1952. if ($p->{name} eq $user) {
  1953. $player = $p;
  1954. last;
  1955. }
  1956. }
  1957. if ($player) {
  1958. my $key = $player->{ID};
  1959. $chatRooms{$currentChatRoom}{ownerID} = $key;
  1960. }
  1961. }
  1962. $chatRooms{$currentChatRoom}{users}{$user} = 2;
  1963. } else {
  1964. $chatRooms{$currentChatRoom}{users}{$user} = 1;
  1965. }
  1966. }
  1967. sub chat_user_join {
  1968. my ($self, $args) = @_;
  1969. my $user = bytesToString($args->{user});
  1970. if ($currentChatRoom ne "") {
  1971. binAdd(@currentChatRoomUsers, $user);
  1972. $chatRooms{$currentChatRoom}{users}{$user} = 1;
  1973. $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
  1974. message TF("%s has joined the Chat Roomn", $user);
  1975. }
  1976. }
  1977. sub chat_user_leave {
  1978. my ($self, $args) = @_;
  1979. my $user = bytesToString($args->{user});
  1980. delete $chatRooms{$currentChatRoom}{users}{$user};
  1981. binRemove(@currentChatRoomUsers, $user);
  1982. $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
  1983. if ($user eq $char->{name}) {
  1984. binRemove(@chatRoomsID, $currentChatRoom);
  1985. delete $chatRooms{$currentChatRoom};
  1986. undef @currentChatRoomUsers;
  1987. $currentChatRoom = "";
  1988. message T("You left the Chat Roomn");
  1989. } else {
  1990. message TF("%s has left the Chat Roomn", $user);
  1991. }
  1992. }
  1993. sub chat_users {
  1994. my ($self, $args) = @_;
  1995. my $newmsg;
  1996. $self->decrypt($newmsg, substr($args->{RAW_MSG}, 8));
  1997. my $msg = substr($args->{RAW_MSG}, 0, 8).$newmsg;
  1998. my $ID = substr($args->{RAW_MSG},4,4);
  1999. $currentChatRoom = $ID;
  2000. my $chat = $chatRooms{$currentChatRoom} ||= {};
  2001. $chat->{num_users} = 0;
  2002. for (my $i = 8; $i < $args->{RAW_MSG_SIZE}; $i += 28) {
  2003. my $type = unpack("C1",substr($msg,$i,1));
  2004. my ($chatUser) = unpack("Z*", substr($msg,$i + 4,24));
  2005. $chatUser = bytesToString($chatUser);
  2006. if ($chat->{users}{$chatUser} eq "") {
  2007. binAdd(@currentChatRoomUsers, $chatUser);
  2008. if ($type == 0) {
  2009. $chat->{users}{$chatUser} = 2;
  2010. } else {
  2011. $chat->{users}{$chatUser} = 1;
  2012. }
  2013. $chat->{num_users}++;
  2014. }
  2015. }
  2016. message TF("You have joined the Chat Room %sn", $chat->{title});
  2017. }
  2018. sub cast_cancelled {
  2019. my ($self, $args) = @_;
  2020. # Cast is cancelled
  2021. my $ID = $args->{ID};
  2022. my $source = Actor::get($ID);
  2023. $source->{cast_cancelled} = time;
  2024. my $skill = $source->{casting}->{skill};
  2025. my $skillName = $skill ? $skill->getName() : 'Unknown';
  2026. my $domain = ($ID eq $accountID) ? "selfSkill" : "skill";
  2027. message TF("%s failed to cast %sn", $source, $skillName), $domain;
  2028. Plugins::callHook('packet_castCancelled', {
  2029. sourceID => $ID
  2030. });
  2031. delete $source->{casting};
  2032. }
  2033. sub chat_removed {
  2034. my ($self, $args) = @_;
  2035. binRemove(@chatRoomsID, $args->{ID});
  2036. delete $chatRooms{ $args->{ID} };
  2037. }
  2038. sub deal_add_other {
  2039. my ($self, $args) = @_;
  2040. if ($args->{nameID} > 0) {
  2041. my $item = $currentDeal{other}{ $args->{nameID} } ||= {};
  2042. $item->{amount} += $args->{amount};
  2043. $item->{nameID} = $args->{nameID};
  2044. $item->{identified} = $args->{identified};
  2045. $item->{broken} = $args->{broken};
  2046. $item->{upgrade} = $args->{upgrade};
  2047. $item->{cards} = $args->{cards};
  2048. $item->{name} = itemName($item);
  2049. message TF("%s added Item to Deal: %s x %sn", $currentDeal{name}, $item->{name}, $args->{amount}), "deal";
  2050. } elsif ($args->{amount} > 0) {
  2051. $currentDeal{other_zenny} += $args->{amount};
  2052. my $amount = formatNumber($args->{amount});
  2053. message TF("%s added %s z to Dealn", $currentDeal{name}, $amount), "deal";
  2054. }
  2055. }
  2056. sub deal_add_you {
  2057. my ($self, $args) = @_;
  2058. if ($args->{fail} == 1) {
  2059. error T("That person is overweight; you cannot trade.n"), "deal";
  2060. return;
  2061. } elsif ($args->{fail} == 2) {
  2062. error T("This item cannot be traded.n"), "deal";
  2063. return;
  2064. } elsif ($args->{fail}) {
  2065. error TF("You cannot trade (fail code %s).n", $args->{fail}), "deal";
  2066. return;
  2067. }
  2068. return unless $args->{index} > 0;
  2069. my $item = $char->inventory->getByServerIndex($args->{index});
  2070. $currentDeal{you}{$item->{nameID}}{amount} += $currentDeal{lastItemAmount};
  2071. $item->{amount} -= $currentDeal{lastItemAmount};
  2072. message TF("You added Item to Deal: %s x %sn", $item->{name}, $currentDeal{lastItemAmount}), "deal";
  2073. $itemChange{$item->{name}} -= $currentDeal{lastItemAmount};
  2074. $currentDeal{you_items}++;
  2075. $args->{item} = $item;
  2076. $char->inventory->remove($item) if ($item->{amount} <= 0);
  2077. }
  2078. sub deal_begin {
  2079. my ($self, $args) = @_;
  2080. if ($args->{type} == 0) {
  2081. error T("That person is too far from you to trade.n");
  2082. } elsif ($args->{type} == 2) {
  2083. error T("That person is in another deal.n");
  2084. } elsif ($args->{type} == 3) {
  2085. if (%incomingDeal) {
  2086. $currentDeal{name} = $incomingDeal{name};
  2087. undef %incomingDeal;
  2088. } else {
  2089. my $ID = $outgoingDeal{ID};
  2090. my $player;
  2091. $player = $playersList->getByID($ID) if (defined $ID);
  2092. $currentDeal{ID} = $ID;
  2093. if ($player) {
  2094. $currentDeal{name} = $player->{name};
  2095. } else {
  2096. $currentDeal{name} = 'Unknown #' . unpack("V", $ID);
  2097. }
  2098. undef %outgoingDeal;
  2099. }
  2100. message TF("Engaged Deal with %sn", $currentDeal{name}), "deal";
  2101. } else {
  2102. error TF("Deal request failed (unknown error %s).n", $args->{type});
  2103. }
  2104. }
  2105. sub deal_cancelled {
  2106. undef %incomingDeal;
  2107. undef %outgoingDeal;
  2108. undef %currentDeal;
  2109. message T("Deal Cancelledn"), "deal";
  2110. }
  2111. sub deal_complete {
  2112. undef %outgoingDeal;
  2113. undef %incomingDeal;
  2114. undef %currentDeal;
  2115. message T("Deal Completen"), "deal";
  2116. }
  2117. sub deal_finalize {
  2118. my ($self, $args) = @_;
  2119. if ($args->{type} == 1) {
  2120. $currentDeal{other_finalize} = 1;
  2121. message TF("%s finalized the Dealn", $currentDeal{name}), "deal";
  2122. } else {
  2123. $currentDeal{you_finalize} = 1;
  2124. # FIXME: shouldn't we do this when we actually complete the deal?
  2125. $char->{zenny} -= $currentDeal{you_zenny};
  2126. message T("You finalized the Dealn"), "deal";
  2127. }
  2128. }
  2129. sub deal_request {
  2130. my ($self, $args) = @_;
  2131. my $level = $args->{level} || 'Unknown';
  2132. my $user = bytesToString($args->{user});
  2133. $incomingDeal{name} = $user;
  2134. $timeout{ai_dealAutoCancel}{time} = time;
  2135. message TF("%s (level %s) Requests a Dealn", $user, $level), "deal";
  2136. message T("Type 'deal' to start dealing, or 'deal no' to deny the deal.n"), "deal";
  2137. }
  2138. sub devotion {
  2139. my ($self, $args) = @_;
  2140. my $source = Actor::get($args->{sourceID});
  2141. my $msg = '';
  2142. for (my $i = 0; $i < 5; $i++) {
  2143. my $ID = substr($args->{data}, $i*4, 4);
  2144. last if unpack("V", $ID) == 0;
  2145. my $actor = Actor::get($ID);
  2146. $msg .= skillUseNoDamage_string($source, $actor, 0, 'devotion');
  2147. }
  2148. message "$msg";
  2149. }
  2150. sub egg_list {
  2151. my ($self, $args) = @_;
  2152. message T("----- Egg Hatch Candidates -----n"), "list";
  2153. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 2) {
  2154. my $index = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
  2155. my $item = $char->inventory->getByServerIndex($index);
  2156. message "$item->{invIndex} $item->{name}n", "list";
  2157. }
  2158. message "------------------------------n", "list";
  2159. }
  2160. sub emoticon {
  2161. my ($self, $args) = @_;
  2162. my $emotion = $emotions_lut{$args->{type}}{display} || "<emotion #$args->{type}>";
  2163. if ($args->{ID} eq $accountID) {
  2164. message "$char->{name}: $emotionn", "emotion";
  2165. chatLog("e", "$char->{name}: $emotionn") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  2166. } elsif (my $player = $playersList->getByID($args->{ID})) {
  2167. my $name = $player->name;
  2168. #my $dist = "unknown";
  2169. my $dist = distance($char->{pos_to}, $player->{pos_to});
  2170. $dist = sprintf("%.1f", $dist) if ($dist =~ /./);
  2171. # Translation Comment: "[dist=$dist] $name ($player->{binID}): $emotionn"
  2172. message TF("[dist=%s] %s (%d): %sn", $dist, $name, $player->{binID}, $emotion), "emotion";
  2173. chatLog("e", "$name".": $emotionn") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  2174. my $index = AI::findAction("follow");
  2175. if ($index ne "") {
  2176. my $masterID = AI::args($index)->{ID};
  2177. if ($config{'followEmotion'} && $masterID eq $args->{ID} &&
  2178.        distance($char->{pos_to}, $player->{pos_to}) <= $config{'followEmotion_distance'})
  2179. {
  2180. my %args = ();
  2181. $args{timeout} = time + rand (1) + 0.75;
  2182. if ($args->{type} == 30) {
  2183. $args{emotion} = 31;
  2184. } elsif ($args->{type} == 31) {
  2185. $args{emotion} = 30;
  2186. } else {
  2187. $args{emotion} = $args->{type};
  2188. }
  2189. AI::queue("sendEmotion", %args);
  2190. }
  2191. }
  2192. } elsif (my $monster = $monstersList->getByID($args->{ID}) || $slavesList->getByID($args->{ID})) {
  2193. my $dist = distance($char->{pos_to}, $monster->{pos_to});
  2194. $dist = sprintf("%.1f", $dist) if ($dist =~ /./);
  2195. # Translation Comment: "[dist=$dist] $monster->name ($monster->{binID}): $emotionn"
  2196. message TF("[dist=%s] %s %s (%d): %sn", $dist, $monster->{actorType}, $monster->name, $monster->{binID}, $emotion), "emotion";
  2197. } else {
  2198. my $actor = Actor::get($args->{ID});
  2199. my $name = $actor->name;
  2200. my $dist = "unknown";
  2201. if (!$actor->isa('Actor::Unknown')) {
  2202. $dist = distance($char->{pos_to}, $actor->{pos_to});
  2203. $dist = sprintf("%.1f", $dist) if ($dist =~ /./);
  2204. }
  2205. message TF("[dist=%s] %s: %sn", $dist, $actor->nameIdx, $emotion), "emotion";
  2206. chatLog("e", "$name".": $emotionn") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  2207. }
  2208. }
  2209. sub equip_item {
  2210. my ($self, $args) = @_;
  2211. my $item = $char->inventory->getByServerIndex($args->{index});
  2212. if (!$args->{success}) {
  2213. message TF("You can't put on %s (%d)n", $item->{name}, $item->{invIndex});
  2214. } else {
  2215. $item->{equipped} = $args->{type};
  2216. if ($args->{type} == 10) {
  2217. $char->{equipment}{arrow} = $item;
  2218. } else {
  2219. foreach (%equipSlot_rlut){
  2220. if ($_ & $args->{type}){
  2221. next if $_ == 10; # work around Arrow bug
  2222. $char->{equipment}{$equipSlot_lut{$_}} = $item;
  2223. }
  2224. }
  2225. }
  2226. message TF("You equip %s (%d) - %s (type %s)n", $item->{name}, $item->{invIndex},
  2227. $equipTypes_lut{$item->{type_equip}}, $args->{type}), 'inventory';
  2228. }
  2229. $ai_v{temp}{waitForEquip}-- if $ai_v{temp}{waitForEquip};
  2230. }
  2231. sub errors {
  2232. my ($self, $args) = @_;
  2233. Plugins::callHook('disconnected') if ($net->getState() == Network::IN_GAME);
  2234. if ($net->getState() == Network::IN_GAME &&
  2235. ($config{dcOnDisconnect} > 1 ||
  2236. ($config{dcOnDisconnect} &&
  2237. $args->{type} != 3 &&
  2238. $args->{type} != 10))) {
  2239. message T("Lost connection; exitingn");
  2240. $quit = 1;
  2241. }
  2242. $net->setState(1);
  2243. undef $conState_tries;
  2244. $timeout_ex{'master'}{'time'} = time;
  2245. $timeout_ex{'master'}{'timeout'} = $timeout{'reconnect'}{'timeout'};
  2246. if (($args->{type} != 0)) {
  2247. $net->serverDisconnect();
  2248. }
  2249. if ($args->{type} == 0) {
  2250. error T("Server shutting downn"), "connection";
  2251. } elsif ($args->{type} == 1) {
  2252. error T("Error: Server is closedn"), "connection";
  2253. } elsif ($args->{type} == 2) {
  2254. if ($config{'dcOnDualLogin'} == 1) {
  2255. $interface->errorDialog(TF("Critical Error: Dual login prohibited - Someone trying to login!nn" .
  2256. "%s will now immediately  disconnect.", $Settings::NAME));
  2257. $quit = 1;
  2258. } elsif ($config{'dcOnDualLogin'} >= 2) {
  2259. error T("Critical Error: Dual login prohibited - Someone trying to login!n"), "connection";
  2260. message TF("Disconnect for %s seconds...n", $config{'dcOnDualLogin'}), "connection";
  2261. $timeout_ex{'master'}{'timeout'} = $config{'dcOnDualLogin'};
  2262. } else {
  2263. error T("Critical Error: Dual login prohibited - Someone trying to login!n"), "connection";
  2264. }
  2265. } elsif ($args->{type} == 3) {
  2266. error T("Error: Out of sync with servern"), "connection";
  2267. } elsif ($args->{type} == 4) {
  2268. error T("Error: Server is jammed due to over-population.n"), "connection";
  2269. } elsif ($args->{type} == 5) {
  2270. error T("Error: You are underaged and cannot join this server.n"), "connection";
  2271. } elsif ($args->{type} == 6) {
  2272. $interface->errorDialog(T("Critical Error: You must pay to play this account!n"));
  2273. $quit = 1 unless ($net->version == 1);
  2274. } elsif ($args->{type} == 8) {
  2275. error T("Error: The server still recognizes your last connectionn"), "connection";
  2276. } elsif ($args->{type} == 9) {
  2277. error T("Error: IP capacity of this Internet Cafe is full. Would you like to pay the personal base?n"), "connection";
  2278. } elsif ($args->{type} == 10) {
  2279. error T("Error: You are out of available time paid forn"), "connection";
  2280. } elsif ($args->{type} == 15) {
  2281. error T("Error: You have been forced to disconnect by a GMn"), "connection";
  2282. } elsif ($args->{type} == 101) {
  2283. error T("Error: Your account has been suspended until the next maintenance period for possible use of 3rd party programsn"), "connection";
  2284. } elsif ($args->{type} == 102) {
  2285. error T("Error: For an hour, more than 10 connections having same IP address, have made. Please check this matter.n"), "connection";
  2286. } else {
  2287. error TF("Unknown error %sn", $args->{type}), "connection";
  2288. }
  2289. }
  2290. sub exp_zeny_info {
  2291. my ($self, $args) = @_;
  2292. return unless changeToInGameState();
  2293. if ($args->{type} == 1) {
  2294. $char->{exp_last} = $char->{exp};
  2295. $char->{exp} = $args->{val};
  2296. debug "Exp: $args->{val}n", "parseMsg";
  2297. if (!$bExpSwitch) {
  2298. $bExpSwitch = 1;
  2299. } else {
  2300. if ($char->{exp_last} > $char->{exp}) {
  2301. $monsterBaseExp = 0;
  2302. } else {
  2303. $monsterBaseExp = $char->{exp} - $char->{exp_last};
  2304. }
  2305. $totalBaseExp += $monsterBaseExp;
  2306. if ($bExpSwitch == 1) {
  2307. $totalBaseExp += $monsterBaseExp;
  2308. $bExpSwitch = 2;
  2309. }
  2310. }
  2311. } elsif ($args->{type} == 2) {
  2312. $char->{exp_job_last} = $char->{exp_job};
  2313. $char->{exp_job} = $args->{val};
  2314. debug "Job Exp: $args->{val}n", "parseMsg";
  2315. if ($jExpSwitch == 0) {
  2316. $jExpSwitch = 1;
  2317. } else {
  2318. if ($char->{exp_job_last} > $char->{exp_job}) {
  2319. $monsterJobExp = 0;
  2320. } else {
  2321. $monsterJobExp = $char->{exp_job} - $char->{exp_job_last};
  2322. }
  2323. $totalJobExp += $monsterJobExp;
  2324. if ($jExpSwitch == 1) {
  2325. $totalJobExp += $monsterJobExp;
  2326. $jExpSwitch = 2;
  2327. }
  2328. }
  2329. my $basePercent = $char->{exp_max} ?
  2330. ($monsterBaseExp / $char->{exp_max} * 100) :
  2331. 0;
  2332. my $jobPercent = $char->{exp_job_max} ?
  2333. ($monsterJobExp / $char->{exp_job_max} * 100) :
  2334. 0;
  2335. message TF("Exp gained: %d/%d (%.2f%%/%.2f%%)n", $monsterBaseExp, $monsterJobExp, $basePercent, $jobPercent), "exp";
  2336. Plugins::callHook('exp_gained');
  2337. } elsif ($args->{type} == 20) {
  2338. my $change = $args->{val} - $char->{zenny};
  2339. if ($change > 0) {
  2340. message TF("You gained %s zeny.n", formatNumber($change));
  2341. } elsif ($change < 0) {
  2342. message TF("You lost %s zeny.n", formatNumber(-$change));
  2343. if ($config{dcOnZeny} && $args->{val} <= $config{dcOnZeny}) {
  2344. $interface->errorDialog(TF("Disconnecting due to zeny lower than %s.", $config{dcOnZeny}));
  2345. $quit = 1;
  2346. }
  2347. }
  2348. $char->{zenny} = $args->{val};
  2349. debug "Zenny: $args->{val}n", "parseMsg";
  2350. } elsif ($args->{type} == 22) {
  2351. $char->{exp_max_last} = $char->{exp_max};
  2352. $char->{exp_max} = $args->{val};
  2353. debug(TF("Required Exp: %sn", $args->{val}), "parseMsg");
  2354. if (!$net->clientAlive() && $initSync && $masterServer->{serverType} == 2) {
  2355. $messageSender->sendSync(1);
  2356. $initSync = 0;
  2357. }
  2358. } elsif ($args->{type} == 23) {
  2359. $char->{exp_job_max_last} = $char->{exp_job_max};
  2360. $char->{exp_job_max} = $args->{val};
  2361. debug("Required Job Exp: $args->{val}n", "parseMsg");
  2362. message TF("BaseExp: %s | JobExp: %sn", $monsterBaseExp, $monsterJobExp), "info", 2 if ($monsterBaseExp);
  2363. }
  2364. }
  2365. sub forge_list {
  2366. my ($self, $args) = @_;
  2367. message T("========Forge List========n");
  2368. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 8) {
  2369. my $viewID = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
  2370. message "$viewID $items_lut{$viewID}n";
  2371. # always 0x0012
  2372. #my $unknown = unpack("v1", substr($args->{RAW_MSG}, $i+2, 2));
  2373. # ???
  2374. #my $charID = substr($args->{RAW_MSG}, $i+4, 4);
  2375. }
  2376. message "=========================n";
  2377. }
  2378. sub friend_list {
  2379. my ($self, $args) = @_;
  2380. # Friend list
  2381. undef @friendsID;
  2382. undef %friends;
  2383. my $msg = $args->{RAW_MSG};
  2384. my $msg_size = $args->{RAW_MSG_SIZE};
  2385. my $ID = 0;
  2386. for (my $i = 4; $i < $msg_size; $i += 32) {
  2387. binAdd(@friendsID, $ID);
  2388. $friends{$ID}{'accountID'} = substr($msg, $i, 4);
  2389. $friends{$ID}{'charID'} = substr($msg, $i + 4, 4);
  2390. $friends{$ID}{'name'} = bytesToString(unpack("Z24", substr($msg, $i + 8 , 24)));
  2391. $friends{$ID}{'online'} = 0;
  2392. $ID++;
  2393. }
  2394. }
  2395. sub friend_logon {
  2396. my ($self, $args) = @_;
  2397. # Friend In/Out
  2398. my $friendAccountID = $args->{friendAccountID};
  2399. my $friendCharID = $args->{friendCharID};
  2400. my $isNotOnline = $args->{isNotOnline};
  2401. for (my $i = 0; $i < @friendsID; $i++) {
  2402. if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
  2403. $friends{$i}{'online'} = 1 - $isNotOnline;
  2404. if ($isNotOnline) {
  2405. message TF("Friend %s has disconnectedn", $friends{$i}{name}), undef, 1;
  2406. } else {
  2407. message TF("Friend %s has connectedn", $friends{$i}{name}), undef, 1;
  2408. }
  2409. last;
  2410. }
  2411. }
  2412. }
  2413. sub friend_request {
  2414. my ($self, $args) = @_;
  2415. # Incoming friend request
  2416. $incomingFriend{'accountID'} = $args->{accountID};
  2417. $incomingFriend{'charID'} = $args->{charID};
  2418. $incomingFriend{'name'} = bytesToString($args->{name});
  2419. message TF("%s wants to be your friendn", $incomingFriend{'name'});
  2420. message TF("Type 'friend accept' to be friend with %s, otherwise type 'friend reject'n", $incomingFriend{'name'});
  2421. }
  2422. sub friend_removed {
  2423. my ($self, $args) = @_;
  2424. # Friend removed
  2425. my $friendAccountID =  $args->{friendAccountID};
  2426. my $friendCharID =  $args->{friendCharID};
  2427. for (my $i = 0; $i < @friendsID; $i++) {
  2428. if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
  2429. message TF("%s is no longer your friendn", $friends{$i}{'name'});
  2430. binRemove(@friendsID, $i);
  2431. delete $friends{$i};
  2432. last;
  2433. }
  2434. }
  2435. }
  2436. sub friend_response {
  2437. my ($self, $args) = @_;
  2438. # Response to friend request
  2439. my $type = $args->{type};
  2440. my $name = bytesToString($args->{name});
  2441. if ($type) {
  2442. message TF("%s rejected to be your friendn", $name);
  2443. } else {
  2444. my $ID = @friendsID;
  2445. binAdd(@friendsID, $ID);
  2446. $friends{$ID}{accountID} = substr($args->{RAW_MSG}, 4, 4);
  2447. $friends{$ID}{charID} = substr($args->{RAW_MSG}, 8, 4);
  2448. $friends{$ID}{name} = $name;
  2449. $friends{$ID}{online} = 1;
  2450. message TF("%s is now your friendn", $incomingFriend{'name'});
  2451. }
  2452. }
  2453. sub homunculus_food {
  2454. my ($self, $args) = @_;
  2455. if ($args->{success}) {
  2456. message TF("Fed homunculus with %sn", itemNameSimple($args->{foodID})), "homunculus";
  2457. } else {
  2458. error TF("Failed to feed homunculus with %s: no food in inventory.n", itemNameSimple($args->{foodID})), "homunculus";
  2459. # auto-vaporize
  2460. if ($char->{homunculus}{hunger} <= 11 && timeOut($char->{homunculus}{vaporize_time}, 5)) {
  2461. $messageSender->sendSkillUse(244, 1, $accountID);
  2462. $char->{homunculus}{vaporize_time} = time;
  2463. error "Critical hunger level reached. Homunculus is put to rest.n", "homunculus";
  2464. }
  2465. }
  2466. }
  2467. use constant {
  2468. HO_PRE_INIT => 0x0,
  2469. HO_RELATIONSHIP_CHANGED => 0x1,
  2470. HO_FULLNESS_CHANGED => 0x2,
  2471. HO_ACCESSORY_CHANGED => 0x3,
  2472. HO_HEADTYPE_CHANGED => 0x4,
  2473. };
  2474. sub homunculus_info {
  2475. my ($self, $args) = @_;
  2476. if ($args->{type} == HO_PRE_INIT) {
  2477. my $state = $char->{homunculus}{state}
  2478. if ($char->{homunculus} && $char->{homunculus}{ID} && $char->{homunculus}{ID} ne $args->{ID});
  2479. $char->{homunculus} = Actor::get($args->{ID});
  2480. $char->{homunculus}{state} = $state if (defined $state);
  2481. $char->{homunculus}{map} = $field{name};
  2482. unless ($char->{slaves}{$char->{homunculus}{ID}}) {
  2483. AI::SlaveManager::addSlave ($char->{homunculus});
  2484. }
  2485. } elsif ($args->{type} == HO_RELATIONSHIP_CHANGED) {
  2486. $char->{homunculus}{intimacy} = $args->{val};
  2487. } elsif ($args->{type} == HO_FULLNESS_CHANGED) {
  2488. $char->{homunculus}{hunger} = $args->{val};
  2489. } elsif ($args->{type} == HO_ACCESSORY_CHANGED) {
  2490. $char->{homunculus}{accessory} = $args->{val};
  2491. } elsif ($args->{type} == HO_HEADTYPE_CHANGED) {
  2492. #
  2493. }
  2494. }
  2495. sub homunculus_stats { # homunculus and mercenary stats
  2496. my ($self, $args) = @_;
  2497. if ($args->{switch} eq '029B') {
  2498. # Mercenary
  2499. $char->{mercenary} = Actor::get ($args->{ID});
  2500. $char->{mercenary}{map} = $field{name};
  2501. unless ($char->{slaves}{$char->{mercenary}{ID}}) {
  2502. AI::SlaveManager::addSlave ($char->{mercenary});
  2503. }
  2504. }
  2505. my $slave;
  2506. if ($args->{switch} eq '022E') {
  2507. $slave = $char->{homunculus};
  2508. } elsif ($args->{switch} eq '029B') {
  2509. $slave = $char->{mercenary};
  2510. }
  2511. $slave->{name} = bytesToString ($args->{name});
  2512. if ($args->{switch} eq '022E') {
  2513. # Homunculus states:
  2514. # 0 - alive
  2515. # 2 - rest
  2516. # 4 - dead
  2517. if (($args->{state} & ~8) > 1) {
  2518. foreach my $handle (@{$char->{homunculus}{slave_skillsID}}) {
  2519. delete $char->{skills}{$handle};
  2520. }
  2521. $char->{homunculus}->clear();
  2522. undef @{$char->{homunculus}{slave_skillsID}};
  2523. if (defined $slave->{state} && $slave->{state} != $args->{state}) {
  2524. if ($args->{state} & 2) {
  2525. message T("Your Homunculus was vaporized!n"), 'homunculus';
  2526. } elsif ($args->{state} & 4) {
  2527. message T("Your Homunculus died!n"), 'homunculus';
  2528. }
  2529. }
  2530. } elsif (defined $slave->{state} && $slave->{state} != $args->{state}) {
  2531. if ($slave->{state} & 2) {
  2532. message T("Your Homunculus was recalled!n"), 'homunculus';
  2533. } elsif ($slave->{state} & 4) {
  2534. message T("Your Homunculus was resurrected!n"), 'homunculus';
  2535. }
  2536. }
  2537. }
  2538. $slave->{level}        = $args->{lvl};
  2539. $slave->{atk}          = $args->{atk};
  2540. $slave->{matk}         = $args->{matk};
  2541. $slave->{hit}          = $args->{hit};
  2542. $slave->{critical}     = $args->{critical};
  2543. $slave->{def}          = $args->{def};
  2544. $slave->{mdef}         = $args->{mdef};
  2545. $slave->{flee}         = $args->{flee};
  2546. $slave->{aspd}         = $args->{aspd};
  2547. $slave->{hp}           = $args->{hp};
  2548. $slave->{hp_max}       = ($args->{hp_max} > 0) ? $args->{hp_max} : $args->{hp};
  2549. $slave->{sp}           = $args->{sp};
  2550. $slave->{sp_max}       = ($args->{sp_max} > 0) ? $args->{sp_max} : $args->{sp};
  2551. $slave->{aspdDisp}     = int (200 - (($args->{aspd} < 10) ? 10 : ($args->{aspd} / 10)));
  2552. $slave->{hpPercent}    = ($slave->{hp} / $slave->{hp_max}) * 100;
  2553. $slave->{spPercent}    = ($slave->{sp} / $slave->{sp_max}) * 100;
  2554. if ($args->{switch} eq '022E') {
  2555. $slave->{state}        = $args->{state};
  2556. $slave->{hunger}       = $args->{hunger};
  2557. $slave->{intimacy}     = $args->{intimacy};
  2558. $slave->{accessory}    = $args->{accessory};
  2559. $slave->{exp}          = $args->{exp};
  2560. $slave->{exp_max}      = $args->{exp_max};
  2561. $slave->{expPercent}   = ($args->{exp_max}) ? ($args->{exp} / $args->{exp_max}) * 100 : 0;
  2562. $slave->{points_skill} = $args->{points_skill};
  2563. } elsif ($args->{switch} eq '029B') {
  2564. #$slave->{contract_end} = $args->{contract_end}
  2565. $slave->{faith}        = $args->{faith};
  2566. $slave->{summons}      = $args->{summons};
  2567. }
  2568. }
  2569. sub gameguard_grant {
  2570. my ($self, $args) = @_;
  2571. if ($args->{server} == 0) {
  2572. error T("The server Denied the login because GameGuard packets where not replied " .
  2573. "correctly or too many time has been spent to send the response.n" .
  2574. "Please verify the version of your poseidon server and try againn"), "poseidon";
  2575. return;
  2576. } elsif ($args->{server} == 1) {
  2577. message T("Server granted login request to account servern"), "poseidon";
  2578. } else {
  2579. message T("Server granted login request to char/map servern"), "poseidon";
  2580. change_to_constate25 if ($config{'gameGuard'} eq "2");
  2581. }
  2582. $net->setState(1.3) if ($net->getState() == 1.2);
  2583. }
  2584. sub gameguard_request {
  2585. my ($self, $args) = @_;
  2586. return if ($net->version == 1 && $config{gameGuard} ne '2');
  2587. Poseidon::Client::getInstance()->query(
  2588. substr($args->{RAW_MSG}, 0, $args->{RAW_MSG_SIZE})
  2589. );
  2590. debug "Querying Poseidonn", "poseidon";
  2591. }
  2592. sub guild_allies_enemy_list {
  2593. my ($self, $args) = @_;
  2594. # Guild Allies/Enemy List
  2595. # <len>.w (<type>.l <guildID>.l <guild name>.24B).*
  2596. # type=0 Ally
  2597. # type=1 Enemy
  2598. # This is the length of the entire packet
  2599. my $msg = $args->{RAW_MSG};
  2600. my $len = unpack("v", substr($msg, 2, 2));
  2601. # clear $guild{enemy} and $guild{ally} otherwise bot will misremember alliances -zdivpsa
  2602. $guild{enemy} = $guild{ally} = {};
  2603. for (my $i = 4; $i < $len; $i += 32) {
  2604. my ($type, $guildID, $guildName) = unpack("V1 V1 Z24", substr($msg, $i, 32));
  2605. $guildName = bytesToString($guildName);
  2606. if ($type) {
  2607. # Enemy guild
  2608. $guild{enemy}{$guildID} = $guildName;
  2609. } else {
  2610. # Allied guild
  2611. $guild{ally}{$guildID} = $guildName;
  2612. }
  2613. debug "Your guild is ".($type ? 'enemy' : 'ally')." with guild $guildID ($guildName)n", "guild";
  2614. }
  2615. }
  2616. sub guild_ally_request {
  2617. my ($self, $args) = @_;
  2618. my $ID = $args->{ID}; # is this a guild ID or account ID? Freya calls it an account ID
  2619. my $name = bytesToString($args->{name}); # Type: String
  2620. message TF("Incoming Request to Ally Guild '%s'n", $name);
  2621. $incomingGuild{ID} = $ID;
  2622. $incomingGuild{Type} = 2;
  2623. $timeout{ai_guildAutoDeny}{time} = time;
  2624. }
  2625. sub guild_broken {
  2626. my ($self, $args) = @_;
  2627. # FIXME: determine the real significance of flag
  2628. my $flag = $args->{flag};
  2629. message T("Guild broken.n");
  2630. undef %{$char->{guild}};
  2631. undef $char->{guildID};
  2632. undef %guild;
  2633. }
  2634. sub guild_member_setting_list {
  2635. my ($self, $args) = @_;
  2636. my $newmsg;
  2637. my $msg = $args->{RAW_MSG};
  2638. my $msg_size = $args->{RAW_MSG_SIZE};
  2639. $self->decrypt($newmsg, substr($msg, 4, length($msg)-4));
  2640. $msg = substr($msg, 0, 4).$newmsg;
  2641. my $gtIndex;
  2642. for (my $i = 4; $i < $msg_size; $i += 16) {
  2643. $gtIndex = unpack("V1", substr($msg, $i, 4));
  2644. $guild{positions}[$gtIndex]{invite} = (unpack("C1", substr($msg, $i + 4, 1)) & 0x01) ? 1 : '';
  2645. $guild{positions}[$gtIndex]{punish} = (unpack("C1", substr($msg, $i + 4, 1)) & 0x10) ? 1 : '';
  2646. $guild{positions}[$gtIndex]{feeEXP} = unpack("V1", substr($msg, $i + 12, 4));
  2647. }
  2648. }
  2649. sub guild_skills_list {
  2650. my ($self, $args) = @_;
  2651. my $msg = $args->{RAW_MSG};
  2652. my $msg_size = $args->{RAW_MSG_SIZE};
  2653. for (my $i = 6; $i < $msg_size; $i += 37) {
  2654. my $skillID = unpack("v1", substr($msg, $i, 2));
  2655. my $targetType = unpack("v1", substr($msg, $i+2, 2));
  2656. my $level = unpack("v1", substr($msg, $i + 6, 2));
  2657. my $sp = unpack("v1", substr($msg, $i + 8, 2));
  2658. my ($skillName) = unpack("Z*", substr($msg, $i + 12, 24));
  2659. my $up = unpack("C1", substr($msg, $i+36, 1));
  2660. $guild{skills}{$skillName}{ID} = $skillID;
  2661. $guild{skills}{$skillName}{sp} = $sp;
  2662. $guild{skills}{$skillName}{up} = $up;
  2663. $guild{skills}{$skillName}{targetType} = $targetType;
  2664. if (!$guild{skills}{$skillName}{lv}) {
  2665. $guild{skills}{$skillName}{lv} = $level;
  2666. }
  2667. }
  2668. }
  2669. sub guild_chat {
  2670. my ($self, $args) = @_;
  2671. my ($chatMsgUser, $chatMsg); # Type: String
  2672. my $chat; # Type: String
  2673. return unless changeToInGameState();
  2674. $chat = bytesToString($args->{message});
  2675. if (($chatMsgUser, $chatMsg) = $chat =~ /(.*?) : (.*)/) {
  2676. $chatMsgUser =~ s/ $//;
  2677. stripLanguageCode($chatMsg);
  2678. $chat = "$chatMsgUser : $chatMsg";
  2679. }
  2680. chatLog("g", "$chatn") if ($config{'logGuildChat'});
  2681. # Translation Comment: Guild Chat
  2682. message TF("[Guild] %sn", $chat), "guildchat";
  2683. # Only queue this if it's a real chat message
  2684. ChatQueue::add('g', 0, $chatMsgUser, $chatMsg) if ($chatMsgUser);
  2685. Plugins::callHook('packet_guildMsg', {
  2686. MsgUser => $chatMsgUser,
  2687. Msg => $chatMsg
  2688. });
  2689. }
  2690. sub guild_create_result {
  2691. my ($self, $args) = @_;
  2692. my $type = $args->{type};
  2693. my %types = (
  2694. 0 => T("Guild create successful.n"),
  2695. 2 => T("Guild create failed: Guild name already exists.n"),
  2696. 3 => T("Guild create failed: Emperium is needed.n")
  2697. );
  2698. if ($types{$type}) {
  2699. message $types{$type};
  2700. } else {
  2701. message TF("Guild create: Unknown error %sn", $type);
  2702. }
  2703. }
  2704. sub guild_expulsionlist {
  2705. my ($self, $args) = @_;
  2706. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 88) {
  2707. my ($name)  = unpack("Z24", substr($args->{'RAW_MSG'}, $i, 24));
  2708. my $acc     = unpack("Z24", substr($args->{'RAW_MSG'}, $i + 24, 24));
  2709. my ($cause) = unpack("Z44", substr($args->{'RAW_MSG'}, $i + 48, 44));
  2710. $guild{expulsion}{$acc}{name} = bytesToString($name);
  2711. $guild{expulsion}{$acc}{cause} = bytesToString($cause);
  2712. }
  2713. }
  2714. sub guild_info {
  2715. my ($self, $args) = @_;
  2716. # Guild Info
  2717. foreach (qw(ID lvl conMember maxMember average exp next_exp members)) {
  2718. $guild{$_} = $args->{$_};
  2719. }
  2720. $guild{name} = bytesToString($args->{name});
  2721. $guild{master} = bytesToString($args->{master});
  2722. $guild{members}++; # count ourselves in the guild members count
  2723. }
  2724. sub guild_invite_result {
  2725. my ($self, $args) = @_;
  2726. my $type = $args->{type};
  2727. my %types = (
  2728. 0 => 'Target is already in a guild.',
  2729. 1 => 'Target has denied.',
  2730. 2 => 'Target has accepted.',
  2731. 3 => 'Your guild is full.'
  2732. );
  2733. if ($types{$type}) {
  2734.     message TF("Guild join request: %sn", $types{$type});
  2735. } else {
  2736.     message TF("Guild join request: Unknown %sn", $type);
  2737. }
  2738. }
  2739. sub guild_location {
  2740. # FIXME: not implemented
  2741. my ($self, $args) = @_;
  2742. }
  2743. sub guild_leave {
  2744. my ($self, $args) = @_;
  2745. message TF("%s has left the guild.n" .
  2746. "Reason: %sn", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
  2747. }
  2748. sub guild_expulsion {
  2749. my ($self, $args) = @_;
  2750. message TF("%s has been removed from the guild.n" .
  2751. "Reason: %sn", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
  2752. }
  2753. sub guild_members_list {
  2754. my ($self, $args) = @_;
  2755. my $newmsg;
  2756. my $jobID;
  2757. my $msg = $args->{RAW_MSG};
  2758. my $msg_size = $args->{RAW_MSG_SIZE};
  2759. $self->decrypt($newmsg, substr($msg, 4, length($msg) - 4));
  2760. $msg = substr($msg, 0, 4) . $newmsg;
  2761. my $c = 0;
  2762. delete $guild{member};
  2763. for (my $i = 4; $i < $msg_size; $i+=104){
  2764. $guild{member}[$c]{ID}    = substr($msg, $i, 4);
  2765. $guild{member}[$c]{charID}   = substr($msg, $i+4, 4);
  2766. $jobID = unpack("v1", substr($msg, $i + 14, 2));
  2767. if ($jobID =~ /^40/) {
  2768. $jobID =~ s/^40/1/;
  2769. $jobID += 60;
  2770. }
  2771. $guild{member}[$c]{jobID} = $jobID;
  2772. $guild{member}[$c]{lvl}   = unpack("v1", substr($msg, $i + 16, 2));
  2773. $guild{member}[$c]{contribution} = unpack("V1", substr($msg, $i + 18, 4));
  2774. $guild{member}[$c]{online} = unpack("v1", substr($msg, $i + 22, 2));
  2775. my $gtIndex = unpack("V1", substr($msg, $i + 26, 4));
  2776. $guild{member}[$c]{title} = $guild{title}[$gtIndex];
  2777. $guild{member}[$c]{name} = bytesToString(unpack("Z24", substr($msg, $i + 80, 24)));
  2778. $c++;
  2779. }
  2780. }
  2781. sub guild_member_online_status {
  2782. my ($self, $args) = @_;
  2783. foreach my $guildmember (@{$guild{member}}) {
  2784. if ($guildmember->{charID} eq $args->{charID}) {
  2785. if ($guildmember->{online} = $args->{online}) {
  2786. message TF("Guild member %s logged in.n", $guildmember->{name}), "guildchat";
  2787. } else {
  2788. message TF("Guild member %s logged out.n", $guildmember->{name}), "guildchat";
  2789. }
  2790. last;
  2791. }
  2792. }
  2793. }
  2794. sub npc_effect {
  2795. my ($self, $args) = @_;
  2796. my $effect = unpack("V1", $args->{effect});
  2797. my $ID = $args->{ID};
  2798. my $name = getNPCName($ID);
  2799. message TF("%s: *%s*n", $name, ''), "npc";
  2800. }
  2801. sub guild_members_title_list {
  2802. my ($self, $args) = @_;
  2803. my $newmsg;
  2804. my $msg = $args->{RAW_MSG};
  2805. my $msg_size = $args->{RAW_MSG_SIZE};
  2806. $self->decrypt($newmsg, substr($msg, 4, length($msg) - 4));
  2807. $msg = substr($msg, 0, 4) . $newmsg;
  2808. my $gtIndex;
  2809. for (my $i = 4; $i < $msg_size; $i+=28) {
  2810. $gtIndex = unpack("V1", substr($msg, $i, 4));
  2811. $guild{positions}[$gtIndex]{title} = bytesToString(unpack("Z24", substr($msg, $i + 4, 24)));
  2812. }
  2813. }
  2814. sub guild_name {
  2815. my ($self, $args) = @_;
  2816. my $guildID = $args->{guildID};
  2817. my $emblemID = $args->{emblemID};
  2818. my $mode = $args->{mode};
  2819. my $guildName = bytesToString($args->{guildName});
  2820. $char->{guild}{name} = $guildName;
  2821. $char->{guildID} = $guildID;
  2822. $char->{guild}{emblem} = $emblemID;
  2823. $messageSender->sendGuildInfoRequest(); # Is this necessary?? (requests for guild info packet 014E)
  2824. $messageSender->sendGuildRequest(0); #requests for guild info packet 01B6 and 014C
  2825. $messageSender->sendGuildRequest(1); #requests for guild member packet 0166 and 0154
  2826. }
  2827. sub guild_notice {
  2828. my ($self, $args) = @_;
  2829. my $msg = $args->{RAW_MSG};
  2830. my ($address) = unpack("Z*", substr($msg, 2, 60));
  2831. my ($message) = unpack("Z*", substr($msg, 62, 120));
  2832. stripLanguageCode($address);
  2833. stripLanguageCode($message);
  2834. $address = bytesToString($address);
  2835. $message = bytesToString($message);
  2836. # don't show the huge guildmessage notice if there is none
  2837. # the client does something similar to this...
  2838. if ($address || $message) {
  2839. my $msg = TF("---Guild Notice---n" .
  2840. "%snn" .
  2841. "%sn" .
  2842. "------------------n", $address, $message);
  2843. message $msg, "guildnotice";
  2844. }
  2845. #message T("Requesting guild information...n"), "info"; # Lets Disable this, its kinda useless.
  2846. $messageSender->sendGuildInfoRequest();
  2847. # Replies 01B6 (Guild Info) and 014C (Guild Ally/Enemy List)
  2848. $messageSender->sendGuildRequest(0);
  2849. # Replies 0166 (Guild Member Titles List) and 0154 (Guild Members List)
  2850. $messageSender->sendGuildRequest(1);
  2851. }
  2852. sub guild_request {
  2853. my ($self, $args) = @_;
  2854. # Guild request
  2855. my $ID = $args->{ID};
  2856. my $name = bytesToString($args->{name});
  2857. message TF("Incoming Request to join Guild '%s'n", $name);
  2858. $incomingGuild{'ID'} = $ID;
  2859. $incomingGuild{'Type'} = 1;
  2860. $timeout{'ai_guildAutoDeny'}{'time'} = time;
  2861. }
  2862. sub identify {
  2863. my ($self, $args) = @_;
  2864. my $index = $args->{index};
  2865. my $item = $char->inventory->getByServerIndex($index);
  2866. $item->{identified} = 1;
  2867. $item->{type_equip} = $itemSlots_lut{$item->{nameID}};
  2868. message TF("Item Identified: %s (%d)n", $item->{name}, $item->{invIndex}), "info";
  2869. undef @identifyID;
  2870. }
  2871. sub identify_list {
  2872. my ($self, $args) = @_;
  2873. my $newmsg;
  2874. my $msg = $args->{RAW_MSG};
  2875. my $msg_size = $args->{RAW_MSG_SIZE};
  2876. $self->decrypt($newmsg, substr($msg, 4));
  2877. $msg = substr($msg, 0, 4).$newmsg;
  2878. undef @identifyID;
  2879. for (my $i = 4; $i < $msg_size; $i += 2) {
  2880. my $index = unpack("v1", substr($msg, $i, 2));
  2881. my $item = $char->inventory->getByServerIndex($index);
  2882. binAdd(@identifyID, $item->{invIndex});
  2883. }
  2884. my $num = @identifyID;
  2885. message TF("Received Possible Identify List (%s item(s)) - type 'identify'n", $num), 'info';
  2886. }
  2887. sub ignore_all_result {
  2888. my ($self, $args) = @_;
  2889. if ($args->{type} == 0) {
  2890. message T("All Players ignoredn");
  2891. } elsif ($args->{type} == 1) {
  2892. if ($args->{error} == 0) {
  2893. message T("All players unignoredn");
  2894. }
  2895. }
  2896. }
  2897. sub ignore_player_result {
  2898. my ($self, $args) = @_;
  2899. if ($args->{type} == 0) {
  2900. message T("Player ignoredn");
  2901. } elsif ($args->{type} == 1) {
  2902. if ($args->{error} == 0) {
  2903. message T("Player unignoredn");
  2904. }
  2905. }
  2906. }
  2907. sub inventory_item_added {
  2908. my ($self, $args) = @_;
  2909. return unless changeToInGameState();
  2910. my ($index, $amount, $fail) = ($args->{index}, $args->{amount}, $args->{fail});
  2911. if (!$fail) {
  2912. my $item = $char->inventory->getByServerIndex($index);
  2913. if (!$item) {
  2914. # Add new item
  2915. $item = new Actor::Item();
  2916. $item->{index} = $index;
  2917. $item->{nameID} = $args->{nameID};
  2918. $item->{type} = $args->{type};
  2919. $item->{type_equip} = $args->{type_equip};
  2920. $item->{amount} = $amount;
  2921. $item->{identified} = $args->{identified};
  2922. $item->{broken} = $args->{broken};
  2923. $item->{upgrade} = $args->{upgrade};
  2924. $item->{cards} = ($args->{switch} eq '029A') ? $args->{cards} + $args->{cards_ext}: $args->{cards};
  2925. if ($args->{switch} eq '029A') {
  2926. $args->{cards} .= $args->{cards_ext};
  2927. }
  2928. $item->{name} = itemName($item);
  2929. $char->inventory->add($item);
  2930. } else {
  2931. # Add stackable item
  2932. $item->{amount} += $amount;
  2933. }
  2934. $itemChange{$item->{name}} += $amount;
  2935. my $disp = TF("Item added to inventory: %s (%d) x %d - %s",
  2936. $item->{name}, $item->{invIndex}, $amount, $itemTypes_lut{$item->{type}});
  2937. message "$dispn", "drop";
  2938. $disp .= " ($field{name})n";
  2939. itemLog($disp);
  2940. Plugins::callHook('item_gathered',{item => $item->{name}});
  2941. $args->{item} = $item;
  2942. # TODO: move this stuff to AI()
  2943. if ($ai_v{npc_talk}{itemID} eq $item->{nameID}) {
  2944. $ai_v{'npc_talk'}{'talk'} = 'buy';
  2945. $ai_v{'npc_talk'}{'time'} = time;
  2946. }
  2947. if ($AI == 2) {
  2948. # Auto-drop item
  2949. if (pickupitems(lc($item->{name})) == -1 && !AI::inQueue('storageAuto', 'buyAuto')) {
  2950. $messageSender->sendDrop($item->{index}, $amount);
  2951. message TF("Auto-dropping item: %s (%d) x %dn", $item->{name}, $item->{invIndex}, $amount), "drop";
  2952. }
  2953. }
  2954. } elsif ($fail == 6) {
  2955. message T("Can't loot item...wait...n"), "drop";
  2956. } elsif ($fail == 2) {
  2957. message T("Cannot pickup item (inventory full)n"), "drop";
  2958. } elsif ($fail == 1) {
  2959. message T("Cannot pickup item (you're Frozen?)n"), "drop";
  2960. } else {
  2961. message TF("Cannot pickup item (failure code %d)n", $fail), "drop";
  2962. }
  2963. }
  2964. sub inventory_item_removed {
  2965. my ($self, $args) = @_;
  2966. return unless changeToInGameState();
  2967. my $item = $char->inventory->getByServerIndex($args->{index});
  2968. if ($item) {
  2969. inventoryItemRemoved($item->{invIndex}, $args->{amount});
  2970. Plugins::callHook('packet_item_removed', {index => $item->{invIndex}});
  2971. }
  2972. }
  2973. sub item_used {
  2974. my ($self, $args) = @_;
  2975. my ($index, $itemID, $ID, $remaining, $success) =
  2976. @{$args}{qw(index itemID ID remaining success)};
  2977. my %hook_args = (
  2978. serverIndex => $index,
  2979. itemID => $itemID,
  2980. userID => $ID,
  2981. remaining => $remaining,
  2982. success => $success
  2983. );
  2984. if ($ID eq $accountID) {
  2985. my $item = $char->inventory->getByServerIndex($index);
  2986. if ($item) {
  2987. if ($success == 1) {
  2988. my $amount = $item->{amount} - $remaining;
  2989. $item->{amount} -= $amount;
  2990. message TF("You used Item: %s (%d) x %d - %d leftn", $item->{name}, $item->{invIndex},
  2991. $amount, $remaining), "useItem", 1;
  2992. $itemChange{$item->{name}}--;
  2993. if ($item->{amount} <= 0) {
  2994. $char->inventory->remove($item);
  2995. }
  2996. $hook_args{item} = $item;
  2997. $hook_args{invIndex} = $item->{invIndex};
  2998. $hook_args{name} => $item->{name};
  2999. $hook_args{amount} = $amount;
  3000. } else {
  3001. message TF("You failed to use item: %s (%d)n", $item ? $item->{name} : "#$itemID", $remaining), "useItem", 1;
  3002. }
  3003.   } else {
  3004. if ($success == 1) {
  3005. message TF("You used unknown item #%d - %d leftn", $itemID, $remaining), "useItem", 1;
  3006. } else {
  3007. message TF("You failed to use unknown item #%d - %d leftn", $itemID, $remaining), "useItem", 1;
  3008. }
  3009. }