RSFramework/
├── Bridge/ Inter-server communication broker
│ ├── Common/ Bridge interface, BridgeChannel
│ ├── Proxium/ Netty-based direct proxy communication
│ │ ├── Common/ Public API & Internal implementation
│ │ ├── Bukkit/ Bukkit-side implementation
│ │ └── Velocity/ Velocity-side implementation
│ └── Redisson/ Redis implementation
├── Configurate/ YAML object mapping wrapper
├── Framework/ Framework core
│ ├── API/ RSPlugin, RSCommand, RSListener
│ ├── Core/ Internal implementation
│ └── NMS/ Version-specific NMS adapters (1.20 R1 ~ 1.21 R7)
├── LightDI/ Lightweight DI container
├── Platform/ Platform adapters
│ ├── Folia/
│ ├── Paper/
│ ├── Spigot/
│ └── Velocity/
├── Storage/ Unified storage system
│ ├── Common/ Common API
│ ├── Json/
│ ├── MariaDB/
│ ├── MongoDB/
│ ├── MySQL/
│ ├── PostgreSQL/
│ └── SQLite/
└── docs/ Technical documentation
├── en/ English documentation
└── kr/ Korean documentation
Build output: ./gradlew shadowJar → builds/plugin/RSFramework-{version}.jar
RSCommand, RSListener, RSInventory share identical protected final fields.
| Field | Type | Description |
|---|---|---|
plugin | T | Owning plugin instance |
framework | Framework | Framework core |
message | MessageTranslation | i18n message translation |
command | CommandTranslation | i18n command translation |
notifier | Notifier | Message sending utility |
RSCommand additionally provides sender, player, audience fields.
// ✅ Direct field accessplugin.reloadConfiguration(MyConfig.class);
notifier.announce(player, "Done!");repositories {
maven {
name ="RTUStudio"
url = uri("https://repo.codemc.io/repository/rtustudio/")
}
}
dependencies {
compileOnly("kr.rtustudio:framework-api:4.7.17")
}importkr.rtustudio.framework.bukkit.api.RSPlugin;
publicclassMyPluginextendsRSPlugin {
@Overrideprotectedvoidenable() {
registerConfiguration(PerkConfig.class, ConfigPath.of("Perk"));
registerCommand(newMainCommand(this), true);
registerEvent(newPlayerAttack(this));
}
@Overrideprotectedvoiddisable() { }
}Lifecycle: onLoad → initialize() → load() → onEnable → enable() → onDisable → disable()
Auto-logging: Console messages are automatically printed on enable/disable/reload.
publicclassJoinListenerextendsRSListener<MyPlugin> {
publicJoinListener(MyPluginplugin) {
super(plugin);
}
@EventHandlerpublicvoidonJoin(PlayerJoinEventevent) {
notifier.announce(event.getPlayer(), "<green>Welcome to the server!");
}
}Supports hierarchical structure, auto-permission registration, cooldowns, and tab completion.
publicclassMainCommandextendsRSCommand<MyPlugin> {
publicMainCommand(MyPluginplugin) {
super(plugin, "test", PermissionDefault.OP);
registerCommand(newSubCommand(plugin));
}
@OverrideprotectedResultexecute(CommandArgsdata) {
notifier.announce("Main command executed!");
returnResult.SUCCESS;
}
@Overrideprotectedvoidreload(CommandArgsdata) {
plugin.reloadConfiguration(TestConfig.class);
}
}registerCommand(cmd, true) — automatically adds /{command} reload.
| Result | Framework Behavior |
|---|---|
SUCCESS / FAILURE | None |
ONLY_PLAYER / ONLY_CONSOLE | Auto info message |
NO_PERMISSION | Auto info message |
NOT_FOUND_ONLINE_PLAYER / NOT_FOUND_OFFLINE_PLAYER | Auto info message |
WRONG_USAGE | Auto-show subcommands |
Translation/Command/{language}.yml:
test:
name: "test"description: "A test command"usage: "/test"commands:
sub:
name: "sub"Configurate-based YAML object mapping.
@Getter@SuppressWarnings({
"unused",
"CanBeFinal",
"FieldCanBeLocal",
"FieldMayBeFinal",
"InnerClassMayBeStatic"
})
publicclassMyConfigextendsConfigurationPart {
@Comment("Welcome message")
privateStringwelcomeMessage = "<green>Welcome!";
@Min(1)
@Comment("Maximum players")
privateintmaxPlayers = 100;
publicConnectionconnection;
@GetterpublicclassConnectionextendsConfigurationPart {
privateStringhost = "127.0.0.1";
privateintport = 25565;
}
}ConfigurationPart provides mutable collection factories that are safe during configuration reload.
Note: Using immutable collections like
List.of(),Map.of()as config defaults will causeUnsupportedOperationExceptionon reload. Always uselistOf(),mapOf()helpers.
publicclassWhitelistConfigextendsConfigurationPart {
// varargs styleprivateList<String> commands = listOf("help", "spawn");
// Consumer style (complex initialization)privateMap<String, List<String>> groups = mapOf(map -> {
map.put("default", listOf("help"));
map.put("worldedit", listOf("/wand", "/copy"));
});
// key-value styleprivateMap<String, String> aliases = mapOf("h", "help", "s", "spawn");
}@Overrideprotectedvoidinitialize() {
registerConfiguration(MyConfig.class, ConfigPath.of("Setting"));
registerConfigurations(RegionConfig.class, ConfigPath.of("Regions"));
}Details → docs/en/configuration.md
Stringmsg = message.get(player, "error.no-money");
notifier.announce(player, msg);MiniMessage format. Supports chat, action bar, title, boss bar, and cross-server broadcast.
notifier.announce(player, "<aqua>Item received!"); // with prefixnotifier.send(player, "<yellow>Warning message"); // without prefixnotifier.title(player, "<bold><gold>Level Up!", "<gray>New skill unlocked");
Notifier.broadcastAll("<green>A new event has started!");Supports inter-server Pub/Sub broadcast and RPC request-response.
Bridge (isConnected + close)
├── Broadcast (register · subscribe · publish · unsubscribe)
├── Transaction (request · respond · getRequestTimeout)
│
├── Redis extends Broadcast
└── Proxium extends Broadcast, Transaction
| Layer | Role |
|---|---|
| Proxium (Bridge) | Pure communication infrastructure — packet serialization, Netty transport, TLS |
| RSFramework Bukkit | Application logic — teleport execution, message handling |
| RSFramework Velocity | Teleport routing — server transfer coordination |
Proxiumproxium = getBridge(Proxium.class);
BridgeChannelchannel = BridgeChannel.of("myplugin", "shop");
// Type-specific subscription (multiple types can be registered individually)proxium.subscribe(channel, BuyRequest.class, buy -> {
getLogger().info(buy.playerName() + " requested a purchase.");
});
// Publishproxium.publish(channel, newBuyRequest("ipecter", "DIAMOND", 64));// Response server (data holder)proxium.respond(channel)
.on(BalanceRequest.class, (sender, req) -> {
returnnewBalanceResponse(req.uuid(), getBalance(req.uuid()));
})
.error(e -> log.error("RPC failed: " + e.getMessage()));
// Request server (needs data)proxium.request("Survival-1", channel, newBalanceRequest(uuid))
.on(BalanceResponse.class, (sender, res) -> {
player.sendMessage("Balance: " + res.balance());
})
.error(e -> player.sendMessage("Request failed: " + e.type()));for (ProxyPlayerp : proxium.getPlayers().values()) {
System.out.println(p.getName() + " → " + p.getServer());
}Redisredis = getBridge(Redis.class);
redis.withLock("player-data-save", () -> { /* safe save */ });Details → docs/en/bridge.md
Unified JSON document-based API managing all databases with an identical interface.
Supported: JSON, SQLite, MySQL, MariaDB, PostgreSQL, MongoDB
registerStorage("PlayerData", StorageType.MARIADB);
Storagestorage = getStorage("PlayerData");
// Insertstorage.add(JSON.of("uuid", uuid.toString()).append("name", "IPECTER").append("coins", 1000));
// Querystorage.get(JSON.of("uuid", uuid.toString())).thenAccept(results -> {
if (!results.isEmpty()) {
intcoins = results.get(0).get("coins").getAsInt();
}
});
// Updatestorage.set(JSON.of("uuid", uuid.toString()), JSON.of("uuid", uuid.toString()).append("coins", 2000));Details → docs/en/storage.md
// Entity-based sync execution (Folia Region compatible)CraftScheduler.sync(player, () -> {
player.teleport(location);
});
// Async/sync chainingCraftScheduler.sync(plugin, task -> {
player.setHealth(20);
}).delay(task -> {
player.setHealth(1);
}, 20L);
// Safe sync result returnCraftScheduler.callSync(location, () -> {
returnlocation.getBlock().getType();
}).thenAccept(material -> {
notifier.announce("Block at location: " + material);
});Details → docs/en/scheduler.md
QuartzScheduler.run("DailyReset", "0 0 0 * * ?", MyJob.class);publicclassMyGUIextendsRSInventory<MyPlugin> {
publicMyGUI(MyPluginplugin) {
super(plugin);
}
publicvoidopen(Playerplayer) {
Inventoryinv = createInventory(27, ComponentFormatter.mini("My Inventory"));
player.openInventory(inv);
}
@OverridepublicbooleanonClick(Event<InventoryClickEvent> event, Clickclick) {
notifier.announce(event.player(), "Slot " + click.slot() + " clicked!");
returntrue;
}
}Unifies Nexo, Oraxen, ItemsAdder, MMOItems, EcoItems under a single API.
ItemStacksword = CustomItems.from("mmoitems:SWORD:FIRE_SWORD");
Stringid = CustomItems.to(player.getInventory().getItemInMainHand());
CustomBlocks.place(location, "oraxen:custom_ore");./gradlew shadowJar # Plugin JAR → builds/plugin/
./gradlew spotlessApply # Code formattingRequirements: JDK 21+, Gradle 9.3+