Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enum-based config settings model with centralized, typed config keys (pending merge in #36)
- Versioned config migration with one-time comment backfill for legacy config files (pending merge in #36)
- Binary replay storage now uses finalized `.br` archives with crash-safe append-log recording, lazy indexed loading, file/MySQL backend support, filtered export tooling, hidden benchmark/debug diagnostics, preserved recording start timestamps, startup recovery of orphaned temp logs, and temporary legacy JSON compatibility during migration
- Replay protection metadata and admin commands for protecting and unprotecting saved replays from manual deletion and retention cleanup
- Config-driven replay retention cleanup with duration parsing, scheduled scans, and protection-aware deletion skipping
- Configurable protected replay highlighting in `/replay list`

### Fixed
- `activeSessions` in `RecorderManager` changed to `ConcurrentHashMap` to prevent `ConcurrentModificationException` (#33)
Expand All @@ -36,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Config settings ownership moved out of `Replay` into a dedicated comment-preserving config manager (pending merge in #36)
- Replay sessions now always start at `1.0x` speed; `Playback.Max-Speed` is enforced to a minimum of `1.0`
- Generated config output now inserts blank lines between root-level keys/sections for readability
- `ReplayManager.deleteSavedReplay` now returns `ReplayDeleteResult`, and the public API also exposes `listSavedReplaySummaries`, `protectSavedReplay`, and `unprotectSavedReplay`

## [1.4.0] - 2026-04-10

Expand Down
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ In short: BetterReplay focuses on server-managed replay workflows and API-driven

- Start and stop recordings
- Save recordings to file or MySQL
- List and delete stored replays
- List, protect, unprotect, and delete stored replays
- Automatic retention cleanup for expired replays
- Replay sessions for viewers
- API-first integration support for other plugins
- Optional Floodgate soft dependency support
Expand All @@ -73,6 +74,8 @@ Subcommands:
- play
- list
- delete
- protect
- unprotect
- export (hidden admin utility command)
- benchmark (hidden admin diagnostic command)
- debug dump (hidden admin dump command)
Expand All @@ -84,6 +87,8 @@ Permissions:
- replay.play
- replay.list
- replay.delete
- replay.protect
- replay.unprotect
- replay.export
- replay.benchmark
- replay.debug
Expand All @@ -107,8 +112,9 @@ Hidden benchmark usage:

## Configuration

Default config keys are initialized in:
- [src/main/java/me/justindevb/replay/Replay.java](src/main/java/me/justindevb/replay/Replay.java)
Default config keys and migrations are defined in:
- [src/main/java/me/justindevb/replay/config/ReplayConfigSetting.java](src/main/java/me/justindevb/replay/config/ReplayConfigSetting.java)
- [src/main/java/me/justindevb/replay/config/ReplayConfigManager.java](src/main/java/me/justindevb/replay/config/ReplayConfigManager.java)

### Storage-Type options

Expand Down Expand Up @@ -150,13 +156,30 @@ General:
Additional key used by command pagination:

```yaml
list-page-size: 10
List:
Page-Size: 10
Protected-Highlight-Color: "&6"
```

Retention cleanup keys:

```yaml
Retention:
Enabled: false
Max-Age: 30d
Check-Interval: 1h
Delete-Partial-Failures: false
Log-Deletions: true
```

Notes:
- If Storage-Type is invalid, plugin falls back to file storage.
- MySQL replay names are stored in a VARCHAR(64) primary key column.
- Binary `.br` payloads require the replay data column to be `LONGBLOB`; the plugin now widens `data` automatically during storage initialization.
- Protected replays are skipped by both retention cleanup and manual delete commands until they are explicitly unprotected.
- Protection stores required audit metadata: `protectedAt` and `protectedBy`.
- Protected replays are highlighted in `/replay list` using `List.Protected-Highlight-Color`; the default is gold (`&6`).
- Retention durations accept `s`, `m`, `h`, and `d` suffixes.
- Legacy JSON replay support is temporary compatibility only and is planned for removal in a later version; new recordings should stay on `.br`.
- The hidden `/replay benchmark` command is now always available to senders with `replay.benchmark`, and `General.Enable-Benchmark-Command` has been removed from config

Expand Down
134 changes: 127 additions & 7 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ BetterReplay exposes a public API that other plugins can use to start/stop recor
- [stopReplay](#stopreplay)
- [getActiveReplays](#getactivereplays)
- [listSavedReplays](#listsavedreplays)
- [listSavedReplaySummaries](#listsavedreplaysummaries)
- [deleteSavedReplay](#deletesavedreplay)
- [protectSavedReplay](#protectsavedreplay)
- [unprotectSavedReplay](#unprotectsavedreplay)
- [getSavedReplayFile](#getsavedreplayfile)
- [Events](#events)
- [RecordingStartEvent](#recordingstartevent)
Expand Down Expand Up @@ -343,30 +346,147 @@ manager.listSavedReplays().thenAccept(names -> {

---

### listSavedReplaySummaries

Lists replay metadata for administrative, retention, and protection-aware workflows.

```java
CompletableFuture<List<ReplaySummary>> listSavedReplaySummaries()
```

Each `ReplaySummary` contains:

| Field | Type | Description |
|---|---|---|
| `name` | `String` | Replay name |
| `createdAt` | `Instant` | Replay creation timestamp |
| `sizeBytes` | `long` | Stored replay size |
| `protectedFromDeletion` | `boolean` | Whether delete and retention flows must skip it |
| `protectedAt` | `Instant` | When protection was last enabled |
| `protectedBy` | `String` | Who enabled protection |
| `storageType` | `ReplayStorageType` | Active backing store |

**Example:**

```java
ReplayManager manager = ReplayAPI.get();

manager.listSavedReplaySummaries().thenAccept(summaries -> {
for (ReplaySummary summary : summaries) {
player.sendMessage(summary.name() + " protected=" + summary.protectedFromDeletion());
}
});
```

---

### deleteSavedReplay

Deletes a saved replay from storage.

```java
CompletableFuture<Boolean> deleteSavedReplay(String name)
CompletableFuture<ReplayDeleteResult> deleteSavedReplay(String name)
```

| Parameter | Type | Description |
|---|---|---|
| `name` | `String` | The name of the replay to delete |

**Returns:** A `CompletableFuture<Boolean>` — `true` if deleted, `false` if it didn't exist or the delete failed.
**Returns:** A `CompletableFuture<ReplayDeleteResult>`.

Possible results:

| Result | Meaning |
|---|---|
| `DELETED` | Replay was deleted |
| `PROTECTED` | Replay is protected from deletion |
| `NOT_FOUND` | Replay does not exist |

**Migration note:** Older plugin versions returned `CompletableFuture<Boolean>`. Update integrations to branch on `ReplayDeleteResult` instead of treating every `false` outcome the same.

**Example:**

```java
ReplayManager manager = ReplayAPI.get();

manager.deleteSavedReplay("pvp-match-42").thenAccept(deleted -> {
if (deleted) {
player.sendMessage("Replay deleted.");
} else {
player.sendMessage("Replay not found or could not be deleted.");
manager.deleteSavedReplay("pvp-match-42").thenAccept(result -> {
switch (result) {
case DELETED -> player.sendMessage("Replay deleted.");
case PROTECTED -> player.sendMessage("Replay is protected.");
case NOT_FOUND -> player.sendMessage("Replay not found.");
}
});
```

---

### protectSavedReplay

Marks a saved replay as protected from both manual deletion and retention cleanup.

```java
CompletableFuture<ReplayProtectionResult> protectSavedReplay(String name, String protectedBy)
```

| Parameter | Type | Description |
|---|---|---|
| `name` | `String` | The replay to protect |
| `protectedBy` | `String` | Audit value describing who enabled protection |

**Returns:** A `CompletableFuture<ReplayProtectionResult>`.

Possible results:

| Result | Meaning |
|---|---|
| `UPDATED` | Protection was enabled |
| `ALREADY_PROTECTED` | Replay was already protected |
| `NOT_FOUND` | Replay does not exist |

**Example:**

```java
ReplayManager manager = ReplayAPI.get();

manager.protectSavedReplay("pvp-match-42", player.getName()).thenAccept(result -> {
if (result == ReplayProtectionResult.UPDATED) {
player.sendMessage("Replay protected.");
}
});
```

---

### unprotectSavedReplay

Removes deletion protection from a saved replay while preserving the last protection audit metadata.

```java
CompletableFuture<ReplayProtectionResult> unprotectSavedReplay(String name)
```

| Parameter | Type | Description |
|---|---|---|
| `name` | `String` | The replay to unprotect |

**Returns:** A `CompletableFuture<ReplayProtectionResult>`.

Possible results:

| Result | Meaning |
|---|---|
| `UPDATED` | Protection was removed |
| `ALREADY_UNPROTECTED` | Replay was already unprotected |
| `NOT_FOUND` | Replay does not exist |

**Example:**

```java
ReplayManager manager = ReplayAPI.get();

manager.unprotectSavedReplay("pvp-match-42").thenAccept(result -> {
if (result == ReplayProtectionResult.UPDATED) {
player.sendMessage("Replay unprotected.");
}
});
```
Expand Down
Loading
Loading