Small server-side Forge mod for Minecraft 1.12.2 that lets specific players
keep their inventory on death without flipping the global keepInventory
gamerule for the whole server. Built against Forge 14.23.5.2860 for use
with RLCraft Dregora.
Server ops opt individual players into one of two preservation modes via
/keepinv. Players not on the list die normally and drop items as usual
(Corail Tombstone, vanilla, whatever you have handling drops).
- Don't want a config file? Use v1.1. Behavior is hardcoded to
the threshold table below, no
config.yml, nodeath-msgs.yml. Stable, small, nothing to tune. - Want to tune thresholds / divisor range / messages, or use the latest
fixes? Use the latest release (currently v2.x). The first
server start writes a
config.ymlyou can edit; everything in there defaults to the v1.1 hardcoded behavior, so it's a drop-in upgrade.
Either way the command surface and mod ID are identical. Switching versions without changing config files is safe: v1.1 ignores any config files present, and v2.x falls back to its defaults if the file is missing.
All require op (permission level 2):
/keepinv add <player> add in default mode (XP-cost preservation)
/keepinv add <player> all add in all mode (keep everything, no cost)
/keepinv remove <player> remove from the list
/keepinv list list everyone protected
/keepinv list <player> show one player's mode
The target player must have joined the server at least once. The mod resolves their name to a UUID through the server's profile cache.
Re-running add with a different mode switches the player. Re-running with the
same mode just reports as already set. The list is saved to
<server>/config/selectivekeepinv/players.json and survives restarts.
all keeps everything on death: vanilla inventory, armor, offhand, baubles,
trinkets, and full XP including partial level progress. Nothing drops. No XP
orbs. No tombstone. Functionally identical to keepInventory true for that one
player.
default is the interesting one. What gets preserved depends on the
player's XP level at the moment of death. XP is the cost: it gets spent for
the things you keep. XP orbs still drop as usual so you can run back and grab
some of it.
Each row unlocks at that level and adds to everything below it.
| Level | Adds |
|---|---|
| 1..9 | hotbar slots 1..N (left to right) |
| 10 | offhand |
| 11 | helmet |
| 16 | chestplate |
| 21 | leggings |
| 26 | boots (full armor) |
| 50 | accessories (baubles + trinkets combined) |
| 100 | the rest of the inventory (27 main slots) plus XP carryover |
The hotbar fills left to right. A level-5 death keeps slots 1-5 and drops 6-9. The cost is fixed by the level you died at. Empty slots count toward it the same way. No refunds for unused slots.
At level 100 and above you start getting XP back on respawn. The amount is a
gamble. On death the mod rolls a divisor of 1, 2, or 3, and you respawn with
(deathLevel - 100) / D XP. So at level 200 you could get 100, 50, or 33
back depending on the roll. You don't see the divisor directly. A flavor line
on respawn hints at how lucky (or not) the roll was.
If Baubles (Azanor's 7-slot one with amulet, head, body, two rings, belt, charm) is installed, the accessories tier at level 50+ covers all 7 of those slots.
If Trinkets and Baubles
by XzeroAir (modid xat) is installed, the same tier covers its extra trinket
slots too.
Other bauble-using mods route their items through the standard Baubles slots so they're covered automatically: Bountiful Baubles, Baubley Elytra, etc. SpartanHUD Baubles is client-side HUD overlay only, unaffected.
The mod runs fine without either of these installed. The accessory code just doesn't fire.
Listed players get a short chat message on respawn. Three lines, with a fourth when XP carryover applies:
[SelectiveKeepInv]
You Died at level 150.
Your funeral procession is 312 blocks away.
Lucky roll. Most of your XP survived.
The lines are picked from pools. ALL mode gets light praise. Default mode in
the same dimension gets walking distance and mockery. Default mode after a
cross-dimension death gets extra savagery (your stuff is somewhere else, deal
with it). Pools live in DeathMessages.java. Edit freely.
- Drop
selectivekeepinv-1.0.jarinto the server'smods/folder. - Start the server. Look for
Selective Keep Inventoryin the load log. - As op:
/keepinv add YourFriendto protect someone.
Clients do not need the jar. It's marked acceptableRemoteVersions = "*" so
anyone can connect. Putting it client-side is harmless but does nothing useful.
Keep vanilla /gamerule keepInventory set to false. This mod runs
independently of that gamerule and only touches players on its own list.
<server>/config/selectivekeepinv/players.json
Plain JSON map from UUID to mode:
{
"550e8400-e29b-41d4-a716-446655440000": "default",
"abc12345-...": "all"
}Editable by hand if the server is stopped. Unknown mode strings are dropped on load. Malformed JSON falls back to an empty list with a stack trace in the log rather than crashing.
The threshold values described above are no longer hardcoded. They live in
config/selectivekeepinv/config.yml, written automatically on first server
start with the v1.1 defaults. Edit and restart the server to apply.
thresholds:
hotbar-per-slot: 1 # XP cost per hotbar slot (leftmost first)
offhand: 10
helmet: 11
chestplate: 16
leggings: 21
boots: 26
accessories: 50
main-inventory: 100
xp-carryover: 100 # level at which the XP gamble begins
xp-carryover:
divisor-min: 1 # set min = max to disable the gamble
divisor-max: 3
# The XP-roll flavor line on respawn (lucky / mid / brutal) is bucketed
# from where your rolled divisor falls in [divisor-min, divisor-max]:
# quality = (max - divisor) / (max - min)
# >= 2/3 -> xp-roll-lucky
# >= 1/3 -> xp-roll-mid
# else -> xp-roll-brutal
# So widening divisor-max doesn't dump every roll into "brutal". You don't
# need to add more pools; the existing three cover any range.
# When divisor-min == divisor-max (no gamble), no flavor line is shown.
behavior:
skip-spectators: true
all-mode-cancels-xp-drops: true
messages:
enabled: true # set false to suppress all mod chat output
show-xp-roll-flavor: true # fourth-line XP-roll hint on respawn
override-corny-msgs: false # use death-msgs.yml instead of built-in poolsMissing fields fall back to defaults silently (won't get a re-write). A
malformed file falls back to defaults entirely so the mod still loads, and
a warning is broadcast to op players on next login (also logged to
latest.log with a [SelectiveKeepInv] WARN: prefix). Reload requires a
server restart — there's no /keepinv reload yet.
Set messages.override-corny-msgs: true in config.yml, then restart.
On first start with the flag enabled, the mod writes
config/selectivekeepinv/death-msgs.yml with the built-in pools so you
have a starting point. Edit, restart, done.
The file has eight sections, each a list of strings:
all-lines:
- "The reaper went home empty-handed."
- "..."
all-lines-with-xp: # extra praise when deathLevel > 0
- "Loot AND levels intact. Disgusting."
all-lines-no-xp: # extra mock for level-0 ALL deaths
- "Nothing to drop, nothing to lose. Tragic."
same-dim-lines: # %s is replaced with distance in blocks
- "Your funeral procession is %s blocks away."
diff-dim-lines: # used when respawn dimension != death dimension
- "Your stuff is in another dimension. Cry about it."
xp-roll-lucky: # divisor = 1
- "The XP gods smiled today."
xp-roll-mid: # divisor = 2
- "The XP roll was meh."
xp-roll-brutal: # divisor = 3
- "Brutal XP roll. Ouch."Validation rules:
- All eight sections must be present.
- No pool may be empty.
- Every
same-dim-linesentry must contain exactly one%s(and no other%Xformat specifier). - No other pool may contain any
%Xformat specifier.
If validation fails for any reason (missing section, empty pool, wrong
%s count, malformed YAML), the mod:
- Reverts
override-corny-msgstofalsefor this run. - Uses the built-in pools so nothing crashes.
- Logs a
[SelectiveKeepInv] WARN:line tolatest.logexplaining what's wrong (which section, which line number, what's expected). - Broadcasts the same warning to op players when they log in, so it surfaces even if you don't open the log.
Fix the file and restart to re-enable custom messages.
- Restore the v1.0 progression (XP carryover only at level 200+) by
setting
xp-carryover: 200andmain-inventory: 100separately. - Disable the XP gamble with
divisor-min: 1, divisor-max: 1. - Effectively disable a tier (e.g. don't preserve accessories at all)
by setting that threshold to something absurd like
9999. - Mute the death messages entirely with
messages.enabled: false.
You need:
- JDK 8. Forge 1.12.2 will not build with newer Java.
Adoptium Temurin 8 or Azul Zulu 8 both work. Set
JAVA_HOMEto point at it. - Forge 1.12.2 MDK build 14.23.5.2860. Download the Mdk zip from https://files.minecraftforge.net/net/minecraftforge/forge/index_1.12.2.html and extract it somewhere.
In the MDK:
- Delete the example mod folder:
src/main/java/com/example/. - Copy this repo's
src/main/java/com/flashminat0/into the MDK'ssrc/main/java/com/. - Copy
src/test/from this repo into the MDK root (sibling ofsrc/main/). - Replace the MDK's
src/main/resources/mcmod.infowith this repo's. - Make a
libs/folder in the MDK root. Drop inBaubles-1.12-1.5.2.jarandTrinkets and Baubles-0.32.5.jar(compile-time only, won't be bundled). - Edit
build.gradle:- Change
version,group, andarchivesBaseNameto match this mod:version = '1.0' group = 'com.flashminat0.selectivekeepinv' archivesBaseName = 'selectivekeepinv'
- In the
dependencies { }block, add:compile fileTree(dir: 'libs', include: '*.jar') testCompile 'junit:junit:4.13.2'
- At the bottom of the file, add:
test { testLogging { events 'passed', 'failed', 'skipped' showStandardStreams = true exceptionFormat = 'full' } }
- Change
- From the MDK folder:
./gradlew test build (macOS / Linux) gradlew test build (Windows)
First run downloads Forge plus dependencies and takes a few minutes. The jar
lands in build/libs/selectivekeepinv-1.0.jar. Test HTML report is at
build/reports/tests/test/index.html.
Curse of Vanishing. Vanilla destroys vanishing-cursed items inside
EntityPlayer.onDeath() which can run before our hook in some code paths.
Vanishing-cursed items may still be destroyed even for protected players.
Other death-handling mods. The mod clears slots in LivingDeathEvent
(HIGHEST priority) before vanilla's dropAllItems() runs. This means
PlayerDropsEvent handlers (Baubles drop logic, Trinkets drop logic, Corail
Tombstone, Baubley Elytra's drop fixer) see partially-empty inventories and
only operate on what wasn't kept. For unlisted players nothing changes:
vanilla drops, those mods do their thing.
Concurrent deaths. Each death is snapshotted and rolled independently on the per-player NBT. No cross-contamination if two listed players die in the same server tick.
Cross-dimension death. Items are preserved correctly even if you die in the Nether (or Twilight Forest, End, etc.) and respawn in the overworld (items live on the player entity, not in the world). The death-message distance reading skips the distance number when the dimensions differ.
Automated unit tests cover the deterministic pieces:
PlayerListTest(18 tests) covers add/remove/getMode/SetResult semantics plus JSON persistence and graceful handling of bad files.PreservationPlanTest(33 tests) walks every threshold in the table and every divisor branch for XP carryover.ModeTest(6 tests) andDeathMessagesTest(11 tests) catch typos in enum names and the%splaceholders in the same-dim message pool.
Run ./gradlew test. HTML report at build/reports/tests/test/index.html.
The runtime event flow (snapshot, clear, restore, message dispatch) is not
unit tested. Verify it on a dev server with ./gradlew runServer and run
through MANUAL_TESTING.md before shipping changes.