Skip to content
Open
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
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ jobs:
- name: Upload built artifacts
uses: actions/upload-artifact@v4
with:
name: Platform Artifacts
name: Build Artifacts
path: |-
api/build/libs/*.jar
paper/build/libs/*-all.jar
velocity/build/libs/*-all.jar
velocity/build/libs/*-all.jar
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ Those are **argument tags**, which contain specific text defined by the plugin (

If multiple arguments are present in a certain translation, they can be identified by the number after the `:`.

## Developer API

ServerChangelogs provides a separately publishable, platform-neutral API for other plugins without exposing internal
storage or platform-specific plugin classes. See the [complete API reference](api/README.md) for dependency setup,
every method and return value, changelog data, events, subscriptions, lifecycle rules, and examples.

## Roadmap
ServerChangelogs was originally made for a **smaller minecraft server** I'm a part of, to make players aware of changes without having to rely on e.g. **Discord**. Because we wanted this feature to be available pretty fast, the plugin initially won't have as many features as I'd like it to have.

Expand All @@ -121,4 +127,4 @@ The roadmap below contains some of the features that I'm **planning to add** in
- [ ] **Other UI types**: The changes in this lower section are planned for a not-so-close future, the stuff above is more relevant for now! Anyway, even though other UI types (like chat, books, etc.) may not be as intuitive, some server owners might still prefer them. While there are plugins out there that can already do this, it would be nice for those to be available along with the additional features this plugin provides.
- [ ] **A poll/voting system?**: A bit of a far-fetched idea, but since changelogs include a lot of, well, *change*, they can sometimes be controversial along players. I feel like a voting system might fit this plugin kind of well, so players can immediately share their opinion on a specific change right when they see it!

> Contributions over on [GitHub](https://github.com/codingcat2468/ServerChangelogs) are always welcome, so if you do want to help out, it'd be nice to see if you are interested in implementing/helping with some of the above ideas!
> Contributions over on [GitHub](https://github.com/codingcat2468/ServerChangelogs) are always welcome, so if you do want to help out, it'd be nice to see if you are interested in implementing/helping with some of the above ideas!
193 changes: 193 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# ServerChangelogs API

The ServerChangelogs API lets Paper and Velocity plugins publish and inspect changelogs, track whether players have
read them, open the changelog menu, and observe publish/read events. It is platform-neutral and does not expose the
plugin's internal storage or dialog implementation.

## Dependency and runtime setup

The API is published separately from the platform plugin:

```kotlin
dependencies {
compileOnly("com.codingcat:serverchangelogs-api:<version>")
}
```

Use `compileOnly`; do not shade or relocate the API into your plugin. Your Paper or Velocity plugin must declare
ServerChangelogs as a required runtime dependency so it loads first and both plugins use the API classes provided by
ServerChangelogs.

The API module is configured to publish sources and Javadocs alongside the main Maven artifact. This repository does
not configure a remote Maven repository; release infrastructure must provide the repository URL and credentials.

## Getting the API

```java
if (!ServerChangelogsApi.isAvailable()) {
throw new IllegalStateException("ServerChangelogs has not enabled");
}

ServerChangelogsApi api = ServerChangelogsApi.get();
```

| Method | What it does |
| --- | --- |
| `ServerChangelogsApi.isAvailable()` | Returns `true` after ServerChangelogs has enabled and registered its provider. It returns `false` before enable and after disable. |
| `ServerChangelogsApi.get()` | Returns the active API instance. It throws `IllegalStateException` when the plugin is unavailable. |

Keep the returned instance only while ServerChangelogs is enabled. A cached instance rejects calls with
`IllegalStateException` after ServerChangelogs disables. API calls are synchronous and the backing storage is not
thread-safe, so call all methods—including queries—from the server's main thread unless the active platform explicitly
guarantees another context.

## Changelog data

`Changelog` is an immutable snapshot with these accessors:

| Accessor | Value |
| --- | --- |
| `id()` | The changelog's current non-negative storage identifier. |
| `lines()` | An immutable `List<Component>` containing the displayed changelog lines. |
| `publishedAt()` | The `Instant` at which the changelog was created. |
| `author()` | An `Optional<Component>` containing the displayed author. |

The public `Changelog` constructor creates an immutable snapshot and rejects a negative ID, an empty line list, null
lines, a null timestamp, or a null `Optional`. Changelog IDs are current storage positions and may change when an
earlier entry is deleted, so integrations must not treat them as permanent external identifiers.

## Creating changelogs

```java
Changelog withoutAuthor = api.createChangelog(
List.of(Component.text("Added a new game mode"))
);

Changelog withAuthor = api.createChangelog(
List.of(
Component.text("Added a new game mode"),
Component.text("Updated the lobby")
),
Component.text("Server Team")
);
```

| Method | What it does |
| --- | --- |
| `createChangelog(List<Component> lines)` | Persists a changelog without an author and returns its immutable snapshot. The line list must not be empty or contain null. |
| `createChangelog(List<Component> lines, Component author)` | Persists a changelog with an author and returns its immutable snapshot. Neither argument may be null. |

Both overloads fire one `ChangelogPublishedEvent` synchronously after storage succeeds.

## Reading changelogs

| Method | What it does |
| --- | --- |
| `getChangelogs()` | Returns an immutable list of immutable snapshots in publication order, oldest first. An empty list means no changelogs exist. |
| `getChangelog(int id)` | Returns the changelog with the current ID, or `Optional.empty()` when it does not exist. |
| `getLatestChangelog()` | Returns the most recently published changelog, or `Optional.empty()` when storage is empty. |

```java
api.getLatestChangelog().ifPresent(changelog ->
logger.info("Latest changelog ID: {}", changelog.id())
);
```

## Player read state

Read state uses player UUIDs, so it also works when a player is offline.

| Method | What it does |
| --- | --- |
| `hasRead(int changelogId, UUID playerId)` | Returns `true` if that player has read the changelog. It returns `false` for an unknown changelog or an unread player. |
| `markAsRead(int changelogId, UUID playerId)` | Changes an existing changelog from unread to read for that player. It returns `true` only for the first successful transition and `false` if the changelog is missing or was already read. |

A successful transition fires one `ChangelogReadEvent`. Repeating `markAsRead` does not fire another event.

## Opening the menu

```java
boolean opened = api.openChangelogMenu(playerUuid);
```

`openChangelogMenu(UUID playerId)` opens the normal ServerChangelogs dialog for an online player and returns `true`.
It returns `false` when that UUID is not currently online. This method must be called from a context in which the
platform allows dialogs to be opened, normally the server's main thread.

## Events and subscriptions

Subscribe by exact event class:

```java
ChangelogSubscription publishedSubscription = api.subscribe(
ChangelogPublishedEvent.class,
event -> logger.info("Published changelog {}", event.changelog().id())
);

ChangelogSubscription readSubscription = api.subscribe(
ChangelogReadEvent.class,
event -> logger.info(
"Player {} read changelog {}",
event.playerId(),
event.changelog().id()
)
);
```

| API | What it does |
| --- | --- |
| `subscribe(Class<T> eventType, Consumer<? super T> listener)` | Registers a listener for that exact event type and returns its subscription handle. The event type and listener must not be null. |
| `ChangelogSubscription.unsubscribe()` | Removes the listener. Calling it repeatedly has no additional effect. |
| `ChangelogSubscription.close()` | Equivalent to `unsubscribe()`, allowing subscriptions to be managed as `AutoCloseable`. |
| `ChangelogPublishedEvent.changelog()` | Returns the immutable snapshot that was successfully published. |
| `ChangelogReadEvent.changelog()` | Returns the immutable snapshot that changed to read. |
| `ChangelogReadEvent.playerId()` | Returns the UUID of the player whose state changed. |

Listeners execute synchronously on the thread that caused the event. A listener exception is logged and isolated so it
does not prevent the remaining listeners from running. Keep every subscription and close it when your plugin disables:

```java
@Override
public void onDisable() {
publishedSubscription.close();
readSubscription.close();
}
```

ServerChangelogs also clears all remaining subscriptions when it shuts down.

## Provider-only members

`ServerChangelogsApi` has a protected constructor plus protected `registerProvider()` and `unregisterProvider()`
methods. These exist only for the implementation in the `base` module. Consumer plugins must not subclass
`ServerChangelogsApi` or attempt to register a provider.

## Complete example

```java
public final class ExampleIntegration {
private final ServerChangelogsApi api = ServerChangelogsApi.get();
private final ChangelogSubscription readSubscription;

public ExampleIntegration() {
this.readSubscription = api.subscribe(ChangelogReadEvent.class, event ->
System.out.println(event.playerId() + " read #" + event.changelog().id())
);
}

public void publishAndShow(UUID playerId) {
Changelog changelog = api.createChangelog(
List.of(Component.text("Maintenance completed")),
Component.text("Operations")
);

if (!api.hasRead(changelog.id(), playerId)) {
api.openChangelogMenu(playerId);
}
}

public void close() {
this.readSubscription.close();
}
}
```
56 changes: 56 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
plugins {
id("java-library")
id("maven-publish")
}

base {
archivesName = "serverchangelogs-api"
}

dependencies {
api(libs.adventure.api)
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.platform.launcher)
}

java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
withSourcesJar()
withJavadocJar()
}

tasks.test {
useJUnitPlatform()
}

publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
artifactId = "serverchangelogs-api"

pom {
name = "ServerChangelogs API"
description = project.property("description") as String
url = project.property("url") as String
licenses {
license {
name = "MIT License"
url = "https://opensource.org/license/mit"
}
}
developers {
developer {
id = "codingcat2468"
name = "codingcat2468"
}
}
scm {
connection = "scm:git:git://github.com/codingcat2468/ServerChangelogs.git"
developerConnection = "scm:git:ssh://github.com/codingcat2468/ServerChangelogs.git"
url = "https://github.com/codingcat2468/ServerChangelogs"
}
}
}
}
}
31 changes: 31 additions & 0 deletions api/src/main/java/com/codingcat/changelogs/api/Changelog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.codingcat.changelogs.api;

import net.kyori.adventure.text.Component;

import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
* An immutable snapshot of a published changelog.
*
* @param id the changelog's current numeric identifier
* @param lines the lines displayed for this changelog
* @param publishedAt the time at which the changelog was published
* @param author the displayed author, if one was supplied
*/
public record Changelog(int id, List<Component> lines, Instant publishedAt, Optional<Component> author) {
/**
* Creates an immutable changelog snapshot.
*/
public Changelog {
if (id < 0) throw new IllegalArgumentException("id must not be negative");
lines = List.copyOf(lines);
if (lines.isEmpty()) throw new IllegalArgumentException("lines must not be empty");
if (lines.stream().anyMatch(Objects::isNull))
throw new IllegalArgumentException("lines must not contain null values");
Objects.requireNonNull(publishedAt, "publishedAt");
Objects.requireNonNull(author, "author");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.codingcat.changelogs.api;

/**
* A live subscription to ServerChangelogs events.
*/
@FunctionalInterface
public interface ChangelogSubscription extends AutoCloseable {
/**
* Stops this subscription. Calling this method more than once has no effect.
*/
void unsubscribe();

@Override
default void close() {
this.unsubscribe();
}
}
Loading
Loading