defmodule Odinsea.Channel.Handler.PlayerShop do @moduledoc """ Handles player shop and hired merchant packets. Ported from: - src/handling/channel/handler/PlayerInteractionHandler.java - src/handling/channel/handler/HiredMerchantHandler.java Handles: - Creating player shops and mini games - Visiting shops - Buying/selling items - Managing visitors - Mini game operations (Omok, Match Card) - Hired merchant operations """ require Logger alias Odinsea.Net.Packet.{In, Out} alias Odinsea.Net.Opcodes alias Odinsea.Game.{PlayerShop, HiredMerchant, MiniGame, ShopItem, Item, Equip} # Interaction action constants (from PlayerInteractionHandler.Interaction enum) # GMS v342 values @action_create 0x06 @action_invite_trade 0x11 @action_deny_trade 0x12 @action_visit 0x09 @action_chat 0x14 @action_exit 0x18 @action_open 0x16 @action_set_items 0x00 @action_set_meso 0x01 @action_confirm_trade 0x02 @action_player_shop_add_item 0x28 @action_buy_item_player_shop 0x22 @action_add_item 0x23 @action_buy_item_store 0x24 @action_buy_item_hired_merchant 0x26 @action_remove_item 0x28 @action_maintenance_off 0x29 @action_maintenance_organise 0x30 @action_close_merchant 0x31 @action_admin_store_namechange 0x35 @action_view_merchant_visitor 0x36 @action_view_merchant_blacklist 0x37 @action_merchant_blacklist_add 0x38 @action_merchant_blacklist_remove 0x39 @action_request_tie 0x51 @action_answer_tie 0x52 @action_give_up 0x53 @action_request_redo 0x55 @action_answer_redo 0x56 @action_exit_after_game 0x57 @action_cancel_exit 0x58 @action_ready 0x59 @action_un_ready 0x60 @action_expel 0x61 @action_start 0x62 @action_skip 0x64 @action_move_omok 0x65 @action_select_card 0x68 # Create type constants @create_type_trade 3 @create_type_omok 1 @create_type_match_card 2 @create_type_player_shop 4 @create_type_hired_merchant 5 @doc """ Main handler for player interaction packets. """ def handle_interaction(packet, client_state) do {action, packet} = In.decode_byte(packet) case action do @action_create -> handle_create(packet, client_state) @action_visit -> handle_visit(packet, client_state) @action_chat -> handle_chat(packet, client_state) @action_exit -> handle_exit(packet, client_state) @action_open -> handle_open(packet, client_state) @action_player_shop_add_item -> handle_add_item(packet, client_state) @action_add_item -> handle_add_item(packet, client_state) @action_buy_item_player_shop -> handle_buy_item(packet, client_state) @action_buy_item_store -> handle_buy_item(packet, client_state) @action_buy_item_hired_merchant -> handle_buy_item(packet, client_state) @action_remove_item -> handle_remove_item(packet, client_state) @action_maintenance_off -> handle_maintenance_off(packet, client_state) @action_maintenance_organise -> handle_maintenance_organise(packet, client_state) @action_close_merchant -> handle_close_merchant(packet, client_state) @action_view_merchant_visitor -> handle_view_visitors(packet, client_state) @action_view_merchant_blacklist -> handle_view_blacklist(packet, client_state) @action_merchant_blacklist_add -> handle_blacklist_add(packet, client_state) @action_merchant_blacklist_remove -> handle_blacklist_remove(packet, client_state) @action_ready -> handle_ready(packet, client_state) @action_un_ready -> handle_ready(packet, client_state) @action_start -> handle_start_game(packet, client_state) @action_give_up -> handle_give_up(packet, client_state) @action_request_tie -> handle_request_tie(packet, client_state) @action_answer_tie -> handle_answer_tie(packet, client_state) @action_skip -> handle_skip(packet, client_state) @action_move_omok -> handle_move_omok(packet, client_state) @action_select_card -> handle_select_card(packet, client_state) @action_exit_after_game -> handle_exit_after_game(packet, client_state) @action_cancel_exit -> handle_exit_after_game(packet, client_state) _ -> Logger.debug("Unhandled player interaction action: #{action}") send_enable_actions(client_state) {:ok, client_state} end end @doc """ Handles hired merchant specific packets. """ def handle_hired_merchant(packet, client_state) do {operation, packet} = In.decode_byte(packet) case operation do # Display Fredrick/Merchant item store 20 -> handle_display_merch(client_state) # Open merch item store 25 -> handle_open_merch_store(client_state) # Retrieve items 26 -> handle_retrieve_items(packet, client_state) # Close dialog 27 -> send_enable_actions(client_state) {:ok, client_state} _ -> Logger.debug("Unhandled hired merchant operation: #{operation}") send_enable_actions(client_state) {:ok, client_state} end end # ============================================================================ # Create Shop/Game Handlers # ============================================================================ defp handle_create(packet, client_state) do {create_type, packet} = In.decode_byte(packet) {description, packet} = In.decode_string(packet) {has_password, packet} = In.decode_byte(packet) password = if has_password > 0 do {pwd, packet} = In.decode_string(packet) pwd else "" end case create_type do @create_type_omok -> {piece, _packet} = In.decode_byte(packet) create_mini_game(client_state, description, password, MiniGame.game_type_omok(), piece) @create_type_match_card -> {piece, _packet} = In.decode_byte(packet) create_mini_game(client_state, description, password, MiniGame.game_type_match_card(), piece) @create_type_player_shop -> # Skip slot and item ID validation for now create_player_shop(client_state, description) @create_type_hired_merchant -> create_hired_merchant(client_state, description) _ -> send_enable_actions(client_state) {:ok, client_state} end end defp create_mini_game(client_state, description, password, game_type, piece_type) do with {:ok, character} <- get_character(client_state) do game_opts = %{ id: generate_id(), owner_id: character.id, owner_name: character.name, description: description, password: password, game_type: game_type, piece_type: piece_type, map_id: character.map_id, channel: client_state.channel } # Start the mini game GenServer case DynamicSupervisor.start_child( Odinsea.MiniGameSupervisor, {MiniGame, game_opts} ) do {:ok, _pid} -> # Send mini game packet packet = encode_mini_game(game_opts) send_packet(client_state, packet) send_enable_actions(client_state) {:ok, %{client_state | player_shop: game_opts.id}} {:error, reason} -> Logger.error("Failed to create mini game: #{inspect(reason)}") send_enable_actions(client_state) {:ok, client_state} end else _ -> send_enable_actions(client_state) {:ok, client_state} end end defp create_player_shop(client_state, description) do with {:ok, character} <- get_character(client_state) do shop_opts = %{ id: generate_id(), owner_id: character.id, owner_account_id: character.account_id, owner_name: character.name, item_id: 5_040_000, description: description, map_id: character.map_id, channel: client_state.channel } case DynamicSupervisor.start_child( Odinsea.ShopSupervisor, {PlayerShop, shop_opts} ) do {:ok, _pid} -> # Send player shop packet packet = encode_player_shop(shop_opts, true) send_packet(client_state, packet) send_enable_actions(client_state) {:ok, %{client_state | player_shop: shop_opts.id}} {:error, reason} -> Logger.error("Failed to create player shop: #{inspect(reason)}") send_enable_actions(client_state) {:ok, client_state} end else _ -> send_enable_actions(client_state) {:ok, client_state} end end defp create_hired_merchant(client_state, description) do with {:ok, character} <- get_character(client_state) do # Check if already has a merchant # In full implementation, check world for existing merchant merchant_opts = %{ id: generate_id(), owner_id: character.id, owner_account_id: character.account_id, owner_name: character.name, item_id: 5_030_000, description: description, map_id: character.map_id, channel: client_state.channel } case DynamicSupervisor.start_child( Odinsea.MerchantSupervisor, {HiredMerchant, merchant_opts} ) do {:ok, _pid} -> # Send hired merchant packet packet = encode_hired_merchant(merchant_opts, true) send_packet(client_state, packet) send_enable_actions(client_state) {:ok, %{client_state | player_shop: merchant_opts.id}} {:error, reason} -> Logger.error("Failed to create hired merchant: #{inspect(reason)}") send_enable_actions(client_state) {:ok, client_state} end else _ -> send_enable_actions(client_state) {:ok, client_state} end end # ============================================================================ # Visit/Exit Handlers # ============================================================================ defp handle_visit(packet, client_state) do {object_id, packet} = In.decode_int(packet) # Try to find shop by object ID # This would need proper map object tracking # For now, simplified version Logger.debug("Visit shop: object_id=#{object_id}") send_enable_actions(client_state) {:ok, client_state} end defp handle_exit(_packet, client_state) do # Close player shop or mini game if client_state.player_shop do # Clean up {:ok, %{client_state | player_shop: nil}} else {:ok, client_state} end end # ============================================================================ # Shop Management Handlers # ============================================================================ defp handle_open(_packet, client_state) do with {:ok, character} <- get_character(client_state), shop_id <- client_state.player_shop, true <- shop_id != nil do # Try player shop first, then hired merchant case PlayerShop.set_open(shop_id, true) do :ok -> PlayerShop.set_available(shop_id, true) :ok {:error, :not_found} -> HiredMerchant.set_open(shop_id, true) HiredMerchant.set_available(shop_id, true) :ok end send_enable_actions(client_state) {:ok, client_state} else _ -> send_enable_actions(client_state) {:ok, client_state} end end defp handle_add_item(packet, client_state) do {inv_type, packet} = In.decode_byte(packet) {slot, packet} = In.decode_short(packet) {bundles, packet} = In.decode_short(packet) {per_bundle, packet} = In.decode_short(packet) {price, _packet} = In.decode_int(packet) with {:ok, character} <- get_character(client_state), shop_id <- client_state.player_shop, true <- shop_id != nil do # Get item from inventory # Create shop item item = %ShopItem{ item: %Item{item_id: 400_0000, quantity: per_bundle}, bundles: bundles, price: price } # Add to shop case PlayerShop.add_item(shop_id, item) do :ok -> # Send item update packet send_shop_item_update(client_state, shop_id) _ -> :ok end send_enable_actions(client_state) {:ok, client_state} else _ -> send_enable_actions(client_state) {:ok, client_state} end end defp handle_buy_item(packet, client_state) do {item_slot, packet} = In.decode_byte(packet) {quantity, _packet} = In.decode_short(packet) with {:ok, character} <- get_character(client_state), shop_id <- client_state.player_shop, true <- shop_id != nil do # Try player shop buy case PlayerShop.buy_item(shop_id, item_slot, quantity, character.id, character.name) do {:ok, item, price, status} -> # Deduct meso and add item # Send update packet send_shop_item_update(client_state, shop_id) if status == :close do # Shop closed (all items sold) send_shop_error_message(client_state, 10, 1) end {:error, reason} -> Logger.debug("Buy item failed: #{reason}") end send_enable_actions(client_state) {:ok, client_state} else _ -> send_enable_actions(client_state) {:ok, client_state} end end defp handle_remove_item(packet, client_state) do {slot, _packet} = In.decode_short(packet) with {:ok, _character} <- get_character(client_state), shop_id <- client_state.player_shop, true <- shop_id != nil do case PlayerShop.remove_item(shop_id, slot) do {:ok, _item} -> send_shop_item_update(client_state, shop_id) _ -> :ok end send_enable_actions(client_state) {:ok, client_state} else _ -> send_enable_actions(client_state) {:ok, client_state} end end # ============================================================================ # Hired Merchant Specific Handlers # ============================================================================ defp handle_maintenance_off(_packet, client_state) do with shop_id <- client_state.player_shop, true <- shop_id != nil do HiredMerchant.set_open(shop_id, true) end send_enable_actions(client_state) {:ok, client_state} end defp handle_maintenance_organise(_packet, client_state) do with shop_id <- client_state.player_shop, true <- shop_id != nil do # Clean up sold out items and give meso # This is a simplified version :ok end send_enable_actions(client_state) {:ok, client_state} end defp handle_close_merchant(_packet, client_state) do with shop_id <- client_state.player_shop, true <- shop_id != nil do HiredMerchant.close_merchant(shop_id, true, true) # Send Fredrick message send_drop_message(client_state, 1, "Please visit Fredrick for your items.") end send_enable_actions(client_state) {:ok, %{client_state | player_shop: nil}} end defp handle_view_visitors(_packet, client_state) do with shop_id <- client_state.player_shop, true <- shop_id != nil, visitors <- HiredMerchant.get_visitors(shop_id) do packet = encode_visitor_view(visitors) send_packet(client_state, packet) end send_enable_actions(client_state) {:ok, client_state} end defp handle_view_blacklist(_packet, client_state) do with shop_id <- client_state.player_shop, true <- shop_id != nil, blacklist <- HiredMerchant.get_blacklist(shop_id) do packet = encode_blacklist_view(blacklist) send_packet(client_state, packet) end send_enable_actions(client_state) {:ok, client_state} end defp handle_blacklist_add(packet, client_state) do {name, _packet} = In.decode_string(packet) with shop_id <- client_state.player_shop, true <- shop_id != nil do HiredMerchant.add_to_blacklist(shop_id, name) end send_enable_actions(client_state) {:ok, client_state} end defp handle_blacklist_remove(packet, client_state) do {name, _packet} = In.decode_string(packet) with shop_id <- client_state.player_shop, true <- shop_id != nil do HiredMerchant.remove_from_blacklist(shop_id, name) end send_enable_actions(client_state) {:ok, client_state} end # ============================================================================ # Mini Game Handlers # ============================================================================ defp handle_ready(_packet, client_state) do with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do MiniGame.set_ready(game_id, character.id) # Send ready update end send_enable_actions(client_state) {:ok, client_state} end defp handle_start_game(_packet, client_state) do with game_id <- client_state.player_shop, true <- game_id != nil do case MiniGame.start_game(game_id) do {:ok, loser} -> # Send game start packet send_game_start(client_state, loser) {:error, :not_ready} -> :ok end end send_enable_actions(client_state) {:ok, client_state} end defp handle_give_up(_packet, client_state) do with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do case MiniGame.give_up(game_id, character.id) do {:give_up, winner} -> # Send game result send_game_result(client_state, 0, winner) _ -> :ok end end send_enable_actions(client_state) {:ok, client_state} end defp handle_request_tie(_packet, client_state) do with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do MiniGame.request_tie(game_id, character.id) end send_enable_actions(client_state) {:ok, client_state} end defp handle_answer_tie(packet, client_state) do {accept, _packet} = In.decode_byte(packet) with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do case MiniGame.answer_tie(game_id, character.id, accept > 0) do {:tie, _} -> send_game_result(client_state, 1, 0) {:deny, _} -> send_deny_tie(client_state) _ -> :ok end end send_enable_actions(client_state) {:ok, client_state} end defp handle_skip(_packet, client_state) do with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do MiniGame.skip_turn(game_id, character.id) end send_enable_actions(client_state) {:ok, client_state} end defp handle_move_omok(packet, client_state) do {x, packet} = In.decode_int(packet) {y, packet} = In.decode_int(packet) {piece_type, _packet} = In.decode_byte(packet) with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do case MiniGame.make_omok_move(game_id, character.id, x, y, piece_type) do {:ok, _won} -> # Broadcast move to all players :ok {:win, winner} -> send_game_result(client_state, 2, winner) {:error, reason} -> Logger.debug("Omok move failed: #{reason}") end end send_enable_actions(client_state) {:ok, client_state} end defp handle_select_card(packet, client_state) do {slot, _packet} = In.decode_byte(packet) with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do case MiniGame.select_card(game_id, character.id, slot) do {:first_card, _} -> :ok {:match, _} -> :ok {:no_match, _} -> :ok {:game_win, winner} -> send_game_result(client_state, 2, winner) _ -> :ok end end send_enable_actions(client_state) {:ok, client_state} end defp handle_exit_after_game(_packet, client_state) do with game_id <- client_state.player_shop, true <- game_id != nil, {:ok, character} <- get_character(client_state) do MiniGame.set_exit_after(game_id, character.id) end send_enable_actions(client_state) {:ok, client_state} end # ============================================================================ # Chat Handler # ============================================================================ defp handle_chat(packet, client_state) do {_tick, packet} = In.decode_int(packet) {message, _packet} = In.decode_string(packet) with {:ok, character} <- get_character(client_state), shop_id <- client_state.player_shop, true <- shop_id != nil do # Broadcast to all visitors packet = encode_shop_chat(character.name, message) PlayerShop.broadcast_to_visitors(shop_id, packet, true) end {:ok, client_state} end # ============================================================================ # Fredrick/Merch Store Handlers # ============================================================================ defp handle_display_merch(client_state) do # Check for stored items # For now, return empty send_enable_actions(client_state) {:ok, client_state} end defp handle_open_merch_store(client_state) do # Open the Fredrick item store dialog packet = encode_merch_item_store() send_packet(client_state, packet) send_enable_actions(client_state) {:ok, client_state} end defp handle_retrieve_items(_packet, client_state) do # Retrieve items from Fredrick # For now, just acknowledge send_enable_actions(client_state) {:ok, client_state} end # ============================================================================ # Packet Encoders # ============================================================================ defp encode_player_shop(shop, is_owner) do # Player shop packet Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x05) # Shop type |> Out.encode_byte(PlayerShop.shop_type()) |> Out.encode_int(shop.id) |> Out.encode_string(shop.owner_name) |> Out.encode_string(shop.description) |> Out.encode_byte(0) # Password flag |> Out.encode_byte(length(shop.items)) |> encode_shop_items(shop.items) |> Out.encode_byte(if is_owner, do: 0, else: 1) |> Out.to_data() end defp encode_hired_merchant(merchant, is_owner) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x05) |> Out.encode_byte(HiredMerchant.shop_type()) |> Out.encode_int(merchant.id) |> Out.encode_string(merchant.owner_name) |> Out.encode_string(merchant.description) |> Out.encode_byte(0) |> Out.encode_int(0) # Time remaining |> Out.encode_byte(0) # Visitor count |> Out.encode_byte(0) # Has items |> Out.encode_byte(if is_owner, do: 0, else: 1) |> Out.to_data() end defp encode_mini_game(game) do game_type = case game.game_type do 1 -> MiniGame.shop_type(%{game_type: 1}) 2 -> MiniGame.shop_type(%{game_type: 2}) end Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x05) |> Out.encode_byte(game_type) |> Out.encode_int(game.id) |> Out.encode_string(game.owner_name) |> Out.encode_string(game.description) |> Out.encode_byte(if game.password != "", do: 1, else: 0) |> Out.encode_byte(0) # Piece type |> Out.encode_byte(1) # Is owner |> Out.encode_byte(0) # Loser |> Out.encode_byte(0) # Turn |> Out.to_data() end defp encode_shop_items(packet, items) do Enum.reduce(items, packet, fn item, p -> p |> Out.encode_short(item.bundles) |> Out.encode_short(item.item.quantity) |> Out.encode_int(item.price) |> encode_item(item.item) end) end defp encode_item(packet, %Item{} = item) do packet |> Out.encode_byte(2) # Item type |> Out.encode_int(item.item_id) |> Out.encode_byte(0) # Has cash ID |> Out.encode_long(0) # Expiration |> Out.encode_short(item.quantity) |> Out.encode_string(item.owner) end defp encode_item(packet, %Equip{} = equip) do packet |> Out.encode_byte(1) # Equip type |> Out.encode_int(equip.item_id) |> Out.encode_byte(0) # Has cash ID |> Out.encode_long(0) # Expiration # Equipment stats would go here |> Out.encode_bytes(<<0::size(100)-unit(8)>>) end defp encode_shop_chat(name, message) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x06) # Chat |> Out.encode_byte(0) # Slot |> Out.encode_string("#{name} : #{message}") |> Out.to_data() end defp encode_shop_item_update(shop) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x07) # Update |> encode_shop_items(shop.items) |> Out.to_data() end defp encode_visitor_view(visitors) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x0A) # Visitor view |> Out.encode_byte(length(visitors)) |> encode_visitor_list(visitors) |> Out.to_data() end defp encode_visitor_list(packet, visitors) do Enum.reduce(visitors, packet, fn name, p -> p |> Out.encode_string(name) |> Out.encode_long(0) # Visit time end) end defp encode_blacklist_view(blacklist) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x0B) # Blacklist view |> Out.encode_byte(length(blacklist)) |> encode_string_list(blacklist) |> Out.to_data() end defp encode_string_list(packet, strings) do Enum.reduce(strings, packet, fn str, p -> Out.encode_string(p, str) end) end defp encode_game_start(loser) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x0C) # Start |> Out.encode_byte(loser) |> Out.to_data() end defp encode_game_result(result_type, winner) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x0D) # Result |> Out.encode_byte(result_type) # 0 = give up, 1 = tie, 2 = win |> Out.encode_byte(winner) |> Out.to_data() end defp encode_deny_tie do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x0E) # Deny tie |> Out.to_data() end defp encode_merch_item_store do Out.new(Opcodes.lp_merch_item_store()) |> Out.encode_byte(0x24) |> Out.to_data() end defp send_shop_item_update(client_state, shop_id) do # Get shop state and send update case PlayerShop.get_state(shop_id) do {:error, _} -> case HiredMerchant.get_state(shop_id) do {:error, _} -> :ok state -> send_packet(client_state, encode_shop_item_update(state)) end state -> send_packet(client_state, encode_shop_item_update(state)) end end defp send_game_start(client_state, loser) do packet = encode_game_start(loser) send_packet(client_state, packet) end defp send_game_result(client_state, result_type, winner) do packet = encode_game_result(result_type, winner) send_packet(client_state, packet) end defp send_deny_tie(client_state) do packet = encode_deny_tie() send_packet(client_state, packet) end defp send_shop_error_message(client_state, error, msg_type) do Out.new(Opcodes.lp_player_interaction()) |> Out.encode_byte(0x0A) # Error |> Out.encode_byte(error) |> Out.encode_byte(msg_type) |> Out.to_data() |> then(&send_packet(client_state, &1)) end defp send_drop_message(client_state, msg_type, message) do Out.new(Opcodes.lp_blow_weather()) |> Out.encode_int(msg_type) |> Out.encode_string(message) |> Out.to_data() |> then(&send_packet(client_state, &1)) end # ============================================================================ # Private Helper Functions # ============================================================================ defp get_character(client_state) do case client_state.character_id do nil -> {:error, :no_character} character_id -> case Registry.lookup(Odinsea.CharacterRegistry, character_id) do [{pid, _}] -> case Odinsea.Game.Character.get_state(pid) do {:ok, state} -> {:ok, state} error -> error end [] -> {:error, :character_not_found} end end end defp send_packet(client_state, data) do if client_state.client_pid do send(client_state.client_pid, {:send_packet, data}) end end defp send_enable_actions(client_state) do packet = <<0x0D, 0x00, 0x00>> send_packet(client_state, packet) end defp generate_id do :erlang.unique_integer([:positive]) end end