defmodule Odinsea.Channel.Packets do @moduledoc """ Channel server packet builders. Ported from Java tools.packet.MaplePacketCreator (relevant parts) """ alias Odinsea.Net.Packet.Out alias Odinsea.Net.Opcodes alias Odinsea.Game.Reactor # ============================================================================= # Character Info & Field Entry # ============================================================================= @doc """ Sends character information on login. Ported from MaplePacketCreator.getCharInfo() """ def get_char_info(character, restored_buffs \\ []) do packet = Out.new(Opcodes.lp_set_field()) # GMS v342 specific header packet = if Odinsea.Constants.Game.gms?() do packet |> Out.encode_short(2) |> Out.encode_long(1) |> Out.encode_long(2) else packet |> Out.encode_int(character.channel - 1) |> Out.encode_int(0) end # Field key and character data flag packet = packet |> Out.encode_byte(0) # Field key |> Out.encode_int(0) # Unknown |> Out.encode_byte(1) # Character data flag |> Out.encode_short(0) # Notification info count # Random seeds for anti-cheat packet = packet |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_int(:rand.uniform(2_147_483_647)) # Full character info packet = encode_character_info(packet, character) # GMS padding packet = if Odinsea.Constants.Game.gms?() do Out.encode_bytes(packet, <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) else packet end # Server timestamp and additional data packet |> Out.encode_long(korean_timestamp(System.currentTimeMillis())) |> Out.encode_int(50) # Unknown |> Out.encode_byte(0) # Unknown |> Out.encode_byte(1) # Unknown |> Out.to_data() end @doc """ Warp to map packet. Ported from MaplePacketCreator.getWarpToMap() """ def warp_to_map(map_id, spawn_point, character) do packet = Out.new(Opcodes.lp_set_field()) # GMS v342 specific header packet = if Odinsea.Constants.Game.gms?() do packet |> Out.encode_short(2) |> Out.encode_long(1) |> Out.encode_long(2) else packet end packet = packet |> Out.encode_long(character.channel - 1) packet = if Odinsea.Constants.Game.gms?() do Out.encode_byte(packet, 0) else packet end packet |> Out.encode_long(0) # Field key |> Out.encode_byte(0) # Not character data |> Out.encode_int(map_id) |> Out.encode_byte(spawn_point) |> Out.encode_int(character.hp) |> Out.encode_byte(0) # Unknown |> Out.encode_long(korean_timestamp(System.currentTimeMillis())) |> Out.encode_int(50) |> Out.encode_byte(0) |> Out.encode_byte(if is_resist?(character.job), do: 0, else: 1) |> Out.to_data() end @doc """ Enables cash shop access. """ def enable_cash_shop do Out.new(Opcodes.lp_set_cash_shop_opened()) |> Out.encode_byte(1) |> Out.to_data() end @doc """ Server blocked message. """ def server_blocked(reason) do Out.new(Opcodes.lp_server_blocked()) |> Out.encode_byte(reason) |> Out.to_data() end @doc """ Enable client actions. """ def enable_actions do Out.new(Opcodes.lp_enable_action()) |> Out.encode_byte(0) |> Out.to_data() end @doc """ Channel change command. """ def get_channel_change(ip, port, character_id) do ip_parts = parse_ip(ip) Out.new(Opcodes.lp_migrate_command()) |> Out.encode_short(0) # Not cash shop |> encode_ip(ip_parts) |> Out.encode_short(port) |> Out.encode_int(character_id) |> Out.encode_bytes(<<0, 0>>) |> Out.to_data() end defp parse_ip(host) when is_binary(host) do case String.split(host, ".") do [a, b, c, d] -> { String.to_integer(a), String.to_integer(b), String.to_integer(c), String.to_integer(d) } _ -> {127, 0, 0, 1} end end defp encode_ip(packet, {a, b, c, d}) do packet |> Out.encode_byte(a) |> Out.encode_byte(b) |> Out.encode_byte(c) |> Out.encode_byte(d) end # ============================================================================= # Player Spawn/Remove # ============================================================================= @doc """ Spawns a player on the map. Ported from MaplePacketCreator.spawnPlayerMapobject() """ def spawn_player(oid, character_state) do packet = Out.new(Opcodes.lp_spawn_player()) |> Out.encode_int(oid) # Damage skin (custom client feature) packet = if Odinsea.Constants.Game.custom_client?() do Out.encode_int(packet, character_state.damage_skin || 0) else packet end packet = packet |> Out.encode_byte(character_state.level) |> Out.encode_string(character_state.name) # Ultimate Explorer name |> Out.encode_string(character_state.ultimate_explorer || "") # Guild info packet = encode_guild_info(packet, character_state.guild) # Buff mask encoding packet = encode_player_buff_mask(packet, character_state.buffs || []) packet = packet # ITEM_EFFECT |> Out.encode_int(character_state.item_effect || 0) # CHAIR |> Out.encode_int(character_state.chair || 0) # Position |> Out.encode_short(character_state.position.x) |> Out.encode_short(character_state.position.y) |> Out.encode_byte(character_state.position.stance) |> Out.encode_short(character_state.position.foothold) # Job |> Out.encode_short(character_state.job) # Appearance encoding (gender, skin, face, hair + equipment) packet = encode_appearance(packet, character_state) packet = packet # Driver ID / passenger ID (for mounts) |> Out.encode_int(0) # Chalkboard text |> Out.encode_string(character_state.chalkboard || "") # Ring info (3 ring slots) packet = encode_ring_info(packet, character_state.rings) # Marriage ring packet = packet |> Out.encode_int(0) # Mount info packet = encode_mount(packet, character_state.mount) packet = packet # Player shop (none for now) |> Out.encode_byte(0) # Admin byte |> Out.encode_byte(if character_state.is_admin, do: 1, else: 0) # Pet info packet = encode_spawn_pets(packet, character_state.pets || []) packet # Taming mob (none) |> Out.encode_int(0) # Mini game info |> Out.encode_byte(0) # Chalkboard flag |> Out.encode_byte(if character_state.chalkboard, do: 1, else: 0) # New year cards |> Out.encode_byte(0) # Berserk flag |> Out.encode_byte(if character_state.berserk, do: 1, else: 0) |> Out.to_data() end @doc """ Removes a player from the map. """ def remove_player(oid) do Out.new(Opcodes.lp_remove_player_from_map()) |> Out.encode_int(oid) |> Out.to_data() end @doc """ Updates character look (appearance change). Ported from MaplePacketCreator.updateCharLook() """ def update_char_look(character_id, character_state) do Out.new(Opcodes.lp_update_char_look()) |> Out.encode_int(character_id) |> Out.encode_byte(1) |> encode_appearance(character_state) |> encode_ring_info(character_state.rings) |> Out.encode_int(0) # -> charid to follow |> Out.to_data() end # ============================================================================= # Equipment & Appearance Encoding # ============================================================================= @doc """ Encodes full character appearance including equipment. Ported from PacketHelper.addCharLook() """ def encode_appearance(packet, character) do packet = packet |> Out.encode_byte(character.gender) |> Out.encode_byte(character.skin_color) |> Out.encode_int(character.face) |> Out.encode_int(character.hair) # Equipment encoding encode_equipment(packet, character.equipment) end @doc """ Encodes equipment for character appearance. Ported from PacketHelper.addCharLook() equipment encoding section. """ def encode_equipment(packet, equipment) when is_map(equipment) do # Separate visible and masked equipment {visible_equip, masked_equip} = process_equipment_slots(equipment) # Encode visible equipment (slots 1-99) packet = Enum.reduce(visible_equip, packet, fn {slot, item_id}, p -> p |> Out.encode_byte(slot) |> Out.encode_int(item_id) end) # End of visible items marker packet = Out.encode_byte(packet, 0xFF) # Encode masked equipment (overrides visible) packet = Enum.reduce(masked_equip, packet, fn {slot, item_id}, p -> p |> Out.encode_byte(slot) |> Out.encode_int(item_id) end) # End of masked items marker packet = Out.encode_byte(packet, 0xFF) # Cash weapon (slot -111) cash_weapon = Map.get(equipment, -111, 0) packet = Out.encode_int(packet, cash_weapon) # Ears (for certain items like elf ears) packet = Out.encode_int(packet, 0) packet end def encode_equipment(packet, _equipment) do # Empty equipment - just send end markers packet |> Out.encode_byte(0xFF) |> Out.encode_byte(0xFF) |> Out.encode_int(0) |> Out.encode_int(0) end defp process_equipment_slots(equipment) do equipment |> Enum.reduce({%{}, %{}}, fn {pos, item_id}, {visible, masked} -> slot = abs(pos) cond do # Hidden equipment (not visible) slot > 127 -> {visible, masked} # Visible equipment (slots 1-99) slot < 100 -> if Map.has_key?(visible, slot) do # Move existing to masked {Map.put(visible, slot, item_id), Map.put(masked, slot, visible[slot])} else {Map.put(visible, slot, item_id), masked} end # Cash equipment (slots 100+, except 111) slot > 100 and slot != 111 -> actual_slot = slot - 100 if Map.has_key?(visible, actual_slot) do {Map.put(visible, actual_slot, item_id), Map.put(masked, actual_slot, visible[actual_slot])} else {Map.put(visible, actual_slot, item_id), masked} end # Special slots true -> {visible, masked} end end) end # ============================================================================= # Buff Encoding # ============================================================================= @doc """ Encodes player buff mask for spawn packets. Ported from spawnPlayerMapobject() buff encoding. """ def encode_player_buff_mask(packet, buffs) do # Default buff mask flags default_mask = 0 # ENERGY_CHARGE | DASH_SPEED | DASH_JUMP | MONSTER_RIDING | SPEED_INFUSION | HOMING_BEACON | DEFAULT_BUFFSTAT # Build mask array (typically 14-16 integers for GMS v342) mask_size = if Odinsea.Constants.Game.gms?(), do: 14, else: 14 # Start with default mask in first position masks = List.duplicate(0, mask_size) masks = [default_mask | tl(masks)] # Apply active buffs to mask masks = Enum.reduce(buffs, masks, fn buff, acc_masks -> position = buff.stat.position value = buff.stat.value List.update_at(acc_masks, mask_size - position, fn existing -> Bitwise.bor(existing, value) end) end) # Encode all mask integers packet = Enum.reduce(masks, packet, fn mask, p -> Out.encode_int(p, mask) end) # Encode buff values for special buffs packet = Enum.reduce(buffs, packet, fn buff, p -> case buff.stat do :combo_counter -> p |> Out.encode_byte(buff.value) :weapon_charge -> p |> Out.encode_short(buff.value) |> Out.encode_int(buff.source_id) :shadow_partner -> p |> Out.encode_short(buff.value) |> Out.encode_int(buff.source_id) _ -> p end end) # CHAR_MAGIC_SPAWN and special buff data packet |> Out.encode_bytes(<<0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00>>) |> Out.encode_byte(0x01) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_bytes(<<0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00>>) |> Out.encode_byte(0x01) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_bytes(<<0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00>>) |> Out.encode_byte(0x01) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_short(0) |> Out.encode_int(0) |> Out.encode_int(0) |> Out.encode_byte(0x01) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_long(0) |> Out.encode_byte(0x01) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_bytes(<<0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00>>) |> Out.encode_byte(0x01) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_bytes(<<0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00>>) |> Out.encode_byte(0x01) |> Out.encode_int(:rand.uniform(2_147_483_647)) |> Out.encode_short(0) |> Out.encode_short(0) end @doc """ Give buff to player. Ported from MaplePacketCreator.giveBuff() """ def give_buff(buff_id, duration, stat_ups) do packet = Out.new(Opcodes.lp_give_buff()) # Encode buff mask packet = encode_buff_mask(packet, stat_ups) # Encode stat values packet = Enum.reduce(stat_ups, packet, fn {stat, value}, p -> if can_stack?(stat) do p else p |> Out.encode_short(value) |> Out.encode_int(buff_id) |> Out.encode_int(duration) end end) packet |> Out.encode_byte(0) |> Out.encode_byte(0) |> Out.encode_byte(0) |> Out.encode_short(0) |> Out.encode_byte(4) |> Out.to_data() end @doc """ Cancel buff. Ported from MaplePacketCreator.onResetTemporaryStat() """ def cancel_buff(stat_ups) do packet = Out.new(Opcodes.lp_cancel_buff()) |> encode_buff_mask(stat_ups) |> Out.encode_byte(3) |> Out.encode_byte(1) |> Out.to_data() end @doc """ Give foreign buff (to other players). Ported from MaplePacketCreator.giveForeignBuff() """ def give_foreign_buff(character_id, stat_ups, effect) do packet = Out.new(Opcodes.lp_give_foreign_buff()) |> Out.encode_int(character_id) |> encode_buff_mask(stat_ups) # Encode stat values for special buffs packet = Enum.reduce(stat_ups, packet, fn {stat, value}, p -> case stat do s when s in [:shadow_partner, :mechanic, :dark_aura, :blue_aura, :yellow_aura, :giant_potion, :spirit_link, :repeat_effect, :weapon_charge, :spirit_surge, :morph] -> p |> Out.encode_short(value) |> Out.encode_int(effect.source_id) :familiar_shadow -> p |> Out.encode_int(value) |> Out.encode_int(effect.char_color) _ -> Out.encode_short(p, value) end end) packet |> Out.encode_short(0) |> Out.encode_short(0) |> Out.encode_byte(1) |> Out.encode_byte(1) |> Out.to_data() end @doc """ Cancel foreign buff. Ported from MaplePacketCreator.cancelForeignBuff() """ def cancel_foreign_buff(character_id, stat_ups) do Out.new(Opcodes.lp_cancel_foreign_buff()) |> Out.encode_int(character_id) |> encode_buff_mask(stat_ups) |> Out.encode_byte(3) |> Out.encode_byte(1) |> Out.to_data() end defp encode_buff_mask(packet, stat_ups) do mask_size = if Odinsea.Constants.Game.gms?(), do: 14, else: 14 masks = Enum.reduce(stat_ups, List.duplicate(0, mask_size), fn {stat, _value}, acc -> position = stat_position(stat) value = stat_value(stat) List.update_at(acc, mask_size - position, fn existing -> Bitwise.bor(existing, value) end) end) Enum.reduce(masks, packet, fn mask, p -> Out.encode_int(p, mask) end) end defp stat_position(_stat), do: 1 defp stat_value(_stat), do: 1 defp can_stack?(_stat), do: false # ============================================================================= # Stat Updates # ============================================================================= @doc """ Update player stats. Ported from MaplePacketCreator.updatePlayerStats() """ def update_stats(stats, item_reaction \\ false, job \\ 0) do packet = Out.new(Opcodes.lp_update_stats()) |> Out.encode_byte(if item_reaction, do: 1, else: 0) # Calculate update mask update_mask = Enum.reduce(stats, 0, fn {stat, _value}, acc -> Bitwise.bor(acc, stat_value(stat)) end) packet = if Odinsea.Constants.Game.gms?() do Out.encode_long(packet, update_mask) else Out.encode_int(packet, update_mask) end # Encode stat values packet = Enum.reduce(stats, packet, fn {stat, value}, p -> encode_stat_value(p, stat, value, job) end) packet |> Out.encode_short(0) |> Out.to_data() end defp encode_stat_value(packet, stat, value, job) do cond do stat in [:skin, :face, :hair] -> Out.encode_int(packet, value) stat in [:level, :job] -> Out.encode_byte(packet, value) stat == :available_sp -> cond do is_evan?(job) or is_resist?(job) or is_mercedes?(job) -> # SP by job level for Evan/Resistance/Mercedes packet true -> Out.encode_short(packet, value) end stat in [:hp, :max_hp, :mp, :max_mp, :exp, :meso] -> Out.encode_int(packet, value) stat in [:str, :dex, :int, :luk, :remaining_ap] -> Out.encode_short(packet, value) true -> Out.encode_int(packet, value) end end # ============================================================================= # EXP & Level Up # ============================================================================= @doc """ Show EXP gain from monster. Ported from MaplePacketCreator.GainEXP_Monster() """ def gain_exp_monster(gain, white \\ true, party_bonus \\ 0, class_bonus \\ 0, equipment_bonus \\ 0, premium_bonus \\ 0, bonus_exp \\ 0) do gain_capped = min(gain, 2_147_483_647) Out.new(Opcodes.lp_show_status_info()) |> Out.encode_byte(3) # 3 = exp |> Out.encode_byte(if white, do: 1, else: 0) |> Out.encode_int(gain_capped) |> Out.encode_byte(0) # Not in chat |> Out.encode_int(bonus_exp) # Event Bonus |> Out.encode_byte(0) |> Out.encode_byte(0) |> Out.encode_int(0) # Wedding bonus |> Out.encode_int(0) # Party ring bonus |> Out.encode_byte(0) |> Out.encode_int(party_bonus) # Party size indicator |> Out.encode_int(equipment_bonus) # Equipment Bonus EXP |> Out.encode_int(premium_bonus) # Premium bonus EXP |> Out.encode_int(0) # Rainbow Week Bonus EXP |> Out.encode_int(class_bonus) # Class bonus EXP |> Out.encode_int(0) # Summer week |> Out.to_data() end @doc """ Show EXP gain (other sources). Ported from MaplePacketCreator.GainEXP_Others() """ def gain_exp_others(gain, in_chat \\ false, white \\ true) do packet = Out.new(Opcodes.lp_show_status_info()) |> Out.encode_byte(3) # 3 = exp |> Out.encode_byte(if white, do: 1, else: 0) |> Out.encode_int(gain) |> Out.encode_byte(if in_chat, do: 1, else: 0) if in_chat do packet |> Out.encode_bytes(<<0, 0, 0, 0, 0, 0>>) else packet |> Out.encode_bytes(<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) end |> Out.to_data() end @doc """ Show level up effect to player. Ported from MaplePacketCreator.showSpecialEffect() """ def show_level_up do Out.new(Opcodes.lp_show_item_gain_inchat()) |> Out.encode_byte(0) # 0 = Level up |> Out.to_data() end @doc """ Show level up effect to other players. Ported from MaplePacketCreator.showForeignEffect() """ def show_foreign_level_up(character_id) do Out.new(Opcodes.lp_show_foreign_effect()) |> Out.encode_int(character_id) |> Out.encode_byte(0) # 0 = Level up |> Out.to_data() end @doc """ Show job change effect. Ported from MaplePacketCreator.showForeignEffect() """ def show_job_change(character_id) do Out.new(Opcodes.lp_show_foreign_effect()) |> Out.encode_int(character_id) |> Out.encode_byte(8) # 8 = Job change |> Out.to_data() end @doc """ Show skill effect. Ported from MaplePacketCreator.showBuffeffect() """ def show_skill_effect(character_id, skill_id, effect_id, player_level, skill_level, direction \\ 3) do packet = Out.new(Opcodes.lp_show_foreign_effect()) |> Out.encode_int(character_id) |> Out.encode_byte(effect_id) |> Out.encode_int(skill_id) |> Out.encode_byte(player_level - 1) |> Out.encode_byte(skill_level) if direction != 3 do Out.encode_byte(packet, direction) else packet end |> Out.to_data() end @doc """ Show own skill effect. Ported from MaplePacketCreator.showOwnBuffEffect() """ def show_own_skill_effect(skill_id, effect_id, player_level, skill_level, direction \\ 3) do packet = Out.new(Opcodes.lp_show_item_gain_inchat()) |> Out.encode_byte(effect_id) |> Out.encode_int(skill_id) |> Out.encode_byte(player_level - 1) |> Out.encode_byte(skill_level) if direction != 3 do Out.encode_byte(packet, direction) else packet end |> Out.to_data() end # ============================================================================= # Portal Packets # ============================================================================= @doc """ Spawn a portal on the map. Ported from MaplePacketCreator.spawnPortal() """ def spawn_portal(town_id, target_id, skill_id, position) do packet = Out.new(Opcodes.lp_spawn_portal()) |> Out.encode_int(town_id) |> Out.encode_int(target_id) if town_id != 999_999_999 and target_id != 999_999_999 do packet |> Out.encode_int(skill_id) |> Out.encode_short(position.x) |> Out.encode_short(position.y) else packet end |> Out.to_data() end @doc """ Spawn a door (mystic door skill). Ported from MaplePacketCreator.spawnDoor() """ def spawn_door(oid, position, animation \\ true) do Out.new(Opcodes.lp_spawn_door()) |> Out.encode_byte(if animation, do: 0, else: 1) |> Out.encode_int(oid) |> Out.encode_short(position.x) |> Out.encode_short(position.y) |> Out.to_data() end @doc """ Remove a door. Ported from MaplePacketCreator.removeDoor() """ def remove_door(oid, animation \\ true) do Out.new(Opcodes.lp_remove_door()) |> Out.encode_byte(if animation, do: 0, else: 1) |> Out.encode_int(oid) |> Out.to_data() end @doc """ Instant map warp (for portals). Ported from MaplePacketCreator.instantMapWarp() """ def instant_map_warp(portal) do Out.new(Opcodes.lp_current_map_warp()) |> Out.encode_byte(0) |> Out.encode_byte(portal) |> Out.to_data() end # ============================================================================= # Chat Packets # ============================================================================= @doc """ User chat packet. Ported from LocalePacket.UserChat() """ def user_chat(character_id, message, is_admin \\ false, only_balloon \\ false) do Out.new(Opcodes.lp_chattext()) |> Out.encode_int(character_id) |> Out.encode_byte(if is_admin, do: 1, else: 0) |> Out.encode_string(message) |> Out.encode_byte(if only_balloon, do: 1, else: 0) |> Out.to_data() end @doc """ Whisper chat packet (received whisper). Ported from LocalePacket.WhisperChat() """ def whisper_received(sender_name, channel, message) do Out.new(Opcodes.lp_whisper()) |> Out.encode_byte(0x12) |> Out.encode_string(sender_name) |> Out.encode_short(channel - 1) |> Out.encode_string(message) |> Out.to_data() end @doc """ Whisper reply packet (sent whisper status). Mode: 1 = success, 0 = failure """ def whisper_reply(recipient_name, mode) do Out.new(Opcodes.lp_whisper()) |> Out.encode_byte(0x0A) |> Out.encode_string(recipient_name) |> Out.encode_byte(mode) |> Out.to_data() end @doc """ Find player reply (player found on channel). """ def find_player_reply(target_name, channel, is_buddy \\ false) do Out.new(Opcodes.lp_whisper()) |> Out.encode_byte(0x09) |> Out.encode_string(target_name) |> Out.encode_byte(if is_buddy, do: 0x48, else: 0x01) |> Out.encode_byte(channel) |> Out.to_data() end @doc """ Find player reply with map (player found on same channel). """ def find_player_with_map(target_name, map_id, is_buddy \\ false) do Out.new(Opcodes.lp_whisper()) |> Out.encode_byte(0x09) |> Out.encode_string(target_name) |> Out.encode_byte(if is_buddy, do: 0x48, else: 0x01) |> Out.encode_int(map_id) |> Out.encode_bytes(<<0, 0, 0>>) |> Out.to_data() end @doc """ Party chat packet. Type: 0 = buddy, 1 = party, 2 = guild, 3 = alliance, 4 = expedition """ def multi_chat(sender_name, message, chat_type) do Out.new(Opcodes.lp_multi_chat()) |> Out.encode_byte(chat_type) |> Out.encode_string(sender_name) |> Out.encode_string(message) |> Out.to_data() end # ============================================================================= # Monster Packets # ============================================================================= @doc """ Spawns a monster on the map (LP_MobEnterField). ## Parameters - monster: Monster.t() struct - spawn_type: spawn animation type (-1 = normal, -2 = regen, -3 = revive, etc.) - link: linked mob OID (for multi-mobs) Reference: MobPacket.spawnMonster() """ def spawn_monster(monster, spawn_type \\ -1, link \\ 0) do packet = Out.new(Opcodes.lp_spawn_monster()) |> Out.encode_int(monster.oid) |> Out.encode_byte(1) # 1 = Control normal, 5 = Control none |> Out.encode_int(monster.mob_id) # Temporary stat encoding (buffs/debuffs) |> encode_mob_temporary_stat(monster) # Position |> Out.encode_short(monster.position.x) |> Out.encode_short(monster.position.y) # Move action (bitfield) |> Out.encode_byte(monster.stance) # Foothold SN |> Out.encode_short(monster.fh) # Origin FH |> Out.encode_short(monster.fh) # Spawn type |> Out.encode_byte(spawn_type) # Link OID (for spawn_type -3 or >= 0) packet = if spawn_type == -3 or spawn_type >= 0 do Out.encode_int(packet, link) else packet end packet # Carnival team |> Out.encode_byte(0) # Aftershock - 8 bytes (0xFF at end for GMS) |> Out.encode_long(0) # GMS specific |> Out.encode_byte(-1) |> Out.to_data() end @doc """ Assigns monster control to a player (LP_MobChangeController). ## Parameters - monster: Monster.t() struct - new_spawn: whether this is a new spawn - aggro: aggro mode (2 = aggro, 1 = normal) Reference: MobPacket.controlMonster() """ def control_monster(monster, new_spawn \\ false, aggro \\ false) do packet = Out.new(Opcodes.lp_spawn_monster_control()) |> Out.encode_byte(if aggro, do: 2, else: 1) |> Out.encode_int(monster.oid) |> Out.encode_byte(1) # 1 = Control normal, 5 = Control none |> Out.encode_int(monster.mob_id) # Temporary stat encoding |> encode_mob_temporary_stat(monster) # Position |> Out.encode_short(monster.position.x) |> Out.encode_short(monster.position.y) # Move action |> Out.encode_byte(monster.stance) # Foothold SN |> Out.encode_short(monster.fh) # Origin FH |> Out.encode_short(monster.fh) # Spawn type (-4 = fake, -2 = new spawn, -1 = normal) |> Out.encode_byte(cond do new_spawn -> -2 true -> -1 end) # Carnival team |> Out.encode_byte(0) # Big bang - another long |> Out.encode_long(0) # GMS specific |> Out.encode_byte(-1) |> Out.to_data() end @doc """ Stops controlling a monster (LP_MobChangeController with byte 0). Reference: MobPacket.stopControllingMonster() """ def stop_controlling_monster(oid) do Out.new(Opcodes.lp_spawn_monster_control()) |> Out.encode_byte(0) |> Out.encode_int(oid) |> Out.to_data() end @doc """ Monster movement packet (LP_MobMove). ## Parameters - oid: monster object ID - next_attack_possible: whether next attack is possible - left: facing direction (raw byte from client) - skill_data: skill data from movement - move_path: movement path data (binary from client) Reference: MobPacket.onMove() """ def move_monster(oid, next_attack_possible, left, skill_data, move_path) do Out.new(Opcodes.lp_move_monster()) |> Out.encode_int(oid) |> Out.encode_byte(0) |> Out.encode_byte(0) |> Out.encode_byte(if next_attack_possible, do: 1, else: 0) |> Out.encode_byte(left) |> Out.encode_int(skill_data) |> Out.encode_int(0) # multi target for ball size |> Out.encode_int(0) # rand time for area attack # Movement path (raw binary from client) |> Out.encode_bytes(move_path) |> Out.to_data() end @doc """ Damage monster packet (LP_MobDamaged). ## Parameters - oid: monster object ID - damage: damage amount (capped at Integer max) Reference: MobPacket.damageMonster() """ def damage_monster(oid, damage) do # Cap damage at max int damage_capped = min(damage, 2_147_483_647) Out.new(Opcodes.lp_damage_monster()) |> Out.encode_int(oid) |> Out.encode_byte(0) |> Out.encode_int(damage_capped) |> Out.to_data() end @doc """ Kill monster packet (LP_MobLeaveField). ## Parameters - monster: Monster.t() struct - leave_type: how the mob is leaving (0 = remain hp, 1 = etc, 2 = self destruct, etc.) Reference: MobPacket.killMonster() """ def kill_monster(monster, leave_type \\ 1) do packet = Out.new(Opcodes.lp_kill_monster()) |> Out.encode_int(monster.oid) |> Out.encode_byte(leave_type) # If swallow type, encode swallower character ID packet = if leave_type == 4 do Out.encode_int(packet, -1) else packet end Out.to_data(packet) end @doc """ Show monster HP indicator (LP_MobHPIndicator). ## Parameters - oid: monster object ID - hp_percentage: HP percentage (0-100) Reference: MobPacket.showMonsterHP() """ def show_monster_hp(oid, hp_percentage) do Out.new(Opcodes.lp_show_monster_hp()) |> Out.encode_int(oid) |> Out.encode_byte(hp_percentage) |> Out.to_data() end @doc """ Show boss HP bar (BOSS_ENV). ## Parameters - monster: Monster.t() struct Reference: MobPacket.showBossHP() """ def show_boss_hp(monster) do # Cap HP at max int for display current_hp = min(monster.hp, 2_147_483_647) max_hp = min(monster.max_hp, 2_147_483_647) Out.new(Opcodes.lp_boss_env()) |> Out.encode_byte(5) |> Out.encode_int(monster.mob_id) |> Out.encode_int(current_hp) |> Out.encode_int(max_hp) |> Out.encode_byte(6) # Tag color (default) |> Out.encode_byte(5) # Tag bg color (default) |> Out.to_data() end @doc """ Monster control acknowledgment (LP_MobCtrlAck). ## Parameters - oid: monster object ID - mob_ctrl_sn: control sequence number - next_attack_possible: whether next attack is possible - mp: monster MP - skill_command: skill command from client - slv: skill level Reference: MobPacket.onCtrlAck() """ def mob_ctrl_ack(oid, mob_ctrl_sn, next_attack_possible, mp, skill_command, slv) do Out.new(Opcodes.lp_move_monster_response()) |> Out.encode_int(oid) |> Out.encode_short(mob_ctrl_sn) |> Out.encode_byte(if next_attack_possible, do: 1, else: 0) |> Out.encode_short(mp) |> Out.encode_byte(skill_command) |> Out.encode_byte(slv) |> Out.encode_int(0) # forced attack idx |> Out.to_data() end # ============================================================================= # Reactor Packets # ============================================================================= @doc """ Spawns a reactor on the map (LP_ReactorEnterField). ## Parameters - reactor: Reactor.t() struct Reference: MaplePacketCreator.spawnReactor() """ def spawn_reactor(reactor) do Out.new(Opcodes.lp_reactor_spawn()) |> Out.encode_int(reactor.oid) |> Out.encode_int(reactor.reactor_id) |> Out.encode_byte(reactor.state) |> Out.encode_short(reactor.x) |> Out.encode_short(reactor.y) |> Out.encode_byte(reactor.facing_direction) |> Out.encode_string(reactor.name) |> Out.to_data() end @doc """ Triggers/hits a reactor (LP_ReactorChangeState). ## Parameters - reactor: Reactor.t() struct - stance: stance value (usually 0) Reference: MaplePacketCreator.triggerReactor() """ def trigger_reactor(reactor, stance \\ 0) do # Cap state for herb/vein reactors (100000-200011 range) state = if reactor.reactor_id >= 100000 and reactor.reactor_id <= 200011 do min(reactor.state, 4) else reactor.state end Out.new(Opcodes.lp_reactor_hit()) |> Out.encode_int(reactor.oid) |> Out.encode_byte(state) |> Out.encode_short(reactor.x) |> Out.encode_short(reactor.y) |> Out.encode_short(stance) |> Out.encode_byte(0) |> Out.encode_byte(0) |> Out.to_data() end @doc """ Destroys/removes a reactor from the map (LP_ReactorLeaveField). ## Parameters - reactor: Reactor.t() struct Reference: MaplePacketCreator.destroyReactor() """ def destroy_reactor(reactor) do Out.new(Opcodes.lp_reactor_destroy()) |> Out.encode_int(reactor.oid) |> Out.encode_byte(reactor.state) |> Out.encode_short(reactor.x) |> Out.encode_short(reactor.y) |> Out.to_data() end # ============================================================================= # Helper Functions for Monster Encoding # ============================================================================= defp encode_mob_temporary_stat(packet, monster) do # For GMS v342, encode changed stats first packet |> encode_mob_changed_stats(monster) # Then encode temporary status effects (buffs/debuffs) |> encode_mob_status_mask(monster) end defp encode_mob_changed_stats(packet, monster) do if Odinsea.Constants.Game.gms?() do changed_stats = monster.changed_stats if changed_stats do packet = Out.encode_byte(packet, 1) packet |> Out.encode_int(min(changed_stats.hp || 0, 2_147_483_647)) |> Out.encode_int(changed_stats.mp || 0) |> Out.encode_int(changed_stats.exp || 0) |> Out.encode_int(changed_stats.watk || 0) |> Out.encode_int(changed_stats.matk || 0) |> Out.encode_int(changed_stats.pdrate || 0) |> Out.encode_int(changed_stats.mdrate || 0) |> Out.encode_int(changed_stats.acc || 0) |> Out.encode_int(changed_stats.eva || 0) |> Out.encode_int(changed_stats.pushed || 0) |> Out.encode_int(changed_stats.level || 0) else Out.encode_byte(packet, 0) end else packet end end defp encode_mob_status_mask(packet, monster) do # Encode status effects (poison, stun, freeze, etc.) # For now, assume no status effects - send empty mask # Mask is typically 14-16 integers (56-64 bytes) for GMS v342 mask_size = if Odinsea.Constants.Game.gms?(), do: 14, else: 16 packet = Enum.reduce(1..mask_size, packet, fn _, p -> Out.encode_int(p, 0) end) # If status effects exist, encode for each effect: # - nOption (short) # - rOption (int) - skill ID | skill level << 16 # - tOption (short) - duration / 500 packet end # ============================================================================= # Admin/System Packets # ============================================================================= @doc """ Drop message packet (system message displayed to player). Types: - 0 = Notice (blue) - 1 = Popup (red) - 2 = Megaphone - 3 = Super Megaphone - 4 = Top scrolling message - 5 = System message (yellow) - 6 = Quiz Reference: MaplePacketCreator.dropMessage() """ def drop_message(type, message) do Out.new(Opcodes.lp_blow_weather()) |> Out.encode_int(type) |> Out.encode_string(message) |> Out.to_data() end @doc """ Server-wide broadcast message. Reference: MaplePacketCreator.serverMessage() """ def server_message(message) do Out.new(Opcodes.lp_event_msg()) |> Out.encode_byte(1) |> Out.encode_string(message) |> Out.to_data() end @doc """ Scrolling server message (top of screen banner). Reference: MaplePacketCreator.serverMessage() """ def scrolling_message(message) do Out.new(Opcodes.lp_event_msg()) |> Out.encode_byte(4) |> Out.encode_string(message) |> Out.to_data() end @doc """ Screenshot request packet (admin tool). Sends a session key to the client for screenshot verification. Reference: ClientPool.getScreenshot() """ def screenshot_request(session_key) do Out.new(Opcodes.lp_screen_msg()) |> Out.encode_long(session_key) |> Out.to_data() end @doc """ Start lie detector on player. Reference: MaplePacketCreator.sendLieDetector() """ def start_lie_detector do Out.new(Opcodes.lp_lie_detector()) |> Out.encode_byte(1) # Start lie detector |> Out.to_data() end @doc """ Lie detector result packet. Status: - 0 = Success - 1 = Failed (wrong answer) - 2 = Timeout - 3 = Error """ def lie_detector_result(status) do Out.new(Opcodes.lp_lie_detector()) |> Out.encode_byte(status) |> Out.to_data() end @doc """ Admin result packet (command acknowledgment). Reference: MaplePacketCreator.getAdminResult() """ def admin_result(success, message) do Out.new(Opcodes.lp_admin_result()) |> Out.encode_byte(if success, do: 1, else: 0) |> Out.encode_string(message) |> Out.to_data() end @doc """ Force disconnect packet (kick player). Reference: MaplePacketCreator.getForcedDisconnect() """ def force_disconnect(reason \\ 0) do Out.new(Opcodes.lp_forced_stat_ex()) |> Out.encode_byte(reason) |> Out.to_data() end # ============================================================================= # Drop Packets # ============================================================================= @doc """ Spawns a drop on the map (LP_DropItemFromMapObject). ## Parameters - drop: Drop.t() struct - source_position: Position where drop originated (monster/player position) - animation: Animation type - 1 = animation (drop falls from source) - 2 = no animation (instant spawn) - 3 = spawn disappearing item [Fade] - 4 = spawn disappearing item - delay: Delay before drop appears (in ms) Reference: MaplePacketCreator.dropItemFromMapObject() """ def spawn_drop(drop, source_position \\ nil, animation \\ 1, delay \\ 0) do source_pos = source_position || drop.position packet = Out.new(Opcodes.lp_drop_item_from_mapobject()) |> Out.encode_byte(animation) |> Out.encode_int(drop.oid) |> Out.encode_byte(if drop.meso > 0, do: 1, else: 0) # 1 = mesos, 0 = item |> Out.encode_int(Drop.display_id(drop)) |> Out.encode_int(drop.owner_id) |> Out.encode_byte(drop.drop_type) |> Out.encode_short(drop.position.x) |> Out.encode_short(drop.position.y) |> Out.encode_int(0) # Unknown # If animation != 2, encode source position packet = if animation != 2 do packet |> Out.encode_short(source_pos.x) |> Out.encode_short(source_pos.y) |> Out.encode_short(delay) else packet end # If not meso, encode expiration time packet = if drop.meso == 0 do # Expiration time - for now, send 0 (no expiration) # In full implementation, this would be the item's expiration timestamp Out.encode_long(packet, 0) else packet end # Pet pickup byte # 0 = player can pick up, 1 = pet can pick up packet |> Out.encode_short(if drop.player_drop, do: 0, else: 1) |> Out.to_data() end @doc """ Removes a drop from the map (LP_RemoveItemFromMap). ## Parameters - oid: Drop object ID - animation: Removal animation - 0 = Expire/fade out - 1 = Without animation - 2 = Pickup animation - 4 = Explode animation - 5 = Pet pickup - character_id: Character ID performing the action (for pickup animations) - slot: Pet slot (for pet pickup animation) Reference: MaplePacketCreator.removeItemFromMap() """ def remove_drop(oid, animation \\ 1, character_id \\ 0, slot \\ 0) do packet = Out.new(Opcodes.lp_remove_item_from_map()) |> Out.encode_byte(animation) |> Out.encode_int(oid) # If animation >= 2, encode character ID packet = if animation >= 2 do Out.encode_int(packet, character_id) else packet end # If animation == 5 (pet pickup), encode slot packet = if animation == 5 do Out.encode_int(packet, slot) else packet end Out.to_data(packet) end @doc """ Spawns multiple drops at once (for explosive drops). Broadcasts a spawn packet for each drop with minimal animation. """ def spawn_drops(drops, source_position, animation \\ 2) do Enum.map(drops, fn drop -> spawn_drop(drop, source_position, animation) end) end @doc """ Sends existing drops to a joining player. """ def send_existing_drops(client_pid, drops) do Enum.each(drops, fn drop -> # Only send drops that haven't been picked up if not drop.picked_up do packet = spawn_drop(drop, nil, 2) # No animation send(client_pid, {:send_packet, packet}) end end) end # ============================================================================= # Pet Packets # ============================================================================= @doc """ Updates pet information in inventory (ModifyInventoryItem). Ported from PetPacket.updatePet() """ def update_pet(pet, item \\ nil, active \\ true) do # Encode inventory update with pet info Out.new(Opcodes.lp_modify_inventory_item()) |> Out.encode_byte(0) # Inventory mode |> Out.encode_byte(2) # Update count |> Out.encode_byte(3) # Mode type |> Out.encode_byte(5) # Inventory type (CASH) |> Out.encode_short(pet.inventory_position) |> Out.encode_byte(0) |> Out.encode_byte(5) # Inventory type (CASH) |> Out.encode_short(pet.inventory_position) |> Out.encode_byte(3) # Item type for pet |> Out.encode_int(pet.pet_item_id) |> Out.encode_byte(1) |> Out.encode_long(pet.unique_id) |> encode_pet_item_info(pet, item, active) |> Out.to_data() end @doc """ Spawns a pet on the map (LP_SpawnPet). Ported from PetPacket.showPet() ## Parameters - character_id: Owner's character ID - pet: Pet.t() struct - remove: If true, removes the pet - hunger: If true and removing, shows hunger message """ def spawn_pet(character_id, pet, remove \\ false, hunger \\ false) do packet = Out.new(Opcodes.lp_spawn_pet()) |> Out.encode_int(character_id) # Encode pet slot based on GMS mode packet = if Odinsea.Constants.Game.gms?() do Out.encode_byte(packet, pet.summoned) else Out.encode_int(packet, pet.summoned) end if remove do packet |> Out.encode_byte(0) # Remove flag |> Out.encode_byte(if hunger, do: 1, else: 0) else packet |> Out.encode_byte(1) # Show flag |> Out.encode_byte(0) # Unknown flag |> Out.encode_int(pet.pet_item_id) |> Out.encode_string(pet.name) |> Out.encode_long(pet.unique_id) |> Out.encode_short(pet.position.x) |> Out.encode_short(pet.position.y - 20) # Offset Y slightly |> Out.encode_byte(pet.stance) |> Out.encode_int(pet.position.fh) end |> Out.to_data() end @doc """ Removes a pet from the map. Ported from PetPacket.removePet() """ def remove_pet(character_id, slot) do packet = Out.new(Opcodes.lp_spawn_pet()) |> Out.encode_int(character_id) # Encode slot based on GMS mode packet = if Odinsea.Constants.Game.gms?() do Out.encode_byte(packet, slot) else Out.encode_int(packet, slot) end packet |> Out.encode_short(0) # Remove flag |> Out.to_data() end @doc """ Moves a pet on the map (LP_PetMove). Ported from PetPacket.movePet() """ def move_pet(character_id, pet_unique_id, slot, movement_data) do packet = Out.new(Opcodes.lp_move_pet()) |> Out.encode_int(character_id) # Encode slot based on GMS mode packet = if Odinsea.Constants.Game.gms?() do Out.encode_byte(packet, slot) else Out.encode_int(packet, slot) end packet |> Out.encode_long(pet_unique_id) |> Out.encode_bytes(movement_data) |> Out.to_data() end @doc """ Pet chat packet (LP_PetChat). Ported from PetPacket.petChat() """ def pet_chat(character_id, slot, command, text) do packet = Out.new(Opcodes.lp_pet_chat()) |> Out.encode_int(character_id) # Encode slot based on GMS mode packet = if Odinsea.Constants.Game.gms?() do Out.encode_byte(packet, slot) else Out.encode_int(packet, slot) end packet |> Out.encode_short(command) |> Out.encode_string(text) |> Out.encode_byte(0) # hasQuoteRing |> Out.to_data() end @doc """ Pet command response (LP_PetCommand). Ported from PetPacket.commandResponse() ## Parameters - character_id: Owner's character ID - slot: Pet slot (0-2) - command: Command ID that was executed - success: Whether the command succeeded - food: Whether this was a food command """ def pet_command_response(character_id, slot, command, success, food) do packet = Out.new(Opcodes.lp_pet_command()) |> Out.encode_int(character_id) # Encode slot based on GMS mode packet = if Odinsea.Constants.Game.gms?() do Out.encode_byte(packet, slot) else Out.encode_int(packet, slot) end # Command encoding differs for food packet = if command == 1 do Out.encode_byte(packet, 1) else Out.encode_byte(packet, 0) end packet = Out.encode_byte(packet, command) packet = if command == 1 do # Food command Out.encode_byte(packet, 0) else # Regular command Out.encode_short(packet, if(success, do: 1, else: 0)) end Out.to_data(packet) end @doc """ Shows own pet level up effect (LP_ShowItemGainInChat). Ported from PetPacket.showOwnPetLevelUp() """ def show_own_pet_level_up(slot) do Out.new(Opcodes.lp_show_item_gain_inchat()) |> Out.encode_byte(6) # Type: pet level up |> Out.encode_byte(0) |> Out.encode_int(slot) |> Out.to_data() end @doc """ Shows pet level up effect to other players (LP_ShowForeignEffect). Ported from PetPacket.showPetLevelUp() """ def show_pet_level_up(character_id, slot) do Out.new(Opcodes.lp_show_foreign_effect()) |> Out.encode_int(character_id) |> Out.encode_byte(6) # Type: pet level up |> Out.encode_byte(0) |> Out.encode_int(slot) |> Out.to_data() end @doc """ Pet name change/update (LP_PetNameChanged). Ported from PetPacket.showPetUpdate() """ def pet_name_change(character_id, pet_unique_id, slot) do packet = Out.new(Opcodes.lp_pet_namechange()) |> Out.encode_int(character_id) # Encode slot based on GMS mode packet = if Odinsea.Constants.Game.gms?() do Out.encode_byte(packet, slot) else Out.encode_int(packet, slot) end packet |> Out.encode_long(pet_unique_id) |> Out.encode_byte(0) # Unknown |> Out.to_data() end @doc """ Pet stat update (UPDATE_STATS with PET flag). Ported from PetPacket.petStatUpdate() """ def pet_stat_update(pets) do # Filter summoned pets summoned_pets = Enum.filter(pets, fn pet -> pet.summoned > 0 end) packet = Out.new(Opcodes.lp_update_stats()) |> Out.encode_byte(0) # Reset mask flag # Encode stat mask based on GMS mode pet_stat_value = if Odinsea.Constants.Game.gms?(), do: 0x200000, else: 0x200000 packet = if Odinsea.Constants.Game.gms?() do Out.encode_long(packet, pet_stat_value) else Out.encode_int(packet, pet_stat_value) end # Encode summoned pet unique IDs (up to 3) packet = Enum.reduce(summoned_pets, packet, fn pet, p -> Out.encode_long(p, pet.unique_id) end) # Fill remaining slots with empty remaining = 3 - length(summoned_pets) packet = Enum.reduce(1..remaining, packet, fn _, p -> Out.encode_long(p, 0) end) packet |> Out.encode_byte(0) |> Out.encode_short(0) |> Out.to_data() end # ============================================================================= # Private Helper Functions # ============================================================================= defp encode_character_info(packet, character) do # Basic stats packet = packet |> Out.encode_int(character.id) |> Out.encode_string(character.name, 13) |> Out.encode_byte(character.gender) |> Out.encode_byte(character.skin_color) |> Out.encode_int(character.face) |> Out.encode_int(character.hair) packet = if Odinsea.Constants.Game.gms?() do Out.encode_bytes(packet, <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) else packet end packet = packet |> Out.encode_byte(character.level) |> Out.encode_short(character.job) # Stats encoding |> encode_char_stats(character) |> Out.encode_short(character.remaining_ap) # SP encoding for Evan/Resistance/Mercedes packet = if is_evan?(character.job) or is_resist?(character.job) or is_mercedes?(character.job) do remaining_sps = character.remaining_sps || [] non_zero = Enum.filter(remaining_sps, fn {_, sp} -> sp > 0 end) packet = Out.encode_byte(packet, length(non_zero)) Enum.reduce(non_zero, packet, fn {job_level, sp}, p -> p |> Out.encode_byte(job_level + 1) |> Out.encode_byte(sp) end) else Out.encode_short(packet, character.remaining_sp) end packet |> Out.encode_int(character.exp) |> Out.encode_int(character.fame) |> Out.encode_int(character.gach_exp) |> Out.encode_int(character.map) |> Out.encode_byte(character.spawnpoint) packet = if Odinsea.Constants.Game.gms?() do Out.encode_int(packet, 0) else packet end packet |> Out.encode_short(character.subcategory) |> Out.encode_byte(character.fatigue) |> Out.encode_int(current_date()) # Traits |> encode_traits(character) |> Out.encode_bytes(<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) |> Out.encode_int(character.pvp_exp) |> Out.encode_byte(character.pvp_rank) |> Out.encode_int(character.battle_points) |> Out.encode_byte(5) if Odinsea.Constants.Game.gms?() do Out.encode_int(packet, 0) else packet end end defp encode_char_stats(packet, character) do stats = character.stats packet |> Out.encode_short(stats.str) |> Out.encode_short(stats.dex) |> Out.encode_short(stats.int) |> Out.encode_short(stats.luk) |> Out.encode_int(stats.hp) |> Out.encode_int(stats.max_hp) |> Out.encode_int(stats.mp) |> Out.encode_int(stats.max_mp) end defp encode_traits(packet, character) do traits = character.traits || %{} Enum.reduce([:charisma, :insight, :will, :craft, :sense, :charm], packet, fn trait, p -> trait_data = Map.get(traits, trait, %{exp: 0}) Out.encode_int(p, trait_data.exp) end) end defp encode_guild_info(packet, nil) do packet |> Out.encode_int(0) |> Out.encode_int(0) end defp encode_guild_info(packet, guild) do packet |> Out.encode_string(guild.name) |> Out.encode_short(guild.logo_bg) |> Out.encode_byte(guild.logo_bg_color) |> Out.encode_short(guild.logo) |> Out.encode_byte(guild.logo_color) end defp encode_ring_info(packet, nil) do packet |> Out.encode_int(0) |> Out.encode_int(0) |> Out.encode_int(0) end defp encode_ring_info(packet, rings) do # Encode 3 ring slots rings = rings || [] {r1, r2, r3} = case rings do [a, b, c | _] -> {a, b, c} [a, b] -> {a, b, nil} [a] -> {a, nil, nil} [] -> {nil, nil, nil} end packet |> encode_single_ring(r1) |> encode_single_ring(r2) |> encode_single_ring(r3) end defp encode_single_ring(packet, nil) do Out.encode_int(packet, 0) end defp encode_single_ring(packet, ring) do packet |> Out.encode_int(1) |> Out.encode_long(ring.id) |> Out.encode_long(ring.partner_ring_id) |> Out.encode_int(ring.item_id) end defp encode_mount(packet, nil) do packet |> Out.encode_byte(0) |> Out.encode_byte(1) |> Out.encode_int(0) |> Out.encode_int(0) end defp encode_mount(packet, mount) do packet |> Out.encode_byte(1) |> Out.encode_int(mount.level) |> Out.encode_int(mount.exp) |> Out.encode_int(mount.fatigue) end defp encode_pet_item_info(packet, pet, _item, active) do # Encode full pet item info structure # This includes pet stats, name, level, closeness, fullness, flags, etc. packet = packet |> Out.encode_string(pet.name) |> Out.encode_byte(pet.level) |> Out.encode_short(pet.closeness) |> Out.encode_byte(pet.fullness) |> Out.encode_long(if active, do: pet.unique_id, else: 0) |> Out.encode_short(pet.inventory_position) |> Out.encode_int(pet.seconds_left) |> Out.encode_short(pet.flags) |> Out.encode_int(pet.pet_item_id) # Add equip info (3 slots: hat, saddle, decor) # For now, encode empty slots packet |> Out.encode_long(0) # Pet equip slot 1 |> Out.encode_long(0) # Pet equip slot 2 |> Out.encode_long(0) # Pet equip slot 3 end @doc """ Encodes pet info for character spawn packet. Called when spawning a player with their active pets. """ def encode_spawn_pets(packet, pets) do # Get summoned pets in slot order summoned_pets = pets |> Enum.filter(fn pet -> pet.summoned > 0 end) |> Enum.sort_by(fn pet -> pet.summoned end) # Encode 3 pet slots (0 = no pet) {pet1, rest1} = List.pop_at(summoned_pets, 0, nil) {pet2, rest2} = List.pop_at(rest1, 0, nil) {pet3, _} = List.pop_at(rest2, 0, nil) packet |> encode_single_pet_for_spawn(pet1) |> encode_single_pet_for_spawn(pet2) |> encode_single_pet_for_spawn(pet3) end defp encode_single_pet_for_spawn(packet, nil) do packet |> Out.encode_byte(0) # No pet end defp encode_single_pet_for_spawn(packet, pet) do packet |> Out.encode_byte(1) # Has pet |> Out.encode_int(pet.pet_item_id) |> Out.encode_string(pet.name) |> Out.encode_long(pet.unique_id) |> Out.encode_short(pet.position.x) |> Out.encode_short(pet.position.y) |> Out.encode_byte(pet.stance) |> Out.encode_int(pet.position.fh) |> Out.encode_byte(pet.level) |> Out.encode_short(pet.closeness) |> Out.encode_byte(pet.fullness) |> Out.encode_short(pet.flags) |> Out.encode_int(0) # Pet equip item ID 1 |> Out.encode_int(0) # Pet equip item ID 2 |> Out.encode_int(0) # Pet equip item ID 3 |> Out.encode_int(0) # Pet equip item ID 4 end # ============================================================================= # Item/Quest Packets # ============================================================================= @doc """ Show item gain effect. Ported from MaplePacketCreator.getShowItemGain() """ def show_item_gain(item_id, quantity, in_chat \\ true) do packet = Out.new(Opcodes.lp_show_status_info()) |> Out.encode_byte(0) # 0 = item |> Out.encode_int(item_id) |> Out.encode_int(quantity) if in_chat do packet |> Out.encode_bytes(<<0, 0, 0, 0, 0, 0>>) else packet end |> Out.to_data() end @doc """ Server notice packet (broadcast message). Ported from MaplePacketCreator.serverNotice() Types: - 0 = Notice (blue) - 1 = Popup (red) - 2 = Megaphone - 3 = Super Megaphone - 4 = Scrolling message (top) - 5 = System message (yellow) """ def server_notice(type, message) do Out.new(Opcodes.lp_event_msg()) |> Out.encode_byte(type) |> Out.encode_string(message) |> Out.to_data() end # ============================================================================= # Utility Functions # ============================================================================= defp korean_timestamp(millis) do ft_ut_offset = 116_444_592_000_000_000 if millis > 0 do trunc(millis * 10_000 + ft_ut_offset) else 150_842_304_000_000_000 # MAX_TIME end end defp current_date do now = DateTime.utc_now() now.year * 10000 + now.month * 100 + now.day end defp is_evan?(job), do: div(job, 100) == 22 defp is_resist?(job), do: div(job, 1000) == 3 defp is_mercedes?(job), do: div(job, 100) == 23 end