From 2c3d0ab580a128342d1c2fbf167da709171899fd Mon Sep 17 00:00:00 2001 From: Rooba Date: Wed, 25 Feb 2026 12:26:26 -0700 Subject: [PATCH] fix login issue* --- SESSION_SUMMARY.md | 215 --- config/config.exs | 5 +- config/runtime.exs | 22 +- docs/PACKET_LOGGING.md | 92 + lib/odinsea/channel/handler/alliance.ex | 11 +- lib/odinsea/channel/packets.ex | 215 +++ lib/odinsea/channel/players.ex | 10 + lib/odinsea/constants/server.ex | 21 +- lib/odinsea/database/context.ex | 818 +++++++-- lib/odinsea/database/schema/account.ex | 52 +- lib/odinsea/game/character.ex | 15 + lib/odinsea/game/inventory.ex | 28 + lib/odinsea/game/inventory_type.ex | 26 + lib/odinsea/game/map.ex | 14 + lib/odinsea/login/client.ex | 210 ++- lib/odinsea/login/handler.ex | 117 +- lib/odinsea/login/packets.ex | 352 +++- lib/odinsea/net/cipher/aes_cipher.ex | 110 +- lib/odinsea/net/cipher/client_crypto.ex | 98 +- lib/odinsea/net/cipher/ig_cipher.ex | 54 +- lib/odinsea/net/cipher/shanda_cipher.ex | 150 ++ lib/odinsea/net/opcodes.ex | 120 +- lib/odinsea/net/packet/out.ex | 24 + lib/odinsea/net/packet_logger.ex | 380 ++++ lib/odinsea/net/processor.ex | 14 +- lib/odinsea/shop/client.ex | 2 +- lib/odinsea/shop/packets.ex | 30 +- logs/odinsea.log | 1582 +++++++++++++++++ .../20260215000001_create_base_tables.exs | 6 +- test/crypto_simple_test.exs | 60 + test/crypto_test.exs | 185 ++ test/debug_crypto.exs | 80 + test/debug_crypto2.exs | 101 ++ test/find_iv.exs | 53 + test/test_aes.exs | 48 + test/test_ivs.exs | 55 + test/test_shanda.exs | 54 + 37 files changed, 4708 insertions(+), 721 deletions(-) delete mode 100644 SESSION_SUMMARY.md create mode 100644 docs/PACKET_LOGGING.md create mode 100644 lib/odinsea/net/cipher/shanda_cipher.ex create mode 100644 lib/odinsea/net/packet_logger.ex create mode 100644 logs/odinsea.log create mode 100644 test/crypto_simple_test.exs create mode 100644 test/crypto_test.exs create mode 100644 test/debug_crypto.exs create mode 100644 test/debug_crypto2.exs create mode 100644 test/find_iv.exs create mode 100644 test/test_aes.exs create mode 100644 test/test_ivs.exs create mode 100644 test/test_shanda.exs diff --git a/SESSION_SUMMARY.md b/SESSION_SUMMARY.md deleted file mode 100644 index 724e0b1..0000000 --- a/SESSION_SUMMARY.md +++ /dev/null @@ -1,215 +0,0 @@ -# Odinsea Elixir Port - Session Summary (2026-02-14) - -## šŸŽÆ Major Milestones Achieved - -### āœ… Data Provider Infrastructure Complete -Implemented the three **CRITICAL BLOCKER** data provider systems that were preventing further game development: - -1. **Item Information Provider** (`Odinsea.Game.ItemInfo`) - - Complete item metadata system (stats, prices, requirements) - - Equipment stat definitions and creation - - ETS-based high-performance caching - - JSON data loading (WZ export compatible) - - 450+ lines of code - -2. **Map Factory** (`Odinsea.Game.MapFactory`) - - Complete map template system - - Portal data structures (9 portal types) - - Foothold/collision data structures - - Field properties (limits, rates, timers) - - ETS-based caching - - JSON data loading - - 450+ lines of code - -3. **Life Factory** (`Odinsea.Game.LifeFactory`) - - Complete monster stats system (40+ stat fields) - - NPC data system (names, shops, scripts) - - ETS-based caching - - JSON data loading - - 350+ lines of code - -### āœ… Monster System Complete -- **Monster Module** (`Odinsea.Game.Monster`) - - Full monster instance management - - HP/MP tracking and damage system - - Attacker logging and top damage tracking - - Controller assignment (player-controlled AI) - - Status effects framework - - Position and movement tracking - - Boss/death detection - - EXP calculation - - 250+ lines of code - -## šŸ“Š Project Statistics - -| Metric | Before | After | Change | -|--------|--------|-------|--------| -| Files | 51 | **55** | +4 | -| Lines of Code | ~11,000 | **12,530** | +1,530 | -| Modules | 45 | **49** | +4 | -| Overall Progress | 55% | **62%** | +7% | -| Game Systems | 35% | **55%** | +20% | - -## šŸ—ļø Architecture Highlights - -### Data Provider Pattern -All three data providers follow a consistent architecture: -- GenServer-based initialization -- ETS tables for high-performance reads -- JSON file loading (WZ data export compatible) -- Fallback data for testing without WZ files -- Integrated into application supervision tree -- Start before game servers (proper initialization order) - -### Key Design Decisions -1. **Monster as Struct, Not Process** - - Monsters are managed by Map GenServer, not as individual processes - - Avoids massive process overhead (1000s of mobs = 1000s of processes) - - Maps track monsters in state, broadcast updates to players - -2. **ETS for Caching** - - All game data cached in ETS tables - - `:read_concurrency` for multi-core performance - - Static data loaded once at startup - -3. **JSON-Based Data Loading** - - Allows easy WZ data export from Java server - - Human-readable for debugging - - Version control friendly - - Future-proof for custom content - -## šŸš€ Next Steps Unlocked - -With data providers complete, these systems can now be implemented: - -### 1. WZ Data Export Utility (High Priority) -Create Java utility to export WZ data to JSON: -- Items: `data/items.json`, `data/equips.json`, `data/item_strings.json` -- Maps: `data/maps.json` (with portals, footholds, properties) -- Life: `data/monsters.json`, `data/npcs.json` -- Validation and testing with real game data - -### 2. Monster Spawning System -- Implement `SpawnPoint` on maps -- Monster respawn timers -- Basic monster AI movement -- Integration with Map GenServer - -### 3. Combat System -- Damage calculation formulas -- Monster damage handler -- Death and EXP distribution -- Drop item creation and spawning - -### 4. Portal System -- Portal-based map transitions -- Script portal execution -- Town portal system -- Integration with Map module - -### 5. Full Gameplay Loop Testing -End-to-end test: -1. Login to server -2. Select character -3. Spawn in Henesys -4. See monsters on map -5. Kill monster -6. Receive EXP and drops -7. Change maps via portal - -## šŸ“ Files Created - -``` -lib/odinsea/game/ -ā”œā”€ā”€ item_info.ex # Item Information Provider (450 lines) -ā”œā”€ā”€ map_factory.ex # Map Factory (450 lines) -ā”œā”€ā”€ life_factory.ex # Life Factory (350 lines) -└── monster.ex # Monster Module (250 lines) - -priv/data/ # Data directory for WZ exports -└── .gitkeep -``` - -## šŸ”§ Files Modified - -``` -lib/odinsea/application.ex # Added 3 data providers to supervision tree -``` - -## āœ… Compilation Status - -Project compiles successfully with **zero errors**: -- All new modules compile without issues -- Only minor warnings (unused variables, deprecated Logger.warn) -- All type specs valid -- Integration tests pending - -## šŸ“ Documentation Updated - -Updated `PORT_PROGRESS.md`: -- Phase 6 (Game Systems): 35% → 55% (+20%) -- Overall progress: 55% → 62% (+7%) -- Updated file mappings (4 new mappings) -- Added detailed session notes -- Updated statistics and metrics - -## šŸŽ“ Key Learnings - -1. **Separation of Data and Instances** - - LifeFactory holds static monster stats - - Monster module manages live instances - - Clean separation enables efficient caching - -2. **ETS Performance** - - ETS read_concurrency enables lock-free reads - - Perfect for static game data - - Microsecond lookup times - -3. **JSON Over Binary** - - WZ binary format complex to parse - - JSON export from Java is simpler - - Enables non-Java contributors - - Easy to inspect and debug - -4. **Supervision Tree Order Matters** - - Data providers must start before servers - - Prevents race conditions on startup - - Clear dependency graph - -## šŸ› Known Issues - -None! All code compiles and integrates cleanly. - -## šŸŽÆ Remaining Work - -Major systems still needed: -- Skills & Buffs (Phase 6.4) -- Scripting Engine (Phase 9) -- Timer System (Phase 10.1) -- Anti-Cheat (Phase 10.2) -- Events (Phase 10.3) -- Admin Commands (Phase 10.4) -- Testing Suite (Phase 11) - -Estimated remaining: ~38% of total port - -## šŸ“ž For Next Session - -**Immediate Priorities:** -1. Create WZ data export utility in Java -2. Export real game data to JSON files -3. Test data providers with real data -4. Implement monster spawning on maps -5. Begin combat system implementation - -**Questions to Consider:** -- Should we implement a simple scripting system first (Lua?) or continue with game systems? -- Do we need drop tables before combat, or can we stub them? -- Should we focus on getting one complete map working end-to-end? - ---- - -**Session Duration:** ~2 hours -**Commits Needed:** Data providers implementation -**Ready for Testing:** Yes (with fallback data) -**Blockers Removed:** 3 critical (ItemInfo, MapFactory, LifeFactory) diff --git a/config/config.exs b/config/config.exs index d754ea6..eb60939 100644 --- a/config/config.exs +++ b/config/config.exs @@ -17,7 +17,7 @@ config :odinsea, :rates, quest: 1 config :odinsea, :login, - port: 8584, + port: 8484, user_limit: 1500, max_characters: 3, flag: 3, @@ -28,8 +28,7 @@ config :odinsea, :game, channel_ports: %{1 => 8585, 2 => 8586}, events: [] -config :odinsea, :shop, - port: 8605 +config :odinsea, :shop, port: 8605 config :odinsea, :features, admin_mode: false, diff --git a/config/runtime.exs b/config/runtime.exs index 1e5f0aa..82b841a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -11,7 +11,8 @@ config :odinsea, :server, host: System.get_env("ODINSEA_HOST", "127.0.0.1"), revision: String.to_integer(System.get_env("ODINSEA_REV", "1")), flag: String.to_integer(System.get_env("ODINSEA_FLAG", "0")), - slide_message: System.get_env("ODINSEA_SLIDE_MESSAGE", "Welcome to Luna v99. The Ultimate Private Server"), + slide_message: + System.get_env("ODINSEA_SLIDE_MESSAGE", "Welcome to Luna v99. The Ultimate Private Server"), data_prefix: System.get_env("ODINSEA_DATA_PREFIX", "Luna") # ==================================================================================================== @@ -31,7 +32,8 @@ config :odinsea, :login, user_limit: String.to_integer(System.get_env("ODINSEA_USER_LIMIT", "1500")), max_characters: String.to_integer(System.get_env("ODINSEA_MAX_CHARACTERS", "3")), flag: String.to_integer(System.get_env("ODINSEA_LOGIN_FLAG", "3")), - event_message: System.get_env("ODINSEA_EVENT_MESSAGE", "#bLuna v99\\r\\n#rThe Ultimate Private Server") + event_message: + System.get_env("ODINSEA_EVENT_MESSAGE", "#bLuna v99\\r\\n#rThe Ultimate Private Server") # ==================================================================================================== # Game Channels @@ -39,7 +41,7 @@ config :odinsea, :login, channel_count = String.to_integer(System.get_env("ODINSEA_CHANNEL_COUNT", "2")) # Generate channel port configuration -channel_ports = +channel_ports = for i <- 1..channel_count do port = String.to_integer(System.get_env("ODINSEA_CHANNEL_PORT_#{i}", "#{8584 + i}")) {i, port} @@ -49,15 +51,17 @@ channel_ports = config :odinsea, :game, channels: channel_count, channel_ports: channel_ports, - events: System.get_env("ODINSEA_EVENTS", - "MiniDungeon,Olivia,PVP,CygnusBattle,ScarTarBattle,VonLeonBattle" - ) |> String.split(",") + events: + System.get_env( + "ODINSEA_EVENTS", + "MiniDungeon,Olivia,PVP,CygnusBattle,ScarTarBattle,VonLeonBattle" + ) + |> String.split(",") # ==================================================================================================== # Cash Shop Server # ==================================================================================================== -config :odinsea, :shop, - port: String.to_integer(System.get_env("ODINSEA_SHOP_PORT", "8605")) +config :odinsea, :shop, port: String.to_integer(System.get_env("ODINSEA_SHOP_PORT", "8605")) # ==================================================================================================== # Database @@ -94,7 +98,7 @@ config :odinsea, :features, script_reload: System.get_env("ODINSEA_SCRIPT_RELOAD", "true") == "true", family_disable: System.get_env("ODINSEA_FAMILY_DISABLE", "false") == "true", custom_lang: System.get_env("ODINSEA_CUSTOM_LANG", "false") == "true", - custom_dmgskin: System.get_env("ODINSEA_CUSTOM_DMGSKIN", "true") == "true", + custom_dmgskin: System.get_env("ODINSEA_CUSTOM_DMGSKIN", "false") == "true", skip_maccheck: System.get_env("ODINSEA_SKIP_MACCHECK", "true") == "true" # ==================================================================================================== diff --git a/docs/PACKET_LOGGING.md b/docs/PACKET_LOGGING.md new file mode 100644 index 0000000..5a62ae6 --- /dev/null +++ b/docs/PACKET_LOGGING.md @@ -0,0 +1,92 @@ +# Packet Logging System + +## Overview + +Comprehensive packet logging system for debugging the MapleStory protocol, matching the Java version's logging format. + +## Features + +- **Direction Indicators**: `[client]` for incoming packets, `[loopback]` for outgoing packets +- **Opcode Names**: Human-readable packet names (e.g., `CP_CheckPassword`, `LP_ServerList`) +- **Opcode Values**: Both decimal and hexadecimal (e.g., `2 / 0x02`) +- **Raw Hex Data**: Space-separated hex bytes +- **ASCII Text**: Printable characters with dots for non-printable +- **Context Information**: IP address, server type, packet size + +## Configuration + +Enable/disable packet logging in `config/config.exs`: + +```elixir +config :odinsea, :features, + log_packet: true # Set to false to disable +``` + +## Example Output + +``` +[client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +[loopback] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes +``` + +## Implementation Details + +### Files Added + +- `lib/odinsea/net/packet_logger.ex` - Main packet logging module + +### Files Modified + +- `lib/odinsea/login/client.ex` - Added packet logging for incoming client packets +- `lib/odinsea/login/handler.ex` - Added packet logging for outgoing server packets +- `lib/odinsea/net/processor.ex` - Fixed handler function name mismatches + +### Key Functions + +#### `PacketLogger.log_client_packet/3` +Logs incoming packets from the client with opcode, data, and context. + +#### `PacketLogger.log_server_packet/3` +Logs outgoing packets to the client with opcode, data, and context. + +#### `PacketLogger.log_raw_packet/4` +Logs raw packets (e.g., hello handshake) that don't follow the standard opcode format. + +## Opcode Name Resolution + +The logger includes comprehensive opcode name mappings for: + +- **Client Opcodes** (CP_*): Login, authentication, character management, gameplay, etc. +- **Server Opcodes** (LP_*): Responses, server lists, character data, game state, etc. + +Unknown opcodes are displayed as `UNKNOWN`. + +## Usage Tips + +1. **Enable during development**: Keep `log_packet: true` to debug connection issues +2. **Disable in production**: Set `log_packet: false` to reduce log noise and improve performance +3. **Compare with Java logs**: Use this to verify protocol compatibility with the Java server +4. **Debug handshake issues**: Check that HELLO packet is sent before CP_PermissionRequest + +## Debugging Login Issues + +The login sequence should look like this: + +1. **[loopback] HELLO** - Server sends handshake with IVs +2. **[client] CP_PermissionRequest** - Client sends version check +3. **[loopback] RSA_KEY / LOGIN_AUTH** - Server sends RSA key and login background +4. **[client] CP_CheckPassword** - Client sends login credentials +5. **[loopback] LOGIN_STATUS** - Server sends authentication result + +If packets are missing or out of order, check: +- Network connectivity +- Client version compatibility (v342) +- Opcode mappings in `opcodes.ex` +- Handler routing in `processor.ex` diff --git a/lib/odinsea/channel/handler/alliance.ex b/lib/odinsea/channel/handler/alliance.ex index 61a871b..3412281 100644 --- a/lib/odinsea/channel/handler/alliance.ex +++ b/lib/odinsea/channel/handler/alliance.ex @@ -59,7 +59,7 @@ defmodule Odinsea.Channel.Handler.Alliance do # Handle deny separately if op == 22 do - handle_deny_invite(client_pid, character_id, char_state, guild_id) + handle_deny_invite(client_pid, char_state) else handle_alliance_op(op, packet, client_pid, character_id, char_state, guild_id) end @@ -255,8 +255,15 @@ defmodule Odinsea.Channel.Handler.Alliance do Also called when op == 22 in alliance operation. Reference: AllianceHandler.DenyInvite() + + ## Parameters + - client_pid: The client process ID + - character_state: The character's current state (includes guild_id) """ - def handle_deny_invite(client_pid, character_id, char_state, guild_id) do + def handle_deny_invite(client_pid, character_state) do + character_id = character_state.id + guild_id = character_state.guild_id + # Get invited alliance ID # invited_alliance_id = World.Guild.get_invited_id(guild_id) diff --git a/lib/odinsea/channel/packets.ex b/lib/odinsea/channel/packets.ex index c04c00f..d8b4d3d 100644 --- a/lib/odinsea/channel/packets.ex +++ b/lib/odinsea/channel/packets.ex @@ -2016,6 +2016,221 @@ defmodule Odinsea.Channel.Packets do |> Out.to_data() end + # ============================================================================= + # Buddy List Packets + # ============================================================================= + + @doc """ + Updates the buddy list for a character. + Ported from MaplePacketCreator.updateBuddylist() + + ## Parameters + - buddy_list: List of buddy entries + - deleted: Operation type (7 = update, or other values for different operations) + """ + def update_buddylist(buddy_list, deleted \\ 7) do + packet = Out.new(Opcodes.lp_buddylist()) + |> Out.encode_byte(deleted) + |> Out.encode_byte(length(buddy_list)) + + # Encode each buddy entry + packet = Enum.reduce(buddy_list, packet, fn buddy, p -> + p + |> Out.encode_int(buddy.character_id) + |> Out.encode_string(buddy.name, 13) + |> Out.encode_byte(if buddy.visible, do: 0, else: 1) + |> Out.encode_int(if buddy.channel == -1, do: -1, else: buddy.channel - 1) + |> Out.encode_string(buddy.group || "ETC", 17) + end) + + # Padding ints for each buddy (always 0) + packet = Enum.reduce(buddy_list, packet, fn _, p -> + Out.encode_int(p, 0) + end) + + Out.to_data(packet) + end + + @doc """ + Sends a buddy request to a character. + Ported from MaplePacketCreator.requestBuddylistAdd() + + ## Parameters + - cid_from: Character ID sending the request + - name_from: Name of character sending the request + - level_from: Level of character sending the request + - job_from: Job ID of character sending the request + """ + def request_buddylist_add(cid_from, name_from, level_from, job_from) do + Out.new(Opcodes.lp_buddylist()) + |> Out.encode_byte(9) + |> Out.encode_int(cid_from) + |> Out.encode_string(name_from) + |> Out.encode_int(level_from) + |> Out.encode_int(job_from) + |> Out.encode_int(cid_from) + |> Out.encode_string(name_from, 13) + |> Out.encode_byte(1) + |> Out.encode_int(0) + |> Out.encode_string("ETC", 16) + |> Out.encode_short(1) + |> Out.to_data() + end + + @doc """ + Sends a buddy list message/status code. + Ported from MaplePacketCreator.buddylistMessage() + + ## Message codes: + - 11: Buddy list full + - 12: Target buddy list full + - 13: Already registered as buddy + - 14: Character not found + - 15: Request sent + - 17: You cannot add yourself as a buddy + - 19: Already requested + - 21: Cannot add GM to buddy list + """ + def buddylist_message(message_code) do + Out.new(Opcodes.lp_buddylist()) + |> Out.encode_byte(message_code) + |> Out.to_data() + end + + # ============================================================================= + # Party Packets + # ============================================================================= + + @doc """ + Party creation confirmation packet. + Ported from MaplePacketCreator.partyCreated() + + ## Parameters + - party_id: The newly created party ID + """ + def party_created(party_id) do + opcode_byte = if Odinsea.Constants.Game.gms?(), do: 10, else: 8 + + Out.new(Opcodes.lp_party_operation()) + |> Out.encode_byte(opcode_byte) + |> Out.encode_int(party_id) + |> Out.encode_int(999_999_999) + |> Out.encode_int(999_999_999) + |> Out.encode_long(0) + |> Out.encode_byte(0) + |> Out.encode_byte(1) + |> Out.to_data() + end + + @doc """ + Party invitation packet (sent to invited player). + Ported from MaplePacketCreator.partyInvite() + + ## Parameters + - from_character: Character struct of the inviter (needs :party_id, :name, :level, :job) + """ + def party_invite(from_character) do + party_id = from_character.party_id || 0 + + Out.new(Opcodes.lp_party_operation()) + |> Out.encode_byte(4) + |> Out.encode_int(party_id) + |> Out.encode_string(from_character.name) + |> Out.encode_int(from_character.level) + |> Out.encode_int(from_character.job) + |> Out.encode_byte(0) + |> Out.to_data() + end + + @doc """ + Party request packet (for request-to-join scenarios). + Ported from MaplePacketCreator.partyRequestInvite() + + ## Parameters + - from_character: Character struct of the requester (needs :id, :name, :level, :job) + """ + def party_request(from_character) do + Out.new(Opcodes.lp_party_operation()) + |> Out.encode_byte(7) + |> Out.encode_int(from_character.id) + |> Out.encode_string(from_character.name) + |> Out.encode_int(from_character.level) + |> Out.encode_int(from_character.job) + |> Out.to_data() + end + + @doc """ + Party status/error message packet. + Ported from MaplePacketCreator.partyStatusMessage() + + ## Message codes: + - 10: A beginner can't create a party + - 11: Your request for a party didn't work due to an unexpected error + - 13: You have yet to join a party + - 16: Already have joined a party + - 17: The party you're trying to join is already in full capacity + - 19: Unable to find the requested character in this channel + - 23: 'Char' have denied request to the party (with charname) + + ## Parameters + - message_code: The status/error code + - charname: Optional character name for personalized messages + """ + def party_status_message(message_code, charname \\ nil) do + opcode_byte = if Odinsea.Constants.Game.gms?() and message_code >= 7, + do: message_code + 2, + else: message_code + + packet = Out.new(Opcodes.lp_party_operation()) + |> Out.encode_byte(opcode_byte) + + packet = if charname do + Out.encode_string(packet, charname) + else + packet + end + + Out.to_data(packet) + end + + # ============================================================================= + # Guild Packets + # ============================================================================= + + @doc """ + Guild invitation packet (sent to invited player). + Ported from MaplePacketCreator.guildInvite() + + ## Parameters + - guild_id: ID of the guild + - from_name: Name of the character sending the invite + - from_level: Level of the character sending the invite + - from_job: Job ID of the character sending the invite + """ + def guild_invite(guild_id, from_name, from_level, from_job) do + Out.new(Opcodes.lp_guild_operation()) + |> Out.encode_byte(0x05) + |> Out.encode_int(guild_id) + |> Out.encode_string(from_name) + |> Out.encode_int(from_level) + |> Out.encode_int(from_job) + |> Out.to_data() + end + + @doc """ + Guild invitation denial packet. + Ported from MaplePacketCreator.denyGuildInvitation() + + ## Parameters + - charname: Name of the character who denied the invitation + """ + def deny_guild_invitation(charname) do + Out.new(Opcodes.lp_guild_operation()) + |> Out.encode_byte(0x3D) + |> Out.encode_string(charname) + |> Out.to_data() + end + # ============================================================================= # Utility Functions # ============================================================================= diff --git a/lib/odinsea/channel/players.ex b/lib/odinsea/channel/players.ex index 0b1b86c..974a4c6 100644 --- a/lib/odinsea/channel/players.ex +++ b/lib/odinsea/channel/players.ex @@ -108,6 +108,16 @@ defmodule Odinsea.Channel.Players do end end + @doc """ + Finds a player by name in the channel. + Returns the player data or nil if not found. + + This is the public API for player lookup by name. + """ + def find_by_name(name, _channel_id \\ nil) do + get_player_by_name(name) + end + @doc """ Updates player data. """ diff --git a/lib/odinsea/constants/server.ex b/lib/odinsea/constants/server.ex index 1b129d4..5db04a4 100644 --- a/lib/odinsea/constants/server.ex +++ b/lib/odinsea/constants/server.ex @@ -4,9 +4,11 @@ defmodule Odinsea.Constants.Server do These define the MapleStory client version and protocol details. """ - # MapleStory Client Version (GMS v342) - @maple_version 342 - @maple_patch "1" + # MapleStory Client Version (MapleSEA v112.4) + # Ported from ServerConstants.java + @maple_version 112 + @maple_patch "4" + @maple_locale 7 @client_version 99 # Protocol constants @@ -15,9 +17,10 @@ defmodule Odinsea.Constants.Server do @block_size 1460 # RSA Keys (from ServerConstants.java) - @pub_key "" - @maplogin_default "default" - @maplogin_custom "custom" + # Default MapleStory RSA public key for password encryption + @pub_key "30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001" + @maplogin_default "MapLogin" + @maplogin_custom "MapLoginLuna" # Packet sequence constants @iv_length 4 @@ -35,6 +38,12 @@ defmodule Odinsea.Constants.Server do """ def maple_patch, do: @maple_patch + @doc """ + Returns the MapleStory locale (region code). + For SEA/MSEA this is 7. + """ + def maple_locale, do: @maple_locale + @doc """ Returns the full client version string. """ diff --git a/lib/odinsea/database/context.ex b/lib/odinsea/database/context.ex index ce3e1ee..5776948 100644 --- a/lib/odinsea/database/context.ex +++ b/lib/odinsea/database/context.ex @@ -2,7 +2,7 @@ defmodule Odinsea.Database.Context do @moduledoc """ Database context module for Odinsea. Provides high-level database operations for accounts, characters, and related entities. - + Ported from Java UnifiedDB.java and MapleClient.java login functionality. """ @@ -11,7 +11,24 @@ defmodule Odinsea.Database.Context do import Ecto.Query alias Odinsea.Repo - alias Odinsea.Database.Schema.{Account, Character, InventoryItem, Buddy, Guild, Skill, QuestStatus, QuestStatusMob} + + alias Odinsea.Database.Schema.{ + Account, + Character, + CharacterSlot, + InventoryItem, + Buddy, + Guild, + Skill, + QuestStatus, + QuestStatusMob, + QuestInfo, + Gift, + NxCode, + IpBan, + MacBan + } + alias Odinsea.Game.InventoryType alias Odinsea.Net.Cipher.LoginCrypto @@ -21,11 +38,11 @@ defmodule Odinsea.Database.Context do @doc """ Authenticates a user with username and password. - + Returns: - {:ok, account_info} on successful authentication - {:error, reason} on failure (reason can be :invalid_credentials, :banned, :already_logged_in, etc.) - + Ported from MapleClient.java login() method """ def authenticate_user(username, password, ip_address \\ "") do @@ -69,7 +86,7 @@ defmodule Odinsea.Database.Context do @doc """ Updates account login state. - + States: - 0 = LOGIN_NOTLOGGEDIN - 1 = LOGIN_SERVER_TRANSITION (migrating between servers) @@ -79,11 +96,12 @@ defmodule Odinsea.Database.Context do def update_login_state(account_id, state, session_ip \\ nil) do updates = [loggedin: state] updates = if session_ip, do: Keyword.put(updates, :session_ip, session_ip), else: updates - + Repo.update_all( from(a in Account, where: a.id == ^account_id), set: updates ) + :ok end @@ -95,6 +113,7 @@ defmodule Odinsea.Database.Context do from(a in Account, where: a.id == ^account_id), set: [loggedin: status] ) + :ok end @@ -113,17 +132,18 @@ defmodule Odinsea.Database.Context do """ def update_last_login(account_id) do now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) - + Repo.update_all( from(a in Account, where: a.id == ^account_id), set: [lastlogin: now] ) + :ok end @doc """ Bans an account. - + Options: - :banned - ban status (1 = banned, 2 = auto-banned) - :banreason - reason for ban @@ -132,7 +152,9 @@ defmodule Odinsea.Database.Context do """ def ban_account(account_id, attrs \\ %{}) do case Repo.get(Account, account_id) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + account -> account |> Account.ban_changeset(attrs) @@ -140,18 +162,94 @@ defmodule Odinsea.Database.Context do end end + @doc """ + Bans an account with explicit reason and duration (in days). + For permanent ban, set duration to nil or 0. + """ + def ban_account(account_id, reason, duration) do + attrs = %{ + banned: 1, + banreason: reason + } + + attrs = + if duration && duration > 0 do + tempban = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(duration * 86400, :second) + |> NaiveDateTime.truncate(:second) + + Map.put(attrs, :tempban, tempban) + else + attrs + end + + ban_account(account_id, attrs) + end + + @doc """ + Checks if an IP address is banned. + Returns true if the IP is in the ipbans table. + """ + def ip_banned?(ip_address) do + IpBan + |> where([i], i.ip == ^ip_address) + |> Repo.exists?() + end + + @doc """ + Checks if a MAC address is banned. + Returns true if the MAC is in the macbans table. + """ + def mac_banned?(mac_address) do + MacBan + |> where([m], m.mac == ^mac_address) + |> Repo.exists?() + end + + @doc """ + Alias for log_ip_address/2. + Also known as ban_ip_address in some contexts. + """ + def ban_ip_address(ip_address, _reason \\ "", _is_mac \\ false, _times \\ 1) do + # For enforcement, we insert into ipbans table + # The original Java code logs and potentially bans + %IpBan{} + |> IpBan.changeset(%{ip: ip_address}) + |> Repo.insert() + |> case do + {:ok, _} -> :ok + {:error, _} -> :error + end + end + + @doc """ + Updates the second password (SPW) for an account. + Input: account_id, hashed_password + """ + def update_second_password(account_id, hashed_password) do + Repo.update_all( + from(a in Account, where: a.id == ^account_id), + set: [second_password: hashed_password] + ) + + :ok + end + @doc """ Records an IP log entry for audit purposes. """ def log_ip_address(account_id, ip_address) do timestamp = format_timestamp(NaiveDateTime.utc_now()) - + # Using raw SQL since iplog may not have an Ecto schema yet sql = "INSERT INTO iplog (accid, ip, time) VALUES (?, ?, ?)" - + case Ecto.Adapters.SQL.query(Repo, sql, [account_id, ip_address, timestamp]) do - {:ok, _} -> :ok - {:error, err} -> + {:ok, _} -> + :ok + + {:error, err} -> Logger.error("Failed to log IP: #{inspect(err)}") :error end @@ -163,8 +261,10 @@ defmodule Odinsea.Database.Context do """ def check_ban_status(account_id) do case Repo.get(Account, account_id) do - nil -> {:error, :not_found} - account -> + nil -> + {:error, :not_found} + + account -> if account.banned > 0 do {:error, :banned} else @@ -178,9 +278,12 @@ defmodule Odinsea.Database.Context do """ def get_temp_ban_info(account_id) do case Repo.get(Account, account_id) do - nil -> nil - account -> - if account.tempban && NaiveDateTime.compare(account.tempban, NaiveDateTime.utc_now()) == :gt do + nil -> + nil + + account -> + if account.tempban && + NaiveDateTime.compare(account.tempban, NaiveDateTime.utc_now()) == :gt do %{reason: account.banreason, expires: account.tempban} else nil @@ -195,14 +298,146 @@ defmodule Odinsea.Database.Context do updates = [acash: acash, mpoints: mpoints] updates = if points, do: Keyword.put(updates, :points, points), else: updates updates = if vpoints, do: Keyword.put(updates, :vpoints, vpoints), else: updates - + Repo.update_all( from(a in Account, where: a.id == ^account_id), set: updates ) + :ok end + # ================================================================================================== + # Cash Shop Operations + # ================================================================================================== + + @doc """ + Gets coupon information by code. + Input: coupon_code string + Output: {:ok, coupon} or {:error, reason} + """ + def get_coupon_info(coupon_code) do + case Repo.get(NxCode, coupon_code) do + nil -> {:error, :not_found} + coupon -> {:ok, coupon} + end + end + + @doc """ + Marks a coupon as used by an account. + Input: coupon_id (code string), account_id + Output: {:ok, _} or {:error, _} + """ + def mark_coupon_used(coupon_id, account_id) do + case Repo.get(NxCode, coupon_id) do + nil -> + {:error, :not_found} + + coupon -> + # Mark as invalid (0) and set user to account name + # We need to look up the account name first + account = Repo.get(Account, account_id) + user_name = if account, do: account.name, else: "" + + coupon + |> NxCode.changeset(%{valid: 0, user: user_name}) + |> Repo.update() + end + end + + @doc """ + Increments character slots for an account in a world. + Input: account_id, amount (default 1) + Output: {:ok, new_slot_count} or {:error, _} + """ + def increment_character_slots(account_id, amount \\ 1, world_id \\ 0) do + case CharacterSlot + |> where([c], c.accid == ^account_id and c.worldid == ^world_id) + |> Repo.one() do + nil -> + # Create new slot record with default + amount + %CharacterSlot{} + |> CharacterSlot.changeset(%{ + accid: account_id, + worldid: world_id, + charslots: 6 + amount + }) + |> Repo.insert() + |> case do + {:ok, slot} -> {:ok, slot.charslots} + error -> error + end + + slot -> + # Update existing + new_count = slot.charslots + amount + + slot + |> CharacterSlot.changeset(%{charslots: new_count}) + |> Repo.update() + |> case do + {:ok, _} -> {:ok, new_count} + error -> error + end + end + end + + @doc """ + Loads gifts for an account. + Input: account_id (used as recipient ID) + Output: {:ok, gifts} or {:error, _} + """ + def load_gifts(account_id) do + gifts = + Gift + |> where([g], g.recipient == ^account_id) + |> Repo.all() + + {:ok, gifts} + end + + @doc """ + Creates a new gift (cash shop gift from one character to another). + + ## Parameters + - attrs: Map with keys: + - :recipient (required) - Account ID of the recipient + - :from (required) - Name of the sender + - :message (optional) - Gift message + - :sn (optional) - Cash shop serial number + - :uniqueid (optional) - Unique item ID + + Output: {:ok, gift} or {:error, changeset} + """ + def create_gift(attrs) do + %Gift{} + |> Gift.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Creates a new gift with explicit parameters. + Alternative to create_gift/1 for direct parameter passing. + + ## Parameters + - recipient_id: Account ID of the recipient + - from_name: Name of the sender + - message: Gift message + - sn: Cash shop serial number + - uniqueid: Unique item ID + """ + def create_gift(recipient_id, from_name, message \\ "", sn \\ 0, uniqueid \\ 0) do + attrs = %{ + recipient: recipient_id, + from: from_name, + message: message, + sn: sn, + uniqueid: uniqueid + } + + create_gift(attrs) + end + # ================================================================================================== # Character Operations # ================================================================================================== @@ -210,7 +445,7 @@ defmodule Odinsea.Database.Context do @doc """ Loads character entries for an account in a specific world. Returns a list of character summaries (id, name, gm level). - + Ported from UnifiedDB.loadCharactersEntry() """ def load_character_entries(account_id, world_id) do @@ -262,13 +497,98 @@ defmodule Odinsea.Database.Context do @doc """ Loads full character data by ID. Returns the Character struct or nil if not found. - + TODO: Expand to load related data (inventory, skills, quests, etc.) """ def load_character(character_id) do Repo.get(Character, character_id) end + @doc """ + Gets a character by ID. + Alias for load_character/1 with {:ok, character} or {:error, reason} return. + """ + def get_character(character_id) do + case Repo.get(Character, character_id) do + nil -> {:error, :not_found} + character -> {:ok, character} + end + end + + @doc """ + Updates character data. + Input: character_id, attrs map + Uses a generic update changeset that allows updating any character field. + """ + def update_character(character_id, attrs) do + case Repo.get(Character, character_id) do + nil -> + {:error, :not_found} + + character -> + # Use a generic changeset that casts all valid character fields + changeset = + Ecto.Changeset.cast(character, attrs, [ + :name, + :level, + :exp, + :str, + :dex, + :luk, + :int, + :hp, + :mp, + :maxhp, + :maxmp, + :ap, + :meso, + :fame, + :job, + :skincolor, + :gender, + :hair, + :face, + :map, + :spawnpoint, + :gm, + :party, + :buddy_capacity, + :guildid, + :guildrank, + :alliance_rank, + :guild_contribution, + :pets, + :sp, + :subcategory, + :rank, + :rank_move, + :job_rank, + :job_rank_move, + :marriage_id, + :familyid, + :seniorid, + :junior1, + :junior2, + :currentrep, + :totalrep, + :gachexp, + :fatigue, + :charm, + :craft, + :charisma, + :will, + :sense, + :insight, + :total_wins, + :total_losses, + :pvp_exp, + :pvp_points + ]) + + Repo.update(changeset) + end + end + @doc """ Checks if a character name is already in use. """ @@ -297,7 +617,7 @@ defmodule Odinsea.Database.Context do @doc """ Creates a new character. - + TODO: Add initial items, quests, and stats based on job type """ def create_character(attrs) do @@ -308,27 +628,28 @@ defmodule Odinsea.Database.Context do @doc """ Deletes a character (soft delete - renames and moves to deleted world). - + Returns {:ok, character} on success, {:error, reason} on failure. - + Ported from UnifiedDB.deleteCharacter() """ def delete_character(character_id) do # TODO: Check guild rank (can't delete if guild leader) # TODO: Remove from family # TODO: Handle sidekick - - deleted_world = -1 # WORLD_DELETED - + + # WORLD_DELETED + deleted_world = -1 + # Soft delete: rename with # prefix and move to deleted world # Need to get the character name first to construct the new name case Repo.get(Character, character_id) do - nil -> + nil -> {:error, :not_found} - + character -> new_name = "#" <> character.name - + Repo.update_all( from(c in Character, where: c.id == ^character_id), set: [ @@ -336,10 +657,10 @@ defmodule Odinsea.Database.Context do world: deleted_world ] ) - + # Clean up related records cleanup_character_assets(character_id) - + :ok end end @@ -349,7 +670,9 @@ defmodule Odinsea.Database.Context do """ def update_character_stats(character_id, attrs) do case Repo.get(Character, character_id) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + character -> character |> Character.stat_changeset(attrs) @@ -362,7 +685,9 @@ defmodule Odinsea.Database.Context do """ def update_character_position(character_id, map_id, spawn_point) do case Repo.get(Character, character_id) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + character -> character |> Character.position_changeset(%{map: map_id, spawnpoint: spawn_point}) @@ -378,6 +703,7 @@ defmodule Odinsea.Database.Context do from(c in Character, where: c.id == ^character_id), set: [meso: meso] ) + :ok end @@ -389,6 +715,7 @@ defmodule Odinsea.Database.Context do from(c in Character, where: c.id == ^character_id), set: [exp: exp] ) + :ok end @@ -400,6 +727,7 @@ defmodule Odinsea.Database.Context do from(c in Character, where: c.id == ^character_id), set: [job: job_id] ) + :ok end @@ -411,6 +739,7 @@ defmodule Odinsea.Database.Context do from(c in Character, where: c.id == ^character_id), set: [level: level] ) + :ok end @@ -420,11 +749,12 @@ defmodule Odinsea.Database.Context do def update_character_guild(character_id, guild_id, guild_rank \\ nil) do updates = [guildid: guild_id] updates = if guild_rank, do: Keyword.put(updates, :guildrank, guild_rank), else: updates - + Repo.update_all( from(c in Character, where: c.id == ^character_id), set: updates ) + :ok end @@ -433,11 +763,12 @@ defmodule Odinsea.Database.Context do """ def update_character_sp(character_id, sp_list) do sp_string = Enum.join(sp_list, ",") - + Repo.update_all( from(c in Character, where: c.id == ^character_id), set: [sp: sp_string] ) + :ok end @@ -447,7 +778,7 @@ defmodule Odinsea.Database.Context do @doc """ Gets default stats for a new character based on job type. - + Job types: - 0 = Resistance - 1 = Adventurer @@ -469,22 +800,33 @@ defmodule Odinsea.Database.Context do int: 4, ap: 0 } - + case job_type do - 0 -> # Resistance + # Resistance + 0 -> %{base_stats | job: 3000, str: 12, dex: 5, int: 4, luk: 4} - 1 -> # Adventurer - if subcategory == 1 do # Dual Blade + + # Adventurer + 1 -> + # Dual Blade + if subcategory == 1 do %{base_stats | job: 430, str: 4, dex: 25, int: 4, luk: 4} else %{base_stats | job: 0, str: 12, dex: 5, int: 4, luk: 4} end - 2 -> # Cygnus + + # Cygnus + 2 -> %{base_stats | job: 1000, str: 12, dex: 5, int: 4, luk: 4} - 3 -> # Aran + + # Aran + 3 -> %{base_stats | job: 2000, str: 12, dex: 5, int: 4, luk: 4} - 4 -> # Evan + + # Evan + 4 -> %{base_stats | job: 2001, str: 4, dex: 4, int: 12, luk: 5} + _ -> base_stats end @@ -495,12 +837,18 @@ defmodule Odinsea.Database.Context do """ def get_default_map_for_job(job_type) do case job_type do - 0 -> 931000000 # Resistance tutorial - 1 -> 0 # Adventurer - Maple Island (handled specially) - 2 -> 130030000 # Cygnus tutorial - 3 -> 914000000 # Aran tutorial - 4 -> 900010000 # Evan tutorial - _ -> 100000000 # Default to Henesys + # Resistance tutorial + 0 -> 931_000_000 + # Adventurer - Maple Island (handled specially) + 1 -> 0 + # Cygnus tutorial + 2 -> 130_030_000 + # Aran tutorial + 3 -> 914_000_000 + # Evan tutorial + 4 -> 900_010_000 + # Default to Henesys + _ -> 100_000_000 end end @@ -513,13 +861,26 @@ defmodule Odinsea.Database.Context do """ def forbidden_name?(name) do forbidden = [ - "admin", "gm", "gamemaster", "moderator", "mod", - "owner", "developer", "dev", "support", "help", - "system", "server", "odinsea", "maplestory", "nexon" + "admin", + "gm", + "gamemaster", + "moderator", + "mod", + "owner", + "developer", + "dev", + "support", + "help", + "system", + "server", + "odinsea", + "maplestory", + "nexon" ] - + name_lower = String.downcase(name) - Enum.any?(forbidden, fn forbidden -> + + Enum.any?(forbidden, fn forbidden -> String.contains?(name_lower, forbidden) end) end @@ -534,42 +895,35 @@ defmodule Odinsea.Database.Context do Logger.warning("Banned account attempted login: #{account.name}") {:error, :banned} else - # Check login state - login_state = account.loggedin - - if login_state > 0 do - # Already logged in - could check if stale session - Logger.warning("Account already logged in: #{account.name}") - {:error, :already_logged_in} - else - verify_password(account, password, ip_address) - end + # Login state check is handled by the handler (stale session kicking) + # not here - Java's CharLoginHandler.OnCheckPassword does the same + verify_password(account, password, ip_address) end end defp verify_password(account, password, ip_address) do # Check various password formats - valid = + valid = check_salted_sha512(account.password, password, account.salt) || - check_plain_match(account.password, password) || - check_admin_bypass(password, ip_address) + check_plain_match(account.password, password) || + check_admin_bypass(password, ip_address) if valid do # Log successful login log_ip_address(account.id, ip_address) update_last_login(account.id) - + # Build account info map account_info = %{ account_id: account.id, username: account.name, gender: account.gender, is_gm: account.gm > 0, - second_password: decrypt_second_password(account.second_password, account.salt2), + second_password: decrypt_second_password(account.second_password, account.second_salt), acash: account.acash, mpoints: account.mpoints } - + {:ok, account_info} else {:error, :invalid_credentials} @@ -578,6 +932,7 @@ defmodule Odinsea.Database.Context do defp check_salted_sha512(_hash, _password, nil), do: false defp check_salted_sha512(_hash, _password, ""), do: false + defp check_salted_sha512(hash, password, salt) do # Use LoginCrypto to verify salted SHA-512 hash case LoginCrypto.verify_salted_sha512(password, salt, hash) do @@ -599,8 +954,9 @@ defmodule Odinsea.Database.Context do defp decrypt_second_password(nil, _), do: nil defp decrypt_second_password("", _), do: nil - defp decrypt_second_password(spw, salt2) do - if salt2 && salt2 != "" do + + defp decrypt_second_password(spw, second_salt) do + if second_salt && second_salt != "" do # Decrypt using rand_r (reverse of rand_s) LoginCrypto.rand_r(spw) else @@ -622,13 +978,13 @@ defmodule Odinsea.Database.Context do defp cleanup_character_assets(character_id) do # Clean up pokemon, buddies, etc. # Using raw SQL for tables that don't have schemas yet - + try do Ecto.Adapters.SQL.query(Repo, "DELETE FROM buddies WHERE buddyid = ?", [character_id]) rescue _ -> :ok end - + :ok end @@ -639,19 +995,19 @@ defmodule Odinsea.Database.Context do @doc """ Loads all inventory items for a character. Returns a map of inventory types to lists of items. - + Ported from ItemLoader.java """ def load_character_inventory(character_id) do - items = + items = InventoryItem |> where([i], i.characterid == ^character_id) |> Repo.all() - + # Group by inventory type items |> Enum.map(&InventoryItem.to_game_item/1) - |> Enum.group_by(fn item -> + |> Enum.group_by(fn item -> db_item = Enum.find(items, fn db -> db.inventoryitemid == item.id end) InventoryType.from_type(db_item.inventorytype) end) @@ -662,7 +1018,7 @@ defmodule Odinsea.Database.Context do """ def get_inventory_items(character_id, inv_type) do type_value = InventoryType.type_value(inv_type) - + InventoryItem |> where([i], i.characterid == ^character_id and i.inventorytype == ^type_value) |> Repo.all() @@ -693,7 +1049,7 @@ defmodule Odinsea.Database.Context do """ def create_inventory_item(character_id, inv_type, item) do attrs = InventoryItem.from_game_item(item, character_id, inv_type) - + %InventoryItem{} |> InventoryItem.changeset(attrs) |> Repo.insert() @@ -704,14 +1060,15 @@ defmodule Odinsea.Database.Context do """ def save_inventory_item(character_id, inv_type, item) do attrs = InventoryItem.from_game_item(item, character_id, inv_type) - + if item.id do # Update existing case Repo.get(InventoryItem, item.id) do - nil -> + nil -> %InventoryItem{} |> InventoryItem.changeset(attrs) |> Repo.insert() + db_item -> db_item |> InventoryItem.changeset(attrs) @@ -730,7 +1087,9 @@ defmodule Odinsea.Database.Context do """ def update_inventory_item(item_id, updates) do case Repo.get(InventoryItem, item_id) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + db_item -> db_item |> InventoryItem.changeset(updates) @@ -746,6 +1105,7 @@ defmodule Odinsea.Database.Context do from(i in InventoryItem, where: i.inventoryitemid == ^item_id), set: [position: position] ) + :ok end @@ -765,7 +1125,7 @@ defmodule Odinsea.Database.Context do Enum.each(inventories, fn {inv_type, inventory} -> Enum.each(inventory.items, fn {_pos, item} -> attrs = InventoryItem.from_game_item(item, character_id, inv_type) - + if item.id do # Update existing update_inventory_item(item.id, attrs) @@ -775,7 +1135,7 @@ defmodule Odinsea.Database.Context do end end) end) - + :ok end @@ -802,7 +1162,7 @@ defmodule Odinsea.Database.Context do pending: 1, groupname: group_name } - + %Buddy{} |> Buddy.changeset(attrs) |> Repo.insert() @@ -813,10 +1173,12 @@ defmodule Odinsea.Database.Context do """ def accept_buddy(character_id, buddy_id) do Repo.update_all( - from(b in Buddy, - where: b.characterid == ^character_id and b.buddyid == ^buddy_id), + from(b in Buddy, + where: b.characterid == ^character_id and b.buddyid == ^buddy_id + ), set: [pending: 0] ) + :ok end @@ -825,10 +1187,13 @@ defmodule Odinsea.Database.Context do """ def delete_buddy(character_id, buddy_id) do Repo.delete_all( - from(b in Buddy, - where: (b.characterid == ^character_id and b.buddyid == ^buddy_id) or - (b.characterid == ^buddy_id and b.buddyid == ^character_id)) + from(b in Buddy, + where: + (b.characterid == ^character_id and b.buddyid == ^buddy_id) or + (b.characterid == ^buddy_id and b.buddyid == ^character_id) + ) ) + :ok end @@ -877,7 +1242,9 @@ defmodule Odinsea.Database.Context do """ def update_guild(guild_id, attrs) do case Repo.get(Guild, guild_id) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + guild -> guild |> Guild.settings_changeset(attrs) @@ -890,7 +1257,9 @@ defmodule Odinsea.Database.Context do """ def update_guild_leader(guild_id, leader_id) do case Repo.get(Guild, guild_id) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + guild -> guild |> Guild.leader_changeset(%{leader: leader_id}) @@ -903,7 +1272,9 @@ defmodule Odinsea.Database.Context do """ def delete_guild(guild_id) do case Repo.get(Guild, guild_id) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + guild -> Repo.delete(guild) end @@ -931,6 +1302,7 @@ defmodule Odinsea.Database.Context do from(g in Guild, where: g.guildid == ^guild_id), set: [gp: gp] ) + :ok end @@ -942,6 +1314,7 @@ defmodule Odinsea.Database.Context do from(g in Guild, where: g.guildid == ^guild_id), set: [capacity: capacity] ) + :ok end @@ -958,6 +1331,7 @@ defmodule Odinsea.Database.Context do logo_bg_color: logo_bg_color ] ) + :ok end @@ -976,7 +1350,7 @@ defmodule Odinsea.Database.Context do @doc """ Starts a quest for a character (inserts new quest status). - + Status values: - 0 = Not started - 1 = In progress @@ -991,9 +1365,9 @@ defmodule Odinsea.Database.Context do forfeited: 0, custom_data: nil } - + attrs = Map.merge(defaults, attrs) - + %QuestStatus{} |> QuestStatus.changeset(attrs) |> Repo.insert() @@ -1004,12 +1378,14 @@ defmodule Odinsea.Database.Context do """ def complete_quest(character_id, quest_id, completion_time \\ nil) do time = if completion_time, do: completion_time, else: System.system_time(:second) - + Repo.update_all( - from(q in QuestStatus, - where: q.characterid == ^character_id and q.quest == ^quest_id), + from(q in QuestStatus, + where: q.characterid == ^character_id and q.quest == ^quest_id + ), set: [status: 2, time: time] ) + :ok end @@ -1018,10 +1394,12 @@ defmodule Odinsea.Database.Context do """ def forfeit_quest(character_id, quest_id) do Repo.update_all( - from(q in QuestStatus, - where: q.characterid == ^character_id and q.quest == ^quest_id), + from(q in QuestStatus, + where: q.characterid == ^character_id and q.quest == ^quest_id + ), set: [status: 0] ) + :ok end @@ -1030,10 +1408,12 @@ defmodule Odinsea.Database.Context do """ def update_quest_custom_data(character_id, quest_id, custom_data) do Repo.update_all( - from(q in QuestStatus, - where: q.characterid == ^character_id and q.quest == ^quest_id), + from(q in QuestStatus, + where: q.characterid == ^character_id and q.quest == ^quest_id + ), set: [custom_data: custom_data] ) + :ok end @@ -1042,14 +1422,16 @@ defmodule Odinsea.Database.Context do """ def update_quest_mob_kills(quest_status_id, mob_id, count) do # Check if entry exists - existing = QuestStatusMob - |> where([m], m.queststatusid == ^quest_status_id and m.mob == ^mob_id) - |> Repo.one() - + existing = + QuestStatusMob + |> where([m], m.queststatusid == ^quest_status_id and m.mob == ^mob_id) + |> Repo.one() + if existing do Repo.update_all( - from(m in QuestStatusMob, - where: m.queststatusid == ^quest_status_id and m.mob == ^mob_id), + from(m in QuestStatusMob, + where: m.queststatusid == ^quest_status_id and m.mob == ^mob_id + ), set: [count: count] ) else @@ -1061,7 +1443,7 @@ defmodule Odinsea.Database.Context do }) |> Repo.insert() end - + :ok end @@ -1083,6 +1465,38 @@ defmodule Odinsea.Database.Context do |> Repo.one() end + @doc """ + Sets quest progress for a character. + Input: character_id, quest_id, progress_type, value + progress_type: 0 = info number, 1 = info ex, 2 = custom data (all stored in questinfo table) + Output: {:ok, _} or {:error, _} + """ + def set_quest_progress(character_id, quest_id, _progress_type, value) do + # Check if quest info entry exists + existing = + QuestInfo + |> where([q], q.characterid == ^character_id and q.quest == ^quest_id) + |> Repo.one() + + case existing do + nil -> + # Create new quest info entry + %QuestInfo{} + |> QuestInfo.changeset(%{ + characterid: character_id, + quest: quest_id, + custom_data: to_string(value) + }) + |> Repo.insert() + + qi -> + # Update existing + qi + |> QuestInfo.changeset(%{custom_data: to_string(value)}) + |> Repo.update() + end + end + @doc """ Gets mob kills for a quest status. """ @@ -1098,19 +1512,20 @@ defmodule Odinsea.Database.Context do def delete_quest_status(character_id, quest_id) do # Get the quest status first to delete mob kills case get_quest_status(character_id, quest_id) do - nil -> :ok + nil -> + :ok + qs -> # Delete mob kills - Repo.delete_all( - from(m in QuestStatusMob, where: m.queststatusid == ^qs.queststatusid) - ) - + Repo.delete_all(from(m in QuestStatusMob, where: m.queststatusid == ^qs.queststatusid)) + # Delete quest status Repo.delete_all( - from(q in QuestStatus, - where: q.characterid == ^character_id and q.quest == ^quest_id) + from(q in QuestStatus, + where: q.characterid == ^character_id and q.quest == ^quest_id + ) ) - + :ok end end @@ -1130,7 +1545,7 @@ defmodule Odinsea.Database.Context do masterlevel: master_level, expiration: expiration } - + %Skill{} |> Skill.changeset(attrs) |> Repo.insert() @@ -1142,12 +1557,14 @@ defmodule Odinsea.Database.Context do def update_skill_level(character_id, skill_id, skill_level, master_level \\ nil) do updates = [skilllevel: skill_level] updates = if master_level, do: Keyword.put(updates, :masterlevel, master_level), else: updates - + Repo.update_all( - from(s in Skill, - where: s.characterid == ^character_id and s.skillid == ^skill_id), + from(s in Skill, + where: s.characterid == ^character_id and s.skillid == ^skill_id + ), set: updates ) + :ok end @@ -1155,9 +1572,12 @@ defmodule Odinsea.Database.Context do Saves a skill (inserts or updates). """ def save_skill(character_id, skill_id, skill_level, master_level \\ 0, expiration \\ -1) do - case Repo.one(from(s in Skill, where: s.characterid == ^character_id and s.skillid == ^skill_id)) do + case Repo.one( + from(s in Skill, where: s.characterid == ^character_id and s.skillid == ^skill_id) + ) do nil -> learn_skill(character_id, skill_id, skill_level, master_level, expiration) + skill -> skill |> Skill.changeset(%{ @@ -1192,9 +1612,11 @@ defmodule Odinsea.Database.Context do """ def delete_skill(character_id, skill_id) do Repo.delete_all( - from(s in Skill, - where: s.characterid == ^character_id and s.skillid == ^skill_id) + from(s in Skill, + where: s.characterid == ^character_id and s.skillid == ^skill_id + ) ) + :ok end @@ -1226,22 +1648,24 @@ defmodule Odinsea.Database.Context do seconds = VALUES(seconds), flags = VALUES(flags) """ - + case Ecto.Adapters.SQL.query( - Repo, - sql, - [ - pet.unique_id, - pet.name, - pet.level, - pet.closeness, - pet.fullness, - pet.seconds_left, - pet.flags - ] - ) do - {:ok, _} -> :ok - {:error, err} -> + Repo, + sql, + [ + pet.unique_id, + pet.name, + pet.level, + pet.closeness, + pet.fullness, + pet.seconds_left, + pet.flags + ] + ) do + {:ok, _} -> + :ok + + {:error, err} -> Logger.error("Failed to save pet: #{inspect(err)}") {:error, err} end @@ -1250,16 +1674,27 @@ defmodule Odinsea.Database.Context do @doc """ Creates a new pet. """ - def create_pet(pet_id, name, level \\ 1, closeness \\ 0, fullness \\ 100, seconds_left \\ 0, flags \\ 0) do - sql = "INSERT INTO pets (petid, name, level, closeness, fullness, seconds, flags) VALUES (?, ?, ?, ?, ?, ?, ?)" - + def create_pet( + pet_id, + name, + level \\ 1, + closeness \\ 0, + fullness \\ 100, + seconds_left \\ 0, + flags \\ 0 + ) do + sql = + "INSERT INTO pets (petid, name, level, closeness, fullness, seconds, flags) VALUES (?, ?, ?, ?, ?, ?, ?)" + case Ecto.Adapters.SQL.query( - Repo, - sql, - [pet_id, name, level, closeness, fullness, seconds_left, flags] - ) do - {:ok, _} -> :ok - {:error, err} -> + Repo, + sql, + [pet_id, name, level, closeness, fullness, seconds_left, flags] + ) do + {:ok, _} -> + :ok + + {:error, err} -> Logger.error("Failed to create pet: #{inspect(err)}") {:error, err} end @@ -1270,19 +1705,20 @@ defmodule Odinsea.Database.Context do """ def get_pet(pet_id) do sql = "SELECT * FROM pets WHERE petid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [pet_id]) do - {:ok, result} -> + {:ok, result} -> if result.num_rows > 0 do row = hd(result.rows) columns = Enum.map(result.columns, &String.to_atom/1) - + Enum.zip(columns, row) |> Map.new() else nil end - {:error, err} -> + + {:error, err} -> Logger.error("Failed to get pet: #{inspect(err)}") nil end @@ -1298,14 +1734,15 @@ defmodule Odinsea.Database.Context do JOIN inventoryitems i ON p.petid = i.petid WHERE i.characterid = ? AND i.petid > -1 """ - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id]) do - {:ok, result} -> + {:ok, result} -> Enum.map(result.rows, fn row -> columns = Enum.map(result.columns, &String.to_atom/1) Enum.zip(columns, row) |> Map.new() end) - {:error, err} -> + + {:error, err} -> Logger.error("Failed to get pets: #{inspect(err)}") [] end @@ -1316,7 +1753,7 @@ defmodule Odinsea.Database.Context do """ def update_pet_closeness(pet_id, closeness) do sql = "UPDATE pets SET closeness = ? WHERE petid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [closeness, pet_id]) do {:ok, _} -> :ok {:error, err} -> {:error, err} @@ -1328,7 +1765,7 @@ defmodule Odinsea.Database.Context do """ def update_pet_level(pet_id, level) do sql = "UPDATE pets SET level = ? WHERE petid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [level, pet_id]) do {:ok, _} -> :ok {:error, err} -> {:error, err} @@ -1340,7 +1777,7 @@ defmodule Odinsea.Database.Context do """ def update_pet_fullness(pet_id, fullness) do sql = "UPDATE pets SET fullness = ? WHERE petid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [fullness, pet_id]) do {:ok, _} -> :ok {:error, err} -> {:error, err} @@ -1360,10 +1797,12 @@ defmodule Odinsea.Database.Context do VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE map = VALUES(map) """ - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id, location_type, map_id]) do - {:ok, _} -> :ok - {:error, err} -> + {:ok, _} -> + :ok + + {:error, err} -> Logger.error("Failed to save location: #{inspect(err)}") {:error, err} end @@ -1374,16 +1813,18 @@ defmodule Odinsea.Database.Context do """ def get_saved_location(character_id, location_type) do sql = "SELECT map FROM savedlocations WHERE characterid = ? AND locationtype = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id, location_type]) do - {:ok, result} -> + {:ok, result} -> if result.num_rows > 0 do [[map_id]] = result.rows map_id else nil end - {:error, _} -> nil + + {:error, _} -> + nil end end @@ -1392,12 +1833,14 @@ defmodule Odinsea.Database.Context do """ def get_saved_locations(character_id) do sql = "SELECT locationtype, map FROM savedlocations WHERE characterid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id]) do - {:ok, result} -> + {:ok, result} -> Enum.map(result.rows, fn [type, map] -> {type, map} end) |> Map.new() - {:error, _} -> %{} + + {:error, _} -> + %{} end end @@ -1413,7 +1856,7 @@ defmodule Odinsea.Database.Context do INSERT INTO skills_cooldowns (charid, SkillID, StartTime, length) VALUES (?, ?, ?, ?) """ - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id, skill_id, start_time, length]) do {:ok, _} -> :ok {:error, err} -> {:error, err} @@ -1425,14 +1868,16 @@ defmodule Odinsea.Database.Context do """ def get_skill_cooldowns(character_id) do sql = "SELECT * FROM skills_cooldowns WHERE charid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id]) do - {:ok, result} -> + {:ok, result} -> Enum.map(result.rows, fn row -> columns = Enum.map(result.columns, &String.to_atom/1) Enum.zip(columns, row) |> Map.new() end) - {:error, _} -> [] + + {:error, _} -> + [] end end @@ -1441,7 +1886,7 @@ defmodule Odinsea.Database.Context do """ def clear_skill_cooldowns(character_id) do sql = "DELETE FROM skills_cooldowns WHERE charid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id]) do {:ok, _} -> :ok {:error, err} -> {:error, err} @@ -1466,7 +1911,7 @@ defmodule Odinsea.Database.Context do etc = VALUES(etc), cash = VALUES(cash) """ - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id, equip, use, setup, etc, cash]) do {:ok, _} -> :ok {:error, err} -> {:error, err} @@ -1478,16 +1923,17 @@ defmodule Odinsea.Database.Context do """ def get_inventory_slots(character_id) do sql = "SELECT equip, use, setup, etc, cash FROM inventoryslot WHERE characterid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id]) do - {:ok, result} -> + {:ok, result} -> if result.num_rows > 0 do [[equip, use, setup, etc, cash]] = result.rows %{equip: equip, use: use, setup: setup, etc: etc, cash: cash} else %{equip: 24, use: 80, setup: 80, etc: 80, cash: 40} end - {:error, _} -> + + {:error, _} -> %{equip: 24, use: 80, setup: 80, etc: 80, cash: 40} end end @@ -1503,14 +1949,14 @@ defmodule Odinsea.Database.Context do # Delete existing sql_delete = "DELETE FROM keymap WHERE characterid = ?" Ecto.Adapters.SQL.query(Repo, sql_delete, [character_id]) - + # Insert new keys sql_insert = "INSERT INTO keymap (characterid, `key`, `type`, `action`) VALUES (?, ?, ?, ?)" - + Enum.each(key_layout, fn {key, {type, action}} -> Ecto.Adapters.SQL.query(Repo, sql_insert, [character_id, key, type, action]) end) - + :ok end @@ -1519,14 +1965,16 @@ defmodule Odinsea.Database.Context do """ def get_key_layout(character_id) do sql = "SELECT `key`, `type`, `action` FROM keymap WHERE characterid = ?" - + case Ecto.Adapters.SQL.query(Repo, sql, [character_id]) do - {:ok, result} -> + {:ok, result} -> Enum.map(result.rows, fn [key, type, action] -> {key, {type, action}} end) |> Map.new() - {:error, _} -> %{} + + {:error, _} -> + %{} end end end diff --git a/lib/odinsea/database/schema/account.ex b/lib/odinsea/database/schema/account.ex index 04582be..16cb628 100644 --- a/lib/odinsea/database/schema/account.ex +++ b/lib/odinsea/database/schema/account.ex @@ -11,33 +11,33 @@ defmodule Odinsea.Database.Schema.Account do @timestamps_opts [inserted_at: :createdat, updated_at: false] schema "accounts" do - field :name, :string - field :password, :string - field :salt, :string - field :second_password, :string, source: :"2ndpassword" - field :salt2, :string - field :loggedin, :integer, default: 0 - field :lastlogin, :naive_datetime - field :createdat, :naive_datetime - field :birthday, :date - field :banned, :integer, default: 0 - field :banreason, :string - field :gm, :integer, default: 0 - field :email, :string - field :macs, :string - field :tempban, :naive_datetime - field :greason, :integer - field :acash, :integer, default: 0, source: :ACash - field :mpoints, :integer, default: 0, source: :mPoints - field :gender, :integer, default: 0 - field :session_ip, :string, source: :SessionIP - field :points, :integer, default: 0 - field :vpoints, :integer, default: 0 - field :totalvotes, :integer, default: 0 - field :lastlogon, :naive_datetime - field :lastvoteip, :string + field(:name, :string) + field(:password, :string) + field(:salt, :string) + field(:second_password, :string, source: :second_password) + field(:second_salt, :string) + field(:loggedin, :integer, default: 0) + field(:lastlogin, :naive_datetime) + field(:createdat, :naive_datetime) + field(:birthday, :date) + field(:banned, :integer, default: 0) + field(:banreason, :string) + field(:gm, :integer, default: 0) + field(:email, :string) + field(:macs, :string) + field(:tempban, :naive_datetime) + field(:greason, :integer) + field(:acash, :integer, default: 0, source: :ACash) + field(:mpoints, :integer, default: 0, source: :mPoints) + field(:gender, :integer, default: 0) + field(:session_ip, :string, source: :SessionIP) + field(:points, :integer, default: 0) + field(:vpoints, :integer, default: 0) + field(:totalvotes, :integer, default: 0) + field(:lastlogon, :naive_datetime) + field(:lastvoteip, :string) - has_many :characters, Odinsea.Database.Schema.Character, foreign_key: :accountid + has_many(:characters, Odinsea.Database.Schema.Character, foreign_key: :accountid) end @doc """ diff --git a/lib/odinsea/game/character.ex b/lib/odinsea/game/character.ex index b936b58..a383111 100644 --- a/lib/odinsea/game/character.ex +++ b/lib/odinsea/game/character.ex @@ -986,6 +986,13 @@ defmodule Odinsea.Game.Character do GenServer.call(via_tuple(character_id), {:remove_item_by_id, item_id, quantity}) end + @doc """ + Updates the buddy list for a character. + """ + def update_buddies(character_id, buddies) do + GenServer.cast(via_tuple(character_id), {:update_buddies, buddies}) + end + # ============================================================================ # GenServer Callbacks - Scripting Operations # ============================================================================ @@ -1014,6 +1021,14 @@ defmodule Odinsea.Game.Character do {:noreply, new_state} end + @impl true + def handle_cast({:update_buddies, buddies}, state) do + # TODO: Store buddies in state when buddy list is added to State struct + # For now, just log the update + Logger.debug("Updated buddy list for character #{state.name}") + {:noreply, state} + end + @impl true def handle_call({:add_item, inventory_type, item}, _from, state) do inventory = Map.get(state.inventories, inventory_type, Inventory.new(inventory_type)) diff --git a/lib/odinsea/game/inventory.ex b/lib/odinsea/game/inventory.ex index 4c07401..0050ed9 100644 --- a/lib/odinsea/game/inventory.ex +++ b/lib/odinsea/game/inventory.ex @@ -155,6 +155,34 @@ defmodule Odinsea.Game.Inventory do inv.slot_limit - map_size(inv.items) end + @doc """ + Checks if the inventory has space for a given item and quantity. + Returns true if there's space, false otherwise. + """ + def has_space?(%__MODULE__{} = inv, item_id, quantity) do + # Check if we can stack with existing items of same type + existing = find_by_id(inv, item_id) + + if existing do + # Can stack - check if existing stack + quantity <= slot_max + # For simplicity, assume slot_max of 9999 for stackable items + slot_max = existing[:slot_max] || 9999 + existing_qty = existing.quantity || 0 + + if existing_qty + quantity <= slot_max do + true + else + # Need additional slots for overflow + overflow = existing_qty + quantity - slot_max + slots_needed = div(overflow, slot_max) + if rem(overflow, slot_max) > 0, do: 1, else: 0 + free_slots(inv) >= slots_needed + end + else + # New item - need at least one free slot + not full?(inv) + end + end + @doc """ Gets the next available slot number. Returns -1 if the inventory is full. diff --git a/lib/odinsea/game/inventory_type.ex b/lib/odinsea/game/inventory_type.ex index 75676f5..f609a9b 100644 --- a/lib/odinsea/game/inventory_type.ex +++ b/lib/odinsea/game/inventory_type.ex @@ -124,4 +124,30 @@ defmodule Odinsea.Game.InventoryType do def slot_limit(type) do default_slot_limit(type) end + + @doc """ + Gets the inventory type from an item_id or type atom. + + For item IDs, determines type based on ID ranges: + - Equip: 1-999,999 + - Use: 2,000,000-2,999,999 + - Setup: 3,000,000-3,999,999 + - Etc: 4,000,000-4,999,999 + - Cash: 5,000,000+ + + For atoms, returns the atom if valid, or :undefined. + """ + def get_type(item_id) when is_integer(item_id) do + from_item_id(item_id) + end + + def get_type(type) when is_atom(type) do + if type in all_types() do + type + else + :undefined + end + end + + def get_type(_), do: :undefined end diff --git a/lib/odinsea/game/map.ex b/lib/odinsea/game/map.ex index 7e3a4c3..878ceac 100644 --- a/lib/odinsea/game/map.ex +++ b/lib/odinsea/game/map.ex @@ -209,6 +209,13 @@ defmodule Odinsea.Game.Map do GenServer.call(via_tuple(map_id, channel_id), :get_monsters) end + @doc """ + Gets all monsters on the map (default channel). + """ + def get_monsters(map_id) do + get_monsters(map_id, 1) + end + @doc """ Spawns a monster at the specified spawn point. """ @@ -230,6 +237,13 @@ defmodule Odinsea.Game.Map do GenServer.call(via_tuple(map_id, channel_id), {:damage_monster, oid, damage, character_id}) end + @doc """ + Damages a monster (default channel). + """ + def damage_monster(map_id, oid, damage, character_id) do + damage_monster(map_id, 1, oid, damage, character_id) + end + @doc """ Hits a reactor, advancing its state and triggering effects. """ diff --git a/lib/odinsea/login/client.ex b/lib/odinsea/login/client.ex index 03d758d..77fc1a4 100644 --- a/lib/odinsea/login/client.ex +++ b/lib/odinsea/login/client.ex @@ -1,7 +1,7 @@ defmodule Odinsea.Login.Client do @moduledoc """ Client connection handler for the login server. - Manages the login session state. + Manages the login session state and packet encryption/decryption. """ use GenServer, restart: :temporary @@ -10,6 +10,10 @@ defmodule Odinsea.Login.Client do alias Odinsea.Net.Packet.In alias Odinsea.Net.Opcodes + alias Odinsea.Net.PacketLogger + alias Odinsea.Login.Packets + alias Odinsea.Net.Cipher.ClientCrypto + alias Odinsea.Util.BitTools defstruct [ :socket, @@ -25,7 +29,25 @@ defmodule Odinsea.Login.Client do :second_password, :gender, :is_gm, - :hardware_info + :hardware_info, + :crypto, + :handshake_complete, + + # === NEW FIELDS - Critical Priority === + :created_at, # Session creation time (for session timeout) + :last_alive_ack, # Last pong received timestamp + :server_transition, # Boolean - migrating between servers + :macs, # [String.t()] - MAC addresses for ban checking + :character_slots, # integer() - Max chars per world (default 3) + + # === NEW FIELDS - Medium Priority === + :birthday, # integer() - YYMMDD format for PIN/SPW verification + :monitored, # boolean() - GM monitoring flag + :tempban, # DateTime.t() | nil - Temporary ban info + :chat_mute, # boolean() - Chat restriction + + buffer: <<>>, + character_ids: [] ] def start_link(socket) do @@ -39,6 +61,16 @@ defmodule Odinsea.Login.Client do Logger.info("Login client connected from #{ip_string}") + # Generate IVs for encryption (4 bytes each) + send_iv = :crypto.strong_rand_bytes(4) + recv_iv = :crypto.strong_rand_bytes(4) + + # Create crypto context + crypto = ClientCrypto.new_from_ivs(112, send_iv, recv_iv) + + # Get current timestamp for session tracking + current_time = System.system_time(:millisecond) + state = %__MODULE__{ socket: socket, ip: ip_string, @@ -53,9 +85,27 @@ defmodule Odinsea.Login.Client do second_password: nil, gender: 0, is_gm: false, - hardware_info: nil + hardware_info: nil, + crypto: crypto, + handshake_complete: false, + buffer: <<>>, + character_ids: [], + + # === NEW FIELDS INITIALIZATION === + created_at: current_time, + last_alive_ack: current_time, + server_transition: false, + macs: [], + character_slots: 3, + birthday: nil, + monitored: false, + tempban: nil, + chat_mute: false } + # Send hello packet (handshake) - unencrypted + send_hello_packet(state, send_iv, recv_iv) + # Start receiving packets send(self(), :receive) @@ -66,8 +116,9 @@ defmodule Odinsea.Login.Client do def handle_info(:receive, %{socket: socket} = state) do case :gen_tcp.recv(socket, 0, 30_000) do {:ok, data} -> - # Handle packet - new_state = handle_packet(data, state) + # Append to buffer and process all complete packets + new_state = %{state | buffer: state.buffer <> data} + new_state = process_buffer(new_state) send(self(), :receive) {:noreply, new_state} @@ -96,23 +147,100 @@ defmodule Odinsea.Login.Client do :ok end - defp handle_packet(data, state) do - packet = In.new(data) + # Process all complete packets from the TCP buffer + defp process_buffer(state) do + case extract_packet(state.buffer, state.crypto) do + {:ok, payload, remaining} -> + # Decrypt the payload (AES then Shanda) and morph recv IV + {updated_crypto, decrypted} = ClientCrypto.decrypt(state.crypto, payload) + + state = %{state | + buffer: remaining, + crypto: updated_crypto, + handshake_complete: true + } + + state = process_decrypted_packet(decrypted, state) + + # Try to process more packets from the buffer + process_buffer(state) + + {:need_more, _} -> + state + + {:error, reason} -> + Logger.error("Packet error from #{state.ip}: #{inspect(reason)}") + send(self(), {:disconnect, reason}) + state + end + end + + # Extract a complete encrypted packet from the buffer using the 4-byte header + defp extract_packet(buffer, _crypto) when byte_size(buffer) < 4 do + {:need_more, buffer} + end + + defp extract_packet(buffer, crypto) do + <> = buffer + + # Validate header against current recv IV + if not ClientCrypto.decode_header_valid?(crypto, raw_seq) do + Logger.warning( + "Invalid packet header: raw_seq=#{raw_seq} (0x#{Integer.to_string(raw_seq, 16)}), " <> + "expected version check failed" + ) + + {:error, :invalid_header} + else + # Decode actual packet length from header + packet_len = ClientCrypto.decode_header_len(crypto, raw_seq, raw_len) + + cond do + packet_len < 2 -> + {:error, :invalid_length_small} + + packet_len > 65535 -> + {:error, :invalid_length_large} + + byte_size(rest) < packet_len -> + # Incomplete packet - wait for more data (don't consume header) + {:need_more, buffer} + + true -> + # Extract the encrypted payload and keep remainder + payload = binary_part(rest, 0, packet_len) + remaining = binary_part(rest, packet_len, byte_size(rest) - packet_len) + {:ok, payload, remaining} + end + end + end + + defp process_decrypted_packet(decrypted_data, state) do + packet = In.new(decrypted_data) # Read opcode (first 2 bytes) case In.decode_short(packet) do {opcode, packet} -> - Logger.debug("Login packet received: opcode=0x#{Integer.to_string(opcode, 16)}") + # Extract remaining data (after opcode) for logging + remaining_data = binary_part(packet.data, packet.index, packet.length - packet.index) + + # Log the decrypted packet + context = %{ + ip: state.ip, + server_type: :login + } + + PacketLogger.log_client_packet(opcode, remaining_data, context) + dispatch_packet(opcode, packet, state) :error -> - Logger.warning("Failed to read packet opcode") + Logger.warning("Failed to read packet opcode from #{state.ip}") state end end defp dispatch_packet(opcode, packet, state) do - # Use PacketProcessor to route packets alias Odinsea.Net.Processor case Processor.handle(opcode, packet, state, :login) do @@ -137,4 +265,66 @@ defmodule Odinsea.Login.Client do defp format_ip({a, b, c, d, e, f, g, h}) do "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}" end + + defp send_hello_packet(state, send_iv, recv_iv) do + # Get maple version from config + maple_version = 112 + + # Build hello packet + hello_packet = Packets.get_hello(maple_version, send_iv, recv_iv) + + # Log the hello packet + context = %{ip: state.ip, server_type: :login} + PacketLogger.log_raw_packet("loopback", "HELLO", hello_packet, context) + + # Send the hello packet (it already includes the length header) + case :gen_tcp.send(state.socket, hello_packet) do + :ok -> + :ok + + {:error, reason} -> + Logger.error("Failed to send hello packet: #{inspect(reason)}") + {:error, reason} + end + end + + @doc """ + Sends a packet to the client with proper encryption. + """ + def send_packet(client_pid, packet_data) when is_pid(client_pid) do + GenServer.call(client_pid, {:send_packet, packet_data}) + end + + @impl true + def handle_call({:send_packet, packet_data}, _from, state) do + case encrypt_and_send(packet_data, state) do + {:ok, new_state} -> + {:reply, :ok, new_state} + + {:error, reason} -> + {:reply, {:error, reason}, state} + end + end + + defp encrypt_and_send(data, state) do + # Encrypt the data (Shanda then AES) and morph send IV + {updated_crypto, encrypted, header} = ClientCrypto.encrypt(state.crypto, data) + + # Combine header and encrypted payload + full_packet = header <> encrypted + + # Log the outgoing packet + context = %{ip: state.ip, server_type: :login} + PacketLogger.log_server_packet("SERVER", data, context) + + # Send the packet + case :gen_tcp.send(state.socket, full_packet) do + :ok -> + {:ok, %{state | crypto: updated_crypto}} + + {:error, reason} -> + Logger.error("Failed to send packet: #{inspect(reason)}") + {:error, reason} + end + end end diff --git a/lib/odinsea/login/handler.ex b/lib/odinsea/login/handler.ex index bf8adb8..7d6b637 100644 --- a/lib/odinsea/login/handler.ex +++ b/lib/odinsea/login/handler.ex @@ -14,7 +14,8 @@ defmodule Odinsea.Login.Handler do require Logger alias Odinsea.Net.Packet.{In, Out} - alias Odinsea.Net.Cipher.LoginCrypto + alias Odinsea.Net.Cipher.{ClientCrypto, LoginCrypto} + alias Odinsea.Net.PacketLogger alias Odinsea.Login.Packets alias Odinsea.Constants.Server alias Odinsea.Database.Context @@ -79,19 +80,26 @@ defmodule Odinsea.Login.Handler do Logger.info("Login attempt: username=#{username} from #{state.ip}") # Check if IP/MAC is banned + features = Application.get_env(:odinsea, :features, []) + skip_maccheck = Keyword.get(features, :skip_maccheck, false) + ip_banned = Context.ip_banned?(state.ip) - mac_banned = Context.mac_banned?(state.mac) + mac_banned = if skip_maccheck or state.macs == [] do + false + else + Enum.any?(state.macs, &Context.mac_banned?/1) + end if (ip_banned || mac_banned) do - Logger.warning("Banned IP/MAC attempted login: ip=#{state.ip}, mac=#{state.mac}") - + Logger.warning("Banned IP/MAC attempted login: ip=#{state.ip}, macs=#{inspect(state.macs)}") + # If MAC banned, also ban the IP for enforcement if mac_banned do Context.ban_ip_address(state.ip, "Enforcing account ban, account #{username}", false, 4) end - + response = Packets.get_login_failed(3) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} else # Authenticate with database @@ -106,7 +114,7 @@ defmodule Odinsea.Login.Handler do format_timestamp(temp_ban_info.expires), temp_ban_info.reason || "" ) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} else # Check if already logged in - kick other session @@ -132,6 +140,8 @@ defmodule Odinsea.Login.Handler do account_info.second_password ) + state = send_packet(state, response) + new_state = state |> Map.put(:logged_in, true) @@ -142,8 +152,8 @@ defmodule Odinsea.Login.Handler do |> Map.put(:second_password, account_info.second_password) |> Map.put(:login_attempts, 0) - send_packet(state, response) - {:ok, new_state} + # Send world info immediately after auth success (Java: LoginWorker.registerClient) + on_world_info_request(new_state) end {:error, :invalid_credentials} -> @@ -156,7 +166,7 @@ defmodule Odinsea.Login.Handler do else # Send login failed (reason 4 = incorrect password) response = Packets.get_login_failed(4) - send_packet(state, response) + state = send_packet(state, response) new_state = Map.put(state, :login_attempts, login_attempts) {:ok, new_state} @@ -165,7 +175,7 @@ defmodule Odinsea.Login.Handler do {:error, :account_not_found} -> # Send login failed (reason 5 = not registered ID) response = Packets.get_login_failed(5) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} {:error, :already_logged_in} -> @@ -176,12 +186,12 @@ defmodule Odinsea.Login.Handler do # Send login failed (reason 7 = already logged in) but client can retry response = Packets.get_login_failed(7) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} {:error, :banned} -> response = Packets.get_perm_ban(0) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} end end @@ -209,19 +219,19 @@ defmodule Odinsea.Login.Handler do channel_load ) - send_packet(state, server_list) + state = send_packet(state, server_list) # Send end of server list end_list = Packets.get_end_of_server_list() - send_packet(state, end_list) + state = send_packet(state, end_list) # Send latest connected world latest_world = Packets.get_latest_connected_world(0) - send_packet(state, latest_world) + state = send_packet(state, latest_world) # Send recommended world message recommend = Packets.get_recommend_world_message(0, "Join now!") - send_packet(state, recommend) + state = send_packet(state, recommend) {:ok, state} end @@ -246,7 +256,7 @@ defmodule Odinsea.Login.Handler do end response = Packets.get_server_status(status) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} end @@ -277,7 +287,7 @@ defmodule Odinsea.Login.Handler do if world_id != 0 do Logger.warning("Invalid world ID: #{world_id}") response = Packets.get_login_failed(10) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} else # TODO: Check if channel is available @@ -299,7 +309,7 @@ defmodule Odinsea.Login.Handler do 3 # character slots ) - send_packet(state, response) + state = send_packet(state, response) new_state = state @@ -332,7 +342,7 @@ defmodule Odinsea.Login.Handler do name_used = check_name_used(char_name, state) response = Packets.get_char_name_response(char_name, name_used) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} end @@ -381,7 +391,7 @@ defmodule Odinsea.Login.Handler do # Validate name is not forbidden and doesn't exist if check_name_used(name, state) do response = Packets.get_add_new_char_entry(nil, false) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} else # TODO: Validate appearance items are eligible for gender/job type @@ -420,8 +430,8 @@ defmodule Odinsea.Login.Handler do # Reload character with full data char_data = Context.load_character(character.id) response = Packets.get_add_new_char_entry(char_data, true) - send_packet(state, response) - + state = send_packet(state, response) + # Add character ID to state's character list new_char_ids = [character.id | Map.get(state, :character_ids, [])] new_state = Map.put(state, :character_ids, new_char_ids) @@ -430,7 +440,7 @@ defmodule Odinsea.Login.Handler do {:error, changeset} -> Logger.error("Failed to create character: #{inspect(changeset.errors)}") response = Packets.get_add_new_char_entry(nil, false) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} end end @@ -511,8 +521,8 @@ defmodule Odinsea.Login.Handler do end response = Packets.get_delete_char_response(character_id, result) - send_packet(state, response) - + state = send_packet(state, response) + # Update state if successful new_state = if result == 0 do @@ -559,7 +569,7 @@ defmodule Odinsea.Login.Handler do # Validate second password length if String.length(new_spw) < 6 || String.length(new_spw) > 16 do response = Packets.get_second_pw_error(0x14) - send_packet(state, response) + state = send_packet(state, response) {:ok, state} else # Update second password @@ -605,9 +615,9 @@ defmodule Odinsea.Login.Handler do # Send migration command response = Packets.get_server_ip(false, channel_ip, channel_port, character_id) - send_packet(state, response) - - new_state = + state = send_packet(state, response) + + new_state = state |> Map.put(:character_id, character_id) |> Map.put(:migration_token, migration_token) @@ -643,7 +653,7 @@ defmodule Odinsea.Login.Handler do else # Failure - send error response = Packets.get_second_pw_error(15) # Incorrect SPW - send_packet(state, response) + state = send_packet(state, response) {:ok, state} end end @@ -662,14 +672,12 @@ defmodule Odinsea.Login.Handler do # TODO: Send damage cap packet if custom client # Send login background - background = Application.get_env(:odinsea, :login_background, "MapLogin") - bg_response = Packets.get_login_background(background) - send_packet(state, bg_response) + bg_response = Packets.get_login_background(Server.maplogin_default()) + state = send_packet(state, bg_response) # Send RSA public key - pub_key = Application.get_env(:odinsea, :rsa_public_key, "") - key_response = Packets.get_rsa_key(pub_key) - send_packet(state, key_response) + key_response = Packets.get_rsa_key(Server.pub_key()) + state = send_packet(state, key_response) {:ok, state} end @@ -678,27 +686,40 @@ defmodule Odinsea.Login.Handler do # Helper Functions # ================================================================================================== - defp send_packet(%{socket: socket} = state, packet_data) do - # Add header (2 bytes: packet length) - packet_length = byte_size(packet_data) - header = <> - full_packet = header <> packet_data + defp send_packet(%{socket: socket, crypto: crypto} = state, packet_data) do + # Flatten iodata to binary for pattern matching + packet_data = IO.iodata_to_binary(packet_data) + + # Extract opcode from packet data (first 2 bytes) + <> = packet_data + + # Log the packet + context = %{ + ip: state.ip, + server_type: :login + } + PacketLogger.log_server_packet(opcode, rest, context) + + # Encrypt the data (Shanda then AES) and morph send IV + {updated_crypto, encrypted, header} = ClientCrypto.encrypt(crypto, packet_data) + + # Send encrypted packet with 4-byte crypto header + full_packet = header <> encrypted case :gen_tcp.send(socket, full_packet) do :ok -> - Logger.debug("Sent packet: #{packet_length} bytes") - :ok + %{state | crypto: updated_crypto} {:error, reason} -> Logger.error("Failed to send packet: #{inspect(reason)}") - {:error, reason} + state end end - defp send_packet(_state, _packet_data) do + defp send_packet(state, _packet_data) do # Socket not available in state Logger.error("Cannot send packet: socket not in state") - :error + state end defp authenticate_user(username, password, _state) do diff --git a/lib/odinsea/login/packets.ex b/lib/odinsea/login/packets.ex index d6ff0be..61cbbb2 100644 --- a/lib/odinsea/login/packets.ex +++ b/lib/odinsea/login/packets.ex @@ -40,7 +40,7 @@ defmodule Odinsea.Login.Packets do |> Out.encode_buffer(recv_iv) |> Out.encode_buffer(send_iv) |> Out.encode_byte(Server.maple_locale()) - |> Out.to_data() + |> Out.to_iodata() end @doc """ @@ -49,27 +49,26 @@ defmodule Odinsea.Login.Packets do """ def get_ping do Out.new(Opcodes.lp_alive_req()) - |> Out.to_data() + |> Out.to_iodata() end @doc """ Sends the login background image path to the client. """ def get_login_background(background_path) do - # Note: In Java this uses LoopbackPacket.LOGIN_AUTH - # Need to verify the correct opcode for this - Out.new(Opcodes.lp_set_client_key()) # TODO: Verify opcode + # Uses LOGIN_AUTH (0x17) opcode + Out.new(Opcodes.lp_login_auth()) |> Out.encode_string(background_path) - |> Out.to_data() + |> Out.to_iodata() end @doc """ Sends the RSA public key to the client for password encryption. """ def get_rsa_key(public_key) do - Out.new(Opcodes.lp_set_client_key()) + Out.new(Opcodes.lp_rsa_key()) |> Out.encode_string(public_key) - |> Out.to_data() + |> Out.to_iodata() end # ================================================================================================== @@ -104,13 +103,13 @@ defmodule Odinsea.Login.Packets do reason == 7 -> # Already logged in - Out.encode_bytes(packet, <<0, 0, 0, 0, 0>>) + Out.encode_buffer(packet, <<0, 0, 0, 0, 0>>) true -> packet end - Out.to_data(packet) + Out.to_iodata(packet) end @doc """ @@ -122,7 +121,7 @@ defmodule Odinsea.Login.Packets do |> Out.encode_int(0) |> Out.encode_short(reason) |> Out.encode_buffer(<<1, 1, 1, 1, 0>>) - |> Out.to_data() + |> Out.to_iodata() end @doc """ @@ -134,7 +133,7 @@ defmodule Odinsea.Login.Packets do |> Out.encode_short(0) |> Out.encode_byte(reason) |> Out.encode_long(timestamp_till) - |> Out.to_data() + |> Out.to_iodata() end @doc """ @@ -147,26 +146,23 @@ defmodule Odinsea.Login.Packets do - `is_gm` - Admin/GM status - `second_password` - Second password (nil if not set) """ - def get_auth_success(account_id, account_name, gender, is_gm, second_password) do + def get_auth_success(account_id, account_name, gender, is_gm, _second_password) do admin_byte = if is_gm, do: 1, else: 0 - spw_byte = get_second_password_byte(second_password) Out.new(Opcodes.lp_check_password_result()) - |> Out.encode_bytes(<<0, 0, 0, 0, 0, 0>>) # GMS specific + |> Out.encode_byte(0) # MSEA: 1 byte padding (not 6 like GMS) |> Out.encode_int(account_id) |> Out.encode_byte(gender) |> Out.encode_byte(admin_byte) # Admin byte - Find, Trade, etc. - |> Out.encode_short(2) # GMS: 2 for existing accounts, 0 for new + # NO encode_short(2) for MSEA - this is GMS only! |> Out.encode_byte(admin_byte) # Admin byte - Commands |> Out.encode_string(account_name) |> Out.encode_int(3) # 3 for existing accounts, 0 for new - |> Out.encode_bytes(<<0, 0, 0, 0, 0, 0>>) - |> Out.encode_long(get_time(System.system_time(:millisecond))) # Account creation date - |> Out.encode_int(4) # 4 for existing accounts, 0 for new - |> Out.encode_byte(1) # 1 = PIN disabled, 0 = PIN enabled - |> Out.encode_byte(spw_byte) # Second password status - |> Out.encode_long(:rand.uniform(1_000_000_000_000_000_000)) # Random long for anti-hack - |> Out.to_data() + |> Out.encode_buffer(<<0, 0, 0, 0, 0, 0>>) # 6 bytes padding + # MSEA ending (different from GMS - no time long, PIN/SPW bytes, random long) + |> Out.encode_short(0) + |> Out.encode_int(get_current_date()) + |> Out.to_iodata() end # ================================================================================================== @@ -187,14 +183,14 @@ defmodule Odinsea.Login.Packets do last_channel = get_last_channel(channel_load) packet = - Out.new(Opcodes.lp_world_information()) + Out.new(Opcodes.lp_server_list()) |> Out.encode_byte(server_id) |> Out.encode_string(world_name) |> Out.encode_byte(flag) |> Out.encode_string(event_message) |> Out.encode_short(100) # EXP rate display |> Out.encode_short(100) # Drop rate display - |> Out.encode_byte(0) # GMS specific + # NO encode_byte(0) for MSEA - this is GMS only! |> Out.encode_byte(last_channel) # Encode channel list @@ -212,16 +208,16 @@ defmodule Odinsea.Login.Packets do packet |> Out.encode_short(0) # Balloon message size |> Out.encode_int(0) - |> Out.to_data() + |> Out.to_iodata() end @doc """ Sends the end-of-server-list marker. """ def get_end_of_server_list do - Out.new(Opcodes.lp_world_information()) + Out.new(Opcodes.lp_server_list()) |> Out.encode_byte(0xFF) - |> Out.to_data() + |> Out.to_iodata() end @doc """ @@ -233,9 +229,9 @@ defmodule Odinsea.Login.Packets do - 2: Full """ def get_server_status(status) do - Out.new(Opcodes.lp_select_world_result()) + Out.new(Opcodes.lp_server_status()) |> Out.encode_short(status) - |> Out.to_data() + |> Out.to_iodata() end @doc """ @@ -244,7 +240,7 @@ defmodule Odinsea.Login.Packets do def get_latest_connected_world(world_id) do Out.new(Opcodes.lp_latest_connected_world()) |> Out.encode_int(world_id) - |> Out.to_data() + |> Out.to_iodata() end @doc """ @@ -263,7 +259,7 @@ defmodule Odinsea.Login.Packets do Out.encode_byte(packet, 0) end - Out.to_data(packet) + Out.to_iodata(packet) end # ================================================================================================== @@ -272,6 +268,7 @@ defmodule Odinsea.Login.Packets do @doc """ Sends character list for selected world. + MSEA v112.4 specific encoding. ## Parameters - `characters` - List of character maps @@ -279,55 +276,70 @@ defmodule Odinsea.Login.Packets do - `char_slots` - Number of character slots (default 3) """ def get_char_list(characters, second_password, char_slots \\ 3) do - spw_byte = get_second_password_byte(second_password) + # MSEA: no special handling for empty string SPW (GMS uses byte 2 for empty) + spw_byte = if second_password != nil and second_password != "", do: 1, else: 0 packet = - Out.new(Opcodes.lp_select_character_result()) + Out.new(Opcodes.lp_char_list()) |> Out.encode_byte(0) |> Out.encode_byte(length(characters)) - # TODO: Encode each character entry - # For now, just encode empty list structure + # Encode each character entry with MSEA-specific encoding packet = - Enum.reduce(characters, packet, fn _char, acc -> - # add_char_entry(acc, char) - acc # TODO: Implement character encoding + Enum.reduce(characters, packet, fn char, acc -> + add_char_entry(acc, char) end) packet |> Out.encode_byte(spw_byte) + |> Out.encode_byte(0) # MSEA ONLY: extra byte after SPW |> Out.encode_long(char_slots) - |> Out.to_data() + |> Out.encode_long(-:rand.uniform(9_223_372_036_854_775_807)) # MSEA ONLY: negative random long + |> Out.to_iodata() end @doc """ Character name check response. """ def get_char_name_response(char_name, name_used) do - Out.new(Opcodes.lp_check_duplicated_id_result()) + Out.new(Opcodes.lp_char_name_response()) |> Out.encode_string(char_name) |> Out.encode_byte(if name_used, do: 1, else: 0) - |> Out.to_data() + |> Out.to_iodata() end @doc """ Character creation response. """ def get_add_new_char_entry(character, worked) do - Out.new(Opcodes.lp_create_new_character_result()) + # Uses LP_AddNewCharEntry (0x0A) opcode + packet = Out.new(Opcodes.lp_add_new_char_entry()) |> Out.encode_byte(if worked, do: 0, else: 1) - # TODO: Add character entry if worked - |> Out.to_data() + + if worked do + # Add character entry for new character (ranking = false for creation) + packet = add_char_stats(packet, character) + packet = add_char_look(packet, character) + + # viewAll = false, ranking = false for char creation + packet + |> Out.encode_byte(0) # viewAll + |> Out.encode_byte(0) # no ranking for new char + else + packet + end + |> Out.to_iodata() end @doc """ Character deletion response. """ def get_delete_char_response(character_id, state) do - Out.new(Opcodes.lp_delete_character_result()) + # Uses LP_DeleteCharResponse (0x0B) opcode + Out.new(Opcodes.lp_delete_char_response()) |> Out.encode_int(character_id) |> Out.encode_byte(state) - |> Out.to_data() + |> Out.to_iodata() end # ================================================================================================== @@ -344,7 +356,7 @@ defmodule Odinsea.Login.Packets do def get_second_pw_error(mode) do Out.new(Opcodes.lp_check_spw_result()) |> Out.encode_byte(mode) - |> Out.to_data() + |> Out.to_iodata() end # ================================================================================================== @@ -361,28 +373,30 @@ defmodule Odinsea.Login.Packets do - `character_id` - Character ID for migration """ def get_server_ip(is_cash_shop, host, port, character_id) do + # Uses LP_ServerIP (0x08) opcode # Parse IP address ip_parts = parse_ip(host) - Out.new(Opcodes.lp_migrate_command()) + Out.new(Opcodes.lp_server_ip()) |> Out.encode_short(if is_cash_shop, do: 1, else: 0) |> encode_ip(ip_parts) |> Out.encode_short(port) |> Out.encode_int(character_id) - |> Out.encode_bytes(<<0, 0>>) - |> Out.to_data() + |> Out.encode_buffer(<<0, 0>>) + |> Out.to_iodata() end # ================================================================================================== # Helper Functions # ================================================================================================== - defp get_second_password_byte(second_password) do - cond do - second_password == nil -> 0 - second_password == "" -> 2 - true -> 1 - end + @doc """ + Returns current date in MSEA format: YYYYMMDD as integer. + Used in authentication success packet. + """ + def get_current_date do + {{year, month, day}, _} = :calendar.local_time() + year * 10000 + month * 100 + day end defp get_last_channel(channel_load) do @@ -431,4 +445,228 @@ defmodule Odinsea.Login.Packets do |> Out.encode_byte(c) |> Out.encode_byte(d) end + + # ============================================================================== + # Character Entry Encoding (MSEA v112.4) + # ============================================================================== + + @doc """ + Adds a character entry to the packet for character list. + Ported from LoginPacket.addCharEntry() for MSEA. + """ + def add_char_entry(packet, character) do + packet = add_char_stats(packet, character) + packet = add_char_look(packet, character) + + # viewAll = false, so encode byte 0 + packet = Out.encode_byte(packet, 0) + + # Ranking (true if not GM and level >= 30) + ranking = not Map.get(character, :is_gm, false) and Map.get(character, :level, 1) >= 30 + packet = Out.encode_byte(packet, if(ranking, do: 1, else: 0)) + + if ranking do + packet + |> Out.encode_int(Map.get(character, :rank, 0)) + |> Out.encode_int(Map.get(character, :rank_move, 0)) + |> Out.encode_int(Map.get(character, :job_rank, 0)) + |> Out.encode_int(Map.get(character, :job_rank_move, 0)) + else + packet + end + end + + @doc """ + Encodes character stats for MSEA v112.4. + Ported from PacketHelper.addCharStats() - MSEA path (GMS = false). + + MSEA Differences from GMS: + - NO 24 bytes padding after hair + - encode_long(0) after Gach EXP + - NO encode_int(0) after spawnpoint + """ + def add_char_stats(packet, character) do + packet + |> Out.encode_int(Map.get(character, :id, 0)) + |> Out.encode_string(Map.get(character, :name, ""), 13) + |> Out.encode_byte(Map.get(character, :gender, 0)) + |> Out.encode_byte(Map.get(character, :skin_color, 0)) + |> Out.encode_int(Map.get(character, :face, 0)) + |> Out.encode_int(Map.get(character, :hair, 0)) + # MSEA: NO 24 bytes padding (GMS has it) + |> Out.encode_byte(Map.get(character, :level, 1)) + |> Out.encode_short(Map.get(character, :job, 0)) + |> encode_char_stats_data(character) + |> Out.encode_short(Map.get(character, :remaining_ap, 0)) + |> encode_remaining_sp(character) + |> Out.encode_int(Map.get(character, :exp, 0)) + |> Out.encode_int(Map.get(character, :fame, 0)) + |> Out.encode_int(Map.get(character, :gach_exp, 0)) + # MSEA ONLY: encode_long(0) after Gach EXP + |> Out.encode_long(0) + |> Out.encode_int(Map.get(character, :map_id, 0)) + |> Out.encode_byte(Map.get(character, :spawnpoint, 0)) + # MSEA: NO encode_int(0) after spawnpoint (GMS has it) + |> Out.encode_short(Map.get(character, :subcategory, 0)) + |> Out.encode_byte(Map.get(character, :fatigue, 0)) + |> Out.encode_int(get_current_date()) + |> encode_traits(character) + |> Out.encode_buffer(<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) # 12 bytes padding + |> Out.encode_int(Map.get(character, :pvp_exp, 0)) + |> Out.encode_byte(Map.get(character, :pvp_rank, 0)) + |> Out.encode_int(Map.get(character, :battle_points, 0)) + |> Out.encode_byte(5) + # MSEA: NO final encode_int(0) that GMS has + end + + # Encodes the main character stats (str, dex, int, luk, hp, mp, max hp, max mp) + defp encode_char_stats_data(packet, character) do + stats = Map.get(character, :stats, %{}) + + packet + |> Out.encode_short(Map.get(stats, :str, 12)) + |> Out.encode_short(Map.get(stats, :dex, 5)) + |> Out.encode_short(Map.get(stats, :int, 4)) + |> Out.encode_short(Map.get(stats, :luk, 4)) + |> Out.encode_short(Map.get(stats, :hp, 50)) + |> Out.encode_short(Map.get(stats, :max_hp, 50)) + |> Out.encode_short(Map.get(stats, :mp, 5)) + |> Out.encode_short(Map.get(stats, :max_mp, 5)) + end + + # Encodes remaining SP based on job type + defp encode_remaining_sp(packet, character) do + job = Map.get(character, :job, 0) + sp_data = Map.get(character, :remaining_sp, %{type: :short, value: 0}) + + # Check for extended SP classes (Evan, Resistance, Mercedes) + if is_extended_sp_job?(job) do + sp_list = Map.get(character, :remaining_sps, []) + packet = Out.encode_byte(packet, length(sp_list)) + + Enum.reduce(sp_list, packet, fn {sp_index, sp_value}, p -> + p + |> Out.encode_byte(sp_index) + |> Out.encode_byte(sp_value) + end) + else + Out.encode_short(packet, Map.get(sp_data, :value, 0)) + end + end + + # Jobs that use extended SP format + defp is_extended_sp_job?(job) do + # Evan: 2200-2218 + # Resistance: 3000-3512 + # Mercedes: 2300-2312 + (job >= 2200 and job <= 2218) or + (job >= 3000 and job <= 3512) or + (job >= 2300 and job <= 2312) + end + + # Encodes trait data (charisma, insight, will, craft, sense, charm) + defp encode_traits(packet, character) do + traits = Map.get(character, :traits, [0, 0, 0, 0, 0, 0]) + + Enum.reduce(traits, packet, fn trait_exp, p -> + Out.encode_int(p, trait_exp) + end) + end + + @doc """ + Encodes character appearance (look) for MSEA v112.4. + Ported from PacketHelper.addCharLook(). + + MSEA uses different equipment slot positions than GMS: + - Mount: MSEA = -23/-24, GMS = -18/-19 + - Pendant: MSEA = -55, GMS = -59 + """ + def add_char_look(packet, character) do + mega = true # For character list, mega = true + + packet = + packet + |> Out.encode_byte(Map.get(character, :gender, 0)) + |> Out.encode_byte(Map.get(character, :skin_color, 0)) + |> Out.encode_int(Map.get(character, :face, 0)) + |> Out.encode_int(Map.get(character, :job, 0)) + |> Out.encode_byte(if mega, do: 0, else: 1) + |> Out.encode_int(Map.get(character, :hair, 0)) + + equipment = Map.get(character, :equipment, %{}) + + # Process equipment slots + {visible_equip, masked_equip} = process_char_look_equipment(equipment) + + # Encode visible equipment + packet = + Enum.reduce(visible_equip, packet, fn {slot, item_id}, p -> + p + |> Out.encode_byte(slot) + |> Out.encode_int(item_id) + end) + + # End of visible items marker + packet = Out.encode_byte(packet, 0xFF) + + # Encode masked equipment (overrides visible) + packet = + Enum.reduce(masked_equip, packet, fn {slot, item_id}, p -> + p + |> Out.encode_byte(slot) + |> Out.encode_int(item_id) + end) + + # End of masked items marker + packet = Out.encode_byte(packet, 0xFF) + + # cash weapon (slot -111) + cash_weapon = Map.get(equipment, -111, 0) + + packet + |> Out.encode_int(cash_weapon) + |> Out.encode_int(0) # Unknown/ears + |> Out.encode_long(0) # Padding + end + + # Processes equipment for char look encoding + # Returns {visible_equip_map, masked_equip_map} + defp process_char_look_equipment(equipment) do + equipment + |> Enum.reduce({%{}, %{}}, fn {pos, item_id}, {visible, masked} = acc -> + # Skip hidden equipment (slot < -127) + if pos < -127 do + acc + else + slot = abs(pos) + + cond do + # Normal visible equipment (slots 1-99) + slot < 100 -> + if Map.has_key?(visible, slot) do + # Move existing to masked, put new in visible + {Map.put(visible, slot, item_id), Map.put(masked, slot, visible[slot])} + else + {Map.put(visible, slot, item_id), masked} + end + + # Cash equipment (slots 100+, except 111) + slot > 100 and slot != 111 -> + actual_slot = slot - 100 + + if Map.has_key?(visible, actual_slot) do + # Replace visible with cash, move old visible to masked + {Map.put(visible, actual_slot, item_id), + Map.put(masked, actual_slot, visible[actual_slot])} + else + {Map.put(visible, actual_slot, item_id), masked} + end + + # Other slots (111 = cash weapon, etc.) - skip for now + true -> + acc + end + end + end) + end end diff --git a/lib/odinsea/net/cipher/aes_cipher.ex b/lib/odinsea/net/cipher/aes_cipher.ex index 4997049..97f03d5 100644 --- a/lib/odinsea/net/cipher/aes_cipher.ex +++ b/lib/odinsea/net/cipher/aes_cipher.ex @@ -6,9 +6,12 @@ defmodule Odinsea.Net.Cipher.AESCipher do Ported from: src/handling/netty/cipher/AESCipher.java """ + import Bitwise + @block_size 1460 - # MapleStory AES key (32 bytes, expanded from the Java version) + # MapleStory AES key (32 bytes = AES-256) + # Must match the Java AES_KEY exactly @aes_key << 0x13, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, @@ -21,7 +24,14 @@ defmodule Odinsea.Net.Cipher.AESCipher do >> @doc """ - Encrypts or decrypts packet data in place using AES-ECB with IV. + Encrypts or decrypts packet data using AES-ECB with IV. + + The algorithm (per block of 1456/1460 bytes): + 1. Expand the 4-byte IV to 16 bytes by repeating it 4 times + 2. Use AES-ECB to encrypt the expanded IV, producing a 16-byte keystream + 3. XOR the next 16 bytes of data with the keystream + 4. The encrypted IV becomes the new IV for the next 16-byte chunk + 5. Repeat until the block is processed ## Parameters - data: Binary data to encrypt/decrypt @@ -32,74 +42,52 @@ defmodule Odinsea.Net.Cipher.AESCipher do """ @spec crypt(binary(), binary()) :: binary() def crypt(data, <<_::binary-size(4)>> = iv) when is_binary(data) do - crypt_recursive(data, iv, 0, byte_size(data), @block_size - 4) + data_list = :binary.bin_to_list(data) + result = crypt_blocks(data_list, 0, @block_size - 4, iv) + :binary.list_to_bin(result) end - # Recursive encryption/decryption function - @spec crypt_recursive(binary(), binary(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: binary() - defp crypt_recursive(data, _iv, start, remaining, _length) when remaining <= 0 do - # Return the portion of data we've processed - binary_part(data, 0, start) - end + # Process data in blocks (first block: 1456 bytes, subsequent: 1460 bytes) + defp crypt_blocks([], _start, _length, _iv), do: [] - defp crypt_recursive(data, iv, start, remaining, length) do - # Multiply the IV by 4 - seq_iv = multiply_bytes(iv, byte_size(iv), 4) - - # Adjust length if remaining is smaller + defp crypt_blocks(data, start, length, iv) do + remaining = Kernel.length(data) actual_length = min(remaining, length) - # Extract the portion of data to process - data_bytes = :binary.bin_to_list(data) + # Expand 4-byte IV to 16 bytes (repeat 4 times) + seq_iv = :binary.copy(iv, 4) + seq_iv_list = :binary.bin_to_list(seq_iv) - # Process the data chunk - {new_data_bytes, _final_seq_iv} = - process_chunk(data_bytes, seq_iv, start, start + actual_length, 0) + # Process this block's bytes, re-encrypting keystream every 16 bytes + {block, rest} = Enum.split(data, actual_length) + {processed, _final_iv} = process_block_bytes(block, seq_iv_list, 0) - # Convert back to binary - new_data = :binary.list_to_bin(new_data_bytes) - - # Continue with next chunk - new_start = start + actual_length - new_remaining = remaining - actual_length - new_length = @block_size - - crypt_recursive(new_data, iv, new_start, new_remaining, new_length) + # Continue with next block using fresh IV expansion + processed ++ crypt_blocks(rest, start + actual_length, @block_size, iv) end - # Process a single chunk of data - @spec process_chunk(list(byte()), binary(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: - {list(byte()), binary()} - defp process_chunk(data_bytes, seq_iv, x, end_x, _offset) when x >= end_x do - {data_bytes, seq_iv} - end + # Process bytes within a single block, re-encrypting keystream every 16 bytes + defp process_block_bytes([], iv_list, _offset), do: {[], iv_list} - defp process_chunk(data_bytes, seq_iv, x, end_x, offset) do - # Check if we need to re-encrypt the IV - {new_seq_iv, new_offset} = - if rem(offset, byte_size(seq_iv)) == 0 do - # Encrypt the IV using AES - encrypted_iv = aes_encrypt_block(seq_iv) - {encrypted_iv, 0} + defp process_block_bytes(data, iv_list, offset) do + # Re-encrypt keystream at every 16-byte boundary + iv_list = + if rem(offset, 16) == 0 do + new_iv = aes_encrypt_block(:binary.list_to_bin(iv_list)) + :binary.bin_to_list(new_iv) else - {seq_iv, offset} + iv_list end - # XOR the data byte with the IV byte - seq_iv_bytes = :binary.bin_to_list(new_seq_iv) - iv_index = rem(new_offset, length(seq_iv_bytes)) - iv_byte = Enum.at(seq_iv_bytes, iv_index) - data_byte = Enum.at(data_bytes, x) - xor_byte = Bitwise.bxor(data_byte, iv_byte) + [byte | rest] = data + iv_byte = Enum.at(iv_list, rem(offset, 16)) + xored = bxor(byte, iv_byte) - # Update the data - updated_data = List.replace_at(data_bytes, x, xor_byte) - - # Continue processing - process_chunk(updated_data, new_seq_iv, x + 1, end_x, new_offset + 1) + {rest_result, final_iv} = process_block_bytes(rest, iv_list, offset + 1) + {[xored | rest_result], final_iv} end - # Encrypt a single 16-byte block using AES-ECB + # Encrypt a single 16-byte block using AES-256-ECB @spec aes_encrypt_block(binary()) :: binary() defp aes_encrypt_block(block) do # Pad or truncate to 16 bytes for AES @@ -110,15 +98,9 @@ defmodule Odinsea.Net.Cipher.AESCipher do size when size > 16 -> binary_part(block, 0, 16) end - # Perform AES encryption in ECB mode - :crypto.crypto_one_time(:aes_128_ecb, @aes_key, padded_block, true) - end - - # Multiply bytes - repeats the first `count` bytes of `input` `mul` times - @spec multiply_bytes(binary(), non_neg_integer(), non_neg_integer()) :: binary() - defp multiply_bytes(input, count, mul) do - # Take first `count` bytes and repeat them `mul` times - chunk = binary_part(input, 0, min(count, byte_size(input))) - :binary.copy(chunk, mul) + # Perform AES encryption in ECB mode (AES-256) + result = :crypto.crypto_one_time(:aes_256_ecb, @aes_key, padded_block, true) + # Take only first 16 bytes (OpenSSL may add PKCS padding) + binary_part(result, 0, 16) end end diff --git a/lib/odinsea/net/cipher/client_crypto.ex b/lib/odinsea/net/cipher/client_crypto.ex index f3c0974..1049f72 100644 --- a/lib/odinsea/net/cipher/client_crypto.ex +++ b/lib/odinsea/net/cipher/client_crypto.ex @@ -6,9 +6,9 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do Ported from: src/handling/netty/ClientCrypto.java """ - use Bitwise + import Bitwise - alias Odinsea.Net.Cipher.{AESCipher, IGCipher} + alias Odinsea.Net.Cipher.{AESCipher, IGCipher, ShandaCipher} defstruct [ :version, @@ -32,7 +32,7 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do Creates a new ClientCrypto instance with random IVs. ## Parameters - - version: MapleStory version number (e.g., 342) + - version: MapleStory version number (e.g., 112) - use_custom_crypt: If false, uses AES encryption. If true, uses basic XOR with 0x69 ## Returns @@ -50,38 +50,97 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do } end + @doc """ + Creates a new ClientCrypto instance from existing IVs (for server handshake). + The server generates its own IVs and sends them to the client. + + ## Parameters + - version: MapleStory version number + - send_iv: 4-byte binary for send IV (server encrypts with this) + - recv_iv: 4-byte binary for recv IV (server decrypts with this) + + ## Returns + - New ClientCrypto struct + """ + @spec new_from_ivs(integer(), binary(), binary()) :: t() + def new_from_ivs(version, send_iv, recv_iv) do + %__MODULE__{ + version: version, + use_custom_crypt: false, + send_iv: send_iv, + send_iv_old: <<0, 0, 0, 0>>, + recv_iv: recv_iv, + recv_iv_old: <<0, 0, 0, 0>> + } + end + + @doc """ + Creates a new ClientCrypto instance from client's IVs (after handshake). + The IVs must be SWAPPED because: + - Server's send IV = Client's recv IV + - Server's recv IV = Client's send IV + + ## Parameters + - version: MapleStory version number + - client_send_iv: Client's send IV (from client's hello packet) + - client_recv_iv: Client's recv IV (from client's hello packet) + + ## Returns + - New ClientCrypto struct with properly swapped IVs + """ + @spec new_from_client_ivs(integer(), binary(), binary()) :: t() + def new_from_client_ivs(version, client_send_iv, client_recv_iv) do + # Swap the IVs: server's send = client's recv, server's recv = client's send + %__MODULE__{ + version: version, + use_custom_crypt: false, + send_iv: client_recv_iv, + send_iv_old: <<0, 0, 0, 0>>, + recv_iv: client_send_iv, + recv_iv_old: <<0, 0, 0, 0>> + } + end + @doc """ Encrypts outgoing packet data and updates the send IV. + Applies Shanda encryption first, then AES encryption. ## Parameters - crypto: ClientCrypto state - data: Binary packet data to encrypt ## Returns - - {updated_crypto, encrypted_data} + - {updated_crypto, encrypted_data, header} """ - @spec encrypt(t(), binary()) :: {t(), binary()} + @spec encrypt(t(), binary()) :: {t(), binary(), binary()} def encrypt(%__MODULE__{} = crypto, data) when is_binary(data) do # Backup current send IV updated_crypto = %{crypto | send_iv_old: crypto.send_iv} - # Encrypt the data + # Generate header BEFORE encryption (uses current IV) + header = encode_header_len(updated_crypto, byte_size(data)) + + # Apply Shanda encryption first + shanda_encrypted = ShandaCipher.encrypt(data) + + # Apply AES encryption encrypted_data = if crypto.use_custom_crypt do - basic_cipher(data) + basic_cipher(shanda_encrypted) else - AESCipher.crypt(data, crypto.send_iv) + AESCipher.crypt(shanda_encrypted, crypto.send_iv) end - # Update the send IV using InnoGames hash + # Update the send IV using InnoGames hash (AFTER encryption) new_send_iv = IGCipher.inno_hash(crypto.send_iv) final_crypto = %{updated_crypto | send_iv: new_send_iv} - {final_crypto, encrypted_data} + {final_crypto, encrypted_data, header} end @doc """ Decrypts incoming packet data and updates the recv IV. + Applies AES decryption first, then Shanda decryption. ## Parameters - crypto: ClientCrypto state @@ -95,15 +154,18 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do # Backup current recv IV updated_crypto = %{crypto | recv_iv_old: crypto.recv_iv} - # Decrypt the data - decrypted_data = + # Apply AES decryption + aes_decrypted = if crypto.use_custom_crypt do basic_cipher(data) else AESCipher.crypt(data, crypto.recv_iv) end - # Update the recv IV using InnoGames hash + # Apply Shanda decryption + decrypted_data = ShandaCipher.decrypt(aes_decrypted) + + # Update the recv IV using InnoGames hash (AFTER decryption) new_recv_iv = IGCipher.inno_hash(crypto.recv_iv) final_crypto = %{updated_crypto | recv_iv: new_recv_iv} @@ -123,13 +185,14 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do """ @spec encode_header_len(t(), non_neg_integer()) :: binary() def encode_header_len(%__MODULE__{} = crypto, data_len) do - <> = crypto.send_iv + <<_s0, _s1, s2, s3>> = crypto.send_iv # Calculate the encoded version new_version = -(crypto.version + 1) &&& 0xFFFF enc_version = (((new_version >>> 8) &&& 0xFF) ||| ((new_version <<< 8) &&& 0xFF00)) &&& 0xFFFF # Calculate raw sequence from send IV + # Note: Using s3 and s2 (high bytes) as in Java version raw_seq = bxor((((s3 &&& 0xFF) ||| ((s2 <<< 8) &&& 0xFF00)) &&& 0xFFFF), enc_version) # Calculate raw length @@ -155,8 +218,8 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do ## Returns - Decoded packet length """ - @spec decode_header_len(integer(), integer()) :: integer() - def decode_header_len(raw_seq, raw_len) do + @spec decode_header_len(t(), integer(), integer()) :: integer() + def decode_header_len(%__MODULE__{}, raw_seq, raw_len) do bxor(raw_seq, raw_len) &&& 0xFFFF end @@ -175,6 +238,7 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do <<_r0, _r1, r2, r3>> = crypto.recv_iv enc_version = crypto.version &&& 0xFFFF + # Note: Using r2 and r3 as in Java version seq_base = ((r2 &&& 0xFF) ||| ((r3 <<< 8) &&& 0xFF00)) &&& 0xFFFF bxor((raw_seq &&& 0xFFFF), seq_base) == enc_version @@ -197,7 +261,7 @@ defmodule Odinsea.Net.Cipher.ClientCrypto do defp basic_cipher(data) do data |> :binary.bin_to_list() - |> Enum.map(fn byte -> Bitwise.bxor(byte, 0x69) end) + |> Enum.map(fn byte -> bxor(byte, 0x69) end) |> :binary.list_to_bin() end end diff --git a/lib/odinsea/net/cipher/ig_cipher.ex b/lib/odinsea/net/cipher/ig_cipher.ex index 4c64c0b..4fbdf1f 100644 --- a/lib/odinsea/net/cipher/ig_cipher.ex +++ b/lib/odinsea/net/cipher/ig_cipher.ex @@ -6,7 +6,28 @@ defmodule Odinsea.Net.Cipher.IGCipher do Ported from: src/handling/netty/cipher/IGCipher.java """ - use Bitwise + import Bitwise + + # Shuffle table - 256 bytes used for IV transformation + # Must be defined before functions that use it + @shuffle_table { + 0xEC, 0x3F, 0x77, 0xA4, 0x45, 0xD0, 0x71, 0xBF, 0xB7, 0x98, 0x20, 0xFC, 0x4B, 0xE9, 0xB3, 0xE1, + 0x5C, 0x22, 0xF7, 0x0C, 0x44, 0x1B, 0x81, 0xBD, 0x63, 0x8D, 0xD4, 0xC3, 0xF2, 0x10, 0x19, 0xE0, + 0xFB, 0xA1, 0x6E, 0x66, 0xEA, 0xAE, 0xD6, 0xCE, 0x06, 0x18, 0x4E, 0xEB, 0x78, 0x95, 0xDB, 0xBA, + 0xB6, 0x42, 0x7A, 0x2A, 0x83, 0x0B, 0x54, 0x67, 0x6D, 0xE8, 0x65, 0xE7, 0x2F, 0x07, 0xF3, 0xAA, + 0x27, 0x7B, 0x85, 0xB0, 0x26, 0xFD, 0x8B, 0xA9, 0xFA, 0xBE, 0xA8, 0xD7, 0xCB, 0xCC, 0x92, 0xDA, + 0xF9, 0x93, 0x60, 0x2D, 0xDD, 0xD2, 0xA2, 0x9B, 0x39, 0x5F, 0x82, 0x21, 0x4C, 0x69, 0xF8, 0x31, + 0x87, 0xEE, 0x8E, 0xAD, 0x8C, 0x6A, 0xBC, 0xB5, 0x6B, 0x59, 0x13, 0xF1, 0x04, 0x00, 0xF6, 0x5A, + 0x35, 0x79, 0x48, 0x8F, 0x15, 0xCD, 0x97, 0x57, 0x12, 0x3E, 0x37, 0xFF, 0x9D, 0x4F, 0x51, 0xF5, + 0xA3, 0x70, 0xBB, 0x14, 0x75, 0xC2, 0xB8, 0x72, 0xC0, 0xED, 0x7D, 0x68, 0xC9, 0x2E, 0x0D, 0x62, + 0x46, 0x17, 0x11, 0x4D, 0x6C, 0xC4, 0x7E, 0x53, 0xC1, 0x25, 0xC7, 0x9A, 0x1C, 0x88, 0x58, 0x2C, + 0x89, 0xDC, 0x02, 0x64, 0x40, 0x01, 0x5D, 0x38, 0xA5, 0xE2, 0xAF, 0x55, 0xD5, 0xEF, 0x1A, 0x7C, + 0xA7, 0x5B, 0xA6, 0x6F, 0x86, 0x9F, 0x73, 0xE6, 0x0A, 0xDE, 0x2B, 0x99, 0x4A, 0x47, 0x9C, 0xDF, + 0x09, 0x76, 0x9E, 0x30, 0x0E, 0xE4, 0xB2, 0x94, 0xA0, 0x3B, 0x34, 0x1D, 0x28, 0x0F, 0x36, 0xE3, + 0x23, 0xB4, 0x03, 0xD8, 0x90, 0xC8, 0x3C, 0xFE, 0x5E, 0x32, 0x24, 0x50, 0x1F, 0x3A, 0x43, 0x8A, + 0x96, 0x41, 0x74, 0xAC, 0x52, 0x33, 0xF0, 0xD9, 0x29, 0x80, 0xB1, 0x16, 0xD3, 0xAB, 0x91, 0xB9, + 0x84, 0x7F, 0x61, 0x1E, 0xCF, 0xC5, 0xD1, 0x56, 0x3D, 0xCA, 0xF4, 0x05, 0xC6, 0xE5, 0x08, 0x49 + } @doc """ Applies the InnoGames hash transformation to a 4-byte IV. @@ -39,11 +60,12 @@ defmodule Odinsea.Net.Cipher.IGCipher do input = value &&& 0xFF table = shuffle_byte(input) - # Apply the transformation operations - new_k0 = k0 + shuffle_byte(k1) - input - new_k1 = k1 - bxor(k2, table) - new_k2 = bxor(k2, shuffle_byte(k3) + input) - new_k3 = k3 - (k0 - table) + # Apply the transformation operations SEQUENTIALLY + # Java modifies key[0] first, then uses modified key[0] for key[3] + new_k0 = (k0 + (shuffle_byte(k1) - input)) &&& 0xFF + new_k1 = (k1 - bxor(k2, table)) &&& 0xFF + new_k2 = bxor(k2, (shuffle_byte(k3) + input) &&& 0xFF) + new_k3 = (k3 - (new_k0 - table)) &&& 0xFF # Combine into 32-bit value (little-endian) val = @@ -69,24 +91,4 @@ defmodule Odinsea.Net.Cipher.IGCipher do defp shuffle_byte(index) do elem(@shuffle_table, index &&& 0xFF) end - - # Shuffle table - 256 bytes used for IV transformation - @shuffle_table { - 0xEC, 0x3F, 0x77, 0xA4, 0x45, 0xD0, 0x71, 0xBF, 0xB7, 0x98, 0x20, 0xFC, 0x4B, 0xE9, 0xB3, 0xE1, - 0x5C, 0x22, 0xF7, 0x0C, 0x44, 0x1B, 0x81, 0xBD, 0x63, 0x8D, 0xD4, 0xC3, 0xF2, 0x10, 0x19, 0xE0, - 0xFB, 0xA1, 0x6E, 0x66, 0xEA, 0xAE, 0xD6, 0xCE, 0x06, 0x18, 0x4E, 0xEB, 0x78, 0x95, 0xDB, 0xBA, - 0xB6, 0x42, 0x7A, 0x2A, 0x83, 0x0B, 0x54, 0x67, 0x6D, 0xE8, 0x65, 0xE7, 0x2F, 0x07, 0xF3, 0xAA, - 0x27, 0x7B, 0x85, 0xB0, 0x26, 0xFD, 0x8B, 0xA9, 0xFA, 0xBE, 0xA8, 0xD7, 0xCB, 0xCC, 0x92, 0xDA, - 0xF9, 0x93, 0x60, 0x2D, 0xDD, 0xD2, 0xA2, 0x9B, 0x39, 0x5F, 0x82, 0x21, 0x4C, 0x69, 0xF8, 0x31, - 0x87, 0xEE, 0x8E, 0xAD, 0x8C, 0x6A, 0xBC, 0xB5, 0x6B, 0x59, 0x13, 0xF1, 0x04, 0x00, 0xF6, 0x5A, - 0x35, 0x79, 0x48, 0x8F, 0x15, 0xCD, 0x97, 0x57, 0x12, 0x3E, 0x37, 0xFF, 0x9D, 0x4F, 0x51, 0xF5, - 0xA3, 0x70, 0xBB, 0x14, 0x75, 0xC2, 0xB8, 0x72, 0xC0, 0xED, 0x7D, 0x68, 0xC9, 0x2E, 0x0D, 0x62, - 0x46, 0x17, 0x11, 0x4D, 0x6C, 0xC4, 0x7E, 0x53, 0xC1, 0x25, 0xC7, 0x9A, 0x1C, 0x88, 0x58, 0x2C, - 0x89, 0xDC, 0x02, 0x64, 0x40, 0x01, 0x5D, 0x38, 0xA5, 0xE2, 0xAF, 0x55, 0xD5, 0xEF, 0x1A, 0x7C, - 0xA7, 0x5B, 0xA6, 0x6F, 0x86, 0x9F, 0x73, 0xE6, 0x0A, 0xDE, 0x2B, 0x99, 0x4A, 0x47, 0x9C, 0xDF, - 0x09, 0x76, 0x9E, 0x30, 0x0E, 0xE4, 0xB2, 0x94, 0xA0, 0x3B, 0x34, 0x1D, 0x28, 0x0F, 0x36, 0xE3, - 0x23, 0xB4, 0x03, 0xD8, 0x90, 0xC8, 0x3C, 0xFE, 0x5E, 0x32, 0x24, 0x50, 0x1F, 0x3A, 0x43, 0x8A, - 0x96, 0x41, 0x74, 0xAC, 0x52, 0x33, 0xF0, 0xD9, 0x29, 0x80, 0xB1, 0x16, 0xD3, 0xAB, 0x91, 0xB9, - 0x84, 0x7F, 0x61, 0x1E, 0xCF, 0xC5, 0xD1, 0x56, 0x3D, 0xCA, 0xF4, 0x05, 0xC6, 0xE5, 0x08, 0x49 - } end diff --git a/lib/odinsea/net/cipher/shanda_cipher.ex b/lib/odinsea/net/cipher/shanda_cipher.ex new file mode 100644 index 0000000..3636cc3 --- /dev/null +++ b/lib/odinsea/net/cipher/shanda_cipher.ex @@ -0,0 +1,150 @@ +defmodule Odinsea.Net.Cipher.ShandaCipher do + @moduledoc """ + Shanda cipher implementation for MapleStory packet encryption. + Direct port from Java ShandaCipher.java + """ + + import Bitwise + + alias Odinsea.Util.BitTools + + @doc """ + Encrypts data using the Shanda cipher. + """ + @spec encrypt(binary()) :: binary() + def encrypt(data) when is_binary(data) do + bytes = :binary.bin_to_list(data) + len = length(bytes) + + result = + Enum.reduce(0..5, bytes, fn j, acc -> + do_encrypt_pass(acc, len, j) + end) + + :binary.list_to_bin(result) + end + + defp do_encrypt_pass(data, len, j) do + data_length = len &&& 0xFF + + if rem(j, 2) == 0 do + # Forward pass: iterate 0..length-1 + {result, _remember, _dl} = + Enum.reduce(data, {[], 0, data_length}, fn cur, {out, remember, dl} -> + new_cur = + cur + |> BitTools.roll_left(3) + |> Kernel.+(dl) + |> band(0xFF) + |> bxor(remember) + + new_remember = new_cur + + final_cur = + new_cur + |> BitTools.roll_right(dl &&& 0xFF) + |> bxor(0xFF) + |> Kernel.+(0x48) + |> band(0xFF) + + {[final_cur | out], new_remember, (dl - 1) &&& 0xFF} + end) + + Enum.reverse(result) + else + # Backward pass: iterate length-1..0 + # Process reversed list, prepend results -> naturally restores forward order + {result, _remember, _dl} = + Enum.reduce(Enum.reverse(data), {[], 0, data_length}, fn cur, {out, remember, dl} -> + new_cur = + cur + |> BitTools.roll_left(4) + |> Kernel.+(dl) + |> band(0xFF) + |> bxor(remember) + + new_remember = new_cur + + final_cur = + new_cur + |> bxor(0x13) + |> BitTools.roll_right(3) + + {[final_cur | out], new_remember, (dl - 1) &&& 0xFF} + end) + + # Do NOT reverse - prepending from reversed input naturally gives forward order + result + end + end + + @doc """ + Decrypts data using the Shanda cipher. + """ + @spec decrypt(binary()) :: binary() + def decrypt(data) when is_binary(data) do + bytes = :binary.bin_to_list(data) + len = length(bytes) + + # Java: for (int j = 1; j <= 6; j++) + result = + Enum.reduce(1..6, bytes, fn j, acc -> + do_decrypt_pass(acc, len, j) + end) + + :binary.list_to_bin(result) + end + + defp do_decrypt_pass(data, len, j) do + data_length = len &&& 0xFF + + if rem(j, 2) == 0 do + # Forward pass (even j in decrypt = matches encrypt's forward) + {result, _remember, _dl} = + Enum.reduce(data, {[], 0, data_length}, fn cur, {out, remember, dl} -> + new_cur = + cur + |> Kernel.-(0x48) + |> band(0xFF) + |> bxor(0xFF) + |> BitTools.roll_left(dl &&& 0xFF) + + next_remember = new_cur + + final_cur = + new_cur + |> bxor(remember) + |> Kernel.-(dl) + |> band(0xFF) + |> BitTools.roll_right(3) + + {[final_cur | out], next_remember, (dl - 1) &&& 0xFF} + end) + + Enum.reverse(result) + else + # Backward pass (odd j in decrypt = matches encrypt's backward) + {result, _remember, _dl} = + Enum.reduce(Enum.reverse(data), {[], 0, data_length}, fn cur, {out, remember, dl} -> + new_cur = + cur + |> BitTools.roll_left(3) + |> bxor(0x13) + + next_remember = new_cur + + final_cur = + new_cur + |> bxor(remember) + |> Kernel.-(dl) + |> band(0xFF) + |> BitTools.roll_right(4) + + {[final_cur | out], next_remember, (dl - 1) &&& 0xFF} + end) + + # Do NOT reverse - prepending from reversed input naturally gives forward order + result + end + end +end diff --git a/lib/odinsea/net/opcodes.ex b/lib/odinsea/net/opcodes.ex index 8c42a1f..c048bbc 100644 --- a/lib/odinsea/net/opcodes.ex +++ b/lib/odinsea/net/opcodes.ex @@ -19,24 +19,31 @@ defmodule Odinsea.Net.Opcodes do # Login/Account def cp_client_hello(), do: 0x01 + @spec cp_login_password() :: 2 def cp_login_password(), do: 0x02 + # Note: 0x03 is CP_ViewServerList in MSEA v112.4 (not used in OdinSea but reserved) + @spec cp_view_server_list() :: 3 + def cp_view_server_list(), do: 0x03 + @spec cp_serverlist_request() :: 4 def cp_serverlist_request(), do: 0x04 - def cp_charlist_request(), do: 0x05 - def cp_serverstatus_request(), do: 0x06 + # CP_SelectWorld(5) - Java: ClientPacket.java + @spec cp_select_world() :: 5 + def cp_select_world(), do: 0x05 def cp_check_char_name(), do: 0x0E def cp_create_char(), do: 0x12 def cp_create_ultimate(), do: 0x14 def cp_delete_char(), do: 0x15 def cp_exception_log(), do: 0x17 def cp_security_packet(), do: 0x18 - def cp_hardware_info(), do: 0x70 - def cp_window_focus(), do: 0x71 + def cp_hardware_info(), do: 0x5001 # Java: CP_HardwareInfo(0x5001) - was 0x70 (collided with cp_cancel_buff) + def cp_window_focus(), do: 0x5004 # Java: CP_WindowFocus(0x5004) - was 0x71 (collided with cp_skill_effect) def cp_char_select(), do: 0x19 def cp_auth_second_password(), do: 0x1A def cp_rsa_key(), do: 0x20 def cp_client_dump_log(), do: 0x1D def cp_create_security_handle(), do: 0x1E - def cp_select_world(), do: 0x03 + # CP_CheckUserLimit(6) - Java: ClientPacket.java + @spec cp_check_user_limit() :: 6 def cp_check_user_limit(), do: 0x06 # Migration/Channel @@ -75,7 +82,7 @@ defmodule Odinsea.Net.Opcodes do # NPC Interaction def cp_npc_talk(), do: 0x40 - def cp_npc_move(), do: 0x41 # NPC animation/movement (added for compatibility) + def cp_npc_move(), do: 0x106 # Java: CP_NpcMove(262) - alias for cp_npc_action. Was 0x41 (wrong) def cp_npc_talk_more(), do: 0x42 def cp_npc_shop(), do: 0x43 def cp_storage(), do: 0x44 @@ -255,6 +262,12 @@ defmodule Odinsea.Net.Opcodes do # Cash Shop def cp_cs_update(), do: 0x135 + + # Alias for cp_cs_update (used in some places) + def cp_cash_shop_update(), do: cp_cs_update() + + # Public NPC (recv - currently unimplemented in Java, using same value as send) + def cp_public_npc(), do: 0xB7 def cp_buy_cs_item(), do: 0x136 def cp_coupon_code(), do: 0x137 @@ -263,6 +276,9 @@ defmodule Odinsea.Net.Opcodes do def cp_touching_mts(), do: 0x159 def cp_mts_tab(), do: 0x15A + # MTS operation opcode (client -> server) + def cp_mts_operation(), do: 0xB4 + # Custom (server-specific) def cp_inject_packet(), do: 0x5002 def cp_set_code_page(), do: 0x5003 @@ -279,16 +295,16 @@ defmodule Odinsea.Net.Opcodes do # General def lp_alive_req(), do: 0x0D - def lp_enable_action(), do: 0x0C - def lp_set_field(), do: 0x14 - def lp_set_cash_shop_opened(), do: 0x15 - def lp_migrate_command(), do: 0x16 + def lp_enable_action(), do: 0x1B # Java: enableActions() uses UPDATE_STATS(27) - was 0x0C (collided with lp_change_channel) + def lp_set_field(), do: 0x90 # Java: LP_SetField(144) - alias for lp_warp_to_map. Was 0x14 (wrong) + def lp_set_cash_shop_opened(), do: 0x92 # Java: LP_SetCashShop(146) - was 0x15 (collided with lp_latest_connected_world) + def lp_migrate_command(), do: 0x0C # Java: CHANGE_CHANNEL(12) - was 0x16 (collided with lp_recommend_world_message) # Login - def lp_login_status(), do: 0x01 - def lp_serverstatus(), do: 0x03 - def lp_serverlist(), do: 0x06 - def lp_charlist(), do: 0x07 + def lp_check_password_result(), do: 0x01 + def lp_server_status(), do: 0x03 + def lp_server_list(), do: 0x06 + def lp_char_list(), do: 0x07 def lp_server_ip(), do: 0x08 def lp_char_name_response(), do: 0x09 def lp_add_new_char_entry(), do: 0x0A @@ -298,10 +314,10 @@ defmodule Odinsea.Net.Opcodes do def lp_channel_selected(), do: 0x10 def lp_relog_response(), do: 0x12 def lp_rsa_key(), do: 0x13 - def lp_enable_recommended(), do: 0x15 - def lp_send_recommended(), do: 0x16 + def lp_latest_connected_world(), do: 0x15 + def lp_recommend_world_message(), do: 0x16 def lp_login_auth(), do: 0x17 - def lp_secondpw_error(), do: 0x18 + def lp_check_spw_result(), do: 0x18 # Inventory/Stats def lp_modify_inventory_item(), do: 0x19 @@ -390,6 +406,13 @@ defmodule Odinsea.Net.Opcodes do # Warps/Shops def lp_warp_to_map(), do: 0x90 def lp_mts_open(), do: 0x91 + + # Alias for lp_mts_open (MTS opened packet) + def lp_set_mts_opened(), do: lp_mts_open() + + # Cash shop initialization (SET_CASH_SHOP from Java) + def lp_set_cash_shop(), do: 0x92 + def lp_cs_open(), do: 0x92 def lp_login_welcome(), do: 0x94 def lp_server_blocked(), do: 0x97 @@ -397,6 +420,9 @@ defmodule Odinsea.Net.Opcodes do # Effects def lp_show_equip_effect(), do: 0x99 + + # Weather effect (BLOW_WEATHER/MAP_EFFECT) + def lp_blow_weather(), do: 0xA1 def lp_multichat(), do: 0x9A def lp_whisper(), do: 0x9B def lp_boss_env(), do: 0x9D @@ -614,6 +640,10 @@ defmodule Odinsea.Net.Opcodes do # Cash Shop def lp_cs_update(), do: 0x1B8 + + # Alias for lp_cs_update (cash shop update) + def lp_cash_shop_update(), do: lp_cs_update() + def lp_cs_operation(), do: 0x1B9 def lp_xmas_surprise(), do: 0x1BD @@ -657,16 +687,26 @@ defmodule Odinsea.Net.Opcodes do # ================================================================================================== @doc """ - Returns a human-readable name for a given opcode value. + Returns a human-readable name for a given client opcode value. Useful for debugging and logging. """ - def name_for(opcode) when is_integer(opcode) do + def name_for_client(opcode) when is_integer(opcode) do case opcode do # Client opcodes (common ones for debugging) 0x01 -> "CP_CLIENT_HELLO" 0x02 -> "CP_LOGIN_PASSWORD" + 0x03 -> "CP_VIEW_SERVER_LIST" + 0x04 -> "CP_SERVERLIST_REQUEST" + 0x05 -> "CP_SELECT_WORLD" + 0x06 -> "CP_CHECK_USER_LIMIT" 0x0D -> "CP_PLAYER_LOGGEDIN" + 0x0E -> "CP_CHECK_CHAR_NAME" + 0x12 -> "CP_CREATE_CHAR" + 0x14 -> "CP_CREATE_ULTIMATE" + 0x15 -> "CP_DELETE_CHAR" 0x19 -> "CP_CHAR_SELECT" + 0x1A -> "CP_AUTH_SECOND_PASSWORD" + 0x20 -> "CP_RSA_KEY" 0x23 -> "CP_CHANGE_MAP" 0x24 -> "CP_CHANGE_CHANNEL" 0x2A -> "CP_MOVE_PLAYER" @@ -675,11 +715,24 @@ defmodule Odinsea.Net.Opcodes do 0xA0 -> "CP_PARTYCHAT" 0xA1 -> "CP_WHISPER" + _ -> "CP_UNKNOWN_0x#{Integer.to_string(opcode, 16) |> String.upcase()}" + end + end + + @doc """ + Returns a human-readable name for a given server opcode value. + Useful for debugging and logging. + """ + def name_for_server(opcode) when is_integer(opcode) do + case opcode do # Server opcodes (common ones for debugging) 0x01 -> "LP_LOGIN_STATUS" 0x06 -> "LP_SERVERLIST" 0x07 -> "LP_CHARLIST" + 0x08 -> "LP_SERVER_IP" 0x0D -> "LP_ALIVE_REQ" + 0x13 -> "LP_RSA_KEY" + 0x17 -> "LP_LOGIN_AUTH" 0xB8 -> "LP_SPAWN_PLAYER" 0xB9 -> "LP_REMOVE_PLAYER_FROM_MAP" 0xBA -> "LP_CHATTEXT" @@ -688,21 +741,30 @@ defmodule Odinsea.Net.Opcodes do 0x9B -> "LP_WHISPER" 0x1A3 -> "LP_NPC_TALK" - _ -> "UNKNOWN_0x#{Integer.to_string(opcode, 16) |> String.upcase()}" + _ -> "LP_UNKNOWN_0x#{Integer.to_string(opcode, 16) |> String.upcase()}" end end + @doc """ + Returns a human-readable name for a given opcode value. + Deprecated: Use name_for_client/1 or name_for_server/1 instead. + """ + def name_for(opcode) when is_integer(opcode) do + name_for_client(opcode) + end + @doc """ Validates if an opcode is a known client packet. """ def valid_client_opcode?(opcode) when is_integer(opcode) do opcode in [ - # Add all valid client opcodes here for validation - 0x01, - 0x02, - 0x04, - 0x05, - 0x06, + # Login opcodes + 0x01, # CP_ClientHello + 0x02, # CP_LoginPassword + 0x03, # CP_ViewServerList + 0x04, # CP_ServerListRequest + 0x05, # CP_SelectWorld + 0x06, # CP_CheckUserLimit 0x0D, 0x0E, 0x12, @@ -895,7 +957,11 @@ defmodule Odinsea.Net.Opcodes do 0x143, 0x144, 0x159, - 0x15A + 0x15A, + 0x5001, # CP_HardwareInfo + 0x5002, # CP_InjectPacket + 0x5003, # CP_SetCodePage + 0x5004 # CP_WindowFocus ] end diff --git a/lib/odinsea/net/packet/out.ex b/lib/odinsea/net/packet/out.ex index 51e9e89..5429072 100644 --- a/lib/odinsea/net/packet/out.ex +++ b/lib/odinsea/net/packet/out.ex @@ -68,6 +68,18 @@ defmodule Odinsea.Net.Packet.Out do %__MODULE__{data: [data | <>]} end + @doc """ + Encodes a fixed-length MapleStory ASCII string. + Format: [2-byte length][ASCII bytes][padding to fixed length] + The third argument specifies the fixed field length. + """ + @spec encode_string(t(), String.t(), non_neg_integer()) :: t() + def encode_string(%__MODULE__{data: data}, value, fixed_length) when is_binary(value) and is_integer(fixed_length) and fixed_length > 0 do + length = byte_size(value) + padding = max(0, fixed_length - length) + %__MODULE__{data: [data | <>]} + end + @doc """ Encodes a boolean (1 byte, 0 = false, 1 = true). """ @@ -88,6 +100,12 @@ defmodule Odinsea.Net.Packet.Out do %__MODULE__{data: [data | buffer]} end + @doc """ + Alias for encode_buffer/2. + """ + @spec encode_bytes(t(), binary()) :: t() + def encode_bytes(packet, data), do: encode_buffer(packet, data) + @doc """ Encodes a fixed-size buffer, padding with zeros if necessary. """ @@ -172,6 +190,12 @@ defmodule Odinsea.Net.Packet.Out do data end + @doc """ + Alias for to_iodata/1. + """ + @spec to_data(t()) :: iodata() + def to_data(packet), do: to_iodata(packet) + @doc """ Converts the packet to a hex string for debugging. """ diff --git a/lib/odinsea/net/packet_logger.ex b/lib/odinsea/net/packet_logger.ex new file mode 100644 index 0000000..ad878a3 --- /dev/null +++ b/lib/odinsea/net/packet_logger.ex @@ -0,0 +1,380 @@ +defmodule Odinsea.Net.PacketLogger do + @moduledoc """ + Comprehensive packet logging system for debugging MapleStory protocol. + + Provides detailed packet logging similar to the Java version with: + - Direction (client/loopback) + - Opcode name and value (decimal/hex) + - Raw hex data + - ASCII text representation + - Context information (session, thread) + """ + + require Logger + + alias Odinsea.Net.{Hex, Opcodes} + + @doc """ + Logs an incoming client packet with full details. + + ## Parameters + - `opcode` - The packet opcode (integer) + - `data` - The packet data (binary, excluding opcode bytes) + - `context` - Map with :ip, :server_type, etc. + """ + def log_client_packet(opcode, data, context \\ %{}) do + if packet_logging_enabled?() do + opcode_name = get_client_opcode_name(opcode) + ip = Map.get(context, :ip, "unknown") + server_type = Map.get(context, :server_type, :unknown) + + # Format opcode header + opcode_header = format_opcode_header("client", opcode_name, opcode) + + # Full packet data includes opcode + full_data = <> <> data + + # Format hex and text + hex_str = Hex.encode(full_data) + text_str = Hex.to_ascii(full_data) + + # Log the packet + Logger.info(""" + #{opcode_header} + [Data] #{hex_str} + [Text] #{text_str} + [Context] IP=#{ip} Server=#{server_type} Size=#{byte_size(full_data)} bytes + """) + + :ok + else + :ok + end + end + + @doc """ + Logs an outgoing server packet with full details. + + ## Parameters + - `opcode` - The packet opcode (integer) + - `data` - The packet data (binary, excluding opcode bytes) + - `context` - Map with :ip, :server_type, etc. + """ + def log_server_packet(opcode, data, context \\ %{}) do + if packet_logging_enabled?() do + opcode_name = get_server_opcode_name(opcode) + ip = Map.get(context, :ip, "unknown") + server_type = Map.get(context, :server_type, :unknown) + + # Format opcode header + opcode_header = format_opcode_header("loopback", opcode_name, opcode) + + # Full packet data includes opcode + full_data = <> <> data + + # Format hex and text + hex_str = Hex.encode(full_data) + text_str = Hex.to_ascii(full_data) + + # Log the packet + Logger.info(""" + #{opcode_header} + [Data] #{hex_str} + [Text] #{text_str} + [Context] IP=#{ip} Server=#{server_type} Size=#{byte_size(full_data)} bytes + """) + + :ok + else + :ok + end + end + + @doc """ + Logs raw packet data (used for handshake/hello packets that don't follow normal format). + """ + def log_raw_packet(direction, label, data, context \\ %{}) do + if packet_logging_enabled?() do + ip = Map.get(context, :ip, "unknown") + + data = IO.iodata_to_binary(data) + hex_str = Hex.encode(data) + text_str = Hex.to_ascii(data) + + Logger.info(""" + [#{direction}] [#{label}] + [Data] #{hex_str} + [Text] #{text_str} + [Context] IP=#{ip} Size=#{byte_size(data)} bytes + """) + + :ok + else + :ok + end + end + + # ================================================================================================== + # Private Helper Functions + # ================================================================================================== + + defp packet_logging_enabled? do + features = Application.get_env(:odinsea, :features, []) + Keyword.get(features, :log_packet, false) + end + + defp format_opcode_header(direction, opcode_name, opcode) do + opcode_hex = Integer.to_string(opcode, 16) |> String.upcase() |> String.pad_leading(2, "0") + "[#{direction}] [#{opcode_name}] #{opcode} / 0x#{opcode_hex}" + end + + # ================================================================================================== + # Opcode Name Resolution + # ================================================================================================== + + defp get_client_opcode_name(opcode) do + case opcode do + # Connection/Security + 0x16 -> "CP_AliveAck" + + # Login/Account + 0x01 -> "CP_PermissionRequest" + 0x02 -> "CP_CheckPassword" + 0x04 -> "CP_ServerlistRequest" + 0x05 -> "CP_SelectWorld" + 0x06 -> "CP_CheckUserLimit" + 0x0E -> "CP_CheckCharName" + 0x12 -> "CP_CreateChar" + 0x14 -> "CP_CreateUltimate" + 0x15 -> "CP_DeleteChar" + 0x17 -> "CP_ExceptionLog" + 0x18 -> "CP_SecurityPacket" + 0x19 -> "CP_CharSelect" + 0x1A -> "CP_AuthSecondPassword" + 0x1D -> "CP_ClientDumpLog" + 0x1E -> "CP_CreateSecurityHandle" + 0x20 -> "RSA_KEY" + 0x5001 -> "CP_HardwareInfo" + 0x5004 -> "CP_WindowFocus" + + # Migration/Channel + 0x0D -> "CP_PlayerLoggedIn" + 0x23 -> "CP_ChangeMap" + 0x24 -> "CP_ChangeChannel" + 0x25 -> "CP_EnterCashShop" + 0x26 -> "CP_EnterPvp" + 0x27 -> "CP_EnterPvpParty" + 0x29 -> "CP_LeavePvp" + 0xB4 -> "CP_EnterMts" + + # Player Movement/Actions + 0x2A -> "CP_MovePlayer" + 0x2C -> "CP_CancelChair" + 0x2D -> "CP_UseChair" + 0x2F -> "CP_CloseRangeAttack" + 0x30 -> "CP_RangedAttack" + 0x31 -> "CP_MagicAttack" + 0x32 -> "CP_PassiveEnergy" + 0x34 -> "CP_TakeDamage" + 0x35 -> "CP_PvpAttack" + 0x36 -> "CP_GeneralChat" + 0x37 -> "CP_CloseChalkboard" + 0x38 -> "CP_FaceExpression" + 0x75 -> "CP_CharInfoRequest" + 0x76 -> "CP_SpawnPet" + 0x78 -> "CP_CancelDebuff" + + # NPC Interaction + 0x40 -> "CP_NpcTalk" + 0x41 -> "CP_NpcMove" + 0x42 -> "CP_NpcTalkMore" + 0x43 -> "CP_NpcShop" + 0x44 -> "CP_Storage" + 0x45 -> "CP_UseHiredMerchant" + 0x47 -> "CP_MerchItemStore" + + # Inventory/Items + 0x4D -> "CP_ItemSort" + 0x4E -> "CP_ItemGather" + 0x4F -> "CP_ItemMove" + 0x53 -> "CP_UseItem" + 0x10C -> "CP_ItemPickup" + + # Stats/Skills + 0x6A -> "CP_DistributeAp" + 0x6B -> "CP_AutoAssignAp" + 0x6E -> "CP_DistributeSp" + 0x6F -> "CP_SpecialMove" + + # Social + 0xA0 -> "CP_PartyChat" + 0xA1 -> "CP_Whisper" + 0xA4 -> "CP_PartyOperation" + 0xA8 -> "CP_GuildOperation" + + # Cash Shop + 0x135 -> "CP_CsUpdate" + 0x136 -> "CP_BuyCsItem" + 0x137 -> "CP_CouponCode" + + # Custom + 0x5002 -> "CP_InjectPacket" + 0x5003 -> "CP_SetCodePage" + + _ -> "UNKNOWN" + end + end + + defp get_server_opcode_name(opcode) do + case opcode do + # General + 0x0D -> "LP_AliveReq" + 0x0C -> "LP_ChangeChannel" + 0x15 -> "LP_LatestConnectedWorld" + 0x16 -> "LP_RecommendWorldMessage" + + # Login + 0x00 -> "HELLO" + 0x01 -> "LOGIN_STATUS" + 0x03 -> "LP_ServerStatus" + 0x06 -> "SERVERLIST" + 0x07 -> "LP_CharList" + 0x08 -> "LP_ServerIp" + 0x09 -> "LP_CharNameResponse" + 0x0A -> "LP_AddNewCharEntry" + 0x0B -> "LP_DeleteCharResponse" + 0x10 -> "LP_ChannelSelected" + 0x12 -> "LP_RelogResponse" + 0x13 -> "RSA_KEY" + 0x17 -> "LOGIN_AUTH" + 0x18 -> "LP_SecondPwError" + + # Inventory/Stats + 0x19 -> "LP_ModifyInventoryItem" + 0x1A -> "LP_UpdateInventorySlot" + 0x1B -> "LP_UpdateStats" + 0x1C -> "LP_GiveBuff" + 0x1D -> "LP_CancelBuff" + 0x20 -> "LP_UpdateSkills" + 0x22 -> "LP_FameResponse" + 0x23 -> "LP_ShowStatusInfo" + 0x25 -> "LP_TrockLocations" + + # Social/Party/Guild + 0x38 -> "LP_PartyOperation" + 0x3A -> "LP_ExpeditionOperation" + 0x3B -> "LP_BuddyList" + 0x3D -> "LP_GuildOperation" + 0x3E -> "LP_AllianceOperation" + + # Map Effects/Environment + 0x3F -> "LP_SpawnPortal" + 0x40 -> "LP_MechPortal" + 0x41 -> "LP_ServerMessage" + 0x4A -> "LP_YellowChat" + + # Family + 0x6D -> "LP_SendPedigree" + 0x6E -> "LP_OpenFamily" + 0x73 -> "LP_Family" + + # Misc UI/Messages + 0x7E -> "LP_TopMsg" + 0x7F -> "LP_MidMsg" + 0x80 -> "LP_ClearMidMsg" + + # Warps/Shops + 0x90 -> "LP_WarpToMap" + 0x91 -> "LP_MtsOpen" + 0x92 -> "LP_CsOpen" + + # Effects + 0x99 -> "LP_ShowEquipEffect" + 0x9A -> "LP_MultiChat" + 0x9B -> "LP_Whisper" + 0xA1 -> "LP_MapEffect" + 0xA6 -> "LP_Clock" + + # Players + 0xB8 -> "LP_SpawnPlayer" + 0xB9 -> "LP_RemovePlayerFromMap" + 0xBA -> "LP_ChatText" + 0xBC -> "LP_Chalkboard" + + # Pets + 0xD1 -> "LP_SpawnPet" + 0xD4 -> "LP_MovePet" + 0xD5 -> "LP_PetChat" + + # Player Actions + 0xE2 -> "LP_MovePlayer" + 0xE4 -> "LP_CloseRangeAttack" + 0xE5 -> "LP_RangedAttack" + 0xE6 -> "LP_MagicAttack" + 0xE8 -> "LP_SkillEffect" + 0xEB -> "LP_DamagePlayer" + 0xEC -> "LP_FacialExpression" + 0xF0 -> "LP_ShowChair" + 0xF1 -> "LP_UpdateCharLook" + + # Summons + 0x131 -> "LP_SpawnSummon" + 0x132 -> "LP_RemoveSummon" + 0x133 -> "LP_MoveSummon" + 0x134 -> "LP_SummonAttack" + + # Monsters + 0x13A -> "LP_SpawnMonster" + 0x13B -> "LP_KillMonster" + 0x13C -> "LP_SpawnMonsterControl" + 0x13D -> "LP_MoveMonster" + 0x144 -> "LP_DamageMonster" + + # NPCs + 0x156 -> "LP_SpawnNpc" + 0x157 -> "LP_RemoveNpc" + 0x158 -> "LP_SpawnNpcRequestController" + 0x159 -> "LP_NpcAction" + 0x1A3 -> "LP_NpcTalk" + 0x1A5 -> "LP_OpenNpcShop" + + # Merchants + 0x161 -> "LP_SpawnHiredMerchant" + 0x162 -> "LP_DestroyHiredMerchant" + + # Map Objects + 0x165 -> "LP_DropItemFromMapObject" + 0x167 -> "LP_RemoveItemFromMap" + 0x16B -> "LP_SpawnMist" + 0x16C -> "LP_RemoveMist" + 0x16D -> "LP_SpawnDoor" + 0x16E -> "LP_RemoveDoor" + + # Reactors + 0x171 -> "LP_ReactorHit" + 0x173 -> "LP_ReactorSpawn" + 0x174 -> "LP_ReactorDestroy" + + # NPC/Shop Interactions + 0x1A6 -> "LP_ConfirmShopTransaction" + 0x1A9 -> "LP_OpenStorage" + 0x1AB -> "LP_MerchItemStore" + + # Cash Shop + 0x1B8 -> "LP_CsUpdate" + 0x1B9 -> "LP_CsOperation" + + # Input + 0x1C5 -> "LP_Keymap" + + # Custom + 0x5000 -> "LP_DamageSkin" + 0x5001 -> "LP_OpenWebsite" + + 0x15 -> "LP_LatestConnectedWorld" + 0x16 -> "LP_RecommendWorldMessage" + + _ -> "UNKNOWN" + end + end +end diff --git a/lib/odinsea/net/processor.ex b/lib/odinsea/net/processor.ex index cc70c4c..dc639d2 100644 --- a/lib/odinsea/net/processor.ex +++ b/lib/odinsea/net/processor.ex @@ -163,11 +163,11 @@ defmodule Odinsea.Net.Processor do # Password check (login authentication) @cp_login_password -> - Handler.on_login_password(packet, state) + Handler.on_check_password(packet, state) # World info request (server list) @cp_serverlist_request -> - Handler.on_serverlist_request(state) + Handler.on_world_info_request(state) # Select world @cp_select_world -> @@ -179,11 +179,11 @@ defmodule Odinsea.Net.Processor do # Check duplicated ID (character name availability) @cp_check_char_name -> - Handler.on_check_char_name(packet, state) + Handler.on_check_duplicated_id(packet, state) # Create new character @cp_create_char -> - Handler.on_create_char(packet, state) + Handler.on_create_new_character(packet, state) # Create ultimate (Cygnus Knights) @cp_create_ultimate -> @@ -191,15 +191,15 @@ defmodule Odinsea.Net.Processor do # Delete character @cp_delete_char -> - Handler.on_delete_char(packet, state) + Handler.on_delete_character(packet, state) # Select character (enter game) @cp_char_select -> - Handler.on_char_select(packet, state) + Handler.on_select_character(packet, state) # Second password check @cp_auth_second_password -> - Handler.on_auth_second_password(packet, state) + Handler.on_check_spw_request(packet, state) # RSA key request @cp_rsa_key -> diff --git a/lib/odinsea/shop/client.ex b/lib/odinsea/shop/client.ex index f620f5e..04a2616 100644 --- a/lib/odinsea/shop/client.ex +++ b/lib/odinsea/shop/client.ex @@ -103,7 +103,7 @@ defmodule Odinsea.Shop.Client do opcode == Opcodes.cp_player_loggedin() -> handle_migrate_in(packet, state) - opcode == Opcodes.cp_cash_shop_update() -> + opcode == Opcodes.cp_cs_update() -> # Cash shop operations handle_cash_shop_operation(packet, state) diff --git a/lib/odinsea/shop/packets.ex b/lib/odinsea/shop/packets.ex index 2a1c14c..ba91a68 100644 --- a/lib/odinsea/shop/packets.ex +++ b/lib/odinsea/shop/packets.ex @@ -126,7 +126,7 @@ defmodule Odinsea.Shop.Packets do """ def enable_cs_use(socket) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> encode_cs_update() |> Out.to_data() @@ -145,7 +145,7 @@ defmodule Odinsea.Shop.Packets do cash_items = character.cash_inventory || [] packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x4A) # Operation code for inventory |> encode_cash_items(cash_items) |> Out.to_data() @@ -178,7 +178,7 @@ defmodule Odinsea.Shop.Packets do """ def show_nx_maple_tokens(socket, character) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x3D) # Operation code for balance |> Out.encode_int(character.nx_cash || 0) |> Out.encode_int(character.maple_points || 0) @@ -196,7 +196,7 @@ defmodule Odinsea.Shop.Packets do """ def show_bought_cs_item(socket, item, sn, account_id) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x53) # Bought item operation |> Out.encode_int(account_id) |> encode_bought_item(item, sn) @@ -220,7 +220,7 @@ defmodule Odinsea.Shop.Packets do """ def show_bought_cs_package(socket, items, account_id) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x5D) # Package operation |> Out.encode_int(account_id) |> Out.encode_short(length(items)) @@ -239,7 +239,7 @@ defmodule Odinsea.Shop.Packets do """ def show_bought_cs_quest_item(socket, item, position) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x73) # Quest item operation |> Out.encode_int(item.price) |> Out.encode_short(item.count) @@ -255,7 +255,7 @@ defmodule Odinsea.Shop.Packets do """ def confirm_from_cs_inventory(socket, item, position) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x69) # From CS inventory |> Out.encode_byte(Odinsea.Game.InventoryType.get_type( Odinsea.Game.InventoryType.from_item_id(item.item_id) @@ -271,7 +271,7 @@ defmodule Odinsea.Shop.Packets do """ def confirm_to_cs_inventory(socket, item, account_id) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x5F) # To CS inventory |> Out.encode_int(account_id) |> encode_cash_item_single(item) @@ -298,7 +298,7 @@ defmodule Odinsea.Shop.Packets do """ def send_gift(socket, price, item_id, count, partner) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x5B) # Gift sent operation |> Out.encode_int(price) |> Out.encode_int(item_id) @@ -314,7 +314,7 @@ defmodule Odinsea.Shop.Packets do """ def get_cs_gifts(socket, gifts) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x48) # Gifts operation |> Out.encode_short(length(gifts)) |> then(fn pkt -> @@ -340,7 +340,7 @@ defmodule Odinsea.Shop.Packets do """ def send_wishlist(socket, _character, wishlist) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x4D) # Wishlist operation |> then(fn pkt -> Enum.reduce(wishlist, pkt, fn sn, p -> @@ -361,7 +361,7 @@ defmodule Odinsea.Shop.Packets do """ def show_coupon_redeemed(socket, items, maple_points, mesos, client_state) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x4B) # Coupon operation |> Out.encode_byte(if safe_map_size(items) > 0, do: 1, else: 0) |> Out.encode_int(safe_map_size(items)) @@ -390,7 +390,7 @@ defmodule Odinsea.Shop.Packets do """ def redeem_response(socket) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0xA1) # Redeem response |> Out.encode_int(0) |> Out.encode_int(0) @@ -409,7 +409,7 @@ defmodule Odinsea.Shop.Packets do """ def send_cs_fail(socket, code) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x49) # Fail operation |> Out.encode_byte(code) |> Out.to_data() @@ -422,7 +422,7 @@ defmodule Odinsea.Shop.Packets do """ def cash_item_expired(socket, unique_id) do packet = - Out.new(Opcodes.lp_cash_shop_update()) + Out.new(Opcodes.lp_cs_update()) |> Out.encode_byte(0x4E) # Expired operation |> Out.encode_long(unique_id) |> Out.to_data() diff --git a/logs/odinsea.log b/logs/odinsea.log new file mode 100644 index 0000000..b9e395c --- /dev/null +++ b/logs/odinsea.log @@ -0,0 +1,1582 @@ +2026-02-15 13:56:08.732 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 13:56:08.735 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 13:56:08.735 [info] Login Server: port=8584, limit=1500 +2026-02-15 13:56:08.735 [info] Game Channels: count=2 +2026-02-15 13:56:08.735 [info] Cash Shop: port=8605 +2026-02-15 13:56:08.866 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 13:56:08.867 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 13:56:08.872 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 13:56:08.873 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 13:56:08.874 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 13:56:08.879 [info] Loaded 4 map templates +2026-02-15 13:56:08.881 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 13:56:08.881 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 13:56:08.881 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 13:56:08.883 [info] Drop table cache initialized +2026-02-15 13:56:08.884 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 13:56:08.884 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 13:56:08.885 [info] Loaded 9 skills +2026-02-15 13:56:08.885 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 13:56:08.888 [info] Created 7 fallback reactor templates +2026-02-15 13:56:08.888 [info] Loaded 7 reactor templates +2026-02-15 13:56:08.888 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 13:56:08.888 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 13:56:08.888 [info] Loaded 5 quest definitions +2026-02-15 13:56:08.888 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 13:56:08.891 [info] Loaded 3 cash shop items +2026-02-15 13:56:08.904 [info] Script Manager initialized +2026-02-15 13:56:08.905 [info] NPC Script Manager initialized +2026-02-15 13:56:08.905 [info] Portal Script Manager initialized +2026-02-15 13:56:08.905 [info] Reactor Script Manager initialized +2026-02-15 13:56:08.905 [info] Event Script Manager initialized +2026-02-15 13:56:08.927 [info] World state initialized +2026-02-15 13:56:08.927 [info] Party service initialized +2026-02-15 13:56:08.927 [info] Guild service initialized with 0 guilds +2026-02-15 13:56:08.927 [info] Family service initialized with 0 families +2026-02-15 13:56:08.929 [info] Login server listening on port 8584 +2026-02-15 13:56:08.929 [info] Starting 2 game channels +2026-02-15 13:56:08.940 [info] Channel 1 listening on port 8585 +2026-02-15 13:56:08.951 [info] Channel 2 listening on port 8586 +2026-02-15 13:56:08.962 [info] Cash shop server listening on port 8605 +2026-02-15 13:59:41.849 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 13:59:41.851 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 13:59:41.851 [info] Login Server: port=8584, limit=1500 +2026-02-15 13:59:41.851 [info] Game Channels: count=2 +2026-02-15 13:59:41.851 [info] Cash Shop: port=8605 +2026-02-15 13:59:41.968 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 13:59:41.969 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 13:59:41.974 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 13:59:41.974 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 13:59:41.977 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 13:59:41.979 [info] Loaded 4 map templates +2026-02-15 13:59:41.979 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 13:59:41.979 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 13:59:41.979 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 13:59:41.979 [info] Drop table cache initialized +2026-02-15 13:59:41.979 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 13:59:41.980 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 13:59:41.980 [info] Loaded 9 skills +2026-02-15 13:59:41.980 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 13:59:41.982 [info] Created 7 fallback reactor templates +2026-02-15 13:59:41.982 [info] Loaded 7 reactor templates +2026-02-15 13:59:41.982 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 13:59:41.982 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 13:59:41.982 [info] Loaded 5 quest definitions +2026-02-15 13:59:41.982 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 13:59:41.984 [info] Loaded 3 cash shop items +2026-02-15 13:59:41.996 [info] Script Manager initialized +2026-02-15 13:59:41.997 [info] NPC Script Manager initialized +2026-02-15 13:59:41.997 [info] Portal Script Manager initialized +2026-02-15 13:59:41.997 [info] Reactor Script Manager initialized +2026-02-15 13:59:41.997 [info] Event Script Manager initialized +2026-02-15 13:59:42.017 [info] World state initialized +2026-02-15 13:59:42.017 [info] Party service initialized +2026-02-15 13:59:42.017 [info] Guild service initialized with 0 guilds +2026-02-15 13:59:42.017 [info] Family service initialized with 0 families +2026-02-15 13:59:42.018 [info] Login server listening on port 8584 +2026-02-15 13:59:42.018 [info] Starting 2 game channels +2026-02-15 13:59:42.029 [info] Channel 1 listening on port 8585 +2026-02-15 13:59:42.039 [info] Channel 2 listening on port 8586 +2026-02-15 13:59:42.050 [info] Cash shop server listening on port 8605 +2026-02-15 13:59:45.148 [info] tzdata release in place is from a file last modified Thu, 16 Jan 2025 17:10:51 GMT. Release file on server was last modified Wed, 10 Dec 2025 23:51:30 GMT. +2026-02-15 13:59:45.690 [info] Tzdata has updated the release from 2025a to 2025c +2026-02-15 14:00:34.582 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 14:00:34.584 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 14:00:34.584 [info] Login Server: port=8584, limit=1500 +2026-02-15 14:00:34.584 [info] Game Channels: count=2 +2026-02-15 14:00:34.584 [info] Cash Shop: port=8605 +2026-02-15 14:00:34.699 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 14:00:34.700 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 14:00:34.703 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 14:00:34.703 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 14:00:34.706 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 14:00:34.710 [info] Loaded 4 map templates +2026-02-15 14:00:34.711 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 14:00:34.711 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 14:00:34.711 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 14:00:34.711 [info] Drop table cache initialized +2026-02-15 14:00:34.711 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 14:00:34.711 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 14:00:34.712 [info] Loaded 9 skills +2026-02-15 14:00:34.712 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 14:00:34.714 [info] Created 7 fallback reactor templates +2026-02-15 14:00:34.714 [info] Loaded 7 reactor templates +2026-02-15 14:00:34.714 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 14:00:34.714 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 14:00:34.714 [info] Loaded 5 quest definitions +2026-02-15 14:00:34.714 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 14:00:34.717 [info] Loaded 3 cash shop items +2026-02-15 14:00:34.728 [info] Script Manager initialized +2026-02-15 14:00:34.729 [info] NPC Script Manager initialized +2026-02-15 14:00:34.729 [info] Portal Script Manager initialized +2026-02-15 14:00:34.729 [info] Reactor Script Manager initialized +2026-02-15 14:00:34.729 [info] Event Script Manager initialized +2026-02-15 14:00:34.749 [info] World state initialized +2026-02-15 14:00:34.749 [info] Party service initialized +2026-02-15 14:00:34.749 [info] Guild service initialized with 0 guilds +2026-02-15 14:00:34.749 [info] Family service initialized with 0 families +2026-02-15 14:00:34.750 [info] Login server listening on port 8584 +2026-02-15 14:00:34.751 [info] Starting 2 game channels +2026-02-15 14:00:34.761 [info] Channel 1 listening on port 8585 +2026-02-15 14:00:34.771 [info] Channel 2 listening on port 8586 +2026-02-15 14:00:34.782 [info] Cash shop server listening on port 8605 +2026-02-15 14:01:17.260 [info] Login client connected from 127.0.0.1 +2026-02-15 14:01:37.776 [info] Login client disconnected: 127.0.0.1 +2026-02-15 14:26:59.539 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 14:26:59.542 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 14:26:59.542 [info] Login Server: port=8584, limit=1500 +2026-02-15 14:26:59.542 [info] Game Channels: count=2 +2026-02-15 14:26:59.542 [info] Cash Shop: port=8605 +2026-02-15 14:26:59.668 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 14:26:59.669 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 14:26:59.674 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 14:26:59.674 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 14:26:59.676 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 14:26:59.680 [info] Loaded 4 map templates +2026-02-15 14:26:59.683 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 14:26:59.683 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 14:26:59.683 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 14:26:59.684 [info] Drop table cache initialized +2026-02-15 14:26:59.685 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 14:26:59.685 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 14:26:59.686 [info] Loaded 9 skills +2026-02-15 14:26:59.687 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 14:26:59.690 [info] Created 7 fallback reactor templates +2026-02-15 14:26:59.690 [info] Loaded 7 reactor templates +2026-02-15 14:26:59.690 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 14:26:59.690 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 14:26:59.690 [info] Loaded 5 quest definitions +2026-02-15 14:26:59.691 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 14:26:59.694 [info] Loaded 3 cash shop items +2026-02-15 14:26:59.707 [info] Script Manager initialized +2026-02-15 14:26:59.707 [info] NPC Script Manager initialized +2026-02-15 14:26:59.707 [info] Portal Script Manager initialized +2026-02-15 14:26:59.708 [info] Reactor Script Manager initialized +2026-02-15 14:26:59.708 [info] Event Script Manager initialized +2026-02-15 14:26:59.729 [info] World state initialized +2026-02-15 14:26:59.729 [info] Party service initialized +2026-02-15 14:26:59.729 [info] Guild service initialized with 0 guilds +2026-02-15 14:26:59.729 [info] Family service initialized with 0 families +2026-02-15 14:26:59.730 [info] Login server listening on port 8584 +2026-02-15 14:26:59.730 [info] Starting 2 game channels +2026-02-15 14:26:59.741 [info] Channel 1 listening on port 8585 +2026-02-15 14:26:59.751 [info] Channel 2 listening on port 8586 +2026-02-15 14:26:59.762 [info] Cash shop server listening on port 8605 +2026-02-15 14:27:11.133 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 14:27:11.135 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 14:27:11.135 [info] Login Server: port=8584, limit=1500 +2026-02-15 14:27:11.135 [info] Game Channels: count=2 +2026-02-15 14:27:11.135 [info] Cash Shop: port=8605 +2026-02-15 14:27:11.250 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 14:27:11.250 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 14:27:11.254 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 14:27:11.254 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 14:27:11.258 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 14:27:11.260 [info] Loaded 4 map templates +2026-02-15 14:27:11.260 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 14:27:11.261 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 14:27:11.261 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 14:27:11.261 [info] Drop table cache initialized +2026-02-15 14:27:11.261 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 14:27:11.261 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 14:27:11.261 [info] Loaded 9 skills +2026-02-15 14:27:11.262 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 14:27:11.264 [info] Created 7 fallback reactor templates +2026-02-15 14:27:11.264 [info] Loaded 7 reactor templates +2026-02-15 14:27:11.264 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 14:27:11.264 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 14:27:11.264 [info] Loaded 5 quest definitions +2026-02-15 14:27:11.264 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 14:27:11.266 [info] Loaded 3 cash shop items +2026-02-15 14:27:11.278 [info] Script Manager initialized +2026-02-15 14:27:11.278 [info] NPC Script Manager initialized +2026-02-15 14:27:11.278 [info] Portal Script Manager initialized +2026-02-15 14:27:11.278 [info] Reactor Script Manager initialized +2026-02-15 14:27:11.278 [info] Event Script Manager initialized +2026-02-15 14:27:11.298 [info] World state initialized +2026-02-15 14:27:11.298 [info] Party service initialized +2026-02-15 14:27:11.298 [info] Guild service initialized with 0 guilds +2026-02-15 14:27:11.298 [info] Family service initialized with 0 families +2026-02-15 14:27:11.299 [info] Login server listening on port 8584 +2026-02-15 14:27:11.299 [info] Starting 2 game channels +2026-02-15 14:27:11.310 [info] Channel 1 listening on port 8585 +2026-02-15 14:27:11.320 [info] Channel 2 listening on port 8586 +2026-02-15 14:27:11.330 [info] Cash shop server listening on port 8605 +2026-02-15 14:31:02.211 [info] Login client connected from 127.0.0.1 +2026-02-15 14:31:02.252 [error] GenServer Odinsea.Login.Listener terminating +** (MatchError) no match of right hand side value: {:error, {:undef, [{Odinsea.Constants.Server, :maple_locale, [], []}, {Odinsea.Login.Packets, :get_hello, 3, [file: ~c"lib/odinsea/login/packets.ex", line: 42]}, {Odinsea.Login.Client, :send_hello_packet, 1, [file: ~c"lib/odinsea/login/client.ex", line: 164]}, {Odinsea.Login.Client, :init, 1, [file: ~c"lib/odinsea/login/client.ex", line: 62]}, {:gen_server, :init_it, 2, [file: ~c"gen_server.erl", line: 2276]}, {:gen_server, :init_it, 6, [file: ~c"gen_server.erl", line: 2236]}, {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 333]}]}} + (odinsea 0.1.0) lib/odinsea/login/listener.ex:36: Odinsea.Login.Listener.handle_info/2 + (stdlib 7.0.2) gen_server.erl:2434: :gen_server.try_handle_info/3 + (stdlib 7.0.2) gen_server.erl:2420: :gen_server.handle_msg/3 + (stdlib 7.0.2) proc_lib.erl:333: :proc_lib.init_p_do_apply/3 +Last message: :accept +2026-02-15 14:31:02.273 [info] Login server listening on port 8584 +2026-02-15 15:19:27.599 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 15:19:27.602 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 15:19:27.602 [info] Login Server: port=8584, limit=1500 +2026-02-15 15:19:27.602 [info] Game Channels: count=2 +2026-02-15 15:19:27.602 [info] Cash Shop: port=8605 +2026-02-15 15:19:27.728 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 15:19:27.728 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 15:19:27.734 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 15:19:27.734 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 15:19:27.734 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 15:19:27.739 [info] Loaded 4 map templates +2026-02-15 15:19:27.739 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 15:19:27.739 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 15:19:27.739 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 15:19:27.740 [info] Drop table cache initialized +2026-02-15 15:19:27.741 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 15:19:27.741 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 15:19:27.742 [info] Loaded 9 skills +2026-02-15 15:19:27.742 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 15:19:27.744 [info] Created 7 fallback reactor templates +2026-02-15 15:19:27.744 [info] Loaded 7 reactor templates +2026-02-15 15:19:27.746 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 15:19:27.746 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 15:19:27.746 [info] Loaded 5 quest definitions +2026-02-15 15:19:27.747 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 15:19:27.749 [info] Loaded 3 cash shop items +2026-02-15 15:19:27.763 [info] Script Manager initialized +2026-02-15 15:19:27.763 [info] NPC Script Manager initialized +2026-02-15 15:19:27.763 [info] Portal Script Manager initialized +2026-02-15 15:19:27.764 [info] Reactor Script Manager initialized +2026-02-15 15:19:27.764 [info] Event Script Manager initialized +2026-02-15 15:19:27.790 [info] World state initialized +2026-02-15 15:19:27.790 [info] Party service initialized +2026-02-15 15:19:27.790 [info] Guild service initialized with 0 guilds +2026-02-15 15:19:27.790 [info] Family service initialized with 0 families +2026-02-15 15:19:27.792 [info] Login server listening on port 8584 +2026-02-15 15:19:27.792 [info] Starting 2 game channels +2026-02-15 15:19:27.803 [info] Channel 1 listening on port 8585 +2026-02-15 15:19:27.814 [info] Channel 2 listening on port 8586 +2026-02-15 15:19:27.825 [info] Cash shop server listening on port 8605 +2026-02-15 15:20:02.252 [info] Login client connected from 127.0.0.1 +2026-02-15 15:20:02.302 [error] GenServer Odinsea.Login.Listener terminating +** (MatchError) no match of right hand side value: {:error, {:function_clause, [{Odinsea.Net.Hex, :encode, [[[[[[[[] | <<14, 0>>] | <<112, 0>>] | <<1, 0, 52>>] | <<146, 76, 105, 198>>] | <<163, 186, 186, 17>>] | "\a"]], [file: ~c"lib/odinsea/net/hex.ex", line: 14]}, {Odinsea.Net.PacketLogger, :log_raw_packet, 4, [file: ~c"lib/odinsea/net/packet_logger.ex", line: 100]}, {Odinsea.Login.Client, :send_hello_packet, 1, [file: ~c"lib/odinsea/login/client.ex", line: 170]}, {Odinsea.Login.Client, :init, 1, [file: ~c"lib/odinsea/login/client.ex", line: 62]}, {:gen_server, :init_it, 2, [file: ~c"gen_server.erl", line: 2276]}, {:gen_server, :init_it, 6, [file: ~c"gen_server.erl", line: 2236]}, {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 333]}]}} + (odinsea 0.1.0) lib/odinsea/login/listener.ex:36: Odinsea.Login.Listener.handle_info/2 + (stdlib 7.0.2) gen_server.erl:2434: :gen_server.try_handle_info/3 + (stdlib 7.0.2) gen_server.erl:2420: :gen_server.handle_msg/3 + (stdlib 7.0.2) proc_lib.erl:333: :proc_lib.init_p_do_apply/3 +Last message: :accept +2026-02-15 15:20:02.324 [info] Login server listening on port 8584 +2026-02-15 15:24:12.161 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 15:24:12.163 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 15:24:12.163 [info] Login Server: port=8584, limit=1500 +2026-02-15 15:24:12.163 [info] Game Channels: count=2 +2026-02-15 15:24:12.163 [info] Cash Shop: port=8605 +2026-02-15 15:24:12.285 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 15:24:12.285 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 15:24:12.288 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 15:24:12.288 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 15:24:12.290 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 15:24:12.296 [info] Loaded 4 map templates +2026-02-15 15:24:12.296 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 15:24:12.296 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 15:24:12.296 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 15:24:12.297 [info] Drop table cache initialized +2026-02-15 15:24:12.298 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 15:24:12.299 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 15:24:12.299 [info] Loaded 9 skills +2026-02-15 15:24:12.301 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 15:24:12.303 [info] Created 7 fallback reactor templates +2026-02-15 15:24:12.304 [info] Loaded 7 reactor templates +2026-02-15 15:24:12.306 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 15:24:12.306 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 15:24:12.306 [info] Loaded 5 quest definitions +2026-02-15 15:24:12.306 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 15:24:12.308 [info] Loaded 3 cash shop items +2026-02-15 15:24:12.321 [info] Script Manager initialized +2026-02-15 15:24:12.321 [info] NPC Script Manager initialized +2026-02-15 15:24:12.322 [info] Portal Script Manager initialized +2026-02-15 15:24:12.322 [info] Reactor Script Manager initialized +2026-02-15 15:24:12.323 [info] Event Script Manager initialized +2026-02-15 15:24:12.347 [info] World state initialized +2026-02-15 15:24:12.347 [info] Party service initialized +2026-02-15 15:24:12.347 [info] Guild service initialized with 0 guilds +2026-02-15 15:24:12.347 [info] Family service initialized with 0 families +2026-02-15 15:24:12.349 [info] Login server listening on port 8584 +2026-02-15 15:24:12.349 [info] Starting 2 game channels +2026-02-15 15:24:12.360 [info] Channel 1 listening on port 8585 +2026-02-15 15:24:12.371 [info] Channel 2 listening on port 8586 +2026-02-15 15:24:12.382 [info] Cash shop server listening on port 8605 +2026-02-15 15:24:14.765 [info] Login client connected from 127.0.0.1 +2026-02-15 15:24:14.812 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 9A 0F 0C A8 BC 0D B3 E6 07 +[Text] ..p...4......... +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 15:24:15.276 [info] [client] [UNKNOWN] -22404 / 0x-5784 +[Data] 7C A8 7B A8 BF 0A CD DE C7 71 AC +[Text] |.{......q. +[Context] IP=127.0.0.1 Server=login Size=11 bytes + +2026-02-15 15:24:15.279 [info] [client] [UNKNOWN] -17770 / 0x-456A +[Data] 96 BA 94 BA 51 06 +[Text] ....Q. +[Context] IP=127.0.0.1 Server=login Size=6 bytes + +2026-02-15 15:24:16.619 [info] [client] [UNKNOWN] -27681 / 0x-6C21 +[Data] DF 93 DD 93 7C 79 +[Text] ....|y +[Context] IP=127.0.0.1 Server=login Size=6 bytes + +2026-02-15 15:44:54.585 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 15:44:54.585 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 15:44:54.585 [info] Login Server: port=8584, limit=1500 +2026-02-15 15:44:54.585 [info] Game Channels: count=2 +2026-02-15 15:44:54.585 [info] Cash Shop: port=8605 +2026-02-15 15:44:54.598 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 15:44:54.598 [warning] Items file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 15:44:54.598 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/equips.json +2026-02-15 15:44:54.598 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 15:44:54.599 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 15:44:54.599 [info] Loaded 4 map templates +2026-02-15 15:44:54.599 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 15:44:54.599 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 15:44:54.599 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 15:44:54.600 [info] Drop table cache initialized +2026-02-15 15:44:54.600 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 15:44:54.600 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 15:44:54.600 [info] Loaded 9 skills +2026-02-15 15:44:54.600 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 15:44:54.600 [info] Created 7 fallback reactor templates +2026-02-15 15:44:54.600 [info] Loaded 7 reactor templates +2026-02-15 15:44:54.600 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 15:44:54.600 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 15:44:54.600 [info] Loaded 5 quest definitions +2026-02-15 15:44:54.601 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 15:44:54.601 [info] Loaded 3 cash shop items +2026-02-15 15:44:54.601 [info] Script Manager initialized +2026-02-15 15:44:54.601 [info] NPC Script Manager initialized +2026-02-15 15:44:54.601 [info] Portal Script Manager initialized +2026-02-15 15:44:54.601 [info] Reactor Script Manager initialized +2026-02-15 15:44:54.602 [info] Event Script Manager initialized +2026-02-15 15:44:54.609 [info] World state initialized +2026-02-15 15:44:54.609 [info] Party service initialized +2026-02-15 15:44:54.609 [info] Guild service initialized with 0 guilds +2026-02-15 15:44:54.610 [info] Family service initialized with 0 families +2026-02-15 15:44:54.612 [info] Login server listening on port 8584 +2026-02-15 15:44:54.612 [info] Starting 2 game channels +2026-02-15 15:44:54.617 [error] Failed to start channel 1 on port 8585: :eaddrinuse +2026-02-15 15:44:54.618 [info] Application odinsea exited: Odinsea.Application.start(:normal, []) returned an error: shutdown: failed to start child: Odinsea.Channel.Supervisor + ** (EXIT) shutdown: failed to start child: {Odinsea.Channel.Server, 1} + ** (EXIT) :eaddrinuse +2026-02-15 15:44:54.626 [info] Application nimble_pool exited: :stopped +2026-02-15 15:44:54.626 [info] Application poolboy exited: :stopped +2026-02-15 15:44:54.627 [info] Application timex exited: :stopped +2026-02-15 15:44:54.628 [info] Application gettext exited: :stopped +2026-02-15 15:44:54.628 [info] Application expo exited: :stopped +2026-02-15 15:44:54.628 [info] Application combine exited: :stopped +2026-02-15 15:44:54.630 [info] Application tzdata exited: :stopped +2026-02-15 15:44:54.632 [info] Application hackney exited: :stopped +2026-02-15 15:44:54.632 [info] Application metrics exited: :stopped +2026-02-15 15:44:54.632 [info] Application ssl_verify_fun exited: :stopped +2026-02-15 15:44:54.632 [info] Application parse_trans exited: :stopped +2026-02-15 15:44:54.632 [info] Application syntax_tools exited: :stopped +2026-02-15 15:44:54.632 [info] Application certifi exited: :stopped +2026-02-15 15:44:54.632 [info] Application mimerl exited: :stopped +2026-02-15 15:44:54.632 [info] Application idna exited: :stopped +2026-02-15 15:44:54.632 [info] Application unicode_util_compat exited: :stopped +2026-02-15 15:44:54.632 [info] Application logger_file_backend exited: :stopped +2026-02-15 15:44:54.632 [info] Application redix exited: :stopped +2026-02-15 15:44:54.632 [info] Application nimble_options exited: :stopped +2026-02-15 15:44:54.633 [info] Application ecto_sql exited: :stopped +2026-02-15 15:44:54.633 [info] Application myxql exited: :stopped +2026-02-15 15:44:54.634 [info] Application db_connection exited: :stopped +2026-02-15 15:44:54.635 [info] Application ecto exited: :stopped +2026-02-15 15:44:54.635 [info] Application jason exited: :stopped +2026-02-15 15:44:54.635 [info] Application decimal exited: :stopped +2026-02-15 15:44:54.636 [info] Application telemetry exited: :stopped +2026-02-15 15:44:54.636 [info] Application eex exited: :stopped +2026-02-15 15:44:54.637 [info] Application gen_state_machine exited: :stopped +2026-02-15 15:44:54.638 [info] Application ranch exited: :stopped +2026-02-15 15:45:06.800 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 15:45:06.802 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 15:45:06.803 [info] Login Server: port=8584, limit=1500 +2026-02-15 15:45:06.803 [info] Game Channels: count=2 +2026-02-15 15:45:06.803 [info] Cash Shop: port=8605 +2026-02-15 15:45:06.932 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 15:45:06.932 [warning] Items file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 15:45:06.936 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/equips.json +2026-02-15 15:45:06.936 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 15:45:06.939 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 15:45:06.941 [info] Loaded 4 map templates +2026-02-15 15:45:06.942 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 15:45:06.942 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 15:45:06.942 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 15:45:06.942 [info] Drop table cache initialized +2026-02-15 15:45:06.943 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 15:45:06.943 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 15:45:06.943 [info] Loaded 9 skills +2026-02-15 15:45:06.943 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 15:45:06.945 [info] Created 7 fallback reactor templates +2026-02-15 15:45:06.945 [info] Loaded 7 reactor templates +2026-02-15 15:45:06.945 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 15:45:06.946 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 15:45:06.946 [info] Loaded 5 quest definitions +2026-02-15 15:45:06.946 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/test/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 15:45:06.948 [info] Loaded 3 cash shop items +2026-02-15 15:45:06.961 [info] Script Manager initialized +2026-02-15 15:45:06.961 [info] NPC Script Manager initialized +2026-02-15 15:45:06.961 [info] Portal Script Manager initialized +2026-02-15 15:45:06.962 [info] Reactor Script Manager initialized +2026-02-15 15:45:06.962 [info] Event Script Manager initialized +2026-02-15 15:45:06.983 [info] World state initialized +2026-02-15 15:45:06.983 [info] Party service initialized +2026-02-15 15:45:06.983 [info] Guild service initialized with 0 guilds +2026-02-15 15:45:06.983 [info] Family service initialized with 0 families +2026-02-15 15:45:06.984 [info] Login server listening on port 8584 +2026-02-15 15:45:06.984 [info] Starting 2 game channels +2026-02-15 15:45:06.997 [error] Failed to start channel 1 on port 8585: :eaddrinuse +2026-02-15 15:45:06.998 [info] Application odinsea exited: Odinsea.Application.start(:normal, []) returned an error: shutdown: failed to start child: Odinsea.Channel.Supervisor + ** (EXIT) shutdown: failed to start child: {Odinsea.Channel.Server, 1} + ** (EXIT) :eaddrinuse +2026-02-15 15:45:07.012 [info] Application nimble_pool exited: :stopped +2026-02-15 15:45:07.012 [info] Application poolboy exited: :stopped +2026-02-15 15:45:07.013 [info] Application timex exited: :stopped +2026-02-15 15:45:07.014 [info] Application gettext exited: :stopped +2026-02-15 15:45:07.014 [info] Application expo exited: :stopped +2026-02-15 15:45:07.014 [info] Application combine exited: :stopped +2026-02-15 15:45:07.016 [info] Application tzdata exited: :stopped +2026-02-15 15:45:07.017 [info] Application hackney exited: :stopped +2026-02-15 15:45:07.017 [info] Application metrics exited: :stopped +2026-02-15 15:45:07.017 [info] Application ssl_verify_fun exited: :stopped +2026-02-15 15:45:07.017 [info] Application parse_trans exited: :stopped +2026-02-15 15:45:07.017 [info] Application syntax_tools exited: :stopped +2026-02-15 15:45:07.017 [info] Application certifi exited: :stopped +2026-02-15 15:45:07.017 [info] Application mimerl exited: :stopped +2026-02-15 15:45:07.017 [info] Application idna exited: :stopped +2026-02-15 15:45:07.017 [info] Application unicode_util_compat exited: :stopped +2026-02-15 15:45:07.017 [info] Application logger_file_backend exited: :stopped +2026-02-15 15:45:07.017 [info] Application redix exited: :stopped +2026-02-15 15:45:07.018 [info] Application nimble_options exited: :stopped +2026-02-15 15:45:07.018 [info] Application ecto_sql exited: :stopped +2026-02-15 15:45:07.018 [info] Application myxql exited: :stopped +2026-02-15 15:45:07.019 [info] Application db_connection exited: :stopped +2026-02-15 15:45:07.020 [info] Application ecto exited: :stopped +2026-02-15 15:45:07.020 [info] Application jason exited: :stopped +2026-02-15 15:45:07.020 [info] Application decimal exited: :stopped +2026-02-15 15:45:07.021 [info] Application telemetry exited: :stopped +2026-02-15 15:45:07.021 [info] Application eex exited: :stopped +2026-02-15 15:45:07.022 [info] Application gen_state_machine exited: :stopped +2026-02-15 15:45:07.023 [info] Application ranch exited: :stopped +2026-02-15 16:19:50.589 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 16:19:50.591 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 16:19:50.591 [info] Login Server: port=8584, limit=1500 +2026-02-15 16:19:50.591 [info] Game Channels: count=2 +2026-02-15 16:19:50.591 [info] Cash Shop: port=8605 +2026-02-15 16:19:50.711 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 16:19:50.711 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 16:19:50.715 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 16:19:50.715 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 16:19:50.718 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 16:19:50.722 [info] Loaded 4 map templates +2026-02-15 16:19:50.722 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 16:19:50.723 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 16:19:50.723 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 16:19:50.724 [info] Drop table cache initialized +2026-02-15 16:19:50.725 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 16:19:50.725 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 16:19:50.725 [info] Loaded 9 skills +2026-02-15 16:19:50.727 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 16:19:50.731 [info] Created 7 fallback reactor templates +2026-02-15 16:19:50.731 [info] Loaded 7 reactor templates +2026-02-15 16:19:50.731 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 16:19:50.731 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 16:19:50.731 [info] Loaded 5 quest definitions +2026-02-15 16:19:50.731 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 16:19:50.734 [info] Loaded 3 cash shop items +2026-02-15 16:19:50.746 [info] Script Manager initialized +2026-02-15 16:19:50.746 [info] NPC Script Manager initialized +2026-02-15 16:19:50.747 [info] Portal Script Manager initialized +2026-02-15 16:19:50.747 [info] Reactor Script Manager initialized +2026-02-15 16:19:50.747 [info] Event Script Manager initialized +2026-02-15 16:19:50.768 [info] World state initialized +2026-02-15 16:19:50.768 [info] Party service initialized +2026-02-15 16:19:50.768 [info] Guild service initialized with 0 guilds +2026-02-15 16:19:50.768 [info] Family service initialized with 0 families +2026-02-15 16:19:50.769 [info] Login server listening on port 8584 +2026-02-15 16:19:50.769 [info] Starting 2 game channels +2026-02-15 16:19:50.783 [error] Failed to start channel 1 on port 8585: :eaddrinuse +2026-02-15 16:19:50.784 [info] Application odinsea exited: Odinsea.Application.start(:normal, []) returned an error: shutdown: failed to start child: Odinsea.Channel.Supervisor + ** (EXIT) shutdown: failed to start child: {Odinsea.Channel.Server, 1} + ** (EXIT) :eaddrinuse +2026-02-15 16:19:50.797 [info] Application nimble_pool exited: :stopped +2026-02-15 16:19:50.797 [info] Application poolboy exited: :stopped +2026-02-15 16:19:50.798 [info] Application timex exited: :stopped +2026-02-15 16:19:50.798 [info] Application gettext exited: :stopped +2026-02-15 16:19:50.798 [info] Application expo exited: :stopped +2026-02-15 16:19:50.799 [info] Application combine exited: :stopped +2026-02-15 16:19:50.801 [info] Application tzdata exited: :stopped +2026-02-15 16:19:50.802 [info] Application hackney exited: :stopped +2026-02-15 16:19:50.802 [info] Application metrics exited: :stopped +2026-02-15 16:19:50.802 [info] Application ssl_verify_fun exited: :stopped +2026-02-15 16:19:50.802 [info] Application parse_trans exited: :stopped +2026-02-15 16:19:50.802 [info] Application syntax_tools exited: :stopped +2026-02-15 16:19:50.802 [info] Application certifi exited: :stopped +2026-02-15 16:19:50.802 [info] Application mimerl exited: :stopped +2026-02-15 16:19:50.802 [info] Application idna exited: :stopped +2026-02-15 16:19:50.802 [info] Application unicode_util_compat exited: :stopped +2026-02-15 16:19:50.802 [info] Application logger_file_backend exited: :stopped +2026-02-15 16:19:50.802 [info] Application redix exited: :stopped +2026-02-15 16:19:50.802 [info] Application nimble_options exited: :stopped +2026-02-15 16:19:50.803 [info] Application ecto_sql exited: :stopped +2026-02-15 16:19:50.803 [info] Application myxql exited: :stopped +2026-02-15 16:19:50.804 [info] Application db_connection exited: :stopped +2026-02-15 16:19:50.804 [info] Application ecto exited: :stopped +2026-02-15 16:19:50.804 [info] Application jason exited: :stopped +2026-02-15 16:19:50.804 [info] Application decimal exited: :stopped +2026-02-15 16:19:50.805 [info] Application telemetry exited: :stopped +2026-02-15 16:19:50.805 [info] Application eex exited: :stopped +2026-02-15 16:19:50.806 [info] Application gen_state_machine exited: :stopped +2026-02-15 16:19:50.807 [info] Application ranch exited: :stopped +2026-02-15 16:19:59.183 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 16:19:59.184 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 16:19:59.184 [info] Login Server: port=8584, limit=1500 +2026-02-15 16:19:59.184 [info] Game Channels: count=2 +2026-02-15 16:19:59.184 [info] Cash Shop: port=8605 +2026-02-15 16:19:59.294 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 16:19:59.294 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 16:19:59.297 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 16:19:59.297 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 16:19:59.300 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 16:19:59.302 [info] Loaded 4 map templates +2026-02-15 16:19:59.303 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 16:19:59.303 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 16:19:59.303 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 16:19:59.303 [info] Drop table cache initialized +2026-02-15 16:19:59.303 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 16:19:59.303 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 16:19:59.303 [info] Loaded 9 skills +2026-02-15 16:19:59.304 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 16:19:59.305 [info] Created 7 fallback reactor templates +2026-02-15 16:19:59.305 [info] Loaded 7 reactor templates +2026-02-15 16:19:59.306 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 16:19:59.306 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 16:19:59.306 [info] Loaded 5 quest definitions +2026-02-15 16:19:59.306 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 16:19:59.308 [info] Loaded 3 cash shop items +2026-02-15 16:19:59.318 [info] Script Manager initialized +2026-02-15 16:19:59.319 [info] NPC Script Manager initialized +2026-02-15 16:19:59.319 [info] Portal Script Manager initialized +2026-02-15 16:19:59.319 [info] Reactor Script Manager initialized +2026-02-15 16:19:59.319 [info] Event Script Manager initialized +2026-02-15 16:19:59.336 [info] World state initialized +2026-02-15 16:19:59.336 [info] Party service initialized +2026-02-15 16:19:59.336 [info] Guild service initialized with 0 guilds +2026-02-15 16:19:59.336 [info] Family service initialized with 0 families +2026-02-15 16:19:59.337 [info] Login server listening on port 8584 +2026-02-15 16:19:59.337 [info] Starting 2 game channels +2026-02-15 16:19:59.348 [info] Channel 1 listening on port 8585 +2026-02-15 16:19:59.359 [info] Channel 2 listening on port 8586 +2026-02-15 16:19:59.370 [info] Cash shop server listening on port 8605 +2026-02-15 16:20:24.618 [info] Login client connected from 127.0.0.1 +2026-02-15 16:20:24.664 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 A9 B3 11 A7 0C 8E 8A 32 07 +[Text] ..p...4.......2. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 16:20:25.123 [info] [client] [UNKNOWN] 13957 / 0x3685 +[Data] 85 36 65 C0 83 FD CD +[Text] .6e.... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 16:20:25.126 [warning] Invalid packet header: raw_seq=27928, expected version check failed +2026-02-15 16:20:25.126 [info] [client] [UNKNOWN] -9672 / 0x-25C8 +[Data] 38 DA +[Text] 8. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:20:26.327 [warning] Invalid packet header: raw_seq=61970, expected version check failed +2026-02-15 16:20:26.327 [info] [client] [UNKNOWN] -28565 / 0x-6F95 +[Data] 6B 90 +[Text] k. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:20:56.331 [warning] Login client error: :timeout +2026-02-15 16:40:48.362 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 16:40:48.364 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 16:40:48.364 [info] Login Server: port=8584, limit=1500 +2026-02-15 16:40:48.364 [info] Game Channels: count=2 +2026-02-15 16:40:48.364 [info] Cash Shop: port=8605 +2026-02-15 16:40:48.487 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 16:40:48.487 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 16:40:48.491 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 16:40:48.491 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 16:40:48.493 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 16:40:48.496 [info] Loaded 4 map templates +2026-02-15 16:40:48.498 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 16:40:48.498 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 16:40:48.499 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 16:40:48.499 [info] Drop table cache initialized +2026-02-15 16:40:48.499 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 16:40:48.499 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 16:40:48.499 [info] Loaded 9 skills +2026-02-15 16:40:48.499 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 16:40:48.502 [info] Created 7 fallback reactor templates +2026-02-15 16:40:48.502 [info] Loaded 7 reactor templates +2026-02-15 16:40:48.502 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 16:40:48.502 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 16:40:48.502 [info] Loaded 5 quest definitions +2026-02-15 16:40:48.502 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 16:40:48.504 [info] Loaded 3 cash shop items +2026-02-15 16:40:48.517 [info] Script Manager initialized +2026-02-15 16:40:48.518 [info] NPC Script Manager initialized +2026-02-15 16:40:48.518 [info] Portal Script Manager initialized +2026-02-15 16:40:48.518 [info] Reactor Script Manager initialized +2026-02-15 16:40:48.518 [info] Event Script Manager initialized +2026-02-15 16:40:48.540 [info] World state initialized +2026-02-15 16:40:48.540 [info] Party service initialized +2026-02-15 16:40:48.540 [info] Guild service initialized with 0 guilds +2026-02-15 16:40:48.540 [info] Family service initialized with 0 families +2026-02-15 16:40:48.542 [info] Login server listening on port 8584 +2026-02-15 16:40:48.542 [info] Starting 2 game channels +2026-02-15 16:40:48.552 [info] Channel 1 listening on port 8585 +2026-02-15 16:40:48.563 [info] Channel 2 listening on port 8586 +2026-02-15 16:40:48.574 [info] Cash shop server listening on port 8605 +2026-02-15 16:41:03.020 [info] Login client connected from 127.0.0.1 +2026-02-15 16:41:03.065 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 8B BE E4 90 B6 6C 58 CC 07 +[Text] ..p...4.....lX.. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 16:41:03.526 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 16:41:03.531 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:41:03.550 [error] Packet processing error: %MatchError{term: [[<<23, 0>>] | <<8, 0, 77, 97, 112, 76, 111, 103, 105, 110>>]} +2026-02-15 16:41:03.550 [error] Packet processing error: :processing_error +2026-02-15 16:41:04.732 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:41:34.732 [warning] Login client error: :timeout +2026-02-15 16:54:33.249 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 16:54:33.251 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 16:54:33.251 [info] Login Server: port=8584, limit=1500 +2026-02-15 16:54:33.251 [info] Game Channels: count=2 +2026-02-15 16:54:33.251 [info] Cash Shop: port=8605 +2026-02-15 16:54:33.373 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 16:54:33.373 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 16:54:33.378 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 16:54:33.378 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 16:54:33.379 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 16:54:33.384 [info] Loaded 4 map templates +2026-02-15 16:54:33.386 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 16:54:33.386 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 16:54:33.387 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 16:54:33.388 [info] Drop table cache initialized +2026-02-15 16:54:33.389 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 16:54:33.389 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 16:54:33.390 [info] Loaded 9 skills +2026-02-15 16:54:33.390 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 16:54:33.392 [info] Created 7 fallback reactor templates +2026-02-15 16:54:33.392 [info] Loaded 7 reactor templates +2026-02-15 16:54:33.392 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 16:54:33.392 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 16:54:33.393 [info] Loaded 5 quest definitions +2026-02-15 16:54:33.393 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 16:54:33.395 [info] Loaded 3 cash shop items +2026-02-15 16:54:33.407 [info] Script Manager initialized +2026-02-15 16:54:33.407 [info] NPC Script Manager initialized +2026-02-15 16:54:33.407 [info] Portal Script Manager initialized +2026-02-15 16:54:33.407 [info] Reactor Script Manager initialized +2026-02-15 16:54:33.407 [info] Event Script Manager initialized +2026-02-15 16:54:33.426 [info] World state initialized +2026-02-15 16:54:33.426 [info] Party service initialized +2026-02-15 16:54:33.426 [info] Guild service initialized with 0 guilds +2026-02-15 16:54:33.426 [info] Family service initialized with 0 families +2026-02-15 16:54:33.428 [info] Login server listening on port 8584 +2026-02-15 16:54:33.428 [info] Starting 2 game channels +2026-02-15 16:54:33.439 [info] Channel 1 listening on port 8585 +2026-02-15 16:54:33.450 [info] Channel 2 listening on port 8586 +2026-02-15 16:54:33.461 [info] Cash shop server listening on port 8605 +2026-02-15 16:54:48.493 [info] Login client connected from 127.0.0.1 +2026-02-15 16:54:48.547 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 B2 92 C5 36 E3 23 56 4A 07 +[Text] ..p...4...6.#VJ. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 16:54:49.030 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 16:54:49.035 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:54:49.037 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 16:54:49.037 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 00 00 +[Text] .... +[Context] IP=127.0.0.1 Server=login Size=4 bytes + +2026-02-15 16:54:50.295 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:54:50.295 [info] Login client disconnected: 127.0.0.1 +2026-02-15 16:58:28.611 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 16:58:28.613 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 16:58:28.613 [info] Login Server: port=8584, limit=1500 +2026-02-15 16:58:28.613 [info] Game Channels: count=2 +2026-02-15 16:58:28.613 [info] Cash Shop: port=8605 +2026-02-15 16:58:28.733 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 16:58:28.733 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 16:58:28.738 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 16:58:28.738 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 16:58:28.739 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 16:58:28.743 [info] Loaded 4 map templates +2026-02-15 16:58:28.745 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 16:58:28.745 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 16:58:28.745 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 16:58:28.746 [info] Drop table cache initialized +2026-02-15 16:58:28.748 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 16:58:28.748 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 16:58:28.749 [info] Loaded 9 skills +2026-02-15 16:58:28.749 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 16:58:28.751 [info] Created 7 fallback reactor templates +2026-02-15 16:58:28.751 [info] Loaded 7 reactor templates +2026-02-15 16:58:28.751 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 16:58:28.751 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 16:58:28.751 [info] Loaded 5 quest definitions +2026-02-15 16:58:28.751 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 16:58:28.754 [info] Loaded 3 cash shop items +2026-02-15 16:58:28.765 [info] Script Manager initialized +2026-02-15 16:58:28.766 [info] NPC Script Manager initialized +2026-02-15 16:58:28.766 [info] Portal Script Manager initialized +2026-02-15 16:58:28.766 [info] Reactor Script Manager initialized +2026-02-15 16:58:28.766 [info] Event Script Manager initialized +2026-02-15 16:58:28.787 [info] World state initialized +2026-02-15 16:58:28.787 [info] Party service initialized +2026-02-15 16:58:28.787 [info] Guild service initialized with 0 guilds +2026-02-15 16:58:28.788 [info] Family service initialized with 0 families +2026-02-15 16:58:28.789 [info] Login server listening on port 8584 +2026-02-15 16:58:28.789 [info] Starting 2 game channels +2026-02-15 16:58:28.800 [info] Channel 1 listening on port 8585 +2026-02-15 16:58:28.811 [info] Channel 2 listening on port 8586 +2026-02-15 16:58:28.822 [info] Cash shop server listening on port 8605 +2026-02-15 16:58:41.997 [info] Login client connected from 127.0.0.1 +2026-02-15 16:58:42.045 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 A0 75 C2 D1 5F FC 2B A5 07 +[Text] ..p...4.u.._.+.. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 16:58:42.499 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 16:58:42.503 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:58:42.506 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 16:58:42.506 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 16:58:43.702 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 16:58:43.702 [info] Login client disconnected: 127.0.0.1 +2026-02-15 17:24:49.994 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 17:24:49.997 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 17:24:49.997 [info] Login Server: port=8584, limit=1500 +2026-02-15 17:24:49.997 [info] Game Channels: count=2 +2026-02-15 17:24:49.997 [info] Cash Shop: port=8605 +2026-02-15 17:24:50.117 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 17:24:50.117 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 17:24:50.121 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 17:24:50.121 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 17:24:50.123 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 17:24:50.126 [info] Loaded 4 map templates +2026-02-15 17:24:50.128 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 17:24:50.128 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 17:24:50.128 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 17:24:50.129 [info] Drop table cache initialized +2026-02-15 17:24:50.131 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 17:24:50.131 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 17:24:50.131 [info] Loaded 9 skills +2026-02-15 17:24:50.132 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 17:24:50.134 [info] Created 7 fallback reactor templates +2026-02-15 17:24:50.134 [info] Loaded 7 reactor templates +2026-02-15 17:24:50.136 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 17:24:50.136 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 17:24:50.136 [info] Loaded 5 quest definitions +2026-02-15 17:24:50.136 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 17:24:50.139 [info] Loaded 3 cash shop items +2026-02-15 17:24:50.151 [info] Script Manager initialized +2026-02-15 17:24:50.152 [info] NPC Script Manager initialized +2026-02-15 17:24:50.152 [info] Portal Script Manager initialized +2026-02-15 17:24:50.152 [info] Reactor Script Manager initialized +2026-02-15 17:24:50.153 [info] Event Script Manager initialized +2026-02-15 17:24:50.174 [info] World state initialized +2026-02-15 17:24:50.174 [info] Party service initialized +2026-02-15 17:24:50.174 [info] Guild service initialized with 0 guilds +2026-02-15 17:24:50.174 [info] Family service initialized with 0 families +2026-02-15 17:24:50.176 [info] Login server listening on port 8584 +2026-02-15 17:24:50.176 [info] Starting 2 game channels +2026-02-15 17:24:50.187 [info] Channel 1 listening on port 8585 +2026-02-15 17:24:50.198 [info] Channel 2 listening on port 8586 +2026-02-15 17:24:50.209 [info] Cash shop server listening on port 8605 +2026-02-15 17:25:05.894 [info] Login client connected from 127.0.0.1 +2026-02-15 17:25:05.905 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 68 72 2F F3 B0 DB 01 88 07 +[Text] ..p...4hr/...... +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 17:25:06.403 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 17:25:06.408 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 17:25:06.412 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 17:25:06.412 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 17:25:07.604 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 17:25:07.604 [info] Login client disconnected: 127.0.0.1 +2026-02-15 18:13:52.676 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 18:13:52.679 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 18:13:52.679 [info] Login Server: port=8584, limit=1500 +2026-02-15 18:13:52.679 [info] Game Channels: count=2 +2026-02-15 18:13:52.679 [info] Cash Shop: port=8605 +2026-02-15 18:13:52.802 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 18:13:52.803 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 18:13:52.807 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 18:13:52.807 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 18:13:52.809 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 18:13:52.814 [info] Loaded 4 map templates +2026-02-15 18:13:52.818 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 18:13:52.818 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 18:13:52.818 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 18:13:52.818 [info] Drop table cache initialized +2026-02-15 18:13:52.819 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 18:13:52.819 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 18:13:52.819 [info] Loaded 9 skills +2026-02-15 18:13:52.819 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 18:13:52.822 [info] Created 7 fallback reactor templates +2026-02-15 18:13:52.822 [info] Loaded 7 reactor templates +2026-02-15 18:13:52.822 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 18:13:52.822 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 18:13:52.822 [info] Loaded 5 quest definitions +2026-02-15 18:13:52.822 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 18:13:52.825 [info] Loaded 3 cash shop items +2026-02-15 18:13:52.839 [info] Script Manager initialized +2026-02-15 18:13:52.839 [info] NPC Script Manager initialized +2026-02-15 18:13:52.839 [info] Portal Script Manager initialized +2026-02-15 18:13:52.839 [info] Reactor Script Manager initialized +2026-02-15 18:13:52.840 [info] Event Script Manager initialized +2026-02-15 18:13:52.862 [info] World state initialized +2026-02-15 18:13:52.863 [info] Party service initialized +2026-02-15 18:13:52.863 [info] Guild service initialized with 0 guilds +2026-02-15 18:13:52.863 [info] Family service initialized with 0 families +2026-02-15 18:13:52.864 [info] Login server listening on port 8584 +2026-02-15 18:13:52.864 [info] Starting 2 game channels +2026-02-15 18:13:52.875 [info] Channel 1 listening on port 8585 +2026-02-15 18:13:52.886 [info] Channel 2 listening on port 8586 +2026-02-15 18:13:52.897 [info] Cash shop server listening on port 8605 +2026-02-15 18:33:42.629 [info] Login client connected from 127.0.0.1 +2026-02-15 18:33:42.678 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 39 E8 36 92 98 E0 9C 08 07 +[Text] ..p...49.6...... +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 18:33:43.140 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 18:33:43.146 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 18:33:43.149 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 18:33:43.149 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 18:33:44.341 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 18:33:44.342 [info] Login client disconnected: 127.0.0.1 +2026-02-15 18:53:43.907 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 18:53:43.909 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 18:53:43.909 [info] Login Server: port=8584, limit=1500 +2026-02-15 18:53:43.909 [info] Game Channels: count=2 +2026-02-15 18:53:43.909 [info] Cash Shop: port=8605 +2026-02-15 18:53:44.034 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 18:53:44.034 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 18:53:44.039 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 18:53:44.039 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 18:53:44.042 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 18:53:44.046 [info] Loaded 4 map templates +2026-02-15 18:53:44.047 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 18:53:44.047 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 18:53:44.047 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 18:53:44.048 [info] Drop table cache initialized +2026-02-15 18:53:44.049 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 18:53:44.049 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 18:53:44.049 [info] Loaded 9 skills +2026-02-15 18:53:44.050 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 18:53:44.052 [info] Created 7 fallback reactor templates +2026-02-15 18:53:44.052 [info] Loaded 7 reactor templates +2026-02-15 18:53:44.052 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 18:53:44.052 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 18:53:44.052 [info] Loaded 5 quest definitions +2026-02-15 18:53:44.052 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 18:53:44.054 [info] Loaded 3 cash shop items +2026-02-15 18:53:44.067 [info] Script Manager initialized +2026-02-15 18:53:44.067 [info] NPC Script Manager initialized +2026-02-15 18:53:44.067 [info] Portal Script Manager initialized +2026-02-15 18:53:44.067 [info] Reactor Script Manager initialized +2026-02-15 18:53:44.068 [info] Event Script Manager initialized +2026-02-15 18:53:44.088 [info] World state initialized +2026-02-15 18:53:44.088 [info] Party service initialized +2026-02-15 18:53:44.088 [info] Guild service initialized with 0 guilds +2026-02-15 18:53:44.088 [info] Family service initialized with 0 families +2026-02-15 18:53:44.090 [info] Login server listening on port 8584 +2026-02-15 18:53:44.090 [info] Starting 2 game channels +2026-02-15 18:53:44.101 [info] Channel 1 listening on port 8585 +2026-02-15 18:53:44.112 [info] Channel 2 listening on port 8586 +2026-02-15 18:53:44.123 [info] Cash shop server listening on port 8605 +2026-02-15 18:54:03.941 [info] Login client connected from 127.0.0.1 +2026-02-15 18:54:03.993 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 32 57 F7 45 83 8E 7E C9 07 +[Text] ..p...42W.E..~.. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 18:54:04.450 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 18:54:04.456 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 18:54:04.459 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 18:54:04.459 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 18:54:05.655 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 18:54:35.662 [warning] Login client error: :timeout +2026-02-15 20:08:02.461 [info] Login client connected from 127.0.0.1 +2026-02-15 20:08:02.473 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 90 91 91 09 01 2E 48 BD 07 +[Text] ..p...4......H.. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 20:08:02.962 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 20:08:02.962 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:08:02.962 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 20:08:02.963 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 20:08:04.179 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:08:10.814 [info] [client] [CP_CheckPassword] 2 / 0x02 +[Data] 02 00 08 00 61 64 6D 69 6E 34 32 30 08 00 70 61 73 73 77 6F 72 64 15 00 30 38 42 46 42 38 31 33 39 41 33 46 5F 39 32 31 30 36 33 32 30 00 00 00 00 00 00 92 10 63 20 00 00 00 00 21 46 00 00 00 00 02 00 +[Text] ....admin420..password..08BFB8139A3F_92106320........c ....!F...... +[Context] IP=127.0.0.1 Server=login Size=67 bytes + +2026-02-15 20:08:10.814 [info] Login attempt: username=admin420 from 127.0.0.1 +2026-02-15 20:08:10.885 [error] Packet processing error: %KeyError{key: :mac, term: %Odinsea.Login.Client{socket: #Port<0.44>, ip: "127.0.0.1", state: :connected, account_id: nil, account_name: nil, character_id: nil, world: nil, channel: nil, logged_in: false, login_attempts: 0, second_password: nil, gender: 0, is_gm: false, hardware_info: nil, crypto: %Odinsea.Net.Cipher.ClientCrypto{version: 112, use_custom_crypt: false, send_iv: <<180, 103, 213, 162>>, send_iv_old: "tGOj", recv_iv: <<47, 71, 22, 200>>, recv_iv_old: <<108, 185, 56, 176>>}, handshake_complete: true, created_at: 1771211282473, last_alive_ack: 1771211282473, server_transition: false, macs: [], character_slots: 3, birthday: nil, monitored: false, tempban: nil, chat_mute: false, buffer: "", character_ids: []}, message: nil} +2026-02-15 20:08:10.885 [error] Packet processing error: :processing_error +2026-02-15 20:08:40.886 [warning] Login client error: :timeout +2026-02-15 20:13:16.125 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 20:13:16.127 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 20:13:16.128 [info] Login Server: port=8584, limit=1500 +2026-02-15 20:13:16.128 [info] Game Channels: count=2 +2026-02-15 20:13:16.128 [info] Cash Shop: port=8605 +2026-02-15 20:13:16.254 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 20:13:16.254 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 20:13:16.260 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 20:13:16.260 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 20:13:16.260 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 20:13:16.265 [info] Loaded 4 map templates +2026-02-15 20:13:16.265 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 20:13:16.265 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 20:13:16.265 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 20:13:16.267 [info] Drop table cache initialized +2026-02-15 20:13:16.268 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 20:13:16.268 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 20:13:16.269 [info] Loaded 9 skills +2026-02-15 20:13:16.269 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 20:13:16.271 [info] Created 7 fallback reactor templates +2026-02-15 20:13:16.271 [info] Loaded 7 reactor templates +2026-02-15 20:13:16.271 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 20:13:16.272 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 20:13:16.272 [info] Loaded 5 quest definitions +2026-02-15 20:13:16.272 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 20:13:16.275 [info] Loaded 3 cash shop items +2026-02-15 20:13:16.288 [info] Script Manager initialized +2026-02-15 20:13:16.288 [info] NPC Script Manager initialized +2026-02-15 20:13:16.288 [info] Portal Script Manager initialized +2026-02-15 20:13:16.288 [info] Reactor Script Manager initialized +2026-02-15 20:13:16.289 [info] Event Script Manager initialized +2026-02-15 20:13:16.309 [info] World state initialized +2026-02-15 20:13:16.310 [info] Party service initialized +2026-02-15 20:13:16.310 [info] Guild service initialized with 0 guilds +2026-02-15 20:13:16.310 [info] Family service initialized with 0 families +2026-02-15 20:13:16.311 [info] Login server listening on port 8584 +2026-02-15 20:13:16.311 [info] Starting 2 game channels +2026-02-15 20:13:16.322 [info] Channel 1 listening on port 8585 +2026-02-15 20:13:16.333 [info] Channel 2 listening on port 8586 +2026-02-15 20:13:16.344 [info] Cash shop server listening on port 8605 +2026-02-15 20:13:46.549 [info] Login client connected from 127.0.0.1 +2026-02-15 20:13:46.595 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 B4 EE 5B 37 8A 37 84 65 07 +[Text] ..p...4..[7.7.e. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 20:13:47.030 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 20:13:47.034 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:13:47.037 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 20:13:47.038 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 20:13:48.171 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:13:53.483 [info] [client] [CP_CheckPassword] 2 / 0x02 +[Data] 02 00 08 00 61 64 6D 69 6E 34 32 30 08 00 70 61 73 73 77 6F 72 64 15 00 30 38 42 46 42 38 31 33 39 41 33 46 5F 39 32 31 30 36 33 32 30 00 00 00 00 00 00 92 10 63 20 00 00 00 00 21 46 00 00 00 00 02 00 +[Text] ....admin420..password..08BFB8139A3F_92106320........c ....!F...... +[Context] IP=127.0.0.1 Server=login Size=67 bytes + +2026-02-15 20:13:53.483 [info] Login attempt: username=admin420 from 127.0.0.1 +2026-02-15 20:13:53.556 [error] Packet processing error: %MyXQL.Error{connection_id: 571, message: "Unknown column 'a0.2ndpassword' in 'SELECT'", mysql: %{code: 1054, name: nil}, statement: "SELECT a0.`id`, a0.`name`, a0.`password`, a0.`salt`, a0.`2ndpassword`, a0.`salt2`, a0.`loggedin`, a0.`lastlogin`, a0.`createdat`, a0.`birthday`, a0.`banned`, a0.`banreason`, a0.`gm`, a0.`email`, a0.`macs`, a0.`tempban`, a0.`greason`, a0.`ACash`, a0.`mPoints`, a0.`gender`, a0.`SessionIP`, a0.`points`, a0.`vpoints`, a0.`totalvotes`, a0.`lastlogon`, a0.`lastvoteip` FROM `accounts` AS a0 WHERE (a0.`name` = ?)"} +2026-02-15 20:13:53.556 [error] Packet processing error: :processing_error +2026-02-15 20:14:23.557 [warning] Login client error: :timeout +2026-02-15 20:15:10.612 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 20:15:10.614 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 20:15:10.614 [info] Login Server: port=8584, limit=1500 +2026-02-15 20:15:10.614 [info] Game Channels: count=2 +2026-02-15 20:15:10.614 [info] Cash Shop: port=8605 +2026-02-15 20:15:10.750 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 20:15:10.750 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 20:15:10.757 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 20:15:10.757 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 20:15:10.757 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 20:15:10.762 [info] Loaded 4 map templates +2026-02-15 20:15:10.763 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 20:15:10.763 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 20:15:10.763 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 20:15:10.764 [info] Drop table cache initialized +2026-02-15 20:15:10.765 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 20:15:10.765 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 20:15:10.765 [info] Loaded 9 skills +2026-02-15 20:15:10.768 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 20:15:10.771 [info] Created 7 fallback reactor templates +2026-02-15 20:15:10.771 [info] Loaded 7 reactor templates +2026-02-15 20:15:10.773 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 20:15:10.773 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 20:15:10.773 [info] Loaded 5 quest definitions +2026-02-15 20:15:10.774 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 20:15:10.776 [info] Loaded 3 cash shop items +2026-02-15 20:15:10.789 [info] Script Manager initialized +2026-02-15 20:15:10.789 [info] NPC Script Manager initialized +2026-02-15 20:15:10.789 [info] Portal Script Manager initialized +2026-02-15 20:15:10.789 [info] Reactor Script Manager initialized +2026-02-15 20:15:10.790 [info] Event Script Manager initialized +2026-02-15 20:15:10.812 [info] World state initialized +2026-02-15 20:15:10.812 [info] Party service initialized +2026-02-15 20:15:10.812 [info] Guild service initialized with 0 guilds +2026-02-15 20:15:10.812 [info] Family service initialized with 0 families +2026-02-15 20:15:10.814 [info] Login server listening on port 8584 +2026-02-15 20:15:10.814 [info] Starting 2 game channels +2026-02-15 20:15:10.818 [info] Channel 1 listening on port 8585 +2026-02-15 20:15:10.829 [info] Channel 2 listening on port 8586 +2026-02-15 20:15:10.840 [info] Cash shop server listening on port 8605 +2026-02-15 20:16:21.997 [info] Login client connected from 127.0.0.1 +2026-02-15 20:16:22.046 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 72 69 E0 31 25 FF 86 91 07 +[Text] ..p...4ri.1%.... +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 20:16:22.518 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 20:16:22.526 [info] [client] [CP_SecurityPacket] 24 / 0x18 +[Data] 18 00 01 9A 36 BA 1F 00 00 00 00 +[Text] ....6...... +[Context] IP=127.0.0.1 Server=login Size=11 bytes + +2026-02-15 20:16:22.526 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:16:22.531 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 20:16:22.532 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 20:16:23.711 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:16:28.903 [info] [client] [CP_CheckPassword] 2 / 0x02 +[Data] 02 00 08 00 61 64 6D 69 6E 34 32 30 08 00 70 61 73 73 77 6F 72 64 15 00 30 38 42 46 42 38 31 33 39 41 33 46 5F 39 32 31 30 36 33 32 30 00 00 00 00 00 00 92 10 63 20 00 00 00 00 21 46 00 00 00 00 02 00 +[Text] ....admin420..password..08BFB8139A3F_92106320........c ....!F...... +[Context] IP=127.0.0.1 Server=login Size=67 bytes + +2026-02-15 20:16:28.903 [info] Login attempt: username=admin420 from 127.0.0.1 +2026-02-15 20:16:28.972 [error] Packet processing error: %MyXQL.Error{connection_id: 605, message: "Unknown column 'a0.second_salt' in 'SELECT'", mysql: %{code: 1054, name: nil}, statement: "SELECT a0.`id`, a0.`name`, a0.`password`, a0.`salt`, a0.`second_password`, a0.`second_salt`, a0.`loggedin`, a0.`lastlogin`, a0.`createdat`, a0.`birthday`, a0.`banned`, a0.`banreason`, a0.`gm`, a0.`email`, a0.`macs`, a0.`tempban`, a0.`greason`, a0.`ACash`, a0.`mPoints`, a0.`gender`, a0.`SessionIP`, a0.`points`, a0.`vpoints`, a0.`totalvotes`, a0.`lastlogon`, a0.`lastvoteip` FROM `accounts` AS a0 WHERE (a0.`name` = ?)"} +2026-02-15 20:16:28.972 [error] Packet processing error: :processing_error +2026-02-15 20:16:58.973 [warning] Login client error: :timeout +2026-02-15 20:17:15.404 [info] Login client connected from 127.0.0.1 +2026-02-15 20:17:15.404 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 5A 0F A5 8A 27 A8 A1 4D 07 +[Text] ..p...4Z...'..M. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 20:17:15.905 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 20:17:15.905 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:17:15.905 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 20:17:15.906 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 20:17:17.120 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:17:23.556 [info] [client] [CP_CheckPassword] 2 / 0x02 +[Data] 02 00 08 00 61 64 6D 69 6E 34 32 30 08 00 70 61 73 73 77 6F 72 64 15 00 30 38 42 46 42 38 31 33 39 41 33 46 5F 39 32 31 30 36 33 32 30 00 00 00 00 00 00 92 10 63 20 00 00 00 00 21 46 00 00 00 00 02 00 +[Text] ....admin420..password..08BFB8139A3F_92106320........c ....!F...... +[Context] IP=127.0.0.1 Server=login Size=67 bytes + +2026-02-15 20:17:23.556 [info] Login attempt: username=admin420 from 127.0.0.1 +2026-02-15 20:17:23.568 [error] Packet processing error: %ArgumentError{message: "cannot load `:zero_datetime` as type :naive_datetime for field :tempban in %Odinsea.Database.Schema.Account{__meta__: #Ecto.Schema.Metadata<:loaded, \"accounts\">, id: nil, name: nil, password: nil, salt: nil, second_password: nil, second_salt: nil, loggedin: 0, lastlogin: nil, createdat: nil, birthday: nil, banned: 0, banreason: nil, gm: 0, email: nil, macs: nil, tempban: nil, greason: nil, acash: 0, mpoints: 0, gender: 0, session_ip: nil, points: 0, vpoints: 0, totalvotes: 0, lastlogon: nil, lastvoteip: nil, characters: #Ecto.Association.NotLoaded}"} +2026-02-15 20:17:23.569 [error] Packet processing error: :processing_error +2026-02-15 20:17:53.569 [warning] Login client error: :timeout +2026-02-15 20:20:14.562 [info] Login client connected from 127.0.0.1 +2026-02-15 20:20:14.562 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 9C B7 DA 63 8A 2A 35 83 07 +[Text] ..p...4...c.*5.. +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 20:20:15.063 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 20:20:15.063 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:20:15.063 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 20:20:15.064 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 20:20:16.278 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:20:20.646 [info] [client] [CP_CheckPassword] 2 / 0x02 +[Data] 02 00 08 00 61 64 6D 69 6E 34 32 30 08 00 70 61 73 73 77 6F 72 64 15 00 30 38 42 46 42 38 31 33 39 41 33 46 5F 39 32 31 30 36 33 32 30 00 00 00 00 00 00 92 10 63 20 00 00 00 00 21 46 00 00 00 00 02 00 +[Text] ....admin420..password..08BFB8139A3F_92106320........c ....!F...... +[Context] IP=127.0.0.1 Server=login Size=67 bytes + +2026-02-15 20:20:20.646 [info] Login attempt: username=admin420 from 127.0.0.1 +2026-02-15 20:20:20.682 [info] [loopback] [LOGIN_STATUS] 1 / 0x01 +[Data] 01 00 00 04 00 00 00 00 01 01 08 00 61 64 6D 69 6E 34 32 30 03 00 00 00 00 00 00 00 00 00 00 00 77 25 35 01 +[Text] ............admin420............w%5. +[Context] IP=127.0.0.1 Server=login Size=36 bytes + +2026-02-15 20:20:50.683 [warning] Login client error: :timeout +2026-02-15 20:26:23.429 [info] Starting Odinsea Server v0.1.0-1 +2026-02-15 20:26:23.431 [info] Server Configuration: + Name: Luna + Host: 127.0.0.1 + Revision: 1 + EXP Rate: 5x + Meso Rate: 3x + +2026-02-15 20:26:23.431 [info] Login Server: port=8584, limit=1500 +2026-02-15 20:26:23.431 [info] Game Channels: count=2 +2026-02-15 20:26:23.431 [info] Cash Shop: port=8605 +2026-02-15 20:26:23.556 [warning] Item strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/item_strings.json, using fallback data +2026-02-15 20:26:23.556 [warning] Items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/items.json, using fallback data +2026-02-15 20:26:23.561 [warning] Equips file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/equips.json +2026-02-15 20:26:23.561 [info] Loaded 5 items and 0 equipment definitions +2026-02-15 20:26:23.564 [warning] Maps file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/maps.json, using fallback data +2026-02-15 20:26:23.568 [info] Loaded 4 map templates +2026-02-15 20:26:23.568 [warning] Monsters file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/monsters.json, using fallback data +2026-02-15 20:26:23.568 [warning] NPCs file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/npcs.json, using fallback data +2026-02-15 20:26:23.568 [info] Loaded 4 monsters and 6 NPCs +2026-02-15 20:26:23.569 [info] Drop table cache initialized +2026-02-15 20:26:23.570 [warning] Skill strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skill_strings.json, using fallback data +2026-02-15 20:26:23.571 [warning] Skills file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/skills.json, using fallback data +2026-02-15 20:26:23.571 [info] Loaded 9 skills +2026-02-15 20:26:23.572 [warning] Reactors file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/reactors.json, using fallback data +2026-02-15 20:26:23.575 [info] Created 7 fallback reactor templates +2026-02-15 20:26:23.575 [info] Loaded 7 reactor templates +2026-02-15 20:26:23.575 [warning] Quest strings file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quest_strings.json, using fallback data +2026-02-15 20:26:23.575 [warning] Quests file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/quests.json, using fallback data +2026-02-15 20:26:23.575 [info] Loaded 5 quest definitions +2026-02-15 20:26:23.575 [warning] Cash items file not found: /home/ra/odinsea-elixir/_build/dev/lib/odinsea/priv/data/cash_items.json, using fallback data +2026-02-15 20:26:23.577 [info] Loaded 3 cash shop items +2026-02-15 20:26:23.590 [info] Script Manager initialized +2026-02-15 20:26:23.590 [info] NPC Script Manager initialized +2026-02-15 20:26:23.590 [info] Portal Script Manager initialized +2026-02-15 20:26:23.590 [info] Reactor Script Manager initialized +2026-02-15 20:26:23.590 [info] Event Script Manager initialized +2026-02-15 20:26:23.612 [info] World state initialized +2026-02-15 20:26:23.612 [info] Party service initialized +2026-02-15 20:26:23.612 [info] Guild service initialized with 0 guilds +2026-02-15 20:26:23.612 [info] Family service initialized with 0 families +2026-02-15 20:26:23.614 [info] Login server listening on port 8584 +2026-02-15 20:26:23.614 [info] Starting 2 game channels +2026-02-15 20:26:23.625 [info] Channel 1 listening on port 8585 +2026-02-15 20:26:23.636 [info] Channel 2 listening on port 8586 +2026-02-15 20:26:23.647 [info] Cash shop server listening on port 8605 +2026-02-15 20:26:38.994 [info] Login client connected from 127.0.0.1 +2026-02-15 20:26:39.040 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 F9 62 2A 3F D1 68 87 8F 07 +[Text] ..p...4.b*?.h... +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 20:26:39.503 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 20:26:39.508 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:26:39.511 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 20:26:39.512 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 20:26:40.703 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:26:44.604 [info] [client] [CP_CheckPassword] 2 / 0x02 +[Data] 02 00 08 00 61 64 6D 69 6E 34 32 30 08 00 70 61 73 73 77 6F 72 64 15 00 30 38 42 46 42 38 31 33 39 41 33 46 5F 39 32 31 30 36 33 32 30 00 00 00 00 00 00 92 10 63 20 00 00 00 00 21 46 00 00 00 00 02 00 +[Text] ....admin420..password..08BFB8139A3F_92106320........c ....!F...... +[Context] IP=127.0.0.1 Server=login Size=67 bytes + +2026-02-15 20:26:44.604 [info] Login attempt: username=admin420 from 127.0.0.1 +2026-02-15 20:26:44.662 [warning] Account already logged in: admin420 +2026-02-15 20:26:44.662 [warning] Already logged in, attempting to kick session for: admin420 +2026-02-15 20:26:44.782 [info] [loopback] [LOGIN_STATUS] 1 / 0x01 +[Data] 01 00 07 00 00 00 00 00 +[Text] ........ +[Context] IP=127.0.0.1 Server=login Size=8 bytes + +2026-02-15 20:27:14.786 [warning] Login client error: :timeout +2026-02-15 20:32:00.633 [info] Login client connected from 127.0.0.1 +2026-02-15 20:32:00.633 [info] [loopback] [HELLO] +[Data] 0E 00 70 00 01 00 34 A4 1C D1 DC 8C D8 80 CD 07 +[Text] ..p...4......... +[Context] IP=127.0.0.1 Size=16 bytes + +2026-02-15 20:32:01.134 [info] [client] [CP_PermissionRequest] 1 / 0x01 +[Data] 01 00 07 70 00 04 00 +[Text] ...p... +[Context] IP=127.0.0.1 Server=login Size=7 bytes + +2026-02-15 20:32:01.134 [info] [client] [RSA_KEY] 32 / 0x20 +[Data] 20 00 +[Text] . +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:32:01.134 [info] [loopback] [LOGIN_AUTH] 23 / 0x17 +[Data] 17 00 08 00 4D 61 70 4C 6F 67 69 6E +[Text] ....MapLogin +[Context] IP=127.0.0.1 Server=login Size=12 bytes + +2026-02-15 20:32:01.134 [info] [loopback] [RSA_KEY] 19 / 0x13 +[Data] 13 00 44 01 33 30 38 31 39 46 33 30 30 44 30 36 30 39 32 41 38 36 34 38 38 36 46 37 30 44 30 31 30 31 30 31 30 35 30 30 30 33 38 31 38 44 30 30 33 30 38 31 38 39 30 32 38 31 38 31 30 30 39 39 34 46 34 45 36 36 42 30 30 33 41 37 38 34 33 43 39 34 34 45 36 37 42 45 34 33 37 35 32 30 33 44 41 41 32 30 33 43 36 37 36 39 30 38 45 35 39 38 33 39 43 39 42 41 44 45 39 35 46 35 33 45 38 34 38 41 41 46 45 36 31 44 42 39 43 30 39 45 38 30 46 34 38 36 37 35 43 41 32 36 39 36 46 34 45 38 39 37 42 37 46 31 38 43 43 42 36 33 39 38 44 32 32 31 43 34 45 43 35 38 32 33 44 31 31 43 41 31 46 42 39 37 36 34 41 37 38 46 38 34 37 31 31 42 38 42 36 46 43 41 39 46 30 31 42 31 37 31 41 35 31 45 43 36 36 43 30 32 43 44 41 39 33 30 38 38 38 37 43 45 45 38 45 35 39 43 34 46 46 30 42 31 34 36 42 46 37 31 46 36 39 37 45 42 31 31 45 44 43 45 42 46 43 45 30 32 46 42 30 31 30 31 41 37 30 37 36 41 33 46 45 42 36 34 46 36 46 36 30 32 32 43 38 34 31 37 45 42 36 42 38 37 32 37 30 32 30 33 30 31 30 30 30 31 +[Text] ..D.30819F300D06092A864886F70D010101050003818D0030818902818100994F4E66B003A7843C944E67BE4375203DAA203C676908E59839C9BADE95F53E848AAFE61DB9C09E80F48675CA2696F4E897B7F18CCB6398D221C4EC5823D11CA1FB9764A78F84711B8B6FCA9F01B171A51EC66C02CDA9308887CEE8E59C4FF0B146BF71F697EB11EDCEBFCE02FB0101A7076A3FEB64F6F6022C8417EB6B87270203010001 +[Context] IP=127.0.0.1 Server=login Size=328 bytes + +2026-02-15 20:32:02.383 [info] [client] [CP_CreateSecurityHandle] 30 / 0x1E +[Data] 1E 00 +[Text] .. +[Context] IP=127.0.0.1 Server=login Size=2 bytes + +2026-02-15 20:32:06.367 [info] [client] [CP_CheckPassword] 2 / 0x02 +[Data] 02 00 08 00 61 64 6D 69 6E 34 32 30 08 00 70 61 73 73 77 6F 72 64 15 00 30 38 42 46 42 38 31 33 39 41 33 46 5F 39 32 31 30 36 33 32 30 00 00 00 00 00 00 92 10 63 20 00 00 00 00 21 46 00 00 00 00 02 00 +[Text] ....admin420..password..08BFB8139A3F_92106320........c ....!F...... +[Context] IP=127.0.0.1 Server=login Size=67 bytes + +2026-02-15 20:32:06.367 [info] Login attempt: username=admin420 from 127.0.0.1 +2026-02-15 20:32:06.397 [info] [loopback] [LOGIN_STATUS] 1 / 0x01 +[Data] 01 00 00 04 00 00 00 00 01 01 08 00 61 64 6D 69 6E 34 32 30 03 00 00 00 00 00 00 00 00 00 00 00 77 25 35 01 +[Text] ............admin420............w%5. +[Context] IP=127.0.0.1 Server=login Size=36 bytes + +2026-02-15 20:32:06.397 [info] [loopback] [SERVERLIST] 6 / 0x06 +[Data] 06 00 00 07 00 4F 64 69 6E 73 65 61 00 13 00 57 65 6C 63 6F 6D 65 20 74 6F 20 4F 64 69 6E 73 65 61 21 64 00 64 00 03 09 00 4F 64 69 6E 73 65 61 2D 31 64 00 00 00 00 00 00 09 00 4F 64 69 6E 73 65 61 2D 32 C8 00 00 00 00 01 00 09 00 4F 64 69 6E 73 65 61 2D 33 96 00 00 00 00 02 00 00 00 00 00 00 00 +[Text] .....Odinsea...Welcome to Odinsea!d.d....Odinsea-1d........Odinsea-2.........Odinsea-3............. +[Context] IP=127.0.0.1 Server=login Size=99 bytes + +2026-02-15 20:32:06.397 [info] [loopback] [SERVERLIST] 6 / 0x06 +[Data] 06 00 FF +[Text] ... +[Context] IP=127.0.0.1 Server=login Size=3 bytes + +2026-02-15 20:32:06.397 [info] [loopback] [LP_SetCashShopOpened] 21 / 0x15 +[Data] 15 00 00 00 00 00 +[Text] ...... +[Context] IP=127.0.0.1 Server=login Size=6 bytes + +2026-02-15 20:32:06.397 [info] [loopback] [LP_MigrateCommand] 22 / 0x16 +[Data] 16 00 01 00 00 00 00 09 00 4A 6F 69 6E 20 6E 6F 77 21 +[Text] .........Join now! +[Context] IP=127.0.0.1 Server=login Size=18 bytes + +2026-02-15 20:32:08.094 [info] [client] [CP_CheckUserLimit] 6 / 0x06 +[Data] 06 00 00 00 +[Text] .... +[Context] IP=127.0.0.1 Server=login Size=4 bytes + +2026-02-15 20:32:38.094 [warning] Login client error: :timeout diff --git a/priv/repo/migrations/20260215000001_create_base_tables.exs b/priv/repo/migrations/20260215000001_create_base_tables.exs index 64cc04a..823c266 100644 --- a/priv/repo/migrations/20260215000001_create_base_tables.exs +++ b/priv/repo/migrations/20260215000001_create_base_tables.exs @@ -5,13 +5,13 @@ defmodule Odinsea.Repo.Migrations.CreateBaseTables do # ============================================================================ # CORE ACCOUNT TABLES # ============================================================================ - + create table(:accounts) do add :name, :string, size: 13, null: false add :password, :string, size: 128, null: false, default: "" add :salt, :string, size: 32 - add :secondpassword, :string, size: 134 - add :salt2, :string, size: 32 + add :second_password, :string, size: 134 + add :second_salt, :string, size: 32 add :loggedin, :boolean, null: false, default: false add :lastlogin, :naive_datetime add :birthday, :date, null: false, default: "0000-01-01" diff --git a/test/crypto_simple_test.exs b/test/crypto_simple_test.exs new file mode 100644 index 0000000..5631cbb --- /dev/null +++ b/test/crypto_simple_test.exs @@ -0,0 +1,60 @@ +defmodule Odinsea.CryptoSimpleTest do + @moduledoc """ + Simple test module for MapleStory crypto implementation. + These tests don't require the full application to start. + """ + + use ExUnit.Case + + alias Odinsea.Net.Cipher.{IGCipher, ShandaCipher, AESCipher} + + describe "IGCipher (IV transformation)" do + test "inno_hash transforms IV correctly" do + iv = <<0x34, 0x9A, 0x0F, 0x0C>> + result = IGCipher.inno_hash(iv) + + # Result should be 4 bytes + assert byte_size(result) == 4 + + # Result should be different from input (usually) + assert result != iv + end + + test "inno_hash produces deterministic results" do + iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + result1 = IGCipher.inno_hash(iv) + result2 = IGCipher.inno_hash(iv) + + assert result1 == result2 + end + end + + describe "ShandaCipher" do + test "encrypt and decrypt are inverse operations" do + original = <<1, 0, 7, 112, 0, 4, 0>> # CP_PermissionRequest payload + encrypted = ShandaCipher.encrypt(original) + decrypted = ShandaCipher.decrypt(encrypted) + + assert decrypted == original + end + + test "encrypt produces different output" do + original = <<1, 0, 7, 112, 0, 4, 0>> + encrypted = ShandaCipher.encrypt(original) + + assert encrypted != original + end + end + + describe "AESCipher" do + test "crypt is self-inverse (XOR property)" do + data = <<1, 0, 7, 112, 0, 4, 0>> + iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + + encrypted = AESCipher.crypt(data, iv) + decrypted = AESCipher.crypt(encrypted, iv) + + assert decrypted == data + end + end +end diff --git a/test/crypto_test.exs b/test/crypto_test.exs new file mode 100644 index 0000000..23ebd9e --- /dev/null +++ b/test/crypto_test.exs @@ -0,0 +1,185 @@ +defmodule Odinsea.CryptoTest do + @moduledoc """ + Test module for MapleStory crypto implementation. + Run with: mix test test/crypto_test.exs + """ + + use ExUnit.Case + + alias Odinsea.Net.Cipher.{ClientCrypto, IGCipher, ShandaCipher, AESCipher} + alias Odinsea.Util.BitTools + + describe "IGCipher (IV transformation)" do + test "inno_hash transforms IV correctly" do + iv = <<0x34, 0x9A, 0x0F, 0x0C>> + result = IGCipher.inno_hash(iv) + + # Result should be 4 bytes + assert byte_size(result) == 4 + + # Result should be different from input (usually) + assert result != iv + end + + test "inno_hash produces deterministic results" do + iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + result1 = IGCipher.inno_hash(iv) + result2 = IGCipher.inno_hash(iv) + + assert result1 == result2 + end + end + + describe "ShandaCipher" do + test "encrypt and decrypt are inverse operations" do + original = <<1, 0, 7, 112, 0, 4, 0>> # CP_PermissionRequest payload + encrypted = ShandaCipher.encrypt(original) + decrypted = ShandaCipher.decrypt(encrypted) + + assert decrypted == original + end + + test "encrypt produces different output" do + original = <<1, 0, 7, 112, 0, 4, 0>> + encrypted = ShandaCipher.encrypt(original) + + assert encrypted != original + end + end + + describe "AESCipher" do + test "crypt is self-inverse (XOR property)" do + data = <<1, 0, 7, 112, 0, 4, 0>> + iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + + encrypted = AESCipher.crypt(data, iv) + decrypted = AESCipher.crypt(encrypted, iv) + + assert decrypted == data + end + end + + describe "ClientCrypto" do + test "new creates crypto with random IVs" do + crypto = ClientCrypto.new(112) + + assert byte_size(crypto.send_iv) == 4 + assert byte_size(crypto.recv_iv) == 4 + assert crypto.version == 112 + end + + test "new_from_ivs creates crypto with specified IVs" do + send_iv = <<0x34, 0x9A, 0x0F, 0x0C>> + recv_iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + + crypto = ClientCrypto.new_from_ivs(112, send_iv, recv_iv) + + assert crypto.send_iv == send_iv + assert crypto.recv_iv == recv_iv + end + + test "new_from_client_ivs swaps IVs correctly" do + # Client's perspective + client_send_iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + client_recv_iv = <<0x34, 0x9A, 0x0F, 0x0C>> + + # Server's perspective (should be swapped) + crypto = ClientCrypto.new_from_client_ivs(112, client_send_iv, client_recv_iv) + + # Server's send = client's recv + assert crypto.send_iv == client_recv_iv + # Server's recv = client's send + assert crypto.recv_iv == client_send_iv + end + + test "decrypt updates recv_iv" do + crypto = ClientCrypto.new(112) + original_recv_iv = crypto.recv_iv + + encrypted_data = <<0x7C, 0xA8, 0x7B, 0xA8, 0xBF, 0x0A, 0xCD, 0xDE>> + {new_crypto, _decrypted} = ClientCrypto.decrypt(crypto, encrypted_data) + + # IV should be updated after decryption + assert new_crypto.recv_iv != original_recv_iv + end + + test "encrypt updates send_iv" do + crypto = ClientCrypto.new(112) + original_send_iv = crypto.send_iv + + data = <<1, 0, 7, 112, 0, 4, 0>> + {new_crypto, _encrypted, _header} = ClientCrypto.encrypt(crypto, data) + + # IV should be updated after encryption + assert new_crypto.send_iv != original_send_iv + end + + test "header encoding produces 4 bytes" do + crypto = ClientCrypto.new(112) + data = <<1, 0, 7, 112, 0, 4, 0>> + + {_new_crypto, _encrypted, header} = ClientCrypto.encrypt(crypto, data) + + assert byte_size(header) == 4 + end + + test "header validation works correctly" do + crypto = ClientCrypto.new(112) + data = <<1, 0, 7, 112, 0, 4, 0>> + + {new_crypto, encrypted, header} = ClientCrypto.encrypt(crypto, data) + + # Extract raw_seq from header + <> = header + + # Validation should pass + assert ClientCrypto.decode_header_valid?(new_crypto, raw_seq) + end + + test "full encrypt/decrypt roundtrip" do + # Create crypto instances with same IVs + send_iv = <<0x34, 0x9A, 0x0F, 0x0C>> + recv_iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + + # Server's crypto (for sending) + server_crypto = ClientCrypto.new_from_ivs(112, send_iv, recv_iv) + + # Client's crypto would have swapped IVs + # For testing, we'll use the same crypto to verify roundtrip + original_data = <<1, 0, 7, 112, 0, 4, 0>> + + # Encrypt + {server_crypto_after, encrypted, header} = ClientCrypto.encrypt(server_crypto, original_data) + + # Decrypt (using recv_iv which should match server's send_iv after swap) + # For this test, we'll create a matching client crypto + client_crypto = ClientCrypto.new_from_ivs(112, recv_iv, send_iv) + {client_crypto_after, decrypted} = ClientCrypto.decrypt(client_crypto, encrypted) + + assert decrypted == original_data + end + end + + describe "BitTools" do + test "roll_left rotates bits correctly" do + # 0b11001101 = 205 + # rotate left by 3: 0b01101101 = 109 (wrapping around) + result = BitTools.roll_left(205, 3) + assert result == 109 + end + + test "roll_right rotates bits correctly" do + # Test that roll_right is inverse of roll_left + original = 205 + rotated = BitTools.roll_left(original, 3) + back = BitTools.roll_right(rotated, 3) + assert back == original + end + + test "multiply_bytes repeats correctly" do + input = <<1, 2, 3, 4>> + result = BitTools.multiply_bytes(input, 4, 2) + assert result == <<1, 2, 3, 4, 1, 2, 3, 4>> + end + end +end diff --git a/test/debug_crypto.exs b/test/debug_crypto.exs new file mode 100644 index 0000000..49d6929 --- /dev/null +++ b/test/debug_crypto.exs @@ -0,0 +1,80 @@ +# Debug script to trace crypto operations +# Run with: cd /home/ra/odinsea-elixir && elixir test/debug_crypto.exs + +Code.require_file("lib/odinsea/util/bit_tools.ex", ".") +Code.require_file("lib/odinsea/net/cipher/ig_cipher.ex", ".") +Code.require_file("lib/odinsea/net/cipher/aes_cipher.ex", ".") +Code.require_file("lib/odinsea/net/cipher/shanda_cipher.ex", ".") + +alias Odinsea.Net.Cipher.{IGCipher, AESCipher, ShandaCipher} +alias Odinsea.Util.BitTools + +import Bitwise + +IO.puts("=== Debugging Crypto ===\n") + +# From the packet log: +# Server's recv_iv = <<0xA8, 0xBC, 0x0D, 0xB3>> (client's send_iv) +recv_iv = <<0xA8, 0xBC, 0x0D, 0xB3>> + +# First encrypted packet from client: +# [Data] 7C A8 7B A8 BF 0A CD DE C7 71 AC +packet = <<0x7C, 0xA8, 0x7B, 0xA8, 0xBF, 0x0A, 0xCD, 0xDE, 0xC7, 0x71, 0xAC>> + +IO.puts("Raw packet: #{Base.encode16(packet)}") +IO.puts("Server recv_iv: #{Base.encode16(recv_iv)}") + +# Extract header +<> = packet +IO.puts("\nHeader:") +IO.puts(" raw_seq: 0x#{Integer.to_string(raw_seq, 16)} (#{raw_seq})") +IO.puts(" raw_len: 0x#{Integer.to_string(raw_len, 16)} (#{raw_len})") +IO.puts(" payload: #{Base.encode16(payload)} (#{byte_size(payload)} bytes)") + +# Validate header +<<_r0, _r1, r2, r3>> = recv_iv +enc_version = 112 +seq_base = ((r2 &&& 0xFF) ||| ((r3 <<< 8) &&& 0xFF00)) &&& 0xFFFF +calculated_version = Bitwise.bxor(raw_seq &&& 0xFFFF, seq_base) + +IO.puts("\nHeader validation:") +IO.puts(" r2: 0x#{Integer.to_string(r2, 16)} (#{r2})") +IO.puts(" r3: 0x#{Integer.to_string(r3, 16)} (#{r3})") +IO.puts(" seq_base: 0x#{Integer.to_string(seq_base, 16)} (#{seq_base})") +IO.puts(" calculated_version: #{calculated_version}") +IO.puts(" expected_version: #{enc_version}") +IO.puts(" valid: #{calculated_version == enc_version}") + +# Get packet length +packet_len = Bitwise.bxor(raw_seq, raw_len) &&& 0xFFFF +IO.puts("\nPacket length: #{packet_len}") + +# Decrypt with AES +IO.puts("\n=== AES Decryption ===") +aes_decrypted = AESCipher.crypt(payload, recv_iv) +IO.puts("After AES: #{Base.encode16(aes_decrypted)}") + +# Decrypt with Shanda +IO.puts("\n=== Shanda Decryption ===") +shanda_decrypted = ShandaCipher.decrypt(aes_decrypted) +IO.puts("After Shanda: #{Base.encode16(shanda_decrypted)}") + +# Expected opcode 0x01 = 1 +if byte_size(shanda_decrypted) >= 2 do + <> = shanda_decrypted + IO.puts("\n=== Result ===") + IO.puts("Opcode: 0x#{Integer.to_string(opcode, 16)} (#{opcode})") + IO.puts("Rest: #{Base.encode16(rest)}") + + if opcode == 1 do + IO.puts("\nāœ“ SUCCESS! This is CP_PermissionRequest (0x01)") + else + IO.puts("\nāœ— FAILED! Expected opcode 0x01, got 0x#{Integer.to_string(opcode, 16)}") + end +end + +# Update IV +new_recv_iv = IGCipher.inno_hash(recv_iv) +IO.puts("\nUpdated recv_iv: #{Base.encode16(new_recv_iv)}") + +IO.puts("\n=== Done ===") diff --git a/test/debug_crypto2.exs b/test/debug_crypto2.exs new file mode 100644 index 0000000..e32733a --- /dev/null +++ b/test/debug_crypto2.exs @@ -0,0 +1,101 @@ +# Debug script to trace crypto operations with CORRECT IV +# Run with: cd /home/ra/odinsea-elixir && elixir test/debug_crypto2.exs + +Code.require_file("lib/odinsea/util/bit_tools.ex", ".") +Code.require_file("lib/odinsea/net/cipher/ig_cipher.ex", ".") +Code.require_file("lib/odinsea/net/cipher/aes_cipher.ex", ".") +Code.require_file("lib/odinsea/net/cipher/shanda_cipher.ex", ".") + +alias Odinsea.Net.Cipher.{IGCipher, AESCipher, ShandaCipher} + +import Bitwise + +IO.puts("=== Debugging Crypto (Corrected IV) ===\n") + +# From the hello packet sent by server: +# [Data] 0E 00 70 00 01 00 34 9A 0F 0C A8 BC 0D B3 E6 07 +# Server sends: recv_iv=34 9A 0F 0C, send_iv=A8 BC 0D B3 +# +# Client SWAPS these: +# Client's send_iv = server's recv_iv = 34 9A 0F 0C +# Client's recv_iv = server's send_iv = A8 BC 0D B3 +# +# Client encrypts with its send_iv (34 9A 0F 0C) +# Server must decrypt with its recv_iv = 34 9A 0F 0C + +server_send_iv = <<0xA8, 0xBC, 0x0D, 0xB3>> # Server sends this, client uses as recv +server_recv_iv = <<0x34, 0x9A, 0x0F, 0x0C>> # Server sends this, client uses as send + +IO.puts("Server perspective:") +IO.puts(" server_send_iv: #{Base.encode16(server_send_iv)}") +IO.puts(" server_recv_iv: #{Base.encode16(server_recv_iv)}") + +IO.puts("\nClient perspective (after swapping):") +IO.puts(" client_send_iv = server_recv_iv: #{Base.encode16(server_recv_iv)}") +IO.puts(" client_recv_iv = server_send_iv: #{Base.encode16(server_send_iv)}") + +# First encrypted packet from client: +# [Data] 7C A8 7B A8 BF 0A CD DE C7 71 AC +packet = <<0x7C, 0xA8, 0x7B, 0xA8, 0xBF, 0x0A, 0xCD, 0xDE, 0xC7, 0x71, 0xAC>> + +IO.puts("\nRaw packet: #{Base.encode16(packet)}") + +# Server decrypts with server_recv_iv +recv_iv = server_recv_iv +IO.puts("Server decrypts with recv_iv: #{Base.encode16(recv_iv)}") + +# Extract header +<> = packet +IO.puts("\nHeader:") +IO.puts(" raw_seq: 0x#{Integer.to_string(raw_seq, 16)} (#{raw_seq})") +IO.puts(" raw_len: 0x#{Integer.to_string(raw_len, 16)} (#{raw_len})") +IO.puts(" payload: #{Base.encode16(payload)} (#{byte_size(payload)} bytes)") + +# Validate header +<<_r0, _r1, r2, r3>> = recv_iv +enc_version = 112 +seq_base = ((r2 &&& 0xFF) ||| ((r3 <<< 8) &&& 0xFF00)) &&& 0xffff +calculated_version = bxor(raw_seq &&& 0xFFFF, seq_base) + +IO.puts("\nHeader validation:") +IO.puts(" r2: 0x#{Integer.to_string(r2, 16)} (#{r2})") +IO.puts(" r3: 0x#{Integer.to_string(r3, 16)} (#{r3})") +IO.puts(" seq_base: 0x#{Integer.to_string(seq_base, 16)} (#{seq_base})") +IO.puts(" raw_seq ^ seq_base: #{calculated_version}") +IO.puts(" expected_version: #{enc_version}") +IO.puts(" valid: #{calculated_version == enc_version}") + +# Get packet length +packet_len = bxor(raw_seq, raw_len) &&& 0xFFFF +IO.puts("\nPacket length: #{packet_len}") + +# Decrypt with AES +IO.puts("\n=== AES Decryption ===") +IO.puts("Input: #{Base.encode16(payload)}") +aes_decrypted = AESCipher.crypt(payload, recv_iv) +IO.puts("After AES: #{Base.encode16(aes_decrypted)}") + +# Decrypt with Shanda +IO.puts("\n=== Shanda Decryption ===") +shanda_decrypted = ShandaCipher.decrypt(aes_decrypted) +IO.puts("After Shanda: #{Base.encode16(shanda_decrypted)}") + +# Expected opcode 0x01 = 1 +if byte_size(shanda_decrypted) >= 2 do + <> = shanda_decrypted + IO.puts("\n=== Result ===") + IO.puts("Opcode: 0x#{Integer.to_string(opcode, 16)} (#{opcode})") + IO.puts("Rest: #{Base.encode16(rest)}") + + if opcode == 1 do + IO.puts("\nāœ“ SUCCESS! This is CP_PermissionRequest (0x01)") + else + IO.puts("\nāœ— FAILED! Expected opcode 0x01, got 0x#{Integer.to_string(opcode, 16)}") + end +end + +# Update IV +new_recv_iv = IGCipher.inno_hash(recv_iv) +IO.puts("\nUpdated recv_iv: #{Base.encode16(new_recv_iv)}") + +IO.puts("\n=== Done ===") diff --git a/test/find_iv.exs b/test/find_iv.exs new file mode 100644 index 0000000..4703a83 --- /dev/null +++ b/test/find_iv.exs @@ -0,0 +1,53 @@ +# Find the correct IV by trying all combinations from hello packet +# Hello packet: 0E 00 70 00 01 00 34 9A 0F 0C A8 BC 0D B3 E6 07 + +import Bitwise + +# Raw packet from client +packet = <<0x7C, 0xA8, 0x7B, 0xA8, 0xBF, 0x0A, 0xCD, 0xDE, 0xC7, 0x71, 0xAC>> +<> = packet + +IO.puts("Raw packet: #{Base.encode16(packet)}") +IO.puts("raw_seq: 0x#{Integer.to_string(raw_seq, 16)} (#{raw_seq})") +IO.puts("") + +# For header validation to pass: raw_seq ^ seq_base == 112 +target_seq_base = bxor(raw_seq, 112) +IO.puts("Need seq_base: 0x#{Integer.to_string(target_seq_base, 16)} (#{target_seq_base})") +IO.puts("") + +# seq_base = (r2 & 0xFF) | ((r3 << 8) & 0xFF00) +# So: r2 = lower byte, r3 = upper byte +target_r2 = target_seq_base &&& 0xFF +target_r3 = (target_seq_base >>> 8) &&& 0xFF +IO.puts("Need recv_iv[2] = 0x#{Integer.to_string(target_r2, 16)} (#{target_r2})") +IO.puts("Need recv_iv[3] = 0x#{Integer.to_string(target_r3, 16)} (#{target_r3})") +IO.puts("") + +# Bytes available in hello packet (positions 6-13): +# 34 9A 0F 0C A8 BC 0D B3 +bytes = [0x34, 0x9A, 0x0F, 0x0C, 0xA8, 0xBC, 0x0D, 0xB3] +IO.puts("Available bytes from hello packet:") +Enum.each(Enum.with_index(bytes), fn {b, i} -> + IO.puts(" [#{i}]: 0x#{Integer.to_string(b, 16)}") +end) +IO.puts("") + +# Find matching bytes +IO.puts("Looking for matches...") +Enum.each(Enum.with_index(bytes), fn {b2, i2} -> + Enum.each(Enum.with_index(bytes), fn {b3, i3} -> + if b2 == target_r2 and b3 == target_r3 do + IO.puts("Found match! recv_iv[2]=0x#{Integer.to_string(b2, 16)} at [#{i2}], recv_iv[3]=0x#{Integer.to_string(b3, 16)} at [#{i3}]") + + # Construct full IV (need to determine r0 and r1 too) + # Try different combinations for r0 and r1 + Enum.each(Enum.with_index(bytes), fn {b0, i0} -> + Enum.each(Enum.with_index(bytes), fn {b1, i1} -> + iv = <> + IO.puts(" Possible IV: #{Base.encode16(iv)} (bytes[#{i0}][#{i1}][#{i2}][#{i3}])") + end) + end) + end + end) +end) diff --git a/test/test_aes.exs b/test/test_aes.exs new file mode 100644 index 0000000..40e0dc2 --- /dev/null +++ b/test/test_aes.exs @@ -0,0 +1,48 @@ +# Test AES cipher with known values + +Code.require_file("lib/odinsea/util/bit_tools.ex", ".") +Code.require_file("lib/odinsea/net/cipher/ig_cipher.ex", ".") +Code.require_file("lib/odinsea/net/cipher/aes_cipher.ex", ".") + +alias Odinsea.Net.Cipher.AESCipher + +import Bitwise + +# Test: AES crypt should be self-inverse (since it's just XOR) +iv = <<0x0F, 0x0C, 0x0C, 0xA8>> +data = <<0xBF, 0x0A, 0xCD, 0xDE, 0xC7, 0x71, 0xAC>> + +IO.puts("Testing AES cipher:") +IO.puts("IV: #{Base.encode16(iv)}") +IO.puts("Data: #{Base.encode16(data)}") +IO.puts("") + +# Expand IV to 16 bytes +expanded_iv = :binary.copy(iv, 4) +IO.puts("Expanded IV (16 bytes): #{Base.encode16(expanded_iv)}") + +# The AES key (16 bytes) +aes_key = << + 0x13, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, + 0xB4, 0x00, 0x00, 0x00 +>> + +# Encrypt the expanded IV to get keystream +keystream = :crypto.crypto_one_time(:aes_128_ecb, aes_key, expanded_iv, true) +IO.puts("Keystream: #{Base.encode16(keystream)}") + +# XOR data with keystream (only use as many bytes as data) +keystream_bytes = :binary.bin_to_list(keystream) |> Enum.take(byte_size(data)) +data_bytes = :binary.bin_to_list(data) +result_bytes = Enum.zip_with(data_bytes, keystream_bytes, fn x, y -> bxor(x, y) end) +result = :binary.list_to_bin(result_bytes) + +IO.puts("XOR result: #{Base.encode16(result)}") +IO.puts("") + +# Compare with AESCipher.crypt +aes_result = AESCipher.crypt(data, iv) +IO.puts("AESCipher.crypt result: #{Base.encode16(aes_result)}") +IO.puts("Match: #{result == aes_result}") diff --git a/test/test_ivs.exs b/test/test_ivs.exs new file mode 100644 index 0000000..b3173f7 --- /dev/null +++ b/test/test_ivs.exs @@ -0,0 +1,55 @@ +# Test specific IV candidates + +Code.require_file("lib/odinsea/util/bit_tools.ex", ".") +Code.require_file("lib/odinsea/net/cipher/ig_cipher.ex", ".") +Code.require_file("lib/odinsea/net/cipher/aes_cipher.ex", ".") +Code.require_file("lib/odinsea/net/cipher/shanda_cipher.ex", ".") + +alias Odinsea.Net.Cipher.{IGCipher, AESCipher, ShandaCipher} + +import Bitwise + +packet = <<0x7C, 0xA8, 0x7B, 0xA8, 0xBF, 0x0A, 0xCD, 0xDE, 0xC7, 0x71, 0xAC>> +<> = packet + +IO.puts("Testing IV candidates for packet: #{Base.encode16(packet)}") +IO.puts("raw_seq: 0x#{Integer.to_string(raw_seq, 16)}, raw_len: 0x#{Integer.to_string(raw_len, 16)}") +IO.puts("") + +# IV candidates based on bytes [2]=0x0F, [3]=0x0C, [4]=0xA8, [5]=0xBC +candidates = [ + <<0x0F, 0x0C, 0x0C, 0xA8>>, # [2][3][3][4] - overlapping + <<0x0C, 0x0C, 0x0C, 0xA8>>, # [3][3][3][4] + <<0x0C, 0xA8, 0x0C, 0xA8>>, # [3][4][3][4] - repeating pattern + <<0x0F, 0x0C, 0xA8, 0xBC>>, # [2][3][4][5] - recv_iv from hello? + <<0x34, 0x9A, 0x0C, 0xA8>>, # [0][1][3][4] + <<0x9A, 0x0F, 0x0C, 0xA8>>, # [1][2][3][4] + <<0x0C, 0xA8, 0xBC, 0x0D>>, # [3][4][5][6] + <<0xA8, 0xBC, 0x0C, 0xA8>>, # [4][5][3][4] +] + +Enum.each(candidates, fn iv -> + <<_r0, _r1, r2, r3>> = iv + seq_base = ((r2 &&& 0xFF) ||| ((r3 <<< 8) &&& 0xFF00)) &&& 0xFFFF + valid = bxor(raw_seq &&& 0xFFFF, seq_base) == 112 + + IO.puts("IV: #{Base.encode16(iv)}") + IO.puts(" seq_base: 0x#{Integer.to_string(seq_base, 16)}, valid: #{valid}") + + if valid do + # Try full decryption + aes_result = AESCipher.crypt(payload, iv) + shanda_result = ShandaCipher.decrypt(aes_result) + + if byte_size(shanda_result) >= 2 do + <> = shanda_result + IO.puts(" *** VALID IV! ***") + IO.puts(" Decrypted opcode: 0x#{Integer.to_string(opcode, 16)} (#{opcode})") + if opcode == 1 do + IO.puts(" *** CORRECT OPCODE! ***") + end + end + end + + IO.puts("") +end) diff --git a/test/test_shanda.exs b/test/test_shanda.exs new file mode 100644 index 0000000..ce4d619 --- /dev/null +++ b/test/test_shanda.exs @@ -0,0 +1,54 @@ +# Test Shanda cipher - encrypt then decrypt should give original + +Code.require_file("lib/odinsea/util/bit_tools.ex", ".") +Code.require_file("lib/odinsea/net/cipher/shanda_cipher.ex", ".") + +alias Odinsea.Net.Cipher.ShandaCipher +alias Odinsea.Util.BitTools + +import Bitwise + +# Test with simple data +data = <<0x01, 0x00, 0x07, 0x70, 0x00, 0x04, 0x00>> +IO.puts("Original data: #{Base.encode16(data)}") + +# Encrypt +encrypted = ShandaCipher.encrypt(data) +IO.puts("Encrypted: #{Base.encode16(encrypted)}") + +# Decrypt +decrypted = ShandaCipher.decrypt(encrypted) +IO.puts("Decrypted: #{Base.encode16(decrypted)}") + +IO.puts("Match: #{data == decrypted}") +IO.puts("") + +# If they don't match, let's debug step by step +if data != decrypted do + IO.puts("MISMATCH! Let's debug...") + IO.puts("") + + # Manual encrypt - first pass only + bytes = :binary.bin_to_list(data) + data_len = length(bytes) + data_length = data_len &&& 0xFF + + IO.puts("Data length: #{data_len}") + IO.puts("Initial bytes: #{inspect(bytes)}") + IO.puts("") + + # First pass (j=0, forward) + IO.puts("=== Pass 0 (forward) ===") + {result0, remember0} = Enum.reduce(Enum.with_index(bytes), {[], 0}, fn {cur, i}, {acc, remember} -> + cur = BitTools.roll_left(cur, 3) + cur = (cur + data_length) &&& 0xFF + cur = bxor(cur, remember) + new_remember = cur + cur = BitTools.roll_right(cur, data_length &&& 0xFF) + cur = bxor(cur, 0xFF) + cur = (cur + 0x48) &&& 0xFF + {[cur | acc], new_remember} + end) + result0 = Enum.reverse(result0) + IO.puts("After pass 0: #{inspect(result0)}, remember: #{remember0}") +end