kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

View File

@@ -0,0 +1,569 @@
defmodule Odinsea.AntiCheat.LieDetector do
@moduledoc """
Lie detector (Anti-Macro) system for bot detection.
Ported from: client.AntiMacro.java, tools.packet.AntiMacroPacket.java
The lie detector system:
- Sends a CAPTCHA image to the player
- Player has 60 seconds to respond
- If failed, player is punished (HP/MP to 0)
- If passed, reward can be given
## Response Types
- 0x00: Req_Fail_InvalidCharacterName
- 0x01: Req_Fail_NotAttack
- 0x02: Req_Fail_NotAvailableTime
- 0x03: Req_Fail_SolvingQuestion
- 0x04: Pended
- 0x05: Success
- 0x06: Res
- 0x07: Res_Fail
- 0x08: Res_TargetFail
- 0x09: Res_Success
- 0x0A: Res_TargetSuccess
- 0x0B: Res_Reward
## State
- `character_id`: The character being tested
- `in_progress`: Whether a test is currently running
- `passed`: Whether the player has passed
- `attempt`: Remaining attempts (-1 = failed)
- `answer`: The correct answer
- `tester`: Who initiated the test
- `type`: 0 = item, 1 = admin
- `last_time`: Timestamp of last test
"""
use GenServer
require Logger
alias Odinsea.Game.Character
alias Odinsea.World
@table :lie_detectors
# Test timeout in milliseconds (60 seconds)
@test_timeout 60_000
# Cooldown between tests (10 minutes)
@test_cooldown 600_000
# Reward for passing (meso)
@pass_reward 5000
# Punishment for failing (meso to tester)
@fail_reward_to_TESTER 7000
# =============================================================================
# Response Types
# =============================================================================
defmodule ResponseType do
@moduledoc "Lie detector response types"
def req_fail_invalid_character_name, do: 0x00
def req_fail_not_attack, do: 0x01
def req_fail_not_available_time, do: 0x02
def req_fail_solving_question, do: 0x03
def pended, do: 0x04
def success, do: 0x05
def res, do: 0x06
def res_fail, do: 0x07
def res_target_fail, do: 0x08
def res_success, do: 0x09
def res_target_success, do: 0x0A
def res_reward, do: 0x0B
end
# =============================================================================
# Public API
# =============================================================================
@doc """
Starts the lie detector system and creates ETS table.
"""
def start_system do
case :ets.info(@table) do
:undefined ->
:ets.new(@table, [:set, :public, :named_table, read_concurrency: true])
:ok
_ ->
:ok
end
end
@doc """
Starts a lie detector session for a character.
"""
def start_session(character_id) do
start_system()
case lookup_session(character_id) do
nil ->
# Create new session
session = %{
character_id: character_id,
in_progress: false,
passed: false,
attempt: 1,
answer: nil,
tester: "",
type: 0,
last_time: 0,
timer_ref: nil
}
:ets.insert(@table, {character_id, session})
{:ok, session}
existing ->
{:ok, existing}
end
end
@doc """
Ends a lie detector session.
"""
def end_session(character_id) do
case lookup_session(character_id) do
nil -> :ok
session ->
# Cancel any pending timer
if session.timer_ref do
Process.cancel_timer(session.timer_ref)
end
:ets.delete(@table, character_id)
:ok
end
end
@doc """
Starts a lie detector test for a character.
Options:
- `tester`: Who initiated the test (default: "Admin")
- `is_item`: Whether started via item (default: false)
- `another_attempt`: Whether this is a retry (default: false)
"""
def start_test(character_id, opts \\ []) do
tester = Keyword.get(opts, :tester, "Admin")
is_item = Keyword.get(opts, :is_item, false)
another_attempt = Keyword.get(opts, :another_attempt, false)
# Ensure session exists
{:ok, session} = start_session(character_id)
# Check if can start test
cond do
not another_attempt and session.passed and is_item ->
{:error, :already_passed}
not another_attempt and session.in_progress ->
{:error, :already_in_progress}
not another_attempt and session.attempt == -1 ->
{:error, :already_failed}
true ->
do_start_test(character_id, tester, is_item, session)
end
end
@doc """
Validates a lie detector response from a player.
"""
def validate_response(character_id, response) do
case lookup_session(character_id) do
nil ->
{:error, :no_session}
session ->
if not session.in_progress do
{:error, :not_in_progress}
else
# Cancel timeout timer
if session.timer_ref do
Process.cancel_timer(session.timer_ref)
end
# Check answer
if String.upcase(response) == String.upcase(session.answer) do
# Correct!
handle_pass(character_id, session)
else
# Wrong!
handle_fail(character_id, session)
end
end
end
end
@doc """
Checks if a character can be tested (cooldown check).
"""
def can_test?(character_id) do
case lookup_session(character_id) do
nil -> true
session ->
now = System.system_time(:millisecond)
now > session.last_time + @test_cooldown
end
end
@doc """
Gets the current state of a lie detector session.
"""
def get_session(character_id) do
lookup_session(character_id)
end
@doc """
Admin command to start lie detector on a player.
"""
def admin_start_lie_detector(target_name, admin_name \\ "Admin") do
# Look up character by name
case World.find_character_by_name(target_name) do
nil ->
{:error, :character_not_found}
character ->
character_id = Map.get(character, :id)
start_test(character_id, tester: admin_name, is_item: false)
end
end
# =============================================================================
# Packet Builders
# =============================================================================
@doc """
Builds the lie detector packet with CAPTCHA image.
Ported from: AntiMacroPacket.sendLieDetector()
"""
def build_lie_detector_packet(image_data, attempts_remaining) do
# Opcode will be added by packet builder
packet = <<>>
# Response type: 0x06 (Res)
packet = packet <> <<ResponseType.res()>>
# Action: 4 (show CAPTCHA)
packet = packet <> <<4>>
# Attempts remaining
packet = packet <> <<attempts_remaining>>
# JPEG image data
packet = packet <> encode_jpeg(image_data)
packet
end
@doc """
Builds a lie detector response packet.
Ported from: AntiMacroPacket.LieDetectorResponse()
"""
def build_response_packet(msg_type, msg2 \\ 0) do
packet = <<>>
packet = packet <> <<msg_type>>
packet = packet <> <<msg2>>
packet
end
@doc """
Builds various lie detector message packets.
"""
def build_message_packet(type, opts \\ []) do
packet = <<>>
packet = packet <> <<type>>
case type do
4 -> # Save screenshot
packet = packet <> <<0>>
packet = packet <> encode_string(Keyword.get(opts, :filename, ""))
5 -> # Success with tester name
packet = packet <> <<1>>
packet = packet <> encode_string(Keyword.get(opts, :tester, ""))
6 -> # Admin picture
packet = packet <> <<4>>
packet = packet <> <<1>>
# Image data would go here
7 -> # Failed
packet = packet <> <<4>>
9 -> # Success/Passed
packet = packet <> <<Keyword.get(opts, :result, 0)>>
10 -> # Passed message
packet = packet <> <<0>>
packet = packet <> encode_string(Keyword.get(opts, :message, ""))
packet = packet <> encode_string("")
_ ->
packet = packet <> <<0>>
end
packet
end
# =============================================================================
# Private Functions
# =============================================================================
defp lookup_session(character_id) do
case :ets.lookup(@table, character_id) do
[{^character_id, session}] -> session
[] -> nil
end
end
defp update_session(character_id, updates) do
case lookup_session(character_id) do
nil -> :error
session ->
new_session = Map.merge(session, updates)
:ets.insert(@table, {character_id, new_session})
{:ok, new_session}
end
end
defp do_start_test(character_id, tester, is_item, session) do
# Generate CAPTCHA
{answer, image_data} = generate_captcha()
# Update session
new_attempt = session.attempt - 1
{:ok, new_session} = update_session(character_id, %{
in_progress: true,
passed: false,
attempt: new_attempt,
answer: answer,
tester: tester,
type: if(is_item, do: 0, else: 1),
last_time: System.system_time(:millisecond)
})
# Schedule timeout
timer_ref = Process.send_after(
self(),
{:lie_detector_timeout, character_id, is_item},
@test_timeout
)
update_session(character_id, %{timer_ref: timer_ref})
# Build packet
packet = build_lie_detector_packet(image_data, new_attempt + 1)
# Send to character
send_to_character(character_id, packet)
{:ok, new_session}
end
defp handle_pass(character_id, session) do
# Mark as passed
update_session(character_id, %{
in_progress: false,
passed: true,
attempt: 1,
last_time: System.system_time(:millisecond)
})
# Send success packet
packet = build_response_packet(ResponseType.res_success(), 0)
send_to_character(character_id, packet)
# Give reward if applicable
if session.type == 0 do
# Item-initiated, give reward
give_reward(character_id)
end
# Log
Logger.info("[LieDetector] Character #{character_id} passed the test")
:ok
end
defp handle_fail(character_id, session) do
if session.attempt == -1 do
# Out of attempts - execute punishment
execute_punishment(character_id, session)
else
# Try again
start_test(character_id,
tester: session.tester,
is_item: session.type == 0,
another_attempt: true
)
end
end
defp execute_punishment(character_id, session) do
# Update session
update_session(character_id, %{
in_progress: false,
passed: false,
attempt: -1
})
# Send fail packet
packet = build_response_packet(ResponseType.res_fail(), 0)
send_to_character(character_id, packet)
# Punish character (set HP/MP to 0)
punish_character(character_id)
# Reward tester if applicable
if session.tester != "" and session.tester != "Admin" do
reward_tester(session.tester, character_id)
end
# Broadcast to GMs
broadcast_gm_alert(character_id)
# Log
Logger.warning("[LieDetector] Character #{character_id} failed the test - punished")
:ok
end
defp punish_character(character_id) do
# Set HP and MP to 0
Character.set_hp(character_id, 0)
Character.set_mp(character_id, 0)
# Update stats
Character.update_single_stat(character_id, :hp, 0)
Character.update_single_stat(character_id, :mp, 0)
end
defp reward_tester(tester_name, failed_character_id) do
# Find tester
case World.find_character_by_name(tester_name) do
nil ->
:ok
tester ->
# Give meso reward
Character.gain_meso(Map.get(tester, :id), @fail_reward_to_TESTER, true)
# Send message
msg = "#{failed_character_id} did not pass the lie detector test. You received #{@fail_reward_to_TESTER} meso."
Character.drop_message(Map.get(tester, :id), 5, msg)
end
end
defp give_reward(character_id) do
# Give reward for passing
Character.gain_meso(character_id, @pass_reward, true)
# Send reward packet
packet = build_response_packet(ResponseType.res_reward(), 1)
send_to_character(character_id, packet)
end
defp send_to_character(character_id, packet) do
# This would send the packet through the character's client connection
# Implementation depends on the channel client system
:ok
end
defp broadcast_gm_alert(character_id) do
# TODO: Broadcast to GMs through World service
Logger.info("[LieDetector] GM Alert: Character #{character_id} failed lie detector")
:ok
end
defp generate_captcha do
# Generate a simple text CAPTCHA
# In production, this would generate an image
# Random 4-6 character alphanumeric code
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
length = Enum.random(4..6)
answer =
1..length
|> Enum.map(fn _ -> String.at(chars, Enum.random(0..(String.length(chars) - 1))) end)
|> Enum.join()
# For now, return a placeholder image
# In production, this would generate a JPEG image with the text
image_data = generate_captcha_image(answer)
{answer, image_data}
end
defp generate_captcha_image(answer) do
# Placeholder - in production, this would use an image generation library
# or pre-generated CAPTCHA images
# Return a simple binary representation
# Real implementation would use something like:
# - Mogrify (ImageMagick wrapper)
# - Imagine (Elixir image library)
# - Pre-generated CAPTCHA images stored in priv/
<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
String.length(answer)::32, answer::binary>>
end
defp encode_string(str) do
len = String.length(str)
<<len::16-little, str::binary>>
end
defp encode_jpeg(data) do
# Prepend length and data
len = byte_size(data)
<<len::32-little, data::binary>>
end
# =============================================================================
# GenServer for timeout handling
# =============================================================================
defmodule TimeoutHandler do
@moduledoc "Handles lie detector timeouts"
use GenServer
alias Odinsea.AntiCheat.LieDetector
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, %{}}
end
def handle_info({:lie_detector_timeout, character_id, is_item}, state) do
# Timeout occurred - treat as failure
case LieDetector.lookup_session(character_id) do
nil ->
{:noreply, state}
session ->
if session.in_progress do
# Execute timeout punishment
LieDetector.execute_punishment(character_id, session)
end
{:noreply, state}
end
end
end
end