diff --git a/sources/Aliases/AliasSource.uc b/sources/Aliases/AliasSource.uc
index 1d6b542..b97bc46 100644
--- a/sources/Aliases/AliasSource.uc
+++ b/sources/Aliases/AliasSource.uc
@@ -68,6 +68,13 @@ protected function OnCreated()
HashValidAliasesFromPerObjectConfig();
}
+protected function OnDestroyed()
+{
+ loadedAliasObjects.length = 0;
+ _.memory.Free(aliasHash);
+ aliasHash = none;
+}
+
// Ensures that our `Aliases` class is properly linked with this
// source's class. Logs failure otherwise.
private final function bool AssertAliasesClassIsOwnedByThisSource()
diff --git a/sources/Commands/BroadcastListener_Commands.uc b/sources/Commands/BroadcastListener_Commands.uc
deleted file mode 100644
index 458d514..0000000
--- a/sources/Commands/BroadcastListener_Commands.uc
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Overloaded broadcast events listener to catch commands input from
- * the in-game chat.
- * Copyright 2020 - 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 .
- */
-class BroadcastListener_Commands extends BroadcastListenerBase
- abstract;
-
-// TODO: reimplement with even to provide `APlayer` in the first place
-static function bool HandleText(
- Actor sender,
- out string message,
- optional name messageType)
-{
- local Text messageAsText;
- local APlayer callerPlayer;
- local Parser parser;
- local Commands_Feature commandFeature;
- local PlayerService service;
- // We only want to catch chat messages
- // and only if `Commands` feature is active
- if (messageType != 'Say') return true;
- commandFeature =
- Commands_Feature(class'Commands_Feature'.static.GetInstance());
- if (commandFeature == none) return true;
- if (!commandFeature.UsingChatInput()) return true;
- // We are only interested in messages that start with "!"
- parser = __().text.ParseString(message);
- if (!parser.Match(P("!")).Ok())
- {
- parser.FreeSelf();
- // Convert color tags into colors
- messageAsText = __().text.FromFormattedString(message);
- message = messageAsText.ToColoredString(,, __().color.White);
- messageAsText.FreeSelf();
- return true;
- }
- // Extract `APlayer` from the `sender`
- service = PlayerService(class'PlayerService'.static.Require());
- if (service != none) {
- callerPlayer = service.GetPlayer(PlayerController(sender));
- }
- // Pass input to command feature
- commandFeature.HandleInput(parser, callerPlayer);
- parser.FreeSelf();
- return false;
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/Commands/BuiltInCommands/ACommandTest.uc b/sources/Commands/BuiltInCommands/ACommandTest.uc
new file mode 100644
index 0000000..731d232
--- /dev/null
+++ b/sources/Commands/BuiltInCommands/ACommandTest.uc
@@ -0,0 +1,72 @@
+/**
+ * Command for changing nickname of the player.
+ * 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 .
+ */
+class ACommandTest extends Command;
+
+protected function BuildData(CommandDataBuilder builder)
+{
+ builder.Name(P("test")).Summary(P("Tests various stuff. Simply call it."))
+ .OptionalParams()
+ .ParamText(P("option"));
+}
+
+protected function Executed(CommandCall result)
+{
+ local Parser parser;
+ local AssociativeArray root;
+ /*local int i;
+ local WeaponLocker lol;
+ local array aaa;
+ local Text message;
+ local Timer testTimer;
+ message = _.text.FromString("Is lobby?" @ _.kf.IsInLobby() @
+ "Is pre game?" @ _.kf.IsInPreGame() @
+ "Is trader?" @ _.kf.IsTraderActive() @
+ "Is wave?" @ _.kf.IsWaveActive() @
+ "Is finished?" @ _.kf.IsGameFinished() @
+ "Is wipe?" @ _.kf.IsWipe());
+ _.console.ForAll().WriteLine(message);
+ testTimer = Timer(_.memory.Allocate(class'Timer'));
+ testTimer.SetInterval(result.GetParameters().GetInt(P("add")));
+ testTimer.Start();
+ testTimer.OnElapsed(self).connect = OnTick;
+ testTimer.SetAutoReset(true);
+ for (i = 0; i < 100; i += 1) {
+ class'WeaponLocker'.default.bCollideWorld = false;
+ class'WeaponLocker'.default.bBlockActors = false;
+ lol = WeaponLocker(_.memory.Allocate(class'WeaponLocker'));
+ aaa[i] = lol;
+ Log("HUH" @ lol.Destroy());
+ class'WeaponLocker'.default.bCollideWorld = true;
+ class'WeaponLocker'.default.bBlockActors = true;
+ }
+ for (i = 0; i < 100; i += 1) {
+ if (aaa[i] != none)
+ {
+ Log("UMBRA" @ aaa[i]);
+ }
+ }*/
+ parser = _.text.ParseString("{\"innerObject\":{\"my_bool\":true,\"array\":[\"Engine.Actor\",false,null,{\"something \\\"here\\\"\":\"yes\",\"maybe\":0.003},56.6],\"one more\":{\"nope\":324532,\"whatever\":false,\"o rly?\":\"ya rly\"},\"my_int\":-9823452},\"some_var\":-7.32,\"another_var\":\"aye!\"}");
+ root = _.json.ParseObjectWith(parser);
+ result.GetCallerPlayer().Console().WriteLine(_.json.PrettyPrint(root));
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Commands/Command.uc b/sources/Commands/Command.uc
index 9d3fd1b..80a805b 100644
--- a/sources/Commands/Command.uc
+++ b/sources/Commands/Command.uc
@@ -165,6 +165,47 @@ protected function Constructor()
dataBuilder = none;
}
+protected function Finalizer()
+{
+ local int i;
+ local array subCommands;
+ local array options;
+ _.memory.Free(commandData.name);
+ _.memory.Free(commandData.summary);
+ subCommands = commandData.subCommands;
+ for (i = 0; i < options.length; i += 1)
+ {
+ _.memory.Free(subCommands[i].name);
+ _.memory.Free(subCommands[i].description);
+ CleanParameters(subCommands[i].required);
+ CleanParameters(subCommands[i].optional);
+ subCommands[i].required.length = 0;
+ subCommands[i].optional.length = 0;
+ }
+ commandData.subCommands.length = 0;
+ options = commandData.options;
+ for (i = 0; i < options.length; i += 1)
+ {
+ _.memory.Free(options[i].longName);
+ _.memory.Free(options[i].description);
+ CleanParameters(options[i].required);
+ CleanParameters(options[i].optional);
+ options[i].required.length = 0;
+ options[i].optional.length = 0;
+ }
+ commandData.options.length = 0;
+}
+
+private final function CleanParameters(array parameters)
+{
+ local int i;
+ for (i = 0; i < parameters.length; i += 1)
+ {
+ _.memory.Free(parameters[i].displayName);
+ _.memory.Free(parameters[i].variableName);
+ }
+}
+
/**
* Overload this method to use `builder` to define parameters and options for
* your command.
diff --git a/sources/Commands/Commands.uc b/sources/Commands/Commands.uc
index 7321612..45b2654 100644
--- a/sources/Commands/Commands.uc
+++ b/sources/Commands/Commands.uc
@@ -46,4 +46,5 @@ protected function DefaultIt()
defaultproperties
{
configName = "AcediaSystem"
+ useChatInput = true
}
\ No newline at end of file
diff --git a/sources/Commands/Commands_Feature.uc b/sources/Commands/Commands_Feature.uc
index 2782539..cd8de52 100644
--- a/sources/Commands/Commands_Feature.uc
+++ b/sources/Commands/Commands_Feature.uc
@@ -37,6 +37,7 @@ var LoggerAPI.Definition errCommandDuplicate;
protected function OnEnabled()
{
registeredCommands = _.collections.EmptyAssociativeArray();
+ _.unreal.broadcasts.OnHandleText(self).connect = HandleText;
// Macro selector
commandDelimiters[0] = P("@");
// Key selector
@@ -49,6 +50,7 @@ protected function OnEnabled()
protected function OnDisabled()
{
+ _.unreal.broadcasts.OnHandleText(self).Disconnect();
_.memory.Free(registeredCommands);
registeredCommands = none;
commandDelimiters.length = 0;
@@ -175,9 +177,44 @@ public final function HandleInput(Parser parser, APlayer callerPlayer)
}
}
+function bool HandleText(
+ Actor sender,
+ out string message,
+ name messageType,
+ bool teamMessage)
+{
+ local Text messageAsText;
+ local APlayer callerPlayer;
+ local Parser parser;
+ local PlayerService service;
+ // We only want to catch chat messages
+ // and only if `Commands` feature is active
+ if (messageType != 'Say') return true;
+ if (!UsingChatInput()) return true;
+ // We are only interested in messages that start with "!"
+ parser = __().text.ParseString(message);
+ if (!parser.Match(P("!")).Ok())
+ {
+ parser.FreeSelf();
+ // Convert color tags into colors
+ messageAsText = __().text.FromFormattedString(message);
+ message = messageAsText.ToColoredString(,, __().color.White);
+ messageAsText.FreeSelf();
+ return true;
+ }
+ // Extract `APlayer` from the `sender`
+ service = PlayerService(class'PlayerService'.static.Require());
+ if (service != none) {
+ callerPlayer = service.GetPlayer(PlayerController(sender));
+ }
+ // Pass input to command feature
+ HandleInput(parser, callerPlayer);
+ parser.FreeSelf();
+ return false;
+}
+
defaultproperties
{
configClass = class'Commands'
- requiredListeners(0) = class'BroadcastListener_Commands'
errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.")
}
\ No newline at end of file
diff --git a/sources/Config/AcediaConfig.uc b/sources/Config/AcediaConfig.uc
index 0a9516f..1228d1b 100644
--- a/sources/Config/AcediaConfig.uc
+++ b/sources/Config/AcediaConfig.uc
@@ -91,6 +91,12 @@ protected function FromData(AssociativeArray source) {}
*/
protected function DefaultIt() {}
+protected static function StaticFinalizer()
+{
+ __().memory.Free(default.existingConfigs);
+ default.existingConfigs = none;
+}
+
/**
* This reads all of the `AcediaConfig`'s settings objects into internal
* storage. Must be called before any other methods. Actual loading might be
@@ -104,6 +110,8 @@ public static function Initialize()
if (default.existingConfigs != none) {
return;
}
+ CoreService(class'CoreService'.static.GetInstance())
+ ._registerObjectClass(default.class);
default.existingConfigs = __().collections.EmptyAssociativeArray();
names = GetPerObjectNames( default.configName, string(default.class.name),
MaxInt);
@@ -160,6 +168,7 @@ public final static function bool NewConfig(Text name)
}
newConfig =
new(none, NameToStorageVersion(name.ToPlainString())) default.class;
+ newConfig._ = __();
newConfig.DefaultIt();
newConfig.SaveConfig();
default.existingConfigs.SetItem(name, newConfig);
@@ -254,6 +263,7 @@ public final static function AcediaConfig GetConfigInstance(Text name)
{
configEntry.value =
new(none, NameToStorageVersion(name.ToPlainString())) default.class;
+ configEntry.value._ = __();
default.existingConfigs.SetItem(configEntry.key, configEntry.value);
}
__().memory.Free(name);
diff --git a/sources/CoreService.uc b/sources/CoreService.uc
index c84678e..4675cd8 100644
--- a/sources/CoreService.uc
+++ b/sources/CoreService.uc
@@ -1,8 +1,9 @@
/**
* Core service that is always running alongside Acedia framework, must be
* created by a launcher.
- * Does nothing, simply used for spawning `Actor`s.
- * Copyright 2020 Anton Tarasenko
+ * Used for booting up and shutting down Acedia.
+ * Also used for spawning `Actor`s as the only must-have `Service`.
+ * Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -19,11 +20,262 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class CoreService extends Service;
+class CoreService extends Service
+ dependson(BroadcastEventsObserver);
+
+// Package's manifest is supposed to always have a name of
+// ".Manifest", this variable stores the ".Manifest" part
+var private const string manifestSuffix;
+// Classes that will need to do some cleaning before Acedia shuts down
+var private array< class > usedObjectClasses;
+var private array< class > usedActorClasses;
+// `Singleton`s are handled as a special case and cleaned up after
+// the rest of the classes.
+var private array< class > usedSingletonClasses;
+
+var array packagesToLoad;
+
+var private LoggerAPI.Definition infoLoadingPackage;
+var private LoggerAPI.Definition infoBootingUp, infoBootingUpFinished;
+var private LoggerAPI.Definition infoShuttingDown;
+var private LoggerAPI.Definition errorNoManifest, errorCannotRunTests;
+
+// We do not implement `OnShutdown()`, because total Acedia's clean up
+// is supposed to happen before that event.
+protected function OnCreated()
+{
+ BootUp();
+ default.packagesToLoad.length = 0;
+}
+
+/**
+ * Static method that starts everything needed by Acedia framework to function.
+ * Must be called before attempting to use any of the Acedia's functionality.
+ *
+ * Acedia needs to be able to spawn actors and for that it first needs to
+ * spawn `CoreService`. To make that possible you need to provide
+ * an `Actor` instance from current level. It can be any valid actor.
+ *
+ * @param source Valid actor instance that Acedia will use to
+ * spawn `CoreService`
+ * @param packages List of acedia packages to load.
+ * Using array of `string`s since Acedia's `Text` wouldn't yet
+ * be available.
+ */
+public final static function LaunchAcedia(Actor source, array packages)
+{
+ default.packagesToLoad = packages;
+ default.blockSpawning = false;
+ // Actual work will be done inside `BootUp()` private method that will be
+ // called from `OnCreated()` event.
+ source.Spawn(class'CoreService');
+ default.blockSpawning = true;
+}
+
+/**
+ * Shuts down Acedia, cleaning up created actors, default values,
+ * changes made to the standard game classes, etc..
+ *
+ * This method must be called before the level change (map change), otherwise
+ * Acedia is not guaranteed to work on the next map and you might
+ * even experience game crashes.
+ */
+public final function ShutdownAcedia()
+{
+ local int i;
+ local AcediaActor nextActor;
+ local MemoryService memoryService;
+ _.logger.Auto(infoShuttingDown);
+ memoryService = MemoryService(class'MemoryService'.static.GetInstance());
+ // Turn off gameplay-related stuff first
+ class'Global'.static.GetInstance().DropGameplayAPI();
+ // Get rid of actors
+ foreach AllActors(class'AcediaActor', nextActor)
+ {
+ if (nextActor == self) continue;
+ if (nextActor == memoryService) continue;
+ nextActor.Destroy();
+ }
+ // Clean all used classes, except for singletons
+ for (i = 0; i < usedObjectClasses.length; i += 1) {
+ usedObjectClasses[i].static._cleanup();
+ }
+ for (i = 0; i < usedActorClasses.length; i += 1) {
+ usedActorClasses[i].static._cleanup();
+ }
+ // Remove remaining objects
+ _.unreal.broadcasts.Remove(class'BroadcastEventsObserver');
+ memoryService.ClearAll();
+ // Finally clean up singletons
+ for (i = 0; i < usedSingletonClasses.length; i += 1) {
+ usedSingletonClasses[i].static._cleanup();
+ }
+ // Clean API
+ class'Global'.static.GetInstance().DropCoreAPI();
+ _ = none;
+ // Get rid of the `MemoryService` and `CoreService` last
+ memoryService.Destroy();
+ Destroy();
+ Log("Acedia has shut down.");
+}
+
+// Loads packages, injects broadcast handler and optionally runs tests
+private final function BootUp()
+{
+ local int i;
+ local Text nextPackageName;
+ local class<_manifest> nextManifest;
+ _.logger.Auto(infoBootingUp);
+ LoadManifest(class'AcediaCore_0_2.Manifest');
+ // Load packages
+ for (i = 0; i < packagesToLoad.length; i += 1)
+ {
+ nextPackageName = _.text.FromString(packagesToLoad[i]);
+ _.logger.Auto(infoLoadingPackage).Arg(nextPackageName.Copy());
+ nextManifest = LoadManifestClass(packagesToLoad[i]);
+ if (nextManifest == none)
+ {
+ _.logger.Auto(errorNoManifest).Arg(nextPackageName.Copy());
+ continue;
+ }
+ LoadManifest(nextManifest);
+ _.memory.Free(nextPackageName);
+ }
+ nextPackageName = none;
+ _.logger.Auto(infoBootingUpFinished);
+ // Other initialization
+ class'UnrealService'.static.Require();
+ if (class'TestingService'.default.runTestsOnStartUp) {
+ RunStartUpTests();
+ }
+}
+
+private final function LoadManifest(class<_manifest> manifestClass)
+{
+ local int i;
+ for (i = 0; i < manifestClass.default.aliasSources.length; i += 1)
+ {
+ if (manifestClass.default.aliasSources[i] == none) continue;
+ _.memory.Allocate(manifestClass.default.aliasSources[i]);
+ }
+ LaunchServicesAndFeatures(manifestClass);
+ if (class'Commands_Feature'.static.IsEnabled()) {
+ RegisterCommands(manifestClass);
+ }
+ for (i = 0; i < manifestClass.default.testCases.length; i += 1)
+ {
+ class'TestingService'.static
+ .RegisterTestCase(manifestClass.default.testCases[i]);
+ }
+}
+
+private final function class<_manifest> LoadManifestClass(string packageName)
+{
+ return class<_manifest>(DynamicLoadObject( packageName $ manifestSuffix,
+ class'Class', true));
+}
+
+private final function RegisterCommands(class<_manifest> manifestClass)
+{
+ local int i;
+ local Commands_Feature commandsFeature;
+ commandsFeature =
+ Commands_Feature(class'Commands_Feature'.static.GetInstance());
+ for (i = 0; i < manifestClass.default.commands.length; i += 1)
+ {
+ if (manifestClass.default.commands[i] == none) continue;
+ commandsFeature.RegisterCommand(manifestClass.default.commands[i]);
+ }
+}
+
+private final function LaunchServicesAndFeatures(class<_manifest> manifestClass)
+{
+ local int i;
+ local Text autoConfigName;
+ // Services
+ for (i = 0; i < manifestClass.default.services.length; i += 1)
+ {
+ if (manifestClass.default.services[i] == none) continue;
+ manifestClass.default.services[i].static.Require();
+ }
+ // Features
+ for (i = 0; i < manifestClass.default.features.length; i += 1)
+ {
+ if (manifestClass.default.features[i] == none) continue;
+ manifestClass.default.features[i].static.LoadConfigs();
+ autoConfigName =
+ manifestClass.default.features[i].static.GetAutoEnabledConfig();
+ if (autoConfigName != none) {
+ manifestClass.default.features[i].static.EnableMe(autoConfigName);
+ }
+ _.memory.Free(autoConfigName);
+ }
+}
+
+private final function RunStartUpTests()
+{
+ local TestingService testService;
+ testService = TestingService(class'TestingService'.static.Require());
+ testService.PrepareTests();
+ if (testService.filterTestsByName) {
+ testService.FilterByName(testService.requiredName);
+ }
+ if (testService.filterTestsByGroup) {
+ testService.FilterByGroup(testService.requiredGroup);
+ }
+ if (!testService.Run()) {
+ _.logger.Auto(errorCannotRunTests);
+ }
+}
+
+/**
+ * Registers class derived from `AcediaObject` for clean up when
+ * Acedia shuts down.
+ *
+ * Does not check for duplicates.
+ *
+ * This is an internal function and should not be used outside of
+ * AcediaCore package.
+ */
+public final function _registerObjectClass(class classToClean)
+{
+ if (classToClean != none) {
+ usedObjectClasses[usedObjectClasses.length] = classToClean;
+ }
+}
+
+/**
+ * Registers class derived from `AcediaActor` for clean up when
+ * Acedia shuts down.
+ *
+ * Does not check for duplicates.
+ *
+ * This is an internal function and should not be used outside of
+ * AcediaCore package.
+ */
+public final function _registerActorClass(class classToClean)
+{
+ local class singletonClass;
+ if (classToClean == none) {
+ return;
+ }
+ singletonClass = class(classToClean);
+ if (singletonClass != none) {
+ usedSingletonClasses[usedSingletonClasses.length] = singletonClass;
+ }
+ else {
+ usedActorClasses[usedActorClasses.length] = classToClean;
+ }
+}
defaultproperties
{
- // Since `CoreService` is what we use to start spawning `Actor`s,
- // we have to allow launcher to spawn it with `Spawn()` call
- blockSpawning = false
+ manifestSuffix = ".Manifest"
+
+ infoBootingUp = (l=LOG_Info,m="Initializing Acedia.")
+ infoBootingUpFinished = (l=LOG_Info,m="Acedia initialized.")
+ infoShuttingDown = (l=LOG_Info,m="Shutting down Acedia.")
+ infoLoadingPackage = (l=LOG_Info,m="BLoading package \"%1\".")
+ errorNoManifest = (l=LOG_Error,m="Cannot load `Manifest` for package \"%1\". Check if it's missing or if its name is spelled incorrectly.")
+ errorCannotRunTests = (l=LOG_Error,m="Could not perform Acedia's tests.")
}
\ No newline at end of file
diff --git a/sources/Data/Collections/AssociativeArray.uc b/sources/Data/Collections/AssociativeArray.uc
index 80a18e6..be9ecbd 100644
--- a/sources/Data/Collections/AssociativeArray.uc
+++ b/sources/Data/Collections/AssociativeArray.uc
@@ -46,15 +46,20 @@ var private array hashTable;
// If one of the keys was deallocated outside of `AssociativeArray`,
// this value may overestimate actual amount of elements.
var private int storedElementCount;
+// Lower limit on hash table capacity, can be changed by the user.
+var private int minimalCapacity;
-// Lower and upper limits on hash table capacity.
-var private const int MINIMUM_CAPACITY;
-var private const int MAXIMUM_CAPACITY;
+// hard lower and upper limits on hash table size, constant.
+var private const int MINIMUM_SIZE;
+var private const int MAXIMUM_SIZE;
// Minimum and maximum allowed density of elements
// (`storedElementCount / hashTable.length`).
// If density falls outside this range, - we have to resize hash table to
// get into (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds, as long as it does not
-// violate capacity restrictions.
+// violate hard size restrictions.
+// Actual size changes in multipliers of 2, so
+// `MINIMUM_DENSITY * 2 < MAXIMUM_DENSITY` must hold or we will constantly
+// oscillate outside of (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds.
var private const float MINIMUM_DENSITY;
var private const float MAXIMUM_DENSITY;
@@ -71,7 +76,7 @@ struct Index
protected function Constructor()
{
- UpdateHashTableCapacity();
+ UpdateHashTableSize();
}
protected function Finalizer()
@@ -187,31 +192,36 @@ private final function CleanBucket(out Bucket bucketToClean)
bucketToClean.entries = bucketEntries;
}
-// Checks if we need to change our current capacity and does so if needed
-private final function UpdateHashTableCapacity()
+// Checks if we need to change our current hash table size
+// and does so if needed
+private final function UpdateHashTableSize()
{
- local int oldCapacity, newCapacity;
- oldCapacity = hashTable.length;
- // Calculate new capacity (and whether it is needed) based on amount of
- // stored properties and current capacity
- newCapacity = oldCapacity;
- if (storedElementCount < newCapacity * MINIMUM_DENSITY) {
- newCapacity /= 2;
+ local int oldSize, newSize;
+ oldSize = hashTable.length;
+ // Calculate new size (and whether it is needed) based on amount of
+ // stored properties and current size
+ newSize = oldSize;
+ if (storedElementCount < newSize * MINIMUM_DENSITY) {
+ newSize /= 2;
}
- if (storedElementCount > newCapacity * MAXIMUM_DENSITY) {
- newCapacity *= 2;
+ else if (storedElementCount > newSize * MAXIMUM_DENSITY) {
+ newSize *= 2;
}
- // Enforce our limits
- newCapacity = Clamp(newCapacity, MINIMUM_CAPACITY, MAXIMUM_CAPACITY);
+ // `table_density = items_amount / table_size`, so to store at least
+ // `items_amount = minimalCapacity` without making table too dense we need
+ // `table_size = minimalCapacity / MAXIMUM_DENSITY`.
+ newSize = Max(newSize, Ceil(minimalCapacity / MAXIMUM_DENSITY));
+ // But everything must fall into the set hard limits
+ newSize = Clamp(newSize, MINIMUM_SIZE, MAXIMUM_SIZE);
// Only resize if difference is huge enough or table does not exists yet
- if (newCapacity != oldCapacity) {
- ResizeHashTable(newCapacity);
+ if (newSize != oldSize) {
+ ResizeHashTable(newSize);
}
}
// Changes size of the hash table, does not check any limits,
-// does not check if `newCapacity` is a valid capacity (`newCapacity > 0`).
-private final function ResizeHashTable(int newCapacity)
+// does not check if `newSize` is a valid size (`newSize > 0`).
+private final function ResizeHashTable(int newSize)
{
local int i, j;
local int newBucketIndex, newEntryIndex;
@@ -220,12 +230,13 @@ private final function ResizeHashTable(int newCapacity)
oldHashTable = hashTable;
// Clean current hash table
hashTable.length = 0;
- hashTable.length = newCapacity;
+ hashTable.length = newSize;
for (i = 0; i < oldHashTable.length; i += 1)
{
CleanBucket(oldHashTable[i]);
bucketEntries = oldHashTable[i].entries;
- for (j = 0; j < bucketEntries.length; j += 1) {
+ for (j = 0; j < bucketEntries.length; j += 1)
+ {
newBucketIndex = GetBucketIndex(bucketEntries[j].key);
newEntryIndex = hashTable[newBucketIndex].entries.length;
hashTable[newBucketIndex].entries[newEntryIndex] = bucketEntries[j];
@@ -233,6 +244,43 @@ private final function ResizeHashTable(int newCapacity)
}
}
+/**
+ * Returns minimal capacity of the caller associative array.
+ *
+ * See `SetMinimalCapacity()` for details.
+ *
+ * @return Minimal capacity of the caller associative array. Default is zero.
+ */
+public final function int GetMinimalCapacity()
+{
+ return minimalCapacity;
+}
+
+/**
+ * Returns minimal capacity of the caller associative array.
+ *
+ * This associative array works like a hash table and needs to allocate
+ * sufficiently large dynamic array as a storage for its items.
+ * If you keep adding new items that storage will eventually become too small
+ * for hash table to work efficiently and we will have to reallocate and
+ * re-fill it. If you want to add a huge enough amount of items, this process
+ * can be repeated several times.
+ * This is not ideal, since it means doing a lot of iteration, each
+ * increasing infinite loop counter (game will crash if it gets high enough).
+ * Setting minimal capacity to the (higher) amount of items you expect to
+ * store in the caller array can remove the need for reallocating the storage.
+ *
+ * @param newMinimalCapacity New minimal capacity of this associative array.
+ * It's recommended to set it to the max amount of items you expect to
+ * store in this associative array
+ * (you will be still allowed to store more).
+ */
+public final function SetMinimalCapacity(int newMinimalCapacity)
+{
+ minimalCapacity = newMinimalCapacity;
+ UpdateHashTableSize();
+}
+
/**
* Checks if caller `AssociativeArray` has value recorded with a given `key`.
*
@@ -326,7 +374,7 @@ public final function Entry TakeEntry(AcediaObject key)
entryToTake = hashTable[bucketIndex].entries[entryIndex];
hashTable[bucketIndex].entries.Remove(entryIndex, 1);
storedElementCount = Max(0, storedElementCount - 1);
- UpdateHashTableCapacity();
+ UpdateHashTableSize();
return entryToTake;
}
@@ -441,7 +489,7 @@ public final function AssociativeArray RemoveItem(AcediaObject key)
entryToRemove = hashTable[bucketIndex].entries[entryIndex];
hashTable[bucketIndex].entries.Remove(entryIndex, 1);
storedElementCount = Max(0, storedElementCount - 1);
- UpdateHashTableCapacity();
+ UpdateHashTableSize();
if (entryToRemove.managed && entryToRemove.value != none) {
entryToRemove.value.FreeSelf(entryToRemove.valueLifeVersion);
}
@@ -484,7 +532,7 @@ public function Empty(optional bool deallocateKeys)
}
hashTable.length = 0;
storedElementCount = 0;
- UpdateHashTableCapacity();
+ UpdateHashTableSize();
}
/**
@@ -950,8 +998,11 @@ public final function DynamicArray GetDynamicArray(AcediaObject key)
defaultproperties
{
iteratorClass = class'AssociativeArrayIterator'
- MINIMUM_CAPACITY = 50
- MAXIMUM_CAPACITY = 10000
- MINIMUM_DENSITY = 0.25
- MAXIMUM_DENSITY = 0.75
+ minimalCapacity = 0
+ MINIMUM_SIZE = 50
+ MAXIMUM_SIZE = 20000
+ // `MINIMUM_DENSITY * 2 < MAXIMUM_DENSITY` must hold for `AssociativeArray`
+ // to work properly
+ MINIMUM_DENSITY = 0.25
+ MAXIMUM_DENSITY = 0.75
}
\ No newline at end of file
diff --git a/sources/Data/Collections/Collection.uc b/sources/Data/Collections/Collection.uc
index e4af8a2..16d03a4 100644
--- a/sources/Data/Collections/Collection.uc
+++ b/sources/Data/Collections/Collection.uc
@@ -22,7 +22,7 @@
class Collection extends AcediaObject
abstract;
-var class iteratorClass;
+var protected class iteratorClass;
/**
* Method that must be overloaded for `GetItemByPointer()` to properly work.
diff --git a/sources/Data/Database/Local/LocalDatabaseInstance.uc b/sources/Data/Database/Local/LocalDatabaseInstance.uc
index 52b02ad..b184eb8 100644
--- a/sources/Data/Database/Local/LocalDatabaseInstance.uc
+++ b/sources/Data/Database/Local/LocalDatabaseInstance.uc
@@ -96,8 +96,11 @@ protected function Finalizer()
// Defaulting variables is not necessary, since this class does not
// use object pool.
CompleteAllTasks();
+ rootRecord = none;
_.unreal.OnTick(self).Disconnect();
_.memory.Free(diskUpdateTimer);
+ diskUpdateTimer = none;
+ configEntry = none;
}
// It only has parameters so that it can be used as a `Tick()` event handler.
diff --git a/sources/Events/Broadcast/BroadcastEvents.uc b/sources/Events/Broadcast/BroadcastEvents.uc
deleted file mode 100644
index fbbe102..0000000
--- a/sources/Events/Broadcast/BroadcastEvents.uc
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * Event generator for events, related to broadcasting messages
- * through standard Unreal Script means:
- * 1. text messages, typed by a player;
- * 2. localized messages, identified by a LocalMessage class and id.
- * Allows to make decisions whether or not to propagate certain messages.
- * Copyright 2020 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 BroadcastEvents extends Events
- abstract;
-
-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 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;
-};
-
-static function bool CallCanBroadcast(Actor broadcaster, int recentSentTextSize)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.CanBroadcast(broadcaster, recentSentTextSize);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleText(
- Actor sender,
- out string message,
- name messageType)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleText(sender, message, messageType);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleTextFor(
- PlayerController receiver,
- Actor sender,
- out string message,
- name messageType)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleTextFor(receiver, sender, message, messageType);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleLocalized(
- Actor sender,
- LocalizedMessage message)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleLocalized(sender, message);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleLocalizedFor(
- PlayerController receiver,
- Actor sender,
- LocalizedMessage message)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleLocalizedFor(receiver, sender, message);
- if (!result) return false;
- }
- return true;
-}
-
-defaultproperties
-{
- relatedListener = class'BroadcastListenerBase'
-}
\ No newline at end of file
diff --git a/sources/Events/Broadcast/BroadcastEventsObserver.uc b/sources/Events/Broadcast/BroadcastEventsObserver.uc
deleted file mode 100644
index db3b904..0000000
--- a/sources/Events/Broadcast/BroadcastEventsObserver.uc
+++ /dev/null
@@ -1,341 +0,0 @@
-/**
- * `BroadcastHandler` class that used by Acedia to catch
- * broadcasting events. For Acedia to work properly it needs to be added to
- * the very beginning of the broadcast handlers' chain.
- * However, for compatibility reasons Acedia also supports less invasive
- * methods to add it at the cost of some functionality degradation.
- * Copyright 2020 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 BroadcastEventsObserver extends Engine.BroadcastHandler
- dependson(BroadcastEvents)
- config(AcediaSystem);
-
-/**
- * Forcing Acedia's own `BroadcastHandler` is rather invasive and might be
- * undesired, since it can lead to incompatibilities with some mutators.
- * To alleviate this issue Acedia allows server admins to control how it's
- * `BroadcastHandler` is injected. Do note however that anything other than
- * `BHIJ_Root` can lead to issues with Acedia's features.
- */
-enum InjectionLevel
-{
- // `BroadcastEventsObserver` will not be added at all, which will
- // effectively disable `BroadcastEvents`.
- BHIJ_None,
- // `BroadcastEventsObserver` will be places in the broadcast handlers'
- // chain as a normal `BroadcastHandler`
- // (through `RegisterBroadcastHandler()` call), which can lead to incorrect
- // handling of `HandleText()` and `HandleLocalized()` events.
- BHIJ_Registered,
- // `BroadcastEventsObserver` will be injected at the very beginning of
- // the broadcast handlers' chain.
- // This option provides full Acedia's functionality.
- BHIJ_Root
-};
-var public config const InjectionLevel usedInjectionLevel;
-// The way vanilla `BroadcastHandler` works - it can check if broadcast is
-// possible for any actor, but for actually sending the text messages it will
-// try to extract player's data from it and will simply pass `none` for
-// a sender if it can't.
-// We remember senders in this array in order to pass real ones to
-// our events.
-// We use an array instead of a single variable is to account for possible
-// folded calls (when handling of broadcast events leads to another
-// message generation).
-// This is only relevant for `BHIJ_Root` injection level.
-var private array storedSenders;
-
-// We want to insert our code in some of the functions between
-// `AllowsBroadcast` check and actual broadcasting,
-// so we can't just use a `super.AllowsBroadcast()` call.
-// Instead we first manually do this check, then perform our logic and then
-// make a super call, but with `blockAllowsBroadcast` flag set to `true`,
-// which causes overloaded `AllowsBroadcast()` to omit checks that we've
-// already performed.
-var private bool blockAllowsBroadcast;
-
-/*
- * In case of `BHIJ_Registered` injection level, we do not get notified
- * when a message starts getting broadcasted through `Broadcast()`,
- * `BroadcastTeam()` and `AcceptBroadcastLocalized()`.
- * Instead we are only notified when a message is broadcasted to
- * a particular player, so with 2 players instead of sequence `Broadcast()`,
- * `AcceptBroadcastText()`, `AcceptBroadcastText()`
- * we get `AcceptBroadcastText()`, `AcceptBroadcastText()`.
- * This means that we can only guess when new broadcast was initiated.
- * We do this by:
- * 1. Recording broadcast instigator (sender) and his message. If any of
- * these variables change - we assume it's a new broadcast.
- * 2. Recording players that already received that message, - if message is
- * resend to one of them - it's a new broadcast
- * (of possibly duplicate message).
- * 3. All broadcasted messages are sent to all players within 1 tick, so
- * any first message within each tick is a start of a new broadcast.
- *
- * Check logic is implemented in `IsFromNewTextBroadcast()` and
- * `IsFromNewLocalizedBroadcast()` methods.
- */
-// Are we already already tracking any broadcast? Helps to track for point 3.
-var private bool trackingBroadcast;
-// Sender of the current broadcast. Helps to track for point 1.
-var private Actor currentBroadcastInstigator;
-// Players that already received current broadcast. Helps to track for point 2.
-var private array currentBroadcastReceivers;
-// Is current broadcast sending a
-// text message (`Broadcast()` and `BroadcastTeam()`)
-// or localized message (`AcceptBroadcastLocalized()`)?
-// Helps to track message for point 1.
-var private bool broadcastingLocalizedMessage;
-// Variables to stored text message. Helps to track for point 1.
-var private string currentTextMessageContent;
-var private name currentTextMessageType;
-// Variables to stored localized message. Helps to track for point 1.
-var private BroadcastEvents.LocalizedMessage currentLocalizedMessage;
-
-private function bool IsCurrentBroadcastReceiver(PlayerController receiver)
-{
- local int i;
- for (i = 0; i < currentBroadcastReceivers.length; i += 1)
- {
- if (currentBroadcastReceivers[i] == receiver) {
- return true;
- }
- }
- return false;
-}
-
-private function bool IsFromNewTextBroadcast(
- PlayerReplicationInfo senderPRI,
- PlayerController receiver,
- string message,
- name messageType)
-{
- local bool isCurrentBroadcastContinuation;
- if (usedInjectionLevel != BHIJ_Registered) return false;
-
- isCurrentBroadcastContinuation = trackingBroadcast
- && (senderPRI == currentBroadcastInstigator)
- && (!broadcastingLocalizedMessage)
- && (message == currentTextMessageContent)
- && (currentTextMessageType == currentTextMessageType)
- && !IsCurrentBroadcastReceiver(receiver);
- if (isCurrentBroadcastContinuation) {
- return false;
- }
- trackingBroadcast = true;
- broadcastingLocalizedMessage = false;
- currentBroadcastInstigator = senderPRI;
- currentTextMessageContent = message;
- currentTextMessageType = messageType;
- currentBroadcastReceivers.length = 0;
- return true;
-}
-
-private function bool IsFromNewLocalizedBroadcast(
- Actor sender,
- PlayerController receiver,
- BroadcastEvents.LocalizedMessage localizedMessage)
-{
- local bool isCurrentBroadcastContinuation;
- if (usedInjectionLevel != BHIJ_Registered) return false;
-
- isCurrentBroadcastContinuation = trackingBroadcast
- && (sender == currentBroadcastInstigator)
- && (broadcastingLocalizedMessage)
- && (localizedMessage == currentLocalizedMessage)
- && !IsCurrentBroadcastReceiver(receiver);
- if (isCurrentBroadcastContinuation) {
- return false;
- }
- trackingBroadcast = true;
- broadcastingLocalizedMessage = true;
- currentBroadcastInstigator = sender;
- currentLocalizedMessage = localizedMessage;
- currentBroadcastReceivers.length = 0;
- return true;
-}
-
-// Functions below simply reroute vanilla's broadcast events to
-// Acedia's 'BroadcastEvents', while keeping original senders
-// and blocking 'AllowsBroadcast()' as described in comments for
-// 'storedSenders' and 'blockAllowsBroadcast'.
-
-public function bool HandlerAllowsBroadcast(Actor broadcaster, int sentTextNum)
-{
- local bool canBroadcast;
- // Check listeners
- canBroadcast = class'BroadcastEvents'.static
- .CallCanBroadcast(broadcaster, sentTextNum);
- // Check other broadcast handlers (if present)
- if (canBroadcast && nextBroadcastHandler != none)
- {
- canBroadcast = nextBroadcastHandler
- .HandlerAllowsBroadcast(broadcaster, sentTextNum);
- }
- return canBroadcast;
-}
-
-function Broadcast(Actor sender, coerce string message, optional name type)
-{
- local bool canTryToBroadcast;
- if (!AllowsBroadcast(sender, Len(message))) return;
- canTryToBroadcast = class'BroadcastEvents'.static
- .CallHandleText(sender, message, type);
- if (canTryToBroadcast)
- {
- storedSenders[storedSenders.length] = sender;
- blockAllowsBroadcast = true;
- super.Broadcast(sender, message, type);
- blockAllowsBroadcast = false;
- storedSenders.length = storedSenders.length - 1;
- }
-}
-
-function BroadcastTeam(
- Controller sender,
- coerce string message,
- optional name type
-)
-{
- local bool canTryToBroadcast;
- if (!AllowsBroadcast(sender, Len(message))) return;
- canTryToBroadcast = class'BroadcastEvents'.static
- .CallHandleText(sender, message, type);
- if (canTryToBroadcast)
- {
- storedSenders[storedSenders.length] = sender;
- blockAllowsBroadcast = true;
- super.BroadcastTeam(sender, message, type);
- blockAllowsBroadcast = false;
- storedSenders.length = storedSenders.length - 1;
- }
-}
-
-event AllowBroadcastLocalized(
- Actor sender,
- class message,
- optional int switch,
- optional PlayerReplicationInfo relatedPRI1,
- optional PlayerReplicationInfo relatedPRI2,
- optional Object optionalObject
-)
-{
- local bool canTryToBroadcast;
- local BroadcastEvents.LocalizedMessage packedMessage;
- packedMessage.class = message;
- packedMessage.id = switch;
- packedMessage.relatedPRI1 = relatedPRI1;
- packedMessage.relatedPRI2 = relatedPRI2;
- packedMessage.relatedObject = optionalObject;
- canTryToBroadcast = class'BroadcastEvents'.static
- .CallHandleLocalized(sender, packedMessage);
- if (canTryToBroadcast)
- {
- super.AllowBroadcastLocalized( sender, message, switch,
- relatedPRI1, relatedPRI2,
- optionalObject);
- }
-}
-
-function bool AllowsBroadcast(Actor broadcaster, int len)
-{
- if (blockAllowsBroadcast)
- return true;
- return super.AllowsBroadcast(broadcaster, len);
-}
-
-function bool AcceptBroadcastText(
- PlayerController receiver,
- PlayerReplicationInfo senderPRI,
- out string message,
- optional name type
-)
-{
- local bool canBroadcast;
- local Actor sender;
- if (senderPRI != none) {
- sender = PlayerController(senderPRI.owner);
- }
- if (sender == none && storedSenders.length > 0) {
- sender = storedSenders[storedSenders.length - 1];
- }
- if (usedInjectionLevel == BHIJ_Registered)
- {
- if (IsFromNewTextBroadcast(senderPRI, receiver, message, type))
- {
- class'BroadcastEvents'.static.CallHandleText(sender, message, type);
- currentBroadcastReceivers.length = 0;
- }
- currentBroadcastReceivers[currentBroadcastReceivers.length] = receiver;
- }
- canBroadcast = class'BroadcastEvents'.static
- .CallHandleTextFor(receiver, sender, message, type);
- if (!canBroadcast) {
- return false;
- }
- return super.AcceptBroadcastText(receiver, senderPRI, message, type);
-}
-
-
-function bool AcceptBroadcastLocalized(
- PlayerController receiver,
- Actor sender,
- class message,
- optional int switch,
- optional PlayerReplicationInfo relatedPRI1,
- optional PlayerReplicationInfo relatedPRI2,
- optional Object obj
-)
-{
- local bool canBroadcast;
- local BroadcastEvents.LocalizedMessage packedMessage;
- packedMessage.class = message;
- packedMessage.id = switch;
- packedMessage.relatedPRI1 = relatedPRI1;
- packedMessage.relatedPRI2 = relatedPRI2;
- packedMessage.relatedObject = obj;
- if (usedInjectionLevel == BHIJ_Registered)
- {
- if (IsFromNewLocalizedBroadcast(sender, receiver, packedMessage))
- {
- class'BroadcastEvents'.static
- .CallHandleLocalized(sender, packedMessage);
- currentBroadcastReceivers.length = 0;
- }
- currentBroadcastReceivers[currentBroadcastReceivers.length] = receiver;
- }
- canBroadcast = class'BroadcastEvents'.static
- .CallHandleLocalizedFor(receiver, sender, packedMessage);
- if (!canBroadcast) {
- return false;
- }
- return super.AcceptBroadcastLocalized( receiver, sender, message, switch,
- relatedPRI1, relatedPRI2, obj);
-}
-
-event Tick(float delta)
-{
- trackingBroadcast = false;
- currentBroadcastReceivers.length = 0;
-}
-
-defaultproperties
-{
- blockAllowsBroadcast = false
- usedInjectionLevel = BHIJ_Root
-}
\ No newline at end of file
diff --git a/sources/Events/Broadcast/BroadcastListenerBase.uc b/sources/Events/Broadcast/BroadcastListenerBase.uc
deleted file mode 100644
index b739a26..0000000
--- a/sources/Events/Broadcast/BroadcastListenerBase.uc
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * Listener for events, related to broadcasting messages
- * through standard Unreal Script means:
- * 1. text messages, typed by a player;
- * 2. localized messages, identified by a LocalMessage class and id.
- * Allows to make decisions whether or not to propagate certain messages.
- * Copyright 2020 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 BroadcastListenerBase extends Listener
- abstract;
-
-/**
- * Helper function for extracting `PlayerController` of the `sender` Actor,
- * if it has one / is one.
- */
-static public final function PlayerController GetController(Actor sender)
-{
- local Pawn senderPawn;
- senderPawn = Pawn(sender);
- if (senderPawn != none) {
- return PlayerController(senderPawn.controller);
- }
- return PlayerController(sender);
-}
-
-/**
- * This event is called whenever registered broadcast handlers are asked if
- * they'd allow given actor ('broadcaster') to broadcast a text message.
- *
- * If injection level for Acedia's broadcast handler is `BHIJ_Root`, this event
- * is guaranteed to be generated before any of the other `BroadcastHandler`s
- * receive it.
- *
- * NOTE: this function is ONLY called when someone tries to
- * broadcast TEXT messages.
- *
- * You can also reject a broadcast after looking at the message itself by
- * using `HandleText()` event.
- *
- * @param broadcaster `Actor` that requested broadcast in question.
- * @param recentSentTextSize Amount of recently broadcasted symbols of text
- * by `broadcaster`. This value is periodically reset in 'GameInfo',
- * by default should be each second.
- * @return If one of the listeners returns 'false', -
- * it will be treated just like one of broadcasters returning 'false'
- * in 'AllowsBroadcast' and this method won't be called for remaining
- * active listeners. Return `true` if you do not wish to block
- * `broadcaster` from broadcasting his next message.
- * By default returns `true`.
- */
-static function bool CanBroadcast(Actor broadcaster, int recentSentTextSize)
-{
- return true;
-}
-
-/**
- * This event is called whenever a someone is trying to broadcast
- * a text message (typically the typed by a player).
- * It is called once per message and allows you to change it
- * (by changing 'message' argument) before any of the players receive it.
- *
- * See also `HandleTextFor()`.
- *
- * @param sender `Actor` that requested broadcast in question.
- * @param message Message that `sender` wants to broadcast, possibly
- * altered by other broadcast listeners.
- * @param messageType Name variable that describes a type of the message.
- * Examples are 'Say' and 'CriticalEvent'.
- * @return If one of the listeners returns 'false', -
- * it will be treated just like one of broadcasters returning 'false'
- * in `AcceptBroadcastText()`: this event won't be called for remaining
- * active listeners and message will not be broadcasted.
- */
-static function bool HandleText(
- Actor sender,
- out string message,
- optional name messageType)
-{
- return true;
-}
-
-/**
- * This event is called whenever a someone is trying to broadcast
- * a text message (typically the typed by a player).
- * This event is similar to 'HandleText', but is called for every player
- * the message is sent to.
- *
- * Method allows you to alter the message, but note that changes are
- * accumulated as events go through the players.
- *
- * @param receiver Player, to which message is supposed to be sent next.
- * @param sender `Actor` that requested broadcast in question.
- * @param message Message that `sender` wants to broadcast, possibly
- * altered by other broadcast listeners.
- * But keep in mind that if you do change the message for one client, -
- * clients that come after it will get an already altered version.
- * That is, changes to the message accumulate between different
- * `HandleTextFor()` calls for one broadcast.
- * @param messageType Name variable that describes a type of the message.
- * Examples are 'Say' and 'CriticalEvent'.
- * @return If one of the listeners returns 'false', -
- * message would not be sent to `receiver` at all
- * (but it would not prevent broadcasting it to the rest of the players).
- * Return `true` if you want it to be broadcasted.
- */
-static function bool HandleTextFor(
- PlayerController receiver,
- Actor sender,
- out string message,
- optional name messageType)
-{
- return true;
-}
-
-/**
- * This event is called whenever a someone is trying to broadcast
- * a localized message. It is called once per message, but,
- * unlike `HandleText()`, does not allow you to change it.
- *
- * @param sender `Actor` that requested broadcast in question.
- * @param message Message that `sender` wants to broadcast.
- * @return If one of the listeners returns 'false', -
- * it will be treated just like one of broadcasters returning 'false'
- * in `AcceptBroadcastLocalized()`: this event won't be called for
- * remaining active listeners and message will not be broadcasted.
- */
-static function bool HandleLocalized(
- Actor sender,
- BroadcastEvents.LocalizedMessage message)
-{
- return true;
-}
-
-/**
- * This event is called whenever a someone is trying to broadcast
- * a localized message. This event is similar to 'HandleLocalized', but is
- * called for every player the message is sent to.
- *
- * Unlike `HandleTextFor()` method does not allow you to alter the message.
- *
- * @param receiver Player, to which message is supposed to be sent next.
- * @param sender `Actor` that requested broadcast in question.
- * @param message Message that `sender` wants to broadcast.
- * @return If one of the listeners returns 'false', -
- * message would not be sent to `receiver` at all
- * (but it would not prevent broadcasting it to the rest of the players).
- * Return `true` if you want it to be broadcasted.
- */
-static function bool HandleLocalizedFor(
- PlayerController receiver,
- Actor sender,
- BroadcastEvents.LocalizedMessage message)
-{
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'BroadcastEvents'
-}
\ No newline at end of file
diff --git a/sources/Events/Events.uc b/sources/Events/Events.uc
deleted file mode 100644
index 84f3519..0000000
--- a/sources/Events/Events.uc
+++ /dev/null
@@ -1,161 +0,0 @@
-/**
- * One of the two classes that make up a core of event system in Acedia.
- *
- * `Events` (or it's child) class shouldn't be instantiated.
- * Usually module would provide '...Events' class that defines
- * certain set of static functions that can generate event calls to
- * all it's active listeners.
- * If you're simply using modules someone made, -
- * you don't need to bother yourself with further specifics.
- * If you wish to create your own event generator,
- * then first create a `...ListenerBase` object
- * (more about it in the description of `Listener` class)
- * and set `relatedListener` variable to point to it's class.
- * Then for each event create a caller function in your `Event` class,
- * following this template:
- * ____________________________________________________________________________
- * | static function CallEVENT_NAME()
- * | {
- * | local int i;
- * | local array< class > listeners;
- * | listeners = GetListeners();
- * | for (i = 0; i < listeners.length; i += 1)
- * | {
- * | class<...ListenerBase>(listeners[i])
- * | .static.EVENT_NAME();
- * | }
- * | }
- * |___________________________________________________________________________
- * If each listener must indicate whether it gives it's permission for
- * something to happen, then use this template:
- * ____________________________________________________________________________
- * | static function CallEVENT_NAME()
- * | {
- * | local int i;
- * | local bool result;
- * | local array< class > listeners;
- * | listeners = GetListeners();
- * | for (i = 0; i < listeners.length; i += 1)
- * | {
- * | result = class<...ListenerBase>(listeners[i])
- * | .static.EVENT_NAME();
- * | if (!result) return false;
- * | }
- * | return true;
- * | }
- * |___________________________________________________________________________
- * For concrete example look at
- * `MutatorEvents` and `MutatorListenerBase`.
- * Copyright 2020 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 Events extends AcediaObject
- abstract;
-
-var private array< class > listeners;
-
-// Reference to the base class of listeners that are allowed to listen to
-// these events
-var public const class relatedListener;
-
-// Event class can also auto-spawn a `Service`,
-// in case it's require to generate events
-var public const class connectedServiceClass;
-// Set this to `true`if you want `connectedServiceClass` service to also
-// auto-shutdown whenever no-one listens to the events.
-var public const bool shutDownServiceWithoutListeners;
-
-static public final function array< class > GetListeners()
-{
- return default.listeners;
-}
-
-// Make given listener active.
-// If listener was already activated also returns 'false'.
-static public final function bool ActivateListener(class newListener)
-{
- local int i;
- if (newListener == none) return false;
- if (!ClassIsChildOf(newListener, default.relatedListener)) return false;
-
- // Spawn service, if absent
- if ( default.listeners.length == 0
- && default.connectedServiceClass != none) {
- default.connectedServiceClass.static.Require();
- }
- // Add listener
- for (i = 0;i < default.listeners.length;i += 1)
- {
- if (default.listeners[i] == newListener) {
- return false;
- }
- }
- default.listeners[default.listeners.length] = newListener;
- return true;
-}
-
-// Make given listener inactive.
-// If listener wasn't active returns 'false'.
-static public final function bool DeactivateListener(class listener)
-{
- local int i;
- local bool removedListener;
- local Service service;
- if (listener == none) return false;
-
- // Remove listener
- for (i = 0; i < default.listeners.length; i += 1)
- {
- if (default.listeners[i] == listener)
- {
- default.listeners.Remove(i, 1);
- removedListener = true;
- break;
- }
- }
- // Remove unneeded service
- if ( default.shutDownServiceWithoutListeners
- && default.listeners.length == 0
- && default.connectedServiceClass != none)
- {
- service = Service(default.connectedServiceClass.static.GetInstance());
- if (service != none) {
- service.Destroy();
- }
- }
- return removedListener;
-}
-
-static public final function bool IsActiveListener(class listener)
-{
- local int i;
- if (listener == none) return false;
-
- for (i = 0; i < default.listeners.length; i += 1)
- {
- if (default.listeners[i] == listener)
- {
- return true;
- }
- }
- return false;
-}
-
-defaultproperties
-{
- relatedListener = class'Listener'
-}
\ No newline at end of file
diff --git a/sources/Events/Listener.uc b/sources/Events/Listener.uc
deleted file mode 100644
index f3946b5..0000000
--- a/sources/Events/Listener.uc
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * One of the two classes that make up a core of event system in Acedia.
- *
- * 'Listener' (or it's child) class shouldn't be instantiated.
- * Usually module would provide '...ListenerBase' class that defines
- * certain set of static functions, corresponding to events it can listen to.
- * In order to handle those events you must create it's child class and
- * override said functions. But they will only be called if
- * 'SetActive(true)' is called for that child class.
- * To create you own '...ListenerBase' class you need to define
- * a static function for each event you wish it to catch and
- * set 'relatedEvents' variable to point at the 'Events' class
- * that will generate your events.
- * For concrete example look at
- * 'ConnectionEvents' and 'ConnectionListenerBase'.
- * Copyright 2019 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 Listener extends AcediaObject
- abstract;
-
-var public const class relatedEvents;
-
-
-static public final function SetActive(bool active)
-{
- if (active)
- {
- default.relatedEvents.static.ActivateListener(default.class);
- }
- else
- {
- default.relatedEvents.static.DeactivateListener(default.class);
- }
-}
-
-static public final function IsActive(bool active)
-{
- default.relatedEvents.static.IsActiveListener(default.class);
-}
-
-defaultproperties
-{
- relatedEvents = class'Events'
-}
\ No newline at end of file
diff --git a/sources/Events/Mutator/MutatorEvents.uc b/sources/Events/Mutator/MutatorEvents.uc
deleted file mode 100644
index 542c320..0000000
--- a/sources/Events/Mutator/MutatorEvents.uc
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Event generator that repeats events of a mutator.
- * Copyright 2019 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 MutatorEvents extends Events
- abstract;
-
-static function bool CallCheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length; i += 1)
- {
- result = class(listeners[i])
- .static.CheckReplacement(other, isSuperRelevant);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallMutate(string command, PlayerController sendingPlayer)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.Mutate(command, sendingPlayer);
- if (!result) return false;
- }
- return true;
-}
-
-defaultproperties
-{
- relatedListener = class'MutatorListenerBase'
-}
\ No newline at end of file
diff --git a/sources/Events/Mutator/MutatorListenerBase.uc b/sources/Events/Mutator/MutatorListenerBase.uc
deleted file mode 100644
index 74c4311..0000000
--- a/sources/Events/Mutator/MutatorListenerBase.uc
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Listener for events, normally propagated by mutators.
- * Copyright 2019 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 MutatorListenerBase extends Listener
- abstract;
-
-// This event is called whenever 'CheckReplacement'
-// check is propagated through mutators.
-// If one of the listeners returns 'false', -
-// it will be treated just like a mutator returning 'false'
-// in 'CheckReplacement' and
-// this method won't be called for remaining active listeners.
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- return true;
-}
-
-// This event is called whenever 'Mutate' is propagated through mutators.
-// If one of the listeners returns 'false', -
-// this method won't be called for remaining active listeners or mutators.
-// If all listeners return 'true', -
-// mutate command will be further propagated to the rest of the mutators.
-static function bool Mutate(string command, PlayerController sendingPlayer)
-{
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/Events/Signal.uc b/sources/Events/Signal.uc
index 56b1ce4..b146b45 100644
--- a/sources/Events/Signal.uc
+++ b/sources/Events/Signal.uc
@@ -7,7 +7,7 @@
* This `Signal`-`Slot` system is essentially a wrapper for delegates
* (`Slot` wraps over a single delegate, allowing us to store them in array),
* but, unlike them, makes it possible to add several handlers for any event in
- * a convenient to use way, e.g..:
+ * a convenient to use way, e.g.:
* `_.unreal.OnTick(self).connect = myTickHandler`
* To create your own `Signal` you need to:
* 1. Make a non-abstract child class of `Signal`;
@@ -52,7 +52,7 @@ class Signal extends AcediaObject
* internal index variable `nextSlotIndex`. To account for removal of `Slot`s
* we will simply have to appropriately correct `nextSlotIndex` variable.
* To account for adding `Slot`s during signal emission we will first add them
- * to a temporary queue `slotQueueToAdd` and only dump signals stored there
+ * to a temporary queue `slotQueueToAdd` and only dump slots stored there
* into actual connected `Slot`s array before next iteration starts.
*/
@@ -86,17 +86,19 @@ var array slotQueueToAdd;
// These arrays could be defined as one array of `SlotRecord` structs.
// We use four different arrays instead for performance reasons.
+// (Acedia is expected to make extensive use of `Signal`s and `Slot`s, so it's
+// reasonable to consider even small optimization in this case).
// They must have the same length at all times and elements with the
// same index correspond to the same "record".
-// Reference to registered `Slot`
+// References to registered `Slot`s
var private array registeredSlots;
-// Life version of the registered `Slot`, to track unexpected deallocations
+// Life versions of the registered `Slot`s, to track unexpected deallocations
var private array slotLifeVersions;
-// Receiver, associated with the `Slot`: when it's deallocated,
-// corresponding `Slot` should be removed
+// Receivers, associated with the `Slot`s: when they're deallocated,
+// corresponding `Slot`s should be removed
var private array slotReceivers;
-// Life version of the registered receiver, to track it's deallocation
+// Life versions of the registered receivers, to track their deallocation
var private array slotReceiversLifeVersions;
/* TEMPLATE for handlers without returned values:
@@ -163,8 +165,8 @@ protected function Finalizer()
for (i = 0; i < registeredSlots.length; i += 1) {
registeredSlots[i].FreeSelf(slotLifeVersions[i]);
}
- registeredSlots.length = 0;
doingSelfCleaning = false;
+ registeredSlots.length = 0;
slotLifeVersions.length = 0;
slotReceivers.length = 0;
slotReceiversLifeVersions.length = 0;
@@ -174,7 +176,7 @@ protected function Finalizer()
* Creates a new slot for `receiver` to catch emitted signals.
* Supposed to be used inside a special interface method only.
*
- * @param receiver Receiver to which new `Slot` would be connected.
+ * @param receiver Receiver to which new `Slot` would be connected to.
* Method connected to a `Slot` generated by this method must belong to
* the `receiver`, otherwise behavior of `Signal`-`Slot` system is
* undefined.
@@ -238,7 +240,7 @@ public final function Disconnect(AcediaObject receiver)
/**
* Adds new `Slot` (`newSlot`) with receiver `receiver` to the caller `Signal`.
*
- * Does nothing if `newSlot` is already added to the caller `Signal`
+ * Won't affect caller `Signal` if `newSlot` is already added to it
* (even if it's added with a different receiver).
*
* @param newSlot Slot to add. Must be initialize for the caller `Signal`.
@@ -250,6 +252,12 @@ public final function Disconnect(AcediaObject receiver)
protected final function AddSlot(Slot newSlot, AcediaObject receiver)
{
local SlotRecord newRecord;
+ // Do not check whether `receiver` is `none`, this requires handling
+ // `newSlot`'s deallocation and it will be dealt with at the moment of
+ // adding new slots from `slotQueueToAdd` queue to the caller `Signal`.
+ // This situation should not normally occur in the first place, so
+ // it does not matter if the `slotQueueToAdd` grows larger than needed when
+ // this does happen.
if (newSlot == none) {
return;
}
@@ -277,10 +285,10 @@ private final function AddSlotRecord(SlotRecord record)
receiver = record.receiver;
if (newSlot.class != relatedSlotClass) return;
if (!newSlot.IsOwnerSignal(self)) return;
- // Slot got outdated while waiting in queue
+ // Slot got deallocated while waiting in queue
if (newSlot.GetLifeVersion() != record.slotLifeVersion) return;
- // Receiver is outright invalid or got outdated
+ // Receiver is outright invalid or got deallocated
if ( receiver == none
|| !receiver.IsAllocated()
|| receiver.GetLifeVersion() != record.receiverLifeVersion)
@@ -299,8 +307,8 @@ private final function AddSlotRecord(SlotRecord record)
// If we have the same instance recorded, but...
// 1. it was reallocated: update it's records;
// 2. it was not reallocated: leave the records intact.
- // Neither would case issues with iterating along `Slot`s if this
- // method is only called right before new iteration.
+ // Neither case would cause issues with iterating along `Slot`s if this
+ // method is only called right before new iteration through `Slot`s.
if (slotLifeVersions[i] != record.slotLifeVersion)
{
slotLifeVersions[i] = record.slotLifeVersion;
diff --git a/sources/Features/Feature.uc b/sources/Features/Feature.uc
index 8c23b8b..ead6b2e 100644
--- a/sources/Features/Feature.uc
+++ b/sources/Features/Feature.uc
@@ -61,9 +61,6 @@ var protected bool blockSpawning;
// Only it's default value is ever used.
var private config bool autoEnable;
-// Listeners listed here will be automatically activated.
-var public const array< class > requiredListeners;
-
// `Service` that will be launched and shut down along with this `Feature`.
// One should never launch or shut down this service manually.
var protected const class serviceClass;
@@ -93,14 +90,15 @@ protected function Constructor()
FreeSelf();
return;
}
- SetListenersActiveStatus(true);
if (serviceClass != none) {
myService = FeatureService(serviceClass.static.Require());
}
if (myService != none) {
myService.SetOwnerFeature(self);
}
+ currentConfigName = none;
ApplyConfig(default.currentConfigName);
+ _.memory.Free(default.currentConfigName);
default.currentConfigName = none;
OnEnabled();
}
@@ -111,7 +109,6 @@ protected function Finalizer()
if (GetInstance() != self) {
return;
}
- SetListenersActiveStatus(false);
OnDisabled();
if (serviceClass != none) {
service = FeatureService(serviceClass.static.GetInstance());
@@ -243,11 +240,7 @@ public static final function Feature EnableMe(Text configName)
if (IsEnabled()) {
return GetInstance();
}
- // This value will be copied and forgotten in `Constructor()`,
- // so we do not actually retain `configName` reference and it can be freed
- // right after `EnableMe()` method call ends.
- // Copying it here will mean doing extra work.
- default.currentConfigName = configName;
+ default.currentConfigName = configName.Copy();
default.blockSpawning = false;
newInstance = Feature(__().memory.Allocate(default.class));
default.activeInstance = newInstance;
@@ -304,16 +297,6 @@ protected function OnDisabled(){}
*/
protected function SwapConfig(FeatureConfig newConfig){}
-private static function SetListenersActiveStatus(bool newStatus)
-{
- local int i;
- for (i = 0; i < default.requiredListeners.length; i += 1)
- {
- if (default.requiredListeners[i] == none) continue;
- default.requiredListeners[i].static.SetActive(newStatus);
- }
-}
-
defaultproperties
{
autoEnable = false
diff --git a/sources/Features/FeatureConfig.uc b/sources/Features/FeatureConfig.uc
index 5439dbf..0cdbbec 100644
--- a/sources/Features/FeatureConfig.uc
+++ b/sources/Features/FeatureConfig.uc
@@ -54,7 +54,7 @@ public static function Initialize()
{
nextConfig = FeatureConfig(GetConfigInstance(names[i]));
if (nextConfig == none) continue;
- if (nextConfig.autoEnable) continue;
+ if (!nextConfig.autoEnable) continue;
if (default.autoEnabledConfig == none) {
default.autoEnabledConfig = names[i].Copy();
}
diff --git a/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc b/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc
index 90a490b..d17d2a5 100644
--- a/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc
+++ b/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc
@@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class KFFrontend extends BaseBackend
+class KFFrontend extends BaseFrontend
abstract;
var private config class tradingClass;
@@ -33,6 +33,7 @@ protected function Constructor()
protected function Finalizer()
{
_.memory.Free(trading);
+ trading = none;
}
defaultproperties
diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc b/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc
index 87bc16c..bf1804a 100644
--- a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc
+++ b/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc
@@ -37,6 +37,9 @@ protected function Finalizer()
_.memory.Free(onStartSignal);
_.memory.Free(onEndSignal);
_.memory.Free(onTraderSelectSignal);
+ onStartSignal = none;
+ onEndSignal = none;
+ onTraderSelectSignal = none;
}
/**
diff --git a/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc b/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
index a141838..0319a76 100644
--- a/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
+++ b/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
@@ -28,8 +28,10 @@ var protected NativeActorRef myShopVolume;
protected function Finalizer()
{
+ _.memory.Free(myName);
_.memory.Free(myShopVolume);
myShopVolume = none;
+ myName = none;
}
/**
diff --git a/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc b/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
index 434b14b..b504285 100644
--- a/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
+++ b/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
@@ -46,7 +46,9 @@ protected function Finalizer()
{
super.Finalizer();
_.unreal.OnTick(self).Disconnect();
+ _.memory.Free(lastSelectedTrader);
_.memory.FreeMany(registeredTraders);
+ lastSelectedTrader = none;
registeredTraders.length = 0;
}
diff --git a/sources/Global.uc b/sources/Global.uc
index e475a65..ca6e9ee 100644
--- a/sources/Global.uc
+++ b/sources/Global.uc
@@ -78,4 +78,32 @@ protected function Initialize()
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI'));
kf = KFFrontend(memory.Allocate(class'KF1_Frontend'));
json.StaticConstructor();
+}
+
+public function DropGameplayAPI()
+{
+ memory.Free(kf);
+ kf = none;
+}
+
+public function DropCoreAPI()
+{
+ memory = none;
+ ref = none;
+ box = none;
+ text = none;
+ collections = none;
+ unreal.DropAPI();
+ unreal = none;
+ time = none;
+ logger = none;
+ alias = none;
+ console = none;
+ color = none;
+ users = none;
+ players = none;
+ json = none;
+ db = none;
+ avarice = none;
+ default.myself = none;
}
\ No newline at end of file
diff --git a/sources/Logger/ConsoleLogger.uc b/sources/Logger/ConsoleLogger.uc
index 1331404..60af88b 100644
--- a/sources/Logger/ConsoleLogger.uc
+++ b/sources/Logger/ConsoleLogger.uc
@@ -36,6 +36,12 @@ public function Write(Text message, LoggerAPI.LogLevel messageLevel)
}
}
+protected static function StaticFinalizer()
+{
+ default.loadedLoggers = none;
+}
+
+
defaultproperties
{
}
\ No newline at end of file
diff --git a/sources/Logger/LogMessage.uc b/sources/Logger/LogMessage.uc
index 10e2dbb..d0a69b0 100644
--- a/sources/Logger/LogMessage.uc
+++ b/sources/Logger/LogMessage.uc
@@ -210,7 +210,6 @@ private final function NormalizeArguments(array argumentsOrder)
*/
public final function LogMessage Arg(Text argument)
{
- local Text assembledMessage;
if (IsArgumentListFull()) {
return self;
}
@@ -220,15 +219,23 @@ public final function LogMessage Arg(Text argument)
}
default.dirtyLogMessage = self; // `self` is dirty with arguments now
collectedArguments[collectedArguments.length] = argument;
+ TryLogging();
+ return self;
+}
+
+/**
+ * Outputs a message at appropriate level, if all of its arguments were filled.
+ */
+public final function TryLogging()
+{
+ local Text assembledMessage;
if (IsArgumentListFull())
{
// Last argument - have to log what we have collected
assembledMessage = Collect();
_.logger.LogAtLevel(assembledMessage, myLevel);
assembledMessage.FreeSelf();
- return self;
}
- return self;
}
// Check whether we have enough arguments to completely make log message:
diff --git a/sources/Logger/Logger.uc b/sources/Logger/Logger.uc
index 7d3e08b..50736c2 100644
--- a/sources/Logger/Logger.uc
+++ b/sources/Logger/Logger.uc
@@ -2,6 +2,13 @@
* Base class for implementing "loggers" - objects that actually write log
* messages somewhere. To use it - simply implement `Write()` method,
* preferably making use of `GetPrefix()` method.
+ * Note that any child class must clean up its loaded loggers:
+ *
+ * protected static function StaticFinalizer()
+ * {
+ * default.loadedLoggers = none;
+ * }
+ *
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@@ -26,7 +33,7 @@ class Logger extends AcediaObject
abstract;
// Named loggers are stored here to avoid recreating them
-var private AssociativeArray loadedLoggers;
+var protected AssociativeArray loadedLoggers;
// Should `Logger` display prefix indicating it's a log message from Acedia?
var protected config bool acediaStamp;
@@ -38,6 +45,11 @@ var protected config bool levelStamp;
var protected const int TDEBUG, TINFO, TWARNING, TERROR, TFATAL, TTIME, TACEDIA;
var protected const int TSPACE;
+protected static function StaticFinalizer()
+{
+ default.loadedLoggers = none;
+}
+
/**
* Method for creating named `Logger`s that can have their settings prepared
* in the config file. Only one `Logger` is made for every
@@ -52,19 +64,16 @@ public final static function Logger GetLogger(Text loggerName)
{
local Logger loggerInstance;
local Text loggerKey;
- if (default.loadedLoggers == none)
- {
- // TODO: do this in static constructor
- default.loadedLoggers = __().collections.EmptyAssociativeArray();
- }
if (loggerName == none) {
return none;
}
+ if (default.loadedLoggers == none) {
+ default.loadedLoggers = __().collections.EmptyAssociativeArray();
+ }
loggerKey = loggerName.LowerCopy();
loggerInstance = Logger(default.loadedLoggers.GetItem(loggerKey));
if (loggerInstance == none)
{
- // TODO: important to redo this via `MemoryAPI` to call constructors
loggerInstance = new(none, loggerName.ToPlainString()) default.class;
loggerInstance._constructor();
default.loadedLoggers.SetItem(loggerKey, loggerInstance);
diff --git a/sources/Logger/LoggerAPI.uc b/sources/Logger/LoggerAPI.uc
index fb30291..2730640 100644
--- a/sources/Logger/LoggerAPI.uc
+++ b/sources/Logger/LoggerAPI.uc
@@ -234,7 +234,8 @@ public final function LogMessage Auto(out Definition definition)
instance.Initialize(definition);
definition.instance = instance;
}
- return instance.Reset();
+ instance.Reset().TryLogging();
+ return instance;
}
/**
diff --git a/sources/Service.uc b/sources/Service.uc
index 11d0ece..5043cae 100644
--- a/sources/Service.uc
+++ b/sources/Service.uc
@@ -21,17 +21,17 @@
class Service extends Singleton
abstract;
-// Listeners listed here will be automatically activated.
-var public const array< class > requiredListeners;
+// `Service`s can use this as a receiver for signal functions
+var protected ServiceAnchor _self;
-var LoggerAPI.Definition errNoService;
+// Log messages
+var private LoggerAPI.Definition errNoService;
// Enables feature of given class.
public static final function Service Require()
{
local Service newInstance;
- if (IsRunning())
- {
+ if (IsRunning()) {
return Service(GetInstance());
}
default.blockSpawning = false;
@@ -55,25 +55,15 @@ protected function OnShutdown(){}
protected function OnCreated()
{
default.blockSpawning = true;
- SetListenersActiveSatus(true);
+ _self = ServiceAnchor(_.memory.Allocate(class'ServiceAnchor'));
OnLaunch();
}
protected function OnDestroyed()
{
- SetListenersActiveSatus(false);
OnShutdown();
-}
-
-// Set listeners' status
-private static function SetListenersActiveSatus(bool newStatus)
-{
- local int i;
- for (i = 0; i < default.requiredListeners.length; i += 1)
- {
- if (default.requiredListeners[i] == none) continue;
- default.requiredListeners[i].static.SetActive(newStatus);
- }
+ _.memory.Free(_self);
+ _self = none;
}
defaultproperties
diff --git a/sources/ServiceAnchor.uc b/sources/ServiceAnchor.uc
new file mode 100644
index 0000000..18561ff
--- /dev/null
+++ b/sources/ServiceAnchor.uc
@@ -0,0 +1,25 @@
+/**
+ * Does nothing. Exists only so that `Service`s can use its instances as
+ * receivers when connecting to `Signal`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 .
+ */
+class ServiceAnchor extends AcediaObject;
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Singleton.uc b/sources/Singleton.uc
index 0eb470c..20e921d 100644
--- a/sources/Singleton.uc
+++ b/sources/Singleton.uc
@@ -25,13 +25,18 @@ class Singleton extends AcediaActor
// Default value of this variable will store one and only existing version
// of actor of this class.
-var private Singleton activeInstance;
+var public Singleton activeInstance;
// Setting default value of this variable to 'true' prevents creation of
// a singleton, even if no instances of it exist.
// Only a default value is ever used.
var protected bool blockSpawning;
+protected static function StaticFinalizer()
+{
+ default.activeInstance = none;
+}
+
public final static function Singleton GetInstance(optional bool spawnIfMissing)
{
local bool instanceExists;
@@ -87,12 +92,12 @@ event PreBeginPlay()
// first call this version of the method.
event Destroyed()
{
- super.Destroyed();
if (self == default.activeInstance)
{
OnDestroyed();
default.activeInstance = none;
}
+ super.Destroyed();
}
defaultproperties
diff --git a/sources/Testing/Service/TestingEvents.uc b/sources/Testing/Service/TestingEvents.uc
deleted file mode 100644
index 167f14b..0000000
--- a/sources/Testing/Service/TestingEvents.uc
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Event generator for events related to testing.
- * Copyright 2020 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 TestingEvents extends Events
- abstract;
-
-static function CallTestingBegan(array< class > testQueue)
-{
- local int i;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length; i += 1)
- {
- class(listeners[i])
- .static.TestingBegan(testQueue);
- }
-}
-
-static function CallCaseTested(
- class testedCase,
- TestCaseSummary result)
-{
- local int i;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length; i += 1)
- {
- class(listeners[i])
- .static.CaseTested(testedCase, result);
- }
-}
-
-static function CallTestingEnded(
- array< class > testQueue,
- array results)
-{
- local int i;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length; i += 1)
- {
- class(listeners[i])
- .static.TestingEnded(testQueue, results);
- }
-}
-
-defaultproperties
-{
- relatedListener = class'TestingListenerBase'
-}
\ No newline at end of file
diff --git a/sources/Testing/Service/TestingListenerBase.uc b/sources/Testing/Service/TestingListenerBase.uc
deleted file mode 100644
index 95da526..0000000
--- a/sources/Testing/Service/TestingListenerBase.uc
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Listener for events related to testing.
- * Copyright 2020 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 TestingListenerBase extends Listener
- abstract;
-
-static function TestingBegan(array< class > testQueue) {}
-
-static function CaseTested(class testCase, TestCaseSummary result) {}
-
-static function TestingEnded(
- array< class > testQueue,
- array results) {}
-
-defaultproperties
-{
- relatedEvents = class'TestingEvents'
-}
\ No newline at end of file
diff --git a/sources/Testing/Service/TestingService.uc b/sources/Testing/Service/TestingService.uc
index 9ca5160..ed902c6 100644
--- a/sources/Testing/Service/TestingService.uc
+++ b/sources/Testing/Service/TestingService.uc
@@ -47,10 +47,6 @@ var public config const bool filterTestsByGroup;
var public config const string requiredName;
var public config const string requiredGroup;
-// Shortcut to `TestingEvents`, so that we don't have to write
-// class'TestingEvents' every time.
-var const class events;
-
var LoggerAPI.Definition warnDuplicateTestCases;
/**
* Registers another `TestCase` class for later testing.
@@ -207,12 +203,10 @@ public final function bool Run()
return false;
}
nextTestCase = 0;
- runningTests = true;
summarizedResults.length = 0;
- events.static.CallTestingBegan(testCasesToRun);
- if (testCasesToRun.length <= 0) {
- runningTests = false;
- events.static.CallTestingEnded(testCasesToRun, summarizedResults);
+ runningTests = (testCasesToRun.length > 0);
+ if (!runningTests) {
+ ReportTestingResult();
}
return true;
}
@@ -224,16 +218,31 @@ private final function DoTestingStep()
{
runningTests = false;
default.summarizedResults = summarizedResults;
- events.static.CallTestingEnded(testCasesToRun, summarizedResults);
+ ReportTestingResult();
return;
}
testCasesToRun[nextTestCase].static.PerformTests();
newResult = testCasesToRun[nextTestCase].static.GetSummary();
- events.static.CallCaseTested(testCasesToRun[nextTestCase], newResult);
summarizedResults[summarizedResults.length] = newResult;
nextTestCase += 1;
}
+private function ReportTestingResult()
+{
+ local int i;
+ local MutableText nextLine;
+ local array textSummary;
+ nextLine = __().text.Empty();
+ textSummary = class'TestCaseSummary'.static
+ .GenerateStringSummary(summarizedResults);
+ for (i = 0; i < textSummary.length; i += 1)
+ {
+ nextLine.Clear();
+ nextLine.AppendFormattedString(textSummary[i]);
+ Log(nextLine.ToPlainString());
+ }
+}
+
event Tick(float delta)
{
// This will destroy us on the next tick after we were
@@ -248,6 +257,5 @@ event Tick(float delta)
defaultproperties
{
runTestsOnStartUp = false
- events = class'TestingEvents'
warnDuplicateTestCases = (l=LOG_Fatal,m="Two different test cases with name \"%1\" in the same group \"%2\"have been registered: \"%3\" and \"%4\". This can lead to issues and it is not something you can fix, - contact developers of the relevant packages.")
}
\ No newline at end of file
diff --git a/sources/Text/JSON/JSONAPI.uc b/sources/Text/JSON/JSONAPI.uc
index 4555721..d1e7898 100644
--- a/sources/Text/JSON/JSONAPI.uc
+++ b/sources/Text/JSON/JSONAPI.uc
@@ -1008,7 +1008,7 @@ public final function MutableText PrintObject(AssociativeArray toPrint)
/**
* "Prints" given `AcediaObject` value, saving it in JSON format.
*
- * "Prints" given `AcediaObject` in a human-readable, for a minimal output
+ * "Prints" given `AcediaObject` in a human-readable way. For a minimal output
* use `Print()` method.
*
* Only certain classes (the same as the ones that can be parsed from JSON
diff --git a/sources/Types/AcediaActor.uc b/sources/Types/AcediaActor.uc
index 5c66982..62e537a 100644
--- a/sources/Types/AcediaActor.uc
+++ b/sources/Types/AcediaActor.uc
@@ -178,6 +178,20 @@ private final static function CreateTextCache(optional bool forceCreation)
}
}
+/**
+ * Acedia actors cannot be deallocated into an object pool, but they still
+ * support constructors and destructors and, therefore, track their own
+ * allocation status (`AcediaActor` is considered allocated between constructor
+ * and finalizer calls).
+ *
+ * @return `true` if actor is allocated and ready to use, `false` otherwise
+ * (`Destroy()` was called for it directly or through deallocation method).
+ */
+public final function bool IsAllocated()
+{
+ return _isAllocated;
+}
+
/**
* Deallocates caller `AcediaActor`, calling its finalizer and then
* destroying it.
@@ -374,6 +388,9 @@ event Destroyed()
*/
public static function _cleanup()
{
+ if (default._staticConstructorWasCalled) {
+ StaticFinalizer();
+ }
default._textCache = none;
default._staticConstructorWasCalled = false;
}
diff --git a/sources/Types/AcediaObject.uc b/sources/Types/AcediaObject.uc
index e5d779f..0d955f3 100644
--- a/sources/Types/AcediaObject.uc
+++ b/sources/Types/AcediaObject.uc
@@ -440,6 +440,9 @@ public static final function Global __()
*/
public static function _cleanup()
{
+ if (default._staticConstructorWasCalled) {
+ StaticFinalizer();
+ }
default._textCache = none;
default._objectPool = none;
default._staticConstructorWasCalled = false;
diff --git a/sources/Unreal/BroadcastsAPI/BroadcastAPI.uc b/sources/Unreal/BroadcastsAPI/BroadcastAPI.uc
new file mode 100644
index 0000000..c97c4e2
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/BroadcastAPI.uc
@@ -0,0 +1,398 @@
+/**
+ * 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 .
+ */
+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 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;
+};
+
+/**
+ * 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 (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());
+ 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 (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());
+ 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 (
+ * 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());
+ 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 (
+ * 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());
+ 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 (
+ * 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());
+ signal = service.GetSignal(class'Broadcast_OnHandleLocalizedFor_Signal');
+ return Broadcast_OnHandleLocalizedFor_Slot(signal.NewSlot(receiver));
+}
+
+/**
+ * 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 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'CoreService'.static.Require().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 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 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 BHClassToFind)
+{
+ return (FindInstance(BHClassToFind) != none);
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc b/sources/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc
new file mode 100644
index 0000000..f0d1350
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc
@@ -0,0 +1,483 @@
+/**
+ * `BroadcastHandler` class that used by Acedia to catch
+ * broadcasting events. For Acedia to work properly it needs to be added to
+ * the very beginning of the broadcast handlers' chain.
+ * However, for compatibility reasons Acedia also supports less invasive
+ * methods to add it at the cost of some functionality degradation.
+ * Copyright 2020 - 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 .
+ */
+class BroadcastEventsObserver extends Engine.BroadcastHandler
+ dependson(BroadcastAPI)
+ config(AcediaSystem);
+
+// Forcing Acedia's own `BroadcastHandler` is rather invasive and might be
+// undesired, since it can lead to incompatibilities with some mutators.
+// To alleviate this issue Acedia allows server admins to control how it's
+// `BroadcastHandler` is injected. Do note however that anything other than
+// `BHIJ_Root` can lead to issues with Acedia's features.
+var public config const BroadcastAPI.InjectionLevel usedInjectionLevel;
+
+/**
+ * To understand how what our broadcast handler does, let us first explain
+ * how `BroadcastHandler` classes work. Here we skip voice and speech
+ * broadcasting topics, since they are not something Acedia or this class
+ * currently uses.
+ * `BroadcastHandler`s are capable of forming a one-way linked list by
+ * referring to the next `BroadcastHandler` by `nextBroadcastHandler` and
+ * `nextBroadcastHandlerClass` variables. New `BroadcastHandler`s can be added
+ * via `RegisterBroadcastHandler()` method.
+ * Actual broadcasting of the messages is done by calling one of
+ * the three methods: `Broadcast()`, `BroadcastTeam()` and weirdly named
+ * `AllowBroadcastLocalized()` on the root `BroadcastHandler` (stored in
+ * the current `GameInfo`). These methods are only ever called for the root
+ * `BroadcastHandler` and **are not** propagated down the chain.
+ * This leads to...
+ * ISSUE 1: we cannot reliably detect start of the message propagation with
+ * `BroadcastHandler` unless we are at the root of the linked list. This is why
+ * it is important for Acedia to use `BHIJ_Root` method for its
+ * `BroadcastHandler`.
+ *
+ * First we will look into `Broadcast()` and `BroadcastTeam()` methods.
+ * First thing either of them does is to call `AllowsBroadcast()` method that
+ * checks whether message is allowed to be broadcasted at all. It is also only
+ * called for the root `BroadcastHandler`, but allows other `BroadcastHandler`s
+ * to block the message for their own reasons by propagating
+ * `HandlerAllowsBroadcast()` method down the chain.
+ * Then they call `BroadcastText()` for every acceptable player controller
+ * they find and the only difference between `Broadcast()` and
+ * `BroadcastTeam()` is that the latter also checks to that they belong to the
+ * same team as the sender.
+ * `BroadcastText()` is propagated down the linked list of
+ * the `BroadcastHandler`s (allowing them to modify or discard message) and,
+ * once list is exhausted, calls `TeamMessage()` method. However it also
+ * propagates additional method `AcceptBroadcastText()` down the linked list.
+ * Supposedly it's `AcceptBroadcastText()` that you should overload when making
+ * your own `BroadcastHandler`, but this setup creates...
+ * ISSUE 2: by default `AcceptBroadcastText()` is propagated ANEW
+ * inside EVERY `BroadcastText()` call (that is also propagated). This means
+ * that if there is several `BroadcastHandler`s in the chain before yours -
+ * every single one of them (including your own!) will call
+ * `AcceptBroadcastText()` for you. This means that `AcceptBroadcastText()` is
+ * going to be called several times for every broadcasted message unless your
+ * `BroadcastHandler` is added at the very root of the linked list.
+ *
+ * All that remains is to consider `AllowBroadcastLocalized()` method.
+ * It works in similar way to the previous two, but is simpler: it does not
+ * have an analogue to the `AllowsBroadcast()` method and simply calls
+ * `BroadcastLocalized()` for every player controller, spectator or not.
+ * `BroadcastLocalized()` works exactly the same way as `BroadcastText()`, but
+ * with uses `AcceptBroadcastLocalized()` instead of `AcceptBroadcastText()`,
+ * completely mirroring issue 2.
+ *
+ * Summary.
+ * Methods only called for root `BroadcastHandler`:
+ * 1. `Broadcast()` - starts text message broadcast;
+ * 2. `BroadcastTeam()` - starts team text message broadcast;
+ * 3. `AllowBroadcastLocalized()` - starts localized message broadcast;
+ * 4. `AllowsBroadcast()` - called for text message broadcasts (team or
+ * not) to check if they are allowed.
+ * Methods that are propagated down the linked list of `BroadcastHandler`s:
+ * 1. `HandlerAllowsBroadcast()` - called before broadcasting text message
+ * (team or not), before any `BroadcastText()` or
+ * `AcceptBroadcastText()` call;
+ * 2. `BroadcastText()` - once for every controller that should receive
+ * a certain text message (unless blocked at some point);
+ * 3. `AcceptBroadcastText()` - called shit ton of times inside
+ * `BroadcastText()` to check if message can be propagated;
+ * 4. `BroadcastLocalized()` - once for every controller that should
+ * receive a certain text message (unless blocked at some point);
+ * 5. `AcceptBroadcastLocalized()` - called shit ton of times inside
+ * `BroadcastLocalized()` to check if message can be propagated;
+ *
+ * What are we going to do?
+ * We want our `BroadcastHandler` to work at any place inside the
+ * linked list, but also to side step issue 2 completely, so we will use
+ * `BroadcastText()` and `BroadcastLocalized()` methods for catching messages
+ * sent to particular players. We do not want to reimplement `Broadcast()`,
+ * `BroadcastTeam()` or `AllowBroadcastLocalized()` (partially because it would
+ * mostly involve copy-pasting copyrighted code) and will instead inject some
+ * code to reliably catch the moment broadcast has started in case we are
+ * actually placed at the root.
+ * We also want to track broadcast by message parameters in
+ * `BroadcastText()` and `BroadcastLocalized()` methods in case we are not
+ * injected at the root to resolve issue 1. When we detect any difference in
+ * passed parameters (or players message was broadcasted to get repeated) -
+ * we declare a new broadcast. This methods is not perfect, but is likely
+ * the best possible guess for the start of broadcast.
+ */
+
+
+// This is only relevant for `BHIJ_Root` injection level.
+// The way vanilla `BroadcastHandler` works - it can check if broadcast is
+// possible for any actor, but for actually sending the text messages it will
+// try to extract `PlayerReplicationInfo` from it and will simply pass `none`
+// for a sender if it can't.
+// We remember senders in this array in order to pass real ones to
+// our events.
+// We use an array instead of a single variable is to account for possible
+// folded calls (when handling of broadcast events leads to another
+// message generation).
+var private array storedSenders;
+
+// This is only relevant for `BHIJ_Root` injection level.
+// We do not want to reimplement functions `Broadcast()`, `BroadcastTeam()`
+// or `AllowBroadcastLocalized()` that root `BroadcastHandler` calls to do
+// checks and send messages to individual players.
+// Instead we would like to inject our own code and call parent version of
+// these methods.
+// We would also like to insert our code in some of the functions between
+// `AllowsBroadcast()` check and actual broadcasting, so we cannot simply use
+// a `super.AllowsBroadcast()` call that calls both of them in order.
+// Instead we move `AllowsBroadcast()` unto our own methods:
+// we first manually do `AllowsBroadcast()` check, then perform our logic and
+// then make a super call, but with `blockAllowsBroadcast` flag set to `true`,
+// which causes overloaded `AllowsBroadcast()` to omit checks that we've
+// already performed.
+var private bool blockAllowsBroadcast;
+
+/*
+ * In case of `BHIJ_Registered` injection level, we do not get notified
+ * when a message starts getting broadcasted through `Broadcast()`,
+ * `BroadcastTeam()` and `AcceptBroadcastLocalized()`.
+ * Instead we are only notified when a message is broadcasted to
+ * a particular player, so with 2 players instead of sequence `Broadcast()`,
+ * `AcceptBroadcastText()`, `AcceptBroadcastText()`
+ * we get `AcceptBroadcastText()`, `AcceptBroadcastText()`.
+ * This means that we can only guess when new broadcast was initiated.
+ * We do this by:
+ * 1. Recording broadcast instigator (sender) and his message. If any of
+ * these variables change - we assume it's a new broadcast.
+ * 2. Recording players that already received that message, - if message is
+ * resend to one of them - it's a new broadcast
+ * (of possibly duplicate message).
+ * 3. All broadcasted messages are sent to all players within 1 tick, so
+ * any first message within each tick is a start of a new broadcast.
+ *
+ * Check logic is implemented in `UpdateTrackingWithTextMessage()` and
+ * `UpdateTrackingWithLocalizedMessage()` methods.
+ */
+// Are we already already tracking any broadcast? Helps to track for point 3.
+var private bool trackingBroadcast;
+// Sender of the current broadcast. Helps to track for point 1.
+var private Actor currentBroadcastInstigator;
+// Players that already received current broadcast. Helps to track for point 2.
+var private array currentBroadcastReceivers;
+// Is current broadcast sending a
+// text message (`Broadcast()` and `BroadcastTeam()`)
+// or localized message (`AcceptBroadcastLocalized()`)?
+// Helps to track message for point 1.
+var private bool broadcastingLocalizedMessage;
+// Variables to stored text message. Helps to track for point 1.
+var private string currentTextMessageContents;
+var private name currentTextMessageType;
+// We allow connected signals to modify message for all players before
+// `BroadcastText()` or `BroadcastLocalized()` calls and can do so in case of
+// `BHIJ_Registered`.
+// But for `BHIJ_Registered` we can only catch those calls and must
+// manually remember modifications we have made. We store those modifications
+// in this variable. It resets when new message is detected.
+var private string currentlyUsedMessage;
+// Remember if currently tracked message was rejected by either
+// `BroadcastText()` or `BroadcastLocalized()`.
+var private bool currentMessageRejected;
+// Variables to stored localized message. Helps to track for point 1.
+var private BroadcastAPI.LocalizedMessage currentLocalizedMessage;
+
+var private Broadcast_OnBroadcastCheck_Signal onBroadcastCheck;
+var private Broadcast_OnHandleLocalized_Signal onHandleLocalized;
+var private Broadcast_OnHandleLocalizedFor_Signal onHandleLocalizedFor;
+var private Broadcast_OnHandleText_Signal onHandleText;
+var private Broadcast_OnHandleTextFor_Signal onHandleTextFor;
+
+public final function Initialize(UnrealService service)
+{
+ if (usedInjectionLevel != BHIJ_Root) {
+ Disable('Tick');
+ }
+ if (service == none) {
+ return;
+ }
+ onBroadcastCheck = Broadcast_OnBroadcastCheck_Signal(
+ service.GetSignal(class'Broadcast_OnBroadcastCheck_Signal'));
+ onHandleLocalized = Broadcast_OnHandleLocalized_Signal(
+ service.GetSignal(class'Broadcast_OnHandleLocalized_Signal'));
+ onHandleLocalizedFor = Broadcast_OnHandleLocalizedFor_Signal(
+ service.GetSignal(class'Broadcast_OnHandleLocalizedFor_Signal'));
+ onHandleText = Broadcast_OnHandleText_Signal(
+ service.GetSignal(class'Broadcast_OnHandleText_Signal'));
+ onHandleTextFor = Broadcast_OnHandleTextFor_Signal(
+ service.GetSignal(class'Broadcast_OnHandleTextFor_Signal'));
+}
+
+private function bool IsCurrentBroadcastReceiver(PlayerController receiver)
+{
+ local int i;
+ for (i = 0; i < currentBroadcastReceivers.length; i += 1)
+ {
+ if (currentBroadcastReceivers[i] == receiver) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Return `true` if new broadcast was detected
+private function bool UpdateTrackingWithTextMessage(
+ PlayerReplicationInfo senderPRI,
+ PlayerController receiver,
+ string message,
+ name messageType)
+{
+ local bool isCurrentBroadcastContinuation;
+ if (usedInjectionLevel != BHIJ_Registered) {
+ return false;
+ }
+ isCurrentBroadcastContinuation = trackingBroadcast
+ && (senderPRI == currentBroadcastInstigator)
+ && (!broadcastingLocalizedMessage)
+ && (message == currentTextMessageContents)
+ && (messageType == currentTextMessageType)
+ && !IsCurrentBroadcastReceiver(receiver);
+ if (isCurrentBroadcastContinuation)
+ {
+ currentBroadcastReceivers[currentBroadcastReceivers.length] = receiver;
+ return false;
+ }
+ trackingBroadcast = true;
+ broadcastingLocalizedMessage = false;
+ currentBroadcastInstigator = senderPRI;
+ currentTextMessageContents = message;
+ currentlyUsedMessage = message;
+ currentTextMessageType = messageType;
+ currentMessageRejected = false;
+ currentBroadcastReceivers.length = 0;
+ return true;
+}
+
+// Return `true` if new broadcast was detected
+private function bool UpdateTrackingWithLocalizedMessage(
+ Actor sender,
+ PlayerController receiver,
+ BroadcastAPI.LocalizedMessage localizedMessage)
+{
+ local bool isCurrentBroadcastContinuation;
+ if (usedInjectionLevel != BHIJ_Registered) {
+ return false;
+ }
+ isCurrentBroadcastContinuation = trackingBroadcast
+ && (sender == currentBroadcastInstigator)
+ && (broadcastingLocalizedMessage)
+ && (localizedMessage == currentLocalizedMessage)
+ && !IsCurrentBroadcastReceiver(receiver);
+ if (isCurrentBroadcastContinuation)
+ {
+ currentBroadcastReceivers[currentBroadcastReceivers.length] = receiver;
+ return false;
+ }
+ trackingBroadcast = true;
+ broadcastingLocalizedMessage = true;
+ currentBroadcastInstigator = sender;
+ currentLocalizedMessage = localizedMessage;
+ currentBroadcastReceivers.length = 0;
+ currentMessageRejected = false;
+ return true;
+}
+
+// Makes us stop tracking current broadcast
+private function ResetTracking()
+{
+ trackingBroadcast = false;
+ // Only important to forget objects and actors, since keeping
+ // references can cause issues.
+ // Other fields can remain "dirty", since they will be rewritten before
+ // they will ever be used.
+ currentBroadcastInstigator = none;
+ currentLocalizedMessage.relatedPRI1 = none;
+ currentLocalizedMessage.relatedPRI2 = none;
+ currentLocalizedMessage.relatedObject = none;
+}
+
+public function bool HandlerAllowsBroadcast(Actor broadcaster, int sentTextNum)
+{
+ local bool canBroadcast;
+ // Fire and check signals
+ canBroadcast = onBroadcastCheck.Emit(broadcaster, sentTextNum);
+ // Check other broadcast handlers (if present)
+ if (canBroadcast && nextBroadcastHandler != none)
+ {
+ canBroadcast = nextBroadcastHandler
+ .HandlerAllowsBroadcast(broadcaster, sentTextNum);
+ }
+ if (canBroadcast && usedInjectionLevel == BHIJ_Registered)
+ {
+ // This method is only really called by the `AllowsBroadcast()` at the
+ // beginning of either `Broadcast()` or `BroadcastTeam()` methods.
+ // Meaning that new broadcast has started for sure.
+ ResetTracking();
+ }
+ return canBroadcast;
+}
+
+function Broadcast(Actor sender, coerce string message, optional name type)
+{
+ local bool canTryToBroadcast;
+ if (!AllowsBroadcast(sender, Len(message))) {
+ return;
+ }
+ canTryToBroadcast = onHandleText.Emit(sender, message, type, false);
+ if (canTryToBroadcast)
+ {
+ storedSenders[storedSenders.length] = sender;
+ blockAllowsBroadcast = true;
+ super.Broadcast(sender, message, type);
+ blockAllowsBroadcast = false;
+ storedSenders.length = storedSenders.length - 1;
+ }
+}
+
+function BroadcastTeam(
+ Controller sender,
+ coerce string message,
+ optional name type)
+{
+ local bool canTryToBroadcast;
+ if (!AllowsBroadcast(sender, Len(message))) {
+ return;
+ }
+ canTryToBroadcast = onHandleText.Emit(sender, message, type, true);
+ if (canTryToBroadcast)
+ {
+ storedSenders[storedSenders.length] = sender;
+ blockAllowsBroadcast = true;
+ super.BroadcastTeam(sender, message, type);
+ blockAllowsBroadcast = false;
+ storedSenders.length = storedSenders.length - 1;
+ }
+}
+
+event AllowBroadcastLocalized(
+ Actor sender,
+ class message,
+ optional int switch,
+ optional PlayerReplicationInfo relatedPRI1,
+ optional PlayerReplicationInfo relatedPRI2,
+ optional Object optionalObject)
+{
+ local bool canTryToBroadcast;
+ local BroadcastAPI.LocalizedMessage packedMessage;
+ packedMessage.class = message;
+ packedMessage.id = switch;
+ packedMessage.relatedPRI1 = relatedPRI1;
+ packedMessage.relatedPRI2 = relatedPRI2;
+ packedMessage.relatedObject = optionalObject;
+ canTryToBroadcast = onHandleLocalized.Emit(sender, packedMessage);
+ if (canTryToBroadcast)
+ {
+ super.AllowBroadcastLocalized( sender, message, switch,
+ relatedPRI1, relatedPRI2,
+ optionalObject);
+ }
+}
+
+function bool AllowsBroadcast(Actor broadcaster, int len)
+{
+ if (blockAllowsBroadcast) {
+ return true; // we have already done this check and it passed
+ }
+ return super.AllowsBroadcast(broadcaster, len);
+}
+
+function BroadcastText(
+ PlayerReplicationInfo senderPRI,
+ PlayerController receiver,
+ string message,
+ optional name type)
+{
+ local bool canBroadcast;
+ local Actor sender;
+ if (senderPRI != none) {
+ sender = PlayerController(senderPRI.owner);
+ }
+ if (sender == none && storedSenders.length > 0) {
+ sender = storedSenders[storedSenders.length - 1];
+ }
+ if (usedInjectionLevel == BHIJ_Registered)
+ {
+ if (UpdateTrackingWithTextMessage(senderPRI, receiver, message, type))
+ {
+ currentMessageRejected = !onHandleText
+ .Emit(sender, message, type, false);
+ currentlyUsedMessage = message;
+ }
+ else {
+ message = currentlyUsedMessage;
+ }
+ if (currentMessageRejected) {
+ return;
+ }
+ }
+ canBroadcast = onHandleTextFor.Emit(receiver, sender, message, type);
+ if (!canBroadcast) {
+ return;
+ }
+ super.BroadcastText(senderPRI, receiver, message, type);
+}
+
+function BroadcastLocalized(
+ Actor sender,
+ PlayerController receiver,
+ class message,
+ optional int switch,
+ optional PlayerReplicationInfo relatedPRI1,
+ optional PlayerReplicationInfo relatedPRI2,
+ optional Object obj)
+{
+ local bool canBroadcast;
+ local BroadcastAPI.LocalizedMessage packedMessage;
+ packedMessage.class = message;
+ packedMessage.id = switch;
+ packedMessage.relatedPRI1 = relatedPRI1;
+ packedMessage.relatedPRI2 = relatedPRI2;
+ packedMessage.relatedObject = obj;
+ if ( usedInjectionLevel == BHIJ_Registered
+ && UpdateTrackingWithLocalizedMessage(sender, receiver, packedMessage))
+ {
+ currentMessageRejected = !onHandleLocalized.Emit(sender, packedMessage);
+ }
+ if (currentMessageRejected) {
+ return;
+ }
+ canBroadcast = onHandleLocalizedFor.Emit(receiver, sender, packedMessage);
+ if (!canBroadcast) {
+ return;
+ }
+ super.BroadcastLocalized( sender, receiver, message, switch,
+ relatedPRI1, relatedPRI2, obj);
+}
+
+event Tick(float delta)
+{
+ ResetTracking();
+}
+
+// senders, out for handletext
+defaultproperties
+{
+ blockAllowsBroadcast = false
+ usedInjectionLevel = BHIJ_Root
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Signal.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Signal.uc
new file mode 100644
index 0000000..4d99c72
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Signal.uc
@@ -0,0 +1,46 @@
+/**
+ * Signal class implementation for `BroadcastAPI`'s `OnBroadcastCheck` signal.
+ * 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 .
+ */
+class Broadcast_OnBroadcastCheck_Signal extends Signal;
+
+public final function bool Emit(Actor broadcaster, int newMessageLength)
+{
+ local Slot nextSlot;
+ local bool nextReply;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ nextReply = Broadcast_OnBroadcastCheck_Slot(nextSlot)
+ .connect(broadcaster, newMessageLength);
+ if (!nextReply && !nextSlot.IsEmpty())
+ {
+ CleanEmptySlots();
+ return false;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return true;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Broadcast_OnBroadcastCheck_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Slot.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Slot.uc
new file mode 100644
index 0000000..710e2d6
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Slot.uc
@@ -0,0 +1,41 @@
+/**
+ * Slot class implementation for `BroadcastAPI`'s `OnBroadcastCheck` signal.
+ * 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 .
+ */
+class Broadcast_OnBroadcastCheck_Slot extends Slot;
+
+delegate bool connect(Actor broadcaster, int newMessageLength)
+{
+ DummyCall();
+ return true;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Signal.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Signal.uc
new file mode 100644
index 0000000..b9387f2
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Signal.uc
@@ -0,0 +1,51 @@
+/**
+ * Signal class implementation for `BroadcastAPI`'s `OnHandleLocalizedFor`
+ * signal.
+ * 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 .
+ */
+class Broadcast_OnHandleLocalizedFor_Signal extends Signal
+ dependson(BroadcastAPI);
+
+public final function bool Emit(
+ PlayerController receiver,
+ Actor sender,
+ BroadcastAPI.LocalizedMessage packedMessage)
+{
+ local Slot nextSlot;
+ local bool nextReply;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ nextReply = Broadcast_OnHandleLocalizedFor_Slot(nextSlot)
+ .connect(receiver, sender, packedMessage);
+ if (!nextReply && !nextSlot.IsEmpty())
+ {
+ CleanEmptySlots();
+ return false;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return true;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Broadcast_OnHandleLocalizedFor_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Slot.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Slot.uc
new file mode 100644
index 0000000..5c7a541
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Slot.uc
@@ -0,0 +1,46 @@
+/**
+ * Slot class implementation for `BroadcastAPI`'s `OnHandleLocalizedFor`
+ * signal.
+ * 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 .
+ */
+class Broadcast_OnHandleLocalizedFor_Slot extends Slot
+ dependson(BroadcastAPI);
+
+delegate bool connect(
+ PlayerController receiver,
+ Actor sender,
+ BroadcastAPI.LocalizedMessage packedMessage)
+{
+ DummyCall();
+ return true;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Signal.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Signal.uc
new file mode 100644
index 0000000..da8c3c9
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Signal.uc
@@ -0,0 +1,50 @@
+/**
+ * Signal class implementation for `BroadcastAPI`'s `OnHandleLocalized`
+ * signal.
+ * 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 .
+ */
+class Broadcast_OnHandleLocalized_Signal extends Signal
+ dependson(BroadcastAPI);
+
+public final function bool Emit(
+ Actor sender,
+ BroadcastAPI.LocalizedMessage packedMessage)
+{
+ local Slot nextSlot;
+ local bool nextReply;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ nextReply = Broadcast_OnHandleLocalized_Slot(nextSlot)
+ .connect(sender, packedMessage);
+ if (!nextReply && !nextSlot.IsEmpty())
+ {
+ CleanEmptySlots();
+ return false;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return true;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Broadcast_OnHandleLocalized_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Slot.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Slot.uc
new file mode 100644
index 0000000..c01ee1b
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Slot.uc
@@ -0,0 +1,44 @@
+/**
+ * Slot class implementation for `BroadcastAPI`'s `OnHandleLocalized` signal.
+ * 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 .
+ */
+class Broadcast_OnHandleLocalized_Slot extends Slot
+ dependson(BroadcastAPI);
+
+delegate bool connect(
+ Actor sender,
+ BroadcastAPI.LocalizedMessage packedMessage)
+{
+ DummyCall();
+ return true;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Signal.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Signal.uc
new file mode 100644
index 0000000..4e9fce8
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Signal.uc
@@ -0,0 +1,50 @@
+/**
+ * Signal class implementation for `BroadcastAPI`'s `OnHandleTextFor` signal.
+ * 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 .
+ */
+class Broadcast_OnHandleTextFor_Signal extends Signal;
+
+public final function bool Emit(
+ PlayerController receiver,
+ Actor sender,
+ string message,
+ name type)
+{
+ local Slot nextSlot;
+ local bool nextReply;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ nextReply = Broadcast_OnHandleTextFor_Slot(nextSlot)
+ .connect(receiver, sender, message, type);
+ if (!nextReply && !nextSlot.IsEmpty())
+ {
+ CleanEmptySlots();
+ return false;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return true;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Broadcast_OnHandleTextFor_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Slot.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Slot.uc
new file mode 100644
index 0000000..47b328b
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Slot.uc
@@ -0,0 +1,45 @@
+/**
+ * Slot class implementation for `BroadcastAPI`'s `OnHandleTextFor` signal.
+ * 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 .
+ */
+class Broadcast_OnHandleTextFor_Slot extends Slot;
+
+delegate bool connect(
+ PlayerController receiver,
+ Actor sender,
+ string message,
+ name type)
+{
+ DummyCall();
+ return true;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Signal.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Signal.uc
new file mode 100644
index 0000000..d93d2af
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Signal.uc
@@ -0,0 +1,50 @@
+/**
+ * Signal class implementation for `BroadcastAPI`'s `OnHandleText` signal.
+ * 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 .
+ */
+class Broadcast_OnHandleText_Signal extends Signal;
+
+public final function bool Emit(
+ Actor sender,
+ out string message,
+ name type,
+ bool teamMessage)
+{
+ local Slot nextSlot;
+ local bool nextReply;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ nextReply = Broadcast_OnHandleText_Slot(nextSlot)
+ .connect(sender, message, type, teamMessage);
+ if (!nextReply && !nextSlot.IsEmpty())
+ {
+ CleanEmptySlots();
+ return false;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return true;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Broadcast_OnHandleText_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Slot.uc b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Slot.uc
new file mode 100644
index 0000000..ca8e437
--- /dev/null
+++ b/sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Slot.uc
@@ -0,0 +1,45 @@
+/**
+ * Slot class implementation for `BroadcastAPI`'s `OnHandleText` signal.
+ * 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 .
+ */
+class Broadcast_OnHandleText_Slot extends Slot;
+
+delegate bool connect(
+ Actor sender,
+ out string message,
+ name type,
+ bool teamMessage)
+{
+ DummyCall();
+ return true;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Services/Connection/ConnectionService.uc b/sources/Unreal/Connections/ConnectionService.uc
similarity index 87%
rename from sources/Services/Connection/ConnectionService.uc
rename to sources/Unreal/Connections/ConnectionService.uc
index 82f8e1f..e24b39a 100644
--- a/sources/Services/Connection/ConnectionService.uc
+++ b/sources/Unreal/Connections/ConnectionService.uc
@@ -24,9 +24,11 @@ class ConnectionService extends Service;
// Stores basic information about a connection
struct Connection
{
+ var public PlayerController controllerReference;
+ // Remember these for the time `controllerReference` dies
+ // and becomes `none`.
var public string networkAddress;
var public string idHash;
- var public PlayerController controllerReference;
// Reference to `AcediaReplicationInfo` for this client,
// in case it was created.
var private AcediaReplicationInfo acediaRI;
@@ -71,6 +73,7 @@ protected function OnLaunch()
{
local Controller nextController;
local PlayerController nextPlayerController;
+ _.unreal.mutator.OnCheckReplacement(_self).connect = TryAddingController;
onConnectionEstablishedSignal =
Connection_Signal(_.memory.Allocate(class'Connection_Signal'));
onConnectionLostSignal =
@@ -92,6 +95,8 @@ protected function OnShutdown()
default.activeConnections = activeConnections;
_.memory.Free(onConnectionEstablishedSignal);
_.memory.Free(onConnectionLostSignal);
+ onConnectionEstablishedSignal = none;
+ onConnectionLostSignal = none;
}
// Returning `true` guarantees that `controllerToCheck != none`
@@ -164,7 +169,9 @@ public final function Connection GetConnection(PlayerController player)
local int connectionIndex;
local Connection emptyConnection;
connectionIndex = GetConnectionIndex(player);
- if (connectionIndex < 0) return emptyConnection;
+ if (connectionIndex < 0) {
+ return emptyConnection;
+ }
return activeConnections[connectionIndex];
}
@@ -217,6 +224,26 @@ public final function array GetActiveConnections(
return activeConnections;
}
+function bool TryAddingController(Actor other, out byte isSuperRelevant)
+{
+ // We are looking for `KFSteamStatsAndAchievements` instead of
+ // `PlayerController` because, by the time they it's created,
+ // controller should have a valid reference to `PlayerReplicationInfo`,
+ // as well as valid network address and IDHash (steam id).
+ // However, neither of those are properly initialized at the point when
+ // `CheckReplacement` is called for `PlayerController`.
+ //
+ // Since `KFSteamStatsAndAchievements`
+ // is created soon after (at the same tick)
+ // for each new `PlayerController`,
+ // we will be detecting new users right after server
+ // detected and properly initialized them.
+ if (KFSteamStatsAndAchievements(other) == none) {
+ RegisterConnection(PlayerController(other.owner));
+ }
+ return true;
+}
+
// Check if connections are still active every tick.
// Should not take any noticeable time when no players are disconnecting.
event Tick(float delta)
@@ -226,5 +253,4 @@ event Tick(float delta)
defaultproperties
{
- requiredListeners(0) = class'MutatorListener_Connection'
}
\ No newline at end of file
diff --git a/sources/Services/Connection/Events/Connection_Signal.uc b/sources/Unreal/Connections/Events/Connection_Signal.uc
similarity index 100%
rename from sources/Services/Connection/Events/Connection_Signal.uc
rename to sources/Unreal/Connections/Events/Connection_Signal.uc
diff --git a/sources/Services/Connection/Events/Connection_Slot.uc b/sources/Unreal/Connections/Events/Connection_Slot.uc
similarity index 100%
rename from sources/Services/Connection/Events/Connection_Slot.uc
rename to sources/Unreal/Connections/Events/Connection_Slot.uc
diff --git a/sources/Services/Connection/MutatorListener_Connection.uc b/sources/Unreal/Connections/MutatorListener_Connection.uc
similarity index 95%
rename from sources/Services/Connection/MutatorListener_Connection.uc
rename to sources/Unreal/Connections/MutatorListener_Connection.uc
index d3d2210..88ff02b 100644
--- a/sources/Services/Connection/MutatorListener_Connection.uc
+++ b/sources/Unreal/Connections/MutatorListener_Connection.uc
@@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class MutatorListener_Connection extends MutatorListenerBase
+class MutatorListener_Connection extends Object
abstract;
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
@@ -49,5 +49,4 @@ static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
defaultproperties
{
- relatedEvents = class'MutatorEvents'
}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/AcediaGameRules.uc b/sources/Unreal/GameRulesAPI/AcediaGameRules.uc
similarity index 65%
rename from sources/Unreal/GameRules/AcediaGameRules.uc
rename to sources/Unreal/GameRulesAPI/AcediaGameRules.uc
index cc02d05..abc4b06 100644
--- a/sources/Unreal/GameRules/AcediaGameRules.uc
+++ b/sources/Unreal/GameRulesAPI/AcediaGameRules.uc
@@ -26,24 +26,42 @@ var private GameRules_OnCheckEndGame_Signal onCheckEndGameSignal;
var private GameRules_OnCheckScore_Signal onCheckScoreSignal;
var private GameRules_OnOverridePickupQuery_Signal onOverridePickupQuery;
var private GameRules_OnNetDamage_Signal onNetDamage;
+var private GameRules_OnPreventDeath_Signal onPreventDeath;
+var private GameRules_OnScoreKill_Signal onScoreKill;
-public final function Initialize(unrealService service)
+public final function Initialize(UnrealService service)
{
if (service == none) {
return;
}
- onFindPlayerStartSignal = GameRules_OnFindPlayerStart_Signal(
+ onFindPlayerStartSignal = GameRules_OnFindPlayerStart_Signal(
service.GetSignal(class'GameRules_OnFindPlayerStart_Signal'));
- onHandleRestartGameSignal = GameRules_OnHandleRestartGame_Signal(
+ onHandleRestartGameSignal = GameRules_OnHandleRestartGame_Signal(
service.GetSignal(class'GameRules_OnHandleRestartGame_Signal'));
- onCheckEndGameSignal = GameRules_OnCheckEndGame_Signal(
+ onCheckEndGameSignal = GameRules_OnCheckEndGame_Signal(
service.GetSignal(class'GameRules_OnCheckEndGame_Signal'));
- onCheckScoreSignal = GameRules_OnCheckScore_Signal(
+ onCheckScoreSignal = GameRules_OnCheckScore_Signal(
service.GetSignal(class'GameRules_OnCheckScore_Signal'));
- onOverridePickupQuery = GameRules_OnOverridePickupQuery_Signal(
+ onOverridePickupQuery = GameRules_OnOverridePickupQuery_Signal(
service.GetSignal(class'GameRules_OnOverridePickupQuery_Signal'));
- onNetDamage = GameRules_OnNetDamage_Signal(
+ onNetDamage = GameRules_OnNetDamage_Signal(
service.GetSignal(class'GameRules_OnNetDamage_Signal'));
+ onPreventDeath = GameRules_OnPreventDeath_Signal(
+ service.GetSignal(class'GameRules_OnPreventDeath_Signal'));
+ onScoreKill = GameRules_OnScoreKill_Signal(
+ service.GetSignal(class'GameRules_OnScoreKill_Signal'));
+}
+
+public final function Cleanup()
+{
+ onFindPlayerStartSignal = none;
+ onHandleRestartGameSignal = none;
+ onCheckEndGameSignal = none;
+ onCheckScoreSignal = none;
+ onOverridePickupQuery = none;
+ onNetDamage = none;
+ onPreventDeath = none;
+ onScoreKill = none;
}
function string GetRules()
@@ -62,6 +80,7 @@ function NavigationPoint FindPlayerStart(
optional string incomingName)
{
local NavigationPoint result;
+ // Use first value returned by anything
if (onFindPlayerStartSignal != none) {
result = onFindPlayerStartSignal.Emit(player, inTeam, incomingName);
}
@@ -74,6 +93,8 @@ function NavigationPoint FindPlayerStart(
function bool HandleRestartGame()
{
local bool result;
+ // `true` return value needs to override `false` values returned by any
+ // other sources
if (onHandleRestartGameSignal != none) {
result = onHandleRestartGameSignal.Emit();
}
@@ -87,10 +108,12 @@ function bool CheckEndGame(PlayerReplicationInfo winner, string reason)
{
local bool result;
result = true;
+ // `false` return value needs to override `true` values returned by any
+ // other sources
if (onCheckEndGameSignal != none) {
result = onCheckEndGameSignal.Emit(winner, reason);
}
- if (nextGameRules != none && !nextGameRules.HandleRestartGame()) {
+ if (nextGameRules != none && !nextGameRules.CheckEndGame(winner, reason)) {
return false;
}
return result;
@@ -99,6 +122,8 @@ function bool CheckEndGame(PlayerReplicationInfo winner, string reason)
function bool CheckScore(PlayerReplicationInfo scorer)
{
local bool result;
+ // `true` return value needs to override `false` values returned by any
+ // other sources
if (onCheckScoreSignal != none) {
result = onCheckScoreSignal.Emit(scorer);
}
@@ -150,6 +175,38 @@ function int NetDamage(
return damage;
}
+function bool PreventDeath(
+ Pawn killed,
+ Controller killer,
+ class damageType,
+ Vector hitLocation)
+{
+ local bool shouldPrevent;
+ if (onPreventDeath != none)
+ {
+ shouldPrevent = onPreventDeath.Emit(killed, killer,
+ damageType, hitLocation);
+ }
+ if (shouldPrevent) {
+ return true;
+ }
+ if (nextGameRules != none)
+ {
+ return nextGameRules.PreventDeath( killed, killer,
+ damageType, hitLocation);
+ }
+ return false;
+}
+
+function ScoreKill(Controller killer, Controller killed)
+{
+ if (onScoreKill != none) {
+ onScoreKill.Emit(killer, killed);
+ }
+ if (nextGameRules != none)
+ nextGameRules.ScoreKill(killer, killed);
+}
+
defaultproperties
{
}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnCheckEndGame_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Signal.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnCheckEndGame_Signal.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Signal.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnCheckEndGame_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Slot.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnCheckEndGame_Slot.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Slot.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnCheckScore_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Signal.uc
similarity index 95%
rename from sources/Unreal/GameRules/Events/GameRules_OnCheckScore_Signal.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Signal.uc
index 012612f..e445902 100644
--- a/sources/Unreal/GameRules/Events/GameRules_OnCheckScore_Signal.uc
+++ b/sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Signal.uc
@@ -25,12 +25,11 @@ public final function bool Emit(PlayerReplicationInfo scorer)
local bool result, nextReply;
StartIterating();
nextSlot = GetNextSlot();
- result = true;
while (nextSlot != none)
{
nextReply = GameRules_OnCheckScore_Slot(nextSlot).connect(scorer);
if (!nextReply && !nextSlot.IsEmpty()) {
- result = result && nextReply;
+ result = result || nextReply;
}
nextSlot = GetNextSlot();
}
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnCheckScore_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Slot.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnCheckScore_Slot.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Slot.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Signal.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Signal.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Signal.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Slot.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Slot.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Slot.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnHandleRestartGame_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Signal.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnHandleRestartGame_Signal.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Signal.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnHandleRestartGame_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Slot.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnHandleRestartGame_Slot.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Slot.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Signal.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Signal.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Signal.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Slot.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Slot.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Slot.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Signal.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Signal.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Signal.uc
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Slot.uc
similarity index 100%
rename from sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Slot.uc
rename to sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Slot.uc
diff --git a/sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Signal.uc
new file mode 100644
index 0000000..e4ec1a0
--- /dev/null
+++ b/sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Signal.uc
@@ -0,0 +1,51 @@
+/**
+ * Signal class implementation for `GameRulesAPI`'s
+ * `OnPreventDeathSignal` signal.
+ * 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 .
+ */
+class GameRules_OnPreventDeath_Signal extends Signal;
+
+public final function bool Emit(
+ Pawn killed,
+ Controller killer,
+ class damageType,
+ Vector hitLocation)
+{
+ local Slot nextSlot;
+ local bool shouldPrevent;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ shouldPrevent = GameRules_OnPreventDeath_Slot(nextSlot)
+ .connect(killed, killer, damageType, hitLocation);
+ if (shouldPrevent && !nextSlot.IsEmpty())
+ {
+ CleanEmptySlots();
+ return shouldPrevent;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return false;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'GameRules_OnPreventDeath_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Slot.uc
new file mode 100644
index 0000000..9fce18f
--- /dev/null
+++ b/sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Slot.uc
@@ -0,0 +1,47 @@
+/**
+ * Slot class implementation for `GameRulesAPI`'s
+ * `OnPreventDeathSignal` signal.
+ * 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 .
+ */
+class GameRules_OnPreventDeath_Slot extends Slot;
+
+delegate bool connect(
+ Pawn killed,
+ Controller killer,
+ class damageType,
+ Vector hitLocation)
+{
+ DummyCall();
+ // Do not override pickup queue by default
+ return false;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Signal.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Signal.uc
new file mode 100644
index 0000000..c7b69b7
--- /dev/null
+++ b/sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Signal.uc
@@ -0,0 +1,38 @@
+/**
+ * Signal class implementation for `GameRulesAPI`'s `OnScoreKill` signal.
+ * 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 .
+ */
+class GameRules_OnScoreKill_Signal extends Signal;
+
+public final function Emit(Controller killer, Controller killed)
+{
+ local Slot nextSlot;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ GameRules_OnScoreKill_Slot(nextSlot).connect(killer, killed);
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'GameRules_OnScoreKill_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Slot.uc b/sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Slot.uc
new file mode 100644
index 0000000..d075280
--- /dev/null
+++ b/sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Slot.uc
@@ -0,0 +1,40 @@
+/**
+ * Slot class implementation for `GameRulesAPI`'s `OnScoreKill` signal.
+ * 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 .
+ */
+class GameRules_OnScoreKill_Slot extends Slot;
+
+delegate connect(Controller killer, Controller killed)
+{
+ DummyCall();
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/GameRulesAPI.uc b/sources/Unreal/GameRulesAPI/GameRulesAPI.uc
similarity index 66%
rename from sources/Unreal/GameRules/GameRulesAPI.uc
rename to sources/Unreal/GameRulesAPI/GameRulesAPI.uc
index a97013e..97d2b54 100644
--- a/sources/Unreal/GameRules/GameRulesAPI.uc
+++ b/sources/Unreal/GameRulesAPI/GameRulesAPI.uc
@@ -58,6 +58,8 @@ public final function GameRules_OnFindPlayerStart_Slot OnFindPlayerStart(
* bool ()
*
* @return `true` if you want to prevent game restart and `false` otherwise.
+ * `true` returned by one of the handlers overrides `false` values returned
+ * by others.
*/
/* SIGNAL */
public final function GameRules_OnHandleRestartGame_Slot OnHandleRestartGame(
@@ -82,7 +84,8 @@ public final function GameRules_OnHandleRestartGame_Slot OnHandleRestartGame(
* @param winner Replication info of the supposed winner of the game.
* @param reason String with a description about how/why `winner` has won.
* @return `false` if you want to prevent game from ending
- * and `false` otherwise.
+ * and `true` otherwise. `false` returned by one of the handlers overrides
+ * `true` values returned by others.
*/
/* SIGNAL */
public final function GameRules_OnCheckEndGame_Slot OnCheckEndGame(
@@ -95,21 +98,21 @@ public final function GameRules_OnCheckEndGame_Slot OnCheckEndGame(
return GameRules_OnCheckEndGame_Slot(signal.NewSlot(receiver));
}
-/* CheckScore()
-
-*/
/**
* Check if this score means the game ends.
*
* Return `true` to override `GameInfo`'s `CheckScore()`, or if game was ended
* (with a call to `Level.Game.EndGame()`).
*
+ * This signal will always be propagated to all registered slots.
+ *
* [Signature]
* bool (PlayerReplicationInfo scorer)
*
* @param scorer For whom to do a score check.
* @return `true` to override `GameInfo`'s `CheckScore()`, or if game was ended
- * and `false` otherwise.
+ * and `false` otherwise. `true` returned by one of the handlers overrides
+ * `false` values returned by others.
*/
/* SIGNAL */
public final function GameRules_OnCheckScore_Slot OnCheckScore(
@@ -123,8 +126,8 @@ public final function GameRules_OnCheckScore_Slot OnCheckScore(
}
/**
- * When pawn wants to pickup something, `GameRule`s are given a chance to
- * modify it. If one of the `Slot`s returns `true`, `allowPickup` will
+ * When pawn wants to pick something up, `GameRule`s are given a chance to
+ * modify it. If one of the `Slot`s returns `true`, `allowPickup` will
* determine if the object can be picked up.
* Overriding via this method allows to completely bypass check against
* `Pawn`'s inventory's `HandlePickupQuery()` method.
@@ -154,24 +157,34 @@ public final function GameRules_OnOverridePickupQuery_Slot
}
/**
- * When pawn wants to pickup something, `GameRule`s are given a chance to
- * modify it. If one of the `Slot`s returns `true`, `allowPickup` will
- * determine if the object can be picked up.
- * Overriding via this method allows to completely bypass check against
- * `Pawn`'s inventory's `HandlePickupQuery()` method.
+ * When pawn gets damaged, `GameRule`s are given a chance to modify that
+ * damage.
*
* [Signature]
- * bool (Pawn other, Pickup item, out byte allowPickup)
+ * int (
+ * int originalDamage,
+ * int damage,
+ * Pawn injured,
+ * Pawn instigatedBy,
+ * Vector hitLocation,
+ * out Vector momentum,
+ * class damageType)
*
- * @param other Pawn which will potentially pickup `item`.
- * @param item Pickup which `other` might potentially pickup.
- * @param allowPickup `true` if you want to force `other` to pickup an item
- * and `false` otherwise. This parameter is ignored if returned value of
- * your slot call is `false`.
- * @return `true` if you wish to override decision about pickup with
- * `allowPickup` and `false` if you do not want to make that decision.
- * If you do decide to override decision by returning `true` - this signal
- * will not be propagated to the rest of the slots.
+ * @param originalDamage Damage that was originally meant to be dealt to
+ * the `Pawn`, before any of th `GameRules`' modifications.
+ * @param damage Damage value to be dealt to the `Pawn` as it was
+ * modified so fat by other `GameRules` and `OnNetDamage()`'s handlers.
+ * @param injured `Pawn` that will be dealt damage in question.
+ * @param instigatedBy `Pawn` that deals this damage.
+ * @param hitLocation "Location of the damage", e.g. place where `injured`
+ * was hit by a bullet.
+ * @param momentum Momentum that this damage source should inflict on
+ * the `injured`. Can also be modified.
+ * @param damageType Type of the damage that will be dealt to
+ * the `injured`.
+ * @return Damage value you want to be dealt to the `injured` instead of
+ * `damage`, given all of he above parameters. Note that it can be further
+ * modified by other handlers or `GameRules`.
*/
/* SIGNAL */
public final function GameRules_OnNetDamage_Slot OnNetDamage(
@@ -184,22 +197,75 @@ public final function GameRules_OnNetDamage_Slot OnNetDamage(
return GameRules_OnNetDamage_Slot(signal.NewSlot(receiver));
}
+/**
+ * When pawn is about to die, `GameRule`s are given a chance to
+ * prevent that.
+ *
+ * [Signature]
+ * bool (
+ * Pawn killed,
+ * Controller killer,
+ * class damageType,
+ * Vector hitLocation)
+ *
+ * @param killed `Pawn` that is about to be killed.
+ * @param killer `Pawn` that dealt the blow that has caused death.
+ * @param damageType `DamageType` with which finishing blow was dealt.
+ * @param hitLocation "Location of the damage", e.g. place where `injured`
+ * was hit by a bullet that caused death.
+ * @return Return `true` if you want to prevent death of the `killed` and
+ * `false` otherwise.
+ * If you do decide to prevent death by returning `true` - this signal
+ * will not be propagated to the rest of the slots.
+ */
+/* SIGNAL */
+public final function GameRules_OnPreventDeath_Slot OnPreventDeath(
+ AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ signal = service.GetSignal(class'GameRules_OnPreventDeath_Signal');
+ return GameRules_OnPreventDeath_Slot(signal.NewSlot(receiver));
+}
+
+/**
+ * Called when one `Pawn` kills another.
+ *
+ * [Signature]
+ * void (Controller killer, Controller killed)
+ *
+ * @param killer `Pawn` that caused death.
+ * @param killed Killed `Pawn`.
+ */
+/* SIGNAL */
+public final function GameRules_OnScoreKill_Slot OnScoreKill(
+ AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ signal = service.GetSignal(class'GameRules_OnScoreKill_Signal');
+ return GameRules_OnScoreKill_Slot(signal.NewSlot(receiver));
+}
+
/**
* Adds new `GameRules` class to the current `GameInfo`.
- * Does nothing if give `GameRules` class was already added before.
+ * Does nothing if given `GameRules` class was already added before.
*
* @param newRulesClass Class of rules to add.
- * @return `true` if `GameRules` were added and `false` otherwise
- * (because they were already active.)
+ * @return `GameRules` instance if it was added and `none` otherwise
+ * (can happen if rules of this class were already added).
*/
-public final function bool Add(class newRulesClass)
+public final function GameRules Add(class newRulesClass)
{
+ local GameRules newGameRules;
if (AreAdded(newRulesClass)) {
- return false;
+ return none;
}
- _.unreal.GetGameType()
- .AddGameModifier(GameRules(_.memory.Allocate(newRulesClass)));
- return true;
+ newGameRules = GameRules(_.memory.Allocate(newRulesClass));
+ _.unreal.GetGameType().AddGameModifier(newGameRules);
+ return newGameRules;
}
/**
@@ -249,8 +315,9 @@ public final function bool Remove(class rulesClassToRemove)
* Returns `none` otherwise.
*
* @param rulesClassToFind Class of rules to find.
- * @return `GameRules` of given class `rulesClassToFind` instance added to
- * `GameInfo`'s records and `none` if no such rules are currently added.
+ * @return `GameRules` instance of given class `rulesClassToFind`, that is
+ * added to `GameInfo`'s records and `none` if no such rules are
+ * currently added.
*/
public final function GameRules FindInstance(
class rulesClassToFind)
diff --git a/sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Signal.uc b/sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Signal.uc
new file mode 100644
index 0000000..50c4827
--- /dev/null
+++ b/sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Signal.uc
@@ -0,0 +1,46 @@
+/**
+ * Signal class implementation for `MutatorAPI`'s `OnCheckReplacement` signal.
+ * 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 .
+ */
+class Mutator_OnCheckReplacement_Signal extends Signal;
+
+public final function bool Emit(Actor other, out byte isSuperRelevant)
+{
+ local bool isRelevant;
+ local Slot nextSlot;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ isRelevant = Mutator_OnCheckReplacement_Slot(nextSlot)
+ .connect(other, isSuperRelevant);
+ if (!isRelevant && !nextSlot.IsEmpty())
+ {
+ CleanEmptySlots();
+ return false;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return true;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Mutator_OnCheckReplacement_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Slot.uc b/sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Slot.uc
new file mode 100644
index 0000000..1ce83fe
--- /dev/null
+++ b/sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Slot.uc
@@ -0,0 +1,41 @@
+/**
+ * Slot class implementation for `MutatorAPI`'s `OnCheckReplacement` signal.
+ * 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 .
+ */
+class Mutator_OnCheckReplacement_Slot extends Slot;
+
+delegate bool connect(Actor other, out byte isSuperRelevant)
+{
+ DummyCall();
+ return true;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Signal.uc b/sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Signal.uc
new file mode 100644
index 0000000..5138c6f
--- /dev/null
+++ b/sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Signal.uc
@@ -0,0 +1,38 @@
+/**
+ * Signal class implementation for `MutatorAPI`'s `OnMutate` signal.
+ * 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 .
+ */
+class Mutator_OnMutate_Signal extends Signal;
+
+public final function Emit(string command, PlayerController sendingPlayer)
+{
+ local Slot nextSlot;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ Mutator_OnMutate_Slot(nextSlot).connect(command, sendingPlayer);
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Mutator_OnMutate_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Slot.uc b/sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Slot.uc
new file mode 100644
index 0000000..e8966f6
--- /dev/null
+++ b/sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Slot.uc
@@ -0,0 +1,40 @@
+/**
+ * Slot class implementation for `MutatorAPI`'s `OnMutate` signal.
+ * 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 .
+ */
+class Mutator_OnMutate_Slot extends Slot;
+
+delegate connect(string command, PlayerController sendingPlayer)
+{
+ DummyCall();
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/MutatorsAPI/MutatorAPI.uc b/sources/Unreal/MutatorsAPI/MutatorAPI.uc
new file mode 100644
index 0000000..9906ce0
--- /dev/null
+++ b/sources/Unreal/MutatorsAPI/MutatorAPI.uc
@@ -0,0 +1,92 @@
+/**
+ * Low-level API that provides set of utility methods for working with
+ * `Mutator`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 .
+ */
+class MutatorAPI extends AcediaObject;
+
+/**
+ * Called whenever mutators (Acedia's mutator) is asked to check whether
+ * an `Actor` should be replaced. This check is done right after that `Actor`
+ * has spawned.
+ *
+ * This check is called in UnrealScript and defined in base `Actor` class
+ * inside `PreBeginPlay()` event. It makes each `Actor` call base mutator's
+ * (the one linked as the head of the mutator linked list in `GameInfo`)
+ * `CheckRelevance()` method for itself as long as it has
+ * `bGameRelevant == false` and current `NetMode` is not `NM_Client`.
+ * `CheckRelevance()` is only called on the base mutator and always first
+ * checks with `AlwaysKeep()` method, that allows any mutator to prevent any
+ * further check altogether and then `IsRelevant()` check that then calls
+ * sub-check `CheckReplacement()` this signal catches.
+ * Any described event that is not `CheckRelevance()` is propagated through
+ * the linked mutator list.
+ *
+ * [Signature]
+ * bool (Actor other, out byte isSuperRelevant)
+ *
+ * @param other `Actor` that is checked for
+ * replacement / modification.
+ * @param isSuperRelevant Variable with unclear intention. It is defined in
+ * base mutator's `CheckRelevance()` method as a local variable and then
+ * passed as an `out` parameter for `IsRelevant()` and `CheckRelevance()`
+ * checks and not really used for anything once these checks are complete.
+ * Some [sources]
+ * (https://wiki.beyondunreal.com/Legacy:Chain_Of_Events_At_Level_Startup)
+ * indicate that it used to omit additional `GameInfo`'s relevancy checks,
+ * however does not to serve any function in Killing Floor.
+ * Mutators might repurpose it for their own uses, but I am not aware of
+ * any that do.
+ * @return `false` if you want `other` to be destroyed and `true` otherwise.
+ */
+/* SIGNAL */
+public final function Mutator_OnCheckReplacement_Slot OnCheckReplacement(
+ AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ signal = service.GetSignal(class'Mutator_OnCheckReplacement_Signal');
+ return Mutator_OnCheckReplacement_Slot(signal.NewSlot(receiver));
+}
+
+/**
+ * Called on a server whenever a player uses a "mutate" console command.
+ *
+ * [Signature]
+ * (string command, PlayerController sendingPlayer)
+ *
+ * @param command Text, typed by the player after "mutate" command,
+ * trimming spaces from the left.
+ * @param sendingPlayer Controller of the player who typed command that
+ * caused this call.
+ */
+/* SIGNAL */
+public final function Mutator_OnMutate_Slot OnMutate(
+ AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ signal = service.GetSignal(class'Mutator_OnMutate_Signal');
+ return Mutator_OnMutate_Slot(signal.NewSlot(receiver));
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/UnrealAPI.uc b/sources/Unreal/UnrealAPI.uc
index 34ef435..7ab7967 100644
--- a/sources/Unreal/UnrealAPI.uc
+++ b/sources/Unreal/UnrealAPI.uc
@@ -20,13 +20,24 @@
*/
class UnrealAPI extends AcediaObject;
+var public MutatorAPI mutator;
var public GameRulesAPI gameRules;
+var public BroadcastAPI broadcasts;
var private LoggerAPI.Definition fatalNoStalker;
protected function Constructor()
{
- gameRules = GameRulesAPI(_.memory.Allocate(class'GameRulesAPI'));
+ mutator = MutatorAPI(_.memory.Allocate(class'MutatorAPI'));
+ gameRules = GameRulesAPI(_.memory.Allocate(class'GameRulesAPI'));
+ broadcasts = BroadcastAPI(_.memory.Allocate(class'BroadcastAPI'));
+}
+
+public function DropAPI()
+{
+ mutator = none;
+ gameRules = none;
+ broadcasts = none;
}
/**
diff --git a/sources/Unreal/UnrealService.uc b/sources/Unreal/UnrealService.uc
index 10cdc57..9b73e40 100644
--- a/sources/Unreal/UnrealService.uc
+++ b/sources/Unreal/UnrealService.uc
@@ -25,22 +25,42 @@ struct SignalRecord
var class signalClass;
var Signal instance;
};
-var private array serviceSignals;
-var private Unreal_OnTick_Signal onTickSignal;
+var private array serviceSignals;
+var private Unreal_OnTick_Signal onTickSignal;
+var private AcediaGameRules gameRules;
protected function OnLaunch()
{
- local AcediaGameRules gameRules;
+ local BroadcastEventsObserver broadcastObserver;
CreateSignals();
- _.unreal.gameRules.Add(class'AcediaGameRules');
- gameRules = AcediaGameRules(
- _.unreal.gameRules.FindInstance(class'AcediaGameRules'));
- gameRules.Initialize(self);
+ // Create broadcast handler
+ broadcastObserver = BroadcastEventsObserver(_.unreal.broadcasts.Add(
+ class'BroadcastEventsObserver',
+ class'BroadcastEventsObserver'.default.usedInjectionLevel));
+ if (broadcastObserver != none) {
+ broadcastObserver.Initialize(self);
+ }
+ // Create game rules
+ gameRules = AcediaGameRules(_.unreal.gameRules.Add(class'AcediaGameRules'));
+ if (gameRules != none) {
+ gameRules.Initialize(self);
+ }
}
protected function OnShutdown()
{
+ local int i;
+ if (gameRules != none) {
+ gameRules.Cleanup();
+ }
+ _.unreal.broadcasts.Remove(class'BroadcastEventsObserver');
_.unreal.gameRules.Remove(class'AcediaGameRules');
+ for (i = 0; i < serviceSignals.length; i += 1) {
+ _.memory.Free(serviceSignals[i].instance);
+ }
+ _.memory.Free(onTickSignal);
+ serviceSignals.length = 0;
+ onTickSignal = none;
}
private final function CreateSignals()
@@ -86,10 +106,17 @@ public event Tick(float delta)
defaultproperties
{
- serviceSignals(0) = (signalClass=class'GameRules_OnFindPlayerStart_Signal')
- serviceSignals(1) = (signalClass=class'GameRules_OnHandleRestartGame_Signal')
- serviceSignals(2) = (signalClass=class'GameRules_OnCheckEndGame_Signal')
- serviceSignals(3) = (signalClass=class'GameRules_OnCheckScore_Signal')
- serviceSignals(4) = (signalClass=class'GameRules_OnOverridePickupQuery_Signal')
- serviceSignals(5) = (signalClass=class'GameRules_OnNetDamage_Signal')
+ serviceSignals(0) = (signalClass=class'GameRules_OnFindPlayerStart_Signal')
+ serviceSignals(1) = (signalClass=class'GameRules_OnHandleRestartGame_Signal')
+ serviceSignals(2) = (signalClass=class'GameRules_OnCheckEndGame_Signal')
+ serviceSignals(3) = (signalClass=class'GameRules_OnCheckScore_Signal')
+ serviceSignals(4) = (signalClass=class'GameRules_OnOverridePickupQuery_Signal')
+ serviceSignals(5) = (signalClass=class'GameRules_OnNetDamage_Signal')
+ serviceSignals(6) = (signalClass=class'Broadcast_OnBroadcastCheck_Signal')
+ serviceSignals(7) = (signalClass=class'Broadcast_OnHandleLocalized_Signal')
+ serviceSignals(8) = (signalClass=class'Broadcast_OnHandleLocalizedFor_Signal')
+ serviceSignals(9) = (signalClass=class'Broadcast_OnHandleText_Signal')
+ serviceSignals(10) = (signalClass=class'Broadcast_OnHandleTextFor_Signal')
+ serviceSignals(11) = (signalClass=class'Mutator_OnCheckReplacement_Slot')
+ serviceSignals(12) = (signalClass=class'Mutator_OnMutate_Signal')
}
\ No newline at end of file
diff --git a/sources/Users/UserDatabase.uc b/sources/Users/UserDatabase.uc
index a1120e6..1088a10 100644
--- a/sources/Users/UserDatabase.uc
+++ b/sources/Users/UserDatabase.uc
@@ -23,7 +23,7 @@ class UserDatabase extends AcediaObject
// This is used as a global variable only (`default.activeDatabase`) to store
// a reference to main database for persistent data, used by Acedia.
-var private UserDatabase activeDatabase;
+var public UserDatabase activeDatabase;
// `User` records that were stored this session
var private array sessionUsers;
// `UserID`s generated during this session.
@@ -31,6 +31,11 @@ var private array sessionUsers;
// This array should not grow too large under normal circumstances.
var private array storedUserIDs;
+protected static function StaticFinalizer()
+{
+ default.activeDatabase = none;
+}
+
/**
* Provides a reference to the database of user records that Acedia was
* set up to use.