diff --git a/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc index c24e018..79fe238 100644 --- a/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc +++ b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc @@ -19,7 +19,8 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class ACommandNotify extends Command; +class ACommandNotify extends Command + dependsOn(ChatApi); protected function BuildData(CommandDataBuilder builder) { builder.Name(P("notify")); diff --git a/sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc b/sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc index 0d0e674..ccb5206 100644 --- a/sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc +++ b/sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc @@ -1,6 +1,6 @@ /** * Set of tests for `Command` class. - * Copyright 2021 - 2022 Anton Tarasenko + * Copyright 2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * diff --git a/sources/BaseAPI/API/Unflect/UnflectApi.uc b/sources/BaseAPI/API/Unflect/UnflectApi.uc index a41e791..ed89d07 100644 --- a/sources/BaseAPI/API/Unflect/UnflectApi.uc +++ b/sources/BaseAPI/API/Unflect/UnflectApi.uc @@ -306,6 +306,7 @@ private final function bool _replaceFunction(Text oldFunctionLowerCase, Text new _.memory.Free(initialCode); } replace.script = with.script; + Log("TRULLY!" @ replace @ with); return true; } diff --git a/sources/Chat/ChatAPI.uc b/sources/Chat/ChatAPI.uc index b8601bc..3864297 100644 --- a/sources/Chat/ChatAPI.uc +++ b/sources/Chat/ChatAPI.uc @@ -1,6 +1,8 @@ /** - * API that provides functions for working with chat. - * Copyright 2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -17,112 +19,342 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class ChatAPI extends AcediaObject; +class ChatApi extends AcediaObject; +///! API for accessing chat-related events. +///! +///! # Implementation +///! +///! Signal functions that track text chat messages `OnMessage()` and `OnMessageFor()` simply +///! hook into [`BroadcastApi`] the first time such signal is requested. +///! +///! Signal function [`OnVoiceMessage()`] for tracking voice replaces a function in +///! [`KFPlayerController`] to track when they are replicated. +///! Then replaced function informs [`ChatApi`] about new voice message transmissions via +///! internal [`_EmitOnVoiceMessage()`] method. + +/// Lists voice messages built-in in the game. +enum BuiltInVoiceMessage { + // Support + BIVM_SupportMedic, + BIVM_SupportHelp, + BIVM_SupportAskForMoney, + BIVM_SupportAskForWeapon, + // Acknowledgements + BIVM_AckYes, + BIVM_AckNo, + BIVM_AckThanks, + BIVM_AckSorry, + // Alert + BIVM_AlretLookOut, + BIVM_AlretRun, + BIVM_AlretWaitForMe, + BIVM_AlretWeldTheDoors, + BIVM_AlretLetsHoleUpHere, + BIVM_AlretFollowMe, + // Direction + BIVM_DirectionGetToTheTrader, + BIVM_DirectionGoUpstairs, + BIVM_DirectionGoDownstairs, + BIVM_DirectionGetOutside, + BIVM_DirectionGetInside, + // Insult + BIVM_InsultSpecimens, + BIVM_InsultPlayers, + // Trader + BIVM_TraderCheckWhereTheShopIs, + BIVM_TraderGetClose, + BIVM_TraderShopOpen, + BIVM_TraderShopOpenFinal, + BIVM_Trader30SecondsUntilShopCloses, + BIVM_TraderShopClosed, + BIVM_TraderCompliment, + BIVM_TraderNotEnoughMoney, + BIVM_TraderCannotCarry, + BIVM_TraderHurryUp1, + BIVM_TraderHurryUp2, + // Auto + BIVM_AutoWelding, + BIVM_AutoUnwelding, + BIVM_AutoReload, + BIVM_AutoOutOfAmmo, + BIVM_AutoDosh, + BIVM_AutoStandStillTryingToHealYou, + BIVM_AutoLowOnHealth, + BIVM_AutoBloatAcid, + BIVM_AutoPatriarchCloack, + BIVM_AutoPatriarchMinigun, + BIVM_AutoPatriarchRocketLauncher, + BIVM_AutoGrabbedByClot, + BIVM_AutoFleshpoundSpotted, + BIVM_AutoGorefastSpotted, + BIVM_AutoScrakeSpotted, + BIVM_AutoSirenSpotten, + BIVM_AutoSirenScream, + BIVM_AutoStalkerSpotted, + BIVM_AutoCrawlertSpotted, + BIVM_AutoMeleeKilledAStalker, + BIVM_AutoUsingFlamethrower, + BIVM_AutoEquipHuntingShotgun, + BIVM_AutoEquipHandcannons, + BIVM_AutoEquipLAW, + BIVM_AutoEquipFireaxe, + // Fallback + BIVM_Unknown +}; + +/// Killing Floor's native voice message is defined by `name` and `byte` pair. +/// This struct is added to allow returning them as a pair. +struct NativeVoiceMessage { + var name type; + var byte index; +}; + +/// Tracks whether we've already connected to broadcast signals. var protected bool connectedToBroadcastAPI; +/// Tracks whether we've already replaced a function that allows us to catch voice messages. +var private bool replacedSendVoiceMessage; -var protected ChatAPI_OnMessage_Signal onMessageSignal; -var protected ChatAPI_OnMessageFor_Signal onMessageForSignal; +/// Auxiliary constants that store amount of values in [`BuiltInVoiceMessage`] before +/// a certain group. +/// Used for conversion between native voice messages and [`BuiltInVoiceMessage`] +var private const int VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS; +var private const int VOICE_MESSAGES_BEFORE_ALERTS; +var private const int VOICE_MESSAGES_BEFORE_DIRECTIONS; +var private const int VOICE_MESSAGES_BEFORE_INSULTS; +var private const int VOICE_MESSAGES_BEFORE_TRADER; +var private const int VOICE_MESSAGES_BEFORE_AUTO; +var private const int VOICE_MESSAGES_TOTAL; -protected function Constructor() -{ - onMessageSignal = ChatAPI_OnMessage_Signal( - _.memory.Allocate(class'ChatAPI_OnMessage_Signal')); - onMessageForSignal = ChatAPI_OnMessageFor_Signal( - _.memory.Allocate(class'ChatAPI_OnMessageFor_Signal')); +var protected ChatAPI_OnMessage_Signal onMessageSignal; +var protected ChatAPI_OnMessageFor_Signal onMessageForSignal; +var protected ChatAPI_OnVoiceMessage_Signal onVoiceMessageSignal; + +protected function Constructor() { + onMessageSignal = ChatAPI_OnMessage_Signal(_.memory.Allocate(class'ChatAPI_OnMessage_Signal')); + onMessageForSignal = + ChatAPI_OnMessageFor_Signal(_.memory.Allocate(class'ChatAPI_OnMessageFor_Signal')); + onVoiceMessageSignal = + ChatAPI_OnVoiceMessage_Signal(_.memory.Allocate(class'ChatAPI_OnVoiceMessage_Signal')); } -protected function Finalizer() -{ +protected function Finalizer() { _.memory.Free(onMessageSignal); _.memory.Free(onMessageForSignal); - onMessageSignal = none; - onMessageForSignal = none; + _.memory.Free(onVoiceMessageSignal); + onMessageSignal = none; + onMessageForSignal = none; + onVoiceMessageSignal = none; _server.unreal.broadcasts.OnHandleText(self).Disconnect(); _server.unreal.broadcasts.OnHandleTextFor(self).Disconnect(); connectedToBroadcastAPI = false; } -private final function TryConnectingBroadcastSignals() -{ - if (connectedToBroadcastAPI) { - return; - } - connectedToBroadcastAPI = true; - _server.unreal.broadcasts.OnHandleText(self).connect = HandleText; - _server.unreal.broadcasts.OnHandleTextFor(self).connect = HandleTextFor; -} - -/** - * Signal that will be emitted when a player sends a message into the chat. - * Allows to modify message before sending it, as well as prevent it from - * being sent at all. - * - * Return `false` to prevent message from being sent. - * If `false` is returned, signal propagation to the remaining handlers will - * also be interrupted. - * - * [Signature] - * bool (EPlayer sender, MutableText message, bool teamMessage) - * - * @param sender `EPlayer` that has sent the message. - * @param message Message that `sender` has sent. This is a mutable - * variable and can be modified from message will be sent. - * @param teamMessage Is this a team message - * (to be sent only to players on the same team)? - * @return Return `false` to prevent this message from being sent at all - * and `true` otherwise. Message will be sent only if all handlers will - * return `true`. - */ -/* SIGNAL */ -public function ChatAPI_OnMessage_Slot OnMessage( - AcediaObject receiver) -{ +/// Signal that will be emitted when a player sends a message into the chat. +/// +/// Allows to modify message before sending it, as well as prevent it from being sent at all. +/// +/// Return `false` to prevent message from being sent. +/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted. +/// +/// # Slot description +/// +/// bool (EPlayer sender, MutableText message, bool teamMessage) +/// +/// ## Parameters +/// +/// * [`sender`]: `EPlayer` that has sent the message. +/// * [`message`]: Message that `sender` has sent. +/// This is a mutable variable and can be modified from message will be sent. +/// * [`teamMessage`]: Is this a team message (to be sent only to players on the same team)? +/// +/// ## Returns +/// +/// Return `false` to prevent this message from being sent at all and `true` otherwise. +/// Message will be sent only if all handlers will return `true`. +public /*signal*/ function ChatAPI_OnMessage_Slot OnMessage(AcediaObject receiver) { TryConnectingBroadcastSignals(); return ChatAPI_OnMessage_Slot(onMessageSignal.NewSlot(receiver)); } -/** - * Signal that will be emitted when a player sends a message into the chat. - * Allows to modify message before sending it, as well as prevent it from - * being sent at all. - * - * Return `false` to prevent message from being sent to a specific player. - * If `false` is returned, signal propagation to the remaining handlers will - * also be interrupted. - * - * [Signature] - * bool (EPlayer receiver, EPlayer sender, BaseText message) - * - * @param receiver `EPlayer` that will receive the message. - * @param sender `EPlayer` that has sent the message. - * @param message Message that `sender` has sent. This is an immutable - * variable and cannot be changed at this point. Use `OnMessage()` - * signal function for that. - * @return Return `false` to prevent this message from being sent to - * a particular player and `true` otherwise. Message will be sent only if - * all handlers will return `true`. - * However decision whether to send message or not is made for - * every player separately. - */ -/* SIGNAL */ -public function ChatAPI_OnMessageFor_Slot OnMessageFor( - AcediaObject receiver) -{ +/// Signal that will be emitted when a player sends a message into the chat. +/// +/// Allows to modify message before sending it, as well as prevent it from being sent at all. +/// +/// Return `false` to prevent message from being sent to a specific player. +/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted. +/// +/// # Slot description +/// +/// bool (EPlayer receiver, EPlayer sender, BaseText message) +/// +/// ## Parameters +/// +/// * [`receiver`]: `EPlayer` that will receive the message. +/// * [`sender`]: `EPlayer` that has sent the message. +/// * [`message`]: Message that `sender` has sent. This is an immutable variable and cannot +/// be changed at this point. Use `OnMessage()` signal function for that. +/// +/// ## Returns +/// +/// Return `false` to prevent this message from being sent to a particular player and +/// `true` otherwise. +/// Message will be sent only if all handlers will return `true`. +/// However decision whether to send message or not is made for every player separately. +public /*signal*/ function ChatAPI_OnMessageFor_Slot OnMessageFor(AcediaObject receiver) { TryConnectingBroadcastSignals(); return ChatAPI_OnMessageFor_Slot(onMessageForSignal.NewSlot(receiver)); } +/// Signal that will be emitted when a player sends a voice message. +/// +/// # Slot description +/// +/// bool (EPlayer sender, ChatApi.BuiltInVoiceMessage message) +/// +/// ## Parameters +/// +/// * [`sender`]: `EPlayer` that has sent the voice message. +/// * [`message`]: Message that `sender` has sent. +public /*signal*/ function ChatAPI_OnVoiceMessage_Slot OnVoiceMessage(AcediaObject receiver) { + if (!replacedSendVoiceMessage) { + _.unflect.ReplaceFunction_S( + "KFMod.KFPlayerController.SendVoiceMessage", + "AcediaCore.Unflect_ChatApi_Controller.SendVoiceMessage", + "`ChatApi` was required to catch voice messages"); + replacedSendVoiceMessage = true; + } + return ChatAPI_OnVoiceMessage_Slot(onVoiceMessageSignal.NewSlot(receiver)); +} + +public /*internal*/ function NativeVoiceMessage _enumIntoNativeVoiceMessage( + BuiltInVoiceMessage voiceMessage +) { + local int enumValue; + local NativeVoiceMessage result; + + enumValue = int(voiceMessage); + if (enumValue < VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS) { + result.type = 'SUPPORT'; + result.index = enumValue; + } else if (enumValue < VOICE_MESSAGES_BEFORE_ALERTS) { + result.type = 'ACK'; + result.index = enumValue - VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS; + } else if (enumValue < VOICE_MESSAGES_BEFORE_DIRECTIONS) { + result.type = 'ALERT'; + result.index = enumValue - VOICE_MESSAGES_BEFORE_ALERTS; + } else if (enumValue < VOICE_MESSAGES_BEFORE_INSULTS) { + result.type = 'DIRECTION'; + result.index = enumValue - VOICE_MESSAGES_BEFORE_DIRECTIONS; + } else if (enumValue < VOICE_MESSAGES_BEFORE_TRADER) { + result.type = 'INSULT'; + result.index = enumValue - VOICE_MESSAGES_BEFORE_INSULTS; + } else if (enumValue < VOICE_MESSAGES_BEFORE_AUTO) { + result.type = 'TRADER'; + result.index = enumValue - VOICE_MESSAGES_BEFORE_TRADER; + if (result.index >= 5) { + result.index += 1; + } + } else if (enumValue < VOICE_MESSAGES_TOTAL) { + result.type = 'AUTO'; + result.index = enumValue - VOICE_MESSAGES_BEFORE_AUTO; + } + return result; +} + +public /*internal*/ function BuiltInVoiceMessage _nativeVoiceMessageIntoEnum( + NativeVoiceMessage voiceMessage +) { + switch (voiceMessage.type) { + case 'SUPPORT': + if (voiceMessage.index < 4) { + return BuiltInVoiceMessage(voiceMessage.index); + } + break; + case 'ACK': + if (voiceMessage.index < 4) { + return BuiltInVoiceMessage( + VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS + voiceMessage.index); + } + break; + case 'ALERT': + if (voiceMessage.index < 6) { + return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_ALERTS + voiceMessage.index); + } + break; + case 'DIRECTION': + if (voiceMessage.index < 5) { + return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_DIRECTIONS + voiceMessage.index); + } + break; + case 'INSULT': + if (voiceMessage.index < 2) { + return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_INSULTS + voiceMessage.index); + } + break; + case 'TRADER': + if (voiceMessage.index < 12) { + if (voiceMessage.index < 5) { + return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index); + } else if (voiceMessage.index > 5) { + return BuiltInVoiceMessage( + VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index - 1); + } + } + break; + case 'AUTO': + if (voiceMessage.index < 25) { + return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_AUTO + voiceMessage.index); + } + break; + default: + return BIVM_Unknown; + } + return BIVM_Unknown; +} + +public final /*internal*/ /*native*/ function _EmitOnVoiceMessage( + PlayerController sender, + name messageType, + byte messageID +) { + local EPlayer wrapper; + local NativeVoiceMessage nativeVoiceMessage; + local BuiltInVoiceMessage builtInVoiceMessage; + + if (sender == none) return; + wrapper = _.players.FromController(sender); + if (wrapper == none) return; + + nativeVoiceMessage.type = messageType; + nativeVoiceMessage.index = messageID; + builtInVoiceMessage = _nativeVoiceMessageIntoEnum(nativeVoiceMessage); + if (builtInVoiceMessage != BIVM_Unknown) { + onVoiceMessageSignal.Emit(wrapper, builtInVoiceMessage); + } + _.memory.Free(wrapper); +} + +private final function TryConnectingBroadcastSignals() { + if (connectedToBroadcastAPI) { + return; + } + connectedToBroadcastAPI = true; + _server.unreal.broadcasts.OnHandleText(self).connect = HandleText; + _server.unreal.broadcasts.OnHandleTextFor(self).connect = HandleTextFor; +} + private function bool HandleText( - Actor sender, - out string message, - name messageType, - bool teamMessage) -{ - local bool result; - local MutableText messageAsText; - local EPlayer senderPlayer; - // We only want to catch chat messages from a player + Actor sender, + out string message, + name messageType, + bool teamMessage +) { + local bool result; + local MutableText messageAsText; + local EPlayer senderPlayer; + + // We only want to catch chat messages from a player if (messageType != 'Say' && messageType != 'TeamSay') return true; senderPlayer = _.players.FromController(PlayerController(sender)); if (senderPlayer == none) return true; @@ -145,34 +377,39 @@ private function bool HandleText( } private function bool HandleTextFor( - PlayerController receiver, - Actor sender, - out string message, - name messageType) -{ - local bool result; - local Text messageAsText; - local EPlayer senderPlayer, receiverPlayer; - // We only want to catch chat messages from another player + PlayerController receiver, + Actor sender, + out string message, + name messageType +) { + local bool result; + local Text messageAsText; + local EPlayer senderPlayer, receiverPlayer; + + // We only want to catch chat messages from another player if (messageType != 'Say' && messageType != 'TeamSay') return true; senderPlayer = _.players.FromController(PlayerController(sender)); if (senderPlayer == none) return true; receiverPlayer = _.players.FromController(receiver); - if (receiverPlayer == none) - { + if (receiverPlayer == none) { _.memory.Free(senderPlayer); return true; } messageAsText = __().text.FromColoredString(message); - result = onMessageForSignal.Emit( receiverPlayer, senderPlayer, - messageAsText); + result = onMessageForSignal.Emit(receiverPlayer, senderPlayer, messageAsText); _.memory.Free(messageAsText); _.memory.Free(senderPlayer); _.memory.Free(receiverPlayer); return result; } -defaultproperties -{ +defaultproperties { + VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS = 4 + VOICE_MESSAGES_BEFORE_ALERTS = 8 + VOICE_MESSAGES_BEFORE_DIRECTIONS = 14 + VOICE_MESSAGES_BEFORE_INSULTS = 19 + VOICE_MESSAGES_BEFORE_TRADER = 21 + VOICE_MESSAGES_BEFORE_AUTO = 32 + VOICE_MESSAGES_TOTAL = 57 } \ No newline at end of file diff --git a/sources/Chat/Events/ChatAPI_OnVoiceMessage_Signal.uc b/sources/Chat/Events/ChatAPI_OnVoiceMessage_Signal.uc new file mode 100644 index 0000000..d754d10 --- /dev/null +++ b/sources/Chat/Events/ChatAPI_OnVoiceMessage_Signal.uc @@ -0,0 +1,39 @@ +/** + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class ChatAPI_OnVoiceMessage_Signal extends Signal + dependsOn(ChatApi); + +public final function Emit(EPlayer sender, ChatApi.BuiltInVoiceMessage message) { + local Slot nextSlot; + + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) { + ChatAPI_OnVoiceMessage_Slot(nextSlot).connect(sender, message); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties { + relatedSlotClass = class'ChatAPI_OnVoiceMessage_Slot' +} \ No newline at end of file diff --git a/sources/Chat/Events/ChatAPI_OnVoiceMessage_Slot.uc b/sources/Chat/Events/ChatAPI_OnVoiceMessage_Slot.uc new file mode 100644 index 0000000..790cec9 --- /dev/null +++ b/sources/Chat/Events/ChatAPI_OnVoiceMessage_Slot.uc @@ -0,0 +1,41 @@ +/** + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class ChatAPI_OnVoiceMessage_Slot extends Slot; + +delegate connect( + EPlayer sender, + ChatApi.BuiltInVoiceMessage message +) { + DummyCall(); +} + +protected function Constructor() { + connect = none; +} + +protected function Finalizer() { + super.Finalizer(); + connect = none; +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/Chat/Tests/TEST_VoiceMessages.uc b/sources/Chat/Tests/TEST_VoiceMessages.uc new file mode 100644 index 0000000..45eb051 --- /dev/null +++ b/sources/Chat/Tests/TEST_VoiceMessages.uc @@ -0,0 +1,81 @@ +/** + * Set of tests for `Command` class. + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class TEST_VoiceMessages extends TestCase + abstract + dependsOn(ChatApi); + +private static function ChatApi.NativeVoiceMessage Make(name type, int index) { + local ChatApi.NativeVoiceMessage result; + + result.type = type; + result.index = index; + return result; +} + +private static function TestEquality(name type, int index, ChatApi.BuiltInVoiceMessage builtIn) { + local ChatApi.NativeVoiceMessage nativeMessage; + + nativeMessage.type = type; + nativeMessage.index = index; + + Issue("Native voice messages are incorrectly converted into `BuiltInVoiceMessage`."); + TEST_ExpectTrue(__().chat._nativeVoiceMessageIntoEnum(nativeMessage) == builtIn); + + Issue("`BuiltInVoiceMessage`s are incorrectly converted into native voice messages."); + TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(builtIn).type == type); + TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(builtIn).index == index); +} + +protected static function TESTS() { + Context("Testing internal conversion methods between voice messages."); + Test_VoiceMessageConversion(); +} +protected static function Test_VoiceMessageConversion() { + local ChatApi.NativeVoiceMessage nothingMessage; + + nothingMessage.type = 'TRADER'; + nothingMessage.index = 5; + + TestEquality('SUPPORT', 2, BIVM_SupportAskForMoney); + TestEquality('ACK', 0, BIVM_AckYes); + TestEquality('ALERT', 5, BIVM_AlretFollowMe); + TestEquality('DIRECTION', 2, BIVM_DirectionGoDownstairs); + TestEquality('INSULT', 1, BIVM_InsultPlayers); + TestEquality('TRADER', 2, BIVM_TraderShopOpen); + TestEquality('TRADER', 4, BIVM_Trader30SecondsUntilShopCloses); + TestEquality('TRADER', 6, BIVM_TraderShopClosed); + TestEquality('TRADER', 7, BIVM_TraderCompliment); + TestEquality('TRADER', 11, BIVM_TraderHurryUp2); + TestEquality('AUTO', 0, BIVM_AutoWelding); + TestEquality('AUTO', 16, BIVM_AutoSirenScream); + TestEquality('AUTO', 24, BIVM_AutoEquipFireaxe); + + // Test unknown separately + Issue("Native voice messages are incorrectly converted into `BuiltInVoiceMessage`."); + TEST_ExpectTrue(__().chat._nativeVoiceMessageIntoEnum(nothingMessage) == BIVM_Unknown); + + Issue("`BuiltInVoiceMessage`s are incorrectly converted into native voice messages."); + TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(BIVM_Unknown).type == ''); + TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(BIVM_Unknown).index == 0); +} +defaultproperties { + caseName = "Voice messages" + caseGroup = "ChatAPI" +} \ No newline at end of file diff --git a/sources/Chat/Unflect/Unflect_ChatApi_Controller.uc b/sources/Chat/Unflect/Unflect_ChatApi_Controller.uc new file mode 100644 index 0000000..4051748 --- /dev/null +++ b/sources/Chat/Unflect/Unflect_ChatApi_Controller.uc @@ -0,0 +1,59 @@ +/** + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful,, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class Unflect_ChatApi_Controller extends KFPlayerController + dependsOn(ChatAPI); + +function SendVoiceMessage( + PlayerReplicationInfo sender, + PlayerReplicationInfo statedRecipient, + name messageType, + byte messageID, + name broadcastType, + optional Pawn soundSender, + optional vector senderLocation +) { + local Controller recepient; + local KFPlayerController kfPlayerRecepient; + + // Don't allow dead people to talk + if (pawn == none) return; + if (!AllowVoiceMessage(messageType)) return; + + class'Global'.static.GetInstance().chat._EmitOnVoiceMessage(self, messageType, messageID); + recepient = level.controllerList; + while (recepient != none) { + kfPlayerRecepient = KFPlayerController(recepient); + if (kfPlayerRecepient != none) { + kfPlayerRecepient.ClientLocationalVoiceMessage( + sender, + statedRecipient, + messagetype, + messageID, + soundSender, + senderLocation); + } + recepient = recepient.nextController; + } +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index ce9bd9c..760968f 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -60,4 +60,5 @@ defaultproperties testCases(31) = class'TEST_UTF8EncoderDecoder' testCases(32) = class'TEST_AvariceStreamReader' testCases(33) = class'TEST_Unflect' + testCases(34) = class'TEST_VoiceMessages' } \ No newline at end of file diff --git a/sources/Players/EPlayer.uc b/sources/Players/EPlayer.uc index 20dd00e..8eed122 100644 --- a/sources/Players/EPlayer.uc +++ b/sources/Players/EPlayer.uc @@ -17,7 +17,8 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class EPlayer extends EInterface; +class EPlayer extends EInterface + dependsOn(ChatApi); // How this `EPlayer` is identified by the server var private User identity; @@ -504,6 +505,19 @@ public final function /* borrow */ ConsoleWriter BorrowConsole() return consoleInstance.ForPlayer(self); } +/// Sends specified voice message from the caller player to all players in the game. +public final function SendVoiceMessage(ChatApi.BuiltInVoiceMessage voiceMessage) { + local PlayerController myController; + local ChatApi.NativeVoiceMessage nativeMessage; + + if (voiceMessage == BIVM_Unknown) return; + myController = GetController(); + if (myController == none) return; + nativeMessage = _.chat._enumIntoNativeVoiceMessage(voiceMessage); + + myController.ServerSpeech(nativeMessage.type, nativeMessage.index, ""); +} + /// Sends a text message to notify the player about a particular event or situation, displayed /// as a notification. ///