diff --git a/addons/godot_playfab/doc_classes/PlayFabParty.xml b/addons/godot_playfab/doc_classes/PlayFabParty.xml index 6e1eaf6..ffb7d3d 100644 --- a/addons/godot_playfab/doc_classes/PlayFabParty.xml +++ b/addons/godot_playfab/doc_classes/PlayFabParty.xml @@ -57,5 +57,72 @@ Local user may send voice audio to the target peer. Local user may receive voice audio from the target peer. Local user may receive text chat from the target peer. (Sending text is per-call: target recipients are listed in the [code]targets[/code] argument of [code]send_text_async()[/code]; there is no separate send-text permission flag.) + The local microphone is working but the player is not speaking. + The local player is currently speaking and audio is being transmitted. + The local microphone is muted (see [method PlayFabPartyChat.set_audio_input_muted_async]). + No usable microphone is bound. Inspect [method PlayFabPartyChat.get_audio_input_state] for the reason. + The remote player's microphone is working but they are not speaking. + The remote player is speaking and their audio is being rendered locally. + The local chat control has no [constant CHAT_PERMISSION_RECEIVE_AUDIO] permission toward this player. + The local player muted this player (see [method PlayFabPartyChat.set_audio_muted_async]). + The remote player has no usable microphone. + The remote player muted their own microphone. + No microphone has been bound. + The microphone initialized successfully and voice capture can flow. + The requested microphone was not found. + The platform denied microphone access. Prompt the player to grant permission. + The microphone's audio format is not supported. + The microphone is exclusively held by another application. + The microphone failed to initialize for an unspecified reason. + No playback device has been bound. + The playback device initialized successfully and voice can be heard. + The requested playback device was not found. + The playback device's audio format is not supported. + The playback device is exclusively held by another application. + The playback device failed to initialize for an unspecified reason. + No device is selected. + The platform's system default communication device is used. + The device associated with the platform user is used. + A specific device id was selected (see [member PlayFabPartyConfig.audio_input]). + No voice audio processing options. + Apply noise suppression to captured voice audio. + Transcription is disabled. [signal PlayFabPartyChat.transcription_received] never fires. + Transcribe the local player's own speech. + Transcribe remote chat controls whose language matches the local one. + Transcribe remote chat controls whose language differs from the local one. + Deliver only finalized phrases, suppressing interim hypotheses. + Translate transcriptions into the local language, surfaced as [member PlayFabPartyChatMessage.translated_text]. + Deliver transcriptions without profanity masking. + Transcribe the local player even when not connected to a network. + No receiver-side text chat processing. + Translate incoming text chat into the local language, surfaced as [member PlayFabPartyChatMessage.translated_text]. Translation is receiver-side in PlayFab Party; there is no per-message translation option. + Ask the service to filter offensive terms from incoming text chat. Filtered messages report it through [member PlayFabPartyChatMessage.options]. + No service filtering was applied to the message. + Offensive terms were masked; [member PlayFabPartyChatMessage.original_text] carries the unfiltered text. + The entire message was suppressed by the filter. + The message was filtered because the filtering service failed. + Synthesized audio is rendered locally only, for screen-reader style narration. + Synthesized audio is transmitted to the chat mesh as if the local player had spoken it. This is the accessibility path for players who cannot use a microphone. + Gender-neutral synthetic voice. + Female synthetic voice. + Male synthetic voice. + Moving-average round trip latency to the relay server, in milliseconds. Reported under the key [code]average_relay_server_round_trip_latency_ms[/code]. + Total protocol packets sent. + Total protocol bytes sent. + Protocol packets retransmitted. + Protocol bytes retransmitted. + Protocol packets assumed dropped. + Total protocol packets received. + Total protocol bytes received. + Messages queued for send but not yet started. A persistently rising value indicates the title is sending faster than the link allows. + Bytes queued for send but not yet started. + Messages currently in flight. + Bytes currently in flight. + Messages discarded after exceeding their send timeout. + Bytes discarded after exceeding their send timeout. + Messages canceled before transmission. + Bytes canceled before transmission. + Traffic to the peer is relayed through the PlayFab Party relay server. + Traffic to the peer flows over a direct peer-to-peer connection. diff --git a/addons/godot_playfab/doc_classes/PlayFabPartyChat.xml b/addons/godot_playfab/doc_classes/PlayFabPartyChat.xml index 8359c6d..7d784a7 100644 --- a/addons/godot_playfab/doc_classes/PlayFabPartyChat.xml +++ b/addons/godot_playfab/doc_classes/PlayFabPartyChat.xml @@ -16,6 +16,16 @@ # React to incoming messages: PlayFab.party.chat.text_message_received.connect(func(entity_key, message): print(entity_key.id, ": ", message.text)) + + # Chat indicators are polled, never signalled: + func _process(_delta: float) -> void: + var speaking := PlayFab.party.chat.get_local_chat_indicator() == PlayFabParty.LOCAL_CHAT_INDICATOR_TALKING + $MicIcon.visible = speaking + for entry in PlayFab.party.chat.get_chat_indicators(): + _update_roster_row(entry.entity_key, entry.indicator) + + # Push-to-talk mutes the local microphone: + PlayFab.party.chat.set_audio_input_muted_async(not Input.is_action_pressed("talk")) [/codeblock] @@ -30,6 +40,37 @@ Mutes or unmutes incoming [b]voice audio[/b] from the chat control identified by the PlayFab entity key [param entity_key] on the local chat control. Independent of [method set_text_muted_async]. Emits [signal audio_muted_changed] on success. Updates the local chat-control relationship permissions for the chat control identified by the PlayFab entity key [param entity_key]. See [enum PlayFabParty.ChatPermission]. Mutes or unmutes incoming [b]text chat[/b] from the chat control identified by the PlayFab entity key [param entity_key] on the local chat control. Independent of [method set_audio_muted_async]. Emits [signal text_muted_changed] on success. + Returns whether the local player is currently talking, silent, muted, or has no working microphone. See [enum PlayFabParty.LocalChatIndicator]. + + PlayFab Party raises no state change when an indicator flips, so this must be polled — typically once per frame or on a UI refresh timer. When [param user] is [code]null[/code] the first local chat control is used. + Returns the voice activity of the chat control identified by the PlayFab entity key [param entity_key], as observed by the local chat control. See [enum PlayFabParty.ChatIndicator]. Polled, not signalled. + + Passing the local player's own entity key reports [constant PlayFabParty.CHAT_INDICATOR_TALKING] while the local capture pipeline is active and [constant PlayFabParty.CHAT_INDICATOR_SILENT] otherwise, so a single roster loop can drive every row. + Returns one dictionary per remote chat control, each shaped [code]{ "entity_key": { "id", "type" }, "indicator": int }[/code], where [code]indicator[/code] is a [enum PlayFabParty.ChatIndicator] value. Convenience for driving a whole voice roster from a single poll. + Returns whether the local microphone is muted. + Mutes or unmutes the local microphone. This is the local player's own mute — use it for push-to-talk and mute toggles. [method set_audio_muted_async] mutes [i]other[/i] players instead. + Returns the playback volume, in the range [code]0.0[/code] to [code]1.0[/code], applied to the chat control identified by [param entity_key]. + Sets the per-player playback volume for the chat control identified by [param entity_key]. [param volume] is clamped to the range [code]0.0[/code] to [code]1.0[/code]. + Returns the currently applied chat permissions toward [param entity_key]. See [enum PlayFabParty.ChatPermission]. + Returns whether incoming voice audio from [param entity_key] is muted locally. + Returns whether incoming text chat from [param entity_key] is muted locally. + Returns the BCP-47 language code of the local chat control. + Sets the BCP-47 language of the local chat control. This is the language used for outgoing text chat and voice transcription, and the target language for receiver-side translation. Usually set once via [member PlayFabPartyConfig.language]; use this to change it at runtime. + Returns the active voice transcription bitmask. See [enum PlayFabParty.TranscriptionOptions]. + Sets which chat controls get transcribed. See [enum PlayFabParty.TranscriptionOptions]. Transcription is off unless at least one flag is set; [member PlayFabPartyConfig.enable_transcription] applies a sensible default at chat-control creation. + Returns the active incoming text-chat bitmask. See [enum PlayFabParty.TextChatOptions]. + Sets receiver-side options applied to incoming text chat, such as translating into the local language or filtering offensive text. See [enum PlayFabParty.TextChatOptions]. + Returns the outgoing voice encoder bitrate in bits per second. + Sets the outgoing voice encoder bitrate in bits per second. Lower values reduce bandwidth at the cost of voice quality. + Returns the active voice audio processing bitmask. See [enum PlayFabParty.VoiceAudioOptions]. + Sets voice audio processing options such as noise suppression. See [enum PlayFabParty.VoiceAudioOptions]. + Returns the last reported microphone state. See [enum PlayFabParty.AudioInputState]. Poll this to show a "no microphone" or "microphone access denied" affordance instead of silently having no voice. + Returns the last reported playback device state. See [enum PlayFabParty.AudioOutputState]. + Asks PlayFab Party for the synthetic voices available to the local chat control. On success the list is readable with [method get_text_to_speech_profiles]. + Returns the [PlayFabPartyTextToSpeechProfile] snapshots cached by the last successful [method populate_text_to_speech_profiles_async]. Empty until that call completes. + Returns the [PlayFabPartyTextToSpeechProfile] currently selected for [param type]. See [enum PlayFabParty.SynthesizeTextToSpeechType]. Returns [code]null[/code] when no profile is selected. + Selects the synthetic voice used for [param type]. [param profile_id] is a [member PlayFabPartyTextToSpeechProfile.identifier] from [method get_text_to_speech_profiles]. See [enum PlayFabParty.SynthesizeTextToSpeechType]. + Synthesizes [param text] using the profile selected for [param type]. [constant PlayFabParty.TEXT_TO_SPEECH_TYPE_VOICE_CHAT] transmits the audio to the chat mesh as if the local player had spoken it, which is the accessibility path for players who cannot use a microphone. [constant PlayFabParty.TEXT_TO_SPEECH_TYPE_NARRATION] renders locally only. diff --git a/addons/godot_playfab/doc_classes/PlayFabPartyChatControl.xml b/addons/godot_playfab/doc_classes/PlayFabPartyChatControl.xml index 5ee85bb..f31d8c7 100644 --- a/addons/godot_playfab/doc_classes/PlayFabPartyChatControl.xml +++ b/addons/godot_playfab/doc_classes/PlayFabPartyChatControl.xml @@ -19,6 +19,36 @@ Mutes or unmutes incoming [b]voice audio[/b] from [param target] on the local chat control. Independent of [method set_text_muted_async]. Mutes or unmutes incoming [b]text chat[/b] from [param target] on the local chat control. Independent of [method set_audio_muted_async]. Destroys the chat control. The owning network may recreate it on demand. + Returns the local capture indicator for this control. See [enum PlayFabParty.LocalChatIndicator]. Only meaningful when [method is_local] is [code]true[/code]; polled, not signalled. + Returns the voice activity of this remote control as observed locally. See [enum PlayFabParty.ChatIndicator]. Polled, not signalled. + Returns the last reported microphone state. See [enum PlayFabParty.AudioInputState]. + Returns the last reported playback device state. See [enum PlayFabParty.AudioOutputState]. + Returns how the microphone was selected. See [enum PlayFabParty.AudioDeviceSelectionType]. + Returns how the playback device was selected. See [enum PlayFabParty.AudioDeviceSelectionType]. + Returns the platform id of the bound microphone, or an empty string when the system default is in use. + Returns the platform id of the bound playback device, or an empty string when the system default is in use. + Returns whether the local microphone is muted. + Mutes or unmutes the local microphone. This is the local player's own mute; [method set_audio_muted_async] mutes other players instead. + Returns the playback volume applied to [param target], in the range [code]0.0[/code] to [code]1.0[/code]. + Sets the playback volume for [param target]. [param volume] is clamped to the range [code]0.0[/code] to [code]1.0[/code]. + Returns the currently applied chat permissions toward [param target]. See [enum PlayFabParty.ChatPermission]. + Returns whether incoming voice audio from [param target] is muted. + Returns whether incoming text chat from [param target] is muted. + Returns the BCP-47 language code of the control. + Sets the BCP-47 language of the control. + Returns the active voice transcription bitmask. See [enum PlayFabParty.TranscriptionOptions]. + Sets which chat controls get transcribed. See [enum PlayFabParty.TranscriptionOptions]. + Returns the active incoming text-chat bitmask. See [enum PlayFabParty.TextChatOptions]. + Sets receiver-side options applied to incoming text chat. See [enum PlayFabParty.TextChatOptions]. + Returns the outgoing voice encoder bitrate in bits per second. + Sets the outgoing voice encoder bitrate in bits per second. + Returns the active voice audio processing bitmask. See [enum PlayFabParty.VoiceAudioOptions]. + Sets voice audio processing options such as noise suppression. See [enum PlayFabParty.VoiceAudioOptions]. + Asks PlayFab Party for the synthetic voices available to this control. + Returns the [PlayFabPartyTextToSpeechProfile] snapshots cached by the last successful [method populate_text_to_speech_profiles_async]. + Returns the profile selected for [param type]. See [enum PlayFabParty.SynthesizeTextToSpeechType]. + Selects the synthetic voice used for [param type]. + Synthesizes [param text] using the profile selected for [param type]. Chat control id. diff --git a/addons/godot_playfab/doc_classes/PlayFabPartyChatMessage.xml b/addons/godot_playfab/doc_classes/PlayFabPartyChatMessage.xml index c0083d3..4227e0d 100644 --- a/addons/godot_playfab/doc_classes/PlayFabPartyChatMessage.xml +++ b/addons/godot_playfab/doc_classes/PlayFabPartyChatMessage.xml @@ -17,6 +17,8 @@ Returns whether this message originated from speech-to-text transcription. Returns the millisecond timestamp captured when the message was produced. Returns the optional metadata dictionary attached to the message. + Returns the unfiltered source text sent by the remote peer. Party sets this equal to [method get_text] when offensive-term filtering is disabled or was not needed, so inspect [method get_options] rather than testing for an empty string to detect filtering. Empty for transcriptions. + Returns a bitmask describing service-applied filtering. See [enum PlayFabParty.ChatMessageOptions]. The sending chat control. @@ -27,7 +29,9 @@ Translated message text. Whether the message originated from speech-to-text transcription. Millisecond timestamp. - Optional metadata dictionary. + Optional metadata dictionary decoded from the sender's [member PlayFabPartyTextMessageConfig.metadata]. + The unfiltered source text sent by the remote peer. Equals [member text] when offensive-term filtering is disabled or was not needed; check [member options] rather than emptiness to detect filtering. Empty for transcriptions. + Bitmask describing service-applied filtering. See [enum PlayFabParty.ChatMessageOptions]. diff --git a/addons/godot_playfab/doc_classes/PlayFabPartyConfig.xml b/addons/godot_playfab/doc_classes/PlayFabPartyConfig.xml index da49b0a..dfad115 100644 --- a/addons/godot_playfab/doc_classes/PlayFabPartyConfig.xml +++ b/addons/godot_playfab/doc_classes/PlayFabPartyConfig.xml @@ -14,11 +14,11 @@ Optional invitation id; the addon generates one if empty. Whether the addon should create and connect a voice chat control. Whether the addon should enable text chat on the chat control. - Whether the addon should enable speech-to-text transcription on the chat control. - Whether the addon should enable text translation on the chat control. + Whether the addon should enable speech-to-text transcription on the chat control. When [code]true[/code] the addon calls [code]SetTranscriptionOptions[/code] on the local chat control for the local user plus all remote controls, in both matching and non-matching languages, so [signal PlayFabPartyChat.transcription_received] fires. + Whether the addon should enable translation on the chat control. Translation is receiver-side in PlayFab Party: when [code]true[/code] the addon asks Party to translate incoming text chat (and, when [member enable_transcription] is also set, incoming transcriptions) into this control's language, surfaced as [member PlayFabPartyChatMessage.translated_text]. Audio input (microphone) device id bound on the local chat control. Empty selects the platform's system default communication device. The device is always bound when the chat control is created; whether voice actually flows still depends on chat permissions and the audio subsystem successfully initializing the chosen device. Audio output (speaker) device id bound on the local chat control. Empty selects the platform's system default communication device. The device is always bound when the chat control is created; whether voice actually flows still depends on chat permissions and the audio subsystem successfully initializing the chosen device. - Title-supplied metadata stored alongside the Party network. + BCP-47 language code applied to the local chat control at creation. Empty keeps the Party default (the platform's language). This is the language used for outgoing text chat and voice transcription, and the target language for receiver-side translation. diff --git a/addons/godot_playfab/doc_classes/PlayFabPartyNetwork.xml b/addons/godot_playfab/doc_classes/PlayFabPartyNetwork.xml index c08e6a7..660daa7 100644 --- a/addons/godot_playfab/doc_classes/PlayFabPartyNetwork.xml +++ b/addons/godot_playfab/doc_classes/PlayFabPartyNetwork.xml @@ -16,6 +16,14 @@ Returns the local [PlayFabPartyPeer] for the network. Returns the local [PlayFabPartyChatControl], if chat is enabled. Returns whether the local participant created the Party network. + Returns live transport statistics for this network, keyed by lower snake case statistic name (for example [code]average_relay_server_round_trip_latency_ms[/code] or [code]currently_queued_send_messages[/code]). Pass [enum PlayFabParty.NetworkStatistic] values in [param statistics] to fetch a subset; an empty array (the default) returns all of them. Returns an empty [Dictionary] when the network has no live handle. + [codeblock] + var stats := network.get_statistics([PlayFabParty.NETWORK_STATISTIC_AVERAGE_RELAY_SERVER_ROUND_TRIP_LATENCY_MS]) + $PingLabel.text = "%d ms" % stats.average_relay_server_round_trip_latency_ms + [/codeblock] + Returns whether traffic to [param peer_id] flows over a direct peer-to-peer connection or is relayed through the PlayFab Party relay server. See [enum PlayFabParty.DeviceConnectionType]. Falls back to [constant PlayFabParty.DEVICE_CONNECTION_TYPE_RELAY_SERVER] for an unknown peer. + + Direct connections are only ever attempted when [member PlayFabPartyConfig.direct_peer_connectivity] permits them; this reports what was actually negotiated. Convenience helper that calls [method PlayFabParty.leave_network_async] for this network. diff --git a/addons/godot_playfab/doc_classes/PlayFabPartyTextMessageConfig.xml b/addons/godot_playfab/doc_classes/PlayFabPartyTextMessageConfig.xml index ba13499..5946cd9 100644 --- a/addons/godot_playfab/doc_classes/PlayFabPartyTextMessageConfig.xml +++ b/addons/godot_playfab/doc_classes/PlayFabPartyTextMessageConfig.xml @@ -4,14 +4,14 @@ Per-message options for [method PlayFabPartyChat.send_text_async] and [method PlayFabPartyChatControl.send_text_async]. - Optional language and translation hints applied when the addon forwards a text message through the local Party chat control. + Title-supplied metadata attached when the addon forwards a text message through the local Party chat control. + + PlayFab Party has no per-message language or translation setting. The sender's language comes from [method PlayFabPartyChat.set_language_async] on the local chat control, and translation is a receiver-side setting applied with [method PlayFabPartyChat.set_text_chat_options_async]. - BCP-47 language code for the source message. - List of BCP-47 language codes the message should be translated into. - Title-supplied metadata attached to the message. + Title-supplied metadata attached to the message. Serialized into a single Party data buffer and delivered verbatim to receivers as [member PlayFabPartyChatMessage.metadata]. Decoded on the receiving side with object instantiation disabled, so only plain Variant data survives the round trip. diff --git a/addons/godot_playfab/doc_classes/PlayFabPartyTextToSpeechProfile.xml b/addons/godot_playfab/doc_classes/PlayFabPartyTextToSpeechProfile.xml new file mode 100644 index 0000000..c152b5e --- /dev/null +++ b/addons/godot_playfab/doc_classes/PlayFabPartyTextToSpeechProfile.xml @@ -0,0 +1,26 @@ + + + + A synthetic voice available to PlayFab Party text-to-speech. + + + An engine-owned snapshot of a PlayFab Party text-to-speech profile. Populate the list with [method PlayFabPartyChat.populate_text_to_speech_profiles_async], read it with [method PlayFabPartyChat.get_text_to_speech_profiles], and select one by passing [member identifier] to [method PlayFabPartyChat.set_text_to_speech_profile_async]. + + The Party SDK invalidates its own profile pointers on each populate call, so this class holds copies and stays valid for as long as the title keeps a reference. + + + + Returns the profile identifier to pass to [method PlayFabPartyChat.set_text_to_speech_profile_async]. + Returns the human-readable profile name. + Returns the BCP-47 language code the profile speaks. + Returns the voice gender. See [enum PlayFabParty.Gender]. + + + Profile identifier. + Human-readable profile name. + BCP-47 language code. + Voice gender. See [enum PlayFabParty.Gender]. + + + + diff --git a/addons/godot_playfab/src/playfab_party.cpp b/addons/godot_playfab/src/playfab_party.cpp index 4311671..7970214 100644 --- a/addons/godot_playfab/src/playfab_party.cpp +++ b/addons/godot_playfab/src/playfab_party.cpp @@ -151,6 +151,27 @@ bool dictionary_entity_key_equals(const Dictionary &a, const Dictionary &b) { String(a.get("type", String())) == String(b.get("type", String())); } +// Copies a Party-owned PartyTextToSpeechProfile into an engine-owned snapshot. +// The SDK invalidates its profile pointers on the next +// PopulateAvailableTextToSpeechProfiles, so nothing may hold them across calls. +Ref text_to_speech_profile_snapshot(Party::PartyTextToSpeechProfile *p_profile) { + Ref wrapper; + if (p_profile == nullptr) { + return wrapper; + } + PartyString identifier = nullptr; + PartyString name = nullptr; + PartyString language_code = nullptr; + Party::PartyGender gender = Party::PartyGender::Neutral; + p_profile->GetIdentifier(&identifier); + p_profile->GetName(&name); + p_profile->GetLanguageCode(&language_code); + p_profile->GetGender(&gender); + wrapper.instantiate(); + wrapper->set_values(party_string(identifier), party_string(name), party_string(language_code), static_cast(gender)); + return wrapper; +} + } // namespace // --------------------------------------------------------------------------- @@ -171,12 +192,12 @@ void PlayFabPartyConfig::_bind_methods() { ClassDB::bind_method(D_METHOD("set_transcription_enabled", "enabled"), &PlayFabPartyConfig::set_transcription_enabled); ClassDB::bind_method(D_METHOD("is_translation_enabled"), &PlayFabPartyConfig::is_translation_enabled); ClassDB::bind_method(D_METHOD("set_translation_enabled", "enabled"), &PlayFabPartyConfig::set_translation_enabled); + ClassDB::bind_method(D_METHOD("get_language"), &PlayFabPartyConfig::get_language); + ClassDB::bind_method(D_METHOD("set_language", "language"), &PlayFabPartyConfig::set_language); ClassDB::bind_method(D_METHOD("get_audio_input"), &PlayFabPartyConfig::get_audio_input); ClassDB::bind_method(D_METHOD("set_audio_input", "audio_input"), &PlayFabPartyConfig::set_audio_input); ClassDB::bind_method(D_METHOD("get_audio_output"), &PlayFabPartyConfig::get_audio_output); ClassDB::bind_method(D_METHOD("set_audio_output", "audio_output"), &PlayFabPartyConfig::set_audio_output); - ClassDB::bind_method(D_METHOD("get_metadata"), &PlayFabPartyConfig::get_metadata); - ClassDB::bind_method(D_METHOD("set_metadata", "metadata"), &PlayFabPartyConfig::set_metadata); ADD_PROPERTY(PropertyInfo(Variant::INT, "max_players"), "set_max_players", "get_max_players"); ADD_PROPERTY(PropertyInfo(Variant::INT, "direct_peer_connectivity"), "set_direct_peer_connectivity", "get_direct_peer_connectivity"); @@ -185,9 +206,9 @@ void PlayFabPartyConfig::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enable_text_chat"), "set_text_chat_enabled", "is_text_chat_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enable_transcription"), "set_transcription_enabled", "is_transcription_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enable_translation"), "set_translation_enabled", "is_translation_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "audio_input"), "set_audio_input", "get_audio_input"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "audio_output"), "set_audio_output", "get_audio_output"); - ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "metadata"), "set_metadata", "get_metadata"); } int64_t PlayFabPartyConfig::get_max_players() const { return m_max_players; } @@ -204,36 +225,53 @@ bool PlayFabPartyConfig::is_transcription_enabled() const { return m_enable_tran void PlayFabPartyConfig::set_transcription_enabled(bool p_enabled) { m_enable_transcription = p_enabled; } bool PlayFabPartyConfig::is_translation_enabled() const { return m_enable_translation; } void PlayFabPartyConfig::set_translation_enabled(bool p_enabled) { m_enable_translation = p_enabled; } +String PlayFabPartyConfig::get_language() const { return m_language; } +void PlayFabPartyConfig::set_language(const String &p_language) { m_language = p_language; } String PlayFabPartyConfig::get_audio_input() const { return m_audio_input; } void PlayFabPartyConfig::set_audio_input(const String &p_audio_input) { m_audio_input = p_audio_input; } String PlayFabPartyConfig::get_audio_output() const { return m_audio_output; } void PlayFabPartyConfig::set_audio_output(const String &p_audio_output) { m_audio_output = p_audio_output; } -Dictionary PlayFabPartyConfig::get_metadata() const { return m_metadata; } -void PlayFabPartyConfig::set_metadata(const Dictionary &p_metadata) { m_metadata = p_metadata; } // --------------------------------------------------------------------------- // PlayFabPartyTextMessageConfig void PlayFabPartyTextMessageConfig::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_language_code"), &PlayFabPartyTextMessageConfig::get_language_code); - ClassDB::bind_method(D_METHOD("set_language_code", "language_code"), &PlayFabPartyTextMessageConfig::set_language_code); - ClassDB::bind_method(D_METHOD("get_translate_to_languages"), &PlayFabPartyTextMessageConfig::get_translate_to_languages); - ClassDB::bind_method(D_METHOD("set_translate_to_languages", "languages"), &PlayFabPartyTextMessageConfig::set_translate_to_languages); ClassDB::bind_method(D_METHOD("get_metadata"), &PlayFabPartyTextMessageConfig::get_metadata); ClassDB::bind_method(D_METHOD("set_metadata", "metadata"), &PlayFabPartyTextMessageConfig::set_metadata); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "language_code"), "set_language_code", "get_language_code"); - ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "translate_to_languages"), "set_translate_to_languages", "get_translate_to_languages"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "metadata"), "set_metadata", "get_metadata"); } -String PlayFabPartyTextMessageConfig::get_language_code() const { return m_language_code; } -void PlayFabPartyTextMessageConfig::set_language_code(const String &p_language_code) { m_language_code = p_language_code; } -PackedStringArray PlayFabPartyTextMessageConfig::get_translate_to_languages() const { return m_translate_to_languages; } -void PlayFabPartyTextMessageConfig::set_translate_to_languages(const PackedStringArray &p_languages) { m_translate_to_languages = p_languages; } Dictionary PlayFabPartyTextMessageConfig::get_metadata() const { return m_metadata; } void PlayFabPartyTextMessageConfig::set_metadata(const Dictionary &p_metadata) { m_metadata = p_metadata; } +// --------------------------------------------------------------------------- +// PlayFabPartyTextToSpeechProfile + +void PlayFabPartyTextToSpeechProfile::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_identifier"), &PlayFabPartyTextToSpeechProfile::get_identifier); + ClassDB::bind_method(D_METHOD("get_name"), &PlayFabPartyTextToSpeechProfile::get_name); + ClassDB::bind_method(D_METHOD("get_language_code"), &PlayFabPartyTextToSpeechProfile::get_language_code); + ClassDB::bind_method(D_METHOD("get_gender"), &PlayFabPartyTextToSpeechProfile::get_gender); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "identifier", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_identifier"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_name"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language_code", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_language_code"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "gender", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_gender"); +} + +void PlayFabPartyTextToSpeechProfile::set_values(const String &p_identifier, const String &p_name, const String &p_language_code, int64_t p_gender) { + m_identifier = p_identifier; + m_name = p_name; + m_language_code = p_language_code; + m_gender = p_gender; +} + +String PlayFabPartyTextToSpeechProfile::get_identifier() const { return m_identifier; } +String PlayFabPartyTextToSpeechProfile::get_name() const { return m_name; } +String PlayFabPartyTextToSpeechProfile::get_language_code() const { return m_language_code; } +int64_t PlayFabPartyTextToSpeechProfile::get_gender() const { return m_gender; } + // --------------------------------------------------------------------------- // PlayFabPartyMember @@ -269,20 +307,24 @@ void PlayFabPartyChatMessage::_bind_methods() { ClassDB::bind_method(D_METHOD("get_sender_entity_key"), &PlayFabPartyChatMessage::get_sender_entity_key); ClassDB::bind_method(D_METHOD("get_targets"), &PlayFabPartyChatMessage::get_targets); ClassDB::bind_method(D_METHOD("get_text"), &PlayFabPartyChatMessage::get_text); + ClassDB::bind_method(D_METHOD("get_original_text"), &PlayFabPartyChatMessage::get_original_text); ClassDB::bind_method(D_METHOD("get_language_code"), &PlayFabPartyChatMessage::get_language_code); ClassDB::bind_method(D_METHOD("get_translated_text"), &PlayFabPartyChatMessage::get_translated_text); ClassDB::bind_method(D_METHOD("is_transcription"), &PlayFabPartyChatMessage::is_transcription); ClassDB::bind_method(D_METHOD("get_timestamp"), &PlayFabPartyChatMessage::get_timestamp); + ClassDB::bind_method(D_METHOD("get_options"), &PlayFabPartyChatMessage::get_options); ClassDB::bind_method(D_METHOD("get_metadata"), &PlayFabPartyChatMessage::get_metadata); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "sender", PROPERTY_HINT_RESOURCE_TYPE, "PlayFabPartyChatControl", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_sender"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "sender_entity_key", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_sender_entity_key"); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "targets", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_targets"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "original_text", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_original_text"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "language_code", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_language_code"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "translated_text", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_translated_text"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_transcription", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "is_transcription"); ADD_PROPERTY(PropertyInfo(Variant::INT, "timestamp", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_timestamp"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "options", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_options"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "metadata", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_metadata"); } @@ -295,7 +337,9 @@ void PlayFabPartyChatMessage::set_values( const String &p_translated_text, bool p_transcription, int64_t p_timestamp, - const Dictionary &p_metadata) { + const Dictionary &p_metadata, + const String &p_original_text, + int64_t p_options) { m_sender = p_sender; m_sender_entity_key = p_sender_entity_key; m_targets = p_targets; @@ -305,16 +349,20 @@ void PlayFabPartyChatMessage::set_values( m_transcription = p_transcription; m_timestamp = p_timestamp; m_metadata = p_metadata; + m_original_text = p_original_text.is_empty() ? p_text : p_original_text; + m_options = p_options; } Ref PlayFabPartyChatMessage::get_sender() const { return m_sender; } Dictionary PlayFabPartyChatMessage::get_sender_entity_key() const { return m_sender_entity_key; } Array PlayFabPartyChatMessage::get_targets() const { return m_targets; } String PlayFabPartyChatMessage::get_text() const { return m_text; } +String PlayFabPartyChatMessage::get_original_text() const { return m_original_text; } String PlayFabPartyChatMessage::get_language_code() const { return m_language_code; } String PlayFabPartyChatMessage::get_translated_text() const { return m_translated_text; } bool PlayFabPartyChatMessage::is_transcription() const { return m_transcription; } int64_t PlayFabPartyChatMessage::get_timestamp() const { return m_timestamp; } +int64_t PlayFabPartyChatMessage::get_options() const { return m_options; } Dictionary PlayFabPartyChatMessage::get_metadata() const { return m_metadata; } // --------------------------------------------------------------------------- @@ -364,6 +412,37 @@ void PlayFabPartyChatControl::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text_muted_async", "target", "muted"), &PlayFabPartyChatControl::set_text_muted_async); ClassDB::bind_method(D_METHOD("destroy_async"), &PlayFabPartyChatControl::destroy_async); + ClassDB::bind_method(D_METHOD("get_local_chat_indicator"), &PlayFabPartyChatControl::get_local_chat_indicator); + ClassDB::bind_method(D_METHOD("get_chat_indicator"), &PlayFabPartyChatControl::get_chat_indicator); + ClassDB::bind_method(D_METHOD("get_audio_input_state"), &PlayFabPartyChatControl::get_audio_input_state); + ClassDB::bind_method(D_METHOD("get_audio_output_state"), &PlayFabPartyChatControl::get_audio_output_state); + ClassDB::bind_method(D_METHOD("get_audio_input_selection_type"), &PlayFabPartyChatControl::get_audio_input_selection_type); + ClassDB::bind_method(D_METHOD("get_audio_output_selection_type"), &PlayFabPartyChatControl::get_audio_output_selection_type); + ClassDB::bind_method(D_METHOD("get_audio_input_device_id"), &PlayFabPartyChatControl::get_audio_input_device_id); + ClassDB::bind_method(D_METHOD("get_audio_output_device_id"), &PlayFabPartyChatControl::get_audio_output_device_id); + ClassDB::bind_method(D_METHOD("is_audio_input_muted"), &PlayFabPartyChatControl::is_audio_input_muted); + ClassDB::bind_method(D_METHOD("set_audio_input_muted_async", "muted"), &PlayFabPartyChatControl::set_audio_input_muted_async); + ClassDB::bind_method(D_METHOD("get_audio_render_volume", "target"), &PlayFabPartyChatControl::get_audio_render_volume); + ClassDB::bind_method(D_METHOD("set_audio_render_volume_async", "target", "volume"), &PlayFabPartyChatControl::set_audio_render_volume_async); + ClassDB::bind_method(D_METHOD("get_audio_encoder_bitrate"), &PlayFabPartyChatControl::get_audio_encoder_bitrate); + ClassDB::bind_method(D_METHOD("set_audio_encoder_bitrate_async", "bitrate"), &PlayFabPartyChatControl::set_audio_encoder_bitrate_async); + ClassDB::bind_method(D_METHOD("get_voice_audio_options"), &PlayFabPartyChatControl::get_voice_audio_options); + ClassDB::bind_method(D_METHOD("set_voice_audio_options_async", "options"), &PlayFabPartyChatControl::set_voice_audio_options_async); + ClassDB::bind_method(D_METHOD("get_language"), &PlayFabPartyChatControl::get_language); + ClassDB::bind_method(D_METHOD("set_language_async", "language_code"), &PlayFabPartyChatControl::set_language_async); + ClassDB::bind_method(D_METHOD("get_transcription_options"), &PlayFabPartyChatControl::get_transcription_options); + ClassDB::bind_method(D_METHOD("set_transcription_options_async", "options"), &PlayFabPartyChatControl::set_transcription_options_async); + ClassDB::bind_method(D_METHOD("get_text_chat_options"), &PlayFabPartyChatControl::get_text_chat_options); + ClassDB::bind_method(D_METHOD("set_text_chat_options_async", "options"), &PlayFabPartyChatControl::set_text_chat_options_async); + ClassDB::bind_method(D_METHOD("get_permissions", "target"), &PlayFabPartyChatControl::get_permissions); + ClassDB::bind_method(D_METHOD("is_audio_muted", "target"), &PlayFabPartyChatControl::is_audio_muted); + ClassDB::bind_method(D_METHOD("is_text_muted", "target"), &PlayFabPartyChatControl::is_text_muted); + ClassDB::bind_method(D_METHOD("populate_text_to_speech_profiles_async"), &PlayFabPartyChatControl::populate_text_to_speech_profiles_async); + ClassDB::bind_method(D_METHOD("get_text_to_speech_profiles"), &PlayFabPartyChatControl::get_text_to_speech_profiles); + ClassDB::bind_method(D_METHOD("get_text_to_speech_profile", "type"), &PlayFabPartyChatControl::get_text_to_speech_profile); + ClassDB::bind_method(D_METHOD("set_text_to_speech_profile_async", "type", "profile_id"), &PlayFabPartyChatControl::set_text_to_speech_profile_async); + ClassDB::bind_method(D_METHOD("synthesize_text_to_speech_async", "type", "text"), &PlayFabPartyChatControl::synthesize_text_to_speech_async); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "id", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_id"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "user", PROPERTY_HINT_RESOURCE_TYPE, "PlayFabUser", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_user"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_voice_enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "is_voice_enabled"); @@ -465,6 +544,361 @@ Signal PlayFabPartyChatControl::destroy_async() { return m_owner->_destroy_chat_control(Ref(this)); } +// --- Chat indicators ------------------------------------------------------- + +int64_t PlayFabPartyChatControl::get_local_chat_indicator() const { + if (!m_local || m_native_handle == nullptr) { + return PlayFabParty::LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT; + } + Party::PartyLocalChatControlChatIndicator indicator = Party::PartyLocalChatControlChatIndicator::Silent; + PartyError err = static_cast(m_native_handle)->GetLocalChatIndicator(&indicator); + if (PARTY_FAILED(err)) { + return PlayFabParty::LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT; + } + return static_cast(indicator); +} + +int64_t PlayFabPartyChatControl::get_chat_indicator() const { + if (m_owner == nullptr || m_native_handle == nullptr || m_local) { + return PlayFabParty::CHAT_INDICATOR_SILENT; + } + Party::PartyLocalChatControl *local = m_owner->_resolve_local_native_chat_control(Ref()); + if (local == nullptr) { + return PlayFabParty::CHAT_INDICATOR_SILENT; + } + Party::PartyChatControlChatIndicator indicator = Party::PartyChatControlChatIndicator::Silent; + PartyError err = local->GetChatIndicator(m_native_handle, &indicator); + if (PARTY_FAILED(err)) { + return PlayFabParty::CHAT_INDICATOR_SILENT; + } + return static_cast(indicator); +} + +// --- Audio device state / selection ---------------------------------------- + +int64_t PlayFabPartyChatControl::get_audio_input_state() const { return m_audio_input_state; } +int64_t PlayFabPartyChatControl::get_audio_output_state() const { return m_audio_output_state; } + +int64_t PlayFabPartyChatControl::get_audio_input_selection_type() const { + if (!m_local || m_native_handle == nullptr) { + return PlayFabParty::AUDIO_DEVICE_SELECTION_NONE; + } + Party::PartyAudioDeviceSelectionType type = Party::PartyAudioDeviceSelectionType::None; + PartyString context = nullptr; + PartyString device_id = nullptr; + PartyError err = static_cast(m_native_handle)->GetAudioInput(&type, &context, &device_id); + return PARTY_FAILED(err) ? int64_t(PlayFabParty::AUDIO_DEVICE_SELECTION_NONE) : static_cast(type); +} + +int64_t PlayFabPartyChatControl::get_audio_output_selection_type() const { + if (!m_local || m_native_handle == nullptr) { + return PlayFabParty::AUDIO_DEVICE_SELECTION_NONE; + } + Party::PartyAudioDeviceSelectionType type = Party::PartyAudioDeviceSelectionType::None; + PartyString context = nullptr; + PartyString device_id = nullptr; + PartyError err = static_cast(m_native_handle)->GetAudioOutput(&type, &context, &device_id); + return PARTY_FAILED(err) ? int64_t(PlayFabParty::AUDIO_DEVICE_SELECTION_NONE) : static_cast(type); +} + +String PlayFabPartyChatControl::get_audio_input_device_id() const { + if (!m_local || m_native_handle == nullptr) { + return String(); + } + Party::PartyAudioDeviceSelectionType type = Party::PartyAudioDeviceSelectionType::None; + PartyString context = nullptr; + PartyString device_id = nullptr; + PartyError err = static_cast(m_native_handle)->GetAudioInput(&type, &context, &device_id); + return PARTY_FAILED(err) ? String() : party_string(device_id); +} + +String PlayFabPartyChatControl::get_audio_output_device_id() const { + if (!m_local || m_native_handle == nullptr) { + return String(); + } + Party::PartyAudioDeviceSelectionType type = Party::PartyAudioDeviceSelectionType::None; + PartyString context = nullptr; + PartyString device_id = nullptr; + PartyError err = static_cast(m_native_handle)->GetAudioOutput(&type, &context, &device_id); + return PARTY_FAILED(err) ? String() : party_string(device_id); +} + +// --- Local capture mute ---------------------------------------------------- + +bool PlayFabPartyChatControl::is_audio_input_muted() const { + if (!m_local || m_native_handle == nullptr) { + return false; + } + PartyBool muted = 0; + PartyError err = static_cast(m_native_handle)->GetAudioInputMuted(&muted); + return PARTY_FAILED(err) ? false : muted != 0; +} + +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."); + } + return m_owner->_set_audio_input_muted(static_cast(m_native_handle), p_muted); +} + +// --- Per-target render volume ---------------------------------------------- + +float PlayFabPartyChatControl::get_audio_render_volume(const Ref &p_target) const { + if (!m_local || m_native_handle == nullptr || !p_target.is_valid() || p_target->get_native_handle() == nullptr) { + return 0.0f; + } + float volume = 0.0f; + PartyError err = static_cast(m_native_handle)->GetAudioRenderVolume(p_target->get_native_handle(), &volume); + return PARTY_FAILED(err) ? 0.0f : volume; +} + +Signal PlayFabPartyChatControl::set_audio_render_volume_async(const Ref &p_target, double p_volume) { + 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_render_volume_async requires a local chat control."); + } + if (!p_target.is_valid() || p_target->get_native_handle() == nullptr) { + return detached_error_signal(E_INVALIDARG, PARTY_CHAT_PERMISSION_FAILED, + "PlayFabPartyChatControl.set_audio_render_volume_async requires a target chat control."); + } + return m_owner->_set_audio_render_volume(static_cast(m_native_handle), p_target->get_native_handle(), p_volume); +} + +// --- Encoder / voice processing -------------------------------------------- + +int64_t PlayFabPartyChatControl::get_audio_encoder_bitrate() const { + if (!m_local || m_native_handle == nullptr) { + return 0; + } + uint32_t bitrate = 0; + PartyError err = static_cast(m_native_handle)->GetAudioEncoderBitrate(&bitrate); + return PARTY_FAILED(err) ? 0 : static_cast(bitrate); +} + +Signal PlayFabPartyChatControl::set_audio_encoder_bitrate_async(int64_t p_bitrate) { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.set_audio_encoder_bitrate_async requires a local chat control."); + } + Party::PartyLocalChatControl *local = static_cast(m_native_handle); + const uint32_t bitrate = static_cast(p_bitrate < 0 ? 0 : p_bitrate); + return m_owner->_dispatch_chat_control_operation( + local, + PlayFabParty::PENDING_SET_AUDIO_ENCODER_BITRATE, + PARTY_CHAT_CONTROL_CREATE_FAILED, + "PartyLocalChatControl::SetAudioEncoderBitrate", + [local, bitrate](void *p_async) { return local->SetAudioEncoderBitrate(bitrate, p_async); }); +} + +int64_t PlayFabPartyChatControl::get_voice_audio_options() const { + if (!m_local || m_native_handle == nullptr) { + return PlayFabParty::VOICE_AUDIO_OPTION_NONE; + } + Party::PartyVoiceAudioOptions options = Party::PartyVoiceAudioOptions::None; + PartyError err = static_cast(m_native_handle)->GetVoiceAudioOptions(&options); + return PARTY_FAILED(err) ? int64_t(PlayFabParty::VOICE_AUDIO_OPTION_NONE) : static_cast(options); +} + +Signal PlayFabPartyChatControl::set_voice_audio_options_async(int64_t p_options) { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.set_voice_audio_options_async requires a local chat control."); + } + // SetVoiceAudioOptions is synchronous in the Party SDK; wrap the result so + // callers get the same awaitable shape as the other setters. + PartyError err = static_cast(m_native_handle) + ->SetVoiceAudioOptions(static_cast(p_options)); + if (PARTY_FAILED(err)) { + return detached_error_signal(E_FAIL, PARTY_CHAT_CONTROL_CREATE_FAILED, + vformat("PartyLocalChatControl::SetVoiceAudioOptions failed (0x%x).", static_cast(err))); + } + return detached_ok_signal(); +} + +// --- Language / transcription / text-chat options --------------------------- + +String PlayFabPartyChatControl::get_language() const { + if (!m_local || m_native_handle == nullptr) { + return String(); + } + PartyString language = nullptr; + PartyError err = static_cast(m_native_handle)->GetLanguage(&language); + return PARTY_FAILED(err) ? String() : party_string(language); +} + +Signal PlayFabPartyChatControl::set_language_async(const String &p_language_code) { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.set_language_async requires a local chat control."); + } + Party::PartyLocalChatControl *local = static_cast(m_native_handle); + // The SDK copies the string during the call, but the CharString must stay + // alive for the duration of the lambda invocation below, so capture by value. + const CharString language_utf8 = p_language_code.utf8(); + return m_owner->_dispatch_chat_control_operation( + local, + PlayFabParty::PENDING_SET_LANGUAGE, + PARTY_CHAT_CONTROL_CREATE_FAILED, + "PartyLocalChatControl::SetLanguage", + [local, language_utf8](void *p_async) { + return local->SetLanguage(language_utf8.length() > 0 ? language_utf8.get_data() : nullptr, p_async); + }); +} + +int64_t PlayFabPartyChatControl::get_transcription_options() const { + if (!m_local || m_native_handle == nullptr) { + return PlayFabParty::TRANSCRIPTION_OPTION_NONE; + } + Party::PartyVoiceChatTranscriptionOptions options = Party::PartyVoiceChatTranscriptionOptions::None; + PartyError err = static_cast(m_native_handle)->GetTranscriptionOptions(&options); + return PARTY_FAILED(err) ? int64_t(PlayFabParty::TRANSCRIPTION_OPTION_NONE) : static_cast(options); +} + +Signal PlayFabPartyChatControl::set_transcription_options_async(int64_t p_options) { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.set_transcription_options_async requires a local chat control."); + } + Party::PartyLocalChatControl *local = static_cast(m_native_handle); + const auto options = static_cast(p_options); + return m_owner->_dispatch_chat_control_operation( + local, + PlayFabParty::PENDING_SET_TRANSCRIPTION_OPTIONS, + PARTY_CHAT_CONTROL_CREATE_FAILED, + "PartyLocalChatControl::SetTranscriptionOptions", + [local, options](void *p_async) { return local->SetTranscriptionOptions(options, p_async); }); +} + +int64_t PlayFabPartyChatControl::get_text_chat_options() const { + if (!m_local || m_native_handle == nullptr) { + return PlayFabParty::TEXT_CHAT_OPTION_NONE; + } + Party::PartyTextChatOptions options = Party::PartyTextChatOptions::None; + PartyError err = static_cast(m_native_handle)->GetTextChatOptions(&options); + return PARTY_FAILED(err) ? int64_t(PlayFabParty::TEXT_CHAT_OPTION_NONE) : static_cast(options); +} + +Signal PlayFabPartyChatControl::set_text_chat_options_async(int64_t p_options) { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.set_text_chat_options_async requires a local chat control."); + } + Party::PartyLocalChatControl *local = static_cast(m_native_handle); + const auto options = static_cast(p_options); + return m_owner->_dispatch_chat_control_operation( + local, + PlayFabParty::PENDING_SET_TEXT_CHAT_OPTIONS, + PARTY_CHAT_CONTROL_CREATE_FAILED, + "PartyLocalChatControl::SetTextChatOptions", + [local, options](void *p_async) { return local->SetTextChatOptions(options, p_async); }); +} + +// --- Readback --------------------------------------------------------------- + +int64_t PlayFabPartyChatControl::get_permissions(const Ref &p_target) const { + if (m_owner == nullptr || !m_local || m_native_handle == nullptr || !p_target.is_valid() || p_target->get_native_handle() == nullptr) { + return PlayFabParty::CHAT_PERMISSION_NONE; + } + Party::PartyChatPermissionOptions native = Party::PartyChatPermissionOptions::None; + PartyError err = static_cast(m_native_handle)->GetPermissions(p_target->get_native_handle(), &native); + if (PARTY_FAILED(err)) { + return PlayFabParty::CHAT_PERMISSION_NONE; + } + return m_owner->_translate_chat_permissions_from_native(static_cast(native)); +} + +bool PlayFabPartyChatControl::is_audio_muted(const Ref &p_target) const { + if (!m_local || m_native_handle == nullptr || !p_target.is_valid() || p_target->get_native_handle() == nullptr) { + return false; + } + PartyBool muted = 0; + PartyError err = static_cast(m_native_handle)->GetIncomingAudioMuted(p_target->get_native_handle(), &muted); + return PARTY_FAILED(err) ? false : muted != 0; +} + +bool PlayFabPartyChatControl::is_text_muted(const Ref &p_target) const { + if (!m_local || m_native_handle == nullptr || !p_target.is_valid() || p_target->get_native_handle() == nullptr) { + return false; + } + PartyBool muted = 0; + PartyError err = static_cast(m_native_handle)->GetIncomingTextMuted(p_target->get_native_handle(), &muted); + return PARTY_FAILED(err) ? false : muted != 0; +} + +// --- Text to speech --------------------------------------------------------- + +Signal PlayFabPartyChatControl::populate_text_to_speech_profiles_async() { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.populate_text_to_speech_profiles_async requires a local chat control."); + } + Party::PartyLocalChatControl *local = static_cast(m_native_handle); + return m_owner->_dispatch_chat_control_operation( + local, + PlayFabParty::PENDING_POPULATE_TEXT_TO_SPEECH_PROFILES, + PARTY_CHAT_CONTROL_CREATE_FAILED, + "PartyLocalChatControl::PopulateAvailableTextToSpeechProfiles", + [local](void *p_async) { return local->PopulateAvailableTextToSpeechProfiles(p_async); }); +} + +Array PlayFabPartyChatControl::get_text_to_speech_profiles() const { + return m_text_to_speech_profiles.duplicate(); +} + +Ref PlayFabPartyChatControl::get_text_to_speech_profile(int64_t p_type) const { + if (!m_local || m_native_handle == nullptr) { + return Ref(); + } + Party::PartyTextToSpeechProfile *profile = nullptr; + PartyError err = static_cast(m_native_handle) + ->GetTextToSpeechProfile(static_cast(p_type), &profile); + if (PARTY_FAILED(err) || profile == nullptr) { + return Ref(); + } + return text_to_speech_profile_snapshot(profile); +} + +Signal PlayFabPartyChatControl::set_text_to_speech_profile_async(int64_t p_type, const String &p_profile_id) { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.set_text_to_speech_profile_async requires a local chat control."); + } + if (p_profile_id.is_empty()) { + return detached_error_signal(E_INVALIDARG, PARTY_INVALID_OPTIONS, + "PlayFabPartyChatControl.set_text_to_speech_profile_async requires a profile identifier."); + } + Party::PartyLocalChatControl *local = static_cast(m_native_handle); + const auto type = static_cast(p_type); + const CharString profile_utf8 = p_profile_id.utf8(); + return m_owner->_dispatch_chat_control_operation( + local, + PlayFabParty::PENDING_SET_TEXT_TO_SPEECH_PROFILE, + PARTY_CHAT_CONTROL_CREATE_FAILED, + "PartyLocalChatControl::SetTextToSpeechProfile", + [local, type, profile_utf8](void *p_async) { return local->SetTextToSpeechProfile(type, profile_utf8.get_data(), p_async); }); +} + +Signal PlayFabPartyChatControl::synthesize_text_to_speech_async(int64_t p_type, const String &p_text) { + if (m_owner == nullptr || m_native_handle == nullptr || !m_local) { + return detached_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_CONTROL_CREATE_FAILED, + "PlayFabPartyChatControl.synthesize_text_to_speech_async requires a local chat control."); + } + if (p_text.is_empty()) { + return detached_error_signal(E_INVALIDARG, PARTY_INVALID_OPTIONS, + "PlayFabPartyChatControl.synthesize_text_to_speech_async requires non-empty text."); + } + Party::PartyLocalChatControl *local = static_cast(m_native_handle); + const auto type = static_cast(p_type); + const CharString text_utf8 = p_text.utf8(); + return m_owner->_dispatch_chat_control_operation( + local, + PlayFabParty::PENDING_SYNTHESIZE_TEXT_TO_SPEECH, + PARTY_CHAT_CONTROL_CREATE_FAILED, + "PartyLocalChatControl::SynthesizeTextToSpeech", + [local, type, text_utf8](void *p_async) { return local->SynthesizeTextToSpeech(type, text_utf8.get_data(), p_async); }); +} + // --------------------------------------------------------------------------- // PlayFabPartyChat @@ -485,6 +919,34 @@ void PlayFabPartyChat::_bind_methods() { DEFVAL(Ref())); ClassDB::bind_method(D_METHOD("destroy_local_chat_control_async", "user"), &PlayFabPartyChat::destroy_local_chat_control_async); + ClassDB::bind_method(D_METHOD("get_local_chat_indicator", "user"), &PlayFabPartyChat::get_local_chat_indicator, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_chat_indicator", "entity_key", "user"), &PlayFabPartyChat::get_chat_indicator, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_chat_indicators", "user"), &PlayFabPartyChat::get_chat_indicators, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("is_audio_input_muted", "user"), &PlayFabPartyChat::is_audio_input_muted, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_audio_input_muted_async", "muted", "user"), &PlayFabPartyChat::set_audio_input_muted_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_audio_render_volume", "entity_key", "user"), &PlayFabPartyChat::get_audio_render_volume, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_audio_render_volume_async", "entity_key", "volume", "user"), &PlayFabPartyChat::set_audio_render_volume_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_chat_permissions", "entity_key", "user"), &PlayFabPartyChat::get_chat_permissions, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("is_audio_muted", "entity_key", "user"), &PlayFabPartyChat::is_audio_muted, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("is_text_muted", "entity_key", "user"), &PlayFabPartyChat::is_text_muted, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_language", "user"), &PlayFabPartyChat::get_language, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_language_async", "language_code", "user"), &PlayFabPartyChat::set_language_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_transcription_options", "user"), &PlayFabPartyChat::get_transcription_options, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_transcription_options_async", "options", "user"), &PlayFabPartyChat::set_transcription_options_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_text_chat_options", "user"), &PlayFabPartyChat::get_text_chat_options, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_text_chat_options_async", "options", "user"), &PlayFabPartyChat::set_text_chat_options_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_audio_encoder_bitrate", "user"), &PlayFabPartyChat::get_audio_encoder_bitrate, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_audio_encoder_bitrate_async", "bitrate", "user"), &PlayFabPartyChat::set_audio_encoder_bitrate_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_voice_audio_options", "user"), &PlayFabPartyChat::get_voice_audio_options, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_voice_audio_options_async", "options", "user"), &PlayFabPartyChat::set_voice_audio_options_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_audio_input_state", "user"), &PlayFabPartyChat::get_audio_input_state, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_audio_output_state", "user"), &PlayFabPartyChat::get_audio_output_state, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("populate_text_to_speech_profiles_async", "user"), &PlayFabPartyChat::populate_text_to_speech_profiles_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_text_to_speech_profiles", "user"), &PlayFabPartyChat::get_text_to_speech_profiles, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("get_text_to_speech_profile", "type", "user"), &PlayFabPartyChat::get_text_to_speech_profile, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("set_text_to_speech_profile_async", "type", "profile_id", "user"), &PlayFabPartyChat::set_text_to_speech_profile_async, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("synthesize_text_to_speech_async", "type", "text", "user"), &PlayFabPartyChat::synthesize_text_to_speech_async, DEFVAL(Ref())); + ADD_SIGNAL(MethodInfo("state_changed", PropertyInfo(Variant::OBJECT, "change", PROPERTY_HINT_RESOURCE_TYPE, "PlayFabPartyChatStateChange"))); ADD_SIGNAL(MethodInfo("chat_control_added", PropertyInfo(Variant::DICTIONARY, "entity_key"), PropertyInfo(Variant::OBJECT, "chat_control", PROPERTY_HINT_RESOURCE_TYPE, "PlayFabPartyChatControl"))); ADD_SIGNAL(MethodInfo("chat_control_removed", PropertyInfo(Variant::DICTIONARY, "entity_key"))); @@ -679,6 +1141,200 @@ Signal PlayFabPartyChat::destroy_local_chat_control_async(const Ref return m_owner->_destroy_local_chat_control(p_user); } +Ref PlayFabPartyChat::_resolve_local_control(const Ref &p_user) const { + if (p_user.is_valid()) { + return get_local_chat_control(p_user); + } + for (int i = 0; i < m_chat_controls.size(); ++i) { + Ref control = m_chat_controls[i]; + if (control.is_valid() && control->is_local() && control->get_native_handle() != nullptr) { + return control; + } + } + return Ref(); +} + +// Shared guard for the per-user convenience wrappers below: every one of them +// needs a local chat control, and the caller should see the same +// party_resource_not_ready failure when none exists yet. +#define PF_PARTY_CHAT_REQUIRE_LOCAL(m_method_name) \ + Ref local = _resolve_local_control(p_user); \ + if (local.is_null()) { \ + return detached_error_signal(E_NOT_VALID_STATE, PARTY_RESOURCE_NOT_READY, \ + "PlayFabPartyChat." m_method_name "() requires a local chat control; call " \ + "create_local_chat_control_async() first."); \ + } + +int64_t PlayFabPartyChat::get_local_chat_indicator(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_local_chat_indicator() : int64_t(PlayFabParty::LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT); +} + +int64_t PlayFabPartyChat::get_chat_indicator(const Dictionary &p_entity_key, const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + Ref target = get_chat_control(p_entity_key); + if (local.is_null() || target.is_null() || target->get_native_handle() == nullptr) { + return PlayFabParty::CHAT_INDICATOR_SILENT; + } + if (target->is_local()) { + // A local control never has an incoming-audio indicator; report what its + // own capture pipeline is doing so a roster row for the local player + // still lights up while talking. + const int64_t local_indicator = target->get_local_chat_indicator(); + return local_indicator == PlayFabParty::LOCAL_CHAT_INDICATOR_TALKING + ? int64_t(PlayFabParty::CHAT_INDICATOR_TALKING) + : int64_t(PlayFabParty::CHAT_INDICATOR_SILENT); + } + return target->get_chat_indicator(); +} + +Array PlayFabPartyChat::get_chat_indicators(const Ref &p_user) const { + Array indicators; + Ref local = _resolve_local_control(p_user); + if (local.is_null()) { + return indicators; + } + for (int i = 0; i < m_chat_controls.size(); ++i) { + Ref control = m_chat_controls[i]; + if (control.is_null() || control->is_local() || control->get_native_handle() == nullptr) { + continue; + } + Dictionary entry; + entry["entity_key"] = entity_key_for_chat_control(control->get_native_handle()); + entry["indicator"] = control->get_chat_indicator(); + indicators.push_back(entry); + } + return indicators; +} + +bool PlayFabPartyChat::is_audio_input_muted(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() && local->is_audio_input_muted(); +} + +Signal PlayFabPartyChat::set_audio_input_muted_async(bool p_muted, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_audio_input_muted_async") + return local->set_audio_input_muted_async(p_muted); +} + +float PlayFabPartyChat::get_audio_render_volume(const Dictionary &p_entity_key, const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + Ref target = get_chat_control(p_entity_key); + return local.is_valid() ? local->get_audio_render_volume(target) : 0.0f; +} + +Signal PlayFabPartyChat::set_audio_render_volume_async(const Dictionary &p_entity_key, double p_volume, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_audio_render_volume_async") + Ref target = get_chat_control(p_entity_key); + if (target.is_null() || target->get_native_handle() == nullptr) { + return detached_error_signal(E_INVALIDARG, PARTY_PEER_NOT_CONNECTED, + String("PlayFabPartyChat.set_audio_render_volume_async() unknown entity key ") + String(p_entity_key.get("id", "")) + "."); + } + return local->set_audio_render_volume_async(target, p_volume); +} + +int64_t PlayFabPartyChat::get_chat_permissions(const Dictionary &p_entity_key, const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_permissions(get_chat_control(p_entity_key)) : int64_t(PlayFabParty::CHAT_PERMISSION_NONE); +} + +bool PlayFabPartyChat::is_audio_muted(const Dictionary &p_entity_key, const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() && local->is_audio_muted(get_chat_control(p_entity_key)); +} + +bool PlayFabPartyChat::is_text_muted(const Dictionary &p_entity_key, const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() && local->is_text_muted(get_chat_control(p_entity_key)); +} + +String PlayFabPartyChat::get_language(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_language() : String(); +} + +Signal PlayFabPartyChat::set_language_async(const String &p_language_code, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_language_async") + return local->set_language_async(p_language_code); +} + +int64_t PlayFabPartyChat::get_transcription_options(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_transcription_options() : int64_t(PlayFabParty::TRANSCRIPTION_OPTION_NONE); +} + +Signal PlayFabPartyChat::set_transcription_options_async(int64_t p_options, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_transcription_options_async") + return local->set_transcription_options_async(p_options); +} + +int64_t PlayFabPartyChat::get_text_chat_options(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_text_chat_options() : int64_t(PlayFabParty::TEXT_CHAT_OPTION_NONE); +} + +Signal PlayFabPartyChat::set_text_chat_options_async(int64_t p_options, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_text_chat_options_async") + return local->set_text_chat_options_async(p_options); +} + +int64_t PlayFabPartyChat::get_audio_encoder_bitrate(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_audio_encoder_bitrate() : 0; +} + +Signal PlayFabPartyChat::set_audio_encoder_bitrate_async(int64_t p_bitrate, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_audio_encoder_bitrate_async") + return local->set_audio_encoder_bitrate_async(p_bitrate); +} + +int64_t PlayFabPartyChat::get_voice_audio_options(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_voice_audio_options() : int64_t(PlayFabParty::VOICE_AUDIO_OPTION_NONE); +} + +Signal PlayFabPartyChat::set_voice_audio_options_async(int64_t p_options, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_voice_audio_options_async") + return local->set_voice_audio_options_async(p_options); +} + +int64_t PlayFabPartyChat::get_audio_input_state(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_audio_input_state() : int64_t(PlayFabParty::AUDIO_INPUT_STATE_NO_INPUT); +} + +int64_t PlayFabPartyChat::get_audio_output_state(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_audio_output_state() : int64_t(PlayFabParty::AUDIO_OUTPUT_STATE_NO_OUTPUT); +} + +Signal PlayFabPartyChat::populate_text_to_speech_profiles_async(const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("populate_text_to_speech_profiles_async") + return local->populate_text_to_speech_profiles_async(); +} + +Array PlayFabPartyChat::get_text_to_speech_profiles(const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_text_to_speech_profiles() : Array(); +} + +Ref PlayFabPartyChat::get_text_to_speech_profile(int64_t p_type, const Ref &p_user) const { + Ref local = _resolve_local_control(p_user); + return local.is_valid() ? local->get_text_to_speech_profile(p_type) : Ref(); +} + +Signal PlayFabPartyChat::set_text_to_speech_profile_async(int64_t p_type, const String &p_profile_id, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("set_text_to_speech_profile_async") + return local->set_text_to_speech_profile_async(p_type, p_profile_id); +} + +Signal PlayFabPartyChat::synthesize_text_to_speech_async(int64_t p_type, const String &p_text, const Ref &p_user) { + PF_PARTY_CHAT_REQUIRE_LOCAL("synthesize_text_to_speech_async") + return local->synthesize_text_to_speech_async(p_type, p_text); +} + +#undef PF_PARTY_CHAT_REQUIRE_LOCAL + // --------------------------------------------------------------------------- // PlayFabPartyNetworkStateChange @@ -736,6 +1392,8 @@ void PlayFabPartyNetwork::_bind_methods() { ClassDB::bind_method(D_METHOD("get_local_peer"), &PlayFabPartyNetwork::get_local_peer); ClassDB::bind_method(D_METHOD("get_local_chat_control"), &PlayFabPartyNetwork::get_local_chat_control); ClassDB::bind_method(D_METHOD("is_host_network"), &PlayFabPartyNetwork::is_host_network); + ClassDB::bind_method(D_METHOD("get_statistics", "statistics"), &PlayFabPartyNetwork::get_statistics, DEFVAL(PackedInt32Array())); + ClassDB::bind_method(D_METHOD("get_device_connection_type", "peer_id"), &PlayFabPartyNetwork::get_device_connection_type); ClassDB::bind_method(D_METHOD("leave_async"), &PlayFabPartyNetwork::leave_async); ADD_PROPERTY(PropertyInfo(Variant::STRING, "network_id", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE), "", "get_network_id"); @@ -804,6 +1462,95 @@ Ref PlayFabPartyNetwork::get_local_peer() const { return m_loc Ref PlayFabPartyNetwork::get_local_chat_control() const { return m_local_chat_control; } bool PlayFabPartyNetwork::is_host_network() const { return m_host; } +Dictionary PlayFabPartyNetwork::get_statistics(const PackedInt32Array &p_statistics) const { + // Keys mirror PartyNetworkStatistic so callers can index the result with the + // PlayFabParty.NETWORK_STATISTIC_* enum name in lower snake case. + static const struct { + Party::PartyNetworkStatistic statistic; + const char *key; + } k_statistics[] = { + { Party::PartyNetworkStatistic::AverageRelayServerRoundTripLatencyInMilliseconds, "average_relay_server_round_trip_latency_ms" }, + { Party::PartyNetworkStatistic::SentProtocolPackets, "sent_protocol_packets" }, + { Party::PartyNetworkStatistic::SentProtocolBytes, "sent_protocol_bytes" }, + { Party::PartyNetworkStatistic::RetriedProtocolPackets, "retried_protocol_packets" }, + { Party::PartyNetworkStatistic::RetriedProtocolBytes, "retried_protocol_bytes" }, + { Party::PartyNetworkStatistic::DroppedProtocolPackets, "dropped_protocol_packets" }, + { Party::PartyNetworkStatistic::ReceivedProtocolPackets, "received_protocol_packets" }, + { Party::PartyNetworkStatistic::ReceivedProtocolBytes, "received_protocol_bytes" }, + { Party::PartyNetworkStatistic::CurrentlyQueuedSendMessages, "currently_queued_send_messages" }, + { Party::PartyNetworkStatistic::CurrentlyQueuedSendMessageBytes, "currently_queued_send_message_bytes" }, + { Party::PartyNetworkStatistic::CurrentlyActiveSendMessages, "currently_active_send_messages" }, + { Party::PartyNetworkStatistic::CurrentlyActiveSendMessageBytes, "currently_active_send_message_bytes" }, + { Party::PartyNetworkStatistic::TimedOutSendMessages, "timed_out_send_messages" }, + { Party::PartyNetworkStatistic::TimedOutSendMessageBytes, "timed_out_send_message_bytes" }, + { Party::PartyNetworkStatistic::CanceledSendMessages, "canceled_send_messages" }, + { Party::PartyNetworkStatistic::CanceledSendMessageBytes, "canceled_send_message_bytes" }, + }; + constexpr int k_statistic_count = static_cast(sizeof(k_statistics) / sizeof(k_statistics[0])); + + Dictionary statistics; + if (m_native_network == nullptr) { + return statistics; + } + + std::vector requested; + std::vector keys; + if (p_statistics.is_empty()) { + requested.reserve(k_statistic_count); + keys.reserve(k_statistic_count); + for (int i = 0; i < k_statistic_count; ++i) { + requested.push_back(k_statistics[i].statistic); + keys.push_back(k_statistics[i].key); + } + } else { + for (int i = 0; i < p_statistics.size(); ++i) { + const int32_t value = p_statistics[i]; + if (value < 0 || value >= k_statistic_count) { + ERR_PRINT(vformat("PlayFabPartyNetwork.get_statistics() ignoring unknown statistic %d.", value)); + continue; + } + requested.push_back(k_statistics[value].statistic); + keys.push_back(k_statistics[value].key); + } + } + if (requested.empty()) { + return statistics; + } + + std::vector values(requested.size(), 0); + const PartyError error = m_native_network->GetNetworkStatistics( + static_cast(requested.size()), requested.data(), values.data()); + if (PARTY_FAILED(error)) { + ERR_PRINT(vformat("PlayFabPartyNetwork.get_statistics() failed with Party error 0x%x.", static_cast(error))); + return statistics; + } + for (size_t i = 0; i < requested.size(); ++i) { + statistics[String(keys[i])] = static_cast(values[i]); + } + return statistics; +} + +int64_t PlayFabPartyNetwork::get_device_connection_type(int64_t p_peer_id) const { + if (m_native_network == nullptr || m_local_peer.is_null()) { + return PlayFabParty::DEVICE_CONNECTION_TYPE_RELAY_SERVER; + } + Party::PartyEndpoint *endpoint = m_local_peer->get_peer_endpoint(static_cast(p_peer_id)); + if (endpoint == nullptr) { + return PlayFabParty::DEVICE_CONNECTION_TYPE_RELAY_SERVER; + } + Party::PartyDevice *device = nullptr; + PartyError error = endpoint->GetDevice(&device); + if (PARTY_FAILED(error) || device == nullptr) { + return PlayFabParty::DEVICE_CONNECTION_TYPE_RELAY_SERVER; + } + Party::PartyDeviceConnectionType connection_type = Party::PartyDeviceConnectionType::RelayServer; + error = m_native_network->GetDeviceConnectionType(device, &connection_type); + if (PARTY_FAILED(error)) { + return PlayFabParty::DEVICE_CONNECTION_TYPE_RELAY_SERVER; + } + return static_cast(connection_type); +} + Signal PlayFabPartyNetwork::leave_async() { if (m_owner == nullptr) { return detached_error_signal(E_NOT_VALID_STATE, PARTY_RESOURCE_NOT_READY, @@ -2125,6 +2872,27 @@ void PlayFabParty::_process_state_change(const Party::PartyStateChange *p_change case Party::PartyStateChangeType::SetChatAudioOutputCompleted: _process_set_chat_audio_output_completed(p_change); break; + case Party::PartyStateChangeType::SetLanguageCompleted: + _process_set_language_completed(p_change); + break; + case Party::PartyStateChangeType::SetTranscriptionOptionsCompleted: + _process_set_transcription_options_completed(p_change); + break; + case Party::PartyStateChangeType::SetTextChatOptionsCompleted: + _process_set_text_chat_options_completed(p_change); + break; + case Party::PartyStateChangeType::SetChatAudioEncoderBitrateCompleted: + _process_set_chat_audio_encoder_bitrate_completed(p_change); + break; + case Party::PartyStateChangeType::PopulateAvailableTextToSpeechProfilesCompleted: + _process_populate_text_to_speech_profiles_completed(p_change); + break; + case Party::PartyStateChangeType::SetTextToSpeechProfileCompleted: + _process_set_text_to_speech_profile_completed(p_change); + break; + case Party::PartyStateChangeType::SynthesizeTextToSpeechCompleted: + _process_synthesize_text_to_speech_completed(p_change); + break; case Party::PartyStateChangeType::LocalChatAudioInputChanged: _process_local_chat_audio_input_changed(p_change); break; @@ -2677,10 +3445,71 @@ void PlayFabParty::_process_create_chat_control_completed(const Party::PartyStat // the device just sits unused; binding it costs nothing and removes a silent // failure mode where voice was implicitly disabled. _configure_chat_audio_devices(wrapper, change->localChatControl, operation->config); + // Apply the config's language / transcription / translation intent. Without + // this the enable_transcription and enable_translation flags are inert: + // Party never generates transcriptions or translations unless + // SetTranscriptionOptions()/SetTextChatOptions() are called explicitly. + _configure_chat_language_options(change->localChatControl, operation->config); _complete_pending(operation, PlayFabResult::ok_result(wrapper)); } +void PlayFabParty::_configure_chat_language_options(Party::PartyLocalChatControl *p_chat_control, const Ref &p_config) { + if (p_chat_control == nullptr) { + return; + } + + // Language. Empty => leave the Party default (the platform's language), + // which is what GetLanguage() reports back. + const String language = p_config.is_valid() ? p_config->get_language() : String(); + if (!language.is_empty()) { + const CharString language_utf8 = language.utf8(); + const PartyError err = p_chat_control->SetLanguage(language_utf8.get_data(), nullptr); + if (PARTY_FAILED(err)) { + Ref result = _party_error_result(err, PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetLanguage"); + WARN_PRINT(vformat("PlayFab.party: SetLanguage(\"%s\") failed; the chat control keeps its default language. %s", + language, result.is_valid() ? result->get_message() : String())); + } + } + + const bool transcription = p_config.is_valid() && p_config->is_transcription_enabled(); + const bool translation = p_config.is_valid() && p_config->is_translation_enabled(); + + // Voice transcription. Transcribe every chat control whose language matches + // and doesn't match the local one, so the title receives captions for all + // participants, plus the local user's own speech. + Party::PartyVoiceChatTranscriptionOptions transcription_options = Party::PartyVoiceChatTranscriptionOptions::None; + if (transcription) { + transcription_options = Party::PartyVoiceChatTranscriptionOptions::TranscribeSelf | + Party::PartyVoiceChatTranscriptionOptions::TranscribeOtherChatControlsWithMatchingLanguages | + Party::PartyVoiceChatTranscriptionOptions::TranscribeOtherChatControlsWithNonMatchingLanguages; + if (translation) { + transcription_options = transcription_options | + Party::PartyVoiceChatTranscriptionOptions::TranslateToLocalLanguage; + } + } + if (transcription_options != Party::PartyVoiceChatTranscriptionOptions::None) { + const PartyError err = p_chat_control->SetTranscriptionOptions(transcription_options, nullptr); + if (PARTY_FAILED(err)) { + Ref result = _party_error_result(err, PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetTranscriptionOptions"); + WARN_PRINT(vformat("PlayFab.party: SetTranscriptionOptions failed; voice transcription stays disabled. %s", + result.is_valid() ? result->get_message() : String())); + } + } + + // Text chat translation is receiver-side: it asks Party to translate + // incoming chat text into this control's language. + if (translation) { + const PartyError err = p_chat_control->SetTextChatOptions( + Party::PartyTextChatOptions::TranslateToLocalLanguage, nullptr); + if (PARTY_FAILED(err)) { + Ref result = _party_error_result(err, PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetTextChatOptions"); + WARN_PRINT(vformat("PlayFab.party: SetTextChatOptions failed; incoming text chat will not be translated. %s", + result.is_valid() ? result->get_message() : String())); + } + } +} + void PlayFabParty::_process_connect_chat_control_completed(const Party::PartyStateChange *p_change) { const auto *change = static_cast(p_change); PendingOperation *operation = static_cast(change->asyncIdentifier); @@ -2779,8 +3608,77 @@ void PlayFabParty::_process_set_chat_audio_output_completed(const Party::PartySt result.is_valid() ? result->get_message() : String("SetAudioOutput completion failed."))); } +void PlayFabParty::_process_set_language_completed(const Party::PartyStateChange *p_change) { + const auto *change = static_cast(p_change); + _complete_chat_control_operation(change->asyncIdentifier, static_cast(change->result), change->errorDetail, + PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetLanguage"); +} + +void PlayFabParty::_process_set_transcription_options_completed(const Party::PartyStateChange *p_change) { + const auto *change = static_cast(p_change); + _complete_chat_control_operation(change->asyncIdentifier, static_cast(change->result), change->errorDetail, + PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetTranscriptionOptions"); +} + +void PlayFabParty::_process_set_text_chat_options_completed(const Party::PartyStateChange *p_change) { + const auto *change = static_cast(p_change); + _complete_chat_control_operation(change->asyncIdentifier, static_cast(change->result), change->errorDetail, + PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetTextChatOptions"); +} + +void PlayFabParty::_process_set_chat_audio_encoder_bitrate_completed(const Party::PartyStateChange *p_change) { + const auto *change = static_cast(p_change); + _complete_chat_control_operation(change->asyncIdentifier, static_cast(change->result), change->errorDetail, + PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetAudioEncoderBitrate"); +} + +void PlayFabParty::_process_populate_text_to_speech_profiles_completed(const Party::PartyStateChange *p_change) { + const auto *change = static_cast(p_change); + // Snapshot the profiles now: the SDK owns the PartyTextToSpeechProfile + // pointers and invalidates them on the next populate call, so nothing may + // hold them past this handler. + if (change->result == Party::PartyStateChangeResult::Succeeded && change->localChatControl != nullptr) { + Ref wrapper = _find_chat_control_wrapper(change->localChatControl); + if (wrapper.is_valid()) { + uint32_t count = 0; + Party::PartyTextToSpeechProfileArray profiles = nullptr; + Array snapshots; + if (!PARTY_FAILED(change->localChatControl->GetAvailableTextToSpeechProfiles(&count, &profiles))) { + for (uint32_t i = 0; i < count; ++i) { + Ref profile = text_to_speech_profile_snapshot(profiles[i]); + if (profile.is_valid()) { + snapshots.push_back(profile); + } + } + } + wrapper->m_text_to_speech_profiles = snapshots; + } + } + _complete_chat_control_operation(change->asyncIdentifier, static_cast(change->result), change->errorDetail, + PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::PopulateAvailableTextToSpeechProfiles"); +} + +void PlayFabParty::_process_set_text_to_speech_profile_completed(const Party::PartyStateChange *p_change) { + const auto *change = static_cast(p_change); + _complete_chat_control_operation(change->asyncIdentifier, static_cast(change->result), change->errorDetail, + PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SetTextToSpeechProfile"); +} + +void PlayFabParty::_process_synthesize_text_to_speech_completed(const Party::PartyStateChange *p_change) { + const auto *change = static_cast(p_change); + _complete_chat_control_operation(change->asyncIdentifier, static_cast(change->result), change->errorDetail, + PARTY_CHAT_CONTROL_CREATE_FAILED, "PartyLocalChatControl::SynthesizeTextToSpeech"); +} + void PlayFabParty::_process_local_chat_audio_input_changed(const Party::PartyStateChange *p_change) { const auto *change = static_cast(p_change); + // Cache the device state so titles can poll it via + // PlayFabPartyChat.get_audio_input_state() and show a "no microphone" / + // "microphone access denied" affordance instead of silently having no voice. + Ref wrapper = _find_chat_control_wrapper(change->localChatControl); + if (wrapper.is_valid()) { + wrapper->m_audio_input_state = static_cast(change->state); + } // SetAudioInput only signals API dispatch success. The audio subsystem // initializing the requested device is reported by this state change. // Anything other than Initialized means voice capture is degraded — log @@ -2807,6 +3705,10 @@ void PlayFabParty::_process_local_chat_audio_input_changed(const Party::PartySta void PlayFabParty::_process_local_chat_audio_output_changed(const Party::PartyStateChange *p_change) { const auto *change = static_cast(p_change); + Ref wrapper = _find_chat_control_wrapper(change->localChatControl); + if (wrapper.is_valid()) { + wrapper->m_audio_output_state = static_cast(change->state); + } if (change->state == Party::PartyAudioOutputState::Initialized) { return; } @@ -2925,12 +3827,31 @@ void PlayFabParty::_process_chat_text_received(const Party::PartyStateChange *p_ } String text = String::utf8(change->chatText != nullptr ? change->chatText : ""); String language = String::utf8(change->languageCode != nullptr ? change->languageCode : ""); + String original = String::utf8(change->originalChatText != nullptr ? change->originalChatText : ""); String translated = text; if (change->translationCount > 0 && change->translations != nullptr && change->translations[0].translation != nullptr) { translated = String::utf8(change->translations[0].translation); } + // Metadata sent alongside the message arrives as one opaque data blob. + // These bytes come from a remote peer, so decode with bytes_to_var(), which + // never instantiates objects. Do NOT switch to bytes_to_var_with_objects(): + // that is the object-allowing variant and would make a remote peer able to + // instantiate script objects on this client. + Dictionary metadata; + if (change->dataSize > 0 && change->data != nullptr) { + PackedByteArray bytes; + bytes.resize(static_cast(change->dataSize)); + memcpy(bytes.ptrw(), change->data, change->dataSize); + const Variant decoded = UtilityFunctions::bytes_to_var(bytes); + if (decoded.get_type() == Variant::DICTIONARY) { + metadata = decoded; + } else if (decoded.get_type() != Variant::NIL) { + WARN_PRINT("PlayFab.party: ignoring chat message metadata that did not decode to a Dictionary."); + } + } Dictionary sender_entity_key = entity_key_for_chat_control(change->senderChatControl); - message->set_values(sender_wrapper, sender_entity_key, targets, text, language, translated, false, 0, Dictionary()); + message->set_values(sender_wrapper, sender_entity_key, targets, text, language, translated, false, 0, metadata, + original, static_cast(change->options)); if (sender_wrapper.is_valid()) { sender_wrapper->emit_signal("message_received", message); } @@ -2957,8 +3878,15 @@ void PlayFabParty::_process_voice_chat_transcription_received(const Party::Party Array targets; String text = String::utf8(change->transcription != nullptr ? change->transcription : ""); String language = String::utf8(change->languageCode != nullptr ? change->languageCode : ""); + // Transcriptions carry translations only when the local control opted into + // TRANSCRIPTION_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE; otherwise translated_text + // mirrors text. + String translated = text; + if (change->translationCount > 0 && change->translations != nullptr && change->translations[0].translation != nullptr) { + translated = String::utf8(change->translations[0].translation); + } Dictionary sender_entity_key = entity_key_for_chat_control(change->senderChatControl); - message->set_values(sender_wrapper, sender_entity_key, targets, text, language, text, true, 0, Dictionary()); + message->set_values(sender_wrapper, sender_entity_key, targets, text, language, translated, true, 0, Dictionary()); if (sender_wrapper.is_valid()) { sender_wrapper->emit_signal("transcription_received", message); } @@ -2986,12 +3914,27 @@ Signal PlayFabParty::_send_text_via_chat_control(Party::PartyLocalChatControl *p return _make_ok_signal(); } const CharString text_utf8 = p_message.utf8(); + // Metadata rides along as a single Party data buffer. Party delivers data + // buffers verbatim to receivers; PlayFabPartyChatMessage.metadata on the + // receiving side is decoded from it. + PackedByteArray metadata_bytes; + const Dictionary metadata = p_config.is_valid() ? p_config->get_metadata() : Dictionary(); + if (!metadata.is_empty()) { + metadata_bytes = UtilityFunctions::var_to_bytes(metadata); + } + Party::PartyDataBuffer buffer = {}; + uint32_t buffer_count = 0; + if (!metadata_bytes.is_empty()) { + buffer.buffer = metadata_bytes.ptr(); + buffer.bufferByteCount = static_cast(metadata_bytes.size()); + buffer_count = 1; + } PartyError err = p_local_chat_control->SendText( static_cast(p_targets.size()), const_cast(p_targets.data()), text_utf8.get_data(), - 0, - nullptr); + buffer_count, + buffer_count > 0 ? &buffer : nullptr); if (PARTY_FAILED(err)) { Ref result = _party_error_result(err, PARTY_CHAT_PERMISSION_FAILED, "PartyLocalChatControl::SendText"); return _make_error_signal(E_FAIL, PARTY_CHAT_PERMISSION_FAILED, result.is_valid() ? result->get_message() : String("PartyLocalChatControl::SendText failed.")); @@ -3069,6 +4012,130 @@ Signal PlayFabParty::_set_incoming_text_muted(Party::PartyLocalChatControl *p_lo return _make_ok_signal(); } +Signal PlayFabParty::_set_audio_input_muted(Party::PartyLocalChatControl *p_local_chat_control, bool p_muted) { + if (m_shutting_down) { + return _make_error_signal(E_ABORT, PARTY_SHUTTING_DOWN, + "PlayFab.party chat mute operations cannot start while PlayFab Party is shutting down."); + } + if (p_local_chat_control == nullptr) { + return _make_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_PERMISSION_FAILED, + "PlayFab.party._set_audio_input_muted() requires a local chat control."); + } + PartyError err = p_local_chat_control->SetAudioInputMuted(p_muted ? PartyBool(1) : PartyBool(0)); + if (PARTY_FAILED(err)) { + Ref result = _party_error_result(err, PARTY_CHAT_PERMISSION_FAILED, "PartyLocalChatControl::SetAudioInputMuted"); + return _make_error_signal(E_FAIL, PARTY_CHAT_PERMISSION_FAILED, result.is_valid() ? result->get_message() : String("PartyLocalChatControl::SetAudioInputMuted failed.")); + } + return _make_ok_signal(); +} + +Signal PlayFabParty::_set_audio_render_volume(Party::PartyLocalChatControl *p_local_chat_control, Party::PartyChatControl *p_target, double p_volume) { + if (m_shutting_down) { + return _make_error_signal(E_ABORT, PARTY_SHUTTING_DOWN, + "PlayFab.party chat volume operations cannot start while PlayFab Party is shutting down."); + } + if (p_local_chat_control == nullptr || p_target == nullptr) { + return _make_error_signal(E_NOT_VALID_STATE, PARTY_CHAT_PERMISSION_FAILED, + "PlayFab.party._set_audio_render_volume() requires both a local chat control and a target."); + } + const float volume = static_cast(CLAMP(p_volume, 0.0, 1.0)); + PartyError err = p_local_chat_control->SetAudioRenderVolume(p_target, volume); + if (PARTY_FAILED(err)) { + Ref result = _party_error_result(err, PARTY_CHAT_PERMISSION_FAILED, "PartyLocalChatControl::SetAudioRenderVolume"); + return _make_error_signal(E_FAIL, PARTY_CHAT_PERMISSION_FAILED, result.is_valid() ? result->get_message() : String("PartyLocalChatControl::SetAudioRenderVolume failed.")); + } + return _make_ok_signal(); +} + +// Shared plumbing for the async PartyLocalChatControl setters (language, +// transcription options, text chat options, encoder bitrate, TTS). They all +// follow the same shape: create a pending op, hand its pointer to the SDK as the +// async identifier, and let the matching *CompletedStateChange resolve it. +Signal PlayFabParty::_dispatch_chat_control_operation( + Party::PartyLocalChatControl *p_chat_control, + int32_t p_kind, + const String &p_error_code, + const String &p_action, + const std::function &p_invoke) { + if (m_shutting_down) { + return _make_error_signal(E_ABORT, PARTY_SHUTTING_DOWN, + vformat("%s cannot start while PlayFab Party is shutting down.", p_action)); + } + if (p_chat_control == nullptr) { + return _make_error_signal(E_NOT_VALID_STATE, PARTY_RESOURCE_NOT_READY, + vformat("%s requires a local chat control.", p_action)); + } + PendingOperation *operation = _create_pending(p_kind); + operation->native_chat_control = p_chat_control; + const PartyError err = static_cast(p_invoke(operation)); + if (PARTY_FAILED(err)) { + Ref result = _party_error_result(err, p_error_code, p_action); + _complete_pending(operation, result); + return _make_error_signal(E_FAIL, p_error_code, + result.is_valid() ? result->get_message() : vformat("%s failed.", p_action)); + } + return operation->pending_signal->get_completed_signal(); +} + +void PlayFabParty::_complete_chat_control_operation( + void *p_async_identifier, + int64_t p_state_change_result, + uint32_t p_error_detail, + const String &p_error_code, + const String &p_action) { + PendingOperation *operation = static_cast(p_async_identifier); + if (operation == nullptr) { + return; + } + const auto result = static_cast(p_state_change_result); + if (result != Party::PartyStateChangeResult::Succeeded) { + _complete_pending(operation, _party_state_change_error_result(result, p_error_detail, p_error_code, p_action)); + return; + } + _complete_pending(operation, PlayFabResult::ok_result()); +} + +Ref PlayFabParty::_find_chat_control_wrapper(Party::PartyChatControl *p_native) const { + if (p_native == nullptr || m_chat.is_null()) { + return Ref(); + } + const Array controls = m_chat->get_chat_controls(); + for (int i = 0; i < controls.size(); ++i) { + Ref control = controls[i]; + if (control.is_valid() && control->get_native_handle() == p_native) { + return control; + } + } + return Ref(); +} + +Party::PartyLocalChatControl *PlayFabParty::_resolve_local_native_chat_control(const Ref &p_user) const { + // Since local chat controls can outlive (and predate) any network, prefer the + // service-level registry and only fall back to the per-network handles. + if (p_user.is_valid()) { + auto it = m_local_chat_controls.find(p_user->get_entity_handle()); + if (it != m_local_chat_controls.end() && it->second.is_valid() && it->second->get_native_handle() != nullptr) { + return static_cast(it->second->get_native_handle()); + } + } else { + for (const auto &entry : m_local_chat_controls) { + if (entry.second.is_valid() && entry.second->get_native_handle() != nullptr) { + return static_cast(entry.second->get_native_handle()); + } + } + } + for (size_t i = 0; i < m_networks.size(); ++i) { + const Ref &network = m_networks[i]; + if (network.is_null() || network->get_native_local_chat_control() == nullptr) { + continue; + } + if (p_user.is_null() || network->get_local_user() == p_user) { + return network->get_native_local_chat_control(); + } + } + return nullptr; +} + Signal PlayFabParty::_destroy_chat_control(const Ref &p_chat_control) { if (m_shutting_down) { return _make_error_signal(E_ABORT, PARTY_SHUTTING_DOWN, @@ -3636,6 +4703,86 @@ void PlayFabParty::_bind_methods() { BIND_ENUM_CONSTANT(CHAT_CHANGE_DESTROYED); BIND_ENUM_CONSTANT(CHAT_CHANGE_PERMISSIONS_CHANGED); BIND_ENUM_CONSTANT(CHAT_CHANGE_MUTED_CHANGED); + + BIND_ENUM_CONSTANT(LOCAL_CHAT_INDICATOR_SILENT); + BIND_ENUM_CONSTANT(LOCAL_CHAT_INDICATOR_TALKING); + BIND_ENUM_CONSTANT(LOCAL_CHAT_INDICATOR_AUDIO_INPUT_MUTED); + BIND_ENUM_CONSTANT(LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT); + + BIND_ENUM_CONSTANT(CHAT_INDICATOR_SILENT); + BIND_ENUM_CONSTANT(CHAT_INDICATOR_TALKING); + BIND_ENUM_CONSTANT(CHAT_INDICATOR_INCOMING_VOICE_DISABLED); + BIND_ENUM_CONSTANT(CHAT_INDICATOR_INCOMING_COMMUNICATIONS_MUTED); + BIND_ENUM_CONSTANT(CHAT_INDICATOR_NO_REMOTE_INPUT); + BIND_ENUM_CONSTANT(CHAT_INDICATOR_REMOTE_AUDIO_INPUT_MUTED); + + BIND_ENUM_CONSTANT(AUDIO_INPUT_STATE_NO_INPUT); + BIND_ENUM_CONSTANT(AUDIO_INPUT_STATE_INITIALIZED); + BIND_ENUM_CONSTANT(AUDIO_INPUT_STATE_NOT_FOUND); + BIND_ENUM_CONSTANT(AUDIO_INPUT_STATE_USER_CONSENT_DENIED); + BIND_ENUM_CONSTANT(AUDIO_INPUT_STATE_UNSUPPORTED_FORMAT); + BIND_ENUM_CONSTANT(AUDIO_INPUT_STATE_ALREADY_IN_USE); + BIND_ENUM_CONSTANT(AUDIO_INPUT_STATE_UNKNOWN_ERROR); + + BIND_ENUM_CONSTANT(AUDIO_OUTPUT_STATE_NO_OUTPUT); + BIND_ENUM_CONSTANT(AUDIO_OUTPUT_STATE_INITIALIZED); + BIND_ENUM_CONSTANT(AUDIO_OUTPUT_STATE_NOT_FOUND); + BIND_ENUM_CONSTANT(AUDIO_OUTPUT_STATE_UNSUPPORTED_FORMAT); + BIND_ENUM_CONSTANT(AUDIO_OUTPUT_STATE_ALREADY_IN_USE); + BIND_ENUM_CONSTANT(AUDIO_OUTPUT_STATE_UNKNOWN_ERROR); + + BIND_ENUM_CONSTANT(AUDIO_DEVICE_SELECTION_NONE); + BIND_ENUM_CONSTANT(AUDIO_DEVICE_SELECTION_SYSTEM_DEFAULT); + BIND_ENUM_CONSTANT(AUDIO_DEVICE_SELECTION_PLATFORM_USER_DEFAULT); + BIND_ENUM_CONSTANT(AUDIO_DEVICE_SELECTION_MANUAL); + + BIND_ENUM_CONSTANT(VOICE_AUDIO_OPTION_NONE); + BIND_ENUM_CONSTANT(VOICE_AUDIO_OPTION_NOISE_SUPPRESSION); + + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_NONE); + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_TRANSCRIBE_SELF); + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_TRANSCRIBE_MATCHING_LANGUAGES); + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_TRANSCRIBE_NON_MATCHING_LANGUAGES); + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_DISABLE_HYPOTHESIS_PHRASES); + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE); + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_DISABLE_PROFANITY_MASKING); + BIND_ENUM_CONSTANT(TRANSCRIPTION_OPTION_TRANSCRIBE_SELF_REGARDLESS_OF_NETWORK_STATE); + + BIND_ENUM_CONSTANT(TEXT_CHAT_OPTION_NONE); + BIND_ENUM_CONSTANT(TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE); + BIND_ENUM_CONSTANT(TEXT_CHAT_OPTION_FILTER_OFFENSIVE_TEXT); + + BIND_ENUM_CONSTANT(CHAT_MESSAGE_OPTION_NONE); + BIND_ENUM_CONSTANT(CHAT_MESSAGE_OPTION_FILTERED_OFFENSIVE_TERMS); + BIND_ENUM_CONSTANT(CHAT_MESSAGE_OPTION_FILTERED_ENTIRE_MESSAGE); + BIND_ENUM_CONSTANT(CHAT_MESSAGE_OPTION_FILTERED_DUE_TO_ERROR); + + BIND_ENUM_CONSTANT(TEXT_TO_SPEECH_TYPE_NARRATION); + BIND_ENUM_CONSTANT(TEXT_TO_SPEECH_TYPE_VOICE_CHAT); + + BIND_ENUM_CONSTANT(GENDER_NEUTRAL); + BIND_ENUM_CONSTANT(GENDER_FEMALE); + BIND_ENUM_CONSTANT(GENDER_MALE); + + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_AVERAGE_RELAY_SERVER_ROUND_TRIP_LATENCY_MS); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_SENT_PROTOCOL_PACKETS); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_SENT_PROTOCOL_BYTES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_RETRIED_PROTOCOL_PACKETS); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_RETRIED_PROTOCOL_BYTES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_DROPPED_PROTOCOL_PACKETS); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_RECEIVED_PROTOCOL_PACKETS); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_RECEIVED_PROTOCOL_BYTES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_CURRENTLY_QUEUED_SEND_MESSAGES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_CURRENTLY_QUEUED_SEND_MESSAGE_BYTES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_CURRENTLY_ACTIVE_SEND_MESSAGES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_CURRENTLY_ACTIVE_SEND_MESSAGE_BYTES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_TIMED_OUT_SEND_MESSAGES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_TIMED_OUT_SEND_MESSAGE_BYTES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_CANCELED_SEND_MESSAGES); + BIND_ENUM_CONSTANT(NETWORK_STATISTIC_CANCELED_SEND_MESSAGE_BYTES); + + BIND_ENUM_CONSTANT(DEVICE_CONNECTION_TYPE_RELAY_SERVER); + BIND_ENUM_CONSTANT(DEVICE_CONNECTION_TYPE_DIRECT_PEER_CONNECTION); } } // namespace godot \ No newline at end of file diff --git a/addons/godot_playfab/src/playfab_party.h b/addons/godot_playfab/src/playfab_party.h index 69a3eaa..64afaca 100644 --- a/addons/godot_playfab/src/playfab_party.h +++ b/addons/godot_playfab/src/playfab_party.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -49,6 +50,7 @@ class PlayFabPartyMember; class PlayFabPartyNetwork; class PlayFabPartyNetworkStateChange; class PlayFabPartyPeer; +class PlayFabPartyTextToSpeechProfile; class PlayFabPendingSignal; class PlayFabResult; class PlayFabRuntime; @@ -64,9 +66,9 @@ class PlayFabPartyConfig : public RefCounted { bool m_enable_text_chat = true; bool m_enable_transcription = false; bool m_enable_translation = false; + String m_language; String m_audio_input; String m_audio_output; - Dictionary m_metadata; protected: static void _bind_methods(); @@ -86,33 +88,56 @@ class PlayFabPartyConfig : public RefCounted { void set_transcription_enabled(bool p_enabled); bool is_translation_enabled() const; void set_translation_enabled(bool p_enabled); + String get_language() const; + void set_language(const String &p_language); String get_audio_input() const; void set_audio_input(const String &p_audio_input); String get_audio_output() const; void set_audio_output(const String &p_audio_output); - Dictionary get_metadata() const; - void set_metadata(const Dictionary &p_metadata); }; +// Per-message options for PlayFabPartyChat.send_text_async(). PlayFab Party has +// no per-message language or translation target: the sender's language comes +// from PartyLocalChatControl::SetLanguage() (PlayFabPartyChat.set_language_async) +// and translation is a *receiver* setting (set_text_chat_options_async with +// TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE). What Party does carry per +// message is an opaque application data blob, which is what `metadata` maps to. class PlayFabPartyTextMessageConfig : public RefCounted { GDCLASS(PlayFabPartyTextMessageConfig, RefCounted); - String m_language_code; - PackedStringArray m_translate_to_languages; Dictionary m_metadata; protected: static void _bind_methods(); public: - String get_language_code() const; - void set_language_code(const String &p_language_code); - PackedStringArray get_translate_to_languages() const; - void set_translate_to_languages(const PackedStringArray &p_languages); Dictionary get_metadata() const; void set_metadata(const Dictionary &p_metadata); }; +// A voice font available to PartyLocalChatControl::SynthesizeTextToSpeech(). +// Instances are snapshots captured when +// PlayFabPartyChat.populate_text_to_speech_profiles_async() completes. +class PlayFabPartyTextToSpeechProfile : public RefCounted { + GDCLASS(PlayFabPartyTextToSpeechProfile, RefCounted); + + String m_identifier; + String m_name; + String m_language_code; + int64_t m_gender = 0; + +protected: + static void _bind_methods(); + +public: + void set_values(const String &p_identifier, const String &p_name, const String &p_language_code, int64_t p_gender); + + String get_identifier() const; + String get_name() const; + String get_language_code() const; + int64_t get_gender() const; +}; + class PlayFabPartyMember : public RefCounted { GDCLASS(PlayFabPartyMember, RefCounted); @@ -140,10 +165,12 @@ class PlayFabPartyChatMessage : public RefCounted { Dictionary m_sender_entity_key; Array m_targets; String m_text; + String m_original_text; String m_language_code; String m_translated_text; bool m_transcription = false; int64_t m_timestamp = 0; + int64_t m_options = 0; Dictionary m_metadata; protected: @@ -159,16 +186,20 @@ class PlayFabPartyChatMessage : public RefCounted { const String &p_translated_text, bool p_transcription, int64_t p_timestamp, - const Dictionary &p_metadata); + const Dictionary &p_metadata, + const String &p_original_text = String(), + int64_t p_options = 0); Ref get_sender() const; Dictionary get_sender_entity_key() const; Array get_targets() const; String get_text() const; + String get_original_text() const; String get_language_code() const; String get_translated_text() const; bool is_transcription() const; int64_t get_timestamp() const; + int64_t get_options() const; Dictionary get_metadata() const; }; @@ -218,6 +249,18 @@ class PlayFabPartyChatControl : public RefCounted { // chat-control lifetime is decoupled from any single network (Phase C). CharString m_audio_input_device_id; CharString m_audio_output_device_id; + // Last reported audio device states, refreshed from + // PartyLocalChatAudioInputChanged / PartyLocalChatAudioOutputChanged. Party + // reports device initialization failures (device removed, consent denied, + // already in use) only through those state changes, so they are cached here + // for polling instead of being lost to a log line. 0 == + // PlayFabParty.AUDIO_INPUT_STATE_NO_INPUT / AUDIO_OUTPUT_STATE_NO_OUTPUT + // (PlayFabParty's enums are declared below this class). + int64_t m_audio_input_state = 0; + int64_t m_audio_output_state = 0; + // Snapshot of PartyLocalChatControl::GetAvailableTextToSpeechProfiles() + // taken when PopulateAvailableTextToSpeechProfilesCompleted arrives. + Array m_text_to_speech_profiles; protected: static void _bind_methods(); @@ -245,6 +288,63 @@ class PlayFabPartyChatControl : public RefCounted { Signal set_audio_muted_async(const Ref &p_target, bool p_muted); Signal set_text_muted_async(const Ref &p_target, bool p_muted); Signal destroy_async(); + + // --- Chat indicators (poll these; Party raises no state change for them). + // Party recomputes both indicators continuously from the live audio + // pipeline, so a UI "who is talking" element should sample them each frame + // (or on a timer) rather than wait for an event. + // + // get_local_chat_indicator() answers "what is *this* local control doing" + // and is only meaningful on a local control. + // get_chat_indicator() answers "what does the local control hear from this + // control" and is only meaningful on a remote control. + int64_t get_local_chat_indicator() const; + int64_t get_chat_indicator() const; + + // --- Local audio device state / selection readback. + int64_t get_audio_input_state() const; + int64_t get_audio_output_state() const; + int64_t get_audio_input_selection_type() const; + int64_t get_audio_output_selection_type() const; + String get_audio_input_device_id() const; + String get_audio_output_device_id() const; + + // --- Local capture mute (push-to-talk). Distinct from the incoming-audio + // mute applied to a remote target by set_audio_muted_async(). + bool is_audio_input_muted() const; + Signal set_audio_input_muted_async(bool p_muted); + + // --- Per-target render volume, 0.0 (silent) to 1.0 (full). + float get_audio_render_volume(const Ref &p_target) const; + Signal set_audio_render_volume_async(const Ref &p_target, double p_volume); + + int64_t get_audio_encoder_bitrate() const; + Signal set_audio_encoder_bitrate_async(int64_t p_bitrate); + + int64_t get_voice_audio_options() const; + Signal set_voice_audio_options_async(int64_t p_options); + + // --- Language, transcription, and text-chat options. These drive whether + // PartyVoiceChatTranscriptionReceived / translated chat text are produced + // at all; without them transcription and translation never run. + String get_language() const; + Signal set_language_async(const String &p_language_code); + int64_t get_transcription_options() const; + Signal set_transcription_options_async(int64_t p_options); + int64_t get_text_chat_options() const; + Signal set_text_chat_options_async(int64_t p_options); + + // --- Readback for state previously pushed with set_*_async(). + int64_t get_permissions(const Ref &p_target) const; + bool is_audio_muted(const Ref &p_target) const; + bool is_text_muted(const Ref &p_target) const; + + // --- Text to speech. + Signal populate_text_to_speech_profiles_async(); + Array get_text_to_speech_profiles() const; + Ref get_text_to_speech_profile(int64_t p_type) const; + Signal set_text_to_speech_profile_async(int64_t p_type, const String &p_profile_id); + Signal synthesize_text_to_speech_async(int64_t p_type, const String &p_text); }; // Public chat surface, reached as PlayFab.party.get_chat(). Chat is meshed by @@ -266,6 +366,12 @@ class PlayFabPartyChat : public RefCounted { // returns the first tracked network's local chat control. Party::PartyLocalChatControl *_local_native_chat_control() const; + // Resolves the local chat-control wrapper every per-user convenience method + // below delegates to: the named user's control, or the first tracked local + // control when p_user is null. Since Phase C a local chat control exists + // independently of any network, so this does not require a joined network. + Ref _resolve_local_control(const Ref &p_user) const; + protected: static void _bind_methods(); @@ -300,6 +406,66 @@ class PlayFabPartyChat : public RefCounted { // Explicitly destroy the calling local user's chat control. Idempotent: a // no-op success when the user has no chat control. Signal destroy_local_chat_control_async(const Ref &p_user); + + // --- Chat indicators. Poll these; PlayFab Party raises no state change + // when an indicator flips, so a "who is talking" UI should sample them on a + // frame or timer tick. All three are cheap synchronous SDK reads. + // + // get_local_chat_indicator() -> PlayFabParty.LocalChatIndicator for the + // local user's own capture pipeline (silent / talking / input muted / no + // input device). + int64_t get_local_chat_indicator(const Ref &p_user = Ref()) const; + // get_chat_indicator() -> PlayFabParty.ChatIndicator describing what the + // local control currently hears from the given remote entity. + int64_t get_chat_indicator(const Dictionary &p_entity_key, const Ref &p_user = Ref()) const; + // One entry per remote chat control: + // { "entity_key": { "id", "type" }, "indicator": PlayFabParty.ChatIndicator }. + Array get_chat_indicators(const Ref &p_user = Ref()) const; + + // --- Local capture mute (push-to-talk), distinct from the per-target + // incoming mute applied by set_audio_muted_async(). + bool is_audio_input_muted(const Ref &p_user = Ref()) const; + Signal set_audio_input_muted_async(bool p_muted, const Ref &p_user = Ref()); + + // --- Per-remote render volume, 0.0 (silent) to 1.0 (full). + float get_audio_render_volume(const Dictionary &p_entity_key, const Ref &p_user = Ref()) const; + Signal set_audio_render_volume_async(const Dictionary &p_entity_key, double p_volume, const Ref &p_user = Ref()); + + // --- Readback for state pushed with set_chat_permissions_async() / + // set_audio_muted_async() / set_text_muted_async(). + int64_t get_chat_permissions(const Dictionary &p_entity_key, const Ref &p_user = Ref()) const; + bool is_audio_muted(const Dictionary &p_entity_key, const Ref &p_user = Ref()) const; + bool is_text_muted(const Dictionary &p_entity_key, const Ref &p_user = Ref()) const; + + // --- Language / transcription / translation. Transcription and translation + // do not run at all unless these options are set (PlayFabPartyConfig's + // enable_transcription / enable_translation apply them at chat-control + // creation; these let a game change them later). + String get_language(const Ref &p_user = Ref()) const; + Signal set_language_async(const String &p_language_code, const Ref &p_user = Ref()); + int64_t get_transcription_options(const Ref &p_user = Ref()) const; + Signal set_transcription_options_async(int64_t p_options, const Ref &p_user = Ref()); + int64_t get_text_chat_options(const Ref &p_user = Ref()) const; + Signal set_text_chat_options_async(int64_t p_options, const Ref &p_user = Ref()); + + // --- Voice encoder / processing options. + int64_t get_audio_encoder_bitrate(const Ref &p_user = Ref()) const; + Signal set_audio_encoder_bitrate_async(int64_t p_bitrate, const Ref &p_user = Ref()); + int64_t get_voice_audio_options(const Ref &p_user = Ref()) const; + Signal set_voice_audio_options_async(int64_t p_options, const Ref &p_user = Ref()); + + // --- Audio device state. Poll after create_local_chat_control_async() to + // detect a missing microphone, denied consent, or a device already in use. + int64_t get_audio_input_state(const Ref &p_user = Ref()) const; + int64_t get_audio_output_state(const Ref &p_user = Ref()) const; + + // --- Text to speech. populate_text_to_speech_profiles_async() must complete + // before get_text_to_speech_profiles() returns anything. + Signal populate_text_to_speech_profiles_async(const Ref &p_user = Ref()); + Array get_text_to_speech_profiles(const Ref &p_user = Ref()) const; + Ref get_text_to_speech_profile(int64_t p_type, const Ref &p_user = Ref()) const; + Signal set_text_to_speech_profile_async(int64_t p_type, const String &p_profile_id, const Ref &p_user = Ref()); + Signal synthesize_text_to_speech_async(int64_t p_type, const String &p_text, const Ref &p_user = Ref()); }; class PlayFabPartyNetworkStateChange : public RefCounted { @@ -396,6 +562,17 @@ class PlayFabPartyNetwork : public RefCounted { Ref get_local_chat_control() const; bool is_host_network() const; + // Transport telemetry for this network. p_statistics is a list of + // PlayFabParty.NetworkStatistic values; an empty array requests every + // statistic. Returns { statistic_id: value } and an empty Dictionary when + // the network is not attached. + Dictionary get_statistics(const PackedInt32Array &p_statistics = PackedInt32Array()) const; + // PlayFabParty.DeviceConnectionType for the device backing p_peer_id: + // whether traffic to that peer is relayed through the Party cloud relay or + // travels over a direct peer-to-peer connection. Returns + // DEVICE_CONNECTION_TYPE_RELAY_SERVER when the peer is unknown. + int64_t get_device_connection_type(int64_t p_peer_id) const; + Signal leave_async(); }; @@ -550,6 +727,129 @@ class PlayFabParty : public RefCounted { CHAT_CHANGE_MUTED_CHANGED = 4, }; + // What the local user's own capture pipeline is doing right now. Poll via + // PlayFabPartyChat.get_local_chat_indicator(); Party raises no state change + // when this flips. Values match PartyLocalChatControlChatIndicator. + enum LocalChatIndicator : int64_t { + LOCAL_CHAT_INDICATOR_SILENT = 0, + LOCAL_CHAT_INDICATOR_TALKING = 1, + LOCAL_CHAT_INDICATOR_AUDIO_INPUT_MUTED = 2, + LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT = 3, + }; + + // What the local chat control currently hears from a remote chat control. + // Poll via PlayFabPartyChat.get_chat_indicator(). Values match + // PartyChatControlChatIndicator. + enum ChatIndicator : int64_t { + CHAT_INDICATOR_SILENT = 0, + CHAT_INDICATOR_TALKING = 1, + CHAT_INDICATOR_INCOMING_VOICE_DISABLED = 2, + CHAT_INDICATOR_INCOMING_COMMUNICATIONS_MUTED = 3, + CHAT_INDICATOR_NO_REMOTE_INPUT = 4, + CHAT_INDICATOR_REMOTE_AUDIO_INPUT_MUTED = 5, + }; + + enum AudioInputState : int64_t { + AUDIO_INPUT_STATE_NO_INPUT = 0, + AUDIO_INPUT_STATE_INITIALIZED = 1, + AUDIO_INPUT_STATE_NOT_FOUND = 2, + AUDIO_INPUT_STATE_USER_CONSENT_DENIED = 3, + AUDIO_INPUT_STATE_UNSUPPORTED_FORMAT = 4, + AUDIO_INPUT_STATE_ALREADY_IN_USE = 5, + AUDIO_INPUT_STATE_UNKNOWN_ERROR = 6, + }; + + enum AudioOutputState : int64_t { + AUDIO_OUTPUT_STATE_NO_OUTPUT = 0, + AUDIO_OUTPUT_STATE_INITIALIZED = 1, + AUDIO_OUTPUT_STATE_NOT_FOUND = 2, + AUDIO_OUTPUT_STATE_UNSUPPORTED_FORMAT = 3, + AUDIO_OUTPUT_STATE_ALREADY_IN_USE = 4, + AUDIO_OUTPUT_STATE_UNKNOWN_ERROR = 5, + }; + + enum AudioDeviceSelectionType : int64_t { + AUDIO_DEVICE_SELECTION_NONE = 0, + AUDIO_DEVICE_SELECTION_SYSTEM_DEFAULT = 1, + AUDIO_DEVICE_SELECTION_PLATFORM_USER_DEFAULT = 2, + AUDIO_DEVICE_SELECTION_MANUAL = 3, + }; + + enum VoiceAudioOptions : int64_t { + VOICE_AUDIO_OPTION_NONE = 0, + VOICE_AUDIO_OPTION_NOISE_SUPPRESSION = 1, + }; + + // Bit flags controlling which chat controls get transcribed. Transcription + // is off by default: without at least one of these set, PlayFab Party never + // produces a transcription_received signal. + enum TranscriptionOptions : int64_t { + TRANSCRIPTION_OPTION_NONE = 0, + TRANSCRIPTION_OPTION_TRANSCRIBE_SELF = 1, + TRANSCRIPTION_OPTION_TRANSCRIBE_MATCHING_LANGUAGES = 2, + TRANSCRIPTION_OPTION_TRANSCRIBE_NON_MATCHING_LANGUAGES = 4, + TRANSCRIPTION_OPTION_DISABLE_HYPOTHESIS_PHRASES = 8, + TRANSCRIPTION_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE = 16, + TRANSCRIPTION_OPTION_DISABLE_PROFANITY_MASKING = 32, + TRANSCRIPTION_OPTION_TRANSCRIBE_SELF_REGARDLESS_OF_NETWORK_STATE = 64, + }; + + // Bit flags applied to *incoming* text chat on the receiving control. + // Translation is a receiver-side setting in PlayFab Party: without + // TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE, + // PlayFabPartyChatMessage.translated_text always equals `text`. + enum TextChatOptions : int64_t { + TEXT_CHAT_OPTION_NONE = 0, + TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE = 1, + TEXT_CHAT_OPTION_FILTER_OFFENSIVE_TEXT = 2, + }; + + // Flags on a received PlayFabPartyChatMessage describing filtering Party + // applied to it. + enum ChatMessageOptions : int64_t { + CHAT_MESSAGE_OPTION_NONE = 0, + CHAT_MESSAGE_OPTION_FILTERED_OFFENSIVE_TERMS = 1, + CHAT_MESSAGE_OPTION_FILTERED_ENTIRE_MESSAGE = 2, + CHAT_MESSAGE_OPTION_FILTERED_DUE_TO_ERROR = 4, + }; + + enum SynthesizeTextToSpeechType : int64_t { + TEXT_TO_SPEECH_TYPE_NARRATION = 0, + TEXT_TO_SPEECH_TYPE_VOICE_CHAT = 1, + }; + + enum Gender : int64_t { + GENDER_NEUTRAL = 0, + GENDER_FEMALE = 1, + GENDER_MALE = 2, + }; + + // Keys accepted by PlayFabPartyNetwork.get_statistics(); values match + // PartyNetworkStatistic. + enum NetworkStatistic : int64_t { + NETWORK_STATISTIC_AVERAGE_RELAY_SERVER_ROUND_TRIP_LATENCY_MS = 0, + NETWORK_STATISTIC_SENT_PROTOCOL_PACKETS = 1, + NETWORK_STATISTIC_SENT_PROTOCOL_BYTES = 2, + NETWORK_STATISTIC_RETRIED_PROTOCOL_PACKETS = 3, + NETWORK_STATISTIC_RETRIED_PROTOCOL_BYTES = 4, + NETWORK_STATISTIC_DROPPED_PROTOCOL_PACKETS = 5, + NETWORK_STATISTIC_RECEIVED_PROTOCOL_PACKETS = 6, + NETWORK_STATISTIC_RECEIVED_PROTOCOL_BYTES = 7, + NETWORK_STATISTIC_CURRENTLY_QUEUED_SEND_MESSAGES = 8, + NETWORK_STATISTIC_CURRENTLY_QUEUED_SEND_MESSAGE_BYTES = 9, + NETWORK_STATISTIC_CURRENTLY_ACTIVE_SEND_MESSAGES = 10, + NETWORK_STATISTIC_CURRENTLY_ACTIVE_SEND_MESSAGE_BYTES = 11, + NETWORK_STATISTIC_TIMED_OUT_SEND_MESSAGES = 12, + NETWORK_STATISTIC_TIMED_OUT_SEND_MESSAGE_BYTES = 13, + NETWORK_STATISTIC_CANCELED_SEND_MESSAGES = 14, + NETWORK_STATISTIC_CANCELED_SEND_MESSAGE_BYTES = 15, + }; + + enum DeviceConnectionType : int64_t { + DEVICE_CONNECTION_TYPE_RELAY_SERVER = 0, + DEVICE_CONNECTION_TYPE_DIRECT_PEER_CONNECTION = 1, + }; + enum PendingOperationKind : int32_t { PENDING_NONE = 0, PENDING_CREATE_NETWORK = 1, @@ -561,6 +861,13 @@ class PlayFabParty : public RefCounted { PENDING_LEAVE_NETWORK = 7, PENDING_DESTROY_CHAT_CONTROL = 8, PENDING_JOIN_HANDSHAKE = 9, + PENDING_SET_LANGUAGE = 10, + PENDING_SET_TRANSCRIPTION_OPTIONS = 11, + PENDING_SET_TEXT_CHAT_OPTIONS = 12, + PENDING_SET_AUDIO_ENCODER_BITRATE = 13, + PENDING_POPULATE_TEXT_TO_SPEECH_PROFILES = 14, + PENDING_SET_TEXT_TO_SPEECH_PROFILE = 15, + PENDING_SYNTHESIZE_TEXT_TO_SPEECH = 16, }; struct PendingOperation { @@ -654,6 +961,13 @@ class PlayFabParty : public RefCounted { void _process_chat_control_destroyed(const Party::PartyStateChange *p_change); void _process_chat_text_received(const Party::PartyStateChange *p_change); void _process_voice_chat_transcription_received(const Party::PartyStateChange *p_change); + void _process_set_language_completed(const Party::PartyStateChange *p_change); + void _process_set_transcription_options_completed(const Party::PartyStateChange *p_change); + void _process_set_text_chat_options_completed(const Party::PartyStateChange *p_change); + void _process_set_chat_audio_encoder_bitrate_completed(const Party::PartyStateChange *p_change); + void _process_populate_text_to_speech_profiles_completed(const Party::PartyStateChange *p_change); + void _process_set_text_to_speech_profile_completed(const Party::PartyStateChange *p_change); + void _process_synthesize_text_to_speech_completed(const Party::PartyStateChange *p_change); #ifdef GODOT_PLAYFAB_TEST_HOOKS Signal _test_enqueue_shutdown_pending(); @@ -673,12 +987,48 @@ class PlayFabParty : public RefCounted { Signal _set_chat_permissions(Party::PartyLocalChatControl *p_local_chat_control, Party::PartyChatControl *p_target, int64_t p_permissions, bool *r_succeeded = nullptr); Signal _set_incoming_audio_muted(Party::PartyLocalChatControl *p_local_chat_control, Party::PartyChatControl *p_target, bool p_muted, bool *r_succeeded = nullptr); Signal _set_incoming_text_muted(Party::PartyLocalChatControl *p_local_chat_control, Party::PartyChatControl *p_target, bool p_muted, bool *r_succeeded = nullptr); + Signal _set_audio_input_muted(Party::PartyLocalChatControl *p_local_chat_control, bool p_muted); + Signal _set_audio_render_volume(Party::PartyLocalChatControl *p_local_chat_control, Party::PartyChatControl *p_target, double p_volume); Signal _destroy_chat_control(const Ref &p_chat_control); // Phase C: explicit, network-independent local chat-control lifecycle backing // PlayFabPartyChat.create_local_chat_control_async/destroy_local_chat_control_async. Signal _create_local_chat_control(const Ref &p_user, const Ref &p_config); Signal _destroy_local_chat_control(const Ref &p_user); + // Applies the language / transcription / text-chat options implied by + // p_config to a freshly created local chat control. Without this the + // config's enable_transcription and enable_translation flags are inert: + // PlayFab Party never generates transcriptions or translations unless + // SetTranscriptionOptions()/SetTextChatOptions() are called. + void _configure_chat_language_options(Party::PartyLocalChatControl *p_chat_control, const Ref &p_config); + // Shared plumbing for the async single-shot local chat-control operations + // (SetLanguage, SetTranscriptionOptions, SetTextChatOptions, + // SetAudioEncoderBitrate, PopulateAvailableTextToSpeechProfiles, + // SetTextToSpeechProfile, SynthesizeTextToSpeech). p_invoke receives the + // PendingOperation to pass as the SDK's asyncIdentifier and returns the + // SDK's PartyError (a uint32_t; the SDK headers are not included here). + Signal _dispatch_chat_control_operation( + Party::PartyLocalChatControl *p_chat_control, + int32_t p_kind, + const String &p_error_code, + const String &p_action, + const std::function &p_invoke); + // Completes the pending operation carried by a *Completed state change's + // asyncIdentifier, mapping a non-Succeeded result onto p_error_code. + // p_state_change_result is a Party::PartyStateChangeResult; it is passed as + // an integer because the SDK headers are not included here. + void _complete_chat_control_operation( + void *p_async_identifier, + int64_t p_state_change_result, + uint32_t p_error_detail, + const String &p_error_code, + const String &p_action); + Ref _find_chat_control_wrapper(Party::PartyChatControl *p_native) const; + // Resolves the PartyLocalChatControl backing p_user, or the first tracked + // local control when p_user is null. Unlike PlayFabPartyChat's network-based + // lookup this also finds controls created before any network join. + Party::PartyLocalChatControl *_resolve_local_native_chat_control(const Ref &p_user) const; + HRESULT _start_create_endpoint_step(PendingOperation *p_operation); HRESULT _start_create_chat_control_step(PendingOperation *p_operation); // Reuse an already-created local chat control by connecting it to the @@ -734,5 +1084,18 @@ VARIANT_ENUM_CAST(godot::PlayFabParty::NetworkState); VARIANT_ENUM_CAST(godot::PlayFabParty::ChatPermission); VARIANT_ENUM_CAST(godot::PlayFabParty::NetworkStateChangeKind); VARIANT_ENUM_CAST(godot::PlayFabParty::ChatStateChangeKind); +VARIANT_ENUM_CAST(godot::PlayFabParty::LocalChatIndicator); +VARIANT_ENUM_CAST(godot::PlayFabParty::ChatIndicator); +VARIANT_ENUM_CAST(godot::PlayFabParty::AudioInputState); +VARIANT_ENUM_CAST(godot::PlayFabParty::AudioOutputState); +VARIANT_ENUM_CAST(godot::PlayFabParty::AudioDeviceSelectionType); +VARIANT_ENUM_CAST(godot::PlayFabParty::VoiceAudioOptions); +VARIANT_ENUM_CAST(godot::PlayFabParty::TranscriptionOptions); +VARIANT_ENUM_CAST(godot::PlayFabParty::TextChatOptions); +VARIANT_ENUM_CAST(godot::PlayFabParty::ChatMessageOptions); +VARIANT_ENUM_CAST(godot::PlayFabParty::SynthesizeTextToSpeechType); +VARIANT_ENUM_CAST(godot::PlayFabParty::Gender); +VARIANT_ENUM_CAST(godot::PlayFabParty::NetworkStatistic); +VARIANT_ENUM_CAST(godot::PlayFabParty::DeviceConnectionType); #endif // GODOT_PLAYFAB_PARTY_H diff --git a/addons/godot_playfab/src/register_types.cpp b/addons/godot_playfab/src/register_types.cpp index 4f7d150..50b7280 100644 --- a/addons/godot_playfab/src/register_types.cpp +++ b/addons/godot_playfab/src/register_types.cpp @@ -234,6 +234,7 @@ void initialize_playfab_extension(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); diff --git a/addons/godot_playfab_csharp/Types/PlayFabPartyChat.cs b/addons/godot_playfab_csharp/Types/PlayFabPartyChat.cs index ee72999..fdb848d 100644 --- a/addons/godot_playfab_csharp/Types/PlayFabPartyChat.cs +++ b/addons/godot_playfab_csharp/Types/PlayFabPartyChat.cs @@ -124,4 +124,88 @@ public Task SetChatPermissionsAsync(Godot.Collections.Dictionary public Task SetTextMutedAsync(Godot.Collections.Dictionary entityKey, bool muted) => CallResultAsync("set_text_muted_async", entityKey, muted); + + // Chat indicators and voice-chat state are polled, not signalled: PlayFab + // Party raises no state change when an indicator flips. + public int GetLocalChatIndicator(PlayFabUser user = null) => + Call("get_local_chat_indicator", user?.Raw).AsInt32(); + + public int GetChatIndicator(Godot.Collections.Dictionary entityKey, PlayFabUser user = null) => + Call("get_chat_indicator", entityKey, user?.Raw).AsInt32(); + + public Godot.Collections.Array GetChatIndicators(PlayFabUser user = null) => + Call("get_chat_indicators", user?.Raw).AsGodotArray(); + + public bool IsAudioInputMuted(PlayFabUser user = null) => + Call("is_audio_input_muted", user?.Raw).AsBool(); + + public Task SetAudioInputMutedAsync(bool muted, PlayFabUser user = null) => + CallResultAsync("set_audio_input_muted_async", muted, user?.Raw); + + public float GetAudioRenderVolume(Godot.Collections.Dictionary entityKey, PlayFabUser user = null) => + Call("get_audio_render_volume", entityKey, user?.Raw).AsSingle(); + + public Task SetAudioRenderVolumeAsync( + Godot.Collections.Dictionary entityKey, float volume, PlayFabUser user = null) => + CallResultAsync("set_audio_render_volume_async", entityKey, volume, user?.Raw); + + public int GetChatPermissions(Godot.Collections.Dictionary entityKey, PlayFabUser user = null) => + Call("get_chat_permissions", entityKey, user?.Raw).AsInt32(); + + public bool IsAudioMuted(Godot.Collections.Dictionary entityKey, PlayFabUser user = null) => + Call("is_audio_muted", entityKey, user?.Raw).AsBool(); + + public bool IsTextMuted(Godot.Collections.Dictionary entityKey, PlayFabUser user = null) => + Call("is_text_muted", entityKey, user?.Raw).AsBool(); + + public string GetLanguage(PlayFabUser user = null) => + Call("get_language", user?.Raw).AsString(); + + public Task SetLanguageAsync(string languageCode, PlayFabUser user = null) => + CallResultAsync("set_language_async", languageCode, user?.Raw); + + public int GetTranscriptionOptions(PlayFabUser user = null) => + Call("get_transcription_options", user?.Raw).AsInt32(); + + public Task SetTranscriptionOptionsAsync(int options, PlayFabUser user = null) => + CallResultAsync("set_transcription_options_async", options, user?.Raw); + + public int GetTextChatOptions(PlayFabUser user = null) => + Call("get_text_chat_options", user?.Raw).AsInt32(); + + public Task SetTextChatOptionsAsync(int options, PlayFabUser user = null) => + CallResultAsync("set_text_chat_options_async", options, user?.Raw); + + public int GetAudioEncoderBitrate(PlayFabUser user = null) => + Call("get_audio_encoder_bitrate", user?.Raw).AsInt32(); + + public Task SetAudioEncoderBitrateAsync(int bitrate, PlayFabUser user = null) => + CallResultAsync("set_audio_encoder_bitrate_async", bitrate, user?.Raw); + + public int GetVoiceAudioOptions(PlayFabUser user = null) => + Call("get_voice_audio_options", user?.Raw).AsInt32(); + + public Task SetVoiceAudioOptionsAsync(int options, PlayFabUser user = null) => + CallResultAsync("set_voice_audio_options_async", options, user?.Raw); + + public int GetAudioInputState(PlayFabUser user = null) => + Call("get_audio_input_state", user?.Raw).AsInt32(); + + public int GetAudioOutputState(PlayFabUser user = null) => + Call("get_audio_output_state", user?.Raw).AsInt32(); + + public Task PopulateTextToSpeechProfilesAsync(PlayFabUser user = null) => + CallResultAsync("populate_text_to_speech_profiles_async", user?.Raw); + + public Godot.Collections.Array GetTextToSpeechProfiles(PlayFabUser user = null) => + Call("get_text_to_speech_profiles", user?.Raw).AsGodotArray(); + + public PlayFabPartyTextToSpeechProfile GetTextToSpeechProfile(int type, PlayFabUser user = null) => + PlayFabPartyTextToSpeechProfile.From(Call("get_text_to_speech_profile", type, user?.Raw).AsGodotObject()); + + public Task SetTextToSpeechProfileAsync(int type, string profileId, PlayFabUser user = null) => + CallResultAsync("set_text_to_speech_profile_async", type, profileId, user?.Raw); + + public Task SynthesizeTextToSpeechAsync(int type, string text, PlayFabUser user = null) => + CallResultAsync("synthesize_text_to_speech_async", type, text, user?.Raw); } diff --git a/addons/godot_playfab_csharp/Types/PlayFabPartyChatControl.cs b/addons/godot_playfab_csharp/Types/PlayFabPartyChatControl.cs index 6f9329e..5b178cb 100644 --- a/addons/godot_playfab_csharp/Types/PlayFabPartyChatControl.cs +++ b/addons/godot_playfab_csharp/Types/PlayFabPartyChatControl.cs @@ -80,4 +80,95 @@ public Task SetTextMutedAsync(PlayFabPartyChatControl target, boo public Task DestroyAsync() => CallResultAsync("destroy_async"); + + // Indicators and device state are polled, not signalled. + public int GetLocalChatIndicator() => + Call("get_local_chat_indicator").AsInt32(); + + public int GetChatIndicator() => + Call("get_chat_indicator").AsInt32(); + + public int GetAudioInputState() => + Call("get_audio_input_state").AsInt32(); + + public int GetAudioOutputState() => + Call("get_audio_output_state").AsInt32(); + + public int GetAudioInputSelectionType() => + Call("get_audio_input_selection_type").AsInt32(); + + public int GetAudioOutputSelectionType() => + Call("get_audio_output_selection_type").AsInt32(); + + public string GetAudioInputDeviceId() => + Call("get_audio_input_device_id").AsString(); + + public string GetAudioOutputDeviceId() => + Call("get_audio_output_device_id").AsString(); + + public bool IsAudioInputMuted() => + Call("is_audio_input_muted").AsBool(); + + public Task SetAudioInputMutedAsync(bool muted) => + CallResultAsync("set_audio_input_muted_async", muted); + + public float GetAudioRenderVolume(PlayFabPartyChatControl target) => + Call("get_audio_render_volume", target?.Raw).AsSingle(); + + public Task SetAudioRenderVolumeAsync(PlayFabPartyChatControl target, float volume) => + CallResultAsync("set_audio_render_volume_async", target?.Raw, volume); + + public int GetPermissions(PlayFabPartyChatControl target) => + Call("get_permissions", target?.Raw).AsInt32(); + + public bool IsAudioMuted(PlayFabPartyChatControl target) => + Call("is_audio_muted", target?.Raw).AsBool(); + + public bool IsTextMuted(PlayFabPartyChatControl target) => + Call("is_text_muted", target?.Raw).AsBool(); + + public string GetLanguage() => + Call("get_language").AsString(); + + public Task SetLanguageAsync(string languageCode) => + CallResultAsync("set_language_async", languageCode); + + public int GetTranscriptionOptions() => + Call("get_transcription_options").AsInt32(); + + public Task SetTranscriptionOptionsAsync(int options) => + CallResultAsync("set_transcription_options_async", options); + + public int GetTextChatOptions() => + Call("get_text_chat_options").AsInt32(); + + public Task SetTextChatOptionsAsync(int options) => + CallResultAsync("set_text_chat_options_async", options); + + public int GetAudioEncoderBitrate() => + Call("get_audio_encoder_bitrate").AsInt32(); + + public Task SetAudioEncoderBitrateAsync(int bitrate) => + CallResultAsync("set_audio_encoder_bitrate_async", bitrate); + + public int GetVoiceAudioOptions() => + Call("get_voice_audio_options").AsInt32(); + + public Task SetVoiceAudioOptionsAsync(int options) => + CallResultAsync("set_voice_audio_options_async", options); + + public Task PopulateTextToSpeechProfilesAsync() => + CallResultAsync("populate_text_to_speech_profiles_async"); + + public Godot.Collections.Array GetTextToSpeechProfiles() => + Call("get_text_to_speech_profiles").AsGodotArray(); + + public PlayFabPartyTextToSpeechProfile GetTextToSpeechProfile(int type) => + PlayFabPartyTextToSpeechProfile.From(Call("get_text_to_speech_profile", type).AsGodotObject()); + + public Task SetTextToSpeechProfileAsync(int type, string profileId) => + CallResultAsync("set_text_to_speech_profile_async", type, profileId); + + public Task SynthesizeTextToSpeechAsync(int type, string text) => + CallResultAsync("synthesize_text_to_speech_async", type, text); } diff --git a/addons/godot_playfab_csharp/Types/PlayFabPartyChatMessage.cs b/addons/godot_playfab_csharp/Types/PlayFabPartyChatMessage.cs index 4e7959e..fcb8825 100644 --- a/addons/godot_playfab_csharp/Types/PlayFabPartyChatMessage.cs +++ b/addons/godot_playfab_csharp/Types/PlayFabPartyChatMessage.cs @@ -31,6 +31,10 @@ internal PlayFabPartyChatMessage(GodotObject o) : base(o) public Godot.Collections.Dictionary Metadata => GetDict("metadata"); + public string OriginalText => GetString("original_text"); + + public int Options => GetInt32("options"); + public PlayFabPartyChatControl GetSender() => PlayFabPartyChatControl.From(Call("get_sender").AsGodotObject()); @@ -55,4 +59,10 @@ public int GetTimestamp() => public Godot.Collections.Dictionary GetMetadata() => Call("get_metadata").AsGodotDictionary(); + + public string GetOriginalText() => + Call("get_original_text").AsString(); + + public int GetOptions() => + Call("get_options").AsInt32(); } diff --git a/addons/godot_playfab_csharp/Types/PlayFabPartyConfig.cs b/addons/godot_playfab_csharp/Types/PlayFabPartyConfig.cs index 25e6aeb..c3e0675 100644 --- a/addons/godot_playfab_csharp/Types/PlayFabPartyConfig.cs +++ b/addons/godot_playfab_csharp/Types/PlayFabPartyConfig.cs @@ -31,5 +31,5 @@ internal PlayFabPartyConfig(GodotObject o) : base(o) public string AudioOutput => GetString("audio_output"); - public Godot.Collections.Dictionary Metadata => GetDict("metadata"); + public string Language => GetString("language"); } diff --git a/addons/godot_playfab_csharp/Types/PlayFabPartyNetwork.cs b/addons/godot_playfab_csharp/Types/PlayFabPartyNetwork.cs index 4bef741..ca7862e 100644 --- a/addons/godot_playfab_csharp/Types/PlayFabPartyNetwork.cs +++ b/addons/godot_playfab_csharp/Types/PlayFabPartyNetwork.cs @@ -61,4 +61,15 @@ public bool IsHostNetwork() => public Task LeaveAsync() => CallResultAsync("leave_async"); + + /// + /// Live transport statistics keyed by lower snake case statistic name. Pass + /// PlayFabParty.NetworkStatistic values to fetch a subset; null or empty + /// returns all of them. + /// + public Godot.Collections.Dictionary GetStatistics(int[] statistics = null) => + Call("get_statistics", statistics ?? Array.Empty()).AsGodotDictionary(); + + public int GetDeviceConnectionType(int peerId) => + Call("get_device_connection_type", peerId).AsInt32(); } diff --git a/addons/godot_playfab_csharp/Types/PlayFabPartyTextMessageConfig.cs b/addons/godot_playfab_csharp/Types/PlayFabPartyTextMessageConfig.cs index a357b04..8cee753 100644 --- a/addons/godot_playfab_csharp/Types/PlayFabPartyTextMessageConfig.cs +++ b/addons/godot_playfab_csharp/Types/PlayFabPartyTextMessageConfig.cs @@ -11,9 +11,5 @@ internal PlayFabPartyTextMessageConfig(GodotObject o) : base(o) public static PlayFabPartyTextMessageConfig From(GodotObject o) => o == null ? null : new PlayFabPartyTextMessageConfig(o); - public string LanguageCode => GetString("language_code"); - - public string[] TranslateToLanguages => Get("translate_to_languages").AsStringArray(); - public Godot.Collections.Dictionary Metadata => GetDict("metadata"); } diff --git a/addons/godot_playfab_csharp/Types/PlayFabPartyTextToSpeechProfile.cs b/addons/godot_playfab_csharp/Types/PlayFabPartyTextToSpeechProfile.cs new file mode 100644 index 0000000..eadf604 --- /dev/null +++ b/addons/godot_playfab_csharp/Types/PlayFabPartyTextToSpeechProfile.cs @@ -0,0 +1,37 @@ +using Godot; +using GodotPlayFab.Internal; + +namespace GodotPlayFab.Types; + +/// +/// A synthetic voice available to PlayFab Party text-to-speech. Populate the +/// list with . +/// +public sealed class PlayFabPartyTextToSpeechProfile : PlayFabObject +{ + internal PlayFabPartyTextToSpeechProfile(GodotObject o) : base(o) + { + } + + public static PlayFabPartyTextToSpeechProfile From(GodotObject o) => o == null ? null : new PlayFabPartyTextToSpeechProfile(o); + + public string Identifier => GetString("identifier"); + + public string Name => GetString("name"); + + public string LanguageCode => GetString("language_code"); + + public int Gender => GetInt32("gender"); + + public string GetIdentifier() => + Call("get_identifier").AsString(); + + public string GetName() => + Call("get_name").AsString(); + + public string GetLanguageCode() => + Call("get_language_code").AsString(); + + public int GetGender() => + Call("get_gender").AsInt32(); +} diff --git a/docs/playfab/plugin.md b/docs/playfab/plugin.md index 232b7ad..567ff7a 100644 --- a/docs/playfab/plugin.md +++ b/docs/playfab/plugin.md @@ -23,6 +23,8 @@ This is the landing page for the `godot_playfab` docs set. - `PlayFab.events` as a reserved service namespace; the current Microsoft GDK PlayFab headers do not expose an active client event/telemetry operation in the client wrapper scope - PlayFab Multiplayer initialization, lobby create/join/search, lobby-owned leave and property updates, match-ticket-owned cancel/status refresh, and explicit arranged-lobby joins - PlayFab Party network host (`create_and_join_network_async`) and join (`join_network_async`) flows over the PartyManager runtime, with peer-id handshake (the host marks its endpoint with an immutable `pf.role=host` shared property so clients identify and target the host deterministically; host is peer id `1`), descriptor publishing, chat controls (voice/text/transcription) created explicitly per local user via `chat.create_local_chat_control_async` and reused across networks, mute, and permission management; the per-network peer object is a Godot `MultiplayerPeerExtension` +- PlayFab Party voice surface: polled chat indicators (`chat.get_local_chat_indicator()`, `chat.get_chat_indicator(entity_key)`, `chat.get_chat_indicators()` — Party raises no state change for indicators, so titles poll them), local mic mute for push-to-talk (`chat.set_audio_input_muted_async`), per-peer render volume, encoder bitrate, voice audio options, chat language, transcription/translation options, text-to-speech, and polled audio device state +- PlayFab Party diagnostics: `PlayFabPartyNetwork.get_statistics()` (the 16 native `NETWORK_STATISTIC_*` counters) and `get_device_connection_type(peer_id)` (relay vs. direct peer connection) - GUT coverage under `tests\godot\playfab\tests\` - idempotent extension registration so duplicated synced `.gdextension` files do not spam duplicate singleton/class registration diff --git a/spec/gdext-csharp.md b/spec/gdext-csharp.md index f12b2e2..24f36a9 100644 --- a/spec/gdext-csharp.md +++ b/spec/gdext-csharp.md @@ -247,7 +247,7 @@ plus the Multiplayer/Party value/config/state types. | `PlayFabLobby`, `PlayFabLobbyConfig`, `PlayFabLobbyJoinConfig`, `PlayFabLobbySearchConfig`, `PlayFabLobbyMember`, `PlayFabLobbyInvite`, `PlayFabLobbySummary`, `PlayFabLobbySearchResult`, `PlayFabLobbyStateChange` | matching wrappers | lobby value/config/state | | `PlayFabMatchTicket`, `PlayFabMatchmakingMember`, `PlayFabMatchmakingTicketConfig`, `PlayFabMatchTicketStateChange` | matching wrappers | matchmaking value/config/state | | `PlayFab.party` / `PlayFabParty` | `PlayFab.Party` | service (real-time net + chat) | -| `PlayFabPartyConfig`, `PlayFabPartyTextMessageConfig`, `PlayFabPartyMember`, `PlayFabPartyNetwork`, `PlayFabPartyNetworkStateChange`, `PlayFabPartyPeer`, `PlayFabPartyChat`, `PlayFabPartyChatControl`, `PlayFabPartyChatMessage`, `PlayFabPartyChatStateChange` | matching wrappers | party value/config/state/peer/chat | +| `PlayFabPartyConfig`, `PlayFabPartyTextMessageConfig`, `PlayFabPartyTextToSpeechProfile`, `PlayFabPartyMember`, `PlayFabPartyNetwork`, `PlayFabPartyNetworkStateChange`, `PlayFabPartyPeer`, `PlayFabPartyChat`, `PlayFabPartyChatControl`, `PlayFabPartyChatMessage`, `PlayFabPartyChatStateChange` | matching wrappers | party value/config/state/peer/chat | PlayFab-specific nuances the C# layer must honor: diff --git a/spec/gdext-playfab-party.md b/spec/gdext-playfab-party.md index ccad32c..bb00542 100644 --- a/spec/gdext-playfab-party.md +++ b/spec/gdext-playfab-party.md @@ -135,7 +135,7 @@ var enable_transcription: bool = false var enable_translation: bool = false var audio_input: String = "" # platform default when empty var audio_output: String = "" # platform default when empty -var metadata: Dictionary = {} +var language: String = "" # BCP-47; Party default when empty ``` `enable_voice_chat`, `enable_text_chat`, transcription, translation, and audio fields are addon policy flags. They decide whether and how the addon creates/connects Party chat controls; they are not native Party network settings. The local chat control's capture and render audio devices are always bound when the chat control is created (regardless of `enable_voice_chat`): `audio_input`/`audio_output` select a specific device id, and an empty string selects the platform's system default communication device. Whether voice actually flows still depends on chat permissions (`SendAudio`/`ReceiveAudio` granted via `set_chat_permissions_async`) and the audio subsystem successfully initializing the chosen devices; the addon logs warnings for non-`Initialized` audio device states to surface silent device-init failures. @@ -154,11 +154,13 @@ Setting platform-type flags without a login-provider flag (or vice versa) — or class_name PlayFabPartyTextMessageConfig extends RefCounted -var language_code: String = "" -var translate_to_languages: PackedStringArray = PackedStringArray() var metadata: Dictionary = {} ``` +`metadata` is serialized into a single Party data buffer and delivered verbatim to receivers as `PlayFabPartyChatMessage.metadata`. It is decoded with object instantiation disabled, so only plain Variant data survives the round trip. + +> **No per-message language or translation.** PlayFab Party has no per-message language or translation setting, so `PlayFabPartyTextMessageConfig` carries none. The sender's language is a property of the local chat control (`set_language_async` / `PlayFabPartyConfig.language`), and translation is applied **receiver-side** via `set_text_chat_options_async(TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE)`. + ## Party network and descriptor model ```gdscript @@ -183,6 +185,10 @@ func get_local_peer() -> PlayFabPartyPeer func get_local_chat_control() -> PlayFabPartyChatControl func is_host_network() -> bool func leave_async() -> Signal + +# Live diagnostics. Both are synchronous reads of native Party state. +func get_statistics(statistics: PackedInt32Array = PackedInt32Array()) -> Dictionary +func get_device_connection_type(peer_id: int) -> int ``` `PlayFabPartyNetwork` is the typed setup result and lifecycle owner. `PlayFab.party` keeps a strong reference to active networks until `leave_network_async()`, `PlayFabPartyNetwork.leave_async()`, `PlayFabPartyPeer.close_with_reason()`, or shutdown settles. Dropping a GDScript reference or replacing `multiplayer.multiplayer_peer` must not silently destroy the Party network. @@ -304,6 +310,68 @@ Peer-facing rules: 6. Native relationship outcomes update the peer permission/mute cache and emit `chat_permissions_changed` / `audio_muted_changed` / `text_muted_changed` (all carry the target peer's entity-key `Dictionary`). 7. Platform privacy and privilege policy is not automatic in the first pass because PartyXbl is out of scope. Title code can resolve policy through existing GDK/Xbox services and apply the result through peer permission helpers. +### Chat indicators and voice-chat state (polled) + +PlayFab Party raises **no state change** when a chat indicator flips, so the addon exposes indicators as polling getters rather than signals. Titles poll them from `_process()` or a UI refresh timer: + +```gdscript +func _process(_delta: float) -> void: + var speaking := PlayFab.party.chat.get_local_chat_indicator() == PlayFabParty.LOCAL_CHAT_INDICATOR_TALKING + $MicIcon.visible = speaking + for entry in PlayFab.party.chat.get_chat_indicators(): + _update_roster_row(entry.entity_key, entry.indicator) +``` + +`get_chat_indicators()` returns one `{ "entity_key": { "id", "type" }, "indicator": int }` dictionary per remote chat control. `get_chat_indicator(entity_key)` answers for a single target; passing the local player's own entity key reports `CHAT_INDICATOR_TALKING`/`CHAT_INDICATOR_SILENT` so one roster loop can drive every row. + +Audio device state is also polled: `get_audio_input_state()` / `get_audio_output_state()` return the last state Party reported for the bound devices, which is how a title distinguishes "no microphone" (`AUDIO_INPUT_STATE_NOT_FOUND`) from "microphone access denied" (`AUDIO_INPUT_STATE_USER_CONSENT_DENIED`) instead of silently having no voice. + +### Voice chat controls + +The local chat control exposes the rest of the Party voice surface, mirrored on `PlayFabPartyChat` with an optional trailing `user` argument (defaulting to the first local chat control): + +```gdscript +# Local microphone mute — this is the local player's own mute, distinct from +# set_audio_muted_async() which mutes *other* players. +PlayFab.party.chat.set_audio_input_muted_async(not Input.is_action_pressed("talk")) +PlayFab.party.chat.is_audio_input_muted() + +# Per-player playback volume (0.0 - 1.0). +PlayFab.party.chat.set_audio_render_volume_async(entity_key, 0.5) +PlayFab.party.chat.get_audio_render_volume(entity_key) + +# Encoder bitrate and voice processing. +PlayFab.party.chat.set_audio_encoder_bitrate_async(24000) +PlayFab.party.chat.set_voice_audio_options_async(PlayFabParty.VOICE_AUDIO_OPTION_NOISE_SUPPRESSION) + +# Language, transcription, and receiver-side translation. +PlayFab.party.chat.set_language_async("en-US") +PlayFab.party.chat.set_transcription_options_async( + PlayFabParty.TRANSCRIPTION_OPTION_TRANSCRIBE_SELF + | PlayFabParty.TRANSCRIPTION_OPTION_TRANSCRIBE_MATCHING_LANGUAGES) +PlayFab.party.chat.set_text_chat_options_async(PlayFabParty.TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE) + +# Text-to-speech (the accessibility path for players who cannot use a mic). +await PlayFab.party.chat.populate_text_to_speech_profiles_async() +var profile = PlayFab.party.chat.get_text_to_speech_profiles()[0] +await PlayFab.party.chat.set_text_to_speech_profile_async(PlayFabParty.TEXT_TO_SPEECH_TYPE_VOICE_CHAT, profile.identifier) +PlayFab.party.chat.synthesize_text_to_speech_async(PlayFabParty.TEXT_TO_SPEECH_TYPE_VOICE_CHAT, "hello everyone") +``` + +`PlayFabPartyConfig.enable_transcription` and `enable_translation` are applied at chat-control creation: the addon calls `SetTranscriptionOptions` (self + matching + non-matching languages) and `SetTextChatOptions(TranslateToLocalLanguage)` respectively. Without those native calls Party never produces transcriptions or translations, so the flags would be inert. + +### Network diagnostics + +```gdscript +var stats := network.get_statistics() # all 16, or pass a PackedInt32Array subset +$PingLabel.text = "%d ms" % stats.average_relay_server_round_trip_latency_ms + +if network.get_device_connection_type(peer_id) == PlayFabParty.DEVICE_CONNECTION_TYPE_DIRECT_PEER_CONNECTION: + print("direct p2p") +``` + +`get_statistics()` keys are the lower snake case statistic names (`average_relay_server_round_trip_latency_ms`, `currently_queued_send_messages`, …), matching the `PlayFabParty.NETWORK_STATISTIC_*` constants. `get_device_connection_type(peer_id)` reports what was actually negotiated; direct connections are only attempted when `direct_peer_connectivity` permits them. + Recommended permission constants: ```gdscript @@ -325,6 +393,49 @@ signal state_changed(change: PlayFabPartyChatStateChange) func get_local_chat_control(user: PlayFabUser) -> PlayFabPartyChatControl func get_chat_controls() -> Array[PlayFabPartyChatControl] + +# Polled state (no signals; Party raises no state change for these). +func get_local_chat_indicator(user: PlayFabUser = null) -> int +func get_chat_indicator(entity_key: Dictionary, user: PlayFabUser = null) -> int +func get_chat_indicators(user: PlayFabUser = null) -> Array[Dictionary] +func get_audio_input_state(user: PlayFabUser = null) -> int +func get_audio_output_state(user: PlayFabUser = null) -> int +func is_audio_input_muted(user: PlayFabUser = null) -> bool +func get_audio_render_volume(entity_key: Dictionary, user: PlayFabUser = null) -> float +func get_chat_permissions(entity_key: Dictionary, user: PlayFabUser = null) -> int +func is_audio_muted(entity_key: Dictionary, user: PlayFabUser = null) -> bool +func is_text_muted(entity_key: Dictionary, user: PlayFabUser = null) -> bool +func get_language(user: PlayFabUser = null) -> String +func get_transcription_options(user: PlayFabUser = null) -> int +func get_text_chat_options(user: PlayFabUser = null) -> int +func get_audio_encoder_bitrate(user: PlayFabUser = null) -> int +func get_voice_audio_options(user: PlayFabUser = null) -> int +func get_text_to_speech_profiles(user: PlayFabUser = null) -> Array[PlayFabPartyTextToSpeechProfile] +func get_text_to_speech_profile(type: int, user: PlayFabUser = null) -> PlayFabPartyTextToSpeechProfile + +# Voice chat controls. +func set_audio_input_muted_async(muted: bool, user: PlayFabUser = null) -> Signal +func set_audio_render_volume_async(entity_key: Dictionary, volume: float, user: PlayFabUser = null) -> Signal +func set_language_async(language_code: String, user: PlayFabUser = null) -> Signal +func set_transcription_options_async(options: int, user: PlayFabUser = null) -> Signal +func set_text_chat_options_async(options: int, user: PlayFabUser = null) -> Signal +func set_audio_encoder_bitrate_async(bitrate: int, user: PlayFabUser = null) -> Signal +func set_voice_audio_options_async(options: int, user: PlayFabUser = null) -> Signal +func populate_text_to_speech_profiles_async(user: PlayFabUser = null) -> Signal +func set_text_to_speech_profile_async(type: int, profile_id: String, user: PlayFabUser = null) -> Signal +func synthesize_text_to_speech_async(type: int, text: String, user: PlayFabUser = null) -> Signal +``` + +Every getter returns a safe default instead of erroring when no local chat control exists yet, so titles can poll them unconditionally every frame. Only the async setters report `party_resource_not_ready`. + +```gdscript +class_name PlayFabPartyTextToSpeechProfile +extends RefCounted + +var identifier: String +var name: String +var language_code: String +var gender: int # PlayFabParty.GENDER_* ``` ```gdscript @@ -348,6 +459,12 @@ func set_permissions_async(target: PlayFabPartyChatControl, permissions: int) -> func set_audio_muted_async(target: PlayFabPartyChatControl, muted: bool) -> Signal func set_text_muted_async(target: PlayFabPartyChatControl, muted: bool) -> Signal func destroy_async() -> Signal + +# The same polled state and voice controls listed under PlayFabPartyChat are +# available directly on the chat control, without the trailing user argument. +# Remote controls additionally expose get_chat_indicator(); local controls +# expose get_local_chat_indicator() and the audio-device accessors +# (get_audio_input_selection_type(), get_audio_input_device_id(), ...). ``` ```gdscript @@ -360,11 +477,15 @@ var targets: Array[PlayFabPartyChatControl] var text: String var language_code: String var translated_text: String +var original_text: String +var options: int # PlayFabParty.CHAT_MESSAGE_OPTION_* filtering flags applied by the service var is_transcription: bool var timestamp: int var metadata: Dictionary ``` +`original_text` is the unfiltered source string sent by the remote peer. Party sets it equal to `text` when offensive-term filtering is disabled or was not needed, so compare against `options` (not against emptiness) to detect filtering. It is empty for transcriptions, which carry no original-text field. + ## Member and state-change wrappers ```gdscript @@ -618,6 +739,17 @@ self-contained slice with its own validation. GUT (API surface) + live MP orchestrator (`party.chat.mute_peer` mutes text and asserts text delivery is blocked; `party.chat.text.three_clients` validates host fan-out to both guests). +- **Phase E — chat indicators, voice controls, and network diagnostics.** Close + the gap between the wrapper and the native Party surface (an audit found only + ~30 of ~95 native methods wrapped). Land polled chat indicators, the local + voice-chat controls (input mute/push-to-talk, render volume, encoder bitrate, + voice audio options, language, transcription options, text chat options, + text-to-speech), and the two requested network diagnostics + (`get_statistics`, `get_device_connection_type`). Fix the bound-but-inert + config surfaces in the same change. **Breaking:** `PlayFabPartyConfig.metadata` + and `PlayFabPartyTextMessageConfig.language_code` / + `.translate_to_languages` are removed. Tier: non-live GUT (API surface, + detached-error behavior, constant values). ## Progress @@ -689,9 +821,59 @@ self-contained slice with its own validation. **live** on sandbox title `10D176`: `party.chat.text.round_trip`, `party.chat.mute.peer` (text mute blocks delivery), and `party.chat.text.three_clients` all green. +- **Phase E: ✅ implemented.** Chat indicators, voice-chat controls, and network + diagnostics. + - **Indicators are polled, never signalled** — Party raises no state change + when an indicator flips. `get_local_chat_indicator()`, + `get_chat_indicator(entity_key)`, and the roster helper + `get_chat_indicators()` land on `PlayFabPartyChat` (optional trailing `user`) + and on `PlayFabPartyChatControl`. All getters return safe defaults rather + than erroring when there is no local chat control, so they are safe to call + every frame. + - **Local voice controls:** `set_audio_input_muted_async` (push-to-talk — the + local mic mute was previously unwrappable; only *incoming* mute existed), + `get/set_audio_render_volume`, `get/set_audio_encoder_bitrate`, + `get/set_voice_audio_options`, plus polled `get_audio_input_state()` / + `get_audio_output_state()` so titles can distinguish "no microphone" from + "consent denied" instead of silently having no voice. + - **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`. `_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`, + `synthesize_text_to_speech_async`) with a new + `PlayFabPartyTextToSpeechProfile` value type. Profiles are **snapshotted** — + 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; + 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. + Audio-device state is cached from the `LocalChatAudioInput/OutputChanged` + state changes instead of only being `ERR_PRINT`ed. + - **Breaking:** `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 **not** wrapped in this phase (per scope): 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. + - Validated: debug build of `godot_playfab` clean, parse gate, and the + non-live GUT tier. **Live tests were not run** (no `-Live`, no + `-AllowLiveWrites`). ### Pre-existing follow-up (not Phase A) - The MP orchestrator (`tests\godot\mp_orchestrator`) exhausts the Party SDK's per-device local-user limit across its ~61 back-to-back scenarios (`party_invalid_user: can't create local user; local user limit reached`), and diff --git a/tests/godot/playfab/tests/test_core.gd b/tests/godot/playfab/tests/test_core.gd index c385108..a58fc83 100644 --- a/tests/godot/playfab/tests/test_core.gd +++ b/tests/godot/playfab/tests/test_core.gd @@ -63,6 +63,7 @@ const REGISTERED_CLASSES := [ "PlayFabParty", "PlayFabPartyConfig", "PlayFabPartyTextMessageConfig", + "PlayFabPartyTextToSpeechProfile", "PlayFabPartyMember", "PlayFabPartyChatMessage", "PlayFabPartyChatStateChange", diff --git a/tests/godot/playfab/tests/test_party.gd b/tests/godot/playfab/tests/test_party.gd index adc9ca2..5d0e780 100644 --- a/tests/godot/playfab/tests/test_party.gd +++ b/tests/godot/playfab/tests/test_party.gd @@ -9,6 +9,7 @@ const PARTY_REGISTERED_CLASSES := [ "PlayFabParty", "PlayFabPartyConfig", "PlayFabPartyTextMessageConfig", + "PlayFabPartyTextToSpeechProfile", "PlayFabPartyMember", "PlayFabPartyChatMessage", "PlayFabPartyChatStateChange", @@ -60,6 +61,33 @@ const PARTY_CHAT_METHODS := [ "set_text_muted_async", "create_local_chat_control_async", "destroy_local_chat_control_async", + "get_local_chat_indicator", + "get_chat_indicator", + "get_chat_indicators", + "is_audio_input_muted", + "set_audio_input_muted_async", + "get_audio_render_volume", + "set_audio_render_volume_async", + "get_chat_permissions", + "is_audio_muted", + "is_text_muted", + "get_language", + "set_language_async", + "get_transcription_options", + "set_transcription_options_async", + "get_text_chat_options", + "set_text_chat_options_async", + "get_audio_encoder_bitrate", + "set_audio_encoder_bitrate_async", + "get_voice_audio_options", + "set_voice_audio_options_async", + "get_audio_input_state", + "get_audio_output_state", + "populate_text_to_speech_profiles_async", + "get_text_to_speech_profiles", + "get_text_to_speech_profile", + "set_text_to_speech_profile_async", + "synthesize_text_to_speech_async", ] const PARTY_CHAT_SIGNALS := [ @@ -137,6 +165,45 @@ func test_party_stable_constants() -> void: assert_eq(get_class_constant("PlayFabParty", "CHAT_PERMISSION_RECEIVE_AUDIO"), 2, "CHAT_PERMISSION_RECEIVE_AUDIO == 2") assert_eq(get_class_constant("PlayFabParty", "CHAT_PERMISSION_RECEIVE_TEXT"), 4, "CHAT_PERMISSION_RECEIVE_TEXT == 4") + # Chat indicators are polled, not signalled. Values mirror the native + # PartyLocalChatControlChatIndicator / PartyChatControlChatIndicator enums. + assert_eq(get_class_constant("PlayFabParty", "LOCAL_CHAT_INDICATOR_SILENT"), 0, "LOCAL_CHAT_INDICATOR_SILENT == 0") + assert_eq(get_class_constant("PlayFabParty", "LOCAL_CHAT_INDICATOR_TALKING"), 1, "LOCAL_CHAT_INDICATOR_TALKING == 1") + assert_eq(get_class_constant("PlayFabParty", "LOCAL_CHAT_INDICATOR_AUDIO_INPUT_MUTED"), 2, "LOCAL_CHAT_INDICATOR_AUDIO_INPUT_MUTED == 2") + assert_eq(get_class_constant("PlayFabParty", "LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT"), 3, "LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT == 3") + + assert_eq(get_class_constant("PlayFabParty", "CHAT_INDICATOR_SILENT"), 0, "CHAT_INDICATOR_SILENT == 0") + assert_eq(get_class_constant("PlayFabParty", "CHAT_INDICATOR_TALKING"), 1, "CHAT_INDICATOR_TALKING == 1") + assert_eq(get_class_constant("PlayFabParty", "CHAT_INDICATOR_INCOMING_VOICE_DISABLED"), 2, "CHAT_INDICATOR_INCOMING_VOICE_DISABLED == 2") + assert_eq(get_class_constant("PlayFabParty", "CHAT_INDICATOR_INCOMING_COMMUNICATIONS_MUTED"), 3, "CHAT_INDICATOR_INCOMING_COMMUNICATIONS_MUTED == 3") + assert_eq(get_class_constant("PlayFabParty", "CHAT_INDICATOR_NO_REMOTE_INPUT"), 4, "CHAT_INDICATOR_NO_REMOTE_INPUT == 4") + assert_eq(get_class_constant("PlayFabParty", "CHAT_INDICATOR_REMOTE_AUDIO_INPUT_MUTED"), 5, "CHAT_INDICATOR_REMOTE_AUDIO_INPUT_MUTED == 5") + + assert_eq(get_class_constant("PlayFabParty", "AUDIO_INPUT_STATE_NO_INPUT"), 0, "AUDIO_INPUT_STATE_NO_INPUT == 0") + assert_eq(get_class_constant("PlayFabParty", "AUDIO_INPUT_STATE_INITIALIZED"), 1, "AUDIO_INPUT_STATE_INITIALIZED == 1") + assert_eq(get_class_constant("PlayFabParty", "AUDIO_INPUT_STATE_USER_CONSENT_DENIED"), 3, "AUDIO_INPUT_STATE_USER_CONSENT_DENIED == 3") + assert_eq(get_class_constant("PlayFabParty", "AUDIO_OUTPUT_STATE_NO_OUTPUT"), 0, "AUDIO_OUTPUT_STATE_NO_OUTPUT == 0") + assert_eq(get_class_constant("PlayFabParty", "AUDIO_OUTPUT_STATE_INITIALIZED"), 1, "AUDIO_OUTPUT_STATE_INITIALIZED == 1") + + assert_eq(get_class_constant("PlayFabParty", "TRANSCRIPTION_OPTION_NONE"), 0, "TRANSCRIPTION_OPTION_NONE == 0") + assert_eq(get_class_constant("PlayFabParty", "TRANSCRIPTION_OPTION_TRANSCRIBE_SELF"), 1, "TRANSCRIPTION_OPTION_TRANSCRIBE_SELF == 1") + assert_eq(get_class_constant("PlayFabParty", "TRANSCRIPTION_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE"), 16, "TRANSCRIPTION_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE == 16") + assert_eq(get_class_constant("PlayFabParty", "TEXT_CHAT_OPTION_NONE"), 0, "TEXT_CHAT_OPTION_NONE == 0") + assert_eq(get_class_constant("PlayFabParty", "TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE"), 1, "TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE == 1") + assert_eq(get_class_constant("PlayFabParty", "TEXT_CHAT_OPTION_FILTER_OFFENSIVE_TEXT"), 2, "TEXT_CHAT_OPTION_FILTER_OFFENSIVE_TEXT == 2") + + assert_eq(get_class_constant("PlayFabParty", "CHAT_MESSAGE_OPTION_NONE"), 0, "CHAT_MESSAGE_OPTION_NONE == 0") + assert_eq(get_class_constant("PlayFabParty", "CHAT_MESSAGE_OPTION_FILTERED_ENTIRE_MESSAGE"), 2, "CHAT_MESSAGE_OPTION_FILTERED_ENTIRE_MESSAGE == 2") + + assert_eq(get_class_constant("PlayFabParty", "TEXT_TO_SPEECH_TYPE_NARRATION"), 0, "TEXT_TO_SPEECH_TYPE_NARRATION == 0") + assert_eq(get_class_constant("PlayFabParty", "TEXT_TO_SPEECH_TYPE_VOICE_CHAT"), 1, "TEXT_TO_SPEECH_TYPE_VOICE_CHAT == 1") + assert_eq(get_class_constant("PlayFabParty", "GENDER_NEUTRAL"), 0, "GENDER_NEUTRAL == 0") + + assert_eq(get_class_constant("PlayFabParty", "NETWORK_STATISTIC_AVERAGE_RELAY_SERVER_ROUND_TRIP_LATENCY_MS"), 0, "NETWORK_STATISTIC_AVERAGE_RELAY_SERVER_ROUND_TRIP_LATENCY_MS == 0") + assert_eq(get_class_constant("PlayFabParty", "NETWORK_STATISTIC_CANCELED_SEND_MESSAGE_BYTES"), 15, "NETWORK_STATISTIC_CANCELED_SEND_MESSAGE_BYTES == 15") + assert_eq(get_class_constant("PlayFabParty", "DEVICE_CONNECTION_TYPE_RELAY_SERVER"), 0, "DEVICE_CONNECTION_TYPE_RELAY_SERVER == 0") + assert_eq(get_class_constant("PlayFabParty", "DEVICE_CONNECTION_TYPE_DIRECT_PEER_CONNECTION"), 1, "DEVICE_CONNECTION_TYPE_DIRECT_PEER_CONNECTION == 1") + func test_party_config_defaults() -> void: var config = instantiate_class("PlayFabPartyConfig") @@ -161,22 +228,18 @@ func test_party_config_defaults() -> void: config.enable_text_chat = false config.audio_input = "capture-device-1" config.audio_output = "render-device-1" - config.metadata = {"map": "arena"} + config.language = "en-US" assert_eq(config.max_players, 4, "PlayFabPartyConfig.max_players setter") assert_eq(config.direct_peer_connectivity, get_class_constant("PlayFabParty", "DIRECT_PEER_CONNECTIVITY_SAME_PLATFORM_TYPE"), "PlayFabPartyConfig.direct_peer_connectivity setter") assert_eq(config.invitation_id, "invite-1", "PlayFabPartyConfig.invitation_id setter") assert_eq(config.audio_input, "capture-device-1", "PlayFabPartyConfig.audio_input setter") assert_eq(config.audio_output, "render-device-1", "PlayFabPartyConfig.audio_output setter") - assert_eq(config.metadata.get("map"), "arena", "PlayFabPartyConfig.metadata setter") + assert_eq(config.language, "en-US", "PlayFabPartyConfig.language setter") var text_config = instantiate_class("PlayFabPartyTextMessageConfig") assert_object_is(text_config, "PlayFabPartyTextMessageConfig", "PlayFabPartyTextMessageConfig can be instantiated") if text_config != null: - text_config.language_code = "en-US" - text_config.translate_to_languages = PackedStringArray(["es-MX", "fr-FR"]) text_config.metadata = {"id": 12} - assert_eq(text_config.language_code, "en-US", "PlayFabPartyTextMessageConfig.language_code setter") - assert_eq(text_config.translate_to_languages.size(), 2, "PlayFabPartyTextMessageConfig.translate_to_languages setter") assert_eq(int(text_config.metadata.get("id", 0)), 12, "PlayFabPartyTextMessageConfig.metadata setter") @@ -339,6 +402,65 @@ func test_party_chat_methods_detached() -> void: await _assert_signal_error(chat.set_text_muted_async({ "id": "guest", "type": "title_player_account" }, true), "party_peer_not_connected", "Detached PlayFabPartyChat.set_text_muted_async() reports party_peer_not_connected") +func test_party_chat_polling_surface_detached() -> void: + # Chat indicators and voice-chat state are polled, never signalled. A + # detached chat surface must answer with safe defaults instead of erroring, + # so a title can poll every frame without null-checking. + var chat = instantiate_class("PlayFabPartyChat") + if chat == null: + return + + var guest := { "id": "guest", "type": "title_player_account" } + assert_eq(chat.get_local_chat_indicator(), get_class_constant("PlayFabParty", "LOCAL_CHAT_INDICATOR_NO_AUDIO_INPUT"), "Detached PlayFabPartyChat.get_local_chat_indicator() reports no audio input") + assert_eq(chat.get_chat_indicator(guest), get_class_constant("PlayFabParty", "CHAT_INDICATOR_SILENT"), "Detached PlayFabPartyChat.get_chat_indicator() reports silent") + assert_eq(chat.get_chat_indicators().size(), 0, "Detached PlayFabPartyChat.get_chat_indicators() is empty") + assert_false(chat.is_audio_input_muted(), "Detached PlayFabPartyChat.is_audio_input_muted() is false") + assert_eq(chat.get_audio_render_volume(guest), 0.0, "Detached PlayFabPartyChat.get_audio_render_volume() is 0") + assert_eq(chat.get_language(), "", "Detached PlayFabPartyChat.get_language() is empty") + assert_eq(chat.get_transcription_options(), get_class_constant("PlayFabParty", "TRANSCRIPTION_OPTION_NONE"), "Detached PlayFabPartyChat.get_transcription_options() is none") + assert_eq(chat.get_text_chat_options(), get_class_constant("PlayFabParty", "TEXT_CHAT_OPTION_NONE"), "Detached PlayFabPartyChat.get_text_chat_options() is none") + assert_eq(chat.get_audio_encoder_bitrate(), 0, "Detached PlayFabPartyChat.get_audio_encoder_bitrate() is 0") + assert_eq(chat.get_audio_input_state(), get_class_constant("PlayFabParty", "AUDIO_INPUT_STATE_NO_INPUT"), "Detached PlayFabPartyChat.get_audio_input_state() is no input") + assert_eq(chat.get_audio_output_state(), get_class_constant("PlayFabParty", "AUDIO_OUTPUT_STATE_NO_OUTPUT"), "Detached PlayFabPartyChat.get_audio_output_state() is no output") + assert_eq(chat.get_text_to_speech_profiles().size(), 0, "Detached PlayFabPartyChat.get_text_to_speech_profiles() is empty") + assert_null(chat.get_text_to_speech_profile(get_class_constant("PlayFabParty", "TEXT_TO_SPEECH_TYPE_NARRATION")), "Detached PlayFabPartyChat.get_text_to_speech_profile() is null") + + # The async setters require a local chat control and must fail deferred. + await _assert_signal_error(chat.set_audio_input_muted_async(true), "party_resource_not_ready", "Detached PlayFabPartyChat.set_audio_input_muted_async() reports party_resource_not_ready") + await _assert_signal_error(chat.set_language_async("en-US"), "party_resource_not_ready", "Detached PlayFabPartyChat.set_language_async() reports party_resource_not_ready") + await _assert_signal_error(chat.set_transcription_options_async(get_class_constant("PlayFabParty", "TRANSCRIPTION_OPTION_TRANSCRIBE_SELF")), "party_resource_not_ready", "Detached PlayFabPartyChat.set_transcription_options_async() reports party_resource_not_ready") + await _assert_signal_error(chat.set_text_chat_options_async(get_class_constant("PlayFabParty", "TEXT_CHAT_OPTION_TRANSLATE_TO_LOCAL_LANGUAGE")), "party_resource_not_ready", "Detached PlayFabPartyChat.set_text_chat_options_async() reports party_resource_not_ready") + await _assert_signal_error(chat.set_audio_encoder_bitrate_async(24000), "party_resource_not_ready", "Detached PlayFabPartyChat.set_audio_encoder_bitrate_async() reports party_resource_not_ready") + await _assert_signal_error(chat.set_voice_audio_options_async(get_class_constant("PlayFabParty", "VOICE_AUDIO_OPTION_NOISE_SUPPRESSION")), "party_resource_not_ready", "Detached PlayFabPartyChat.set_voice_audio_options_async() reports party_resource_not_ready") + await _assert_signal_error(chat.populate_text_to_speech_profiles_async(), "party_resource_not_ready", "Detached PlayFabPartyChat.populate_text_to_speech_profiles_async() reports party_resource_not_ready") + await _assert_signal_error(chat.set_text_to_speech_profile_async(get_class_constant("PlayFabParty", "TEXT_TO_SPEECH_TYPE_VOICE_CHAT"), "profile"), "party_resource_not_ready", "Detached PlayFabPartyChat.set_text_to_speech_profile_async() reports party_resource_not_ready") + await _assert_signal_error(chat.synthesize_text_to_speech_async(get_class_constant("PlayFabParty", "TEXT_TO_SPEECH_TYPE_NARRATION"), "hello"), "party_resource_not_ready", "Detached PlayFabPartyChat.synthesize_text_to_speech_async() reports party_resource_not_ready") + + +func test_party_text_to_speech_profile_contract() -> void: + var profile = instantiate_class("PlayFabPartyTextToSpeechProfile") + assert_object_is(profile, "PlayFabPartyTextToSpeechProfile", "PlayFabPartyTextToSpeechProfile can be instantiated") + if profile == null: + return + for method_name in ["get_identifier", "get_name", "get_language_code", "get_gender"]: + assert_has_method_named(profile, method_name) + assert_eq(profile.identifier, "", "PlayFabPartyTextToSpeechProfile.identifier default") + assert_eq(profile.language_code, "", "PlayFabPartyTextToSpeechProfile.language_code default") + assert_eq(profile.gender, get_class_constant("PlayFabParty", "GENDER_NEUTRAL"), "PlayFabPartyTextToSpeechProfile.gender default") + + +func test_party_network_statistics_detached() -> void: + var network = instantiate_class("PlayFabPartyNetwork") + if network == null: + return + for method_name in ["get_statistics", "get_device_connection_type"]: + assert_has_method_named(network, method_name) + # No native network handle => empty statistics and the conservative + # relay-server answer rather than a crash. + assert_eq(network.get_statistics().size(), 0, "Detached PlayFabPartyNetwork.get_statistics() is empty") + assert_eq(network.get_device_connection_type(1), get_class_constant("PlayFabParty", "DEVICE_CONNECTION_TYPE_RELAY_SERVER"), "Detached PlayFabPartyNetwork.get_device_connection_type() reports relay server") + + func test_party_chat_control_helpers() -> void: var control = instantiate_class("PlayFabPartyChatControl") if control == null: @@ -356,6 +478,29 @@ func test_party_chat_control_helpers() -> void: "set_audio_muted_async", "set_text_muted_async", "destroy_async", + "get_local_chat_indicator", + "get_chat_indicator", + "get_audio_input_state", + "get_audio_output_state", + "is_audio_input_muted", + "set_audio_input_muted_async", + "get_audio_render_volume", + "set_audio_render_volume_async", + "get_language", + "set_language_async", + "get_transcription_options", + "set_transcription_options_async", + "get_text_chat_options", + "set_text_chat_options_async", + "get_audio_encoder_bitrate", + "set_audio_encoder_bitrate_async", + "get_voice_audio_options", + "set_voice_audio_options_async", + "populate_text_to_speech_profiles_async", + "get_text_to_speech_profiles", + "get_text_to_speech_profile", + "set_text_to_speech_profile_async", + "synthesize_text_to_speech_async", ]: assert_has_method_named(control, method_name) for signal_name in ["state_changed", "message_received", "transcription_received"]: