defmodule Odinsea.Game.Movement.Path do @moduledoc """ MovePath for mob movement (newer movement system). Ported from Java MovePath.java This is an alternative movement system used by mobs in newer versions of MapleStory. It uses a more compact encoding. Structure: - Initial position (x, y, vx, vy) - List of movement elements - Optional passive data (keypad states, movement rect) """ import Bitwise alias Odinsea.Net.Packet.In defstruct [ :x, # Initial X position :y, # Initial Y position :vx, # Initial X velocity :vy, # Initial Y velocity elements: [], # List of MoveElem key_pad_states: [], # Keypad states (passive mode) move_rect: nil # Movement rectangle (passive mode) ] @type t :: %__MODULE__{ x: integer() | nil, y: integer() | nil, vx: integer() | nil, vy: integer() | nil, elements: list(MoveElem.t()), key_pad_states: list(integer()), move_rect: map() | nil } defmodule MoveElem do @moduledoc """ Individual movement element within a MovePath. """ @type t :: %__MODULE__{ attribute: integer(), # Movement type/attribute x: integer(), # X position y: integer(), # Y position vx: integer(), # X velocity vy: integer(), # Y velocity fh: integer(), # Foothold fall_start: integer(), # Fall start position offset_x: integer(), # X offset offset_y: integer(), # Y offset sn: integer(), # Skill/stat number move_action: integer(), # Move action/stance elapse: integer() # Elapsed time } defstruct [ :attribute, :x, :y, :vx, :vy, :fh, :fall_start, :offset_x, :offset_y, :sn, :move_action, :elapse ] end @doc """ Decodes a MovePath from a packet. ## Parameters - packet: The incoming packet - passive: Whether to decode passive data (keypad, rect) ## Returns %MovePath{} struct with decoded data """ def decode(packet, passive \\ false) do old_x = In.decode_short(packet) old_y = In.decode_short(packet) old_vx = In.decode_short(packet) old_vy = In.decode_short(packet) count = In.decode_byte(packet) {elements, final_x, final_y, final_vx, final_vy, _fh_last} = decode_elements(packet, count, old_x, old_y, old_vx, old_vy, []) path = %__MODULE__{ x: old_x, y: old_y, vx: old_vx, vy: old_vy, elements: Enum.reverse(elements) } if passive do {key_pad_states, move_rect} = decode_passive_data(packet) %{path | x: final_x, y: final_y, vx: final_vx, vy: final_vy, key_pad_states: key_pad_states, move_rect: move_rect } else %{path | x: final_x, y: final_y, vx: final_vx, vy: final_vy } end end @doc """ Encodes a MovePath to binary for packet output. """ def encode(%__MODULE__{} = path, _passive \\ false) do elements_data = Enum.map_join(path.elements, &encode_element/1) <> end @doc """ Gets the final position from the MovePath. """ def get_final_position(%__MODULE__{} = path) do case List.last(path.elements) do nil -> %{x: path.x, y: path.y} elem -> %{x: elem.x, y: elem.y} end end @doc """ Gets the final move action/stance from the MovePath. """ def get_final_action(%__MODULE__{} = path) do case List.last(path.elements) do nil -> 0 elem -> elem.move_action end end @doc """ Gets the final foothold from the MovePath. """ def get_final_foothold(%__MODULE__{} = path) do case List.last(path.elements) do nil -> 0 elem -> elem.fh end end # ============================================================================ # Private Functions # ============================================================================ defp decode_elements(_packet, 0, old_x, old_y, old_vx, old_vy, acc), do: {acc, old_x, old_y, old_vx, old_vy, 0} defp decode_elements(packet, count, old_x, old_y, old_vx, old_vy, acc) do attr = In.decode_byte(packet) {elem, new_x, new_y, new_vx, new_vy, _fh_last} = case attr do # Absolute with foothold a when a in [0, 6, 13, 15, 37, 38] -> x = In.decode_short(packet) y = In.decode_short(packet) vx = In.decode_short(packet) vy = In.decode_short(packet) fh = In.decode_short(packet) fall_start = if attr == 13, do: In.decode_short(packet), else: 0 offset_x = In.decode_short(packet) offset_y = In.decode_short(packet) elem = %MoveElem{ attribute: attr, x: x, y: y, vx: vx, vy: vy, fh: fh, fall_start: fall_start, offset_x: offset_x, offset_y: offset_y } {elem, x, y, vx, vy, fh} # Velocity only a when a in [1, 2, 14, 17, 19, 32, 33, 34, 35] -> vx = In.decode_short(packet) vy = In.decode_short(packet) elem = %MoveElem{ attribute: attr, x: old_x, y: old_y, vx: vx, vy: vy, fh: 0 } {elem, old_x, old_y, vx, vy, 0} # Position with foothold a when a in [3, 4, 5, 7, 8, 9, 11] -> x = In.decode_short(packet) y = In.decode_short(packet) fh = In.decode_short(packet) elem = %MoveElem{ attribute: attr, x: x, y: y, vx: 0, vy: 0, fh: fh } {elem, x, y, 0, 0, fh} # Stat change 10 -> sn = In.decode_byte(packet) elem = %MoveElem{ attribute: attr, sn: sn, x: old_x, y: old_y, vx: 0, vy: 0, fh: 0, elapse: 0, move_action: 0 } {elem, old_x, old_y, 0, 0, 0} # Start fall down 12 -> vx = In.decode_short(packet) vy = In.decode_short(packet) fall_start = In.decode_short(packet) elem = %MoveElem{ attribute: attr, x: old_x, y: old_y, vx: vx, vy: vy, fh: 0, fall_start: fall_start } {elem, old_x, old_y, vx, vy, 0} # Flying block 18 -> x = In.decode_short(packet) y = In.decode_short(packet) vx = In.decode_short(packet) vy = In.decode_short(packet) elem = %MoveElem{ attribute: attr, x: x, y: y, vx: vx, vy: vy, fh: 0 } {elem, x, y, vx, vy, 0} # No change (21-31) a when a in [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] -> elem = %MoveElem{ attribute: attr, x: old_x, y: old_y, vx: old_vx, vy: old_vy, fh: 0 } {elem, old_x, old_y, old_vx, old_vy, 0} # Special case 36 36 -> x = In.decode_short(packet) y = In.decode_short(packet) vx = In.decode_short(packet) vy = In.decode_short(packet) fh = In.decode_short(packet) elem = %MoveElem{ attribute: attr, x: x, y: y, vx: vx, vy: vy, fh: fh } {elem, x, y, vx, vy, fh} # Unknown attribute - skip gracefully _unknown -> elem = %MoveElem{ attribute: attr, x: old_x, y: old_y, vx: old_vx, vy: old_vy, fh: 0 } {elem, old_x, old_y, old_vx, old_vy, 0} end # Read move action and elapse (except for stat change) {elem, new_x, new_y, new_vx, new_vy} = if attr != 10 do move_action = In.decode_byte(packet) elapse = In.decode_short(packet) {%{elem | move_action: move_action, elapse: elapse }, elem.x, elem.y, elem.vx, elem.vy} else {elem, new_x, new_y, new_vx, new_vy} end decode_elements( packet, count - 1, new_x, new_y, new_vx, new_vy, [elem | acc] ) end defp decode_passive_data(packet) do keys = In.decode_byte(packet) key_pad_states = if keys > 0 do decode_keypad_states(packet, keys, 0, []) else [] end move_rect = %{ left: In.decode_short(packet), top: In.decode_short(packet), right: In.decode_short(packet), bottom: In.decode_short(packet) } {Enum.reverse(key_pad_states), move_rect} end defp decode_keypad_states(_packet, 0, _value, acc), do: acc defp decode_keypad_states(packet, remaining, value, acc) do {new_value, decoded} = if rem(length(acc), 2) != 0 do {bsr(value, 4), band(value, 0x0F)} else v = In.decode_byte(packet) {v, band(v, 0x0F)} end decode_keypad_states(packet, remaining - 1, new_value, [decoded | acc]) end defp encode_element(%MoveElem{} = elem) do attr = elem.attribute base = <> data = case attr do a when a in [0, 6, 13, 15, 37, 38] -> <> <> if attr == 13 do <> else <<>> end <> <> a when a in [1, 2, 14, 17, 19, 32, 33, 34, 35] -> <> a when a in [3, 4, 5, 7, 8, 9, 11] -> <> 10 -> <> 12 -> <> 18 -> <> a when a in [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] -> <<>> 36 -> <> _ -> <<>> end footer = if attr != 10 do <> else <<>> end base <> data <> footer end end