Skip to content

feat(guild): cluster-mode guild creation - #81

Open
3kynox wants to merge 12 commits into
walkline:masterfrom
3kynox:feat/guild-cluster-creation
Open

feat(guild): cluster-mode guild creation#81
3kynox wants to merge 12 commits into
walkline:masterfrom
3kynox:feat/guild-cluster-creation

Conversation

@3kynox

@3kynox 3kynox commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

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

  • CreateGuild creates 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.
  • The cached membership is rechecked against the source before rejecting a creation, so a stale cache no longer blocks a legitimate one.
  • The leader is marked online when the freshly created guild enters the cache.
  • The default Officer rank gets the same rights as the core gives it.

guildserver — roster freshness

  • The cached roster is refreshed from the source on reads.
  • The online state of the members is recovered from the characters service at startup, so a restart no longer shows a guild full of offline players.

libsidecar — worldserver side of a petition

  • A worldserver endpoint that validates a petition (owner, signature count, charter item) with both the Go and the native C++ implementations.
  • A guild.created hook so the worldserver mirrors a guild created outside of it.

gateway — charter turn-in

  • CMSG_TURN_IN_PETITION is handled at the gateway: validation on the worldserver, creation through the guildserver, then the client results.
  • Arena team charters are forwarded untouched, and a worldserver without the validation endpoint keeps the legacy path, so a mixed cluster still works.

gateway — invites stay inside the faction

  • The last commit fills the // TODO: check fraction left in HandleGuildInvite: the world server refuses a cross faction invite in Guild::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 gets ERR_GUILD_NOT_ALLIED. A realm running with AllowTwoSide.Interaction.Guild enabled can restore the previous behaviour with the new ALLOW_CROSS_FACTION_GUILDS setting, 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.

3kynox and others added 11 commits July 26, 2026 16:54
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.
Copilot AI review requested due to automatic review settings July 26, 2026 15:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.
@3kynox

3kynox commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

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 game-server/libsidecar-cpp/include/ and game-server/libsidecar/ have 0 deleted lines: no symbol removed, no signature altered.

This PR adds two entry points:

  • TC9SetOnGuildCreatedHook (+ the OnGuildCreatedHook typedef and CallOnGuildCreatedHook)
  • TC9SetCanTurnInGuildPetitionHandler (+ typedef and Call…)

#82 stacks two more on top:

  • TC9SetGetPlayerItemByPosHandler
  • TC9SetSetPlayerGuildFieldsHandler

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 (events-guild.h, petition-api.h, player-guild-api.h) and the namespaced libsidecar/tc9_events.h + tc9_types.h. I declared each new hook in both, following what was already there, and #82 also touches src/compat_api.cpp.

Would you rather I declare new hooks only in the TC9* convention and let the compat layer bridge the historical one? That would roughly halve the header surface these PRs add, and I am happy to rework them that way — it would not change the two-PR split you asked for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants