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
68 changes: 0 additions & 68 deletions .claude/known_errors.md

This file was deleted.

27 changes: 4 additions & 23 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,18 @@ permissions:
contents: read

jobs:
test:
build:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.4
env:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: friends_test
ports: ["3306:3306"]
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1"
--health-interval=5s --health-timeout=5s --health-retries=30
redis:
image: redis:8
ports: ["6379:6379"]
options: >-
--health-cmd="redis-cli ping"
--health-interval=5s --health-timeout=5s --health-retries=30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "25"
- uses: gradle/actions/setup-gradle@v6
# Runs every suite: SQLite, MySQL and real Redis (throwaway tables / key namespaces).
- run: "./gradlew build -Dfriends.mysql=127.0.0.1:3306/friends_test:root: -Dfriends.redis=127.0.0.1:6379"
# Test sources stay local (not in the repository): this compiles everything and checks the shipped jars.
- run: ./gradlew build
- uses: actions/upload-artifact@v7
with:
name: friends-plugins
path: |
velocity/build/libs/friends-velocity-*.jar
bungee/build/libs/friends-bungee-*.jar
paper/build/libs/friends-paper-[0-9]*.jar
path: builds/*.jar
27 changes: 23 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
# Build outputs (the jars to ship are written to builds/)
.gradle/
build/
builds/
bin/
.backup/
*.db
*.db-shm
*.db-wal
out/
target/
*.log

# IDEs and OS
.idea/
*.iml
.vscode/
.DS_Store

# Claude Code
.claude/
claude.json
.claude.json
CLAUDE.local.md

# Tests stay local (not published)
**/src/test/
__pycache__/
*.pyc

# Local data and backups
.backup/
*.db
*.db-shm
*.db-wal
3 changes: 0 additions & 3 deletions .vscode/settings.json

This file was deleted.

97 changes: 25 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
# Friends

A Hypixel-style friends system for **Velocity**, **BungeeCord** and **Paper 1.8.8 → 26.3**, built for Java 25.

Modules:
- `api` — the API for other plugins (Java 8, no dependencies), see [API for other plugins](#api-for-other-plugins).
- `common` — platform-agnostic core (rules, cache, storage, messages, multi-proxy network) + tests.
- `velocity`, `bungee` — proxy plugins (thin glue). Both support multi-proxy networks through Redis, even mixed.
- `paper` — standalone Paper servers, one jar for 1.8.8 to 26.x.
A Hypixel-style friends system for **Velocity**, **BungeeCord**, **Paper 1.8.8 → 26.3** and **Folia**, built for Java 25.

Modules (one folder per part; a new platform gets its own top-level folder next to these):
- `api/` — the API for other plugins (Java 8, no dependencies): interfaces, data models, events. Usage guide:
[`api/README.md`](api/README.md).
- `core/` — platform-agnostic core: rules, cache, storage (SQLite/MySQL), messages, commands, multi-proxy network.
- `paper/` — Paper and Folia, one jar for 1.8.8 to 26.x.
- `bungee/`, `velocity/` — proxy plugins (thin glue). Both support multi-proxy networks through Redis, even mixed.
- `builds/` — the jars to ship, written by every build (not in git).
- `tools/e2e` — end-to-end tests on real servers with bot players; `tools/e2e/probe` is a test plugin using the API.

## Build

```bash
./gradlew build # tests + velocity/build/libs/friends-velocity-0.5.0.jar,
# bungee/build/libs/friends-bungee-0.5.0.jar, paper/build/libs/friends-paper-0.5.0.jar
./gradlew build # tests, then builds/friends-paper.jar, friends-velocity.jar, friends-bungee.jar, friends-api.jar
```

Put the jar for your platform in `plugins/`. Every server/proxy must run **Java 25** (Velocity 4.2 and Paper 26.x
Expand All @@ -25,6 +26,7 @@ flags to silence the JDK warning about SQLite's native library.
| Platform | Tested | Notes |
|---|---|---|
| Paper 1.8.8, 1.12.2, 1.16.5, 1.20.6, 1.21.11, 26.3 | full friend flow with bot players | 1.16.5 needs `-DPaper.IgnoreJavaVersion=true` to start on Java 25 |
| Folia 1.21.11 | full friend flow with bot players (same jar as Paper) | regionized Paper: Friends uses no Bukkit scheduler |
| BungeeCord 26.1 (build 2100) | players on 1.8.8 and 26.3 backends | |
| Velocity 4.2.0 | players on 1.8.8 and 26.3 backends | |
| BungeeCord + Velocity sharing MySQL + Redis | cross-proxy friend flow | |
Expand Down Expand Up @@ -73,61 +75,9 @@ Use the plugin either on the proxies or on standalone Paper servers, not both.

## API for other plugins

`FriendsAPI` (artifact `com.friends:friends-api`) is the same on Paper, Velocity and BungeeCord: friend lists,
status, current server and pending requests, plus actions (request, accept, deny, remove, best friend, nickname,
status). Each platform also has six events (`com.friends.api.bukkit`, `.velocity`, `.bungee`). Every public method is
documented: `./gradlew :api:javadoc`.

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

```kotlin
repositories { mavenLocal() }
dependencies { compileOnly("com.friends:friends-api:0.5.0") } // Friends provides it at runtime
```

Declare the dependency: `depend: [Friends]` in plugin.yml, `depends: [Friends]` in bungee.yml,
`@Dependency(id = "friends")` on Velocity.

```java
FriendsAPI friends = FriendsAPI.get(); // on Paper also getServer().getServicesManager().load(FriendsAPI.class)

boolean pals = friends.areFriends(alice, bob); // memory only: fine on the server thread
friends.loadFriends(someone) // offline players come from the database (cached 30 s)
.thenAccept(list -> ...); // completes on Friends' threads, not the server thread
friends.sendRequest(alice, bob).thenAccept(result -> {
if (result == Result.CANCELLED) ... // a plugin cancelled the event
});

@EventHandler
public void onRequest(FriendRequestSendEvent e) { // asynchronous: never on the server thread
if (isMuted(e.getPlayerId())) e.setCancelled(true); // Friends says nothing: tell the player yourself
}
```

| Event | Cancellable | Fired |
|---|---|---|
| `FriendRequestSendEvent` | yes | before a request is sent |
| `FriendAddEvent` | yes | before two players become friends (accept, or asking back) |
| `FriendAddedEvent` | no | after, exactly once per new friendship |
| `FriendRemoveEvent` | yes | before a friendship ends, once per friend (`/f removeall` too) |
| `FriendRemovedEvent` | no | after, exactly once |
| `FriendStatusChangeEvent` | yes | before `/status` changes |

- **Threads.** Methods returning a value read memory and never block. Methods returning a `CompletableFuture`
return at once and do their work (database included) on Friends' threads. Events run on Friends' threads too
(Bukkit: asynchronous events): switch to the server thread before touching the world.
- **Commands and API actions fire the same events.** An action doesn't reply to the acting player (the `Result` says
what happened, e.g. `NOT_LOADED`, `ALREADY_FRIENDS`, `CANCELLED`); the other player is still notified. The acting
player must be online on this server/proxy.
- **Exactly once:** cancellable events fire outside Friends' lock and the action is checked again afterwards, so in
a rare race (one request accepted twice at the same moment) an action can still fail after its event. Rewards and
statistics belong in `FriendAddedEvent` / `FriendRemovedEvent`.
- **Where to listen:** on a network Friends runs on the proxy, so listen to its Velocity/BungeeCord events (each
fires once network-wide, on the proxy where the change happened). Bukkit events fire on standalone Paper servers.
- **Nothing throws:** null or invalid arguments give empty results, `false` or `Result.INVALID_ARGUMENT`; database
errors give `Result.ERROR`. Events and snapshots hold UUIDs and names, never `Player` objects.
`FriendsAPI` is the same on Paper, Velocity and BungeeCord: friend lists, status, current server and pending
requests, actions (request, accept, deny, remove, best friend, nickname, status) and six events per platform. See
[`api/README.md`](api/README.md) for setup, examples and the threading rules.

## How it works

Expand All @@ -149,13 +99,16 @@ public void onRequest(FriendRequestSendEvent e) { // asynchronous: never on the

## Tests

`./gradlew build` runs everything that needs no services: SQLite, in-memory multi-proxy, randomized
cache-vs-database checks with a deliberately slow database, a concurrency stress test, chat conversion on the
newest and on the 1.8 BungeeCord chat API, and the SQLite suites on sqlite-jdbc 3.7.2 / 3.21.0.1 (the drivers
Paper 1.8.8 / 1.12.2 bundle). Add `-Dfriends.mysql=host:port/db:user:password` and/or `-Dfriends.redis=host:port`
to also run the MySQL and real-Redis suites (throwaway tables/key namespaces). The API has its own suite (results,
null/offline/invalid input, the offline cache under a paused database, cancelling, exactly-once events), and the
`paper` module runs the real plugin wiring on MockBukkit 4.116.3 against the shipped implementation jar (service,
asynchronous Bukkit events, cancelling); the proxies' events are tested on their own event APIs.
Test sources are kept locally and are not published in this repository. `./gradlew build` always runs
`verifyBuilds`, which opens the jars in `builds/` and checks their descriptors and versions, that each platform ships
only its own events, that bundled libraries are relocated, that the Paper and API jars are Java 8 bytecode, and that
no file-sync copies (`Foo 2.class`) slipped in.

With the local suites present, `./gradlew build` also runs SQLite, in-memory multi-proxy, randomized cache-vs-database
checks with a deliberately slow database, a concurrency stress test, chat conversion on the newest and on the 1.8
BungeeCord chat API, and the SQLite suites on sqlite-jdbc 3.7.2 / 3.21.0.1 (the drivers Paper 1.8.8 / 1.12.2 bundle).
Add `-Dfriends.mysql=host:port/db:user:password` and/or `-Dfriends.redis=host:port` to also run the MySQL and
real-Redis suites (throwaway tables/key namespaces). The `paper` module runs the real plugin wiring on MockBukkit
against the shipped implementation jar, and the proxies' events are tested on their own event APIs.

End-to-end on real servers: see [`tools/e2e/README.md`](tools/e2e/README.md).
Loading
Loading