kimi gone wild
This commit is contained in:
438
lib/odinsea/anticheat/validator.ex
Normal file
438
lib/odinsea/anticheat/validator.ex
Normal file
@@ -0,0 +1,438 @@
|
||||
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
|
||||
Reference in New Issue
Block a user