1533 lines
40 KiB
Elixir
1533 lines
40 KiB
Elixir
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.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
import Ecto.Query
|
|
|
|
alias Odinsea.Repo
|
|
alias Odinsea.Database.Schema.{Account, Character, InventoryItem, Buddy, Guild, Skill, QuestStatus, QuestStatusMob}
|
|
alias Odinsea.Game.InventoryType
|
|
alias Odinsea.Net.Cipher.LoginCrypto
|
|
|
|
# ==================================================================================================
|
|
# Account Operations
|
|
# ==================================================================================================
|
|
|
|
@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
|
|
# Filter username (sanitization)
|
|
username = sanitize_username(username)
|
|
|
|
case Repo.get_by(Account, name: username) do
|
|
nil ->
|
|
Logger.warning("Login attempt for non-existent account: #{username}")
|
|
{:error, :account_not_found}
|
|
|
|
account ->
|
|
check_account_login(account, password, ip_address)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets an account by name.
|
|
Returns nil if not found.
|
|
"""
|
|
def get_account_by_name(name) do
|
|
Repo.get_by(Account, name: name)
|
|
end
|
|
|
|
@doc """
|
|
Gets an account by ID.
|
|
Returns nil if not found.
|
|
"""
|
|
def get_account(account_id) do
|
|
Repo.get(Account, account_id)
|
|
end
|
|
|
|
@doc """
|
|
Updates all accounts to logged out state.
|
|
Used during server startup/shutdown.
|
|
"""
|
|
def set_all_accounts_logged_off do
|
|
Repo.update_all(Account, set: [loggedin: 0])
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates account login state.
|
|
|
|
States:
|
|
- 0 = LOGIN_NOTLOGGEDIN
|
|
- 1 = LOGIN_SERVER_TRANSITION (migrating between servers)
|
|
- 2 = LOGIN_LOGGEDIN
|
|
- 3 = CHANGE_CHANNEL
|
|
"""
|
|
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
|
|
|
|
@doc """
|
|
Updates account logged in status.
|
|
"""
|
|
def update_logged_in_status(account_id, status) do
|
|
Repo.update_all(
|
|
from(a in Account, where: a.id == ^account_id),
|
|
set: [loggedin: status]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets the current login state for an account.
|
|
"""
|
|
def get_login_state(account_id) do
|
|
case Repo.get(Account, account_id) do
|
|
nil -> 0
|
|
account -> account.loggedin
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates account last login time.
|
|
"""
|
|
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
|
|
- :tempban - temporary ban expiry datetime
|
|
- :greason - GM reason code
|
|
"""
|
|
def ban_account(account_id, attrs \\ %{}) do
|
|
case Repo.get(Account, account_id) do
|
|
nil -> {:error, :not_found}
|
|
account ->
|
|
account
|
|
|> Account.ban_changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
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} ->
|
|
Logger.error("Failed to log IP: #{inspect(err)}")
|
|
:error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if an account is banned.
|
|
Returns {:ok, account} if not banned, {:error, :banned} if banned.
|
|
"""
|
|
def check_ban_status(account_id) do
|
|
case Repo.get(Account, account_id) do
|
|
nil -> {:error, :not_found}
|
|
account ->
|
|
if account.banned > 0 do
|
|
{:error, :banned}
|
|
else
|
|
{:ok, account}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets temporary ban information if account is temp banned.
|
|
"""
|
|
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
|
|
%{reason: account.banreason, expires: account.tempban}
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates account NX cash (ACash) and Maple Points.
|
|
"""
|
|
def update_account_cash(account_id, acash, mpoints, points \\ nil, vpoints \\ nil) 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
|
|
|
|
# ==================================================================================================
|
|
# Character Operations
|
|
# ==================================================================================================
|
|
|
|
@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
|
|
Character
|
|
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
|
|> select([c], %{id: c.id, name: c.name, gm: c.gm})
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets the count of characters for an account in a world.
|
|
"""
|
|
def character_count(account_id, world_id) do
|
|
Character
|
|
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
|
|> Repo.aggregate(:count, :id)
|
|
end
|
|
|
|
@doc """
|
|
Gets all character IDs for an account in a world.
|
|
"""
|
|
def load_character_ids(account_id, world_id) do
|
|
Character
|
|
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
|
|> select([c], c.id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets all character names for an account in a world.
|
|
"""
|
|
def load_character_names(account_id, world_id) do
|
|
Character
|
|
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
|
|> select([c], c.name)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets all characters for an account in a world.
|
|
Returns full Character structs.
|
|
"""
|
|
def get_characters_by_account(account_id, world_id) do
|
|
Character
|
|
|> where([c], c.accountid == ^account_id and c.world == ^world_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@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 """
|
|
Checks if a character name is already in use.
|
|
"""
|
|
def character_name_exists?(name) do
|
|
Character
|
|
|> where([c], c.name == ^name)
|
|
|> Repo.exists?()
|
|
end
|
|
|
|
@doc """
|
|
Gets character ID by name and world.
|
|
"""
|
|
def get_character_id(name, world_id) do
|
|
Character
|
|
|> where([c], c.name == ^name and c.world == ^world_id)
|
|
|> select([c], c.id)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets a character by name.
|
|
"""
|
|
def get_character_by_name(name) do
|
|
Repo.get_by(Character, name: name)
|
|
end
|
|
|
|
@doc """
|
|
Creates a new character.
|
|
|
|
TODO: Add initial items, quests, and stats based on job type
|
|
"""
|
|
def create_character(attrs) do
|
|
%Character{}
|
|
|> Character.creation_changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@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
|
|
|
|
# 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 ->
|
|
{:error, :not_found}
|
|
|
|
character ->
|
|
new_name = "#" <> character.name
|
|
|
|
Repo.update_all(
|
|
from(c in Character, where: c.id == ^character_id),
|
|
set: [
|
|
name: new_name,
|
|
world: deleted_world
|
|
]
|
|
)
|
|
|
|
# Clean up related records
|
|
cleanup_character_assets(character_id)
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates character stats.
|
|
"""
|
|
def update_character_stats(character_id, attrs) do
|
|
case Repo.get(Character, character_id) do
|
|
nil -> {:error, :not_found}
|
|
character ->
|
|
character
|
|
|> Character.stat_changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates character position (map, spawn point).
|
|
"""
|
|
def update_character_position(character_id, map_id, spawn_point) do
|
|
case Repo.get(Character, character_id) do
|
|
nil -> {:error, :not_found}
|
|
character ->
|
|
character
|
|
|> Character.position_changeset(%{map: map_id, spawnpoint: spawn_point})
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates character meso.
|
|
"""
|
|
def update_character_meso(character_id, meso) do
|
|
Repo.update_all(
|
|
from(c in Character, where: c.id == ^character_id),
|
|
set: [meso: meso]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates character EXP.
|
|
"""
|
|
def update_character_exp(character_id, exp) do
|
|
Repo.update_all(
|
|
from(c in Character, where: c.id == ^character_id),
|
|
set: [exp: exp]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates character job.
|
|
"""
|
|
def update_character_job(character_id, job_id) do
|
|
Repo.update_all(
|
|
from(c in Character, where: c.id == ^character_id),
|
|
set: [job: job_id]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates character level.
|
|
"""
|
|
def update_character_level(character_id, level) do
|
|
Repo.update_all(
|
|
from(c in Character, where: c.id == ^character_id),
|
|
set: [level: level]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates character guild information.
|
|
"""
|
|
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
|
|
|
|
@doc """
|
|
Updates character SP (skill points).
|
|
"""
|
|
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
|
|
|
|
# ==================================================================================================
|
|
# Character Creation Helpers
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Gets default stats for a new character based on job type.
|
|
|
|
Job types:
|
|
- 0 = Resistance
|
|
- 1 = Adventurer
|
|
- 2 = Cygnus
|
|
- 3 = Aran
|
|
- 4 = Evan
|
|
"""
|
|
def get_default_stats_for_job(job_type, subcategory \\ 0) do
|
|
base_stats = %{
|
|
level: 1,
|
|
exp: 0,
|
|
hp: 50,
|
|
mp: 5,
|
|
maxhp: 50,
|
|
maxmp: 5,
|
|
str: 12,
|
|
dex: 5,
|
|
luk: 4,
|
|
int: 4,
|
|
ap: 0
|
|
}
|
|
|
|
case job_type do
|
|
0 -> # Resistance
|
|
%{base_stats | job: 3000, str: 12, dex: 5, int: 4, luk: 4}
|
|
1 -> # Adventurer
|
|
if subcategory == 1 do # Dual Blade
|
|
%{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
|
|
%{base_stats | job: 1000, str: 12, dex: 5, int: 4, luk: 4}
|
|
3 -> # Aran
|
|
%{base_stats | job: 2000, str: 12, dex: 5, int: 4, luk: 4}
|
|
4 -> # Evan
|
|
%{base_stats | job: 2001, str: 4, dex: 4, int: 12, luk: 5}
|
|
_ ->
|
|
base_stats
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets the default map ID for a job type.
|
|
"""
|
|
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
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Forbidden Names
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Checks if a character name is forbidden.
|
|
"""
|
|
def forbidden_name?(name) do
|
|
forbidden = [
|
|
"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 ->
|
|
String.contains?(name_lower, forbidden)
|
|
end)
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Private Functions
|
|
# ==================================================================================================
|
|
|
|
defp check_account_login(account, password, ip_address) do
|
|
# Check if banned
|
|
if account.banned > 0 && account.gm == 0 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
|
|
end
|
|
end
|
|
|
|
defp verify_password(account, password, ip_address) do
|
|
# Check various password formats
|
|
valid =
|
|
check_salted_sha512(account.password, password, account.salt) ||
|
|
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),
|
|
acash: account.acash,
|
|
mpoints: account.mpoints
|
|
}
|
|
|
|
{:ok, account_info}
|
|
else
|
|
{:error, :invalid_credentials}
|
|
end
|
|
end
|
|
|
|
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
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
defp check_plain_match(hash, password) do
|
|
# Direct comparison (legacy/insecure, but needed for compatibility)
|
|
hash == password
|
|
end
|
|
|
|
defp check_admin_bypass(password, ip_address) do
|
|
# Check for admin bypass password from specific IPs
|
|
# TODO: Load admin passwords and allowed IPs from config
|
|
false
|
|
end
|
|
|
|
defp decrypt_second_password(nil, _), do: nil
|
|
defp decrypt_second_password("", _), do: nil
|
|
defp decrypt_second_password(spw, salt2) do
|
|
if salt2 && salt2 != "" do
|
|
# Decrypt using rand_r (reverse of rand_s)
|
|
LoginCrypto.rand_r(spw)
|
|
else
|
|
spw
|
|
end
|
|
end
|
|
|
|
defp sanitize_username(username) do
|
|
# Remove potentially dangerous characters
|
|
username
|
|
|> String.trim()
|
|
|> String.replace(~r/[<>"'%;()&+\-]/, "")
|
|
end
|
|
|
|
defp format_timestamp(naive_datetime) do
|
|
NaiveDateTime.to_string(naive_datetime)
|
|
end
|
|
|
|
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
|
|
|
|
# ==================================================================================================
|
|
# Inventory Operations
|
|
# ==================================================================================================
|
|
|
|
@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 =
|
|
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 ->
|
|
db_item = Enum.find(items, fn db -> db.inventoryitemid == item.id end)
|
|
InventoryType.from_type(db_item.inventorytype)
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Gets items for a specific inventory type.
|
|
"""
|
|
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()
|
|
|> Enum.map(&InventoryItem.to_game_item/1)
|
|
end
|
|
|
|
@doc """
|
|
Gets all inventory items for a character (raw database records).
|
|
"""
|
|
def get_inventory_by_character(character_id) do
|
|
InventoryItem
|
|
|> where([i], i.characterid == ^character_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a single inventory item by ID.
|
|
"""
|
|
def get_inventory_item(item_id) do
|
|
case Repo.get(InventoryItem, item_id) do
|
|
nil -> nil
|
|
db_item -> InventoryItem.to_game_item(db_item)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a new inventory item.
|
|
"""
|
|
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()
|
|
end
|
|
|
|
@doc """
|
|
Saves an inventory item (inserts or updates).
|
|
"""
|
|
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 ->
|
|
%InventoryItem{}
|
|
|> InventoryItem.changeset(attrs)
|
|
|> Repo.insert()
|
|
db_item ->
|
|
db_item
|
|
|> InventoryItem.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
else
|
|
# Insert new
|
|
%InventoryItem{}
|
|
|> InventoryItem.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates an existing inventory item.
|
|
"""
|
|
def update_inventory_item(item_id, updates) do
|
|
case Repo.get(InventoryItem, item_id) do
|
|
nil -> {:error, :not_found}
|
|
db_item ->
|
|
db_item
|
|
|> InventoryItem.changeset(updates)
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates an inventory item's position.
|
|
"""
|
|
def update_inventory_item_position(item_id, position) do
|
|
Repo.update_all(
|
|
from(i in InventoryItem, where: i.inventoryitemid == ^item_id),
|
|
set: [position: position]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Deletes an inventory item.
|
|
"""
|
|
def delete_inventory_item(item_id) do
|
|
Repo.delete_all(from(i in InventoryItem, where: i.inventoryitemid == ^item_id))
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Saves all inventory items for a character.
|
|
Used during character save.
|
|
"""
|
|
def save_character_inventory(character_id, inventories) 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)
|
|
else
|
|
# Insert new
|
|
create_inventory_item(character_id, inv_type, item)
|
|
end
|
|
end)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets the count of items for a character.
|
|
"""
|
|
def count_inventory_items(character_id) do
|
|
InventoryItem
|
|
|> where([i], i.characterid == ^character_id)
|
|
|> Repo.aggregate(:count, :inventoryitemid)
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Buddy Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Adds a buddy request.
|
|
"""
|
|
def add_buddy(character_id, buddy_id, group_name \\ "ETC") do
|
|
attrs = %{
|
|
characterid: character_id,
|
|
buddyid: buddy_id,
|
|
pending: 1,
|
|
groupname: group_name
|
|
}
|
|
|
|
%Buddy{}
|
|
|> Buddy.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Accepts a buddy request (sets pending to 0).
|
|
"""
|
|
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),
|
|
set: [pending: 0]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Deletes a buddy relationship.
|
|
"""
|
|
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))
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets all buddies for a character.
|
|
"""
|
|
def get_buddies_by_character(character_id) do
|
|
Buddy
|
|
|> where([b], b.characterid == ^character_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Checks if two characters are buddies.
|
|
"""
|
|
def are_buddies?(character_id, buddy_id) do
|
|
Buddy
|
|
|> where([b], b.characterid == ^character_id and b.buddyid == ^buddy_id and b.pending == 0)
|
|
|> Repo.exists?()
|
|
end
|
|
|
|
@doc """
|
|
Gets pending buddy requests for a character.
|
|
"""
|
|
def get_pending_buddies(character_id) do
|
|
Buddy
|
|
|> where([b], b.buddyid == ^character_id and b.pending == 1)
|
|
|> Repo.all()
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Guild Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Creates a new guild.
|
|
"""
|
|
def create_guild(attrs) do
|
|
%Guild{}
|
|
|> Guild.creation_changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Updates a guild.
|
|
"""
|
|
def update_guild(guild_id, attrs) do
|
|
case Repo.get(Guild, guild_id) do
|
|
nil -> {:error, :not_found}
|
|
guild ->
|
|
guild
|
|
|> Guild.settings_changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates guild leader.
|
|
"""
|
|
def update_guild_leader(guild_id, leader_id) do
|
|
case Repo.get(Guild, guild_id) do
|
|
nil -> {:error, :not_found}
|
|
guild ->
|
|
guild
|
|
|> Guild.leader_changeset(%{leader: leader_id})
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes a guild.
|
|
"""
|
|
def delete_guild(guild_id) do
|
|
case Repo.get(Guild, guild_id) do
|
|
nil -> {:error, :not_found}
|
|
guild ->
|
|
Repo.delete(guild)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a guild by ID.
|
|
"""
|
|
def get_guild_by_id(guild_id) do
|
|
Repo.get(Guild, guild_id)
|
|
end
|
|
|
|
@doc """
|
|
Gets a guild by name.
|
|
"""
|
|
def get_guild_by_name(name) do
|
|
Repo.get_by(Guild, name: name)
|
|
end
|
|
|
|
@doc """
|
|
Updates guild GP (guild points).
|
|
"""
|
|
def update_guild_gp(guild_id, gp) do
|
|
Repo.update_all(
|
|
from(g in Guild, where: g.guildid == ^guild_id),
|
|
set: [gp: gp]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates guild capacity.
|
|
"""
|
|
def update_guild_capacity(guild_id, capacity) do
|
|
Repo.update_all(
|
|
from(g in Guild, where: g.guildid == ^guild_id),
|
|
set: [capacity: capacity]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates guild logo/emblem.
|
|
"""
|
|
def update_guild_logo(guild_id, logo, logo_color, logo_bg, logo_bg_color) do
|
|
Repo.update_all(
|
|
from(g in Guild, where: g.guildid == ^guild_id),
|
|
set: [
|
|
logo: logo,
|
|
logo_color: logo_color,
|
|
logo_bg: logo_bg,
|
|
logo_bg_color: logo_bg_color
|
|
]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Checks if a guild name already exists.
|
|
"""
|
|
def guild_name_exists?(name) do
|
|
Guild
|
|
|> where([g], g.name == ^name)
|
|
|> Repo.exists?()
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Quest Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Starts a quest for a character (inserts new quest status).
|
|
|
|
Status values:
|
|
- 0 = Not started
|
|
- 1 = In progress
|
|
- 2 = Completed
|
|
"""
|
|
def start_quest(character_id, quest_id, attrs \\ %{}) do
|
|
defaults = %{
|
|
characterid: character_id,
|
|
quest: quest_id,
|
|
status: 1,
|
|
time: 0,
|
|
forfeited: 0,
|
|
custom_data: nil
|
|
}
|
|
|
|
attrs = Map.merge(defaults, attrs)
|
|
|
|
%QuestStatus{}
|
|
|> QuestStatus.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Completes a quest for a character.
|
|
"""
|
|
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),
|
|
set: [status: 2, time: time]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Forfeits a quest for a character.
|
|
"""
|
|
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),
|
|
set: [status: 0]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates quest custom data.
|
|
"""
|
|
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),
|
|
set: [custom_data: custom_data]
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Updates quest mob kills.
|
|
"""
|
|
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()
|
|
|
|
if existing do
|
|
Repo.update_all(
|
|
from(m in QuestStatusMob,
|
|
where: m.queststatusid == ^quest_status_id and m.mob == ^mob_id),
|
|
set: [count: count]
|
|
)
|
|
else
|
|
%QuestStatusMob{}
|
|
|> QuestStatusMob.changeset(%{
|
|
queststatusid: quest_status_id,
|
|
mob: mob_id,
|
|
count: count
|
|
})
|
|
|> Repo.insert()
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets all quest status records for a character.
|
|
"""
|
|
def get_quests_by_character(character_id) do
|
|
QuestStatus
|
|
|> where([q], q.characterid == ^character_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific quest status for a character.
|
|
"""
|
|
def get_quest_status(character_id, quest_id) do
|
|
QuestStatus
|
|
|> where([q], q.characterid == ^character_id and q.quest == ^quest_id)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets mob kills for a quest status.
|
|
"""
|
|
def get_quest_mob_kills(quest_status_id) do
|
|
QuestStatusMob
|
|
|> where([m], m.queststatusid == ^quest_status_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a quest status and its mob kills.
|
|
"""
|
|
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
|
|
qs ->
|
|
# Delete mob kills
|
|
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)
|
|
)
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Skill Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Learns a new skill for a character.
|
|
"""
|
|
def learn_skill(character_id, skill_id, skill_level, master_level \\ 0, expiration \\ -1) do
|
|
attrs = %{
|
|
characterid: character_id,
|
|
skillid: skill_id,
|
|
skilllevel: skill_level,
|
|
masterlevel: master_level,
|
|
expiration: expiration
|
|
}
|
|
|
|
%Skill{}
|
|
|> Skill.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Updates a skill level for a character.
|
|
"""
|
|
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),
|
|
set: updates
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
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
|
|
nil ->
|
|
learn_skill(character_id, skill_id, skill_level, master_level, expiration)
|
|
skill ->
|
|
skill
|
|
|> Skill.changeset(%{
|
|
skilllevel: skill_level,
|
|
masterlevel: master_level,
|
|
expiration: expiration
|
|
})
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets all skills for a character.
|
|
"""
|
|
def get_skills_by_character(character_id) do
|
|
Skill
|
|
|> where([s], s.characterid == ^character_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific skill for a character.
|
|
"""
|
|
def get_skill(character_id, skill_id) do
|
|
Skill
|
|
|> where([s], s.characterid == ^character_id and s.skillid == ^skill_id)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Deletes a skill from a character.
|
|
"""
|
|
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)
|
|
)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Deletes all skills for a character.
|
|
"""
|
|
def delete_all_skills(character_id) do
|
|
Repo.delete_all(from(s in Skill, where: s.characterid == ^character_id))
|
|
:ok
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Pet Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Saves (creates or updates) a pet.
|
|
Uses raw SQL since pets table may not have an Ecto schema yet.
|
|
"""
|
|
def save_pet(pet) do
|
|
sql = """
|
|
INSERT INTO pets (petid, name, level, closeness, fullness, seconds, flags)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
name = VALUES(name),
|
|
level = VALUES(level),
|
|
closeness = VALUES(closeness),
|
|
fullness = VALUES(fullness),
|
|
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} ->
|
|
Logger.error("Failed to save pet: #{inspect(err)}")
|
|
{:error, err}
|
|
end
|
|
end
|
|
|
|
@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 (?, ?, ?, ?, ?, ?, ?)"
|
|
|
|
case Ecto.Adapters.SQL.query(
|
|
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
|
|
end
|
|
|
|
@doc """
|
|
Gets a pet by ID.
|
|
"""
|
|
def get_pet(pet_id) do
|
|
sql = "SELECT * FROM pets WHERE petid = ?"
|
|
|
|
case Ecto.Adapters.SQL.query(Repo, sql, [pet_id]) do
|
|
{: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} ->
|
|
Logger.error("Failed to get pet: #{inspect(err)}")
|
|
nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets all pets for a character.
|
|
"""
|
|
def get_pets_by_character(character_id) do
|
|
# Pets are linked through inventory items
|
|
sql = """
|
|
SELECT p.* FROM pets p
|
|
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} ->
|
|
Enum.map(result.rows, fn row ->
|
|
columns = Enum.map(result.columns, &String.to_atom/1)
|
|
Enum.zip(columns, row) |> Map.new()
|
|
end)
|
|
{:error, err} ->
|
|
Logger.error("Failed to get pets: #{inspect(err)}")
|
|
[]
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates pet closeness (pet affection).
|
|
"""
|
|
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}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates pet level.
|
|
"""
|
|
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}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates pet fullness.
|
|
"""
|
|
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}
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Saved Locations Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Saves a saved location for a character.
|
|
"""
|
|
def save_saved_location(character_id, location_type, map_id) do
|
|
sql = """
|
|
INSERT INTO savedlocations (characterid, locationtype, map)
|
|
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} ->
|
|
Logger.error("Failed to save location: #{inspect(err)}")
|
|
{:error, err}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a saved location for a character.
|
|
"""
|
|
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} ->
|
|
if result.num_rows > 0 do
|
|
[[map_id]] = result.rows
|
|
map_id
|
|
else
|
|
nil
|
|
end
|
|
{:error, _} -> nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets all saved locations for a character.
|
|
"""
|
|
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} ->
|
|
Enum.map(result.rows, fn [type, map] -> {type, map} end)
|
|
|> Map.new()
|
|
{:error, _} -> %{}
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Cooldown Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Saves a skill cooldown.
|
|
"""
|
|
def save_skill_cooldown(character_id, skill_id, start_time, length) do
|
|
sql = """
|
|
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}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets all skill cooldowns for a character.
|
|
"""
|
|
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} ->
|
|
Enum.map(result.rows, fn row ->
|
|
columns = Enum.map(result.columns, &String.to_atom/1)
|
|
Enum.zip(columns, row) |> Map.new()
|
|
end)
|
|
{:error, _} -> []
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes all skill cooldowns for a character.
|
|
"""
|
|
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}
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Inventory Slot Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Saves inventory slot limits for a character.
|
|
"""
|
|
def save_inventory_slots(character_id, equip, use, setup, etc, cash) do
|
|
sql = """
|
|
INSERT INTO inventoryslot (characterid, equip, use, setup, etc, cash)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
equip = VALUES(equip),
|
|
use = VALUES(use),
|
|
setup = VALUES(setup),
|
|
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}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets inventory slot limits for a character.
|
|
"""
|
|
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} ->
|
|
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, _} ->
|
|
%{equip: 24, use: 80, setup: 80, etc: 80, cash: 40}
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Key Layout Operations
|
|
# ==================================================================================================
|
|
|
|
@doc """
|
|
Saves key layout for a character.
|
|
"""
|
|
def save_key_layout(character_id, key_layout) 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
|
|
|
|
@doc """
|
|
Gets key layout for a character.
|
|
"""
|
|
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} ->
|
|
Enum.map(result.rows, fn [key, type, action] ->
|
|
{key, {type, action}}
|
|
end)
|
|
|> Map.new()
|
|
{:error, _} -> %{}
|
|
end
|
|
end
|
|
end
|