data displaying properly now
This commit is contained in:
31
cobblemon.md
Normal file
31
cobblemon.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Cobblemon UI — Project Reference
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
Phoenix 1.8 LiveView app (Elixir). Dark mode daisyUI. Docker dev setup with SSHFS mount to remote Minecraft server.
|
||||||
|
|
||||||
|
## Data Source
|
||||||
|
`.cobblemon-data/` — SSHFS mount of remote Minecraft server root (`dev.sh` mounts it). Mounted into Docker container at `/cobblemon-data`.
|
||||||
|
|
||||||
|
### Key server files
|
||||||
|
- `usercache.json` — JSON array of `{"name", "uuid", "expiresOn"}`. Maps UUIDs to player names.
|
||||||
|
- `world/pokemon/playerpartystore/<2-char-prefix>/<uuid>.dat` — NBT binary, player's party (6 slots).
|
||||||
|
- `world/pokemon/pcstore/<2-char-prefix>/<uuid>.dat` — NBT binary, player's PC boxes (30 boxes × 30 slots).
|
||||||
|
- `world/playerdata/<uuid>.dat` — vanilla Minecraft player data (not currently parsed).
|
||||||
|
|
||||||
|
**Important:** `.dat` files use 2-character UUID prefix subdirectories (e.g. `54/54e75a91-...dat`).
|
||||||
|
|
||||||
|
## App Modules
|
||||||
|
|
||||||
|
### `lib/cobblemon_ui/cobblemon_fs/`
|
||||||
|
- `cobblemon_fs.ex` — GenServer. Cached player data access. Reads from `/cobblemon-data`. Parses `usercache.json` for names. API: `list_players/0`, `get_player/1`, `get_party/1`, `get_pc/1`, `get_pokemon/2`.
|
||||||
|
- `nbt.ex` — Pure Elixir NBT decoder. Handles gzip + uncompressed. All 13 tag types.
|
||||||
|
- `pokemon.ex` — Normalizes raw NBT compound → `%{species, level, form, shiny, nature, gender, ivs, evs, moves, ...}`.
|
||||||
|
- `party_store.ex` — Parses party `.dat` → list of 6 pokemon slots.
|
||||||
|
- `pc_store.ex` — Parses PC `.dat` → list of `%{box: n, pokemon: [...]}`.
|
||||||
|
|
||||||
|
### `lib/cobblemon_ui_web/live/`
|
||||||
|
- `dashboard_live.ex` — Main LiveView. Player sidebar (names from usercache), party/PC tabs, pokemon detail panel with IV/EV bars.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
- `docker-compose.yml` — Dev container. Bind mounts `.cobblemon-data` → `/cobblemon-data` (needs `allow_other` SSHFS).
|
||||||
|
- `dev.sh` — SSHFS mount script for `.cobblemon-data`.
|
||||||
@@ -19,8 +19,7 @@ defmodule CobblemonUi.CobblemonFS.PartyStore do
|
|||||||
def parse(path) do
|
def parse(path) do
|
||||||
with {:ok, data} <- File.read(path),
|
with {:ok, data} <- File.read(path),
|
||||||
{:ok, {_name, root}} <- NBT.decode(data) do
|
{:ok, {_name, root}} <- NBT.decode(data) do
|
||||||
party = Map.get(root, "party", %{})
|
slots = for i <- 0..(@party_slots - 1), do: Pokemon.normalize(Map.get(root, "Slot#{i}"))
|
||||||
slots = for i <- 0..(@party_slots - 1), do: Pokemon.normalize(Map.get(party, "slot#{i}"))
|
|
||||||
{:ok, slots}
|
{:ok, slots}
|
||||||
else
|
else
|
||||||
{:error, :enoent} -> {:error, :not_found}
|
{:error, :enoent} -> {:error, :not_found}
|
||||||
|
|||||||
@@ -17,17 +17,18 @@ defmodule CobblemonUi.CobblemonFS.PCStore do
|
|||||||
def parse(path) do
|
def parse(path) do
|
||||||
with {:ok, data} <- File.read(path),
|
with {:ok, data} <- File.read(path),
|
||||||
{:ok, {_name, root}} <- NBT.decode(data) do
|
{:ok, {_name, root}} <- NBT.decode(data) do
|
||||||
boxes = Map.get(root, "boxes", %{})
|
{:ok, normalize_boxes(root)}
|
||||||
{:ok, normalize_boxes(boxes)}
|
|
||||||
else
|
else
|
||||||
{:error, :enoent} -> {:error, :not_found}
|
{:error, :enoent} -> {:error, :not_found}
|
||||||
{:error, reason} -> {:error, {:corrupt_data, reason}}
|
{:error, reason} -> {:error, {:corrupt_data, reason}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_boxes(boxes) when is_map(boxes) do
|
defp normalize_boxes(root) when is_map(root) do
|
||||||
boxes
|
root
|
||||||
|> Enum.filter(fn {key, _} -> String.starts_with?(key, "box") end)
|
|> Enum.filter(fn {key, val} ->
|
||||||
|
String.match?(key, ~r/^Box\d+$/) and is_map(val) and map_size(val) > 1
|
||||||
|
end)
|
||||||
|> Enum.sort_by(fn {key, _} -> extract_index(key) end)
|
|> Enum.sort_by(fn {key, _} -> extract_index(key) end)
|
||||||
|> Enum.map(fn {key, box_data} ->
|
|> Enum.map(fn {key, box_data} ->
|
||||||
%{
|
%{
|
||||||
@@ -41,7 +42,7 @@ defmodule CobblemonUi.CobblemonFS.PCStore do
|
|||||||
|
|
||||||
defp normalize_box_slots(box) when is_map(box) do
|
defp normalize_box_slots(box) when is_map(box) do
|
||||||
box
|
box
|
||||||
|> Enum.filter(fn {key, _} -> String.starts_with?(key, "slot") end)
|
|> Enum.filter(fn {key, _} -> String.match?(key, ~r/^Slot\d+$/) end)
|
||||||
|> Enum.sort_by(fn {key, _} -> extract_index(key) end)
|
|> Enum.sort_by(fn {key, _} -> extract_index(key) end)
|
||||||
|> Enum.map(fn {_key, slot_data} -> Pokemon.normalize(slot_data) end)
|
|> Enum.map(fn {_key, slot_data} -> Pokemon.normalize(slot_data) end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ defmodule CobblemonUi.CobblemonFS.Pokemon do
|
|||||||
Normalizes raw NBT Pokémon compound data into a structured map.
|
Normalizes raw NBT Pokémon compound data into a structured map.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@stat_keys ~w(hp attack defense special_attack special_defense speed)
|
@stat_keys ~w(hp attack defence special_attack special_defence speed)
|
||||||
|
@stat_display_keys %{
|
||||||
|
"hp" => :hp,
|
||||||
|
"attack" => :attack,
|
||||||
|
"defence" => :defense,
|
||||||
|
"special_attack" => :special_attack,
|
||||||
|
"special_defence" => :special_defense,
|
||||||
|
"speed" => :speed
|
||||||
|
}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Normalizes a raw NBT compound map representing a single Pokémon
|
Normalizes a raw NBT compound map representing a single Pokémon
|
||||||
@@ -16,34 +24,44 @@ defmodule CobblemonUi.CobblemonFS.Pokemon do
|
|||||||
|
|
||||||
def normalize(raw) when is_map(raw) do
|
def normalize(raw) when is_map(raw) do
|
||||||
%{
|
%{
|
||||||
species: get_string(raw, "species"),
|
species: strip_namespace(get_string(raw, "Species")),
|
||||||
level: get_int(raw, "level"),
|
level: get_int(raw, "Level"),
|
||||||
form: get_string(raw, "form", "default"),
|
form: get_string(raw, "FormId", "default"),
|
||||||
shiny: get_boolean(raw, "shiny"),
|
shiny: get_boolean(raw, "Shiny"),
|
||||||
nature: get_string(raw, "nature"),
|
nature: strip_namespace(get_string(raw, "Nature")),
|
||||||
gender: get_string(raw, "gender"),
|
gender: downcase(get_string(raw, "Gender")),
|
||||||
experience: get_int(raw, "experience"),
|
experience: get_int(raw, "Experience"),
|
||||||
friendship: get_int(raw, "friendship"),
|
friendship: get_int(raw, "Friendship"),
|
||||||
ability: get_string(raw, "ability"),
|
ability: extract_ability(Map.get(raw, "Ability")),
|
||||||
ivs: normalize_stats(Map.get(raw, "ivs")),
|
ivs: normalize_stats(nested_get(raw, ["IVs", "Base"])),
|
||||||
evs: normalize_stats(Map.get(raw, "evs")),
|
evs: normalize_stats(Map.get(raw, "EVs")),
|
||||||
moves: normalize_moves(Map.get(raw, "moves"))
|
moves: normalize_moves(Map.get(raw, "MoveSet"))
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_stats(nil), do: nil
|
defp normalize_stats(nil), do: nil
|
||||||
|
|
||||||
defp normalize_stats(stats) when is_map(stats) do
|
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)
|
Map.new(@stat_keys, fn key ->
|
||||||
|
# Stats may be keyed with "cobblemon:" namespace prefix
|
||||||
|
val = Map.get(stats, "cobblemon:#{key}", Map.get(stats, key, 0))
|
||||||
|
display_key = Map.get(@stat_display_keys, key, String.to_atom(key))
|
||||||
|
{display_key, val}
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_moves(nil), do: []
|
defp normalize_moves(nil), do: []
|
||||||
|
|
||||||
defp normalize_moves(moves) when is_list(moves) do
|
defp normalize_moves(moves) when is_list(moves) do
|
||||||
Enum.map(moves, fn
|
Enum.map(moves, fn
|
||||||
move when is_map(move) -> get_string(move, "id")
|
move when is_map(move) ->
|
||||||
move when is_binary(move) -> move
|
get_string(move, "MoveName") || get_string(move, "id")
|
||||||
_ -> nil
|
|
||||||
|
move when is_binary(move) ->
|
||||||
|
move
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
end
|
end
|
||||||
@@ -73,4 +91,26 @@ defmodule CobblemonUi.CobblemonFS.Pokemon do
|
|||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp strip_namespace(nil), do: nil
|
||||||
|
|
||||||
|
defp strip_namespace(val) when is_binary(val) do
|
||||||
|
case String.split(val, ":", parts: 2) do
|
||||||
|
[_ns, name] -> name
|
||||||
|
_ -> val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp downcase(nil), do: nil
|
||||||
|
defp downcase(val) when is_binary(val), do: String.downcase(val)
|
||||||
|
|
||||||
|
defp extract_ability(nil), do: nil
|
||||||
|
defp extract_ability(%{"AbilityName" => name}) when is_binary(name), do: name
|
||||||
|
defp extract_ability(val) when is_binary(val), do: val
|
||||||
|
defp extract_ability(_), do: nil
|
||||||
|
|
||||||
|
defp nested_get(map, []), do: map
|
||||||
|
defp nested_get(nil, _keys), do: nil
|
||||||
|
defp nested_get(map, [key | rest]) when is_map(map), do: nested_get(Map.get(map, key), rest)
|
||||||
|
defp nested_get(_map, _keys), do: nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
players =
|
players =
|
||||||
case CobblemonUi.CobblemonFS.list_players() do
|
case CobblemonUi.CobblemonFS.list_players() do
|
||||||
{:ok, list} -> list
|
{:ok, list} -> list
|
||||||
_ -> []
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
@@ -86,7 +85,6 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
players =
|
players =
|
||||||
case CobblemonUi.CobblemonFS.list_players() do
|
case CobblemonUi.CobblemonFS.list_players() do
|
||||||
{:ok, list} -> list
|
{:ok, list} -> list
|
||||||
_ -> []
|
|
||||||
end
|
end
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
|
|||||||
Reference in New Issue
Block a user