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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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_cash_shop_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