Start repo, claude & kimi still vibing tho

This commit is contained in:
ra
2026-02-14 17:04:21 -07:00
commit f5b8aeb39d
54 changed files with 9466 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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