Skip to content

feat(godot_playfab): Party chat indicators, voice controls, TTS, and network diagnostics - #154

Merged
James Lenell (jameslen-atg) merged 3 commits into
mainfrom
feat/party-feature-additions
Aug 14, 2026
Merged

feat(godot_playfab): Party chat indicators, voice controls, TTS, and network diagnostics#154
James Lenell (jameslen-atg) merged 3 commits into
mainfrom
feat/party-feature-additions

Conversation

@jameslen-atg

Copy link
Copy Markdown
Member

Lands Party spec Phase E: chat indicators, local voice controls, working transcription/translation, text-to-speech, and network diagnostics for godot_playfab.

This work was written but never committed — it had been sitting as uncommitted edits in an unrelated worktree. This PR moves it onto its own branch off main with no functional changes.

Chat indicators

Indicators are polled, never signalled — Party raises no state change when an indicator flips, so titles poll them. All getters return safe defaults instead of erroring when there is no local chat control, so they are safe to call every frame.

# Am I talking? (drives a local mic glow)
var speaking := PlayFab.party.chat.get_local_chat_indicator() == PlayFabParty.LOCAL_CHAT_INDICATOR_TALKING

# Drive a whole voice roster from one poll.
for entry in PlayFab.party.chat.get_chat_indicators():
    var row := _roster_row(entry["entity_key"]["id"])
    row.talking = entry["indicator"] == PlayFabParty.CHAT_INDICATOR_TALKING

# Or ask about one peer. Passing your own entity key reports TALKING/SILENT,
# so a single roster loop can cover every row including yourself.
var state := PlayFab.party.chat.get_chat_indicator({ "id": peer_entity_id, "type": "title_player_account" })

New enums: PlayFabParty.LocalChatIndicator (4 values) and PlayFabParty.ChatIndicator (6 values), mirroring PartyLocalChatControlChatIndicator / PartyChatControlChatIndicator. PlayFabPartyChatControl gains the same two getters.

Local voice controls

The local mic mute was previously unwrappable — only incoming mute existed, so push-to-talk was impossible.

# Push-to-talk.
func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("talk"):
        await PlayFab.party.chat.set_audio_input_muted_async(false).completed
    elif event.is_action_released("talk"):
        await PlayFab.party.chat.set_audio_input_muted_async(true).completed

await PlayFab.party.chat.set_audio_render_volume_async(peer_entity_key, 0.5).completed
await PlayFab.party.chat.set_audio_encoder_bitrate_async(24000).completed

# Distinguish "no microphone" from "consent denied" instead of silently having no voice.
var mic := PlayFab.party.chat.get_audio_input_state()
if mic != PlayFabParty.AUDIO_INPUT_STATE_INITIALIZED:
    show_mic_warning(mic)   # NOT_FOUND / USER_CONSENT_DENIED / ALREADY_IN_USE / ...

Also adds is_audio_input_muted, get_audio_render_volume, get_audio_encoder_bitrate, get/set_voice_audio_options, and get_audio_output_state. Audio-device state is now cached from the LocalChatAudioInput/OutputChanged state changes instead of only being ERR_PRINTed.

Language / transcription / translation now actually work

PlayFabPartyConfig.enable_transcription and enable_translation were bound but inert — the addon never called SetTranscriptionOptions or SetTextChatOptions, so transcription_received was unreachable and translated_text always equalled text.

var cfg := PlayFabPartyConfig.new()
cfg.language = "en-US"          # new; replaces the removed `metadata` slot
cfg.enable_transcription = true
cfg.enable_translation = true
await PlayFab.party.create_and_join_network_async(user, cfg).completed

PlayFab.party.chat.transcription_received.connect(func(entity_key, msg):
    print("%s: %s" % [entity_key.id, msg.translated_text]))

_configure_chat_language_options() applies them at chat-control creation, and get_language / set_language_async, get_transcription_options / set_transcription_options_async, and get_text_chat_options / set_text_chat_options_async are exposed directly.

Text-to-speech

await PlayFab.party.chat.populate_text_to_speech_profiles_async().completed
for profile in PlayFab.party.chat.get_text_to_speech_profiles():
    if profile.language_code == "en-US":
        await PlayFab.party.chat.set_text_to_speech_profile_async(
            PlayFabParty.TEXT_TO_SPEECH_TYPE_VOICE_CHAT, profile.identifier).completed
        break

await PlayFab.party.chat.synthesize_text_to_speech_async(
    PlayFabParty.TEXT_TO_SPEECH_TYPE_VOICE_CHAT, "Ready up!").completed

New PlayFabPartyTextToSpeechProfile value type (identifier, name, language_code, gender). Profiles are snapshotted because the SDK invalidates PartyTextToSpeechProfile* on the next populate call.

Network diagnostics

for network in PlayFab.party.get_networks():
    var stats := network.get_statistics([])   # empty subset returns all 16
    print(stats["average_relay_server_round_trip_latency_ms"])

    var link := network.get_device_connection_type(peer_id)

Keys are lower-snake-case and match the 16 NETWORK_STATISTIC_* constants.

Bug fixes

  • send_text_async passed dataBufferCount = 0, nullptr, so PlayFabPartyTextMessageConfig.metadata was silently dropped. It is now serialized into a single PartyDataBuffer and decoded on receipt with object instantiation disabled.
  • PlayFabPartyChatMessage gains original_text and options, and transcriptions now carry translations.

⚠️ Breaking changes

  • PlayFabPartyConfig.metadata is removed — it was never read. PlayFabPartyConfig.language replaces the slot.
  • PlayFabPartyTextMessageConfig.language_code and .translate_to_languages are removed. Party has no per-message language or translation, so these fields could never have worked.

Migration:

# Before (never worked)
msg_cfg.language_code = "en-US"
msg_cfg.translate_to_languages = ["fr-FR"]

# After — sender language is a chat-control property, translation is receiver-side
await PlayFab.party.chat.set_language_async("en-US").completed
await PlayFab.party.chat.set_text_chat_options_async(
    PlayFabParty.TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE).completed

No in-repo consumer referenced any of the removed fields (verified across samples, tutorials, the MP test client, and the C# facades).

Out of scope for this phase

Endpoint/network shared properties (the native setters are no-ops for the addon's usage), chat-control audio-device selection setters, PartyXbl, and the remaining unhandled state-change types.

Validation

  • cmake --preset default + cmake --build build --preset debug — clean, addon mirrored to all sample/test hosts.
  • ✅ Parse gate (tools\check_gd_scripts_headless.ps1) — passed.
  • Full non-live orchestrator (tools\run_all_tests.ps1), Godot 4.6.2:
    • parse gate, debug build, C++ doctest: PASS
    • GUT tests\godot\gdk: 330 tests, 3092/3092 asserts, 0 failed
    • GUT tests\godot\playfab: 84 tests, 1932/1932 asserts, 0 failed
    • GUT tests\godot\gameinput: 60 tests, 10534/10534 asserts, 0 failed
    • 13 bootstrap mini-runners: PASS
    • Overall: pass
  • ✅ C# facade parity gate (tools\run_csharp_tests.ps1): 107 passed, 0 failed — confirms every new doc_classes method/member has a managed wrapper, including the new PlayFabPartyTextToSpeechProfile.
  • Live tests were NOT run. No -Live, no -AllowLiveWrites. The PlayFab MP orchestrator stage was skipped. Voice capture, TTS synthesis, transcription, and translation are inherently live-service paths and have not been exercised against a real title — a reviewer with a sandbox title should run run_all_tests.ps1 -Live before merge.

Docs / spec / tests updated in this change

addons/godot_playfab/doc_classes/*.xml (incl. new PlayFabPartyTextToSpeechProfile.xml), the C# facades under addons/godot_playfab_csharp/Types/, docs/playfab/plugin.md, spec/gdext-playfab-party.md (Phase E marked shipped), spec/gdext-csharp.md, and the tests/godot/playfab GUT surface / constant-value / detached-error assertions.

Copilot AI added 2 commits August 14, 2026 10:18
…network diagnostics

Lands Party spec Phase E. Indicators are polled, never signalled — Party
raises no state change when an indicator flips, so titles poll them.

Chat indicators
- New PlayFabParty.LocalChatIndicator (4 values) and PlayFabParty.ChatIndicator
  (6 values) enums, mirroring PartyLocalChatControlChatIndicator and
  PartyChatControlChatIndicator.
- PlayFabPartyChat gains get_local_chat_indicator(user),
  get_chat_indicator(entity_key, user), and the roster helper
  get_chat_indicators(user); PlayFabPartyChatControl gains
  get_local_chat_indicator() and get_chat_indicator().
- All getters return safe defaults instead of erroring when there is no local
  chat control, so they are safe to call every frame. Passing the local
  player's own entity key to get_chat_indicator() reports TALKING/SILENT, so a
  single roster loop can drive every row.

Local voice controls
- set_audio_input_muted_async for push-to-talk. The local mic mute was
  previously unwrappable — only incoming mute existed.
- get/set_audio_render_volume, get/set_audio_encoder_bitrate, and
  get/set_voice_audio_options.
- Polled get_audio_input_state() / get_audio_output_state() so titles can
  distinguish "no microphone" from "consent denied" instead of silently having
  no voice. Device state is now cached from the LocalChatAudioInput/OutputChanged
  state changes instead of only being ERR_PRINTed.

Language, transcription, and translation now actually work
PlayFabPartyConfig.enable_transcription and enable_translation were bound but
inert: the addon never called SetTranscriptionOptions or SetTextChatOptions, so
transcription_received was unreachable and translated_text always equalled
text. _configure_chat_language_options() now applies them (plus the new
PlayFabPartyConfig.language) at chat-control creation, and get/set_language,
get/set_transcription_options, and get/set_text_chat_options are exposed
directly.

Text-to-speech
populate_text_to_speech_profiles_async, get_text_to_speech_profiles,
get/set_text_to_speech_profile, and synthesize_text_to_speech_async, with a new
PlayFabPartyTextToSpeechProfile value type. Profiles are snapshotted because
the SDK invalidates PartyTextToSpeechProfile* on the next populate call.

Network diagnostics
PlayFabPartyNetwork.get_statistics(subset) (lower-snake-case keys matching the
16 NETWORK_STATISTIC_* constants; an empty subset returns all) and
get_device_connection_type(peer_id).

Bug fixes
- send_text_async passed dataBufferCount = 0, nullptr, so
  PlayFabPartyTextMessageConfig.metadata was silently dropped. It is now
  serialized into a single PartyDataBuffer and decoded on receipt with object
  instantiation disabled.
- PlayFabPartyChatMessage gains original_text and options, and transcriptions
  now carry translations.

Breaking changes
- PlayFabPartyConfig.metadata is removed (it was never read;
  PlayFabPartyConfig.language replaces the slot).
- PlayFabPartyTextMessageConfig.language_code and .translate_to_languages are
  removed. Party has no per-message language or translation, so those fields
  could never have worked. Sender language is set_language_async; translation is
  receiver-side set_text_chat_options_async(TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE).

Deliberately out of scope for this phase: endpoint/network shared properties
(the native setters are no-ops for the addon's usage), chat-control audio-device
selection setters, PartyXbl, and the remaining unhandled state-change types.

doc_classes, the C# facades under addons/godot_playfab_csharp/Types/,
docs/playfab/plugin.md, spec/gdext-playfab-party.md (Phase E), and the
tests/godot/playfab GUT surface/constant/detached-error assertions are updated
in this change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 48b0f6a8-798d-4e79-9e61-e9b5e9084b09
…pper table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 48b0f6a8-798d-4e79-9e61-e9b5e9084b09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR advances the godot_playfab Party integration to Phase E, expanding the Godot-facing Party chat surface with polled chat indicators, local voice controls, text-to-speech, and network diagnostics, while also fixing previously inert transcription/translation configuration and text-message metadata delivery.

Changes:

  • Add Party chat polling surfaces (indicators + device state), local voice controls, TTS profile APIs, and network diagnostics.
  • Fix Party text message metadata transport and enrich received message fields (original text + options), plus ensure transcription/translation settings are actually applied.
  • Update tests/spec/docs and C# facade parity, including a new PlayFabPartyTextToSpeechProfile value type.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/godot/playfab/tests/test_party.gd Expands API-surface and constant-value assertions for new Party Phase E features and breaking changes.
tests/godot/playfab/tests/test_core.gd Registers the new Party TTS profile type in the PlayFab test host’s class list.
spec/gdext-playfab-party.md Updates Party spec (Phase E shipped), documents new polling/voice/TTS/diagnostics surfaces, and reflects breaking changes.
spec/gdext-csharp.md Updates the C# parity matrix to include PlayFabPartyTextToSpeechProfile.
docs/playfab/plugin.md Updates user-facing PlayFab docs overview to mention the expanded Party voice/diagnostics surface.
addons/godot_playfab/src/register_types.cpp Registers the new PlayFabPartyTextToSpeechProfile class with Godot.
addons/godot_playfab/src/playfab_party.h Adds new enums/APIs and new value type; removes deprecated config fields; extends message fields.
addons/godot_playfab/src/playfab_party.cpp Implements new Party chat control APIs (polling + setters), TTS profile snapshotting, network diagnostics, and metadata send/receive fixes.
addons/godot_playfab/doc_classes/PlayFabPartyTextToSpeechProfile.xml Adds new class documentation for the TTS profile value type.
addons/godot_playfab/doc_classes/PlayFabPartyTextMessageConfig.xml Removes per-message language/translation docs; documents metadata transport behavior.
addons/godot_playfab/doc_classes/PlayFabPartyNetwork.xml Documents new network diagnostics methods (get_statistics, get_device_connection_type).
addons/godot_playfab/doc_classes/PlayFabPartyConfig.xml Replaces deprecated metadata with language and clarifies transcription/translation behavior.
addons/godot_playfab/doc_classes/PlayFabPartyChatMessage.xml Documents new original_text and options fields on received messages.
addons/godot_playfab/doc_classes/PlayFabPartyChatControl.xml Documents new chat-control polling state and local voice/TTS controls.
addons/godot_playfab/doc_classes/PlayFabPartyChat.xml Documents new Party chat polling, voice controls, translation/transcription, and TTS entry points.
addons/godot_playfab/doc_classes/PlayFabParty.xml Adds enums/constants for indicators, audio device state, transcription/text-chat options, message options, TTS, and diagnostics.
addons/godot_playfab_csharp/Types/PlayFabPartyTextToSpeechProfile.cs Adds C# wrapper for the new Party TTS profile value type.
addons/godot_playfab_csharp/Types/PlayFabPartyTextMessageConfig.cs Removes per-message language/translation accessors, keeping metadata only.
addons/godot_playfab_csharp/Types/PlayFabPartyNetwork.cs Adds C# wrappers for network diagnostics APIs.
addons/godot_playfab_csharp/Types/PlayFabPartyConfig.cs Replaces Metadata with Language in the C# config wrapper.
addons/godot_playfab_csharp/Types/PlayFabPartyChatMessage.cs Adds C# accessors for OriginalText and Options.
addons/godot_playfab_csharp/Types/PlayFabPartyChatControl.cs Adds C# wrappers for new polling state, voice controls, and TTS control APIs.
addons/godot_playfab_csharp/Types/PlayFabPartyChat.cs Adds C# wrappers for Party chat polling, voice controls, language/options, and TTS APIs.
Suppressed comments (1)

addons/godot_playfab/doc_classes/PlayFabPartyChatMessage.xml:34

  • The original_text member description says it is empty when no filtering occurred, but the implementation sets original_text to text when Party doesn't supply originalChatText. This should be documented as an untranslated-source field (and equal to text when not translated), with filtering indicated by options.
		<member name="original_text" type="String" setter="" getter="get_original_text">The unfiltered message text as sent, when the service applied offensive-term filtering. Empty when no filtering occurred.</member>
		<member name="options" type="int" setter="" getter="get_options">Bitmask describing service-applied filtering. See [enum PlayFabParty.ChatMessageOptions].</member>

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread addons/godot_playfab/src/playfab_party.cpp
Comment thread addons/godot_playfab/src/playfab_party.cpp Outdated
Comment thread spec/gdext-playfab-party.md Outdated
Comment thread addons/godot_playfab/doc_classes/PlayFabPartyChatMessage.xml Outdated
…d chat-message doc drift

- Restore the collapsed PlayFabPartyNetwork::leave_async() function header.
- Correct spec: PlayFabPartyChatMessage.options carries CHAT_MESSAGE_OPTION_*
  (Party's PartyChatTextReceivedOptions filtering flags), not TEXT_CHAT_OPTION_*
  (receiver-side text chat configuration).
- Correct original_text docs in both doc_classes and spec. It is populated from
  Party's originalChatText, which is always the unfiltered source text and is
  set equal to chatText when filtering is disabled or was not needed -- it is
  NOT empty in that case. Titles must inspect options rather than test for an
  empty string to detect filtering. It is empty only for transcriptions, which
  omit the field.
- Clarify the metadata decode comment: bytes_to_var() is itself the
  objects-disabled API (there is no allow_objects parameter in godot-cpp); the
  object-allowing variant is the separate bytes_to_var_with_objects().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 48b0f6a8-798d-4e79-9e61-e9b5e9084b09
Copilot AI review requested due to automatic review settings August 14, 2026 17:38
@jameslen-atg

Copy link
Copy Markdown
Member Author

Thanks — three of four applied in 09c4f04.

Fixed

  • Formatting regression (playfab_party.cpp:1554): confirmed, the PlayFabPartyNetwork::leave_async() header had collapsed onto the first if. Restored.
  • options enum in the spec: correct catch. It is populated from the text-received state change's options, which is Party's PartyChatTextReceivedOptions (filtering flags). Our CHAT_MESSAGE_OPTION_* enum mirrors it exactly (None=0, FilteredOffensiveTerms=1, FilteredEntireMessage=2, FilteredDueToError=4). The spec pointed at TEXT_CHAT_OPTION_*, which is the unrelated receiver-side config enum. Spec now says CHAT_MESSAGE_OPTION_*; doc_classes was already right.
  • original_text docs: the drift was real, though for a different reason than described. Per Party.h, originalChatText is "always the unfiltered source text sent by the remote user" and points at chatText when filtering is disabled or was not needed — it is not the untranslated source, and it is not empty when unfiltered. Both doc_classes (method + member) and the spec now say it equals text when no filtering occurred, direct readers to check options rather than emptiness, and note it is empty only for transcriptions (that path calls set_values() without the trailing original/options args).

Not applied

  • bytes_to_var(..., allow_objects=false): not implementable — godot-cpp exposes bytes_to_var(const PackedByteArray &) with no allow_objects parameter. Object decoding is a separate function, bytes_to_var_with_objects(). The call is already the objects-disabled path, so the security intent holds today and is not default-dependent. I did rewrite the comment, since it implied a parameter that does not exist and could invite someone to "fix" it by switching to the unsafe variant.

Validation: debug build clean; full non-live run_all_tests.ps1 green (474 GUT tests, 0 failed); C# facade parity gate 107/107. Live tiers still not run — flagged in the PR description.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (2)

addons/godot_playfab/src/playfab_party.cpp:352

  • PlayFabPartyChatMessage.original_text is documented as empty for transcriptions, but set_values() currently forces original_text to fall back to text whenever p_original_text is empty. Since the transcription path passes no original text, transcriptions will incorrectly report original_text == text instead of empty.
    m_original_text = p_original_text.is_empty() ? p_text : p_original_text;

addons/godot_playfab/src/playfab_party.cpp:641

  • PlayFabPartyChatControl.set_audio_input_muted_async() reports party_chat_permission_failed when the caller doesn't have a valid connected local chat control. This is misleading (it's not a permissions failure) and is inconsistent with other local chat-control setters in this file that use party_chat_control_create_failed for the same precondition.
Signal PlayFabPartyChatControl::set_audio_input_muted_async(bool p_muted) {
    if (m_owner == nullptr || m_native_handle == nullptr || !m_local) {
        return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_PERMISSION_FAILED,
                "PlayFabPartyChatControl.set_audio_input_muted_async requires a local chat control.");
    }

@jameslen-atg
James Lenell (jameslen-atg) merged commit 7f024e1 into main Aug 14, 2026
10 checks passed
@jameslen-atg
James Lenell (jameslen-atg) deleted the feat/party-feature-additions branch August 14, 2026 18:02
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.

3 participants