/** * Author: dkanus * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore * License: GPL * Copyright 2022-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 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; /// 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; 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() { _.memory.Free(onMessageSignal); _.memory.Free(onMessageForSignal); _.memory.Free(onVoiceMessageSignal); onMessageSignal = none; onMessageForSignal = none; onVoiceMessageSignal = none; _server.unreal.broadcasts.OnHandleText(self).Disconnect(); _server.unreal.broadcasts.OnHandleTextFor(self).Disconnect(); connectedToBroadcastAPI = false; } /// 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. /// /// # 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 /// /// void (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 if (messageType != 'Say' && messageType != 'TeamSay') return true; senderPlayer = _.players.FromController(PlayerController(sender)); if (senderPlayer == none) return true; messageAsText = __().text.FromColoredStringM(message); result = onMessageSignal.Emit(senderPlayer, messageAsText, teamMessage); message = messageAsText.ToColoredString(); // To correctly display chat messages we want to drop default color tag // at the beginning (the one `ToColoredString()` adds if first character // has no defined color). // This is a compatibility consideration with vanilla UI that expects // uncolored text. Not removing initial color tag will make chat text // appear black. if (!messageAsText.GetFormatting(0).isColored) { message = Mid(message, 4); } _.memory.Free(messageAsText); _.memory.Free(senderPlayer); return result; } 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 if (messageType != 'Say' && messageType != 'TeamSay') return true; senderPlayer = _.players.FromController(PlayerController(sender)); if (senderPlayer == none) return true; receiverPlayer = _.players.FromController(receiver); if (receiverPlayer == none) { _.memory.Free(senderPlayer); return true; } messageAsText = __().text.FromColoredString(message); result = onMessageForSignal.Emit(receiverPlayer, senderPlayer, messageAsText); _.memory.Free(messageAsText); _.memory.Free(senderPlayer); _.memory.Free(receiverPlayer); return result; } 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 }