Developer Notes
For contributors and curious admins. Built with Gradle (Kotlin DSL), Java 21, Paper API 1.21.4, packetevents 2.12.2 (compileOnly). Optional integrations are reflection-based.
Build
./gradlew build
# output: build/libs/CradGacha-1.0.0.jarPackage layout
com.threebstudio.cradgacha
├─ CradGacha # main plugin: wiring, config merge, lifecycle
├─ Crate / CrateManager # crate model + loading from crates.yml
├─ Reward / Rarity # reward + rarity records
├─ CostService # charges NONE/ITEM/MONEY/TOKEN, affordableCount()
├─ CooldownManager # in-memory open cooldowns
├─ DataStore # data.yml (pity counters)
├─ PendingStore # pending.yml (crash-safe reward delivery)
├─ TokenStore # tokens.yml (token currency)
├─ ItemService # "namespace:id" -> ItemStack (vanilla/Nexo/Oraxen/ItemsAdder)
├─ OpenFlow # cost + cooldown + checks, then start a reveal
├─ Util # colour text, effects/titles
├─ cursor/ # the hologram-cursor engine + menus
│ ├─ CursorMenuManager # core engine: spawn page, spectator freeze, cursor tick, clicks
│ ├─ CursorMenuLoader # loads YAML menu pages (optional/legacy)
│ ├─ CursorSession # per-player runtime state
│ ├─ HologramPage / Hologram / ClickArea / AnimationStep # page model
│ ├─ CursorCrateMenu # the crate-select menu (theme-driven)
│ ├─ CursorRevealMenu # the card reveal page (cursor mode)
│ ├─ CursorResultPanel # the "open again" panel
│ └─ CursorRatesMenu # the drop-rates page
├─ reveal/ # fallback world-card reveal (no packetevents)
│ ├─ RevealManager / RevealSession / RevealDispatcher
├─ packet/ # packetevents glue
│ ├─ CameraLock # rotation + Set Camera packets
│ └─ CursorInput # reads client yaw/pitch + click packets
├─ hook/ # reflection integrations
│ ├─ VaultHook / ItemsAdderHook / NexoHook / OraxenHook
│ └─ BetterModelHook / ModelEngineHook
└─ command/GachaCommand # /gacha + tab completionConfig merge
config.yml is the base. cursor.yml, crates.yml, and theme.yml are merged into it at runtime under the keys cursor.*, crates.*, and theme.* (flattened leaf paths). This lets the code read one config while admins edit separate files. theme.yml is chosen by the theme: key (defaults to theme.yml, otherwise themes/<name>.yml).
Main flow
/gacha→CursorCrateMenu.open→CursorMenuManager.openPage: locks the player, spawns the hologram page, sets up the spectator camera freeze, starts the tick.- Click Open → a registered action calls
OpenFlow.startOpen: clamps count, checks ground/cooldown/inventory space, charges cost viaCostService. RevealDispatcher.open→ (cursor)CursorRevealMenu.openor (fallback)RevealManager.open.- Rewards are rolled (
Crate.roll, with pity fromDataStore) and immediately written toPendingStore(pending.yml). - Cards flip; on close,
PendingStore.deliverhands rewards to the player (online + alive checks), removing them from disk atomically to avoid duplicates.
The cursor / camera technique
mode: spectator uses: player → SPECTATOR gamemode, mounted on an invisible armor stand (so head rotation still flows as packets), plus a Set Camera packet so the view renders from the still armor stand (frozen view). CursorInput reads the raw yaw/pitch and click packets; the cursor is a display entity moved each tick. Teardown is wrapped in try/catch and backed by gamemode-recovery.yml.
Reward safety
The source of truth for delivery is PendingStore, not the UI. Rewards survive crash, disconnect, death, and /reload because they are persisted at roll time and delivered on join/respawn/enable.
Extension points / future ideas
- Animation presets (pulse/pop/shake) —
AnimationStepis currently scale + glow only. - More billboard/rotation options for holograms.
- Additional cost types or a placeholder/PlaceholderAPI hook.
- A namespaced ItemsAdder content pack of its own (currently reuses
crates_gacha:*).
Adding a custom menu action
CursorMenuManager.registerAction(name, handler) lets you bind new theme actions without touching the engine — that's how select_crate, open_count, open_all, open_rates, close_menu, and link_url are wired in CursorCrateMenu.register.