Repository navigation
Conversation
A stored vault that could not be read was left out of the cache, which moved every vault after it one place up. The player who asked for vault 2 was served the contents of vault 3. The save then compared the length of the cache with the length of the stored list, and one unreadable entry meant the two never matched, so nothing was written for that guild again and everything a player put into any of its vaults was lost on the next restart. The same check also kept a vault a player opened for the first time from ever being stored. The cache now has an entry for every stored vault. An unreadable one gets an empty vault in its place, so vault N is vault N. The save writes the cache back position by position: a vault that could not be read keeps the data it has until something is put into it, a stored vault the cache does not have is kept, and a vault the player opened for the first time is added. Reading a vault is per item now as well. A slot key that is not a number, a slot outside the inventory, and a value that is not an item were all thrown out of the deserializer, where they took the guild load with them. They are skipped, and the rest of the vault is read. The stored format does not change. A vault is still a size and an items section keyed by the slot, the shape every guild file on disk already has.
The vault warnings said "of a guild" and left the admin to work out which one. They name the guild now, and the cache and the save take the guild itself rather than its stored list.
This was referenced Oct 22, 2026
Reading a stored vault is per item, so an item the running server cannot make sense of costs that item and not the vault. The inventory that came back was missing it all the same, and that inventory was what the save wrote: the item was gone from the guild file for good, and the player was left with a vault that had a hole in it. The read now reports the slots it could not reconstruct, and the save keeps the stored data of a vault that was not read in full. Such a vault is not opened either, because an inventory without the items that were left out looks complete to the player who is handed it, and the stored data is the only copy of those items. The player is told why the vault stays closed. The file a guild is stored in is copied aside when one of its vaults could not be read, so that the data that could not be read is in more than one place. The file itself is left where it was.
out of range failure of getGuildVault The plugin rewrites the guild file on every save, so its last modified time changes and the copy of the guild file landed in a new folder on every restart. The damage had not changed, so the operator collected a copy of the same corruption every time the server came back up. The identity of the damage is the stored data of the vaults that could not be read: that is what is being preserved, so that is what the copies are compared on. A copy that already holds the same data is the same corruption and the file is not copied again. A vault that holds a different unreadable item is a different piece of damage and gets its own copy. getGuildVault returned nothing for a vault number outside the guild's vaults, where it used to throw. A caller that relied on the failure now got a null it may dereference, so the old failure is back and only a vault that could not be read in full returns nothing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A guild vault is stored in the guild file as a JSON document. A vault that cannot be read used to disappear from the in-memory cache, and that had two consequences.
The cache is positional and
getGuildVaultreads it by position. With one unreadable vault in a guild that had three, the cache held two entries, so the player who opened vault 2 was served the contents of vault 3, and vault 3 was out of range.The save compared the length of the cache with the length of the stored list and gave up when they differed. One unreadable vault meant they always differed, so nothing was written for that guild again, on any autosave and on shutdown. Everything a player put into any of that guild's vaults was lost on the next restart. The same check meant a vault a player opened for the first time was never stored either.
Reading a vault had the same problem one level down.
deserializeInventoryparsed the slot key withInteger.parseIntand set the item without a bounds check, and it cast the stored value to anItemStackwithout checking what it was. A slot key that was not a number threw aNumberFormatException, a slot outside the inventory threw anArrayIndexOutOfBoundsException, and a value that was not an item threw aClassCastException. OnlyInvalidConfigurationExceptionwas caught, so all three escaped the deserializer and out of the guild load.The hole in reading a vault one item at a time
Reading a vault one item at a time has a second half to it. An item the running server cannot make sense of, a material that only exists on 1.8.8 for instance, costs that item and not the vault, and the inventory that comes back is missing it. That inventory was what the save wrote, so the item was gone from the guild file for good. The player was left with a vault that had a hole in it, and the next save wrote that hole over the data that was still in the file.
The read now reports the slots it could not reconstruct, and a vault that was not read in full keeps its stored data. Such a vault is not opened: an inventory without the items that were left out looks complete to the player who is handed it, and the stored data is the only copy of those items. The player is told that the vault stays closed and why.
The file a guild is stored in is copied aside when one of its vaults could not be read, so the data that could not be read is in more than one place. The file itself stays where it was. A piece of damage is copied once: the identity of the damage is the stored data of the vaults that could not be read, so a guild file that a save rewrote because something else in the guild changed is not copied again, while a vault that holds a different unreadable item is a different piece of damage and gets its own copy.
What changed
Serialization.readInventoryreports what the document held: the inventory that could be read and the slots that were left out.deserializeInventorykeeps its old contract and returns nothing unless the whole document could be read, so no caller can be handed an inventory with a hole in it.getGuildVaultreturns nothing for it, and the new overload that takes the player tells the player why the vault stays closed.plugins/Guilds/backups/corrupt/<timestamp>/guilds/when one of its vaults could not be read, with the console saying where the copy is. The file is copied byte for byte and is left where it was.The stored format does not change. A vault is a
sizeand anitemssection keyed by the slot, the shape every guild file on disk already has, andserializeInventoryis untouched. A test pins that shape.The new message
vaults.unreadableis a new key, and it is inen-US.ymlonly. The other locale files are Crowdin-managed, so they pick the key up on the next Crowdin sync. Until then ACF resolves the key against the manager's default locale, which the plugin takes fromsettings.messagesLanguageand which defaults toen-US, so the message is never shown as a raw key.Tests added
src/test/java/me/glaremasters/guilds/utils/SerializationTest.java: the round trip, the stored shape, a slot key that is not a number, a slot outside the inventory, a value that is not an item, an item with an unknown material, an item with meta that cannot be read, and two documents that are not a document. The four cases that lose an item also check that the read reports the slot it could not reconstruct and thatdeserializeInventoryhands out nothing.src/test/java/me/glaremasters/guilds/guild/GuildVaultTest.java: a corrupt vault in the middle keeps the other vaults at their positions, the vault a player asks for is the vault they get, the data of an unreadable vault survives a save, the other vaults of a guild are saved anyway, a vault opened for the first time is stored, a stored vault the cache does not have is not dropped, an item that cannot be read stays in the stored data, the stored data survives a save, a reload and a second save, a vault with an unreadable item is not opened, the player is told why, the vault GUI refuses it, the other vaults of the same guild still open and still save, the guild file is copied byte for byte, the same damage is not copied again when the file is rewritten, a different piece of damage is copied again, a vault number outside the guild's vaults still fails the way it always did, and a guild whose vaults are all healthy is not affected at all.The tests were run against the code before the fix first, and the failures are the ones described above: the cache held 2 entries where 3 were stored, vault 2 served vault 3's items, the item a player put into vault 1 was gone after the save, a vault opened for the first time was never stored, the item that could not be read was gone from the stored vault after the next save, and a guild file that was rewritten while the damage stayed the same was copied a second time.
Test results
The unit suite covers the 19 vault tests and the 9 serialization tests, and the whole existing suite still passes on Java 11 and on the current JVM.
Compatibility
net.minidev:json-smartis the parser the bundled configuration library already used, and it is now declared in the version catalog instead of coming in transitively, so the vault code can read a document with it directly. It is bundled in the plugin jar, so it does not depend on the server version.checkpasses.getGuildVault(guild, vault)returns nothing for a vault that could not be read in full. A vault number that is not one of the guild's vaults still throws, exactly as it did before, and a guild that has no vault cache still fails the way it did.GuildsAPI.getGuildVaulthands back what the handler hands back, so it hands back nothing for a vault that could not be read in full. Its Kotlin signature still saysInventory, so a Kotlin caller of the API has to be ready for that.The path of the guild file
GuildHandler.guildDataFilerebuilds the path of the file a guild is stored in,<plugin folder>/data/<guild id>.json, because the backup needs the file and the storage provider does not hand it back. The duplication stands until the provider grows a method that returns the file for a guild, and the handler should use that method instead of rebuilding the path when it does.Limitations