A configurable CS2-inspired lootbox system for Minecraft Forge 1.20.1.
Create animated cases, optional matching keys, free-to-open drops, weighted loot tables, legendary sub-rolls, rarity effects, StatTrak-style rewards, custom sounds, and CS-style opening screens — all configurable through KubeJS.
CS2 Lootbox provides a reusable, registry-driven case system designed for modpacks and custom content.
-
CS2-inspired case opening UI
- Animated 3D case using GeckoLib / GeckoMesh
- Roulette-style item carousel
- Rarity-colored cards and glow effects
- Legendary / special reward stage
- Optional second legendary sub-loot carousel
- Prize reveal and result screen
- Responsive UI scaling for different GUI sizes and resolutions
-
3D item inspection
- Left-click normal case rewards to inspect them
- Drag to rotate
- Scroll to zoom
- Supports Minecraft item models, including 3D items and foil/glint rendering
-
Server-authoritative rewards
- The server decides the reward before the visual carousel finishes
- The client carousel is presentation only
- Closing the UI after a committed roll does not destroy the pending reward
-
KubeJS configuration
- Add completely new keyed or free-to-open cases
- Modify built-in cases
- Configure models, textures, animation names and transforms
- Configure weighted loot and stack counts
- Add custom names, descriptions and rarity colors
- Add NBT to rewards
- Configure sounds per case
- Configure legendary second-stage loot
-
StatTrak-style modifiers
- Optional StatTrak roll per loot entry
- Optional one-star / two-star variants
- Persistent kill counter stored on the awarded
ItemStack - Main-hand StatTrak item is preferred; offhand is used when applicable
-
Forge config options
- Toggle case opening server-side
- Toggle 3D inspection
- Toggle rarity glows
- Toggle UI and carousel sounds
- Toggle case-content tooltips
- Toggle StatTrak rolls and kill counting
- Suppress verbose GeckoMesh logging
| Dependency | Version |
|---|---|
| Minecraft | 1.20.1 |
| Minecraft Forge | 47.x |
| Java | 17 |
| KubeJS | 2001.6.5-build.16+ |
| GeckoLib | 4.8.4+ |
| GeckoMesh | 1.0.0+ |
| LDLib | 1.0.52.a+ |
KubeJS also uses its normal dependencies such as Rhino and Architectury API.
- Install Minecraft Forge 1.20.1.
- Install the required dependencies listed above.
- Place
cs2lootbox-<version>.jarin your Minecraftmodsfolder. - Start the game once so Forge can generate the configuration files.
- Add custom case definitions to:
kubejs/startup_scripts/
Custom models, textures, animations and translations belong under:
kubejs/assets/<namespace>/
Case registration happens during startup. Changes to case definitions require a full game restart;
/reloadis not enough.
For multiplayer, the client and server should use matching startup scripts and assets.
A complete case can be registered with CS2LootboxEvents.register.
CS2LootboxEvents.register(event => {
event.addCrate('kubejs:revolution', crate => {
crate.caseItem('kubejs:revolution_case')
crate.keyItem('kubejs:revolution_key')
crate.model('kubejs:geo/revolution.geo.json')
crate.texture('kubejs:textures/lootbox/revolution.png')
crate.animation('kubejs:animations/revolution.animation.json')
crate.keyTexture('kubejs:item/revolution_key')
// Item display transforms are applied by the GeckoLib item renderer.
// Values follow vanilla model-JSON semantics, but every context has its
// own independent KubeJS definition.
crate.firstPersonRight(0, 1, 0, 0, 135, 0, 1.3)
crate.firstPersonLeft(0, 1, 0, 0, 225, 0, 1.3)
crate.thirdPersonRight(0, 2.5, 0, 75, 45, 0, 1.3)
crate.thirdPersonLeft(0, 2.5, 0, 75, 45, 0, 1.3)
crate.ground(0, 1, 0, 0, 0, 0, 1.5)
crate.gui(0, -3, 0, 30, 135, 0, 1.5)
crate.fixed(0, -1.5, 0, 0, 0, 0, 1.5)
crate.position(22, 30)
crate.scale(78)
crate.rotation(0, 180, 0)
crate.animations('fall', 'idle', 'open', 'open_idle')
crate.itemIdleAnimation('idle')
crate.defaultSound(0.60, 1.0)
crate.dropSound('cs2lootbox:case_drop')
crate.openSound('minecraft:block.chest.open')
// Optional: repeated for the full OPEN animation.
// crate.openLoopSound('cs2lootbox:case_pins_fall')
crate.caseName('item.kubejs.revolution_case')
crate.keyName('item.kubejs.revolution_key')
// Optional opening-screen message: literal text or a translation key.
crate.singleOpenText('This container grants one reward')
crate.collection(
'The Revolution Collection',
'kubejs:textures/gui/collections/revolution.png'
)
crate.loot('minecraft:iron_ingot', 55, loot => {
loot.count(1, 4)
loot.name('loot.kubejs.iron')
loot.rarity('rarity.kubejs.industrial', 0x5E98D9)
loot.description('loot.kubejs.iron.description')
})
crate.loot('minecraft:diamond', 12, loot => {
loot.count(1, 2)
loot.name('loot.kubejs.diamond')
loot.rarity('rarity.kubejs.classified', 0xD32CE6)
})
// Absolute first-stage chance: 0.26%.
crate.legendary(0.26)
.itemListName('tooltip.kubejs.rare_item')
.tooltip('tooltip.kubejs.rare_item')
.foreground('kubejs:textures/gui/revolution_rare_item.png')
// Optional: skip the second legendary-only carousel.
// The server still rolls subLoot() and the final item is revealed directly.
.subLootCarousel(false)
.subLoot()
.add('minecraft:netherite_ingot', 5)
.add('minecraft:nether_star', 4)
})
})addCase(...) is also available as an alias of addCrate(...).
Keys are optional per case. Existing cases stay keyed by default. For a dossier, sticker case/capsule, or any other free drop:
CS2LootboxEvents.register(event => {
event.addCrate('kubejs:sticker_case', crate => {
crate.freeToOpen()
crate.model('kubejs:geo/sticker_case.geo.json')
crate.texture('kubejs:textures/lootbox/sticker_case.png')
crate.animation('kubejs:animations/sticker_case.animation.json')
crate.loot('minecraft:paper', 100)
})
})requiresKey(false) is equivalent. Free-to-open cases do not register, display, validate, or consume a key item. The case itself is still consumed after the reward is granted.
Use changeCase(...) to modify an already registered definition.
Existing values are copied first, so only the fields you change are replaced.
CS2LootboxEvents.register(event => {
event.changeCase('cs2lootbox:csgo_case_weapon', crate => {
crate.scale(105)
crate.rotation(90, 165, 0)
crate.clearLoot()
crate.loot('minecraft:iron_ingot', 70)
crate.loot('minecraft:diamond', 30)
crate.clearLegendary()
crate.legendary(0.26)
.subLoot()
.add('minecraft:netherite_ingot', 80)
.add('minecraft:nether_star', 20)
})
})Useful helpers include:
crate.clearLoot()
crate.removeLoot('namespace:item')
crate.clearLegendary()
crate.clearLegendaryLoot()The target case must already exist when changeCase(...) runs. Built-in cases are registered before the KubeJS startup event.
Several overloads are available:
crate.loot('minecraft:diamond', 10)
crate.lootCount('minecraft:diamond', 10, 3)
crate.loot('minecraft:diamond', 10, 1, 4)
crate.loot('minecraft:diamond', 10, loot => {
loot.count(1, 4)
})Normal loot weights are normalized against the total enabled normal loot weight.
For example:
A = 70
B = 30
produces a 70% / 30% split inside the normal-loot branch.
Available loot configuration includes:
loot.count(1)
loot.count(1, 4)
loot.name('translation.key')
loot.rarity('translation.key')
loot.rarity('translation.key', 0xRRGGBB)
loot.rarityColor(0xRRGGBB)
loot.rarityTier('covert')
loot.description('translation.key')
loot.nbt('{CustomModelData:123}')
loot.clearNbt()
loot.position(x, y)
loot.position(x, y, z)
loot.rotation(x, y, z)
loot.scale(scale)
loot.scale(x, y, z)
loot.transform(x, y, z, rotX, rotY, rotZ, scale)Item-list transforms only affect how that item appears in the case-content list.
Reward counts are capped to the selected item's real maximum stack size.
Invalid/unregistered item IDs and entries with zero weight are ignored by the roller.
Legendary rewards use a separate two-stage system.
crate.legendary(0.26)
.foreground('kubejs:textures/gui/my_special_item.png')
.subLoot()
.add('minecraft:netherite_ingot', 5)
.add('minecraft:nether_star', 4)The value passed to:
crate.legendary(0.26)is an absolute first-stage percentage, so 0.26 means exactly 0.26% per opening.
It is not added to the normal loot weight total.
The entries inside subLoot() form a second independent weighted table. They do not need to total 100.
The second legendary-only carousel is enabled by default. It can be disabled per legendary panel:
crate.legendary(0.26)
.itemListName('tooltip.kubejs.rare_item')
.tooltip('tooltip.kubejs.rare_item')
.subLootCarousel(false)
.subLoot()
.add('minecraft:netherite_ingot', 5)
.add('minecraft:nether_star', 4)When disabled, the server still performs the authoritative subLoot() roll immediately. After the primary gold card stops, the UI skips the second carousel and reveals the already-selected final item directly. subCarousel(false) and subListCarousel(false) are aliases.
The UI also skips pointless roulette stages automatically: if the primary carousel has only one valid configured result, it goes directly to the prize panel; if a legendary subLoot() table has only one valid entry, its second carousel is skipped even when subLootCarousel(true) is left enabled.
Legendary display text is split cleanly between the case contents and the case tooltip:
crate.legendary(0.26)
.itemListName('legendary.kubejs.rare_gloves') // name under the gold item_list entry
.tooltip('tooltip.kubejs.rare_gloves') // case-item hover tooltip lineitemListName(...) (or its short alias name(...)) accepts literal text or a translation key and controls only the legendary entry shown in the case contents item_list. tooltip(...) / tooltipText(...) controls the case item's hover tooltip. For backwards compatibility, the item_list falls back to tooltip(...) when no explicit item-list name is configured.
The primary legendary roulette card renders no text at all; it only shows the gold panel/foreground artwork.
For example:
netherite_ingot = 5
nether_star = 4
becomes approximately:
55.56% / 44.44%
after the legendary panel has already been selected.
Multiple legendary panels are supported, but their combined absolute first-stage chance cannot exceed 100%.
Loot entries can roll an optional StatTrak modifier.
crate.loot('minecraft:diamond_sword', 10, loot => {
loot.modifiers.add('modifier.stat_track', 15)
})This gives the reward a 15% absolute chance to become StatTrak.
Optional star rolls can also be configured:
crate.loot('minecraft:diamond_sword', 10, loot => {
loot.modifiers.add(
'modifier.stat_track',
15, // StatTrak chance
10, // conditional one-star chance
2 // conditional two-star chance after one-star succeeds
)
})StatTrak data is stored directly on the awarded ItemStack.
When enabled by server config, a kill caused by the player increments the counter of the held StatTrak item. The main hand is checked first, followed by the offhand.
Each case has default volume and pitch values:
crate.defaultSound(0.60, 1.0)
crate.defaultSoundVolume(0.60)
crate.defaultSoundPitch(1.0)Individual cues can then be overridden:
crate.dropSound('namespace:fall_sound')
crate.openSound('namespace:open_sound')
crate.openLoopSound('namespace:open_loop_sound') // optional, repeats only during OPEN
crate.carouselTickSound('namespace:sound')
crate.rewardSound('namespace:sound')
crate.uncommonSound('namespace:sound')
crate.rareSound('namespace:sound')
crate.mythicalSound('namespace:sound')
crate.classifiedSound('namespace:sound')
crate.covertSound('namespace:sound')
crate.specialSound('namespace:sound')Each sound setter also supports explicit volume and pitch:
crate.dropSound('namespace:fall_sound', 0.60, 1.0)
crate.openSound('namespace:open_sound', 0.50, 1.0)
crate.openLoopSound('namespace:open_loop_sound', 0.45, 1.0)
crate.carouselTickSound('namespace:sound', 0.30, 1.0)openLoopSound(...) is stopped automatically as soon as GeckoLib's OPEN animation ends, or if the UI closes. Use noOpenLoopSound() to clear it when modifying an existing case.
The mod includes its own CS-style case UI sound set, including carousel ticks, reveal sounds and rarity-specific awarded sounds.
The mod includes these built-in CS2-style rarity translation keys:
cs2lootbox.rarity.consumer
cs2lootbox.rarity.industrial
cs2lootbox.rarity.milspec
cs2lootbox.rarity.restricted
cs2lootbox.rarity.classified
cs2lootbox.rarity.covert
cs2lootbox.rarity.special
Custom translation keys and colors can be supplied per loot entry.
Example asset layout for a case using the namespace kubejs:
kubejs/
├─ startup_scripts/
│ └─ cs2lootbox_crates.js
│
└─ assets/
└─ kubejs/
├─ geo/
│ └─ revolution.geo.json
├─ animations/
│ └─ revolution.animation.json
├─ textures/
│ ├─ lootbox/
│ │ └─ revolution.png
│ ├─ item/
│ │ └─ revolution_key.png
│ └─ gui/
│ └─ collections/
│ └─ revolution.png
└─ lang/
└─ en_us.json
The repository also contains example files under:
examples/kubejs/
including example startup scripts and translations.
The mod generates three Forge config files.
Controls presentation only:
showCaseLootTooltip
showStatTrackKillCount
enable3DItemInspection
enableRarityGlows
enableUiSounds
enableCarouselTickSound
silenceGeckoMeshInfoLogs
GeckoMesh can be very verbose while loading mesh data, so this is enabled by default.
Server-authoritative gameplay settings:
allowCaseOpening
enableStatTrackRolls
enableStatTrackKillCounting
Forge server configs are world-specific and are stored in the world's serverconfig directory.
The opening sequence is intentionally server-authoritative.
- The case is displayed and animated through GeckoLib.
- The player requests an opening.
- The server validates the case and loot definition, plus the matching key only when
requiresKey(true). - The server rolls and locks the actual reward.
- For keyed cases, the matching key is consumed; free-to-open cases skip this step.
- The case-opening animation plays.
- If the primary pool has more than one valid configured result, the client displays the cosmetic roulette; otherwise it goes directly to the prize screen.
- When shown, the carousel snaps the winning card to the center marker.
- A legendary result transitions into its second legendary-only carousel only when that carousel is enabled and the
subLoot()table contains more than one valid entry. - The final prize screen is shown.
- Accept grants the already-rolled reward and consumes the case.
- If the UI closes after the roll is committed, the same pending prize is finalized rather than lost.
The roulette animation cannot change the server-selected reward.
Cases use GeckoLib / GeckoMesh for their animated 3D presentation.
Carousel rewards use Minecraft's normal ItemStack renderer, which means:
- generated item models remain 2D;
- normal 3D item/block models remain 3D;
- foil/glint items are supported;
- custom resource-pack or modded item models can be displayed.
The item inspector provides a dedicated 3D viewport with independent rotation and zoom.
The opening interface is authored around a 960 × 540 reference canvas and scales uniformly to the current Minecraft GUI size.
Clone the repository and build with the included Gradle wrapper.
gradlew.bat build./gradlew buildThe built mod JAR will be placed in:
build/libs/
The project uses:
- Java 17
- ForgeGradle 6
- Official Mojang mappings for Minecraft 1.20.1
The main systems are split into reusable packages:
api/lootbox/
LootboxDefinition
LootboxDefinitionBuilder
LootEntry
LootEntryBuilder
LegendaryLoot
LegendaryLootBuilder
LegendarySubLootBuilder
loot/
LootboxLootRoller
StatTrackUtil
registry/
LootboxRegistry
LootboxRegistrationService
BuiltInLootboxes
ModSounds
integration/kubejs/
CS2LootboxKubeJSPlugin
LootboxStartupRegisterEvent
client/widget/
LootboxModelWidget
LootboxOverlayWidget
The same generic case item, optional key item, and UI implementation are reused by registered lootbox definitions rather than requiring a new Java class for every case.
CS2 Lootbox is licensed under the Apache License 2.0.
See LICENSE for details.
This is an independent Minecraft mod inspired by the presentation of Counter-Strike case openings.
It is not affiliated with, endorsed by, or sponsored by Valve Corporation. Counter-Strike, CS:GO and CS2 are trademarks of their respective owners.
Lootbox definitions can customize the one-shot fall/appearance sound with dropSound(...) and an optional sample that repeats for the full GeckoLib OPEN animation with openLoopSound(...). The loop stops automatically when OPEN ends or the UI closes. The built-in agent dossier uses case_patch_fall and case_pins_fall.