Compare commits
No commits in common. 'new' and 'master' have entirely different histories.
51 changed files with 4695 additions and 1233 deletions
@ -1,2 +1,255 @@ |
|||||||
[Acedia.Packages] |
[Acedia.FixDualiesCost] |
||||||
useGameModes=false |
; 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 |
@ -1,7 +0,0 @@ |
|||||||
[hard GameMode] |
|
||||||
title={$green Hard difficulty} |
|
||||||
difficulty=normal |
|
||||||
|
|
||||||
[hell GameMode] |
|
||||||
title={$crimson Hell On Earth} |
|
||||||
difficulty=hoe |
|
@ -0,0 +1,131 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<Service> > 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<Manifest> 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" |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class AcediaReplicationInfo extends ReplicationInfo; |
||||||
|
|
||||||
|
var public PlayerController linkOwner; |
||||||
|
|
||||||
|
replication |
||||||
|
{ |
||||||
|
reliable if (role == ROLE_Authority) |
||||||
|
linkOwner; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,142 @@ |
|||||||
|
/** |
||||||
|
* Event generator for events, related to broadcasting messages |
||||||
|
* through standard Unreal Script means: |
||||||
|
* 1. text messages, typed by a player; |
||||||
|
* 2. localized messages, identified by a LocalMessage class and id. |
||||||
|
* Allows to make decisions whether or not to propagate certain messages. |
||||||
|
* Copyright 2020 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class BroadcastEvents extends Events |
||||||
|
abstract; |
||||||
|
|
||||||
|
struct LocalizedMessage |
||||||
|
{ |
||||||
|
// Every localized message is described by a class and id. |
||||||
|
// For example, consider 'KFMod.WaitingMessage': |
||||||
|
// if passed 'id' is '1', |
||||||
|
// then it's supposed to be a message about new wave, |
||||||
|
// but if passed 'id' is '2', |
||||||
|
// then it's about completing the wave. |
||||||
|
var class<LocalMessage> class; |
||||||
|
var int id; |
||||||
|
// Localized messages in unreal script can be passed along with |
||||||
|
// optional arguments, described by variables below. |
||||||
|
var PlayerReplicationInfo relatedPRI1; |
||||||
|
var PlayerReplicationInfo relatedPRI2; |
||||||
|
var Object relatedObject; |
||||||
|
}; |
||||||
|
|
||||||
|
static function bool CallCanBroadcast(Actor broadcaster, int recentSentTextSize) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool result; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0;i < listeners.length;i += 1) |
||||||
|
{ |
||||||
|
result = class<BroadcastListenerBase>(listeners[i]) |
||||||
|
.static.CanBroadcast(broadcaster, recentSentTextSize); |
||||||
|
if (!result) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static function bool CallHandleText |
||||||
|
( |
||||||
|
Actor sender, |
||||||
|
out string message, |
||||||
|
name messageType |
||||||
|
) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool result; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0;i < listeners.length;i += 1) |
||||||
|
{ |
||||||
|
result = class<BroadcastListenerBase>(listeners[i]) |
||||||
|
.static.HandleText(sender, message, messageType); |
||||||
|
if (!result) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static function bool CallHandleTextFor |
||||||
|
( |
||||||
|
PlayerController receiver, |
||||||
|
Actor sender, |
||||||
|
out string message, |
||||||
|
name messageType |
||||||
|
) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool result; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0;i < listeners.length;i += 1) |
||||||
|
{ |
||||||
|
result = class<BroadcastListenerBase>(listeners[i]) |
||||||
|
.static.HandleTextFor(receiver, sender, message, messageType); |
||||||
|
if (!result) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static function bool CallHandleLocalized |
||||||
|
( |
||||||
|
Actor sender, |
||||||
|
LocalizedMessage message |
||||||
|
) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool result; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0;i < listeners.length;i += 1) |
||||||
|
{ |
||||||
|
result = class<BroadcastListenerBase>(listeners[i]) |
||||||
|
.static.HandleLocalized(sender, message); |
||||||
|
if (!result) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static function bool CallHandleLocalizedFor |
||||||
|
( |
||||||
|
PlayerController receiver, |
||||||
|
Actor sender, |
||||||
|
LocalizedMessage message |
||||||
|
) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool result; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0;i < listeners.length;i += 1) |
||||||
|
{ |
||||||
|
result = class<BroadcastListenerBase>(listeners[i]) |
||||||
|
.static.HandleLocalizedFor(receiver, sender, message); |
||||||
|
if (!result) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedListener = class'BroadcastListenerBase' |
||||||
|
} |
@ -0,0 +1,197 @@ |
|||||||
|
/** |
||||||
|
* '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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
// 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<Actor> storedSenders; |
||||||
|
|
||||||
|
// We want to insert our code in some of the functions between |
||||||
|
// 'AllowsBroadcast' check and actual broadcasting, |
||||||
|
// so we can't just use a 'super.AllowsBroadcast()' call. |
||||||
|
// Instead we first manually do this check, then perform our logic and then |
||||||
|
// make a super call, but with 'blockAllowsBroadcast' flag set to 'true', |
||||||
|
// which causes overloaded 'AllowsBroadcast()' to omit 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<LocalMessage> 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<LocalMessage> message, |
||||||
|
optional int switch, |
||||||
|
optional PlayerReplicationInfo relatedPRI1, |
||||||
|
optional PlayerReplicationInfo relatedPRI2, |
||||||
|
optional Object obj |
||||||
|
) |
||||||
|
{ |
||||||
|
local bool canBroadcast; |
||||||
|
local BroadcastEvents.LocalizedMessage packedMessage; |
||||||
|
packedMessage.class = message; |
||||||
|
packedMessage.id = switch; |
||||||
|
packedMessage.relatedPRI1 = relatedPRI1; |
||||||
|
packedMessage.relatedPRI2 = relatedPRI2; |
||||||
|
packedMessage.relatedObject = obj; |
||||||
|
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 |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
/** |
||||||
|
* Listener for events, related to broadcasting messages |
||||||
|
* through standard Unreal Script means: |
||||||
|
* 1. text messages, typed by a player; |
||||||
|
* 2. localized messages, identified by a LocalMessage class and id. |
||||||
|
* Allows to make decisions whether or not to propagate certain messages. |
||||||
|
* Copyright 2020 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class BroadcastListenerBase extends Listener |
||||||
|
abstract; |
||||||
|
|
||||||
|
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'. |
@ -0,0 +1,133 @@ |
|||||||
|
/** |
||||||
|
* One of the two classes that make up a core of event system in Acedia. |
||||||
|
* |
||||||
|
* 'Events' (or it's child) class shouldn't be instantiated. |
||||||
|
* Usually module would provide '...Events' class that defines |
||||||
|
* certain set of static functions that can generate event calls to |
||||||
|
* all it's active listeners. |
||||||
|
* If you're simply using modules someone made, - |
||||||
|
* you don't need to bother yourself with further specifics. |
||||||
|
* If you wish to create your own event generator, |
||||||
|
* then first create a '...ListenerBase' object |
||||||
|
* (more about it in the description of 'Listener' class) |
||||||
|
* and set 'relatedListener' variable to point to it's class. |
||||||
|
* Then for each event create a caller function in your 'Event' class, |
||||||
|
* following this template: |
||||||
|
* ____________________________________________________________________________ |
||||||
|
* | static function CallEVENT_NAME(<ARGUMENTS>) |
||||||
|
* | { |
||||||
|
* | local int i; |
||||||
|
* | local array< class<Listener> > listeners; |
||||||
|
* | listeners = GetListeners(); |
||||||
|
* | for (i = 0; i < listeners.length; i += 1) |
||||||
|
* | { |
||||||
|
* | class<...ListenerBase>(listeners[i]) |
||||||
|
* | .static.EVENT_NAME(<ARGUMENTS>); |
||||||
|
* | } |
||||||
|
* | } |
||||||
|
* |___________________________________________________________________________ |
||||||
|
* If each listener must indicate whether it gives it's permission for |
||||||
|
* something to happen, then use this template: |
||||||
|
* ____________________________________________________________________________ |
||||||
|
* | static function CallEVENT_NAME(<ARGUMENTS>) |
||||||
|
* | { |
||||||
|
* | local int i; |
||||||
|
* | local bool result; |
||||||
|
* | local array< class<Listener> > listeners; |
||||||
|
* | listeners = GetListeners(); |
||||||
|
* | for (i = 0; i < listeners.length; i += 1) |
||||||
|
* | { |
||||||
|
* | result = class<...ListenerBase>(listeners[i]) |
||||||
|
* | .static.EVENT_NAME(<ARGUMENTS>); |
||||||
|
* | if (!result) return false; |
||||||
|
* | } |
||||||
|
* | return true; |
||||||
|
* | } |
||||||
|
* |___________________________________________________________________________ |
||||||
|
* For concrete example look at |
||||||
|
* 'MutatorEvents' and 'MutatorListenerBase'. |
||||||
|
* Copyright 2020 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Events extends Object |
||||||
|
abstract; |
||||||
|
|
||||||
|
var private array< class<Listener> > listeners; |
||||||
|
|
||||||
|
var public const class<Listener> relatedListener; |
||||||
|
|
||||||
|
static public final function array< class<Listener> > GetListeners() |
||||||
|
{ |
||||||
|
return default.listeners; |
||||||
|
} |
||||||
|
|
||||||
|
// Make given listener active. |
||||||
|
// If listener was already activated also returns 'false'. |
||||||
|
static public final function bool ActivateListener(class<Listener> newListener) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
if (newListener == none) return false; |
||||||
|
if (!ClassIsChildOf(newListener, default.relatedListener)) return false; |
||||||
|
|
||||||
|
for (i = 0;i < default.listeners.length;i += 1) |
||||||
|
{ |
||||||
|
if (default.listeners[i] == newListener) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
default.listeners[default.listeners.length] = newListener; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// Make given listener inactive. |
||||||
|
// If listener wasn't active returns 'false'. |
||||||
|
static public final function bool DeactivateListener(class<Listener> listener) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
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> 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' |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<Listener> > 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 |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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 |
||||||
|
} |
@ -0,0 +1,395 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<KFWeapon> abusableWeapon; |
||||||
|
var class<KFWeaponPickup> pickupReplacement; |
||||||
|
}; |
||||||
|
|
||||||
|
// Actual list of abusable weapons. |
||||||
|
var private const array<ReplacementRule> 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<WeaponRecord> 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<AmmoPickupStalker> 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<Actor> 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<KFWeaponPickup> vanillaPickupClass, fixPickupClass; |
||||||
|
if (kfWeapon == none || kfWeapon.instigator == none) return 0.0; |
||||||
|
fixPickupClass = class<KFWeaponPickup>(kfWeapon.pickupClass); |
||||||
|
vanillaPickupClass = class<KFWeaponPickup>(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' |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_CamoM32Pickup extends CamoM32Pickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 42 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_CrossbowPickup extends CrossbowPickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 11.6 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_GoldenM79Pickup extends GoldenM79Pickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 7 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_LAWPickup extends LAWPickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 21 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_M32Pickup extends M32Pickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 42 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_M79Pickup extends M79Pickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 7 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_PipeBombPickup extends PipeBombPickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 195 |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_SPGrenadePickup extends SPGrenadePickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 7 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_SealSquealPickup extends SealSquealPickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 21 |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FixAmmoSellingClass_SeekerSixPickup extends SeekerSixPickup; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
AmmoCost = 10.5 |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<Inventory> 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' |
||||||
|
} |
@ -0,0 +1,252 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<DoshStreamPerPlayer> currentContributors; |
||||||
|
|
||||||
|
// Wads of cash that are lying around on the map. |
||||||
|
var private array<CashPickup> 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<DoshStreamPerPlayer> 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' |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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' |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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 |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,454 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<KFWeapon> single; |
||||||
|
var class<KFWeapon> dual; |
||||||
|
}; |
||||||
|
var private const array<DualiesPair> 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<WeaponValuePair> pendingValues; |
||||||
|
|
||||||
|
// Describe sell values of all currently existing single pistols. |
||||||
|
struct WeaponDataRecord |
||||||
|
{ |
||||||
|
var KFWeapon reference; |
||||||
|
var class<KFWeapon> 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<WeaponDataRecord> 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<KFWeapon> 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<KFWeaponPickup> pickupClass; |
||||||
|
local KFPlayerReplicationInfo instigatorRI; |
||||||
|
if (weapon == none) return 0.0; |
||||||
|
pickupClass = class<KFWeaponPickup>(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' |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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' |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class FFHackRule extends GameRules; |
||||||
|
|
||||||
|
function int NetDamage |
||||||
|
( |
||||||
|
int originalDamage, |
||||||
|
int damage, |
||||||
|
Pawn injured, |
||||||
|
Pawn instigator, |
||||||
|
Vector hitLocation, |
||||||
|
out Vector momentum, |
||||||
|
class<DamageType> 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 |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,152 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<DamageType> > alwaysScale; |
||||||
|
// Damage types, for which we should never reapply friendly fire scaling. |
||||||
|
var private config const array< class<DamageType> > 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> damageType) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array< class<DamageType> > 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' |
||||||
|
} |
@ -0,0 +1,233 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<FragAmmoRecord> 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<FragAmmoRecord> 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' |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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 |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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 |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,225 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<KFWeaponPickup> single; |
||||||
|
var class<KFWeaponPickup> 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<DualiesPair> 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<KFWeaponPickup> GetRootPickupClass(KFWeapon weapon) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local class<KFWeaponPickup> root; |
||||||
|
if (weapon == none) return none; |
||||||
|
// Start with a pickup of the given weapons |
||||||
|
root = class<KFWeaponPickup>(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<KFWeaponPickup>(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<KFWeaponPickup> rootClass; |
||||||
|
local array< class<Pickup> > 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<KFWeapon> 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<ConnectionService.Connection> 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') |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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 |
||||||
|
} |
@ -0,0 +1,291 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<CooldownRecord> 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<PlayerController> 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<ConnectionService.Connection> 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' |
||||||
|
} |
@ -0,0 +1,188 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<ConnectionService.Connection> 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 |
||||||
|
} |
@ -1,400 +0,0 @@ |
|||||||
/** |
|
||||||
* 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 <https://www.gnu.org/licenses/>. |
|
||||||
*/ |
|
||||||
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<string> includeMutator; |
|
||||||
// `Feature`s to include (with "default" config) |
|
||||||
var protected config array<string> includeFeature; |
|
||||||
// `Feature`s to exclude from game mode, regardless of other settings |
|
||||||
// (this one has highest priority) |
|
||||||
var protected config array<string> 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<FeatureConfigPair> 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<string> DynamicIntoStringArray(ArrayList source) |
|
||||||
{ |
|
||||||
local int i; |
|
||||||
local Text nextText; |
|
||||||
local array<string> 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<Text> StringToTextArray(array<string> input) |
|
||||||
{ |
|
||||||
local int i; |
|
||||||
local array<Text> 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<Packages.FeatureConfigPair> featuresToEnable) |
|
||||||
{ |
|
||||||
local int i; |
|
||||||
local array<string> 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<string> subset, |
|
||||||
array<string> 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<Packages.FeatureConfigPair> 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<Text> GetIncludedMutators() |
|
||||||
{ |
|
||||||
local int i; |
|
||||||
local array<string> 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.") |
|
||||||
} |
|
@ -1,195 +0,0 @@ |
|||||||
/** |
|
||||||
* 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 <https://www.gnu.org/licenses/>. |
|
||||||
*/ |
|
||||||
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<GameOption> 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.") |
|
||||||
} |
|
@ -0,0 +1,59 @@ |
|||||||
|
/** |
||||||
|
* One of the two classes that make up a core of event system in Acedia. |
||||||
|
* |
||||||
|
* 'Listener' (or it's child) class shouldn't be instantiated. |
||||||
|
* Usually module would provide '...ListenerBase' class that defines |
||||||
|
* certain set of static functions, corresponding to events it can listen to. |
||||||
|
* In order to handle those events you must create it's child class and |
||||||
|
* override said functions. But they will only be called if |
||||||
|
* 'SetActive(true)' is called for that child class. |
||||||
|
* To create you own '...ListenerBase' class you need to define |
||||||
|
* a static function for each event you wish it to catch and |
||||||
|
* set 'relatedEvents' variable to point at the 'Events' class |
||||||
|
* that will generate your events. |
||||||
|
* For concrete example look at |
||||||
|
* 'ConnectionEvents' and 'ConnectionListenerBase'. |
||||||
|
* Copyright 2019 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Listener extends Object |
||||||
|
abstract; |
||||||
|
|
||||||
|
var public const class<Events> relatedEvents; |
||||||
|
|
||||||
|
|
||||||
|
static public final function SetActive(bool active) |
||||||
|
{ |
||||||
|
if (active) |
||||||
|
{ |
||||||
|
default.relatedEvents.static.ActivateListener(default.class); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
default.relatedEvents.static.DeactivateListener(default.class); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static public final function IsActive(bool active) |
||||||
|
{ |
||||||
|
default.relatedEvents.static.IsActiveListener(default.class); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedEvents = class'Events' |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Manifest extends Object |
||||||
|
abstract; |
||||||
|
|
||||||
|
// List of features in this manifest's package. |
||||||
|
var public const array< class<Feature> > features; |
||||||
|
|
||||||
|
// Listeners listed here will be automatically activated. |
||||||
|
var public const array< class<Listener> > 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' |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
/** |
||||||
|
* Event generator that repeats events of a mutator. |
||||||
|
* Copyright 2019 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class MutatorEvents extends Events |
||||||
|
abstract; |
||||||
|
|
||||||
|
static function bool CallCheckReplacement(Actor other, out byte isSuperRelevant) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool result; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0; i < listeners.length; i += 1) |
||||||
|
{ |
||||||
|
result = class<MutatorListenerBase>(listeners[i]) |
||||||
|
.static.CheckReplacement(other, isSuperRelevant); |
||||||
|
if (!result) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static function bool CallMutate(string command, PlayerController sendingPlayer) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool result; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0; i < listeners.length;i += 1) |
||||||
|
{ |
||||||
|
result = class<MutatorListenerBase>(listeners[i]) |
||||||
|
.static.Mutate(command, sendingPlayer); |
||||||
|
if (!result) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedListener = class'MutatorListenerBase' |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
/** |
||||||
|
* Listener for events, normally propagated by mutators. |
||||||
|
* Copyright 2019 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class MutatorListenerBase extends Listener |
||||||
|
abstract; |
||||||
|
|
||||||
|
// This event is called whenever 'CheckReplacement' |
||||||
|
// check is propagated through mutators. |
||||||
|
// If one of the listeners returns 'false', - |
||||||
|
// it will be treated just like a mutator returning 'false' |
||||||
|
// in 'CheckReplacement' and |
||||||
|
// this method won't be called for remaining active listeners. |
||||||
|
static function bool CheckReplacement(Actor other, out byte isSuperRelevant) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// This event is called whenever 'Mutate' is propagated through mutators. |
||||||
|
// If one of the listeners returns 'false', - |
||||||
|
// this method won't be called for remaining active listeners or mutators. |
||||||
|
// If all listeners return 'true', - |
||||||
|
// mutate command will be further propagated to the rest of the mutators. |
||||||
|
static function bool Mutate(string command, PlayerController sendingPlayer) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedEvents = class'MutatorEvents' |
||||||
|
} |
@ -1,272 +0,0 @@ |
|||||||
/** |
|
||||||
* 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 <https://www.gnu.org/licenses/>. |
|
||||||
*/ |
|
||||||
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<string> 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<Feature> 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<FeatureConfigPair> 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<FeatureConfigPair> GetAutoConfigurationInfo() |
|
||||||
{ |
|
||||||
local int i; |
|
||||||
local array< class<Feature> > availableFeatures; |
|
||||||
local FeatureConfigPair nextPair; |
|
||||||
local array<FeatureConfigPair> 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<FeatureConfigPair> 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.") |
|
||||||
} |
|
@ -0,0 +1,28 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Service extends Singleton |
||||||
|
abstract; |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
remoteRole = ROLE_None |
||||||
|
DrawType = DT_None |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class ConnectionEvents extends Events |
||||||
|
dependson(ConnectionService) |
||||||
|
abstract; |
||||||
|
|
||||||
|
static function CallPlayerConnected(ConnectionService.Connection connection) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0; i < listeners.length; i += 1) |
||||||
|
{ |
||||||
|
class<ConnectionListenerBase>(listeners[i]) |
||||||
|
.static.PlayerConnected(connection); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static function CallPlayerDisconnected(ConnectionService.Connection connection) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array< class<Listener> > listeners; |
||||||
|
listeners = GetListeners(); |
||||||
|
for (i = 0; i < listeners.length; i += 1) |
||||||
|
{ |
||||||
|
class<ConnectionListenerBase>(listeners[i]) |
||||||
|
.static.PlayerDisconnected(connection); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedListener = class'ConnectionListenerBase' |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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' |
||||||
|
} |
@ -0,0 +1,142 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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<Connection> activeConnections; |
||||||
|
|
||||||
|
// Shortcut to 'ConnectionEvents', so that we don't have to write |
||||||
|
// class'ConnectionEvents' every time. |
||||||
|
var const class<ConnectionEvents> 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<Connection> GetActiveConnections() |
||||||
|
{ |
||||||
|
return activeConnections; |
||||||
|
} |
||||||
|
|
||||||
|
event Tick(float delta) |
||||||
|
{ |
||||||
|
RemoveBrokenConnections(); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
events = class'ConnectionEvents' |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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' |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
/** |
||||||
|
* 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 <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
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 |
||||||
|
} |
@ -1,354 +0,0 @@ |
|||||||
/** |
|
||||||
* 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 <https://www.gnu.org/licenses/>. |
|
||||||
*/ |
|
||||||
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<string> beginnerSynonyms; |
|
||||||
var private const array<string> normalSynonyms; |
|
||||||
var private const array<string> hardSynonyms; |
|
||||||
var private const array<string> suicidalSynonyms; |
|
||||||
var private const array<string> 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<Text> 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<VotingHandler.MapVoteGameConfig> 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<VotingHandler.MapVoteGameConfig> 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<Text> 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<GameInfo> 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<GameInfo>(_.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.") |
|
||||||
} |
|
Reference in new issue