439 lines
13 KiB
Elixir
439 lines
13 KiB
Elixir
defmodule Odinsea.AntiCheat.Validator do
|
|
@moduledoc """
|
|
Validation functions for anti-cheat detection.
|
|
|
|
Ported from: handling.channel.handler.DamageParse.java
|
|
|
|
This module provides validation for:
|
|
- Damage validation (checking against calculated max damage)
|
|
- Movement validation (speed hacking detection)
|
|
- Item validation (dupe detection, unavailable items)
|
|
- EXP validation (leveling too fast)
|
|
- Attack validation (skill timing, bullet count)
|
|
"""
|
|
|
|
require Logger
|
|
|
|
alias Odinsea.AntiCheat.CheatTracker
|
|
alias Odinsea.Game.Character
|
|
alias Odinsea.Constants.Game
|
|
|
|
# Maximum damage cap (from Plugin.java DamageCap)
|
|
@damage_cap 9_999_999
|
|
|
|
# Maximum distance for attacking (squared, for distance check)
|
|
@max_attack_distance_sq 500_000
|
|
|
|
# Maximum movement speed
|
|
@max_movement_speed 400
|
|
|
|
# Maximum jump height
|
|
@max_jump_height 200
|
|
|
|
# =============================================================================
|
|
# Damage Validation
|
|
# =============================================================================
|
|
|
|
@doc """
|
|
Validates damage dealt to a monster.
|
|
|
|
Returns {:ok, validated_damage} or {:error, reason}
|
|
"""
|
|
def validate_damage(character_id, damage, expected_max, monster_id, skill_id) do
|
|
# Check if damage exceeds expected max
|
|
state = %{character_id: character_id}
|
|
|
|
# Check for high damage
|
|
state = if damage > expected_max do
|
|
param = build_damage_param(damage, expected_max, monster_id, skill_id, character_id)
|
|
CheatTracker.register_offense(character_id, :high_damage, param)
|
|
|
|
# Check for same damage (potential damage hack)
|
|
CheatTracker.check_same_damage(character_id, damage, expected_max)
|
|
|
|
state
|
|
else
|
|
state
|
|
end
|
|
|
|
# Check for damage exceeding 2x expected (HIGH_DAMAGE_2)
|
|
state = if damage > expected_max * 2 do
|
|
param = build_damage_param(damage, expected_max, monster_id, skill_id, character_id)
|
|
CheatTracker.register_offense(character_id, :high_damage_2, param)
|
|
|
|
# Cap the damage
|
|
capped_damage = trunc(expected_max * 2)
|
|
|
|
{:ok, capped_damage}
|
|
else
|
|
{:ok, damage}
|
|
end
|
|
|
|
# Check against global damage cap
|
|
state = if damage > @damage_cap do
|
|
CheatTracker.register_offense(character_id, :exceed_damage_cap,
|
|
"Damage: #{damage}, Cap: #{@damage_cap}")
|
|
{:ok, @damage_cap}
|
|
else
|
|
state
|
|
end
|
|
|
|
state
|
|
end
|
|
|
|
@doc """
|
|
Validates magic damage dealt to a monster.
|
|
"""
|
|
def validate_magic_damage(character_id, damage, expected_max, monster_id, skill_id) do
|
|
# Check for high magic damage
|
|
if damage > expected_max do
|
|
param = build_damage_param(damage, expected_max, monster_id, skill_id, character_id)
|
|
CheatTracker.register_offense(character_id, :high_damage_magic, param)
|
|
|
|
# Check for same damage
|
|
CheatTracker.check_same_damage(character_id, damage, expected_max)
|
|
end
|
|
|
|
# Check for damage exceeding 2x expected (HIGH_DAMAGE_MAGIC_2)
|
|
if damage > expected_max * 2 do
|
|
param = build_damage_param(damage, expected_max, monster_id, skill_id, character_id)
|
|
CheatTracker.register_offense(character_id, :high_damage_magic_2, param)
|
|
|
|
# Cap the damage
|
|
{:ok, trunc(expected_max * 2)}
|
|
else
|
|
{:ok, damage}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Calculates maximum weapon damage per hit for validation.
|
|
|
|
Ported from: DamageParse.CalculateMaxWeaponDamagePerHit()
|
|
"""
|
|
def calculate_max_weapon_damage(character, monster, attack_skill) do
|
|
# Base damage calculation
|
|
base_damage = Character.get_stat(character, :max_base_damage) || 100
|
|
|
|
# Apply skill multipliers
|
|
damage = if attack_skill && attack_skill > 0 do
|
|
skill_damage = Game.get_skill_damage(attack_skill)
|
|
base_damage * (skill_damage / 100.0)
|
|
else
|
|
base_damage
|
|
end
|
|
|
|
# Apply monster defense
|
|
# pdr_rate = Map.get(monster, :pdr_rate, 0)
|
|
# damage = damage * (1 - pdr_rate / 100.0)
|
|
|
|
# Apply boss damage modifier if monster is boss
|
|
# damage = if Map.get(monster, :is_boss, false) do
|
|
# boss_dam_r = Character.get_stat(character, :bossdam_r) || 0
|
|
# damage * (1 + boss_dam_r / 100.0)
|
|
# else
|
|
# damage
|
|
# end
|
|
|
|
# Apply damage rate
|
|
# dam_r = Character.get_stat(character, :dam_r) || 100
|
|
# damage = damage * (dam_r / 100.0)
|
|
|
|
trunc(max(damage, 1))
|
|
end
|
|
|
|
@doc """
|
|
Calculates maximum magic damage per hit for validation.
|
|
|
|
Ported from: DamageParse.CalculateMaxMagicDamagePerHit()
|
|
"""
|
|
def calculate_max_magic_damage(character, monster, attack_skill) do
|
|
# Base magic damage calculation
|
|
base_damage = Character.get_stat(character, :max_base_damage) || 100
|
|
|
|
# Magic has different multipliers
|
|
damage = if attack_skill && attack_skill > 0 do
|
|
skill_damage = Game.get_skill_damage(attack_skill)
|
|
base_damage * (skill_damage / 100.0) * 1.5
|
|
else
|
|
base_damage * 1.5
|
|
end
|
|
|
|
# Apply monster magic defense
|
|
# mdr_rate = Map.get(monster, :mdr_rate, 0)
|
|
# damage = damage * (1 - mdr_rate / 100.0)
|
|
|
|
trunc(max(damage, 1))
|
|
end
|
|
|
|
@doc """
|
|
Checks if attack is at valid range.
|
|
"""
|
|
def validate_attack_range(character_id, attacker_pos, target_pos, skill_id) do
|
|
# Calculate distance
|
|
distance_sq = calculate_distance_sq(attacker_pos, target_pos)
|
|
|
|
# Get expected range for skill
|
|
expected_range = Game.get_attack_range(skill_id)
|
|
|
|
if distance_sq > expected_range * expected_range do
|
|
param = "Distance: #{distance_sq}, Expected: #{expected_range * expected_range}, Skill: #{skill_id}"
|
|
CheatTracker.register_offense(character_id, :attack_faraway_monster, param)
|
|
{:error, :out_of_range}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
# =============================================================================
|
|
# Attack Validation
|
|
# =============================================================================
|
|
|
|
@doc """
|
|
Validates attack count matches skill expectations.
|
|
"""
|
|
def validate_attack_count(character_id, skill_id, hits, targets, expected_hits, expected_targets) do
|
|
# Skip certain skills that have special handling
|
|
if skill_id in [4211006, 3221007, 23121003, 1311001] do
|
|
:ok
|
|
else
|
|
# Check hits
|
|
if hits > expected_hits do
|
|
CheatTracker.register_offense(character_id, :mismatching_bulletcount,
|
|
"Hits: #{hits}, Expected: #{expected_hits}")
|
|
{:error, :invalid_hits}
|
|
else
|
|
# Check targets
|
|
if targets > expected_targets do
|
|
CheatTracker.register_offense(character_id, :mismatching_bulletcount,
|
|
"Targets: #{targets}, Expected: #{expected_targets}")
|
|
{:error, :invalid_targets}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates the character is alive before attacking.
|
|
"""
|
|
def validate_alive(character_id, is_alive) do
|
|
if not is_alive do
|
|
CheatTracker.register_offense(character_id, :attacking_while_dead, nil)
|
|
{:error, :dead}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates skill usage in specific maps (e.g., Mu Lung, Pyramid).
|
|
"""
|
|
def validate_skill_map(character_id, skill_id, map_id) do
|
|
# Check Mu Lung skills
|
|
if Game.is_mulung_skill?(skill_id) do
|
|
if div(map_id, 10000) != 92502 do
|
|
# Using Mu Lung skill outside dojo
|
|
{:error, :wrong_map}
|
|
else
|
|
:ok
|
|
end
|
|
else
|
|
# Check Pyramid skills
|
|
if Game.is_pyramid_skill?(skill_id) do
|
|
if div(map_id, 1000000) != 926 do
|
|
# Using Pyramid skill outside pyramid
|
|
{:error, :wrong_map}
|
|
else
|
|
:ok
|
|
end
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
|
|
# =============================================================================
|
|
# Movement Validation
|
|
# =============================================================================
|
|
|
|
@doc """
|
|
Validates player movement for speed hacking.
|
|
|
|
Returns :ok if valid, or {:error, reason} if suspicious.
|
|
"""
|
|
def validate_movement(character_id, old_pos, new_pos, time_diff_ms) do
|
|
# Calculate distance
|
|
distance = calculate_distance(old_pos, new_pos)
|
|
|
|
# Calculate speed
|
|
if time_diff_ms > 0 do
|
|
speed = distance / (time_diff_ms / 1000.0)
|
|
|
|
# Check if speed exceeds maximum
|
|
if speed > @max_movement_speed do
|
|
# Could be speed hacking or lag
|
|
# Only flag if significantly over
|
|
if speed > @max_movement_speed * 1.5 do
|
|
Logger.warning("[AntiCheat] Speed hack suspected for char #{character_id}: #{speed} px/s")
|
|
# TODO: Add to offense tracking when FAST_MOVE offense is enabled
|
|
{:error, :speed_exceeded}
|
|
else
|
|
:ok
|
|
end
|
|
else
|
|
:ok
|
|
end
|
|
else
|
|
# Instant movement - check distance
|
|
if distance > @max_movement_speed do
|
|
{:error, :teleport_detected}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates jump height for high jump detection.
|
|
"""
|
|
def validate_jump(character_id, y_delta) do
|
|
# Check if jump exceeds maximum
|
|
if y_delta < -@max_jump_height do
|
|
CheatTracker.register_offense(character_id, :high_jump,
|
|
"Jump: #{y_delta}, Max: #{@max_jump_height}")
|
|
{:error, :high_jump}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates portal usage distance.
|
|
"""
|
|
def validate_portal_distance(character_id, player_pos, portal_pos) do
|
|
distance_sq = calculate_distance_sq(player_pos, portal_pos)
|
|
max_portal_distance_sq = 200 * 200 # 200 pixels
|
|
|
|
if distance_sq > max_portal_distance_sq do
|
|
CheatTracker.register_offense(character_id, :using_faraway_portal,
|
|
"Distance: #{:math.sqrt(distance_sq)}")
|
|
{:error, :too_far}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
# =============================================================================
|
|
# Item Validation
|
|
# =============================================================================
|
|
|
|
@doc """
|
|
Validates item usage (checks if item is available to character).
|
|
"""
|
|
def validate_item_usage(character_id, item_id, inventory) do
|
|
# Check if item exists in inventory
|
|
has_item = Enum.any?(inventory, fn item ->
|
|
Map.get(item, :item_id) == item_id
|
|
end)
|
|
|
|
if not has_item do
|
|
CheatTracker.register_offense(character_id, :using_unavailable_item,
|
|
"ItemID: #{item_id}")
|
|
{:error, :item_not_found}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates item quantity (dupe detection).
|
|
"""
|
|
def validate_item_quantity(character_id, item_id, quantity, expected_max) do
|
|
if quantity > expected_max do
|
|
# Potential dupe
|
|
Logger.warning("[AntiCheat] Suspicious item quantity for char #{character_id}: #{item_id} x#{quantity}")
|
|
{:error, :quantity_exceeded}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates meso explosion (checks if meso exists on map).
|
|
"""
|
|
def validate_meso_explosion(character_id, map_item) do
|
|
if map_item == nil do
|
|
CheatTracker.register_offense(character_id, :exploding_nonexistant, nil)
|
|
{:error, :no_meso}
|
|
else
|
|
meso = Map.get(map_item, :meso, 0)
|
|
if meso <= 0 do
|
|
CheatTracker.register_offense(character_id, :etc_explosion, nil)
|
|
{:error, :invalid_meso}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
|
|
# =============================================================================
|
|
# EXP Validation
|
|
# =============================================================================
|
|
|
|
@doc """
|
|
Validates EXP gain rate.
|
|
"""
|
|
def validate_exp_gain(character_id, exp_gained, time_since_last_gain_ms) do
|
|
# Calculate EXP per minute
|
|
if time_since_last_gain_ms > 0 do
|
|
exp_per_minute = exp_gained / (time_since_last_gain_ms / 60000.0)
|
|
|
|
# Maximum reasonable EXP per minute (varies by level, this is a rough check)
|
|
max_exp_per_minute = 10_000_000
|
|
|
|
if exp_per_minute > max_exp_per_minute do
|
|
Logger.warning("[AntiCheat] High EXP rate for char #{character_id}: #{exp_per_minute}/min")
|
|
# TODO: Add to offense tracking
|
|
{:warning, :high_exp_rate}
|
|
else
|
|
:ok
|
|
end
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates level progression (checks for impossible jumps).
|
|
"""
|
|
def validate_level_progression(old_level, new_level) do
|
|
max_level_jump = 5
|
|
|
|
if new_level - old_level > max_level_jump do
|
|
{:error, :impossible_level_jump}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
# =============================================================================
|
|
# Helper Functions
|
|
# =============================================================================
|
|
|
|
defp build_damage_param(damage, expected, monster_id, skill_id, character_id) do
|
|
"[Damage: #{damage}, Expected: #{expected}, Mob: #{monster_id}] [Skill: #{skill_id}]"
|
|
end
|
|
|
|
defp calculate_distance_sq(pos1, pos2) do
|
|
dx = Map.get(pos1, :x, 0) - Map.get(pos2, :x, 0)
|
|
dy = Map.get(pos1, :y, 0) - Map.get(pos2, :y, 0)
|
|
dx * dx + dy * dy
|
|
end
|
|
|
|
defp calculate_distance(pos1, pos2) do
|
|
:math.sqrt(calculate_distance_sq(pos1, pos2))
|
|
end
|
|
end
|