> ## Documentation Index
> Fetch the complete documentation index at: https://wiki.refineryteam.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Utilities

> Small focused helpers: duration formatting, inventory snapshots, and proxy messaging.

Alongside the larger frameworks, RefineryCore ships a few small, self-contained utilities that almost every plugin eventually needs.

## `TimeFormat`

`xyz.refineryteam.refinerycore.api.time.TimeFormat` produces the human-friendly duration strings every cooldown, ban, and timer UI needs — plus the inverse parse for command arguments like `7d12h`.

```java BanCommand.java icon="java" theme={"system"}
TimeFormat.format(Duration.ofMinutes(135));     // "2h 15m"
TimeFormat.formatShort(Duration.ofSeconds(45)); // "45s"
TimeFormat.format(Duration.ofDays(400), 3);     // "1y 1mo 5d"

Duration parsed = TimeFormat.parse("1d6h30m");  // Duration
Duration bare = TimeFormat.parse("300");        // 300 seconds → 5 minutes
Duration strict = TimeFormat.parseOrThrow("7d"); // throws for invalid input
```

| Method                              | Description                                                                                                 |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `format(Duration)`                  | Formats with up to two significant units: `"1y 3mo"`, `"4h 15m"`, `"45s"`, `"0s"`                           |
| `formatShort(Duration)`             | Single significant unit: `"2h"`, `"15m"`, `"45s"`                                                           |
| `format(long millis, int maxUnits)` | Full control over unit count (1–7)                                                                          |
| `parse(String input)`               | Parses compact strings like `"90s"`, `"1d6h"`, `"2w"`, or plain seconds. Returns `null` when nothing parses |
| `parseOrThrow(String input)`        | Same parsing rules, but throws `IllegalArgumentException` when the input is invalid                         |

Supported units are `y`, `mo`, `w`, `d`, `h`, `m`, `s`, and `ms`; negative durations format as zero.

## `InventorySnapshot`

`xyz.refineryteam.refinerycore.api.inventory.InventorySnapshot` captures a player's complete inventory — storage contents, armor, and off hand — as an immutable object serializable to a single Base64 string. Useful for kits, backups, punishment rollbacks, and inventory viewers.

```java KitManager.java icon="java" theme={"system"}
// Capture and store
String saved = InventorySnapshot.capture(player).serialize();
String oneLiner = InventorySnapshot.captureToString(player);

// Restore later (replaces the current inventory entirely)
InventorySnapshot.deserialize(saved).restore(player);

// Merge into the current inventory instead of replacing it
InventorySnapshot.deserialize(saved).merge(player);

// One-liner restore
InventorySnapshot.restoreFromString(player, saved);
```

| Method                              | Description                                                                                                            |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `capture(player)`                   | Snapshots the full inventory; items are cloned so later changes don't affect the snapshot                              |
| `restore(player)`                   | Replaces the player's entire inventory with the snapshot                                                               |
| `merge(player)`                     | Fills empty slots without clearing first; armor/off-hand only replace empty slots, overflow drops at the player's feet |
| `serialize()` / `deserialize(data)` | Compact Base64 round-trip for database/config storage; corrupt data throws `IllegalArgumentException`                  |
| `captureToString(player)`           | Convenience method combining `capture(player).serialize()`                                                             |
| `restoreFromString(player, data)`   | Convenience method combining `deserialize(data).restore(player)`                                                       |

## `ProxyMessenger`

`xyz.refineryteam.refinerycore.api.messaging.ProxyMessenger` sends and receives plugin messages over the BungeeCord-compatible channel that Velocity also supports — the standard way for a backend Paper plugin to ask the proxy to move players between servers or broadcast custom payloads, with no proxy-side mod needed.

```java ServerSwitcher.java icon="java" theme={"system"}
ProxyMessenger messenger = new ProxyMessenger(plugin);

// Built-in subchannel: move a player to another backend server
messenger.connect(player, "survival-2");

// Custom payload delivered to your plugin on every backend server
messenger.forwardToAll("myplugin:sync", dataBytes);
messenger.forwardToServer("survival-2", "myplugin:sync", dataBytes);
messenger.forwardToAllIncludingSelf("myplugin:sync", dataBytes);

// Receive custom payloads
messenger.onMessage("myplugin:sync", (player, data) -> {
    String name = new String(data, StandardCharsets.UTF_8);
    handleRemoteJoin(name);
});
```

| Method                                                      | Description                                                                                                                       |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `new ProxyMessenger(plugin)`                                | Registers the proxy channel on the plugin's behalf; call `shutdown()` from `onDisable()`                                          |
| `connect(player, serverName)`                               | Asks the proxy to connect the player to another backend server                                                                    |
| `connectOther(playerName, serverName)`                      | Same, for a named player who may not be online on this server                                                                     |
| `forwardToAll(subchannel, bytes)`                           | Forwards a custom payload to every backend server                                                                                 |
| `forwardToServer(serverName, subchannel, bytes)`            | Forwards a custom payload to one backend server                                                                                   |
| `forwardToAllIncludingSelf(subchannel, bytes)`              | Forwards a custom payload to all servers, including this one                                                                      |
| `requestPlayerCount(serverName)`                            | Requests a server's player count; use an `onMessage("PlayerCount", ...)` handler for the reply. Pass `"ALL"` for the global count |
| `requestRealIp(playerName)`                                 | Requests a player's real IP; the reply arrives on the `"IP"` subchannel                                                           |
| `onMessage(subchannel, handler)` / `offMessage(subchannel)` | Registers/removes a handler for a custom subchannel; the handler receives the transport player and payload bytes                  |

<Warning>
  On modern Velocity setups this requires `settings.toml → proxies.velocity.enable = true`. Also note the receiving `Player` in `onMessage` is whoever the proxy used to deliver the message — often arbitrary — so treat it as transport context, not as the message subject.
</Warning>
