Files
odinsea-elixir/lib/odinsea/shop/packets.ex
2026-02-25 12:26:26 -07:00

712 lines
18 KiB
Elixir

defmodule Odinsea.Shop.Packets do
@moduledoc """
Cash Shop and MTS packet builders.
Ported from Java tools.packet.MTSCSPacket
"""
alias Odinsea.Net.Packet.Out
alias Odinsea.Net.Opcodes
alias Odinsea.Shop.CashItem
# Cash shop operation codes for responses
@cs_success 0x00
@cs_fail 0x01
# ==============================================================================
# Cash Shop Entry/Setup Packets
# ==============================================================================
@doc """
Sets up the cash shop for a player.
Sent when player enters the cash shop.
"""
def set_cash_shop(character) do
Out.new(Opcodes.lp_set_cash_shop())
|> encode_cash_shop_info(character)
|> Out.to_data()
end
@doc """
Encodes the full cash shop info structure.
"""
defp encode_cash_shop_info(packet, character) do
# Best items (featured items)
best_items = Odinsea.Shop.CashItemFactory.get_best_items()
packet
# encodeStock
|> encode_stock()
# encodeCategory
|> encode_categories()
# encodeBest
|> encode_best_items(best_items)
# encodeGateway
|> encode_gateway()
# encodeLimitGoods
|> encode_limit_goods()
# encodeZeroGoods
|> encode_zero_goods()
# encodeCategoryInfo
|> encode_category_info()
# Character info
|> encode_character_cash_info(character)
end
defp encode_stock(packet) do
# Stock counts - simplified, no limited stock items
Out.encode_short(packet, 0)
end
defp encode_categories(packet) do
categories = Odinsea.Shop.CashItemFactory.get_categories()
packet
|> Out.encode_short(length(categories))
|> then(fn pkt ->
Enum.reduce(categories, pkt, fn cat, p ->
p
|> Out.encode_byte(cat.category)
|> Out.encode_byte(cat.sub_category)
|> Out.encode_byte(cat.discount_rate)
end)
end)
end
defp encode_best_items(packet, items) do
# Best items for each category/gender combination
packet
|> Out.encode_short(5) # Category count
|> Out.encode_short(2) # Gender count (male/female)
|> Out.encode_short(1) # Items per cell
|> then(fn pkt ->
Enum.reduce(0..4, pkt, fn _i, p1 ->
Enum.reduce(0..1, p1, fn _j, p2 ->
# Featured item for this category/gender
item_sn = Enum.random(items) || 0
p2
|> Out.encode_int(item_sn)
|> Out.encode_short(10000) # Category SN
end)
end)
end)
end
defp encode_gateway(packet) do
# Gateway info - empty for now
Out.encode_byte(packet, 0)
end
defp encode_limit_goods(packet) do
# Limited goods - empty
Out.encode_short(packet, 0)
end
defp encode_zero_goods(packet) do
# Zero goods - empty
Out.encode_short(packet, 0)
end
defp encode_category_info(packet) do
# Category parent info
Out.encode_byte(packet, 0)
end
defp encode_character_cash_info(packet, character) do
packet
|> Out.encode_int(character.nx_cash || 0)
|> Out.encode_int(character.maple_points || 0)
|> Out.encode_int(character.id)
# Gift token - not implemented
|> Out.encode_int(0)
end
@doc """
Enables cash shop usage.
"""
def enable_cs_use(socket) do
packet =
Out.new(Opcodes.lp_cs_update())
|> encode_cs_update()
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
defp encode_cs_update(packet) do
# Flag indicating update type
Out.encode_byte(packet, @cs_success)
end
@doc """
Sends the player's cash inventory.
"""
def get_cs_inventory(socket, character) do
cash_items = character.cash_inventory || []
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x4A) # Operation code for inventory
|> encode_cash_items(cash_items)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
defp encode_cash_items(packet, items) do
packet
|> Out.encode_short(length(items))
|> then(fn pkt ->
Enum.reduce(items, pkt, fn item, p ->
encode_cash_item(p, item)
end)
end)
end
defp encode_cash_item(packet, item) do
packet
|> Out.encode_long(item.unique_id)
|> Out.encode_int(item.item_id)
|> Out.encode_int(item.sn || 0)
|> Out.encode_short(item.quantity)
|> Out.encode_string(item.gift_from || "")
|> Out.encode_long(item.expiration || -1)
end
@doc """
Shows NX and Maple Point balances.
"""
def show_nx_maple_tokens(socket, character) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x3D) # Operation code for balance
|> Out.encode_int(character.nx_cash || 0)
|> Out.encode_int(character.maple_points || 0)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
# ==============================================================================
# Purchase Response Packets
# ==============================================================================
@doc """
Shows a successfully purchased cash item.
"""
def show_bought_cs_item(socket, item, sn, account_id) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x53) # Bought item operation
|> Out.encode_int(account_id)
|> encode_bought_item(item, sn)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
defp encode_bought_item(packet, item, sn) do
packet
|> Out.encode_long(item.unique_id)
|> Out.encode_int(item.item_id)
|> Out.encode_int(sn)
|> Out.encode_short(item.quantity)
|> Out.encode_string(item.gift_from || "")
|> Out.encode_long(item.expiration || -1)
end
@doc """
Shows a successfully purchased package.
"""
def show_bought_cs_package(socket, items, account_id) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x5D) # Package operation
|> Out.encode_int(account_id)
|> Out.encode_short(length(items))
|> then(fn pkt ->
Enum.reduce(items, pkt, fn item, p ->
encode_bought_item(p, item, item.sn)
end)
end)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Shows a successfully purchased quest item.
"""
def show_bought_cs_quest_item(socket, item, position) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x73) # Quest item operation
|> Out.encode_int(item.price)
|> Out.encode_short(item.count)
|> Out.encode_short(position)
|> Out.encode_int(item.item_id)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Confirms item moved from cash inventory to regular inventory.
"""
def confirm_from_cs_inventory(socket, item, position) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x69) # From CS inventory
|> Out.encode_byte(Odinsea.Game.InventoryType.get_type(
Odinsea.Game.InventoryType.from_item_id(item.item_id)
))
|> Out.encode_short(position)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Confirms item moved to cash inventory.
"""
def confirm_to_cs_inventory(socket, item, account_id) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x5F) # To CS inventory
|> Out.encode_int(account_id)
|> encode_cash_item_single(item)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
defp encode_cash_item_single(packet, item) do
packet
|> Out.encode_long(item.unique_id)
|> Out.encode_int(item.item_id)
|> Out.encode_int(item.sn || 0)
|> Out.encode_short(item.quantity)
|> Out.encode_string(item.gift_from || "")
end
# ==============================================================================
# Gift Packets
# ==============================================================================
@doc """
Sends gift confirmation.
"""
def send_gift(socket, price, item_id, count, partner) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x5B) # Gift sent operation
|> Out.encode_int(price)
|> Out.encode_int(item_id)
|> Out.encode_short(count)
|> Out.encode_string(partner)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Gets gifts for the player.
"""
def get_cs_gifts(socket, gifts) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x48) # Gifts operation
|> Out.encode_short(length(gifts))
|> then(fn pkt ->
Enum.reduce(gifts, pkt, fn {item, msg}, p ->
p
|> Out.encode_int(item.sn)
|> Out.encode_string(item.gift_from)
|> Out.encode_string(msg)
|> encode_cash_item_single(item)
end)
end)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
# ==============================================================================
# Wishlist Packets
# ==============================================================================
@doc """
Sends the wishlist to the player.
"""
def send_wishlist(socket, _character, wishlist) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x4D) # Wishlist operation
|> then(fn pkt ->
Enum.reduce(wishlist, pkt, fn sn, p ->
Out.encode_int(p, sn)
end)
end)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
# ==============================================================================
# Coupon Packets
# ==============================================================================
@doc """
Shows coupon redemption result.
"""
def show_coupon_redeemed(socket, items, maple_points, mesos, client_state) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x4B) # Coupon operation
|> Out.encode_byte(if safe_map_size(items) > 0, do: 1, else: 0)
|> Out.encode_int(safe_map_size(items))
|> then(fn pkt ->
Enum.reduce(items, pkt, fn {_sn, item}, p ->
encode_coupon_item(p, item, client_state)
end)
end)
|> Out.encode_int(mesos)
|> Out.encode_int(maple_points)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
defp encode_coupon_item(packet, item, _client_state) do
packet
|> Out.encode_int(item.sn)
|> Out.encode_byte(0) # Unknown
|> Out.encode_int(item.item_id)
|> Out.encode_int(item.count)
end
@doc """
Redeem response (simplified).
"""
def redeem_response(socket) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0xA1) # Redeem response
|> Out.encode_int(0)
|> Out.encode_int(0)
|> Out.encode_int(0)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
# ==============================================================================
# Error Packets
# ==============================================================================
@doc """
Sends a cash shop failure code.
"""
def send_cs_fail(socket, code) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x49) # Fail operation
|> Out.encode_byte(code)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Notifies that a cash item has expired.
"""
def cash_item_expired(socket, unique_id) do
packet =
Out.new(Opcodes.lp_cs_update())
|> Out.encode_byte(0x4E) # Expired operation
|> Out.encode_long(unique_id)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
# ==============================================================================
# MTS Packets
# ==============================================================================
@doc """
Starts the MTS for a player.
"""
def start_mts(character) do
Out.new(Opcodes.lp_set_mts_opened())
|> encode_mts_info(character)
|> Out.to_data()
end
defp encode_mts_info(packet, character) do
packet
|> encode_mts_tax_rates()
|> encode_character_mts_info(character)
end
defp encode_mts_tax_rates(packet) do
# Tax rates for different price ranges
packet
|> Out.encode_int(5) # Number of brackets
# Bracket 1: 0-5000000
|> Out.encode_int(0)
|> Out.encode_int(5_000_000)
|> Out.encode_int(10) # 10% tax
# Bracket 2: 5000000-10000000
|> Out.encode_int(5_000_001)
|> Out.encode_int(10_000_000)
|> Out.encode_int(9)
# Bracket 3: 10000000-50000000
|> Out.encode_int(10_000_001)
|> Out.encode_int(50_000_000)
|> Out.encode_int(8)
# Bracket 4: 50000000-100000000
|> Out.encode_int(50_000_001)
|> Out.encode_int(100_000_000)
|> Out.encode_int(7)
# Bracket 5: 100000000+
|> Out.encode_int(100_000_001)
|> Out.encode_int(999_999_999)
|> Out.encode_int(6)
end
defp encode_character_mts_info(packet, character) do
packet
|> Out.encode_int(character.nx_cash || 0)
|> Out.encode_int(character.maple_points || 0)
|> Out.encode_int(character.id)
end
@doc """
Shows MTS cash balance.
"""
def show_mts_cash(socket, character) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x17) # Show cash
|> Out.encode_int(character.nx_cash || 0)
|> Out.encode_int(character.maple_points || 0)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Sends current MTS listings.
"""
def send_current_mts(socket, cart) do
items = Odinsea.Shop.MTS.get_current_mts(cart)
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x16) # Current MTS
|> Out.encode_int(length(items))
|> Out.encode_int(0) # Total count (for pagination)
|> Out.encode_int(cart.page)
|> Out.encode_int(cart.tab)
|> then(fn pkt ->
Enum.reduce(items, pkt, fn item, p ->
encode_mts_item(p, item)
end)
end)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Sends "not yet sold" listings.
"""
def send_not_yet_sold(socket, cart) do
items = Odinsea.Shop.MTS.get_not_yet_sold(cart.character_id)
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x18) # Not yet sold
|> Out.encode_int(length(items))
|> then(fn pkt ->
Enum.reduce(items, pkt, fn item, p ->
encode_mts_item(p, item)
end)
end)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Sends transfer inventory.
"""
def send_transfer(socket, cart, changed \\ false) do
items = Odinsea.Shop.MTS.get_transfer(cart.character_id)
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x19) # Transfer
|> Out.encode_byte(if changed, do: 1, else: 0)
|> Out.encode_int(length(items))
|> then(fn pkt ->
Enum.reduce(items, pkt, fn item, p ->
encode_mts_transfer_item(p, item)
end)
end)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
defp encode_mts_item(packet, item) do
packet
|> Out.encode_int(item.id)
|> Out.encode_int(item.item.item_id)
|> Out.encode_int(item.price)
|> Out.encode_int(item.price) # Current price (can change)
|> Out.encode_int(item.seller_id)
|> Out.encode_string(item.seller_name)
|> Out.encode_long(item.expiration)
|> encode_item_stats(item.item)
end
defp encode_mts_transfer_item(packet, item) do
packet
|> Out.encode_int(item.item_id)
|> Out.encode_short(item.quantity)
|> encode_item_stats(item)
end
defp encode_item_stats(packet, item) do
# Full item encoding with stats
# This is simplified - full version would encode equipment stats
packet
|> Out.encode_byte(0) # Has stats flag
end
@doc """
MTS wanted listing over.
"""
def get_mts_wanted_listing_over(socket, nx, maple_points) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x1A)
|> Out.encode_int(nx)
|> Out.encode_int(maple_points)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
MTS confirm sell.
"""
def get_mts_confirm_sell(socket) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x02)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
MTS fail sell.
"""
def get_mts_fail_sell(socket) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x03)
|> Out.encode_byte(0) # Error code
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
MTS confirm cancel.
"""
def get_mts_confirm_cancel(socket) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x08)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
MTS fail cancel.
"""
def get_mts_fail_cancel(socket) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x09)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
MTS confirm buy.
"""
def get_mts_confirm_buy(socket) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x0C)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
MTS fail buy.
"""
def get_mts_fail_buy(socket) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x0D)
|> Out.encode_byte(0)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
MTS confirm transfer.
"""
def get_mts_confirm_transfer(socket, inv_type, position) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x11)
|> Out.encode_byte(inv_type)
|> Out.encode_short(position)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
@doc """
Add to cart message.
"""
def add_to_cart_message(socket, failed, deleted) do
packet =
Out.new(Opcodes.lp_mts_operation())
|> Out.encode_byte(0x15)
|> Out.encode_byte(if failed, do: 0, else: 1)
|> Out.encode_byte(if deleted, do: 0, else: 1)
|> Out.to_data()
:gen_tcp.send(socket, packet)
end
# ==============================================================================
# Utility Functions
# ==============================================================================
defp safe_map_size(map) when is_map(map), do: Kernel.map_size(map)
defp safe_map_size(_), do: 0
end