Start repo, claude & kimi still vibing tho

This commit is contained in:
ra
2026-02-14 17:04:21 -07:00
commit f5b8aeb39d
54 changed files with 9466 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
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
@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
end