basics being displayed
This commit is contained in:
256
lib/cobblemon_ui/cobblemon_fs/cobblemon_fs.ex
Normal file
256
lib/cobblemon_ui/cobblemon_fs/cobblemon_fs.ex
Normal file
@@ -0,0 +1,256 @@
|
||||
defmodule CobblemonUi.CobblemonFS do
|
||||
@moduledoc """
|
||||
GenServer that reads Cobblemon player data from the filesystem.
|
||||
|
||||
Provides cached access to player party and PC storage data parsed
|
||||
from NBT `.dat` files under the configured data directory.
|
||||
|
||||
## Configuration
|
||||
|
||||
config :cobblemon_ui, CobblemonUi.CobblemonFS,
|
||||
data_dir: "/cobblemon-data",
|
||||
cache_ttl_ms: 2_000
|
||||
|
||||
## Data directory structure
|
||||
|
||||
<data_dir>/world/pokemon/playerpartystore/<uuid>.dat
|
||||
<data_dir>/world/pokemon/pcstore/<uuid>.dat
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
alias CobblemonUi.CobblemonFS.{PartyStore, PCStore}
|
||||
|
||||
@default_data_dir "/cobblemon-data"
|
||||
@default_cache_ttl_ms 2_000
|
||||
@uuid_regex ~r/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
|
||||
|
||||
# --- Client API ---
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
name = Keyword.get(opts, :name, __MODULE__)
|
||||
GenServer.start_link(__MODULE__, opts, name: name)
|
||||
end
|
||||
|
||||
@doc "Returns full player data: party + PC."
|
||||
@spec get_player(String.t()) :: {:ok, map()} | {:error, term()}
|
||||
def get_player(uuid), do: call({:get_player, uuid})
|
||||
|
||||
@doc "Returns only the player's party Pokémon."
|
||||
@spec get_party(String.t()) :: {:ok, list()} | {:error, term()}
|
||||
def get_party(uuid), do: call({:get_party, uuid})
|
||||
|
||||
@doc "Returns only the player's PC boxes."
|
||||
@spec get_pc(String.t()) :: {:ok, list()} | {:error, term()}
|
||||
def get_pc(uuid), do: call({:get_pc, uuid})
|
||||
|
||||
@doc "Returns a specific Pokémon by index (0-5 = party, 6+ = PC)."
|
||||
@spec get_pokemon(String.t(), non_neg_integer()) :: {:ok, map() | nil} | {:error, term()}
|
||||
def get_pokemon(uuid, index), do: call({:get_pokemon, uuid, index})
|
||||
|
||||
@doc "Lists all players as `%{uuid: String.t(), name: String.t() | nil}` maps."
|
||||
@spec list_players() :: {:ok, list(map())}
|
||||
def list_players, do: GenServer.call(__MODULE__, :list_players)
|
||||
|
||||
defp call(msg), do: GenServer.call(__MODULE__, msg)
|
||||
|
||||
# --- Server Callbacks ---
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
config = Application.get_env(:cobblemon_ui, __MODULE__, [])
|
||||
|
||||
data_dir = Keyword.get(config, :data_dir, Keyword.get(opts, :data_dir, @default_data_dir))
|
||||
cache_ttl = Keyword.get(config, :cache_ttl_ms, @default_cache_ttl_ms)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
data_dir: data_dir,
|
||||
cache_ttl: cache_ttl,
|
||||
cache: %{}
|
||||
}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_player, uuid}, _from, state) do
|
||||
with :ok <- validate_uuid(uuid),
|
||||
{player_data, state} <- fetch_player(uuid, state) do
|
||||
{:reply, {:ok, player_data}, state}
|
||||
else
|
||||
{:error, _} = err -> {:reply, err, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:get_party, uuid}, _from, state) do
|
||||
with :ok <- validate_uuid(uuid),
|
||||
{player_data, state} <- fetch_player(uuid, state) do
|
||||
{:reply, {:ok, player_data.party}, state}
|
||||
else
|
||||
{:error, _} = err -> {:reply, err, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:get_pc, uuid}, _from, state) do
|
||||
with :ok <- validate_uuid(uuid),
|
||||
{player_data, state} <- fetch_player(uuid, state) do
|
||||
{:reply, {:ok, player_data.pc}, state}
|
||||
else
|
||||
{:error, _} = err -> {:reply, err, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:get_pokemon, uuid, index}, _from, state) do
|
||||
with :ok <- validate_uuid(uuid),
|
||||
{player_data, state} <- fetch_player(uuid, state) do
|
||||
pokemon = find_pokemon(player_data, index)
|
||||
{:reply, {:ok, pokemon}, state}
|
||||
else
|
||||
{:error, _} = err -> {:reply, err, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(:list_players, _from, state) do
|
||||
party_dir = Path.join([state.data_dir, "world", "pokemon", "playerpartystore"])
|
||||
pc_dir = Path.join([state.data_dir, "world", "pokemon", "pcstore"])
|
||||
usercache = load_usercache(state.data_dir)
|
||||
|
||||
uuids =
|
||||
[party_dir, pc_dir]
|
||||
|> Enum.flat_map(&list_uuids_in_dir/1)
|
||||
|> Enum.uniq()
|
||||
|> Enum.sort()
|
||||
|
||||
players =
|
||||
Enum.map(uuids, fn uuid ->
|
||||
%{uuid: uuid, name: Map.get(usercache, uuid)}
|
||||
end)
|
||||
|
||||
{:reply, {:ok, players}, state}
|
||||
end
|
||||
|
||||
# --- Private Helpers ---
|
||||
|
||||
defp validate_uuid(uuid) do
|
||||
if Regex.match?(@uuid_regex, uuid), do: :ok, else: {:error, :invalid_uuid}
|
||||
end
|
||||
|
||||
defp fetch_player(uuid, state) do
|
||||
now = System.monotonic_time(:millisecond)
|
||||
|
||||
case Map.get(state.cache, uuid) do
|
||||
{data, expires_at} when expires_at > now ->
|
||||
{data, state}
|
||||
|
||||
_ ->
|
||||
case load_player(uuid, state.data_dir) do
|
||||
{:ok, data} ->
|
||||
expires_at = now + state.cache_ttl
|
||||
cache = Map.put(state.cache, uuid, {data, expires_at})
|
||||
{data, %{state | cache: cache}}
|
||||
|
||||
{:error, _} = err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp uuid_prefix(uuid), do: String.slice(uuid, 0, 2)
|
||||
|
||||
defp load_player(uuid, data_dir) do
|
||||
prefix = uuid_prefix(uuid)
|
||||
|
||||
party_path =
|
||||
Path.join([data_dir, "world", "pokemon", "playerpartystore", prefix, "#{uuid}.dat"])
|
||||
|
||||
pc_path = Path.join([data_dir, "world", "pokemon", "pcstore", prefix, "#{uuid}.dat"])
|
||||
|
||||
party_exists? = File.exists?(party_path)
|
||||
pc_exists? = File.exists?(pc_path)
|
||||
|
||||
if not party_exists? and not pc_exists? do
|
||||
{:error, :not_found}
|
||||
else
|
||||
party_result = if party_exists?, do: PartyStore.parse(party_path), else: {:ok, []}
|
||||
pc_result = if pc_exists?, do: PCStore.parse(pc_path), else: {:ok, []}
|
||||
|
||||
case {party_result, pc_result} do
|
||||
{{:ok, party}, {:ok, pc}} ->
|
||||
{:ok, %{uuid: uuid, party: party, pc: pc}}
|
||||
|
||||
{{:error, reason}, _} ->
|
||||
{:error, reason}
|
||||
|
||||
{_, {:error, reason}} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp find_pokemon(%{party: party, pc: pc}, index) when is_integer(index) do
|
||||
party_size = length(party)
|
||||
|
||||
if index < party_size do
|
||||
Enum.at(party, index)
|
||||
else
|
||||
pc_index = index - party_size
|
||||
all_pc_pokemon = Enum.flat_map(pc, fn box -> Map.get(box, :pokemon, []) end)
|
||||
Enum.at(all_pc_pokemon, pc_index)
|
||||
end
|
||||
end
|
||||
|
||||
defp list_uuids_in_dir(dir) do
|
||||
case File.ls(dir) do
|
||||
{:ok, entries} ->
|
||||
entries
|
||||
|> Enum.flat_map(fn entry ->
|
||||
subdir = Path.join(dir, entry)
|
||||
|
||||
case File.dir?(subdir) do
|
||||
true ->
|
||||
case File.ls(subdir) do
|
||||
{:ok, files} ->
|
||||
files
|
||||
|> Enum.filter(&String.ends_with?(&1, ".dat"))
|
||||
|> Enum.reject(&String.ends_with?(&1, ".dat.old"))
|
||||
|> Enum.map(&String.trim_trailing(&1, ".dat"))
|
||||
|> Enum.filter(&Regex.match?(@uuid_regex, &1))
|
||||
|
||||
{:error, _} ->
|
||||
[]
|
||||
end
|
||||
|
||||
false ->
|
||||
if String.ends_with?(entry, ".dat") and not String.ends_with?(entry, ".dat.old") do
|
||||
uuid = String.trim_trailing(entry, ".dat")
|
||||
if Regex.match?(@uuid_regex, uuid), do: [uuid], else: []
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
{:error, _} ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp load_usercache(data_dir) do
|
||||
path = Path.join(data_dir, "usercache.json")
|
||||
|
||||
case File.read(path) do
|
||||
{:ok, data} ->
|
||||
case Jason.decode(data) do
|
||||
{:ok, entries} when is_list(entries) ->
|
||||
Map.new(entries, fn entry ->
|
||||
{Map.get(entry, "uuid", ""), Map.get(entry, "name")}
|
||||
end)
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
end
|
||||
135
lib/cobblemon_ui/cobblemon_fs/nbt.ex
Normal file
135
lib/cobblemon_ui/cobblemon_fs/nbt.ex
Normal file
@@ -0,0 +1,135 @@
|
||||
defmodule CobblemonUi.CobblemonFS.NBT do
|
||||
@moduledoc """
|
||||
Decoder for Minecraft's Named Binary Tag (NBT) format.
|
||||
|
||||
Supports both gzip-compressed and uncompressed NBT files.
|
||||
All multi-byte integers are big-endian and signed unless noted.
|
||||
"""
|
||||
|
||||
# NBT tag type constants
|
||||
@tag_end 0
|
||||
@tag_byte 1
|
||||
@tag_short 2
|
||||
@tag_int 3
|
||||
@tag_long 4
|
||||
@tag_float 5
|
||||
@tag_double 6
|
||||
@tag_byte_array 7
|
||||
@tag_string 8
|
||||
@tag_list 9
|
||||
@tag_compound 10
|
||||
@tag_int_array 11
|
||||
@tag_long_array 12
|
||||
|
||||
@doc """
|
||||
Decodes an NBT binary (possibly gzip-compressed) into an Elixir map.
|
||||
|
||||
Returns `{:ok, {name, value}}` or `{:error, reason}`.
|
||||
"""
|
||||
@spec decode(binary()) :: {:ok, {String.t(), map()}} | {:error, term()}
|
||||
def decode(data) when is_binary(data) do
|
||||
data = maybe_decompress(data)
|
||||
|
||||
case decode_named_tag(data) do
|
||||
{:ok, {name, value}, _rest} -> {:ok, {name, value}}
|
||||
{:error, _} = err -> err
|
||||
end
|
||||
rescue
|
||||
e -> {:error, {:decode_failed, Exception.message(e)}}
|
||||
end
|
||||
|
||||
defp maybe_decompress(<<0x1F, 0x8B, _rest::binary>> = data), do: :zlib.gunzip(data)
|
||||
defp maybe_decompress(data), do: data
|
||||
|
||||
# Decodes a single named tag: type byte + name + payload
|
||||
defp decode_named_tag(<<type, rest::binary>>) do
|
||||
with {:ok, name, rest} <- decode_string_payload(rest),
|
||||
{:ok, value, rest} <- decode_payload(type, rest) do
|
||||
{:ok, {name, value}, rest}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_named_tag(_), do: {:error, :unexpected_eof}
|
||||
|
||||
# String payload: 2-byte unsigned length prefix + UTF-8 bytes
|
||||
defp decode_string_payload(<<len::unsigned-big-16, str::binary-size(len), rest::binary>>) do
|
||||
{:ok, str, rest}
|
||||
end
|
||||
|
||||
defp decode_string_payload(_), do: {:error, :invalid_string}
|
||||
|
||||
# --- Payload decoders by tag type ---
|
||||
|
||||
defp decode_payload(@tag_byte, <<val::signed-8, rest::binary>>), do: {:ok, val, rest}
|
||||
defp decode_payload(@tag_short, <<val::signed-big-16, rest::binary>>), do: {:ok, val, rest}
|
||||
defp decode_payload(@tag_int, <<val::signed-big-32, rest::binary>>), do: {:ok, val, rest}
|
||||
defp decode_payload(@tag_long, <<val::signed-big-64, rest::binary>>), do: {:ok, val, rest}
|
||||
defp decode_payload(@tag_float, <<val::float-big-32, rest::binary>>), do: {:ok, val, rest}
|
||||
defp decode_payload(@tag_double, <<val::float-big-64, rest::binary>>), do: {:ok, val, rest}
|
||||
|
||||
defp decode_payload(
|
||||
@tag_byte_array,
|
||||
<<len::signed-big-32, bytes::binary-size(len), rest::binary>>
|
||||
) do
|
||||
{:ok, :binary.bin_to_list(bytes), rest}
|
||||
end
|
||||
|
||||
defp decode_payload(@tag_string, data), do: decode_string_payload(data) |> wrap_string_result()
|
||||
|
||||
defp decode_payload(@tag_list, <<elem_type::8, len::signed-big-32, rest::binary>>) do
|
||||
decode_list_items(elem_type, len, rest, [])
|
||||
end
|
||||
|
||||
defp decode_payload(@tag_compound, data), do: decode_compound(data, %{})
|
||||
|
||||
defp decode_payload(@tag_int_array, <<len::signed-big-32, rest::binary>>) do
|
||||
decode_int_array(len, rest, [])
|
||||
end
|
||||
|
||||
defp decode_payload(@tag_long_array, <<len::signed-big-32, rest::binary>>) do
|
||||
decode_long_array(len, rest, [])
|
||||
end
|
||||
|
||||
defp decode_payload(type, _), do: {:error, {:unsupported_tag_type, type}}
|
||||
|
||||
# --- Compound decoder ---
|
||||
|
||||
defp decode_compound(<<@tag_end, rest::binary>>, acc), do: {:ok, acc, rest}
|
||||
|
||||
defp decode_compound(<<type, rest::binary>>, acc) do
|
||||
with {:ok, name, rest} <- decode_string_payload(rest),
|
||||
{:ok, value, rest} <- decode_payload(type, rest) do
|
||||
decode_compound(rest, Map.put(acc, name, value))
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_compound(<<>>, _acc), do: {:error, :unexpected_eof_in_compound}
|
||||
|
||||
# --- List decoder ---
|
||||
|
||||
defp decode_list_items(_type, 0, rest, acc), do: {:ok, Enum.reverse(acc), rest}
|
||||
|
||||
defp decode_list_items(type, n, rest, acc) when n > 0 do
|
||||
with {:ok, value, rest} <- decode_payload(type, rest) do
|
||||
decode_list_items(type, n - 1, rest, [value | acc])
|
||||
end
|
||||
end
|
||||
|
||||
# --- Array decoders ---
|
||||
|
||||
defp decode_int_array(0, rest, acc), do: {:ok, Enum.reverse(acc), rest}
|
||||
|
||||
defp decode_int_array(n, <<val::signed-big-32, rest::binary>>, acc) do
|
||||
decode_int_array(n - 1, rest, [val | acc])
|
||||
end
|
||||
|
||||
defp decode_long_array(0, rest, acc), do: {:ok, Enum.reverse(acc), rest}
|
||||
|
||||
defp decode_long_array(n, <<val::signed-big-64, rest::binary>>, acc) do
|
||||
decode_long_array(n - 1, rest, [val | acc])
|
||||
end
|
||||
|
||||
# Helper to wrap string decode result to match expected return shape
|
||||
defp wrap_string_result({:ok, str, rest}), do: {:ok, str, rest}
|
||||
defp wrap_string_result(err), do: err
|
||||
end
|
||||
30
lib/cobblemon_ui/cobblemon_fs/party_store.ex
Normal file
30
lib/cobblemon_ui/cobblemon_fs/party_store.ex
Normal file
@@ -0,0 +1,30 @@
|
||||
defmodule CobblemonUi.CobblemonFS.PartyStore do
|
||||
@moduledoc """
|
||||
Parses a player's party storage `.dat` file.
|
||||
|
||||
Party layout:
|
||||
Root -> party -> slot0..slot5
|
||||
"""
|
||||
|
||||
alias CobblemonUi.CobblemonFS.{NBT, Pokemon}
|
||||
|
||||
@party_slots 6
|
||||
|
||||
@doc """
|
||||
Reads and parses a party `.dat` file at the given path.
|
||||
|
||||
Returns `{:ok, [pokemon | nil]}` or `{:error, reason}`.
|
||||
"""
|
||||
@spec parse(String.t()) :: {:ok, list(map() | nil)} | {:error, term()}
|
||||
def parse(path) do
|
||||
with {:ok, data} <- File.read(path),
|
||||
{:ok, {_name, root}} <- NBT.decode(data) do
|
||||
party = Map.get(root, "party", %{})
|
||||
slots = for i <- 0..(@party_slots - 1), do: Pokemon.normalize(Map.get(party, "slot#{i}"))
|
||||
{:ok, slots}
|
||||
else
|
||||
{:error, :enoent} -> {:error, :not_found}
|
||||
{:error, reason} -> {:error, {:corrupt_data, reason}}
|
||||
end
|
||||
end
|
||||
end
|
||||
57
lib/cobblemon_ui/cobblemon_fs/pc_store.ex
Normal file
57
lib/cobblemon_ui/cobblemon_fs/pc_store.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defmodule CobblemonUi.CobblemonFS.PCStore do
|
||||
@moduledoc """
|
||||
Parses a player's PC storage `.dat` file.
|
||||
|
||||
PC layout:
|
||||
Root -> boxes -> box0..boxN -> slot0..slotN
|
||||
"""
|
||||
|
||||
alias CobblemonUi.CobblemonFS.{NBT, Pokemon}
|
||||
|
||||
@doc """
|
||||
Reads and parses a PC storage `.dat` file at the given path.
|
||||
|
||||
Returns `{:ok, [%{box: integer, pokemon: list}]}` or `{:error, reason}`.
|
||||
"""
|
||||
@spec parse(String.t()) :: {:ok, list(map())} | {:error, term()}
|
||||
def parse(path) do
|
||||
with {:ok, data} <- File.read(path),
|
||||
{:ok, {_name, root}} <- NBT.decode(data) do
|
||||
boxes = Map.get(root, "boxes", %{})
|
||||
{:ok, normalize_boxes(boxes)}
|
||||
else
|
||||
{:error, :enoent} -> {:error, :not_found}
|
||||
{:error, reason} -> {:error, {:corrupt_data, reason}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_boxes(boxes) when is_map(boxes) do
|
||||
boxes
|
||||
|> Enum.filter(fn {key, _} -> String.starts_with?(key, "box") end)
|
||||
|> Enum.sort_by(fn {key, _} -> extract_index(key) end)
|
||||
|> Enum.map(fn {key, box_data} ->
|
||||
%{
|
||||
box: extract_index(key),
|
||||
pokemon: normalize_box_slots(box_data)
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp normalize_boxes(_), do: []
|
||||
|
||||
defp normalize_box_slots(box) when is_map(box) do
|
||||
box
|
||||
|> Enum.filter(fn {key, _} -> String.starts_with?(key, "slot") end)
|
||||
|> Enum.sort_by(fn {key, _} -> extract_index(key) end)
|
||||
|> Enum.map(fn {_key, slot_data} -> Pokemon.normalize(slot_data) end)
|
||||
end
|
||||
|
||||
defp normalize_box_slots(_), do: []
|
||||
|
||||
defp extract_index(key) do
|
||||
case Integer.parse(String.replace(key, ~r/[^\d]/, "")) do
|
||||
{n, _} -> n
|
||||
:error -> 0
|
||||
end
|
||||
end
|
||||
end
|
||||
76
lib/cobblemon_ui/cobblemon_fs/pokemon.ex
Normal file
76
lib/cobblemon_ui/cobblemon_fs/pokemon.ex
Normal file
@@ -0,0 +1,76 @@
|
||||
defmodule CobblemonUi.CobblemonFS.Pokemon do
|
||||
@moduledoc """
|
||||
Normalizes raw NBT Pokémon compound data into a structured map.
|
||||
"""
|
||||
|
||||
@stat_keys ~w(hp attack defense special_attack special_defense speed)
|
||||
|
||||
@doc """
|
||||
Normalizes a raw NBT compound map representing a single Pokémon
|
||||
into the standardized format.
|
||||
|
||||
Returns `nil` if the input is `nil` (empty slot).
|
||||
"""
|
||||
@spec normalize(map() | nil) :: map() | nil
|
||||
def normalize(nil), do: nil
|
||||
|
||||
def normalize(raw) when is_map(raw) do
|
||||
%{
|
||||
species: get_string(raw, "species"),
|
||||
level: get_int(raw, "level"),
|
||||
form: get_string(raw, "form", "default"),
|
||||
shiny: get_boolean(raw, "shiny"),
|
||||
nature: get_string(raw, "nature"),
|
||||
gender: get_string(raw, "gender"),
|
||||
experience: get_int(raw, "experience"),
|
||||
friendship: get_int(raw, "friendship"),
|
||||
ability: get_string(raw, "ability"),
|
||||
ivs: normalize_stats(Map.get(raw, "ivs")),
|
||||
evs: normalize_stats(Map.get(raw, "evs")),
|
||||
moves: normalize_moves(Map.get(raw, "moves"))
|
||||
}
|
||||
end
|
||||
|
||||
defp normalize_stats(nil), do: nil
|
||||
|
||||
defp normalize_stats(stats) when is_map(stats) do
|
||||
Map.new(@stat_keys, fn key -> {String.to_atom(key), get_int(stats, key, 0)} end)
|
||||
end
|
||||
|
||||
defp normalize_moves(nil), do: []
|
||||
|
||||
defp normalize_moves(moves) when is_list(moves) do
|
||||
Enum.map(moves, fn
|
||||
move when is_map(move) -> get_string(move, "id")
|
||||
move when is_binary(move) -> move
|
||||
_ -> nil
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp normalize_moves(_), do: []
|
||||
|
||||
defp get_string(map, key, default \\ nil) do
|
||||
case Map.get(map, key) do
|
||||
val when is_binary(val) -> val
|
||||
_ -> default
|
||||
end
|
||||
end
|
||||
|
||||
defp get_int(map, key, default \\ nil) do
|
||||
case Map.get(map, key) do
|
||||
val when is_integer(val) -> val
|
||||
_ -> default
|
||||
end
|
||||
end
|
||||
|
||||
defp get_boolean(map, key) do
|
||||
case Map.get(map, key) do
|
||||
1 -> true
|
||||
0 -> false
|
||||
true -> true
|
||||
false -> false
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user