Skip to content

Repository files navigation

Bond

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.

What it does

  • 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 /pc on 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-api is 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.

Requirements

  • Paper 26.2 or newer
  • Java 25
  • MySQL/MariaDB or SQLite (SQLite is the default and needs no setup)
  • Redis, only if you want the cross server side

Build

./gradlew build

bond-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.

Commands

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.

Permissions

Node Default Grants
bond.friend everyone the friend commands
bond.party everyone the party commands
bond.admin op admin

Developer API

Install the API into your local maven cache:

./gradlew :bond-api:publishToMavenLocal

Then 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.

How the cross server side works

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.

Configuration

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.

Notes

  • 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.

License

MIT. See LICENSE.

Written by Am4er.

About

Friends, parties and party chat for Paper. Cross-server over Redis, with a developer API other plugins can hook into.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages