> For the complete documentation index, see [llms.txt](https://mending-dev.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://mending-dev.gitbook.io/docs/bossbarhud/readme.md).

# Overview

## BossbarHUD

BossbarHUD renders text boxes with a resource pack background at the top of the screen, where the bossbar title is. Every box has the correct length for its text, boxes are placed next to each other and centered automatically. Other plugins can add their own HUDs through a small developer API, and normal bossbars keep working.

* Boxes with rounded background, sized to the text (vanilla font widths, bold, custom glyphs)
* Config HUD with MiniMessage and PlaceholderAPI
* Multiple lines: boxes next to each other and below each other
* Conditions per box: `%player_world% = world_nether && permission=hud.nether`
* The HUD always stays above the bossbars of other plugins
* Nexo glyph support (configure widths by glyph name)
* Developer API: global HUDs, personal HUDs, per-player show/hide, conditions, automatic cleanup

The full documentation is in the [`docs/`](https://github.com/mending-dev/BossbarHUD/tree/main/docs/docs/README.md) folder (GitBook).

### Requirements

| Requirement    | Version                                              |
| -------------- | ---------------------------------------------------- |
| Paper          | 26.x                                                 |
| Java           | 25                                                   |
| PlaceholderAPI | optional, for `%placeholders%` in the config HUD     |
| Nexo           | optional, for configuring glyph widths by glyph name |

***

## Admin guide

### Installation

1. Download `BossbarHUD-<version>.jar` from the [Releases](https://github.com/mending-dev/BossbarHUD/tree/main/releases/README.md) page.
2. Put the jar into your server's `plugins/` folder.
3. Install the resource pack (see [Resource pack](#resource-pack)).
4. Start the server. `plugins/BossbarHUD/config.yml` is created on the first start.

If you use PlaceholderAPI, install the expansions you need, for example:

```
/papi ecloud download Player
/papi reload
```

### Resource pack

The plugin draws the box background with a custom font (`hud:bg`) and hides its own bossbar with transparent sprites. Both are part of a resource pack. You have two options:

| Option                       | Use it if                                                                     |
| ---------------------------- | ----------------------------------------------------------------------------- |
| **A. Bundled resource pack** | You want to get started quickly with the default look.                        |
| **B. Generate your own**     | You want your own colors, a different bossbar color or a different box shape. |

**Important:** every bossbar with the transparent color becomes invisible. Choose a color that no other plugin on your server uses, and set the same color in `bossbar.color` of the config.

#### Option A: bundled resource pack

The resource pack is attached to every [release](https://github.com/mending-dev/BossbarHUD/tree/main/releases/README.md) as a download.

1. Download the resource pack from the files of the release.
2. Add it to your server:
   * If you already use a resource pack (for example the one that Nexo generates), copy the `assets` folder of the bundled pack into it and republish your pack.
   * Otherwise you can use the bundled pack as your server resource pack.
3. Keep `bossbar.color` on `PURPLE` (the default). The bundled pack makes the purple bossbar transparent.

#### Option B: generate your own

Generate the assets with the included script (requires Python 3 and Pillow):

```bash
pip install pillow
python generate_hud_pack.py --out ./pack --fill 2b3a55 --alpha 170 --border 4a5f8a
```

| Option            | Description                            | Default  |
| ----------------- | -------------------------------------- | -------- |
| `--out`           | output folder                          | `./pack` |
| `--fill`          | fill color (hex)                       | `2b3a55` |
| `--alpha`         | fill opacity, 1-255                    | `170`    |
| `--border`        | optional 1px border color (hex)        | none     |
| `--border-alpha`  | border opacity, 1-255                  | `255`    |
| `--bossbar-color` | bossbar color that is made transparent | `purple` |

Copy the generated `assets` folder into your resource pack (for example the pack that Nexo generates for you) and republish the pack. If you change `--bossbar-color`, set the same color in `bossbar.color` of the config.

### Configuration

`plugins/BossbarHUD/config.yml`

```yaml
bossbar:
  # Use a color that no other bossbar on your server uses
  color: PURPLE
  overlay: PROGRESS

update-interval-ticks: 20

layout:
  gap: 4        # pixels between two boxes
  padding: 4    # pixels left and right of the text inside a box
  min-width: 0  # minimum box width in pixels

# Pixel width of glyphs, so the box length stays correct.
# Width = round(texture width * height / texture height) + 1
glyph-widths:
  # Nexo glyph id (as defined in Nexo)
  "nexo:player_icon": 9
  # A raw character works too
  # "\uE000": 9

# Global HUD from the config (PlaceholderAPI + MiniMessage).
# Other plugins can add their own HUDs through the API.
#   line:      row of the box (default 1). Boxes with the same line are placed next to each other.
#   order:     position inside the line, from left to right.
#   condition: optional, the box is only shown to players for whom it is true. Always put it in quotes.
#              Compare with = != > >= < <= (numbers or text), combine with && || ! and ( ),
#              "quoted text" for values with spaces, permission=<node> checks a permission.
segments:
  position:
    line: 1
    order: 20
    text: "<white>Your Position: %player_x%, %player_y%, %player_z%"
  # Second row:
  # player:
  #   line: 2
  #   order: 10
  #   text: "<white>Player: %player_name%"
  # Only in the nether, for players with a permission:
  # nether:
  #   line: 2
  #   order: 20
  #   text: "<red>Nether"
  #   condition: '%player_world% = world_nether && permission=hud.nether'
```

#### Options

| Path                      | Description                                                                                               |
| ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `bossbar.color`           | Bossbar color used for the HUD. Must match the transparent sprites in your resource pack.                 |
| `bossbar.overlay`         | Bossbar overlay (`PROGRESS`, `NOTCHED_6`, `NOTCHED_10`, `NOTCHED_12`, `NOTCHED_20`).                      |
| `update-interval-ticks`   | How often placeholders are re-evaluated. 20 ticks = 1 second.                                             |
| `layout.gap`              | Space in pixels between two boxes.                                                                        |
| `layout.padding`          | Space in pixels between the text and the left/right edge of a box.                                        |
| `layout.min-width`        | Minimum width of a box in pixels.                                                                         |
| `glyph-widths`            | Width in pixels of custom glyphs (see below).                                                             |
| `segments.<id>.line`      | Row of the box. Default `1`. See [Lines](#lines).                                                         |
| `segments.<id>.order`     | Position of the box inside its line, from left to right.                                                  |
| `segments.<id>.text`      | Text of the box. MiniMessage and PlaceholderAPI placeholders are supported.                               |
| `segments.<id>.condition` | Optional. The box is only shown to players for whom the condition is true. See [Conditions](#conditions). |

#### Lines

By default all boxes are placed next to each other in one row. With `line` you can put boxes below each other:

```yaml
segments:
  position:
    line: 1
    order: 20
    text: "<white>Your Position: %player_x%, %player_y%, %player_z%"
  player:
    line: 2
    order: 10
    text: "<white>Player: %player_name%"
```

* `line` is the row of the box. The default is `1`.
* Only the relative order of the lines counts. If the lines are `1` and `5`, there are two rows. A single box is always in the first row, no matter which line is configured.
* Several boxes can share a line. They are placed next to each other, sorted by `order`.
* Boxes of other plugins use the same lines. A box on line 2 from the API ends up in the same row as a config box on line 2.

Every row is its own bossbar with the configured color, so the resource pack hides all of them. The vertical distance between two rows is fixed by the Minecraft client (19 pixels with the default box height) and cannot be changed. The client only draws bossbars in the upper part of the screen (about a third of the height), so the number of rows you can use depends on the GUI scale.

#### Conditions

With `condition` a box is only shown to the players for whom the condition is true. A box without a condition is shown to everyone.

```yaml
segments:
  nether:
    line: 2
    order: 10
    text: "<red>You are in the nether"
    condition: '%player_world% = world_nether'
  health:
    order: 30
    text: "<red>Low health: %player_health%"
    condition: '%player_health% < 6 && %player_gamemode% != CREATIVE'
  vip:
    order: 40
    text: "<gold>VIP"
    condition: 'permission=server.vip || %player_name% = "Notch"'
  not-in-lobby:
    order: 50
    text: "<white>Kills: %statistic_player_kills%"
    condition: '!(%player_world% = lobby || %player_world% = "lobby 2")'
```

| Syntax                                                    | Meaning                                                                           |
| --------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `a = b` or `a == b`                                       | equal                                                                             |
| `a != b`                                                  | not equal                                                                         |
| `a > b`, `a >= b`, `a < b`, `a <= b`                      | greater, greater or equal, less, less or equal                                    |
| `x && y`                                                  | both are true                                                                     |
| `x \|\| y`                                                | at least one is true                                                              |
| `!x`                                                      | not                                                                               |
| `( ... )`                                                 | grouping                                                                          |
| `"text"` or `'text'`                                      | a value with spaces or special characters; `\"`, `\'` and `\\` escape a character |
| `permission=node`, `permission!=node`, `!permission=node` | checks a permission of the player (no LuckPerms needed)                           |
| `%placeholder%` alone                                     | true if the value is `true` or `yes` (any case), for example `%player_is_flying%` |

* If both sides are plain numbers (`12`, `-3`, `0.5`), they are compared as numbers: `%player_level% >= 10`. Otherwise they are compared as text. Text comparison is case-sensitive; `>` and `<` compare text alphabetically.
* `&&` binds stronger than `||`: `a || b && c` means `a || (b && c)`. `!` applies to the comparison right after it, so `!%player_world% = lobby` means "not in the world lobby".
* Always put the condition in quotes. Use single quotes outside, then you can use double quotes inside: `condition: '%server_name% = "My Server"'`. Without quotes, YAML fails on a leading `%` or `!`.
* A value without quotes ends at a space or at one of `( ) & | ! = < > " '`. A placeholder (`%...%` without spaces) is always one value, even if it contains one of these characters.
* Conditions are read on start and on `/bossbarhud reload`. A condition with a syntax error logs one warning that names the box and the position of the error, and the box is hidden until the condition is fixed.
* Conditions are checked for every player on every update (`update-interval-ticks`), before the text of the box is built. A box whose condition is false costs no text rendering.
* **Without PlaceholderAPI** placeholders are not replaced: the condition sees the literal text, so `%player_world% = world_nether` is always false and `%player_world% != world_nether` always true. BossbarHUD logs a warning on load for every box whose condition uses placeholders while PlaceholderAPI is missing. `permission=...` works without PlaceholderAPI.

#### Glyph widths

The plugin knows the widths of the vanilla font. Custom glyphs, such as Nexo glyphs, have their own width, so you have to tell the plugin how wide they are. Otherwise the box is too short or too long.

Use the Nexo glyph id as the key. The `nexo:` prefix is optional:

```yaml
glyph-widths:
  "nexo:player_icon": 9
  weather_icon: 9
```

Width formula: `round(texture width * glyph height / texture height) + 1`. A glyph texture that is 8px wide and has the same height value as its texture height has a width of `9`.

Nexo glyph names are resolved when the server starts and again every time Nexo reloads. Unknown names are logged as warnings.

#### Glyphs and shifts in text

The text of a box supports the Nexo tags `<glyph:id>` and `<shift:pixels>`:

```yaml
segments:
  player:
    text: "<glyph:player_icon> <white>%player_name%"
```

BossbarHUD resolves these tags itself, before it calculates the length of the box. A Nexo glyph used as a tag, or through the PlaceholderAPI placeholder `%nexo_<id>%`, is therefore measured with the width from `glyph-widths`.

* `<glyph:id>` inserts the glyph. Extra arguments of the Nexo tag (index, shadow) are not supported and are ignored.
* `<shift:pixels>` moves the cursor. Positive values move to the right, negative values to the left.
* The glyph is inserted in white, so the texture is not tinted by the text color.
* Unknown glyph ids are logged as a warning.

### Commands and permissions

| Command                                    | Description                         | Permission                  | Default   |
| ------------------------------------------ | ----------------------------------- | --------------------------- | --------- |
| `/bossbarhud toggle` (alias `/hud toggle`) | Hides or shows the HUD for yourself | `bossbarhud.command.toggle` | everyone  |
| `/bossbarhud reload` (alias `/hud reload`) | Reloads `config.yml`                | `bossbarhud.command.reload` | operators |

### Troubleshooting

| Problem                                   | Solution                                                                                                                                                                                                                                            |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Nothing is visible                        | Check that the resource pack with the `hud:bg` font is active and that `bossbar.color` is set.                                                                                                                                                      |
| The bossbar bar itself is visible         | The transparent sprites do not match `bossbar.color`. Regenerate with `--bossbar-color`.                                                                                                                                                            |
| Other plugins' bossbars are invisible     | They use the same color as the HUD. Change `bossbar.color` and the sprites.                                                                                                                                                                         |
| Box is too short or too long with an icon | Add the glyph to `glyph-widths` and write it as `<glyph:id>` or as the raw character.                                                                                                                                                               |
| Placeholders show as `%player_x%`         | PlaceholderAPI or the expansion is missing.                                                                                                                                                                                                         |
| The HUD is below another bossbar          | BossbarHUD moves bars of the Paper API below its rows. Bars of the old Bukkit API (`Bukkit.createBossBar`) and vanilla bars (wither, ender dragon, raid, `/bossbar`) cannot be moved: they stay where they are if they were shown before a HUD row. |
| A box with a condition is never shown     | Look for "has an invalid condition" in the console. Without PlaceholderAPI, placeholders in conditions are compared as plain text.                                                                                                                  |
| Not all lines are visible                 | The client only draws bossbars in the upper part of the screen. Use fewer lines or a smaller GUI scale.                                                                                                                                             |

***

## Developer guide

### Dependency

The API is published to the Maven repository of mending-dev. The artifact contains only interfaces, the implementation is the BossbarHUD plugin on the server.

|             |                                        |
| ----------- | -------------------------------------- |
| Group       | `dev.mending.bossbarhud`               |
| Artifact    | `api`                                  |
| Coordinates | `dev.mending.bossbarhud:api:<version>` |
| Releases    | `https://repo.mending.dev/releases`    |
| Snapshots   | `https://repo.mending.dev/snapshots`   |

#### Which version do I use?

| You want                     | Version                                                    | Repository |
| ---------------------------- | ---------------------------------------------------------- | ---------- |
| A stable release             | the tag without the `v`, e.g. `1.0.0` for the tag `v1.0.0` | releases   |
| The latest development build | the snapshot version, e.g. `1.0.0-SNAPSHOT`                | snapshots  |

A release version never changes. A snapshot version is overwritten with every push to `main` or `develop`.

#### Gradle (Kotlin DSL)

Release:

```kotlin
repositories {
    maven("https://repo.mending.dev/releases") {
        mavenContent { releasesOnly() }
        content { includeGroup("dev.mending.bossbarhud") }
    }
}

dependencies {
    compileOnly("dev.mending.bossbarhud:api:1.0.0")
}
```

Snapshot:

```kotlin
repositories {
    maven("https://repo.mending.dev/snapshots") {
        mavenContent { snapshotsOnly() }
        content { includeGroup("dev.mending.bossbarhud") }
    }
}

// Gradle caches snapshots for 24 hours by default
configurations.all {
    resolutionStrategy.cacheChangingModulesFor(0, "seconds")
}

dependencies {
    compileOnly("dev.mending.bossbarhud:api:1.0.0-SNAPSHOT")
}
```

#### Gradle (Groovy DSL)

Release:

```groovy
repositories {
    maven {
        url = uri("https://repo.mending.dev/releases")
        mavenContent { releasesOnly() }
        content { includeGroup("dev.mending.bossbarhud") }
    }
}

dependencies {
    compileOnly "dev.mending.bossbarhud:api:1.0.0"
}
```

Snapshot:

```groovy
repositories {
    maven {
        url = uri("https://repo.mending.dev/snapshots")
        mavenContent { snapshotsOnly() }
        content { includeGroup("dev.mending.bossbarhud") }
    }
}

// Gradle caches snapshots for 24 hours by default
configurations.all {
    resolutionStrategy.cacheChangingModulesFor 0, "seconds"
}

dependencies {
    compileOnly "dev.mending.bossbarhud:api:1.0.0-SNAPSHOT"
}
```

#### Gradle version catalog

`gradle/libs.versions.toml`:

```toml
[versions]
bossbarhud = "1.0.0"

[libraries]
bossbarhud-api = { module = "dev.mending.bossbarhud:api", version.ref = "bossbarhud" }
```

`build.gradle.kts`:

```kotlin
dependencies {
    compileOnly(libs.bossbarhud.api)
}
```

#### Maven

Release:

```xml
<repositories>
    <repository>
        <id>mending-releases</id>
        <url>https://repo.mending.dev/releases</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>dev.mending.bossbarhud</groupId>
        <artifactId>api</artifactId>
        <version>1.0.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
```

Snapshot:

```xml
<repositories>
    <repository>
        <id>mending-snapshots</id>
        <url>https://repo.mending.dev/snapshots</url>
        <releases>
            <enabled>false</enabled>
        </releases>
        <snapshots>
            <enabled>true</enabled>
            <updatePolicy>always</updatePolicy>
        </snapshots>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>dev.mending.bossbarhud</groupId>
        <artifactId>api</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
```

Use `compileOnly` / `provided`. Never shade the API into your plugin.

#### Updating to a new version

* **Release:** change the version number, then reload the Gradle project in your IDE.
* **Snapshot:** run `./gradlew build --refresh-dependencies` to fetch the newest build.

#### Server

The API only covers compiling. On the server the BossbarHUD plugin must be installed:

* Release: `BossbarHUD-<version>.jar` from the [Releases](https://github.com/mending-dev/BossbarHUD/tree/main/releases/README.md) page.
* Snapshot: the `BossbarHUD-snapshot` artifact of the latest "Publish SNAPSHOT" workflow run in the Actions tab.

#### Plugin descriptor

`paper-plugin.yml`:

```yaml
dependencies:
  server:
    BossbarHUD:
      load: BEFORE
      required: true
      join-classpath: true
```

`plugin.yml`:

```yaml
depend: [BossbarHUD]
```

### Concepts

* **Hud**: a named group of boxes with a priority. HUDs with a lower priority are rendered further to the left.
* **Segment**: one box inside a HUD, identified by an id. A segment is placed on a `line` (default `1`) and sorted by `order` inside that line. It may have a `HudCondition` that decides who sees it. Its content comes from a `HudSegmentProvider` (dynamic, per player) or a fixed `Component`.
* **Global HUD**: visible to everyone. `hide(player)` excludes a single player.
* **Personal HUD**: visible to nobody until you call `show(player)`.
* A player sees the boxes of all HUDs they can see, grouped by line and, inside a line, ordered by HUD priority and then by segment order.

HUD ids are unique per plugin and may only contain lowercase letters, digits and `_ - . /`.

### Usage

#### Get the API

```java
HudAPI api = HudAPI.get();
```

`HudAPI.get()` throws an `IllegalStateException` if BossbarHUD is not enabled.

#### Personal HUD for one player

```java
Hud hud = api.createHud(plugin, "minigame")
        .priority(10)
        .segment("score", 0, player -> Component.text("Score: " + scores.get(player.getUniqueId())))
        .segment("time", 1, Component.text("Time: 1:20"))
        .build();

hud.show(player);   // send the HUD to one player
hud.hide(player);   // remove it from one player
hud.unregister();   // remove it completely
```

#### Global HUD

```java
Hud hud = api.createHud(plugin, "server-info")
        .global()
        .segment("online", 0, p -> Component.text("Online: " + Bukkit.getOnlinePlayers().size()))
        .build();

hud.hide(player);   // exclude one player
hud.show(player);   // undo
hud.reset();        // back to the default audience (everyone)
```

#### Lines

By default every box is on line 1, so all boxes are placed next to each other. Use the `line` parameter to put boxes below each other:

```java
Hud hud = api.createHud(plugin, "stats")
        .segment("kills", 1, 0, p -> Component.text("Kills: " + kills(p)))    // line 1, left
        .segment("deaths", 1, 1, p -> Component.text("Deaths: " + deaths(p))) // line 1, right of kills
        .segment("time", 2, 0, Component.text("Time: 1:20"))                  // line 2
        .build();

hud.setSegment("best", 2, 1, Component.text("Best: 0:58"));                  // line 2, right of time
```

The parameters are `id, line, order, content`. The overloads without `line` use `Hud.DEFAULT_LINE` (`1`).

Lines are shared between all HUDs: boxes of different HUDs and the config HUD on the same line are placed in the same row. Only the relative order of the lines counts, so if the lines are `1` and `5` there are two rows, and a single box is always in the first row.

#### Conditions

A box can have a condition. It is checked for every viewer before the provider is called. If it is false, the box is hidden for that player and the provider is not called at all.

```java
HudCondition inNether = player -> player.getWorld().getEnvironment() == World.Environment.NETHER;
HudCondition vip = api.condition("permission=server.vip && %player_level% >= 10"); // same syntax as config.yml

Hud hud = api.createHud(plugin, "status")
        .segment("nether", 1, 0, p -> Component.text("Nether"), inNether)
        .segment("vip", 1, 1, Component.text("VIP"), vip.and(inNether.negate()))
        .build();

hud.setSegment("bonus", 2, 0, p -> Component.text("Bonus: " + bonus(p)), vip.or(inNether));
```

* `HudCondition` is a functional interface (`boolean test(Player)`) with `and`, `or`, `negate` and `HudCondition.always()`.
* `api.condition(String)` parses an expression with the syntax of `segments.<id>.condition` in the config (comparisons, `&&`, `||`, `!`, parentheses, quoted strings, PlaceholderAPI placeholders, `permission=node`). It throws an `IllegalArgumentException` that names the position of the error. Parse once and keep the result: the parsed condition only resolves the placeholders and compares. Without PlaceholderAPI the placeholders stay literal text.
* The overloads with a condition always take a `line`: `segment(id, line, order, provider|content, condition)` and `setSegment(id, line, order, provider|content, condition)`.
* A condition that throws an exception hides its box; the exception is logged once.
* A provider that returns `null` still hides a box as well. Prefer a condition when the check does not need the content, because it skips the provider.

#### Change a HUD at runtime

```java
hud.setSegment("time", 1, Component.text("Time: 1:19")); // add or replace, viewers update immediately
hud.removeSegment("time");
hud.segmentIds();
```

#### Update without waiting for the interval

Dynamic segments are re-evaluated every `update-interval-ticks`. If you want to show a change immediately:

```java
hud.refresh(player);   // one player
hud.refresh();         // all viewers of this HUD
api.refresh(player);   // everything this player sees
```

#### Temporary message

```java
// Removed automatically when the player leaves
Hud hud = api.send(plugin, player, Component.text("Welcome!"), Component.text("Have fun"));
hud.unregister(); // remove earlier
```

#### Remove HUDs

```java
hud.unregister();               // one HUD
api.unregisterAll(plugin);      // all HUDs of your plugin
api.unregisterAll();            // all HUDs of all plugins, including the config HUD
api.clear(player);              // hide every HUD from this player
api.reset(player);              // undo clear(player) and all per-HUD overrides
api.setHidden(player, true);    // master switch, used by /bossbarhud toggle
```

#### Query

```java
Optional<Hud> hud = api.getHud(plugin, "minigame");
Collection<Hud> all = api.getHuds();
Collection<Hud> mine = api.getHuds(plugin);
Collection<Hud> visible = api.getHuds(player);
```

#### Glyphs and shifts

You can use the Nexo tags `<glyph:id>` and `<shift:pixels>` in any component that you pass to BossbarHUD, for example one that you built with MiniMessage:

```java
Component text = MiniMessage.miniMessage().deserialize("<glyph:mouse_left> <white>Click");

hud.setSegment("action", 0, text);
```

BossbarHUD finds these tags in the text and replaces them with the real glyph characters before it calculates the length of the box. You do not have to do anything else: the glyph is displayed and the box has the correct length.

* Only `<glyph:id>` (requires Nexo) and `<shift:pixels>` are resolved. Extra arguments of the Nexo tag (index, shadow) are ignored.
* The width of every custom glyph must be known. The server admin sets it in `glyph-widths` of the config, which is the recommended way because it is one place for all plugins. You can also register it in code with `api.registerGlyphWidth('\uE000', 9)`. A glyph without a width is measured as 6 pixels. This also applies to components that already contain real glyph characters.

#### Utilities

```java
Component box = api.box(Component.text("Hello"));       // text wrapped in a box background
int width = api.measure(Component.text("Hello"));       // pixel width of a component
api.registerGlyphWidth('\uE000', 9);                    // width of a custom glyph character
```

### Lifecycle and threading

* HUDs of a plugin are removed automatically when that plugin is disabled. You do not have to clean up in `onDisable`.
* Personal HUD assignments of a player are removed when the player leaves.
* All methods can be called from any thread. Updates are applied on the main thread.
* Segment providers and conditions are always called on the main thread, so keep them fast and do not block.
* The HUD rows always stay above the bossbars of other plugins. The client puts a new bossbar at the bottom, so when a row has to be added (a line whose boxes were all hidden, or after `clear(player)` and `show`), BossbarHUD briefly hides the other bars this player sees and shows them again below the HUD, in their order. This works for bars shown through the Paper/Adventure API (`player.showBossBar(bar)`); bars of the old Bukkit API (`Bukkit.createBossBar`) and vanilla bars (wither, ender dragon, raid, `/bossbar`) cannot be moved. A row that becomes empty is hidden one tick later, so `clear(player)` followed by `show` in the same tick moves nothing.

### API reference

| Type                 | Purpose                                                                            |
| -------------------- | ---------------------------------------------------------------------------------- |
| `HudAPI`             | Entry point: create, query and remove HUDs, utilities, parse conditions            |
| `Hud`                | A registered HUD: segments, audience, refresh, unregister                          |
| `HudBuilder`         | Builds a HUD: `priority`, `global`, `segment`, `build`                             |
| `HudSegmentProvider` | Supplies the content of a box for a player, `null` hides the box                   |
| `HudCondition`       | Decides per player whether a box is shown; `HudAPI#condition` parses an expression |

***

## Building from source

Requirements: JDK 25.

```bash
./gradlew build
```

| Output     | Path                                          |
| ---------- | --------------------------------------------- |
| Plugin jar | `core/build/libs/BossbarHUD-<version>.jar`    |
| API jar    | `api/build/libs/bossbarhud-api-<version>.jar` |

The plugin jar already contains the API classes.

Publish the API to your local Maven repository:

```bash
./gradlew :api:publishToMavenLocal
```

### Project structure

| Module | Description                                                     |
| ------ | --------------------------------------------------------------- |
| `api`  | Public API (interfaces only), published to the Maven repository |
| `core` | The server plugin, including a copy of the API classes          |

### Publishing a version

The version comes from `version` in `gradle.properties` (for example `1.0.0-SNAPSHOT`). The published coordinates are `dev.mending.bossbarhud:api:<version>`.

#### Snapshot

Push to `main` or `develop`. The workflow "Publish SNAPSHOT" publishes the API to `repo/snapshots` of `mending-dev/maven-repo` and uploads the plugin jar as the workflow artifact `BossbarHUD-snapshot`.

#### Release

```bash
git tag v1.0.0
git push origin v1.0.0
```

The workflow "Publish to Maven Repo" takes the tag name without the `v` as the version, publishes the API to `repo/releases` and creates a GitHub release with the plugin jar. A tag whose version ends with `-SNAPSHOT` (for example `v1.1.0-SNAPSHOT`) is published as a snapshot instead, without a GitHub release.

#### Check the result

When the workflow has finished, the files are available in the Maven repository:

```
https://repo.mending.dev/releases/dev/mending/bossbarhud/api/1.0.0/api-1.0.0.pom
https://repo.mending.dev/snapshots/dev/mending/bossbarhud/api/1.0.0-SNAPSHOT/
```

Then use the version in your project as described under [Dependency](#dependency).
