Start repo, claude & kimi still vibing tho
This commit is contained in:
16
lib/odinsea/world/expedition.ex
Normal file
16
lib/odinsea/world/expedition.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule Odinsea.World.Expedition do
|
||||
@moduledoc """
|
||||
Expedition management service.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{expeditions: %{}, next_id: 1}}
|
||||
end
|
||||
end
|
||||
16
lib/odinsea/world/family.ex
Normal file
16
lib/odinsea/world/family.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule Odinsea.World.Family do
|
||||
@moduledoc """
|
||||
Family management service.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{families: %{}}}
|
||||
end
|
||||
end
|
||||
16
lib/odinsea/world/guild.ex
Normal file
16
lib/odinsea/world/guild.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule Odinsea.World.Guild do
|
||||
@moduledoc """
|
||||
Guild management service.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{guilds: %{}}}
|
||||
end
|
||||
end
|
||||
16
lib/odinsea/world/messenger.ex
Normal file
16
lib/odinsea/world/messenger.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule Odinsea.World.Messenger do
|
||||
@moduledoc """
|
||||
Messenger (chat) management service.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{messengers: %{}}}
|
||||
end
|
||||
end
|
||||
277
lib/odinsea/world/migration.ex
Normal file
277
lib/odinsea/world/migration.ex
Normal file
@@ -0,0 +1,277 @@
|
||||
defmodule Odinsea.World.Migration do
|
||||
@moduledoc """
|
||||
Character migration system for server transfers.
|
||||
Manages transfer tokens when moving between login/channel/cash shop servers.
|
||||
|
||||
Ported from Java handling.world.CharacterTransfer and World.ChannelChange_Data
|
||||
|
||||
In the Java version, this uses Redis for cross-server communication.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Database.Context
|
||||
|
||||
@token_ttl 60 # Token expires after 60 seconds
|
||||
|
||||
@doc """
|
||||
Creates a migration token for character transfer.
|
||||
|
||||
Called when:
|
||||
- Player selects character (login -> channel)
|
||||
- Player changes channel (channel -> channel)
|
||||
- Player enters cash shop (channel -> cash shop)
|
||||
|
||||
## Parameters
|
||||
- character_id: Character ID being transferred
|
||||
- account_id: Account ID
|
||||
- source_server: :login, :channel, or :shop
|
||||
- target_channel: Target channel ID (or -10 for cash shop, -20 for MTS)
|
||||
- character_data: Optional character state to preserve during transfer
|
||||
|
||||
## Returns
|
||||
- {:ok, token_id} on success
|
||||
- {:error, reason} on failure
|
||||
"""
|
||||
def create_migration_token(character_id, account_id, source_server, target_channel, character_data \\ %{}) do
|
||||
token_id = generate_token_id()
|
||||
|
||||
token = %{
|
||||
id: token_id,
|
||||
character_id: character_id,
|
||||
account_id: account_id,
|
||||
source_server: source_server,
|
||||
target_channel: target_channel,
|
||||
character_data: character_data,
|
||||
created_at: System.system_time(:second),
|
||||
status: :pending
|
||||
}
|
||||
|
||||
# Store in ETS (local cache)
|
||||
:ets.insert(:migration_tokens, {token_id, token})
|
||||
|
||||
# Also store in Redis for cross-server visibility
|
||||
case store_in_redis(token_id, token) do
|
||||
:ok ->
|
||||
Logger.info("Created migration token #{token_id} for character #{character_id} to channel #{target_channel}")
|
||||
{:ok, token_id}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to store migration token in Redis: #{inspect(reason)}")
|
||||
# Still return OK since ETS has it (for single-node deployments)
|
||||
{:ok, token_id}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a migration token when a character migrates into a server.
|
||||
|
||||
Called by InterServerHandler.MigrateIn when player connects to channel/shop.
|
||||
|
||||
## Parameters
|
||||
- token_id: The migration token ID
|
||||
- character_id: Expected character ID
|
||||
- target_server: :channel or :shop
|
||||
|
||||
## Returns
|
||||
- {:ok, token} if valid
|
||||
- {:error, reason} if invalid/expired
|
||||
"""
|
||||
def validate_migration_token(token_id, character_id, target_server) do
|
||||
# Try Redis first (for cross-server)
|
||||
token =
|
||||
case get_from_redis(token_id) do
|
||||
{:ok, nil} ->
|
||||
# Try ETS (local fallback)
|
||||
get_from_ets(token_id)
|
||||
|
||||
{:ok, token} ->
|
||||
token
|
||||
|
||||
{:error, _} ->
|
||||
get_from_ets(token_id)
|
||||
end
|
||||
|
||||
cond do
|
||||
is_nil(token) ->
|
||||
Logger.warning("Migration token #{token_id} not found")
|
||||
{:error, :token_not_found}
|
||||
|
||||
token.character_id != character_id ->
|
||||
Logger.warning("Migration token character mismatch: expected #{character_id}, got #{token.character_id}")
|
||||
{:error, :character_mismatch}
|
||||
|
||||
token_expired?(token) ->
|
||||
Logger.warning("Migration token #{token_id} expired")
|
||||
delete_token(token_id)
|
||||
{:error, :token_expired}
|
||||
|
||||
token.status != :pending ->
|
||||
Logger.warning("Migration token #{token_id} already used")
|
||||
{:error, :token_already_used}
|
||||
|
||||
true ->
|
||||
# Mark as used
|
||||
update_token_status(token_id, :used)
|
||||
{:ok, token}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets pending character data for a migration.
|
||||
Returns nil if no pending migration exists.
|
||||
"""
|
||||
def get_pending_character(character_id) do
|
||||
# Search for pending tokens for this character
|
||||
case :ets.select(:migration_tokens, [
|
||||
{{:_, %{character_id: character_id, status: :pending}}, [], [:_]}])
|
||||
do
|
||||
[{_key, token}] -> token
|
||||
[] -> nil
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a migration token.
|
||||
"""
|
||||
def delete_token(token_id) do
|
||||
:ets.delete(:migration_tokens, token_id)
|
||||
delete_from_redis(token_id)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cleans up expired tokens.
|
||||
Should be called periodically.
|
||||
"""
|
||||
def cleanup_expired_tokens do
|
||||
now = System.system_time(:second)
|
||||
|
||||
expired = :ets.select(:migration_tokens, [
|
||||
{{:_, %{created_at: :"$1", id: :"$2"}},
|
||||
[{:<, {:+, :"$1", @token_ttl}, now}],
|
||||
[:"$2"]}
|
||||
])
|
||||
|
||||
Enum.each(expired, fn token_id ->
|
||||
Logger.debug("Cleaning up expired migration token #{token_id}")
|
||||
delete_token(token_id)
|
||||
end)
|
||||
|
||||
length(expired)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the count of pending migrations (for server capacity checking).
|
||||
"""
|
||||
def pending_count do
|
||||
:ets.info(:migration_tokens, :size)
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# GenServer Callbacks for Token Cleanup
|
||||
# ==================================================================================================
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_opts) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
# Create ETS table for migration tokens
|
||||
:ets.new(:migration_tokens, [
|
||||
:set,
|
||||
:public,
|
||||
:named_table,
|
||||
read_concurrency: true,
|
||||
write_concurrency: true
|
||||
])
|
||||
|
||||
# Schedule cleanup every 30 seconds
|
||||
schedule_cleanup()
|
||||
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:cleanup, state) do
|
||||
count = cleanup_expired_tokens()
|
||||
|
||||
if count > 0 do
|
||||
Logger.debug("Cleaned up #{count} expired migration tokens")
|
||||
end
|
||||
|
||||
schedule_cleanup()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp schedule_cleanup do
|
||||
Process.send_after(self(), :cleanup, 30_000)
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Private Functions
|
||||
# ==================================================================================================
|
||||
|
||||
defp generate_token_id do
|
||||
:crypto.strong_rand_bytes(16)
|
||||
|> Base.encode16(case: :lower)
|
||||
end
|
||||
|
||||
defp token_expired?(token) do
|
||||
now = System.system_time(:second)
|
||||
now > token.created_at + @token_ttl
|
||||
end
|
||||
|
||||
defp update_token_status(token_id, status) do
|
||||
case :ets.lookup(:migration_tokens, token_id) do
|
||||
[{^token_id, token}] ->
|
||||
updated = %{token | status: status}
|
||||
:ets.insert(:migration_tokens, {token_id, updated})
|
||||
store_in_redis(token_id, updated)
|
||||
|
||||
[] ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp get_from_ets(token_id) do
|
||||
case :ets.lookup(:migration_tokens, token_id) do
|
||||
[{^token_id, token}] -> token
|
||||
[] -> nil
|
||||
end
|
||||
end
|
||||
|
||||
# Redis operations (for cross-server communication)
|
||||
|
||||
defp store_in_redis(token_id, token) do
|
||||
case Redix.command(:redix, ["SETEX", "migration:#{token_id}", @token_ttl, :erlang.term_to_binary(token)]) do
|
||||
{:ok, _} -> :ok
|
||||
error -> error
|
||||
end
|
||||
rescue
|
||||
_ -> {:error, :redis_unavailable}
|
||||
end
|
||||
|
||||
defp get_from_redis(token_id) do
|
||||
case Redix.command(:redix, ["GET", "migration:#{token_id}"]) do
|
||||
{:ok, nil} -> {:ok, nil}
|
||||
{:ok, data} -> {:ok, :erlang.binary_to_term(data)}
|
||||
error -> error
|
||||
end
|
||||
rescue
|
||||
_ -> {:error, :redis_unavailable}
|
||||
end
|
||||
|
||||
defp delete_from_redis(token_id) do
|
||||
case Redix.command(:redix, ["DEL", "migration:#{token_id}"]) do
|
||||
{:ok, _} -> :ok
|
||||
error -> error
|
||||
end
|
||||
rescue
|
||||
_ -> {:ok}
|
||||
end
|
||||
end
|
||||
16
lib/odinsea/world/party.ex
Normal file
16
lib/odinsea/world/party.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule Odinsea.World.Party do
|
||||
@moduledoc """
|
||||
Party management service.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{parties: %{}, next_id: 1}}
|
||||
end
|
||||
end
|
||||
37
lib/odinsea/world/supervisor.ex
Normal file
37
lib/odinsea/world/supervisor.ex
Normal file
@@ -0,0 +1,37 @@
|
||||
defmodule Odinsea.World.Supervisor do
|
||||
@moduledoc """
|
||||
Supervisor for the game world services.
|
||||
Manages parties, guilds, families, and global state.
|
||||
"""
|
||||
|
||||
use Supervisor
|
||||
|
||||
def start_link(init_arg) do
|
||||
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_init_arg) do
|
||||
children = [
|
||||
# World state
|
||||
Odinsea.World,
|
||||
|
||||
# Party management
|
||||
Odinsea.World.Party,
|
||||
|
||||
# Guild management
|
||||
Odinsea.World.Guild,
|
||||
|
||||
# Family management
|
||||
Odinsea.World.Family,
|
||||
|
||||
# Expedition management
|
||||
Odinsea.World.Expedition,
|
||||
|
||||
# Messenger system
|
||||
Odinsea.World.Messenger
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user