Browse Source

Refactor to do clean up and use signals/slots

I shamefully ended up doing another mega-commit, because a lot of things
needed to be redone at once and it was easier that way on me. No one
really consistently tracks what I'm doing with these commits anyway.

This adds a whole bunch of code to deal with proper clean up for Acedia,
so it doesn't crash on map change and also replaces old event/listener
system with new signals/slots one.
pull/8/head
Anton Tarasenko 3 years ago
parent
commit
f1e77dc3d9
  1. 7
      sources/Aliases/AliasSource.uc
  2. 66
      sources/Commands/BroadcastListener_Commands.uc
  3. 72
      sources/Commands/BuiltInCommands/ACommandTest.uc
  4. 41
      sources/Commands/Command.uc
  5. 1
      sources/Commands/Commands.uc
  6. 39
      sources/Commands/Commands_Feature.uc
  7. 10
      sources/Config/AcediaConfig.uc
  8. 264
      sources/CoreService.uc
  9. 113
      sources/Data/Collections/AssociativeArray.uc
  10. 2
      sources/Data/Collections/Collection.uc
  11. 3
      sources/Data/Database/Local/LocalDatabaseInstance.uc
  12. 134
      sources/Events/Broadcast/BroadcastEvents.uc
  13. 341
      sources/Events/Broadcast/BroadcastEventsObserver.uc
  14. 175
      sources/Events/Broadcast/BroadcastListenerBase.uc
  15. 161
      sources/Events/Events.uc
  16. 59
      sources/Events/Listener.uc
  17. 56
      sources/Events/Mutator/MutatorEvents.uc
  18. 47
      sources/Events/Mutator/MutatorListenerBase.uc
  19. 36
      sources/Events/Signal.uc
  20. 23
      sources/Features/Feature.uc
  21. 2
      sources/Features/FeatureConfig.uc
  22. 3
      sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc
  23. 3
      sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc
  24. 2
      sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
  25. 2
      sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
  26. 28
      sources/Global.uc
  27. 6
      sources/Logger/ConsoleLogger.uc
  28. 13
      sources/Logger/LogMessage.uc
  29. 23
      sources/Logger/Logger.uc
  30. 3
      sources/Logger/LoggerAPI.uc
  31. 26
      sources/Service.uc
  32. 25
      sources/ServiceAnchor.uc
  33. 9
      sources/Singleton.uc
  34. 66
      sources/Testing/Service/TestingEvents.uc
  35. 34
      sources/Testing/Service/TestingListenerBase.uc
  36. 32
      sources/Testing/Service/TestingService.uc
  37. 2
      sources/Text/JSON/JSONAPI.uc
  38. 17
      sources/Types/AcediaActor.uc
  39. 3
      sources/Types/AcediaObject.uc
  40. 398
      sources/Unreal/BroadcastsAPI/BroadcastAPI.uc
  41. 483
      sources/Unreal/BroadcastsAPI/BroadcastEventsObserver.uc
  42. 46
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Signal.uc
  43. 41
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnBroadcastCheck_Slot.uc
  44. 51
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Signal.uc
  45. 46
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalizedFor_Slot.uc
  46. 50
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Signal.uc
  47. 44
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleLocalized_Slot.uc
  48. 50
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Signal.uc
  49. 45
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleTextFor_Slot.uc
  50. 50
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Signal.uc
  51. 45
      sources/Unreal/BroadcastsAPI/Events/Broadcast_OnHandleText_Slot.uc
  52. 32
      sources/Unreal/Connections/ConnectionService.uc
  53. 0
      sources/Unreal/Connections/Events/Connection_Signal.uc
  54. 0
      sources/Unreal/Connections/Events/Connection_Slot.uc
  55. 3
      sources/Unreal/Connections/MutatorListener_Connection.uc
  56. 73
      sources/Unreal/GameRulesAPI/AcediaGameRules.uc
  57. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Signal.uc
  58. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Slot.uc
  59. 3
      sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Signal.uc
  60. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Slot.uc
  61. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Signal.uc
  62. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Slot.uc
  63. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Signal.uc
  64. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Slot.uc
  65. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Signal.uc
  66. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Slot.uc
  67. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Signal.uc
  68. 0
      sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Slot.uc
  69. 51
      sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Signal.uc
  70. 47
      sources/Unreal/GameRulesAPI/Events/GameRules_OnPreventDeath_Slot.uc
  71. 38
      sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Signal.uc
  72. 40
      sources/Unreal/GameRulesAPI/Events/GameRules_OnScoreKill_Slot.uc
  73. 131
      sources/Unreal/GameRulesAPI/GameRulesAPI.uc
  74. 46
      sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Signal.uc
  75. 41
      sources/Unreal/MutatorsAPI/Events/Mutator_OnCheckReplacement_Slot.uc
  76. 38
      sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Signal.uc
  77. 40
      sources/Unreal/MutatorsAPI/Events/Mutator_OnMutate_Slot.uc
  78. 92
      sources/Unreal/MutatorsAPI/MutatorAPI.uc
  79. 13
      sources/Unreal/UnrealAPI.uc
  80. 53
      sources/Unreal/UnrealService.uc
  81. 7
      sources/Users/UserDatabase.uc

7
sources/Aliases/AliasSource.uc

@ -68,6 +68,13 @@ protected function OnCreated()
HashValidAliasesFromPerObjectConfig(); HashValidAliasesFromPerObjectConfig();
} }
protected function OnDestroyed()
{
loadedAliasObjects.length = 0;
_.memory.Free(aliasHash);
aliasHash = none;
}
// Ensures that our `Aliases` class is properly linked with this // Ensures that our `Aliases` class is properly linked with this
// source's class. Logs failure otherwise. // source's class. Logs failure otherwise.
private final function bool AssertAliasesClassIsOwnedByThisSource() private final function bool AssertAliasesClassIsOwnedByThisSource()

66
sources/Commands/BroadcastListener_Commands.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
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
{
}

72
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 <https://www.gnu.org/licenses/>.
*/
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<WeaponLocker> 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
{
}

41
sources/Commands/Command.uc

@ -165,6 +165,47 @@ protected function Constructor()
dataBuilder = none; dataBuilder = none;
} }
protected function Finalizer()
{
local int i;
local array<SubCommand> subCommands;
local array<Option> 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<Parameter> 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 * Overload this method to use `builder` to define parameters and options for
* your command. * your command.

1
sources/Commands/Commands.uc

@ -46,4 +46,5 @@ protected function DefaultIt()
defaultproperties defaultproperties
{ {
configName = "AcediaSystem" configName = "AcediaSystem"
useChatInput = true
} }

39
sources/Commands/Commands_Feature.uc

@ -37,6 +37,7 @@ var LoggerAPI.Definition errCommandDuplicate;
protected function OnEnabled() protected function OnEnabled()
{ {
registeredCommands = _.collections.EmptyAssociativeArray(); registeredCommands = _.collections.EmptyAssociativeArray();
_.unreal.broadcasts.OnHandleText(self).connect = HandleText;
// Macro selector // Macro selector
commandDelimiters[0] = P("@"); commandDelimiters[0] = P("@");
// Key selector // Key selector
@ -49,6 +50,7 @@ protected function OnEnabled()
protected function OnDisabled() protected function OnDisabled()
{ {
_.unreal.broadcasts.OnHandleText(self).Disconnect();
_.memory.Free(registeredCommands); _.memory.Free(registeredCommands);
registeredCommands = none; registeredCommands = none;
commandDelimiters.length = 0; 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 defaultproperties
{ {
configClass = class'Commands' 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.") errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.")
} }

10
sources/Config/AcediaConfig.uc

@ -91,6 +91,12 @@ protected function FromData(AssociativeArray source) {}
*/ */
protected function DefaultIt() {} 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 * This reads all of the `AcediaConfig`'s settings objects into internal
* storage. Must be called before any other methods. Actual loading might be * storage. Must be called before any other methods. Actual loading might be
@ -104,6 +110,8 @@ public static function Initialize()
if (default.existingConfigs != none) { if (default.existingConfigs != none) {
return; return;
} }
CoreService(class'CoreService'.static.GetInstance())
._registerObjectClass(default.class);
default.existingConfigs = __().collections.EmptyAssociativeArray(); default.existingConfigs = __().collections.EmptyAssociativeArray();
names = GetPerObjectNames( default.configName, string(default.class.name), names = GetPerObjectNames( default.configName, string(default.class.name),
MaxInt); MaxInt);
@ -160,6 +168,7 @@ public final static function bool NewConfig(Text name)
} }
newConfig = newConfig =
new(none, NameToStorageVersion(name.ToPlainString())) default.class; new(none, NameToStorageVersion(name.ToPlainString())) default.class;
newConfig._ = __();
newConfig.DefaultIt(); newConfig.DefaultIt();
newConfig.SaveConfig(); newConfig.SaveConfig();
default.existingConfigs.SetItem(name, newConfig); default.existingConfigs.SetItem(name, newConfig);
@ -254,6 +263,7 @@ public final static function AcediaConfig GetConfigInstance(Text name)
{ {
configEntry.value = configEntry.value =
new(none, NameToStorageVersion(name.ToPlainString())) default.class; new(none, NameToStorageVersion(name.ToPlainString())) default.class;
configEntry.value._ = __();
default.existingConfigs.SetItem(configEntry.key, configEntry.value); default.existingConfigs.SetItem(configEntry.key, configEntry.value);
} }
__().memory.Free(name); __().memory.Free(name);

264
sources/CoreService.uc

@ -1,8 +1,9 @@
/** /**
* Core service that is always running alongside Acedia framework, must be * Core service that is always running alongside Acedia framework, must be
* created by a launcher. * created by a launcher.
* Does nothing, simply used for spawning `Actor`s. * Used for booting up and shutting down Acedia.
* Copyright 2020 Anton Tarasenko * Also used for spawning `Actor`s as the only must-have `Service`.
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -19,11 +20,262 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class CoreService extends Service; class CoreService extends Service
dependson(BroadcastEventsObserver);
// Package's manifest is supposed to always have a name of
// "<package_name>.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<AcediaObject> > usedObjectClasses;
var private array< class<AcediaActor> > usedActorClasses;
// `Singleton`s are handled as a special case and cleaned up after
// the rest of the classes.
var private array< class<Singleton> > usedSingletonClasses;
var array<string> 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<string> 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<AcediaObject> 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<AcediaActor> classToClean)
{
local class<Singleton> singletonClass;
if (classToClean == none) {
return;
}
singletonClass = class<Singleton>(classToClean);
if (singletonClass != none) {
usedSingletonClasses[usedSingletonClasses.length] = singletonClass;
}
else {
usedActorClasses[usedActorClasses.length] = classToClean;
}
}
defaultproperties defaultproperties
{ {
// Since `CoreService` is what we use to start spawning `Actor`s, manifestSuffix = ".Manifest"
// we have to allow launcher to spawn it with `Spawn()` call
blockSpawning = false 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.")
} }

113
sources/Data/Collections/AssociativeArray.uc

@ -46,15 +46,20 @@ var private array<Bucket> hashTable;
// If one of the keys was deallocated outside of `AssociativeArray`, // If one of the keys was deallocated outside of `AssociativeArray`,
// this value may overestimate actual amount of elements. // this value may overestimate actual amount of elements.
var private int storedElementCount; 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. // hard lower and upper limits on hash table size, constant.
var private const int MINIMUM_CAPACITY; var private const int MINIMUM_SIZE;
var private const int MAXIMUM_CAPACITY; var private const int MAXIMUM_SIZE;
// Minimum and maximum allowed density of elements // Minimum and maximum allowed density of elements
// (`storedElementCount / hashTable.length`). // (`storedElementCount / hashTable.length`).
// If density falls outside this range, - we have to resize hash table to // 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 // 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 MINIMUM_DENSITY;
var private const float MAXIMUM_DENSITY; var private const float MAXIMUM_DENSITY;
@ -71,7 +76,7 @@ struct Index
protected function Constructor() protected function Constructor()
{ {
UpdateHashTableCapacity(); UpdateHashTableSize();
} }
protected function Finalizer() protected function Finalizer()
@ -187,31 +192,36 @@ private final function CleanBucket(out Bucket bucketToClean)
bucketToClean.entries = bucketEntries; bucketToClean.entries = bucketEntries;
} }
// Checks if we need to change our current capacity and does so if needed // Checks if we need to change our current hash table size
private final function UpdateHashTableCapacity() // and does so if needed
private final function UpdateHashTableSize()
{ {
local int oldCapacity, newCapacity; local int oldSize, newSize;
oldCapacity = hashTable.length; oldSize = hashTable.length;
// Calculate new capacity (and whether it is needed) based on amount of // Calculate new size (and whether it is needed) based on amount of
// stored properties and current capacity // stored properties and current size
newCapacity = oldCapacity; newSize = oldSize;
if (storedElementCount < newCapacity * MINIMUM_DENSITY) { if (storedElementCount < newSize * MINIMUM_DENSITY) {
newCapacity /= 2; newSize /= 2;
} }
if (storedElementCount > newCapacity * MAXIMUM_DENSITY) { else if (storedElementCount > newSize * MAXIMUM_DENSITY) {
newCapacity *= 2; newSize *= 2;
} }
// Enforce our limits // `table_density = items_amount / table_size`, so to store at least
newCapacity = Clamp(newCapacity, MINIMUM_CAPACITY, MAXIMUM_CAPACITY); // `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 // Only resize if difference is huge enough or table does not exists yet
if (newCapacity != oldCapacity) { if (newSize != oldSize) {
ResizeHashTable(newCapacity); ResizeHashTable(newSize);
} }
} }
// Changes size of the hash table, does not check any limits, // Changes size of the hash table, does not check any limits,
// does not check if `newCapacity` is a valid capacity (`newCapacity > 0`). // does not check if `newSize` is a valid size (`newSize > 0`).
private final function ResizeHashTable(int newCapacity) private final function ResizeHashTable(int newSize)
{ {
local int i, j; local int i, j;
local int newBucketIndex, newEntryIndex; local int newBucketIndex, newEntryIndex;
@ -220,12 +230,13 @@ private final function ResizeHashTable(int newCapacity)
oldHashTable = hashTable; oldHashTable = hashTable;
// Clean current hash table // Clean current hash table
hashTable.length = 0; hashTable.length = 0;
hashTable.length = newCapacity; hashTable.length = newSize;
for (i = 0; i < oldHashTable.length; i += 1) for (i = 0; i < oldHashTable.length; i += 1)
{ {
CleanBucket(oldHashTable[i]); CleanBucket(oldHashTable[i]);
bucketEntries = oldHashTable[i].entries; 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); newBucketIndex = GetBucketIndex(bucketEntries[j].key);
newEntryIndex = hashTable[newBucketIndex].entries.length; newEntryIndex = hashTable[newBucketIndex].entries.length;
hashTable[newBucketIndex].entries[newEntryIndex] = bucketEntries[j]; 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`. * 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]; entryToTake = hashTable[bucketIndex].entries[entryIndex];
hashTable[bucketIndex].entries.Remove(entryIndex, 1); hashTable[bucketIndex].entries.Remove(entryIndex, 1);
storedElementCount = Max(0, storedElementCount - 1); storedElementCount = Max(0, storedElementCount - 1);
UpdateHashTableCapacity(); UpdateHashTableSize();
return entryToTake; return entryToTake;
} }
@ -441,7 +489,7 @@ public final function AssociativeArray RemoveItem(AcediaObject key)
entryToRemove = hashTable[bucketIndex].entries[entryIndex]; entryToRemove = hashTable[bucketIndex].entries[entryIndex];
hashTable[bucketIndex].entries.Remove(entryIndex, 1); hashTable[bucketIndex].entries.Remove(entryIndex, 1);
storedElementCount = Max(0, storedElementCount - 1); storedElementCount = Max(0, storedElementCount - 1);
UpdateHashTableCapacity(); UpdateHashTableSize();
if (entryToRemove.managed && entryToRemove.value != none) { if (entryToRemove.managed && entryToRemove.value != none) {
entryToRemove.value.FreeSelf(entryToRemove.valueLifeVersion); entryToRemove.value.FreeSelf(entryToRemove.valueLifeVersion);
} }
@ -484,7 +532,7 @@ public function Empty(optional bool deallocateKeys)
} }
hashTable.length = 0; hashTable.length = 0;
storedElementCount = 0; storedElementCount = 0;
UpdateHashTableCapacity(); UpdateHashTableSize();
} }
/** /**
@ -950,8 +998,11 @@ public final function DynamicArray GetDynamicArray(AcediaObject key)
defaultproperties defaultproperties
{ {
iteratorClass = class'AssociativeArrayIterator' iteratorClass = class'AssociativeArrayIterator'
MINIMUM_CAPACITY = 50 minimalCapacity = 0
MAXIMUM_CAPACITY = 10000 MINIMUM_SIZE = 50
MINIMUM_DENSITY = 0.25 MAXIMUM_SIZE = 20000
MAXIMUM_DENSITY = 0.75 // `MINIMUM_DENSITY * 2 < MAXIMUM_DENSITY` must hold for `AssociativeArray`
// to work properly
MINIMUM_DENSITY = 0.25
MAXIMUM_DENSITY = 0.75
} }

2
sources/Data/Collections/Collection.uc

@ -22,7 +22,7 @@
class Collection extends AcediaObject class Collection extends AcediaObject
abstract; abstract;
var class<Iter> iteratorClass; var protected class<Iter> iteratorClass;
/** /**
* Method that must be overloaded for `GetItemByPointer()` to properly work. * Method that must be overloaded for `GetItemByPointer()` to properly work.

3
sources/Data/Database/Local/LocalDatabaseInstance.uc

@ -96,8 +96,11 @@ protected function Finalizer()
// Defaulting variables is not necessary, since this class does not // Defaulting variables is not necessary, since this class does not
// use object pool. // use object pool.
CompleteAllTasks(); CompleteAllTasks();
rootRecord = none;
_.unreal.OnTick(self).Disconnect(); _.unreal.OnTick(self).Disconnect();
_.memory.Free(diskUpdateTimer); _.memory.Free(diskUpdateTimer);
diskUpdateTimer = none;
configEntry = none;
} }
// It only has parameters so that it can be used as a `Tick()` event handler. // It only has parameters so that it can be used as a `Tick()` event handler.

134
sources/Events/Broadcast/BroadcastEvents.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
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<LocalMessage> class;
var int id;
// Localized messages in unreal script can be passed along with
// optional arguments, described by variables below.
var PlayerReplicationInfo relatedPRI1;
var PlayerReplicationInfo relatedPRI2;
var Object relatedObject;
};
static function bool CallCanBroadcast(Actor broadcaster, int recentSentTextSize)
{
local int i;
local bool result;
local array< class<Listener> > listeners;
listeners = GetListeners();
for (i = 0;i < listeners.length;i += 1)
{
result = class<BroadcastListenerBase>(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<Listener> > listeners;
listeners = GetListeners();
for (i = 0;i < listeners.length;i += 1)
{
result = class<BroadcastListenerBase>(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<Listener> > listeners;
listeners = GetListeners();
for (i = 0;i < listeners.length;i += 1)
{
result = class<BroadcastListenerBase>(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<Listener> > listeners;
listeners = GetListeners();
for (i = 0;i < listeners.length;i += 1)
{
result = class<BroadcastListenerBase>(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<Listener> > listeners;
listeners = GetListeners();
for (i = 0;i < listeners.length;i += 1)
{
result = class<BroadcastListenerBase>(listeners[i])
.static.HandleLocalizedFor(receiver, sender, message);
if (!result) return false;
}
return true;
}
defaultproperties
{
relatedListener = class'BroadcastListenerBase'
}

341
sources/Events/Broadcast/BroadcastEventsObserver.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Actor> 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<PlayerController> 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<LocalMessage> 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<LocalMessage> 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
}

175
sources/Events/Broadcast/BroadcastListenerBase.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
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'
}

161
sources/Events/Events.uc

@ -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(<ARGUMENTS>)
* | {
* | local int i;
* | local array< class<Listener> > listeners;
* | listeners = GetListeners();
* | for (i = 0; i < listeners.length; i += 1)
* | {
* | class<...ListenerBase>(listeners[i])
* | .static.EVENT_NAME(<ARGUMENTS>);
* | }
* | }
* |___________________________________________________________________________
* If each listener must indicate whether it gives it's permission for
* something to happen, then use this template:
* ____________________________________________________________________________
* | static function CallEVENT_NAME(<ARGUMENTS>)
* | {
* | local int i;
* | local bool result;
* | local array< class<Listener> > listeners;
* | listeners = GetListeners();
* | for (i = 0; i < listeners.length; i += 1)
* | {
* | result = class<...ListenerBase>(listeners[i])
* | .static.EVENT_NAME(<ARGUMENTS>);
* | 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 <https://www.gnu.org/licenses/>.
*/
class Events extends AcediaObject
abstract;
var private array< class<Listener> > listeners;
// Reference to the base class of listeners that are allowed to listen to
// these events
var public const class<Listener> relatedListener;
// Event class can also auto-spawn a `Service`,
// in case it's require to generate events
var public const class<Service> 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<Listener> > GetListeners()
{
return default.listeners;
}
// Make given listener active.
// If listener was already activated also returns 'false'.
static public final function bool ActivateListener(class<Listener> 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> 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> 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'
}

59
sources/Events/Listener.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
class Listener extends AcediaObject
abstract;
var public const class<Events> 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'
}

56
sources/Events/Mutator/MutatorEvents.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
class MutatorEvents extends Events
abstract;
static function bool CallCheckReplacement(Actor other, out byte isSuperRelevant)
{
local int i;
local bool result;
local array< class<Listener> > listeners;
listeners = GetListeners();
for (i = 0; i < listeners.length; i += 1)
{
result = class<MutatorListenerBase>(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<Listener> > listeners;
listeners = GetListeners();
for (i = 0; i < listeners.length;i += 1)
{
result = class<MutatorListenerBase>(listeners[i])
.static.Mutate(command, sendingPlayer);
if (!result) return false;
}
return true;
}
defaultproperties
{
relatedListener = class'MutatorListenerBase'
}

47
sources/Events/Mutator/MutatorListenerBase.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
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'
}

36
sources/Events/Signal.uc

@ -7,7 +7,7 @@
* This `Signal`-`Slot` system is essentially a wrapper for delegates * This `Signal`-`Slot` system is essentially a wrapper for delegates
* (`Slot` wraps over a single delegate, allowing us to store them in array), * (`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 * 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` * `_.unreal.OnTick(self).connect = myTickHandler`
* To create your own `Signal` you need to: * To create your own `Signal` you need to:
* 1. Make a non-abstract child class of `Signal`; * 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 * internal index variable `nextSlotIndex`. To account for removal of `Slot`s
* we will simply have to appropriately correct `nextSlotIndex` variable. * we will simply have to appropriately correct `nextSlotIndex` variable.
* To account for adding `Slot`s during signal emission we will first add them * 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. * into actual connected `Slot`s array before next iteration starts.
*/ */
@ -86,17 +86,19 @@ var array<SlotRecord> slotQueueToAdd;
// These arrays could be defined as one array of `SlotRecord` structs. // These arrays could be defined as one array of `SlotRecord` structs.
// We use four different arrays instead for performance reasons. // 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 // They must have the same length at all times and elements with the
// same index correspond to the same "record". // same index correspond to the same "record".
// Reference to registered `Slot` // References to registered `Slot`s
var private array<Slot> registeredSlots; var private array<Slot> 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<int> slotLifeVersions; var private array<int> slotLifeVersions;
// Receiver, associated with the `Slot`: when it's deallocated, // Receivers, associated with the `Slot`s: when they're deallocated,
// corresponding `Slot` should be removed // corresponding `Slot`s should be removed
var private array<AcediaObject> slotReceivers; var private array<AcediaObject> 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<int> slotReceiversLifeVersions; var private array<int> slotReceiversLifeVersions;
/* TEMPLATE for handlers without returned values: /* TEMPLATE for handlers without returned values:
@ -163,8 +165,8 @@ protected function Finalizer()
for (i = 0; i < registeredSlots.length; i += 1) { for (i = 0; i < registeredSlots.length; i += 1) {
registeredSlots[i].FreeSelf(slotLifeVersions[i]); registeredSlots[i].FreeSelf(slotLifeVersions[i]);
} }
registeredSlots.length = 0;
doingSelfCleaning = false; doingSelfCleaning = false;
registeredSlots.length = 0;
slotLifeVersions.length = 0; slotLifeVersions.length = 0;
slotReceivers.length = 0; slotReceivers.length = 0;
slotReceiversLifeVersions.length = 0; slotReceiversLifeVersions.length = 0;
@ -174,7 +176,7 @@ protected function Finalizer()
* Creates a new slot for `receiver` to catch emitted signals. * Creates a new slot for `receiver` to catch emitted signals.
* Supposed to be used inside a special interface method only. * 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 * Method connected to a `Slot` generated by this method must belong to
* the `receiver`, otherwise behavior of `Signal`-`Slot` system is * the `receiver`, otherwise behavior of `Signal`-`Slot` system is
* undefined. * undefined.
@ -238,7 +240,7 @@ public final function Disconnect(AcediaObject receiver)
/** /**
* Adds new `Slot` (`newSlot`) with receiver `receiver` to the caller `Signal`. * 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). * (even if it's added with a different receiver).
* *
* @param newSlot Slot to add. Must be initialize for the caller `Signal`. * @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) protected final function AddSlot(Slot newSlot, AcediaObject receiver)
{ {
local SlotRecord newRecord; 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) { if (newSlot == none) {
return; return;
} }
@ -277,10 +285,10 @@ private final function AddSlotRecord(SlotRecord record)
receiver = record.receiver; receiver = record.receiver;
if (newSlot.class != relatedSlotClass) return; if (newSlot.class != relatedSlotClass) return;
if (!newSlot.IsOwnerSignal(self)) 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; if (newSlot.GetLifeVersion() != record.slotLifeVersion) return;
// Receiver is outright invalid or got outdated // Receiver is outright invalid or got deallocated
if ( receiver == none if ( receiver == none
|| !receiver.IsAllocated() || !receiver.IsAllocated()
|| receiver.GetLifeVersion() != record.receiverLifeVersion) || receiver.GetLifeVersion() != record.receiverLifeVersion)
@ -299,8 +307,8 @@ private final function AddSlotRecord(SlotRecord record)
// If we have the same instance recorded, but... // If we have the same instance recorded, but...
// 1. it was reallocated: update it's records; // 1. it was reallocated: update it's records;
// 2. it was not reallocated: leave the records intact. // 2. it was not reallocated: leave the records intact.
// Neither would case issues with iterating along `Slot`s if this // Neither case would cause issues with iterating along `Slot`s if this
// method is only called right before new iteration. // method is only called right before new iteration through `Slot`s.
if (slotLifeVersions[i] != record.slotLifeVersion) if (slotLifeVersions[i] != record.slotLifeVersion)
{ {
slotLifeVersions[i] = record.slotLifeVersion; slotLifeVersions[i] = record.slotLifeVersion;

23
sources/Features/Feature.uc

@ -61,9 +61,6 @@ var protected bool blockSpawning;
// Only it's default value is ever used. // Only it's default value is ever used.
var private config bool autoEnable; var private config bool autoEnable;
// Listeners listed here will be automatically activated.
var public const array< class<Listener> > requiredListeners;
// `Service` that will be launched and shut down along with this `Feature`. // `Service` that will be launched and shut down along with this `Feature`.
// One should never launch or shut down this service manually. // One should never launch or shut down this service manually.
var protected const class<FeatureService> serviceClass; var protected const class<FeatureService> serviceClass;
@ -93,14 +90,15 @@ protected function Constructor()
FreeSelf(); FreeSelf();
return; return;
} }
SetListenersActiveStatus(true);
if (serviceClass != none) { if (serviceClass != none) {
myService = FeatureService(serviceClass.static.Require()); myService = FeatureService(serviceClass.static.Require());
} }
if (myService != none) { if (myService != none) {
myService.SetOwnerFeature(self); myService.SetOwnerFeature(self);
} }
currentConfigName = none;
ApplyConfig(default.currentConfigName); ApplyConfig(default.currentConfigName);
_.memory.Free(default.currentConfigName);
default.currentConfigName = none; default.currentConfigName = none;
OnEnabled(); OnEnabled();
} }
@ -111,7 +109,6 @@ protected function Finalizer()
if (GetInstance() != self) { if (GetInstance() != self) {
return; return;
} }
SetListenersActiveStatus(false);
OnDisabled(); OnDisabled();
if (serviceClass != none) { if (serviceClass != none) {
service = FeatureService(serviceClass.static.GetInstance()); service = FeatureService(serviceClass.static.GetInstance());
@ -243,11 +240,7 @@ public static final function Feature EnableMe(Text configName)
if (IsEnabled()) { if (IsEnabled()) {
return GetInstance(); return GetInstance();
} }
// This value will be copied and forgotten in `Constructor()`, default.currentConfigName = configName.Copy();
// 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.blockSpawning = false; default.blockSpawning = false;
newInstance = Feature(__().memory.Allocate(default.class)); newInstance = Feature(__().memory.Allocate(default.class));
default.activeInstance = newInstance; default.activeInstance = newInstance;
@ -304,16 +297,6 @@ protected function OnDisabled(){}
*/ */
protected function SwapConfig(FeatureConfig newConfig){} 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 defaultproperties
{ {
autoEnable = false autoEnable = false

2
sources/Features/FeatureConfig.uc

@ -54,7 +54,7 @@ public static function Initialize()
{ {
nextConfig = FeatureConfig(GetConfigInstance(names[i])); nextConfig = FeatureConfig(GetConfigInstance(names[i]));
if (nextConfig == none) continue; if (nextConfig == none) continue;
if (nextConfig.autoEnable) continue; if (!nextConfig.autoEnable) continue;
if (default.autoEnabledConfig == none) { if (default.autoEnabledConfig == none) {
default.autoEnabledConfig = names[i].Copy(); default.autoEnabledConfig = names[i].Copy();
} }

3
sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class KFFrontend extends BaseBackend class KFFrontend extends BaseFrontend
abstract; abstract;
var private config class<ATradingComponent> tradingClass; var private config class<ATradingComponent> tradingClass;
@ -33,6 +33,7 @@ protected function Constructor()
protected function Finalizer() protected function Finalizer()
{ {
_.memory.Free(trading); _.memory.Free(trading);
trading = none;
} }
defaultproperties defaultproperties

3
sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc

@ -37,6 +37,9 @@ protected function Finalizer()
_.memory.Free(onStartSignal); _.memory.Free(onStartSignal);
_.memory.Free(onEndSignal); _.memory.Free(onEndSignal);
_.memory.Free(onTraderSelectSignal); _.memory.Free(onTraderSelectSignal);
onStartSignal = none;
onEndSignal = none;
onTraderSelectSignal = none;
} }
/** /**

2
sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc

@ -28,8 +28,10 @@ var protected NativeActorRef myShopVolume;
protected function Finalizer() protected function Finalizer()
{ {
_.memory.Free(myName);
_.memory.Free(myShopVolume); _.memory.Free(myShopVolume);
myShopVolume = none; myShopVolume = none;
myName = none;
} }
/** /**

2
sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc

@ -46,7 +46,9 @@ protected function Finalizer()
{ {
super.Finalizer(); super.Finalizer();
_.unreal.OnTick(self).Disconnect(); _.unreal.OnTick(self).Disconnect();
_.memory.Free(lastSelectedTrader);
_.memory.FreeMany(registeredTraders); _.memory.FreeMany(registeredTraders);
lastSelectedTrader = none;
registeredTraders.length = 0; registeredTraders.length = 0;
} }

28
sources/Global.uc

@ -79,3 +79,31 @@ protected function Initialize()
kf = KFFrontend(memory.Allocate(class'KF1_Frontend')); kf = KFFrontend(memory.Allocate(class'KF1_Frontend'));
json.StaticConstructor(); 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;
}

6
sources/Logger/ConsoleLogger.uc

@ -36,6 +36,12 @@ public function Write(Text message, LoggerAPI.LogLevel messageLevel)
} }
} }
protected static function StaticFinalizer()
{
default.loadedLoggers = none;
}
defaultproperties defaultproperties
{ {
} }

13
sources/Logger/LogMessage.uc

@ -210,7 +210,6 @@ private final function NormalizeArguments(array<int> argumentsOrder)
*/ */
public final function LogMessage Arg(Text argument) public final function LogMessage Arg(Text argument)
{ {
local Text assembledMessage;
if (IsArgumentListFull()) { if (IsArgumentListFull()) {
return self; return self;
} }
@ -220,15 +219,23 @@ public final function LogMessage Arg(Text argument)
} }
default.dirtyLogMessage = self; // `self` is dirty with arguments now default.dirtyLogMessage = self; // `self` is dirty with arguments now
collectedArguments[collectedArguments.length] = argument; 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()) if (IsArgumentListFull())
{ {
// Last argument - have to log what we have collected // Last argument - have to log what we have collected
assembledMessage = Collect(); assembledMessage = Collect();
_.logger.LogAtLevel(assembledMessage, myLevel); _.logger.LogAtLevel(assembledMessage, myLevel);
assembledMessage.FreeSelf(); assembledMessage.FreeSelf();
return self;
} }
return self;
} }
// Check whether we have enough arguments to completely make log message: // Check whether we have enough arguments to completely make log message:

23
sources/Logger/Logger.uc

@ -2,6 +2,13 @@
* Base class for implementing "loggers" - objects that actually write log * Base class for implementing "loggers" - objects that actually write log
* messages somewhere. To use it - simply implement `Write()` method, * messages somewhere. To use it - simply implement `Write()` method,
* preferably making use of `GetPrefix()` 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 * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
@ -26,7 +33,7 @@ class Logger extends AcediaObject
abstract; abstract;
// Named loggers are stored here to avoid recreating them // 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? // Should `Logger` display prefix indicating it's a log message from Acedia?
var protected config bool acediaStamp; 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 TDEBUG, TINFO, TWARNING, TERROR, TFATAL, TTIME, TACEDIA;
var protected const int TSPACE; var protected const int TSPACE;
protected static function StaticFinalizer()
{
default.loadedLoggers = none;
}
/** /**
* Method for creating named `Logger`s that can have their settings prepared * Method for creating named `Logger`s that can have their settings prepared
* in the config file. Only one `Logger` is made for every * 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 Logger loggerInstance;
local Text loggerKey; local Text loggerKey;
if (default.loadedLoggers == none)
{
// TODO: do this in static constructor
default.loadedLoggers = __().collections.EmptyAssociativeArray();
}
if (loggerName == none) { if (loggerName == none) {
return none; return none;
} }
if (default.loadedLoggers == none) {
default.loadedLoggers = __().collections.EmptyAssociativeArray();
}
loggerKey = loggerName.LowerCopy(); loggerKey = loggerName.LowerCopy();
loggerInstance = Logger(default.loadedLoggers.GetItem(loggerKey)); loggerInstance = Logger(default.loadedLoggers.GetItem(loggerKey));
if (loggerInstance == none) if (loggerInstance == none)
{ {
// TODO: important to redo this via `MemoryAPI` to call constructors
loggerInstance = new(none, loggerName.ToPlainString()) default.class; loggerInstance = new(none, loggerName.ToPlainString()) default.class;
loggerInstance._constructor(); loggerInstance._constructor();
default.loadedLoggers.SetItem(loggerKey, loggerInstance); default.loadedLoggers.SetItem(loggerKey, loggerInstance);

3
sources/Logger/LoggerAPI.uc

@ -234,7 +234,8 @@ public final function LogMessage Auto(out Definition definition)
instance.Initialize(definition); instance.Initialize(definition);
definition.instance = instance; definition.instance = instance;
} }
return instance.Reset(); instance.Reset().TryLogging();
return instance;
} }
/** /**

26
sources/Service.uc

@ -21,17 +21,17 @@
class Service extends Singleton class Service extends Singleton
abstract; abstract;
// Listeners listed here will be automatically activated. // `Service`s can use this as a receiver for signal functions
var public const array< class<Listener> > requiredListeners; var protected ServiceAnchor _self;
var LoggerAPI.Definition errNoService; // Log messages
var private LoggerAPI.Definition errNoService;
// Enables feature of given class. // Enables feature of given class.
public static final function Service Require() public static final function Service Require()
{ {
local Service newInstance; local Service newInstance;
if (IsRunning()) if (IsRunning()) {
{
return Service(GetInstance()); return Service(GetInstance());
} }
default.blockSpawning = false; default.blockSpawning = false;
@ -55,25 +55,15 @@ protected function OnShutdown(){}
protected function OnCreated() protected function OnCreated()
{ {
default.blockSpawning = true; default.blockSpawning = true;
SetListenersActiveSatus(true); _self = ServiceAnchor(_.memory.Allocate(class'ServiceAnchor'));
OnLaunch(); OnLaunch();
} }
protected function OnDestroyed() protected function OnDestroyed()
{ {
SetListenersActiveSatus(false);
OnShutdown(); OnShutdown();
} _.memory.Free(_self);
_self = none;
// 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);
}
} }
defaultproperties defaultproperties

25
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 <https://www.gnu.org/licenses/>.
*/
class ServiceAnchor extends AcediaObject;
defaultproperties
{
}

9
sources/Singleton.uc

@ -25,13 +25,18 @@ class Singleton extends AcediaActor
// Default value of this variable will store one and only existing version // Default value of this variable will store one and only existing version
// of actor of this class. // of actor of this class.
var private Singleton activeInstance; var public Singleton activeInstance;
// Setting default value of this variable to 'true' prevents creation of // Setting default value of this variable to 'true' prevents creation of
// a singleton, even if no instances of it exist. // a singleton, even if no instances of it exist.
// Only a default value is ever used. // Only a default value is ever used.
var protected bool blockSpawning; var protected bool blockSpawning;
protected static function StaticFinalizer()
{
default.activeInstance = none;
}
public final static function Singleton GetInstance(optional bool spawnIfMissing) public final static function Singleton GetInstance(optional bool spawnIfMissing)
{ {
local bool instanceExists; local bool instanceExists;
@ -87,12 +92,12 @@ event PreBeginPlay()
// first call this version of the method. // first call this version of the method.
event Destroyed() event Destroyed()
{ {
super.Destroyed();
if (self == default.activeInstance) if (self == default.activeInstance)
{ {
OnDestroyed(); OnDestroyed();
default.activeInstance = none; default.activeInstance = none;
} }
super.Destroyed();
} }
defaultproperties defaultproperties

66
sources/Testing/Service/TestingEvents.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
class TestingEvents extends Events
abstract;
static function CallTestingBegan(array< class<TestCase> > testQueue)
{
local int i;
local array< class<Listener> > listeners;
listeners = GetListeners();
for (i = 0; i < listeners.length; i += 1)
{
class<TestingListenerBase>(listeners[i])
.static.TestingBegan(testQueue);
}
}
static function CallCaseTested(
class<TestCase> testedCase,
TestCaseSummary result)
{
local int i;
local array< class<Listener> > listeners;
listeners = GetListeners();
for (i = 0; i < listeners.length; i += 1)
{
class<TestingListenerBase>(listeners[i])
.static.CaseTested(testedCase, result);
}
}
static function CallTestingEnded(
array< class<TestCase> > testQueue,
array<TestCaseSummary> results)
{
local int i;
local array< class<Listener> > listeners;
listeners = GetListeners();
for (i = 0; i < listeners.length; i += 1)
{
class<TestingListenerBase>(listeners[i])
.static.TestingEnded(testQueue, results);
}
}
defaultproperties
{
relatedListener = class'TestingListenerBase'
}

34
sources/Testing/Service/TestingListenerBase.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
class TestingListenerBase extends Listener
abstract;
static function TestingBegan(array< class<TestCase> > testQueue) {}
static function CaseTested(class<TestCase> testCase, TestCaseSummary result) {}
static function TestingEnded(
array< class<TestCase> > testQueue,
array<TestCaseSummary> results) {}
defaultproperties
{
relatedEvents = class'TestingEvents'
}

32
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 requiredName;
var public config const string requiredGroup; var public config const string requiredGroup;
// Shortcut to `TestingEvents`, so that we don't have to write
// class'TestingEvents' every time.
var const class<TestingEvents> events;
var LoggerAPI.Definition warnDuplicateTestCases; var LoggerAPI.Definition warnDuplicateTestCases;
/** /**
* Registers another `TestCase` class for later testing. * Registers another `TestCase` class for later testing.
@ -207,12 +203,10 @@ public final function bool Run()
return false; return false;
} }
nextTestCase = 0; nextTestCase = 0;
runningTests = true;
summarizedResults.length = 0; summarizedResults.length = 0;
events.static.CallTestingBegan(testCasesToRun); runningTests = (testCasesToRun.length > 0);
if (testCasesToRun.length <= 0) { if (!runningTests) {
runningTests = false; ReportTestingResult();
events.static.CallTestingEnded(testCasesToRun, summarizedResults);
} }
return true; return true;
} }
@ -224,16 +218,31 @@ private final function DoTestingStep()
{ {
runningTests = false; runningTests = false;
default.summarizedResults = summarizedResults; default.summarizedResults = summarizedResults;
events.static.CallTestingEnded(testCasesToRun, summarizedResults); ReportTestingResult();
return; return;
} }
testCasesToRun[nextTestCase].static.PerformTests(); testCasesToRun[nextTestCase].static.PerformTests();
newResult = testCasesToRun[nextTestCase].static.GetSummary(); newResult = testCasesToRun[nextTestCase].static.GetSummary();
events.static.CallCaseTested(testCasesToRun[nextTestCase], newResult);
summarizedResults[summarizedResults.length] = newResult; summarizedResults[summarizedResults.length] = newResult;
nextTestCase += 1; nextTestCase += 1;
} }
private function ReportTestingResult()
{
local int i;
local MutableText nextLine;
local array<string> 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) event Tick(float delta)
{ {
// This will destroy us on the next tick after we were // This will destroy us on the next tick after we were
@ -248,6 +257,5 @@ event Tick(float delta)
defaultproperties defaultproperties
{ {
runTestsOnStartUp = false 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.") 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.")
} }

2
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` 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. * use `Print()` method.
* *
* Only certain classes (the same as the ones that can be parsed from JSON * Only certain classes (the same as the ones that can be parsed from JSON

17
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 * Deallocates caller `AcediaActor`, calling its finalizer and then
* destroying it. * destroying it.
@ -374,6 +388,9 @@ event Destroyed()
*/ */
public static function _cleanup() public static function _cleanup()
{ {
if (default._staticConstructorWasCalled) {
StaticFinalizer();
}
default._textCache = none; default._textCache = none;
default._staticConstructorWasCalled = false; default._staticConstructorWasCalled = false;
} }

3
sources/Types/AcediaObject.uc

@ -440,6 +440,9 @@ public static final function Global __()
*/ */
public static function _cleanup() public static function _cleanup()
{ {
if (default._staticConstructorWasCalled) {
StaticFinalizer();
}
default._textCache = none; default._textCache = none;
default._objectPool = none; default._objectPool = none;
default._staticConstructorWasCalled = false; default._staticConstructorWasCalled = false;

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

483
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 <https://www.gnu.org/licenses/>.
*/
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<Actor> 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<PlayerController> 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<LocalMessage> 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<LocalMessage> 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
}

46
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 <https://www.gnu.org/licenses/>.
*/
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'
}

41
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

51
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 <https://www.gnu.org/licenses/>.
*/
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'
}

46
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

50
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 <https://www.gnu.org/licenses/>.
*/
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'
}

44
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

50
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 <https://www.gnu.org/licenses/>.
*/
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'
}

45
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

50
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 <https://www.gnu.org/licenses/>.
*/
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'
}

45
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

32
sources/Services/Connection/ConnectionService.uc → sources/Unreal/Connections/ConnectionService.uc

@ -24,9 +24,11 @@ class ConnectionService extends Service;
// Stores basic information about a connection // Stores basic information about a connection
struct Connection struct Connection
{ {
var public PlayerController controllerReference;
// Remember these for the time `controllerReference` dies
// and becomes `none`.
var public string networkAddress; var public string networkAddress;
var public string idHash; var public string idHash;
var public PlayerController controllerReference;
// Reference to `AcediaReplicationInfo` for this client, // Reference to `AcediaReplicationInfo` for this client,
// in case it was created. // in case it was created.
var private AcediaReplicationInfo acediaRI; var private AcediaReplicationInfo acediaRI;
@ -71,6 +73,7 @@ protected function OnLaunch()
{ {
local Controller nextController; local Controller nextController;
local PlayerController nextPlayerController; local PlayerController nextPlayerController;
_.unreal.mutator.OnCheckReplacement(_self).connect = TryAddingController;
onConnectionEstablishedSignal = onConnectionEstablishedSignal =
Connection_Signal(_.memory.Allocate(class'Connection_Signal')); Connection_Signal(_.memory.Allocate(class'Connection_Signal'));
onConnectionLostSignal = onConnectionLostSignal =
@ -92,6 +95,8 @@ protected function OnShutdown()
default.activeConnections = activeConnections; default.activeConnections = activeConnections;
_.memory.Free(onConnectionEstablishedSignal); _.memory.Free(onConnectionEstablishedSignal);
_.memory.Free(onConnectionLostSignal); _.memory.Free(onConnectionLostSignal);
onConnectionEstablishedSignal = none;
onConnectionLostSignal = none;
} }
// Returning `true` guarantees that `controllerToCheck != none` // Returning `true` guarantees that `controllerToCheck != none`
@ -164,7 +169,9 @@ public final function Connection GetConnection(PlayerController player)
local int connectionIndex; local int connectionIndex;
local Connection emptyConnection; local Connection emptyConnection;
connectionIndex = GetConnectionIndex(player); connectionIndex = GetConnectionIndex(player);
if (connectionIndex < 0) return emptyConnection; if (connectionIndex < 0) {
return emptyConnection;
}
return activeConnections[connectionIndex]; return activeConnections[connectionIndex];
} }
@ -217,6 +224,26 @@ public final function array<Connection> GetActiveConnections(
return activeConnections; 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. // Check if connections are still active every tick.
// Should not take any noticeable time when no players are disconnecting. // Should not take any noticeable time when no players are disconnecting.
event Tick(float delta) event Tick(float delta)
@ -226,5 +253,4 @@ event Tick(float delta)
defaultproperties defaultproperties
{ {
requiredListeners(0) = class'MutatorListener_Connection'
} }

0
sources/Services/Connection/Events/Connection_Signal.uc → sources/Unreal/Connections/Events/Connection_Signal.uc

0
sources/Services/Connection/Events/Connection_Slot.uc → sources/Unreal/Connections/Events/Connection_Slot.uc

3
sources/Services/Connection/MutatorListener_Connection.uc → sources/Unreal/Connections/MutatorListener_Connection.uc

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class MutatorListener_Connection extends MutatorListenerBase class MutatorListener_Connection extends Object
abstract; abstract;
static function bool CheckReplacement(Actor other, out byte isSuperRelevant) static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
@ -49,5 +49,4 @@ static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
defaultproperties defaultproperties
{ {
relatedEvents = class'MutatorEvents'
} }

73
sources/Unreal/GameRules/AcediaGameRules.uc → sources/Unreal/GameRulesAPI/AcediaGameRules.uc

@ -26,24 +26,42 @@ var private GameRules_OnCheckEndGame_Signal onCheckEndGameSignal;
var private GameRules_OnCheckScore_Signal onCheckScoreSignal; var private GameRules_OnCheckScore_Signal onCheckScoreSignal;
var private GameRules_OnOverridePickupQuery_Signal onOverridePickupQuery; var private GameRules_OnOverridePickupQuery_Signal onOverridePickupQuery;
var private GameRules_OnNetDamage_Signal onNetDamage; 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) { if (service == none) {
return; return;
} }
onFindPlayerStartSignal = GameRules_OnFindPlayerStart_Signal( onFindPlayerStartSignal = GameRules_OnFindPlayerStart_Signal(
service.GetSignal(class'GameRules_OnFindPlayerStart_Signal')); service.GetSignal(class'GameRules_OnFindPlayerStart_Signal'));
onHandleRestartGameSignal = GameRules_OnHandleRestartGame_Signal( onHandleRestartGameSignal = GameRules_OnHandleRestartGame_Signal(
service.GetSignal(class'GameRules_OnHandleRestartGame_Signal')); service.GetSignal(class'GameRules_OnHandleRestartGame_Signal'));
onCheckEndGameSignal = GameRules_OnCheckEndGame_Signal( onCheckEndGameSignal = GameRules_OnCheckEndGame_Signal(
service.GetSignal(class'GameRules_OnCheckEndGame_Signal')); service.GetSignal(class'GameRules_OnCheckEndGame_Signal'));
onCheckScoreSignal = GameRules_OnCheckScore_Signal( onCheckScoreSignal = GameRules_OnCheckScore_Signal(
service.GetSignal(class'GameRules_OnCheckScore_Signal')); service.GetSignal(class'GameRules_OnCheckScore_Signal'));
onOverridePickupQuery = GameRules_OnOverridePickupQuery_Signal( onOverridePickupQuery = GameRules_OnOverridePickupQuery_Signal(
service.GetSignal(class'GameRules_OnOverridePickupQuery_Signal')); service.GetSignal(class'GameRules_OnOverridePickupQuery_Signal'));
onNetDamage = GameRules_OnNetDamage_Signal( onNetDamage = GameRules_OnNetDamage_Signal(
service.GetSignal(class'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() function string GetRules()
@ -62,6 +80,7 @@ function NavigationPoint FindPlayerStart(
optional string incomingName) optional string incomingName)
{ {
local NavigationPoint result; local NavigationPoint result;
// Use first value returned by anything
if (onFindPlayerStartSignal != none) { if (onFindPlayerStartSignal != none) {
result = onFindPlayerStartSignal.Emit(player, inTeam, incomingName); result = onFindPlayerStartSignal.Emit(player, inTeam, incomingName);
} }
@ -74,6 +93,8 @@ function NavigationPoint FindPlayerStart(
function bool HandleRestartGame() function bool HandleRestartGame()
{ {
local bool result; local bool result;
// `true` return value needs to override `false` values returned by any
// other sources
if (onHandleRestartGameSignal != none) { if (onHandleRestartGameSignal != none) {
result = onHandleRestartGameSignal.Emit(); result = onHandleRestartGameSignal.Emit();
} }
@ -87,10 +108,12 @@ function bool CheckEndGame(PlayerReplicationInfo winner, string reason)
{ {
local bool result; local bool result;
result = true; result = true;
// `false` return value needs to override `true` values returned by any
// other sources
if (onCheckEndGameSignal != none) { if (onCheckEndGameSignal != none) {
result = onCheckEndGameSignal.Emit(winner, reason); result = onCheckEndGameSignal.Emit(winner, reason);
} }
if (nextGameRules != none && !nextGameRules.HandleRestartGame()) { if (nextGameRules != none && !nextGameRules.CheckEndGame(winner, reason)) {
return false; return false;
} }
return result; return result;
@ -99,6 +122,8 @@ function bool CheckEndGame(PlayerReplicationInfo winner, string reason)
function bool CheckScore(PlayerReplicationInfo scorer) function bool CheckScore(PlayerReplicationInfo scorer)
{ {
local bool result; local bool result;
// `true` return value needs to override `false` values returned by any
// other sources
if (onCheckScoreSignal != none) { if (onCheckScoreSignal != none) {
result = onCheckScoreSignal.Emit(scorer); result = onCheckScoreSignal.Emit(scorer);
} }
@ -150,6 +175,38 @@ function int NetDamage(
return damage; return damage;
} }
function bool PreventDeath(
Pawn killed,
Controller killer,
class<DamageType> 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 defaultproperties
{ {
} }

0
sources/Unreal/GameRules/Events/GameRules_OnCheckEndGame_Signal.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Signal.uc

0
sources/Unreal/GameRules/Events/GameRules_OnCheckEndGame_Slot.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckEndGame_Slot.uc

3
sources/Unreal/GameRules/Events/GameRules_OnCheckScore_Signal.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Signal.uc

@ -25,12 +25,11 @@ public final function bool Emit(PlayerReplicationInfo scorer)
local bool result, nextReply; local bool result, nextReply;
StartIterating(); StartIterating();
nextSlot = GetNextSlot(); nextSlot = GetNextSlot();
result = true;
while (nextSlot != none) while (nextSlot != none)
{ {
nextReply = GameRules_OnCheckScore_Slot(nextSlot).connect(scorer); nextReply = GameRules_OnCheckScore_Slot(nextSlot).connect(scorer);
if (!nextReply && !nextSlot.IsEmpty()) { if (!nextReply && !nextSlot.IsEmpty()) {
result = result && nextReply; result = result || nextReply;
} }
nextSlot = GetNextSlot(); nextSlot = GetNextSlot();
} }

0
sources/Unreal/GameRules/Events/GameRules_OnCheckScore_Slot.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnCheckScore_Slot.uc

0
sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Signal.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Signal.uc

0
sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Slot.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnFindPlayerStart_Slot.uc

0
sources/Unreal/GameRules/Events/GameRules_OnHandleRestartGame_Signal.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Signal.uc

0
sources/Unreal/GameRules/Events/GameRules_OnHandleRestartGame_Slot.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnHandleRestartGame_Slot.uc

0
sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Signal.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Signal.uc

0
sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Slot.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnNetDamage_Slot.uc

0
sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Signal.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Signal.uc

0
sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Slot.uc → sources/Unreal/GameRulesAPI/Events/GameRules_OnOverridePickupQuery_Slot.uc

51
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 <https://www.gnu.org/licenses/>.
*/
class GameRules_OnPreventDeath_Signal extends Signal;
public final function bool Emit(
Pawn killed,
Controller killer,
class<DamageType> 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'
}

47
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 <https://www.gnu.org/licenses/>.
*/
class GameRules_OnPreventDeath_Slot extends Slot;
delegate bool connect(
Pawn killed,
Controller killer,
class<DamageType> 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
{
}

38
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 <https://www.gnu.org/licenses/>.
*/
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'
}

40
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

131
sources/Unreal/GameRules/GameRulesAPI.uc → sources/Unreal/GameRulesAPI/GameRulesAPI.uc

@ -58,6 +58,8 @@ public final function GameRules_OnFindPlayerStart_Slot OnFindPlayerStart(
* bool <slot>() * bool <slot>()
* *
* @return `true` if you want to prevent game restart and `false` otherwise. * @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 */ /* SIGNAL */
public final function GameRules_OnHandleRestartGame_Slot OnHandleRestartGame( 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 winner Replication info of the supposed winner of the game.
* @param reason String with a description about how/why `winner` has won. * @param reason String with a description about how/why `winner` has won.
* @return `false` if you want to prevent game from ending * @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 */ /* SIGNAL */
public final function GameRules_OnCheckEndGame_Slot OnCheckEndGame( 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)); return GameRules_OnCheckEndGame_Slot(signal.NewSlot(receiver));
} }
/* CheckScore()
*/
/** /**
* Check if this score means the game ends. * Check if this score means the game ends.
* *
* Return `true` to override `GameInfo`'s `CheckScore()`, or if game was ended * Return `true` to override `GameInfo`'s `CheckScore()`, or if game was ended
* (with a call to `Level.Game.EndGame()`). * (with a call to `Level.Game.EndGame()`).
* *
* This signal will always be propagated to all registered slots.
*
* [Signature] * [Signature]
* bool <slot>(PlayerReplicationInfo scorer) * bool <slot>(PlayerReplicationInfo scorer)
* *
* @param scorer For whom to do a score check. * @param scorer For whom to do a score check.
* @return `true` to override `GameInfo`'s `CheckScore()`, or if game was ended * @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 */ /* SIGNAL */
public final function GameRules_OnCheckScore_Slot OnCheckScore( 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 * 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 * modify it. If one of the `Slot`s returns `true`, `allowPickup` will
* determine if the object can be picked up. * determine if the object can be picked up.
* Overriding via this method allows to completely bypass check against * Overriding via this method allows to completely bypass check against
* `Pawn`'s inventory's `HandlePickupQuery()` method. * `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 * When pawn gets damaged, `GameRule`s are given a chance to modify that
* modify it. If one of the `Slot`s returns `true`, `allowPickup` will * damage.
* determine if the object can be picked up.
* Overriding via this method allows to completely bypass check against
* `Pawn`'s inventory's `HandlePickupQuery()` method.
* *
* [Signature] * [Signature]
* bool <slot>(Pawn other, Pickup item, out byte allowPickup) * int <slot>(
* int originalDamage,
* int damage,
* Pawn injured,
* Pawn instigatedBy,
* Vector hitLocation,
* out Vector momentum,
* class<DamageType> damageType)
* *
* @param other Pawn which will potentially pickup `item`. * @param originalDamage Damage that was originally meant to be dealt to
* @param item Pickup which `other` might potentially pickup. * the `Pawn`, before any of th `GameRules`' modifications.
* @param allowPickup `true` if you want to force `other` to pickup an item * @param damage Damage value to be dealt to the `Pawn` as it was
* and `false` otherwise. This parameter is ignored if returned value of * modified so fat by other `GameRules` and `OnNetDamage()`'s handlers.
* your slot call is `false`. * @param injured `Pawn` that will be dealt damage in question.
* @return `true` if you wish to override decision about pickup with * @param instigatedBy `Pawn` that deals this damage.
* `allowPickup` and `false` if you do not want to make that decision. * @param hitLocation "Location of the damage", e.g. place where `injured`
* If you do decide to override decision by returning `true` - this signal * was hit by a bullet.
* will not be propagated to the rest of the slots. * @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 */ /* SIGNAL */
public final function GameRules_OnNetDamage_Slot OnNetDamage( 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)); return GameRules_OnNetDamage_Slot(signal.NewSlot(receiver));
} }
/**
* When pawn is about to die, `GameRule`s are given a chance to
* prevent that.
*
* [Signature]
* bool <slot>(
* Pawn killed,
* Controller killer,
* class<DamageType> 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 <slot>(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`. * 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. * @param newRulesClass Class of rules to add.
* @return `true` if `GameRules` were added and `false` otherwise * @return `GameRules` instance if it was added and `none` otherwise
* (because they were already active.) * (can happen if rules of this class were already added).
*/ */
public final function bool Add(class<GameRules> newRulesClass) public final function GameRules Add(class<GameRules> newRulesClass)
{ {
local GameRules newGameRules;
if (AreAdded(newRulesClass)) { if (AreAdded(newRulesClass)) {
return false; return none;
} }
_.unreal.GetGameType() newGameRules = GameRules(_.memory.Allocate(newRulesClass));
.AddGameModifier(GameRules(_.memory.Allocate(newRulesClass))); _.unreal.GetGameType().AddGameModifier(newGameRules);
return true; return newGameRules;
} }
/** /**
@ -249,8 +315,9 @@ public final function bool Remove(class<GameRules> rulesClassToRemove)
* Returns `none` otherwise. * Returns `none` otherwise.
* *
* @param rulesClassToFind Class of rules to find. * @param rulesClassToFind Class of rules to find.
* @return `GameRules` of given class `rulesClassToFind` instance added to * @return `GameRules` instance of given class `rulesClassToFind`, that is
* `GameInfo`'s records and `none` if no such rules are currently added. * added to `GameInfo`'s records and `none` if no such rules are
* currently added.
*/ */
public final function GameRules FindInstance( public final function GameRules FindInstance(
class<GameRules> rulesClassToFind) class<GameRules> rulesClassToFind)

46
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 <https://www.gnu.org/licenses/>.
*/
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'
}

41
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

38
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 <https://www.gnu.org/licenses/>.
*/
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'
}

40
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

92
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 <https://www.gnu.org/licenses/>.
*/
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 <slot>(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]
* <slot>(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
{
}

13
sources/Unreal/UnrealAPI.uc

@ -20,13 +20,24 @@
*/ */
class UnrealAPI extends AcediaObject; class UnrealAPI extends AcediaObject;
var public MutatorAPI mutator;
var public GameRulesAPI gameRules; var public GameRulesAPI gameRules;
var public BroadcastAPI broadcasts;
var private LoggerAPI.Definition fatalNoStalker; var private LoggerAPI.Definition fatalNoStalker;
protected function Constructor() 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;
} }
/** /**

53
sources/Unreal/UnrealService.uc

@ -25,22 +25,42 @@ struct SignalRecord
var class<Signal> signalClass; var class<Signal> signalClass;
var Signal instance; var Signal instance;
}; };
var private array<SignalRecord> serviceSignals; var private array<SignalRecord> serviceSignals;
var private Unreal_OnTick_Signal onTickSignal; var private Unreal_OnTick_Signal onTickSignal;
var private AcediaGameRules gameRules;
protected function OnLaunch() protected function OnLaunch()
{ {
local AcediaGameRules gameRules; local BroadcastEventsObserver broadcastObserver;
CreateSignals(); CreateSignals();
_.unreal.gameRules.Add(class'AcediaGameRules'); // Create broadcast handler
gameRules = AcediaGameRules( broadcastObserver = BroadcastEventsObserver(_.unreal.broadcasts.Add(
_.unreal.gameRules.FindInstance(class'AcediaGameRules')); class'BroadcastEventsObserver',
gameRules.Initialize(self); 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() protected function OnShutdown()
{ {
local int i;
if (gameRules != none) {
gameRules.Cleanup();
}
_.unreal.broadcasts.Remove(class'BroadcastEventsObserver');
_.unreal.gameRules.Remove(class'AcediaGameRules'); _.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() private final function CreateSignals()
@ -86,10 +106,17 @@ public event Tick(float delta)
defaultproperties defaultproperties
{ {
serviceSignals(0) = (signalClass=class'GameRules_OnFindPlayerStart_Signal') serviceSignals(0) = (signalClass=class'GameRules_OnFindPlayerStart_Signal')
serviceSignals(1) = (signalClass=class'GameRules_OnHandleRestartGame_Signal') serviceSignals(1) = (signalClass=class'GameRules_OnHandleRestartGame_Signal')
serviceSignals(2) = (signalClass=class'GameRules_OnCheckEndGame_Signal') serviceSignals(2) = (signalClass=class'GameRules_OnCheckEndGame_Signal')
serviceSignals(3) = (signalClass=class'GameRules_OnCheckScore_Signal') serviceSignals(3) = (signalClass=class'GameRules_OnCheckScore_Signal')
serviceSignals(4) = (signalClass=class'GameRules_OnOverridePickupQuery_Signal') serviceSignals(4) = (signalClass=class'GameRules_OnOverridePickupQuery_Signal')
serviceSignals(5) = (signalClass=class'GameRules_OnNetDamage_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')
} }

7
sources/Users/UserDatabase.uc

@ -23,7 +23,7 @@ class UserDatabase extends AcediaObject
// This is used as a global variable only (`default.activeDatabase`) to store // This is used as a global variable only (`default.activeDatabase`) to store
// a reference to main database for persistent data, used by Acedia. // 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 // `User` records that were stored this session
var private array<User> sessionUsers; var private array<User> sessionUsers;
// `UserID`s generated during this session. // `UserID`s generated during this session.
@ -31,6 +31,11 @@ var private array<User> sessionUsers;
// This array should not grow too large under normal circumstances. // This array should not grow too large under normal circumstances.
var private array<UserID> storedUserIDs; var private array<UserID> storedUserIDs;
protected static function StaticFinalizer()
{
default.activeDatabase = none;
}
/** /**
* Provides a reference to the database of user records that Acedia was * Provides a reference to the database of user records that Acedia was
* set up to use. * set up to use.

Loading…
Cancel
Save