Faction Chat Channel
Three private channels whose audiences are computed from the diplomacy graph at send time rather than from a subscriber list.
A player in a Faction can direct their chat to one of three audiences: FACTION, VASSALS, or ALLIES. The selection is a nullable field on the same persisted player record that carries Player Power — null means ordinary server chat — so the channel a player was last talking in is stored rather than held for the session.
The audience is a query, not a list
No channel has members. Each is a rule evaluated against the diplomacy graph every time someone speaks, which means the audience tracks Faction Relationship changes with no subscription to maintain.
FACTION— the faction's own members.VASSALS— not the speaker's direct vassals. The service walks the liege chain to its top and addresses that faction's members plus every faction in its vassal tree. Realm-wide chat, reached from anywhere in the hierarchy; see Vassalage.ALLIES— own members, plus the members of allies whose alliance is reciprocated. An ally faction is included only if the reverse relationship is alsoALLY.
That last rule is worth noticing. Faction Relationship stores directed edges, and the service checks the pairing for liege and vassal edges; here the same suspicion is applied to alliance, at the call site, so a one-sided ALLY row grants no access to allied chat.
Written down, not read back
After delivery the message is persisted asynchronously as an MfChatChannelMessage — timestamp, sender, faction, channel, and text — keeping the database write off the main thread, per Main Thread Safety. The service offers paged reads and a count over a faction's history.
At this commit nothing outside the chat package calls those read methods. The log is written and retained; no comment, commit message or document in the repository records what it is for.
Formatting
Each channel has a configurable format string under chat in config.yml, with placeholders for faction colour, name, role and message. One detail is worth flagging for anyone editing this code: the hard-coded fallback used when a format key is missing selects its template from the sending player's currently selected channel rather than from the channel argument the method was called with. The two normally agree. Nothing in the repository explains the difference.