Repository navigation
Conversation
/guild create checks membership, the guild name, the blacklist, the name and prefix requirements and the balance, then registers a pending action and waits for /guild confirm. The confirmation only re-checked the balance and trusted the rest of the answers from the moment the command ran. A player who joined another guild before confirming was put in a second guild while the first one still listed them, and a name another guild had taken in the meantime was created a second time. The checks now live in one method that the command path and the confirmation path both call, so the confirmation re-runs the same validators and refuses with the same messages the command path uses. A refused confirmation drops the pending action, so the offer cannot be confirmed later by accident.
This was referenced Oct 22, 2026
darbyjack
pushed a commit
that referenced
this pull request
Oct 22, 2026
The coverage inventory is contended between the open pull requests, and the header and the per-section counts have to be the ones the merged file will have whatever order they land in: 160 tests across 14 spec files, 18 in the invite codes section, 21 in the guild lifecycle and membership sections and 8 in the admin commands section. The rows for the tests that #848 and #851 add are in the tables here, since neither of those pull requests touches this file, and the rows for #849's tests were already here. Every test in the merged suite is listed once. The membership section heading said 19 while the spec file holds 20 tests, which is where the 21 comes from.
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.
/guild createruns a set of checks, shows a confirmation prompt and registers a pending action. When the player answered the prompt, the confirmation trusted the answers the command had collected, so anything that changed in between went through.What the unfixed code did
The membership check is the
NoGuildcondition on the command, and it is evaluated when the command runs. A player who joined another guild before confirming was put into the new guild as well, while the guild they had joined kept them on its member list, and the member cache was pointed at the new one. The two guilds then disagreed about where the player was.The name was checked with
GuildInputValidator.isNameTakenat command time only. Another guild claiming the name in between produced a second guild with the same visible name, which is what every command that resolves a guild by name has to disambiguate.The blacklist, the name and prefix requirements and the balance were checked once, at command time. Only the balance was re-checked at confirmation.
Against the unfixed code the two new E2E cases failed with
Expected message matching "You're already in a guild!" not receivedand the same forThis name is already taken!. The plugin's own storage was left holdingE2E0009joined, a guild for a player who was already in one, and a second guild namedE2E000ctakennext to the one that already held the name.The fix
CommandCreatenow has a singlecheckCreationAllowedmethod, which the command path and the confirmation path both call. The confirmation re-runs the same validators and refuses with the same message keys the command path uses, so the player reads the refusal they would have got at command time:error.already-in-guildcreate.guild-name-takenerror.blacklistcreate.requirementscreate.prefix-too-long, orcreate.name-too-longwhen the name has to serve as the prefixerror.not-enough-moneyA refused confirmation drops the pending action. The offer is void once it has been refused, and leaving the action registered meant a later
/guild confirmcould create the guild and charge for it long after the player had been told it could not be created.The quoted price is still what gets charged. Reading
cost.creationagain at confirmation time would charge the player an amount they were never shown, so the price stays as quoted and affordability is what gets re-checked.How this composes with the economy change
PR #847 takes the creation cost before the guild is added to the handler, so a provider that refuses the withdrawal leaves the player with no guild and no membership. The two changes sit in the same method and neither undoes the other. In the merged result the confirmation runs in this order:
GuildCreateEventis firedTwo rules hold in that order. Nothing is charged before the checks pass, because the money moves in step 3 and the checks are step 1. Nothing is added to the guild before the money moved, because the guild is added in step 4.
A failed check throws before step 3, so the player is neither charged nor given a guild. A refused withdrawal returns before step 4, so there is no guild and no membership either. Both paths drop the pending action, so a refused confirmation leaves nothing to confirm a second time.
Each side pins its own rule with a test. The E2E case in this PR asserts that a refused confirmation leaves the balance untouched, which fails if the payment is ever moved in front of the checks.
EconomyTransactionTestin #847 asserts that a refused payment adds no guild and no membership, which fails if the guild is ever added before the money moves. The two branches merge cleanly. The merged tree compiles, passes 78 unit tests, and passes the same 146 E2E tests.Tests
Two cases in
lifecycle.spec.ts:refuses the confirmation when the player joined a guild in the meantimeopens the prompt, has the player join another guild, then confirms. It expectserror.already-in-guild, no guild with the proposed name, no guild the player masters, the other guild holding exactly its two members, and an unchanged balance. A second/guild confirmis expected to answerconfirm.error, which covers the dropped action.refuses the confirmation when another guild took the name in the meantimeopens the prompt, has another player claim the name, then confirms. It expectscreate.guild-name-taken, exactly one guild with that name and the other player as its master, the player in no guild at all, and an unchanged balance.Results
./gradlew test testJava11 spotlessCheck: 68 unit tests pass on Java 21 and the same 68 pass on Java 11, spotless clean../gradlew check: passes, including the 1.8.8 legacy Material probe../gradlew plugwrightTeston Paper 1.21.8 withTEST_TIMEOUT=60000: 146 passed, 0 failed, 0 skipped, 7m 26s.EconomyTransactionTest, and passes the 146 E2E tests.Compatibility
No dependency, API or configuration change. The refusals reuse the
Messageskeys the command path already used, so the player-visible strings are the ones the E2E suite already asserts on.GuildInputValidatorwas not touched. The build still targets Java 11 bytecode, andcheckruns the 1.8.8 legacy Material probe, which passes.What a failed save can lose
A new guild is not written to disk when it is created.
guildHandler.addGuildputs it in the in-memory map, and the file appears with the next save: the autosave task, which runs everystorage.save-intervalminutes, one minute by default, or the save on shutdown. A server that dies in that window comes back without the guild, and the guild's bank and vault items go with it. The money has already left the player's balance by then, so a player can end up having paid for a guild that no longer exists. Whether the economy plugin has persisted that balance by the time the server comes back is the economy plugin's own business.A save that fails does no damage on its own.
JsonFileUtils.writeAtomicallywrites a temporary file and moves it into place, so a write that fails leaves the previous file untouched, andsaveGuildslogs the guild that failed and carries on with the others. The guild stays in the map, so a later successful save writes it. If the failure persists, the guild is never written and is lost at the next restart, and the player is never told: the creation was reported as successful when it happened.Nothing in the create path can overwrite an existing record. The new guild gets a fresh UUID, and
createGuildreturns without writing when a file with that id is already there.The same shape applies to the leave and kick actions. A member who leaves is taken out of the guild's member list in memory, and the file that still lists them is rewritten at the next save, so a crash in that window brings the player back into the guild after the restart:
loadGuildsrebuilds the member cache from that file.A master who leaves, and a guild that is deleted, are handled differently. The guild's file is removed at once, so the disband lands immediately. The JSON provider ignores the result of
file.delete(), though, so a read-only folder or a locked file leaves the file on disk without a word in the log. The guild is no longer in the map, so the autosave never touches it, and the guild comes back on the next restart with its master, its bank and its vault items.That is the storage layer's business rather than the confirmation's, and #846, #850 and #845 own that code, so this PR reports it and leaves it alone.
Limitations
/guild createagain, because the refused confirmation drops the action./guild upgradekeeps its action on a money refusal, and that is unchanged.