Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
8583bef23d | |||
6a13c509a2 | |||
925f9a100d | |||
e1d61ed7e8 | |||
7f7221e1b7 | |||
51beae9849 | |||
83f137c063 | |||
d3d6ae4627 | |||
0bb7f89040 | |||
7721b520f5 | |||
a026b731ee | |||
1e9e146a9f | |||
824a6e270a | |||
57f11ad644 | |||
0274c0c31f | |||
845930f8f1 | |||
d3afb611d8 | |||
195c671df4 | |||
636e614ba4 | |||
3681fcabf7 | |||
f94103f382 | |||
e2eedc1d28 | |||
280cf5af57 | |||
53f16e794c | |||
c42edfb88f | |||
e513749119 | |||
04bcbacc3e | |||
35ca9d6cb8 | |||
d62e4d8e67 | |||
ba85300315 | |||
507c7ba12c | |||
740f98edbd | |||
826e6272a4 | |||
b6e75a44b0 | |||
cef551b660 | |||
f6364229ed | |||
2c1a43f9d4 | |||
8845d69b1d | |||
070211158e | |||
e3f3296c2d | |||
341c4aaf89 | |||
b954c718aa | |||
6125289040 | |||
5a14bb6d2c | |||
27c88b8707 | |||
3849fd5c9d | |||
960e787de7 | |||
fbc8abd48c | |||
4e07eb6c51 | |||
c690572663 | |||
97569f9568 | |||
54e87437c2 | |||
3969eb3824 | |||
dd0bdc8904 | |||
53caa5766f | |||
076ebe8fcf | |||
dd45e692fc | |||
a4a1c21cd7 | |||
427d6fea58 |
@ -1,255 +1,2 @@
|
|||||||
[Acedia.FixDualiesCost]
|
[Acedia.Packages]
|
||||||
; This feature fixes several issues, related to the selling price of both
|
useGameModes=false
|
||||||
; 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
|
|
7
config/AcediaGameModes.ini
Normal file
7
config/AcediaGameModes.ini
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[hard GameMode]
|
||||||
|
title={$green Hard difficulty}
|
||||||
|
difficulty=normal
|
||||||
|
|
||||||
|
[hell GameMode]
|
||||||
|
title={$crimson Hell On Earth}
|
||||||
|
difficulty=hoe
|
@ -1,131 +0,0 @@
|
|||||||
/**
|
|
||||||
* Main and only Acedia mutator used for initialization of necessary services
|
|
||||||
* and providing access to mutator events' calls.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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"
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Facilitates some core replicated functions between client and server.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class AcediaReplicationInfo extends ReplicationInfo;
|
|
||||||
|
|
||||||
var public PlayerController linkOwner;
|
|
||||||
|
|
||||||
replication
|
|
||||||
{
|
|
||||||
reliable if (role == ROLE_Authority)
|
|
||||||
linkOwner;
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
/**
|
|
||||||
* Event generator for events, related to broadcasting messages
|
|
||||||
* through standard Unreal Script means:
|
|
||||||
* 1. text messages, typed by a player;
|
|
||||||
* 2. localized messages, identified by a LocalMessage class and id.
|
|
||||||
* Allows to make decisions whether or not to propagate certain messages.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,197 +0,0 @@
|
|||||||
/**
|
|
||||||
* 'BroadcastHandler' class that used by Acedia to catch
|
|
||||||
* broadcasting events. For Acedia to work properly it needs to be added to
|
|
||||||
* the very beginning of the broadcast handlers' chain.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* Listener for events, related to broadcasting messages
|
|
||||||
* through standard Unreal Script means:
|
|
||||||
* 1. text messages, typed by a player;
|
|
||||||
* 2. localized messages, identified by a LocalMessage class and id.
|
|
||||||
* Allows to make decisions whether or not to propagate certain messages.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'.
|
|
@ -1,133 +0,0 @@
|
|||||||
/**
|
|
||||||
* One of the two classes that make up a core of event system in Acedia.
|
|
||||||
*
|
|
||||||
* 'Events' (or it's child) class shouldn't be instantiated.
|
|
||||||
* Usually module would provide '...Events' class that defines
|
|
||||||
* certain set of static functions that can generate event calls to
|
|
||||||
* all it's active listeners.
|
|
||||||
* If you're simply using modules someone made, -
|
|
||||||
* you don't need to bother yourself with further specifics.
|
|
||||||
* If you wish to create your own event generator,
|
|
||||||
* then first create a '...ListenerBase' object
|
|
||||||
* (more about it in the description of 'Listener' class)
|
|
||||||
* and set 'relatedListener' variable to point to it's class.
|
|
||||||
* Then for each event create a caller function in your 'Event' class,
|
|
||||||
* following this template:
|
|
||||||
* ____________________________________________________________________________
|
|
||||||
* | static function CallEVENT_NAME(<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'
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
/**
|
|
||||||
* Feature represents a certain subset of Acedia's functionality that
|
|
||||||
* can be enabled or disabled, according to server owner's wishes.
|
|
||||||
* In the current version of Acedia enabling or disabling a feature requires
|
|
||||||
* manually editing configuration file and restarting a server.
|
|
||||||
* Factually feature is just a collection of settings with one universal
|
|
||||||
* 'isActive' setting that tells Acedia whether or not to load a feature.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* This actor attaches itself to the ammo boxes
|
|
||||||
* and imitates their collision to let us detect when they're picked up.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
}
|
|
@ -1,395 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature addressed an oversight in vanilla code that
|
|
||||||
* allows clients to sell weapon's ammunition.
|
|
||||||
* Moreover, when being sold, ammunition cost is always multiplied by 0.75,
|
|
||||||
* without taking into an account possible discount a player might have.
|
|
||||||
* This allows cheaters to "print money" by buying and selling ammo over and
|
|
||||||
* over again ammunition for some weapons,
|
|
||||||
* notably pipe bombs (74% discount for lvl6 demolition)
|
|
||||||
* and crossbow (42% discount for lvl6 sharpshooter).
|
|
||||||
*
|
|
||||||
* This feature fixes this problem by setting 'pickupClass' variable in
|
|
||||||
* potentially abusable weapons to our own value that won't receive a discount.
|
|
||||||
* Luckily for us, it seems that pickup spawn and discount checks are the only
|
|
||||||
* two place where variable is directly checked in a vanilla game's code
|
|
||||||
* ('default.pickupClass' is used everywhere else),
|
|
||||||
* so we can easily deal with the side effects of such change.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that
|
|
||||||
* of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_CamoM32Pickup extends CamoM32Pickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 42
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for xbow to that
|
|
||||||
* of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_CrossbowPickup extends CrossbowPickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 11.6
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for m79 to that
|
|
||||||
* of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_GoldenM79Pickup extends GoldenM79Pickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 7
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for LAW to that
|
|
||||||
* of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_LAWPickup extends LAWPickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 21
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that
|
|
||||||
* of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_M32Pickup extends M32Pickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 42
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for M79 to that
|
|
||||||
* of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_M79Pickup extends M79Pickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 7
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for pipes
|
|
||||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_PipeBombPickup extends PipeBombPickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 195
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for
|
|
||||||
* orca grnade launcher to that of a level 6 player
|
|
||||||
* and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_SPGrenadePickup extends SPGrenadePickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 7
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for harpoon
|
|
||||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_SealSquealPickup extends SealSquealPickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 21
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for seeker
|
|
||||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class FixAmmoSellingClass_SeekerSixPickup extends SeekerSixPickup;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
AmmoCost = 10.5
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
/**
|
|
||||||
* Overloaded mutator events listener to register every new
|
|
||||||
* spawned weapon and ammo pickup.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,252 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature addressed two dosh-related issues:
|
|
||||||
* 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash';
|
|
||||||
* 2. Breaking collision detection logic by stacking large amount of
|
|
||||||
* 'CashPickup' actors in one place, which allows one to either
|
|
||||||
* reach unintended locations or even instantly kill zeds.
|
|
||||||
*
|
|
||||||
* It fixes them by limiting speed, with which dosh can spawn, and
|
|
||||||
* allowing this limit to decrease when there's already too much dosh
|
|
||||||
* present on the map.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* Overloaded mutator events listener to catch and, possibly,
|
|
||||||
* prevent spawning dosh actors.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* This rule detects any pickup events to allow us to
|
|
||||||
* properly record and/or fix pistols' prices.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
{
|
|
||||||
}
|
|
@ -1,454 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature fixes several issues related to the selling price of both
|
|
||||||
* single and dual pistols, all originating from the existence of dual weapons.
|
|
||||||
* Most notable issue is the ability to "print" money by buying and
|
|
||||||
* selling pistols in a certain way.
|
|
||||||
*
|
|
||||||
* It fixes all of the issues by manually setting pistols'
|
|
||||||
* 'SellValue' variables to proper values.
|
|
||||||
* Fix only works with vanilla pistols, as it's unpredictable what
|
|
||||||
* custom ones can do and they can handle these issues on their own
|
|
||||||
* in a better way.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* Overloaded mutator events listener to catch when pistol-type weapons
|
|
||||||
* (single or dual) are spawned and to correct their price.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
/**
|
|
||||||
* This rule detects suspicious attempts to deal damage and
|
|
||||||
* applies friendly fire scaling according to 'FixFFHack's rules.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
{
|
|
||||||
}
|
|
@ -1,152 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature fixes a bug that can allow players to bypass server's
|
|
||||||
* friendly fire limitations and teamkill.
|
|
||||||
* Usual fixes apply friendly fire scale to suspicious damage themselves, which
|
|
||||||
* also disables some of the environmental damage.
|
|
||||||
* In order to avoid that, this fix allows server owner to define precisely
|
|
||||||
* to what damage types to apply the friendly fire scaling.
|
|
||||||
* It should be all damage types related to projectiles.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,233 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature fixes a vulnerability in a code of 'Frag' that can allow
|
|
||||||
* player to throw grenades even when he no longer has any.
|
|
||||||
* There's also no cooldowns on the throw, which can lead to a server crash.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* A replacement for vanilla 'FragFire' fire class for 'Frag' weapon that
|
|
||||||
* adds additional ammo check in accordance to ammo records
|
|
||||||
* of 'FixInfiniteNades'.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
{
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Overloaded mutator events listener to catch
|
|
||||||
* new 'Frag' weapons and 'Nade' projectiles.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
{
|
|
||||||
}
|
|
@ -1,225 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature addressed two inventory issues:
|
|
||||||
* 1. Players carrying amount of weapons that shouldn't be allowed by the
|
|
||||||
* weight limit.
|
|
||||||
* 2. Players carrying two variants of the same gun.
|
|
||||||
* For example carrying both M32 and camo M32.
|
|
||||||
* Single and dual version of the same weapon are also considered
|
|
||||||
* the same gun, so you can't carry both MK23 and dual MK23 or
|
|
||||||
* dual handcannons and golden handcannon.
|
|
||||||
*
|
|
||||||
* It fixes them by doing repeated checks to find violations of those rules
|
|
||||||
* and destroys all droppable weapons of people that use this exploit.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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')
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* Overloaded broadcast events listener to catch the moment
|
|
||||||
* someone becomes alive player / spectator.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
}
|
|
@ -1,291 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature attempts to prevent server crashes caused by someone
|
|
||||||
* quickly switching between being spectator and an active player.
|
|
||||||
*
|
|
||||||
* We do so by disconnecting players who start switching way too fast
|
|
||||||
* (more than twice in a short amount of time) and temporarily faking a large
|
|
||||||
* amount of players on the server, to prevent such spam from affecting the server.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,188 +0,0 @@
|
|||||||
/**
|
|
||||||
* This feature fixes lags caused by a zed time that can occur
|
|
||||||
* on some maps when a lot of zeds are present at once.
|
|
||||||
* As a side effect it also fixes an issue where during zed time speed up
|
|
||||||
* 'zedTimeSlomoScale' was assumed to be default value of '0.2'.
|
|
||||||
* Now zed time will behave correctly with mods that
|
|
||||||
* change 'zedTimeSlomoScale'.
|
|
||||||
* Copyright 2020 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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
|
|
||||||
}
|
|
400
sources/GameModes/BaseGameMode.uc
Normal file
400
sources/GameModes/BaseGameMode.uc
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
/**
|
||||||
|
* Base class for a game mode config, contains all the information Acedia's
|
||||||
|
* game modes must have, including settings
|
||||||
|
* (`includeFeature`, `includeFeatureAs` and `excludeFeature`)
|
||||||
|
* for picking used `Feature`s.
|
||||||
|
*
|
||||||
|
* Contains three types of methods:
|
||||||
|
* 1. Getters for its values;
|
||||||
|
* 2. `UpdateFeatureArray()` method for updating list of `Feature`s to
|
||||||
|
* be used based on game info's settings;
|
||||||
|
* 3. `Report...()` methods that perform various validation checks
|
||||||
|
* (and log them) on config data.
|
||||||
|
* Copyright 2021-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <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.")
|
||||||
|
}
|
195
sources/GameModes/GameMode.uc
Normal file
195
sources/GameModes/GameMode.uc
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* The only implementation for `BaseGameMode` suitable for standard
|
||||||
|
* killing floor game types.
|
||||||
|
* Copyright 2021-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <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.")
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
/**
|
|
||||||
* One of the two classes that make up a core of event system in Acedia.
|
|
||||||
*
|
|
||||||
* 'Listener' (or it's child) class shouldn't be instantiated.
|
|
||||||
* Usually module would provide '...ListenerBase' class that defines
|
|
||||||
* certain set of static functions, corresponding to events it can listen to.
|
|
||||||
* In order to handle those events you must create it's child class and
|
|
||||||
* override said functions. But they will only be called if
|
|
||||||
* 'SetActive(true)' is called for that child class.
|
|
||||||
* To create you own '...ListenerBase' class you need to define
|
|
||||||
* a static function for each event you wish it to catch and
|
|
||||||
* set 'relatedEvents' variable to point at the 'Events' class
|
|
||||||
* that will generate your events.
|
|
||||||
* For concrete example look at
|
|
||||||
* 'ConnectionEvents' and 'ConnectionListenerBase'.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class Listener extends 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'
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Manifest is meant to describe contents of the package (mutator file)
|
|
||||||
* as well as what actors/objects should be automatically created when package
|
|
||||||
* is loaded and what event listeners should be activated.
|
|
||||||
* Currently only implements automatic listener activation.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
/**
|
|
||||||
* Event generator that repeats events of a mutator.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class MutatorEvents extends Events
|
|
||||||
abstract;
|
|
||||||
|
|
||||||
static function bool CallCheckReplacement(Actor other, out byte isSuperRelevant)
|
|
||||||
{
|
|
||||||
local int i;
|
|
||||||
local bool result;
|
|
||||||
local array< class<Listener> > listeners;
|
|
||||||
listeners = GetListeners();
|
|
||||||
for (i = 0; i < listeners.length; i += 1)
|
|
||||||
{
|
|
||||||
result = class<MutatorListenerBase>(listeners[i])
|
|
||||||
.static.CheckReplacement(other, isSuperRelevant);
|
|
||||||
if (!result) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function bool CallMutate(string command, PlayerController sendingPlayer)
|
|
||||||
{
|
|
||||||
local int i;
|
|
||||||
local bool result;
|
|
||||||
local array< class<Listener> > listeners;
|
|
||||||
listeners = GetListeners();
|
|
||||||
for (i = 0; i < listeners.length;i += 1)
|
|
||||||
{
|
|
||||||
result = class<MutatorListenerBase>(listeners[i])
|
|
||||||
.static.Mutate(command, sendingPlayer);
|
|
||||||
if (!result) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
relatedListener = class'MutatorListenerBase'
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/**
|
|
||||||
* Listener for events, normally propagated by mutators.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class MutatorListenerBase extends Listener
|
|
||||||
abstract;
|
|
||||||
|
|
||||||
// This event is called whenever 'CheckReplacement'
|
|
||||||
// check is propagated through mutators.
|
|
||||||
// If one of the listeners returns 'false', -
|
|
||||||
// it will be treated just like a mutator returning 'false'
|
|
||||||
// in 'CheckReplacement' and
|
|
||||||
// this method won't be called for remaining active listeners.
|
|
||||||
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This event is called whenever 'Mutate' is propagated through mutators.
|
|
||||||
// If one of the listeners returns 'false', -
|
|
||||||
// this method won't be called for remaining active listeners or mutators.
|
|
||||||
// If all listeners return 'true', -
|
|
||||||
// mutate command will be further propagated to the rest of the mutators.
|
|
||||||
static function bool Mutate(string command, PlayerController sendingPlayer)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
relatedEvents = class'MutatorEvents'
|
|
||||||
}
|
|
272
sources/Packages.uc
Normal file
272
sources/Packages.uc
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* Main and only Acedia mutator used for loading Acedia packages
|
||||||
|
* and providing access to mutator events' calls.
|
||||||
|
* Name is chosen to make config files more readable.
|
||||||
|
* Copyright 2020-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <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.")
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* Parent class for all services used in Acedia.
|
|
||||||
* Currently simply makes itself server-only.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
class Service extends Singleton
|
|
||||||
abstract;
|
|
||||||
|
|
||||||
defaultproperties
|
|
||||||
{
|
|
||||||
remoteRole = ROLE_None
|
|
||||||
DrawType = DT_None
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* Event generator for 'ConnectionService'.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Listener for events generated by 'ConnectionService'.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
/**
|
|
||||||
* This service tracks current connections to the server
|
|
||||||
* as well as their basic information,
|
|
||||||
* like IP or steam ID of connecting player.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Overloaded mutator events listener to catch connecting players.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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'
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
/**
|
|
||||||
* Singleton is an auxiliary class, meant to be used as a base for others,
|
|
||||||
* that allows for only one instance of it to exist.
|
|
||||||
* To make sure your child class properly works, either don't overload
|
|
||||||
* 'PreBeginPlay' or make sure to call it's parent's version.
|
|
||||||
* Copyright 2019 Anton Tarasenko
|
|
||||||
*------------------------------------------------------------------------------
|
|
||||||
* This file is part of Acedia.
|
|
||||||
*
|
|
||||||
* Acedia is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Acedia is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Acedia. If not, see <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,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* This actor's role is to add Acedia mutator on listen and dedicated servers.
|
* This actor's role is to add Acedia mutator on listen and dedicated servers.
|
||||||
* Copyright 2019 Anton Tarasenko
|
* Copyright 2019-2022 Anton Tarasenko
|
||||||
*------------------------------------------------------------------------------
|
*------------------------------------------------------------------------------
|
||||||
* This file is part of Acedia.
|
* This file is part of Acedia.
|
||||||
*
|
*
|
||||||
@ -23,9 +23,8 @@ class StartUp extends Actor;
|
|||||||
function PreBeginPlay()
|
function PreBeginPlay()
|
||||||
{
|
{
|
||||||
super.PreBeginPlay();
|
super.PreBeginPlay();
|
||||||
if (level != none && level.game != none)
|
if (level != none && level.game != none) {
|
||||||
{
|
level.game.AddMutator(string(class'Packages'));
|
||||||
level.game.AddMutator(string(class'Acedia'));
|
|
||||||
}
|
}
|
||||||
Destroy();
|
Destroy();
|
||||||
}
|
}
|
||||||
|
354
sources/VotingHandlerAdapter.uc
Normal file
354
sources/VotingHandlerAdapter.uc
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
/**
|
||||||
|
* Acedia currently lacks its own means to provide a map/mode voting
|
||||||
|
* (and new voting mod with proper GUI would not be whitelisted anyway).
|
||||||
|
* This is why this class was made - to inject existing voting handlers with
|
||||||
|
* data from Acedia's game modes.
|
||||||
|
* Requires `GameInfo`'s voting handler to be derived from
|
||||||
|
* `XVotingHandler`, which is satisfied by pretty much every used handler.
|
||||||
|
* Copyright 2021-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <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
Block a user