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