모듈화된 Bukkit/Paper 플러그인 개발 프레임워크.
버전: 4.2.0 · Java: 21 · 지원 서버: 1.20.1+ (Spigot/Paper/Folia) · 라이선스: GPL-3.0
RSFramework/
├── LightDI/ 경량 DI 컨테이너 (kr.rtustudio.cdi)
├── Configurate/ YAML 객체 매핑 래퍼 (kr.rtustudio.configurate.model)
├── Storage/ 통합 스토리지 시스템
│ ├── Common/ 공통 API (Storage, StorageType)
│ ├── MySQL / MariaDB / PostgreSQL / MongoDB / SQLite / Json
├── Bridge/ 서버 간 Pub/Sub 브로커
│ ├── Common/ Bridge 인터페이스, BridgeChannel, BridgeOptions
│ ├── Redisson/ Redis 구현체
│ └── Proxium/ Netty 기반 자체 프록시 통신
│ ├── Common/API Proxium 공개 API
│ ├── Common/Core AbstractProxium, ProxiumServer, ProxiumProxy
│ ├── Bukkit / Bungee / Velocity
├── Platform/ 플랫폼 어댑터
│ ├── Spigot / Paper / Folia Bukkit 계열
│ ├── Bungee / Velocity 프록시 계열
├── Framework/ 프레임워크 본체
│ ├── API/ RSPlugin, RSCommand, RSListener 등 공개 API
│ ├── Core/ 내부 구현
│ └── NMS/ 버전별 NMS 어댑터 (1.20 R1 ~ 1.21 R7)
└── docs/ 기술 문서 (bridge, configuration, storage)
빌드 산출물: 루트 shadowJar 태스크가 모든 모듈을 하나의 플러그인 JAR로 합칩니다. (builds/plugin/RSFramework-{version}.jar)
RSCommand, RSListener, RSInventory는 모두 동일한 공통 필드를 protected final로 제공합니다. getPlugin(), getFramework() 같은 Getter 없이 필드에 직접 접근하여 사용합니다.
| 필드 | 타입 | 설명 |
|---|---|---|
plugin |
T (플러그인 타입) |
소유 플러그인 인스턴스 |
framework |
Framework |
프레임워크 코어 |
message |
MessageTranslation |
다국어 메시지 번역 |
command |
CommandTranslation |
다국어 명령어 번역 |
notifier |
Notifier |
메시지 전송 유틸리티 |
RSCommand는 추가로 다음 필드를 제공한다.
| 필드 | 타입 | 설명 |
|---|---|---|
sender |
CommandSender |
명령어 실행자 (getter 접근) |
player |
Player |
명령어 실행 플레이어 (플레이어가 아닌 경우 null 반환) |
audience |
Audience |
Adventure Audience (getter 접근) |
// ✅ 올바른 사용
plugin.reloadConfiguration(MyConfig.class);
notifier.announce(player, "완료!");
// ❌ 불필요한 getter 호출
getPlugin().reloadConfiguration(MyConfig.class);RSPlugin을 상속받아 메인 클래스를 작성합니다.
import kr.rtustudio.framework.bukkit.api.RSPlugin;
public class MyPlugin extends RSPlugin {
@Override
protected void enable() {
// 명령어·리스너·설정 등록
}
@Override
protected void disable() { }
}RSPlugin은 onLoad → initialize() → load() → onEnable → enable() → onDisable → disable() 순서로 라이프사이클을 제공합니다.
프레임워크가 플러그인의 활성화, 비활성화, 리로드 시점에 콘솔 메시지를 자동으로 출력합니다. 개발자가 직접 로깅 코드를 작성할 필요가 없습니다.
// ✅ 올바른 예시 — 로깅 없이 깔끔하게
@Override
protected void enable() {
registerConfiguration(PerkConfig.class, ConfigPath.of("Perk"));
registerCommand(new MainCommand(this), true);
registerEvent(new PlayerAttack(this));
}
@Override
protected void disable() {
if (perkModule != null) {
perkModule.close();
}
}// ❌ 잘못된 예시 — 프레임워크가 이미 출력하므로 중복됨
@Override
protected void enable() {
// ...
getLogger().info("MyPlugin Enabled!"); // 불필요
}
@Override
protected void disable() {
// ...
getLogger().info("MyPlugin Disabled!"); // 불필요
}RSListener<T>를 상속하면 DI를 통해 이벤트가 자동으로 등록됩니다.
import kr.rtustudio.framework.bukkit.api.listener.RSListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
public class JoinListener extends RSListener<MyPlugin> {
public JoinListener(MyPlugin plugin) {
super(plugin);
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
notifier.announce(event.getPlayer(), "<green>서버에 오신 것을 환영합니다!");
}
}계층형 구조, 권한 자동 등록, 쿨다운, 탭 자동완성을 지원합니다.
import kr.rtustudio.framework.bukkit.api.command.RSCommand;
import kr.rtustudio.framework.bukkit.api.command.CommandArgs;
import org.bukkit.permissions.PermissionDefault;
public class MainCommand extends RSCommand<MyPlugin> {
public MainCommand(MyPlugin plugin) {
super(plugin, "test", PermissionDefault.OP);
registerCommand(new SubCommand(plugin));
}
@Override
protected Result execute(CommandArgs data) {
notifier.announce("메인 명령어 실행됨!");
return Result.SUCCESS;
}
@Override
protected void reload(CommandArgs data) {
plugin.reloadConfiguration(TestConfig.class);
plugin.reloadConfigurations(ListConfig.class);
}
}
public class SubCommand extends RSCommand<MyPlugin> {
public SubCommand(MyPlugin plugin) {
super(plugin, "sub", PermissionDefault.OP);
}
@Override
protected Result execute(CommandArgs data) {
notifier.announce("서브 명령어 실행됨!");
return Result.SUCCESS;
}
}enable()에서 등록 시 true를 전달하면 /{명령어} reload 서브 명령어가 자동 추가됩니다.
이 자동 생성된 reload 명령어는 프레임워크가 자체적으로 처리하여 완료 메시지까지 출력하므로 별도의 번역 파일 정의, 탭 자동완성 구현, 또는 execute() 로직 작성이 전혀 필요하지 않습니다. 오직 reload() 메서드만 오버라이드하여 리로드 시 실행할 커스텀 로직을 정의하면 됩니다.
@Override
protected void enable() {
framework.registerCommand(new MainCommand(this), true);
}execute() 메서드의 반환값에 따라 프레임워크가 자동으로 공통 안내 메시지를 발송합니다. 개발자가 직접 메시지를 작성하거나 조건 분기를 할 필요가 없습니다.
| Result | 설명 | 프레임워크 동작 |
|---|---|---|
SUCCESS |
성공 | 없음 |
FAILURE |
실패 (개별 처리 필요) | 없음 — 필요 시 직접 notifier로 안내 |
ONLY_PLAYER |
플레이어만 실행 가능 | 자동 안내 메시지 출력 |
ONLY_CONSOLE |
콘솔만 실행 가능 | 자동 안내 메시지 출력 |
NO_PERMISSION |
권한 없음 | 자동 안내 메시지 출력 |
NOT_FOUND_ONLINE_PLAYER |
온라인 플레이어를 찾을 수 없음 | 자동 안내 메시지 출력 |
NOT_FOUND_OFFLINE_PLAYER |
오프라인 플레이어를 찾을 수 없음 | 자동 안내 메시지 출력 |
NOT_FOUND_ITEM |
아이템을 찾을 수 없음 | 자동 안내 메시지 출력 |
WRONG_USAGE |
잘못된 사용법 | 서브 명령어 목록 및 usage 자동 표시 |
// ✅ 올바른 예시 1 — player() 메서드로 간단히 체크
@Override
protected Result execute(CommandArgs data) {
Player player = player();
if (player == null) return Result.ONLY_PLAYER;
notifier.announce("환영합니다!");
return Result.SUCCESS;
}// ✅ 올바른 예시 2 — 대상 플레이어가 온라인이어야 하는 경우
@Override
protected Result execute(CommandArgs data) {
Player target = Bukkit.getPlayer(data.get(0));
if (target == null) return Result.NOT_FOUND_ONLINE_PLAYER;
notifier.announce(target.getName() + "님에게 아이템을 지급했습니다!");
return Result.SUCCESS;
}// ❌ 잘못된 예시 — 프레임워크가 이미 처리하는 메시지를 직접 작성
@Override
protected Result execute(CommandArgs data) {
Player target = Bukkit.getPlayer(data.get(0));
if (target == null) {
getSender().sendMessage("온라인 플레이어를 찾을 수 없습니다."); // 불필요 (NOT_FOUND_ONLINE_PLAYER 반환으로 대체)
return Result.FAILURE;
}
return Result.SUCCESS;
}메시지 전송 시
getSender().sendMessage()가 아닌notifier를 사용합니다.notifier는 MiniMessage 포맷과 접두사를 자동으로 처리합니다.
RSCommand의execute()와tabComplete()내부에서는 명령어 실행자(sender/player)가 자동으로 수신자로 설정되므로,notifier.announce("메시지")처럼 대상 지정 없이 사용할 수 있다. (명시적으로 지정하려면audience()사용 가능)
RSCommand 생성자에 전달되는 식별자(예: "test")는 Translation/Command/{언어}.yml 파일에서 명령어의 이름, 설명, 사용법, 서브 명령어 등을 정의하는 최상위 키로 사용됩니다. 이를 통해 명령어의 메타데이터를 소스 코드가 아닌 설정 파일에서 유연하게 관리할 수 있습니다.
기본 구조 (단일 명령어):
test:
name: "테스트"서브 명령어가 포함된 계층형 구조:
test:
name: "테스트"
command:
sub:
name: "서브"설명(description) 및 사용법(usage)을 포함한 상세 구조:
test:
name: "테스트"
description: "테스트 명령어 입니다"
usage: "/테스트"
command:
sub:
name: "서브"
description: "서브 테스트 명령어 입니다"
usage: "/테스트 서브"Sponge Configurate 기반 YAML 객체 매핑을 지원합니다. ConfigurationPart를 상속하거나 @ConfigSerializable record를 사용합니다.
@ConfigSerializable을 일반 클래스에 붙이면 기본 생성자(NoArgsConstructor)가 필요합니다.record를 사용하면 생성자 제약 없이 불변 객체를 매핑할 수 있습니다.
import kr.rtustudio.configurate.model.ConfigurationPart;
public class MyConfig extends ConfigurationPart {
public String welcomeMessage = "<green>환영합니다!";
public int maxPlayers = 100;
}import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ConfigSerializable
public record MyConfig(String welcomeMessage, int maxPlayers) {
public MyConfig() {
this("<green>환영합니다!", 100);
}
}import kr.rtustudio.configurate.model.ConfigPath;
import kr.rtustudio.configurate.model.ConfigList;
@Override
protected void enable() {
// 단일 파일: Config/Setting.yml
registerConfiguration(MyConfig.class, ConfigPath.of("Setting"));
MyConfig config = getConfiguration(MyConfig.class);
// 폴더: Config/Regions/*.yml
registerConfigurations(RegionConfig.class, ConfigPath.of("Regions"));
ConfigList<RegionConfig> regions = getConfigurations(RegionConfig.class);
RegionConfig spawn = regions.get("spawn"); // spawn.yml
for (RegionConfig r : regions.values()) { ... }
}/reload 호출 시 파일 추가·삭제까지 자동 반영됩니다. 상세 내부 구조는 docs/configuration.md를 참조하세요.
플레이어 클라이언트 언어(Locale)에 맞춰 자동으로 번역본을 반환합니다.
// Translation/Message/{locale}.yml 에서 키로 검색
String msg = message.get(player, "error.no-money");
notifier.announce(player, msg);
// 프레임워크 공통 번역
String common = message.getCommon("prefix");MiniMessage 포맷 지원. 채팅, 액션바, 타이틀, 보스바, 크로스서버 브로드캐스트를 제공합니다.
getSender().sendMessage()또는player.sendMessage()를 직접 호출하지 마세요. 항상notifier를 통해 메시지를 전송합니다.RSCommand의execute()와tabComplete()내부에서는 명령어 실행자가 자동으로 수신자로 설정되므로 대상 지정 파라미터를 생략할 수 있습니다.
// RSCommand 외부 (RSListener, RSInventory 등)
notifier.announce(player, "<aqua>아이템을 지급받았습니다!"); // 접두사 포함
notifier.send(player, "<yellow>경고 메시지"); // 접두사 제외
notifier.title(player, "<bold><gold>레벨 업!", "<gray>새 스킬 해제");
// RSCommand 내부 (파라미터 생략 가능)
notifier.announce("<aqua>명령어 실행 완료!");
// 전체 서버
Notifier.broadcastAll("<green>새로운 이벤트가 시작되었습니다!");Redis(Redisson) 또는 Proxium을 통한 서버 간 Pub/Sub 메시징입니다. 구현체와 관계없이 동일한 코드 패턴을 사용합니다.
import kr.rtustudio.bridge.Bridge;
import kr.rtustudio.bridge.BridgeChannel;
Bridge bridge = framework.getBridge(Proxium.class); // 또는 Redis.class
BridgeChannel channel = BridgeChannel.of("myplugin", "shop");
bridge.register(channel, BuyRequest.class, SellRequest.class);
bridge.subscribe(channel, packet -> {
if (packet instanceof BuyRequest buy) {
getLogger().info(buy.playerName() + "님이 구매를 요청했습니다.");
}
});
bridge.publish(channel, new BuyRequest("ipecter", "DIAMOND", 64));import kr.rtustudio.bridge.redis.Redis;
Redis redis = framework.getBridgeRegistry().get(Redis.class);
redis.withLock("player-data-save", () -> { /* 안전한 저장 */ });
boolean ok = redis.tryLockOnce("daily-reward", () -> { /* 보상 지급 */ });import kr.rtustudio.bridge.proxium.api.Proxium;
import kr.rtustudio.bridge.proxium.api.proxy.ProxyPlayer;
Proxium proxium = framework.getBridge(Proxium.class);
for (ProxyPlayer p : proxium.getPlayers().values()) {
System.out.println(p.name() + " → " + p.server());
}상세 아키텍처는 docs/bridge.md를 참조하세요.
다양한 데이터베이스를 통합 관리합니다. 설정 변경 시 변경된 커넥션만 재연결합니다.
import kr.rtustudio.storage.Storage;
import kr.rtustudio.storage.StorageType;
registerStorage("UserData", StorageType.MYSQL);
Storage storage = getStorage("UserData");
if (storage != null && storage.isConnected()) {
Object connection = storage.getConnection();
}지원 타입: JSON, SQLite, MySQL, MariaDB, PostgreSQL, MongoDB
상세 내용은 docs/storage.md를 참조하세요.
Folia와 호환되며 체이닝을 통해 후속 작업을 연결할 수 있습니다.
import kr.rtustudio.framework.bukkit.api.scheduler.CraftScheduler;
CraftScheduler.sync(plugin, task -> {
player.setHealth(20);
}).delay(task -> {
player.setHealth(1);
}, 20L);
CraftScheduler.delay(plugin, task -> {
plugin.getLogger().info("비동기 1초 뒤 실행");
}, 20L, true);import kr.rtustudio.framework.bukkit.api.scheduler.QuartzScheduler;
QuartzScheduler.run("DailyReset", "0 0 0 * * ?", MyJob.class);import kr.rtustudio.framework.bukkit.api.inventory.RSInventory;
import org.bukkit.event.inventory.InventoryClickEvent;
public class MyGUI extends RSInventory<MyPlugin> {
public MyGUI(MyPlugin plugin) {
super(plugin);
}
public void open(Player player) {
Inventory inv = createInventory(27, ComponentFormatter.mini("내 인벤토리"));
player.openInventory(inv);
}
@Override
public boolean onClick(Event<InventoryClickEvent> event, Click click) {
notifier.announce(event.player(), "슬롯 " + click.slot() + " 클릭됨!");
return true; // 이벤트 취소
}
}Nexo, Oraxen, ItemsAdder, MMOItems, EcoItems 등을 단일 API로 통합합니다. 식별자는 플러그인:아이디 형식을 사용합니다.
import kr.rtustudio.framework.bukkit.api.registry.CustomItems;
import kr.rtustudio.framework.bukkit.api.registry.CustomBlocks;
ItemStack sword = CustomItems.from("mmoitems:SWORD:FIRE_SWORD");
String id = CustomItems.to(player.getInventory().getItemInMainHand());
CustomBlocks.place(location, "oraxen:custom_ore");
String blockId = CustomBlocks.to(location.getBlock());./gradlew shadowJar # 플러그인 JAR 빌드 → builds/plugin/
./gradlew spotlessApply # 코드 포맷팅요구사항: JDK 21+, Gradle 9.3+