> For the complete documentation index, see [llms.txt](https://xpressdev.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xpressdev.gitbook.io/docs/mods/prettyprofiles.md).

# PrettyProfiles

PrettyProfiles adds **player profiles**, a **friends system**, and a **badge collection** to Cobblemon. Right-click any player to see their profile head and displayed badges; open the Friends menu to manage requests without touching chat.

Every icon, message and badge is config-driven - nothing is hardcoded, and everything reloads live with `/pprofiles reload`.

***

## Features

* **Profile GUI** - right-click any player (empty hand) to view their profile head and displayed badges. Works for offline players too via `/profile <name>`.
* **Friends system** - send, accept, deny and cancel friend requests, from chat commands or the in-game Friends menu. Friends are notified when the other logs in or out.
* **Live GUI refresh** - profile and friends screens update in real time when the underlying data changes, e.g. a friend request comes in while you're looking at that player's profile.
* **Badges** - server owners define a catalog of badges and award them with a command. Players choose which owned badges display on their profile, visible to anyone viewing it. Pokédex progress and an economy balance are just two ordinary preloaded badges, not fixed icons - a server owner can delete, re-skin, or leave them out entirely.
* **Fully re-skinnable GUIs** - every icon (item, texture, name, lore) is defined in config.
* **Self-migrating config** - upgrading from a 1.x install picks up the old `lang.json` and friend data automatically. No manual file edits required.

{% hint style="success" %}
Configs from older versions load fine and are migrated to the current format automatically - nothing needs hand-editing.
{% endhint %}

***

## How it works

### Profiles

Right-clicking another player with an empty hand opens their profile, subject to a per-player cooldown (`interactions.profileClickCooldownTicks`). The cooldown is tracked **per player** - one player interacting doesn't stall anyone else.

A profile shows the subject's head and up to `display.maxDisplayedBadges` displayed badges. Opening your own profile also shows a **Manage Badges** button; opening someone else's shows an add/remove friend button instead, based on your current relationship with them.

Profiles opened for **offline** players (`/profile <name>`) resolve placeholders against that player's stored profile rather than a live session - placeholders that need an online player (e.g. live playtime) may not resolve for offline subjects, depending on how the placeholder itself is implemented.

### Badges

A badge is an item with a name, lore and an id, defined once in config and owned per-player. Owning a badge doesn't display it - from the **Manage Badges** GUI, a player picks which of their owned badges show on their profile, up to `display.maxDisplayedBadges`. Anyone viewing that profile sees exactly what was picked.

Badges ship with three defaults so the catalog isn't empty on a fresh install - delete or replace any of them freely:

| Id                 | Purpose                                                      |
| ------------------ | ------------------------------------------------------------ |
| `test_badge`       | No behaviour, just confirms the badge system is wired up.    |
| `pokedex_progress` | Lore uses the caught/total Pokédex placeholders - see below. |
| `balance`          | Lore uses an Impactor economy placeholder - see below.       |

{% hint style="info" %}
Awarding a badge that doesn't exist in config errors out, and a player who already owns a badge can't be given it again - `/pprofiles badge give` reports both cases back to whoever ran it.
{% endhint %}

### The badge/head layout adapts to `maxDisplayedBadges`

The profile GUI's layout isn't fixed - it reshapes around how many badges a server allows a player to display at once:

| `maxDisplayedBadges` | Layout                                                                |
| -------------------- | --------------------------------------------------------------------- |
| `0`                  | No badge row at all - the player head moves to the center of the GUI. |
| `1`-`3`              | One row of badge slots next to the head.                              |
| `4`-`6`              | A second row is added below the first.                                |
| `7`-`9`              | A third row is added. Values above `9` are clamped to `9`.            |

### Placeholder lore

Any lore line, on a badge or a GUI icon, is run through [Text Placeholder API](https://github.com/Patbox/TextPlaceholderAPI) - the same generic mechanism Spigot's PlaceholderAPI provides, but for the Fabric ecosystem. Any placeholder registered by **any** other installed mod (`%namespace:placeholder%` syntax) works automatically, with zero PrettyProfiles-side code or config beyond pasting the placeholder into a lore line.

```json
"pokedex_progress": {
  "name": "<red><bold>Pokedex Progress",
  "lore": ["%prettyprofiles:dex_caught%/%prettyprofiles:dex_total%"],
  "item": "cobblemon:poke_ball"
}
```

PrettyProfiles itself registers two placeholders for the Pokédex badge - `%prettyprofiles:dex_caught%` and `%prettyprofiles:dex_total%` - reading Cobblemon's Pokédex API directly, honouring `display.dexTotalOverride` if set. Everything else, including the `balance` badge's `%impactor:account%`, comes from whatever economy or utility mod the server has installed; PrettyProfiles has no economy dependency of its own.

{% hint style="warning" %}
If the placeholder isn't registered by anything installed, it renders literally instead of crashing - you'll see the raw `%namespace:placeholder%` text in the tooltip, which is a config problem, not a bug report.
{% endhint %}

{% hint style="info" %}
**Impactor's balance placeholder caches asynchronously.** `%impactor:account%` may briefly show "Fetching balance..." on the very first view after a long idle gap - Impactor's own cache expires after 10 seconds and the read is non-blocking by design. Reopening the profile shows the real value.
{% endhint %}

***

## Commands

| Command                               | Description                                    | Permission               |
| ------------------------------------- | ---------------------------------------------- | ------------------------ |
| `/profile`                            | Open your own profile.                         | `pprofiles.use`          |
| `/profile <player>`                   | Open another player's profile (works offline). | `pprofiles.use.other`    |
| `/friends`                            | Open the Friends menu GUI.                     | `pprofiles.use.friends`  |
| `/friends add <player>`               | Send a friend request.                         | `pprofiles.use.friends`  |
| `/friends remove <player>`            | Remove an existing friend.                     | `pprofiles.use.friends`  |
| `/friends accept <player>`            | Accept a pending friend request.               | `pprofiles.use.friends`  |
| `/friends deny <player>`              | Deny a pending friend request.                 | `pprofiles.use.friends`  |
| `/friends cancel <player>`            | Cancel a friend request you sent.              | `pprofiles.use.friends`  |
| `/pprofiles reload`                   | Reload every config file.                      | `pprofiles.admin.reload` |
| `/pprofiles badge give <id> <player>` | Award a badge to an online player.             | `pprofiles.admin.badge`  |

Players manage which of their owned badges are displayed from the **Manage Badges** button on their own profile - no chat command, GUI only.

***

## Permissions

Uses [LuckPerms' Fabric permissions API](https://github.com/lucko/fabric-permissions-api), falling back to permission level 2 (operator) where noted.

| Node                     | Default   | Notes                                                                            |
| ------------------------ | --------- | -------------------------------------------------------------------------------- |
| `pprofiles.use`          | everyone  | Open your own profile.                                                           |
| `pprofiles.use.other`    | everyone  | Open another player's profile.                                                   |
| `pprofiles.use.friends`  | everyone  | Use `/friends` and all its subcommands.                                          |
| `pprofiles.admin.reload` | operators | Also granted to anyone at permission level 2, regardless of the permission node. |
| `pprofiles.admin.badge`  | operators | Also granted to anyone at permission level 2, regardless of the permission node. |

***

## Configuration

Config files live in `config/PrettyProfiles/` and are created on first run.

| File          | Purpose                                    |
| ------------- | ------------------------------------------ |
| `config.json` | Interaction cooldown and display settings. |
| `gui.json`    | Every icon in both GUIs.                   |
| `badges.json` | The badge catalog.                         |
| `lang.json`   | Every player-facing message.               |

Edit a file and run `/pprofiles reload`, or restart the server, to apply changes.

### `config.json`

| Key                                      | Default | Description                                                                                                                                                  |
| ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `interactions.profileClickCooldownTicks` | `20`    | Cooldown (in ticks, 20 = 1 second) between right-click profile opens, per player.                                                                            |
| `display.dexTotalOverride`               | `0`     | Fixed Pokédex total shown by the `pokedex_progress` badge's `%prettyprofiles:dex_total%` placeholder. `0` asks Cobblemon for the National Dex total instead. |
| `display.maxDisplayedBadges`             | `3`     | How many badges a player may choose to display on their profile at once. Clamped to `0`-`9` - see the layout table above.                                    |

### `gui.json`

Every icon in both GUIs is the same shape:

| Field             | Description                                                                                                                                                                       |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `item`            | Minecraft/Cobblemon item registry id.                                                                                                                                             |
| `texture`         | Optional base64 player-head skin texture. Overrides `item`'s appearance for skulls.                                                                                               |
| `customModelData` | Optional custom model data integer, for resource packs that reskin items by CMD. Omitted (`null`) by default - it will not show up in a fresh config entry unless added manually. |
| `name`            | MiniMessage-formatted display name (`<gold>`, `<bold>`, `&#RRGGBB`, `&c`, `§c` all supported).                                                                                    |
| `lore`            | List of MiniMessage-formatted lines. Supports placeholders - see above.                                                                                                           |

Icons: `defaultFill`, `defaultElementFill`, `secondFill`, `player`, `badgeSlotFiller`, `addFriend`, `removeFriend`, `pendingFriend`, `sentFriend`, `friendsList`, `invitesSent`, `invitesPending`, `back`, `manageBadges`. `badgeSlotFiller` fills any badge slot the player hasn't filled with a displayed badge.

### `badges.json`

Root object keyed by badge id, one entry per badge:

```json
{
  "rock_badge": {
    "name": "<gray><bold>Rock Badge",
    "lore": ["<italic>Awarded for defeating the Rock Gym"],
    "item": "cobblemon:relic_coin_sack",
    "texture": null,
    "customModelData": null
  }
}
```

| Field             | Description                                                                                         |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| `name`            | MiniMessage-formatted badge name.                                                                   |
| `lore`            | List of MiniMessage-formatted lore lines. Runs through the same placeholder pipeline as `gui.json`. |
| `item`            | Minecraft/Cobblemon item registry id.                                                               |
| `texture`         | Optional base64 player-head skin texture.                                                           |
| `customModelData` | Optional custom model data integer. Omitted by default - see above.                                 |

{% hint style="warning" %}
A malformed entry in `badges.json` is skipped and logged rather than crashing the reload - check the server log after editing this file by hand.
{% endhint %}

### `lang.json`

Every player-facing message, MiniMessage-formatted. `{player}` and `{amount}` are substituted per-message where applicable.

| Key                                    | When it's shown                                                         |
| -------------------------------------- | ----------------------------------------------------------------------- |
| `action.friend_added`                  | A friend request is accepted, shown to the accepter.                    |
| `action.friend_removed`                | A friend is removed.                                                    |
| `action.friend_request_sent`           | A friend request is sent.                                               |
| `action.friend_request_received`       | A friend request arrives.                                               |
| `action.friend_request_accepted`       | You accept someone's request.                                           |
| `action.friend_request_accepted_other` | Someone accepts your request.                                           |
| `action.friend_request_denied`         | A friend request is denied.                                             |
| `action.friend_request_cancelled`      | A sent request is cancelled.                                            |
| `action.reloaded_config`               | `/pprofiles reload` succeeds.                                           |
| `action.friend_joined`                 | A friend logs in.                                                       |
| `action.friend_left`                   | A friend logs out.                                                      |
| `action.player_logged_in`              | Login summary: how many friends are online.                             |
| `info.pending_requests`                | Login summary: how many pending requests you have.                      |
| `action.badge_given`                   | `/pprofiles badge give` succeeds, shown to the admin.                   |
| `action.badge_received`                | A badge is given, shown to the recipient.                               |
| `error.player_not_found`               | A player argument doesn't resolve.                                      |
| `error.not_friends`                    | Removing someone who isn't a friend.                                    |
| `error.already_friends`                | Adding someone already a friend.                                        |
| `error.request_already_sent`           | Sending a duplicate request.                                            |
| `error.pending_request_from_player`    | Sending a request to someone who already sent you one.                  |
| `error.no_pending_request`             | Accepting/denying with no pending request.                              |
| `error.badge_not_found`                | `/pprofiles badge give` with an unknown id.                             |
| `error.badge_already_owned`            | Giving a badge the player already owns.                                 |
| `error.badge_display_limit`            | Trying to display more badges than `display.maxDisplayedBadges` allows. |

***

## Requirements

* Fabric Loader `>=0.16.2`
* Fabric API
* Fabric Language Kotlin `>=1.13.6`
* Minecraft `1.21.1`
* Cobblemon `>=1.7.3+1.21.1`

No economy mod is required - the `balance` badge is placeholder-driven and simply shows nothing resolvable if no economy placeholder is registered.
