Mawu · Features

Pheme Voice & Hera Social Graph

A focused page within the Mawu Features documentation. The full map and every sibling page live in the Features hub.

5sections12 minread1diagram

On this page

Two things turn a server full of strangers into a place. The first is being able to hear the person standing next to you — to whisper a deal across a market stall, to key a radio and have only the dispatcher's net carry it, to take a phone call from across the map. The second is belonging — a crew with a name, a treasury, officers and recruits, a reputation that a whitelisted realm reads at its door, and an identity that survives when its members cross into another world entirely. V7 ("Mawu") names those two planes for two Greek goddesses: Pheme, the carrying, spreading voice, and Hera, goddess of alliance and organized society. This page is the feature tour of both.

Pheme is the positional, proximity-attenuated voice that immersive roleplay requires — the pma-voice model FiveM made the genre standard — layered with radio nets, phone calls, and the accessibility and child-safety guarantees the platform never delegates to a realm. Hera is the persistent player social graph: crews, guilds, and organizations as first-class, cross-realm, cross-version aggregates with role-gated treasuries, an audit ledger, a reputation whitelist gate, and an Ori-passport bridge that carries a group into V2–V6. Both are grounded in real, test-driven code, and this page follows the code where the prose runs ahead of it.

The authority that validates every Hera treasury debit and that hosts the radio nets lives in ./moremi-roleplay-framework.md; the way a crew travels into other Oshun games — and the streamer-mode redaction that protects a broadcasting player — lives in ./streaming-incarnation-and-live-service.md; the engineering companion that owns the data-model and voice-DSP detail is ../architecture/nana-data-model-and-pheme-voice.md. For the full V7 scope this slots into, start at the hub: ../V7_features.md.

What ships, honestly#

These two planes are real, test-covered code, but they ship at three different levels of assembly, and one of them — Pheme — is more implemented than the architecture companion's quick summary lets on. The honest picture:

  • Real and test-covered. Pheme has two genuine evaluators. The client-side mix is UMawuVoiceMixLibrary::EvaluateVoiceEmitter (V7/ue/Source/MawuVoice/Private/MawuVoiceMix.cpp) — real UE C++ distance falloff, radio floor, and equal-power panning. The server-side audibility, admission, and captioning model is real Rust in apps/v7/mawu-gateway (src/lib.rs): proximity gain with ray-vs-AABB occlusion, scriptable whisper/normal/shout ranges, radio channel admission (PTT / squelch / role / on-duty), proximity-independent phone, a hearing-impaired caption projection, and a platform voice-safety tap that ignores a realm's request to disable it — four of the gateway's 16 #[test] cases pin these. Hera is a real 1,085-line social graph (apps/v7/hera-social-service/src/lib.rs) with role-gated audited treasury withdrawals, a cross-realm reputation whitelist gate, an Ori group-presence passport with a continuity hash, and a platform-ban + device-attestation ban-evasion eval (7 tests).
  • Library-grade, not a running daemon. Both mawu-gateway (owner "Mawu", port 47204) and hera-social-service (owner "Hera", port 47206) expose their behaviour as libraries plus a health-only daemon — each main() calls run_service(), which binds the shared service_contract health server and answers one route. The audibility/admission model and the social graph are exercised by evals and tests, not by a live socket mixing Opus packets or serving group mutations.
  • Spec-level where it is genuinely reused or deferred. The codec and SFU themselves — Opus over the WebRTC SFU — are the reused V6 Egbe-Gateway transport, not re-implemented in V7; the gateway's own comment says "the SFU transport itself (V6) is out of scope; this is the audibility/admission/ captioning model the SFU is driven from." The Pheme scale budget ("≤40 audible streams per listener" nearest-N AoI culling, "≤2 cores per 256 speakers", "p95 ≤250 ms mouth-to-ear") is a written launch target — the audibility evaluator runs per speaker-listener pair, with no nearest-N selection yet. And Hera group kinds are Crew | Guild | Organization (the monolith's "gang"/"posse"/"org" are flavour, not enum variants).

Pheme — proximity and radio voice#

Two evaluators, one falloff model#

Pheme computes "what can this listener hear from that speaker" in two honest places. On the client, the Mawu UE module ships UMawuVoiceMixLibrary::EvaluateVoiceEmitter, a UBlueprintFunctionLibrary (deps Core / CoreUObject / Engine only, per MawuVoice.Build.cs) that takes an FMawuVoiceListener (location, forward, HearingRadiusCm default 3200, the RadioChannels it monitors) and an FMawuVoiceEmitter (location, VoiceRadiusCm default 2400, SpokenLevelDb default −12, bRadioTransmitting, RadioChannel) and returns an FMawuVoiceMixResult. The math is real: the effective radius is min(HearingRadiusCm, VoiceRadiusCm), the distance alpha is clamp(distance / radius, 0, 1), the falloff is (1 − alpha)² (a smooth inverse-square-shaped curve), and the speech level converts from decibels with 10^(clamp(SpokenLevelDb, −80, 12) / 20). Panning is equal-power-shaped — the listener's right vector is cross(Up, Forward), the pan is dot(directionToEmitter, right) clamped to [−1, 1] — and bAudible trips only above a 0.001 linear-gain floor.

On the server, pheme_evaluate_audibility (mawu-gateway/src/lib.rs) does the same falloff but adds two things the client evaluator does not: scriptable voice modes and occlusion. PhemeVoiceMode gives whisper / normal / shout distinct effective ranges of 300 / 1,800 / 5,000 cm; the distance gain is the same (1 − alpha)² quadratic; and a PhemeOccluder (an axis-aligned box with an attenuation_db) is tested by pheme_segment_hits_occluder, a real ray-vs-AABB slab intersection along the speaker→listener segment. Each occluder the segment crosses adds its dB, converted back to a linear factor as 10^(−occlusion_db/20), and the received level is source_level_db + 20·log10(gain), floored at PHEME_INAUDIBLE_FLOOR_DB = −120. The eval run_pheme_proximity_eval asserts the properties that matter: gain is monotonically decreasing with distance, a voice is silent beyond range, an occluded path is quieter than an unoccluded one at the same distance, and whisper < normal < shout range ordering holds.

Radio nets and phone calls#

Radio is a parallel, distance-independent path. On the client, when the emitter transmits on a non-empty channel the listener monitors, EvaluateVoiceEmitter floors the gain at 0.18 regardless of distance and pulls the pan toward centre (Pan *= 0.25) — the familiar "in your head" radio feel. On the server, pheme_evaluate_radio_transmit decides admission onto a channel. PhemeRadioChannel carries an optional required_role, a require_on_duty flag (for dispatch nets), and a squelch_threshold; the result is a PhemeRadioAdmission that is one of Admitted, PttNotKeyed, NotChannelMember, NotRoleAuthorized, NotOnDuty, or SquelchedWeakSignal. The run_pheme_radio_phone_eval fixture is a police dispatch net: an on-duty dispatcher is admitted, an off-duty dispatcher is NotOnDuty, a civilian is NotRoleAuthorized, a released push-to-talk is PttNotKeyed, and a −110 signal under a −90 squelch is SquelchedWeakSignal. Phone is pheme_connect_phone_callproximity_independent: true, point-to-point or is_conference for more than two — and the eval connects two endpoints 50,000 cm apart (far beyond shout range) to prove distance never gates a call.

That dispatch radio net is also where Pheme meets the Ixchel audio-mod model in the realm server. A police radio is declared as a MoremiForgeAudioRoute("radio.police", "dispatch-radio", "voice") (apps/v7/moremi-realm-server/src/lib.rs), and the forge-conflict resolver keeps it on an independent mix bus from a music.club route ("club-music", music) — while flagging a genuine dispatch-radio<->radio-hijack contradiction when a hostile pack tries to seize the police channel onto the wrong bus. Radio channels are not just a voice routing concept; they are a named audio resource the composition layer protects.

Voice on the wire, and the safety tap the realm cannot turn off#

Voice negotiation is a first-class citizen of the realm protocol. RealmWireMessageKind::VoiceSignal (libs/v7/realm-protocol/src/lib.rs) routes onto the dedicated RealmWireChannel::Voice, is reliable-ordered (SDP offers and answers cannot be dropped the way an AuthoritativeDelta can), carries the wire label "voice-signal", and holds a distinct authority scope, "realm.voice.signal", separate from the state-mutation scope. It is one of the six required message kinds the wire-conformance harness drives through reordering and loss, so the signalling path is held to the same framing contract as movement. The substrate boundary names this seam explicitly: substrate-bridge tags a "V6 realtime gateway and Pheme transport boundary" — which is why no codec lives in the V7 crates.

Wrapped around the mix is a platform-owned safety tap the realm operator cannot remove. MawuVoiceSafetyPolicy::platform_default() sets platform_tap_required: true, realm_delegation_allowed: false, and a route threshold of 7,000 basis points into the kuanyin-human-review queue. screen_voice_segment scores a segment on direct-audio-grooming, coercive-tone, distress, and minor-context signals, and — critically — when a realm sets realm_requested_tap_disabled, the decision records realm_disable_request_ignored: true and screens anyway. This is the trust-boundary posture in miniature, proven by platform_voice_safety_tap_routes_flagged_segments_and_ignores_realm_disable. Child-safety reaches further: Sekhmet makes ProximityVoice a MinorProtectionFeature (apps/v7/sekhmet-scanner/src/lib.rs) and screens a VoiceGroomingSignal endangerment kind on a tight 2,400 ms SLA.

Accessibility parity#

No voice surface ships without a text/visual equivalent. On the server, pheme_build_accessibility_view projects live PhemeVoiceEvents into a PhemeAccessibilityView: for a hearing-impaired profile with captions on, every event becomes a caption line carrying who (speaker), which channel (proximity / radio / phone + id), and where (distance + direction for proximity, "off-proximity" for radio/phone), plus distinct speaker and channel indicators. A parental_mute profile suppresses all surfaced voice and emits no captions at all. The contract layer enforces the same bar statically: lintVoiceRadioParity (libs/v7/contracts/src/accessibility-eval.ts) requires captions, transcript, and speaker / range / channel / mute-state visual indicators for both proximity and radio voice, emitting voice_radio_parity_missing:<field> against the voice-radio-text-visual-parity rule when a surface omits one. The mix evaluator decides what you hear; the accessibility view and the parity linter decide what the platform must guarantee around it.

Hera — crews, guilds, and the persistent social graph#

The group aggregate#

A Hera group is a real aggregate, not a join table. HeraGroup carries a kind (Crew | Guild | Organization), a home_realm_id, a map of HeraGroupRoles (each a permission set drawn from TreasuryDeposit, TreasuryWithdraw, SharedAssetManage, ReputationRead), members with their assigned role_ids, a HeraTreasuryAccount (an aje_account_id plus a balance_cents), shared assets, and a cross_realm_reputation_score. Everything a group can do is mediated by actor_has_permission, which walks a member's roles and checks the requested permission — there is no "owner can do anything" shortcut.

flowchart TD G["HeraGroup<br/>kind: Crew | Guild | Organization<br/>home_realm_id · cross_realm_reputation_score"] G --> R["Roles → permissions<br/>TreasuryWithdraw · SharedAssetManage<br/>TreasuryDeposit · ReputationRead"] R --> M["Members → role_ids"] M --> PERM{"actor_has_permission?"} PERM -->|TreasuryWithdraw| T["withdraw_from_treasury<br/>debit Aje treasury OR deny<br/>→ append HeraGroupAuditEntry"] PERM -->|SharedAssetManage| A["publish_shared_asset"] G --> WG["evaluate_cross_realm_whitelist_gate<br/>score ≥ required_minimum_score"] G --> P["mint_ori_group_presence_passport<br/>FNV-1a continuity_hash"] P --> V2["bridge_to_v2_match → HeraV2MatchGroupRoster<br/>recompute hash → tamper-detect"] BAN["PlatformBanLedger<br/>platform_account_id + device_attestation_id"] --> J{"evaluate_realm_join"} J -->|banned acct OR same device| BLK["Blocked…<br/>reputation → negative"]

Role-gated, audited treasury#

The treasury is the load-bearing example. withdraw_from_treasury checks actor_has_permission(TreasuryWithdraw) and then either denies with missing_treasury_withdraw_permission / insufficient_treasury_funds or debits the balance — and every attempt, permitted or not, appends a sequenced HeraGroupAuditEntry recording the actor, action, decision, reason, amount, and the treasury balance after. publish_shared_asset is gated the same way on SharedAssetManage. The gate run_hera_groups_eval only passes when a permissionless member's 2,500-cent withdrawal is denied (the 25,000-cent balance untouched), a treasurer's 7,500-cent withdrawal succeeds (balance → 17,500), a quartermaster's shared-asset publish succeeds, and the audit ledger shows at least two permitted and one denied entry — proven by groups_eval_grants_role_gated_treasury_and_cross_realm_reputation. The treasury balance is in aje_account_id terms, so a group's shared money is settled through Aje, never minted inside the social service.

Cross-realm reputation and the whitelist gate#

Reputation is the thing a group earns in one realm that another realm reads at its door. evaluate_cross_realm_whitelist_gate compares a group's cross_realm_reputation_score against a target realm's required_minimum_score and returns a HeraGroupWhitelistDecision — in the eval, a score of 82 clears a required 50, so an established crew is admitted to a whitelisted RP realm without re-vetting every member. A group is also a governance tier: the Eunomia service (apps/v7/eunomia-governance-service/src/service.ts) makes 'guild' a first-class EunomiaGovernanceTier whose parent is 'server', with reputation-weighted voting, so a guild's charter and treasury decisions run through the same governance plane as the realm above it.

Group presence travels — the Ori passport#

The "a crew travels into V2–V6" promise is implemented, not asserted. mint_ori_group_presence_passport snapshots each member's role_ids into a HeraOriGroupPresencePassport and stamps a continuity hash — a deterministic FNV-1a fold over the group id and the per-account role assignments (stable_group_presence_hash, formatted fnv64:…). bridge_group_presence_passport_to_v2_match then projects that passport into a HeraV2MatchGroupRoster, recomputing the same hash so any tamper is detectable. run_hera_cross_version_group_presence_eval passes only when the V2 roster preserves the member count, every role assignment, and a matching continuity hash — group identity that survives the version boundary, the Hera analogue of the Ori passport the substrate bridge mints for individual characters. A "Moonlit Couriers" crew formed in a market realm can therefore field a verified two-runner roster in a V2 ranked match with its roles intact.

Ban propagation and device-attestation evasion#

Hera closes the smurf / ban-evasion loop FiveM left open, and it is the platform — never the realm — that owns it. PlatformBanLedger records a PlatformBan keyed on both a platform_account_id and a device_attestation_id, and evaluate_realm_join returns a RealmJoinDecision that blocks not only the banned account (BlockedPlatformAccountBan) but a fresh account joining from the same attested device (BlockedDeviceAttestation). A ban also drives the cross_realm_reputation_score negative. run_ban_evasion_eval passes only when both the original account and a fresh same-device account are blocked, propagation lands within V7_BAN_PROPAGATION_SLA_MS (5,000 ms — the fixture lands in 2,000), and the reputation score is negative — pinned by device_attestation_blocks_fresh_account_realm_hop and ban_evasion_eval_blocks_platform_ban_and_device_hop. This is the trust boundary's "platform owns identity and bans, realms cannot launder them" posture made concrete in the social graph: a banned player cannot simply re-register and realm-hop their way back in.

Where this connects#

Pheme and Hera are the payload the realm backbone carries: a voice signal is a reliable-ordered frame on the realm wire, and every treasury debit is a server-authoritative mutation. The authority that validates those writes, hosts the radio nets, and owns the Nàná characters those voices belong to is ./moremi-roleplay-framework.md. The way a Hera crew incarnates into V2–V6 via the Ori passport — and the streamer-mode redaction that protects a broadcasting voice — lives in ./streaming-incarnation-and-live-service.md. The data-model and voice-DSP engineering behind everything on this page, including the character record and economy ledger these social and voice planes sit beside, is in the architecture companion ../architecture/nana-data-model-and-pheme-voice.md. For the full V7 feature scope, return to the hub: ../V7_features.md.