Plugin Development

Building a Plugin

Everything a new feature plugin needs to integrate correctly with the framework — assumes you're hooking into the existing system, not modifying it.

Project setup

name: RK<Name>
main: rk.<name>.<Name>
version: 1.0.0
author: <your name>
description: "..."
license: MIT
loadorder: 5
loadorder must be higher than RKClient's 0 — this is load-bearing, not cosmetic. RKClient builds its packet registry and event pipeline in onEnable(); if your plugin loads first, both are still null when you try to register.

Manifest is only needed if you have your own dependency jar: Class-Path: Libs/<Name>Packets.jar — nothing else. gson, RKPackets, PluginAPI resolve for free at runtime via Rising World's shared classloader once RKClient/RKUpdater have loaded them.

Document your dependencies on your own doc page. Your Web/doc.html (see deployment) should list any private Libs/ jar you ship (e.g. your own packets jar) and which of RKClient's SharedLibs you actually require — see any existing plugin's page for the expected format.

Talking to the backend (packets)

Packets live in their own repo (<Name>Packets), each class extends rk.packets.Packet — a serialization contract, so both sides always read from one canonical deployed file.

Client — receive:

rkClient.registerPacketHandler(MyResponsePacket.class, packet -> {
    MyResponsePacket response = (MyResponsePacket) packet;
});

Client — send:

rkClient.getBackendClient().sendPacket(new MyRequestPacket(...));

Backend — respond, via BackendModule:

public class MyModule implements BackendModule {
    public void registerHandlers(PacketRegistry registry) {
        registry.register(MyRequestPacket.class, (packet, clientHandler) -> {
            MyRequestPacket request = (MyRequestPacket) packet;
            clientHandler.sendPacket(new MyResponsePacket(...));
        });
    }
}

Then one line in RKBackend.start(): List<BackendModule> modules = List.of(new MyModule(...)); — nothing else needs an edit.

Backend database access, same shape as the existing TransferManager/FreetravelManager:

public class MyManager {
    private final Connection conn;
    public MyManager(Database database) throws SQLException {
        this.conn = database.getConnection();
        try (Statement s = conn.createStatement()) {
            s.execute("CREATE TABLE IF NOT EXISTS my_table (...)");
        }
    }
}

No generic table-registration API — direct constructor injection is the whole pattern.

Reacting to player lifecycle (events)

Don't listen to raw PlayerConnectEvent/PlayerDisconnectEvent directly. RKClient already owns those. If your plugin independently listens too, you can't tell whether a player will actually stay, and you risk two plugins fighting over the same cancelable event with no idea about each other — this happened for real in a prior project (a Taming plugin cancelling NPCDeathEvent to protect tamed animals, a separate Butcher plugin listening to the same event for kill resources — result, an infinite-hide duplication exploit, neither plugin wrong in isolation).

Register for RKClient's own resolved callback instead:

rkClient.registerEventHandler(PlayerVerifiedEvent.class, event -> {
    PlayerVerifiedEvent verified = (PlayerVerifiedEvent) event;
    Player player = verified.getPlayer();
    // fires only once this player has passed the wrong-server check
});
rkClient.registerEventHandler(PlayerLeftEvent.class, event -> {
    PlayerLeftEvent left = (PlayerLeftEvent) event;
});

Publish your own events for other plugins to react to — same mechanism, reversed:

public class MyBalanceChangedEvent implements RKClientEvent {
    private final Player player;
    private final long newBalance;
}
// wherever your state actually changes:
rkClient.getEventPipeline().fireEvent(new MyBalanceChangedEvent(player, newBalance));

Note fireEvent lives on EventPipeline, not on RKClient directly — rkClient.getEventPipeline().fireEvent(...). A UI plugin subscribes with registerEventHandler(MyBalanceChangedEvent.class, ...), no compile-time dependency beyond your event class, no network round trip since this is purely in-process.

Anything not cancelable/conflict-prone (PlayerEnterSectorEvent, PlayerCommandEvent, etc.) is fine to listen to directly via Rising World's normal event system.

Settings

Loading, with additive defaults:

Path settingsDir = Path.of(getPath(), "Settings");
JsonObject merged = SettingsMerger.loadAndMerge(MyPlugin.class,
    settingsDir.resolve("MySettings.json"), "Settings/MySettings.json");

Pass your own class, not RKClient.classresourceAnchor resolves the bundled default relative to whichever class you pass. A key present in the default but missing locally gets inserted; anything already customized is never touched. Arrays are atomic — use an object keyed by stable ID if new default entries need to reach existing installs automatically.

Bundle the default at src/Settings/MySettings.json — physically under src/, IntelliJ packages it into the jar automatically.

Renaming a file or key later, without losing a customized value:

public final class MySettingsUpdates {
    private static final List<String> FILES = List.of("MySettings.json");
    private static final List<SettingsMigrator.FileRename> FILE_RENAMES = List.of(
        // new SettingsMigrator.FileRename("Old.json", "New.json")
    );
    private static final List<SettingsMigrator.Migration> KEY_MIGRATIONS = List.of(
        // new SettingsMigrator.KeyRename("New.json:oldKey", "New.json:newKey")
    );
    public static void apply(Path settingsDir) throws IOException {
        SettingsMigrator.run(settingsDir, FILES, FILE_RENAMES, KEY_MIGRATIONS);
    }
}
These lists only ever grow — deleting an entry once a later rename supersedes it is exactly what causes silent data loss for a server offline through multiple renames in a row.

Deployment layout

Client side, per plugin:

Plugins/<Name>/
├── <Name>.jar
├── Libs/<Name>Packets.jar      (Libs, NOT SharedLibs)
└── Settings/*.json              (auto-created on first run)

SharedLibs is reserved for RKClient's own folder only. Backend side, if you have packets: Updates/Plugins/<Name>/Libs/<Name>Packets.jar — the single canonical file, RKServer's manifest should redirect to it rather than keeping a separately-synced local copy.

Your docs page is separate from all of the above and never goes near a game server: author Web/doc.html in your own repo (not under src/ — it's not bundled into your jar), then copy it to Web/Plugins/<Name>/doc.html on the backend, the same motion as deploying your jar. It's picked up automatically at /documentation/plugins/<Name> and listed on the site's index and every page's sidebar — no code change anywhere. Editing it later never needs a recompile or restart, just re-copy the file.

Participating in the updater lifecycle (recommended)

public class MyPlugin extends Plugin implements Listener, UpdateAware {
    public void onEnable() {
        Plugin updaterPlugin = getPluginByName("RKUpdater");
        if (updaterPlugin instanceof RKUpdater updater) {
            updater.registerPlugin(this);
        } else {
            onUpdatesFinished(); // dev/test fallback
        }
    }

    public void onUpdatesFinished() {
        initializePlugin(); // real init here, not onEnable()
    }

    private void initializePlugin() {
        // register packet handlers, event handlers, load settings ...
        reportReady();
    }
}

Gets you auto-install, auto-update via hash comparison with no manual jar copying, and your plugin's readiness genuinely gates player connections alongside everyone else's.

Real gotchas already hit

Design principles