Friends, parties and party chat for Paper, with optional Redis so everything works across a whole network instead of one server. Other plugins can hook into it through a separate API jar.
Built for Minecraft 26.2, Java 25.
- Friends: mutual friend lists that survive restarts, favourites, per player limits, and a ping when someone on your list comes online anywhere on the network.
- Friend requests: expire on their own, and if you send a request to someone who already sent you one it just accepts instead of stacking up.
- Parties: leader, mods and members, invites, kick, promote, transfer, and open parties that anyone can walk into.
- Party chat:
/pc <message>to send one, or/pcon its own to toggle so normal chat goes to the party until you turn it off. - Cross server: a party can have members sitting on four different servers and chat still reaches all of them. Presence is tracked so you can see who is online and where.
- Redis messaging: one pooled connection with a subscriber that reconnects on its own, shared out to other plugins so they do not each need their own.
- Developer API:
bond-apiis a normal jar you compile against, with services, events and a messenger for your own cross server payloads.
Redis is optional. With it switched off Bond still runs, it just only knows about this server.
- Paper
26.2or newer - Java 25
- MySQL/MariaDB or SQLite (SQLite is the default and needs no setup)
- Redis, only if you want the cross server side
./gradlew buildbond-plugin/build/libs/Bond-1.0.jar is the plugin, drop it in plugins/.
bond-api/build/libs/bond-api-1.0.jar is what other developers compile against.
| Command | What it does |
|---|---|
/friend |
your friend list |
/friend add <player> |
send a request |
/friend accept <player> |
accept one |
/friend deny <player> |
turn one down |
/friend remove <player> |
remove a friend |
/friend fav <player> |
pin them to the top of your list |
/friend requests |
what is waiting on you |
/party |
your party, or your invites if you have none |
/party create |
start one |
/party invite <player> |
invite someone, makes a party first if you have none |
/party accept [player] |
accept an invite |
/party kick <player> |
remove a member |
/party promote <player> |
make them a mod |
/party transfer <player> |
hand over the party |
/party open |
let anyone join without an invite |
/party leave |
leave |
/party disband |
close the party |
/pc [message] |
send to party chat, or toggle it |
/friend is aliased to /f, /party to /p, /partychat to /pc.
| Node | Default | Grants |
|---|---|---|
bond.friend |
everyone | the friend commands |
bond.party |
everyone | the party commands |
bond.admin |
op | admin |
Install the API into your local maven cache:
./gradlew :bond-api:publishToMavenLocalThen depend on it. It is compileOnly, the plugin provides it at runtime:
repositories {
mavenLocal()
}
dependencies {
compileOnly("me.am4er:bond-api:1.0")
}Put Bond in your depend or softdepend in plugin.yml, otherwise Bond.api() will throw
because it registers itself during its own onEnable.
Reads that only touch this server are synchronous:
Bond.api().parties().of(player.getUniqueId()).ifPresent(party -> {
player.sendMessage("You are in a party of " + party.size());
});Anything that might have to leave the server returns a future. They complete on Bond's worker pool, so get back on the main thread before touching Bukkit:
Bond.api().friends().friends(uuid).thenAccept(list -> {
Bukkit.getScheduler().runTask(this, () -> {
for (FriendEntry f : list) {
player.sendMessage(f.name());
}
});
});Anything that changes something gives you a Result back rather than a boolean, so you can tell
PARTY_FULL from NOT_LEADER:
Bond.api().parties().invite(me, them).thenAccept(result -> {
if (result.failed()) {
player.sendMessage("Could not invite them: " + result);
}
});Events all fire on the main thread even when the thing that caused them happened on another
server, so you never have to worry about which thread you are on. remote() tells you where it
came from. PartyChatEvent, PartyInviteEvent and FriendRequestEvent are cancellable:
@EventHandler
public void onPartyChat(PartyChatEvent e) {
if (muted.contains(e.sender())) {
e.setCancelled(true);
return;
}
e.message(filter.clean(e.message()));
}If you want to send your own things between servers, use the messenger instead of opening a second Redis connection. Your channels get namespaced so they cannot collide with Bond's:
Messenger m = Bond.api().messenger();
Messenger.Subscription sub = m.subscribe("duels", msg -> {
// runs on the redis thread, keep it short
getLogger().info(msg.from() + " said " + msg.body());
});
m.publish("duels", "someone won");Handlers never receive your own messages back.
Every server picks an id, from server-id or the hostname and port if that is left on auto.
Parties live in Redis, one hash each. Members and invites are separate fields inside that hash rather than one blob, so two servers changing different members cannot overwrite each other. When a server changes something it saves, then publishes a small "party X changed" message, and the other servers read it back. Each key carries a 12 hour TTL that gets bumped on every write, so a party is never left behind if every server holding it dies at once.
Presence is a Redis hash of player to server, plus a heartbeat per server. If a server stops beating for 60 seconds its players get dropped, otherwise a crash would leave ghosts online forever. On startup a server also clears anything still pointing at itself from a previous run that did not shut down cleanly.
Friends are in SQL rather than Redis because they have to survive everything going down. Both directions of a friendship are written in one transaction, so you never end up with a one sided friend.
plugins/Bond/config.yml. Values are clamped on load so a typo cannot take the server down.
# leave as auto to use the machine hostname and port
server-id: auto
storage:
# sqlite or mysql
type: sqlite
file: bond.db
host: localhost
port: 3306
database: bond
username: root
password: ""
pool-size: 8
# turn this on to share friends and parties between servers
redis:
enabled: false
host: localhost
port: 6379
password: ""
friends:
max: 200
request-expiry-seconds: 300
notify-on-join: true
party:
max-size: 8
invite-expiry-seconds: 120
leader-only-invite: false
chat-format: "&8[&dParty&8] &f{name}&7: &r{message}"Every server on a network needs the same Redis and the same database. The server-id is the only
thing that should differ.
- SQLite runs with a pool of one because it only takes a single writer. It is fine for one server, use MySQL for a network since every server has to see the same friend data.
- The bundled SQLite driver only ships natives for linux, windows and mac on x86_64 and aarch64, including musl. That covers what servers actually run on and keeps the jar at 7MB instead of 17.
- Jedis, HikariCP and both JDBC drivers are shaded and relocated under
me.am4er.bond.lib, so Bond cannot fight with another plugin that ships its own copy. - Party state is cache plus Redis, so a party is only visible on servers that have at least one of its members. That is deliberate, there is no reason for a lobby to hold every party on the network in memory.
MIT. See LICENSE.
Written by Am4er.