From 43dc14745f88357b6fabda54c6f054483548f4a1 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Mon, 16 Mar 2026 12:52:48 -0600 Subject: [PATCH] data displaying properly now --- cobblemon.md | 31 ++++++++ lib/cobblemon_ui/cobblemon_fs/party_store.ex | 3 +- lib/cobblemon_ui/cobblemon_fs/pc_store.ex | 13 ++-- lib/cobblemon_ui/cobblemon_fs/pokemon.ex | 74 +++++++++++++++----- lib/cobblemon_ui_web/live/dashboard_live.ex | 2 - 5 files changed, 96 insertions(+), 27 deletions(-) create mode 100644 cobblemon.md diff --git a/cobblemon.md b/cobblemon.md new file mode 100644 index 0000000..96a07b4 --- /dev/null +++ b/cobblemon.md @@ -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>/.dat` — NBT binary, player's party (6 slots). +- `world/pokemon/pcstore/<2-char-prefix>/.dat` — NBT binary, player's PC boxes (30 boxes × 30 slots). +- `world/playerdata/.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`. diff --git a/lib/cobblemon_ui/cobblemon_fs/party_store.ex b/lib/cobblemon_ui/cobblemon_fs/party_store.ex index b984cf8..e2cd122 100644 --- a/lib/cobblemon_ui/cobblemon_fs/party_store.ex +++ b/lib/cobblemon_ui/cobblemon_fs/party_store.ex @@ -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} diff --git a/lib/cobblemon_ui/cobblemon_fs/pc_store.ex b/lib/cobblemon_ui/cobblemon_fs/pc_store.ex index 1b1768d..a52dc08 100644 --- a/lib/cobblemon_ui/cobblemon_fs/pc_store.ex +++ b/lib/cobblemon_ui/cobblemon_fs/pc_store.ex @@ -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 diff --git a/lib/cobblemon_ui/cobblemon_fs/pokemon.ex b/lib/cobblemon_ui/cobblemon_fs/pokemon.ex index fda47e3..18a743e 100644 --- a/lib/cobblemon_ui/cobblemon_fs/pokemon.ex +++ b/lib/cobblemon_ui/cobblemon_fs/pokemon.ex @@ -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 diff --git a/lib/cobblemon_ui_web/live/dashboard_live.ex b/lib/cobblemon_ui_web/live/dashboard_live.ex index 1f7e85c..519f76d 100644 --- a/lib/cobblemon_ui_web/live/dashboard_live.ex +++ b/lib/cobblemon_ui_web/live/dashboard_live.ex @@ -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 =