Files
odinsea-elixir/lib/odinsea/game/movement/path.ex
2026-02-14 23:12:33 -07:00

444 lines
11 KiB
Elixir

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)
<<path.x::16-little, path.y::16-little,
path.vx::16-little, path.vy::16-little,
length(path.elements)::8,
elements_data::binary>>
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 = <<attr::8>>
data =
case attr do
a when a in [0, 6, 13, 15, 37, 38] ->
<<elem.x::16-little, elem.y::16-little,
elem.vx::16-little, elem.vy::16-little,
elem.fh::16-little>> <>
if attr == 13 do
<<elem.fall_start::16-little>>
else
<<>>
end <>
<<elem.offset_x::16-little, elem.offset_y::16-little>>
a when a in [1, 2, 14, 17, 19, 32, 33, 34, 35] ->
<<elem.vx::16-little, elem.vy::16-little>>
a when a in [3, 4, 5, 7, 8, 9, 11] ->
<<elem.x::16-little, elem.y::16-little,
elem.fh::16-little>>
10 ->
<<elem.sn::8>>
12 ->
<<elem.vx::16-little, elem.vy::16-little,
elem.fall_start::16-little>>
18 ->
<<elem.x::16-little, elem.y::16-little,
elem.vx::16-little, elem.vy::16-little>>
a when a in [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] ->
<<>>
36 ->
<<elem.x::16-little, elem.y::16-little,
elem.vx::16-little, elem.vy::16-little,
elem.fh::16-little>>
_ ->
<<>>
end
footer =
if attr != 10 do
<<elem.move_action::8, elem.elapse::16-little>>
else
<<>>
end
base <> data <> footer
end
end