diff --git a/config/Acedia.ini b/config/Acedia.ini
index 80b4ec9..51be774 100644
--- a/config/Acedia.ini
+++ b/config/Acedia.ini
@@ -1,255 +1,2 @@
-[Acedia.FixDualiesCost]
-; This feature fixes several issues, related to the selling price of both
-; single and dual pistols, all originating from the existence of dual weapons.
-; Most notable issue is the ability to "print" money by buying and
-; selling pistols in a certain way.
-;
-; Fix only works with vanilla pistols, as it's unpredictable what
-; custom ones can do and they can handle these issues on their own
-; in a better way.
-autoEnable=true
-; Some issues involve possible decrease in pistols' price and
-; don't lead to the exploit, but are still bugs and require fixing.
-; If you have a Deagle in your inventory and then get another one
-; (by either buying or picking it off the ground) - the price of resulting
-; dual pistols will be set to the price of the last deagle,
-; like the first one wasn't worth anything at all.
-; In particular this means that (prices are off-perk for more clarity):
-; 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of
-; the cost (+750 do$h), you lose 250 do$h;
-; 2. If you first buy a deagle (-500 do$h), then buy
-; the second one (-500 do$h) and then sell them, you'll only get
-; 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h;
-; 3. So if you already have bought a deagle (-500 do$h),
-; you can get a more expensive weapon by doing a stupid thing
-; and first selling your Deagle (+375 do$h),
-; then buying dual deagles (-1000 do$h).
-; If you sell them after that, you'll gain 75% of the cost of
-; dual deagles (+750 do$h), leaving you with losing only 375 do$h.
-; Of course, situations described above are only relevant if you're planning
-; to sell your weapons at some point and most players won't even
-; notice these issues.
-; But such an oversight still shouldn't exist in a game and we fix it by
-; setting sell value of dualies as a sum of values of each pistol.
-; Yet, fixing this issue leads to players having more expensive
-; (while fairly priced) weapons than on vanilla, technically making
-; the game easier. And some people might object to having that in
-; a whitelisted bug-fixing feature.
-; These people are, without a question, complete degenerates.
-; But making mods for only non-mentally challenged isn't inclusive.
-; So we add this option.
-; Set it to 'false' if you only want to fix ammo printing
-; and leave the rest of the bullshit as-is.
-allowSellValueIncrease=true
-
-
-[Acedia.FixAmmoSelling]
-; This feature addressed an oversight in vanilla code that
-; allows clients to sell weapon's ammunition.
-; Due to the implementation of ammo selling, this allows cheaters to
-; "print money" by buying and selling ammo over and over again.
-autoEnable=true
-; Due to how this fix works, players with level below 6 get charged less
-; than necessary by the shop and this fix must take the rest of
-; the cost by itself.
-; The problem is, due to how ammo purchase is coded, low-level (<6 lvl)
-; players can actually buy more ammo for "fixed" weapons than they can afford
-; by filling ammo for one or all weapons.
-; Setting this flag to 'true' will allow us to still take full cost
-; from them, putting them in "debt" (having negative dosh amount).
-; If you don't want to have players with negative dosh values on your server
-; as a side-effect of this fix, then leave this flag as 'false',
-; letting low level players buy ammo cheaper
-; (but not cheaper than lvl6 could).
-; NOTE: this issue doesn't affect level 6 players.
-; NOTE #2: this fix does give players below level 6 some
-; technical advantage compared to vanilla game, but this advantage
-; cannot exceed benefits of having level 6.
-allowNegativeDosh=false
-
-
-[Acedia.FixInventoryAbuse]
-; This feature addressed two issues with the inventory:
-; 1. Players carrying amount of weapons that shouldn't be allowed by the
-; weight limit.
-; 2. Players carrying two variants of the same gun.
-; For example carrying both M32 and camo M32.
-; Single and dual version of the same weapon are also considered
-; the same type of gun, so you shouldn't be able to carry
-; both MK23 and dual MK23 or dual handcannons and golden handcannon.
-; But cheaters do. But not with this fix.
-autoEnable=true
-; How often (in seconds) should we do inventory validation checks?
-; You shouldn't really worry about performance, but there's also no need to
-; do this check too often.
-checkInterval=0.25
-; For this fix to properly work, this array must contain an entry for
-; every dual weapon in the game (like pistols, with single and dual versions).
-; It's made configurable in case of custom dual weapons.
-dualiesClasses=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup')
-dualiesClasses=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup')
-dualiesClasses=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup')
-dualiesClasses=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup')
-dualiesClasses=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup')
-dualiesClasses=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup')
-
-
-[Acedia.FixInfiniteNades]
-; This feature fixes a vulnerability in a code of 'Frag' that can allow
-; player to throw grenades even when he no longer has any.
-; There's also no cooldowns on the throw, which can lead to a server crash.
-autoEnable=true
-; Setting this flag to 'true' will allow to throw grenades by calling
-; 'ServerThrow' directly, as long as player has necessary ammo.
-; This can allow some players to throw grenades much quicker than intended,
-; therefore it's suggested to keep this flag set to 'false'.
-ignoreTossFlags=false
-
-
-[Acedia.FixDoshSpam]
-; This feature addressed two dosh-related issues:
-; 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash';
-; 2. Breaking collision detection logic by stacking large amount of
-; 'CashPickup' actors in one place, which allows one to either
-; reach unintended locations or even instantly kill zeds.
-;
-; It fixes them by limiting speed, with which dosh can spawn, and
-; allowing this limit to decrease when there's already too much dosh
-; present on the map.
-autoEnable=true
-; Highest and lowest speed with which players can throw dosh wads.
-; It'll be evenly spread between all players.
-; For example, if speed is set to 6 and only one player will be spamming dosh,
-; - he'll be able to throw 6 wads of dosh per second;
-; but if all 6 players are spamming it, - each will throw only 1 per second.
-; NOTE: these speed values can be exceeded, since a player is guaranteed
-; to be able to throw at least one wad of dosh, if he didn't do so in awhile.
-; NOTE #2: if maximum value is less than minimum one,
-; the lowest (maximum one) will be used.
-doshPerSecondLimitMax=50
-doshPerSecondLimitMin=5
-; Amount of dosh pickups on the map at which we must set dosh per second
-; to 'doshPerSecondLimitMin'.
-; We use 'doshPerSecondLimitMax' when there's no dosh on the map and
-; scale linearly between them as it's amount grows.
-criticalDoshAmount=25
-
-
-[Acedia.FixSpectatorCrash]
-; This feature attempts to prevent server crashes caused by someone
-; quickly switching between being spectator and an active player.
-autoEnable=true
-; This fix will try to kick any player that switches between active player
-; and cooldown faster than time (in seconds) in this value.
-; NOTE: raising this value past default value of '0.25'
-; won't actually improve crash prevention, but might cause regular players to
-; get accidentally kicked.
-spectatorChangeTimeout=0.25
-; [ADVANCED] Don't change this setting unless you know what you're doing.
-; Allows you to turn off server blocking.
-; Players that don't respect timeout will still be kicked.
-; This might be needed if this fix conflicts with another mutator
-; that also changes 'numPlayers'.
-; This option is necessary to block aggressive enough server crash
-; attempts, but can cause compatibility issues with some mutators.
-; It's highly recommended to rewrite such a mutator to be compatible instead.
-; NOTE: fix should be compatible with most faked players-type mutators,
-; since this it remembers the difference between amount of
-; real players and 'numPlayers'.
-; After unblocking, it sets 'numPlayers' to
-; the current amount of real players + that difference.
-; So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes
-; 3 players + 3 (=6 numPlayers).
-allowServerBlock=true
-
-
-[Acedia.FixFFHack]
-; This feature fixes a bug that can allow players to bypass server's
-; friendly fire limitations and teamkill.
-; Usual fixes apply friendly fire scale to suspicious damage themselves, which
-; also disables some of the environmental damage.
-; In oder to avoid that, this fix allows server owner to define precisely
-; to what damage types to apply the friendly fire scaling.
-; It should be all damage types related to projectiles.
-autoEnable=true
-; Defines a general rule for chosing whether or not to apply
-; friendly fire scaling.
-; This can be overwritten by exceptions ('alwaysScale' or 'neverScale').
-; Enabling scaling by default without any exceptions in 'neverScale' will
-; make this fix behave almost identically to Mutant's 'Explosives Fix Mutator'.
-scaleByDefault=false
-; Damage types, for which we should always reaaply friendly fire scaling.
-alwaysScale=Class'KFMod.DamTypeCrossbuzzsawHeadShot'
-alwaysScale=Class'KFMod.DamTypeCrossbuzzsaw'
-alwaysScale=Class'KFMod.DamTypeFrag'
-alwaysScale=Class'KFMod.DamTypePipeBomb'
-alwaysScale=Class'KFMod.DamTypeM203Grenade'
-alwaysScale=Class'KFMod.DamTypeM79Grenade'
-alwaysScale=Class'KFMod.DamTypeM79GrenadeImpact'
-alwaysScale=Class'KFMod.DamTypeM32Grenade'
-alwaysScale=Class'KFMod.DamTypeLAW'
-alwaysScale=Class'KFMod.DamTypeLawRocketImpact'
-alwaysScale=Class'KFMod.DamTypeFlameNade'
-alwaysScale=Class'KFMod.DamTypeFlareRevolver'
-alwaysScale=Class'KFMod.DamTypeFlareProjectileImpact'
-alwaysScale=Class'KFMod.DamTypeBurned'
-alwaysScale=Class'KFMod.DamTypeTrenchgun'
-alwaysScale=Class'KFMod.DamTypeHuskGun'
-alwaysScale=Class'KFMod.DamTypeCrossbow'
-alwaysScale=Class'KFMod.DamTypeCrossbowHeadShot'
-alwaysScale=Class'KFMod.DamTypeM99SniperRifle'
-alwaysScale=Class'KFMod.DamTypeM99HeadShot'
-alwaysScale=Class'KFMod.DamTypeShotgun'
-alwaysScale=Class'KFMod.DamTypeNailGun'
-alwaysScale=Class'KFMod.DamTypeDBShotgun'
-alwaysScale=Class'KFMod.DamTypeKSGShotgun'
-alwaysScale=Class'KFMod.DamTypeBenelli'
-alwaysScale=Class'KFMod.DamTypeSPGrenade'
-alwaysScale=Class'KFMod.DamTypeSPGrenadeImpact'
-alwaysScale=Class'KFMod.DamTypeSeekerSixRocket'
-alwaysScale=Class'KFMod.DamTypeSeekerRocketImpact'
-alwaysScale=Class'KFMod.DamTypeSealSquealExplosion'
-alwaysScale=Class'KFMod.DamTypeRocketImpact'
-alwaysScale=Class'KFMod.DamTypeBlowerThrower'
-alwaysScale=Class'KFMod.DamTypeSPShotgun'
-alwaysScale=Class'KFMod.DamTypeZEDGun'
-alwaysScale=Class'KFMod.DamTypeZEDGunMKII'
-alwaysScale=Class'KFMod.DamTypeZEDGunMKII'
-; Damage types, for which we should never reaply friendly fire scaling.
-;neverScale=Class'KFMod.???'
-
-
-[Acedia.FixZedTimeLags]
-; When zed time activates, game speed is immediately set to
-; 'zedTimeSlomoScale' (0.2 by default), defined, like all other variables,
-; in 'KFGameType'. Zed time lasts 'zedTimeDuration' seconds (3.0 by default),
-; but during last 'zedTimeDuration * 0.166' seconds (by default 0.498)
-; it starts to speed back up, causing game speed to update every tick.
-; This makes animations look more smooth when exiting zed-time.
-; However, updating speed every tick for that purpose seems like
-; an overkill and, combined with things like
-; increased tick rate, certain open maps and increased zed limit,
-; it can lead to noticable lags at the end of the zed time.
-; This fix limits amount of actual game speed updates, alleviating the issue.
-;
-; As a side effect it also fixes an issue where during zed time speed up
-; 'zedTimeSlomoScale' was assumed to be default value of '0.2'.
-; Now zed time will behave correctly with mods that change 'zedTimeSlomoScale'.
-autoEnable=true
-; Maximum amount of game speed updates upon leaving zed time.
-; 2 or 3 seem to provide a good enough result that,
-; i.e. it should be hard to notice difference with vanilla game behavior.
-; 1 is a smallest possible value, resulting in effectively removing any
-; smooting via speed up, simply changing speed from
-; the slowest (0.2) to the highest.
-; For the reference: on servers with default 30 tick rate there's usually
-; about 13 updates total (without this fix).
-maxGameSpeedUpdatesAmount=3
-; [ADVANCED] Don't change this setting unless you know what you're doing.
-; Compatibility setting that allows to keep 'GameInfo' 's 'Tick' event
-; from being disabled.
-; Useful when running Acedia along with custom 'GameInfo'
-; (that isn't 'KFGameType') that relies on 'Tick' event.
-; Note, however, that in order to keep this fix working properly,
-; it's on you to make sure 'KFGameType.Tick()' logic isn't executed.
-disableTick=true
\ No newline at end of file
+[Acedia.Packages]
+useGameModes=false
\ No newline at end of file
diff --git a/config/AcediaGameModes.ini b/config/AcediaGameModes.ini
new file mode 100644
index 0000000..484b88a
--- /dev/null
+++ b/config/AcediaGameModes.ini
@@ -0,0 +1,7 @@
+[hard GameMode]
+title={$green Hard difficulty}
+difficulty=normal
+
+[hell GameMode]
+title={$crimson Hell On Earth}
+difficulty=hoe
diff --git a/sources/Acedia.uc b/sources/Acedia.uc
deleted file mode 100644
index fe3cde6..0000000
--- a/sources/Acedia.uc
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * Main and only Acedia mutator used for initialization of necessary services
- * and providing access to mutator events' calls.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class Acedia extends Mutator
- config(Acedia);
-
-// Default value of this variable will be used to store
-// reference to the active Acedia mutator,
-// as well as to ensure there's only one copy of it.
-// We can't use 'Singleton' class for that,
-// as we have to derive from 'Mutator'.
-var private Acedia selfReference;
-
-// Array of predefined services that must be started along with Acedia mutator.
-var private array< class > systemServices;
-
-static public final function Acedia GetInstance()
-{
- return default.selfReference;
-}
-
-event PreBeginPlay()
-{
- // Enforce one copy rule and remember a reference to that copy
- if (default.selfReference != none)
- {
- Destroy();
- return;
- }
- default.selfReference = self;
- // Boot up Acedia
- LoadManifest(class'Manifest');
- LaunchServices();
- InjectBroadcastHandler(); // TODO: move this to 'SideEffect' mechanic
-}
-
-private final function LoadManifest(class manifestClass)
-{
- local int i;
- // Activate manifest's listeners
- for (i = 0; i < manifestClass.default.requiredListeners.length; i += 1)
- {
- if (manifestClass.default.requiredListeners[i] == none) continue;
- manifestClass.default.requiredListeners[i].static.SetActive(true);
- }
- // Enable features
- for (i = 0; i < manifestClass.default.features.length; i += 1)
- {
- if (manifestClass.default.features[i] == none) continue;
- if (manifestClass.default.features[i].static.IsAutoEnabled())
- {
- manifestClass.default.features[i].static.EnableMe();
- }
- }
-}
-
-private final function LaunchServices()
-{
- local int i;
- for (i = 0; i < systemServices.length; i += 1)
- {
- if (systemServices[i] == none) continue;
- Spawn(systemServices[i]);
- }
-}
-
-private final function InjectBroadcastHandler()
-{
- local BroadcastHandler ourBroadcastHandler;
- if (level == none || level.game == none) return;
-
- ourBroadcastHandler = Spawn(class'BroadcastHandler');
- // Swap out level's first handler with ours
- // (needs to be done for both actor reference and it's class)
- ourBroadcastHandler.nextBroadcastHandler = level.game.broadcastHandler;
- ourBroadcastHandler.nextBroadcastHandlerClass = level.game.broadcastClass;
- level.game.broadcastHandler = ourBroadcastHandler;
- level.game.broadcastClass = class'BroadcastHandler';
-}
-
-// Acedia is only able to run in a server mode right now,
-// so this function is just a stub.
-public final function bool IsServerOnly()
-{
- return true;
-}
-
-// Provide a way to handle CheckReplacement event
-function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- return class'MutatorEvents'.static.
- CallCheckReplacement(other, isSuperRelevant);
-}
-
-function Mutate(string command, PlayerController sendingPlayer)
-{
- if (class'MutatorEvents'.static.CallMutate(command, sendingPlayer))
- {
- super.Mutate(command, sendingPlayer);
- }
-}
-
-defaultproperties
-{
- // List built-in services
- systemServices(0) = class'ConnectionService'
- // This is a server-only mutator
- remoteRole = ROLE_None
- bAlwaysRelevant = true
- // Mutator description
- GroupName = "Core mutator"
- FriendlyName = "Acedia"
- Description = "Mutator for all your degenerate needs"
-}
\ No newline at end of file
diff --git a/sources/AcediaReplicationInfo.uc b/sources/AcediaReplicationInfo.uc
deleted file mode 100644
index 74141ed..0000000
--- a/sources/AcediaReplicationInfo.uc
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Facilitates some core replicated functions between client and server.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class AcediaReplicationInfo extends ReplicationInfo;
-
-var public PlayerController linkOwner;
-
-replication
-{
- reliable if (role == ROLE_Authority)
- linkOwner;
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/BroadcastEvents.uc b/sources/BroadcastEvents.uc
deleted file mode 100644
index 61cacfc..0000000
--- a/sources/BroadcastEvents.uc
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * Event generator for events, related to broadcasting messages
- * through standard Unreal Script means:
- * 1. text messages, typed by a player;
- * 2. localized messages, identified by a LocalMessage class and id.
- * Allows to make decisions whether or not to propagate certain messages.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class BroadcastEvents extends Events
- abstract;
-
-struct LocalizedMessage
-{
- // Every localized message is described by a class and id.
- // For example, consider 'KFMod.WaitingMessage':
- // if passed 'id' is '1',
- // then it's supposed to be a message about new wave,
- // but if passed 'id' is '2',
- // then it's about completing the wave.
- var class class;
- var int id;
- // Localized messages in unreal script can be passed along with
- // optional arguments, described by variables below.
- var PlayerReplicationInfo relatedPRI1;
- var PlayerReplicationInfo relatedPRI2;
- var Object relatedObject;
-};
-
-static function bool CallCanBroadcast(Actor broadcaster, int recentSentTextSize)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.CanBroadcast(broadcaster, recentSentTextSize);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleText
-(
- Actor sender,
- out string message,
- name messageType
-)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleText(sender, message, messageType);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleTextFor
-(
- PlayerController receiver,
- Actor sender,
- out string message,
- name messageType
-)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleTextFor(receiver, sender, message, messageType);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleLocalized
-(
- Actor sender,
- LocalizedMessage message
-)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleLocalized(sender, message);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallHandleLocalizedFor
-(
- PlayerController receiver,
- Actor sender,
- LocalizedMessage message
-)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0;i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.HandleLocalizedFor(receiver, sender, message);
- if (!result) return false;
- }
- return true;
-}
-
-defaultproperties
-{
- relatedListener = class'BroadcastListenerBase'
-}
\ No newline at end of file
diff --git a/sources/BroadcastHandler.uc b/sources/BroadcastHandler.uc
deleted file mode 100644
index fd98140..0000000
--- a/sources/BroadcastHandler.uc
+++ /dev/null
@@ -1,197 +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.
- * 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 .
- */
-// TODO: make it work from any place in the chain.
-class BroadcastHandler extends Engine.BroadcastHandler
- dependson(BroadcastEvents);
-
-// 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' if it can't.
-// We remember senders in this array in order to pass real ones to our events.
-// Array instead of variable is to account for folded calls
-// (when handling of broadcast events leads to another message generation).
-var private array storedSenders;
-
-// We want to insert our code in some of the functions between
-// 'AllowsBroadcast' check and actual broadcasting,
-// so we can't just use a 'super.AllowsBroadcast()' call.
-// Instead we first manually do this check, then perform our logic and then
-// make a super call, but with 'blockAllowsBroadcast' flag set to 'true',
-// which causes overloaded 'AllowsBroadcast()' to omit actual checks.
-var private bool blockAllowsBroadcast;
-
-// Functions below simply reroute vanilla's broadcast events to
-// Acedia's 'BroadcastEvents', while keeping original senders
-// and blocking 'AllowsBroadcast()' as described in comments for
-// 'storedSenders' and 'blockAllowsBroadcast'.
-
-public function bool HandlerAllowsBroadcast(Actor broadcaster, int sentTextNum)
-{
- local bool canBroadcast;
- // Check listeners
- canBroadcast = class'BroadcastEvents'.static
- .CallCanBroadcast(broadcaster, sentTextNum);
- // Check other broadcast handlers (if present)
- if (canBroadcast && nextBroadcastHandler != none)
- {
- canBroadcast = nextBroadcastHandler
- .HandlerAllowsBroadcast(broadcaster, sentTextNum);
- }
- return canBroadcast;
-}
-
-function Broadcast(Actor sender, coerce string message, optional name type)
-{
- local bool canTryToBroadcast;
- if (!AllowsBroadcast(sender, Len(message)))
- return;
- canTryToBroadcast = class'BroadcastEvents'.static
- .CallHandleText(sender, message, type);
- if (canTryToBroadcast)
- {
- storedSenders[storedSenders.length] = sender;
- blockAllowsBroadcast = true;
- super.Broadcast(sender, message, type);
- blockAllowsBroadcast = false;
- storedSenders.length = storedSenders.length - 1;
- }
-}
-
-function BroadcastTeam
-(
- Controller sender,
- coerce string message,
- optional name type
-)
-{
- local bool canTryToBroadcast;
- if (!AllowsBroadcast(sender, Len(message)))
- return;
- canTryToBroadcast = class'BroadcastEvents'.static
- .CallHandleText(sender, message, type);
- if (canTryToBroadcast)
- {
- storedSenders[storedSenders.length] = sender;
- blockAllowsBroadcast = true;
- super.BroadcastTeam(sender, message, type);
- blockAllowsBroadcast = false;
- storedSenders.length = storedSenders.length - 1;
- }
-}
-
-event AllowBroadcastLocalized
-(
- Actor sender,
- class message,
- optional int switch,
- optional PlayerReplicationInfo relatedPRI1,
- optional PlayerReplicationInfo relatedPRI2,
- optional Object optionalObject
-)
-{
- local bool canTryToBroadcast;
- local BroadcastEvents.LocalizedMessage packedMessage;
- if (!AllowsBroadcast(sender, Len(message)))
- return;
- 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];
- }
- canBroadcast = class'BroadcastEvents'.static
- .CallHandleTextFor(receiver, sender, message, type);
- if (!canBroadcast)
- {
- return false;
- }
- return super.AcceptBroadcastText(receiver, senderPRI, message, type);
-}
-
-
-function bool AcceptBroadcastLocalized
-(
- PlayerController receiver,
- Actor sender,
- class message,
- optional int switch,
- optional PlayerReplicationInfo relatedPRI1,
- optional PlayerReplicationInfo relatedPRI2,
- optional Object obj
-)
-{
- local bool canBroadcast;
- local BroadcastEvents.LocalizedMessage packedMessage;
- packedMessage.class = message;
- packedMessage.id = switch;
- packedMessage.relatedPRI1 = relatedPRI1;
- packedMessage.relatedPRI2 = relatedPRI2;
- packedMessage.relatedObject = obj;
- canBroadcast = class'BroadcastEvents'.static
- .CallHandleLocalizedFor(receiver, sender, packedMessage);
- if (!canBroadcast)
- {
- return false;
- }
- return super.AcceptBroadcastLocalized( receiver, sender, message, switch,
- relatedPRI1, relatedPRI2, obj);
-}
-
-defaultproperties
-{
- blockAllowsBroadcast = false
-}
\ No newline at end of file
diff --git a/sources/BroadcastListenerBase.uc b/sources/BroadcastListenerBase.uc
deleted file mode 100644
index 6cf6b0d..0000000
--- a/sources/BroadcastListenerBase.uc
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Listener for events, related to broadcasting messages
- * through standard Unreal Script means:
- * 1. text messages, typed by a player;
- * 2. localized messages, identified by a LocalMessage class and id.
- * Allows to make decisions whether or not to propagate certain messages.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class BroadcastListenerBase extends Listener
- abstract;
-
-static 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,
-// given that none so far rejected it and he recently already broadcasted
-// or tried to broadcast 'recentSentTextSize' symbols of text
-// (that value is periodically reset in 'GameInfo',
-// by default should be each second).
-// NOTE: this function is ONLY called when someone tries to
-// broadcast TEXT messages.
-// 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.
-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).
-// This function is called once per message and allows you to change it
-// (by changing 'message' argument) before any of the players receive it.
-// Return 'true' to allow the message through.
-// If one of the listeners returns 'false', -
-// it will be treated just like one of broadcasters returning 'false'
-// in 'AcceptBroadcastText' and this method won't be called for remaining
-// active listeners.
-static function bool HandleText
-(
- Actor sender,
- out string message,
- optional name messageType
-)
-{
- return true;
-}
-
-// This event is similar to 'HandleText', but is called for every player
-// the message is sent to.
-// If allows you to alter the message, but the changes are accumulated
-// as events go through the players.
-static function bool HandleTextFor
-(
- PlayerController receiver,
- Actor sender,
- out string message,
- optional name messageType
-)
-{
- return true;
-}
-
-// This event is called whenever a localized message is trying to
-// get broadcasted to a certain player ('receiver').
-// Return 'true' to allow the message through.
-// If one of the listeners returns 'false', -
-// it will be treated just like one of broadcasters returning 'false'
-// in 'AcceptBroadcastText' and this method won't be called for remaining
-// active listeners.
-static function bool HandleLocalized
-(
- Actor sender,
- BroadcastEvents.LocalizedMessage message
-)
-{
- return true;
-}
-
-// This event is similar to 'HandleLocalized', but is called for
-// every player the message is sent to.
-static function bool HandleLocalizedFor
-(
- PlayerController receiver,
- Actor sender,
- BroadcastEvents.LocalizedMessage message
-)
-{
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'BroadcastEvents'
-}
-
- // Text messages can (optionally) have their type specified.
- // Examples of it are names 'Say' and 'CriticalEvent'.
\ No newline at end of file
diff --git a/sources/Events.uc b/sources/Events.uc
deleted file mode 100644
index 86cf877..0000000
--- a/sources/Events.uc
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * One of the two classes that make up a core of event system in Acedia.
- *
- * 'Events' (or it's child) class shouldn't be instantiated.
- * Usually module would provide '...Events' class that defines
- * certain set of static functions that can generate event calls to
- * all it's active listeners.
- * If you're simply using modules someone made, -
- * you don't need to bother yourself with further specifics.
- * If you wish to create your own event generator,
- * then first create a '...ListenerBase' object
- * (more about it in the description of 'Listener' class)
- * and set 'relatedListener' variable to point to it's class.
- * Then for each event create a caller function in your 'Event' class,
- * following this template:
- * ____________________________________________________________________________
- * | static function CallEVENT_NAME()
- * | {
- * | local int i;
- * | local array< class > listeners;
- * | listeners = GetListeners();
- * | for (i = 0; i < listeners.length; i += 1)
- * | {
- * | class<...ListenerBase>(listeners[i])
- * | .static.EVENT_NAME();
- * | }
- * | }
- * |___________________________________________________________________________
- * If each listener must indicate whether it gives it's permission for
- * something to happen, then use this template:
- * ____________________________________________________________________________
- * | static function CallEVENT_NAME()
- * | {
- * | local int i;
- * | local bool result;
- * | local array< class > listeners;
- * | listeners = GetListeners();
- * | for (i = 0; i < listeners.length; i += 1)
- * | {
- * | result = class<...ListenerBase>(listeners[i])
- * | .static.EVENT_NAME();
- * | if (!result) return false;
- * | }
- * | return true;
- * | }
- * |___________________________________________________________________________
- * For concrete example look at
- * 'MutatorEvents' and 'MutatorListenerBase'.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class Events extends Object
- abstract;
-
-var private array< class > listeners;
-
-var public const class relatedListener;
-
-static public final function array< class > GetListeners()
-{
- return default.listeners;
-}
-
-// Make given listener active.
-// If listener was already activated also returns 'false'.
-static public final function bool ActivateListener(class newListener)
-{
- local int i;
- if (newListener == none) return false;
- if (!ClassIsChildOf(newListener, default.relatedListener)) return false;
-
- for (i = 0;i < default.listeners.length;i += 1)
- {
- if (default.listeners[i] == newListener)
- {
- return false;
- }
- }
- default.listeners[default.listeners.length] = newListener;
- return true;
-}
-
-// Make given listener inactive.
-// If listener wasn't active returns 'false'.
-static public final function bool DeactivateListener(class listener)
-{
- local int i;
- if (listener == none) return false;
-
- for (i = 0; i < default.listeners.length; i += 1)
- {
- if (default.listeners[i] == listener)
- {
- default.listeners.Remove(i, 1);
- return true;
- }
- }
- return false;
-}
-
-static public final function bool IsActiveListener(class listener)
-{
- local int i;
- if (listener == none) return false;
-
- for (i = 0; i < default.listeners.length; i += 1)
- {
- if (default.listeners[i] == listener)
- {
- return true;
- }
- }
- return false;
-}
-
-defaultproperties
-{
- relatedListener = class'Listener'
-}
\ No newline at end of file
diff --git a/sources/Feature.uc b/sources/Feature.uc
deleted file mode 100644
index a59eabf..0000000
--- a/sources/Feature.uc
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * Feature represents a certain subset of Acedia's functionality that
- * can be enabled or disabled, according to server owner's wishes.
- * In the current version of Acedia enabling or disabling a feature requires
- * manually editing configuration file and restarting a server.
- * Factually feature is just a collection of settings with one universal
- * 'isActive' setting that tells Acedia whether or not to load a feature.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class Feature extends Singleton
- abstract
- config(Acedia);
-
-// Setting that tells Acedia whether or not to enable this feature
-// during initialization.
-// Only it's default value is ever used.
-var private config bool autoEnable;
-
-// Listeners listed here will be automatically activated.
-var public const array< class > requiredListeners;
-
-// Sets whether to enable this feature by default.
-public static final function SetAutoEnable(bool doEnable)
-{
- default.autoEnable = doEnable;
- StaticSaveConfig();
-}
-
-public static final function bool IsAutoEnabled()
-{
- return default.autoEnable;
-}
-
-// Whether feature is enabled is determined by
-public static final function bool IsEnabled()
-{
- return (GetInstance() != none);
-}
-
-// Enables feature of given class.
-// To disable a feature simply use 'Destroy'.
-public static final function Feature EnableMe()
-{
- local Feature newInstance;
- if (IsEnabled())
- {
- return Feature(GetInstance());
- }
- default.blockSpawning = false;
- newInstance = class'Acedia'.static.GetInstance().Spawn(default.class);
- default.blockSpawning = true;
- return newInstance;
-}
-
-// Event functions that are called when
-public function OnEnabled(){}
-public function OnDisabled(){}
-
-// 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);
- }
-}
-
-// 'OnEnabled' and 'OnDisabled' should be called from functions that
-// will be called regardless of whether 'Feature' was created
-// with 'ChangeEnabledState' or in some other way.
-event PreBeginPlay()
-{
- super.PreBeginPlay();
- // '!bDeleteMe' means that we will be a singleton instance,
- // meaning that we only just got enabled.
- if (!bDeleteMe && IsEnabled())
- {
- // Block spawning this feature before calling any other events
- default.blockSpawning = true;
- SetListenersActiveSatus(true);
- OnEnabled();
- return;
- }
-}
-
-event Destroyed()
-{
- super.Destroyed();
- SetListenersActiveSatus(false);
- OnDisabled();
-}
-
-defaultproperties
-{
- autoEnable = false
- // Prevent spawning this feature by any other means than 'EnableMe()'.
- blockSpawning = true
- // Features are server-only actors
- remoteRole = ROLE_None
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/AmmoPickupStalker.uc b/sources/Features/FixAmmoSelling/AmmoPickupStalker.uc
deleted file mode 100644
index 603f2f9..0000000
--- a/sources/Features/FixAmmoSelling/AmmoPickupStalker.uc
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * This actor attaches itself to the ammo boxes
- * and imitates their collision to let us detect when they're picked up.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class AmmoPickupStalker extends Actor;
-
-// Ammo box this stalker is attached to.
-// If it is destroyed (not just picked up) - stalker must die too.
-var private KFAmmoPickup target;
-
-// This variable is used to record if our 'target' ammo box was in
-// active state ('Pickup') last time we've checked.
-// We need this because ammo box's 'Touch' event can fire off first and
-// force the box to sleep before stalker could catch same event.
-// Without this variable we would have no way to know if player
-// simply walked near the place of a sleeping box or actually grabbed it.
-var private bool wasActive;
-
-// Static function that spawns a new stalker for the given box.
-// Careful, as there's no checks for whether a stalker is
-// already attached to it.
-// Ensuring that is on the user of the function.
-public final static function StalkAmmoPickup(KFAmmoPickup newTarget)
-{
- local AmmoPickupStalker newStalker;
- if (newTarget == none) return;
-
- newStalker = newTarget.Spawn(class'AmmoPickupStalker');
- newStalker.target = newTarget;
- newStalker.SetBase(newTarget);
- newStalker.SetCollision(true);
- newStalker.SetCollisionSize(newTarget.collisionRadius,
- newTarget.collisionHeight);
-}
-
-event Touch(Actor other)
-{
- local FixAmmoSelling ammoSellingFix;
- if (target == none) return;
- // If our box was sleeping for while (more than a tick), -
- // player couldn't have gotten any ammo.
- if (!wasActive && !target.IsInState('Pickup')) return;
-
- ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance());
- if (ammoSellingFix != none)
- {
- ammoSellingFix.RecordAmmoPickup(Pawn(other), target);
- }
-}
-
-event Tick(float delta)
-{
- if (target != none)
- {
- wasActive = target.IsInState('Pickup');
- }
- else
- {
- Destroy();
- }
-}
-
-defaultproperties
-{
- // Server-only, hidden
- remoteRole = ROLE_None
- bAlwaysRelevant = true
- drawType = DT_None
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixAmmoSelling.uc b/sources/Features/FixAmmoSelling/FixAmmoSelling.uc
deleted file mode 100644
index e078a58..0000000
--- a/sources/Features/FixAmmoSelling/FixAmmoSelling.uc
+++ /dev/null
@@ -1,395 +0,0 @@
-/**
- * This feature addressed an oversight in vanilla code that
- * allows clients to sell weapon's ammunition.
- * Moreover, when being sold, ammunition cost is always multiplied by 0.75,
- * without taking into an account possible discount a player might have.
- * This allows cheaters to "print money" by buying and selling ammo over and
- * over again ammunition for some weapons,
- * notably pipe bombs (74% discount for lvl6 demolition)
- * and crossbow (42% discount for lvl6 sharpshooter).
- *
- * This feature fixes this problem by setting 'pickupClass' variable in
- * potentially abusable weapons to our own value that won't receive a discount.
- * Luckily for us, it seems that pickup spawn and discount checks are the only
- * two place where variable is directly checked in a vanilla game's code
- * ('default.pickupClass' is used everywhere else),
- * so we can easily deal with the side effects of such change.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSelling extends Feature;
-
-/**
- * We will replace 'pickupClass' variable for all instances of potentially
- * abusable weapons. That is weapons, that have a discount for their ammunition
- * (via 'GetAmmoCostScaling' function in a corresponding perk class).
- * They are defined (along with our pickup replacements) in 'rules' array.
- * That array isn't configurable, since the abusable status is hardcoded into
- * perk classes and the main mod that allows to change those (ServerPerks),
- * also solves ammo selling by a more direct method
- * (only available for the mods that replace player pawn class).
- * This change already completely fixes ammo printing.
- * Possible concern with changing the value of 'pickupClass' is that
- * it might affect gameplay in too many ways.
- * But, luckily for us, that value is only used when spawning a new pickup and
- * in 'ServerBuyAmmo' function of 'KFPawn'
- * (all the other places use it's default value instead).
- * This means that the only two side-effects of our change are:
- * 1. That wrong pickup class will be spawned. This problem is easily
- * solved by replacing spawned actor in 'CheckReplacement'.
- * 2. That ammo will be sold at a different (lower for us) price,
- * while trader would still display and require the original price.
- * This problem is solved by manually taking from player the difference
- * between what he should have had to pay and what he actually paid.
- * This brings us to the second issue -
- * detecting when player bought the ammo.
- * Unfortunately, it doesn't seem possible to detect with 100% certainty
- * without replacing pawn or shop classes,
- * so we have to eliminate other possibilities.
- * There are seem to be three ways for players to get more ammo:
- * 1. For some mod to give it;
- * 2. Found it an ammo box;
- * 3. To buy ammo (can only happen in trader).
- * We don't want to provide mods with low-level API for bug fixes,
- * so to ensure the compatibility, mods that want to increase ammo values
- * will have to solve compatibility issue by themselves:
- * either by reimplementing this fix (possibly the best option)
- * or by giving players appropriate money along with the ammo.
- * The only other case we have to eliminate is ammo boxes.
- * First, all cases of ammo boxes outside the trader are easy to detect,
- * since in this case we can be sure that player didn't buy ammo
- * (and mods that can allow it can just get rid of
- * 'ServerSellAmmo' function directly, similarly to how ServerPerks does it).
- * We'll detect all the other boxes by attaching an auxiliary actor
- * ('AmmoPickupStalker') to them, that will fire off 'Touch' event
- * at the same time as ammo boxes.
- * The only possible problem is that part of the ammo cost is
- * taken with a slight delay, which leaves cheaters a window of opportunity
- * to buy more than they can afford.
- * This issue is addressed by each ammo type costing as little as possible
- * (its' cost for corresponding perk at lvl6)
- * and a flag that does allow players to go into negative dosh values
- * (the cost is potential bugs in this fix itself, that
- * can somewhat affect regular players).
- */
-
-// Due to how this fix works, players with level below 6 get charged less
-// than necessary by the shop and this fix must take the rest of
-// the cost by itself.
-// The problem is, due to how ammo purchase is coded, low-level (<6 lvl)
-// players can actually buy more ammo for "fixed" weapons than they can afford
-// by filling ammo for one or all weapons.
-// Setting this flag to 'true' will allow us to still take full cost
-// from them, putting them in "debt" (having negative dosh amount).
-// If you don't want to have players with negative dosh values on your server
-// as a side-effect of this fix, then leave this flag as 'false',
-// letting low level players buy ammo cheaper
-// (but not cheaper than lvl6 could).
-// NOTE: this issue doesn't affect level 6 players.
-// NOTE #2: this fix does give players below level 6 some
-// technical advantage compared to vanilla game, but this advantage
-// cannot exceed benefits of having level 6.
-var private config const bool allowNegativeDosh;
-
-// This structure records what classes of weapons can be abused
-// and what pickup class we should use to fix the exploit.
-struct ReplacementRule
-{
- var class abusableWeapon;
- var class pickupReplacement;
-};
-
-// Actual list of abusable weapons.
-var private const array rules;
-
-// We create one such record for any
-// abusable weapon instance in the game to store:
-struct WeaponRecord
-{
- // The instance itself.
- var KFWeapon weapon;
- // Corresponding ammo instance
- // (all abusable weapons only have one ammo type).
- var KFAmmunition ammo;
- // Last ammo amount we've seen, used to detect players gaining ammo
- // (from either ammo boxes or buying it).
- var int lastAmmoAmount;
-};
-
-// All weapons we've detected so far.
-var private array registeredWeapons;
-
-public function OnEnabled()
-{
- local KFWeapon nextWeapon;
- local KFAmmoPickup nextPickup;
- // Find all abusable weapons
- foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon)
- {
- FixWeapon(nextWeapon);
- }
- // Start tracking all ammo boxes
- foreach level.DynamicActors(class'KFMod.KFAmmoPickup', nextPickup)
- {
- class'AmmoPickupStalker'.static.StalkAmmoPickup(nextPickup);
- }
-}
-
-public function OnDisabled()
-{
- local int i;
- local AmmoPickupStalker nextStalker;
- local array stalkers;
- // Restore all the 'pickupClass' variables we've changed.
- for (i = 0; i < registeredWeapons.length; i += 1)
- {
- if (registeredWeapons[i].weapon != none)
- {
- registeredWeapons[i].weapon.pickupClass =
- registeredWeapons[i].weapon.default.pickupClass;
- }
- }
- registeredWeapons.length = 0;
- // Kill all the stalkers;
- // to be safe, avoid destroying them directly in the iterator.
- foreach level.DynamicActors(class'AmmoPickupStalker', nextStalker)
- {
- stalkers[stalkers.length] = nextStalker;
- }
- for (i = 0; i < stalkers.length; i += 1)
- {
- if (stalkers[i] != none)
- {
- stalkers[i].Destroy();
- }
- }
-}
-
-// Checks if given class is a one of our pickup replacer classes.
-public static final function bool IsReplacer(class pickupClass)
-{
- local int i;
- if (pickupClass == none) return false;
- for (i = 0; i < default.rules.length; i += 1)
- {
- if (pickupClass == default.rules[i].pickupReplacement)
- {
- return true;
- }
- }
- return false;
-}
-
-// 1. Checks if weapon can be abused and if it can, - fixes the problem.
-// 2. Starts tracking abusable weapon to detect when player buys ammo for it.
-public final function FixWeapon(KFWeapon potentialAbuser)
-{
- local int i;
- local WeaponRecord newRecord;
- if (potentialAbuser == none) return;
-
- for (i = 0; i < registeredWeapons.length; i += 1)
- {
- if (registeredWeapons[i].weapon == potentialAbuser)
- {
- return;
- }
- }
- for (i = 0; i < rules.length; i += 1)
- {
- if (potentialAbuser.class == rules[i].abusableWeapon)
- {
- potentialAbuser.pickupClass = rules[i].pickupReplacement;
- newRecord.weapon = potentialAbuser;
- registeredWeapons[registeredWeapons.length] = newRecord;
- return;
- }
- }
-}
-
-// Finds ammo instance for recorded weapon in it's owner's inventory.
-private final function WeaponRecord FindAmmoInstance(WeaponRecord record)
-{
- local Inventory invIter;
- local KFAmmunition ammo;
- if (record.weapon == none) return record;
- if (record.weapon.instigator == none) return record;
-
- // Find instances anew
- invIter = record.weapon.instigator.inventory;
- while (invIter != none)
- {
- if (record.weapon.ammoClass[0] == invIter.class)
- {
- ammo = KFAmmunition(invIter);
- }
- invIter = invIter.inventory;
- }
- // Add missing instances
- if (ammo != none)
- {
- record.ammo = ammo;
- record.lastAmmoAmount = ammo.ammoAmount;
- }
- return record;
-}
-
-// Calculates how much more player should have paid for 'ammoAmount'
-// amount of ammo, compared to how much trader took after our fix.
-private final function float GetPriceCorrection
-(
- KFWeapon kfWeapon,
- int ammoAmount
-)
-{
- local float boughtMagFraction;
- // 'vanillaPrice' - price that would be calculated
- // without our interference
- // 'fixPrice' - price that will be calculated after
- // we've replaced pickup class
- local float vanillaPrice, fixPrice;
- local KFPlayerReplicationInfo kfRI;
- local class vanillaPickupClass, fixPickupClass;
- if (kfWeapon == none || kfWeapon.instigator == none) return 0.0;
- fixPickupClass = class(kfWeapon.pickupClass);
- vanillaPickupClass = class(kfWeapon.default.pickupClass);
- if (fixPickupClass == none || vanillaPickupClass == none) return 0.0;
-
- // Calculate base prices
- boughtMagFraction = (float(ammoAmount) / kfWeapon.default.magCapacity);
- fixPrice = boughtMagFraction * fixPickupClass.default.AmmoCost;
- vanillaPrice = boughtMagFraction * vanillaPickupClass.default.AmmoCost;
- // Apply perk discount for vanilla price
- // (we don't need to consider secondary ammo or husk gun special cases,
- // since such weapons can't be abused via ammo dosh-printing)
- kfRI = KFPlayerReplicationInfo(kfWeapon.instigator.playerReplicationInfo);
- if (kfRI != none && kfRI.clientVeteranSkill != none)
- {
- vanillaPrice *= kfRI.clientVeteranSkill.static.
- GetAmmoCostScaling(kfRI, vanillaPickupClass);
- }
- // TWI's code rounds up ammo cost
- // to the integer value whenever ammo is bought,
- // so to calculate exactly how much we need to correct the cost,
- // we must find difference between the final, rounded cost values.
- return float(Max(0, int(vanillaPrice) - int(fixPrice)));
-}
-
-// Takes current ammo and last recorded in 'record' value to calculate
-// how much money to take from the player
-// (calculations are done via 'GetPriceCorrection').
-private final function WeaponRecord TaxAmmoChange(WeaponRecord record)
-{
- local int ammoDiff;
- local KFPawn taxPayer;
- local PlayerReplicationInfo replicationInfo;
- taxPayer = KFPawn(record.weapon.instigator);
- if (record.weapon == none || taxPayer == none) return record;
- // No need to charge money if player couldn't have
- // possibly bought the ammo.
- if (!taxPayer.CanBuyNow()) return record;
- // Find ammo difference with recorded value.
- if (record.ammo != none)
- {
- ammoDiff = Max(0, record.ammo.ammoAmount - record.lastAmmoAmount);
- record.lastAmmoAmount = record.ammo.ammoAmount;
- }
- // Make player pay dosh
- replicationInfo = taxPayer.playerReplicationInfo;
- if (replicationInfo != none)
- {
- replicationInfo.score -= GetPriceCorrection(record.weapon, ammoDiff);
- // This shouldn't happen, since shop is supposed to make sure
- // player has enough dosh to buy ammo at full price
- // (actual price + our correction).
- // But if user is extra concerned about it, -
- // we can additionally for force the score above 0.
- if (!allowNegativeDosh)
- {
- replicationInfo.score = FMax(0, replicationInfo.score);
- }
- }
- return record;
-}
-
-// Changes our records to account for player picking up the ammo box,
-// to avoid charging his for it.
-public final function RecordAmmoPickup(Pawn pawnWithAmmo, KFAmmoPickup pickup)
-{
- local int i;
- local int newAmount;
- // Check conditions from 'KFAmmoPickup' code ('Touch' function)
- if (pickup == none) return;
- if (pawnWithAmmo == none) return;
- if (pawnWithAmmo.controller == none) return;
- if (!pawnWithAmmo.bCanPickupInventory) return;
- if (!FastTrace(pawnWithAmmo.location, pickup.location)) return;
-
- // Add relevant amount of ammo to our records
- for (i = 0; i < registeredWeapons.length; i += 1)
- {
- if (registeredWeapons[i].weapon == none) continue;
- if (registeredWeapons[i].weapon.instigator == pawnWithAmmo)
- {
- newAmount = registeredWeapons[i].lastAmmoAmount
- + registeredWeapons[i].ammo.ammoPickupAmount;
- newAmount = Min(registeredWeapons[i].ammo.maxAmmo, newAmount);
- registeredWeapons[i].lastAmmoAmount = newAmount;
- }
- }
-}
-
-event Tick(float delta)
-{
- local int i;
- // For all the weapon records...
- i = 0;
- while (i < registeredWeapons.length)
- {
- // ...remove dead records
- if (registeredWeapons[i].weapon == none)
- {
- registeredWeapons.Remove(i, 1);
- continue;
- }
- // ...find ammo if it's missing
- if (registeredWeapons[i].ammo == none)
- {
- registeredWeapons[i] = FindAmmoInstance(registeredWeapons[i]);
- }
- // ...tax for ammo, if we can
- registeredWeapons[i] = TaxAmmoChange(registeredWeapons[i]);
- i += 1;
- }
-}
-
-defaultproperties
-{
- allowNegativeDosh = false
- rules(0)=(abusableWeapon=class'KFMod.Crossbow',pickupReplacement=class'FixAmmoSellingClass_CrossbowPickup')
- rules(1)=(abusableWeapon=class'KFMod.PipeBombExplosive',pickupReplacement=class'FixAmmoSellingClass_PipeBombPickup')
- rules(2)=(abusableWeapon=class'KFMod.M79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M79Pickup')
- rules(3)=(abusableWeapon=class'KFMod.GoldenM79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_GoldenM79Pickup')
- rules(4)=(abusableWeapon=class'KFMod.M32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M32Pickup')
- rules(5)=(abusableWeapon=class'KFMod.CamoM32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_CamoM32Pickup')
- rules(6)=(abusableWeapon=class'KFMod.LAW',pickupReplacement=class'FixAmmoSellingClass_LAWPickup')
- rules(7)=(abusableWeapon=class'KFMod.SPGrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_SPGrenadePickup')
- rules(8)=(abusableWeapon=class'KFMod.SealSquealHarpoonBomber',pickupReplacement=class'FixAmmoSellingClass_SealSquealPickup')
- rules(9)=(abusableWeapon=class'KFMod.SeekerSixRocketLauncher',pickupReplacement=class'FixAmmoSellingClass_SeekerSixPickup')
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixAmmoSelling'
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc
deleted file mode 100644
index 9c55c9e..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CamoM32Pickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that
- * of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_CamoM32Pickup extends CamoM32Pickup;
-
-defaultproperties
-{
- AmmoCost = 42
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc
deleted file mode 100644
index 6fd07c1..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_CrossbowPickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for xbow to that
- * of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_CrossbowPickup extends CrossbowPickup;
-
-defaultproperties
-{
- AmmoCost = 11.6
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc
deleted file mode 100644
index ae1e1a3..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_GoldenM79Pickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for m79 to that
- * of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_GoldenM79Pickup extends GoldenM79Pickup;
-
-defaultproperties
-{
- AmmoCost = 7
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc
deleted file mode 100644
index d1cc802..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_LAWPickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for LAW to that
- * of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_LAWPickup extends LAWPickup;
-
-defaultproperties
-{
- AmmoCost = 21
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc
deleted file mode 100644
index 04f0321..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M32Pickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that
- * of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_M32Pickup extends M32Pickup;
-
-defaultproperties
-{
- AmmoCost = 42
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc
deleted file mode 100644
index 21a93f4..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_M79Pickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for M79 to that
- * of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_M79Pickup extends M79Pickup;
-
-defaultproperties
-{
- AmmoCost = 7
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc
deleted file mode 100644
index ff2041c..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_PipeBombPickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for pipes
- * to that of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_PipeBombPickup extends PipeBombPickup;
-
-defaultproperties
-{
- AmmoCost = 195
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc
deleted file mode 100644
index 5a1a29c..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SPGrenadePickup.uc
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for
- * orca grnade launcher to that of a level 6 player
- * and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_SPGrenadePickup extends SPGrenadePickup;
-
-defaultproperties
-{
- AmmoCost = 7
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc
deleted file mode 100644
index c80c56f..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SealSquealPickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for harpoon
- * to that of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_SealSquealPickup extends SealSquealPickup;
-
-defaultproperties
-{
- AmmoCost = 21
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc b/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc
deleted file mode 100644
index 31d3c26..0000000
--- a/sources/Features/FixAmmoSelling/FixedClasses/FixAmmoSellingClass_SeekerSixPickup.uc
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A helper class for 'FixAmmoSelling' that sets ammo cost for seeker
- * to that of a level 6 player and doesn't allow for a perk discount.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixAmmoSellingClass_SeekerSixPickup extends SeekerSixPickup;
-
-defaultproperties
-{
- AmmoCost = 10.5
-}
\ No newline at end of file
diff --git a/sources/Features/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc b/sources/Features/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc
deleted file mode 100644
index 78178de..0000000
--- a/sources/Features/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * Overloaded mutator events listener to register every new
- * spawned weapon and ammo pickup.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class MutatorListener_FixAmmoSelling extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- if (other == none) return true;
-
- // We need to replace pickup classes back,
- // as they might not even exist on clients.
- if (class'FixAmmoSelling'.static.IsReplacer(other.class))
- {
- ReplacePickupWith(Pickup(other));
- return false;
- }
- CheckAbusableWeapon(KFWeapon(other));
- // If it's ammo pickup - we need to stalk it
- class'AmmoPickupStalker'.static.StalkAmmoPickup(KFAmmoPickup(other));
- return true;
-}
-
-private static function CheckAbusableWeapon(KFWeapon newWeapon)
-{
- local FixAmmoSelling ammoSellingFix;
- if (newWeapon == none) return;
- ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance());
- if (ammoSellingFix == none) return;
- ammoSellingFix.FixWeapon(newWeapon);
-}
-
-// This function recreates the logic of 'KFWeapon.DropFrom()',
-// since standard 'ReplaceWith' function produces bad results.
-private static function ReplacePickupWith(Pickup oldPickup)
-{
- local Pawn instigator;
- local Pickup newPickup;
- local KFWeapon relevantWeapon;
- if (oldPickup == none) return;
- instigator = oldPickup.instigator;
- if (instigator == none) return;
- relevantWeapon = GetWeaponOfClass(instigator, oldPickup.inventoryType);
- if (relevantWeapon == none) return;
-
- newPickup = relevantWeapon.Spawn( relevantWeapon.default.pickupClass,,,
- relevantWeapon.location);
- newPickup.InitDroppedPickupFor(relevantWeapon);
- newPickup.velocity = relevantWeapon.velocity +
- Vector(instigator.rotation) * 100;
- if (instigator.health > 0)
- KFWeaponPickup(newPickup).bThrown = true;
-}
-
-// TODO: this is code duplication, some sort of solution is needed
-static final function KFWeapon GetWeaponOfClass
-(
- Pawn playerPawn,
- class weaponClass
-)
-{
- local Inventory invIter;
- if (playerPawn == none) return none;
-
- invIter = playerPawn.inventory;
- while (invIter != none)
- {
- if (invIter.class == weaponClass)
- {
- return KFWeapon(invIter);
- }
- invIter = invIter.inventory;
- }
- return none;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/Features/FixDoshSpam/FixDoshSpam.uc b/sources/Features/FixDoshSpam/FixDoshSpam.uc
deleted file mode 100644
index eecf2f3..0000000
--- a/sources/Features/FixDoshSpam/FixDoshSpam.uc
+++ /dev/null
@@ -1,252 +0,0 @@
-/**
- * This feature addressed two dosh-related issues:
- * 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash';
- * 2. Breaking collision detection logic by stacking large amount of
- * 'CashPickup' actors in one place, which allows one to either
- * reach unintended locations or even instantly kill zeds.
- *
- * It fixes them by limiting speed, with which dosh can spawn, and
- * allowing this limit to decrease when there's already too much dosh
- * present on the map.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixDoshSpam extends Feature;
-
-/**
- * First, we limit amount of dosh that can be spawned simultaneously.
- * The simplest method is to place a cooldown on spawning 'CashPickup' actors,
- * i.e. after spawning one 'CashPickup' we'd completely prevent spawning
- * any other instances of it for a fixed amount of time.
- * However, that might allow a malicious spammer to block others from
- * throwing dosh, - all he needs to do is to spam dosh at right time intervals.
- * We'll resolve this issue by recording how many 'CashPickup' actors
- * each player has spawned as their "contribution" and decay
- * that value with time, only allowing to spawn new dosh after
- * contribution decayed to zero. Speed of decay is derived from current dosh
- * spawning speed limit and decreases with amount of players
- * with non-zero contributions (since it means that they're throwing dosh).
- * Second issue is player amassing a large amount of dosh in one point
- * that leads to skipping collision checks, which then allows players to pass
- * through level geometry or enter zeds' collisions, instantly killing them.
- * Since dosh disappears on it's own, the easiest method to prevent that is to
- * severely limit how much dosh players can throw per second,
- * so that there's never enough dosh laying around to affect collision logic.
- * The downside to such severe limitations is that game behaves less
- * vanilla-like, where you could throw away streams of dosh.
- * To solve that we'll first use a more generous limit on dosh players can
- * throw per second, but will track how much dosh is currently present
- * in a level and linearly decelerate speed, according to that amount.
- */
-
-// Highest and lowest speed with which players can throw dosh wads.
-// It'll be evenly spread between all players.
-// For example, if speed is set to 6 and only one player will be spamming dosh,
-// - he'll be able to throw 6 wads of dosh per second;
-// but if all 6 players are spamming it, - each will throw only 1 per second.
-// NOTE: these speed values can be exceeded, since a player is guaranteed
-// to be able to throw at least one wad of dosh, if he didn't do so in awhile.
-// NOTE #2: if maximum value is less than minimum one,
-// the lowest (maximum one) will be used.
-var private config const float doshPerSecondLimitMax;
-var private config const float doshPerSecondLimitMin;
-// Amount of dosh pickups on the map at which we must set dosh per second
-// to 'doshPerSecondLimitMin'.
-// We use 'doshPerSecondLimitMax' when there's no dosh on the map and
-// scale linearly between them as it's amount grows.
-var private config const int criticalDoshAmount;
-
-// To limit dosh spawning speed we need some measure of
-// time passage between ticks.
-// This variable stores last value seen by us as a good approximation.
-// It's a real (not in-game) time.
-var private float lastTickDuration;
-
-// This structure records how much a certain player has
-// contributed to an overall dosh creation.
-struct DoshStreamPerPlayer
-{
- var PlayerController player;
- // Amount of dosh we remember this player creating, decays with time.
- var float contribution;
-};
-var private array currentContributors;
-
-// Wads of cash that are lying around on the map.
-var private array wads;
-
-public function OnEnabled()
-{
- local CashPickup nextCash;
- // Find all wads of cash laying around on the map,
- // so that we could accordingly limit the cash spam.
- foreach level.DynamicActors(class'KFMod.CashPickup', nextCash)
- {
- wads[wads.length] = nextCash;
- }
-}
-
-public function OnDisabled()
-{
- wads.length = 0;
- currentContributors.length = 0;
-}
-
-// Did player with this controller contribute to the latest dosh generation?
-public final function bool IsContributor(PlayerController player)
-{
- return (GetContributorIndex(player) >= 0);
-}
-
-// Did we already reach allowed limit of dosh per second?
-public final function bool IsDoshStreamOverLimit()
-{
- local int i;
- local float overallContribution;
- overallContribution = 0.0;
- for (i = 0; i < currentContributors.length; i += 1)
- {
- overallContribution += currentContributors[i].contribution;
- }
- return (overallContribution > lastTickDuration * GetCurrentDPSLimit());
-}
-
-// What is our current dosh per second limit?
-private final function float GetCurrentDPSLimit()
-{
- local float speedScale;
- if (doshPerSecondLimitMax < doshPerSecondLimitMin)
- {
- return doshPerSecondLimitMax;
- }
- speedScale = Float(wads.length) / Float(criticalDoshAmount);
- speedScale = FClamp(speedScale, 0.0, 1.0);
- // At 0.0 scale (no dosh on the map) - use max speed
- // At 1.0 scale (critical dosh on the map) - use min speed
- return Lerp(speedScale, doshPerSecondLimitMax, doshPerSecondLimitMin);
-}
-
-// Returns index of the contributor corresponding to the given controller.
-// Returns '-1' if no connection correspond to the given controller.
-// Returns '-1' if given controller is equal to 'none'.
-private final function int GetContributorIndex(PlayerController player)
-{
- local int i;
- if (player == none) return -1;
-
- for (i = 0; i < currentContributors.length; i += 1)
- {
- if (currentContributors[i].player == player)
- {
- return i;
- }
- }
- return -1;
-}
-
-// Adds given cash to given player contribution record and
-// registers that cash in our wads array.
-// Does nothing if given cash was already registered.
-public final function AddContribution(PlayerController player, CashPickup cash)
-{
- local int i;
- local int playerIndex;
- local DoshStreamPerPlayer newStreamRecord;
- // Check if given dosh was already accounted for.
- for (i = 0; i < wads.length; i += 1)
- {
- if (cash == wads[i])
- {
- return;
- }
- }
- wads[wads.length] = cash;
- // Add contribution to player
- playerIndex = GetContributorIndex(player);
- if (playerIndex >= 0)
- {
- currentContributors[playerIndex].contribution += 1.0;
- return;
- }
- newStreamRecord.player = player;
- newStreamRecord.contribution = 1.0;
- currentContributors[currentContributors.length] = newStreamRecord;
-}
-
-private final function ReducePlayerContributions(float trueTimePassed)
-{
- local int i;
- local float streamReduction;
- streamReduction = trueTimePassed *
- (GetCurrentDPSLimit() / currentContributors.length);
- for (i = 0; i < currentContributors.length; i += 1)
- {
- currentContributors[i].contribution -= streamReduction;
- }
-}
-
-// Clean out wads that disappeared or were picked up by players.
-private final function CleanWadsArray()
-{
- local int i;
- i = 0;
- while (i < wads.length)
- {
- if (wads[i] == none)
- {
- wads.Remove(i, 1);
- }
- else
- {
- i += 1;
- }
- }
-}
-
-// Don't track players that no longer contribute to dosh generation.
-private final function RemoveNonContributors()
-{
- local int i;
- local array updContributors;
- for (i = 0; i < currentContributors.length; i += 1)
- {
- // We want to keep on record even players that quit,
- // since their contribution still must be accounted for.
- if (currentContributors[i].contribution <= 0.0) continue;
- updContributors[updContributors.length] = currentContributors[i];
- }
- currentContributors = updContributors;
-}
-
-event Tick(float delta)
-{
- local float trueTimePassed;
- trueTimePassed = delta * (1.1 / level.timeDilation);
- CleanWadsArray();
- ReducePlayerContributions(trueTimePassed);
- RemoveNonContributors();
- lastTickDuration = trueTimePassed;
-}
-
-defaultproperties
-{
- doshPerSecondLimitMax = 50
- doshPerSecondLimitMin = 5
- criticalDoshAmount = 25
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixDoshSpam'
-}
\ No newline at end of file
diff --git a/sources/Features/FixDoshSpam/MutatorListener_FixDoshSpam.uc b/sources/Features/FixDoshSpam/MutatorListener_FixDoshSpam.uc
deleted file mode 100644
index 5b85353..0000000
--- a/sources/Features/FixDoshSpam/MutatorListener_FixDoshSpam.uc
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Overloaded mutator events listener to catch and, possibly,
- * prevent spawning dosh actors.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class MutatorListener_FixDoshSpam extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local FixDoshSpam doshFix;
- local PlayerController player;
- if (other.class != class'CashPickup') return true;
- // This means this dosh wasn't spawned in 'TossCash' of 'KFPawn',
- // so it isn't related to the exploit we're trying to fix.
- if (other.instigator == none) return true;
- doshFix = FixDoshSpam(class'FixDoshSpam'.static.GetInstance());
- if (doshFix == none) return true;
-
- // We only want to prevent spawning cash if we're already over
- // the limit and the one trying to throw this cash contributed to it.
- // We allow other players to throw at least one wad of cash.
- player = PlayerController(other.instigator.controller);
- if (doshFix.IsDoshStreamOverLimit() && doshFix.IsContributor(player))
- {
- return false;
- }
- // If we do spawn cash - record this contribution.
- doshFix.AddContribution(player, CashPickup(other));
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/Features/FixDualiesCost/DualiesCostRule.uc b/sources/Features/FixDualiesCost/DualiesCostRule.uc
deleted file mode 100644
index aa03156..0000000
--- a/sources/Features/FixDualiesCost/DualiesCostRule.uc
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * This rule detects any pickup events to allow us to
- * properly record and/or fix pistols' prices.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class DualiesCostRule extends GameRules;
-
-function bool OverridePickupQuery
-(
- Pawn other,
- Pickup item,
- out byte allowPickup
-)
-{
- local KFWeaponPickup weaponPickup;
- local FixDualiesCost dualiesCostFix;
- weaponPickup = KFWeaponPickup(item);
- dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance());
- if (weaponPickup != none && dualiesCostFix != none)
- {
- dualiesCostFix.ApplyPendingValues();
- dualiesCostFix.StoreSinglePistolValues();
- dualiesCostFix.SetNextSellValue(weaponPickup.sellValue);
- }
- return super.OverridePickupQuery(other, item, allowPickup);
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/Features/FixDualiesCost/FixDualiesCost.uc b/sources/Features/FixDualiesCost/FixDualiesCost.uc
deleted file mode 100644
index 2fd3822..0000000
--- a/sources/Features/FixDualiesCost/FixDualiesCost.uc
+++ /dev/null
@@ -1,454 +0,0 @@
-/**
- * This feature fixes several issues related to the selling price of both
- * single and dual pistols, all originating from the existence of dual weapons.
- * Most notable issue is the ability to "print" money by buying and
- * selling pistols in a certain way.
- *
- * It fixes all of the issues by manually setting pistols'
- * 'SellValue' variables to proper values.
- * Fix only works with vanilla pistols, as it's unpredictable what
- * custom ones can do and they can handle these issues on their own
- * in a better way.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixDualiesCost extends Feature;
-
-/**
- * Issues with pistols' cost may look varied and surface in
- * a plethora of ways, but all of them originate from the two main errors
- * in vanilla's code:
- * 1. If you have a pistol in your inventory at the time when you
- * buy/pickup another one - the sell value of resulting dualies is
- * incorrectly set to the sell value of the second pistol;
- * 2. When player has dual pistols and drops one on the floor, -
- * the sell value for the one left with the player isn't set.
- * All weapons in Killing Floor get sell value assigned to them
- * (appropriately, in a 'SellValue' variable). This is to ensure that the sell
- * price is set the moment players buys the gun. Otherwise, due to ridiculous
- * perked discounts, you'd be able to buy a pistol at 30% price
- * as sharpshooter, but sell at 75% of a price as any other perk,
- * resulting in 45% of pure profit.
- * Unfortunately, that's exactly what happens when 'SellValue' isn't set
- * (left as it's default value of '-1'): sell value of such weapons is
- * determined only at the moment of sale and depends on the perk of the seller,
- * allowing for possible exploits.
- *
- * These issues are fixed by directly assigning
- * proper values to 'SellValue'. To do that we need to detect when player
- * buys/sells/drops/picks up weapons, which we accomplish by catching
- * 'CheckReplacement' event for weapon instances. This approach has two issues.
- * One is that, if vanilla's code sets an incorrect sell value, -
- * it's doing it after weapon is spawned and, therefore,
- * after 'CheckReplacement' call, so we have, instead, to remember to do
- * it later, as early as possible
- * (either the next tick or before another operation with weapons).
- * Another issue is that when you have a pistol and pick up a pistol of
- * the same type, - at the moment dualies instance is spawned,
- * the original pistol in player's inventory is gone and we can't use
- * it's sell value to calculate new value of dual pistols.
- * This problem is solved by separately recording the value for every
- * single pistol every tick.
- * However, if pistol pickups are placed close enough together on the map,
- * player can start touching them (which triggers a pickup) at the same time,
- * picking them both in a single tick. This leaves us no room to record
- * the value of a single pistol players picks up first.
- * To get it we use game rules to catch 'OverridePickupQuery' event that's
- * called before the first one gets destroyed,
- * but after it's sell value was already set.
- * Last issue is that when player picks up a second pistol - we don't know
- * it's sell value and, therefore, can't calculate value of dual pistols.
- * This is resolved by recording that value directly from a pickup,
- * in abovementioned function 'OverridePickupQuery'.
- * NOTE: 9mm is an exception due to the fact that you always have at least
- * one and the last one can't be sold. We'll deal with it by setting
- * the following rule: sell value of the un-droppable pistol is always 0
- * and the value of a pair of 9mms is the value of the single droppable pistol.
- */
-
-// Some issues involve possible decrease in pistols' price and
-// don't lead to exploit, but are still bugs and require fixing.
-// If you have a Deagle in your inventory and then get another one
-// (by either buying or picking it off the ground) - the price of resulting
-// dual pistols will be set to the price of the last deagle,
-// like the first one wasn't worth anything at all.
-// In particular this means that (prices are off-perk for more clarity):
-// 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of
-// the cost (+750 do$h), you lose 250 do$h;
-// 2. If you first buy a deagle (-500 do$h), then buy
-// the second one (-500 do$h) and then sell them, you'll only get
-// 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h;
-// 3. So if you already have bought a deagle (-500 do$h),
-// you can get a more expensive weapon by doing a stupid thing
-// and first selling your Deagle (+375 do$h),
-// then buying dual deagles (-1000 do$h).
-// If you sell them after that, you'll gain 75% of the cost of
-// dual deagles (+750 do$h), leaving you with losing only 375 do$h.
-// Of course, situations described above are only relevant if you're planning
-// to sell your weapons at some point and most people won't even notice it.
-// But such an oversight still shouldn't exist in a game and we fix it by
-// setting sell value of dualies as a sum of values of each pistol.
-// Yet, fixing this issue leads to players having more expensive
-// (while fairly priced) weapons than on vanilla, technically making
-// the game easier. And some people might object to having that in
-// a whitelisted bug-fixing feature.
-// These people are, without a question, complete degenerates.
-// But making mods for only non-mentally challenged isn't inclusive.
-// So we add this option.
-// Set it to 'false' if you only want to fix ammo printing
-// and leave the rest of the bullshit as-is.
-var private config const bool allowSellValueIncrease;
-
-// Describe all the possible pairs of dual pistols in a vanilla game.
-struct DualiesPair
-{
- var class single;
- var class dual;
-};
-var private const array dualiesClasses;
-
-// Describe sell values that need to be applied at earliest later point.
-struct WeaponValuePair
-{
- var KFWeapon weapon;
- var float value;
-};
-var private const array pendingValues;
-
-// Describe sell values of all currently existing single pistols.
-struct WeaponDataRecord
-{
- var KFWeapon reference;
- var class class;
- var float value;
- // The whole point of this structure is to remember value of a weapon
- // after it's destroyed. Since 'reference' will become 'none' by then,
- // we will use the 'owner' reference to identify the weapon.
- var Pawn owner;
-};
-var private const array storedValues;
-
-// Sell value of the last seen pickup in 'OverridePickupQuery'
-var private int nextSellValue;
-
-public function OnEnabled()
-{
- local KFWeapon nextWeapon;
- // Find all frags, that spawned when this fix wasn't running.
- foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon)
- {
- RegisterSinglePistol(nextWeapon, false);
- }
- level.game.AddGameModifier(Spawn(class'DualiesCostRule'));
-}
-
-public function OnDisabled()
-{
- local GameRules rulesIter;
- local DualiesCostRule ruleToDestroy;
- // Check first rule
- if (level.game.gameRulesModifiers == none) return;
-
- ruleToDestroy = DualiesCostRule(level.game.gameRulesModifiers);
- if (ruleToDestroy != none)
- {
- level.game.gameRulesModifiers = ruleToDestroy.nextGameRules;
- ruleToDestroy.Destroy();
- return;
- }
- // Check rest of the rules
- rulesIter = level.game.gameRulesModifiers;
- while (rulesIter != none)
- {
- ruleToDestroy = DualiesCostRule(rulesIter.nextGameRules);
- if (ruleToDestroy != none)
- {
- rulesIter.nextGameRules = ruleToDestroy.nextGameRules;
- ruleToDestroy.Destroy();
- }
- rulesIter = rulesIter.nextGameRules;
- }
-}
-
-public final function SetNextSellValue(int newValue)
-{
- nextSellValue = newValue;
-}
-
-// Finds a weapon of a given class in given 'Pawn' 's inventory.
-// Returns 'none' if weapon isn't there.
-private final function KFWeapon GetWeaponOfClass
-(
- Pawn playerPawn,
- class weaponClass
-)
-{
- local Inventory invIter;
- if (playerPawn == none) return none;
-
- invIter = playerPawn.inventory;
- while (invIter != none)
- {
- if (invIter.class == weaponClass)
- {
- return KFWeapon(invIter);
- }
- invIter = invIter.inventory;
- }
- return none;
-}
-
-// Gets weapon index in our record of dual pistol classes.
-// Second variable determines whether we're searching for single
-// or dual variant:
-// ~ 'true' - searching for single
-// ~ 'false' - for dual
-// Returns '-1' if weapon isn't found
-// (dual MK23 won't be found as a single weapon).
-private final function int GetIndexAs(KFWeapon weapon, bool asSingle)
-{
- local int i;
- if (weapon == none) return -1;
-
- for (i = 0; i < dualiesClasses.length; i += 1)
- {
- if (asSingle && dualiesClasses[i].single == weapon.class)
- {
- return i;
- }
- if (!asSingle && dualiesClasses[i].dual == weapon.class)
- {
- return i;
- }
- }
- return -1;
-}
-
-// Calculates full cost of a weapon with a discount,
-// dependent on it's instigator's perk.
-private final function float GetFullCost(KFWeapon weapon)
-{
- local float cost;
- local class pickupClass;
- local KFPlayerReplicationInfo instigatorRI;
- if (weapon == none) return 0.0;
- pickupClass = class(weapon.default.pickupClass);
- if (pickupClass == none) return 0.0;
-
- cost = pickupClass.default.cost;
- if (weapon.instigator != none)
- {
- instigatorRI =
- KFPlayerReplicationInfo(weapon.instigator.playerReplicationInfo);
- }
- if (instigatorRI != none && instigatorRI.clientVeteranSkill != none)
- {
- cost *= instigatorRI.clientVeteranSkill.static
- .GetCostScaling(instigatorRI, pickupClass);
- }
- return cost;
-}
-
-// If passed weapon is a pistol - we start tracking it's value;
-// Otherwise - do nothing.
-public final function RegisterSinglePistol
-(
- KFWeapon singlePistol,
- bool justSpawned
-)
-{
- local WeaponDataRecord newRecord;
- if (singlePistol == none) return;
- if (GetIndexAs(singlePistol, true) < 0) return;
-
- newRecord.reference = singlePistol;
- newRecord.class = singlePistol.class;
- newRecord.owner = singlePistol.instigator;
- if (justSpawned)
- {
- newRecord.value = nextSellValue;
- }
- else
- {
- newRecord.value = singlePistol.sellValue;
- }
- storedValues[storedValues.length] = newRecord;
-}
-
-// Fixes sell value after player throws one pistol out of a pair.
-public final function FixCostAfterThrow(KFWeapon singlePistol)
-{
- local int index;
- local KFWeapon dualPistols;
- if (singlePistol == none) return;
- index = GetIndexAs(singlePistol, true);
- if (index < 0) return;
- dualPistols = GetWeaponOfClass( singlePistol.instigator,
- dualiesClasses[index].dual);
- if (dualPistols == none) return;
-
- // Sell value recorded into 'dualPistols' will end up as a value of
- // a dropped pickup.
- // Sell value of 'singlePistol' will be the value for the pistol,
- // left in player's hands.
- if (dualPistols.class == class'KFMod.Single')
- {
- // 9mm is an exception.
- // Remaining weapon costs nothing.
- singlePistol.sellValue = 0;
- // We don't change the sell value of the dropped weapon,
- // as it's default behavior to transfer full value of a pair to it.
- return;
- }
- // For other pistols - divide the value.
- singlePistol.sellValue = dualPistols.sellValue / 2;
- dualPistols.sellValue = singlePistol.sellValue;
-}
-
-// Fixes sell value after buying a pair of dual pistols,
-// if player already had a single version.
-public final function FixCostAfterBuying(KFWeapon dualPistols)
-{
- local int index;
- local KFWeapon singlePistol;
- local WeaponValuePair newPendingValue;
- if (dualPistols == none) return;
- index = GetIndexAs(dualPistols, false);
- if (index < 0) return;
- singlePistol = GetWeaponOfClass(dualPistols.instigator,
- dualiesClasses[index].single);
- if (singlePistol == none) return;
-
- // 'singlePistol' will get destroyed, so it's sell value is irrelevant.
- // 'dualPistols' will be the new pair of pistols, but it's value will
- // get overwritten by vanilla's code after this function.
- // So we must add it to pending values to be changed later.
- newPendingValue.weapon = dualPistols;
- if (dualPistols.class == class'KFMod.Dualies')
- {
- // 9mm is an exception.
- // The value of pair of 9mms is the price of additional pistol,
- // that defined as a price of a pair in game.
- newPendingValue.value = GetFullCost(dualPistols) * 0.75;
- }
- else
- {
- // Otherwise price of a pair is the price of two pistols:
- // 'singlePistol.sellValue' - the one we had
- // '(FullCost / 2) * 0.75' - and the one we bought
- newPendingValue.value = singlePistol.sellValue
- + (GetFullCost(dualPistols) / 2) * 0.75;
- }
- pendingValues[pendingValues.length] = newPendingValue;
-}
-
-// Fixes sell value after player picks up a single pistol,
-// while already having one of the same time in his inventory.
-public final function FixCostAfterPickUp(KFWeapon dualPistols)
-{
- local int i;
- local int index;
- local KFWeapon singlePistol;
- local WeaponValuePair newPendingValue;
- if (dualPistols == none) return;
- // In both cases of:
- // 1. buying dualies, without having a single pistol of
- // corresponding type;
- // 2. picking up a second pistol, while having another one;
- // by the time of 'CheckReplacement' (and, therefore, this function)
- // is called, there's no longer any single pistol in player's inventory
- // (in first case it never was there, in second - it got destroyed).
- // To distinguish between those possibilities we can check the owner of
- // the spawned weapon, since it's only set to instigator at the time of
- // 'CheckReplacement' when player picks up a weapon.
- // So we require that owner exists.
- if (dualPistols.owner == none) return;
- index = GetIndexAs(dualPistols, false);
- if (index < 0) return;
- singlePistol = GetWeaponOfClass(dualPistols.instigator,
- dualiesClasses[index].single);
- if (singlePistol != none) return;
-
- if (nextSellValue == -1)
- {
- nextSellValue = GetFullCost(dualPistols) * 0.75;
- }
- for (i = 0; i < storedValues.length; i += 1)
- {
- if (storedValues[i].reference != none) continue;
- if (storedValues[i].class != dualiesClasses[index].single) continue;
- if (storedValues[i].owner != dualPistols.instigator) continue;
- newPendingValue.weapon = dualPistols;
- newPendingValue.value = storedValues[i].value + nextSellValue;
- pendingValues[pendingValues.length] = newPendingValue;
- break;
- }
-}
-
-public final function ApplyPendingValues()
-{
- local int i;
- for (i = 0; i < pendingValues.length; i += 1)
- {
- if (pendingValues[i].weapon == none) continue;
- // Our fixes can only increase the correct ('!= -1')
- // sell value of weapons, so if we only need to change sell value
- // if we're allowed to increase it or it's incorrect.
- if (allowSellValueIncrease || pendingValues[i].weapon.sellValue == -1)
- {
- pendingValues[i].weapon.sellValue = pendingValues[i].value;
- }
- }
- pendingValues.length = 0;
-}
-
-public final function StoreSinglePistolValues()
-{
- local int i;
- i = 0;
- while (i < storedValues.length)
- {
- if (storedValues[i].reference == none)
- {
- storedValues.Remove(i, 1);
- continue;
- }
- storedValues[i].owner = storedValues[i].reference.instigator;
- storedValues[i].value = storedValues[i].reference.sellValue;
- i += 1;
- }
-}
-
-event Tick(float delta)
-{
- ApplyPendingValues();
- StoreSinglePistolValues();
-}
-
-defaultproperties
-{
- allowSellValueIncrease = true
- // Inner variables
- dualiesClasses(0)=(single=class'KFMod.Single',dual=class'KFMod.Dualies')
- dualiesClasses(1)=(single=class'KFMod.Magnum44Pistol',dual=class'KFMod.Dual44Magnum')
- dualiesClasses(2)=(single=class'KFMod.MK23Pistol',dual=class'KFMod.DualMK23Pistol')
- dualiesClasses(3)=(single=class'KFMod.Deagle',dual=class'KFMod.DualDeagle')
- dualiesClasses(4)=(single=class'KFMod.GoldenDeagle',dual=class'KFMod.GoldenDualDeagle')
- dualiesClasses(5)=(single=class'KFMod.FlareRevolver',dual=class'KFMod.DualFlareRevolver')
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixDualiesCost'
-}
\ No newline at end of file
diff --git a/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc b/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc
deleted file mode 100644
index 0f209ae..0000000
--- a/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Overloaded mutator events listener to catch when pistol-type weapons
- * (single or dual) are spawned and to correct their price.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class MutatorListener_FixDualiesCost extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local KFWeapon weapon;
- local FixDualiesCost dualiesCostFix;
- weapon = KFWeapon(other);
- if (weapon == none) return true;
- dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance());
- if (dualiesCostFix == none) return true;
-
- dualiesCostFix.RegisterSinglePistol(weapon, true);
- dualiesCostFix.FixCostAfterThrow(weapon);
- dualiesCostFix.FixCostAfterBuying(weapon);
- dualiesCostFix.FixCostAfterPickUp(weapon);
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/Features/FixFFHack/FFHackRule.uc b/sources/Features/FixFFHack/FFHackRule.uc
deleted file mode 100644
index efe0556..0000000
--- a/sources/Features/FixFFHack/FFHackRule.uc
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * This rule detects suspicious attempts to deal damage and
- * applies friendly fire scaling according to 'FixFFHack's rules.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FFHackRule extends GameRules;
-
-function int NetDamage
-(
- int originalDamage,
- int damage,
- Pawn injured,
- Pawn instigator,
- Vector hitLocation,
- out Vector momentum,
- class damageType
-)
-{
- local KFGameType gameType;
- local FixFFHack ffHackFix;
- gameType = KFGameType(level.game);
- // Something is very wrong and we can just bail on this damage
- if (damageType == none || gameType == none) return 0;
-
- // We only check when suspicious instigators that aren't a world
- if (!damageType.default.bCausedByWorld && IsSuspicious(instigator))
- {
- ffHackFix = FixFFHack(class'FixFFHack'.static.GetInstance());
- if (ffHackFix != none && ffHackFix.ShouldScaleDamage(damageType))
- {
- // Remove pushback to avoid environmental kills
- momentum = Vect(0.0, 0.0, 0.0);
- damage *= gameType.friendlyFireScale;
- }
- }
- return super.NetDamage( originalDamage, damage, injured, instigator,
- hitLocation, momentum, damageType);
-}
-
-private function bool IsSuspicious(Pawn instigator)
-{
- // Instigator vanished
- if (instigator == none) return true;
-
- // Instigator already became spectator
- if (KFPawn(instigator) != none)
- {
- if (instigator.playerReplicationInfo != none)
- {
- return instigator.playerReplicationInfo.bOnlySpectator;
- }
- return true; // Replication info is gone => suspicious
- }
- return false;
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/Features/FixFFHack/FixFFHack.uc b/sources/Features/FixFFHack/FixFFHack.uc
deleted file mode 100644
index 33ad62a..0000000
--- a/sources/Features/FixFFHack/FixFFHack.uc
+++ /dev/null
@@ -1,152 +0,0 @@
-/**
- * This feature fixes a bug that can allow players to bypass server's
- * friendly fire limitations and teamkill.
- * Usual fixes apply friendly fire scale to suspicious damage themselves, which
- * also disables some of the environmental damage.
- * In order to avoid that, this fix allows server owner to define precisely
- * to what damage types to apply the friendly fire scaling.
- * It should be all damage types related to projectiles.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixFFHack extends Feature;
-
-/**
- * It's possible to bypass friendly fire damage scaling and always deal
- * full damage to other players, if one were to either leave the server or
- * spectate right after shooting a projectile. We use game rules to catch
- * such occurrences and apply friendly fire scaling to weapons,
- * specified by server admins.
- * To specify required subset of weapons, one must first
- * chose a general rule (scale by default / don't scale by default) and then,
- * optionally, add exceptions to it.
- * Choosing 'scaleByDefault == true' as a general rule will make this fix
- * behave in the similar way to 'KFExplosiveFix' by mutant and will disable
- * some environmental sources of damage on some maps. One can then add relevant
- * damage classes as exceptions to fix that downside, but making an extensive
- * list of such sources might prove problematic.
- * On the other hand, setting 'scaleByDefault == false' will allow to get
- * rid of team-killing exploits by simply adding damage types of all
- * projectile weapons, used on a server. This fix comes with such filled-in
- * list of all vanilla projectile classes.
- */
-
-// Defines a general rule for choosing whether or not to apply
-// friendly fire scaling.
-// This can be overwritten by exceptions ('alwaysScale' or 'neverScale').
-// Enabling scaling by default without any exceptions in 'neverScale' will
-// make this fix behave almost identically to Mutant's 'Explosives Fix Mutator'.
-var private config const bool scaleByDefault;
-// Damage types, for which we should always reapply friendly fire scaling.
-var private config const array< class > alwaysScale;
-// Damage types, for which we should never reapply friendly fire scaling.
-var private config const array< class > neverScale;
-
-public function OnEnabled()
-{
- level.game.AddGameModifier(Spawn(class'FFHackRule'));
-}
-
-public function OnDisabled()
-{
- local GameRules rulesIter;
- local FFHackRule ruleToDestroy;
- // Check first rule
- if (level.game.gameRulesModifiers == none) return;
-
- ruleToDestroy = FFHackRule(level.game.gameRulesModifiers);
- if (ruleToDestroy != none)
- {
- level.game.gameRulesModifiers = ruleToDestroy.nextGameRules;
- ruleToDestroy.Destroy();
- return;
- }
- // Check rest of the rules
- rulesIter = level.game.gameRulesModifiers;
- while (rulesIter != none)
- {
- ruleToDestroy = FFHackRule(rulesIter.nextGameRules);
- if (ruleToDestroy != none)
- {
- rulesIter.nextGameRules = ruleToDestroy.nextGameRules;
- ruleToDestroy.Destroy();
- }
- rulesIter = rulesIter.nextGameRules;
- }
-}
-
-// Checks general rule and exception list
-public final function bool ShouldScaleDamage(class damageType)
-{
- local int i;
- local array< class > exceptions;
- if (damageType == none) return false;
-
- if (scaleByDefault)
- exceptions = neverScale;
- else
- exceptions = alwaysScale;
- for (i = 0; i < exceptions.length; i += 1)
- {
- if (exceptions[i] == damageType)
- {
- return (!scaleByDefault);
- }
- }
- return scaleByDefault;
-}
-
-defaultproperties
-{
- scaleByDefault = false
- // Vanilla damage types for projectiles
- alwaysScale(0) = class'KFMod.DamTypeCrossbuzzsawHeadShot'
- alwaysScale(1) = class'KFMod.DamTypeCrossbuzzsaw'
- alwaysScale(2) = class'KFMod.DamTypeFrag'
- alwaysScale(3) = class'KFMod.DamTypePipeBomb'
- alwaysScale(4) = class'KFMod.DamTypeM203Grenade'
- alwaysScale(5) = class'KFMod.DamTypeM79Grenade'
- alwaysScale(6) = class'KFMod.DamTypeM79GrenadeImpact'
- alwaysScale(7) = class'KFMod.DamTypeM32Grenade'
- alwaysScale(8) = class'KFMod.DamTypeLAW'
- alwaysScale(9) = class'KFMod.DamTypeLawRocketImpact'
- alwaysScale(10) = class'KFMod.DamTypeFlameNade'
- alwaysScale(11) = class'KFMod.DamTypeFlareRevolver'
- alwaysScale(12) = class'KFMod.DamTypeFlareProjectileImpact'
- alwaysScale(13) = class'KFMod.DamTypeBurned'
- alwaysScale(14) = class'KFMod.DamTypeTrenchgun'
- alwaysScale(15) = class'KFMod.DamTypeHuskGun'
- alwaysScale(16) = class'KFMod.DamTypeCrossbow'
- alwaysScale(17) = class'KFMod.DamTypeCrossbowHeadShot'
- alwaysScale(18) = class'KFMod.DamTypeM99SniperRifle'
- alwaysScale(19) = class'KFMod.DamTypeM99HeadShot'
- alwaysScale(20) = class'KFMod.DamTypeShotgun'
- alwaysScale(21) = class'KFMod.DamTypeNailGun'
- alwaysScale(22) = class'KFMod.DamTypeDBShotgun'
- alwaysScale(23) = class'KFMod.DamTypeKSGShotgun'
- alwaysScale(24) = class'KFMod.DamTypeBenelli'
- alwaysScale(25) = class'KFMod.DamTypeSPGrenade'
- alwaysScale(26) = class'KFMod.DamTypeSPGrenadeImpact'
- alwaysScale(27) = class'KFMod.DamTypeSeekerSixRocket'
- alwaysScale(28) = class'KFMod.DamTypeSeekerRocketImpact'
- alwaysScale(29) = class'KFMod.DamTypeSealSquealExplosion'
- alwaysScale(30) = class'KFMod.DamTypeRocketImpact'
- alwaysScale(31) = class'KFMod.DamTypeBlowerThrower'
- alwaysScale(32) = class'KFMod.DamTypeSPShotgun'
- alwaysScale(33) = class'KFMod.DamTypeZEDGun'
- alwaysScale(34) = class'KFMod.DamTypeZEDGunMKII'
-}
\ No newline at end of file
diff --git a/sources/Features/FixInfiniteNades/FixInfiniteNades.uc b/sources/Features/FixInfiniteNades/FixInfiniteNades.uc
deleted file mode 100644
index bec784f..0000000
--- a/sources/Features/FixInfiniteNades/FixInfiniteNades.uc
+++ /dev/null
@@ -1,233 +0,0 @@
- /**
- * This feature fixes a vulnerability in a code of 'Frag' that can allow
- * player to throw grenades even when he no longer has any.
- * There's also no cooldowns on the throw, which can lead to a server crash.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixInfiniteNades extends Feature;
-
-/**
- * It is possible to call 'ServerThrow' function from client,
- * forcing it to get executed on a server. This function consumes the grenade
- * ammo and spawns a nade, but it doesn't check if player had any grenade ammo
- * in the first place, allowing you him to throw however many grenades
- * he wants. Moreover, unlike a regular throwing method, calling this function
- * allows to spawn many grenades without any delay,
- * which can lead to a server crash.
- *
- * This fix tracks every instance of 'Frag' weapon that's responsible for
- * throwing grenades and records how much ammo they have have.
- * This is necessary, because whatever means we use, when we get a say in
- * preventing grenade from spawning the ammo was already reduced.
- * This means that we can't distinguished between a player abusing a bug by
- * throwing grenade when he doesn't have necessary ammo and player throwing
- * his last nade, as in both cases current ammo visible to us will be 0.
- * Then, before every nade throw, it checks if player has enough ammo and
- * blocks grenade from spawning if he doesn't.
- * We change a 'FireModeClass[0]' from 'FragFire' to 'FixedFragFire' and
- * only call 'super.DoFireEffect()' if we decide spawning grenade
- * should be allowed. The side effect is a change in server's 'FireModeClass'.
- */
-
-// Setting this flag to 'true' will allow to throw grenades by calling
-// 'ServerThrow' directly, as long as player has necessary ammo.
-// This can allow some players to throw grenades much quicker than intended,
-// therefore it's suggested to keep this flag set to 'false'.
-var private config const bool ignoreTossFlags;
-
-// Records how much ammo given frag grenade ('Frag') has.
-struct FragAmmoRecord
-{
- var public Frag fragReference;
- var public int amount;
-};
-var private array ammoRecords;
-
-public function OnEnabled()
-{
- local Frag nextFrag;
- // Find all frags, that spawned when this fix wasn't running.
- foreach level.DynamicActors(class'KFMod.Frag', nextFrag)
- {
- RegisterFrag(nextFrag);
- }
- RecreateFrags();
-}
-
-public function OnDisabled()
-{
- RecreateFrags();
- ammoRecords.length = 0;
-}
-
-// Returns index of the connection corresponding to the given controller.
-// Returns '-1' if no connection correspond to the given controller.
-// Returns '-1' if given controller is equal to 'none'.
-private final function int GetAmmoIndex(Frag fragToCheck)
-{
- local int i;
- if (fragToCheck == none) return -1;
-
- for (i = 0; i < ammoRecords.length; i += 1)
- {
- if (ammoRecords[i].fragReference == fragToCheck)
- {
- return i;
- }
- }
- return -1;
-}
-
-// Recreates all the 'Frag' actors, to change their fire mode mid-game.
-private final function RecreateFrags()
-{
- local int i;
- local float maxAmmo, currentAmmo;
- local Frag newFrag;
- local Pawn fragOwner;
- local array oldRecords;
- oldRecords = ammoRecords;
- for (i = 0; i < oldRecords.length; i += 1)
- {
- // Check if we even need to recreate that instance of 'Frag'
- if (oldRecords[i].fragReference == none) continue;
- fragOwner = oldRecords[i].fragReference.instigator;
- if (fragOwner == none) continue;
- // Recreate
- oldRecords[i].fragReference.Destroy();
- fragOwner.CreateInventory("KFMod.Frag");
- newFrag = GetPawnFrag(fragOwner);
- // Restore ammo amount
- if (newFrag != none)
- {
- newFrag.GetAmmoCount(maxAmmo, currentAmmo);
- newFrag.AddAmmo(oldRecords[i].amount - Int(currentAmmo), 0);
- }
- }
-}
-
-// Utility function to help find a 'Frag' instance in a given pawn's inventory.
-static private final function Frag GetPawnFrag(Pawn pawnWithFrag)
-{
- local Frag foundFrag;
- local Inventory invIter;
- if (pawnWithFrag == none) return none;
- invIter = pawnWithFrag.inventory;
- while (invIter != none)
- {
- foundFrag = Frag(invIter);
- if (foundFrag != none)
- {
- return foundFrag;
- }
- invIter = invIter.inventory;
- }
- return none;
-}
-
-// Utility function for extracting current ammo amount from a frag class.
-private final function int GetFragAmmo(Frag fragReference)
-{
- local float maxAmmo;
- local float currentAmmo;
- if (fragReference == none) return 0;
-
- fragReference.GetAmmoCount(maxAmmo, currentAmmo);
- return Int(currentAmmo);
-}
-
-// Attempts to add new 'Frag' instance to our records.
-public final function RegisterFrag(Frag newFrag)
-{
- local int index;
- local FragAmmoRecord newRecord;
- index = GetAmmoIndex(newFrag);
- if (index >= 0) return;
-
- newRecord.fragReference = newFrag;
- newRecord.amount = GetFragAmmo(newFrag);
- ammoRecords[ammoRecords.length] = newRecord;
-}
-
-// This function tells our fix that there was a nade throw and we should
-// reduce current 'Frag' ammo in our records.
-// Returns 'true' if we had ammo for that, and 'false' if we didn't.
-public final function bool RegisterNadeThrow(Frag relevantFrag)
-{
- if (CanThrowGrenade(relevantFrag))
- {
- ReduceGrenades(relevantFrag);
- return true;
- }
- return false;
-}
-
-// Can we throw grenade according to our rules?
-// A throw can be prevented if:
-// - we think that player doesn't have necessary ammo;
-// - Player isn't currently 'tossing' a nade,
-// meaning it was a direct call of 'ServerThrow'.
-private final function bool CanThrowGrenade(Frag fragToCheck)
-{
- local int index;
- // Nothing to check
- if (fragToCheck == none) return false;
- // No ammo
- index = GetAmmoIndex(fragToCheck);
- if (index < 0) return false;
- if (ammoRecords[index].amount <= 0) return false;
- // Not tossing
- if (ignoreTossFlags) return true;
- if (!fragToCheck.bTossActive || fragToCheck.bTossSpawned) return false;
- return true;
-}
-
-// Reduces recorded amount of ammo in our records for the given nade.
-private final function ReduceGrenades(Frag relevantFrag)
-{
- local int index;
- index = GetAmmoIndex(relevantFrag);
- if (index < 0) return;
- ammoRecords[index].amount -= 1;
-}
-
-event Tick(float delta)
-{
- local int i;
- // Update our ammo records with current, correct data.
- i = 0;
- while (i < ammoRecords.length)
- {
- if (ammoRecords[i].fragReference != none)
- {
- ammoRecords[i].amount = GetFragAmmo(ammoRecords[i].fragReference);
- i += 1;
- }
- else
- {
- ammoRecords.Remove(i, 1);
- }
- }
-}
-
-defaultproperties
-{
- ignoreTossFlags = false
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixInfiniteNades'
-}
\ No newline at end of file
diff --git a/sources/Features/FixInfiniteNades/FixedFragFire.uc b/sources/Features/FixInfiniteNades/FixedFragFire.uc
deleted file mode 100644
index e0e356c..0000000
--- a/sources/Features/FixInfiniteNades/FixedFragFire.uc
+++ /dev/null
@@ -1,36 +0,0 @@
- /**
- * A replacement for vanilla 'FragFire' fire class for 'Frag' weapon that
- * adds additional ammo check in accordance to ammo records
- * of 'FixInfiniteNades'.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixedFragFire extends KFMod.FragFire;
-
-function DoFireEffect()
-{
- local FixInfiniteNades nadeFix;
- nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance());
- if (nadeFix == none || nadeFix.RegisterNadeThrow(Frag(weapon)))
- {
- super.DoFireEffect();
- }
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/Features/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc b/sources/Features/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc
deleted file mode 100644
index 29e6e3e..0000000
--- a/sources/Features/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Overloaded mutator events listener to catch
- * new 'Frag' weapons and 'Nade' projectiles.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class MutatorListener_FixInfiniteNades extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local Frag relevantFrag;
- local FixInfiniteNades nadeFix;
- nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance());
- if (nadeFix == none) return true;
-
- // Handle detecting new frag (weapons that allows to throw nades)
- relevantFrag = Frag(other);
- if (relevantFrag != none)
- {
- nadeFix.RegisterFrag(relevantFrag);
- relevantFrag.FireModeClass[0] = class'FixedFragFire';
- return true;
- }
- return true;
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc b/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc
deleted file mode 100644
index 4881386..0000000
--- a/sources/Features/FixInventoryAbuse/FixInventoryAbuse.uc
+++ /dev/null
@@ -1,225 +0,0 @@
-/**
- * This feature addressed two inventory issues:
- * 1. Players carrying amount of weapons that shouldn't be allowed by the
- * weight limit.
- * 2. Players carrying two variants of the same gun.
- * For example carrying both M32 and camo M32.
- * Single and dual version of the same weapon are also considered
- * the same gun, so you can't carry both MK23 and dual MK23 or
- * dual handcannons and golden handcannon.
- *
- * It fixes them by doing repeated checks to find violations of those rules
- * and destroys all droppable weapons of people that use this exploit.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixInventoryAbuse extends Feature;
-
-// How often (in seconds) should we do our inventory validations?
-// We shouldn't really worry about performance, but there's also no need to
-// do this check too often.
-var private config const float checkInterval;
-
-struct DualiesPair
-{
- var class single;
- var class dual;
-};
-// For this fix to properly work, this array must contain an entry for
-// every dual weapon in the game (like pistols, with single and dual versions).
-// It's made configurable in case of custom dual weapons.
-var private config const array dualiesClasses;
-
-public function OnEnabled()
-{
- local float actualInterval;
- actualInterval = checkInterval;
- if (actualInterval <= 0)
- {
- actualInterval = 0.25;
- }
- SetTimer(actualInterval, true);
-}
-
-public function OnDisabled()
-{
- SetTimer(0.0f, false);
-}
-
-// Did player with this controller contribute to the latest dosh generation?
-private final function bool IsWeightLimitViolated(KFHumanPawn playerPawn)
-{
- if (playerPawn == none) return false;
- return (playerPawn.currentWeight > playerPawn.maxCarryWeight);
-}
-
-// Returns a root pickup class.
-// For non-dual weapons, root class is defined as either:
-// 1. the first variant (reskin), if there are variants for that weapon;
-// 2. and as the class itself, if there are no variants.
-// For dual weapons (all dual pistols) root class is defined as
-// a root of their single version.
-// This definition is useful because:
-// ~ Vanilla game rules are such that player can only have two weapons
-// in the inventory if they have different roots;
-// ~ Root is easy to find.
-private final function class GetRootPickupClass(KFWeapon weapon)
-{
- local int i;
- local class root;
- if (weapon == none) return none;
- // Start with a pickup of the given weapons
- root = class(weapon.default.pickupClass);
- if (root == none) return none;
-
- // In case it's a dual version - find corresponding single pickup class
- // (it's root would be the same).
- for (i = 0; i < dualiesClasses.length; i += 1)
- {
- if (dualiesClasses[i].dual == root)
- {
- root = dualiesClasses[i].single;
- break;
- }
- }
- // Take either first variant class or the class itself, -
- // it's going to be root by definition.
- if (root.default.variantClasses.length > 0)
- {
- root = class(root.default.variantClasses[0]);
- }
- return root;
-}
-
-// Returns 'true' if passed pawn has two weapons that are just variants of
-// each other (they have the same root, see 'GetRootPickupClass').
-private final function bool HasDuplicateGuns(KFHumanPawn playerPawn)
-{
- local int i, j;
- local Inventory inv;
- local KFWeapon nextWeapon;
- local class rootClass;
- local array< class > rootList;
- if (playerPawn == none) return false;
-
- // First find a root for every weapon in the pawn's inventory.
- for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
- {
- nextWeapon = KFWeapon(inv);
- if (nextWeapon == none) continue;
- if (nextWeapon.bKFNeverThrow) continue;
- rootClass = GetRootPickupClass(nextWeapon);
- if (rootClass != none)
- {
- rootList[rootList.length] = rootClass;
- }
- }
- // Then just check obtained roots for duplicates.
- for (i = 0; i < rootList.length; i += 1)
- {
- for (j = i + 1; j < rootList.length; j += 1)
- {
- if (rootList[i] == rootList[j])
- {
- return true;
- }
- }
- }
- return false;
-}
-
-private final function Vector DropWeapon(KFWeapon weaponToDrop)
-{
- local Vector x, y, z;
- local Vector weaponVelocity;
- local Vector dropLocation;
- local KFHumanPawn playerPawn;
- if (weaponToDrop == none) return Vect(0, 0, 0);
- playerPawn = KFHumanPawn(weaponToDrop.instigator);
- if (playerPawn == none) return Vect(0, 0, 0);
-
- // Calculations from 'PlayerController.ServerThrowWeapon'
- weaponVelocity = Vector(playerPawn.GetViewRotation());
- weaponVelocity *= (playerPawn.velocity dot weaponVelocity) + 150;
- weaponVelocity += Vect(0, 0, 100);
- // Calculations from 'Pawn.TossWeapon'
- GetAxes(playerPawn.rotation, x, y, z);
- dropLocation = playerPawn.location + 0.8 * playerPawn.collisionRadius * x -
- 0.5 * playerPawn.collisionRadius * y;
- // Do the drop
- weaponToDrop.velocity = weaponVelocity;
- weaponToDrop.DropFrom(dropLocation);
-}
-
-// Kill the gun devil!
-private final function DropEverything(KFHumanPawn playerPawn)
-{
- local int i;
- local Inventory inv;
- local KFWeapon nextWeapon;
- local array weaponList;
- if (playerPawn == none) return;
- // Going through the linked list while removing items can be tricky,
- // so just find all weapons first.
- for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
- {
- nextWeapon = KFWeapon(inv);
- if (nextWeapon == none) continue;
- if (nextWeapon.bKFNeverThrow) continue;
- weaponList[weaponList.length] = nextWeapon;
- }
- // And destroy them later.
- for(i = 0; i < weaponList.length; i += 1)
- {
- DropWeapon(weaponList[i]);
- }
-}
-
-event Timer()
-{
- local int i;
- local KFHumanPawn nextPawn;
- local ConnectionService service;
- local array connections;
- service = ConnectionService(class'ConnectionService'.static.GetInstance());
- if (service == none) return;
-
- connections = service.GetActiveConnections();
- for (i = 0; i < connections.length; i += 1)
- {
- nextPawn = none;
- if (connections[i].controllerReference != none)
- {
- nextPawn = KFHumanPawn(connections[i].controllerReference.pawn);
- }
- if (IsWeightLimitViolated(nextPawn) || HasDuplicateGuns(nextPawn))
- {
- DropEverything(nextPawn);
- }
- }
-}
-
-defaultproperties
-{
- checkInterval = 0.25
- dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup')
- dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup')
- dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup')
- dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup')
- dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup')
- dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup')
-}
\ No newline at end of file
diff --git a/sources/Features/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc b/sources/Features/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc
deleted file mode 100644
index 297dc5b..0000000
--- a/sources/Features/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Overloaded broadcast events listener to catch the moment
- * someone becomes alive player / spectator.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class BroadcastListener_FixSpectatorCrash extends BroadcastListenerBase
- abstract;
-
-var private const int becomeAlivePlayerID;
-var private const int becomeSpectatorID;
-
-static function bool HandleLocalized
-(
- Actor sender,
- BroadcastEvents.LocalizedMessage message
-)
-{
- local FixSpectatorCrash specFix;
- local PlayerController senderController;
- if (sender == none) return true;
- if (sender.level == none || sender.level.game == none) return true;
- if (message.class != sender.level.game.gameMessageClass) return true;
- if ( message.id != default.becomeAlivePlayerID
- && message.id != default.becomeSpectatorID) return true;
-
- specFix = FixSpectatorCrash(class'FixSpectatorCrash'.static.GetInstance());
- senderController = GetController(sender);
- specFix.NotifyStatusChange(senderController);
- return (!specFix.IsViolator(senderController));
-}
-
-defaultproperties
-{
- becomeAlivePlayerID = 1
- becomeSpectatorID = 14
-}
\ No newline at end of file
diff --git a/sources/Features/FixSpectatorCrash/FixSpectatorCrash.uc b/sources/Features/FixSpectatorCrash/FixSpectatorCrash.uc
deleted file mode 100644
index f843056..0000000
--- a/sources/Features/FixSpectatorCrash/FixSpectatorCrash.uc
+++ /dev/null
@@ -1,291 +0,0 @@
-/**
- * This feature attempts to prevent server crashes caused by someone
- * quickly switching between being spectator and an active player.
- *
- * We do so by disconnecting players who start switching way too fast
- * (more than twice in a short amount of time) and temporarily faking a large
- * amount of players on the server, to prevent such spam from affecting the server.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixSpectatorCrash extends Feature
- dependson(ConnectionService);
-
-/**
- * We use broadcast events to track when someone is switching
- * to active player or spectator and remember such people
- * for a short time (cooldown), defined by ('spectatorChangeTimeout').
- * If one of the player we've remembered tries to switch again,
- * before the defined cooldown ran out, - we kick him
- * by destroying his controller.
- * One possible problem arises from the fact that controllers aren't
- * immediately destroyed and instead initiate player disconnection, -
- * exploiter might have enough time to cause a lag or even crash the server.
- * We address this issue by temporarily blocking anyone from
- * becoming active player (we do this by setting 'numPlayers' variable in
- * killing floor's game info to a large value).
- * After all malicious players have successfully disconnected, -
- * we remove the block.
- */
-
-// This fix will try to kick any player that switches between active player
-// and cooldown faster than time (in seconds) in this value.
-// NOTE: raising this value past default value of '0.25'
-// won't actually improve crash prevention.
-var private config const float spectatorChangeTimeout;
-
-// [ADVANCED] Don't change this setting unless you know what you're doing.
-// Allows you to turn off server blocking.
-// Players that don't respect timeout will still be kicked.
-// This might be needed if this fix conflicts with another mutator
-// that also changes 'numPlayers'.
-// However, it is necessary to block aggressive enough server crash attempts,
-// but can cause compatibility issues with some mutators.
-// It's highly preferred to rewrite such a mutator to be compatible.
-// NOTE: it should be compatible with most faked players-type mutators,
-// since this fix remembers the difference between amount of
-// real players and 'numPlayers'.
-// After unblocking, it sets 'numPlayers' to
-// the current amount of real players + that difference.
-// So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes
-// 3 players + 3 (=6 numPlayers).
-var private config const bool allowServerBlock;
-
-// Stores remaining cooldown value before the next allowed
-// spectator change per player.
-struct CooldownRecord
-{
- var PlayerController player;
- var float cooldown;
-};
-
-// Currently active cooldowns
-var private array currentCooldowns;
-
-// Players who were decided to be violators and
-// were marked for disconnecting.
-// We'll be maintaining server block as long as even one
-// of them hasn't yet disconnected.
-var private array violators;
-
-// Is server currently blocked?
-var private bool becomingActiveBlocked;
-// This value introduced to accommodate mods such as faked player that can
-// change 'numPlayers' to a value that isn't directly tied to the
-// current number of active players.
-// We remember the difference between active players and 'numPlayers'
-/// variable in game type before server block and add it after block is over.
-// If some mod introduces a more complicated relation between amount of
-// active players and 'numPlayers', then it must take care of
-// compatibility on it's own.
-var private int recordedNumPlayersMod;
-
-// If given 'PlayerController' is registered in our cooldown records, -
-// returns it's index.
-// If it doesn't exists (or 'none' value was passes), - returns '-1'.
-private final function int GetCooldownIndex(PlayerController player)
-{
- local int i;
- if (player == none) return -1;
-
- for (i = 0; i < currentCooldowns.length; i += 1)
- {
- if (currentCooldowns[i].player == player)
- {
- return i;
- }
- }
- return -1;
-}
-
-// Checks if given 'PlayerController' is registered as a violator.
-// 'none' value isn't a violator.
-public final function bool IsViolator(PlayerController player)
-{
- local int i;
- if (player == none) return false;
-
- for (i = 0; i < violators.length; i += 1)
- {
- if (violators[i] == player)
- {
- return true;
- }
- }
- return false;
-}
-
-// This function is to notify our fix that some player just changed status
-// of active player / spectator.
-// If passes value isn't 'none', it puts given player on cooldown or kicks him.
-public final function NotifyStatusChange(PlayerController player)
-{
- local int index;
- local CooldownRecord newRecord;
- if (player == none) return;
-
- index = GetCooldownIndex(player);
- // Players already on cool down must be kicked and marked as violators
- if (index >= 0)
- {
- player.Destroy();
- currentCooldowns.Remove(index, 1);
- violators[violators.length] = player;
- if (allowServerBlock)
- {
- SetBlock(true);
- }
- }
- // Players that aren't on cooldown are
- // either violators (do nothing, just wait for their disconnect)
- // or didn't recently change their status (put them on cooldown).
- else if (!IsViolator(player))
- {
- newRecord.player = player;
- newRecord.cooldown = spectatorChangeTimeout;
- currentCooldowns[currentCooldowns.length] = newRecord;
- }
-}
-
-// Pass 'true' to block server, 'false' to unblock.
-// Only works if 'allowServerBlock' is set to 'true'.
-private final function SetBlock(bool activateBlock)
-{
- local KFGameType kfGameType;
- // Do we even need to do anything?
- if (!allowServerBlock) return;
- if (activateBlock == becomingActiveBlocked) return;
- // Only works with 'KFGameType' and it's children.
- if (level != none) kfGameType = KFGameType(level.game);
- if (kfGameType == none) return;
-
- // Actually block/unblock
- becomingActiveBlocked = activateBlock;
- if (activateBlock)
- {
- recordedNumPlayersMod = GetNumPlayersMod();
- // This value both can't realistically fall below
- // 'kfGameType.maxPlayer' and won't overflow from random increase
- // in vanilla code.
- kfGameType.numPlayers = maxInt / 2;
- }
- else
- {
- // Adding 'recordedNumPlayersMod' should prevent
- // faked players from breaking.
- kfGameType.numPlayers = GetRealPlayers() + recordedNumPlayersMod;
- }
-}
-
-// Performs server blocking if violators have disconnected.
-private final function TryUnblocking()
-{
- local int i;
- if (!allowServerBlock) return;
- if (!becomingActiveBlocked) return;
-
- for (i = 0; i < violators.length; i += 1)
- {
- if (violators[i] != none)
- {
- return;
- }
- }
- SetBlock(false);
-}
-
-// Counts current amount of "real" active players
-// (connected to the server and not spectators).
-// Need 'ConnectionService' to be running, otherwise return '-1'.
-private final function int GetRealPlayers()
-{
- // Auxiliary variables
- local int i;
- local int realPlayersAmount;
- local PlayerController player;
- // Information extraction
- local ConnectionService service;
- local array connections;
- service = ConnectionService(class'ConnectionService'.static.GetInstance());
- if (service == none) return -1;
-
- // Count non-spectators
- connections = service.GetActiveConnections();
- realPlayersAmount = 0;
- for (i = 0; i < connections.length; i += 1)
- {
- player = connections[i].controllerReference;
- if (player == none) continue;
- if (player.playerReplicationInfo == none) continue;
- if (!player.playerReplicationInfo.bOnlySpectator)
- {
- realPlayersAmount += 1;
- }
- }
- return realPlayersAmount;
-}
-
-// Calculates difference between current amount of "real" active players
-// and 'numPlayers' from 'KFGameType'.
-// Most typically this difference will be non-zero when using
-// faked players-type mutators
-// (difference will be equal to the amount of faked players).
-private final function int GetNumPlayersMod()
-{
- local KFGameType kfGameType;
- if (level != none) kfGameType = KFGameType(level.game);
- if (kfGameType == none) return 0;
- return kfGameType.numPlayers - GetRealPlayers();
-}
-
-private final function ReduceCooldowns(float timePassed)
-{
- local int i;
- i = 0;
- while (i < currentCooldowns.length)
- {
- currentCooldowns[i].cooldown -= timePassed;
- if ( currentCooldowns[i].player != none
- && currentCooldowns[i].cooldown > 0.0)
- {
- i += 1;
- }
- else
- {
- currentCooldowns.Remove(i, 1);
- }
- }
-}
-
-event Tick(float delta)
-{
- local float trueTimePassed;
- trueTimePassed = delta * (1.1 / level.timeDilation);
- TryUnblocking();
- ReduceCooldowns(trueTimePassed);
-}
-
-defaultproperties
-{
- // Configurable variables
- spectatorChangeTimeout = 0.25
- allowServerBlock = true
- // Inner variables
- becomingActiveBlocked = false
- // Listeners
- requiredListeners(0) = class'BroadcastListener_FixSpectatorCrash'
-}
\ No newline at end of file
diff --git a/sources/Features/FixZedTimeLags/FixZedTimeLags.uc b/sources/Features/FixZedTimeLags/FixZedTimeLags.uc
deleted file mode 100644
index 61e1092..0000000
--- a/sources/Features/FixZedTimeLags/FixZedTimeLags.uc
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * This feature fixes lags caused by a zed time that can occur
- * on some maps when a lot of zeds are present at once.
- * As a side effect it also fixes an issue where during zed time speed up
- * 'zedTimeSlomoScale' was assumed to be default value of '0.2'.
- * Now zed time will behave correctly with mods that
- * change 'zedTimeSlomoScale'.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class FixZedTimeLags extends Feature
- dependson(ConnectionService);
-
-/**
- * When zed time activates, game speed is immediately set to
- * 'zedTimeSlomoScale' (0.2 by default), defined, like all other variables,
- * in 'KFGameType'. Zed time lasts 'zedTimeDuration' seconds (3.0 by default),
- * but during last 'zedTimeDuration * 0.166' seconds (by default 0.498)
- * it starts to speed back up, causing game speed to update every tick.
- * This makes animations look more smooth when exiting zed-time;
- * however, updating speed every tick for that purpose seems like
- * an overkill and, combined with things like
- * increased tick rate, certain maps and raised zed limit,
- * it can lead to noticeable lags at the end of zed time.
- * To fix this issue we disable 'Tick' event in
- * 'KFGameType' and then repeat that functionality in our own 'Tick' event,
- * but only perform game speed updates occasionally,
- * to make sure that overall amount of updates won't go over a limit,
- * that can be configured via 'maxGameSpeedUpdatesAmount'
- * Author's test (looking really hard on clots' animations)
- * seem to suggest that there shouldn't be much visible difference if
- * we limit game speed updates to about 2 or 3.
- */
-
-// Max amount of game speed updates during speed up phase
-// (actual amount of updates can't be larger than amount of ticks).
-// On servers with default 30 tick rate there's usually
-// about 13 updates total on vanilla game.
-// Values lower than 1 are treated like 1.
-var private config const int maxGameSpeedUpdatesAmount;
-// [ADVANCED] Don't change this setting unless you know what you're doing.
-// Compatibility setting that allows to keep 'GameInfo' 's 'Tick' event
-// from being disabled.
-// Useful when running Acedia along with custom 'GameInfo'
-// (that isn't 'KFGameType') that relies on 'Tick' event.
-// Note, however, that in order to keep this fix working properly,
-// it's on you to make sure 'KFGameType.Tick()' logic isn't executed.
-var private config const bool disableTick;
-// Counts how much time is left until next update
-var private float updateCooldown;
-// Recorded game type, to avoid constant conversions every tick
-var private KFGameType gameType;
-
-public function OnEnabled()
-{
- gameType = KFGameType(level.game);
- if (gameType == none)
- {
- Destroy();
- }
- else if (disableTick)
- {
- gameType.Disable('Tick');
- }
-}
-
-public function OnDisabled()
-{
- gameType = KFGameType(level.game);
- if (gameType != none && disableTick)
- {
- gameType.Enable('Tick');
- }
-}
-
-event Tick(float delta)
-{
- local float trueTimePassed;
- if (gameType == none) return;
- if (!gameType.bZEDTimeActive) return;
- // Unfortunately we need to keep disabling 'Tick' probe function,
- // because it constantly gets enabled back and I don't know where
- // (maybe native code?); only really matters during zed time.
- if (disableTick)
- {
- gameType.Disable('Tick');
- }
- // How much real (not in-game) time has passed
- trueTimePassed = delta * (1.1 / level.timeDilation);
- gameType.currentZEDTimeDuration -= trueTimePassed;
-
- // Handle speeding up phase
- if (gameType.bSpeedingBackUp)
- {
- DoSpeedBackUp(trueTimePassed);
- }
- else if (gameType.currentZEDTimeDuration < GetSpeedupDuration())
- {
- gameType.bSpeedingBackUp = true;
- updateCooldown = GetFullUpdateCooldown();
- TellClientsZedTimeEnds();
- DoSpeedBackUp(trueTimePassed);
- }
- // End zed time once it's duration has passed
- if (gameType.currentZEDTimeDuration <= 0)
- {
- gameType.bZEDTimeActive = false;
- gameType.bSpeedingBackUp = false;
- gameType.zedTimeExtensionsUsed = 0;
- gameType.SetGameSpeed(1.0);
- }
-}
-
-private final function TellClientsZedTimeEnds()
-{
- local int i;
- local KFPlayerController player;
- local ConnectionService service;
- local array connections;
- service = ConnectionService(class'ConnectionService'.static.GetInstance());
- if (service == none) return;
- connections = service.GetActiveConnections();
- for (i = 0; i < connections.length; i += 1)
- {
- player = KFPlayerController(connections[i].controllerReference);
- if (player != none)
- {
- // Play sound of leaving zed time
- player.ClientExitZedTime();
- }
- }
-}
-
-// This function is called every tick during speed up phase and manages
-// gradual game speed increase.
-private final function DoSpeedBackUp(float trueTimePassed)
-{
- // Game speed will always be updated in our 'Tick' event
- // at the very end of the zed time.
- // The rest of the updates will be uniformly distributed
- // over the speed up duration.
-
- local float newGameSpeed;
- local float slowdownScale;
- if (maxGameSpeedUpdatesAmount <= 1) return;
- if (updateCooldown > 0.0)
- {
- updateCooldown -= trueTimePassed;
- return;
- }
- else
- {
- updateCooldown = GetFullUpdateCooldown();
- }
- slowdownScale = gameType.currentZEDTimeDuration / GetSpeedupDuration();
- newGameSpeed = Lerp(slowdownScale, 1.0, gameType.zedTimeSlomoScale);
- gameType.SetGameSpeed(newGameSpeed);
-}
-
-private final function float GetSpeedupDuration()
-{
- return gameType.zedTimeDuration * 0.166;
-}
-
-private final function float GetFullUpdateCooldown()
-{
- return GetSpeedupDuration() / maxGameSpeedUpdatesAmount;
-}
-
-defaultproperties
-{
- maxGameSpeedUpdatesAmount = 3
- disableTick = true
-}
\ No newline at end of file
diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc
new file mode 100644
index 0000000..cb7d8fe
--- /dev/null
+++ b/sources/GameModes/BaseGameMode.uc
@@ -0,0 +1,400 @@
+/**
+ * Base class for a game mode config, contains all the information Acedia's
+ * game modes must have, including settings
+ * (`includeFeature`, `includeFeatureAs` and `excludeFeature`)
+ * for picking used `Feature`s.
+ *
+ * Contains three types of methods:
+ * 1. Getters for its values;
+ * 2. `UpdateFeatureArray()` method for updating list of `Feature`s to
+ * be used based on game info's settings;
+ * 3. `Report...()` methods that perform various validation checks
+ * (and log them) on config data.
+ * Copyright 2021-2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class BaseGameMode extends AcediaConfig
+ dependson(Packages)
+ abstract;
+
+// Name of the game mode players will see in voting (formatted string)
+var protected config string title;
+// Preferable difficulty level (plain string)
+var protected config string difficulty;
+// `Mutator`s to add with this game mode
+var protected config array includeMutator;
+// `Feature`s to include (with "default" config)
+var protected config array includeFeature;
+// `Feature`s to exclude from game mode, regardless of other settings
+// (this one has highest priority)
+var protected config array excludeFeature;
+
+struct FeatureConfigPair
+{
+ var public string feature;
+ var public string config;
+};
+// `Feature`s to include (with specified config).
+// Higher priority than `includeFeature`, but lower than `excludeFeature`.
+var protected config array includeFeatureAs;
+
+var private LoggerAPI.Definition warnBadMutatorName, warnBadFeatureName;
+
+protected function HashTable ToData()
+{
+ local int i;
+ local HashTable result;
+ local HashTable nextPair;
+ local ArrayList nextArray;
+
+ result = _.collections.EmptyHashTable();
+ result.SetFormattedString(P("title"), title);
+ result.SetString(P("difficulty"), difficulty);
+ nextArray = _.collections.EmptyArrayList();
+ for (i = 0; i < includeFeature.length; i += 1) {
+ nextArray.AddString(includeFeature[i]);
+ }
+ result.SetItem(P("includeFeature"), nextArray);
+ _.memory.Free(nextArray);
+ nextArray = _.collections.EmptyArrayList();
+ for (i = 0; i < excludeFeature.length; i += 1) {
+ nextArray.AddItem(_.text.FromString(excludeFeature[i]));
+ }
+ result.SetItem(P("excludeFeature"), nextArray);
+ _.memory.Free(nextArray);
+ nextArray = _.collections.EmptyArrayList();
+ for (i = 0; i < includeMutator.length; i += 1) {
+ nextArray.AddItem(_.text.FromString(includeFeature[i]));
+ }
+ result.SetItem(P("includeMutator"), nextArray);
+ _.memory.Free(nextArray);
+ nextArray = _.collections.EmptyArrayList();
+ for (i = 0; i < includeFeatureAs.length; i += 1)
+ {
+ nextPair = _.collections.EmptyHashTable();
+ nextPair.SetString(P("feature"), includeFeatureAs[i].feature);
+ nextPair.SetString(P("config"), includeFeatureAs[i].config);
+ nextArray.AddItem(nextPair);
+ _.memory.Free(nextPair);
+ }
+ result.SetItem(P("includeFeatureAs"), nextArray);
+ _.memory.Free(nextArray);
+ return result;
+}
+
+protected function FromData(HashTable source)
+{
+ local int i;
+ local ArrayList nextArray;
+ local HashTable nextPair;
+
+ if (source == none) {
+ return;
+ }
+ title = source.GetFormattedString(P("title"));
+ title = source.GetString(P("title"));
+ nextArray = source.GetArrayList(P("includeFeature"));
+ includeFeature = DynamicIntoStringArray(nextArray);
+ _.memory.Free(nextArray);
+ nextArray = source.GetArrayList(P("excludeFeature"));
+ excludeFeature = DynamicIntoStringArray(nextArray);
+ _.memory.Free(nextArray);
+ nextArray = source.GetArrayList(P("includeMutator"));
+ includeMutator = DynamicIntoStringArray(nextArray);
+ _.memory.Free(nextArray);
+ nextArray = source.GetArrayList(P("includeFeatureAs"));
+ if (nextArray == none) {
+ return;
+ }
+ includeFeatureAs.length = 0;
+ for (i = 0; i < nextArray.GetLength(); i += 1)
+ {
+ nextPair = nextArray.GetHashTable(i);
+ includeFeatureAs[i] = HashTableIntoPair(nextPair);
+ _.memory.Free(nextPair);
+ }
+ _.memory.Free(nextArray);
+}
+
+private final function FeatureConfigPair HashTableIntoPair(HashTable source)
+{
+ local Text nextText;
+ local FeatureConfigPair result;
+
+ if (source == none) {
+ return result;
+ }
+ nextText = source.GetText(P("feature"));
+ if (nextText != none) {
+ result.feature = nextText.ToString();
+ }
+ nextText = source.GetText(P("config"));
+ if (nextText != none) {
+ result.config = nextText.ToString();
+ }
+ return result;
+}
+
+private final function array DynamicIntoStringArray(ArrayList source)
+{
+ local int i;
+ local Text nextText;
+ local array result;
+
+ if (source == none) {
+ return result;
+ }
+ for (i = 0; i < source.GetLength(); i += 1)
+ {
+ nextText = source.GetText(i);
+ if (nextText != none) {
+ includeFeature[i] = nextText.ToString();
+ }
+ }
+}
+
+protected function array StringToTextArray(array input)
+{
+ local int i;
+ local array result;
+
+ for (i = 0; i < input.length; i += 1) {
+ result[i] = _.text.FromString(input[i]);
+ }
+ return result;
+}
+
+/**
+ * @return Name of the `GameInfo` class to be used with the caller game mode.
+ */
+public function Text GetGameTypeClass()
+{
+ return none;
+}
+
+/**
+ * @return Human-readable name of the caller game mode.
+ * Players will see it as the name of the mode in the voting options.
+ */
+public function Text GetTitle()
+{
+ return _.text.FromFormattedString(title);
+}
+
+/**
+ * @return Specified difficulty for the game mode.
+ * Interpretation of this value can depend on each particular game mode.
+ */
+public function Text GetDifficulty()
+{
+ return _.text.FromString(difficulty);
+}
+
+/**
+ * Checks `Feature`-related settings (`includeFeature`, `includeFeatureAs` and
+ * `excludeFeature`) for correctness and reports any issues.
+ * Currently correctness check simply ensures that all listed `Feature`s
+ * actually exist.
+ */
+public function ReportIncorrectSettings(
+ array featuresToEnable)
+{
+ local int i;
+ local array featureNames, featuresToReplace;
+
+ for (i = 0; i < featuresToEnable.length; i += 1) {
+ featureNames[i] = string(featuresToEnable[i].featureClass);
+ }
+ ValidateFeatureArray(includeFeature, featureNames, "includeFeatures");
+ ValidateFeatureArray(excludeFeature, featureNames, "excludeFeatures");
+ for (i = 0; i < includeFeatureAs.length; i += 1) {
+ featuresToReplace[i] = includeFeatureAs[i].feature;
+ }
+ ValidateFeatureArray(featuresToReplace, featureNames, "includeFeatureAs");
+}
+
+/**
+ * Checks `Mutator`-related settings (`includeMutator`) for correctness and
+ * reports any issues.
+ * Currently correctness check performs a simple validity check for mutator,
+ * to make sure it would not define a new option in server's URL.
+ *
+ * See `ValidateServerURLName()` for more information.
+ */
+public function ReportBadMutatorNames()
+{
+ local int i;
+
+ for (i = 0; i < includeMutator.length; i += 1)
+ {
+ if (!ValidateServerURLName(includeMutator[i]))
+ {
+ _.logger.Auto(warnBadMutatorName)
+ .Arg(_.text.FromString(includeMutator[i]))
+ .Arg(_.text.FromString(string(name)));
+ }
+ }
+}
+
+/**
+ * Makes sure that a word to be used in server URL as a part of an option
+ * does not contain "," / "?" / "=" or whitespace.
+ * This is useful to make sure that user-specified mutator entries only add
+ * one mutator or option's key / values will not specify only one pair,
+ * avoiding "?opt1=value1?opt2=value2" entries.
+ */
+protected function bool ValidateServerURLName(string entry)
+{
+ if (InStr(entry, "=") >= 0) return false;
+ if (InStr(entry, "?") >= 0) return false;
+ if (InStr(entry, ",") >= 0) return false;
+ if (InStr(entry, " ") >= 0) return false;
+ return true;
+}
+
+// Is every element `subset` present inside `whole`?
+private function ValidateFeatureArray(
+ array subset,
+ array whole,
+ string arrayName)
+{
+ local int i, j;
+ local bool foundItem;
+
+ for (i = 0; i < subset.length; i += 1)
+ {
+ foundItem = false;
+ for (j = 0; j < whole.length; j += 1)
+ {
+ if (subset[i] ~= whole[j])
+ {
+ foundItem = true;
+ break;
+ }
+ }
+ if (!foundItem)
+ {
+ _.logger.Auto(warnBadFeatureName)
+ .Arg(_.text.FromString(includeMutator[i]))
+ .Arg(_.text.FromString(string(name)))
+ .Arg(_.text.FromString(arrayName));
+ }
+ }
+}
+
+/**
+ * Updates passed `Feature` settings according to this game mode's settings.
+ *
+ * @param featuresToEnable Settings to update.
+ * `FeatureConfigPair` is a pair of `Feature` (`featureClass`) and its
+ * config's name (`configName`).
+ * If `configName` is set to `none`, then corresponding `Feature`
+ * should not be enabled.
+ * Otherwise it should be enabled with a specified config.
+ */
+public function UpdateFeatureArray(
+ out array featuresToEnable)
+{
+ local int i;
+ local Text newConfigName;
+ local string nextFeatureClassName;
+
+ for (i = 0; i < featuresToEnable.length; i += 1)
+ {
+ nextFeatureClassName = string(featuresToEnable[i].featureClass);
+ // `excludeFeature`
+ if (FeatureExcluded(nextFeatureClassName))
+ {
+ _.memory.Free(featuresToEnable[i].configName);
+ featuresToEnable[i].configName = none;
+ continue;
+ }
+ // `includeFeatureAs`
+ newConfigName = TryReplacingFeatureConfig(nextFeatureClassName);
+ if (newConfigName != none)
+ {
+ _.memory.Free(featuresToEnable[i].configName);
+ featuresToEnable[i].configName = newConfigName;
+ }
+ // `includeFeature`
+ if ( featuresToEnable[i].configName == none
+ && FeatureInIncludedArray(nextFeatureClassName))
+ {
+ featuresToEnable[i].configName = P("default").Copy();
+ }
+ }
+}
+
+private function bool FeatureExcluded(string featureClassName)
+{
+ local int i;
+
+ for (i = 0; i < excludeFeature.length; i += 1)
+ {
+ if (excludeFeature[i] ~= featureClassName) {
+ return true;
+ }
+ }
+ return false;
+}
+
+private function Text TryReplacingFeatureConfig(string featureClassName)
+{
+ local int i;
+
+ for (i = 0; i < includeFeatureAs.length; i += 1)
+ {
+ if (includeFeatureAs[i].feature ~= featureClassName) {
+ return _.text.FromString(includeFeatureAs[i].config);
+ }
+ }
+ return none;
+}
+
+private function bool FeatureInIncludedArray(string featureClassName)
+{
+ local int i;
+
+ for (i = 0; i < includeFeature.length; i += 1)
+ {
+ if (includeFeature[i] ~= featureClassName) {
+ return true;
+ }
+ }
+ return false;
+}
+
+public function array GetIncludedMutators()
+{
+ local int i;
+ local array validatedMutators;
+
+ for (i = 0; i < includeMutator.length; i += 1)
+ {
+ if (ValidateServerURLName(includeMutator[i])) {
+ validatedMutators[validatedMutators.length] = includeMutator[i];
+ }
+ }
+ return StringToTextArray(validatedMutators);
+}
+
+defaultproperties
+{
+ configName = "AcediaGameModes"
+ warnBadMutatorName = (l=LOG_Warning,m="Mutator \"%1\" specified for game mode \"%2\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.")
+ warnBadFeatureName = (l=LOG_Warning,m="Feature \"%1\" specified for game mode \"%2\" in array `%3` does not exist in enabled packages and will be ignored. This is a configuration error, you should fix it.")
+}
\ No newline at end of file
diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc
new file mode 100644
index 0000000..e314ec3
--- /dev/null
+++ b/sources/GameModes/GameMode.uc
@@ -0,0 +1,195 @@
+/**
+ * The only implementation for `BaseGameMode` suitable for standard
+ * killing floor game types.
+ * Copyright 2021-2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class GameMode extends BaseGameMode
+ perobjectconfig
+ config(AcediaGameModes);
+
+struct GameOption
+{
+ var public string key;
+ var public string value;
+};
+// Allow to specify additional server options for this game mode
+var protected config array option;
+// Specify `GameInfo`'s class to use, default is "KFMod.KFGameType"
+// (plain string)
+var protected config string gameTypeClass;
+// Short version of the name of the game mode players will see in
+// voting handler messages sometimes (plain string)
+var protected config string acronym;
+// Map prefix - only maps that start with specified prefix will be voteable for
+// this game mode (plain string)
+var protected config string mapPrefix;
+
+var private LoggerAPI.Definition warnBadOption;
+
+protected function DefaultIt()
+{
+ title = "Acedia game mode";
+ difficulty = "Hell On Earth";
+ gameTypeClass = "KFMod.KFGameType";
+ acronym = "";
+ mapPrefix = "KF";
+ includeFeature.length = 0;
+ excludeFeature.length = 0;
+ includeMutator.length = 0;
+ option.length = 0;
+}
+
+protected function HashTable ToData()
+{
+ local int i;
+ local ArrayList nextArray;
+ local HashTable result, nextPair;
+
+ result = super.ToData();
+ if (result == none) {
+ return none;
+ }
+ result.SetString(P("gameTypeClass"), gameTypeClass);
+ result.SetString(P("acronym"), acronym);
+ result.SetString(P("mapPrefix"), mapPrefix);
+ nextArray = _.collections.EmptyArrayList();
+ for (i = 0; i < option.length; i += 1)
+ {
+ nextPair = _.collections.EmptyHashTable();
+ nextPair.SetString(P("key"), option[i].key);
+ nextPair.SetString(P("value"), option[i].value);
+ nextArray.AddItem(nextPair);
+ _.memory.Free(nextPair);
+ }
+ result.SetItem(P("option"), nextArray);
+ _.memory.Free(nextArray);
+ return result;
+}
+
+protected function FromData(HashTable source)
+{
+ local int i;
+ local GameOption nextGameOption;
+ local ArrayList nextArray;
+ local HashTable nextPair;
+
+ super.FromData(source);
+ if (source == none) {
+ return;
+ }
+ gameTypeClass = source.GetString(P("gameTypeClass"));
+ acronym = source.GetString(P("acronym"));
+ mapPrefix = source.GetString(P("mapPrefix"));
+ nextArray = source.GetArrayList(P("option"));
+ if (nextArray == none) {
+ return;
+ }
+ option.length = 0;
+ for (i = 0; i < nextArray.GetLength(); i += 1)
+ {
+ nextPair = HashTable(nextArray.GetItem(i));
+ if (nextPair == none) {
+ continue;
+ }
+ nextGameOption.key = nextPair.GetString(P("key"));
+ nextGameOption.value = nextPair.GetString(P("value"));
+ option[option.length] = nextGameOption;
+ _.memory.Free(nextPair);
+ }
+ _.memory.Free(nextArray);
+}
+
+public function Text GetGameTypeClass()
+{
+ if (gameTypeClass == "") {
+ return P("KFMod.KFGameType").Copy();
+ }
+ else {
+ return _.text.FromString(gameTypeClass);
+ }
+}
+
+public function Text GetAcronym()
+{
+ if (acronym == "") {
+ return _.text.FromString(string(name));
+ }
+ else {
+ return _.text.FromString(acronym);
+ }
+}
+
+public function Text GetMapPrefix()
+{
+ if (acronym == "") {
+ return _.text.FromString("KF-");
+ }
+ else {
+ return _.text.FromString(mapPrefix);
+ }
+}
+
+/**
+ * Checks option-related settings (`option`) for correctness and reports
+ * any issues.
+ * Currently correctness check performs a simple validity check for mutator,
+ * to make sure it would not define a new option in server's URL.
+ *
+ * See `ValidateServerURLName()` in `BaseGameMode` for more information.
+ */
+public function ReportBadOptions()
+{
+ local int i;
+
+ for (i = 0; i < option.length; i += 1)
+ {
+ if ( !ValidateServerURLName(option[i].key)
+ || !ValidateServerURLName(option[i].value))
+ {
+ _.logger.Auto(warnBadOption)
+ .Arg(_.text.FromString(option[i].key))
+ .Arg(_.text.FromString(option[i].value))
+ .Arg(_.text.FromString(string(name)));
+ }
+ }
+}
+
+/**
+ * @return Server options as key-value pairs in an `HashTable`.
+ */
+public function HashTable GetOptions()
+{
+ local int i;
+ local HashTable result;
+
+ result = _.collections.EmptyHashTable();
+ for (i = 0; i < option.length; i += 1)
+ {
+ if (!ValidateServerURLName(option[i].key)) continue;
+ if (!ValidateServerURLName(option[i].value)) continue;
+ result.SetItem( _.text.FromString(option[i].key),
+ _.text.FromString(option[i].value));
+ }
+ return result;
+}
+
+defaultproperties
+{
+ configName = "AcediaGameModes"
+ warnBadOption = (l=LOG_Warning,m="Option with key \"%1\" and value \"%2\" specified for game mode \"%3\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.")
+}
\ No newline at end of file
diff --git a/sources/Listener.uc b/sources/Listener.uc
deleted file mode 100644
index b5f0fe6..0000000
--- a/sources/Listener.uc
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * One of the two classes that make up a core of event system in Acedia.
- *
- * 'Listener' (or it's child) class shouldn't be instantiated.
- * Usually module would provide '...ListenerBase' class that defines
- * certain set of static functions, corresponding to events it can listen to.
- * In order to handle those events you must create it's child class and
- * override said functions. But they will only be called if
- * 'SetActive(true)' is called for that child class.
- * To create you own '...ListenerBase' class you need to define
- * a static function for each event you wish it to catch and
- * set 'relatedEvents' variable to point at the 'Events' class
- * that will generate your events.
- * For concrete example look at
- * 'ConnectionEvents' and 'ConnectionListenerBase'.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class Listener extends Object
- abstract;
-
-var public const class relatedEvents;
-
-
-static public final function SetActive(bool active)
-{
- if (active)
- {
- default.relatedEvents.static.ActivateListener(default.class);
- }
- else
- {
- default.relatedEvents.static.DeactivateListener(default.class);
- }
-}
-
-static public final function IsActive(bool active)
-{
- default.relatedEvents.static.IsActiveListener(default.class);
-}
-
-defaultproperties
-{
- relatedEvents = class'Events'
-}
\ No newline at end of file
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
deleted file mode 100644
index 535e92a..0000000
--- a/sources/Manifest.uc
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Manifest is meant to describe contents of the package (mutator file)
- * as well as what actors/objects should be automatically created when package
- * is loaded and what event listeners should be activated.
- * Currently only implements automatic listener activation.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
- class Manifest extends Object
- abstract;
-
-// List of features in this manifest's package.
-var public const array< class > features;
-
-// Listeners listed here will be automatically activated.
-var public const array< class > requiredListeners;
-
-defaultproperties
-{
- features(0) = class'FixZedTimeLags'
- features(1) = class'FixDoshSpam'
- features(2) = class'FixFFHack'
- features(3) = class'FixInfiniteNades'
- features(4) = class'FixAmmoSelling'
- features(5) = class'FixSpectatorCrash'
- features(6) = class'FixDualiesCost'
- features(7) = class'FixInventoryAbuse'
- // Listeners
- requiredListeners(0) = class'MutatorListener_Connection'
-}
\ No newline at end of file
diff --git a/sources/MutatorEvents.uc b/sources/MutatorEvents.uc
deleted file mode 100644
index 542c320..0000000
--- a/sources/MutatorEvents.uc
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Event generator that repeats events of a mutator.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class MutatorEvents extends Events
- abstract;
-
-static function bool CallCheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length; i += 1)
- {
- result = class(listeners[i])
- .static.CheckReplacement(other, isSuperRelevant);
- if (!result) return false;
- }
- return true;
-}
-
-static function bool CallMutate(string command, PlayerController sendingPlayer)
-{
- local int i;
- local bool result;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length;i += 1)
- {
- result = class(listeners[i])
- .static.Mutate(command, sendingPlayer);
- if (!result) return false;
- }
- return true;
-}
-
-defaultproperties
-{
- relatedListener = class'MutatorListenerBase'
-}
\ No newline at end of file
diff --git a/sources/MutatorListenerBase.uc b/sources/MutatorListenerBase.uc
deleted file mode 100644
index 74c4311..0000000
--- a/sources/MutatorListenerBase.uc
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Listener for events, normally propagated by mutators.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class MutatorListenerBase extends Listener
- abstract;
-
-// This event is called whenever 'CheckReplacement'
-// check is propagated through mutators.
-// If one of the listeners returns 'false', -
-// it will be treated just like a mutator returning 'false'
-// in 'CheckReplacement' and
-// this method won't be called for remaining active listeners.
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- return true;
-}
-
-// This event is called whenever 'Mutate' is propagated through mutators.
-// If one of the listeners returns 'false', -
-// this method won't be called for remaining active listeners or mutators.
-// If all listeners return 'true', -
-// mutate command will be further propagated to the rest of the mutators.
-static function bool Mutate(string command, PlayerController sendingPlayer)
-{
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/Packages.uc b/sources/Packages.uc
new file mode 100644
index 0000000..d35e0e5
--- /dev/null
+++ b/sources/Packages.uc
@@ -0,0 +1,272 @@
+/**
+ * Main and only Acedia mutator used for loading Acedia packages
+ * and providing access to mutator events' calls.
+ * Name is chosen to make config files more readable.
+ * Copyright 2020-2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class Packages extends Mutator
+ config(Acedia);
+
+// Acedia's reference to a `Global` object.
+var private Global _;
+var private ServerGlobal _server;
+var private ClientGlobal _client;
+
+// Load Acedia on the client as well?
+var private config bool clientside;
+// Array of predefined services that must be started along with Acedia mutator.
+var private config array package;
+// Set to `true` to activate Acedia's game modes system
+var private config bool useGameModes;
+// Responsible for setting up Acedia's game modes in current voting system
+var VotingHandlerAdapter votingAdapter;
+
+var Mutator_OnMutate_Signal onMutateSignal;
+var Mutator_OnModifyLogin_Signal onModifyLoginSignal;
+var Mutator_OnCheckReplacement_Signal onCheckReplacementSignal;
+
+var private LoggerAPI.Definition infoFeatureEnabled;
+var private LoggerAPI.Definition errNoServerLevelCore, errorCannotRunTests;
+
+struct FeatureConfigPair
+{
+ var public class featureClass;
+ var public Text configName;
+};
+
+// "Constructor"
+simulated function PreBeginPlay()
+{
+ if (level.netMode == NM_DedicatedServer) {
+ InitializeServer();
+ }
+ else {
+ InitializeClient();
+ }
+}
+
+private simulated function InitializeClient()
+{
+ _ = class'Global'.static.GetInstance();
+ class'ClientLevelCore'.static.CreateLevelCore(self);
+}
+
+private function InitializeServer()
+{
+ local int i;
+ local LevelCore serverCore;
+ local GameMode currentGameMode;
+ local array availableFeatures;
+
+ if (clientside) {
+ AddToPackageMap("Acedia");
+ }
+ CheckForGarbage();
+ // Launch and setup core Acedia
+ _ = class'Global'.static.GetInstance();
+ _server = class'ServerGlobal'.static.GetInstance();
+ _client = class'ClientGlobal'.static.GetInstance();
+ serverCore = class'ServerLevelCore'.static.CreateLevelCore(self);
+ for (i = 0; i < package.length; i += 1) {
+ _.environment.RegisterPackage_S(package[i]);
+ }
+ if (serverCore != none) {
+ _server.ConnectServerLevelCore();
+ }
+ else
+ {
+ _.logger.Auto(errNoServerLevelCore);
+ return;
+ }
+ if (class'TestingService'.default.runTestsOnStartUp) {
+ RunStartUpTests();
+ }
+ SetupMutatorSignals();
+ // Determine required features and launch them
+ availableFeatures = GetAutoConfigurationInfo();
+ if (useGameModes)
+ {
+ votingAdapter = VotingHandlerAdapter(
+ _.memory.Allocate(class'VotingHandlerAdapter'));
+ votingAdapter.InjectIntoVotingHandler();
+ currentGameMode = votingAdapter.SetupGameModeAfterTravel();
+ if (currentGameMode != none) {
+ currentGameMode.UpdateFeatureArray(availableFeatures);
+ }
+ }
+ EnableFeatures(availableFeatures);
+}
+
+// "Finalizer"
+function ServerTraveling(string URL, bool bItems)
+{
+ if (votingAdapter != none)
+ {
+ votingAdapter.PrepareForServerTravel();
+ votingAdapter.RestoreVotingHandlerConfigBackup();
+ _.memory.Free(votingAdapter);
+ votingAdapter = none;
+ }
+ _.environment.ShutDown();
+ if (nextMutator != none) {
+ nextMutator.ServerTraveling(URL, bItems);
+ }
+ Destroy();
+}
+
+// Checks whether Acedia has left garbage after the previous map.
+// This can lead to serious problems, so such diagnostic check is warranted.
+private function CheckForGarbage()
+{
+ local int leftoverObjectAmount;
+ local int leftoverActorAmount;
+ local int leftoverDBRAmount;
+ local AcediaObject nextObject;
+ local AcediaActor nextActor;
+ local DBRecord nextRecord;
+
+ foreach AllObjects(class'AcediaObject', nextObject) {
+ leftoverObjectAmount += 1;
+ }
+ foreach AllActors(class'AcediaActor', nextActor) {
+ leftoverActorAmount += 1;
+ }
+ foreach AllObjects(class'DBRecord', nextRecord) {
+ leftoverDBRAmount += 1;
+ }
+ if ( leftoverObjectAmount == 0 && leftoverActorAmount == 0
+ && leftoverDBRAmount == 0)
+ {
+ Log("Acedia garbage check: nothing was found.");
+ }
+ else
+ {
+ Log("Acedia garbage check: garbage was found." @
+ "This can cause problems, report it.");
+ Log("Leftover object:" @ leftoverObjectAmount);
+ Log("Leftover actors:" @ leftoverActorAmount);
+ Log("Leftover database records:" @ leftoverDBRAmount);
+ }
+}
+
+public final function array GetAutoConfigurationInfo()
+{
+ local int i;
+ local array< class > availableFeatures;
+ local FeatureConfigPair nextPair;
+ local array result;
+
+ availableFeatures = _.environment.GetAvailableFeatures();
+ for (i = 0; i < availableFeatures.length; i += 1)
+ {
+ nextPair.featureClass = availableFeatures[i];
+ nextPair.configName = availableFeatures[i].static
+ .GetAutoEnabledConfig();
+ result[result.length] = nextPair;
+ }
+ return result;
+}
+
+private function EnableFeatures(array features)
+{
+ local int i;
+
+ for (i = 0; i < features.length; i += 1)
+ {
+ if (features[i].featureClass == none) continue;
+ if (features[i].configName == none) continue;
+ features[i].featureClass.static.EnableMe(features[i].configName);
+ _.logger.Auto(infoFeatureEnabled)
+ .Arg(_.text.FromString(string(features[i].featureClass)))
+ .Arg(features[i].configName); // consumes `configName`
+ }
+}
+
+// Fetches and sets up signals that `Mutator` needs to provide
+private function SetupMutatorSignals()
+{
+ local ServerUnrealService service;
+
+ service = ServerUnrealService(class'ServerUnrealService'.static.Require());
+ onMutateSignal = Mutator_OnMutate_Signal(
+ service.GetSignal(class'Mutator_OnMutate_Signal'));
+ onModifyLoginSignal = Mutator_OnModifyLogin_Signal(
+ service.GetSignal(class'Mutator_OnModifyLogin_Signal'));
+ onCheckReplacementSignal = Mutator_OnCheckReplacement_Signal(
+ service.GetSignal(class'Mutator_OnCheckReplacement_Signal'));
+}
+
+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);
+ }
+}
+
+/**
+ * Below `Mutator` events are redirected into appropriate signals.
+ */
+function bool CheckReplacement(Actor other, out byte isSuperRelevant)
+{
+ if (onCheckReplacementSignal != none) {
+ return onCheckReplacementSignal.Emit(other, isSuperRelevant);
+ }
+ return true;
+}
+
+function Mutate(string command, PlayerController sendingController)
+{
+ if (onMutateSignal != none) {
+ onMutateSignal.Emit(command, sendingController);
+ }
+ super.Mutate(command, sendingController);
+}
+
+function ModifyLogin(out string portal, out string options)
+{
+ if (onModifyLoginSignal != none) {
+ onModifyLoginSignal.Emit(portal, options);
+ }
+ super.ModifyLogin(portal, options);
+}
+
+defaultproperties
+{
+ clientside = false
+ useGameModes = false
+ // This is a server-only mutator
+ remoteRole = ROLE_SimulatedProxy
+ bAlwaysRelevant = true
+ // Mutator description
+ GroupName = "Package loader"
+ FriendlyName = "Acedia loader"
+ Description = "Launcher for Acedia packages"
+ infoFeatureEnabled = (l=LOG_Info,m="Feature `%1` enabled with config \"%2\".")
+ errNoServerLevelCore = (l=LOG_Error,m="Cannot create `ServerLevelCore`!")
+ errorCannotRunTests = (l=LOG_Error,m="Could not perform Acedia's tests.")
+}
\ No newline at end of file
diff --git a/sources/Service.uc b/sources/Service.uc
deleted file mode 100644
index 6296aff..0000000
--- a/sources/Service.uc
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Parent class for all services used in Acedia.
- * Currently simply makes itself server-only.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class Service extends Singleton
- abstract;
-
-defaultproperties
-{
- remoteRole = ROLE_None
- DrawType = DT_None
-}
\ No newline at end of file
diff --git a/sources/Services/Connection/ConnectionEvents.uc b/sources/Services/Connection/ConnectionEvents.uc
deleted file mode 100644
index 8b204d3..0000000
--- a/sources/Services/Connection/ConnectionEvents.uc
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Event generator for 'ConnectionService'.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class ConnectionEvents extends Events
- dependson(ConnectionService)
- abstract;
-
-static function CallPlayerConnected(ConnectionService.Connection connection)
-{
- local int i;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length; i += 1)
- {
- class(listeners[i])
- .static.PlayerConnected(connection);
- }
-}
-
-static function CallPlayerDisconnected(ConnectionService.Connection connection)
-{
- local int i;
- local array< class > listeners;
- listeners = GetListeners();
- for (i = 0; i < listeners.length; i += 1)
- {
- class(listeners[i])
- .static.PlayerDisconnected(connection);
- }
-}
-
-defaultproperties
-{
- relatedListener = class'ConnectionListenerBase'
-}
\ No newline at end of file
diff --git a/sources/Services/Connection/ConnectionListenerBase.uc b/sources/Services/Connection/ConnectionListenerBase.uc
deleted file mode 100644
index b224b3c..0000000
--- a/sources/Services/Connection/ConnectionListenerBase.uc
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Listener for events generated by 'ConnectionService'.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class ConnectionListenerBase extends Listener
- dependson(ConnectionService)
- abstract;
-
-// 'PlayerConnected' is called the moment we detect a new player on a server.
-static function PlayerConnected(ConnectionService.Connection connection);
-
-// 'PlayerDisconnected' is called the moment we
-// detect a player leaving the server.
-static function PlayerDisconnected(ConnectionService.Connection connection);
-
-defaultproperties
-{
- relatedEvents = class'ConnectionEvents'
-}
\ No newline at end of file
diff --git a/sources/Services/Connection/ConnectionService.uc b/sources/Services/Connection/ConnectionService.uc
deleted file mode 100644
index 89375fa..0000000
--- a/sources/Services/Connection/ConnectionService.uc
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * This service tracks current connections to the server
- * as well as their basic information,
- * like IP or steam ID of connecting player.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class ConnectionService extends Service;
-
-// Stores basic information about a connection
-struct Connection
-{
- var public string networkAddress;
- var public string steamID;
- var public PlayerController controllerReference;
- // Reference to 'AcediaReplicationInfo' for this client,
- // in case it was created.
- var private AcediaReplicationInfo acediaRI;
-};
-
-var private array activeConnections;
-
-// Shortcut to 'ConnectionEvents', so that we don't have to write
-// class'ConnectionEvents' every time.
-var const class events;
-
-// Returning 'true' guarantees that 'controllerToCheck != none'
-// and either 'controllerToCheck.playerReplicationInfo != none'
-// or 'auxiliaryRepInfo != none'.
-private function bool IsHumanController(PlayerController controllerToCheck)
-{
- local PlayerReplicationInfo replicationInfo;
- if (controllerToCheck == none) return false;
- if (!controllerToCheck.bIsPlayer) return false;
- // Is this a WebAdmin that didn't yet set 'bIsPlayer = false'
- if (MessagingSpectator(controllerToCheck) != none) return false;
- // Check replication info
- replicationInfo = controllerToCheck.playerReplicationInfo;
- if (replicationInfo == none) return false;
- if (replicationInfo.bBot) return false;
- return true;
-}
-
-// Returns index of the connection corresponding to the given controller.
-// Returns '-1' if no connection correspond to the given controller.
-// Returns '-1' if given controller is equal to 'none'.
-private function int GetConnectionIndex(PlayerController controllerToCheck)
-{
- local int i;
- if (controllerToCheck == none) return -1;
- for (i = 0; i < activeConnections.length; i += 1)
- {
- if (activeConnections[i].controllerReference == controllerToCheck)
- {
- return i;
- }
- }
- return -1;
-}
-
-// Remove connections with now invalid ('none') player controller reference.
-private function RemoveBrokenConnections()
-{
- local int i;
- i = 0;
- while (i < activeConnections.length)
- {
- if (activeConnections[i].controllerReference == none)
- {
- if (activeConnections[i].acediaRI != none)
- {
- activeConnections[i].acediaRI.Destroy();
- }
- events.static.CallPlayerDisconnected(activeConnections[i]);
- activeConnections.Remove(i, 1);
- }
- else
- {
- i += 1;
- }
- }
-}
-
-// Return connection, corresponding to a given player controller.
-public final function Connection GetConnection(PlayerController player)
-{
- local int connectionIndex;
- local Connection emptyConnection;
- connectionIndex = GetConnectionIndex(player);
- if (connectionIndex < 0) return emptyConnection;
- return activeConnections[connectionIndex];
-}
-
-// Attempts to register a connection for this player controller.
-// Shouldn't be used outside of 'ConnectionService' module.
-// Returns 'true' if connection is registered (even if it was already added).
-public final function bool RegisterConnection(PlayerController player)
-{
- local Connection newConnection;
- if (!IsHumanController(player)) return false;
- if (GetConnectionIndex(player) >= 0) return true;
- newConnection.controllerReference = player;
- if (!class'Acedia'.static.GetInstance().IsServerOnly())
- {
- newConnection.acediaRI = Spawn(class'AcediaReplicationInfo', player);
- newConnection.acediaRI.linkOwner = player;
- }
- newConnection.networkAddress = player.GetPlayerNetworkAddress();
- newConnection.steamID = player.GetPlayerIDHash();
- activeConnections[activeConnections.length] = newConnection;
- events.static.CallPlayerConnected(newConnection);
- return true;
-}
-
-public final function array GetActiveConnections()
-{
- return activeConnections;
-}
-
-event Tick(float delta)
-{
- RemoveBrokenConnections();
-}
-
-defaultproperties
-{
- events = class'ConnectionEvents'
-}
\ No newline at end of file
diff --git a/sources/Services/Connection/MutatorListener_Connection.uc b/sources/Services/Connection/MutatorListener_Connection.uc
deleted file mode 100644
index acbf404..0000000
--- a/sources/Services/Connection/MutatorListener_Connection.uc
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Overloaded mutator events listener to catch connecting players.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class MutatorListener_Connection extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local KFSteamStatsAndAchievements playerSteamStatsAndAchievements;
- local PlayerController player;
- local ConnectionService service;
- // 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'll be detecting new users right after server
- // detected and properly initialized them.
- playerSteamStatsAndAchievements = KFSteamStatsAndAchievements(other);
- if (playerSteamStatsAndAchievements == none) return true;
- service = ConnectionService(class'ConnectionService'.static.GetInstance());
- if (service == none) return true;
-
- player = PlayerController(playerSteamStatsAndAchievements.owner);
- service.RegisterConnection(player);
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/Singleton.uc b/sources/Singleton.uc
deleted file mode 100644
index 5271cbc..0000000
--- a/sources/Singleton.uc
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Singleton is an auxiliary class, meant to be used as a base for others,
- * that allows for only one instance of it to exist.
- * To make sure your child class properly works, either don't overload
- * 'PreBeginPlay' or make sure to call it's parent's version.
- * Copyright 2019 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class Singleton extends Actor
- abstract;
-
-// Default value of this variable will store one and only existing version\
-// of actor of this class.
-var private Singleton activeInstance;
-
-// Setting default value of this variable to 'true' prevents creation of
-// a singleton, even if no instances of it exist.
-// Only a default value is ever used.
-var protected bool blockSpawning;
-
-public final static function Singleton GetInstance()
-{
- if (default.activeInstance != none && default.activeInstance.bPendingDelete)
- return none;
- return default.activeInstance;
-}
-
-public final static function bool IsSingletonCreationBlocked()
-{
- return default.blockSpawning;
-}
-
-// Make sure only one instance of 'Singleton' exists at any point in time.
-// If you overload this function in any child class -
-// first call this version of the method and then check if
-// you are about to be deleted 'bDeleteMe == true':
-// ____________________________________________________________________________
-// | super.PreBeginPlay();
-// | // ^^^ If singleton wasn't already created, - only after that call
-// | // will instance, returned by 'GetInstance()', be set.
-// | if (bDeleteMe)
-// | return;
-// |___________________________________________________________________________
-event PreBeginPlay()
-{
- if (default.blockSpawning || GetInstance() != none)
- {
- Destroy();
- }
- else
- {
- default.activeInstance = self;
- }
-}
-
-defaultproperties
-{
- blockSpawning = false
-}
\ No newline at end of file
diff --git a/sources/StartUp.uc b/sources/StartUp.uc
index 180eaf6..0755d39 100644
--- a/sources/StartUp.uc
+++ b/sources/StartUp.uc
@@ -1,6 +1,6 @@
/**
* This actor's role is to add Acedia mutator on listen and dedicated servers.
- * Copyright 2019 Anton Tarasenko
+ * Copyright 2019-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -23,9 +23,8 @@ class StartUp extends Actor;
function PreBeginPlay()
{
super.PreBeginPlay();
- if (level != none && level.game != none)
- {
- level.game.AddMutator(string(class'Acedia'));
+ if (level != none && level.game != none) {
+ level.game.AddMutator(string(class'Packages'));
}
Destroy();
}
diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc
new file mode 100644
index 0000000..69b72ec
--- /dev/null
+++ b/sources/VotingHandlerAdapter.uc
@@ -0,0 +1,354 @@
+/**
+ * Acedia currently lacks its own means to provide a map/mode voting
+ * (and new voting mod with proper GUI would not be whitelisted anyway).
+ * This is why this class was made - to inject existing voting handlers with
+ * data from Acedia's game modes.
+ * Requires `GameInfo`'s voting handler to be derived from
+ * `XVotingHandler`, which is satisfied by pretty much every used handler.
+ * Copyright 2021-2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class VotingHandlerAdapter extends AcediaObject
+ dependson(VotingHandler);
+
+/**
+ * All usage of this object should start with `InjectIntoVotingHandler()`
+ * method that will read all the `GameMode` configs and fill voting handler's
+ * config with their data, while making a backup of all values.
+ * Backup can be restored with `RestoreVotingHandlerConfigBackup()` method.
+ * How that affects the clients depends on whether restoration was done before,
+ * during or after the replication. It is intended to be done after
+ * server travel has started.
+ * the process of injection is to create an ordered list of game modes
+ * (`availableGameModes`) and generate appropriate voting handler's configs
+ * with `BuildVotingHandlerConfig()`, saving them in the same order inside
+ * the voting handler. Picked game mode is then determined by index of
+ * the picked voting handler's option.
+ *
+ * Additionally this class has a static internal state that allows it to
+ * transfer data along the server travel - it is used mainly to remember picked
+ * game mode and enforce game's difficulty by altering and restoring
+ * `GameInfo`'s variable.
+ * To make such transfer happen one must call `PrepareForServerTravel()` before
+ * server travel to set the internal static state and
+ * then `SetupGameModeAfterTravel()` after travel (when the new map is loading)
+ * to read (and forget) from internal state.
+ */
+
+// Aliases are an unnecessary overkill for difficulty names, so just define
+// them in special `string` arrays.
+// We accept detect not just these exact words, but any of their prefixes.
+var private const array beginnerSynonyms;
+var private const array normalSynonyms;
+var private const array hardSynonyms;
+var private const array suicidalSynonyms;
+var private const array hoeSynonyms;
+
+// All available game modes for Acedia, loaded during initialization.
+// This array is directly produces replacement for `XVotingHandler`'s
+// `gameConfig` array and records of `availableGameModes` relate to those of
+// `gameConfig` with the same index.
+// So if we know that a voting option with a certain index was chosen -
+// it means that user picked game mode from `availableGameModes` with
+// the same index.
+var private array availableGameModes;
+
+// Finding voting handler is not cheap, so only do it once and then store it.
+var private NativeActorRef votingHandlerReference;
+// Save `VotingHandler`'s config to restore it before server travel -
+// otherwise Acedia will alter its config
+var private array backupVotingHandlerConfig;
+
+// Setting default value of this flag to `true` indicates that map switching
+// just occurred and we need to recover some information from the previous map.
+var private bool isServerTraveling;
+// We should not rely on "VotingHandler" to inform us from which game mode its
+// selected config option originated after server travel, so we need to
+// remember it in this default variable before switching maps.
+var private string targetGameMode;
+// Acedia's game modes intend on supporting difficulty switching, but
+// `KFGameType` does not support appropriate flags, so we enforce default
+// difficulty by overwriting default value of its `gameDifficulty` variable.
+// But to not affect game's configs we must restore old value after new map is
+// loaded. Store it in default variable for that.
+var private float storedGameDifficulty;
+
+var private LoggerAPI.Definition fatNoXVotingHandler, fatBadGameConfigIndexVH;
+var private LoggerAPI.Definition fatBadGameConfigIndexAdapter;
+
+protected function Finalizer()
+{
+ _.memory.Free(votingHandlerReference);
+ _.memory.FreeMany(availableGameModes);
+ votingHandlerReference = none;
+ availableGameModes.length = 0;
+}
+
+/**
+ * Replaces `XVotingHandler`'s configs with Acedia's game modes.
+ * Backup of replaced configs is made internally, so that they can be restored
+ * on map change.
+ */
+public final function InjectIntoVotingHandler()
+{
+ local int i;
+ local GameMode nextGameMode;
+ local XVotingHandler votingHandler;
+ local array newVotingHandlerConfig;
+
+ if (votingHandlerReference != none) {
+ return;
+ }
+ votingHandler = XVotingHandler(_server.unreal.FindActorInstance(
+ _server.unreal.GetGameType().VotingHandlerClass));
+ if (votingHandler == none)
+ {
+ _.logger.Auto(fatNoXVotingHandler);
+ return;
+ }
+ votingHandlerReference = _server.unreal.ActorRef(votingHandler);
+ class'GameMode'.static.Initialize();
+ availableGameModes = class'GameMode'.static.AvailableConfigs();
+ for (i = 0; i < availableGameModes.length; i += 1)
+ {
+ nextGameMode = GameMode(class'GameMode'.static
+ .GetConfigInstance(availableGameModes[i]));
+ newVotingHandlerConfig[i] = BuildVotingHandlerConfig(nextGameMode);
+ // Report omitted mutators / server options
+ nextGameMode.ReportBadMutatorNames();
+ nextGameMode.ReportBadOptions();
+ }
+ backupVotingHandlerConfig = votingHandler.gameConfig;
+ votingHandler.gameConfig = newVotingHandlerConfig;
+}
+
+private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig(
+ GameMode gameMode)
+{
+ local VotingHandler.MapVoteGameConfig result;
+
+ result.gameClass = _.text.IntoString(gameMode.GetGameTypeClass());
+ result.gameName = _.text.ToColoredString(gameMode.GetTitle());
+ result.prefix = _.text.IntoString(gameMode.GetMapPrefix());
+ result.acronym = _.text.IntoString(gameMode.GetAcronym());
+ result.mutators = BuildMutatorString(gameMode);
+ result.options = BuildOptionsString(gameMode);
+ return result;
+}
+
+private function string BuildMutatorString(GameMode gameMode)
+{
+ local int i;
+ local string result;
+ local array usedMutators;
+
+ usedMutators = gameMode.GetIncludedMutators();
+ for (i = 0; i < usedMutators.length; i += 1)
+ {
+ if (i > 0) {
+ result $= ",";
+ }
+ result $= _.text.IntoString(usedMutators[i]);
+ }
+ return result;
+}
+
+private function string BuildOptionsString(GameMode gameMode)
+{
+ local bool optionWasAdded;
+ local string result;
+ local string nextKey, nextValue;
+ local CollectionIterator iter;
+ local HashTable options;
+
+ options = gameMode.GetOptions();
+ for (iter = options.Iterate(); !iter.HasFinished(); iter.Next())
+ {
+ nextKey = _.text.IntoString(Text(iter.GetKey()));
+ nextValue = _.text.IntoString(Text(iter.Get()));
+ if (optionWasAdded) {
+ result $= "?";
+ }
+ result $= (nextKey $ "=" $ nextValue);
+ optionWasAdded = true;
+ }
+ options.FreeSelf();
+ iter.FreeSelf();
+ return result;
+}
+
+/**
+ * Makes necessary preparations for the server travel.
+ */
+public final function PrepareForServerTravel()
+{
+ local int pickedVHConfig;
+ local GameMode nextGameMode;
+ local string nextGameClassName;
+ local class nextGameClass;
+ local XVotingHandler votingHandler;
+
+ if (votingHandlerReference == none) return;
+ votingHandler = XVotingHandler(votingHandlerReference.Get());
+ if (votingHandler == none) return;
+ // Server travel caused by something else than `XVotingHandler`
+ if (!votingHandler.bLevelSwitchPending) return;
+
+ pickedVHConfig = votingHandler.currentGameConfig;
+ if (pickedVHConfig < 0 || pickedVHConfig >= votingHandler.gameConfig.length)
+ {
+ _.logger.Auto(fatBadGameConfigIndexVH)
+ .ArgInt(pickedVHConfig)
+ .ArgInt(votingHandler.gameConfig.length);
+ return;
+ }
+ if (pickedVHConfig >= availableGameModes.length)
+ {
+ _.logger.Auto(fatBadGameConfigIndexAdapter)
+ .ArgInt(pickedVHConfig)
+ .ArgInt(availableGameModes.length);
+ return;
+ }
+ nextGameClassName = votingHandler.gameConfig[pickedVHConfig].gameClass;
+ if (string(_server.unreal.GetGameType().class) ~= nextGameClassName) {
+ nextGameClass = _server.unreal.GetGameType().class;
+ }
+ else
+ {
+ nextGameClass =
+ class(_.memory.LoadClass_S(nextGameClassName));
+ }
+ default.isServerTraveling = true;
+ default.targetGameMode = availableGameModes[pickedVHConfig].ToString();
+ nextGameMode = GetConfigFromString(default.targetGameMode);
+ default.storedGameDifficulty = nextGameClass.default.gameDifficulty;
+ nextGameClass.default.gameDifficulty = GetNumericDifficulty(nextGameMode);
+}
+
+/**
+ * Restore `GameInfo`'s settings after the server travel and
+ * apply selected `GameMode`.
+ *
+ * @return `GameMode` picked before server travel
+ * (the one that must be running now).
+ */
+public final function GameMode SetupGameModeAfterTravel()
+{
+ if (!default.isServerTraveling) {
+ return none;
+ }
+ _server.unreal.GetGameType().default.gameDifficulty =
+ default.storedGameDifficulty;
+ default.isServerTraveling = false;
+ return GetConfigFromString(targetGameMode);
+}
+
+/**
+ * Restores `XVotingHandler`'s config to the values that were overridden by
+ * `VHAdapter`'s `InjectIntoVotingHandler()` method.
+ */
+public final function RestoreVotingHandlerConfigBackup()
+{
+ local XVotingHandler votingHandler;
+
+ if (votingHandlerReference == none) return;
+ votingHandler = XVotingHandler(votingHandlerReference.Get());
+ if (votingHandler == none) return;
+
+ votingHandler.gameConfig = backupVotingHandlerConfig;
+ votingHandler.default.gameConfig = backupVotingHandlerConfig;
+ votingHandler.SaveConfig();
+}
+
+// `GameMode`'s name as a `string` -> `GameMode` instance
+private function GameMode GetConfigFromString(string configName)
+{
+ local GameMode result;
+ local Text nextConfigName;
+ nextConfigName = _.text.FromString(configName);
+ result = GameMode(class'GameMode'.static.GetConfigInstance(nextConfigName));
+ _.memory.Free(nextConfigName);
+ return result;
+}
+
+// Convert `GameMode`'s difficulty's textual representation into
+// KF's numeric one.
+private final function int GetNumericDifficulty(GameMode gameMode)
+{
+ local int i;
+ local string difficulty;
+
+ difficulty = Locs(_.text.IntoString(gameMode.GetDifficulty()));
+ for (i = 0; i < default.beginnerSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.beginnerSynonyms[i])) {
+ return 1;
+ }
+ }
+ for (i = 0; i < default.normalSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.normalSynonyms[i])) {
+ return 2;
+ }
+ }
+ for (i = 0; i < default.hardSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.hardSynonyms[i])) {
+ return 4;
+ }
+ }
+ for (i = 0; i < default.suicidalSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.suicidalSynonyms[i])) {
+ return 5;
+ }
+ }
+ for (i = 0; i < default.hoeSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.hoeSynonyms[i])) {
+ return 7;
+ }
+ }
+ return int(difficulty);
+}
+
+protected final static function bool IsPrefixOf(string prefix, string value)
+{
+ return (InStr(value, prefix) == 0);
+}
+
+defaultproperties
+{
+ beginnerSynonyms(0) = "easy"
+ beginnerSynonyms(1) = "beginer"
+ beginnerSynonyms(2) = "beginner"
+ beginnerSynonyms(3) = "begginer"
+ beginnerSynonyms(4) = "begginner"
+ normalSynonyms(0) = "regular"
+ normalSynonyms(1) = "default"
+ normalSynonyms(2) = "normal"
+ hardSynonyms(0) = "harder" // "hard" is prefix of this, so it will count
+ hardSynonyms(1) = "difficult"
+ suicidalSynonyms(0) = "suicidal"
+ hoeSynonyms(0) = "hellonearth"
+ hoeSynonyms(1) = "hellon earth"
+ hoeSynonyms(2) = "hell onearth"
+ hoeSynonyms(3) = "hoe"
+ fatNoXVotingHandler = (l=LOG_Fatal,m="`XVotingHandler` class is missing. Make sure your server setup supports Acedia's game modes (by used voting handler derived from `XVotingHandler`).")
+ fatBadGameConfigIndexVH = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `XVotingHandler.gameConfig` of length %2. Report this issue.")
+ fatBadGameConfigIndexAdapter = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `VHAdapter` of length %2. Report this issue.")
+}
\ No newline at end of file