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