Skip to content

Commit

Permalink
Merge pull request #74 from ImNotAVirus/refacto/elven_database
Browse files Browse the repository at this point in the history
Refacto/elven database
  • Loading branch information
ImNotAVirus authored Sep 9, 2024
2 parents 2645716 + 98a7798 commit 70a15c3
Show file tree
Hide file tree
Showing 16 changed files with 1,653 additions and 214 deletions.
26 changes: 26 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
locals_without_parens = [
# Ecto
from: 2,
field: 1,
field: 2,
field: 3,
timestamps: 1,
belongs_to: 2,
belongs_to: 3,
has_one: 2,
has_one: 3,
has_many: 2,
has_many: 3,
many_to_many: 2,
many_to_many: 3,
embeds_one: 2,
embeds_one: 3,
embeds_one: 4,
embeds_many: 2,
embeds_many: 3,
embeds_many: 4
]

[
locals_without_parens: locals_without_parens
]
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ defmodule ChannelService.AuthActions do
# FIXME: maybe use the Session struct here
%{username: username, password: password} = session

case Accounts.log_in(username, password) do
%Account{} = acc -> {:ok, acc}
_ -> {:error, :cant_fetch_account}
case Accounts.authenticate(username, password) do
{:ok, account} -> {:ok, account}
{:error, :not_found} -> {:error, :cant_fetch_account}
end
end

defp send_character_list(%Account{} = account, socket) do
character_list = Characters.all_by_account_id(account.id)
character_list = Characters.list_by_account(account)
Socket.send(socket, LobbyViews.render(:clist_start, %{}))

Enum.each(character_list, fn character ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ defmodule ChannelService.LobbyActions do

require Logger

alias ElvenDatabase.Players.Characters
alias ElvenGard.Network.Socket
alias ElvenDatabase.Players.{Account, Characters}
alias ElvenPackets.Views.LobbyViews

## Public API
Expand All @@ -26,19 +26,18 @@ defmodule ChannelService.LobbyActions do
@spec select_character(String.t(), map, Socket.t()) :: {:cont, Socket.t()}
def select_character("select", %{slot: slot}, socket) do
account = socket.assigns.account
%Account{id: account_id} = account

new_socket =
case Characters.get_by_account_id_and_slot(account_id, slot) do
nil ->
Logger.warning("Invalid character slot", socket_id: socket.id)
socket

character ->
case Characters.get_by_account_and_slot(account, slot) do
{:ok, character} ->
Socket.send(socket, LobbyViews.render(:ok))

# Temporary store the character (deleted when you enter in game)
Socket.assign(socket, character_id: character.id, character: character)

{:error, :not_found} ->
Logger.warning("Invalid character slot", socket_id: socket.id)
socket
end

{:cont, new_socket}
Expand Down
38 changes: 30 additions & 8 deletions apps/elven_database/lib/elven_database/players/account.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
defmodule ElvenDatabase.Players.Account do
@moduledoc """
TODO: Documentation
Holds information about an Account.
"""

use Ecto.Schema

import Ecto.Changeset

require ElvenData.Enums.PlayerEnums
require ElvenData.Enums.PlayerEnums, as: PlayerEnums

alias ElvenData.Enums.PlayerEnums
alias __MODULE__
alias ElvenDatabase.Players.Character

@type id :: non_neg_integer()
@type t :: %Account{
id: id(),
username: String.t(),
password: String.t(),
hashed_password: String.t(),
authority: PlayerEnums.authority_keys(),
language: PlayerEnums.language_keys(),
characters: [Character.t()],
# Ecto fields
__meta__: Ecto.Schema.Metadata.t(),
inserted_at: any(),
updated_at: any()
}

## Schema

Expand All @@ -18,16 +34,22 @@ defmodule ElvenDatabase.Players.Account do
field :password, :string, virtual: true
field :hashed_password, :string
field :authority, Ecto.Enum, values: PlayerEnums.authority(:__keys__)
field :language, Ecto.Enum, values: [:en, :fr]
field :language, Ecto.Enum, values: PlayerEnums.language(:__keys__)

has_many :characters, Character

timestamps()
end

## Public API

def changeset(account, attrs) do
@fields [:username, :password, :hashed_password, :authority, :language]

@spec changeset(t(), map()) :: Ecto.Changeset.t()
def changeset(%Account{} = account, attrs) do
account
|> cast(attrs, [:username, :password, :authority, :language])
|> cast(attrs, @fields)
|> cast_assoc(:characters, with: &Character.assoc_changeset/2)
|> unique_constraint(:username)
|> maybe_hash_password()
|> validate_required([:username, :hashed_password])
Expand All @@ -39,8 +61,8 @@ defmodule ElvenDatabase.Players.Account do
password = get_change(changeset, :password)
hashed_password = get_change(changeset, :hashed_password)

if changeset.valid?() && password && is_nil(hashed_password) do
new_password = :sha512 |> :crypto.hash(password) |> Base.encode16()
if changeset.valid?() and not is_nil(password) and is_nil(hashed_password) do
new_password = password |> then(&:crypto.hash(:sha512, &1)) |> Base.encode16()

changeset
|> put_change(:hashed_password, new_password)
Expand Down
42 changes: 36 additions & 6 deletions apps/elven_database/lib/elven_database/players/accounts.ex
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
defmodule ElvenDatabase.Players.Accounts do
@moduledoc """
TODO: Documentation
Module for querying Accounts information from the database.
"""

alias ElvenDatabase.Players.Account
alias ElvenDatabase.Repo

@spec log_in(String.t(), String.t()) :: Ecto.Schema.t() | nil
def log_in(username, hashed_password) do
Repo.get_by(Account, username: username, hashed_password: hashed_password)
# Dyalizer doesn't like `Account.changeset(%Account{}, attrs)`
# because fields on Account struct can't be nil
@dialyzer [
{:no_return, create: 1, create!: 1},
{:no_fail_call, create: 1, create!: 1}
]

## Public API

@spec authenticate(String.t(), String.t()) :: {:ok, Account.t()} | {:error, :not_found}
def authenticate(username, hashed_password) do
case Repo.get_by(Account, username: username, hashed_password: hashed_password) do
%Account{} = account -> {:ok, account}
nil -> {:error, :not_found}
end
end

@spec create(map) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
@spec create(map()) :: {:ok, Account.t()} | {:error, Ecto.Changeset.t()}
def create(attrs) do
%Account{}
|> Account.changeset(attrs)
|> Repo.insert()
end

@spec create!(map) :: Ecto.Schema.t()
@spec create!(map()) :: Account.t()
def create!(attrs) do
%Account{}
|> Account.changeset(attrs)
|> Repo.insert!()
end

@spec get(Account.id()) :: {:ok, Account.t()} | {:error, :not_found}
def get(id) do
case Repo.get(Account, id) do
nil -> {:error, :not_found}
item -> {:ok, item}
end
end

@spec get!(Account.id()) :: Account.t()
def get!(id) do
Repo.get!(Account, id)
end

@spec preload_characters(Account.t()) :: Account.t()
def preload_characters(account) do
Repo.preload(account, :characters)
end
end
114 changes: 90 additions & 24 deletions apps/elven_database/lib/elven_database/players/character.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
defmodule ElvenDatabase.Players.Character do
@moduledoc false
@moduledoc """
Holds information about a Character.
"""

use Ecto.Schema

Expand All @@ -8,10 +10,60 @@ defmodule ElvenDatabase.Players.Character do

require ElvenData.Enums.PlayerEnums, as: PlayerEnums

alias ElvenDatabase.Players.Item

# FIXME: Later improve this typespec
@type t :: %__MODULE__{}
alias __MODULE__
alias ElvenDatabase.Players.{Account, Item}

@type id :: non_neg_integer()
@type slot :: 0..4
@type t :: %Character{
id: id(),
account_id: Account.id(),
name: String.t(),
slot: slot(),
class: PlayerEnums.character_class_keys(),
faction: PlayerEnums.faction_keys(),
gender: PlayerEnums.gender_keys(),
hair_color: PlayerEnums.hair_color_keys(),
hair_style: PlayerEnums.hair_style_keys(),
map_id: non_neg_integer(),
map_x: non_neg_integer(),
map_y: non_neg_integer(),
additional_hp: integer(),
additional_mp: integer(),
gold: non_neg_integer(),
bank_gold: non_neg_integer(),
biography: String.t(),
level: pos_integer(),
job_level: pos_integer(),
hero_level: non_neg_integer(),
level_xp: non_neg_integer(),
job_level_xp: non_neg_integer(),
hero_level_xp: non_neg_integer(),
sp_points: non_neg_integer(),
sp_additional_points: non_neg_integer(),
rage_points: non_neg_integer(),
max_mate_count: non_neg_integer(),
reputation: non_neg_integer(),
dignity: non_neg_integer(),
compliment: non_neg_integer(),
act4_dead: non_neg_integer(),
act4_kill: non_neg_integer(),
act4_points: non_neg_integer(),
arena_winner: boolean(),
talent_win: non_neg_integer(),
talent_lose: non_neg_integer(),
talent_surrender: non_neg_integer(),
master_points: non_neg_integer(),
master_ticket: non_neg_integer(),
miniland_intro: String.t(),
miniland_state: PlayerEnums.miniland_state_keys(),
miniland_makepoints: non_neg_integer(),
items: [Item.t()],
# Ecto fields
__meta__: Ecto.Schema.Metadata.t(),
inserted_at: any(),
updated_at: any()
}

# defbitfield GameOptions,
# exchange_blocked: round(:math.pow(2, 1)),
Expand All @@ -31,11 +83,11 @@ defmodule ElvenDatabase.Players.Character do

## Schema

schema "characters" do
schema "visible_characters" do
belongs_to :account, ElvenDatabase.Players.Account

field :name, :string
field :slot, :integer
field :disabled, :boolean

field :class, Ecto.Enum, values: PlayerEnums.character_class(:__keys__)
field :faction, Ecto.Enum, values: PlayerEnums.faction(:__keys__)
Expand Down Expand Up @@ -87,6 +139,7 @@ defmodule ElvenDatabase.Players.Character do

has_many :items, Item, foreign_key: :owner_id

field :deleted_at, :utc_datetime
timestamps()
end

Expand All @@ -103,7 +156,6 @@ defmodule ElvenDatabase.Players.Character do
]

@optional_fields [
:disabled,
:class,
:faction,
:additional_hp,
Expand Down Expand Up @@ -139,27 +191,41 @@ defmodule ElvenDatabase.Players.Character do
# :game_options
]

## Public API

@fields @required_fields ++ @optional_fields
@name_regex ~r/^[\x21-\x7E\xA1-\xAC\xAE-\xFF\x{4e00}-\x{9fa5}\x{0E01}-\x{0E3A}\x{0E3F}-\x{0E5B}\x2E]{4,14}$/u
@name_regex ~r/^[\x21-\x7E\xA1-\xAC\xAE-\xFF\x{4e00}-\x{9fa5}\x{0E01}-\x{0E3A}\x{0E3F}-\x{0E5B}\x2E]+$/u

@doc false
def changeset(character, attrs) do
character
|> cast(attrs, @fields)
|> cast_assoc(:account)
|> validate_required(@required_fields)
|> validate_format(:name, @name_regex)
|> update_change(:name, &String.trim/1)
|> unique_constraint(:name)
@spec changeset(t(), map()) :: Ecto.Changeset.t()
def changeset(%Character{} = character, attrs) do
changeset(character, attrs, @required_fields)
end

@doc false
def disabled_changeset(character, attrs) do
@spec assoc_changeset(t(), map()) :: Ecto.Changeset.t()
def assoc_changeset(%Character{} = character, attrs) do
# In case of cast_assoc, :account_id field is automatically created so it's
# not required
changeset(character, attrs, List.delete(@required_fields, :account_id))
end

## Private functions

defp changeset(character, attrs, required_fields) do
attrs =
case attrs do
%{account: %Account{} = account} -> Map.put(attrs, :account_id, account.id)
attrs -> attrs
end

character
|> cast(attrs, @fields)
|> cast_assoc(:account)
|> validate_required(@required_fields)
|> validate_length(:name, min: 4, max: 32)
|> unique_constraint(:name)
|> cast_assoc(:items, with: &Item.assoc_changeset/2)
|> validate_required(required_fields)
# TODO: Later support encoding for others languages like CH, JP, ...
|> validate_length(:name, min: 4, max: 14)
|> validate_format(:name, @name_regex)
|> assoc_constraint(:account)
|> unique_constraint(:name, name: :characters_name)
|> unique_constraint(:slot, name: :account_slot)
end
end
Loading

0 comments on commit 70a15c3

Please sign in to comment.