Advertisement
Guest User

Untitled

a guest
Jun 28th, 2013
1,016
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 264.33 KB | None | 0 0
  1. #########################################################################
  2. # OpenKore - Network subsystem
  3. # Copyright (c) 2006 OpenKore Team
  4. #
  5. # This software is open source, licensed under the GNU General Public
  6. # License, version 2.
  7. # Basically, this means that you're allowed to modify and distribute
  8. # this software. However, if you distribute modified versions, you MUST
  9. # also distribute the source code.
  10. # See http://www.gnu.org/licenses/gpl.html for the full license.
  11. #########################################################################
  12. # Servertype overview: http://wiki.openkore.com/index.php/ServerType
  13. package Network::Receive::ServerType0;
  14.  
  15. use strict;
  16. use Network::Receive ();
  17. use base qw(Network::Receive);
  18. use Time::HiRes qw(time usleep);
  19.  
  20. use AI;
  21. use Log qw(message warning error debug);
  22.  
  23. # from old receive.pm
  24. use Task::Wait;
  25. use Task::Function;
  26. use Task::Chained;
  27. use encoding 'utf8';
  28. use Carp::Assert;
  29. use Scalar::Util;
  30. use Exception::Class ('Network::Receive::InvalidServerType', 'Network::Receive::CreationError');
  31.  
  32. use Globals;
  33. use Actor;
  34. use Actor::You;
  35. use Actor::Player;
  36. use Actor::Monster;
  37. use Actor::Party;
  38. use Actor::Item;
  39. use Actor::Unknown;
  40. use Field;
  41. use Settings;
  42. use FileParsers;
  43. use Interface;
  44. use Misc;
  45. use Network;
  46. use Network::MessageTokenizer;
  47. use Network::Send ();
  48. use Plugins;
  49. use Utils;
  50. use Skill;
  51. use Utils::Assert;
  52. use Utils::Exceptions;
  53. use Utils::Crypton;
  54. use Translation;
  55. use I18N qw(bytesToString stringToBytes);
  56. # from old receive.pm
  57.  
  58. sub new {
  59. my ($class) = @_;
  60. my $self = $class->SUPER::new();
  61.  
  62. $self->{packet_list} = {
  63. '0069' => ['account_server_info', 'x2 a4 a4 a4 a4 a26 C a*', [qw(sessionID accountID sessionID2 lastLoginIP lastLoginTime accountSex serverInfo)]],
  64. '006A' => ['login_error', 'C Z20', [qw(type date)]],
  65. '006B' => ['received_characters', 'v C3 a*', [qw(len total_slot premium_start_slot premium_end_slot charInfo)]], # struct varies a lot, this one is from XKore 2
  66. '006C' => ['login_error_game_login_server'],
  67. # OLD '006D' => ['character_creation_successful', 'a4 x4 V x62 Z24 C7', [qw(ID zeny name str agi vit int dex luk slot)]],
  68. '006D' => ['character_creation_successful', 'a4 V9 v V2 v14 Z24 C6 v2', [qw(ID exp zeny exp_job lv_job opt1 opt2 option stance manner points_free hp hp_max sp sp_max walk_speed type hair_style weapon lv points_skill lowhead shield tophead midhead hair_color clothes_color name str agi vit int dex luk slot renameflag)]],
  69. '006E' => ['character_creation_failed', 'C' ,[qw(type)]],
  70. '006F' => ['character_deletion_successful'],
  71. '0070' => ['character_deletion_failed'],
  72. '0071' => ['received_character_ID_and_Map', 'a4 Z16 a4 v', [qw(charID mapName mapIP mapPort)]],
  73. '0072' => ['received_characters', 'v a*', [qw(len charInfo)]], # struct unknown, this one is from XKore 2
  74. '0073' => ['map_loaded', 'V a3', [qw(syncMapSync coords)]],
  75. '0075' => ['changeToInGameState'],
  76. '0077' => ['changeToInGameState'],
  77. # OLD '0078' => ['actor_exists', 'a4 v14 a4 x7 C a3 x2 C v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID sex coords act lv)]],
  78. '0078' => ['actor_exists', 'a4 v14 a4 a2 v2 C2 a3 C3 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 act lv)]], #standing
  79. # OLD'0079' => ['actor_connected', 'a4 v14 a4 x7 C a3 x2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID sex coords lv)]],
  80. '0079' => ['actor_connected', 'a4 v14 a4 a2 v2 C2 a3 C2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 lv)]], #spawning
  81. '007A' => ['changeToInGameState'],
  82. # OLD '007B' => ['actor_moved', 'a4 v8 x4 v6 a4 x7 C a5 x3 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon lowhead shield tophead midhead hair_color clothes_color head_dir guildID sex coords lv)]], #walking
  83. '007B' => ['actor_moved', 'a4 v8 V v6 a4 a2 v2 C2 a6 C2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon lowhead tick shield tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 lv)]], #walking
  84. #VERY OLD '007C' => ['actor_exists', 'a4 v1 v1 v1 v1 x6 v1 C1 x12 C1 a3', [qw(ID walk_speed opt1 opt2 option type pet sex coords)]],
  85. #OLD '007C' => ($rpackets{'007C'} == 41 # or 42
  86. #OLD ? ['actor_exists', 'x a4 v14 C2 a3 C', [qw(ID walk_speed opt1 opt2 option hair_style weapon lowhead type shield tophead midhead hair_color clothes_color head_dir stance sex coords unknown1)]]
  87. #OLD : ['actor_exists', 'x a4 v14 C2 a3 C2', [qw(ID walk_speed opt1 opt2 option hair_style weapon lowhead type shield tophead midhead hair_color clothes_color head_dir stance sex coords unknown1 unknown2)]]
  88. #OLD),
  89. '007C' => ['actor_spawned', 'a4 v14 C2 a3 C2', [qw(ID walk_speed opt1 opt2 option hair_style weapon lowhead type shield tophead midhead hair_color clothes_color head_dir stance sex coords unknown1 unknown2)]], #spawning: eA does not send this for players
  90. '007F' => ['received_sync', 'V', [qw(time)]],
  91. '0080' => ['actor_died_or_disappeared', 'a4 C', [qw(ID type)]],
  92. '0081' => ['errors', 'C', [qw(type)]],
  93. '0086' => ['actor_display', 'a4 a6 V', [qw(ID coords tick)]],
  94. '0087' => ['character_moves', 'a4 a6', [qw(move_start_time coords)]], # 12
  95. '0088' => ['actor_movement_interrupted', 'a4 v2', [qw(ID x y)]],
  96. '008A' => ['actor_action', 'a4 a4 a4 V2 v2 C v', [qw(sourceID targetID tick src_speed dst_speed damage div type dual_wield_damage)]],
  97. '008D' => ['public_chat', 'v a4 Z*', [qw(len ID message)]],
  98. '008E' => ['self_chat', 'x2 Z*', [qw(message)]],
  99. '0091' => ['map_change', 'Z16 v2', [qw(map x y)]],
  100. '0092' => ['map_changed', 'Z16 v2 a4 v', [qw(map x y IP port)]], # 28
  101. '0095' => ['actor_info', 'a4 Z24', [qw(ID name)]],
  102. '0097' => ['private_message', 'v Z24 Z*', [qw(len privMsgUser privMsg)]],
  103. '0098' => ['private_message_sent', 'C', [qw(type)]],
  104. '009A' => ['system_chat', 'v a*', [qw(len message)]], #maybe use a* instead and $message =~ /\000$//; if there are problems
  105. '009C' => ['actor_look_at', 'a4 v C', [qw(ID head body)]],
  106. '009D' => ['item_exists', 'a4 v C v3 C2', [qw(ID nameID identified x y amount subx suby)]],
  107. '009E' => ['item_appeared', 'a4 v C v2 C2 v', [qw(ID nameID identified x y subx suby amount)]],
  108. '00A0' => ['inventory_item_added', 'v3 C3 a8 v C2', [qw(index amount nameID identified broken upgrade cards type_equip type fail)]],
  109. '00A1' => ['item_disappeared', 'a4', [qw(ID)]],
  110. '00A3' => ['inventory_items_stackable', 'v a*', [qw(len itemInfo)]],
  111. '00A4' => ['inventory_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  112. '00A5' => ['storage_items_stackable', 'v a*', [qw(len itemInfo)]],
  113. '00A6' => ['storage_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  114. '00A8' => ['use_item', 'v x2 C', [qw(index amount)]],
  115. '00AA' => ($rpackets{'00AA'} == 7) # or 9
  116. ? ['equip_item', 'v2 C', [qw(index type success)]]
  117. : ['equip_item', 'v3 C', [qw(index type viewid success)]],
  118. '00AC' => ['unequip_item', 'v2 C', [qw(index type success)]],
  119. '00AF' => ['inventory_item_removed', 'v2', [qw(index amount)]],
  120. '00B0' => ['stat_info', 'v V', [qw(type val)]],
  121. '00B1' => ['stat_info', 'v V', [qw(type val)]], # was "exp_zeny_info"
  122. '00B3' => ['switch_character', 'C', [qw(result)]], # 3
  123. '00B4' => ['npc_talk', 'v a4 Z*', [qw(len ID msg)]],
  124. '00B5' => ['npc_talk_continue', 'a4', [qw(ID)]],
  125. '00B6' => ['npc_talk_close', 'a4', [qw(ID)]],
  126. '00B7' => ['npc_talk_responses'],
  127. '00BC' => ['stats_added', 'v x C', [qw(type val)]], # actually 'v C2', 'type result val'
  128. '00BD' => ['stats_info', 'v C12 v14', [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 stance manner)]], # (stance manner) actually are (ASPD plusASPD)
  129. '00BE' => ['stat_info', 'v C', [qw(type val)]], # was "stats_points_needed"
  130. '00C0' => ['emoticon', 'a4 C', [qw(ID type)]],
  131. '00CA' => ['buy_result', 'C', [qw(fail)]],
  132. '00CB' => ['sell_result', 'C', [qw(fail)]], # 3
  133. '00C2' => ['users_online', 'V', [qw(users)]],
  134. '00C3' => ['job_equipment_hair_change', 'a4 C2', [qw(ID part number)]],
  135. '00C4' => ['npc_store_begin', 'a4', [qw(ID)]],
  136. '00C6' => ['npc_store_info'],
  137. '00C7' => ['npc_sell_list', 'v a*', [qw(len itemsdata)]],
  138. '00D1' => ['ignore_player_result', 'C2', [qw(type error)]],
  139. '00D2' => ['ignore_all_result', 'C2', [qw(type error)]],
  140. '00D4' => ['whisper_list'],
  141. '00D6' => ['chat_created'],
  142. '00D7' => ['chat_info', 'x2 a4 a4 v2 C a*', [qw(ownerID ID limit num_users public title)]],
  143. '00D8' => ['chat_removed', 'a4', [qw(ID)]],
  144. '00DA' => ['chat_join_result', 'C', [qw(type)]],
  145. '00DB' => ['chat_users'],
  146. '00DC' => ['chat_user_join', 'v Z24', [qw(num_users user)]],
  147. '00DD' => ['chat_user_leave', 'v Z24 C', [qw(num_users user flag)]],
  148. '00DF' => ['chat_modified', 'x2 a4 a4 v2 C a*', [qw(ownerID ID limit num_users public title)]],
  149. '00E1' => ['chat_newowner', 'C x3 Z24', [qw(type user)]],
  150. '00E5' => ['deal_request', 'Z24', [qw(user)]],
  151. '00E7' => ['deal_begin', 'C', [qw(type)]],
  152. '00E9' => ['deal_add_other', 'V v C3 a8', [qw(amount nameID identified broken upgrade cards)]],
  153. '00EA' => ['deal_add_you', 'v C', [qw(index fail)]],
  154. '00EC' => ['deal_finalize', 'C', [qw(type)]],
  155. '00EE' => ['deal_cancelled'],
  156. '00F0' => ['deal_complete'],
  157. '00F2' => ['storage_opened', 'v2', [qw(items items_max)]],
  158. '00F4' => ['storage_item_added', 'v V v C3 a8', [qw(index amount nameID identified broken upgrade cards)]],
  159. '00F6' => ['storage_item_removed', 'v V', [qw(index amount)]],
  160. '00F8' => ['storage_closed'],
  161. '00FA' => ['party_organize_result', 'C', [qw(fail)]],
  162. '00FB' => ['party_users_info', 'x2 Z24', [qw(party_name)]],
  163. '00FD' => ['party_invite_result', 'Z24 C', [qw(name type)]],
  164. '00FE' => ['party_invite', 'a4 Z24', [qw(ID name)]],
  165. '0101' => ['party_exp', 'v x2', [qw(type)]],
  166. '0104' => ['party_join', 'a4 V v2 C Z24 Z24 Z16', [qw(ID role x y type name user map)]],
  167. '0105' => ['party_leave', 'a4 Z24 C', [qw(ID name result)]],
  168. '0106' => ['party_hp_info', 'a4 v2', [qw(ID hp hp_max)]],
  169. '0107' => ['party_location', 'a4 v2', [qw(ID x y)]],
  170. '0108' => ['item_upgrade', 'v3', [qw(type index upgrade)]],
  171. '0109' => ['party_chat', 'x2 a4 Z*', [qw(ID message)]],
  172. '0110' => ['skill_use_failed', 'v3 C2', [qw(skillID btype unknown fail type)]],
  173. '010A' => ['mvp_item', 'v', [qw(itemID)]],
  174. '010B' => ['mvp_you', 'V', [qw(expAmount)]],
  175. '010C' => ['mvp_other', 'a4', [qw(ID)]],
  176. '010E' => ['skill_update', 'v4 C', [qw(skillID lv sp range up)]], # range = skill range, up = this skill can be leveled up further
  177. '010F' => ['skills_list'],
  178. '0111' => ['skill_add', 'v2 x2 v3 Z24', [qw(skillID target lv sp range name)]],
  179. '0114' => ['skill_use', 'v a4 a4 V3 v3 C', [qw(skillID sourceID targetID tick src_speed dst_speed damage level option type)]],
  180. '0117' => ['skill_use_location', 'v a4 v3 V', [qw(skillID sourceID lv x y tick)]],
  181. '0119' => ['character_status', 'a4 v3 C', [qw(ID opt1 opt2 option stance)]],
  182. '011A' => ['skill_used_no_damage', 'v2 a4 a4 C', [qw(skillID amount targetID sourceID success)]],
  183. '011C' => ['warp_portal_list', 'v Z16 Z16 Z16 Z16', [qw(type memo1 memo2 memo3 memo4)]],
  184. '011E' => ['memo_success', 'C', [qw(fail)]],
  185. '011F' => ['area_spell', 'a4 a4 v2 C2', [qw(ID sourceID x y type fail)]],
  186. '0120' => ['area_spell_disappears', 'a4', [qw(ID)]],
  187. '0121' => ['cart_info', 'v2 V2', [qw(items items_max weight weight_max)]],
  188. '0122' => ['cart_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  189. '0123' => ['cart_items_stackable', 'v a*', [qw(len itemInfo)]],
  190. '0124' => ['cart_item_added', 'v V v C3 a8', [qw(index amount nameID identified broken upgrade cards)]],
  191. '0125' => ['cart_item_removed', 'v V', [qw(index amount)]],
  192. '012B' => ['cart_off'],
  193. '012C' => ['cart_add_failed', 'C', [qw(fail)]],
  194. '012D' => ['shop_skill', 'v', [qw(number)]],
  195. '0131' => ['vender_found', 'a4 A80', [qw(ID title)]],
  196. '0132' => ['vender_lost', 'a4', [qw(ID)]],
  197. '0133' => ['vender_items_list', 'v a4', [qw(len venderID)]],
  198. '0135' => ['vender_buy_fail', 'v2 C', [qw(index amount fail)]],
  199. '0136' => ['vending_start'],
  200. '0137' => ['shop_sold', 'v2', [qw(number amount)]],
  201. '0139' => ['monster_ranged_attack', 'a4 v5', [qw(ID sourceX sourceY targetX targetY range)]],
  202. '013A' => ['attack_range', 'v', [qw(type)]],
  203. '013B' => ['arrow_none', 'v', [qw(type)]],
  204. '013C' => ['arrow_equipped', 'v', [qw(index)]],
  205. '013D' => ['hp_sp_changed', 'v2', [qw(type amount)]],
  206. '013E' => ['skill_cast', 'a4 a4 v5 V', [qw(sourceID targetID x y skillID unknown type wait)]],
  207. '0141' => ['stat_info2', 'V2 l', [qw(type val val2)]],
  208. '0142' => ['npc_talk_number', 'a4', [qw(ID)]],
  209. '0144' => ['minimap_indicator', 'a4 V3 C5', [qw(npcID type x y ID blue green red alpha)]],
  210. '0147' => ['item_skill', 'v6 A*', [qw(skillID targetType unknown skillLv sp unknown2 skillName)]],
  211. '0148' => ['resurrection', 'a4 v', [qw(targetID type)]],
  212. '014A' => ['manner_message', 'V', [qw(type)]],
  213. '014B' => ['GM_silence', 'C Z24', [qw(type name)]],
  214. '014C' => ['guild_allies_enemy_list'],
  215. '014E' => ['guild_master_member', 'V', [qw(type)]],
  216. '0152' => ['guild_emblem', 'v a4 a4 a*', [qw(len guildID emblemID emblem)]],
  217. '0154' => ['guild_members_list'],
  218. '0156' => ['guild_member_position_changed', 'v V3', [qw(unknown accountID charID positionID)]],
  219. '015A' => ['guild_leave', 'Z24 Z40', [qw(name message)]],
  220. '015C' => ['guild_expulsion', 'Z24 Z40 Z24', [qw(name message unknown)]],
  221. '015E' => ['guild_broken', 'V', [qw(flag)]], # clif_guild_broken
  222. '0160' => ['guild_member_setting_list'],
  223. '0162' => ['guild_skills_list'],
  224. '0163' => ['guild_expulsionlist'],
  225. '0166' => ['guild_members_title_list'],
  226. '0167' => ['guild_create_result', 'C', [qw(type)]],
  227. '0169' => ['guild_invite_result', 'C', [qw(type)]],
  228. '016A' => ['guild_request', 'a4 Z24', [qw(ID name)]],
  229. '016C' => ['guild_name', 'a4 a4 V x5 Z24', [qw(guildID emblemID mode guildName)]],
  230. '016D' => ['guild_member_online_status', 'a4 a4 V', [qw(ID charID online)]],
  231. '016F' => ['guild_notice'],
  232. '0171' => ['guild_ally_request', 'a4 Z24', [qw(ID guildName)]],
  233. '0173' => ['guild_alliance', 'C', [qw(flag)]],
  234. '0174' => ['guild_position_changed', 'v a4 a4 a4 V Z20', [qw(unknown ID mode sameID exp position_name)]],
  235. '0177' => ['identify_list'],
  236. '0179' => ['identify', 'v C', [qw(index flag)]],
  237. '017B' => ['card_merge_list'],
  238. '017D' => ['card_merge_status', 'v2 C', [qw(item_index card_index fail)]],
  239. '017F' => ['guild_chat', 'x2 Z*', [qw(message)]],
  240. '0181' => ['guild_opposition_result', 'C', [qw(flag)]], # clif_guild_oppositionack
  241. '0182' => ['guild_member_add', 'a4 a4 v5 V3 Z50 Z24', [qw(AID GID head_type head_color sex job lv contribution_exp current_state positionID intro name)]], # 106 # TODO: rename the vars and add sub
  242. '0184' => ['guild_unally', 'a4 V', [qw(guildID flag)]], # clif_guild_delalliance
  243. '0185' => ['guild_alliance_added', 'a4 a4 Z24', [qw(opposition alliance_guildID name)]], # clif_guild_allianceadded
  244. '0187' => ['sync_request', 'a4', [qw(ID)]],
  245. '0188' => ['item_upgrade', 'v3', [qw(type index upgrade)]],
  246. '0189' => ['no_teleport', 'v', [qw(fail)]],
  247. '018B' => ['quit_response', 'v', [qw(fail)]], # 4 # ported from kRO_Sakexe_0
  248. '018C' => ['sense_result', 'v3 V v4 C9', [qw(nameID level size hp def race mdef element ice earth fire wind poison holy dark spirit undead)]],
  249. '018D' => ['forge_list'],
  250. '018F' => ['refine_result', 'v2', [qw(fail nameID)]],
  251. '0191' => ['talkie_box', 'a4 Z80', [qw(ID message)]], # talkie box message
  252. '0192' => ['map_change_cell', 'v3 Z16', [qw(x y type map_name)]], # ex. due to ice wall
  253. '0194' => ['character_name', 'a4 Z24', [qw(ID name)]],
  254. '0195' => ['actor_info', 'a4 Z24 Z24 Z24 Z24', [qw(ID name partyName guildName guildTitle)]],
  255. '0196' => ['actor_status_active', 'v a4 C', [qw(type ID flag)]],
  256. '0199' => ['map_property', 'v', [qw(type)]],
  257. '019A' => ['pvp_rank', 'V3', [qw(ID rank num)]],
  258. '019B' => ['unit_levelup', 'a4 V', [qw(ID type)]],
  259. '019E' => ['pet_capture_process'],
  260. '01A0' => ['pet_capture_result', 'C', [qw(success)]],
  261. #'01A2' => ($rpackets{'01A2'} == 35 # or 37
  262. # ? ['pet_info', 'Z24 C v4', [qw(name renameflag level hungry friendly accessory)]]
  263. # : ['pet_info', 'Z24 C v5', [qw(name renameflag level hungry friendly accessory type)]]
  264. #),
  265. '01A2' => ['pet_info', 'Z24 C v5', [qw(name renameflag level hungry friendly accessory type)]],
  266. '01A3' => ['pet_food', 'C v', [qw(success foodID)]],
  267. '01A4' => ['pet_info2', 'C a4 V', [qw(type ID value)]],
  268. '01A6' => ['egg_list'],
  269. '01AA' => ['pet_emotion', 'a4 V', [qw(ID type)]],
  270. '01AB' => ['stat_info', 'a4 v V', [qw(ID type val)]], # was "actor_muted"; is struct/handler correct at all?
  271. '01AC' => ['actor_trapped', 'a4', [qw(ID)]],
  272. '01AD' => ['arrowcraft_list'],
  273. '01B0' => ['monster_typechange', 'a4 a V', [qw(ID unknown type)]],
  274. '01B3' => ['npc_image', 'Z64 C', [qw(npc_image type)]],
  275. '01B4' => ['guild_emblem_update', 'a4 a4 a2', [qw(ID guildID emblemID)]],
  276. '01B5' => ['account_payment_info', 'V2', [qw(D_minute H_minute)]],
  277. '01B6' => ['guild_info', 'a4 V9 a4 Z24 Z24 Z20', [qw(ID lv conMember maxMember average exp exp_next tax tendency_left_right tendency_down_up emblemID name master castles_string)]],
  278. '01B9' => ['cast_cancelled', 'a4', [qw(ID)]],
  279. '01C3' => ['local_broadcast', 'x2 a3 x9 Z*', [qw(color message)]],
  280. '01C4' => ['storage_item_added', 'v V v C4 a8', [qw(index amount nameID type identified broken upgrade cards)]],
  281. '01C5' => ['cart_item_added', 'v V v C4 a8', [qw(index amount nameID type identified broken upgrade cards)]],
  282. '01C8' => ['item_used', 'v2 a4 v C', [qw(index itemID ID remaining success)]],
  283. '01C9' => ['area_spell', 'a4 a4 v2 C2 C Z80', [qw(ID sourceID x y type fail scribbleLen scribbleMsg)]],
  284. '01CD' => ['sage_autospell', 'x2 a*', [qw(autospell_list)]],
  285. '01CF' => ['devotion', 'a4 a20 v', [qw(sourceID targetIDs range)]],
  286. '01D0' => ['revolving_entity', 'a4 v', [qw(sourceID entity)]],
  287. '01D1' => ['blade_stop', 'a4 a4 V', [qw(sourceID targetID active)]],
  288. '01D2' => ['combo_delay', 'a4 V', [qw(ID delay)]],
  289. '01D3' => ['sound_effect', 'Z24 C V a4', [qw(name type term ID)]],
  290. '01D4' => ['npc_talk_text', 'a4', [qw(ID)]],
  291. '01D7' => ['player_equipment', 'a4 C v2', [qw(sourceID type ID1 ID2)]],
  292. # OLD' 01D8' => ['actor_exists', 'a4 v14 a4 x4 v x C a3 x2 C v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID skillstatus sex coords act lv)]],
  293. '01D8' => ['actor_exists', 'a4 v14 a4 a2 v2 C2 a3 C3 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 act lv)]], # standing
  294. # OLD '01D9' => ['actor_connected', 'a4 v14 a4 x4 v x C a3 x2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID skillstatus sex coords lv)]],
  295. '01D9' => ['actor_connected', 'a4 v14 a4 a2 v2 C2 a3 C2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 lv)]], # spawning
  296. # OLD '01DA' => ['actor_moved', 'a4 v5 C x v3 x4 v5 a4 x4 v x C a5 x3 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID skillstatus sex coords lv)]],
  297. '01DA' => ['actor_moved', 'a4 v9 V v5 a4 a2 v2 C2 a6 C2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tick tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 lv)]], # walking
  298. '01DC' => ['secure_login_key', 'x2 a*', [qw(secure_key)]],
  299. '01D6' => ['map_property2', 'v', [qw(type)]],
  300. '01DE' => ['skill_use', 'v a4 a4 V4 v2 C', [qw(skillID sourceID targetID tick src_speed dst_speed damage level option type)]],
  301. '01E0' => ['GM_req_acc_name', 'a4 Z24', [qw(targetID accountName)]],
  302. '01E1' => ['revolving_entity', 'a4 v', [qw(sourceID entity)]],
  303. #'01E2' => ['marriage_unknown'], clif_parse_ReqMarriage
  304. #'01E4' => ['marriage_unknown'], clif_marriage_process
  305. ##
  306. '01E6' => ['marriage_partner_name', 'Z24', [qw(name)]],
  307. '01E9' => ['party_join', 'a4 V v2 C Z24 Z24 Z16 v C2', [qw(ID role x y type name user map lv item_pickup item_share)]],
  308. '01EA' => ['married', 'a4', [qw(ID)]],
  309. '01EB' => ['guild_location', 'a4 v2', [qw(ID x y)]],
  310. '01EC' => ['guild_member_map_change', 'a4 a4 Z16', [qw(GDID AID mapName)]], # 26 # TODO: change vars, add sub
  311. '01EE' => ['inventory_items_stackable', 'v a*', [qw(len itemInfo)]],
  312. '01EF' => ['cart_items_stackable', 'v a*', [qw(len itemInfo)]],
  313. '01F0' => ['storage_items_stackable', 'v a*', [qw(len itemInfo)]],
  314. '01F2' => ['guild_member_online_status', 'a4 a4 V v3', [qw(ID charID online sex hair_style hair_color)]],
  315. '01F3' => ['misc_effect', 'a4 V', [qw(ID effect)]], # weather/misceffect2 packet
  316. '01F4' => ['deal_request', 'Z24 a4 v', [qw(user ID level)]],
  317. '01F5' => ['deal_begin', 'C a4 v', [qw(type targetID level)]],
  318. '01F6' => ['adopt_request', 'a4 a4 Z24', [qw(sourceID targetID name)]],
  319. #'01F8' => ['adopt_unknown'], # clif_adopt_process
  320. '01FC' => ['repair_list'],
  321. '01FE' => ['repair_result', 'v C', [qw(nameID flag)]],
  322. '01FF' => ['high_jump', 'a4 v2', [qw(ID x y)]],
  323. '0201' => ['friend_list'],
  324. '0205' => ['divorced', 'Z24', [qw(name)]], # clif_divorced
  325. '0206' => ['friend_logon', 'a4 a4 C', [qw(friendAccountID friendCharID isNotOnline)]],
  326. '0207' => ['friend_request', 'a4 a4 Z24', [qw(accountID charID name)]],
  327. '0209' => ['friend_response', 'v a4 a4 Z24', [qw(type accountID charID name)]],
  328. '020A' => ['friend_removed', 'a4 a4', [qw(friendAccountID friendCharID)]],
  329. '020E' => ['taekwon_packets', 'Z24 a4 C2', [qw(name ID value flag)]],
  330. '020F' => ['pvp_point', 'V2', [qw(AID GID)]], #TODO: PACKET_CZ_REQ_PVPPOINT
  331. '0215' => ['gospel_buff_aligned', 'a4', [qw(ID)]],
  332. '0216' => ['adopt_reply', 'V', [qw(type)]],
  333. '0219' => ['top10_blacksmith_rank'],
  334. '021A' => ['top10_alchemist_rank'],
  335. '021B' => ['blacksmith_points', 'V2', [qw(points total)]],
  336. '021C' => ['alchemist_point', 'V2', [qw(points total)]],
  337. '0221' => ['upgrade_list'],
  338. '0223' => ['upgrade_message', 'a4 v', [qw(type itemID)]],
  339. '0224' => ['taekwon_rank', 'V2', [qw(type rank)]],
  340. '0226' => ['top10_taekwon_rank'],
  341. '0227' => ['gameguard_request'],
  342. '0229' => ['character_status', 'a4 v2 V C', [qw(ID opt1 opt2 option stance)]],
  343. # OLD '022A' => ['actor_exists', 'a4 v4 x2 v8 x2 v a4 a4 v x2 C2 a3 x2 C v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color head_dir guildID emblemID visual_effects stance sex coords act lv)]],
  344. '022A' => ['actor_exists', 'a4 v3 V v10 a4 a2 v V C2 a3 C3 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 act lv)]], # standing
  345. # OLD '022B' => ['actor_connected', 'a4 v4 x2 v8 x2 v a4 a4 v x2 C2 a3 x2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color head_dir guildID emblemID visual_effects stance sex coords lv)]],
  346. '022B' => ['actor_connected', 'a4 v3 V v10 a4 a2 v V C2 a3 C2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 lv)]], # spawning
  347. # OLD '022C' => ['actor_moved', 'a4 v4 x2 v5 V v3 x4 a4 a4 v x2 C2 a5 x3 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead timestamp tophead midhead hair_color guildID emblemID visual_effects stance sex coords lv)]],
  348. '022C' => ['actor_moved', 'a4 v3 V v5 V v5 a4 a2 v V C2 a6 C2 v', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tick tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords unknown1 unknown2 lv)]], # walking
  349. '022E' => ['homunculus_property', 'Z24 C v16 V2 v2', [qw(name state level hunger intimacy accessory atk matk hit critical def mdef flee aspd hp hp_max sp sp_max exp exp_max points_skill attack_range)]],
  350. '022F' => ['homunculus_food', 'C v', [qw(success foodID)]],
  351. '0230' => ['homunculus_info', 'C2 a4 V',[qw(type state ID val)]],
  352. '0235' => ['skills_list'], # homunculus skills
  353. '0238' => ['top10_pk_rank'],
  354. # homunculus skill update
  355. '0239' => ['skill_update', 'v4 C', [qw(skillID lv sp range up)]], # range = skill range, up = this skill can be leveled up further
  356. '023A' => ['storage_password_request', 'v', [qw(flag)]],
  357. '023C' => ['storage_password_result', 'v2', [qw(type val)]],
  358. '023E' => ['storage_password_request', 'v', [qw(flag)]],
  359. '0240' => ['mail_refreshinbox', 'v V', [qw(size count)]],
  360. '0242' => ['mail_read', 'v V Z40 Z24 V3 v2 C3 a8 C Z*', [qw(len mailID title sender delete_time zeny amount nameID type identified broken upgrade cards msg_len message)]],
  361. '0245' => ['mail_getattachment', 'C', [qw(fail)]],
  362. '0249' => ['mail_send', 'C', [qw(fail)]],
  363. '024A' => ['mail_new', 'V Z40 Z24', [qw(mailID title sender)]],
  364. '0250' => ['auction_result', 'C', [qw(flag)]],
  365. '0252' => ['auction_item_request_search', 'v V2', [qw(size pages count)]],
  366. '0255' => ['mail_setattachment', 'v C', [qw(index fail)]],
  367. '0256' => ['auction_add_item', 'v C', [qw(index fail)]],
  368. '0257' => ['mail_delete', 'V v', [qw(mailID fail)]],
  369. '0259' => ['gameguard_grant', 'C', [qw(server)]],
  370. '025A' => ['cooking_list', 'v', [qw(type)]],
  371. '025D' => ['auction_my_sell_stop', 'V', [qw(flag)]],
  372. '025F' => ['auction_windows', 'V C4 v', [qw(flag unknown1 unknown2 unknown3 unknown4 unknown5)]],
  373. '0260' => ['mail_window', 'v', [qw(flag)]],
  374. '0274' => ['mail_return', 'V v', [qw(mailID fail)]],
  375. # mail_return packet: '0274' => ['account_server_info', 'x2 a4 a4 a4 x30 C1 x4 a*', [qw(sessionID accountID sessionID2 accountSex serverInfo)]],
  376. '027B' => ['premium_rates_info', 'V3', [qw(exp death drop)]],
  377. # tRO new packets, need some work on them
  378. '0283' => ['account_id', 'a4', [qw(accountID)]],
  379. '0284' => ['GANSI_RANK', 'c24 c24 c24 c24 c24 c24 c24 c24 c24 c24 V10 v', [qw(name1 name2 name3 name4 name5 name6 name7 name8 name9 name10 pt1 pt2 pt3 pt4 pt5 pt6 pt7 pt8 pt9 pt10 switch)]], #TODO: PACKET_ZC_GANGSI_RANK
  380. '0287' => ['cash_dealer'],
  381. '0289' => ['cash_buy_fail', 'V2 v', [qw(cash_points kafra_points fail)]],
  382. '028A' => ['character_status', 'a4 V3', [qw(ID option lv opt3)]],
  383. '0291' => ['message_string', 'v', [qw(msg_id)]],
  384. '0293' => ['boss_map_info', 'C V2 v2 x4 Z24', [qw(flag x y hours minutes name)]],
  385. '0294' => ['book_read', 'a4 a4', [qw(bookID page)]],
  386. '0295' => ['inventory_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  387. '0296' => ['storage_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  388. '0297' => ['cart_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  389. '0298' => ['rental_time', 'v V', [qw(nameID seconds)]],
  390. '0299' => ['rental_expired', 'v2', [qw(unknown nameID)]],
  391. '029A' => ['inventory_item_added', 'v3 C3 a8 v C2 a4', [qw(index amount nameID identified broken upgrade cards type_equip type fail cards_ext)]],
  392. '029B' => ($rpackets{'029B'} == 72 # or 80
  393. ? ['mercenary_init', 'a4 v8 Z24 v5 V v2', [qw(ID atk matk hit critical def mdef flee aspd name level hp hp_max sp sp_max contract_end faith summons)]]
  394. : ['mercenary_init', 'a4 v8 Z24 v V5 v V2 v', [qw(ID atk matk hit critical def mdef flee aspd name level hp hp_max sp sp_max contract_end faith summons kills attack_range)]]
  395. ),
  396. '029D' => ['skills_list'], # mercenary skills
  397. '02A2' => ['stat_info', 'v V', [qw(type val)]], # was "mercenary_param_change"
  398. # tRO HShield packet challenge.
  399. # Borrow sub gameguard_request because it use the same mechanic.
  400. '02A6' => ['gameguard_request'],
  401. '02AA' => ['cash_password_request', 'v', [qw(info)]], #TODO: PACKET_ZC_REQ_CASH_PASSWORD
  402. '02AC' => ['cash_password_result', 'v2', [qw(info count)]], #TODO: PACKET_ZC_RESULT_CASH_PASSWORD
  403. # mRO PIN code Check
  404. '02AD' => ['login_pin_code_request', 'v V', [qw(flag key)]],
  405. # Packet Prefix encryption Support
  406. '02AE' => ['initialize_message_id_encryption', 'V2', [qw(param1 param2)]],
  407. # tRO new packets (2008-09-16Ragexe12_Th)
  408. '02B1' => ['quest_all_list', 'v V', [qw(len amount)]],
  409. '02B2' => ['quest_all_mission', 'v V', [qw(len amount)]], # var len
  410. '02B3' => ['quest_add', 'V C V2 v', [qw(questID active time_start time amount)]],
  411. '02B4' => ['quest_delete', 'V', [qw(questID)]],
  412. '02B5' => ['quest_update_mission_hunt', 'v2 a*', [qw(len amount mobInfo)]], # var len
  413. '02B7' => ['quest_active', 'V C', [qw(questID active)]],
  414. '02B8' => ['party_show_picker', 'a4 v C3 a8 v C', [qw(sourceID nameID identified broken upgrade cards location type)]],
  415. '02B9' => ['hotkeys'],
  416. '02C5' => ['party_invite_result', 'Z24 V', [qw(name type)]],
  417. '02C6' => ['party_invite', 'a4 Z24', [qw(ID name)]],
  418. '02C9' => ['party_allow_invite', 'C', [qw(type)]],
  419. '02CA' => ['login_error_game_login_server', 'C', [qw(type)]],
  420. '02CB' => ['instance_window_start', 'Z61 v', [qw(name flag)]],
  421. '02CC' => ['instance_window_queue', 'C', [qw(flag)]],
  422. '02CD' => ['instance_window_join', 'Z61 V2', [qw(name time_remaining time_close)]],
  423. '02CE' => ['instance_window_leave', 'C', [qw(flag)]],
  424. '02D0' => ['inventory_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  425. '02D1' => ['storage_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  426. '02D2' => ['cart_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  427. '02D4' => ['inventory_item_added', 'v3 C3 a8 v C2 a4 v', [qw(index amount nameID identified broken upgrade cards type_equip type fail expire unknown)]],
  428. '02D5' => ['ISVR_DISCONNECT'], #TODO: PACKET_ZC_ISVR_DISCONNECT
  429. '02D7' => ['show_eq', 'v Z24 v7 C a*', [qw(len name type hair_style tophead midhead lowhead hair_color clothes_color sex equips_info)]], #type is job
  430. '02D9' => ['show_eq_msg_other', 'V2', [qw(unknown flag)]],
  431. '02DA' => ['show_eq_msg_self', 'C', [qw(type)]],
  432. '02DC' => ['battleground_message', 'v a4 Z24 Z*', [qw(len ID name message)]],
  433. '02DD' => ['battleground_emblem', 'a4 Z24 v', [qw(emblemID name ID)]],
  434. '02DE' => ['battleground_score', 'v2', [qw(score_lion score_eagle)]],
  435. '02DF' => ['battleground_position', 'a4 Z24 v3', [qw(ID name job x y)]],
  436. '02E0' => ['battleground_hp', 'a4 Z24 v2', [qw(ID name hp max_hp)]],
  437. # 02E1 packet unsure of dual_wield_damage needs more testing
  438. # a4 a4 a4 V3 v C V ?
  439. #'02E1' => ['actor_action', 'a4 a4 a4 V2 v x2 v x2 C v', [qw(sourceID targetID tick src_speed dst_speed damage div type dual_wield_damage)]],
  440. '02E1' => ['actor_action', 'a4 a4 a4 V3 v C V', [qw(sourceID targetID tick src_speed dst_speed damage div type dual_wield_damage)]],
  441. '02E7' => ['map_property', 'v2 a*', [qw(len type info_table)]],
  442. '02E8' => ['inventory_items_stackable', 'v a*', [qw(len itemInfo)]],
  443. '02E9' => ['cart_items_stackable', 'v a*', [qw(len itemInfo)]],
  444. '02EA' => ['storage_items_stackable', 'v a*', [qw(len itemInfo)]],
  445. '02EB' => ['map_loaded', 'V a3 x2 v', [qw(syncMapSync coords unknown)]],
  446. '02EC' => ['actor_exists', 'x a4 v3 V v5 V v5 a4 a4 V C2 a6 x2 v2',[qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tick tophead midhead hair_color clothes_color head_dir guildID emblemID opt3 stance sex coords lv unknown)]], # Moving
  447. '02ED' => ['actor_connected', 'a4 v3 V v10 a4 a4 V C2 a3 v3', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID opt3 stance sex coords act lv unknown)]], # Spawning
  448. '02EE' => ['actor_moved', 'a4 v3 V v10 a4 a4 V C2 a3 x v3', [qw(ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID opt3 stance sex coords act lv unknown)]], # Standing
  449. '02EF' => ['font', 'a4 v', [qw(ID fontID)]],
  450. '02F0' => ['progress_bar', 'V2', [qw(color time)]],
  451. '02F2' => ['progress_bar_stop'],
  452.  
  453. '040C' => ['local_broadcast', 'v a4 v4 Z*', [qw(len color font_type font_size font_align font_y message)]], #TODO: PACKET_ZC_BROADCAST3
  454. '043D' => ['skill_post_delay', 'v V', [qw(ID time)]],
  455. '043E' => ['skill_post_delaylist'],
  456. '043F' => ['actor_status_active', 'v a4 C V4', [qw(type ID flag tick unknown1 unknown2 unknown3)]],
  457. '0440' => ['millenium_shield', 'a4 v2', [qw(ID num state)]],
  458. '0441' => ['skill_delete', 'v', [qw(ID)]], #TODO: PACKET_ZC_SKILLINFO_DELETE
  459. '0442' => ['sage_autospell', 'x2 V a*', [qw(why autoshadowspell_list)]],
  460. '0444' => ['cash_item_list', 'v V3 c v', [qw(len cash_point price discount_price type item_id)]], #TODO: PACKET_ZC_SIMPLE_CASH_POINT_ITEMLIST
  461. '0446' => ['minimap_indicator', 'a4 v4', [qw(npcID x y effect qtype)]],
  462.  
  463. '0449' => ['hack_shield_alarm'],
  464. '07D8' => ['party_exp', 'V C2', [qw(type itemPickup itemDivision)]],
  465. '07D9' => ['hotkeys'], # 268 # hotkeys:38
  466. '07DB' => ['stat_info', 'v V', [qw(type val)]], # 8
  467. '07E1' => ['skill_update', 'v V v3 C', [qw(skillID type lv sp range up)]],
  468. '07E3' => ['skill_exchange_item', 'V', [qw(type)]], #TODO: PACKET_ZC_ITEMLISTWIN_OPEN
  469. '07E2' => ['msg_string', 'v V', [qw(index para1)]],
  470. '07E6' => ['skill_msg', 'v V', [qw(id msgid)]],
  471. '07E8' => ['captcha_image', 'v a*', [qw(len image)]], # -1
  472. '07E9' => ['captcha_answer', 'v C', [qw(code flag)]], # 5
  473.  
  474. '07F6' => ['exp', 'a4 V v2', [qw(ID val type flag)]], # 14 # type: 1 base, 2 job; flag: 0 normal, 1 quest # TODO: use. I think this replaces the exp gained message trough guildchat hack
  475. '07F7' => ['actor_exists', 'v C a4 v3 V v5 a4 v5 a4 a2 v V C2 a6 C2 v2 Z*', [qw(len object_type ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tick tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords xSize ySize lv font name)]], # -1 # walking
  476. '07F8' => ['actor_connected', 'v C a4 v3 V v10 a4 a2 v V C2 a3 C2 v2 Z*', [qw(len object_type ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords xSize ySize lv font name)]], # -1 # spawning
  477. '07F9' => ['actor_moved', 'v C a4 v3 V v10 a4 a2 v V C2 a3 C3 v2 Z*', [qw(len object_type ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir guildID emblemID manner opt3 stance sex coords xSize ySize act lv font name)]], # -1 # standing
  478. '07FA' => ['inventory_item_removed', 'v3', [qw(reason index amount)]], #//0x07fa,8
  479. '07FB' => ['skill_cast', 'a4 a4 v5 V C', [qw(sourceID targetID x y skillID unknown type wait dispose)]],
  480. '07FC' => ['party_leader', 'V2', [qw(old new)]],
  481. '07FD' => ['special_item_obtain', 'v C v c/Z a*', [qw(len type nameID holder etc)]],
  482. '07FE' => ['sound_effect', 'Z24', [qw(name)]],
  483. '07FF' => ['define_check', 'v V', [qw(len result)]], #TODO: PACKET_ZC_DEFINE_CHECK
  484. '0800' => ['vender_items_list', 'v a4 a4', [qw(len venderID venderCID)]], # -1
  485. '0803' => ['booking_register_request', 'v', [qw(result)]],
  486. '0805' => ['booking_search_request', 'x2 a a*', [qw(IsExistMoreResult innerData)]],
  487. '0807' => ['booking_delete_request', 'v', [qw(result)]],
  488. '0809' => ['booking_insert', 'V Z24 V v8', [qw(index name expire lvl map_id job1 job2 job3 job4 job5 job6)]],
  489. '080A' => ['booking_update', 'V v6', [qw(index job1 job2 job3 job4 job5 job6)]],
  490. '080B' => ['booking_delete', 'V', [qw(index)]],
  491. '080E' => ['party_hp_info', 'a4 V2', [qw(ID hp hp_max)]],
  492. '080F' => ['deal_add_other', 'v C V C3 a8', [qw(nameID type amount identified broken upgrade cards)]], # 0x080F,20
  493. '0810' => ['open_buying_store', 'c', [qw(amount)]],
  494. '0812' => ['open_buying_store_fail', 'v', [qw(result)]],
  495. '0813' => ['open_buying_store_item_list', 'v a4 V', [qw(len AID zeny)]],
  496. '0814' => ['buying_store_found', 'a4 Z*', [qw(ID title)]],
  497. '0816' => ['buying_store_lost', 'a4', [qw(ID)]],
  498. '0818' => ['buying_store_items_list', 'v a4 a4', [qw(len buyerID buyingStoreID zeny)]],
  499. '081C' => ['buying_store_item_delete', 'v2 V', [qw(index amount zeny)]],
  500. '081E' => ['stat_info', 'v V', [qw(type val)]], # 8, Sorcerer's Spirit - not implemented in Kore
  501. '0824' => ['buying_store_fail', 'v2', [qw(result itemID)]],
  502. '0828' => ['char_delete2_result', 'a4 V2', [qw(charID result deleteDate)]], # 14
  503. '082C' => ['char_delete2_cancel_result', 'a4 V', [qw(charID result)]], # 14
  504. '082D' => ['received_characters', 'x2 C5 x20 a*', [qw(normal_slot premium_slot billing_slot producible_slot valid_slot charInfo)]],
  505. '0839' => ['guild_expulsion', 'Z40 Z24', [qw(message name)]],
  506. '083E' => ['login_error', 'V Z20', [qw(type date)]],
  507. '0840' => ['escape_map_select', 'v a*', [qw(len mapInfo)]],
  508. '084B' => ['item_appeared', 'a4 v2 C v4', [qw(ID nameID unknown1 identified x y unknown2 amount)]], # 19 TODO provided by try71023, modified sofax222
  509. '0856' => ['actor_moved', 'v C a4 v3 V v5 a4 v6 a4 a2 v V C2 a6 C2 v2 Z*', [qw(len object_type ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tick tophead midhead hair_color clothes_color head_dir costume guildID emblemID manner opt3 stance sex coords xSize ySize lv font name)]], # -1 # walking provided by try71023 TODO: costume
  510. '0857' => ['actor_exists', 'v C a4 v3 V v11 a4 a2 v V C2 a3 C3 v2 Z*', [qw(len object_type ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir costume guildID emblemID manner opt3 stance sex coords xSize ySize act lv font name)]], # -1 # spawning provided by try71023
  511. '0858' => ['actor_connected', 'v C a4 v3 V v11 a4 a2 v V C2 a3 C2 v2 Z*', [qw(len object_type ID walk_speed opt1 opt2 option type hair_style weapon shield lowhead tophead midhead hair_color clothes_color head_dir costume guildID emblemID manner opt3 stance sex coords xSize ySize lv font name)]], # -1 # standing provided by try71023
  512. '0859' => ['show_eq', 'v Z24 v7 v C a*', [qw(len name jobID hair_style tophead midhead lowhead robe hair_color clothes_color sex equips_info)]],
  513. #'08B9' => ['account_id', 'x4 V v', [qw(accountID unknown)]], # len: 12 Conflict with the struct (found in twRO 29032013)
  514. '08B9' => ['login_pin_code_request', 'V a4 v', [qw(seed accountID flag)]],
  515. '08BB' => ['login_pin_new_code_result', 'v V', [qw(flag seed)]],
  516. '08C7' => ['area_spell', 'x2 a4 a4 v2 C3', [qw(ID sourceID x y type range fail)]], # -1
  517. '08C8' => ['actor_action', 'a4 a4 a4 V3 x v C V', [qw(sourceID targetID tick src_speed dst_speed damage div type dual_wield_damage)]],
  518. '08CB' => ['rates_info', 's4 a*', [qw(len exp death drop detail)]],
  519. '08CF' => ['revolving_entity', 'a4 v v', [qw(sourceID type entity)]],
  520. '08D2' => ['player_jump', 'a4 v2', [qw(ID x y)]],
  521. '0900' => ['inventory_items_stackable', 'v a*', [qw(len itemInfo)]],
  522. '0901' => ['inventory_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  523. '0902' => ['cart_items_stackable', 'v a*', [qw(len itemInfo)]],
  524. '0903' => ['cart_items_nonstackable', 'v a*', [qw(len itemInfo)]],
  525. '0975' => ['storage_items_stackable', 'v Z24 a*', [qw(len title itemInfo)]],
  526. '0976' => ['storage_items_nonstackable', 'v Z24 a*', [qw(len title itemInfo)]],
  527. '0977' => ['monster_hp_info', 'a4 V V', [qw(ID hp hp_max)]],
  528. '097A' => ['quest_all_list2', 'v3 a*', [qw(len count unknown message)]],
  529. '099D' => ['received_characters', 'v a*', [qw(len charInfo)]],
  530. '09A0' => ['sync_received_characters', 'V', [qw(sync_Count)]],
  531. };
  532.  
  533. # Item RECORD Struct's
  534. $self->{nested} = {
  535. items_nonstackable => { # EQUIPMENTITEM_EXTRAINFO
  536. type1 => {
  537. len => 20,
  538. types => 'v2 C2 v2 C2 a8',
  539. keys => [qw(index nameID type identified type_equip equipped broken upgrade cards)],
  540. },
  541. type2 => {
  542. len => 24,
  543. types => 'v2 C2 v2 C2 a8 l',
  544. keys => [qw(index nameID type identified type_equip equipped broken upgrade cards expire)],
  545. },
  546. type3 => {
  547. len => 26,
  548. types => 'v2 C2 v2 C2 a8 l v',
  549. keys => [qw(index nameID type identified type_equip equipped broken upgrade cards expire bindOnEquipType)],
  550. },
  551. type4 => {
  552. len => 28,
  553. types => 'v2 C2 v2 C2 a8 l v2',
  554. keys => [qw(index nameID type identified type_equip equipped broken upgrade cards expire bindOnEquipType sprite_id)],
  555. },
  556. type5 => {
  557. len => 27,
  558. types => 'v2 C v2 C a8 l v2 C',
  559. keys => [qw(index nameID type type_equip equipped upgrade cards expire bindOnEquipType sprite_id identified)],
  560. },
  561. },
  562. items_stackable => {
  563. type1 => {
  564. len => 10,
  565. types => 'v2 C2 v2',
  566. keys => [qw(index nameID type identified amount type_equip)], # type_equip or equipped?
  567. },
  568. type2 => {
  569. len => 18,
  570. types => 'v2 C2 v2 a8',
  571. keys => [qw(index nameID type identified amount type_equip cards)],
  572. },
  573. type3 => {
  574. len => 22,
  575. types => 'v2 C2 v2 a8 l',
  576. keys => [qw(index nameID type identified amount type_equip cards expire)],
  577. },
  578. type5 => {
  579. len => 22,
  580. types => 'v2 C v2 a8 l C',
  581. keys => [qw(index nameID type amount type_equip cards expire identified)],
  582. },
  583. },
  584. };
  585.  
  586. return $self;
  587. }
  588.  
  589. use constant {
  590. REFUSE_INVALID_ID => 0x0,
  591. REFUSE_INVALID_PASSWD => 0x1,
  592. REFUSE_ID_EXPIRED => 0x2,
  593. ACCEPT_ID_PASSWD => 0x3,
  594. REFUSE_NOT_CONFIRMED => 0x4,
  595. REFUSE_INVALID_VERSION => 0x5,
  596. REFUSE_BLOCK_TEMPORARY => 0x6,
  597. REFUSE_BILLING_NOT_READY => 0x7,
  598. REFUSE_NONSAKRAY_ID_BLOCKED => 0x8,
  599. REFUSE_BAN_BY_DBA => 0x9,
  600. REFUSE_EMAIL_NOT_CONFIRMED => 0xa,
  601. REFUSE_BAN_BY_GM => 0xb,
  602. REFUSE_TEMP_BAN_FOR_DBWORK => 0xc,
  603. REFUSE_SELF_LOCK => 0xd,
  604. REFUSE_NOT_PERMITTED_GROUP => 0xe,
  605. REFUSE_WAIT_FOR_SAKRAY_ACTIVE => 0xf,
  606. REFUSE_NOT_CHANGED_PASSWD => 0x10,
  607. REFUSE_BLOCK_INVALID => 0x11,
  608. REFUSE_WARNING => 0x12,
  609. REFUSE_NOT_OTP_USER_INFO => 0x13,
  610. REFUSE_OTP_AUTH_FAILED => 0x14,
  611. REFUSE_SSO_AUTH_FAILED => 0x15,
  612. REFUSE_NOT_ALLOWED_IP_ON_TESTING => 0x16,
  613. REFUSE_OVER_BANDWIDTH => 0x17,
  614. REFUSE_OVER_USERLIMIT => 0x18,
  615. REFUSE_UNDER_RESTRICTION => 0x19,
  616. REFUSE_BY_OUTER_SERVER => 0x1a,
  617. REFUSE_BY_UNIQUESERVER_CONNECTION => 0x1b,
  618. REFUSE_BY_AUTHSERVER_CONNECTION => 0x1c,
  619. REFUSE_BY_BILLSERVER_CONNECTION => 0x1d,
  620. REFUSE_BY_AUTH_WAITING => 0x1e,
  621. REFUSE_DELETED_ACCOUNT => 0x63,
  622. REFUSE_ALREADY_CONNECT => 0x64,
  623. REFUSE_TEMP_BAN_HACKING_INVESTIGATION => 0x65,
  624. REFUSE_TEMP_BAN_BUG_INVESTIGATION => 0x66,
  625. REFUSE_TEMP_BAN_DELETING_CHAR => 0x67,
  626. REFUSE_TEMP_BAN_DELETING_SPOUSE_CHAR => 0x68,
  627. REFUSE_USER_PHONE_BLOCK => 0x69,
  628. ACCEPT_LOGIN_USER_PHONE_BLOCK => 0x6a,
  629. ACCEPT_LOGIN_CHILD => 0x6b,
  630. REFUSE_IS_NOT_FREEUSER => 0x6c,
  631. REFUSE_INVALID_ONETIMELIMIT => 0x6d,
  632. REFUSE_CHANGE_PASSWD_FORCE => 0x6e,
  633. REFUSE_OUTOFDATE_PASSWORD => 0x6f,
  634. REFUSE_NOT_CHANGE_ACCOUNTID => 0xf0,
  635. REFUSE_NOT_CHANGE_CHARACTERID => 0xf1,
  636. REFUSE_SSO_AUTH_BLOCK_USER => 0x1394,
  637. REFUSE_SSO_AUTH_GAME_APPLY => 0x1395,
  638. REFUSE_SSO_AUTH_INVALID_GAMENUM => 0x1396,
  639. REFUSE_SSO_AUTH_INVALID_USER => 0x1397,
  640. REFUSE_SSO_AUTH_OTHERS => 0x1398,
  641. REFUSE_SSO_AUTH_INVALID_AGE => 0x1399,
  642. REFUSE_SSO_AUTH_INVALID_MACADDRESS => 0x139a,
  643. REFUSE_SSO_AUTH_BLOCK_ETERNAL => 0x13c6,
  644. REFUSE_SSO_AUTH_BLOCK_ACCOUNT_STEAL => 0x13c7,
  645. REFUSE_SSO_AUTH_BLOCK_BUG_INVESTIGATION => 0x13c8,
  646. REFUSE_SSO_NOT_PAY_USER => 0x13ba,
  647. REFUSE_SSO_ALREADY_LOGIN_USER => 0x13bb,
  648. REFUSE_SSO_CURRENT_USED_USER => 0x13bc,
  649. REFUSE_SSO_OTHER_1 => 0x13bd,
  650. REFUSE_SSO_DROP_USER => 0x13be,
  651. REFUSE_SSO_NOTHING_USER => 0x13bf,
  652. REFUSE_SSO_OTHER_2 => 0x13c0,
  653. REFUSE_SSO_WRONG_RATETYPE_1 => 0x13c1,
  654. REFUSE_SSO_EXTENSION_PCBANG_TIME => 0x13c2,
  655. REFUSE_SSO_WRONG_RATETYPE_2 => 0x13c3,
  656. };
  657.  
  658. use constant {
  659. VAR_SPEED => 0x0,
  660. VAR_EXP => 0x1,
  661. VAR_JOBEXP => 0x2,
  662. VAR_VIRTUE => 0x3,
  663. VAR_HONOR => 0x4,
  664. VAR_HP => 0x5,
  665. VAR_MAXHP => 0x6,
  666. VAR_SP => 0x7,
  667. VAR_MAXSP => 0x8,
  668. VAR_POINT => 0x9,
  669. VAR_HAIRCOLOR => 0xa,
  670. VAR_CLEVEL => 0xb,
  671. VAR_SPPOINT => 0xc,
  672. VAR_STR => 0xd,
  673. VAR_AGI => 0xe,
  674. VAR_VIT => 0xf,
  675. VAR_INT => 0x10,
  676. VAR_DEX => 0x11,
  677. VAR_LUK => 0x12,
  678. VAR_JOB => 0x13,
  679. VAR_MONEY => 0x14,
  680. VAR_SEX => 0x15,
  681. VAR_MAXEXP => 0x16,
  682. VAR_MAXJOBEXP => 0x17,
  683. VAR_WEIGHT => 0x18,
  684. VAR_MAXWEIGHT => 0x19,
  685. VAR_POISON => 0x1a,
  686. VAR_STONE => 0x1b,
  687. VAR_CURSE => 0x1c,
  688. VAR_FREEZING => 0x1d,
  689. VAR_SILENCE => 0x1e,
  690. VAR_CONFUSION => 0x1f,
  691. VAR_STANDARD_STR => 0x20,
  692. VAR_STANDARD_AGI => 0x21,
  693. VAR_STANDARD_VIT => 0x22,
  694. VAR_STANDARD_INT => 0x23,
  695. VAR_STANDARD_DEX => 0x24,
  696. VAR_STANDARD_LUK => 0x25,
  697. VAR_ATTACKMT => 0x26,
  698. VAR_ATTACKEDMT => 0x27,
  699. VAR_NV_BASIC => 0x28,
  700. VAR_ATTPOWER => 0x29,
  701. VAR_REFININGPOWER => 0x2a,
  702. VAR_MAX_MATTPOWER => 0x2b,
  703. VAR_MIN_MATTPOWER => 0x2c,
  704. VAR_ITEMDEFPOWER => 0x2d,
  705. VAR_PLUSDEFPOWER => 0x2e,
  706. VAR_MDEFPOWER => 0x2f,
  707. VAR_PLUSMDEFPOWER => 0x30,
  708. VAR_HITSUCCESSVALUE => 0x31,
  709. VAR_AVOIDSUCCESSVALUE => 0x32,
  710. VAR_PLUSAVOIDSUCCESSVALUE => 0x33,
  711. VAR_CRITICALSUCCESSVALUE => 0x34,
  712. VAR_ASPD => 0x35,
  713. VAR_PLUSASPD => 0x36,
  714. VAR_JOBLEVEL => 0x37,
  715. VAR_ACCESSORY2 => 0x38,
  716. VAR_ACCESSORY3 => 0x39,
  717. VAR_HEADPALETTE => 0x3a,
  718. VAR_BODYPALETTE => 0x3b,
  719. VAR_PKHONOR => 0x3c,
  720. VAR_CURXPOS => 0x3d,
  721. VAR_CURYPOS => 0x3e,
  722. VAR_CURDIR => 0x3f,
  723. VAR_CHARACTERID => 0x40,
  724. VAR_ACCOUNTID => 0x41,
  725. VAR_MAPID => 0x42,
  726. VAR_MAPNAME => 0x43,
  727. VAR_ACCOUNTNAME => 0x44,
  728. VAR_CHARACTERNAME => 0x45,
  729. VAR_ITEM_COUNT => 0x46,
  730. VAR_ITEM_ITID => 0x47,
  731. VAR_ITEM_SLOT1 => 0x48,
  732. VAR_ITEM_SLOT2 => 0x49,
  733. VAR_ITEM_SLOT3 => 0x4a,
  734. VAR_ITEM_SLOT4 => 0x4b,
  735. VAR_HEAD => 0x4c,
  736. VAR_WEAPON => 0x4d,
  737. VAR_ACCESSORY => 0x4e,
  738. VAR_STATE => 0x4f,
  739. VAR_MOVEREQTIME => 0x50,
  740. VAR_GROUPID => 0x51,
  741. VAR_ATTPOWERPLUSTIME => 0x52,
  742. VAR_ATTPOWERPLUSPERCENT => 0x53,
  743. VAR_DEFPOWERPLUSTIME => 0x54,
  744. VAR_DEFPOWERPLUSPERCENT => 0x55,
  745. VAR_DAMAGENOMOTIONTIME => 0x56,
  746. VAR_BODYSTATE => 0x57,
  747. VAR_HEALTHSTATE => 0x58,
  748. VAR_RESETHEALTHSTATE => 0x59,
  749. VAR_CURRENTSTATE => 0x5a,
  750. VAR_RESETEFFECTIVE => 0x5b,
  751. VAR_GETEFFECTIVE => 0x5c,
  752. VAR_EFFECTSTATE => 0x5d,
  753. VAR_SIGHTABILITYEXPIREDTIME => 0x5e,
  754. VAR_SIGHTRANGE => 0x5f,
  755. VAR_SIGHTPLUSATTPOWER => 0x60,
  756. VAR_STREFFECTIVETIME => 0x61,
  757. VAR_AGIEFFECTIVETIME => 0x62,
  758. VAR_VITEFFECTIVETIME => 0x63,
  759. VAR_INTEFFECTIVETIME => 0x64,
  760. VAR_DEXEFFECTIVETIME => 0x65,
  761. VAR_LUKEFFECTIVETIME => 0x66,
  762. VAR_STRAMOUNT => 0x67,
  763. VAR_AGIAMOUNT => 0x68,
  764. VAR_VITAMOUNT => 0x69,
  765. VAR_INTAMOUNT => 0x6a,
  766. VAR_DEXAMOUNT => 0x6b,
  767. VAR_LUKAMOUNT => 0x6c,
  768. VAR_MAXHPAMOUNT => 0x6d,
  769. VAR_MAXSPAMOUNT => 0x6e,
  770. VAR_MAXHPPERCENT => 0x6f,
  771. VAR_MAXSPPERCENT => 0x70,
  772. VAR_HPACCELERATION => 0x71,
  773. VAR_SPACCELERATION => 0x72,
  774. VAR_SPEEDAMOUNT => 0x73,
  775. VAR_SPEEDDELTA => 0x74,
  776. VAR_SPEEDDELTA2 => 0x75,
  777. VAR_PLUSATTRANGE => 0x76,
  778. VAR_DISCOUNTPERCENT => 0x77,
  779. VAR_AVOIDABLESUCCESSPERCENT => 0x78,
  780. VAR_STATUSDEFPOWER => 0x79,
  781. VAR_PLUSDEFPOWERINACOLYTE => 0x7a,
  782. VAR_MAGICITEMDEFPOWER => 0x7b,
  783. VAR_MAGICSTATUSDEFPOWER => 0x7c,
  784. VAR_CLASS => 0x7d,
  785. VAR_PLUSATTACKPOWEROFITEM => 0x7e,
  786. VAR_PLUSDEFPOWEROFITEM => 0x7f,
  787. VAR_PLUSMDEFPOWEROFITEM => 0x80,
  788. VAR_PLUSARROWPOWEROFITEM => 0x81,
  789. VAR_PLUSATTREFININGPOWEROFITEM => 0x82,
  790. VAR_PLUSDEFREFININGPOWEROFITEM => 0x83,
  791. VAR_IDENTIFYNUMBER => 0x84,
  792. VAR_ISDAMAGED => 0x85,
  793. VAR_ISIDENTIFIED => 0x86,
  794. VAR_REFININGLEVEL => 0x87,
  795. VAR_WEARSTATE => 0x88,
  796. VAR_ISLUCKY => 0x89,
  797. VAR_ATTACKPROPERTY => 0x8a,
  798. VAR_STORMGUSTCNT => 0x8b,
  799. VAR_MAGICATKPERCENT => 0x8c,
  800. VAR_MYMOBCOUNT => 0x8d,
  801. VAR_ISCARTON => 0x8e,
  802. VAR_GDID => 0x8f,
  803. VAR_NPCXSIZE => 0x90,
  804. VAR_NPCYSIZE => 0x91,
  805. VAR_RACE => 0x92,
  806. VAR_SCALE => 0x93,
  807. VAR_PROPERTY => 0x94,
  808. VAR_PLUSATTACKPOWEROFITEM_RHAND => 0x95,
  809. VAR_PLUSATTACKPOWEROFITEM_LHAND => 0x96,
  810. VAR_PLUSATTREFININGPOWEROFITEM_RHAND => 0x97,
  811. VAR_PLUSATTREFININGPOWEROFITEM_LHAND => 0x98,
  812. VAR_TOLERACE => 0x99,
  813. VAR_ARMORPROPERTY => 0x9a,
  814. VAR_ISMAGICIMMUNE => 0x9b,
  815. VAR_ISFALCON => 0x9c,
  816. VAR_ISRIDING => 0x9d,
  817. VAR_MODIFIED => 0x9e,
  818. VAR_FULLNESS => 0x9f,
  819. VAR_RELATIONSHIP => 0xa0,
  820. VAR_ACCESSARY => 0xa1,
  821. VAR_SIZETYPE => 0xa2,
  822. VAR_SHOES => 0xa3,
  823. VAR_STATUSATTACKPOWER => 0xa4,
  824. VAR_BASICAVOIDANCE => 0xa5,
  825. VAR_BASICHIT => 0xa6,
  826. VAR_PLUSASPDPERCENT => 0xa7,
  827. VAR_CPARTY => 0xa8,
  828. VAR_ISMARRIED => 0xa9,
  829. VAR_ISGUILD => 0xaa,
  830. VAR_ISFALCONON => 0xab,
  831. VAR_ISPECOON => 0xac,
  832. VAR_ISPARTYMASTER => 0xad,
  833. VAR_ISGUILDMASTER => 0xae,
  834. VAR_BODYSTATENORMAL => 0xaf,
  835. VAR_HEALTHSTATENORMAL => 0xb0,
  836. VAR_STUN => 0xb1,
  837. VAR_SLEEP => 0xb2,
  838. VAR_UNDEAD => 0xb3,
  839. VAR_BLIND => 0xb4,
  840. VAR_BLOODING => 0xb5,
  841. VAR_BSPOINT => 0xb6,
  842. VAR_ACPOINT => 0xb7,
  843. VAR_BSRANK => 0xb8,
  844. VAR_ACRANK => 0xb9,
  845. VAR_CHANGESPEED => 0xba,
  846. VAR_CHANGESPEEDTIME => 0xbb,
  847. VAR_MAGICATKPOWER => 0xbc,
  848. VAR_MER_KILLCOUNT => 0xbd,
  849. VAR_MER_FAITH => 0xbe,
  850. VAR_MDEFPERCENT => 0xbf,
  851. VAR_CRITICAL_DEF => 0xc0,
  852. VAR_ITEMPOWER => 0xc1,
  853. VAR_MAGICDAMAGEREDUCE => 0xc2,
  854. VAR_STATUSMAGICPOWER => 0xc3,
  855. VAR_PLUSMAGICPOWEROFITEM => 0xc4,
  856. VAR_ITEMMAGICPOWER => 0xc5,
  857. VAR_NAME => 0xc6,
  858. VAR_FSMSTATE => 0xc7,
  859. VAR_ATTMPOWER => 0xc8,
  860. VAR_CARTWEIGHT => 0xc9,
  861. VAR_HP_SELF => 0xca,
  862. VAR_SP_SELF => 0xcb,
  863. VAR_COSTUME_BODY => 0xcc,
  864. VAR_RESET_COSTUMES => 0xcd,
  865. };
  866.  
  867. use constant {
  868. LEVELUP_EFFECT => 0x0,
  869. JOBLEVELUP_EFFECT => 0x1,
  870. REFINING_FAIL_EFFECT => 0x2,
  871. REFINING_SUCCESS_EFFECT => 0x3,
  872. GAME_OVER_EFFECT => 0x4,
  873. MAKEITEM_AM_SUCCESS_EFFECT => 0x5,
  874. MAKEITEM_AM_FAIL_EFFECT => 0x6,
  875. LEVELUP_EFFECT2 => 0x7,
  876. JOBLEVELUP_EFFECT2 => 0x8,
  877. LEVELUP_EFFECT3 => 0x9,
  878. };
  879.  
  880. use constant {
  881. DEFINE__BROADCASTING_SPECIAL_ITEM_OBTAIN => 1 << 0,
  882. DEFINE__RENEWAL_ADD_2 => 1 << 1,
  883. DEFINE__CHANNELING_SERVICE => 1 << 2,
  884. };
  885.  
  886. ######################################
  887. #### Packet inner struct handlers ####
  888. ######################################
  889.  
  890. # Override this function if you need to.
  891. sub items_nonstackable {
  892. my ($self, $args) = @_;
  893.  
  894. my $items = $self->{nested}->{items_nonstackable};
  895.  
  896. if($args->{switch} eq '00A4' || # inventory
  897. $args->{switch} eq '00A6' || # storage
  898. $args->{switch} eq '0122' # cart
  899. ) {
  900. return $items->{type1};
  901.  
  902. } elsif ($args->{switch} eq '0295' || # inventory
  903. $args->{switch} eq '0296' || # storage
  904. $args->{switch} eq '0297' # cart
  905. ) {
  906. return $items->{type2};
  907.  
  908. } elsif ($args->{switch} eq '02D0' || # inventory
  909. $args->{switch} eq '02D1' || # storage
  910. $args->{switch} eq '02D2' # cart
  911. ) {
  912. return $items->{$rpackets{'00AA'} == 7 ? 'type3' : 'type4'};
  913. } elsif ($args->{switch} eq '0901' # inventory
  914. || $args->{switch} eq '0976' # storage
  915. || $args->{switch} eq '0903' # cart
  916. ) {
  917. return $items->{type5};
  918. } else {
  919. warning "items_nonstackable: unsupported packet ($args->{switch})!\n";
  920. }
  921. }
  922.  
  923. # Override this function if you need to.
  924. sub items_stackable {
  925. my ($self, $args) = @_;
  926.  
  927. my $items = $self->{nested}->{items_stackable};
  928.  
  929. if($args->{switch} eq '00A3' || # inventory
  930. $args->{switch} eq '00A5' || # storage
  931. $args->{switch} eq '0123' # cart
  932. ) {
  933. return $items->{type1};
  934.  
  935. } elsif ($args->{switch} eq '01EE' || # inventory
  936. $args->{switch} eq '01F0' || # storage
  937. $args->{switch} eq '01EF' # cart
  938. ) {
  939. return $items->{type2};
  940.  
  941. } elsif ($args->{switch} eq '02E8' || # inventory
  942. $args->{switch} eq '02EA' || # storage
  943. $args->{switch} eq '02E9' # cart
  944. ) {
  945. return $items->{type3};
  946.  
  947. } elsif ($args->{switch} eq '0900' # inventory
  948. || $args->{switch} eq '0975' # storage
  949. || $args->{switch} eq '0902' # cart
  950. ) {
  951. return $items->{type5};
  952.  
  953. } else {
  954. warning "items_stackable: unsupported packet ($args->{switch})!\n";
  955. }
  956. }
  957.  
  958. sub parse_items {
  959. my ($self, $args, $unpack, $process) = @_;
  960. my @itemInfo;
  961.  
  962. my $length = length $args->{itemInfo};
  963. for (my $i = 0; $i < $length; $i += $unpack->{len}) {
  964. my $item;
  965. @{$item}{@{$unpack->{keys}}} = unpack($unpack->{types}, substr($args->{itemInfo}, $i, $unpack->{len}));
  966.  
  967. $process->($item);
  968.  
  969. push @itemInfo, $item;
  970. }
  971.  
  972. @itemInfo
  973. }
  974.  
  975. =pod
  976. parse_items_nonstackable
  977.  
  978. Change in packet behavior: the amount is not specified, but this is a
  979. non-stackable item (equipment), so the amount is obviously "1".
  980.  
  981. =cut
  982. sub parse_items_nonstackable {
  983. my ($self, $args) = @_;
  984.  
  985. $self->parse_items($args, $self->items_nonstackable($args), sub {
  986. my ($item) = @_;
  987.  
  988. #$item->{placeEtcTab} = $item->{identified} & (1 << 2);
  989.  
  990. # Non stackable items now have no amount normally given in the
  991. # packet, so we must assume one. We'll even play it safe, and
  992. # not change the amount if it's already a non-zero value.
  993. $item->{amount} = 1 unless ($item->{amount});
  994. $item->{broken} = $item->{identified} & (1 << 1) unless exists $item->{broken};
  995. $item->{idenfitied} = $item->{identified} & (1 << 0);
  996. })
  997. }
  998.  
  999. sub parse_items_stackable {
  1000. my ($self, $args) = @_;
  1001.  
  1002. $self->parse_items($args, $self->items_stackable($args), sub {
  1003. my ($item) = @_;
  1004.  
  1005. #$item->{placeEtcTab} = $item->{identified} & (1 << 1);
  1006. $item->{idenfitied} = $item->{identified} & (1 << 0);
  1007. })
  1008. }
  1009.  
  1010. sub _items_list {
  1011. my ($self, $args) = @_;
  1012.  
  1013. for my $item (@{$args->{items}}) {
  1014. my ($local_item, $add);
  1015.  
  1016. unless ($local_item = $args->{getter} && $args->{getter}($item)) {
  1017. $local_item = $args->{class}->new;
  1018. $add = 1;
  1019. }
  1020.  
  1021. for ([keys %$item]) {
  1022. @{$local_item}{@$_} = @{$item}{@$_};
  1023. }
  1024. $local_item->{name} = itemName($local_item);
  1025.  
  1026. $args->{callback}($local_item) if $args->{callback};
  1027.  
  1028. $args->{adder}($local_item) if $add;
  1029.  
  1030. my $index = ($local_item->{invIndex} >= 0) ? $local_item->{invIndex} : $local_item->{index};
  1031. debug "$args->{debug_str}: $local_item->{name} ($index) x $local_item->{amount} - $itemTypes_lut{$local_item->{type}}\n", 'parseMsg';
  1032. Plugins::callHook($args->{hook}, {index => $index, item => $local_item});
  1033. }
  1034. }
  1035.  
  1036. #######################################
  1037. ###### Packet handling callbacks ######
  1038. #######################################
  1039.  
  1040. # from old ServerType0
  1041. sub map_loaded {
  1042. my ($self, $args) = @_;
  1043. $net->setState(Network::IN_GAME);
  1044. undef $conState_tries;
  1045. $char = $chars[$config{char}];
  1046. return unless changeToInGameState();
  1047. # assertClass($char, 'Actor::You');
  1048.  
  1049. if ($net->version == 1) {
  1050. $net->setState(4);
  1051. message(T("Waiting for map to load...\n"), "connection");
  1052. ai_clientSuspend(0, 10);
  1053. main::initMapChangeVars();
  1054. } else {
  1055. $messageSender->sendGuildMasterMemberCheck();
  1056.  
  1057. # Replies 01B6 (Guild Info) and 014C (Guild Ally/Enemy List)
  1058. $messageSender->sendGuildRequestInfo(0);
  1059.  
  1060. # Replies 0166 (Guild Member Titles List) and 0154 (Guild Members List)
  1061. $messageSender->sendGuildRequestInfo(1);
  1062. message(T("You are now in the game\n"), "connection");
  1063. Plugins::callHook('in_game');
  1064. $messageSender->sendMapLoaded();
  1065. $timeout{'ai'}{'time'} = time;
  1066. }
  1067.  
  1068. $char->{pos} = {};
  1069. makeCoordsDir($char->{pos}, $args->{coords}, \$char->{look}{body});
  1070. $char->{pos_to} = {%{$char->{pos}}};
  1071. message(TF("Your Coordinates: %s, %s\n", $char->{pos}{x}, $char->{pos}{y}), undef, 1);
  1072.  
  1073. $messageSender->sendIgnoreAll("all") if ($config{ignoreAll});
  1074. }
  1075.  
  1076. sub actor_look_at {
  1077. my ($self, $args) = @_;
  1078. return unless changeToInGameState();
  1079.  
  1080. my $actor = Actor::get($args->{ID});
  1081. $actor->{look}{head} = $args->{head};
  1082. $actor->{look}{body} = $args->{body};
  1083. debug $actor->nameString . " looks at $args->{body}, $args->{head}\n", "parseMsg";
  1084. }
  1085.  
  1086. sub actor_movement_interrupted {
  1087. my ($self, $args) = @_;
  1088. return unless changeToInGameState();
  1089. my %coords;
  1090. $coords{x} = $args->{x};
  1091. $coords{y} = $args->{y};
  1092.  
  1093. my $actor = Actor::get($args->{ID});
  1094. $actor->{pos} = {%coords};
  1095. $actor->{pos_to} = {%coords};
  1096. if ($actor->isa('Actor::You') || $actor->isa('Actor::Player')) {
  1097. $actor->{sitting} = 0;
  1098. }
  1099. if ($actor->isa('Actor::You')) {
  1100. debug "Movement interrupted, your coordinates: $coords{x}, $coords{y}\n", "parseMsg_move";
  1101. AI::clear("move");
  1102. }
  1103. if ($char->{homunculus} && $char->{homunculus}{ID} eq $actor->{ID}) {
  1104. AI::clear("move");
  1105. }
  1106. }
  1107.  
  1108. # TODO: translation-friendly messages
  1109. sub actor_status_active {
  1110. my ($self, $args) = @_;
  1111.  
  1112. return unless changeToInGameState();
  1113. # my ($type, $ID, $flag, $tick) = @{$args}{qw(type ID flag tick)};
  1114. my ($type, $ID, $flag, $tick, $unknown1, $unknown2, $unknown3) = @{$args}{qw(type ID flag tick unknown1 unknown2 unknown3)};
  1115. my $status = defined $statusHandle{$type} ? $statusHandle{$type} : "UNKNOWN_STATUS_$type";
  1116. $cart{type} = $unknown1 if ($type == 673 && defined $unknown1 && ($ID eq $accountID)); # for Cart active
  1117. $args->{skillName} = defined $statusName{$status} ? $statusName{$status} : $status;
  1118. ($args->{actor} = Actor::get($ID))->setStatus($status, $flag, $tick == 9999 ? undef : $tick);
  1119. }
  1120.  
  1121. sub actor_trapped {
  1122. my ($self, $args) = @_;
  1123. # original comment was that ID is not a valid ID
  1124. # but it seems to be, at least on eAthena/Freya
  1125. my $actor = Actor::get($args->{ID});
  1126. debug "$actor->nameString() is trapped.\n";
  1127. }
  1128.  
  1129. sub area_spell {
  1130. my ($self, $args) = @_;
  1131.  
  1132. # Area effect spell; including traps!
  1133. my $ID = $args->{ID};
  1134. my $sourceID = $args->{sourceID};
  1135. my $x = $args->{x};
  1136. my $y = $args->{y};
  1137. my $type = $args->{type};
  1138. my $fail = $args->{fail};
  1139. my $binID;
  1140.  
  1141. if ($spells{$ID} && $spells{$ID}{'sourceID'} eq $sourceID) {
  1142. $binID = binFind(\@spellsID, $ID);
  1143. $binID = binAdd(\@spellsID, $ID) if ($binID eq "");
  1144. } else {
  1145. $binID = binAdd(\@spellsID, $ID);
  1146. }
  1147.  
  1148. $spells{$ID}{'sourceID'} = $sourceID;
  1149. $spells{$ID}{'pos'}{'x'} = $x;
  1150. $spells{$ID}{'pos'}{'y'} = $y;
  1151. $spells{$ID}{'pos_to'}{'x'} = $x;
  1152. $spells{$ID}{'pos_to'}{'y'} = $y;
  1153. $spells{$ID}{'binID'} = $binID;
  1154. $spells{$ID}{'type'} = $type;
  1155. if ($type == 0x81) {
  1156. message TF("%s opened Warp Portal on (%d, %d)\n", getActorName($sourceID), $x, $y), "skill";
  1157. }
  1158. debug "Area effect ".getSpellName($type)." ($binID) from ".getActorName($sourceID)." appeared on ($x, $y)\n", "skill", 2;
  1159.  
  1160. if ($args->{switch} eq "01C9") {
  1161. message TF("%s has scribbled: %s on (%d, %d)\n", getActorName($sourceID), $args->{scribbleMsg}, $x, $y);
  1162. }
  1163.  
  1164. Plugins::callHook('packet_areaSpell', {
  1165. fail => $fail,
  1166. sourceID => $sourceID,
  1167. type => $type,
  1168. x => $x,
  1169. y => $y
  1170. });
  1171. }
  1172.  
  1173. sub area_spell_disappears {
  1174. my ($self, $args) = @_;
  1175.  
  1176. # The area effect spell with ID dissappears
  1177. my $ID = $args->{ID};
  1178. my $spell = $spells{$ID};
  1179. debug "Area effect ".getSpellName($spell->{type})." ($spell->{binID}) from ".getActorName($spell->{sourceID})." disappeared from ($spell->{pos}{x}, $spell->{pos}{y})\n", "skill", 2;
  1180. delete $spells{$ID};
  1181. binRemove(\@spellsID, $ID);
  1182. }
  1183.  
  1184. sub arrow_none {
  1185. my ($self, $args) = @_;
  1186.  
  1187. my $type = $args->{type};
  1188. if ($type == 0) {
  1189. delete $char->{'arrow'};
  1190. if ($config{'dcOnEmptyArrow'}) {
  1191. $interface->errorDialog(T("Please equip arrow first."));
  1192. quit();
  1193. } else {
  1194. error T("Please equip arrow first.\n");
  1195. }
  1196. } elsif ($type == 1) {
  1197. debug "You can't Attack or use Skills because your Weight Limit has been exceeded.\n";
  1198. } elsif ($type == 2) {
  1199. debug "You can't use Skills because Weight Limit has been exceeded.\n";
  1200. } elsif ($type == 3) {
  1201. debug "Arrow equipped\n";
  1202. }
  1203. }
  1204.  
  1205. sub arrowcraft_list {
  1206. my ($self, $args) = @_;
  1207.  
  1208. my $newmsg;
  1209. my $msg = $args->{RAW_MSG};
  1210. my $msg_size = $args->{RAW_MSG_SIZE};
  1211. $self->decrypt(\$newmsg, substr($msg, 4));
  1212. $msg = substr($msg, 0, 4).$newmsg;
  1213.  
  1214. undef @arrowCraftID;
  1215. for (my $i = 4; $i < $msg_size; $i += 2) {
  1216. my $ID = unpack("v1", substr($msg, $i, 2));
  1217. my $item = $char->inventory->getByNameID($ID);
  1218. binAdd(\@arrowCraftID, $item->{invIndex});
  1219. }
  1220.  
  1221. message T("Received Possible Arrow Craft List - type 'arrowcraft'\n");
  1222. }
  1223.  
  1224. sub attack_range {
  1225. my ($self, $args) = @_;
  1226.  
  1227. my $type = $args->{type};
  1228. debug "Your attack range is: $type\n";
  1229. return unless changeToInGameState();
  1230.  
  1231. $char->{attack_range} = $type;
  1232. if ($config{attackDistanceAuto} && $config{attackDistance} != $type) {
  1233. message TF("Autodetected attackDistance = %s\n", $type), "success";
  1234. configModify('attackDistance', $type, 1);
  1235. configModify('attackMaxDistance', $type, 1);
  1236. }
  1237. }
  1238.  
  1239. sub buy_result {
  1240. my ($self, $args) = @_;
  1241. if ($args->{fail} == 0) {
  1242. message T("Buy completed.\n"), "success";
  1243. } elsif ($args->{fail} == 1) {
  1244. error T("Buy failed (insufficient zeny).\n");
  1245. } elsif ($args->{fail} == 2) {
  1246. error T("Buy failed (insufficient weight capacity).\n");
  1247. } elsif ($args->{fail} == 3) {
  1248. error T("Buy failed (too many different inventory items).\n");
  1249. } elsif ($args->{fail} == 4) {
  1250. error T("Buy failed (item does not exist in store).\n");
  1251. } elsif ($args->{fail} == 5) {
  1252. error T("Buy failed (item cannot be exchanged).\n");
  1253. } elsif ($args->{fail} == 6) {
  1254. error T("Buy failed (invalid store).\n");
  1255. } else {
  1256. error TF("Buy failed (failure code %s).\n", $args->{fail});
  1257. }
  1258. }
  1259.  
  1260. sub card_merge_list {
  1261. my ($self, $args) = @_;
  1262.  
  1263. # You just requested a list of possible items to merge a card into
  1264. # The RO client does this when you double click a card
  1265. my $newmsg;
  1266. my $msg = $args->{RAW_MSG};
  1267. $self->decrypt(\$newmsg, substr($msg, 4));
  1268. $msg = substr($msg, 0, 4).$newmsg;
  1269. my ($len) = unpack("x2 v1", $msg);
  1270.  
  1271. my $display;
  1272. $display .= T("-----Card Merge Candidates-----\n");
  1273.  
  1274. my $index;
  1275. for (my $i = 4; $i < $len; $i += 2) {
  1276. $index = unpack("v1", substr($msg, $i, 2));
  1277. my $item = $char->inventory->getByServerIndex($index);
  1278. binAdd(\@cardMergeItemsID, $item->{invIndex});
  1279. $display .= "$item->{invIndex} $item->{name}\n";
  1280. }
  1281.  
  1282. $display .= "-------------------------------\n";
  1283. message $display, "list";
  1284. }
  1285.  
  1286. sub card_merge_status {
  1287. my ($self, $args) = @_;
  1288.  
  1289. # something about successful compound?
  1290. my $item_index = $args->{item_index};
  1291. my $card_index = $args->{card_index};
  1292. my $fail = $args->{fail};
  1293.  
  1294. if ($fail) {
  1295. message T("Card merging failed\n");
  1296. } else {
  1297. my $item = $char->inventory->getByServerIndex($item_index);
  1298. my $card = $char->inventory->getByServerIndex($card_index);
  1299. message TF("%s has been successfully merged into %s\n",
  1300. $card->{name}, $item->{name}), "success";
  1301.  
  1302. # Remove one of the card
  1303. $card->{amount} -= 1;
  1304. if ($card->{amount} <= 0) {
  1305. $char->inventory->remove($card);
  1306. }
  1307.  
  1308. # Rename the slotted item now
  1309. # FIXME: this is unoptimized
  1310. use bytes;
  1311. no encoding 'utf8';
  1312. my $newcards = '';
  1313. my $addedcard;
  1314. for (my $i = 0; $i < 4; $i++) {
  1315. my $cardData = substr($item->{cards}, $i * 2, 2);
  1316. if (unpack("v", $cardData)) {
  1317. $newcards .= $cardData;
  1318. } elsif (!$addedcard) {
  1319. $newcards .= pack("v", $card->{nameID});
  1320. $addedcard = 1;
  1321. } else {
  1322. $newcards .= pack("v", 0);
  1323. }
  1324. }
  1325. $item->{cards} = $newcards;
  1326. $item->setName(itemName($item));
  1327. }
  1328.  
  1329. undef @cardMergeItemsID;
  1330. undef $cardMergeIndex;
  1331. }
  1332.  
  1333. sub cart_info {
  1334. my ($self, $args) = @_;
  1335.  
  1336. $cart{items} = $args->{items};
  1337. $cart{items_max} = $args->{items_max};
  1338. $cart{weight} = int($args->{weight} / 10);
  1339. $cart{weight_max} = int($args->{weight_max} / 10);
  1340. $cart{exists} = 1;
  1341. debug "[cart_info] received.\n", "parseMsg";
  1342. }
  1343.  
  1344. sub cart_add_failed {
  1345. my ($self, $args) = @_;
  1346.  
  1347. my $reason;
  1348. if ($args->{fail} == 0) {
  1349. $reason = T('overweight');
  1350. } elsif ($args->{fail} == 1) {
  1351. $reason = T('too many items');
  1352. } else {
  1353. $reason = TF("Unknown code %s",$args->{fail});
  1354. }
  1355. error TF("Can't Add Cart Item (%s)\n", $reason);
  1356. }
  1357.  
  1358. sub cart_items_nonstackable {
  1359. my ($self, $args) = @_;
  1360.  
  1361. $self->_items_list({
  1362. # TODO: different classes for inventory/cart/storage items
  1363. class => 'Actor::Item',
  1364. hook => 'packet_cart',
  1365. debug_str => 'Non-Stackable Cart Item',
  1366. items => [$self->parse_items_nonstackable($args)],
  1367. getter => sub { $cart{inventory}[$_[0]{index}] },
  1368. adder => sub { $cart{inventory}[$_[0]{index}] = $_[0] },
  1369. });
  1370.  
  1371. $ai_v{'inventory_time'} = time + 1;
  1372. $ai_v{'cart_time'} = time + 1;
  1373. }
  1374.  
  1375. sub cart_items_stackable {
  1376. my ($self, $args) = @_;
  1377.  
  1378. $self->_items_list({
  1379. class => 'Actor::Item',
  1380. hook => 'packet_cart',
  1381. debug_str => 'Stackable Cart Item',
  1382. items => [$self->parse_items_stackable($args)],
  1383. getter => sub { $cart{inventory}[$_[0]{index}] },
  1384. adder => sub { $cart{inventory}[$_[0]{index}] = $_[0] },
  1385. });
  1386.  
  1387. $ai_v{'inventory_time'} = time + 1;
  1388. $ai_v{'cart_time'} = time + 1;
  1389. }
  1390.  
  1391. sub cart_item_added {
  1392. my ($self, $args) = @_;
  1393.  
  1394. my $item = $cart{inventory}[$args->{index}] ||= Actor::Item->new;
  1395. if ($item->{amount}) {
  1396. $item->{amount} += $args->{amount};
  1397. } else {
  1398. $item->{index} = $args->{index};
  1399. $item->{nameID} = $args->{nameID};
  1400. $item->{amount} = $args->{amount};
  1401. $item->{identified} = $args->{identified};
  1402. $item->{broken} = $args->{broken};
  1403. $item->{upgrade} = $args->{upgrade};
  1404. $item->{cards} = $args->{cards};
  1405. $item->{type} = $args->{type} if (exists $args->{type});
  1406. $item->{name} = itemName($item);
  1407. }
  1408. message TF("Cart Item Added: %s (%d) x %s\n", $item->{name}, $args->{index}, $args->{amount});
  1409. $itemChange{$item->{name}} += $args->{amount};
  1410. $args->{item} = $item;
  1411. }
  1412.  
  1413. sub cash_dealer {
  1414. my ($self, $args) = @_;
  1415.  
  1416. undef @cashList;
  1417. my $cashList = 0;
  1418. $char->{cashpoint} = unpack("x4 V", $args->{RAW_MSG});
  1419.  
  1420. for (my $i = 8; $i < $args->{RAW_MSG_SIZE}; $i += 11) {
  1421. my ($price, $dcprice, $type, $ID) = unpack("V2 C v", substr($args->{RAW_MSG}, $i, 11));
  1422. my $store = $cashList[$cashList] = {};
  1423. # TODO: use itemName() or itemNameSimple()?
  1424. my $display = ($items_lut{$ID} ne "") ? $items_lut{$ID} : "Unknown $ID";
  1425. $store->{name} = $display;
  1426. $store->{nameID} = $ID;
  1427. $store->{type} = $type;
  1428. $store->{price} = $dcprice;
  1429. $cashList++;
  1430. }
  1431.  
  1432. $ai_v{npc_talk}{talk} = 'cash';
  1433. # continue talk sequence now
  1434. $ai_v{npc_talk}{time} = time;
  1435.  
  1436. message TF("------------CashList (Cash Point: %-5d)-------------\n" .
  1437. "# Name Type Price\n", $char->{cashpoint}), "list";
  1438. my $display;
  1439. for (my $i = 0; $i < @cashList; $i++) {
  1440. $display = $cashList[$i]{name};
  1441. message(swrite(
  1442. "@<<< @<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<< @>>>>>>>p",
  1443. [$i, $display, $itemTypes_lut{$cashList[$i]{type}}, $cashList[$i]{price}]),
  1444. "list");
  1445. }
  1446. message("-----------------------------------------------------\n", "list");
  1447. }
  1448.  
  1449. sub combo_delay {
  1450. my ($self, $args) = @_;
  1451.  
  1452. $char->{combo_packet} = ($args->{delay}); #* 15) / 100000;
  1453. # How was the above formula derived? I think it's better that the manipulation be
  1454. # done in functions.pl (or whatever sub that handles this) instead of here.
  1455.  
  1456. $args->{actor} = Actor::get($args->{ID});
  1457. my $verb = $args->{actor}->verb('have', 'has');
  1458. debug "$args->{actor} $verb combo delay $args->{delay}\n", "parseMsg_comboDelay";
  1459. }
  1460.  
  1461. sub cart_item_removed {
  1462. my ($self, $args) = @_;
  1463.  
  1464. my ($index, $amount) = @{$args}{qw(index amount)};
  1465.  
  1466. my $item = $cart{inventory}[$index];
  1467. $item->{amount} -= $amount;
  1468. message TF("Cart Item Removed: %s (%d) x %s\n", $item->{name}, $index, $amount);
  1469. $itemChange{$item->{name}} -= $amount;
  1470. if ($item->{amount} <= 0) {
  1471. $cart{'inventory'}[$index] = undef;
  1472. }
  1473. $args->{item} = $item;
  1474. }
  1475.  
  1476. sub change_to_constate25 {
  1477. $net->setState(2.5);
  1478. undef $accountID;
  1479. }
  1480.  
  1481. sub changeToInGameState {
  1482. Network::Receive::changeToInGameState;
  1483. }
  1484.  
  1485. sub character_creation_failed {
  1486. my ($self, $args) = @_;
  1487. if ($args->{flag} == 0x0) {
  1488. message T("Charname already exists.\n"), "info";
  1489. } elsif ($args->{flag} == 0xFF) {
  1490. message T("Char creation denied.\n"), "info";
  1491. } elsif ($args->{flag} == 0x01) {
  1492. message T("You are underaged.\n"), "info";
  1493. } else {
  1494. message T("Character creation failed. " .
  1495. "If you didn't make any mistake, then the name you chose already exists.\n"), "info";
  1496. }
  1497. if (charSelectScreen() == 1) {
  1498. $net->setState(3);
  1499. $firstLoginMap = 1;
  1500. $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  1501. $sentWelcomeMessage = 1;
  1502. }
  1503. }
  1504.  
  1505. sub character_creation_successful {
  1506. my ($self, $args) = @_;
  1507. my $char = new Actor::You;
  1508. foreach (@{$self->{packet_list}{$args->{switch}}->[2]}) {
  1509. $char->{$_} = $args->{$_} if (exists $args->{$_});
  1510. }
  1511. $char->{name} = bytesToString($args->{name});
  1512. $char->{jobID} = 0;
  1513. $char->{headgear}{low} = 0;
  1514. $char->{headgear}{top} = 0;
  1515. $char->{headgear}{mid} = 0;
  1516. $char->{nameID} = unpack("V", $accountID);
  1517. #$char->{lv} = 1;
  1518. #$char->{lv_job} = 1;
  1519. $char->{sex} = $accountSex2;
  1520. $chars[$char->{slot}] = $char;
  1521.  
  1522.  
  1523.  
  1524. $net->setState(3);
  1525. message TF("Character %s (%d) created.\n", $char->{name}, $char->{slot}), "info";
  1526. if (charSelectScreen() == 1) {
  1527. $firstLoginMap = 1;
  1528. $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  1529. $sentWelcomeMessage = 1;
  1530. }
  1531. }
  1532.  
  1533. sub character_deletion_successful {
  1534. if (defined $AI::temp::delIndex) {
  1535. message TF("Character %s (%d) deleted.\n", $chars[$AI::temp::delIndex]{name}, $AI::temp::delIndex), "info";
  1536. delete $chars[$AI::temp::delIndex];
  1537. undef $AI::temp::delIndex;
  1538. for (my $i = 0; $i < @chars; $i++) {
  1539. delete $chars[$i] if ($chars[$i] && !scalar(keys %{$chars[$i]}))
  1540. }
  1541. } else {
  1542. message T("Character deleted.\n"), "info";
  1543. }
  1544.  
  1545. if (charSelectScreen() == 1) {
  1546. $net->setState(3);
  1547. $firstLoginMap = 1;
  1548. $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  1549. $sentWelcomeMessage = 1;
  1550. }
  1551. }
  1552.  
  1553. sub character_deletion_failed {
  1554. error T("Character cannot be deleted. Your e-mail address was probably wrong.\n");
  1555. undef $AI::temp::delIndex;
  1556. if (charSelectScreen() == 1) {
  1557. $net->setState(3);
  1558. $firstLoginMap = 1;
  1559. $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  1560. $sentWelcomeMessage = 1;
  1561. }
  1562. }
  1563.  
  1564. sub character_moves {
  1565. my ($self, $args) = @_;
  1566.  
  1567. return unless changeToInGameState();
  1568. makeCoordsFromTo($char->{pos}, $char->{pos_to}, $args->{coords});
  1569. my $dist = sprintf("%.1f", distance($char->{pos}, $char->{pos_to}));
  1570. debug "You're moving from ($char->{pos}{x}, $char->{pos}{y}) to ($char->{pos_to}{x}, $char->{pos_to}{y}) - distance $dist\n", "parseMsg_move";
  1571. $char->{time_move} = time;
  1572. $char->{time_move_calc} = distance($char->{pos}, $char->{pos_to}) * ($char->{walk_speed} || 0.12);
  1573.  
  1574. # Correct the direction in which we're looking
  1575. my (%vec, $degree);
  1576. getVector(\%vec, $char->{pos_to}, $char->{pos});
  1577. $degree = vectorToDegree(\%vec);
  1578. if (defined $degree) {
  1579. my $direction = int sprintf("%.0f", (360 - $degree) / 45);
  1580. $char->{look}{body} = $direction & 0x07;
  1581. $char->{look}{head} = 0;
  1582. }
  1583.  
  1584. # Ugly; AI code in network subsystem! This must be fixed.
  1585. if (AI::action eq "mapRoute" && $config{route_escape_reachedNoPortal} && $dist eq "0.0"){
  1586. if (!$portalsID[0]) {
  1587. if ($config{route_escape_shout} ne "" && !defined($timeout{ai_route_escape}{time})){
  1588. sendMessage("c", $config{route_escape_shout});
  1589. }
  1590. $timeout{ai_route_escape}{time} = time;
  1591. AI::queue("escape");
  1592. }
  1593. }
  1594. }
  1595.  
  1596. sub character_name {
  1597. my ($self, $args) = @_;
  1598. my $name; # Type: String
  1599.  
  1600. $name = bytesToString($args->{name});
  1601. debug "Character name received: $name\n";
  1602. }
  1603.  
  1604. sub character_status {
  1605. my ($self, $args) = @_;
  1606.  
  1607. my $actor = Actor::get($args->{ID});
  1608.  
  1609. if ($args->{switch} eq '028A') {
  1610. $actor->{lv} = $args->{lv}; # TODO: test if it is ok to use this piece of information
  1611. $actor->{opt3} = $args->{opt3};
  1612. } elsif ($args->{switch} eq '0229' || $args->{switch} eq '0119') {
  1613. $actor->{opt1} = $args->{opt1};
  1614. $actor->{opt2} = $args->{opt2};
  1615. }
  1616.  
  1617. $actor->{option} = $args->{option};
  1618.  
  1619. setStatus($actor, $args->{opt1}, $args->{opt2}, $args->{option});
  1620. }
  1621.  
  1622. sub chat_created {
  1623. my ($self, $args) = @_;
  1624.  
  1625. $currentChatRoom = $accountID;
  1626. $chatRooms{$accountID} = {%createdChatRoom};
  1627. binAdd(\@chatRoomsID, $accountID);
  1628. binAdd(\@currentChatRoomUsers, $char->{name});
  1629. message T("Chat Room Created\n");
  1630. }
  1631.  
  1632. sub chat_info {
  1633. my ($self, $args) = @_;
  1634.  
  1635. my $title;
  1636. $self->decrypt(\$title, $args->{title});
  1637. $title = bytesToString($title);
  1638.  
  1639. my $chat = $chatRooms{$args->{ID}};
  1640. if (!$chat || !%{$chat}) {
  1641. $chat = $chatRooms{$args->{ID}} = {};
  1642. binAdd(\@chatRoomsID, $args->{ID});
  1643. }
  1644. $chat->{title} = $title;
  1645. $chat->{ownerID} = $args->{ownerID};
  1646. $chat->{limit} = $args->{limit};
  1647. $chat->{public} = $args->{public};
  1648. $chat->{num_users} = $args->{num_users};
  1649.  
  1650. Plugins::callHook('packet_chatinfo', {
  1651. title => $title,
  1652. ownerID => $args->{ownerID},
  1653. limit => $args->{limit},
  1654. public => $args->{public},
  1655. num_users => $args->{num_users}
  1656. });
  1657. }
  1658.  
  1659. sub chat_join_result {
  1660. my ($self, $args) = @_;
  1661.  
  1662. if ($args->{type} == 1) {
  1663. message T("Can't join Chat Room - Incorrect Password\n");
  1664. } elsif ($args->{type} == 2) {
  1665. message T("Can't join Chat Room - You're banned\n");
  1666. }
  1667. }
  1668.  
  1669. sub chat_modified {
  1670. my ($self, $args) = @_;
  1671.  
  1672. my $title;
  1673. $self->decrypt(\$title, $args->{title});
  1674. $title = bytesToString($title);
  1675.  
  1676. my ($ownerID, $ID, $limit, $public, $num_users) = @{$args}{qw(ownerID ID limit public num_users)};
  1677.  
  1678. if ($ownerID eq $accountID) {
  1679. $chatRooms{new}{title} = $title;
  1680. $chatRooms{new}{ownerID} = $ownerID;
  1681. $chatRooms{new}{limit} = $limit;
  1682. $chatRooms{new}{public} = $public;
  1683. $chatRooms{new}{num_users} = $num_users;
  1684. } else {
  1685. $chatRooms{$ID}{title} = $title;
  1686. $chatRooms{$ID}{ownerID} = $ownerID;
  1687. $chatRooms{$ID}{limit} = $limit;
  1688. $chatRooms{$ID}{public} = $public;
  1689. $chatRooms{$ID}{num_users} = $num_users;
  1690. }
  1691. message T("Chat Room Properties Modified\n");
  1692. }
  1693.  
  1694. sub chat_newowner {
  1695. my ($self, $args) = @_;
  1696.  
  1697. my $user = bytesToString($args->{user});
  1698. if ($args->{type} == 0) {
  1699. if ($user eq $char->{name}) {
  1700. $chatRooms{$currentChatRoom}{ownerID} = $accountID;
  1701. } else {
  1702. my $players = $playersList->getItems();
  1703. my $player;
  1704. foreach my $p (@{$players}) {
  1705. if ($p->{name} eq $user) {
  1706. $player = $p;
  1707. last;
  1708. }
  1709. }
  1710.  
  1711. if ($player) {
  1712. my $key = $player->{ID};
  1713. $chatRooms{$currentChatRoom}{ownerID} = $key;
  1714. }
  1715. }
  1716. $chatRooms{$currentChatRoom}{users}{$user} = 2;
  1717. } else {
  1718. $chatRooms{$currentChatRoom}{users}{$user} = 1;
  1719. }
  1720. }
  1721.  
  1722. sub chat_user_join {
  1723. my ($self, $args) = @_;
  1724.  
  1725. my $user = bytesToString($args->{user});
  1726. if ($currentChatRoom ne "") {
  1727. binAdd(\@currentChatRoomUsers, $user);
  1728. $chatRooms{$currentChatRoom}{users}{$user} = 1;
  1729. $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
  1730. message TF("%s has joined the Chat Room\n", $user);
  1731. }
  1732. }
  1733.  
  1734. sub chat_user_leave {
  1735. my ($self, $args) = @_;
  1736.  
  1737. my $user = bytesToString($args->{user});
  1738. delete $chatRooms{$currentChatRoom}{users}{$user};
  1739. binRemove(\@currentChatRoomUsers, $user);
  1740. $chatRooms{$currentChatRoom}{num_users} = $args->{num_users};
  1741. if ($user eq $char->{name}) {
  1742. binRemove(\@chatRoomsID, $currentChatRoom);
  1743. delete $chatRooms{$currentChatRoom};
  1744. undef @currentChatRoomUsers;
  1745. $currentChatRoom = "";
  1746. message T("You left the Chat Room\n");
  1747. } else {
  1748. message TF("%s has left the Chat Room\n", $user);
  1749. }
  1750. }
  1751.  
  1752. sub chat_users {
  1753. my ($self, $args) = @_;
  1754.  
  1755. my $newmsg;
  1756. $self->decrypt(\$newmsg, substr($args->{RAW_MSG}, 8));
  1757. my $msg = substr($args->{RAW_MSG}, 0, 8).$newmsg;
  1758.  
  1759. my $ID = substr($args->{RAW_MSG},4,4);
  1760. $currentChatRoom = $ID;
  1761.  
  1762. my $chat = $chatRooms{$currentChatRoom} ||= {};
  1763.  
  1764. $chat->{num_users} = 0;
  1765. for (my $i = 8; $i < $args->{RAW_MSG_SIZE}; $i += 28) {
  1766. my $type = unpack("C1",substr($msg,$i,1));
  1767. my ($chatUser) = unpack("Z*", substr($msg,$i + 4,24));
  1768. $chatUser = bytesToString($chatUser);
  1769.  
  1770. if ($chat->{users}{$chatUser} eq "") {
  1771. binAdd(\@currentChatRoomUsers, $chatUser);
  1772. if ($type == 0) {
  1773. $chat->{users}{$chatUser} = 2;
  1774. } else {
  1775. $chat->{users}{$chatUser} = 1;
  1776. }
  1777. $chat->{num_users}++;
  1778. }
  1779. }
  1780.  
  1781. message TF("You have joined the Chat Room %s\n", $chat->{title});
  1782. }
  1783.  
  1784. sub cast_cancelled {
  1785. my ($self, $args) = @_;
  1786.  
  1787. # Cast is cancelled
  1788. my $ID = $args->{ID};
  1789.  
  1790. my $source = Actor::get($ID);
  1791. $source->{cast_cancelled} = time;
  1792. my $skill = $source->{casting}->{skill};
  1793. my $skillName = $skill ? $skill->getName() : T('Unknown');
  1794. my $domain = ($ID eq $accountID) ? "selfSkill" : "skill";
  1795. message sprintf($source->verb(T("%s failed to cast %s\n"), T("%s failed to cast %s\n")), $source, $skillName), $domain;
  1796. Plugins::callHook('packet_castCancelled', {
  1797. sourceID => $ID
  1798. });
  1799. delete $source->{casting};
  1800. }
  1801.  
  1802. sub chat_removed {
  1803. my ($self, $args) = @_;
  1804.  
  1805. binRemove(\@chatRoomsID, $args->{ID});
  1806. delete $chatRooms{ $args->{ID} };
  1807. }
  1808.  
  1809. sub deal_add_other {
  1810. my ($self, $args) = @_;
  1811.  
  1812. if ($args->{nameID} > 0) {
  1813. my $item = $currentDeal{other}{ $args->{nameID} } ||= {};
  1814. $item->{amount} += $args->{amount};
  1815. $item->{nameID} = $args->{nameID};
  1816. $item->{identified} = $args->{identified};
  1817. $item->{broken} = $args->{broken};
  1818. $item->{upgrade} = $args->{upgrade};
  1819. $item->{cards} = $args->{cards};
  1820. $item->{name} = itemName($item);
  1821. message TF("%s added Item to Deal: %s x %s\n", $currentDeal{name}, $item->{name}, $args->{amount}), "deal";
  1822. } elsif ($args->{amount} > 0) {
  1823. $currentDeal{other_zeny} += $args->{amount};
  1824. my $amount = formatNumber($args->{amount});
  1825. message TF("%s added %s z to Deal\n", $currentDeal{name}, $amount), "deal";
  1826. }
  1827. }
  1828.  
  1829. sub deal_add_you {
  1830. my ($self, $args) = @_;
  1831.  
  1832. if ($args->{fail} == 1) {
  1833. error T("That person is overweight; you cannot trade.\n"), "deal";
  1834. return;
  1835. } elsif ($args->{fail} == 2) {
  1836. error T("This item cannot be traded.\n"), "deal";
  1837. return;
  1838. } elsif ($args->{fail}) {
  1839. error TF("You cannot trade (fail code %s).\n", $args->{fail}), "deal";
  1840. return;
  1841. }
  1842.  
  1843. return unless $args->{index} > 0;
  1844.  
  1845. my $item = $char->inventory->getByServerIndex($args->{index});
  1846. # FIXME: quickly add two items => lastItemAmount is lost => inventory corruption; see also Misc::dealAddItem
  1847. # FIXME: what will be in case of two items with the same nameID?
  1848. # TODO: no info about items is stored
  1849. $currentDeal{you}{$item->{nameID}}{amount} += $currentDeal{lastItemAmount};
  1850. $currentDeal{you}{$item->{nameID}}{nameID} = $item->{nameID};
  1851. $item->{amount} -= $currentDeal{lastItemAmount};
  1852. message TF("You added Item to Deal: %s x %s\n", $item->{name}, $currentDeal{lastItemAmount}), "deal";
  1853. $itemChange{$item->{name}} -= $currentDeal{lastItemAmount};
  1854. $currentDeal{you_items}++;
  1855. $args->{item} = $item;
  1856. $char->inventory->remove($item) if ($item->{amount} <= 0);
  1857. }
  1858.  
  1859. sub deal_begin {
  1860. my ($self, $args) = @_;
  1861.  
  1862. if ($args->{type} == 0) {
  1863. error T("That person is too far from you to trade.\n"), "deal";
  1864. } elsif ($args->{type} == 2) {
  1865. error T("That person is in another deal.\n"), "deal";
  1866. } elsif ($args->{type} == 3) {
  1867. if (%incomingDeal) {
  1868. $currentDeal{name} = $incomingDeal{name};
  1869. undef %incomingDeal;
  1870. } else {
  1871. my $ID = $outgoingDeal{ID};
  1872. my $player;
  1873. $player = $playersList->getByID($ID) if (defined $ID);
  1874. $currentDeal{ID} = $ID;
  1875. if ($player) {
  1876. $currentDeal{name} = $player->{name};
  1877. } else {
  1878. $currentDeal{name} = T('Unknown #') . unpack("V", $ID);
  1879. }
  1880. undef %outgoingDeal;
  1881. }
  1882. message TF("Engaged Deal with %s\n", $currentDeal{name}), "deal";
  1883. } elsif ($args->{type} == 5) {
  1884. error T("That person is opening storage.\n"), "deal";
  1885. } else {
  1886. error TF("Deal request failed (unknown error %s).\n", $args->{type}), "deal";
  1887. }
  1888. }
  1889.  
  1890. sub deal_cancelled {
  1891. undef %incomingDeal;
  1892. undef %outgoingDeal;
  1893. undef %currentDeal;
  1894. message T("Deal Cancelled\n"), "deal";
  1895. }
  1896.  
  1897. sub deal_complete {
  1898. undef %outgoingDeal;
  1899. undef %incomingDeal;
  1900. undef %currentDeal;
  1901. message T("Deal Complete\n"), "deal";
  1902. }
  1903.  
  1904. sub deal_finalize {
  1905. my ($self, $args) = @_;
  1906. if ($args->{type} == 1) {
  1907. $currentDeal{other_finalize} = 1;
  1908. message TF("%s finalized the Deal\n", $currentDeal{name}), "deal";
  1909.  
  1910. } else {
  1911. $currentDeal{you_finalize} = 1;
  1912. # FIXME: shouldn't we do this when we actually complete the deal?
  1913. $char->{zeny} -= $currentDeal{you_zeny};
  1914. message T("You finalized the Deal\n"), "deal";
  1915. }
  1916. }
  1917.  
  1918. sub deal_request {
  1919. my ($self, $args) = @_;
  1920. my $level = $args->{level} || 'Unknown'; # TODO: store this info
  1921. my $user = bytesToString($args->{user});
  1922.  
  1923. $incomingDeal{name} = $user;
  1924. $timeout{ai_dealAutoCancel}{time} = time;
  1925. message TF("%s (level %s) Requests a Deal\n", $user, $level), "deal";
  1926. message T("Type 'deal' to start dealing, or 'deal no' to deny the deal.\n"), "deal";
  1927. }
  1928.  
  1929. sub devotion {
  1930. my ($self, $args) = @_;
  1931. my $msg = '';
  1932. my $source = Actor::get($args->{sourceID});
  1933.  
  1934. undef $devotionList->{$args->{sourceID}};
  1935. for (my $i = 0; $i < 5; $i++) {
  1936. my $ID = substr($args->{targetIDs}, $i*4, 4);
  1937. last if unpack("V", $ID) == 0;
  1938. $devotionList->{$args->{sourceID}}->{targetIDs}->{$ID} = $i;
  1939. my $actor = Actor::get($ID);
  1940. #FIXME: Need a better display
  1941. $msg .= skillUseNoDamage_string($source, $actor, 0, 'devotion');
  1942. }
  1943. $devotionList->{$args->{sourceID}}->{range} = $args->{range};
  1944.  
  1945. message TF("$msg"), "devotion";
  1946. }
  1947.  
  1948. sub egg_list {
  1949. my ($self, $args) = @_;
  1950. message T("----- Egg Hatch Candidates -----\n"), "list";
  1951. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 2) {
  1952. my $index = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
  1953. my $item = $char->inventory->getByServerIndex($index);
  1954. message "$item->{invIndex} $item->{name}\n", "list";
  1955. }
  1956. message "------------------------------\n", "list";
  1957. }
  1958.  
  1959. sub emoticon {
  1960. my ($self, $args) = @_;
  1961. my $emotion = $emotions_lut{$args->{type}}{display} || "<emotion #$args->{type}>";
  1962.  
  1963. if ($args->{ID} eq $accountID) {
  1964. message "$char->{name}: $emotion\n", "emotion";
  1965. chatLog("e", "$char->{name}: $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  1966.  
  1967. } elsif (my $player = $playersList->getByID($args->{ID})) {
  1968. my $name = $player->name;
  1969.  
  1970. #my $dist = "unknown";
  1971. my $dist = distance($char->{pos_to}, $player->{pos_to});
  1972. $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  1973.  
  1974. # Translation Comment: "[dist=$dist] $name ($player->{binID}): $emotion\n"
  1975. message TF("[dist=%s] %s (%d): %s\n", $dist, $name, $player->{binID}, $emotion), "emotion";
  1976. chatLog("e", "$name".": $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  1977.  
  1978. my $index = AI::findAction("follow");
  1979. if ($index ne "") {
  1980. my $masterID = AI::args($index)->{ID};
  1981. if ($config{'followEmotion'} && $masterID eq $args->{ID} &&
  1982. distance($char->{pos_to}, $player->{pos_to}) <= $config{'followEmotion_distance'})
  1983. {
  1984. my %args = ();
  1985. $args{timeout} = time + rand (1) + 0.75;
  1986.  
  1987. if ($args->{type} == 30) {
  1988. $args{emotion} = 31;
  1989. } elsif ($args->{type} == 31) {
  1990. $args{emotion} = 30;
  1991. } else {
  1992. $args{emotion} = $args->{type};
  1993. }
  1994.  
  1995. AI::queue("sendEmotion", \%args);
  1996. }
  1997. }
  1998. } elsif (my $monster = $monstersList->getByID($args->{ID}) || $slavesList->getByID($args->{ID})) {
  1999. my $dist = distance($char->{pos_to}, $monster->{pos_to});
  2000. $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  2001.  
  2002. # Translation Comment: "[dist=$dist] $monster->name ($monster->{binID}): $emotion\n"
  2003. message TF("[dist=%s] %s %s (%d): %s\n", $dist, $monster->{actorType}, $monster->name, $monster->{binID}, $emotion), "emotion";
  2004.  
  2005. } else {
  2006. my $actor = Actor::get($args->{ID});
  2007. my $name = $actor->name;
  2008.  
  2009. my $dist = T("unknown");
  2010. if (!$actor->isa('Actor::Unknown')) {
  2011. $dist = distance($char->{pos_to}, $actor->{pos_to});
  2012. $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  2013. }
  2014.  
  2015. message TF("[dist=%s] %s: %s\n", $dist, $actor->nameIdx, $emotion), "emotion";
  2016. chatLog("e", "$name".": $emotion\n") if (existsInList($config{'logEmoticons'}, $args->{type}) || $config{'logEmoticons'} eq "all");
  2017. }
  2018. Plugins::callHook('packet_emotion', {
  2019. emotion => $emotion,
  2020. ID => $args->{ID}
  2021. });
  2022. }
  2023.  
  2024. sub equip_item {
  2025. my ($self, $args) = @_;
  2026. my $item = $char->inventory->getByServerIndex($args->{index});
  2027. if (!$args->{success}) {
  2028. message TF("You can't put on %s (%d)\n", $item->{name}, $item->{invIndex});
  2029. } else {
  2030. $item->{equipped} = $args->{type};
  2031. if ($args->{type} == 10 || $args->{type} == 32768) {
  2032. $char->{equipment}{arrow} = $item;
  2033. } else {
  2034. foreach (%equipSlot_rlut){
  2035. if ($_ & $args->{type}){
  2036. next if $_ == 10; # work around Arrow bug
  2037. next if $_ == 32768;
  2038. $char->{equipment}{$equipSlot_lut{$_}} = $item;
  2039. }
  2040. }
  2041. }
  2042. message TF("You equip %s (%d) - %s (type %s)\n", $item->{name}, $item->{invIndex},
  2043. $equipTypes_lut{$item->{type_equip}}, $args->{type}), 'inventory';
  2044. }
  2045. $ai_v{temp}{waitForEquip}-- if $ai_v{temp}{waitForEquip};
  2046. }
  2047.  
  2048. sub errors {
  2049. my ($self, $args) = @_;
  2050.  
  2051. Plugins::callHook('disconnected') if ($net->getState() == Network::IN_GAME);
  2052. if ($net->getState() == Network::IN_GAME &&
  2053. ($config{dcOnDisconnect} > 1 ||
  2054. ($config{dcOnDisconnect} &&
  2055. $args->{type} != 3 &&
  2056. $args->{type} != 10))) {
  2057. message T("Lost connection; exiting\n");
  2058. $quit = 1;
  2059. }
  2060.  
  2061. $net->setState(1);
  2062. undef $conState_tries;
  2063.  
  2064. $timeout_ex{'master'}{'time'} = time;
  2065. $timeout_ex{'master'}{'timeout'} = $timeout{'reconnect'}{'timeout'};
  2066. if (($args->{type} != 0)) {
  2067. $net->serverDisconnect();
  2068. }
  2069. if ($args->{type} == 0) {
  2070. # FIXME BAN_SERVER_SHUTDOWN is 0x1, 0x0 is BAN_UNFAIR
  2071. error T("Server shutting down\n"), "connection";
  2072. if($config{'dcOnServerShutDown'} == 1) {
  2073. $quit = 1;
  2074. }
  2075. } elsif ($args->{type} == 1) {
  2076. error T("Error: Server is closed\n"), "connection";
  2077. if($config{'dcOnServerClose'} == 1) {
  2078. $quit = 1;
  2079. }
  2080. } elsif ($args->{type} == 2) {
  2081. if ($config{'dcOnDualLogin'} == 1) {
  2082. $interface->errorDialog(TF("Critical Error: Dual login prohibited - Someone trying to login!\n\n" .
  2083. "%s will now immediately disconnect.", $Settings::NAME));
  2084. $quit = 1;
  2085. } elsif ($config{'dcOnDualLogin'} >= 2) {
  2086. error T("Critical Error: Dual login prohibited - Someone trying to login!\n"), "connection";
  2087. message TF("Disconnect for %s seconds...\n", $config{'dcOnDualLogin'}), "connection";
  2088. $timeout_ex{'master'}{'timeout'} = $config{'dcOnDualLogin'};
  2089. } else {
  2090. error T("Critical Error: Dual login prohibited - Someone trying to login!\n"), "connection";
  2091. }
  2092.  
  2093. } elsif ($args->{type} == 3) {
  2094. error T("Error: Out of sync with server\n"), "connection";
  2095. } elsif ($args->{type} == 4) {
  2096. # fRO: "Your account is not validated, please click on the validation link in your registration mail."
  2097. error T("Error: Server is jammed due to over-population.\n"), "connection";
  2098. } elsif ($args->{type} == 5) {
  2099. error T("Error: You are underaged and cannot join this server.\n"), "connection";
  2100. } elsif ($args->{type} == 6) {
  2101. $interface->errorDialog(T("Critical Error: You must pay to play this account!\n"));
  2102. $quit = 1 unless ($net->version == 1);
  2103. } elsif ($args->{type} == 8) {
  2104. error T("Error: The server still recognizes your last connection\n"), "connection";
  2105. } elsif ($args->{type} == 9) {
  2106. error T("Error: IP capacity of this Internet Cafe is full. Would you like to pay the personal base?\n"), "connection";
  2107. } elsif ($args->{type} == 10) {
  2108. error T("Error: You are out of available time paid for\n"), "connection";
  2109. } elsif ($args->{type} == 15) {
  2110. error T("Error: You have been forced to disconnect by a GM\n"), "connection";
  2111. } elsif ($args->{type} == 101) {
  2112. error T("Error: Your account has been suspended until the next maintenance period for possible use of 3rd party programs\n"), "connection";
  2113. } elsif ($args->{type} == 102) {
  2114. error T("Error: For an hour, more than 10 connections having same IP address, have made. Please check this matter.\n"), "connection";
  2115. } else {
  2116. error TF("Unknown error %s\n", $args->{type}), "connection";
  2117. }
  2118. }
  2119.  
  2120. sub forge_list {
  2121. my ($self, $args) = @_;
  2122.  
  2123. message T("========Forge List========\n");
  2124. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 8) {
  2125. my $viewID = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
  2126. message "$viewID $items_lut{$viewID}\n";
  2127. # always 0x0012
  2128. #my $unknown = unpack("v1", substr($args->{RAW_MSG}, $i+2, 2));
  2129. # ???
  2130. #my $charID = substr($args->{RAW_MSG}, $i+4, 4);
  2131. }
  2132. message "=========================\n";
  2133. }
  2134.  
  2135. sub friend_list {
  2136. my ($self, $args) = @_;
  2137.  
  2138. # Friend list
  2139. undef @friendsID;
  2140. undef %friends;
  2141. my $msg = $args->{RAW_MSG};
  2142. my $msg_size = $args->{RAW_MSG_SIZE};
  2143.  
  2144. my $ID = 0;
  2145. for (my $i = 4; $i < $msg_size; $i += 32) {
  2146. binAdd(\@friendsID, $ID);
  2147. $friends{$ID}{'accountID'} = substr($msg, $i, 4);
  2148. $friends{$ID}{'charID'} = substr($msg, $i + 4, 4);
  2149. $friends{$ID}{'name'} = bytesToString(unpack("Z24", substr($msg, $i + 8 , 24)));
  2150. $friends{$ID}{'online'} = 0;
  2151. $ID++;
  2152. }
  2153. }
  2154.  
  2155. sub friend_logon {
  2156. my ($self, $args) = @_;
  2157.  
  2158. # Friend In/Out
  2159. my $friendAccountID = $args->{friendAccountID};
  2160. my $friendCharID = $args->{friendCharID};
  2161. my $isNotOnline = $args->{isNotOnline};
  2162.  
  2163. for (my $i = 0; $i < @friendsID; $i++) {
  2164. if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
  2165. $friends{$i}{'online'} = 1 - $isNotOnline;
  2166. if ($isNotOnline) {
  2167. message TF("Friend %s has disconnected\n", $friends{$i}{name}), undef, 1;
  2168. } else {
  2169. message TF("Friend %s has connected\n", $friends{$i}{name}), undef, 1;
  2170. }
  2171. last;
  2172. }
  2173. }
  2174. }
  2175.  
  2176. sub friend_request {
  2177. my ($self, $args) = @_;
  2178.  
  2179. # Incoming friend request
  2180. $incomingFriend{'accountID'} = $args->{accountID};
  2181. $incomingFriend{'charID'} = $args->{charID};
  2182. $incomingFriend{'name'} = bytesToString($args->{name});
  2183. message TF("%s wants to be your friend\n", $incomingFriend{'name'});
  2184. message TF("Type 'friend accept' to be friend with %s, otherwise type 'friend reject'\n", $incomingFriend{'name'});
  2185. }
  2186.  
  2187. sub friend_removed {
  2188. my ($self, $args) = @_;
  2189.  
  2190. # Friend removed
  2191. my $friendAccountID = $args->{friendAccountID};
  2192. my $friendCharID = $args->{friendCharID};
  2193. for (my $i = 0; $i < @friendsID; $i++) {
  2194. if ($friends{$i}{'accountID'} eq $friendAccountID && $friends{$i}{'charID'} eq $friendCharID) {
  2195. message TF("%s is no longer your friend\n", $friends{$i}{'name'});
  2196. binRemove(\@friendsID, $i);
  2197. delete $friends{$i};
  2198. last;
  2199. }
  2200. }
  2201. }
  2202.  
  2203. sub friend_response {
  2204. my ($self, $args) = @_;
  2205.  
  2206. # Response to friend request
  2207. my $type = $args->{type};
  2208. my $name = bytesToString($args->{name});
  2209. if ($type) {
  2210. message TF("%s rejected to be your friend\n", $name);
  2211. } else {
  2212. my $ID = @friendsID;
  2213. binAdd(\@friendsID, $ID);
  2214. $friends{$ID}{accountID} = substr($args->{RAW_MSG}, 4, 4);
  2215. $friends{$ID}{charID} = substr($args->{RAW_MSG}, 8, 4);
  2216. $friends{$ID}{name} = $name;
  2217. $friends{$ID}{online} = 1;
  2218. message TF("%s is now your friend\n", $name);
  2219. }
  2220. }
  2221.  
  2222. sub homunculus_food {
  2223. my ($self, $args) = @_;
  2224. if ($args->{success}) {
  2225. message TF("Fed homunculus with %s\n", itemNameSimple($args->{foodID})), "homunculus";
  2226. } else {
  2227. error TF("Failed to feed homunculus with %s: no food in inventory.\n", itemNameSimple($args->{foodID})), "homunculus";
  2228. # auto-vaporize
  2229. if ($char->{homunculus} && $char->{homunculus}{hunger} <= 11 && timeOut($char->{homunculus}{vaporize_time}, 5)) {
  2230. $messageSender->sendSkillUse(244, 1, $accountID);
  2231. $char->{homunculus}{vaporize_time} = time;
  2232. error "Critical hunger level reached. Homunculus is put to rest.\n", "homunculus";
  2233. }
  2234. }
  2235. }
  2236.  
  2237. use constant {
  2238. HO_PRE_INIT => 0x0,
  2239. HO_RELATIONSHIP_CHANGED => 0x1,
  2240. HO_FULLNESS_CHANGED => 0x2,
  2241. HO_ACCESSORY_CHANGED => 0x3,
  2242. HO_HEADTYPE_CHANGED => 0x4,
  2243. };
  2244.  
  2245. # 0230
  2246. # TODO: what is type?
  2247. sub homunculus_info {
  2248. my ($self, $args) = @_;
  2249. debug "homunculus_info type: $args->{type}\n";
  2250. if ($args->{state} == HO_PRE_INIT) {
  2251. my $state = $char->{homunculus}{state}
  2252. if ($char->{homunculus} && $char->{homunculus}{ID} && $char->{homunculus}{ID} ne $args->{ID});
  2253. $char->{homunculus} = Actor::get($args->{ID});
  2254. $char->{homunculus}{state} = $state if (defined $state);
  2255. $char->{homunculus}{map} = $field->baseName;
  2256. unless ($char->{slaves}{$char->{homunculus}{ID}}) {
  2257. AI::SlaveManager::addSlave ($char->{homunculus});
  2258. }
  2259. } elsif ($args->{state} == HO_RELATIONSHIP_CHANGED) {
  2260. $char->{homunculus}{intimacy} = $args->{val} if $char->{homunculus};
  2261. } elsif ($args->{state} == HO_FULLNESS_CHANGED) {
  2262. $char->{homunculus}{hunger} = $args->{val} if $char->{homunculus};
  2263. } elsif ($args->{state} == HO_ACCESSORY_CHANGED) {
  2264. $char->{homunculus}{accessory} = $args->{val} if $char->{homunculus};
  2265. } elsif ($args->{state} == HO_HEADTYPE_CHANGED) {
  2266. #
  2267. }
  2268. }
  2269.  
  2270. # 029B
  2271. sub mercenary_init {
  2272. my ($self, $args) = @_;
  2273.  
  2274. $char->{mercenary} = Actor::get ($args->{ID}); # TODO: was it added to an actorList yet?
  2275. $char->{mercenary}{map} = $field->baseName;
  2276. unless ($char->{slaves}{$char->{mercenary}{ID}}) {
  2277. AI::SlaveManager::addSlave ($char->{mercenary});
  2278. }
  2279.  
  2280. my $slave = $char->{mercenary};
  2281.  
  2282. foreach (@{$args->{KEYS}}) {
  2283. $slave->{$_} = $args->{$_};
  2284. }
  2285. $slave->{name} = bytesToString($args->{name});
  2286.  
  2287. slave_calcproperty_handler($slave, $args);
  2288.  
  2289. # ST0's counterpart for ST kRO, since it attempts to support all servers
  2290. # TODO: we do this for homunculus, mercenary and our char... make 1 function and pass actor and attack_range?
  2291. if ($config{mercenary_attackDistanceAuto} && $config{attackDistance} != $slave->{attack_range} && exists $slave->{attack_range}) {
  2292. message TF("Autodetected attackDistance for mercenary = %s\n", $slave->{attack_range}), "success";
  2293. configModify('mercenary_attackDistance', $slave->{attack_range}, 1);
  2294. configModify('mercenary_attackMaxDistance', $slave->{attack_range}, 1);
  2295. }
  2296. }
  2297.  
  2298. # 022E
  2299. sub homunculus_property {
  2300. my ($self, $args) = @_;
  2301.  
  2302. my $slave = $char->{homunculus} or return;
  2303.  
  2304. foreach (@{$args->{KEYS}}) {
  2305. $slave->{$_} = $args->{$_};
  2306. }
  2307. $slave->{name} = bytesToString($args->{name});
  2308.  
  2309. slave_calcproperty_handler($slave, $args);
  2310. homunculus_state_handler($slave, $args);
  2311.  
  2312. # ST0's counterpart for ST kRO, since it attempts to support all servers
  2313. # TODO: we do this for homunculus, mercenary and our char... make 1 function and pass actor and attack_range?
  2314. # or make function in Actor class
  2315. if ($config{homunculus_attackDistanceAuto} && $config{attackDistance} != $slave->{attack_range} && exists $slave->{attack_range}) {
  2316. message TF("Autodetected attackDistance for homunculus = %s\n", $slave->{attack_range}), "success";
  2317. configModify('homunculus_attackDistance', $slave->{attack_range}, 1);
  2318. configModify('homunculus_attackMaxDistance', $slave->{attack_range}, 1);
  2319. }
  2320. }
  2321.  
  2322. sub homunculus_state_handler {
  2323. my ($slave, $args) = @_;
  2324. # Homunculus states:
  2325. # 0 - alive and unnamed
  2326. # 2 - rest
  2327. # 4 - dead
  2328.  
  2329. return unless $char->{homunculus};
  2330.  
  2331. if ($args->{state} == 0) {
  2332. $char->{homunculus}{renameflag} = 1;
  2333. } else {
  2334. $char->{homunculus}{renameflag} = 0;
  2335. }
  2336.  
  2337. if (($args->{state} & ~8) > 1) {
  2338. foreach my $handle (@{$char->{homunculus}{slave_skillsID}}) {
  2339. delete $char->{skills}{$handle};
  2340. }
  2341. $char->{homunculus}->clear();
  2342. undef @{$char->{homunculus}{slave_skillsID}};
  2343. if (defined $slave->{state} && $slave->{state} != $args->{state}) {
  2344. if ($args->{state} & 2) {
  2345. message T("Your Homunculus was vaporized!\n"), 'homunculus';
  2346. } elsif ($args->{state} & 4) {
  2347. message T("Your Homunculus died!\n"), 'homunculus';
  2348. }
  2349. }
  2350. } elsif (defined $slave->{state} && $slave->{state} != $args->{state}) {
  2351. if ($slave->{state} & 2) {
  2352. message T("Your Homunculus was recalled!\n"), 'homunculus';
  2353. } elsif ($slave->{state} & 4) {
  2354. message T("Your Homunculus was resurrected!\n"), 'homunculus';
  2355. }
  2356. }
  2357. }
  2358.  
  2359. # TODO: wouldn't it be better if we calculated these only at (first) request after a change in value, if requested at all?
  2360. sub slave_calcproperty_handler {
  2361. my ($slave, $args) = @_;
  2362. # so we don't devide by 0
  2363. # wtf
  2364. =pod
  2365. $slave->{hp_max} = ($args->{hp_max} > 0) ? $args->{hp_max} : $args->{hp};
  2366. $slave->{sp_max} = ($args->{sp_max} > 0) ? $args->{sp_max} : $args->{sp};
  2367. =cut
  2368.  
  2369. $slave->{attack_speed} = int (200 - (($args->{attack_delay} < 10) ? 10 : ($args->{attack_delay} / 10)));
  2370. $slave->{hpPercent} = $slave->{hp_max} ? ($slave->{hp} / $slave->{hp_max}) * 100 : undef;
  2371. $slave->{spPercent} = $slave->{sp_max} ? ($slave->{sp} / $slave->{sp_max}) * 100 : undef;
  2372. $slave->{expPercent} = ($args->{exp_max}) ? ($args->{exp} / $args->{exp_max}) * 100 : undef;
  2373. }
  2374.  
  2375. sub gameguard_grant {
  2376. my ($self, $args) = @_;
  2377.  
  2378. if ($args->{server} == 0) {
  2379. error T("The server Denied the login because GameGuard packets where not replied " .
  2380. "correctly or too many time has been spent to send the response.\n" .
  2381. "Please verify the version of your poseidon server and try again\n"), "poseidon";
  2382. return;
  2383. } elsif ($args->{server} == 1) {
  2384. message T("Server granted login request to account server\n"), "poseidon";
  2385. } else {
  2386. message T("Server granted login request to char/map server\n"), "poseidon";
  2387. # FIXME
  2388. change_to_constate25 if ($config{'gameGuard'} eq "2");
  2389. }
  2390. $net->setState(1.3) if ($net->getState() == 1.2);
  2391. }
  2392.  
  2393. sub gameguard_request {
  2394. # my ($self, $args) = @_;
  2395. #
  2396. # return if ($net->version == 1 && $config{gameGuard} ne '2');
  2397. # Poseidon::Client::getInstance()->query(
  2398. # substr($args->{RAW_MSG}, 0, $args->{RAW_MSG_SIZE})
  2399. # );
  2400. # debug "Querying Poseidon\n", "poseidon";
  2401. }
  2402.  
  2403. sub guild_allies_enemy_list {
  2404. my ($self, $args) = @_;
  2405.  
  2406. # Guild Allies/Enemy List
  2407. # <len>.w (<type>.l <guildID>.l <guild name>.24B).*
  2408. # type=0 Ally
  2409. # type=1 Enemy
  2410.  
  2411. # This is the length of the entire packet
  2412. my $msg = $args->{RAW_MSG};
  2413. my $len = unpack("v", substr($msg, 2, 2));
  2414.  
  2415. # clear $guild{enemy} and $guild{ally} otherwise bot will misremember alliances -zdivpsa
  2416. $guild{enemy} = {}; $guild{ally} = {};
  2417.  
  2418. for (my $i = 4; $i < $len; $i += 32) {
  2419. my ($type, $guildID, $guildName) = unpack('V2 Z24', substr($msg, $i, 32));
  2420. $guildName = bytesToString($guildName);
  2421. if ($type) {
  2422. # Enemy guild
  2423. $guild{enemy}{$guildID} = $guildName;
  2424. } else {
  2425. # Allied guild
  2426. $guild{ally}{$guildID} = $guildName;
  2427. }
  2428. debug "Your guild is ".($type ? 'enemy' : 'ally')." with guild $guildID ($guildName)\n", "guild";
  2429. }
  2430. }
  2431.  
  2432. sub guild_ally_request {
  2433. my ($self, $args) = @_;
  2434.  
  2435. my $ID = $args->{ID}; # is this a guild ID or account ID? Freya calls it an account ID
  2436. my $name = bytesToString($args->{guildName}); # Type: String
  2437.  
  2438. message TF("Incoming Request to Ally Guild '%s'\n", $name);
  2439. $incomingGuild{ID} = $ID;
  2440. $incomingGuild{Type} = 2;
  2441. $timeout{ai_guildAutoDeny}{time} = time;
  2442. }
  2443.  
  2444. sub guild_broken {
  2445. my ($self, $args) = @_;
  2446. my $flag = $args->{flag};
  2447.  
  2448. if ($flag == 2) {
  2449. error T("Guild can not be undone: there are still members in the guild\n");
  2450. } elsif ($flag == 1) {
  2451. error T("Guild can not be undone: invalid key\n");
  2452. } elsif ($flag == 0) {
  2453. message T("Guild broken.\n");
  2454. undef %{$char->{guild}};
  2455. undef $char->{guildID};
  2456. undef %guild;
  2457. } else {
  2458. error TF("Guild can not be undone: unknown reason (flag: %s)\n", $flag);
  2459. }
  2460. }
  2461.  
  2462. sub guild_member_setting_list {
  2463. my ($self, $args) = @_;
  2464. my $newmsg;
  2465. my $msg = $args->{RAW_MSG};
  2466. my $msg_size = $args->{RAW_MSG_SIZE};
  2467. $self->decrypt(\$newmsg, substr($msg, 4, length($msg)-4));
  2468. $msg = substr($msg, 0, 4).$newmsg;
  2469. my $gtIndex;
  2470. for (my $i = 4; $i < $msg_size; $i += 16) {
  2471. $gtIndex = unpack("V1", substr($msg, $i, 4));
  2472. $guild{positions}[$gtIndex]{invite} = (unpack("C1", substr($msg, $i + 4, 1)) & 0x01) ? 1 : '';
  2473. $guild{positions}[$gtIndex]{punish} = (unpack("C1", substr($msg, $i + 4, 1)) & 0x10) ? 1 : '';
  2474. $guild{positions}[$gtIndex]{feeEXP} = unpack("V1", substr($msg, $i + 12, 4));
  2475. }
  2476. }
  2477.  
  2478. # TODO: merge with skills_list?
  2479. sub guild_skills_list {
  2480. my ($self, $args) = @_;
  2481. my $msg = $args->{RAW_MSG};
  2482. my $msg_size = $args->{RAW_MSG_SIZE};
  2483. for (my $i = 6; $i < $msg_size; $i += 37) {
  2484. my $skillID = unpack("v1", substr($msg, $i, 2));
  2485. my $targetType = unpack("v1", substr($msg, $i+2, 2));
  2486. my $level = unpack("v1", substr($msg, $i + 6, 2));
  2487. my $sp = unpack("v1", substr($msg, $i + 8, 2));
  2488. my ($skillName) = unpack("Z*", substr($msg, $i + 12, 24));
  2489.  
  2490. my $up = unpack("C1", substr($msg, $i+36, 1));
  2491. $guild{skills}{$skillName}{ID} = $skillID;
  2492. $guild{skills}{$skillName}{sp} = $sp;
  2493. $guild{skills}{$skillName}{up} = $up;
  2494. $guild{skills}{$skillName}{targetType} = $targetType;
  2495. if (!$guild{skills}{$skillName}{lv}) {
  2496. $guild{skills}{$skillName}{lv} = $level;
  2497. }
  2498. }
  2499. }
  2500.  
  2501. sub guild_chat {
  2502. my ($self, $args) = @_;
  2503. my ($chatMsgUser, $chatMsg); # Type: String
  2504. my $chat; # Type: String
  2505.  
  2506. return unless changeToInGameState();
  2507.  
  2508. $chat = bytesToString($args->{message});
  2509. if (($chatMsgUser, $chatMsg) = $chat =~ /(.*?) : (.*)/) {
  2510. $chatMsgUser =~ s/ $//;
  2511. stripLanguageCode(\$chatMsg);
  2512. $chat = "$chatMsgUser : $chatMsg";
  2513. }
  2514.  
  2515. chatLog("g", "$chat\n") if ($config{'logGuildChat'});
  2516. # Translation Comment: Guild Chat
  2517. message TF("[Guild] %s\n", $chat), "guildchat";
  2518. # Only queue this if it's a real chat message
  2519. ChatQueue::add('g', 0, $chatMsgUser, $chatMsg) if ($chatMsgUser);
  2520.  
  2521. Plugins::callHook('packet_guildMsg', {
  2522. MsgUser => $chatMsgUser,
  2523. Msg => $chatMsg
  2524. });
  2525. }
  2526.  
  2527. sub guild_create_result {
  2528. my ($self, $args) = @_;
  2529. my $type = $args->{type};
  2530.  
  2531. my %types = (
  2532. 0 => T("Guild create successful.\n"),
  2533. 2 => T("Guild create failed: Guild name already exists.\n"),
  2534. 3 => T("Guild create failed: Emperium is needed.\n")
  2535. );
  2536. if ($types{$type}) {
  2537. message $types{$type};
  2538. } else {
  2539. message TF("Guild create: Unknown error %s\n", $type);
  2540. }
  2541. }
  2542.  
  2543. sub guild_expulsionlist {
  2544. my ($self, $args) = @_;
  2545.  
  2546. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 88) {
  2547. my ($name) = unpack("Z24", substr($args->{'RAW_MSG'}, $i, 24));
  2548. my $acc = unpack("Z24", substr($args->{'RAW_MSG'}, $i + 24, 24));
  2549. my ($cause) = unpack("Z44", substr($args->{'RAW_MSG'}, $i + 48, 44));
  2550. $guild{expulsion}{$acc}{name} = bytesToString($name);
  2551. $guild{expulsion}{$acc}{cause} = bytesToString($cause);
  2552. }
  2553. }
  2554.  
  2555. sub guild_info {
  2556. my ($self, $args) = @_;
  2557. # Guild Info
  2558. foreach (qw(ID lv conMember maxMember average exp exp_next tax tendency_left_right tendency_down_up name master castles_string)) {
  2559. $guild{$_} = $args->{$_};
  2560. }
  2561. $guild{name} = bytesToString($args->{name});
  2562. $guild{master} = bytesToString($args->{master});
  2563. $guild{members}++; # count ourselves in the guild members count
  2564. }
  2565.  
  2566. sub guild_invite_result {
  2567. my ($self, $args) = @_;
  2568.  
  2569. my $type = $args->{type};
  2570.  
  2571. my %types = (
  2572. 0 => T('Target is already in a guild.'),
  2573. 1 => T('Target has denied.'),
  2574. 2 => T('Target has accepted.'),
  2575. 3 => T('Your guild is full.')
  2576. );
  2577. if ($types{$type}) {
  2578. message TF("Guild join request: %s\n", $types{$type});
  2579. } else {
  2580. message TF("Guild join request: Unknown %s\n", $type);
  2581. }
  2582. }
  2583.  
  2584. sub guild_location {
  2585. # FIXME: not implemented
  2586. my ($self, $args) = @_;
  2587. unless ($args->{x} > 0 && $args->{y} > 0) {
  2588. # delete locator for ID
  2589. } else {
  2590. # add/replace locator for ID
  2591. }
  2592. }
  2593.  
  2594. sub guild_leave {
  2595. my ($self, $args) = @_;
  2596.  
  2597. message TF("%s has left the guild.\n" .
  2598. "Reason: %s\n", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
  2599. }
  2600.  
  2601. sub guild_expulsion {
  2602. my ($self, $args) = @_;
  2603.  
  2604. message TF("%s has been removed from the guild.\n" .
  2605. "Reason: %s\n", bytesToString($args->{name}), bytesToString($args->{message})), "schat";
  2606. }
  2607.  
  2608. sub guild_members_list {
  2609. my ($self, $args) = @_;
  2610.  
  2611. my ($newmsg, $jobID);
  2612. my $msg = $args->{RAW_MSG};
  2613. my $msg_size = $args->{RAW_MSG_SIZE};
  2614. $self->decrypt(\$newmsg, substr($msg, 4, length($msg) - 4));
  2615. $msg = substr($msg, 0, 4) . $newmsg;
  2616.  
  2617. my $c = 0;
  2618. delete $guild{member};
  2619. for (my $i = 4; $i < $msg_size; $i+=104){
  2620. $guild{member}[$c]{ID} = substr($msg, $i, 4);
  2621. $guild{member}[$c]{charID} = substr($msg, $i+4, 4);
  2622. $jobID = unpack('v', substr($msg, $i + 14, 2));
  2623. # wtf? i guess this was a 'hack' for when the 40xx jobs weren't added to the globals yet...
  2624. #if ($jobID =~ /^40/) {
  2625. # $jobID =~ s/^40/1/;
  2626. # $jobID += 60;
  2627. #}
  2628. $guild{member}[$c]{jobID} = $jobID;
  2629. $guild{member}[$c]{lv} = unpack('v', substr($msg, $i + 16, 2));
  2630. $guild{member}[$c]{contribution} = unpack('V', substr($msg, $i + 18, 4));
  2631. $guild{member}[$c]{online} = unpack('v', substr($msg, $i + 22, 2));
  2632. # TODO: we shouldn't store the guildtitle of a guildmember both in $guild{positions} and $guild{member}, instead we should just store the rank index of the guildmember and get the title from the $guild{positions}
  2633. my $gtIndex = unpack('V', substr($msg, $i + 26, 4));
  2634. $guild{member}[$c]{title} = $guild{positions}[$gtIndex]{title};
  2635. $guild{member}[$c]{name} = bytesToString(unpack('Z24', substr($msg, $i + 80, 24)));
  2636. $c++;
  2637. }
  2638.  
  2639. }
  2640.  
  2641. sub guild_member_online_status {
  2642. my ($self, $args) = @_;
  2643.  
  2644. foreach my $guildmember (@{$guild{member}}) {
  2645. if ($guildmember->{charID} eq $args->{charID}) {
  2646. if ($guildmember->{online} = $args->{online}) {
  2647. message TF("Guild member %s logged in.\n", $guildmember->{name}), "guildchat";
  2648. } else {
  2649. message TF("Guild member %s logged out.\n", $guildmember->{name}), "guildchat";
  2650. }
  2651. last;
  2652. }
  2653. }
  2654. }
  2655.  
  2656. sub misc_effect {
  2657. my ($self, $args) = @_;
  2658.  
  2659. my $actor = Actor::get($args->{ID});
  2660. message sprintf(
  2661. $actor->verb(T("%s use effect: %s\n"), T("%s uses effect: %s\n")),
  2662. $actor, defined $effectName{$args->{effect}} ? $effectName{$args->{effect}} : "Unknown #$args->{effect}"
  2663. ), 'effect'
  2664. }
  2665.  
  2666. sub guild_members_title_list {
  2667. my ($self, $args) = @_;
  2668.  
  2669. my $newmsg;
  2670. my $msg = $args->{RAW_MSG};
  2671. my $msg_size = $args->{RAW_MSG_SIZE};
  2672.  
  2673. $self->decrypt(\$newmsg, substr($msg, 4, length($msg) - 4));
  2674. $msg = substr($msg, 0, 4) . $newmsg;
  2675. my $gtIndex;
  2676. for (my $i = 4; $i < $msg_size; $i+=28) {
  2677. $gtIndex = unpack('V', substr($msg, $i, 4));
  2678. $guild{positions}[$gtIndex]{title} = bytesToString(unpack('Z24', substr($msg, $i + 4, 24)));
  2679. }
  2680. }
  2681.  
  2682. sub guild_name {
  2683. my ($self, $args) = @_;
  2684.  
  2685. my $guildID = $args->{guildID};
  2686. my $emblemID = $args->{emblemID};
  2687. my $mode = $args->{mode};
  2688. my $guildName = bytesToString($args->{guildName});
  2689. $char->{guild}{name} = $guildName;
  2690. $char->{guildID} = $guildID;
  2691. $char->{guild}{emblem} = $emblemID;
  2692.  
  2693. $messageSender->sendGuildRequestInfo(0); #requests for guild info packet 01B6 and 014C
  2694. $messageSender->sendGuildRequestInfo(1); #requests for guild member packet 0166 and 0154
  2695. debug "guild name: $guildName\n";
  2696. }
  2697.  
  2698. sub guild_notice {
  2699. my ($self, $args) = @_;
  2700.  
  2701. my $msg = $args->{RAW_MSG};
  2702. my ($address) = unpack("Z*", substr($msg, 2, 60));
  2703. my ($message) = unpack("Z*", substr($msg, 62, 120));
  2704. stripLanguageCode(\$address);
  2705. stripLanguageCode(\$message);
  2706. $address = bytesToString($address);
  2707. $message = bytesToString($message);
  2708.  
  2709. # don't show the huge guildmessage notice if there is none
  2710. # the client does something similar to this...
  2711. if ($address || $message) {
  2712. my $msg = TF("---Guild Notice---\n" .
  2713. "%s\n\n" .
  2714. "%s\n" .
  2715. "------------------\n", $address, $message);
  2716. message $msg, "guildnotice";
  2717. }
  2718.  
  2719. #message T("Requesting guild information...\n"), "info"; # Lets Disable this, its kinda useless.
  2720. $messageSender->sendGuildMasterMemberCheck();
  2721.  
  2722. # Replies 01B6 (Guild Info) and 014C (Guild Ally/Enemy List)
  2723. $messageSender->sendGuildRequestInfo(0);
  2724.  
  2725. # Replies 0166 (Guild Member Titles List) and 0154 (Guild Members List)
  2726. $messageSender->sendGuildRequestInfo(1);
  2727.  
  2728. }
  2729.  
  2730. sub guild_request {
  2731. my ($self, $args) = @_;
  2732.  
  2733. # Guild request
  2734. my $ID = $args->{ID};
  2735. my $name = bytesToString($args->{name});
  2736. message TF("Incoming Request to join Guild '%s'\n", $name);
  2737. $incomingGuild{'ID'} = $ID;
  2738. $incomingGuild{'Type'} = 1;
  2739. $timeout{'ai_guildAutoDeny'}{'time'} = time;
  2740. }
  2741.  
  2742. sub identify {
  2743. my ($self, $args) = @_;
  2744. if ($args->{flag} == 0) {
  2745. my $item = $char->inventory->getByServerIndex($args->{index});
  2746. $item->{identified} = 1;
  2747. $item->{type_equip} = $itemSlots_lut{$item->{nameID}};
  2748. message TF("Item Identified: %s (%d)\n", $item->{name}, $item->{invIndex}), "info";
  2749. } else {
  2750. message T("Item Appraisal has failed.\n");
  2751. }
  2752. undef @identifyID;
  2753. }
  2754.  
  2755. sub identify_list {
  2756. my ($self, $args) = @_;
  2757.  
  2758. my $newmsg;
  2759. my $msg = $args->{RAW_MSG};
  2760. my $msg_size = $args->{RAW_MSG_SIZE};
  2761. $self->decrypt(\$newmsg, substr($msg, 4));
  2762. $msg = substr($msg, 0, 4).$newmsg;
  2763.  
  2764. undef @identifyID;
  2765. for (my $i = 4; $i < $msg_size; $i += 2) {
  2766. my $index = unpack("v1", substr($msg, $i, 2));
  2767. my $item = $char->inventory->getByServerIndex($index);
  2768. binAdd(\@identifyID, $item->{invIndex});
  2769. }
  2770.  
  2771. my $num = @identifyID;
  2772. message TF("Received Possible Identify List (%s item(s)) - type 'identify'\n", $num), 'info';
  2773. }
  2774.  
  2775. # TODO: store this state
  2776. sub ignore_all_result {
  2777. my ($self, $args) = @_;
  2778. if ($args->{type} == 0) {
  2779. message T("All Players ignored\n");
  2780. } elsif ($args->{type} == 1) {
  2781. if ($args->{error} == 0) {
  2782. message T("All players unignored\n");
  2783. }
  2784. }
  2785. }
  2786.  
  2787. # TODO: store list of ignored players
  2788. sub ignore_player_result {
  2789. my ($self, $args) = @_;
  2790. if ($args->{type} == 0) {
  2791. message T("Player ignored\n");
  2792. } elsif ($args->{type} == 1) {
  2793. if ($args->{error} == 0) {
  2794. message T("Player unignored\n");
  2795. }
  2796. }
  2797. }
  2798.  
  2799. sub whisper_list {
  2800. my ($self, $args) = @_;
  2801.  
  2802. my @whisperList = unpack 'x4' . (' Z24' x (($args->{RAW_MSG_SIZE}-4)/24)), $args->{RAW_MSG};
  2803.  
  2804. debug "whisper_list: @whisperList\n", "parseMsg";
  2805. }
  2806.  
  2807. sub inventory_item_added {
  2808. my ($self, $args) = @_;
  2809.  
  2810. return unless changeToInGameState();
  2811.  
  2812. my ($index, $amount, $fail) = ($args->{index}, $args->{amount}, $args->{fail});
  2813.  
  2814. if (!$fail) {
  2815. my $item = $char->inventory->getByServerIndex($index);
  2816. if (!$item) {
  2817. # Add new item
  2818. $item = new Actor::Item();
  2819. $item->{index} = $index;
  2820. $item->{nameID} = $args->{nameID};
  2821. $item->{type} = $args->{type};
  2822. $item->{type_equip} = $args->{type_equip};
  2823. $item->{amount} = $amount;
  2824. $item->{identified} = $args->{identified};
  2825. $item->{broken} = $args->{broken};
  2826. $item->{upgrade} = $args->{upgrade};
  2827. $item->{cards} = ($args->{switch} eq '029A') ? $args->{cards} + $args->{cards_ext}: $args->{cards};
  2828. if ($args->{switch} eq '029A') {
  2829. $args->{cards} .= $args->{cards_ext};
  2830. } elsif ($args->{switch} eq '02D4') {
  2831. $item->{expire} = $args->{expire} if (exists $args->{expire}); #a4 or V1 unpacking?
  2832. }
  2833. $item->{name} = itemName($item);
  2834. $char->inventory->add($item);
  2835. } else {
  2836. # Add stackable item
  2837. $item->{amount} += $amount;
  2838. }
  2839.  
  2840. $itemChange{$item->{name}} += $amount;
  2841. my $disp = TF("Item added to inventory: %s (%d) x %d - %s",
  2842. $item->{name}, $item->{invIndex}, $amount, $itemTypes_lut{$item->{type}});
  2843. message "$disp\n", "drop";
  2844. $disp .= " (". $field->baseName . ")\n";
  2845. itemLog($disp);
  2846.  
  2847. Plugins::callHook('item_gathered',{item => $item->{name}});
  2848.  
  2849. $args->{item} = $item;
  2850.  
  2851. # TODO: move this stuff to AI()
  2852. if (grep {$_ eq $item->{nameID}} @{$ai_v{npc_talk}{itemsIDlist}}, $ai_v{npc_talk}{itemID}) {
  2853.  
  2854. $ai_v{'npc_talk'}{'talk'} = 'buy';
  2855. $ai_v{'npc_talk'}{'time'} = time;
  2856. }
  2857.  
  2858. if ($AI == AI::AUTO) {
  2859. # Auto-drop item
  2860. if (pickupitems(lc($item->{name})) == -1 && !AI::inQueue('storageAuto', 'buyAuto')) {
  2861. $messageSender->sendDrop($item->{index}, $amount);
  2862. message TF("Auto-dropping item: %s (%d) x %d\n", $item->{name}, $item->{invIndex}, $amount), "drop";
  2863. }
  2864. }
  2865.  
  2866. } elsif ($fail == 6) {
  2867. message T("Can't loot item...wait...\n"), "drop";
  2868. } elsif ($fail == 2) {
  2869. message T("Cannot pickup item (inventory full)\n"), "drop";
  2870. } elsif ($fail == 1) {
  2871. message T("Cannot pickup item (you're Frozen?)\n"), "drop";
  2872. } else {
  2873. message TF("Cannot pickup item (failure code %d)\n", $fail), "drop";
  2874. }
  2875. }
  2876.  
  2877. sub item_used {
  2878. my ($self, $args) = @_;
  2879.  
  2880. my ($index, $itemID, $ID, $remaining, $success) =
  2881. @{$args}{qw(index itemID ID remaining success)};
  2882. my %hook_args = (
  2883. serverIndex => $index,
  2884. itemID => $itemID,
  2885. userID => $ID,
  2886. remaining => $remaining,
  2887. success => $success
  2888. );
  2889.  
  2890. if ($ID eq $accountID) {
  2891. my $item = $char->inventory->getByServerIndex($index);
  2892. if ($item) {
  2893. if ($success == 1) {
  2894. my $amount = $item->{amount} - $remaining;
  2895. $item->{amount} -= $amount;
  2896.  
  2897. message TF("You used Item: %s (%d) x %d - %d left\n", $item->{name}, $item->{invIndex},
  2898. $amount, $remaining), "useItem", 1;
  2899. $itemChange{$item->{name}}--;
  2900. if ($item->{amount} <= 0) {
  2901. $char->inventory->remove($item);
  2902. }
  2903.  
  2904. $hook_args{item} = $item;
  2905. $hook_args{invIndex} = $item->{invIndex};
  2906. $hook_args{name} => $item->{name};
  2907. $hook_args{amount} = $amount;
  2908.  
  2909. } else {
  2910. message TF("You failed to use item: %s (%d)\n", $item ? $item->{name} : "#$itemID", $remaining), "useItem", 1;
  2911. }
  2912. } else {
  2913. if ($success == 1) {
  2914. message TF("You used unknown item #%d - %d left\n", $itemID, $remaining), "useItem", 1;
  2915. } else {
  2916. message TF("You failed to use unknown item #%d - %d left\n", $itemID, $remaining), "useItem", 1;
  2917. }
  2918. }
  2919. } else {
  2920. my $actor = Actor::get($ID);
  2921. my $itemDisplay = itemNameSimple($itemID);
  2922. message TF("%s used Item: %s - %s left\n", $actor, $itemDisplay, $remaining), "useItem", 2;
  2923. }
  2924. Plugins::callHook('packet_useitem', \%hook_args);
  2925. }
  2926.  
  2927. sub married {
  2928. my ($self, $args) = @_;
  2929.  
  2930. my $actor = Actor::get($args->{ID});
  2931. message TF("%s got married!\n", $actor);
  2932. }
  2933.  
  2934. sub inventory_items_nonstackable {
  2935. my ($self, $args) = @_;
  2936. return unless changeToInGameState();
  2937.  
  2938. $self->_items_list({
  2939. class => 'Actor::Item',
  2940. hook => 'packet_inventory',
  2941. debug_str => 'Non-Stackable Inventory Item',
  2942. items => [$self->parse_items_nonstackable($args)],
  2943. getter => sub { $char->inventory->getByServerIndex($_[0]{index}) },
  2944. adder => sub { $char->inventory->add($_[0]) },
  2945. callback => sub {
  2946. my ($local_item) = @_;
  2947.  
  2948. if ($local_item->{equipped}) {
  2949. foreach (%equipSlot_rlut){
  2950. if ($_ & $local_item->{equipped}){
  2951. next if $_ == 10; #work around Arrow bug
  2952. next if $_ == 32768;
  2953. $char->{equipment}{$equipSlot_lut{$_}} = $local_item;
  2954. }
  2955. }
  2956. }
  2957. }
  2958. });
  2959.  
  2960. $ai_v{'inventory_time'} = time + 1;
  2961. $ai_v{'cart_time'} = time + 1;
  2962. }
  2963.  
  2964. sub inventory_items_stackable {
  2965. my ($self, $args) = @_;
  2966. return unless changeToInGameState();
  2967.  
  2968. $self->_items_list({
  2969. class => 'Actor::Item',
  2970. hook => 'packet_inventory',
  2971. debug_str => 'Stackable Inventory Item',
  2972. items => [$self->parse_items_stackable($args)],
  2973. getter => sub { $char->inventory->getByServerIndex($_[0]{index}) },
  2974. adder => sub { $char->inventory->add($_[0]) },
  2975. callback => sub {
  2976. my ($local_item) = @_;
  2977.  
  2978. if (defined $char->{arrow} && $local_item->{index} == $char->{arrow}) {
  2979. $local_item->{equipped} = 32768;
  2980. $char->{equipment}{arrow} = $local_item;
  2981. }
  2982. }
  2983. });
  2984.  
  2985. $ai_v{'inventory_time'} = time + 1;
  2986. $ai_v{'cart_time'} = time + 1;
  2987. }
  2988.  
  2989. sub item_appeared {
  2990. my ($self, $args) = @_;
  2991. return unless changeToInGameState();
  2992.  
  2993. my $item = $itemsList->getByID($args->{ID});
  2994. my $mustAdd;
  2995. if (!$item) {
  2996. $item = new Actor::Item();
  2997. $item->{appear_time} = time;
  2998. $item->{amount} = $args->{amount};
  2999. $item->{nameID} = $args->{nameID};
  3000. $item->{identified} = $args->{identified};
  3001. $item->{name} = itemName($item);
  3002. $item->{ID} = $args->{ID};
  3003. $mustAdd = 1;
  3004. }
  3005. $item->{pos}{x} = $args->{x};
  3006. $item->{pos}{y} = $args->{y};
  3007. $item->{pos_to}{x} = $args->{x};
  3008. $item->{pos_to}{y} = $args->{y};
  3009. $itemsList->add($item) if ($mustAdd);
  3010.  
  3011. # Take item as fast as possible
  3012. if ($AI == AI::AUTO && pickupitems(lc($item->{name})) == 2
  3013. && ($config{'itemsTakeAuto'} || $config{'itemsGatherAuto'})
  3014. && (percent_weight($char) < $config{'itemsMaxWeight'})
  3015. && distance($item->{pos}, $char->{pos_to}) <= 5) {
  3016. $messageSender->sendTake($args->{ID});
  3017. }
  3018.  
  3019. message TF("Item Appeared: %s (%d) x %d (%d, %d)\n", $item->{name}, $item->{binID}, $item->{amount}, $args->{x}, $args->{y}), "drop", 1;
  3020.  
  3021. }
  3022.  
  3023. sub item_exists {
  3024. my ($self, $args) = @_;
  3025. return unless changeToInGameState();
  3026.  
  3027. my $item = $itemsList->getByID($args->{ID});
  3028. my $mustAdd;
  3029. if (!$item) {
  3030. $item = new Actor::Item();
  3031. $item->{appear_time} = time;
  3032. $item->{amount} = $args->{amount};
  3033. $item->{nameID} = $args->{nameID};
  3034. $item->{ID} = $args->{ID};
  3035. $item->{identified} = $args->{identified};
  3036. $item->{name} = itemName($item);
  3037. $mustAdd = 1;
  3038. }
  3039. $item->{pos}{x} = $args->{x};
  3040. $item->{pos}{y} = $args->{y};
  3041. $item->{pos_to}{x} = $args->{x};
  3042. $item->{pos_to}{y} = $args->{y};
  3043. $itemsList->add($item) if ($mustAdd);
  3044.  
  3045. message TF("Item Exists: %s (%d) x %d\n", $item->{name}, $item->{binID}, $item->{amount}), "drop", 1;
  3046. }
  3047.  
  3048. sub item_disappeared {
  3049. my ($self, $args) = @_;
  3050. return unless changeToInGameState();
  3051.  
  3052. my $item = $itemsList->getByID($args->{ID});
  3053. if ($item) {
  3054. if ($config{attackLooters} && AI::action ne "sitAuto" && pickupitems(lc($item->{name})) > 0) {
  3055. foreach my Actor::Monster $monster (@{$monstersList->getItems()}) { # attack looter code
  3056. if (my $control = mon_control($monster->name,$monster->{nameID})) {
  3057. next if ( ($control->{attack_auto} ne "" && $control->{attack_auto} == -1)
  3058. || ($control->{attack_lvl} ne "" && $control->{attack_lvl} > $char->{lv})
  3059. || ($control->{attack_jlvl} ne "" && $control->{attack_jlvl} > $char->{lv_job})
  3060. || ($control->{attack_hp} ne "" && $control->{attack_hp} > $char->{hp})
  3061. || ($control->{attack_sp} ne "" && $control->{attack_sp} > $char->{sp})
  3062. );
  3063. }
  3064. if (distance($item->{pos}, $monster->{pos}) == 0) {
  3065. attack($monster->{ID});
  3066. message TF("Attack Looter: %s looted %s\n", $monster->nameIdx, $item->{name}), "looter";
  3067. last;
  3068. }
  3069. }
  3070. }
  3071.  
  3072. debug "Item Disappeared: $item->{name} ($item->{binID})\n", "parseMsg_presence";
  3073. my $ID = $args->{ID};
  3074. $items_old{$ID} = $item->deepCopy();
  3075. $items_old{$ID}{disappeared} = 1;
  3076. $items_old{$ID}{gone_time} = time;
  3077. $itemsList->removeByID($ID);
  3078. }
  3079. }
  3080.  
  3081. sub item_skill {
  3082. my ($self, $args) = @_;
  3083.  
  3084. my $skillID = $args->{skillID};
  3085. my $targetType = $args->{targetType}; # we don't use this yet
  3086. my $skillLv = $args->{skillLv};
  3087. my $sp = $args->{sp}; # we don't use this yet
  3088. my $skillName = $args->{skillName};
  3089.  
  3090. my $skill = new Skill(idn => $skillID, level => $skillLv);
  3091. message TF("Permitted to use %s (%d), level %d\n", $skill->getName, $skill->getIDN, $skill->getLevel);
  3092.  
  3093. unless ($config{noAutoSkill}) {
  3094. $messageSender->sendSkillUse($skillID, $skillLv, $accountID);
  3095. undef $char->{permitSkill};
  3096. } else {
  3097. $char->{permitSkill} = $skill;
  3098. }
  3099.  
  3100. Plugins::callHook('item_skill', {
  3101. ID => $skillID,
  3102. level => $skillLv,
  3103. name => $skillName
  3104. });
  3105. }
  3106.  
  3107. sub item_upgrade {
  3108. my ($self, $args) = @_;
  3109. my ($type, $index, $upgrade) = @{$args}{qw(type index upgrade)};
  3110.  
  3111. my $item = $char->inventory->getByServerIndex($index);
  3112. if ($item) {
  3113. $item->{upgrade} = $upgrade;
  3114. message TF("Item %s has been upgraded to +%s\n", $item->{name}, $upgrade), "parseMsg/upgrade";
  3115. $item->setName(itemName($item));
  3116. }
  3117. }
  3118.  
  3119. sub job_equipment_hair_change {
  3120. my ($self, $args) = @_;
  3121. return unless changeToInGameState();
  3122.  
  3123. my $actor = Actor::get($args->{ID});
  3124. assert(UNIVERSAL::isa($actor, "Actor")) if DEBUG;
  3125.  
  3126. if ($args->{part} == 0) {
  3127. # Job change
  3128. $actor->{jobID} = $args->{number};
  3129. message TF("%s changed job to: %s\n", $actor, $jobs_lut{$args->{number}}), "parseMsg/job", ($actor->isa('Actor::You') ? 0 : 2);
  3130.  
  3131. } elsif ($args->{part} == 3) {
  3132. # Bottom headgear change
  3133. message TF("%s changed bottom headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
  3134. $actor->{headgear}{low} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
  3135.  
  3136. } elsif ($args->{part} == 4) {
  3137. # Top headgear change
  3138. message TF("%s changed top headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
  3139. $actor->{headgear}{top} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
  3140.  
  3141. } elsif ($args->{part} == 5) {
  3142. # Middle headgear change
  3143. message TF("%s changed middle headgear to: %s\n", $actor, headgearName($args->{number})), "parseMsg_statuslook", 2 unless $actor->isa('Actor::You');
  3144. $actor->{headgear}{mid} = $args->{number} if ($actor->isa('Actor::Player') || $actor->isa('Actor::You'));
  3145.  
  3146. } elsif ($args->{part} == 6) {
  3147. # Hair color change
  3148. $actor->{hair_color} = $args->{number};
  3149. message TF("%s changed hair color to: %s (%s)\n", $actor, $haircolors{$args->{number}}, $args->{number}), "parseMsg/hairColor", ($actor->isa('Actor::You') ? 0 : 2);
  3150. }
  3151.  
  3152. #my %parts = (
  3153. # 0 => 'Body',
  3154. # 2 => 'Right Hand',
  3155. # 3 => 'Low Head',
  3156. # 4 => 'Top Head',
  3157. # 5 => 'Middle Head',
  3158. # 8 => 'Left Hand'
  3159. #);
  3160. #if ($part == 3) {
  3161. # $part = 'low';
  3162. #} elsif ($part == 4) {
  3163. # $part = 'top';
  3164. #} elsif ($part == 5) {
  3165. # $part = 'mid';
  3166. #}
  3167. #
  3168. #my $name = getActorName($ID);
  3169. #if ($part == 3 || $part == 4 || $part == 5) {
  3170. # my $actor = Actor::get($ID);
  3171. # $actor->{headgear}{$part} = $items_lut{$number} if ($actor);
  3172. # my $itemName = $items_lut{$itemID};
  3173. # $itemName = 'nothing' if (!$itemName);
  3174. # debug "$name changes $parts{$part} ($part) equipment to $itemName\n", "parseMsg";
  3175. #} else {
  3176. # debug "$name changes $parts{$part} ($part) equipment to item #$number\n", "parseMsg";
  3177. #}
  3178.  
  3179. }
  3180.  
  3181. # Leap, Snap, Back Slide... Various knockback
  3182. sub high_jump {
  3183. my ($self, $args) = @_;
  3184. return unless changeToInGameState();
  3185.  
  3186. my $actor = Actor::get ($args->{ID});
  3187. if (!defined $actor) {
  3188. $actor = new Actor::Unknown;
  3189. $actor->{appear_time} = time;
  3190. $actor->{nameID} = unpack ('V', $args->{ID});
  3191. } elsif ($actor->{pos_to}{x} == $args->{x} && $actor->{pos_to}{y} == $args->{y}) {
  3192. message TF("%s failed to instantly move\n", $actor->nameString), 'skill';
  3193. return;
  3194. }
  3195.  
  3196. $actor->{pos} = {x => $args->{x}, y => $args->{y}};
  3197. $actor->{pos_to} = {x => $args->{x}, y => $args->{y}};
  3198.  
  3199. message TF("%s instantly moved to %d, %d\n", $actor->nameString, $actor->{pos_to}{x}, $actor->{pos_to}{y}), 'skill', 2;
  3200.  
  3201. $actor->{time_move} = time;
  3202. $actor->{time_move_calc} = 0;
  3203. }
  3204.  
  3205. sub hp_sp_changed {
  3206. my ($self, $args) = @_;
  3207. return unless changeToInGameState();
  3208.  
  3209. my $type = $args->{type};
  3210. my $amount = $args->{amount};
  3211. if ($type == 5) {
  3212. $char->{hp} += $amount;
  3213. $char->{hp} = $char->{hp_max} if ($char->{hp} > $char->{hp_max});
  3214. } elsif ($type == 7) {
  3215. $char->{sp} += $amount;
  3216. $char->{sp} = $char->{sp_max} if ($char->{sp} > $char->{sp_max});
  3217. }
  3218. }
  3219.  
  3220. sub local_broadcast {
  3221. my ($self, $args) = @_;
  3222. my $message = bytesToString($args->{message});
  3223. stripLanguageCode(\$message);
  3224. chatLog("lb", "$message\n");# if ($config{logLocalBroadcast});
  3225. message "$message\n", "schat";
  3226. Plugins::callHook('packet_localBroadcast', {
  3227. Msg => $message
  3228. });
  3229. }
  3230.  
  3231. sub login_error {
  3232. my ($self, $args) = @_;
  3233.  
  3234. $net->serverDisconnect();
  3235. if ($args->{type} == REFUSE_INVALID_ID) {
  3236. error TF("Account name [%s] doesn't exist\n", $config{'username'}), "connection";
  3237. if (!$net->clientAlive() && !$config{'ignoreInvalidLogin'} && !UNIVERSAL::isa($net, 'Network::XKoreProxy')) {
  3238. my $username = $interface->query(T("Enter your Ragnarok Online username again."));
  3239. if (defined($username)) {
  3240. configModify('username', $username, 1);
  3241. $timeout_ex{master}{time} = 0;
  3242. $conState_tries = 0;
  3243. } else {
  3244. quit();
  3245. return;
  3246. }
  3247. }
  3248. } elsif ($args->{type} == REFUSE_INVALID_PASSWD) {
  3249. error TF("Password Error for account [%s]\n", $config{'username'}), "connection";
  3250. if (!$net->clientAlive() && !$config{'ignoreInvalidLogin'} && !UNIVERSAL::isa($net, 'Network::XKoreProxy')) {
  3251. my $password = $interface->query(T("Enter your Ragnarok Online password again."), isPassword => 1);
  3252. if (defined($password)) {
  3253. configModify('password', $password, 1);
  3254. $timeout_ex{master}{time} = 0;
  3255. $conState_tries = 0;
  3256. } else {
  3257. quit();
  3258. return;
  3259. }
  3260. }
  3261. } elsif ($args->{type} == ACCEPT_ID_PASSWD) {
  3262. error T("The server has denied your connection.\n"), "connection";
  3263. } elsif ($args->{type} == REFUSE_NOT_CONFIRMED) {
  3264. $interface->errorDialog(T("Critical Error: Your account has been blocked."));
  3265. $quit = 1 unless ($net->clientAlive());
  3266. } elsif ($args->{type} == REFUSE_INVALID_VERSION) {
  3267. my $master = $masterServer;
  3268. error TF("Connect failed, something is wrong with the login settings:\n" .
  3269. "version: %s\n" .
  3270. "master_version: %s\n" .
  3271. "serverType: %s\n", $master->{version}, $master->{master_version}, $config{serverType}), "connection";
  3272. relog(30);
  3273. } elsif ($args->{type} == REFUSE_BLOCK_TEMPORARY) {
  3274. error TF("The server is temporarily blocking your connection until %s\n", $args->{date}), "connection";
  3275. } elsif ($args->{type} == REFUSE_USER_PHONE_BLOCK) { #Phone lock
  3276. error T("Please dial to activate the login procedure.\n"), "connection";
  3277. Plugins::callHook('dial');
  3278. relog(10);
  3279. } elsif ($args->{type} == ACCEPT_LOGIN_USER_PHONE_BLOCK) {
  3280. error T("Mobile Authentication: Max number of simultaneous IP addresses reached.\n"), "connection";
  3281. } else {
  3282. error TF("The server has denied your connection for unknown reason (%d).\n", $args->{type}), 'connection';
  3283. }
  3284.  
  3285. if ($args->{type} != REFUSE_INVALID_VERSION && $versionSearch) {
  3286. $versionSearch = 0;
  3287. writeSectionedFileIntact(Settings::getTableFilename("servers.txt"), \%masterServers);
  3288. }
  3289. }
  3290.  
  3291. sub login_error_game_login_server {
  3292. error T("Error logging into Character Server (invalid character specified)...\n"), 'connection';
  3293. $net->setState(1);
  3294. undef $conState_tries;
  3295. $timeout_ex{master}{time} = time;
  3296. $timeout_ex{master}{timeout} = $timeout{'reconnect'}{'timeout'};
  3297. $net->serverDisconnect();
  3298. }
  3299.  
  3300. # The difference between map_change and map_changed is that map_change
  3301. # represents a map change event on the current map server, while
  3302. # map_changed means that you've changed to a different map server.
  3303. # map_change also represents teleport events.
  3304. sub map_change {
  3305. my ($self, $args) = @_;
  3306. return unless changeToInGameState();
  3307.  
  3308. my $oldMap = $field ? $field->baseName : undef; # Get old Map name without InstanceID
  3309. my ($map) = $args->{map} =~ /([\s\S]*)\./;
  3310. my $map_noinstance;
  3311. ($map_noinstance, undef) = Field::nameToBaseName(undef, $map); # Hack to clean up InstanceID
  3312.  
  3313. checkAllowedMap($map_noinstance);
  3314. if (!$field || $map ne $field->name()) {
  3315. eval {
  3316. $field = new Field(name => $map);
  3317. };
  3318. if (my $e = caught('FileNotFoundException', 'IOException')) {
  3319. error TF("Cannot load field %s: %s\n", $map_noinstance, $e);
  3320. undef $field;
  3321. } elsif ($@) {
  3322. die $@;
  3323. }
  3324. }
  3325.  
  3326. if ($ai_v{temp}{clear_aiQueue}) {
  3327. AI::clear;
  3328. AI::SlaveManager::clear();
  3329. }
  3330.  
  3331. main::initMapChangeVars();
  3332. for (my $i = 0; $i < @ai_seq; $i++) {
  3333. ai_setMapChanged($i);
  3334. }
  3335. AI::SlaveManager::setMapChanged ();
  3336. if ($net->version == 0) {
  3337. $ai_v{portalTrace_mapChanged} = time;
  3338. }
  3339.  
  3340. my %coords = (
  3341. x => $args->{x},
  3342. y => $args->{y}
  3343. );
  3344. $char->{pos} = {%coords};
  3345. $char->{pos_to} = {%coords};
  3346. message TF("Map Change: %s (%s, %s)\n", $args->{map}, $char->{pos}{x}, $char->{pos}{y}), "connection";
  3347. if ($net->version == 1) {
  3348. ai_clientSuspend(0, 10);
  3349. } else {
  3350. $messageSender->sendMapLoaded();
  3351. # $messageSender->sendSync(1);
  3352. $timeout{ai}{time} = time;
  3353. }
  3354.  
  3355. Plugins::callHook('Network::Receive::map_changed', {
  3356. oldMap => $oldMap,
  3357. });
  3358.  
  3359. $timeout{ai}{time} = time;
  3360. }
  3361.  
  3362. sub map_changed {
  3363. my ($self, $args) = @_;
  3364. $net->setState(4);
  3365.  
  3366. my $oldMap = $field ? $field->baseName : undef; # Get old Map name without InstanceID
  3367. my ($map) = $args->{map} =~ /([\s\S]*)\./;
  3368. my $map_noinstance;
  3369. ($map_noinstance, undef) = Field::nameToBaseName(undef, $map); # Hack to clean up InstanceID
  3370.  
  3371. checkAllowedMap($map_noinstance);
  3372. if (!$field || $map ne $field->name()) {
  3373. eval {
  3374. $field = new Field(name => $map);
  3375. };
  3376. if (my $e = caught('FileNotFoundException', 'IOException')) {
  3377. error TF("Cannot load field %s: %s\n", $map_noinstance, $e);
  3378. undef $field;
  3379. } elsif ($@) {
  3380. die $@;
  3381. }
  3382. }
  3383.  
  3384. my %coords = (
  3385. x => $args->{x},
  3386. y => $args->{y}
  3387. );
  3388. $char->{pos} = {%coords};
  3389. $char->{pos_to} = {%coords};
  3390.  
  3391. undef $conState_tries;
  3392. for (my $i = 0; $i < @ai_seq; $i++) {
  3393. ai_setMapChanged($i);
  3394. }
  3395. AI::SlaveManager::setMapChanged ();
  3396. $ai_v{portalTrace_mapChanged} = time;
  3397.  
  3398. $map_ip = makeIP($args->{IP});
  3399. $map_port = $args->{port};
  3400. message(swrite(
  3401. "---------Map Info----------", [],
  3402. "MAP Name: @<<<<<<<<<<<<<<<<<<",
  3403. [$args->{map}],
  3404. "MAP IP: @<<<<<<<<<<<<<<<<<<",
  3405. [$map_ip],
  3406. "MAP Port: @<<<<<<<<<<<<<<<<<<",
  3407. [$map_port],
  3408. "-------------------------------", []),
  3409. "connection");
  3410.  
  3411. message T("Closing connection to Map Server\n"), "connection";
  3412. $net->serverDisconnect unless ($net->version == 1);
  3413.  
  3414. # Reset item and skill times. The effect of items (like aspd potions)
  3415. # and skills (like Twohand Quicken) disappears when we change map server.
  3416. # NOTE: with the newer servers, this isn't true anymore
  3417. my $i = 0;
  3418. while (exists $config{"useSelf_item_$i"}) {
  3419. if (!$config{"useSelf_item_$i"}) {
  3420. $i++;
  3421. next;
  3422. }
  3423.  
  3424. $ai_v{"useSelf_item_$i"."_time"} = 0;
  3425. $i++;
  3426. }
  3427. $i = 0;
  3428. while (exists $config{"useSelf_skill_$i"}) {
  3429. if (!$config{"useSelf_skill_$i"}) {
  3430. $i++;
  3431. next;
  3432. }
  3433.  
  3434. $ai_v{"useSelf_skill_$i"."_time"} = 0;
  3435. $i++;
  3436. }
  3437. $i = 0;
  3438. while (exists $config{"doCommand_$i"}) {
  3439. if (!$config{"doCommand_$i"}) {
  3440. $i++;
  3441. next;
  3442. }
  3443.  
  3444. $ai_v{"doCommand_$i"."_time"} = 0;
  3445. $i++;
  3446. }
  3447. if ($char) {
  3448. delete $char->{statuses};
  3449. $char->{spirits} = 0;
  3450. delete $char->{permitSkill};
  3451. delete $char->{encoreSkill};
  3452. }
  3453. $cart{exists} = 0;
  3454. undef %guild;
  3455.  
  3456. Plugins::callHook('Network::Receive::map_changed', {
  3457. oldMap => $oldMap,
  3458. });
  3459. $timeout{ai}{time} = time;
  3460. }
  3461.  
  3462. sub memo_success {
  3463. my ($self, $args) = @_;
  3464. if ($args->{fail}) {
  3465. warning T("Memo Failed\n");
  3466. } else {
  3467. message T("Memo Succeeded\n"), "success";
  3468. }
  3469. }
  3470.  
  3471. # +message_string
  3472. sub mercenary_off {
  3473. $slavesList->removeByID($char->{mercenary}{ID});
  3474. delete $char->{slaves}{$char->{mercenary}{ID}};
  3475. delete $char->{mercenary};
  3476. }
  3477. # -message_string
  3478.  
  3479. # not only for mercenaries, this is an all purpose packet !
  3480. sub message_string {
  3481. my ($self, $args) = @_;
  3482.  
  3483. if (@msgTable[$args->{msg_id}++]) { # show message from msgstringtable
  3484. warning T(@msgTable[$args->{msg_id}++]."\n");
  3485. $self->mercenary_off() if ($args->{msg_id} >= 0x04F2 && $args->{msg_id} <= 0x04F5);
  3486.  
  3487. } else {
  3488. if ($args->{msg_id} == 0x04F2) {
  3489. message T("Mercenary soldier's duty hour is over.\n"), "info";
  3490. $self->mercenary_off ();
  3491.  
  3492. } elsif ($args->{msg_id} == 0x04F3) {
  3493. message T("Your mercenary soldier has been killed.\n"), "info";
  3494. $self->mercenary_off ();
  3495.  
  3496. } elsif ($args->{msg_id} == 0x04F4) {
  3497. message T("Your mercenary soldier has been fired.\n"), "info";
  3498. $self->mercenary_off ();
  3499.  
  3500. } elsif ($args->{msg_id} == 0x04F5) {
  3501. message T("Your mercenary soldier has ran away.\n"), "info";
  3502. $self->mercenary_off ();
  3503.  
  3504. } elsif ($args->{msg_id} == 0x054D) {
  3505. message T("View player equip request denied.\n"), "info";
  3506.  
  3507. } elsif ($args->{msg_id} == 0x06AF) {
  3508. message T("You need to be at least base level 10 to send private messages.\n"), "info";
  3509.  
  3510. } else {
  3511. warning TF("msg_id: %s gave unknown results in: %s\n", $args->{msg_id}, $self->{packet_list}{$args->{switch}}->[0]);
  3512. }
  3513. }
  3514. }
  3515.  
  3516. sub monster_typechange {
  3517. my ($self, $args) = @_;
  3518.  
  3519. # Class change / monster type change
  3520. # 01B0 : long ID, byte WhateverThisIs, long type
  3521. my $ID = $args->{ID};
  3522. my $type = $args->{type};
  3523. my $monster = $monstersList->getByID($ID);
  3524. if ($monster) {
  3525. my $oldName = $monster->name;
  3526. if ($monsters_lut{$type}) {
  3527. $monster->setName($monsters_lut{$type});
  3528. } else {
  3529. $monster->setName(undef);
  3530. }
  3531. $monster->{nameID} = $type;
  3532. $monster->{dmgToParty} = 0;
  3533. $monster->{dmgFromParty} = 0;
  3534. $monster->{missedToParty} = 0;
  3535. message TF("Monster %s (%d) changed to %s\n", $oldName, $monster->{binID}, $monster->name);
  3536. }
  3537. }
  3538.  
  3539. sub monster_ranged_attack {
  3540. my ($self, $args) = @_;
  3541.  
  3542. my $ID = $args->{ID};
  3543. my $range = $args->{range};
  3544.  
  3545. my %coords1;
  3546. $coords1{x} = $args->{sourceX};
  3547. $coords1{y} = $args->{sourceY};
  3548. my %coords2;
  3549. $coords2{x} = $args->{targetX};
  3550. $coords2{y} = $args->{targetY};
  3551.  
  3552. my $monster = $monstersList->getByID($ID);
  3553. $monster->{pos_attack_info} = {%coords1} if ($monster);
  3554. $char->{pos} = {%coords2};
  3555. $char->{pos_to} = {%coords2};
  3556. debug "Received attack location - monster: $coords1{x},$coords1{y} - " .
  3557. "you: $coords2{x},$coords2{y}\n", "parseMsg_move", 2;
  3558. }
  3559.  
  3560. sub mvp_item {
  3561. my ($self, $args) = @_;
  3562. my $display = itemNameSimple($args->{itemID});
  3563. message TF("Get MVP item %s\n", $display);
  3564. chatLog("k", TF("Get MVP item %s\n", $display));
  3565. }
  3566.  
  3567. sub mvp_other {
  3568. my ($self, $args) = @_;
  3569. my $display = Actor::get($args->{ID});
  3570. message TF("%s become MVP!\n", $display);
  3571. chatLog("k", TF("%s became MVP!\n", $display));
  3572. }
  3573.  
  3574. sub mvp_you {
  3575. my ($self, $args) = @_;
  3576. my $msg = TF("Congratulations, you are the MVP! Your reward is %s exp!\n", $args->{expAmount});
  3577. message $msg;
  3578. chatLog("k", $msg);
  3579. }
  3580.  
  3581. sub npc_image {
  3582. my ($self, $args) = @_;
  3583. my ($imageName) = bytesToString($args->{npc_image});
  3584. if ($args->{type} == 2) {
  3585. debug "Show NPC image: $imageName\n", "parseMsg";
  3586. } elsif ($args->{type} == 255) {
  3587. debug "Hide NPC image: $imageName\n", "parseMsg";
  3588. } else {
  3589. debug "NPC image: $imageName ($args->{type})\n", "parseMsg";
  3590. }
  3591. }
  3592.  
  3593. sub npc_sell_list {
  3594. my ($self, $args) = @_;
  3595. #sell list, similar to buy list
  3596. if (length($args->{RAW_MSG}) > 4) {
  3597. my $newmsg;
  3598. $self->decrypt(\$newmsg, substr($args->{RAW_MSG}, 4));
  3599. my $msg = substr($args->{RAW_MSG}, 0, 4).$newmsg;
  3600. }
  3601. undef $talk{buyOrSell};
  3602. message T("Ready to start selling items\n");
  3603.  
  3604. debug "You can sell:\n", "info";
  3605. for (my $i = 0; $i < length($args->{itemsdata}); $i += 10) {
  3606. my ($index, $price, $price_overcharge) = unpack("v L L", substr($args->{itemsdata},$i,($i + 10)));
  3607. my $item = $char->inventory->getByServerIndex($index);
  3608. $item->{sellable} = 1; # flag this item as sellable
  3609. debug "[$item->{amount} x $item->{name}] for $price_overcharge z each. \n", "info";
  3610. }
  3611.  
  3612. # continue talk sequence now
  3613. $ai_v{npc_talk}{time} = time;
  3614. }
  3615.  
  3616. sub npc_store_begin {
  3617. my ($self, $args) = @_;
  3618. undef %talk;
  3619. $talk{buyOrSell} = 1;
  3620. $talk{ID} = $args->{ID};
  3621. $ai_v{npc_talk}{talk} = 'buy';
  3622. $ai_v{npc_talk}{time} = time;
  3623.  
  3624. my $name = getNPCName($args->{ID});
  3625.  
  3626. message TF("%s: Type 'store' to start buying, or type 'sell' to start selling\n", $name), "npc";
  3627. }
  3628.  
  3629. sub npc_store_info {
  3630. my ($self, $args) = @_;
  3631. my $newmsg;
  3632. $self->decrypt(\$newmsg, substr($args->{RAW_MSG}, 4));
  3633. my $msg = substr($args->{RAW_MSG}, 0, 4).$newmsg;
  3634. undef @storeList;
  3635. my $storeList = 0;
  3636. undef $talk{'buyOrSell'};
  3637. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 11) {
  3638. my $price = unpack("V1", substr($msg, $i, 4));
  3639. my $type = unpack("C1", substr($msg, $i + 8, 1));
  3640. my $ID = unpack("v1", substr($msg, $i + 9, 2));
  3641.  
  3642. my $store = $storeList[$storeList] = {};
  3643. # TODO: use itemName() or itemNameSimple()?
  3644. my $display = ($items_lut{$ID} ne "")
  3645. ? $items_lut{$ID}
  3646. : "Unknown ".$ID;
  3647. $store->{name} = $display;
  3648. $store->{nameID} = $ID;
  3649. $store->{type} = $type;
  3650. $store->{price} = $price;
  3651. debug "Item added to Store: $store->{name} - $price z\n", "parseMsg", 2;
  3652. $storeList++;
  3653. }
  3654.  
  3655. # Real RO client can be receive this message without NPC Information. We should mimic this behavior.
  3656. my $name = (defined $talk{ID}) ? getNPCName($talk{ID}) : 'Unknown';
  3657.  
  3658. $ai_v{npc_talk}{talk} = 'store';
  3659. # continue talk sequence now
  3660. $ai_v{'npc_talk'}{'time'} = time;
  3661.  
  3662. if (AI::action ne 'buyAuto') {
  3663. message TF("----------%s's Store List-----------\n" .
  3664. "# Name Type Price\n", $name), "list";
  3665. my $display;
  3666. for (my $i = 0; $i < @storeList; $i++) {
  3667. $display = $storeList[$i]{'name'};
  3668. message(swrite(
  3669. "@< @<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<< @>>>>>>>z",
  3670. [$i, $display, $itemTypes_lut{$storeList[$i]{'type'}}, $storeList[$i]{'price'}]),
  3671. "list");
  3672. }
  3673. message("-------------------------------\n", "list");
  3674. }
  3675. }
  3676.  
  3677. sub npc_talk {
  3678. my ($self, $args) = @_;
  3679.  
  3680. $talk{ID} = $args->{ID};
  3681. $talk{nameID} = unpack 'V', $args->{ID};
  3682. $talk{msg} = bytesToString ($args->{msg});
  3683.  
  3684. =pod
  3685. my $newmsg;
  3686. $self->decrypt(\$newmsg, substr($args->{RAW_MSG}, 8));
  3687.  
  3688. my $msg = substr($args->{RAW_MSG}, 0, 8) . $newmsg;
  3689. my $ID = substr($msg, 4, 4);
  3690. my $talkMsg = unpack("Z*", substr($msg, 8));
  3691. $talk{ID} = $ID;
  3692. $talk{nameID} = unpack("V1", $ID);
  3693. $talk{msg} = bytesToString($talkMsg);
  3694. =cut
  3695.  
  3696. # Remove RO color codes
  3697. $talk{msg} =~ s/\^[a-fA-F0-9]{6}//g;
  3698.  
  3699. $ai_v{npc_talk}{talk} = 'initiated';
  3700. $ai_v{npc_talk}{time} = time;
  3701.  
  3702. my $name = getNPCName($talk{ID});
  3703. Plugins::callHook('npc_talk', {
  3704. ID => $talk{ID},
  3705. nameID => $talk{nameID},
  3706. name => $name,
  3707. msg => $talk{msg},
  3708. });
  3709. message "$name: $talk{msg}\n", "npc";
  3710. }
  3711.  
  3712. sub npc_talk_close {
  3713. my ($self, $args) = @_;
  3714. # 00b6: long ID
  3715. # "Close" icon appreared on the NPC message dialog
  3716. my $ID = $args->{ID};
  3717. my $name = getNPCName($ID);
  3718.  
  3719. message TF("%s: Done talking\n", $name), "npc";
  3720.  
  3721. # I noticed that the RO client doesn't send a 'talk cancel' packet
  3722. # when it receives a 'npc_talk_closed' packet from the server'.
  3723. # But on pRO Thor (with Kapra password) this is required in order to
  3724. # open the storage.
  3725. #
  3726. # UPDATE: not sending 'talk cancel' breaks autostorage on iRO.
  3727. # This needs more investigation.
  3728. if (!$talk{canceled}) {
  3729. $messageSender->sendTalkCancel($ID);
  3730. }
  3731.  
  3732. $ai_v{npc_talk}{talk} = 'close';
  3733. $ai_v{npc_talk}{time} = time;
  3734. undef %talk;
  3735.  
  3736. Plugins::callHook('npc_talk_done', {ID => $ID});
  3737. }
  3738.  
  3739. sub npc_talk_continue {
  3740. my ($self, $args) = @_;
  3741. my $ID = substr($args->{RAW_MSG}, 2, 4);
  3742. my $name = getNPCName($ID);
  3743.  
  3744. $ai_v{npc_talk}{talk} = 'next';
  3745. $ai_v{npc_talk}{time} = time;
  3746.  
  3747. if ($config{autoTalkCont}) {
  3748. message TF("%s: Auto-continuing talking\n", $name), "npc";
  3749. $messageSender->sendTalkContinue($ID);
  3750. # This time will be reset once the NPC responds
  3751. $ai_v{npc_talk}{time} = time + $timeout{'ai_npcTalk'}{'timeout'} + 5;
  3752. } else {
  3753. message TF("%s: Type 'talk cont' to continue talking\n", $name), "npc";
  3754. }
  3755. }
  3756.  
  3757. sub npc_talk_number {
  3758. my ($self, $args) = @_;
  3759.  
  3760. my $ID = $args->{ID};
  3761.  
  3762. my $name = getNPCName($ID);
  3763. $ai_v{npc_talk}{talk} = 'number';
  3764. $ai_v{npc_talk}{time} = time;
  3765.  
  3766. message TF("%s: Type 'talk num <number #>' to input a number.\n", $name), "input";
  3767. $ai_v{'npc_talk'}{'talk'} = 'num';
  3768. $ai_v{'npc_talk'}{'time'} = time;
  3769. }
  3770.  
  3771. sub npc_talk_responses {
  3772. my ($self, $args) = @_;
  3773. # 00b7: word len, long ID, string str
  3774. # A list of selections appeared on the NPC message dialog.
  3775. # Each item is divided with ':'
  3776. my $newmsg;
  3777. $self->decrypt(\$newmsg, substr($args->{RAW_MSG}, 8));
  3778. my $msg = substr($args->{RAW_MSG}, 0, 8).$newmsg;
  3779.  
  3780. my $ID = substr($msg, 4, 4);
  3781. $talk{ID} = $ID;
  3782. my $talk = unpack("Z*", substr($msg, 8));
  3783. $talk = substr($msg, 8) if (!defined $talk);
  3784. $talk = bytesToString($talk);
  3785.  
  3786. my @preTalkResponses = split /:/, $talk;
  3787. $talk{responses} = [];
  3788. foreach my $response (@preTalkResponses) {
  3789. # Remove RO color codes
  3790. $response =~ s/\^[a-fA-F0-9]{6}//g;
  3791. if ($response =~ /^\^nItemID\^(\d+)$/) {
  3792. $response = itemNameSimple($1);
  3793. }
  3794.  
  3795. push @{$talk{responses}}, $response if ($response ne "");
  3796. }
  3797.  
  3798. $talk{responses}[@{$talk{responses}}] = "Cancel Chat";
  3799.  
  3800. $ai_v{'npc_talk'}{'talk'} = 'select';
  3801. $ai_v{'npc_talk'}{'time'} = time;
  3802.  
  3803. my $list = T("----------Responses-----------\n" .
  3804. "# Response\n");
  3805. for (my $i = 0; $i < @{$talk{responses}}; $i++) {
  3806. $list .= swrite(
  3807. "@< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<",
  3808. [$i, $talk{responses}[$i]]);
  3809. }
  3810. $list .= "-------------------------------\n";
  3811. message($list, "list");
  3812. my $name = getNPCName($ID);
  3813. Plugins::callHook('npc_talk_responses', {
  3814. ID => $ID,
  3815. name => $name,
  3816. responses => $talk{responses},
  3817. });
  3818. message TF("%s: Type 'talk resp #' to choose a response.\n", $name), "npc";
  3819. }
  3820.  
  3821. sub npc_talk_text {
  3822. my ($self, $args) = @_;
  3823.  
  3824. my $ID = $args->{ID};
  3825.  
  3826. my $name = getNPCName($ID);
  3827. message TF("%s: Type 'talk text' (Respond to NPC)\n", $name), "npc";
  3828. $ai_v{npc_talk}{talk} = 'text';
  3829. $ai_v{npc_talk}{time} = time;
  3830. }
  3831.  
  3832. # TODO: store this state
  3833. sub party_allow_invite {
  3834. my ($self, $args) = @_;
  3835.  
  3836. if ($args->{type}) {
  3837. message T("Not allowed other player invite to Party\n"), "party", 1;
  3838. } else {
  3839. message T("Allowed other player invite to Party\n"), "party", 1;
  3840. }
  3841. }
  3842.  
  3843. sub party_chat {
  3844. my ($self, $args) = @_;
  3845. my $msg;
  3846.  
  3847. $self->decrypt(\$msg, $args->{message});
  3848. $msg = bytesToString($msg);
  3849.  
  3850. # Type: String
  3851. my ($chatMsgUser, $chatMsg) = $msg =~ /(.*?) : (.*)/;
  3852. $chatMsgUser =~ s/ $//;
  3853.  
  3854. stripLanguageCode(\$chatMsg);
  3855. # Type: String
  3856. my $chat = "$chatMsgUser : $chatMsg";
  3857. message TF("[Party] %s\n", $chat), "partychat";
  3858.  
  3859. chatLog("p", "$chat\n") if ($config{'logPartyChat'});
  3860. ChatQueue::add('p', $args->{ID}, $chatMsgUser, $chatMsg);
  3861.  
  3862. Plugins::callHook('packet_partyMsg', {
  3863. MsgUser => $chatMsgUser,
  3864. Msg => $chatMsg
  3865. });
  3866. }
  3867.  
  3868. # TODO: itemPickup itemDivision
  3869. sub party_exp {
  3870. my ($self, $args) = @_;
  3871. $char->{party}{share} = $args->{type};
  3872. if ($args->{type} == 0) {
  3873. message T("Party EXP set to Individual Take\n"), "party", 1;
  3874. } elsif ($args->{type} == 1) {
  3875. message T("Party EXP set to Even Share\n"), "party", 1;
  3876. } else {
  3877. error T("Error setting party option\n");
  3878. }
  3879. if ($args->{itemPickup} == 0) {
  3880. message T("Party item set to Individual Take\n"), "party", 1;
  3881. } elsif ($args->{itemPickup} == 1) {
  3882. message T("Party item set to Even Share\n"), "party", 1;
  3883. } else {
  3884. error T("Error setting party option\n");
  3885. }
  3886. if ($args->{itemDivision} == 0) {
  3887. message T("Party item division set to Individual Take\n"), "party", 1;
  3888. } elsif ($args->{itemDivision} == 1) {
  3889. message T("Party item division set to Even Share\n"), "party", 1;
  3890. } else {
  3891. error T("Error setting party option\n");
  3892. }
  3893. }
  3894.  
  3895. sub party_leader {
  3896. my ($self, $args) = @_;
  3897. for (my $i = 0; $i < @partyUsersID; $i++) {
  3898. if (unpack("V",$partyUsersID[$i]) eq $args->{new}) {
  3899. $char->{party}{users}{$partyUsersID[$i]}{admin} = 1;
  3900. message TF("New party leader: %s\n", $char->{party}{users}{$partyUsersID[$i]}{name}), "party", 1;
  3901. }
  3902. if (unpack("V",$partyUsersID[$i]) eq $args->{old}) {
  3903. $char->{party}{users}{$partyUsersID[$i]}{admin} = '';
  3904. }
  3905. }
  3906. }
  3907.  
  3908. # 0x803
  3909. sub booking_register_request {
  3910. my ($self, $args) = @_;
  3911. my $result = $args->{result};
  3912.  
  3913. if ($result == 0) {
  3914. message T("Booking successfully created!\n"), "booking";
  3915. } elsif ($result == 2) {
  3916. error T("You already got a reservation group active!\n"), "booking";
  3917. } else {
  3918. error TF("Unknown error in creating the group booking (Error %s)\n", $result), "booking";
  3919. }
  3920. }
  3921.  
  3922. # 0x805
  3923. sub booking_search_request {
  3924. my ($self, $args) = @_;
  3925.  
  3926. if (length($args->{innerData}) == 0) {
  3927. error T("Without results!\n"), "booking";
  3928. return;
  3929. }
  3930.  
  3931. message "-------------- Booking Search ---------------\n";
  3932. for (my $offset = 0; $offset < length($args->{innerData}); $offset += 48) {
  3933. my ($index, $charName, $expireTime, $level, $mapID, @job) = unpack("V Z24 V s8", substr($args->{innerData}, $offset, 48));
  3934. message swrite(T("Name: @<<<<<<<<<<<<<<<<<<<<<<<< Index: @>>>>\n" .
  3935. "Created: @<<<<<<<<<<<<<<<<<<<<< Level: @>>>\n" .
  3936. "MapID: @<<<<<\n".
  3937. "Job: @<<<< @<<<< @<<<< @<<<< @<<<<\n" .
  3938. "---------------------------------------------"),
  3939. [bytesToString($charName), $index, getFormattedDate($expireTime), $level, $mapID, @job]), "booking";
  3940. }
  3941. }
  3942.  
  3943. # 0x807
  3944. sub booking_delete_request {
  3945. my ($self, $args) = @_;
  3946. my $result = $args->{result};
  3947.  
  3948. if ($result == 0) {
  3949. message T("Reserve deleted successfully!\n"), "booking";
  3950. } elsif ($result == 3) {
  3951. error T("You're not with a group booking active!\n"), "booking";
  3952. } else {
  3953. error TF("Unknown error in deletion of group booking (Error %s)\n", $result), "booking";
  3954. }
  3955. }
  3956.  
  3957. # 0x809
  3958. sub booking_insert {
  3959. my ($self, $args) = @_;
  3960.  
  3961. message TF("%s has created a new group booking (index: %s)\n", bytesToString($args->{name}), $args->{index});
  3962. }
  3963.  
  3964. # 0x80A
  3965. sub booking_update {
  3966. my ($self, $args) = @_;
  3967.  
  3968. message TF("Reserve index of %s has changed its settings\n", $args->{index});
  3969. }
  3970.  
  3971. # 0x80B
  3972. sub booking_delete {
  3973. my ($self, $args) = @_;
  3974.  
  3975. message TF("Deleted reserve group index %s\n", $args->{index});
  3976. }
  3977.  
  3978. sub party_hp_info {
  3979. my ($self, $args) = @_;
  3980. my $ID = $args->{ID};
  3981.  
  3982. if ($char->{party}{users}{$ID}) {
  3983. $char->{party}{users}{$ID}{hp} = $args->{hp};
  3984. $char->{party}{users}{$ID}{hp_max} = $args->{hp_max};
  3985. }
  3986. }
  3987.  
  3988. sub party_invite {
  3989. my ($self, $args) = @_;
  3990. message TF("Incoming Request to join party '%s'\n", bytesToString($args->{name}));
  3991. $incomingParty{ID} = $args->{ID};
  3992. $incomingParty{ACK} = $args->{switch} eq '02C6' ? '02C7' : '00FF';
  3993. $timeout{ai_partyAutoDeny}{time} = time;
  3994. }
  3995.  
  3996. use constant {
  3997. ANSWER_ALREADY_OTHERGROUPM => 0x0,
  3998. ANSWER_JOIN_REFUSE => 0x1,
  3999. ANSWER_JOIN_ACCEPT => 0x2,
  4000. ANSWER_MEMBER_OVERSIZE => 0x3,
  4001. ANSWER_DUPLICATE => 0x4,
  4002. ANSWER_JOINMSG_REFUSE => 0x5,
  4003. ANSWER_UNKNOWN_ERROR => 0x6,
  4004. ANSWER_UNKNOWN_CHARACTER => 0x7,
  4005. ANSWER_INVALID_MAPPROPERTY => 0x8,
  4006. };
  4007.  
  4008. sub party_invite_result {
  4009. my ($self, $args) = @_;
  4010. my $name = bytesToString($args->{name});
  4011. if ($args->{type} == ANSWER_ALREADY_OTHERGROUPM) {
  4012. warning TF("Join request failed: %s is already in a party\n", $name);
  4013. } elsif ($args->{type} == ANSWER_JOIN_REFUSE) {
  4014. warning TF("Join request failed: %s denied request\n", $name);
  4015. } elsif ($args->{type} == ANSWER_JOIN_ACCEPT) {
  4016. message TF("%s accepted your request\n", $name), "info";
  4017. } elsif ($args->{type} == ANSWER_MEMBER_OVERSIZE) {
  4018. message T("Join request failed: Party is full.\n"), "info";
  4019. } elsif ($args->{type} == ANSWER_DUPLICATE) {
  4020. message TF("Join request failed: same account of %s allready joined the party.\n", $name), "info";
  4021. } elsif ($args->{type} == ANSWER_JOINMSG_REFUSE) {
  4022. message TF("Join request failed: ANSWER_JOINMSG_REFUSE.\n", $name), "info";
  4023. } elsif ($args->{type} == ANSWER_UNKNOWN_ERROR) {
  4024. message TF("Join request failed: unknown error.\n", $name), "info";
  4025. } elsif ($args->{type} == ANSWER_UNKNOWN_CHARACTER) {
  4026. message TF("Join request failed: the character is not currently online or does not exist.\n", $name), "info";
  4027. } elsif ($args->{type} == ANSWER_INVALID_MAPPROPERTY) {
  4028. message TF("Join request failed: ANSWER_INVALID_MAPPROPERTY.\n", $name), "info";
  4029. }
  4030. }
  4031.  
  4032. sub party_join {
  4033. my ($self, $args) = @_;
  4034.  
  4035. return unless changeToInGameState();
  4036. my ($ID, $role, $x, $y, $type, $name, $user, $map) = @{$args}{qw(ID role x y type name user map)};
  4037. $name = bytesToString($name);
  4038. $user = bytesToString($user);
  4039.  
  4040. if (!$char->{party} || !%{$char->{party}} || !$char->{party}{users}{$ID} || !%{$char->{party}{users}{$ID}}) {
  4041. binAdd(\@partyUsersID, $ID) if (binFind(\@partyUsersID, $ID) eq "");
  4042. if ($ID eq $accountID) {
  4043. message TF("You joined party '%s'\n", $name), undef, 1;
  4044. $char->{party} = {};
  4045. Plugins::callHook('packet_partyJoin', { partyName => $name });
  4046. } else {
  4047. message TF("%s joined your party '%s'\n", $user, $name), undef, 1;
  4048. }
  4049. }
  4050.  
  4051. my $actor = $char->{party}{users}{$ID} && %{$char->{party}{users}{$ID}} ? $char->{party}{users}{$ID} : new Actor::Party;
  4052.  
  4053. $actor->{admin} = !$role;
  4054. delete $actor->{statuses} unless $actor->{online} = !$type;
  4055. $actor->{pos}{x} = $x;
  4056. $actor->{pos}{y} = $y;
  4057. $actor->{map} = $map;
  4058. $actor->{name} = $user;
  4059. $actor->{ID} = $ID;
  4060. $char->{party}{users}{$ID} = $actor;
  4061.  
  4062. =pod
  4063. $char->{party}{users}{$ID} = new Actor::Party if ($char->{party}{users}{$ID}{name});
  4064. $char->{party}{users}{$ID}{admin} = !$role;
  4065. if ($type == 0) {
  4066. $char->{party}{users}{$ID}{online} = 1;
  4067. } elsif ($type == 1) {
  4068. $char->{party}{users}{$ID}{online} = 0;
  4069. delete $char->{party}{users}{$ID}{statuses};
  4070. }
  4071. =cut
  4072. $char->{party}{name} = $name;
  4073. =pod
  4074. $char->{party}{users}{$ID}{pos}{x} = $x;
  4075. $char->{party}{users}{$ID}{pos}{y} = $y;
  4076. $char->{party}{users}{$ID}{map} = $map;
  4077. $char->{party}{users}{$ID}{name} = $user;
  4078. $char->{party}{users}{$ID}->{ID} = $ID;
  4079. =cut
  4080.  
  4081. if (($config{partyAutoShare} || $config{partyAutoShareItem} || $config{partyAutoShareItemDiv}) && $char->{party} && %{$char->{party}} && $char->{party}{users}{$accountID}{admin}) {
  4082. $messageSender->sendPartyOption($config{partyAutoShare}, $config{partyAutoShareItem}, $config{partyAutoShareItemDiv});
  4083.  
  4084. }
  4085. }
  4086.  
  4087. use constant {
  4088. GROUPMEMBER_DELETE_LEAVE => 0x0,
  4089. GROUPMEMBER_DELETE_EXPEL => 0x1,
  4090. };
  4091.  
  4092. sub party_leave {
  4093. my ($self, $args) = @_;
  4094.  
  4095. my $ID = $args->{ID};
  4096. my $actor = $char->{party}{users}{$ID}; # bytesToString($args->{name})
  4097. delete $char->{party}{users}{$ID};
  4098. binRemove(\@partyUsersID, $ID);
  4099. if ($ID eq $accountID) {
  4100. $actor = $char;
  4101. delete $char->{party};
  4102. undef @partyUsersID;
  4103. }
  4104.  
  4105. if ($args->{result} == GROUPMEMBER_DELETE_LEAVE) {
  4106. message TF("%s left the party\n", $actor);
  4107. } elsif ($args->{result} == GROUPMEMBER_DELETE_EXPEL) {
  4108. message TF("%s left the party (kicked)\n", $actor);
  4109. } else {
  4110. message TF("%s left the party (unknown reason: %d)\n", $actor, $args->{result});
  4111. }
  4112. }
  4113.  
  4114. sub party_location {
  4115. my ($self, $args) = @_;
  4116.  
  4117. my $ID = $args->{ID};
  4118.  
  4119. if ($char->{party}{users}{$ID}) {
  4120. $char->{party}{users}{$ID}{pos}{x} = $args->{x};
  4121. $char->{party}{users}{$ID}{pos}{y} = $args->{y};
  4122. $char->{party}{users}{$ID}{online} = 1;
  4123. debug "Party member location: $char->{party}{users}{$ID}{name} - $args->{x}, $args->{y}\n", "parseMsg";
  4124. }
  4125. }
  4126.  
  4127. sub party_organize_result {
  4128. my ($self, $args) = @_;
  4129.  
  4130. unless ($args->{fail}) {
  4131. $char->{party}{users}{$accountID}{admin} = 1 if $char->{party}{users}{$accountID};
  4132. } elsif ($args->{fail} == 1) {
  4133. warning T("Can't organize party - party name exists\n");
  4134. } elsif ($args->{fail} == 2) {
  4135. warning T("Can't organize party - you are already in a party\n");
  4136. } elsif ($args->{fail} == 3) {
  4137. warning T("Can't organize party - not allowed in current map\n");
  4138. } else {
  4139. warning TF("Can't organize party - unknown (%d)\n", $args->{fail});
  4140. }
  4141. }
  4142.  
  4143. sub party_users_info {
  4144. my ($self, $args) = @_;
  4145. return unless changeToInGameState();
  4146.  
  4147. my $msg;
  4148. $self->decrypt(\$msg, substr($args->{RAW_MSG}, 28));
  4149. $msg = substr($args->{RAW_MSG}, 0, 28).$msg;
  4150. $char->{party}{name} = bytesToString($args->{party_name});
  4151.  
  4152. for (my $i = 28; $i < $args->{RAW_MSG_SIZE}; $i += 46) {
  4153. my $ID = substr($msg, $i, 4);
  4154. if (binFind(\@partyUsersID, $ID) eq "") {
  4155. binAdd(\@partyUsersID, $ID);
  4156. }
  4157. $char->{party}{users}{$ID} = new Actor::Party();
  4158. $char->{party}{users}{$ID}{name} = bytesToString(unpack("Z24", substr($msg, $i + 4, 24)));
  4159. message TF("Party Member: %s\n", $char->{party}{users}{$ID}{name}), "party", 1;
  4160. $char->{party}{users}{$ID}{map} = unpack("Z16", substr($msg, $i + 28, 16));
  4161. $char->{party}{users}{$ID}{admin} = !(unpack("C1", substr($msg, $i + 44, 1)));
  4162. $char->{party}{users}{$ID}{online} = !(unpack("C1",substr($msg, $i + 45, 1)));
  4163. $char->{party}{users}{$ID}->{ID} = $ID;
  4164. }
  4165.  
  4166. if (($config{partyAutoShare} || $config{partyAutoShareItem} || $config{partyAutoShareItemDiv}) && $char->{party} && %{$char->{party}} && $char->{party}{users}{$accountID}{admin}) {
  4167. $messageSender->sendPartyOption($config{partyAutoShare}, $config{partyAutoShareItem}, $config{partyAutoShareItemDiv});
  4168. }
  4169. }
  4170.  
  4171. sub pet_capture_result {
  4172. my ($self, $args) = @_;
  4173.  
  4174. if ($args->{success}) {
  4175. message T("Pet capture success\n"), "info";
  4176. } else {
  4177. message T("Pet capture failed\n"), "info";
  4178. }
  4179. }
  4180.  
  4181. sub pet_emotion {
  4182. my ($self, $args) = @_;
  4183.  
  4184. my ($ID, $type) = ($args->{ID}, $args->{type});
  4185.  
  4186. my $emote = $emotions_lut{$type}{display} || "/e$type";
  4187. if ($pets{$ID}) {
  4188. message $pets{$ID}->name . " : $emote\n", "emotion";
  4189. }
  4190. }
  4191.  
  4192. sub pet_food {
  4193. my ($self, $args) = @_;
  4194. if ($args->{success}) {
  4195. message TF("Fed pet with %s\n", itemNameSimple($args->{foodID})), "pet";
  4196. } else {
  4197. error TF("Failed to feed pet with %s: no food in inventory.\n", itemNameSimple($args->{foodID}));
  4198. }
  4199. }
  4200.  
  4201. sub pet_info {
  4202. my ($self, $args) = @_;
  4203. $pet{name} = bytesToString($args->{name});
  4204. $pet{renameflag} = $args->{renameflag};
  4205. $pet{level} = $args->{level};
  4206. $pet{hungry} = $args->{hungry};
  4207. $pet{friendly} = $args->{friendly};
  4208. $pet{accessory} = $args->{accessory};
  4209. $pet{type} = $args->{type} if (exists $args->{type});
  4210. debug "Pet status: name=$pet{name} name_set=". ($pet{renameflag} ? 'yes' : 'no') ." level=$pet{level} hungry=$pet{hungry} intimacy=$pet{friendly} accessory=".itemNameSimple($pet{accessory})." type=".($pet{type}||"N/A")."\n", "pet";
  4211. }
  4212.  
  4213. sub pet_info2 {
  4214. my ($self, $args) = @_;
  4215. my ($type, $ID, $value) = @{$args}{qw(type ID value)};
  4216.  
  4217. # receive information about your pet
  4218.  
  4219. # related freya functions: clif_pet_equip clif_pet_performance clif_send_petdata
  4220.  
  4221. # these should never happen, pets should spawn like normal actors (at least on Freya)
  4222. # this isn't even very useful, do we want random pets with no location info?
  4223. #if (!$pets{$ID} || !%{$pets{$ID}}) {
  4224. # binAdd(\@petsID, $ID);
  4225. # $pets{$ID} = {};
  4226. # %{$pets{$ID}} = %{$monsters{$ID}} if ($monsters{$ID} && %{$monsters{$ID}});
  4227. # $pets{$ID}{'name_given'} = "Unknown";
  4228. # $pets{$ID}{'binID'} = binFind(\@petsID, $ID);
  4229. # debug "Pet spawned (unusually): $pets{$ID}{'name'} ($pets{$ID}{'binID'})\n", "parseMsg";
  4230. #}
  4231. #if ($monsters{$ID}) {
  4232. # if (%{$monsters{$ID}}) {
  4233. # objectRemoved('monster', $ID, $monsters{$ID});
  4234. # }
  4235. # # always clear these in case
  4236. # binRemove(\@monstersID, $ID);
  4237. # delete $monsters{$ID};
  4238. #}
  4239.  
  4240. if ($type == 0) {
  4241. # You own no pet.
  4242. undef $pet{ID};
  4243.  
  4244. } elsif ($type == 1) {
  4245. $pet{friendly} = $value;
  4246. debug "Pet friendly: $value\n";
  4247.  
  4248. } elsif ($type == 2) {
  4249. $pet{hungry} = $value;
  4250. debug "Pet hungry: $value\n";
  4251.  
  4252. } elsif ($type == 3) {
  4253. # accessory info for any pet in range
  4254. #debug "Pet accessory info: $value\n";
  4255.  
  4256. } elsif ($type == 4) {
  4257. # performance info for any pet in range
  4258. #debug "Pet performance info: $value\n";
  4259.  
  4260. } elsif ($type == 5) {
  4261. # You own pet with this ID
  4262. $pet{ID} = $ID;
  4263. }
  4264. }
  4265.  
  4266. sub player_equipment {
  4267. my ($self, $args) = @_;
  4268.  
  4269. my ($sourceID, $type, $ID1, $ID2) = @{$args}{qw(sourceID type ID1 ID2)};
  4270. my $player = ($sourceID ne $accountID)? $playersList->getByID($sourceID) : $char;
  4271. return unless $player;
  4272.  
  4273. if ($type == 0) {
  4274. # Player changed job
  4275. $player->{jobID} = $ID1;
  4276.  
  4277. } elsif ($type == 2) {
  4278. if ($ID1 ne $player->{weapon}) {
  4279. message TF("%s changed Weapon to %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
  4280. $player->{weapon} = $ID1;
  4281. }
  4282. if ($ID2 ne $player->{shield}) {
  4283. message TF("%s changed Shield to %s\n", $player, itemName({nameID => $ID2})), "parseMsg_statuslook", 2;
  4284. $player->{shield} = $ID2;
  4285. }
  4286. } elsif ($type == 3) {
  4287. $player->{headgear}{low} = $ID1;
  4288. } elsif ($type == 4) {
  4289. $player->{headgear}{top} = $ID1;
  4290. } elsif ($type == 5) {
  4291. $player->{headgear}{mid} = $ID1;
  4292. } elsif ($type == 9) {
  4293. if ($player->{shoes} && $ID1 ne $player->{shoes}) {
  4294. message TF("%s changed Shoes to: %s\n", $player, itemName({nameID => $ID1})), "parseMsg_statuslook", 2;
  4295. }
  4296. $player->{shoes} = $ID1;
  4297. }
  4298. }
  4299.  
  4300. sub public_chat {
  4301. my ($self, $args) = @_;
  4302. # Type: String
  4303. my $message = bytesToString($args->{message});
  4304. my ($chatMsgUser, $chatMsg); # Type: String
  4305. my ($actor, $dist);
  4306.  
  4307. if ($message =~ / : /) {
  4308. ($chatMsgUser, $chatMsg) = split / : /, $message, 2;
  4309. $chatMsgUser =~ s/ $//;
  4310. $chatMsg =~ s/^ //;
  4311. stripLanguageCode(\$chatMsg);
  4312.  
  4313. $actor = Actor::get($args->{ID});
  4314. $dist = "unknown";
  4315. if (!$actor->isa('Actor::Unknown')) {
  4316. $dist = distance($char->{pos_to}, $actor->{pos_to});
  4317. $dist = sprintf("%.1f", $dist) if ($dist =~ /\./);
  4318. }
  4319. $message = "$chatMsgUser ($actor->{binID}): $chatMsg";
  4320.  
  4321. } else {
  4322. $chatMsg = $message;
  4323. }
  4324.  
  4325. my $position = sprintf("[%s %d, %d]",
  4326. $field ? $field->baseName : T("Unknown field,"),
  4327. $char->{pos_to}{x}, $char->{pos_to}{y});
  4328. my $distInfo;
  4329. if ($actor) {
  4330. $position .= sprintf(" [%d, %d] [dist=%s] (%d)",
  4331. $actor->{pos_to}{x}, $actor->{pos_to}{y},
  4332. $dist, $actor->{nameID});
  4333. $distInfo = "[dist=$dist] ";
  4334. }
  4335.  
  4336. # this code autovivifies $actor->{pos_to} but it doesnt matter
  4337. chatLog("c", "$position $message\n") if ($config{logChat});
  4338. message TF("%s%s\n", $distInfo, $message), "publicchat";
  4339.  
  4340. ChatQueue::add('c', $args->{ID}, $chatMsgUser, $chatMsg);
  4341. Plugins::callHook('packet_pubMsg', {
  4342. pubID => $args->{ID},
  4343. pubMsgUser => $chatMsgUser,
  4344. pubMsg => $chatMsg,
  4345. MsgUser => $chatMsgUser,
  4346. Msg => $chatMsg
  4347. });
  4348. }
  4349.  
  4350. sub private_message {
  4351. my ($self, $args) = @_;
  4352. my ($newmsg, $msg); # Type: Bytes
  4353.  
  4354. return unless changeToInGameState();
  4355.  
  4356. # Type: String
  4357. my $privMsgUser = bytesToString($args->{privMsgUser});
  4358. my $privMsg = bytesToString($args->{privMsg});
  4359.  
  4360. if ($privMsgUser ne "" && binFind(\@privMsgUsers, $privMsgUser) eq "") {
  4361. push @privMsgUsers, $privMsgUser;
  4362. Plugins::callHook('parseMsg/addPrivMsgUser', {
  4363. user => $privMsgUser,
  4364. msg => $privMsg,
  4365. userList => \@privMsgUsers
  4366. });
  4367. }
  4368.  
  4369. stripLanguageCode(\$privMsg);
  4370. chatLog("pm", TF("(From: %s) : %s\n", $privMsgUser, $privMsg)) if ($config{'logPrivateChat'});
  4371. message TF("(From: %s) : %s\n", $privMsgUser, $privMsg), "pm";
  4372.  
  4373. ChatQueue::add('pm', undef, $privMsgUser, $privMsg);
  4374. Plugins::callHook('packet_privMsg', {
  4375. privMsgUser => $privMsgUser,
  4376. privMsg => $privMsg,
  4377. MsgUser => $privMsgUser,
  4378. Msg => $privMsg
  4379. });
  4380.  
  4381. if ($config{dcOnPM} && $AI == AI::AUTO) {
  4382. chatLog("k", T("*** You were PM'd, auto disconnect! ***\n"));
  4383. message T("Disconnecting on PM!\n");
  4384. quit();
  4385. }
  4386. }
  4387.  
  4388. sub private_message_sent {
  4389. my ($self, $args) = @_;
  4390. if ($args->{type} == 0) {
  4391. message TF("(To %s) : %s\n", $lastpm[0]{'user'}, $lastpm[0]{'msg'}), "pm/sent";
  4392. chatLog("pm", "(To: $lastpm[0]{user}) : $lastpm[0]{msg}\n") if ($config{'logPrivateChat'});
  4393.  
  4394. Plugins::callHook('packet_sentPM', {
  4395. to => $lastpm[0]{user},
  4396. msg => $lastpm[0]{msg}
  4397. });
  4398.  
  4399. } elsif ($args->{type} == 1) {
  4400. warning TF("%s is not online\n", $lastpm[0]{user});
  4401. } elsif ($args->{type} == 2) {
  4402. warning TF("Player %s ignored your message\n", $lastpm[0]{user});
  4403. } else {
  4404. warning TF("Player %s doesn't want to receive messages\n", $lastpm[0]{user});
  4405. }
  4406. shift @lastpm;
  4407. }
  4408.  
  4409. sub sync_received_characters {
  4410. my ($self, $args) = @_;
  4411.  
  4412. $charSvrSet{sync_Count} = $args->{sync_Count} if (exists $args->{sync_Count});
  4413.  
  4414. unless ($net->clientAlive) {
  4415. for (1..$args->{sync_Count}) {
  4416. $messageSender->sendToServer($messageSender->reconstruct({switch => 'sync_received_characters'}));
  4417. }
  4418. }
  4419. }
  4420.  
  4421. sub received_characters {
  4422. return if ($net->getState() == Network::IN_GAME);
  4423. my ($self, $args) = @_;
  4424. $net->setState(Network::CONNECTED_TO_LOGIN_SERVER);
  4425.  
  4426. $charSvrSet{total_slot} = $args->{total_slot} if (exists $args->{total_slot});
  4427. $charSvrSet{premium_start_slot} = $args->{premium_start_slot} if (exists $args->{premium_start_slot});
  4428. $charSvrSet{premium_end_slot} = $args->{premium_end_slot} if (exists $args->{premium_end_slot});
  4429.  
  4430. $charSvrSet{normal_slot} = $args->{normal_slot} if (exists $args->{normal_slot});
  4431. $charSvrSet{premium_slot} = $args->{premium_slot} if (exists $args->{premium_slot});
  4432. $charSvrSet{billing_slot} = $args->{billing_slot} if (exists $args->{billing_slot});
  4433.  
  4434. $charSvrSet{producible_slot} = $args->{producible_slot} if (exists $args->{producible_slot});
  4435. $charSvrSet{valid_slot} = $args->{valid_slot} if (exists $args->{valid_slot});
  4436.  
  4437. undef $conState_tries;
  4438.  
  4439. Plugins::callHook('parseMsg/recvChars', $args->{options});
  4440. if ($args->{options} && exists $args->{options}{charServer}) {
  4441. $charServer = $args->{options}{charServer};
  4442. } else {
  4443. $charServer = $net->serverPeerHost . ":" . $net->serverPeerPort;
  4444. }
  4445.  
  4446. # PACKET_HC_ACCEPT_ENTER2 contains no character info
  4447. return unless exists $args->{charInfo};
  4448.  
  4449. my $blockSize = $self->received_characters_blockSize();
  4450. for (my $i = $args->{RAW_MSG_SIZE} % $blockSize; $i < $args->{RAW_MSG_SIZE}; $i += $blockSize) {
  4451. #exp display bugfix - chobit andy 20030129
  4452. my $unpack_string = $self->received_characters_unpackString;
  4453. # TODO: What would be the $unknown ?
  4454. my ($cID,$exp,$zeny,$jobExp,$jobLevel, $opt1, $opt2, $option, $stance, $manner, $statpt,
  4455. $hp,$maxHp,$sp,$maxSp, $walkspeed, $jobId,$hairstyle, $weapon, $level, $skillpt,$headLow, $shield,$headTop,$headMid,$hairColor,
  4456. $clothesColor,$name,$str,$agi,$vit,$int,$dex,$luk,$slot, $rename, $unknown, $mapname, $deleteDate) =
  4457. unpack($unpack_string, substr($args->{RAW_MSG}, $i));
  4458. $chars[$slot] = new Actor::You;
  4459.  
  4460. # Re-use existing $char object instead of re-creating it.
  4461. # Required because existing AI sequences (eg, route) keep a reference to $char.
  4462. $chars[$slot] = $char if $char && $char->{ID} eq $accountID && $char->{charID} eq $cID;
  4463.  
  4464. $chars[$slot]{ID} = $accountID;
  4465. $chars[$slot]{charID} = $cID;
  4466. $chars[$slot]{exp} = $exp;
  4467. $chars[$slot]{zeny} = $zeny;
  4468. $chars[$slot]{exp_job} = $jobExp;
  4469. $chars[$slot]{lv_job} = $jobLevel;
  4470. $chars[$slot]{hp} = $hp;
  4471. $chars[$slot]{hp_max} = $maxHp;
  4472. $chars[$slot]{sp} = $sp;
  4473. $chars[$slot]{sp_max} = $maxSp;
  4474. $chars[$slot]{jobID} = $jobId;
  4475. $chars[$slot]{hair_style} = $hairstyle;
  4476. $chars[$slot]{lv} = $level;
  4477. $chars[$slot]{headgear}{low} = $headLow;
  4478. $chars[$slot]{headgear}{top} = $headTop;
  4479. $chars[$slot]{headgear}{mid} = $headMid;
  4480. $chars[$slot]{hair_color} = $hairColor;
  4481. $chars[$slot]{clothes_color} = $clothesColor;
  4482. $chars[$slot]{name} = $name;
  4483. $chars[$slot]{str} = $str;
  4484. $chars[$slot]{agi} = $agi;
  4485. $chars[$slot]{vit} = $vit;
  4486. $chars[$slot]{int} = $int;
  4487. $chars[$slot]{dex} = $dex;
  4488. $chars[$slot]{luk} = $luk;
  4489. $chars[$slot]{sex} = $accountSex2;
  4490.  
  4491. $chars[$slot]{deleteDate} = getFormattedDate($deleteDate) if ($deleteDate);
  4492. $chars[$slot]{nameID} = unpack("V", $chars[$slot]{ID});
  4493. $chars[$slot]{name} = bytesToString($chars[$slot]{name});
  4494. }
  4495.  
  4496. my $nChars = 0;
  4497. foreach (@chars) { $nChars++ if($_); }
  4498.  
  4499. # FIXME better support for multiple received_characters packets
  4500. if ($args->{switch} eq '099D' && $args->{RAW_MSG_SIZE} >= ($blockSize * 3)) { #charBlockSize: 144
  4501. $net->setState(1.5);
  4502. # FIXME really needed in both this and 09A0 sync_received_characters?
  4503. if ($nChars < $charSvrSet{normal_slot} && $config{'XKore'} ne '1') {
  4504. $messageSender->sendToServer($messageSender->reconstruct({switch => 'sync_received_characters'}));
  4505. }
  4506. return;
  4507. }
  4508.  
  4509. message T("Received characters from Character Server\n"), "connection";
  4510.  
  4511. # gradeA says it's supposed to send this packet here, but
  4512. # it doesn't work...
  4513. # 30 Dec 2005: it didn't work before because it wasn't sending the accountiD -> fixed (kaliwanagan)
  4514. $messageSender->sendBanCheck($accountID) if (!$net->clientAlive && $config{serverType} == 2);
  4515. return if ($args->{switch} eq '099D' && $config{serverType} eq 'twRO'); #charBlockSize: 144
  4516. if (charSelectScreen(1) == 1) {
  4517. $firstLoginMap = 1;
  4518. $startingzeny = $chars[$config{'char'}]{'zeny'} unless defined $startingzeny;
  4519. $sentWelcomeMessage = 1;
  4520. }
  4521. }
  4522.  
  4523. sub received_character_ID_and_Map {
  4524. my ($self, $args) = @_;
  4525. message T("Received character ID and Map IP from Character Server\n"), "connection";
  4526. $net->setState(4);
  4527. undef $conState_tries;
  4528. $charID = $args->{charID};
  4529.  
  4530. if ($net->version == 1) {
  4531. undef $masterServer;
  4532. $masterServer = $masterServers{$config{master}} if ($config{master} ne "");
  4533. }
  4534.  
  4535. my ($map) = $args->{mapName} =~ /([\s\S]*)\./; # cut off .gat
  4536. my $map_noinstance;
  4537. ($map_noinstance, undef) = Field::nameToBaseName(undef, $map); # Hack to clean up InstanceID
  4538. if (!$field || $map ne $field->name()) {
  4539. eval {
  4540. $field = new Field(name => $map);
  4541. };
  4542. if (my $e = caught('FileNotFoundException', 'IOException')) {
  4543. error TF("Cannot load field %s: %s\n", $map_noinstance, $e);
  4544. undef $field;
  4545. } elsif ($@) {
  4546. die $@;
  4547. }
  4548. }
  4549.  
  4550. $map_ip = makeIP($args->{mapIP});
  4551. $map_ip = $masterServer->{ip} if ($masterServer && $masterServer->{private});
  4552. $map_port = $args->{mapPort};
  4553. message TF("----------Game Info----------\n" .
  4554. "Char ID: %s (%s)\n" .
  4555. "MAP Name: %s\n" .
  4556. "MAP IP: %s\n" .
  4557. "MAP Port: %s\n" .
  4558. "-----------------------------\n", getHex($charID), unpack("V1", $charID),
  4559. $args->{mapName}, $map_ip, $map_port), "connection";
  4560. checkAllowedMap($map_noinstance);
  4561. message(T("Closing connection to Character Server\n"), "connection") unless ($net->version == 1);
  4562. $net->serverDisconnect(1);
  4563. main::initStatVars();
  4564. }
  4565.  
  4566. sub received_sync {
  4567. return unless changeToInGameState();
  4568. debug "Received Sync\n", 'parseMsg', 2;
  4569. $timeout{'play'}{'time'} = time;
  4570. }
  4571.  
  4572. sub refine_result {
  4573. my ($self, $args) = @_;
  4574. if ($args->{fail} == 0) {
  4575. message TF("You successfully refined a weapon (ID %s)!\n", $args->{nameID});
  4576. } elsif ($args->{fail} == 1) {
  4577. message TF("You failed to refine a weapon (ID %s)!\n", $args->{nameID});
  4578. } elsif ($args->{fail} == 2) {
  4579. message TF("You successfully made a potion (ID %s)!\n", $args->{nameID});
  4580. } elsif ($args->{fail} == 3) {
  4581. message TF("You failed to make a potion (ID %s)!\n", $args->{nameID});
  4582. } else {
  4583. message TF("You tried to refine a weapon (ID %s); result: unknown %s\n", $args->{nameID}, $args->{fail});
  4584. }
  4585. }
  4586.  
  4587. sub blacksmith_points {
  4588. my ($self, $args) = @_;
  4589. message TF("[POINT] Blacksmist Ranking Point is increasing by %s. Now, The total is %s points.\n", $args->{points}, $args->{total}, "list");
  4590. }
  4591.  
  4592. sub alchemist_point {
  4593. my ($self, $args) = @_;
  4594. message TF("[POINT] Alchemist Ranking Point is increasing by %s. Now, The total is %s points.\n", $args->{points}, $args->{total}, "list");
  4595. }
  4596.  
  4597. sub repair_list {
  4598. my ($self, $args) = @_;
  4599. my $msg = T("--------Repair List--------\n");
  4600. undef $repairList;
  4601. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 13) {
  4602. my $listID = unpack("C1", substr($args->{RAW_MSG}, $i+12, 1));
  4603. $repairList->[$listID]->{index} = unpack("v1", substr($args->{RAW_MSG}, $i, 2));
  4604. $repairList->[$listID]->{nameID} = unpack("v1", substr($args->{RAW_MSG}, $i+2, 2));
  4605. # what are these two?
  4606. $repairList->[$listID]->{status} = unpack("V1", substr($args->{RAW_MSG}, $i+4, 4));
  4607. $repairList->[$listID]->{status2} = unpack("V1", substr($args->{RAW_MSG}, $i+8, 4));
  4608. $repairList->[$listID]->{listID} = $listID;
  4609.  
  4610. my $name = itemNameSimple($repairList->[$listID]->{nameID});
  4611. $msg .= "$listID $name\n";
  4612. }
  4613. $msg .= "---------------------------\n";
  4614. message $msg, "list";
  4615. }
  4616.  
  4617. sub repair_result {
  4618. my ($self, $args) = @_;
  4619. undef $repairList;
  4620. my $itemName = itemNameSimple($args->{nameID});
  4621. if ($args->{flag}) {
  4622. message TF("Repair of %s failed.\n", $itemName);
  4623. } else {
  4624. message TF("Successfully repaired %s.\n", $itemName);
  4625. }
  4626. }
  4627.  
  4628. sub resurrection {
  4629. my ($self, $args) = @_;
  4630.  
  4631. my $targetID = $args->{targetID};
  4632. my $player = $playersList->getByID($targetID);
  4633. my $type = $args->{type};
  4634.  
  4635. if ($targetID eq $accountID) {
  4636. message T("You have been resurrected\n"), "info";
  4637. undef $char->{'dead'};
  4638. undef $char->{'dead_time'};
  4639. $char->{'resurrected'} = 1;
  4640.  
  4641. } else {
  4642. if ($player) {
  4643. undef $player->{'dead'};
  4644. $player->{deltaHp} = 0;
  4645. }
  4646. message TF("%s has been resurrected\n", getActorName($targetID)), "info";
  4647. }
  4648. }
  4649.  
  4650. sub secure_login_key {
  4651. my ($self, $args) = @_;
  4652. $secureLoginKey = $args->{secure_key};
  4653. debug sprintf("Secure login key: %s\n", getHex($args->{secure_key})), 'connection';
  4654. }
  4655.  
  4656. sub self_chat {
  4657. my ($self, $args) = @_;
  4658. my ($message, $chatMsgUser, $chatMsg); # Type: String
  4659.  
  4660. $message = bytesToString($args->{message});
  4661.  
  4662. ($chatMsgUser, $chatMsg) = $message =~ /([\s\S]*?) : ([\s\S]*)/;
  4663. # Note: $chatMsgUser/Msg may be undefined. This is the case on
  4664. # eAthena servers: it uses this packet for non-chat server messages.
  4665.  
  4666. if (defined $chatMsgUser) {
  4667. stripLanguageCode(\$chatMsg);
  4668. $message = $chatMsgUser . " : " . $chatMsg;
  4669. }
  4670.  
  4671. chatLog("c", "$message\n") if ($config{'logChat'});
  4672. message "$message\n", "selfchat";
  4673.  
  4674. Plugins::callHook('packet_selfChat', {
  4675. user => $chatMsgUser,
  4676. msg => $chatMsg
  4677. });
  4678. }
  4679.  
  4680. sub sync_request {
  4681. my ($self, $args) = @_;
  4682.  
  4683. # 0187 - long ID
  4684. # I'm not sure what this is. In inRO this seems to have something
  4685. # to do with logging into the game server, while on
  4686. # oRO it has got something to do with the sync packet.
  4687. if ($masterServer->{serverType} == 1) {
  4688. my $ID = $args->{ID};
  4689. if ($ID == $accountID) {
  4690. $timeout{ai_sync}{time} = time;
  4691. $messageSender->sendSync() unless ($net->clientAlive);
  4692. debug "Sync packet requested\n", "connection";
  4693. } else {
  4694. warning T("Sync packet requested for wrong ID\n");
  4695. }
  4696. }
  4697. }
  4698.  
  4699. sub taekwon_rank {
  4700. my ($self, $args) = @_;
  4701. message T("TaeKwon Mission Rank : ".$args->{rank}."\n"), "info";
  4702. }
  4703.  
  4704. sub gospel_buff_aligned {
  4705. my ($self, $args) = @_;
  4706. my $status = unpack("V1", $args->{ID});
  4707.  
  4708. if ($status == 21) {
  4709. message T("All abnormal status effects have been removed.\n"), "info";
  4710. } elsif ($status == 22) {
  4711. message T("You will be immune to abnormal status effects for the next minute.\n"), "info";
  4712. } elsif ($status == 23) {
  4713. message T("Your Max HP will stay increased for the next minute.\n"), "info";
  4714. } elsif ($status == 24) {
  4715. message T("Your Max SP will stay increased for the next minute.\n"), "info";
  4716. } elsif ($status == 25) {
  4717. message T("All of your Stats will stay increased for the next minute.\n"), "info";
  4718. } elsif ($status == 28) {
  4719. message T("Your weapon will remain blessed with Holy power for the next minute.\n"), "info";
  4720. } elsif ($status == 29) {
  4721. message T("Your armor will remain blessed with Holy power for the next minute.\n"), "info";
  4722. } elsif ($status == 30) {
  4723. message T("Your Defense will stay increased for the next 10 seconds.\n"), "info";
  4724. } elsif ($status == 31) {
  4725. message T("Your Attack strength will stay increased for the next minute.\n"), "info";
  4726. } elsif ($status == 32) {
  4727. message T("Your Accuracy and Flee Rate will stay increased for the next minute.\n"), "info";
  4728. } else {
  4729. #message T("Unknown buff from Gospel: " . $status . "\n"), "info";
  4730. }
  4731. }
  4732.  
  4733. sub no_teleport {
  4734. my ($self, $args) = @_;
  4735. my $fail = $args->{fail};
  4736.  
  4737. if ($fail == 0) {
  4738. error T("Unavailable Area To Teleport\n");
  4739. AI::clear(qw/teleport/);
  4740. } elsif ($fail == 1) {
  4741. error T("Unavailable Area To Memo\n");
  4742. } else {
  4743. error TF("Unavailable Area To Teleport (fail code %s)\n", $fail);
  4744. }
  4745. }
  4746.  
  4747. sub map_property {
  4748. my ($self, $args) = @_;
  4749.  
  4750. if($config{'status_mapProperty'}){
  4751. $char->setStatus(@$_) for map {[$_->[1], $args->{type} == $_->[0]]}
  4752. grep { $args->{type} == $_->[0] || $char->{statuses}{$_->[1]} }
  4753. map {[$_, defined $mapPropertyTypeHandle{$_} ? $mapPropertyTypeHandle{$_} : "UNKNOWN_MAPPROPERTY_TYPE_$_"]}
  4754. 1 .. List::Util::max $args->{type}, keys %mapPropertyTypeHandle;
  4755.  
  4756. if ($args->{info_table}) {
  4757. my @info_table = unpack 'C*', $args->{info_table};
  4758. $char->setStatus(@$_) for map {[
  4759. defined $mapPropertyInfoHandle{$_} ? $mapPropertyInfoHandle{$_} : "UNKNOWN_MAPPROPERTY_INFO_$_",
  4760. $info_table[$_],
  4761. ]} 0 .. @info_table-1;
  4762. }
  4763. }
  4764. $pvp = {1 => 1, 3 => 2}->{$args->{type}};
  4765. if ($pvp) {
  4766. Plugins::callHook('pvp_mode', {
  4767. pvp => $pvp # 1 PvP, 2 GvG
  4768. });
  4769. }
  4770. }
  4771.  
  4772. sub map_property2 {
  4773. my ($self, $args) = @_;
  4774.  
  4775. if($config{'status_mapType'}){
  4776. $char->setStatus(@$_) for map {[$_->[1], $args->{type} == $_->[0]]}
  4777. grep { $args->{type} == $_->[0] || $char->{statuses}{$_->[1]} }
  4778. map {[$_, defined $mapTypeHandle{$_} ? $mapTypeHandle{$_} : "UNKNOWN_MAPTYPE_$_"]}
  4779. 0 .. List::Util::max $args->{type}, keys %mapTypeHandle;
  4780. }
  4781. $pvp = {6 => 1, 8 => 2, 19 => 3}->{$args->{type}};
  4782. if ($pvp) {
  4783. Plugins::callHook('pvp_mode', {
  4784. pvp => $pvp # 1 PvP, 2 GvG, 3 Battleground
  4785. });
  4786. }
  4787. }
  4788.  
  4789. sub pvp_rank {
  4790. my ($self, $args) = @_;
  4791.  
  4792. # 9A 01 - 14 bytes long
  4793. my $ID = $args->{ID};
  4794. my $rank = $args->{rank};
  4795. my $num = $args->{num};;
  4796. if ($rank != $ai_v{temp}{pvp_rank} ||
  4797. $num != $ai_v{temp}{pvp_num}) {
  4798. $ai_v{temp}{pvp_rank} = $rank;
  4799. $ai_v{temp}{pvp_num} = $num;
  4800. if ($ai_v{temp}{pvp}) {
  4801. message TF("Your PvP rank is: %s/%s\n", $rank, $num), "map_event";
  4802. }
  4803. }
  4804. }
  4805.  
  4806. sub sense_result {
  4807. my ($self, $args) = @_;
  4808. # nameID level size hp def race mdef element ice earth fire wind poison holy dark spirit undead
  4809. my @race_lut = qw(Formless Undead Beast Plant Insect Fish Demon Demi-Human Angel Dragon Boss Non-Boss);
  4810. my @size_lut = qw(Small Medium Large);
  4811. message TF("=====================Sense========================\n" .
  4812. "Monster: %-16s Level: %-12s\n" .
  4813. "Size: %-16s Race: %-12s\n" .
  4814. "Def: %-16s MDef: %-12s\n" .
  4815. "Element: %-16s HP: %-12s\n" .
  4816. "=================Damage Modifiers=================\n" .
  4817. "Ice: %-3s Earth: %-3s Fire: %-3s Wind: %-3s\n" .
  4818. "Poison: %-3s Holy: %-3s Dark: %-3s Spirit: %-3s\n" .
  4819. "Undead: %-3s\n" .
  4820. "==================================================\n",
  4821. $monsters_lut{$args->{nameID}}, $args->{level}, $size_lut[$args->{size}], $race_lut[$args->{race}],
  4822. $args->{def}, $args->{mdef}, $elements_lut{$args->{element}}, $args->{hp},
  4823. $args->{ice}, $args->{earth}, $args->{fire}, $args->{wind}, $args->{poison}, $args->{holy}, $args->{dark},
  4824. $args->{spirit}, $args->{undead}), "list";
  4825. }
  4826.  
  4827. sub shop_sold {
  4828. my ($self, $args) = @_;
  4829.  
  4830. # sold something
  4831. my $number = $args->{number};
  4832. my $amount = $args->{amount};
  4833.  
  4834. $articles[$number]{sold} += $amount;
  4835. my $earned = $amount * $articles[$number]{price};
  4836. $shopEarned += $earned;
  4837. $articles[$number]{quantity} -= $amount;
  4838. my $msg = TF("sold: %s - %s %sz\n", $amount, $articles[$number]{name}, $earned);
  4839. shopLog($msg);
  4840. message($msg, "sold");
  4841. if ($articles[$number]{quantity} < 1) {
  4842. message TF("sold out: %s\n", $articles[$number]{name}), "sold";
  4843. #$articles[$number] = "";
  4844. if (!--$articles){
  4845. message T("Items have been sold out.\n"), "sold";
  4846. closeShop();
  4847. }
  4848. }
  4849. }
  4850.  
  4851. # TODO:
  4852. # Add 'dispose' support
  4853. sub skill_cast {
  4854. my ($self, $args) = @_;
  4855.  
  4856. return unless changeToInGameState();
  4857. my $sourceID = $args->{sourceID};
  4858. my $targetID = $args->{targetID};
  4859. my $x = $args->{x};
  4860. my $y = $args->{y};
  4861. my $skillID = $args->{skillID};
  4862. my $type = $args->{type};
  4863. my $wait = $args->{wait};
  4864. my ($dist, %coords);
  4865.  
  4866. # Resolve source and target
  4867. my $source = Actor::get($sourceID);
  4868. my $target = Actor::get($targetID);
  4869. my $verb = $source->verb('are casting', 'is casting');
  4870.  
  4871. Misc::checkValidity("skill_cast part 1");
  4872.  
  4873. my $skill = new Skill(idn => $skillID);
  4874. $source->{casting} = {
  4875. skill => $skill,
  4876. target => $target,
  4877. x => $x,
  4878. y => $y,
  4879. startTime => time,
  4880. castTime => $wait
  4881. };
  4882. # Since we may have a circular reference, weaken this reference
  4883. # to prevent memory leaks.
  4884. Scalar::Util::weaken($source->{casting}{target});
  4885.  
  4886. my $targetString;
  4887. if ($x != 0 || $y != 0) {
  4888. # If $dist is positive we are in range of the attack?
  4889. $coords{x} = $x;
  4890. $coords{y} = $y;
  4891. $dist = judgeSkillArea($skillID) - distance($char->{pos_to}, \%coords);
  4892. $targetString = "location ($x, $y)";
  4893. undef $targetID;
  4894. } else {
  4895. $targetString = $target->nameString($source);
  4896. }
  4897.  
  4898. # Perform trigger actions
  4899. if ($sourceID eq $accountID) {
  4900. $char->{time_cast} = time;
  4901. $char->{time_cast_wait} = $wait / 1000;
  4902. delete $char->{cast_cancelled};
  4903. }
  4904. countCastOn($sourceID, $targetID, $skillID, $x, $y);
  4905.  
  4906. Misc::checkValidity("skill_cast part 2");
  4907.  
  4908. my $domain = ($sourceID eq $accountID) ? "selfSkill" : "skill";
  4909. my $disp = skillCast_string($source, $target, $x, $y, $skill->getName(), $wait);
  4910. message $disp, $domain, 1;
  4911.  
  4912. Plugins::callHook('is_casting', {
  4913. sourceID => $sourceID,
  4914. targetID => $targetID,
  4915. source => $source,
  4916. target => $target,
  4917. skillID => $skillID,
  4918. skill => $skill,
  4919. time => $source->{casting}{time},
  4920. castTime => $wait,
  4921. x => $x,
  4922. y => $y
  4923. });
  4924.  
  4925. Misc::checkValidity("skill_cast part 3");
  4926.  
  4927. # Skill Cancel
  4928. my $monster = $monstersList->getByID($sourceID);
  4929. my $control;
  4930. $control = mon_control($monster->name,$monster->{nameID}) if ($monster);
  4931. if ($AI == AI::AUTO && $control->{skillcancel_auto}) {
  4932. if ($targetID eq $accountID || $dist > 0 || (AI::action eq "attack" && AI::args->{ID} ne $sourceID)) {
  4933. message TF("Monster Skill - switch Target to : %s (%d)\n", $monster->name, $monster->{binID});
  4934. $char->sendAttackStop;
  4935. AI::dequeue;
  4936. attack($sourceID);
  4937. }
  4938.  
  4939. # Skill area casting -> running to monster's back
  4940. my $ID;
  4941. if ($dist > 0 && AI::action eq "attack" && ($ID = AI::args->{ID}) && (my $monster2 = $monstersList->getByID($ID))) {
  4942. # Calculate X axis
  4943. if ($char->{pos_to}{x} - $monster2->{pos_to}{x} < 0) {
  4944. $coords{x} = $monster2->{pos_to}{x} + 3;
  4945. } else {
  4946. $coords{x} = $monster2->{pos_to}{x} - 3;
  4947. }
  4948. # Calculate Y axis
  4949. if ($char->{pos_to}{y} - $monster2->{pos_to}{y} < 0) {
  4950. $coords{y} = $monster2->{pos_to}{y} + 3;
  4951. } else {
  4952. $coords{y} = $monster2->{pos_to}{y} - 3;
  4953. }
  4954.  
  4955. my (%vec, %pos);
  4956. getVector(\%vec, \%coords, $char->{pos_to});
  4957. moveAlongVector(\%pos, $char->{pos_to}, \%vec, distance($char->{pos_to}, \%coords));
  4958. ai_route($field->baseName, $pos{x}, $pos{y},
  4959. maxRouteDistance => $config{attackMaxRouteDistance},
  4960. maxRouteTime => $config{attackMaxRouteTime},
  4961. noMapRoute => 1);
  4962. message TF("Avoid casting Skill - switch position to : %s,%s\n", $pos{x}, $pos{y}), 1;
  4963. }
  4964.  
  4965. Misc::checkValidity("skill_cast part 4");
  4966. }
  4967. }
  4968.  
  4969. # TODO: use $args->{type} if present
  4970. sub skill_update {
  4971. my ($self, $args) = @_;
  4972.  
  4973. my ($ID, $lv, $sp, $range, $up) = ($args->{skillID}, $args->{lv}, $args->{sp}, $args->{range}, $args->{up});
  4974.  
  4975. my $skill = new Skill(idn => $ID);
  4976. my $handle = $skill->getHandle();
  4977. my $name = $skill->getName();
  4978. $char->{skills}{$handle}{lv} = $lv;
  4979. $char->{skills}{$handle}{sp} = $sp;
  4980. $char->{skills}{$handle}{range} = $range;
  4981. $char->{skills}{$handle}{up} = $up;
  4982.  
  4983. Skill::DynamicInfo::add($ID, $handle, $lv, $sp, $range, $skill->getTargetType(), Skill::OWNER_CHAR);
  4984.  
  4985. Plugins::callHook('packet_charSkills', {
  4986. ID => $ID,
  4987. handle => $handle,
  4988. level => $lv,
  4989. upgradable => $up,
  4990. });
  4991.  
  4992. debug "Skill $name: $lv\n", "parseMsg";
  4993. }
  4994.  
  4995. sub skill_use {
  4996. my ($self, $args) = @_;
  4997. return unless changeToInGameState();
  4998.  
  4999. if (my $spell = $spells{$args->{sourceID}}) {
  5000. # Resolve source of area attack skill
  5001. $args->{sourceID} = $spell->{sourceID};
  5002. }
  5003.  
  5004. my $source = Actor::get($args->{sourceID});
  5005. my $target = Actor::get($args->{targetID});
  5006. $args->{source} = $source;
  5007. $args->{target} = $target;
  5008. delete $source->{casting};
  5009.  
  5010. # Perform trigger actions
  5011. if ($args->{switch} eq "0114") {
  5012. $args->{damage} = intToSignedShort($args->{damage});
  5013. } else {
  5014. $args->{damage} = intToSignedInt($args->{damage});
  5015. }
  5016. updateDamageTables($args->{sourceID}, $args->{targetID}, $args->{damage}) if ($args->{damage} != -30000);
  5017. setSkillUseTimer($args->{skillID}, $args->{targetID}) if (
  5018. $args->{sourceID} eq $accountID
  5019. or $char->{slaves} && $char->{slaves}{$args->{sourceID}}
  5020. );
  5021. setPartySkillTimer($args->{skillID}, $args->{targetID}) if (
  5022. $args->{sourceID} eq $accountID
  5023. or $char->{slaves} && $char->{slaves}{$args->{sourceID}}
  5024. or $args->{sourceID} eq $args->{targetID} # wtf?
  5025. );
  5026. countCastOn($args->{sourceID}, $args->{targetID}, $args->{skillID});
  5027.  
  5028. # Resolve source and target names
  5029. my $skill = new Skill(idn => $args->{skillID});
  5030. $args->{skill} = $skill;
  5031. my $disp = skillUse_string($source, $target, $skill->getName(), $args->{damage},
  5032. $args->{level}, ($args->{src_speed}));
  5033.  
  5034. if ($args->{damage} != -30000 &&
  5035. $args->{sourceID} eq $accountID &&
  5036. $args->{targetID} ne $accountID) {
  5037. calcStat($args->{damage});
  5038. }
  5039.  
  5040. my $domain = ($args->{sourceID} eq $accountID) ? "selfSkill" : "skill";
  5041.  
  5042. if ($args->{damage} == 0) {
  5043. $domain = "attackMonMiss" if (($args->{sourceID} eq $accountID && $args->{targetID} ne $accountID) || ($char->{homunculus} && $args->{sourceID} eq $char->{homunculus}{ID} && $args->{targetID} ne $char->{homunculus}{ID}));
  5044. $domain = "attackedMiss" if (($args->{sourceID} ne $accountID && $args->{targetID} eq $accountID) || ($char->{homunculus} && $args->{sourceID} ne $char->{homunculus}{ID} && $args->{targetID} eq $char->{homunculus}{ID}));
  5045.  
  5046. } elsif ($args->{damage} != -30000) {
  5047. $domain = "attackMon" if (($args->{sourceID} eq $accountID && $args->{targetID} ne $accountID) || ($char->{homunculus} && $args->{sourceID} eq $char->{homunculus}{ID} && $args->{targetID} ne $char->{homunculus}{ID}));
  5048. $domain = "attacked" if (($args->{sourceID} ne $accountID && $args->{targetID} eq $accountID) || ($char->{homunculus} && $args->{sourceID} ne $char->{homunculus}{ID} && $args->{targetID} eq $char->{homunculus}{ID}));
  5049. }
  5050.  
  5051. if ((($args->{sourceID} eq $accountID) && ($args->{targetID} ne $accountID)) ||
  5052. (($args->{sourceID} ne $accountID) && ($args->{targetID} eq $accountID))) {
  5053. my $status = sprintf("[%3d/%3d] ", $char->hp_percent, $char->sp_percent);
  5054. $disp = $status.$disp;
  5055. } elsif ($char->{slaves} && $char->{slaves}{$args->{sourceID}} && !$char->{slaves}{$args->{targetID}}) {
  5056. my $status = sprintf("[%3d/%3d] ", $char->{slaves}{$args->{sourceID}}{hpPercent}, $char->{slaves}{$args->{sourceID}}{spPercent});
  5057. $disp = $status.$disp;
  5058. } elsif ($char->{slaves} && !$char->{slaves}{$args->{sourceID}} && $char->{slaves}{$args->{targetID}}) {
  5059. my $status = sprintf("[%3d/%3d] ", $char->{slaves}{$args->{targetID}}{hpPercent}, $char->{slaves}{$args->{targetID}}{spPercent});
  5060. $disp = $status.$disp;
  5061. }
  5062. $target->{sitting} = 0 unless $args->{type} == 4 || $args->{type} == 9 || $args->{damage} == 0;
  5063.  
  5064. Plugins::callHook('packet_skilluse', {
  5065. 'skillID' => $args->{skillID},
  5066. 'sourceID' => $args->{sourceID},
  5067. 'targetID' => $args->{targetID},
  5068. 'damage' => $args->{damage},
  5069. 'amount' => 0,
  5070. 'x' => 0,
  5071. 'y' => 0,
  5072. 'disp' => \$disp
  5073. });
  5074. message $disp, $domain, 1;
  5075.  
  5076. if ($args->{targetID} eq $accountID && $args->{damage} > 0) {
  5077. $damageTaken{$source->{name}}{$skill->getName()} += $args->{damage};
  5078. }
  5079. }
  5080.  
  5081. sub skill_use_failed {
  5082. my ($self, $args) = @_;
  5083.  
  5084. # skill fail/delay
  5085. my $skillID = $args->{skillID};
  5086. my $btype = $args->{btype};
  5087. my $fail = $args->{fail};
  5088. my $type = $args->{type};
  5089.  
  5090. my %failtype = (
  5091. 0 => T('Basic'),
  5092. 1 => T('Insufficient SP'),
  5093. 2 => T('Insufficient HP'),
  5094. 3 => T('No Memo'),
  5095. 4 => T('Mid-Delay'),
  5096. 5 => T('No Zeny'),
  5097. 6 => T('Wrong Weapon Type'),
  5098. 7 => T('Red Gem Needed'),
  5099. 8 => T('Blue Gem Needed'),
  5100. 9 => T('90% Overweight'),
  5101. 10 => T('Requirement'),
  5102. 13 => T('Need this within the water'),
  5103. 19 => T('Full Amulet'),
  5104. 29 => T('Must have at least 1% of base XP'),
  5105. 83 => T('Location not allowed to create chatroom/market')
  5106. );
  5107.  
  5108. my $errorMessage;
  5109. if (exists $failtype{$type}) {
  5110. $errorMessage = $failtype{$type};
  5111. } else {
  5112. $errorMessage = 'Unknown error';
  5113. }
  5114.  
  5115. warning TF("Skill %s failed: %s (error number %s)\n", Skill->new(idn => $skillID)->getName(), $errorMessage, $type), "skill";
  5116. Plugins::callHook('packet_skillfail', {
  5117. skillID => $skillID,
  5118. failType => $type,
  5119. failMessage => $errorMessage
  5120. });
  5121. }
  5122.  
  5123. sub skill_use_location {
  5124. my ($self, $args) = @_;
  5125.  
  5126. # Skill used on coordinates
  5127. my $skillID = $args->{skillID};
  5128. my $sourceID = $args->{sourceID};
  5129. my $lv = $args->{lv};
  5130. my $x = $args->{x};
  5131. my $y = $args->{y};
  5132.  
  5133. # Perform trigger actions
  5134. setSkillUseTimer($skillID) if $sourceID eq $accountID;
  5135.  
  5136. # Resolve source name
  5137. my $source = Actor::get($sourceID);
  5138. my $skillName = Skill->new(idn => $skillID)->getName();
  5139. my $disp = skillUseLocation_string($source, $skillName, $args);
  5140.  
  5141. # Print skill use message
  5142. my $domain = ($sourceID eq $accountID) ? "selfSkill" : "skill";
  5143. message $disp, $domain;
  5144.  
  5145. Plugins::callHook('packet_skilluse', {
  5146. 'skillID' => $skillID,
  5147. 'sourceID' => $sourceID,
  5148. 'targetID' => '',
  5149. 'damage' => 0,
  5150. 'amount' => $lv,
  5151. 'x' => $x,
  5152. 'y' => $y
  5153. });
  5154. }
  5155. # TODO: a skill can fail, do something with $args->{success} == 0 (this means that the skill failed)
  5156. sub skill_used_no_damage {
  5157. my ($self, $args) = @_;
  5158. return unless changeToInGameState();
  5159.  
  5160. # Skill used on target, with no damage done
  5161. if (my $spell = $spells{$args->{sourceID}}) {
  5162. # Resolve source of area attack skill
  5163. $args->{sourceID} = $spell->{sourceID};
  5164. }
  5165.  
  5166. # Perform trigger actions
  5167. # FIXME: setSkillUseTimer does many different things, so which of them "screw up monk comboing"?
  5168. setSkillUseTimer($args->{skillID}, $args->{targetID}) if ($args->{sourceID} eq $accountID
  5169. && $skillsArea{$args->{skillHandle}} != 2); # ignore these skills because they screw up monk comboing
  5170. setPartySkillTimer($args->{skillID}, $args->{targetID}) if
  5171. $args->{sourceID} eq $accountID or $args->{sourceID} eq $args->{targetID};
  5172. countCastOn($args->{sourceID}, $args->{targetID}, $args->{skillID});
  5173. if ($args->{sourceID} eq $accountID) {
  5174. my $pos = calcPosition($char);
  5175. $char->{pos_to} = $pos;
  5176. $char->{time_move} = 0;
  5177. $char->{time_move_calc} = 0;
  5178. }
  5179.  
  5180. # Resolve source and target names
  5181. my ($source, $target);
  5182. $target = $args->{target} = Actor::get($args->{targetID});
  5183. $source = $args->{source} = (
  5184. $args->{sourceID} ne "\000\000\000\000"
  5185. ? Actor::get($args->{sourceID})
  5186. : $target # for Heal generated by Potion Pitcher sourceID = 0
  5187. );
  5188. my $verb = $source->verb('use', 'uses');
  5189.  
  5190. delete $source->{casting};
  5191.  
  5192. # Print skill use message
  5193. my $extra = "";
  5194. if ($args->{skillID} == 28) {
  5195. $extra = ": $args->{amount} hp gained";
  5196. updateDamageTables($args->{sourceID}, $args->{targetID}, -$args->{amount});
  5197. } elsif ($args->{amount} != 65535) {
  5198. $extra = ": Lv $args->{amount}";
  5199. }
  5200.  
  5201. my $domain = ($args->{sourceID} eq $accountID) ? "selfSkill" : "skill";
  5202. my $skill = $args->{skill} = new Skill(idn => $args->{skillID});
  5203. my $disp = skillUseNoDamage_string($source, $target, $skill->getIDN(), $skill->getName(), $args->{amount});
  5204. message $disp, $domain;
  5205.  
  5206. # Set teleport time
  5207. if ($args->{sourceID} eq $accountID && $skill->getHandle() eq 'AL_TELEPORT') {
  5208. $timeout{ai_teleport_delay}{time} = time;
  5209. }
  5210.  
  5211. if ($AI == AI::AUTO && $config{'autoResponseOnHeal'}) {
  5212. # Handle auto-response on heal
  5213. my $player = $playersList->getByID($args->{sourceID});
  5214. if ($player && ($args->{skillID} == 28 || $args->{skillID} == 29 || $args->{skillID} == 34)) {
  5215. if ($args->{targetID} eq $accountID) {
  5216. chatLog("k", "***$source ".$skill->getName()." on $target$extra***\n");
  5217. sendMessage("pm", getResponse("skillgoodM"), $player->name);
  5218. } elsif ($monstersList->getByID($args->{targetID})) {
  5219. chatLog("k", "***$source ".$skill->getName()." on $target$extra***\n");
  5220. sendMessage("pm", getResponse("skillbadM"), $player->name);
  5221. }
  5222. }
  5223. }
  5224. Plugins::callHook('packet_skilluse', {
  5225. skillID => $args->{skillID},
  5226. sourceID => $args->{sourceID},
  5227. targetID => $args->{targetID},
  5228. damage => 0,
  5229. amount => $args->{amount},
  5230. x => 0,
  5231. y => 0
  5232. });
  5233. }
  5234.  
  5235. # TODO: move @skillsID to Actor, per-actor {skills}, Skill::DynamicInfo
  5236. sub skills_list {
  5237. my ($self, $args) = @_;
  5238.  
  5239. return unless changeToInGameState;
  5240.  
  5241. my ($msg, $newmsg);
  5242. $msg = $args->{RAW_MSG};
  5243. $self->decrypt(\$newmsg, substr $msg, 4);
  5244. $msg = substr ($msg, 0, 4) . $newmsg;
  5245.  
  5246. # TODO: per-actor, if needed at all
  5247. # Skill::DynamicInfo::clear;
  5248.  
  5249. my ($ownerType, $hook, $actor) = @{{
  5250. '010F' => [Skill::OWNER_CHAR, 'packet_charSkills'],
  5251. '0235' => [Skill::OWNER_HOMUN, 'packet_homunSkills', $char->{homunculus}],
  5252. '029D' => [Skill::OWNER_MERC, 'packet_mercSkills', $char->{mercenary}],
  5253. }->{$args->{switch}}};
  5254.  
  5255. my $skillsIDref = $actor ? \@{$actor->{slave_skillsID}} : \@skillsID;
  5256. delete @{$char->{skills}}{@$skillsIDref};
  5257. @$skillsIDref = ();
  5258.  
  5259. # TODO: $actor can be undefined here
  5260. undef @{$actor->{slave_skillsID}};
  5261. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 37) {
  5262. my ($ID, $targetType, $lv, $sp, $range, $handle, $up) = unpack 'v1 V1 v3 Z24 C1', substr $msg, $i, 37;
  5263. $handle ||= Skill->new(idn => $ID)->getHandle;
  5264.  
  5265. @{$char->{skills}{$handle}}{qw(ID targetType lv sp range up)} = ($ID, $targetType, $lv, $sp, $range, $up);
  5266. # $char->{skills}{$handle}{lv} = $lv unless $char->{skills}{$handle}{lv};
  5267.  
  5268. binAdd($skillsIDref, $handle) unless defined binFind($skillsIDref, $handle);
  5269. Skill::DynamicInfo::add($ID, $handle, $lv, $sp, $range, $targetType, $ownerType);
  5270.  
  5271. Plugins::callHook($hook, {
  5272. ID => $ID,
  5273. handle => $handle,
  5274. level => $lv,
  5275. upgradable => $up,
  5276. });
  5277. }
  5278. }
  5279.  
  5280. sub skill_add {
  5281. my ($self, $args) = @_;
  5282.  
  5283. return unless changeToInGameState();
  5284. my $handle = ($args->{name}) ? $args->{name} : Skill->new(idn => $args->{skillID})->getHandle();
  5285.  
  5286. $char->{skills}{$handle}{ID} = $args->{skillID};
  5287. $char->{skills}{$handle}{sp} = $args->{sp};
  5288. $char->{skills}{$handle}{range} = $args->{range};
  5289. $char->{skills}{$handle}{up} = 0;
  5290. $char->{skills}{$handle}{targetType} = $args->{target};
  5291. $char->{skills}{$handle}{lv} = $args->{lv};
  5292. $char->{skills}{$handle}{new} = 1;
  5293.  
  5294. #Fix bug , receive status "Night" 2 time
  5295. binAdd(\@skillsID, $handle) if (binFind(\@skillsID, $handle) eq "");
  5296.  
  5297. Skill::DynamicInfo::add($args->{skillID}, $handle, $args->{lv}, $args->{sp}, $args->{target}, $args->{target}, Skill::OWNER_CHAR);
  5298.  
  5299. Plugins::callHook('packet_charSkills', {
  5300. ID => $args->{skillID},
  5301. handle => $handle,
  5302. level => $args->{lv},
  5303. upgradable => 0,
  5304. });
  5305. }
  5306.  
  5307. # TODO: merge with stat_info
  5308. sub stats_added {
  5309. my ($self, $args) = @_;
  5310.  
  5311. if ($args->{val} == 207) { # client really checks this and not the result field?
  5312. error T("Not enough stat points to add\n");
  5313. } else {
  5314. if ($args->{type} == VAR_STR) {
  5315. $char->{str} = $args->{val};
  5316. debug "Strength: $args->{val}\n", "parseMsg";
  5317.  
  5318. } elsif ($args->{type} == VAR_AGI) {
  5319. $char->{agi} = $args->{val};
  5320. debug "Agility: $args->{val}\n", "parseMsg";
  5321.  
  5322. } elsif ($args->{type} == VAR_VIT) {
  5323. $char->{vit} = $args->{val};
  5324. debug "Vitality: $args->{val}\n", "parseMsg";
  5325.  
  5326. } elsif ($args->{type} == VAR_INT) {
  5327. $char->{int} = $args->{val};
  5328. debug "Intelligence: $args->{val}\n", "parseMsg";
  5329.  
  5330. } elsif ($args->{type} == VAR_DEX) {
  5331. $char->{dex} = $args->{val};
  5332. debug "Dexterity: $args->{val}\n", "parseMsg";
  5333.  
  5334. } elsif ($args->{type} == VAR_LUK) {
  5335. $char->{luk} = $args->{val};
  5336. debug "Luck: $args->{val}\n", "parseMsg";
  5337.  
  5338. } else {
  5339. debug "Something: $args->{val}\n", "parseMsg";
  5340. }
  5341. }
  5342. Plugins::callHook('packet_charStats', {
  5343. type => $args->{type},
  5344. val => $args->{val},
  5345. });
  5346. }
  5347.  
  5348. sub stats_info {
  5349. my ($self, $args) = @_;
  5350. return unless changeToInGameState();
  5351. $char->{points_free} = $args->{points_free};
  5352. $char->{str} = $args->{str};
  5353. $char->{points_str} = $args->{points_str};
  5354. $char->{agi} = $args->{agi};
  5355. $char->{points_agi} = $args->{points_agi};
  5356. $char->{vit} = $args->{vit};
  5357. $char->{points_vit} = $args->{points_vit};
  5358. $char->{int} = $args->{int};
  5359. $char->{points_int} = $args->{points_int};
  5360. $char->{dex} = $args->{dex};
  5361. $char->{points_dex} = $args->{points_dex};
  5362. $char->{luk} = $args->{luk};
  5363. $char->{points_luk} = $args->{points_luk};
  5364. $char->{attack} = $args->{attack};
  5365. $char->{attack_bonus} = $args->{attack_bonus};
  5366. $char->{attack_magic_min} = $args->{attack_magic_min};
  5367. $char->{attack_magic_max} = $args->{attack_magic_max};
  5368. $char->{def} = $args->{def};
  5369. $char->{def_bonus} = $args->{def_bonus};
  5370. $char->{def_magic} = $args->{def_magic};
  5371. $char->{def_magic_bonus} = $args->{def_magic_bonus};
  5372. $char->{hit} = $args->{hit};
  5373. $char->{flee} = $args->{flee};
  5374. $char->{flee_bonus} = $args->{flee_bonus};
  5375. $char->{critical} = $args->{critical};
  5376. debug "Strength: $char->{str} #$char->{points_str}\n"
  5377. ."Agility: $char->{agi} #$char->{points_agi}\n"
  5378. ."Vitality: $char->{vit} #$char->{points_vit}\n"
  5379. ."Intelligence: $char->{int} #$char->{points_int}\n"
  5380. ."Dexterity: $char->{dex} #$char->{points_dex}\n"
  5381. ."Luck: $char->{luk} #$char->{points_luk}\n"
  5382. ."Attack: $char->{attack}\n"
  5383. ."Attack Bonus: $char->{attack_bonus}\n"
  5384. ."Magic Attack Min: $char->{attack_magic_min}\n"
  5385. ."Magic Attack Max: $char->{attack_magic_max}\n"
  5386. ."Defense: $char->{def}\n"
  5387. ."Defense Bonus: $char->{def_bonus}\n"
  5388. ."Magic Defense: $char->{def_magic}\n"
  5389. ."Magic Defense Bonus: $char->{def_magic_bonus}\n"
  5390. ."Hit: $char->{hit}\n"
  5391. ."Flee: $char->{flee}\n"
  5392. ."Flee Bonus: $char->{flee_bonus}\n"
  5393. ."Critical: $char->{critical}\n"
  5394. ."Status Points: $char->{points_free}\n", "parseMsg";
  5395. }
  5396.  
  5397. our %stat_info_handlers = (
  5398. VAR_SPEED, sub { $_[0]{walk_speed} = $_[1] / 1000 },
  5399. VAR_EXP, sub {
  5400. my ($actor, $value) = @_;
  5401.  
  5402. $actor->{exp_last} = $actor->{exp};
  5403. $actor->{exp} = $value;
  5404.  
  5405. return unless $actor->isa('Actor::You');
  5406.  
  5407. unless ($bExpSwitch) {
  5408. $bExpSwitch = 1;
  5409. } else {
  5410. if ($actor->{exp_last} > $actor->{exp}) {
  5411. $monsterBaseExp = 0;
  5412. } else {
  5413. $monsterBaseExp = $actor->{exp} - $actor->{exp_last};
  5414. }
  5415. $totalBaseExp += $monsterBaseExp;
  5416. if ($bExpSwitch == 1) {
  5417. $totalBaseExp += $monsterBaseExp;
  5418. $bExpSwitch = 2;
  5419. }
  5420. }
  5421.  
  5422. # no VAR_JOBEXP next - no message?
  5423. },
  5424. VAR_JOBEXP, sub {
  5425. my ($actor, $value) = @_;
  5426.  
  5427. $actor->{exp_job_last} = $actor->{exp_job};
  5428. $actor->{exp_job} = $value;
  5429.  
  5430. # TODO: message for all actors
  5431. return unless $actor->isa('Actor::You');
  5432. # TODO: exp report (statistics) - no globals, move to plugin
  5433.  
  5434. if ($jExpSwitch == 0) {
  5435. $jExpSwitch = 1;
  5436. } else {
  5437. if ($char->{exp_job_last} > $char->{exp_job}) {
  5438. $monsterJobExp = 0;
  5439. } else {
  5440. $monsterJobExp = $char->{exp_job} - $char->{exp_job_last};
  5441. }
  5442. $totalJobExp += $monsterJobExp;
  5443. if ($jExpSwitch == 1) {
  5444. $totalJobExp += $monsterJobExp;
  5445. $jExpSwitch = 2;
  5446. }
  5447. }
  5448. my $basePercent = $char->{exp_max} ?
  5449. ($monsterBaseExp / $char->{exp_max} * 100) :
  5450. 0;
  5451. my $jobPercent = $char->{exp_job_max} ?
  5452. ($monsterJobExp / $char->{exp_job_max} * 100) :
  5453. 0;
  5454. message TF("%s have gained %d/%d (%.2f%%/%.2f%%) Exp\n", $char, $monsterBaseExp, $monsterJobExp, $basePercent, $jobPercent), "exp";
  5455. Plugins::callHook('exp_gained');
  5456. },
  5457. #VAR_VIRTUE
  5458. VAR_HONOR, sub {
  5459. my ($actor, $value) = @_;
  5460.  
  5461. if ($value > 0) {
  5462. my $duration = 0xffffffff - $value + 1;
  5463. $actor->{mute_period} = $duration * 60;
  5464. $actor->{muted} = time;
  5465. message sprintf(
  5466. $actor->verb(T("%s have been muted for %d minutes\n"), T("%s has been muted for %d minutes\n")),
  5467. $actor, $duration
  5468. ), "parseMsg_statuslook", $actor->isa('Actor::You') ? 1 : 2;
  5469. } else {
  5470. delete $actor->{muted};
  5471. delete $actor->{mute_period};
  5472. message sprintf(
  5473. $actor->verb(T("%s are no longer muted."), T("%s is no longer muted.")), $actor
  5474. ), "parseMsg_statuslook", $actor->isa('Actor::You') ? 1 : 2;
  5475. }
  5476.  
  5477. return unless $actor->isa('Actor::You');
  5478.  
  5479. if ($config{dcOnMute} && $actor->{muted}) {
  5480. chatLog("k", TF("*** %s have been muted for %d minutes, auto disconnect! ***\n", $actor, $actor->{mute_period}/60));
  5481. quit();
  5482. }
  5483. },
  5484. VAR_HP, sub {
  5485. $_[0]{hp} = $_[1];
  5486. $_[0]{hpPercent} = $_[0]{hp_max} ? 100 * $_[0]{hp} / $_[0]{hp_max} : undef;
  5487. },
  5488. VAR_MAXHP, sub {
  5489. $_[0]{hp_max} = $_[1];
  5490. $_[0]{hpPercent} = $_[0]{hp_max} ? 100 * $_[0]{hp} / $_[0]{hp_max} : undef;
  5491. },
  5492. VAR_SP, sub {
  5493. $_[0]{sp} = $_[1];
  5494. $_[0]{spPercent} = $_[0]{sp_max} ? 100 * $_[0]{sp} / $_[0]{sp_max} : undef;
  5495. },
  5496. VAR_MAXSP, sub {
  5497. $_[0]{sp_max} = $_[1];
  5498. $_[0]{spPercent} = $_[0]{sp_max} ? 100 * $_[0]{sp} / $_[0]{sp_max} : undef;
  5499. },
  5500. VAR_POINT, sub { $_[0]{points_free} = $_[1] },
  5501. #VAR_HAIRCOLOR
  5502. VAR_CLEVEL, sub {
  5503. my ($actor, $value) = @_;
  5504.  
  5505. $actor->{lv} = $value;
  5506.  
  5507. message sprintf($actor->verb(T("%s are now level %d\n"), T("%s is now level %d\n")), $actor, $value), "success", $actor->isa('Actor::You') ? 1 : 2;
  5508.  
  5509. return unless $actor->isa('Actor::You');
  5510.  
  5511. if ($config{dcOnLevel} && $actor->{lv} >= $config{dcOnLevel}) {
  5512. message TF("Disconnecting on level %s!\n", $config{dcOnLevel});
  5513. chatLog("k", TF("Disconnecting on level %s!\n", $config{dcOnLevel}));
  5514. quit();
  5515. }
  5516. },
  5517. VAR_SPPOINT, sub { $_[0]{points_skill} = $_[1] },
  5518. #VAR_STR
  5519. #VAR_AGI
  5520. #VAR_VIT
  5521. #VAR_INT
  5522. #VAR_DEX
  5523. #VAR_LUK
  5524. #VAR_JOB
  5525. VAR_MONEY, sub {
  5526. my ($actor, $value) = @_;
  5527.  
  5528. my $change = $value - $actor->{zeny};
  5529. $actor->{zeny} = $value;
  5530.  
  5531. message sprintf(
  5532. $change > 0
  5533. ? $actor->verb(T("%s gained %s zeny.\n"), T("%s gained %s zeny.\n"))
  5534. : $actor->verb(T("%s lost %s zeny.\n"), T("%s lost %s zeny.\n")),
  5535. $actor, formatNumber(abs $change)
  5536. ), 'info', $actor->isa('Actor::You') ? 1 : 2 if $change;
  5537.  
  5538. return unless $actor->isa('Actor::You');
  5539.  
  5540. if ($config{dcOnZeny} && $actor->{zeny} <= $config{dcOnZeny}) {
  5541. $interface->errorDialog(TF("Disconnecting due to zeny lower than %s.", $config{dcOnZeny}));
  5542. $quit = 1;
  5543. }
  5544. },
  5545. #VAR_SEX
  5546. VAR_MAXEXP, sub {
  5547. $_[0]{exp_max_last} = $_[0]{exp_max};
  5548. $_[0]{exp_max} = $_[1];
  5549.  
  5550. if (!$net->clientAlive() && $initSync && $masterServer->{serverType} == 2) {
  5551. $messageSender->sendSync(1);
  5552. $initSync = 0;
  5553. }
  5554. },
  5555. VAR_MAXJOBEXP, sub {
  5556. $_[0]{exp_job_max_last} = $_[0]{exp_job_max};
  5557. $_[0]{exp_job_max} = $_[1];
  5558. #message TF("BaseExp: %s | JobExp: %s\n", $monsterBaseExp, $monsterJobExp), "info", 2 if ($monsterBaseExp);
  5559. },
  5560. VAR_WEIGHT, sub { $_[0]{weight} = $_[1] / 10 },
  5561. VAR_MAXWEIGHT, sub { $_[0]{weight_max} = int($_[1] / 10) },
  5562. #VAR_POISON
  5563. #VAR_STONE
  5564. #VAR_CURSE
  5565. #VAR_FREEZING
  5566. #VAR_SILENCE
  5567. #VAR_CONFUSION
  5568. VAR_STANDARD_STR, sub { $_[0]{points_str} = $_[1] },
  5569. VAR_STANDARD_AGI, sub { $_[0]{points_agi} = $_[1] },
  5570. VAR_STANDARD_VIT, sub { $_[0]{points_vit} = $_[1] },
  5571. VAR_STANDARD_INT, sub { $_[0]{points_int} = $_[1] },
  5572. VAR_STANDARD_DEX, sub { $_[0]{points_dex} = $_[1] },
  5573. VAR_STANDARD_LUK, sub { $_[0]{points_luk} = $_[1] },
  5574. #VAR_ATTACKMT
  5575. #VAR_ATTACKEDMT
  5576. #VAR_NV_BASIC
  5577. VAR_ATTPOWER, sub { $_[0]{attack} = $_[1] },
  5578. VAR_REFININGPOWER, sub { $_[0]{attack_bonus} = $_[1] },
  5579. VAR_MAX_MATTPOWER, sub { $_[0]{attack_magic_max} = $_[1] },
  5580. VAR_MIN_MATTPOWER, sub { $_[0]{attack_magic_min} = $_[1] },
  5581. VAR_ITEMDEFPOWER, sub { $_[0]{def} = $_[1] },
  5582. VAR_PLUSDEFPOWER, sub { $_[0]{def_bonus} = $_[1] },
  5583. VAR_MDEFPOWER, sub { $_[0]{def_magic} = $_[1] },
  5584. VAR_PLUSMDEFPOWER, sub { $_[0]{def_magic_bonus} = $_[1] },
  5585. VAR_HITSUCCESSVALUE, sub { $_[0]{hit} = $_[1] },
  5586. VAR_AVOIDSUCCESSVALUE, sub { $_[0]{flee} = $_[1] },
  5587. VAR_PLUSAVOIDSUCCESSVALUE, sub { $_[0]{flee_bonus} = $_[1] },
  5588. VAR_CRITICALSUCCESSVALUE, sub { $_[0]{critical} = $_[1] },
  5589. VAR_ASPD, sub {
  5590. $_[0]{attack_delay} = $_[1] >= 10 ? $_[1] : 10; # at least for mercenary
  5591. $_[0]{attack_speed} = 200 - $_[0]{attack_delay} / 10;
  5592. },
  5593. #VAR_PLUSASPD
  5594. VAR_JOBLEVEL, sub {
  5595. my ($actor, $value) = @_;
  5596.  
  5597. $actor->{lv_job} = $value;
  5598. message sprintf($actor->verb("%s are now job level %d\n", "%s is now job level %d\n"), $actor, $actor->{lv_job}), "success", $actor->isa('Actor::You') ? 1 : 2;
  5599.  
  5600. return unless $actor->isa('Actor::You');
  5601.  
  5602. if ($config{dcOnJobLevel} && $actor->{lv_job} >= $config{dcOnJobLevel}) {
  5603. message TF("Disconnecting on job level %d!\n", $config{dcOnJobLevel});
  5604. chatLog("k", TF("Disconnecting on job level %d!\n", $config{dcOnJobLevel}));
  5605. quit();
  5606. }
  5607. },
  5608. #...
  5609. VAR_MER_KILLCOUNT, sub { $_[0]{kills} = $_[1] },
  5610. VAR_MER_FAITH, sub { $_[0]{faith} = $_[1] },
  5611. #...
  5612. );
  5613.  
  5614. sub stat_info {
  5615. my ($self, $args) = @_;
  5616.  
  5617. return unless changeToInGameState;
  5618.  
  5619. my $actor = {
  5620. '00B0' => $char,
  5621. '00B1' => $char,
  5622. '00BE' => $char,
  5623. '0141' => $char,
  5624. '01AB' => exists $args->{ID} && Actor::get($args->{ID}),
  5625. '02A2' => $char->{mercenary},
  5626. '07DB' => $char->{homunculus},
  5627. #'081E' => Sorcerer's Spirit - not implemented in Kore
  5628. }->{$args->{switch}};
  5629.  
  5630. unless ($actor) {
  5631. warning sprintf "Actor is unknown or not ready for stat information (switch %s, type %d, val %d)\n", @{$args}{qw(switch type val)};
  5632. return;
  5633. }
  5634.  
  5635. if (exists $stat_info_handlers{$args->{type}}) {
  5636. # TODO: introduce Actor->something() to determine per-actor configurable verbosity level? (not only here)
  5637. debug "Stat: $args->{type} => $args->{val}\n", "parseMsg", $_[0]->isa('Actor::You') ? 1 : 2;
  5638. $stat_info_handlers{$args->{type}}($actor, $args->{val});
  5639. } else {
  5640. warning sprintf "Unknown stat (%d => %d) received for %s\n", @{$args}{qw(type val)}, $actor;
  5641. }
  5642.  
  5643. if (!$char->{walk_speed}) {
  5644. $char->{walk_speed} = 0.15; # This is the default speed, since xkore requires this and eA (And aegis?) do not send this if its default speed
  5645. }
  5646. }
  5647.  
  5648. sub stat_info2 {
  5649. my ($self, $args) = @_;
  5650. return unless changeToInGameState();
  5651. my ($type, $val, $val2) = @{$args}{qw(type val val2)};
  5652. if ($type == VAR_STR) {
  5653. $char->{str} = $val;
  5654. $char->{str_bonus} = $val2;
  5655. debug "Strength: $val + $val2\n", "parseMsg";
  5656. } elsif ($type == VAR_AGI) {
  5657. $char->{agi} = $val;
  5658. $char->{agi_bonus} = $val2;
  5659. debug "Agility: $val + $val2\n", "parseMsg";
  5660. } elsif ($type == VAR_VIT) {
  5661. $char->{vit} = $val;
  5662. $char->{vit_bonus} = $val2;
  5663. debug "Vitality: $val + $val2\n", "parseMsg";
  5664. } elsif ($type == VAR_INT) {
  5665. $char->{int} = $val;
  5666. $char->{int_bonus} = $val2;
  5667. debug "Intelligence: $val + $val2\n", "parseMsg";
  5668. } elsif ($type == VAR_DEX) {
  5669. $char->{dex} = $val;
  5670. $char->{dex_bonus} = $val2;
  5671. debug "Dexterity: $val + $val2\n", "parseMsg";
  5672. } elsif ($type == VAR_LUK) {
  5673. $char->{luk} = $val;
  5674. $char->{luk_bonus} = $val2;
  5675. debug "Luck: $val + $val2\n", "parseMsg";
  5676. }
  5677. }
  5678.  
  5679. sub storage_closed {
  5680. message T("Storage closed.\n"), "storage";
  5681. delete $ai_v{temp}{storage_opened};
  5682. delete $storage{opened};
  5683. Plugins::callHook('packet_storage_close');
  5684.  
  5685. # Storage log
  5686. writeStorageLog(0);
  5687.  
  5688. if ($char->{dcOnEmptyItems} ne "") {
  5689. message TF("Disconnecting on empty %s!\n", $char->{dcOnEmptyItems});
  5690. chatLog("k", TF("Disconnecting on empty %s!\n", $char->{dcOnEmptyItems}));
  5691. quit();
  5692. }
  5693. }
  5694.  
  5695. sub storage_item_added {
  5696. my ($self, $args) = @_;
  5697.  
  5698. my $index = $args->{index};
  5699. my $amount = $args->{amount};
  5700.  
  5701. my $item = $storage{$index} ||= Actor::Item->new;
  5702. if ($item->{amount}) {
  5703. $item->{amount} += $amount;
  5704. } else {
  5705. binAdd(\@storageID, $index);
  5706. $item->{nameID} = $args->{nameID};
  5707. $item->{index} = $index;
  5708. $item->{amount} = $amount;
  5709. $item->{type} = $args->{type};
  5710. $item->{identified} = $args->{identified};
  5711. $item->{broken} = $args->{broken};
  5712. $item->{upgrade} = $args->{upgrade};
  5713. $item->{cards} = $args->{cards};
  5714. $item->{name} = itemName($item);
  5715. $item->{binID} = binFind(\@storageID, $index);
  5716. }
  5717. message TF("Storage Item Added: %s (%d) x %s\n", $item->{name}, $item->{binID}, $amount), "storage", 1;
  5718. $itemChange{$item->{name}} += $amount;
  5719. $args->{item} = $item;
  5720. }
  5721.  
  5722. sub storage_item_removed {
  5723. my ($self, $args) = @_;
  5724.  
  5725. my ($index, $amount) = @{$args}{qw(index amount)};
  5726.  
  5727. my $item = $storage{$index};
  5728. $item->{amount} -= $amount;
  5729. message TF("Storage Item Removed: %s (%d) x %s\n", $item->{name}, $item->{binID}, $amount), "storage";
  5730. $itemChange{$item->{name}} -= $amount;
  5731. $args->{item} = $item;
  5732. if ($item->{amount} <= 0) {
  5733. delete $storage{$index};
  5734. binRemove(\@storageID, $index);
  5735. }
  5736. }
  5737.  
  5738. sub storage_items_nonstackable {
  5739. my ($self, $args) = @_;
  5740.  
  5741. $self->_items_list({
  5742. class => 'Actor::Item',
  5743. hook => 'packet_storage',
  5744. debug_str => 'Non-Stackable Storage Item',
  5745. items => [$self->parse_items_nonstackable($args)],
  5746. adder => sub { $_[0]{binID} = binAdd(\@storageID, $_[0]{index}); $storage{$_[0]{index}} = $_[0] },
  5747. });
  5748.  
  5749. $storageTitle = exists $args->{title} ? $args->{title} : undef;
  5750. }
  5751.  
  5752. sub storage_items_stackable {
  5753. my ($self, $args) = @_;
  5754.  
  5755. undef %storage;
  5756. undef @storageID;
  5757.  
  5758. $self->_items_list({
  5759. class => 'Actor::Item',
  5760. hook => 'packet_storage',
  5761. debug_str => 'Stackable Storage Item',
  5762. items => [$self->parse_items_stackable($args)],
  5763. adder => sub { $_[0]{binID} = binAdd(\@storageID, $_[0]{index}); $storage{$_[0]{index}} = $_[0] },
  5764. callback => sub {
  5765. my ($local_item) = @_;
  5766.  
  5767. $local_item->{amount} = $local_item->{amount} & ~0x80000000;
  5768. },
  5769. });
  5770.  
  5771. $storageTitle = exists $args->{title} ? $args->{title} : undef;
  5772. }
  5773.  
  5774. sub storage_opened {
  5775. my ($self, $args) = @_;
  5776. $storage{items} = $args->{items};
  5777. $storage{items_max} = $args->{items_max};
  5778.  
  5779. $ai_v{temp}{storage_opened} = 1;
  5780. if (!$storage{opened}) {
  5781. $storage{opened} = 1;
  5782. $storage{openedThisSession} = 1;
  5783. message defined $storageTitle ? TF("Storage '%s' opened.\n", $storageTitle) : T("Storage opened.\n"), "storage";
  5784. Plugins::callHook('packet_storage_open');
  5785. }
  5786. }
  5787.  
  5788. sub storage_password_request {
  5789. my ($self, $args) = @_;
  5790.  
  5791. if ($args->{flag} == 0) {
  5792. if ($args->{switch} eq '023E') {
  5793. message T("Please enter a new character password:\n");
  5794. } else {
  5795. if ($config{storageAuto_password} eq '') {
  5796. my $input = $interface->query(T("You've never set a storage password before.\nYou must set a storage password before you can use the storage.\nPlease enter a new storage password:"), isPassword => 1);
  5797. if (!defined($input)) {
  5798. return;
  5799. }
  5800. configModify('storageAuto_password', $input, 1);
  5801. }
  5802. }
  5803.  
  5804. my @key = split /[, ]+/, $config{storageEncryptKey};
  5805. if (!@key) {
  5806. error (($args->{switch} eq '023E') ?
  5807. T("Unable to send character password. You must set the 'storageEncryptKey' option in config.txt or servers.txt.\n") :
  5808. T("Unable to send storage password. You must set the 'storageEncryptKey' option in config.txt or servers.txt.\n"));
  5809. return;
  5810. }
  5811. my $crypton = new Utils::Crypton(pack("V*", @key), 32);
  5812. my $num = ($args->{switch} eq '023E') ? $config{charSelect_password} : $config{storageAuto_password};
  5813. $num = sprintf("%d%08d", length($num), $num);
  5814. my $ciphertextBlock = $crypton->encrypt(pack("V*", $num, 0, 0, 0));
  5815. message TF("Storage password set to: %s\n", $config{storageAuto_password}), "success";
  5816. $messageSender->sendStoragePassword($ciphertextBlock, 2);
  5817. $messageSender->sendStoragePassword($ciphertextBlock, 3);
  5818.  
  5819. } elsif ($args->{flag} == 1) {
  5820. if ($args->{switch} eq '023E') {
  5821. if ($config{charSelect_password} eq '') {
  5822. my $input = $interface->query(T("Please enter your character password."), isPassword => 1);
  5823. if (!defined($input)) {
  5824. return;
  5825. }
  5826. configModify('charSelect_password', $input, 1);
  5827. message TF("Character password set to: %s\n", $input), "success";
  5828. }
  5829. } else {
  5830. if ($config{storageAuto_password} eq '') {
  5831. my $input = $interface->query(T("Please enter your storage password."), isPassword => 1);
  5832. if (!defined($input)) {
  5833. return;
  5834. }
  5835. configModify('storageAuto_password', $input, 1);
  5836. message TF("Storage password set to: %s\n", $input), "success";
  5837. }
  5838. }
  5839.  
  5840. my @key = split /[, ]+/, $config{storageEncryptKey};
  5841. if (!@key) {
  5842. error (($args->{switch} eq '023E') ?
  5843. T("Unable to send character password. You must set the 'storageEncryptKey' option in config.txt or servers.txt.\n") :
  5844. T("Unable to send storage password. You must set the 'storageEncryptKey' option in config.txt or servers.txt.\n"));
  5845. return;
  5846. }
  5847. my $crypton = new Utils::Crypton(pack("V*", @key), 32);
  5848. my $num = ($args->{switch} eq '023E') ? $config{charSelect_password} : $config{storageAuto_password};
  5849. $num = sprintf("%d%08d", length($num), $num);
  5850. my $ciphertextBlock = $crypton->encrypt(pack("V*", $num, 0, 0, 0));
  5851. $messageSender->sendStoragePassword($ciphertextBlock, 3);
  5852.  
  5853. } elsif ($args->{flag} == 8) { # apparently this flag means that you have entered the wrong password
  5854. # too many times, and now the server is blocking you from using storage
  5855. error T("You have entered the wrong password 5 times. Please try again later.\n");
  5856. # temporarily disable storageAuto
  5857. $config{storageAuto} = 0;
  5858. my $index = AI::findAction('storageAuto');
  5859. if (defined $index) {
  5860. AI::args($index)->{done} = 1;
  5861. while (AI::action ne 'storageAuto') {
  5862. AI::dequeue;
  5863. }
  5864. }
  5865. } else {
  5866. debug(($args->{switch} eq '023E') ?
  5867. "Character password: unknown flag $args->{flag}\n" :
  5868. "Storage password: unknown flag $args->{flag}\n");
  5869. }
  5870. }
  5871.  
  5872. # TODO
  5873. sub storage_password_result {
  5874. my ($self, $args) = @_;
  5875.  
  5876. # TODO:
  5877. # STORE_PASSWORD_EMPTY = 0x0
  5878. # STORE_PASSWORD_EXIST = 0x1
  5879. # STORE_PASSWORD_CHANGE = 0x2
  5880. # STORE_PASSWORD_CHECK = 0x3
  5881. # STORE_PASSWORD_PANALTY = 0x8
  5882.  
  5883. if ($args->{type} == 4) { # STORE_PASSWORD_CHANGE_OK = 0x4
  5884. message T("Successfully changed storage password.\n"), "success";
  5885. } elsif ($args->{type} == 5) { # STORE_PASSWORD_CHANGE_NG = 0x5
  5886. error T("Error: Incorrect storage password.\n");
  5887. } elsif ($args->{type} == 6) { # STORE_PASSWORD_CHECK_OK = 0x6
  5888. message T("Successfully entered storage password.\n"), "success";
  5889. } elsif ($args->{type} == 7) { # STORE_PASSWORD_CHECK_NG = 0x7
  5890. error T("Error: Incorrect storage password.\n");
  5891. # disable storageAuto or the Kafra storage will be blocked
  5892. configModify("storageAuto", 0);
  5893. my $index = AI::findAction('storageAuto');
  5894. if (defined $index) {
  5895. AI::args($index)->{done} = 1;
  5896. while (AI::action ne 'storageAuto') {
  5897. AI::dequeue;
  5898. }
  5899. }
  5900. } else {
  5901. #message "Storage password: unknown type $args->{type}\n";
  5902. }
  5903.  
  5904. # $args->{val}
  5905. # unknown, what is this for?
  5906. }
  5907.  
  5908. sub initialize_message_id_encryption {
  5909. my ($self, $args) = @_;
  5910. if ($masterServer->{messageIDEncryption} ne '0') {
  5911. $messageSender->sendMessageIDEncryptionInitialized();
  5912.  
  5913. my @c;
  5914. my $shtmp = $args->{param1};
  5915. for (my $i = 8; $i > 0; $i--) {
  5916. $c[$i] = $shtmp & 0x0F;
  5917. $shtmp >>= 4;
  5918. }
  5919. my $w = ($c[6]<<12) + ($c[4]<<8) + ($c[7]<<4) + $c[1];
  5920. $enc_val1 = ($c[2]<<12) + ($c[3]<<8) + ($c[5]<<4) + $c[8];
  5921. $enc_val2 = (((($enc_val1 ^ 0x0000F3AC) + $w) << 16) | (($enc_val1 ^ 0x000049DF) + $w)) ^ $args->{param2};
  5922. }
  5923. }
  5924.  
  5925. # TODO: known prefixes (chat domains): micc | ssss | ...
  5926. sub system_chat {
  5927. my ($self, $args) = @_;
  5928.  
  5929. my $message = bytesToString($args->{message});
  5930. if (substr($message,0,4) eq 'micc') {
  5931. $message = bytesToString(substr($args->{message},34));
  5932. }
  5933. $message =~ s/\000//g; # remove null charachters
  5934. $message =~ s/^(tool[0-9a-fA-F]{6})//g; # remove those annoying toolDDDDDD from bRO (and maybe some other server?)
  5935. $message =~ s/^ssss//g; # remove those annoying ssss from bRO (and maybe some other server?)
  5936. $message =~ s/^ +//g; $message =~ s/ +$//g; # remove whitespace in the beginning and the end of $message
  5937. stripLanguageCode(\$message);
  5938. chatLog("s", "$message\n") if ($config{logSystemChat});
  5939. # Translation Comment: System/GM chat
  5940. message TF("[GM] %s\n", $message), "schat";
  5941. ChatQueue::add('gm', undef, undef, $message);
  5942.  
  5943. Plugins::callHook('packet_sysMsg', {
  5944. Msg => $message
  5945. });
  5946. }
  5947.  
  5948. sub top10_alchemist_rank {
  5949. my ($self, $args) = @_;
  5950.  
  5951. my $textList = bytesToString(top10Listing($args));
  5952. message TF("============= ALCHEMIST RANK ================\n" .
  5953. "# Name Points\n".
  5954. "%s" .
  5955. "=============================================\n", $textList), "list";
  5956. }
  5957.  
  5958. sub top10_blacksmith_rank {
  5959. my ($self, $args) = @_;
  5960.  
  5961. my $textList = bytesToString(top10Listing($args));
  5962. message TF("============= BLACKSMITH RANK ===============\n" .
  5963. "# Name Points\n".
  5964. "%s" .
  5965. "=============================================\n", $textList), "list";
  5966. }
  5967.  
  5968. sub top10_pk_rank {
  5969. my ($self, $args) = @_;
  5970.  
  5971. my $textList = bytesToString(top10Listing($args));
  5972. message TF("================ PVP RANK ===================\n" .
  5973. "# Name Points\n".
  5974. "%s" .
  5975. "=============================================\n", $textList), "list";
  5976. }
  5977.  
  5978. sub top10_taekwon_rank {
  5979. my ($self, $args) = @_;
  5980.  
  5981. my $textList = bytesToString(top10Listing($args));
  5982. message TF("=============== TAEKWON RANK ================\n" .
  5983. "# Name Points\n".
  5984. "%s" .
  5985. "=============================================\n", $textList), "list";
  5986. }
  5987.  
  5988. sub unequip_item {
  5989. my ($self, $args) = @_;
  5990.  
  5991. return unless changeToInGameState();
  5992. my $item = $char->inventory->getByServerIndex($args->{index});
  5993. delete $item->{equipped};
  5994.  
  5995. if ($args->{type} == 10 || $args->{type} == 32768) {
  5996. delete $char->{equipment}{arrow};
  5997. delete $char->{arrow};
  5998. } else {
  5999. foreach (%equipSlot_rlut){
  6000. if ($_ & $args->{type}){
  6001. next if $_ == 10; #work around Arrow bug
  6002. next if $_ == 32768;
  6003. delete $char->{equipment}{$equipSlot_lut{$_}};
  6004. }
  6005. }
  6006. }
  6007. if ($item) {
  6008. message TF("You unequip %s (%d) - %s\n",
  6009. $item->{name}, $item->{invIndex},
  6010. $equipTypes_lut{$item->{type_equip}}), 'inventory';
  6011. }
  6012. }
  6013.  
  6014. sub unit_levelup {
  6015. my ($self, $args) = @_;
  6016.  
  6017. my $ID = $args->{ID};
  6018. my $type = $args->{type};
  6019. my $actor = Actor::get($ID);
  6020. if ($type == LEVELUP_EFFECT) {
  6021. message TF("%s gained a level!\n", $actor);
  6022. Plugins::callHook('base_level', {name => $actor});
  6023. } elsif ($type == JOBLEVELUP_EFFECT) {
  6024. message TF("%s gained a job level!\n", $actor);
  6025. Plugins::callHook('job_level', {name => $actor});
  6026. } elsif ($type == REFINING_FAIL_EFFECT) {
  6027. message TF("%s failed to refine a weapon!\n", $actor), "refine";
  6028. } elsif ($type == REFINING_SUCCESS_EFFECT) {
  6029. message TF("%s successfully refined a weapon!\n", $actor), "refine";
  6030. } elsif ($type == MAKEITEM_AM_SUCCESS_EFFECT) {
  6031. message TF("%s successfully created a potion!\n", $actor), "refine";
  6032. } elsif ($type == MAKEITEM_AM_FAIL_EFFECT) {
  6033. message TF("%s failed to create a potion!\n", $actor), "refine";
  6034. } else {
  6035. message TF("%s unknown unit_levelup effect (%d)\n", $actor, $type);
  6036. }
  6037. }
  6038.  
  6039. sub use_item {
  6040. my ($self, $args) = @_;
  6041.  
  6042. return unless changeToInGameState();
  6043. my $item = $char->inventory->getByServerIndex($args->{index});
  6044. if ($item) {
  6045. $item->{amount} -= $args->{amount};
  6046. message TF("You used Item: %s (%d) x %s\n", $item->{name}, $item->{invIndex}, $args->{amount}), "useItem";
  6047. if ($item->{amount} <= 0) {
  6048. $char->inventory->remove($item);
  6049. }
  6050. }
  6051. }
  6052.  
  6053. sub users_online {
  6054. my ($self, $args) = @_;
  6055.  
  6056. message TF("There are currently %s users online\n", $args->{users}), "info";
  6057. }
  6058.  
  6059. sub vender_found {
  6060. my ($self, $args) = @_;
  6061. my $ID = $args->{ID};
  6062.  
  6063. if (!$venderLists{$ID} || !%{$venderLists{$ID}}) {
  6064. binAdd(\@venderListsID, $ID);
  6065. Plugins::callHook('packet_vender', {ID => $ID});
  6066. }
  6067. $venderLists{$ID}{title} = bytesToString($args->{title});
  6068. $venderLists{$ID}{id} = $ID;
  6069. }
  6070.  
  6071. sub vender_items_list {
  6072. my ($self, $args) = @_;
  6073.  
  6074. my $msg = $args->{RAW_MSG};
  6075. my $msg_size = $args->{RAW_MSG_SIZE};
  6076. my $headerlen;
  6077.  
  6078. # a hack, but the best we can do now
  6079. if ($args->{switch} eq "0133") {
  6080. $headerlen = 8;
  6081. } else { # switch 0800
  6082. $headerlen = 12;
  6083. }
  6084.  
  6085. undef @venderItemList;
  6086. undef $venderID;
  6087. undef $venderCID;
  6088. $venderID = $args->{venderID};
  6089. $venderCID = $args->{venderCID} if exists $args->{venderCID};
  6090. my $player = Actor::get($venderID);
  6091.  
  6092. message TF("%s\n" .
  6093. "# Name Type Amount Price\n",
  6094. center(' Vender: ' . $player->nameIdx . ' ', 79, '-')), "list";
  6095. for (my $i = $headerlen; $i < $args->{RAW_MSG_SIZE}; $i+=22) {
  6096. my $item = {};
  6097. my $index;
  6098.  
  6099. ($item->{price},
  6100. $item->{amount},
  6101. $index,
  6102. $item->{type},
  6103. $item->{nameID},
  6104. $item->{identified}, # should never happen
  6105. $item->{broken}, # should never happen
  6106. $item->{upgrade},
  6107. $item->{cards}) = unpack('V v2 C v C3 a8', substr($args->{RAW_MSG}, $i, 22));
  6108.  
  6109. $item->{name} = itemName($item);
  6110. $venderItemList[$index] = $item;
  6111.  
  6112. debug("Item added to Vender Store: $item->{name} - $item->{price} z\n", "vending", 2);
  6113.  
  6114. Plugins::callHook('packet_vender_store', {
  6115. venderID => $venderID,
  6116. number => $index,
  6117. name => $item->{name},
  6118. amount => $item->{amount},
  6119. price => $item->{price},
  6120. upgrade => $item->{upgrade},
  6121. cards => $item->{cards},
  6122. type => $item->{type},
  6123. id => $item->{nameID}
  6124. });
  6125.  
  6126. message(swrite(
  6127. "@<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<< @>>>>> @>>>>>>>>>z",
  6128. [$index, $item->{name}, $itemTypes_lut{$item->{type}}, $item->{amount}, formatNumber($item->{price})]),
  6129. "list");
  6130. }
  6131. message("-------------------------------------------------------------------------------\n", "list");
  6132.  
  6133. Plugins::callHook('packet_vender_store2', {
  6134. venderID => $venderID,
  6135. itemList => \@venderItemList
  6136. });
  6137. }
  6138.  
  6139. sub vender_lost {
  6140. my ($self, $args) = @_;
  6141.  
  6142. my $ID = $args->{ID};
  6143. binRemove(\@venderListsID, $ID);
  6144. delete $venderLists{$ID};
  6145. }
  6146.  
  6147. sub vender_buy_fail {
  6148. my ($self, $args) = @_;
  6149.  
  6150. my $reason;
  6151. if ($args->{fail} == 1) {
  6152. error TF("Failed to buy %s of item #%s from vender (insufficient zeny).\n", $args->{amount}, $args->{index});
  6153. } elsif ($args->{fail} == 2) {
  6154. error TF("Failed to buy %s of item #%s from vender (overweight).\n", $args->{amount}, $args->{index});
  6155. } else {
  6156. error TF("Failed to buy %s of item #%s from vender (unknown code %s).\n", $args->{amount}, $args->{index}, $args->{fail});
  6157. }
  6158. }
  6159.  
  6160. # TODO
  6161. sub vending_start {
  6162. my ($self, $args) = @_;
  6163.  
  6164. my $msg = $args->{RAW_MSG};
  6165. my $msg_size = unpack("v1",substr($msg, 2, 2));
  6166.  
  6167. #started a shop.
  6168. message TF("Shop '%s' opened!\n", $shop{title}), "success";
  6169. @articles = ();
  6170. # FIXME: why do we need a seperate variable to track how many items are left in the store?
  6171. $articles = 0;
  6172.  
  6173. # FIXME: Read the packet the server sends us to determine
  6174. # the shop title instead of using $shop{title}.
  6175. message TF("%s\n" .
  6176. "# Name Type Amount Price\n",
  6177. center(" $shop{title} ", 79, '-')), "list";
  6178. for (my $i = 8; $i < $msg_size; $i += 22) {
  6179. my $number = unpack("v1", substr($msg, $i + 4, 2));
  6180. my $item = $articles[$number] = {};
  6181. $item->{nameID} = unpack("v1", substr($msg, $i + 9, 2));
  6182. $item->{quantity} = unpack("v1", substr($msg, $i + 6, 2));
  6183. $item->{type} = unpack("C1", substr($msg, $i + 8, 1));
  6184. $item->{identified} = unpack("C1", substr($msg, $i + 11, 1));
  6185. $item->{broken} = unpack("C1", substr($msg, $i + 12, 1));
  6186. $item->{upgrade} = unpack("C1", substr($msg, $i + 13, 1));
  6187. $item->{cards} = substr($msg, $i + 14, 8);
  6188. $item->{price} = unpack("V1", substr($msg, $i, 4));
  6189. $item->{name} = itemName($item);
  6190. $articles++;
  6191.  
  6192. debug("Item added to Vender Store: $item->{name} - $item->{price} z\n", "vending", 2);
  6193.  
  6194. message(swrite(
  6195. "@< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<< @>>>>> @>>>>>>>>>z",
  6196. [$articles, $item->{name}, $itemTypes_lut{$item->{type}}, $item->{quantity}, formatNumber($item->{price})]),
  6197. "list");
  6198. }
  6199. message(('-'x79)."\n", "list");
  6200. $shopEarned ||= 0;
  6201. }
  6202.  
  6203. sub warp_portal_list {
  6204. my ($self, $args) = @_;
  6205.  
  6206. # strip gat extension
  6207. ($args->{memo1}) = $args->{memo1} =~ /^(.*)\.gat/;
  6208. ($args->{memo2}) = $args->{memo2} =~ /^(.*)\.gat/;
  6209. ($args->{memo3}) = $args->{memo3} =~ /^(.*)\.gat/;
  6210. ($args->{memo4}) = $args->{memo4} =~ /^(.*)\.gat/;
  6211. # Auto-detect saveMap
  6212. if ($args->{type} == 26) {
  6213. configModify('saveMap', $args->{memo2}) if ($args->{memo2} && $config{'saveMap'} ne $args->{memo2});
  6214. } elsif ($args->{type} == 27) {
  6215. configModify('saveMap', $args->{memo1}) if ($args->{memo1} && $config{'saveMap'} ne $args->{memo1});
  6216. }
  6217.  
  6218. $char->{warp}{type} = $args->{type};
  6219. undef @{$char->{warp}{memo}};
  6220. push @{$char->{warp}{memo}}, $args->{memo1} if $args->{memo1} ne "";
  6221. push @{$char->{warp}{memo}}, $args->{memo2} if $args->{memo2} ne "";
  6222. push @{$char->{warp}{memo}}, $args->{memo3} if $args->{memo3} ne "";
  6223. push @{$char->{warp}{memo}}, $args->{memo4} if $args->{memo4} ne "";
  6224.  
  6225. message T("----------------- Warp Portal --------------------\n" .
  6226. "# Place Map\n"), "list";
  6227. for (my $i = 0; $i < @{$char->{warp}{memo}}; $i++) {
  6228. message(swrite(
  6229. "@< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<",
  6230. [$i, $maps_lut{$char->{warp}{memo}[$i].'.rsw'},
  6231. $char->{warp}{memo}[$i]]),
  6232. "list");
  6233. }
  6234. message("--------------------------------------------------\n", "list");
  6235. if ($args->{type} == 26 && AI::action eq 'teleport') {
  6236. # We have already successfully used the Teleport skill.
  6237. $messageSender->sendWarpTele(26, AI::args->{lv} == 2 ? "$config{saveMap}.gat" : "Random");
  6238. AI::dequeue;
  6239. }
  6240. }
  6241.  
  6242. sub mail_refreshinbox {
  6243. my ($self, $args) = @_;
  6244.  
  6245. undef $mailList;
  6246. my $count = $args->{count};
  6247.  
  6248. if (!$count) {
  6249. message T("There is no mail in your inbox.\n"), "info";
  6250. return;
  6251. }
  6252.  
  6253. message TF("You've got Mail! (%s)\n", $count), "info";
  6254. my $msg;
  6255. $msg .= center(" " . T("Inbox") . " ", 79, '-') . "\n";
  6256. # truncating the title from 39 to 34, the user will be able to read the full title when reading the mail
  6257. # truncating the date with precision of minutes and leave year out
  6258. $msg .= swrite(TF("\@> R \@%s \@%s \@%s", ('<'x34), ('<'x24), ('<'x11)),
  6259. ["#", "Title", "Sender", "Date"]);
  6260. $msg .= sprintf("%s\n", ('-'x79));
  6261.  
  6262. my $j = 0;
  6263. for (my $i = 8; $i < 8 + $count * 73; $i+=73) {
  6264. $mailList->[$j]->{mailID} = unpack("V1", substr($args->{RAW_MSG}, $i, 4));
  6265. $mailList->[$j]->{title} = bytesToString(unpack("Z40", substr($args->{RAW_MSG}, $i+4, 40)));
  6266. $mailList->[$j]->{read} = unpack("C1", substr($args->{RAW_MSG}, $i+44, 1));
  6267. $mailList->[$j]->{sender} = bytesToString(unpack("Z24", substr($args->{RAW_MSG}, $i+45, 24)));
  6268. $mailList->[$j]->{timestamp} = unpack("V1", substr($args->{RAW_MSG}, $i+69, 4));
  6269. $msg .= swrite(
  6270. TF("\@> %s \@%s \@%s \@%s", $mailList->[$j]->{read}, ('<'x34), ('<'x24), ('<'x11)),
  6271. [$j, $mailList->[$j]->{title}, $mailList->[$j]->{sender}, getFormattedDate(int($mailList->[$j]->{timestamp}))]);
  6272. $j++;
  6273. }
  6274.  
  6275. $msg .= ("%s\n", ('-'x79));
  6276. message($msg . "\n", "list");
  6277. }
  6278.  
  6279. sub mail_read {
  6280. my ($self, $args) = @_;
  6281.  
  6282. my $item = {};
  6283. $item->{nameID} = $args->{nameID};
  6284. $item->{upgrade} = $args->{upgrade};
  6285. $item->{cards} = $args->{cards};
  6286. $item->{broken} = $args->{broken};
  6287. $item->{name} = itemName($item);
  6288.  
  6289. my $msg;
  6290. $msg .= center(" " . T("Mail") . " ", 79, '-') . "\n";
  6291. $msg .= swrite(TF("Title: \@%s Sender: \@%s", ('<'x39), ('<'x24)),
  6292. [bytesToString($args->{title}), bytesToString($args->{sender})]);
  6293. $msg .= TF("Message: %s\n", bytesToString($args->{message}));
  6294. $msg .= ("%s\n", ('-'x79));
  6295. $msg .= TF( "Item: %s %s\n" .
  6296. "Zeny: %sz\n",
  6297. $item->{name}, ($args->{amount}) ? "x " . $args->{amount} : "", formatNumber($args->{zeny}));
  6298. $msg .= sprintf("%s\n", ('-'x79));
  6299.  
  6300. message($msg, "info");
  6301. }
  6302.  
  6303. sub mail_getattachment {
  6304. my ($self, $args) = @_;
  6305. if (!$args->{fail}) {
  6306. message T("Successfully added attachment to inventory.\n"), "info";
  6307. } elsif ($args->{fail} == 2) {
  6308. error T("Failed to get the attachment to inventory due to your weight.\n"), "info";
  6309. } else {
  6310. error T("Failed to get the attachment to inventory.\n"), "info";
  6311. }
  6312. }
  6313.  
  6314. sub mail_send {
  6315. my ($self, $args) = @_;
  6316. ($args->{fail}) ?
  6317. error T("Failed to send mail, the recipient does not exist.\n"), "info" :
  6318. message T("Mail sent succesfully.\n"), "info";
  6319. }
  6320.  
  6321. sub mail_new {
  6322. my ($self, $args) = @_;
  6323. message TF("New mail from sender: %s titled: %s.\n", bytesToString($args->{sender}), bytesToString($args->{title})), "info";
  6324. }
  6325.  
  6326. sub mail_setattachment {
  6327. my ($self, $args) = @_;
  6328. if ($args->{fail}) {
  6329. if (defined $AI::temp::mailAttachAmount) {
  6330. undef $AI::temp::mailAttachAmount;
  6331. }
  6332. message TF("Failed to attach %s.\n", ($args->{index}) ? T("item: ").$char->inventory->getByServerIndex($args->{index}) : T("zeny")), "info";
  6333. } else {
  6334. if (($args->{index})) {
  6335. message TF("Succeeded to attach %s.\n", T("item: ").$char->inventory->getByServerIndex($args->{index})), "info";
  6336. if (defined $AI::temp::mailAttachAmount) {
  6337. my $item = $char->inventory->getByServerIndex($args->{index});
  6338. if ($item) {
  6339. my $change = min($item->{amount},$AI::temp::mailAttachAmount);
  6340. inventoryItemRemoved($item->{invIndex}, $change);
  6341. Plugins::callHook('packet_item_removed', {index => $item->{invIndex}});
  6342. }
  6343. undef $AI::temp::mailAttachAmount;
  6344. }
  6345. } else {
  6346. message TF("Succeeded to attach %s.\n", T("zeny")), "info";
  6347. if (defined $AI::temp::mailAttachAmount) {
  6348. my $change = min($char->{zeny},$AI::temp::mailAttachAmount);
  6349. $char->{zeny} = $char->{zeny} - $change;
  6350. message TF("You lost %s zeny.\n", formatNumber($change));
  6351. }
  6352. }
  6353. }
  6354. }
  6355.  
  6356. sub mail_delete {
  6357. my ($self, $args) = @_;
  6358. if ($args->{fail}) {
  6359. message TF("Failed to delete mail with ID: %s.\n", $args->{mailID}), "info";
  6360. }
  6361. else {
  6362. message TF("Succeeded to delete mail with ID: %s.\n", $args->{mailID}), "info";
  6363. }
  6364. }
  6365.  
  6366. sub mail_window {
  6367. my ($self, $args) = @_;
  6368. if ($args->{flag}) {
  6369. message T("Mail window is now closed.\n"), "info";
  6370. }
  6371. else {
  6372. message T("Mail window is now opened.\n"), "info";
  6373. }
  6374. }
  6375.  
  6376. sub mail_return {
  6377. my ($self, $args) = @_;
  6378. ($args->{fail}) ?
  6379. error TF("The mail with ID: %s does not exist.\n", $args->{mailID}), "info" :
  6380. message TF("The mail with ID: %s is returned to the sender.\n", $args->{mailID}), "info";
  6381. }
  6382.  
  6383. # 08CB
  6384. sub rates_info {
  6385. my ($self, $args) = @_;
  6386. my %rates = (
  6387. exp => { total => $args->{exp} },
  6388. death => { total => $args->{death} },
  6389. drop => { total => $args->{drop} },
  6390. );
  6391.  
  6392. # get details
  6393. for (my $offset = 0; $offset < length($args->{detail}); $offset += 7) {
  6394. my ($type, $exp, $death, $drop) = unpack("C s3", substr($args->{detail}, $offset, 7));
  6395. $rates{exp}{$type} = $exp; $rates{death}{$type} = $death; $rates{drop}{$type} = $drop;
  6396. }
  6397.  
  6398. # we have 4 kinds of detail:
  6399. # $rates{exp or drop or death}{DETAIL_KIND}
  6400. # 0 = base server exp (?)
  6401. # 1 = premium acc additional exp
  6402. # 2 = server additional exp
  6403. # 3 = not sure, maybe it's for "extra exp" events? never seen this using the official client (bRO)
  6404. message T("=========================== Server Infos ===========================\n"), "info";
  6405. message TF("EXP Rates: %s\% (Base %s\% + Premium %s\% + Server %s\% + Plus %s\%) \n", $rates{exp}{total}, $rates{exp}{0}, $rates{exp}{1}, $rates{exp}{2}, $rates{exp}{3}), "info";
  6406. message TF("Drop Rates: %s\% (Base %s\% + Premium %s\% + Server %s\% + Plus %s\%) \n", $rates{drop}{total}, $rates{drop}{0}, $rates{drop}{1}, $rates{drop}{2}, $rates{drop}{3}), "info";
  6407. message TF("Death Penalty: %s\% (Base %s\% + Premium %s\% + Server %s\% + Plus %s\%) \n", $rates{death}{total}, $rates{death}{0}, $rates{death}{1}, $rates{death}{2}, $rates{death}{3}), "info";
  6408. message T("=====================================================================\n"), "info";
  6409. }
  6410.  
  6411. sub premium_rates_info {
  6412. my ($self, $args) = @_;
  6413. message TF("Premium rates: exp %+i%%, death %+i%%, drop %+i%%.\n", $args->{exp}, $args->{death}, $args->{drop}), "info";
  6414. }
  6415.  
  6416. sub auction_result {
  6417. my ($self, $args) = @_;
  6418. my $flag = $args->{flag};
  6419.  
  6420. if ($flag == 0) {
  6421. message T("You have failed to bid into the auction.\n"), "info";
  6422. } elsif ($flag == 1) {
  6423. message T("You have successfully bid in the auction.\n"), "info";
  6424. } elsif ($flag == 2) {
  6425. message T("The auction has been canceled.\n"), "info";
  6426. } elsif ($flag == 3) {
  6427. message T("An auction with at least one bidder cannot be canceled.\n"), "info";
  6428. } elsif ($flag == 4) {
  6429. message T("You cannot register more than 5 items in an auction at a time.\n"), "info";
  6430. } elsif ($flag == 5) {
  6431. message T("You do not have enough Zeny to pay the Auction Fee.\n"), "info";
  6432. } elsif ($flag == 6) {
  6433. message T("You have won the auction.\n"), "info";
  6434. } elsif ($flag == 7) {
  6435. message T("You have failed to win the auction.\n"), "info";
  6436. } elsif ($flag == 8) {
  6437. message T("You do not have enough Zeny.\n"), "info";
  6438. } elsif ($flag == 9) {
  6439. message T("You cannot place more than 5 bids at a time.\n"), "info";
  6440. } else {
  6441. warning TF("flag: %s gave unknown results in: %s\n", $args->{flag}, $self->{packet_list}{$args->{switch}}->[0]);
  6442. }
  6443. }
  6444.  
  6445. sub auction_item_request_search {
  6446. my ($self, $args) = @_;
  6447.  
  6448. #$pages = $args->{pages};$size = $args->{size};
  6449. undef $auctionList;
  6450. my $count = $args->{count};
  6451.  
  6452. if (!$count) {
  6453. message T("No item in auction.\n"), "info";
  6454. return;
  6455. }
  6456.  
  6457. message TF("Found %s items in auction.\n", $count), "info";
  6458. my $msg;
  6459. $msg .= center(" " . T("Auction") . " ", 79, '-') . "\n";
  6460. $msg .= swrite(TF("\@%s \@%s \@%s \@%s \@%s", ('>'x2), ('<'x37), ('>'x10), ('>'x10), ('<'x11)),
  6461. ["#", "Item", "High Bid", "Purchase", "End-Date"]);
  6462. $msg .= sprintf("%s\n", ('-'x79));
  6463.  
  6464. my $j = 0;
  6465. for (my $i = 12; $i < 12 + $count * 83; $i += 83) {
  6466. $auctionList->[$j]->{ID} = unpack("V1", substr($args->{RAW_MSG}, $i, 4));
  6467. $auctionList->[$j]->{seller} = bytesToString(unpack("Z24", substr($args->{RAW_MSG}, $i+4, 24)));
  6468. $auctionList->[$j]->{nameID} = unpack("v1", substr($args->{RAW_MSG}, $i+28, 2));
  6469. $auctionList->[$j]->{type} = unpack("v1", substr($args->{RAW_MSG}, $i+30, 2));
  6470. $auctionList->[$j]->{unknown} = unpack("v1", substr($args->{RAW_MSG}, $i+32, 2));
  6471. $auctionList->[$j]->{amount} = unpack("v1", substr($args->{RAW_MSG}, $i+34, 2));
  6472. $auctionList->[$j]->{identified} = unpack("C1", substr($args->{RAW_MSG}, $i+36, 1));
  6473. $auctionList->[$j]->{broken} = unpack("C1", substr($args->{RAW_MSG}, $i+37, 1));
  6474. $auctionList->[$j]->{upgrade} = unpack("C1", substr($args->{RAW_MSG}, $i+38, 1));
  6475. # TODO
  6476. #$auctionList->[$j]->{card}->[0] = unpack("v1", substr($args->{RAW_MSG}, $i+39, 2));
  6477. #$auctionList->[$j]->{card}->[1] = unpack("v1", substr($args->{RAW_MSG}, $i+41, 2));
  6478. #$auctionList->[$j]->{card}->[2] = unpack("v1", substr($args->{RAW_MSG}, $i+43, 2));
  6479. #$auctionList->[$j]->{card}->[3] = unpack("v1", substr($args->{RAW_MSG}, $i+45, 2));
  6480. $auctionList->[$j]->{cards} = unpack("a8", substr($args->{RAW_MSG}, $i+39, 8));
  6481. $auctionList->[$j]->{price} = unpack("V1", substr($args->{RAW_MSG}, $i+47, 4));
  6482. $auctionList->[$j]->{buynow} = unpack("V1", substr($args->{RAW_MSG}, $i+51, 4));
  6483. $auctionList->[$j]->{buyer} = bytesToString(unpack("Z24", substr($args->{RAW_MSG}, $i+55, 24)));
  6484. $auctionList->[$j]->{timestamp} = unpack("V1", substr($args->{RAW_MSG}, $i+79, 4));
  6485.  
  6486. my $item = {};
  6487. $item->{nameID} = $auctionList->[$j]->{nameID};
  6488. $item->{upgrade} = $auctionList->[$j]->{upgrade};
  6489. $item->{cards} = $auctionList->[$j]->{cards};
  6490. $item->{broken} = $auctionList->[$j]->{broken};
  6491. $item->{name} = itemName($item);
  6492.  
  6493. $msg .= swrite(TF("\@%s \@%s \@%s \@%s \@%s", ('>'x2),, ('<'x37), ('>'x10), ('>'x10), ('<'x11)),
  6494. [$j, $item->{name}, formatNumber($auctionList->[$j]->{price}),
  6495. formatNumber($auctionList->[$j]->{buynow}), getFormattedDate(int($auctionList->[$j]->{timestamp}))]);
  6496. $j++;
  6497. }
  6498.  
  6499. $msg .= sprintf("%s\n", ('-'x79));
  6500. message($msg, "list");
  6501. }
  6502.  
  6503. sub auction_my_sell_stop {
  6504. my ($self, $args) = @_;
  6505. my $flag = $args->{flag};
  6506.  
  6507. if ($flag == 0) {
  6508. message T("You have ended the auction.\n"), "info";
  6509. } elsif ($flag == 1) {
  6510. message T("You cannot end the auction.\n"), "info";
  6511. } elsif ($flag == 2) {
  6512. message T("Bid number is incorrect.\n"), "info";
  6513. } else {
  6514. warning TF("flag: %s gave unknown results in: %s\n", $args->{flag}, $self->{packet_list}{$args->{switch}}->[0]);
  6515. }
  6516. }
  6517.  
  6518. sub auction_windows {
  6519. my ($self, $args) = @_;
  6520. if ($args->{flag}) {
  6521. message T("Auction window is now closed.\n"), "info";
  6522. }
  6523. else {
  6524. message T("Auction window is now opened.\n"), "info";
  6525. }
  6526. }
  6527.  
  6528. sub auction_add_item {
  6529. my ($self, $args) = @_;
  6530. if ($args->{fail}) {
  6531. message TF("Failed (note: usable items can't be auctioned) to add item with index: %s.\n", $args->{index}), "info";
  6532. }
  6533. else {
  6534. message TF("Succeeded to add item with index: %s.\n", $args->{index}), "info";
  6535. }
  6536. }
  6537.  
  6538. # this info will be sent to xkore 2 clients
  6539. sub hotkeys {
  6540. my ($self, $args) = @_;
  6541. undef $hotkeyList;
  6542. my $msg;
  6543. $msg .= center(" " . T("Hotkeys") . " ", 79, '-') . "\n";
  6544. $msg .= swrite(sprintf("\@%s \@%s \@%s \@%s", ('>'x3), ('<'x30), ('<'x5), ('>'x3)),
  6545. ["#", T("Name"), T("Type"), T("Lv")]);
  6546. $msg .= sprintf("%s\n", ('-'x79));
  6547. my $j = 0;
  6548. for (my $i = 2; $i < $args->{RAW_MSG_SIZE}; $i+=7) {
  6549. $hotkeyList->[$j]->{type} = unpack("C1", substr($args->{RAW_MSG}, $i, 1));
  6550. $hotkeyList->[$j]->{ID} = unpack("V1", substr($args->{RAW_MSG}, $i+1, 4));
  6551. $hotkeyList->[$j]->{lv} = unpack("v1", substr($args->{RAW_MSG}, $i+5, 2));
  6552.  
  6553. $msg .= swrite(TF("\@%s \@%s \@%s \@%s", ('>'x3), ('<'x30), ('<'x5), ('>'x3)),
  6554. [$j, $hotkeyList->[$j]->{type} ? Skill->new(idn => $hotkeyList->[$j]->{ID})->getName() : itemNameSimple($hotkeyList->[$j]->{ID}),
  6555. $hotkeyList->[$j]->{type} ? T("skill") : T("item"),
  6556. $hotkeyList->[$j]->{lv}]);
  6557. $j++;
  6558. }
  6559. $msg .= sprintf("%s\n", ('-'x79));
  6560. debug($msg, "list");
  6561. }
  6562.  
  6563. sub hack_shield_alarm {
  6564. error T("Error: You have been forced to disconnect by a Hack Shield.\n Please check Poseidon.\n"), "connection";
  6565. Commands::run('relog 100000000');
  6566. }
  6567.  
  6568. sub guild_alliance {
  6569. my ($self, $args) = @_;
  6570. if ($args->{flag} == 0) {
  6571. message T("Already allied.\n"), "info";
  6572. } elsif ($args->{flag} == 1) {
  6573. message T("You rejected the offer.\n"), "info";
  6574. } elsif ($args->{flag} == 2) {
  6575. message T("You accepted the offer.\n"), "info";
  6576. } elsif ($args->{flag} == 3) {
  6577. message T("They have too any alliances\n"), "info";
  6578. } elsif ($args->{flag} == 4) {
  6579. message T("You have too many alliances.\n"), "info";
  6580. } else {
  6581. warning TF("flag: %s gave unknown results in: %s\n", $args->{flag}, $self->{packet_list}{$args->{switch}}->[0]);
  6582. }
  6583. }
  6584.  
  6585. sub talkie_box {
  6586. my ($self, $args) = @_;
  6587. message TF("%s's talkie box message: %s.\n", Actor::get($args->{ID})->nameString(), $args->{message}), "info";
  6588. }
  6589.  
  6590. sub manner_message {
  6591. my ($self, $args) = @_;
  6592. if ($args->{flag} == 0) {
  6593. message T("A manner point has been successfully aligned.\n"), "info";
  6594. } elsif ($args->{flag} == 3) {
  6595. message T("Chat Block has been applied by GM due to your ill-mannerous action.\n"), "info";
  6596. } elsif ($args->{flag} == 4) {
  6597. message T("Automated Chat Block has been applied due to Anti-Spam System.\n"), "info";
  6598. } elsif ($args->{flag} == 5) {
  6599. message T("You got a good point.\n"), "info";
  6600. } else {
  6601. warning TF("flag: %s gave unknown results in: %s\n", $args->{flag}, $self->{packet_list}{$args->{switch}}->[0]);
  6602. }
  6603. }
  6604.  
  6605. sub GM_silence {
  6606. my ($self, $args) = @_;
  6607. if ($args->{flag}) {
  6608. message TF("You have been: muted by %s.\n", bytesToString($args->{name})), "info";
  6609. }
  6610. else {
  6611. message TF("You have been: unmuted by %s.\n", bytesToString($args->{name})), "info";
  6612. }
  6613. }
  6614.  
  6615. # TODO test if we must use ID to know if the packets are meant for us.
  6616. # ID is monsterID
  6617. sub taekwon_packets {
  6618. my ($self, $args) = @_;
  6619. my $string = ($args->{value} == 1) ? T("Sun") : ($args->{value} == 2) ? T("Moon") : ($args->{value} == 3) ? T("Stars") : TF("Unknown (%d)", $args->{value});
  6620. if ($args->{flag} == 0) { # Info about Star Gladiator save map: Map registered
  6621. message TF("You have now marked: %s as Place of the %s.\n", bytesToString($args->{name}), $string), "info";
  6622. } elsif ($args->{flag} == 1) { # Info about Star Gladiator save map: Information
  6623. message TF("%s is marked as Place of the %s.\n", bytesToString($args->{name}), $string), "info";
  6624. } elsif ($args->{flag} == 10) { # Info about Star Gladiator hate mob: Register mob
  6625. message TF("You have now marked %s as Target of the %s.\n", bytesToString($args->{name}), $string), "info";
  6626. } elsif ($args->{flag} == 11) { # Info about Star Gladiator hate mob: Information
  6627. message TF("%s is marked as Target of the %s.\n", bytesToString($args->{name}), $string);
  6628. } elsif ($args->{flag} == 20) { #Info about TaeKwon Do TK_MISSION mob
  6629. message TF("[TaeKwon Mission] Target Monster : %s (%d%)"."\n", bytesToString($args->{name}), $args->{value}), "info";
  6630. } elsif ($args->{flag} == 30) { #Feel/Hate reset
  6631. message T("Your Hate and Feel targets have been resetted.\n"), "info";
  6632. } else {
  6633. warning TF("flag: %s gave unknown results in: %s\n", $args->{flag}, $self->{packet_list}{$args->{switch}}->[0]);
  6634. }
  6635. }
  6636.  
  6637. sub guild_master_member {
  6638. my ($self, $args) = @_;
  6639. if ($args->{type} == 0xd7) {
  6640. } elsif ($args->{type} == 0x57) {
  6641. message T("You are not a guildmaster.\n"), "info";
  6642. return;
  6643. } else {
  6644. warning TF("type: %s gave unknown results in: %s\n", $args->{type}, $self->{packet_list}{$args->{switch}}->[0]);
  6645. return;
  6646. }
  6647. message T("You are a guildmaster.\n"), "info";
  6648. }
  6649.  
  6650. # 0151
  6651. # TODO
  6652. sub guild_emblem_img {
  6653. my ($self, $args) = @_;
  6654. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6655. }
  6656.  
  6657. # 0152
  6658. # TODO
  6659. sub guild_emblem {
  6660. my ($self, $args) = @_;
  6661. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6662. }
  6663.  
  6664. # 0156
  6665. # TODO
  6666. sub guild_member_position_changed {
  6667. my ($self, $args) = @_;
  6668. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6669. }
  6670.  
  6671. # 01B4
  6672. # TODO
  6673. sub guild_emblem_update {
  6674. my ($self, $args) = @_;
  6675. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6676. }
  6677.  
  6678. # 0174
  6679. # TODO
  6680. sub guild_position_changed {
  6681. my ($self, $args) = @_;
  6682. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6683. }
  6684.  
  6685. # 0184
  6686. # TODO
  6687. sub guild_unally {
  6688. my ($self, $args) = @_;
  6689. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6690. }
  6691.  
  6692. # 0181
  6693. # TODO
  6694. sub guild_opposition_result {
  6695. my ($self, $args) = @_;
  6696. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6697. }
  6698.  
  6699. # 0185
  6700. # TODO: this packet doesn't exist in eA
  6701. sub guild_alliance_added {
  6702. my ($self, $args) = @_;
  6703. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6704. }
  6705.  
  6706. # 0192
  6707. # TODO: add actual functionality, maybe alter field?
  6708. sub map_change_cell {
  6709. my ($self, $args) = @_;
  6710. debug "Cell on ($args->{x}, $args->{y}) has been changed to $args->{type} on $args->{map_name}\n", "info";
  6711. }
  6712.  
  6713. # 01D1
  6714. sub blade_stop {
  6715. my ($self, $args) = @_;
  6716. if($args->{active} == 0) {
  6717. message TF("Blade Stop by %s on %s is deactivated.\n", Actor::get($args->{sourceID})->nameString(), Actor::get($args->{targetID})->nameString()), "info";
  6718. } elsif($args->{active} == 1) {
  6719. message TF("Blade Stop by %s on %s is active.\n", Actor::get($args->{sourceID})->nameString(), Actor::get($args->{targetID})->nameString()), "info";
  6720. }
  6721. }
  6722.  
  6723. sub divorced {
  6724. my ($self, $args) = @_;
  6725. message TF("%s and %s have divorced from each other.\n", $char->{name}, $args->{name}), "info"; # is it $char->{name} or is this packet also used for other players?
  6726. }
  6727.  
  6728. # 0221
  6729. # TODO -> Check If we use correct unpack string
  6730. sub upgrade_list {
  6731. my ($self, $args) = @_;
  6732. my $msg;
  6733. $msg .= center(" " . T("Upgrade List") . " ", 79, '-') . "\n";
  6734. for (my $i = 4; $i < $args->{RAW_MSG_SIZE}; $i += 13) {
  6735. my ($index, $nameID) = unpack('v x6 C', substr($args->{RAW_MSG}, $i, 13));
  6736. my $item = $char->inventory->getByServerIndex($index);
  6737. $msg .= swrite(sprintf("\@%s \@%s", ('>'x2), ('<'x50)), [$item->{invIndex}, itemName($item)]);
  6738. }
  6739. $msg .= sprintf("%s\n", ('-'x79));
  6740. message($msg, "list");
  6741. }
  6742.  
  6743. # 0223
  6744. sub upgrade_message {
  6745. my ($self, $args) = @_;
  6746. if($args->{type} == 0) { # Success
  6747. message TF("Weapon upgraded: %s\n", itemName(Actor::Item::get($args->{nameID}))), "info";
  6748. } elsif($args->{type} == 1) { # Fail
  6749. message TF("Weapon not upgraded: %s\n", itemName(Actor::Item::get($args->{nameID}))), "info";
  6750. # message TF("Weapon upgraded: %s\n", itemName(Actor::Item::get($args->{nameID}))), "info";
  6751. } elsif($args->{type} == 2) { # Fail Lvl
  6752. message TF("Cannot upgrade %s until you level up the upgrade weapon skill.\n", itemName(Actor::Item::get($args->{nameID}))), "info";
  6753. } elsif($args->{type} == 3) { # Fail Item
  6754. message TF("You lack item %s to upgrade the weapon.\n", itemNameSimple($args->{nameID})), "info";
  6755. }
  6756. }
  6757.  
  6758. # 025A
  6759. # TODO
  6760. sub cooking_list {
  6761. my ($self, $args) = @_;
  6762. undef $cookingList;
  6763. my $k = 0;
  6764. my $msg;
  6765. $msg .= center(" " . T("Cooking List") . " ", 79, '-') . "\n";
  6766. for (my $i = 6; $i < $args->{RAW_MSG_SIZE}; $i += 2) {
  6767. my $nameID = unpack('v', substr($args->{RAW_MSG}, $i, 2));
  6768. $cookingList->[$k] = $nameID;
  6769. $msg .= swrite(sprintf("\@%s \@%s", ('>'x2), ('<'x50)), [$k, itemNameSimple($nameID)]);
  6770. $k++;
  6771. }
  6772. $msg .= sprintf("%s\n", ('-'x79));
  6773. message($msg, "list");
  6774. message T("You can now use the 'cook' command.\n"), "info";
  6775. }
  6776.  
  6777. sub party_show_picker {
  6778. my ($self, $args) = @_;
  6779.  
  6780. # wtf the server sends this packet for your own character? (rRo)
  6781. return if $args->{sourceID} eq $accountID;
  6782.  
  6783. my $string = ($char->{party}{users}{$args->{sourceID}} && %{$char->{party}{users}{$args->{sourceID}}}) ? $char->{party}{users}{$args->{sourceID}}->name() : $args->{sourceID};
  6784. my $item = {};
  6785. $item->{nameID} = $args->{nameID};
  6786. $item->{identified} = $args->{identified};
  6787. $item->{upgrade} = $args->{upgrade};
  6788. $item->{cards} = $args->{cards};
  6789. $item->{broken} = $args->{broken};
  6790. message TF("Party member %s has picked up item %s.\n", $string, itemName($item)), "info";
  6791. }
  6792.  
  6793. # 02CB
  6794. # TODO
  6795. # Required to start the instancing information window on Client
  6796. # This window re-appear each "refresh" of client automatically until 02CD is send to client.
  6797. sub instance_window_start {
  6798. my ($self, $args) = @_;
  6799. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6800. }
  6801.  
  6802. # 02CC
  6803. # TODO
  6804. # To announce Instancing queue creation if no maps available
  6805. sub instance_window_queue {
  6806. my ($self, $args) = @_;
  6807. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6808. }
  6809.  
  6810. # 02CD
  6811. # TODO
  6812. sub instance_window_join {
  6813. my ($self, $args) = @_;
  6814. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6815. }
  6816.  
  6817. # 02CE
  6818. #0 = "The Memorial Dungeon reservation has been canceled."
  6819. # Re-innit Window, in some rare cases.
  6820. #1 = "The Memorial Dungeon expired; it has been destroyed."
  6821. #2 = "The Memorial Dungeon's entry time limit expired; it has been destroyed."
  6822. #3 = "The Memorial Dungeon has been removed."
  6823. #4 = "A system error has occurred in the Memorial Dungeon. Please relog in to the game to continue playing."
  6824. # Just remove the window, maybe party/guild leave.
  6825. # TODO: test if correct message displays, no type == 0 ?
  6826. sub instance_window_leave {
  6827. my ($self, $args) = @_;
  6828. # TYPE_NOTIFY = 0x0; Ihis one will make Window, as Client logic do.
  6829. if($args->{flag} == 1) { # TYPE_DESTROY_LIVE_TIMEOUT = 0x1
  6830. message T("The Memorial Dungeon expired it has been destroyed.\n"), "info";
  6831. } elsif($args->{flag} == 2) { # TYPE_DESTROY_ENTER_TIMEOUT = 0x2
  6832. message T("The Memorial Dungeon's entry time limit expired it has been destroyed.\n"), "info";
  6833. } elsif($args->{flag} == 3) { # TYPE_DESTROY_USER_REQUEST = 0x3
  6834. message T("The Memorial Dungeon has been removed.\n"), "info";
  6835. } elsif ($args->{flag} == 4) { # TYPE_CREATE_FAIL = 0x4
  6836. message T("The instance windows has been removed, possibly due to party/guild leave.\n"), "info";
  6837. } else {
  6838. warning TF("flag: %s gave unknown results in: %s\n", $args->{flag}, $self->{packet_list}{$args->{switch}}->[0]);
  6839. }
  6840. }
  6841.  
  6842. # 02DC
  6843. # TODO
  6844. sub battleground_message {
  6845. my ($self, $args) = @_;
  6846. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6847. }
  6848.  
  6849. # 02DD
  6850. # TODO
  6851. sub battleground_emblem {
  6852. my ($self, $args) = @_;
  6853. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6854. }
  6855.  
  6856. sub battleground_score {
  6857. my ($self, $args) = @_;
  6858. message TF("Battleground score - Lions: '%d' VS Eagles: '%d'\n", $args->{score_lion}, $args->{score_eagle}), "info";
  6859. }
  6860.  
  6861. sub battleground_position {
  6862. my ($self, $args) = @_;
  6863. }
  6864.  
  6865. sub battleground_hp {
  6866. my ($self, $args) = @_;
  6867. }
  6868.  
  6869. # 02EF
  6870. # TODO
  6871. sub font {
  6872. my ($self, $args) = @_;
  6873. debug "Account: $args->{ID} is using fontID: $args->{fontID}\n", "info";
  6874. }
  6875.  
  6876. # 01D3
  6877. # TODO
  6878. sub sound_effect {
  6879. my ($self, $args) = @_;
  6880. # $args->{type} seems like 0 => once, 1 => start, 2 => stop
  6881. # $args->{term} seems like duration or repeat count
  6882. # continuous sound effects can be implemented as actor statuses
  6883.  
  6884. my $actor = exists $args->{ID} && Actor::get($args->{ID});
  6885. message sprintf(
  6886. $actor
  6887. ? $args->{type} == 0
  6888. ? $actor->verb(T("%2\$s play: %s\n"), T("%2\$s plays: %s\n"))
  6889. : $args->{type} == 1
  6890. ? $actor->verb(T("%2\$s are now playing: %s\n"), T("%2\$s is now playing: %s\n"))
  6891. : $actor->verb(T("%2\$s stopped playing: %s\n"), T("%2\$s stopped playing: %s\n"))
  6892. : T("Now playing: %s\n"),
  6893. $args->{name}, $actor), 'effect'
  6894. }
  6895.  
  6896. # 019E
  6897. # TODO
  6898. # note: this is probably the trigger for the client's slotmachine effect or so.
  6899. sub pet_capture_process {
  6900. my ($self, $args) = @_;
  6901. message T("Attempting to capture pet (slot machine).\n"), "info";
  6902. }
  6903.  
  6904. # 0294
  6905. # TODO -> maybe add table file?
  6906. sub book_read {
  6907. my ($self, $args) = @_;
  6908. debug "Reading book: $args->{bookID} page: $args->{page}\n", "info";
  6909. }
  6910.  
  6911. # TODO can we use itemName($actor)? -> tech: don't think so because it seems that this packet is received before the inventory list
  6912. sub rental_time {
  6913. my ($self, $args) = @_;
  6914. message TF("The '%s' item will disappear in %d minutes.\n", itemNameSimple($args->{nameID}), $args->{seconds}/60), "info";
  6915. }
  6916.  
  6917. # TODO can we use itemName($actor)? -> tech: don't think so because the item might be removed from inventory before this packet is sent -> untested
  6918. sub rental_expired {
  6919. my ($self, $args) = @_;
  6920. message TF("Rental item '%s' has expired!\n", itemNameSimple($args->{nameID})), "info";
  6921. }
  6922.  
  6923. # 0289
  6924. # TODO
  6925. sub cash_buy_fail {
  6926. my ($self, $args) = @_;
  6927. debug "cash_buy_fail $args->{cash_points} $args->{kafra_points} $args->{fail}\n";
  6928. }
  6929.  
  6930. sub adopt_reply {
  6931. my ($self, $args) = @_;
  6932. if($args->{type} == 0) {
  6933. message T("You cannot adopt more than 1 child.\n"), "info";
  6934. } elsif($args->{type} == 1) {
  6935. message T("You must be at least character level 70 in order to adopt someone.\n"), "info";
  6936. } elsif($args->{type} == 2) {
  6937. message T("You cannot adopt a married person.\n"), "info";
  6938. }
  6939. }
  6940.  
  6941. # TODO do something with sourceID, targetID? -> tech: maybe your spouses adopt_request will also display this message for you.
  6942. sub adopt_request {
  6943. my ($self, $args) = @_;
  6944. message TF("%s wishes to adopt you. Do you accept?\n", $args->{name}), "info";
  6945. # TODO how to accept?
  6946. }
  6947.  
  6948. # 0293
  6949. sub boss_map_info {
  6950. my ($self, $args) = @_;
  6951.  
  6952. if ($args->{flag} == 0) {
  6953. message T("You cannot find any trace of a Boss Monster in this area.\n"), "info";
  6954. } elsif ($args->{flag} == 1) {
  6955. message TF("MVP Boss %s is now on location: (%d, %d)\n", $args->{name}, $args->{x}, $args->{y}), "info";
  6956. } elsif ($args->{flag} == 2) {
  6957. message TF("MVP Boss %s has been detected on this map!\n", $args->{name}), "info";
  6958. } elsif ($args->{flag} == 3) {
  6959. message TF("MVP Boss %s is dead, but will spawn again in %d hour(s) and %d minutes(s).\n", $args->{name}, $args->{hours}, $args->{minutes}), "info";
  6960. } else {
  6961. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  6962. warning TF("flag: %s gave unknown results in: %s\n", $args->{flag}, $self->{packet_list}{$args->{switch}}->[0]);
  6963. }
  6964. }
  6965.  
  6966. # 02B1
  6967. sub quest_all_list {
  6968. my ($self, $args) = @_;
  6969. $questList = {};
  6970. for (my $i = 8; $i < $args->{amount}*5+8; $i += 5) {
  6971. my ($questID, $active) = unpack('V C', substr($args->{RAW_MSG}, $i, 5));
  6972. $questList->{$questID}->{active} = $active;
  6973. debug "$questID $active\n", "info";
  6974. }
  6975. }
  6976.  
  6977. # 02B2
  6978. # note: this packet shows all quests + their missions and has variable length
  6979. sub quest_all_mission {
  6980. my ($self, $args) = @_;
  6981. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
  6982. for (my $i = 8; $i < $args->{amount}*104+8; $i += 104) {
  6983. my ($questID, $time_start, $time, $mission_amount) = unpack('V3 v', substr($args->{RAW_MSG}, $i, 14));
  6984. my $quest = \%{$questList->{$questID}};
  6985. $quest->{time_start} = $time_start;
  6986. $quest->{time} = $time;
  6987. debug "$questID $time_start $time $mission_amount\n", "info";
  6988. for (my $j = 0; $j < $mission_amount; $j++) {
  6989. my ($mobID, $count, $mobName) = unpack('V v Z24', substr($args->{RAW_MSG}, 14+$i+$j*30, 30));
  6990. my $mission = \%{$quest->{missions}->{$mobID}};
  6991. $mission->{mobID} = $mobID;
  6992. $mission->{count} = $count;
  6993. $mission->{mobName} = bytesToString($mobName);
  6994. debug "- $mobID $count $mobName\n", "info";
  6995. }
  6996. }
  6997. }
  6998.  
  6999. # 02B3
  7000. # note: this packet shows all missions for 1 quest and has fixed length
  7001. sub quest_add {
  7002. my ($self, $args) = @_;
  7003. my $questID = $args->{questID};
  7004. my $quest = \%{$questList->{$questID}};
  7005.  
  7006. unless (%$quest) {
  7007. message TF("Quest: %s has been added.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
  7008. }
  7009.  
  7010. $quest->{time_start} = $args->{time_start};
  7011. $quest->{time} = $args->{time};
  7012. $quest->{active} = $args->{active};
  7013. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) ."\n";
  7014. for (my $i = 0; $i < $args->{amount}; $i++) {
  7015. my ($mobID, $count, $mobName) = unpack('V v Z24', substr($args->{RAW_MSG}, 17+$i*30, 30));
  7016. my $mission = \%{$quest->{missions}->{$mobID}};
  7017. $mission->{mobID} = $mobID;
  7018. $mission->{count} = $count;
  7019. $mission->{mobName} = bytesToString($mobName);
  7020. debug "- $mobID $count $mobName\n", "info";
  7021. }
  7022. }
  7023.  
  7024. # 02B4
  7025. sub quest_delete {
  7026. my ($self, $args) = @_;
  7027. my $questID = $args->{questID};
  7028. message TF("Quest: %s has been deleted.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID), "info";
  7029. delete $questList->{$questID};
  7030. }
  7031.  
  7032. sub parse_quest_update_mission_hunt {
  7033. my ($self, $args) = @_;
  7034. @{$args->{mobs}} = map {
  7035. my %result; @result{qw(questID mobID count)} = unpack 'V2 v', $_; \%result
  7036. } unpack '(a10)*', $args->{mobInfo};
  7037. }
  7038.  
  7039. sub reconstruct_quest_update_mission_hunt {
  7040. my ($self, $args) = @_;
  7041. $args->{mobInfo} = pack '(a10)*', map { pack 'V2 v', @{$_}{qw(questID mobID count)} } @{$args->{mobs}};
  7042. }
  7043.  
  7044. sub parse_quest_update_mission_hunt_v2 {
  7045. my ($self, $args) = @_;
  7046. @{$args->{mobs}} = map {
  7047. my %result; @result{qw(questID mobID goal count)} = unpack 'V2 v2', $_; \%result
  7048. } unpack '(a12)*', $args->{mobInfo};
  7049. }
  7050.  
  7051. sub reconstruct_quest_update_mission_hunt_v2 {
  7052. my ($self, $args) = @_;
  7053. $args->{mobInfo} = pack '(a12)*', map { pack 'V2 v2', @{$_}{qw(questID mobID goal count)} } @{$args->{mobs}};
  7054. }
  7055.  
  7056. # 02B5
  7057. # note: this packet updates the objectives counters
  7058. sub quest_update_mission_hunt {
  7059. my ($self, $args) = @_;
  7060. for my $mob (@{$args->{mobs}}) {
  7061. @{$questList->{$mob->{questID}}{missions}{$mob->{mobID}}}{@$_} = @{$mob}{@$_} for [qw(mobID goal count)];
  7062. }
  7063. }
  7064.  
  7065. # 02B7
  7066. sub quest_active {
  7067. my ($self, $args) = @_;
  7068. my $questID = $args->{questID};
  7069.  
  7070. message $args->{active}
  7071. ? TF("Quest %s is now active.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
  7072. : TF("Quest %s is now inactive.\n", $quests_lut{$questID} ? "$quests_lut{$questID}{title} ($questID)" : $questID)
  7073. , "info";
  7074.  
  7075. $questList->{$args->{questID}}->{active} = $args->{active};
  7076. }
  7077.  
  7078. # 018B
  7079. sub quit_response {
  7080. my ($self, $args) = @_;
  7081. if ($args->{fail}) { # NOTDISCONNECTABLE_STATE = 0x1
  7082. error T("Please wait 10 seconds before trying to log out.\n"); # MSI_CANT_EXIT_NOW = 0x1f6
  7083. } else { # DISCONNECTABLE_STATE = 0x0
  7084. message T("Logged out from the server succesfully.\n"), "success";
  7085. }
  7086. }
  7087.  
  7088. sub GM_req_acc_name {
  7089. my ($self, $args) = @_;
  7090. message TF("The accountName for ID %s is %s.\n", $args->{targetID}, $args->{accountName}), "info";
  7091. }
  7092.  
  7093. # 00CB
  7094. sub sell_result {
  7095. my ($self, $args) = @_;
  7096. if ($args->{fail}) {
  7097. error T("Sell failed.\n");
  7098. } else {
  7099. message T("Sell completed.\n"), "success";
  7100. }
  7101. }
  7102.  
  7103. # 018B
  7104. sub quit_response {
  7105. my ($self, $args) = @_;
  7106. if ($args->{fail}) { # NOTDISCONNECTABLE_STATE = 0x1
  7107. error T("Please wait 10 seconds before trying to log out.\n"); # MSI_CANT_EXIT_NOW = 0x1f6
  7108. } else { # DISCONNECTABLE_STATE = 0x0
  7109. message T("Logged out from the server succesfully.\n"), "success";
  7110. }
  7111. }
  7112.  
  7113. # 00B3
  7114. # TODO: add real client messages and logic?
  7115. # ClientLogic: LoginStartMode = 5; ShowLoginScreen;
  7116. sub switch_character {
  7117. my ($self, $args) = @_;
  7118. # User is switching characters in X-Kore
  7119. $net->setState(Network::CONNECTED_TO_MASTER_SERVER);
  7120. $net->serverDisconnect();
  7121.  
  7122. # FIXME better support for multiple received_characters packets
  7123. undef @chars;
  7124.  
  7125. debug "result: $args->{result}\n";
  7126. }
  7127.  
  7128. use constant {
  7129. EXP_FROM_BATTLE => 0x0,
  7130. EXP_FROM_QUEST => 0x1,
  7131. };
  7132.  
  7133. # 07F6 (exp) doesn't change any exp information because 00B1 (exp_zeny_info) is always sent with it
  7134. # r7643 - copy-pasted to RagexeRE_2009_10_27a.pm
  7135. sub exp {
  7136. my ($self, $args) = @_;
  7137.  
  7138. my $max = {VAR_EXP, $char->{exp_max}, VAR_JOBEXP, $char->{exp_job_max}}->{$args->{type}};
  7139. $args->{percent} = $max ? $args->{val} / $max * 100 : 0;
  7140.  
  7141. if ($args->{flag} == EXP_FROM_BATTLE) {
  7142. if ($args->{type} == VAR_EXP) {
  7143. message TF("Base Exp gained: %d (%.2f%%)\n", @{$args}{qw(val percent)}), 'exp2', 2;
  7144. } elsif ($args->{type} == VAR_JOBEXP) {
  7145. message TF("Job Exp gained: %d (%.2f%%)\n", @{$args}{qw(val percent)}), 'exp2', 2;
  7146. } else {
  7147. message TF("Unknown (type=%d) Exp gained: %d\n", @{$args}{qw(type val)}), 'exp2', 2;
  7148. }
  7149. } elsif ($args->{flag} == EXP_FROM_QUEST) {
  7150. if ($args->{type} == VAR_EXP) {
  7151. message TF("Base Quest Exp gained: %d (%.2f%%)\n", @{$args}{qw(val percent)}), 'exp2', 2;
  7152. } elsif ($args->{type} == VAR_JOBEXP) {
  7153. message TF("Job Quest Exp gained: %d (%.2f%%)\n", @{$args}{qw(val percent)}), 'exp2', 2;
  7154. } else {
  7155. message TF("Unknown (type=%d) Quest Exp gained: %d\n", @{$args}{qw(type val)}), 'exp2', 2;
  7156. }
  7157. } else {
  7158. if ($args->{type} == VAR_EXP) {
  7159. message TF("Base Unknown (flag=%d) Exp gained: %d (%.2f%%)\n", @{$args}{qw(flag val percent)}), 'exp2', 2;
  7160. } elsif ($args->{type} == VAR_JOBEXP) {
  7161. message TF("Job Unknown (flag=%d) Exp gained: %d (%.2f%%)\n", @{$args}{qw(flag val percent)}), 'exp2', 2;
  7162. } else {
  7163. message TF("Unknown (type=%d) Unknown (flag=%d) Exp gained: %d\n", @{$args}{qw(type flag val)}), 'exp2', 2;
  7164. }
  7165. }
  7166. }
  7167.  
  7168. # captcha packets from kRO::RagexeRE_2009_09_22a
  7169.  
  7170. # 0x07e8,-1
  7171. # todo: debug + remove debug message
  7172. sub captcha_image {
  7173. my ($self, $args) = @_;
  7174. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  7175.  
  7176. my $hookArgs = {image => $args->{image}};
  7177. Plugins::callHook ('captcha_image', $hookArgs);
  7178. return 1 if $hookArgs->{return};
  7179.  
  7180. my $file = $Settings::logs_folder . "/captcha.bmp";
  7181. open my $DUMP, '>', $file;
  7182. print $DUMP $args->{image};
  7183. close $DUMP;
  7184.  
  7185. $hookArgs = {file => $file};
  7186. Plugins::callHook ('captcha_file', $hookArgs);
  7187. return 1 if $hookArgs->{return};
  7188.  
  7189. warning "captcha.bmp has been saved to: " . $Settings::logs_folder . ", open it, solve it and use the command: captcha <text>\n";
  7190. }
  7191.  
  7192. # 0x07e9,5
  7193. # todo: debug + remove debug message
  7194. sub captcha_answer {
  7195. my ($self, $args) = @_;
  7196. debug $self->{packet_list}{$args->{switch}}->[0] . " " . join(', ', @{$args}{@{$self->{packet_list}{$args->{switch}}->[2]}}) . "\n";
  7197. debug ($args->{flag} ? "good" : "bad") . " answer\n";
  7198. $captcha_state = $args->{flag};
  7199.  
  7200. Plugins::callHook ('captcha_answer', {flag => $args->{flag}});
  7201. }
  7202.  
  7203. use constant {
  7204. TYPE_BOXITEM => 0x0,
  7205. TYPE_MONSTER_ITEM => 0x1,
  7206. };
  7207.  
  7208. # TODO: more meaningful messages?
  7209. sub special_item_obtain {
  7210. my ($self, $args) = @_;
  7211.  
  7212. my $item_name = itemNameSimple($args->{nameID});
  7213. my $holder = bytesToString($args->{holder});
  7214. stripLanguageCode(\$holder);
  7215. if ($args->{type} == TYPE_BOXITEM) {
  7216. @{$args}{qw(box_nameID)} = unpack 'c/v', $args->{etc};
  7217.  
  7218. my $box_item_name = itemNameSimple($args->{box_nameID});
  7219. chatLog("GM", "$holder has got $item_name from $box_item_name\n") if ($config{logSystemChat});
  7220. message TF("%s has got %s from %s.\n", $holder, $item_name, $box_item_name), 'schat';
  7221.  
  7222. } elsif ($args->{type} == TYPE_MONSTER_ITEM) {
  7223. @{$args}{qw(len monster_name)} = unpack 'c Z*', $args->{etc};
  7224. my $monster_name = bytesToString($args->{monster_name});
  7225. stripLanguageCode(\$monster_name);
  7226. chatLog("GM", "$holder has got $item_name from $monster_name\n") if ($config{logSystemChat});
  7227. message TF("%s has got %s from %s.\n", $holder, $item_name, $monster_name), 'schat';
  7228.  
  7229. } else {
  7230. warning TF("%s has got %s (from Unknown type %d).\n", $holder, $item_name, $args->{type}), 'schat';
  7231. }
  7232. }
  7233.  
  7234. # TODO
  7235. sub buyer_items
  7236. {
  7237. my($self, $args) = @_;
  7238.  
  7239. my $BinaryID = $args->{venderID};
  7240. my $Player = Actor::get($BinaryID);
  7241. my $Name = $Player->name;
  7242.  
  7243. my $headerlen = 12;
  7244. my $Total = unpack('V4', substr($args->{msg}, $headerlen, 4));
  7245. $headerlen += 4;
  7246.  
  7247. for (my $i = $headerlen; $i < $args->{msg_size}; $i+=9)
  7248. {
  7249. my $Item = {};
  7250.  
  7251. ($Item->{price},
  7252. $Item->{amount},
  7253. undef,
  7254. $Item->{nameID}) = unpack('V v C v', substr($args->{msg}, $i, 9));
  7255. }
  7256. }
  7257.  
  7258. sub progress_bar {
  7259. my($self, $args) = @_;
  7260. message TF("Progress bar loading (time: %d).\n", $args->{time}), 'info';
  7261. $taskManager->add(
  7262. new Task::Chained(tasks => [new Task::Wait(seconds => $args->{time}),
  7263. new Task::Function(function => sub {
  7264. $messageSender->sendProgress();
  7265. message TF("Progress bar finished.\n"), 'info';
  7266. $_[0]->setDone;
  7267. })]));
  7268. }
  7269.  
  7270. sub progress_bar_stop {
  7271. my($self, $args) = @_;
  7272. message TF("Progress bar finished.\n", 'info');
  7273. }
  7274.  
  7275. sub open_buying_store { #0x810
  7276. my($self, $args) = @_;
  7277. my $amount = $args->{amount};
  7278. message TF("Your buying store can buy %d items \n", $amount);
  7279. }
  7280.  
  7281. sub open_buying_store_fail { #0x812
  7282. my ($self, $args) = @_;
  7283. my $result = $args->{result};
  7284. if($result == 1){
  7285. message TF("Failed to open Purchasing Store.\n"),"info";
  7286. } elsif ($result == 2){
  7287. message TF("The total weight of the item exceeds your weight limit. Please reconfigure.\n"), "info";
  7288. } elsif ($result == 8){
  7289. message TF("Shop information is incorrect and cannot be opened.\n"), "info";
  7290. } else {
  7291. message TF("Failed opening your buying store.\n");
  7292. }
  7293. }
  7294.  
  7295. sub open_buying_store_item_list {
  7296. my ($self, $args) = @_;
  7297.  
  7298. my $msg = $args->{RAW_MSG};
  7299. my $msg_size = $args->{RAW_MSG_SIZE};
  7300. my $headerlen = 12;
  7301.  
  7302. undef @selfBuyerItemList;
  7303.  
  7304. #started a shop.
  7305. message TF("Buying Shop opened!\n"), "BuyShop";
  7306. @articles = ();
  7307. $articles = 0;
  7308. my $index = 0;
  7309.  
  7310. message TF("%s\n" .
  7311. "# Name Type Amount Price\n",
  7312. center(' Buyer Shop ', 79-7, '-')), "list";
  7313. for (my $i = $headerlen; $i < $msg_size; $i += 9) {
  7314. my $item = {};
  7315.  
  7316. ($item->{price},
  7317. $item->{amount},
  7318. $item->{type},
  7319. $item->{nameID}) = unpack('V v C v', substr($msg, $i, 9));
  7320.  
  7321. $item->{name} = itemName($item);
  7322. $selfBuyerItemList[$index] = $item;
  7323.  
  7324. Plugins::callHook('packet_open_buying_store', {
  7325. name => $item->{name},
  7326. amount => $item->{amount},
  7327. price => $item->{price},
  7328. type => $item->{type}
  7329. });
  7330.  
  7331. message(swrite(
  7332. "@<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<< @>>>>> @>>>>>>>>>z",
  7333. [$index, $item->{name}, $itemTypes_lut{$item->{type}}, $item->{amount}, formatNumber($item->{price})]),
  7334. "list");
  7335.  
  7336. $index++;
  7337. }
  7338. }
  7339.  
  7340. sub buying_store_found {
  7341. my ($self, $args) = @_;
  7342. my $ID = $args->{ID};
  7343.  
  7344. if (!$buyerLists{$ID} || !%{$buyerLists{$ID}}) {
  7345. binAdd(\@buyerListsID, $ID);
  7346. Plugins::callHook('packet_buying', {ID => unpack 'V', $ID});
  7347. }
  7348. $buyerLists{$ID}{title} = bytesToString($args->{title});
  7349. $buyerLists{$ID}{id} = $ID;
  7350. }
  7351.  
  7352. sub buying_store_lost {
  7353. my ($self, $args) = @_;
  7354.  
  7355. my $ID = $args->{ID};
  7356. binRemove(\@buyerListsID, $ID);
  7357. delete $buyerLists{$ID};
  7358. }
  7359.  
  7360. sub buying_store_items_list {
  7361. my($self, $args) = @_;
  7362.  
  7363. my $msg = $args->{RAW_MSG};
  7364. my $msg_size = $args->{RAW_MSG_SIZE};
  7365. my $headerlen = 16;
  7366. undef @buyerItemList;
  7367. undef $buyerID;
  7368. undef $buyingStoreID;
  7369. $buyerID = $args->{buyerID};
  7370. $buyingStoreID = $args->{buyingStoreID};
  7371. my $player = Actor::get($buyerID);
  7372. my $index = 0;
  7373.  
  7374. message TF("%s\n" .
  7375. "# Name Type Amount Price\n",
  7376. center(' Buyer: ' . $player->nameIdx . ' ', 79-7, '-')), "list";
  7377.  
  7378. for (my $i = $headerlen; $i < $args->{RAW_MSG_SIZE}; $i+=9) {
  7379. my $item = {};
  7380.  
  7381. ($item->{price},
  7382. $item->{amount},
  7383. $item->{type},
  7384. $item->{nameID}) = unpack('V v C v', substr($args->{RAW_MSG}, $i, 9));
  7385.  
  7386. $item->{name} = itemName($item);
  7387. $buyerItemList[$index] = $item;
  7388.  
  7389. debug("Item added to Buying Store: $item->{name} - $item->{price} z\n", "buying_store", 2);
  7390.  
  7391. Plugins::callHook('packet_buying_store', {
  7392. buyerID => $buyerID,
  7393. number => $index,
  7394. name => $item->{name},
  7395. amount => $item->{amount},
  7396. price => $item->{price},
  7397. type => $item->{type}
  7398. });
  7399.  
  7400. message(swrite(
  7401. "@<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<< @>>>>> @>>>>>>>>>z",
  7402. [$index, $item->{name}, $itemTypes_lut{$item->{type}}, $item->{amount}, formatNumber($item->{price})]),
  7403. "list");
  7404.  
  7405. $index++;
  7406. }
  7407. message("-------------------------------------------------------------------------------\n", "list");
  7408.  
  7409. Plugins::callHook('packet_buying_store2', {
  7410. venderID => $buyerID,
  7411. itemList => \@buyerItemList
  7412. });
  7413. }
  7414.  
  7415. sub buying_store_item_delete {
  7416. my($self, $args) = @_;
  7417. return unless changeToInGameState();
  7418. my $item = $char->inventory->getByServerIndex($args->{index});
  7419. if ($item) {
  7420. buyingstoreitemdelete($item->{invIndex}, $args->{amount});
  7421. Plugins::callHook('buying_store_item_delete', {index => $item->{invIndex}});
  7422. }
  7423. }
  7424.  
  7425. sub define_check {
  7426. my ($self, $args) = @_;
  7427. #TODO
  7428. }
  7429.  
  7430. sub buyer_found {
  7431. my($self, $args) = @_;
  7432. my $ID = $args->{ID};
  7433.  
  7434. if (!$buyerLists{$ID} || !%{$buyerLists{$ID}}) {
  7435. binAdd(\@buyerListsID, $ID);
  7436. Plugins::callHook('packet_buyer', {ID => $ID});
  7437. }
  7438. $buyerLists{$ID}{title} = bytesToString($args->{title});
  7439. $buyerLists{$ID}{id} = $ID;
  7440. }
  7441.  
  7442. sub buyer_lost {
  7443. my ($self, $args) = @_;
  7444.  
  7445. my $ID = $args->{ID};
  7446. binRemove(\@buyerListsID, $ID);
  7447. delete $buyerLists{$ID};
  7448. }
  7449.  
  7450. sub battlefield_position {
  7451. my ($self, $args) = @_;
  7452.  
  7453. my $ID = $args->{ID};
  7454. my $name = $args->{name};
  7455. }
  7456.  
  7457. sub battlefield_hp {
  7458. my ($self, $args) = @_;
  7459.  
  7460. my $ID = $args->{ID};
  7461. my $name = $args->{name};
  7462.  
  7463. }
  7464.  
  7465. sub guild_member_map_change {
  7466. my ($self, $args) = @_;
  7467. debug("AID: %d (GID: %d) changed map to %s\n",$args->{AID}, $args->{GDID}, $args->{mapName});
  7468. }
  7469.  
  7470. sub guild_member_add {
  7471. my ($self, $args) = @_;
  7472.  
  7473. my $name = bytesToString($args->{name});
  7474. message TF("Guild member added: %s\n",$name), "guildchat";
  7475. }
  7476.  
  7477. sub millenium_shield {
  7478. my ($self, $args) = @_;
  7479. }
  7480.  
  7481. sub skill_post_delaylist {
  7482. my ($self, $args) = @_;
  7483.  
  7484. my $msg = $args->{RAW_MSG};
  7485. my $msg_size = $args->{RAW_MSG_SIZE};
  7486. for (my $i = 4; $i < $args->{msg_size}; $i += 6){
  7487. my ($ID,$time) = unpack("v V", substr($msg, $i,6));
  7488. my $skillName = (new Skill(idn => $ID))->getName;
  7489. my $status = defined $statusName{'EFST_DELAY'} ? $statusName{'EFST_DELAY'} : ' Delay';
  7490. $char->setStatus($skillName.$status, 1, $time);
  7491. }
  7492. }
  7493.  
  7494. sub msg_string {
  7495. my ($self, $args) = @_;
  7496. message TF("index: %s para1: %s\n", $args->{index}, $args->{para1}), "info";
  7497. # '07E2' => ['msg_string', 'v V', [qw(index para1)]], #TODO PACKET_ZC_MSG_VALUE **msgtable
  7498. }
  7499.  
  7500. sub skill_msg {
  7501. my ($self, $args) = @_;
  7502. message TF("id: %s msgid: %s\n", $args->{id}, $args->{msgid}), "info";
  7503.  
  7504. # '07E6' => ['skill_msg', 'v V', [qw(id msgid)]], #TODO: PACKET_ZC_MSG_SKILL **msgtable
  7505. }
  7506.  
  7507. sub quest_all_list2 {
  7508. my ($self, $args) = @_;
  7509. $questList = {};
  7510. my $msg;
  7511. my ($questID, $active, $time_start, $time, $mission_amount);
  7512. my $i = 0;
  7513. my ($mobID, $count, $amount, $mobName);
  7514. while ($i < $args->{RAW_MSG_SIZE} - 8) {
  7515. $msg = substr($args->{message}, $i, 15);
  7516. ($questID, $active, $time_start, $time, $mission_amount) = unpack('V C V2 v', $msg);
  7517. $questList->{$questID}->{active} = $active;
  7518. debug "$questID $active\n", "info";
  7519.  
  7520. my $quest = \%{$questList->{$questID}};
  7521. $quest->{time_start} = $time_start;
  7522. $quest->{time} = $time;
  7523. $quest->{mission_amount} = $mission_amount;
  7524. debug "$questID $time_start $time $mission_amount\n", "info";
  7525. $i += 15;
  7526.  
  7527. if ($mission_amount > 0) {
  7528. for (my $j = 0 ; $j < $mission_amount ; $j++) {
  7529. $msg = substr($args->{message}, $i, 32);
  7530. ($mobID, $count, $amount, $mobName) = unpack('V v2 Z24', $msg);
  7531. my $mission = \%{$quest->{missions}->{$mobID}};
  7532. $mission->{mobID} = $mobID;
  7533. $mission->{count} = $count;
  7534. $mission->{amount} = $amount;
  7535. $mission->{mobName_org} = $mobName;
  7536. $mission->{mobName} = bytesToString($mobName);
  7537. debug "- $mobID $count / $amount $mobName\n", "info";
  7538. $i += 32;
  7539. }
  7540. }
  7541. }
  7542. }
  7543.  
  7544. sub player_jump {
  7545. my ($self, $args) = @_;
  7546. my $actor = Actor::get($args->{ID});
  7547. message TF("%s jump to (%d, %d)\n", $actor, $args->{x}, $args->{y});
  7548. }
  7549.  
  7550. sub escape_map_select {
  7551. return if ($net->getState() == Network::IN_GAME);
  7552. my ($self, $args) = @_;
  7553. my @cities;
  7554. my $Odr = 0;
  7555. my $idx = 0;
  7556. my $nSav = -1;
  7557. my $mlist = '';
  7558. return unless exists $args->{mapInfo};
  7559.  
  7560. for (my $i = $args->{RAW_MSG_SIZE} % 20; $i < $args->{RAW_MSG_SIZE}; $i += 20) {
  7561. my ($flag, $baseName) = unpack('V Z*', substr($args->{RAW_MSG}, $i, 20));
  7562. if ($flag == 0) {
  7563. $baseName =~ s/\.gat$//;
  7564. $nSav = $idx if ($config{'escapeMap'} && $config{'escapeMap'} eq $baseName);
  7565. my $mapName = (defined $maps_lut{$baseName.'.rsw'} ? $maps_lut{$baseName.'.rsw'} : T('Unknown Area'));
  7566. push @cities, $Odr;
  7567. $idx++;
  7568. $mlist .= TF("%2d. %s\n", $idx, $mapName);
  7569. }
  7570. $Odr++;
  7571. }
  7572. if ($idx > 0) {
  7573. message TF("---------------- Map List ----------------\n" .
  7574. "%s" .
  7575. "------------------------------------------\n", $mlist);
  7576. $nSav = int(rand($idx)) if ($nSav < 0);
  7577.  
  7578. # Send the select back to RO server
  7579. my $msg = pack("C*", 0x41, 0x08, $config{char}, $cities[$nSav]);
  7580. $messageSender->sendToServer($msg);
  7581. }
  7582. }
  7583.  
  7584. 1;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement