data displaying properly now

This commit is contained in:
2026-03-16 12:52:48 -06:00
parent c252ef0e11
commit 43dc14745f
5 changed files with 96 additions and 27 deletions

31
cobblemon.md Normal file
View 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`.

View File

@@ -19,8 +19,7 @@ defmodule CobblemonUi.CobblemonFS.PartyStore do
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}"))
slots = for i <- 0..(@party_slots - 1), do: Pokemon.normalize(Map.get(root, "Slot#{i}"))
{:ok, slots}
else
{:error, :enoent} -> {:error, :not_found}

View File

@@ -17,17 +17,18 @@ defmodule CobblemonUi.CobblemonFS.PCStore do
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)}
{:ok, normalize_boxes(root)}
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)
defp normalize_boxes(root) when is_map(root) do
root
|> 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.map(fn {key, box_data} ->
%{
@@ -41,7 +42,7 @@ defmodule CobblemonUi.CobblemonFS.PCStore do
defp normalize_box_slots(box) when is_map(box) do
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.map(fn {_key, slot_data} -> Pokemon.normalize(slot_data) end)
end

View File

@@ -3,7 +3,15 @@ defmodule CobblemonUi.CobblemonFS.Pokemon do
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 """
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
%{
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"))
species: strip_namespace(get_string(raw, "Species")),
level: get_int(raw, "Level"),
form: get_string(raw, "FormId", "default"),
shiny: get_boolean(raw, "Shiny"),
nature: strip_namespace(get_string(raw, "Nature")),
gender: downcase(get_string(raw, "Gender")),
experience: get_int(raw, "Experience"),
friendship: get_int(raw, "Friendship"),
ability: extract_ability(Map.get(raw, "Ability")),
ivs: normalize_stats(nested_get(raw, ["IVs", "Base"])),
evs: normalize_stats(Map.get(raw, "EVs")),
moves: normalize_moves(Map.get(raw, "MoveSet"))
}
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)
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
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
move when is_map(move) ->
get_string(move, "MoveName") || get_string(move, "id")
move when is_binary(move) ->
move
_ ->
nil
end)
|> Enum.reject(&is_nil/1)
end
@@ -73,4 +91,26 @@ defmodule CobblemonUi.CobblemonFS.Pokemon do
_ -> false
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

View File

@@ -6,7 +6,6 @@ defmodule CobblemonUiWeb.DashboardLive do
players =
case CobblemonUi.CobblemonFS.list_players() do
{:ok, list} -> list
_ -> []
end
{:ok,
@@ -86,7 +85,6 @@ defmodule CobblemonUiWeb.DashboardLive do
players =
case CobblemonUi.CobblemonFS.list_players() do
{:ok, list} -> list
_ -> []
end
socket =