feat(guild): cluster-mode guild creation - #81
Conversation
Adds a CreateGuild RPC: the guild row, the five default 3.3.5 ranks and the
leader as guild master are inserted in one transaction, the guild id being
allocated as MAX(guildid)+1 with a retry on primary key conflicts (a
worldserver can still create guilds in-process concurrently). The created
guild is cached hydrated and a guild.created event is published.
Also implements the GuildByRealmAndID mysql repo method that was left as
panic("implement me") - the cache needs it to hydrate the new guild.
(cherry picked from commit 3608d5a3f14348c31ca54faafb07f27948919bfc)
…ild creation The world can disband guilds without going through the guild service (e.g. GM .guild delete handled in-process), leaving a stale positive membership in the in-memory cache. CreateGuild now rechecks a positive answer against the database and evicts the stale cache entry, instead of rejecting with 'already in guild' until the service restarts. Also treats a missing guild_member row as 'not in a guild' in the MySQL repo instead of surfacing sql.ErrNoRows. (cherry picked from commit 2c779169db1722cb20bfa58faf8674a482af09f1)
Guild creation is always driven by a live session of the leader, but in cluster mode the world doesn't flush the online flag to the characters table, so the hydration read the leader back as offline. This left the fresh guild with an empty online-members list: the roster showed the leader disconnected and guild events (e.g. MOTD updates) were broadcast to nobody until the leader relogged. (cherry picked from commit 7000ccaa6c9f19ce89c1f3884f355243a74628d9)
…ity) The core's CreateDefaultGuildRanks gives both Guild Master and Officer GR_RIGHT_ALL; the service only gave the Officer chat rights, so a freshly promoted officer had every guild action greyed out client-side (invite, promote, etc.). Match the core layout and assert it in the create test.
- CreateGuildParams gains signatoryGUIDs; members are inserted with the lowest rank in the same transaction, skipping characters that joined another guild since signing. - guild.created event now carries the added member guids so worldservers can populate the state for the new guild. - New worldserver RPC CanTurnInGuildPetition for the gateway to validate petitions on the worldserver side before calling CreateGuild.
The world can mutate guild_member without going through the service (petition turn-in, in-process invites, GM commands), leaving the cache blind to those members until restart. Guild reads now re-hydrate the guild from the repo, throttled to one refresh per guild every 3s. Online statuses are overlaid from a new gateway-events-driven online set that also tracks characters that aren't guild members yet, since characters.online isn't maintained for cluster sessions. (cherry picked from commit 4d9df78)
…rvice on startup Login events observed before the process started are gone, so members hydrated from the DB stay offline until they relog. On startup the online state is recovered from the characters service (GetOnlineCharacters) and overlaid on the cached rosters, including guilds created in-process. Best effort: disabled when the address is empty, degrades to stale statuses on failure.
…eated hook Both implementations (Go and native C++) gain: - CanTurnInGuildPetition gRPC method dispatched on the world update thread through a new TC9SetCanTurnInGuildPetitionHandler binding; - a guild.created NATS consumer that calls the new TC9SetOnGuildCreatedHook so worldservers can populate state for guilds created by the guild service.
The gateway intercepts CMSG_TURN_IN_PETITION, asks the player's worldserver to validate the petition over gRPC (charter item, ownership, signatures), then creates the guild through the guild service with the signatories as members. Arena charters are forwarded to the worldserver untouched. The guild service maps business failures of CreateGuild to gRPC status codes so the gateway can render the proper client errors (name taken, already in guild, invalid name) without matching error strings. Sessions of online signatories are linked to the new guild through the guild.created event.
If the worldserver doesn't implement CanTurnInGuildPetition yet (mixed versions in the cluster), the turn-in was silently dropped: the gRPC call failed and the client never got an answer nor the packet. Fall back to the in-process turn-in flow on Unimplemented.
The fixture carried the name of a live realm's guild, which says nothing about what the test checks.
The world server refuses to invite a player of the other faction in Guild::HandleInviteMember, but in cluster mode the opcode never reaches it: the gateway hands the invite straight to the guild service, so a guild ends up with members of both factions. Compare the two races before calling the guild service and answer ERR_GUILD_NOT_ALLIED instead. A realm that runs with AllowTwoSide.Interaction.Guild enabled can restore the old behaviour with the new ALLOW_CROSS_FACTION_GUILDS setting; the gateway cannot read the world server config, so the two have to be kept in sync. An unknown race leaves the invite alone rather than refusing it.
|
Following up on your Discord note this morning about the changes that break sidecar ABI compatibility — I audited what these two PRs actually touch on that surface, in case it helps you sequence the AC-side work. The ABI surface is extended, never broken. Across both PRs, the headers under This PR adds two entry points:
#82 stacks two more on top:
The practical consequence is that an older worldserver linking the newer libsidecar still resolves every symbol it uses. Only the reverse direction constrains — a worldserver that calls the new hooks needs the new lib. So the dependency is one-directional, which should let the order you described (merge the TC9 side first, then bring the AC-side changes into your fork) work without a flag day. One thing I am genuinely unsure about, and where I would rather follow your preference. The headers currently carry two parallel conventions: the historical ones ( Would you rather I declare new hooks only in the |
Following your suggestion, I regrouped the guild work into two pull requests instead of the previous chain. This is the first one; the guild bank and the remaining control fixes are stacked on top of it in a second PR.
This supersedes #55, #56, #57, #58 and #80 — same content, rebased on current master, with all the review points already applied. I will close them once this one is up.
guildserver — guild creation
CreateGuildcreates the guild, its default ranks and its members in one transaction, and accepts the petition signatories so a charter turn-in lands everybody in the guild at once.guildserver — roster freshness
libsidecar — worldserver side of a petition
guild.createdhook so the worldserver mirrors a guild created outside of it.gateway — charter turn-in
CMSG_TURN_IN_PETITIONis handled at the gateway: validation on the worldserver, creation through the guildserver, then the client results.gateway — invites stay inside the faction
// TODO: check fractionleft inHandleGuildInvite: the world server refuses a cross faction invite inGuild::HandleInviteMember, but in cluster mode the opcode never reaches it, so guilds ended up with members of both factions. The two races are compared before calling the guild service, and the client getsERR_GUILD_NOT_ALLIED. A realm running withAllowTwoSide.Interaction.Guildenabled can restore the previous behaviour with the newALLOW_CROSS_FACTION_GUILDSsetting, since the gateway cannot read the world server config. An unknown race leaves the invite alone rather than refusing it.Every commit builds on its own. Unit tests cover the creation service, the cache, and the turn-in handler; the whole thing also runs on my four-shard test cluster.