Skip to content

RTUStudio/RSFramework

Repository files navigation

RSFramework

모듈화된 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)


공통 제공 필드 (protected final)

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() { }
}

RSPluginonLoadinitialize()load()onEnableenable()onDisabledisable() 순서로 라이프사이클을 제공합니다.

자동 로깅

프레임워크가 플러그인의 활성화, 비활성화, 리로드 시점에 콘솔 메시지를 자동으로 출력합니다. 개발자가 직접 로깅 코드를 작성할 필요가 없습니다.

// ✅ 올바른 예시 — 로깅 없이 깔끔하게
@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)

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>서버에 오신 것을 환영합니다!");
    }
}

명령어 시스템 (RSCommand)

계층형 구조, 권한 자동 등록, 쿨다운, 탭 자동완성을 지원합니다.

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);
}

Result (명령어 실행 결과)

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 포맷과 접두사를 자동으로 처리합니다.
RSCommandexecute()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: "/테스트 서브"

설정 파일 관리 (Configuration)

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를 참조하세요.


다국어 지원 (Translation)

플레이어 클라이언트 언어(Locale)에 맞춰 자동으로 번역본을 반환합니다.

// Translation/Message/{locale}.yml 에서 키로 검색
String msg = message.get(player, "error.no-money");
notifier.announce(player, msg);

// 프레임워크 공통 번역
String common = message.getCommon("prefix");

메시지 전송 (Notifier)

MiniMessage 포맷 지원. 채팅, 액션바, 타이틀, 보스바, 크로스서버 브로드캐스트를 제공합니다.

getSender().sendMessage() 또는 player.sendMessage()를 직접 호출하지 마세요. 항상 notifier를 통해 메시지를 전송합니다. RSCommandexecute()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>새로운 이벤트가 시작되었습니다!");

브릿지 통신 (Bridge)

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));

Redis 전용 — 분산 락

import kr.rtustudio.bridge.redis.Redis;

Redis redis = framework.getBridgeRegistry().get(Redis.class);
redis.withLock("player-data-save", () -> { /* 안전한 저장 */ });
boolean ok = redis.tryLockOnce("daily-reward", () -> { /* 보상 지급 */ });

Proxium 전용 — 네트워크 정보

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를 참조하세요.


스토리지 (Storage)

다양한 데이터베이스를 통합 관리합니다. 설정 변경 시 변경된 커넥션만 재연결합니다.

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를 참조하세요.


스케줄러

CraftScheduler (Bukkit/Paper/Folia)

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);

QuartzScheduler (Cron)

import kr.rtustudio.framework.bukkit.api.scheduler.QuartzScheduler;

QuartzScheduler.run("DailyReset", "0 0 0 * * ?", MyJob.class);

인벤토리 UI (RSInventory)

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; // 이벤트 취소
    }
}

커스텀 블록/아이템 통합 (Registry)

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+

About

Framework for RTUStudio plugins

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages