Private business chat — a channel for a firm's employees (proprietor + current employees) to talk in-game, akin to Factions' /f chat. A player toggles into firm chat and their messages go only to fellow online firm members.
The server's chat plugin is CarbonChat (Hexaoxide/Carbon, docs carbonmod.gg). CarbonChat is channel-based with a first-class public API. Implement this as a single dynamically-scoped CarbonChat ChatChannel registered by business-rian — the same pattern Carbon's own Factions/Towny/Party integrations use. (Project: Business / business-rian — it owns the firm-employee model and adds the Carbon dependency.)
Carbon's own group-chat integrations (FactionChannel/ResidentListChannel, Towny TownChannel, PartyChatChannel) register one channel whose recipients(sender) resolves the sender's group at send time. Membership lives in the integrating plugin, not in Carbon's registry. We follow that exactly: one firmchat channel, firm membership stays in business-rian.
Do NOT register one channel per firm. Verified against Carbon source (trunk): the registry is a cheap HashMap<Key,ChatChannel>, BUT WrappedCarbonPlayer.channelForMessage(...) iterates every registered channel on every chat message (to check each channel's quickPrefix()). Tens of thousands of firm channels = tens of thousands of iterations per message per player. Plus, command-bearing channels register ~2 Brigadier commands each (commandName() + the /ch <name> proxy), bloating the command tree. A per-firm design is therefore a non-starter at firm scale.
Dependency — compileOnly("de.hexaoxi:carbonchat-api:<pin a 3.0.0-beta.x>") (Maven Central, GPL-3.0). Add Carbon as a Paper depend/softdepend in plugin.yml so we register after it loads. ⚠️ The 3.0.0-beta API line churns across betas — pin one version and verify signatures against that version's Javadoc.
On enable (after CarbonChatProvider.carbonChat() is available), build and register a single FirmChatChannel implements ChatChannel:
recipients(CarbonPlayer sender) → resolve the sender's active firm (see multi-firm note), return its online members as Audiences. Primitive exists: StaffApi.getOnlineEmployees(firmId) / FirmStaffService.getOnlineEmployees(firm) returns online staff + proprietor.permissions() → ChannelPermissions.uniformDynamic(p -> isInAFirm(p) ? allowed() : denied(reason)), so dynamic() is true (Carbon re-evaluates per-context instead of caching). Membership: StaffApi/FirmStaffService.isEmployedBy(firmId, uuid) or proprietor.radius() → -1 (org-wide); quickPrefix() → keep cheap; commandName() → e.g. firm/fc (single command, fine), or set shouldRegisterCommands() → false and drive everything through our own /firm chat command.carbonChat.channelRegistry().register(firmChannel).Toggling in (the real /f chat behaviour) — resolve CarbonPlayer via carbonChat.userManager() and call carbonPlayer.selectedChannel(firmChannel); joinChannel/leaveChannel handle opt-out/opt-in. Routing by selected channel is O(1).
Unlike Factions (one faction per player), a player can belong to multiple firms (FirmApi.getPlayerFirms(uuid) returns a list). Since there's one shared channel, business-rian holds a per-player active-firm pointer deciding which firm recipients(sender) routes to:
/firm chat <firm> (or a /firm setchannel <firm>) sets the active firm for multi-firm players.This keeps membership/identity in our plugin — exactly how Factions keeps faction state out of Carbon — without inflating Carbon's per-message channel loop.
If we want something before the full Carbon integration: a ChatCommands @Route("chat <firm> <message>") (greedy arg) resolving recipients via getOnlineEmployees filtered to the sender's membership, broadcasting with a business.chat.format key, registered in CommanderModule. One-shot only (no toggle), bypasses Carbon's formatting/channel UX — a stopgap, with the single Carbon channel as the real deliverable.
Code context — No chat feature today, but the recipient primitive exists:
FirmStaffService.getOnlineEmployees(firm)returns online staff + proprietor, andMessage.send(Collection<Player>, …)is already used elsewhere.Approach: add a
ChatCommands@Route("chat <firm> <message>")(greedy arg), resolve recipients viagetOnlineEmployeesfiltered to the sender's membership, broadcast with abusiness.chat.formatmessage key, register inCommanderModule. A toggle-channel mode (like/f chat) is a later enhancement on top of the one-shot form.