1214 lines
33 KiB
Elixir
1214 lines
33 KiB
Elixir
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
|
|
|
|
@doc """
|
|
Sends character information on login.
|
|
"""
|
|
def get_char_info(character, restored_buffs \\ []) do
|
|
# TODO: Full character encoding
|
|
# For now, send minimal info
|
|
Out.new(Opcodes.lp_set_field())
|
|
|> Out.encode_int(character.id)
|
|
|> Out.encode_byte(0) # Channel
|
|
|> Out.encode_byte(1) # Admin byte
|
|
|> Out.encode_byte(1) # Enabled
|
|
|> Out.encode_int(character.map)
|
|
|> Out.encode_byte(character.spawnpoint)
|
|
|> Out.encode_int(character.hp)
|
|
|> 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
|
|
|
|
@doc """
|
|
Spawns a player on the map.
|
|
Minimal implementation - will expand with equipment, buffs, etc.
|
|
"""
|
|
def spawn_player(oid, character_state) do
|
|
# Reference: MaplePacketCreator.spawnPlayerMapobject()
|
|
# This is a minimal implementation - full version needs equipment, buffs, etc.
|
|
|
|
Out.new(Opcodes.lp_spawn_player())
|
|
|> Out.encode_int(oid)
|
|
# Damage skin (custom client feature)
|
|
# |> Out.encode_int(0)
|
|
|> Out.encode_byte(character_state.level)
|
|
|> Out.encode_string(character_state.name)
|
|
# Ultimate Explorer name (empty for now)
|
|
|> Out.encode_string("")
|
|
# Guild info (no guild for now)
|
|
|> Out.encode_int(0)
|
|
|> Out.encode_int(0)
|
|
# Buff mask (no buffs for now - TODO: implement buffs)
|
|
# For now, send minimal buff data
|
|
|> encode_buff_mask()
|
|
# Foreign buff end
|
|
|> Out.encode_short(0)
|
|
# ITEM_EFFECT
|
|
|> Out.encode_int(0)
|
|
# CHAIR
|
|
|> Out.encode_int(0)
|
|
# Position
|
|
|> Out.encode_short(character_state.position.x)
|
|
|> Out.encode_short(character_state.position.y)
|
|
|> Out.encode_byte(character_state.position.stance)
|
|
# Foothold
|
|
|> Out.encode_short(character_state.position.foothold)
|
|
# Appearance (gender, skin, face, hair)
|
|
|> Out.encode_byte(character_state.gender)
|
|
|> Out.encode_byte(character_state.skin_color)
|
|
|> Out.encode_int(character_state.face)
|
|
# Mega - shown in rankings
|
|
|> Out.encode_byte(0)
|
|
# Equipment (TODO: implement proper equipment encoding)
|
|
|> encode_appearance_minimal(character_state)
|
|
# Driver ID / passenger ID (for mounts)
|
|
|> Out.encode_int(0)
|
|
# Chalkboard text
|
|
|> Out.encode_string("")
|
|
# Ring info (3 ring slots)
|
|
|> Out.encode_int(0)
|
|
|> Out.encode_int(0)
|
|
|> Out.encode_int(0)
|
|
# Marriage ring
|
|
|> Out.encode_int(0)
|
|
# Mount info (no mount for now)
|
|
|> encode_mount_minimal()
|
|
# Player shop (none for now)
|
|
|> Out.encode_byte(0)
|
|
# Admin byte
|
|
|> Out.encode_byte(0)
|
|
# Pet info (no pets for now)
|
|
|> encode_pets_minimal()
|
|
# Taming mob (none)
|
|
|> Out.encode_int(0)
|
|
# Mini game info
|
|
|> Out.encode_byte(0)
|
|
# Chalkboard
|
|
|> Out.encode_byte(0)
|
|
# New year cards
|
|
|> Out.encode_byte(0)
|
|
# Berserk
|
|
|> Out.encode_byte(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
|
|
|
|
# ============================================================================
|
|
# Helper Functions for Spawn Encoding
|
|
# ============================================================================
|
|
|
|
defp encode_buff_mask(packet) do
|
|
# Buff mask is an array of integers representing active buffs
|
|
# For GMS v342, this is typically 14-16 integers (56-64 bytes)
|
|
# For now, send all zeros (no buffs)
|
|
packet
|
|
|> Out.encode_bytes(<<0::size(14 * 32)-little>>)
|
|
end
|
|
|
|
defp encode_appearance_minimal(packet, character) do
|
|
# Equipment encoding:
|
|
# Map of slot -> item_id
|
|
# For minimal implementation, just show hair
|
|
packet
|
|
# Equipped items map (empty for now)
|
|
|> Out.encode_byte(0)
|
|
# Masked items map (empty for now)
|
|
|> Out.encode_byte(0)
|
|
# Weapon (cash weapon)
|
|
|> Out.encode_int(0)
|
|
# Hair
|
|
|> Out.encode_int(character.hair)
|
|
# Ears (12 bit encoding for multiple items)
|
|
|> Out.encode_int(0)
|
|
end
|
|
|
|
defp encode_mount_minimal(packet) do
|
|
packet
|
|
|> Out.encode_byte(0)
|
|
# Mount level
|
|
|> Out.encode_byte(1)
|
|
# Mount exp
|
|
|> Out.encode_int(0)
|
|
# Mount fatigue
|
|
|> Out.encode_int(0)
|
|
end
|
|
|
|
defp encode_pets_minimal(packet) do
|
|
# 3 pet slots
|
|
packet
|
|
|> Out.encode_byte(0)
|
|
|> Out.encode_byte(0)
|
|
|> Out.encode_byte(0)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Chat Packets
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
User chat packet.
|
|
Ported from LocalePacket.UserChat()
|
|
Reference: src/tools/packet/LocalePacket.java
|
|
"""
|
|
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
|
|
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)
|
|
|> then(fn packet ->
|
|
if spawn_type == -3 or spawn_type >= 0 do
|
|
Out.encode_int(packet, link)
|
|
else
|
|
packet
|
|
end
|
|
end)
|
|
# 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
|
|
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
|
|
Out.new(Opcodes.lp_kill_monster())
|
|
|> Out.encode_int(monster.oid)
|
|
|> Out.encode_byte(leave_type)
|
|
# If swallow type, encode swallower character ID
|
|
|> then(fn packet ->
|
|
if leave_type == 4 do
|
|
Out.encode_int(packet, -1)
|
|
else
|
|
packet
|
|
end
|
|
end)
|
|
|> Out.to_data()
|
|
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(0x9D) # BOSS_ENV opcode
|
|
|> 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
|
|
# ============================================================================
|
|
|
|
@doc false
|
|
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
|
|
|
|
@doc false
|
|
defp encode_mob_changed_stats(packet, _monster) do
|
|
# For GMS: encode byte 1 if stats are changed, 0 if not
|
|
# For now, assume no changed stats
|
|
packet
|
|
|> Out.encode_byte(0)
|
|
# If changed stats exist, encode:
|
|
# - hp (int)
|
|
# - mp (int)
|
|
# - exp (int)
|
|
# - watk (int)
|
|
# - matk (int)
|
|
# - PDRate (int)
|
|
# - MDRate (int)
|
|
# - acc (int)
|
|
# - eva (int)
|
|
# - pushed (int)
|
|
# - level (int)
|
|
end
|
|
|
|
@doc false
|
|
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 16 integers (64 bytes) for GMS
|
|
packet
|
|
|> Out.encode_bytes(<<0::size(16 * 32)-little>>)
|
|
# If status effects exist, encode for each effect:
|
|
# - nOption (short)
|
|
# - rOption (int) - skill ID | skill level << 16
|
|
# - tOption (short) - duration / 500
|
|
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
|
|
|
|
# ============================================================================
|
|
# Pet Encoding Helpers
|
|
# ============================================================================
|
|
|
|
@doc false
|
|
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
|
|
end
|