UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

451 lines
18 KiB

/**
* Low-level API that provides set of utility methods for working with
* `BroadcastHandler`s.
* Copyright 2021 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 <https://www.gnu.org/licenses/>.
*/
class BroadcastAPI extends AcediaObject;
/**
* Defines ways to add a new `BroadcastHandler` into the `GameInfo`'s
* `BroadcastHandler` linked list.
*/
enum InjectionLevel
{
// `BroadcastHandler` will be places in the broadcast handlers'
// chain as a normal `BroadcastHandler`
// (through `RegisterBroadcastHandler()` call).
BHIJ_Registered,
// `BroadcastHandler` will not be added at all.
BHIJ_None,
// `BroadcastEventsObserver` will be injected at the very beginning of
// the broadcast handlers' chain.
BHIJ_Root
};
/**
* Describes propagated localized message.
*/
struct LocalizedMessage
{
// Every localized message is described by a class and id.
// For example, consider 'KFMod.WaitingMessage':
// if passed 'id' is '1',
// then it's supposed to be a message about new wave,
// but if passed 'id' is '2',
// then it's about completing the wave.
var class<LocalMessage> class;
var int id;
// Localized messages in unreal script can be passed along with
// optional arguments, described by variables below.
var PlayerReplicationInfo relatedPRI1;
var PlayerReplicationInfo relatedPRI2;
var Object relatedObject;
};
var private LoggerAPI.Definition infoInjectedBroadcastEventsObserver;
/**
* Called before text message is sent to any player, during the check for
* whether it is at all allowed to be broadcasted. Corresponds to
* the `HandlerAllowsBroadcast()` method from `BroadcastHandler`.
* Return `false` to prevent message from being broadcast. If a `false` is
* returned, signal propagation will be interrupted.
*
* Only guaranteed to be called for a message if `BHIJ_Root` was used to
* inject `BroadcastEventsObserver`. Otherwise it depends on what other
* `BroadcastHandler`s are added to `GameInfo`'s linked list. However for
* `BHIJ_Registered` this signal function should be more reliable than
* `OnHandleText()`, with the downside of not providing you with
* an actual message.
*
* [Signature]
* bool <slot>(Actor broadcaster, int newMessageLength)
*
* @param broadcaster `Actor` that attempts to broadcast next
* text message.
* @param newMessageLength Length of the message (amount of code points).
* @return `false` if you want to prevent message from being broadcast
* and `true` otherwise. `false` returned by one of the handlers overrides
* `true` values returned by others.
*/
/* SIGNAL */
public final function Broadcast_OnBroadcastCheck_Slot OnBroadcastCheck(
AcediaObject receiver)
{
local Signal signal;
local UnrealService service;
service = UnrealService(class'UnrealService'.static.Require());
TryInjectBroadcastHandler(service);
signal = service.GetSignal(class'Broadcast_OnBroadcastCheck_Signal');
return Broadcast_OnBroadcastCheck_Slot(signal.NewSlot(receiver));
}
/**
* Called before text message is sent to any player, but after the check
* for whether it is at all allowed to be broadcasted. Corresponds to
* the `Broadcast()` or `BroadcastTeam()` method from `BroadcastHandler` if
* `BHIJ_Root` injection method was used and to `BroadcastText()` for
* `BHIJ_Registered`.
* Return `false` to prevent message from being broadcast. If `false` is
* returned, signal propagation to the remaining handlers will also
* be interrupted.
*
* Only guaranteed to be called for a message if `BHIJ_Root` was used to
* inject `BroadcastEventsObserver`. Otherwise:
* 1. Whether it gets emitted at all depends on what other
* `BroadcastHandler`s are added to `GameInfo`'s linked list;
* 2. This event is actually inaccessible for `BroadcastEventsObserver`
* and Acedia tries to make a guess on whether it occurred based on
* parameters of `BroadcastText()` call - in some cases it can be
* called twice for the same message or not be called at all.
* Although conditions for that are exotic and unlikely.
* If you do not care about actual contents of the `message` and simply want to
* detect (and possibly prevent) message broadcast as early as possible,
* consider using `OnBroadcastCheck()` signal function instead.
*
* [Signature]
* bool <slot>(Actor sender, out string message, name type, bool teamMessage)
*
* @param sender `Actor` that attempts to broadcast next text message.
* @param message Message that is being broadcasted. Can be changed, but
* with `BHIJ_Registered` level of injection such change can actually
* affect detection of new broadcasts and lead to weird behavior.
* If one of the handler modifies the `message`, then all the handlers
* after it will get a modified version.
* @param type Type of the message.
* @param teamMessage `true` if this message is a message that is being
* broadcasted within `sender`'s team. Only works if `BHIJ_Root` injection
* method was used, otherwise, always stays `false`.
* @return `false` if you want to prevent message from being broadcast
* and `true` otherwise. `false` returned by one of the handlers overrides
* `true` values returned by others.
*/
/* SIGNAL */
public final function Broadcast_OnHandleText_Slot OnHandleText(
AcediaObject receiver)
{
local Signal signal;
local UnrealService service;
service = UnrealService(class'UnrealService'.static.Require());
TryInjectBroadcastHandler(service);
signal = service.GetSignal(class'Broadcast_OnHandleText_Signal');
return Broadcast_OnHandleText_Slot(signal.NewSlot(receiver));
}
/**
* Called before text message is sent to a particular player. Corresponds
* to the `BroadcastText()` method from `BroadcastHandler`.
* Return `false` to prevent message from being broadcast to a
* specified player. If `false` is returned, signal propagation to
* the remaining handlers will also be interrupted.
*
* [Signature]
* bool <slot>(
* PlayerController receiver,
* Actor sender,
* string message,
* name type)
*
* @param receiver Player that is about to receive message in question.
* @param sender `Actor` that attempts to broadcast next text message.
* With `BHIJ_Root` injection level an actual sender `Actor` is passed,
* instead of extracted `PlayerReplicationInfo` that is given inside
* `BroadcastText()` for `Pawn`s and `Controller`s.
* Otherwise returns `PlayerReplicationInfo` provided in
* the `BroadcastText()`.
* @param message Message that is being broadcasted.
* @param type Type of the message.
* @return `false` if you want to prevent message from being broadcast
* and `true` otherwise. `false` returned by one of the handlers overrides
* `true` values returned by others.
*/
/* SIGNAL */
public final function Broadcast_OnHandleTextFor_Slot OnHandleTextFor(
AcediaObject receiver)
{
local Signal signal;
local UnrealService service;
service = UnrealService(class'UnrealService'.static.Require());
TryInjectBroadcastHandler(service);
signal = service.GetSignal(class'Broadcast_OnHandleTextFor_Signal');
return Broadcast_OnHandleTextFor_Slot(signal.NewSlot(receiver));
}
/**
* Called before localized message is sent to any player. Corresponds to
* the `AllowBroadcastLocalized()` method from `BroadcastHandler` if
* `BHIJ_Root` injection method was used and to `BroadcastLocalized()` for
* `BHIJ_Registered`.
* Return `false` to prevent message from being broadcast. If `false` is
* returned, signal propagation for remaining handlers will also
* be interrupted.
*
* Only guaranteed to be called for a message if `BHIJ_Root` was used to
* inject `BroadcastEventsObserver`. Otherwise:
* 1. Whether it gets emitted at all depends on what other
* `BroadcastHandler`s are added to `GameInfo`'s linked list;
* 2. This event is actually inaccessible for `BroadcastEventsObserver`
* and Acedia tries to make a guess on whether it occurred based on
* parameters of `BroadcastLocalized()` call - in some cases it can be
* called twice for the same message or not be called at all.
* Although conditions for that are exotic and unlikely.
*
* [Signature]
* bool <slot>(
* Actor sender,
* LocalizedMessage packedMessage)
*
* @param sender `Actor` that attempts to broadcast next text message.
* @param packedMessage Message that is being broadcasted, represented as
* struct that contains all the normal parameters associate with
* localized messages.
* @return `false` if you want to prevent message from being broadcast
* and `true` otherwise. `false` returned by one of the handlers overrides
* `true` values returned by others.
*/
/* SIGNAL */
public final function Broadcast_OnHandleLocalized_Slot OnHandleLocalized(
AcediaObject receiver)
{
local Signal signal;
local UnrealService service;
service = UnrealService(class'UnrealService'.static.Require());
TryInjectBroadcastHandler(service);
signal = service.GetSignal(class'Broadcast_OnHandleLocalized_Signal');
return Broadcast_OnHandleLocalized_Slot(signal.NewSlot(receiver));
}
/**
* Called before localized message is sent to a particular player.
* Corresponds to the `BroadcastLocalized()` method from `BroadcastHandler`.
* Return `false` to prevent message from being broadcast to a
* specified player. If `false` is returned, signal propagation to
* the remaining handlers will also be interrupted.
*
* [Signature]
* bool <slot>(
* PlayerController receiver,
* Actor sender,
* LocalizedMessage packedMessage)
*
* @param receiver Player that is about to receive message in question.
* @param sender `Actor` that attempts to broadcast next localized
* message. Unlike `OnHandleTextFor()`, this parameter always corresponds
* to the real sender, regardless of the injection level.
* @param packedMessage Message that is being broadcasted, represented as
* struct that contains all the normal parameters associate with
* localized messages.
* @return `false` if you want to prevent message from being broadcast
* and `true` otherwise. `false` returned by one of the handlers overrides
* `true` values returned by others.
*/
/* SIGNAL */
public final function Broadcast_OnHandleLocalizedFor_Slot OnHandleLocalizedFor(
AcediaObject receiver)
{
local Signal signal;
local UnrealService service;
service = UnrealService(class'UnrealService'.static.Require());
TryInjectBroadcastHandler(service);
signal = service.GetSignal(class'Broadcast_OnHandleLocalizedFor_Signal');
return Broadcast_OnHandleLocalizedFor_Slot(signal.NewSlot(receiver));
}
private final function TryInjectBroadcastHandler(UnrealService service)
{
local InjectionLevel usedLevel;
local BroadcastSideEffect sideEffect;
local BroadcastEventsObserver broadcastObserver;
if (IsAdded(class'BroadcastEventsObserver')) {
return;
}
usedLevel = class'BroadcastEventsObserver'.default.usedInjectionLevel;
broadcastObserver = BroadcastEventsObserver(_.unreal.broadcasts.Add(
class'BroadcastEventsObserver', usedLevel));
if (broadcastObserver != none)
{
broadcastObserver.Initialize(service);
sideEffect =
BroadcastSideEffect(_.memory.Allocate(class'BroadcastSideEffect'));
sideEffect.Initialize(usedLevel);
_server.sideEffects.Add(sideEffect);
_.memory.Free(sideEffect);
_.logger
.Auto(infoInjectedBroadcastEventsObserver)
.Arg(InjectionLevelIntoText(usedLevel));
}
}
private final function Text InjectionLevelIntoText(
InjectionLevel injectionLevel)
{
if (injectionLevel == BHIJ_Root) {
return P("BHIJ_Root");
}
if (injectionLevel == BHIJ_Registered) {
return P("BHIJ_Registered");
}
return P("BHIJ_None");
}
/**
* Adds new `BroadcastHandler` class to the current `GameInfo`.
* Does nothing if given `BroadcastHandler` class was already added before.
*
* @param newBHClass Class of `BroadcastHandler` to add.
* @return `BroadcastHandler` instance if it was added and `none` otherwise.
*/
public final function BroadcastHandler Add(
class<BroadcastHandler> newBHClass,
optional InjectionLevel injectionLevel)
{
local LevelInfo level;
local BroadcastHandler newBroadcastHandler;
if (injectionLevel == BHIJ_None) return none;
level = _.unreal.GetLevel();
if (level == none || level.game == none) return none;
if (IsAdded(newBHClass)) return none;
// For some reason `default.nextBroadcastHandlerClass` variable can be
// auto-set after the level switch.
// I don't know why, I don't know when exactly, but not resetting it
// can lead to certain issues, including infinite recursion crashes.
class'BroadcastHandler'.default.nextBroadcastHandlerClass = none;
newBroadcastHandler = class'ServerLevelCore'.static
.GetInstance()
.Spawn(newBHClass);
if (injectionLevel == BHIJ_Registered)
{
// There is guaranteed to be SOME broadcast handler
level.game.broadcastHandler
.RegisterBroadcastHandler(newBroadcastHandler);
return newBroadcastHandler;
}
// Here `injectionLevel == BHIJ_Root` holds.
// Swap out level's first handler with ours
// (needs to be done for both actor reference and it's class)
newBroadcastHandler.nextBroadcastHandler = level.game.broadcastHandler;
newBroadcastHandler.nextBroadcastHandlerClass = level.game.broadcastClass;
level.game.broadcastHandler = newBroadcastHandler;
level.game.broadcastClass = newBHClass;
return newBroadcastHandler;
}
/**
* Removes given `BroadcastHandler` class from the current `GameInfo`,
* if it is active. Does nothing otherwise.
*
* @param BHClassToRemove Class of `BroadcastHandler` to try and remove.
* @return `true` if `BHClassToRemove` was removed and `false` otherwise
* (if they were not active in the first place).
*/
public final function bool Remove(class<BroadcastHandler> BHClassToRemove)
{
local LevelInfo level;
local BroadcastHandler previousBH, currentBH;
level = _.unreal.GetLevel();
if (level == none || level.game == none) {
return false;
}
currentBH = level.game.broadcastHandler;
if (currentBH == none) {
return false;
}
// Special case of our `BroadcastHandler` being inserted in the root
if (currentBH == BHClassToRemove)
{
level.game.broadcastHandler = currentBH.nextBroadcastHandler;
level.game.broadcastClass = currentBH.nextBroadcastHandlerClass;
currentBH.Destroy();
return true;
}
// And after the root
previousBH = currentBH;
currentBH = currentBH.nextBroadcastHandler;
while (currentBH != none)
{
if (currentBH.class != BHClassToRemove)
{
previousBH = currentBH;
currentBH = currentBH.nextBroadcastHandler;
}
else
{
previousBH.nextBroadcastHandler =
currentBH.nextBroadcastHandler;
previousBH.default.nextBroadcastHandlerClass =
currentBH.default.nextBroadcastHandlerClass;
previousBH.nextBroadcastHandlerClass =
currentBH.nextBroadcastHandlerClass;
currentBH.default.nextBroadcastHandlerClass = none;
currentBH.Destroy();
return true;
}
}
return false;
}
/**
* Finds given class of `BroadcastHandler` if it's currently active in
* `GameInfo`. Returns `none` otherwise.
*
* @param BHClassToFind Class of `BroadcastHandler` to find.
* @return `BroadcastHandler` instance of given class `BHClassToFind`, that is
* added to `GameInfo`'s linked list and `none` if no such
* `BroadcastHandler` is currently in the list.
*/
public final function BroadcastHandler FindInstance(
class<BroadcastHandler> BHClassToFind)
{
local BroadcastHandler BHIter;
if (BHClassToFind == none) {
return none;
}
BHIter = _.unreal.GetGameType().broadcastHandler;
while (BHIter != none)
{
if (BHIter.class == BHClassToFind) {
return BHIter;
}
BHIter = BHIter.nextBroadcastHandler;
}
return none;
}
/**
* Checks if given class of `BroadcastHandler` is currently active in
* `GameInfo`.
*
* @param rulesClassToCheck Class of rules to check for.
* @return `true` if `GameRules` are active and `false` otherwise.
*/
public final function bool IsAdded(class<BroadcastHandler> BHClassToFind)
{
return (FindInstance(BHClassToFind) != none);
}
defaultproperties
{
infoInjectedBroadcastEventsObserver = (l=LOG_Info,m="Injected AcediaCore's `BroadcastEventsObserver` with level `%1`.")
}