Compare commits

..

59 Commits
master ... new

Author SHA1 Message Date
8583bef23d Fix formatting 2022-08-08 13:20:02 +07:00
6a13c509a2 Remove selfReference from Packages mutator
This variable didn't serve any really useful purpose, but has led to
game crashes. While adding proper cleanup could also solve these
crashes, there is no real point to keeping it at all.

NOTE: It's only purpose was to make sure only one instance of
corresponding mutator exists, but duplicates shouldn't happen in the
first place.
2022-08-08 04:54:52 +07:00
925f9a100d Change LevelCores to be loaded after base API 2022-08-03 10:30:26 +07:00
e1d61ed7e8 Change to use new TextAPI.IntoString() name 2022-07-25 02:52:03 +07:00
7f7221e1b7 Change to use ServerUnrealAPI instead 2022-07-18 02:03:08 +07:00
51beae9849 Change to support Acedia's new shutdown proces 2022-07-17 02:30:35 +07:00
83f137c063 Change to new AcediaCore collections 2022-07-15 20:37:04 +07:00
d3d6ae4627 Change to use UnrealAPI from _server 2022-07-15 04:35:29 +07:00
0bb7f89040 Change to support new AcediaCore changes 2022-07-12 04:49:37 +07:00
7721b520f5 Change code to adapt to iterator's refactor 2022-07-08 04:16:27 +07:00
a026b731ee Change to adapt to AcediaCore's changes 2022-07-05 01:47:52 +07:00
1e9e146a9f Change to supprot new AcediaCore's events 2022-07-02 05:27:56 +07:00
824a6e270a Change to account for AcediaCore's refactor 2022-06-23 02:52:05 +07:00
57f11ad644 Adapt to AcediaCore's Text changes 2022-06-19 00:23:42 +07:00
0274c0c31f Add OnModifyLogin() signal support 2022-01-12 02:16:34 +07:00
845930f8f1 Add game modes default config 2021-11-30 02:04:50 +07:00
d3afb611d8 Remove unused config entries 2021-11-30 01:18:05 +07:00
195c671df4 Fix Feature validator using wrong log warning 2021-11-07 16:04:29 +07:00
636e614ba4 Fix using old method name ToPlainString() 2021-11-06 02:34:35 +07:00
3681fcabf7 Add game modes support to Acedia 2021-11-05 03:43:36 +07:00
f94103f382 Change feature loading to work with new AcediaCore 2021-08-03 15:42:21 +07:00
e2eedc1d28 Change Feature loading to support new AcediaCore 2021-07-28 02:29:11 +07:00
280cf5af57 Remove outdated log messages 2021-07-28 02:28:38 +07:00
53f16e794c Add loading capabilities for acedia v0.1.dev2
In new Acedia version we must create `CoreService` and register
commands.
2021-02-26 22:06:01 +07:00
c42edfb88f Move most files to other Acedia packages 2020-07-18 19:21:03 +07:00
e513749119 Add stub for color docs 2020-07-18 02:40:07 +07:00
04bcbacc3e Add aliases docs 2020-07-18 02:39:39 +07:00
35ca9d6cb8 Update configs 2020-07-18 02:39:22 +07:00
d62e4d8e67 Update Acedia's Manifest 2020-07-18 02:39:05 +07:00
ba85300315 Change Acedia's loading process 2020-07-18 02:38:39 +07:00
507c7ba12c Fix Singleton not calling OnDestroyed() event 2020-07-18 02:37:20 +07:00
740f98edbd Add new APIs to a Global object 2020-07-18 02:36:01 +07:00
826e6272a4 Add proper method to disable a Feature 2020-07-18 02:35:25 +07:00
b6e75a44b0 Change ConnectionService to add it's listeners 2020-07-18 02:34:11 +07:00
cef551b660 Add global string to Text conversion 2020-07-18 02:32:52 +07:00
f6364229ed Add MemoryAPI 2020-07-18 02:32:11 +07:00
2c1a43f9d4 Add basic version of LoggerAPI 2020-07-18 02:31:58 +07:00
8845d69b1d Add ConsoleAPI 2020-07-18 02:31:39 +07:00
070211158e Add ColorAPI 2020-07-18 02:31:11 +07:00
e3f3296c2d Add ability to auto-create Singleton
Add an optional parameter that allows to auto-spawn
`Singleton` with `GetInstance()` call.
2020-07-18 02:30:55 +07:00
341c4aaf89 Add functionality to Service
Add ability to auto-launch and obtain instance of
a `Service` with a single command.

Add `Service`-specific events `Launch()` / `ShutDown()`

Add ability to auto-register required listeners on launch.
2020-07-18 02:28:40 +07:00
b954c718aa Change ConnectionService to be auto-started
To avoid launching `ConnectionService` all the time -
only do so when it is needed by someone
2020-07-18 02:25:02 +07:00
6125289040 Add events ability to start/shutdown Service
Events might rely on a particular `Service` to generate them,
this patch allows them to auto-launch/shutdown a service, depending on
whether anybody is listening to it's events.
2020-07-18 02:21:23 +07:00
5a14bb6d2c Add testing to Aliases subsystem 2020-07-18 02:17:47 +07:00
27c88b8707 Refactor Aliases subsystem 2020-07-18 02:17:22 +07:00
3849fd5c9d Refactor Testing subsystem 2020-07-18 02:16:41 +07:00
960e787de7 Refactor Text subsystem 2020-07-18 02:16:21 +07:00
fbc8abd48c Fix JSONAPI class description 2020-04-14 01:51:49 +07:00
4e07eb6c51 Fix deprecated Feature file after refactoring 2020-04-14 01:51:09 +07:00
c690572663 Add aliases functionality
Add aliases functionality to acedia.
Aliases allow to add "synonyms" to class names or other text values.
2020-04-14 01:49:45 +07:00
97569f9568 Change file structure of Acedia
Improves grouping of some files in project's directories.
2020-04-09 14:43:45 +07:00
54e87437c2 Add TODO list for a TestCase 2020-04-08 01:49:58 +07:00
3969eb3824 Refactor Feature
Make events functions `OnEnabled()` and `OnEnabled()`
protected rather than public.

Move initialization and clean up logic into
`OnCreated()` and `OnDestroyed()` event functions.
2020-04-08 01:49:17 +07:00
dd0bdc8904 Add events for a Singleton
Add `OnCreated()` and `OnDestroyed` event functions to provide
a more simple and safe way to handle these events.
2020-04-08 01:46:57 +07:00
53caa5766f Refactor JSON implementation
Change base class for JSON objects from `Object` to `AcediaObject`.

Rename some classes/functions/structures/variables to
be more compact and/or better convey their meaning.

Add appropriate API that contains constructors for JSON objects.
2020-04-08 01:43:11 +07:00
076ebe8fcf Add global namespace to Acedia actors and objects
We want all actors and objects defined in Acedia to share
a global namespace that provides an access to important variables
(such as `Acedia` reference) and functions.

We add a `Global` singleton and `AcediaActor` / `AcediaObject`
base classes with a quick accessor to it's instance (`_` / `_()`).
2020-04-08 01:36:29 +07:00
dd45e692fc Add unit tests for JSON implementation 2020-03-31 13:29:11 +07:00
a4a1c21cd7 Add basic unit test support
Add class that can automatically perform defined tests on user request.

Developers that wish to implemet unit tests for some functionality
must extend that class (`TestCase`) and add it to the manifest,
so that Acedia can read, register and later use it to perform tests.
2020-03-31 13:28:20 +07:00
427d6fea58 Add basic JSON data support
Add classes to store data that can be transferred in a text JSON format.
2020-03-31 13:23:55 +07:00
51 changed files with 1233 additions and 4695 deletions

View File

@ -1,255 +1,2 @@
[Acedia.FixDualiesCost]
; This feature fixes several issues, related to the selling price of both
; single and dual pistols, all originating from the existence of dual weapons.
; Most notable issue is the ability to "print" money by buying and
; selling pistols in a certain way.
;
; Fix only works with vanilla pistols, as it's unpredictable what
; custom ones can do and they can handle these issues on their own
; in a better way.
autoEnable=true
; Some issues involve possible decrease in pistols' price and
; don't lead to the exploit, but are still bugs and require fixing.
; If you have a Deagle in your inventory and then get another one
; (by either buying or picking it off the ground) - the price of resulting
; dual pistols will be set to the price of the last deagle,
; like the first one wasn't worth anything at all.
; In particular this means that (prices are off-perk for more clarity):
; 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of
; the cost (+750 do$h), you lose 250 do$h;
; 2. If you first buy a deagle (-500 do$h), then buy
; the second one (-500 do$h) and then sell them, you'll only get
; 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h;
; 3. So if you already have bought a deagle (-500 do$h),
; you can get a more expensive weapon by doing a stupid thing
; and first selling your Deagle (+375 do$h),
; then buying dual deagles (-1000 do$h).
; If you sell them after that, you'll gain 75% of the cost of
; dual deagles (+750 do$h), leaving you with losing only 375 do$h.
; Of course, situations described above are only relevant if you're planning
; to sell your weapons at some point and most players won't even
; notice these issues.
; But such an oversight still shouldn't exist in a game and we fix it by
; setting sell value of dualies as a sum of values of each pistol.
; Yet, fixing this issue leads to players having more expensive
; (while fairly priced) weapons than on vanilla, technically making
; the game easier. And some people might object to having that in
; a whitelisted bug-fixing feature.
; These people are, without a question, complete degenerates.
; But making mods for only non-mentally challenged isn't inclusive.
; So we add this option.
; Set it to 'false' if you only want to fix ammo printing
; and leave the rest of the bullshit as-is.
allowSellValueIncrease=true
[Acedia.FixAmmoSelling]
; This feature addressed an oversight in vanilla code that
; allows clients to sell weapon's ammunition.
; Due to the implementation of ammo selling, this allows cheaters to
; "print money" by buying and selling ammo over and over again.
autoEnable=true
; Due to how this fix works, players with level below 6 get charged less
; than necessary by the shop and this fix must take the rest of
; the cost by itself.
; The problem is, due to how ammo purchase is coded, low-level (<6 lvl)
; players can actually buy more ammo for "fixed" weapons than they can afford
; by filling ammo for one or all weapons.
; Setting this flag to 'true' will allow us to still take full cost
; from them, putting them in "debt" (having negative dosh amount).
; If you don't want to have players with negative dosh values on your server
; as a side-effect of this fix, then leave this flag as 'false',
; letting low level players buy ammo cheaper
; (but not cheaper than lvl6 could).
; NOTE: this issue doesn't affect level 6 players.
; NOTE #2: this fix does give players below level 6 some
; technical advantage compared to vanilla game, but this advantage
; cannot exceed benefits of having level 6.
allowNegativeDosh=false
[Acedia.FixInventoryAbuse]
; This feature addressed two issues with the inventory:
; 1. Players carrying amount of weapons that shouldn't be allowed by the
; weight limit.
; 2. Players carrying two variants of the same gun.
; For example carrying both M32 and camo M32.
; Single and dual version of the same weapon are also considered
; the same type of gun, so you shouldn't be able to carry
; both MK23 and dual MK23 or dual handcannons and golden handcannon.
; But cheaters do. But not with this fix.
autoEnable=true
; How often (in seconds) should we do inventory validation checks?
; You shouldn't really worry about performance, but there's also no need to
; do this check too often.
checkInterval=0.25
; For this fix to properly work, this array must contain an entry for
; every dual weapon in the game (like pistols, with single and dual versions).
; It's made configurable in case of custom dual weapons.
dualiesClasses=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup')
dualiesClasses=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup')
dualiesClasses=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup')
dualiesClasses=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup')
dualiesClasses=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup')
dualiesClasses=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup')
[Acedia.FixInfiniteNades]
; This feature fixes a vulnerability in a code of 'Frag' that can allow
; player to throw grenades even when he no longer has any.
; There's also no cooldowns on the throw, which can lead to a server crash.
autoEnable=true
; Setting this flag to 'true' will allow to throw grenades by calling
; 'ServerThrow' directly, as long as player has necessary ammo.
; This can allow some players to throw grenades much quicker than intended,
; therefore it's suggested to keep this flag set to 'false'.
ignoreTossFlags=false
[Acedia.FixDoshSpam]
; This feature addressed two dosh-related issues:
; 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash';
; 2. Breaking collision detection logic by stacking large amount of
; 'CashPickup' actors in one place, which allows one to either
; reach unintended locations or even instantly kill zeds.
;
; It fixes them by limiting speed, with which dosh can spawn, and
; allowing this limit to decrease when there's already too much dosh
; present on the map.
autoEnable=true
; Highest and lowest speed with which players can throw dosh wads.
; It'll be evenly spread between all players.
; For example, if speed is set to 6 and only one player will be spamming dosh,
; - he'll be able to throw 6 wads of dosh per second;
; but if all 6 players are spamming it, - each will throw only 1 per second.
; NOTE: these speed values can be exceeded, since a player is guaranteed
; to be able to throw at least one wad of dosh, if he didn't do so in awhile.
; NOTE #2: if maximum value is less than minimum one,
; the lowest (maximum one) will be used.
doshPerSecondLimitMax=50
doshPerSecondLimitMin=5
; Amount of dosh pickups on the map at which we must set dosh per second
; to 'doshPerSecondLimitMin'.
; We use 'doshPerSecondLimitMax' when there's no dosh on the map and
; scale linearly between them as it's amount grows.
criticalDoshAmount=25
[Acedia.FixSpectatorCrash]
; This feature attempts to prevent server crashes caused by someone
; quickly switching between being spectator and an active player.
autoEnable=true
; This fix will try to kick any player that switches between active player
; and cooldown faster than time (in seconds) in this value.
; NOTE: raising this value past default value of '0.25'
; won't actually improve crash prevention, but might cause regular players to
; get accidentally kicked.
spectatorChangeTimeout=0.25
; [ADVANCED] Don't change this setting unless you know what you're doing.
; Allows you to turn off server blocking.
; Players that don't respect timeout will still be kicked.
; This might be needed if this fix conflicts with another mutator
; that also changes 'numPlayers'.
; This option is necessary to block aggressive enough server crash
; attempts, but can cause compatibility issues with some mutators.
; It's highly recommended to rewrite such a mutator to be compatible instead.
; NOTE: fix should be compatible with most faked players-type mutators,
; since this it remembers the difference between amount of
; real players and 'numPlayers'.
; After unblocking, it sets 'numPlayers' to
; the current amount of real players + that difference.
; So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes
; 3 players + 3 (=6 numPlayers).
allowServerBlock=true
[Acedia.FixFFHack]
; This feature fixes a bug that can allow players to bypass server's
; friendly fire limitations and teamkill.
; Usual fixes apply friendly fire scale to suspicious damage themselves, which
; also disables some of the environmental damage.
; In oder to avoid that, this fix allows server owner to define precisely
; to what damage types to apply the friendly fire scaling.
; It should be all damage types related to projectiles.
autoEnable=true
; Defines a general rule for chosing whether or not to apply
; friendly fire scaling.
; This can be overwritten by exceptions ('alwaysScale' or 'neverScale').
; Enabling scaling by default without any exceptions in 'neverScale' will
; make this fix behave almost identically to Mutant's 'Explosives Fix Mutator'.
scaleByDefault=false
; Damage types, for which we should always reaaply friendly fire scaling.
alwaysScale=Class'KFMod.DamTypeCrossbuzzsawHeadShot'
alwaysScale=Class'KFMod.DamTypeCrossbuzzsaw'
alwaysScale=Class'KFMod.DamTypeFrag'
alwaysScale=Class'KFMod.DamTypePipeBomb'
alwaysScale=Class'KFMod.DamTypeM203Grenade'
alwaysScale=Class'KFMod.DamTypeM79Grenade'
alwaysScale=Class'KFMod.DamTypeM79GrenadeImpact'
alwaysScale=Class'KFMod.DamTypeM32Grenade'
alwaysScale=Class'KFMod.DamTypeLAW'
alwaysScale=Class'KFMod.DamTypeLawRocketImpact'
alwaysScale=Class'KFMod.DamTypeFlameNade'
alwaysScale=Class'KFMod.DamTypeFlareRevolver'
alwaysScale=Class'KFMod.DamTypeFlareProjectileImpact'
alwaysScale=Class'KFMod.DamTypeBurned'
alwaysScale=Class'KFMod.DamTypeTrenchgun'
alwaysScale=Class'KFMod.DamTypeHuskGun'
alwaysScale=Class'KFMod.DamTypeCrossbow'
alwaysScale=Class'KFMod.DamTypeCrossbowHeadShot'
alwaysScale=Class'KFMod.DamTypeM99SniperRifle'
alwaysScale=Class'KFMod.DamTypeM99HeadShot'
alwaysScale=Class'KFMod.DamTypeShotgun'
alwaysScale=Class'KFMod.DamTypeNailGun'
alwaysScale=Class'KFMod.DamTypeDBShotgun'
alwaysScale=Class'KFMod.DamTypeKSGShotgun'
alwaysScale=Class'KFMod.DamTypeBenelli'
alwaysScale=Class'KFMod.DamTypeSPGrenade'
alwaysScale=Class'KFMod.DamTypeSPGrenadeImpact'
alwaysScale=Class'KFMod.DamTypeSeekerSixRocket'
alwaysScale=Class'KFMod.DamTypeSeekerRocketImpact'
alwaysScale=Class'KFMod.DamTypeSealSquealExplosion'
alwaysScale=Class'KFMod.DamTypeRocketImpact'
alwaysScale=Class'KFMod.DamTypeBlowerThrower'
alwaysScale=Class'KFMod.DamTypeSPShotgun'
alwaysScale=Class'KFMod.DamTypeZEDGun'
alwaysScale=Class'KFMod.DamTypeZEDGunMKII'
alwaysScale=Class'KFMod.DamTypeZEDGunMKII'
; Damage types, for which we should never reaply friendly fire scaling.
;neverScale=Class'KFMod.???'
[Acedia.FixZedTimeLags]
; When zed time activates, game speed is immediately set to
; 'zedTimeSlomoScale' (0.2 by default), defined, like all other variables,
; in 'KFGameType'. Zed time lasts 'zedTimeDuration' seconds (3.0 by default),
; but during last 'zedTimeDuration * 0.166' seconds (by default 0.498)
; it starts to speed back up, causing game speed to update every tick.
; This makes animations look more smooth when exiting zed-time.
; However, updating speed every tick for that purpose seems like
; an overkill and, combined with things like
; increased tick rate, certain open maps and increased zed limit,
; it can lead to noticable lags at the end of the zed time.
; This fix limits amount of actual game speed updates, alleviating the issue.
;
; As a side effect it also fixes an issue where during zed time speed up
; 'zedTimeSlomoScale' was assumed to be default value of '0.2'.
; Now zed time will behave correctly with mods that change 'zedTimeSlomoScale'.
autoEnable=true
; Maximum amount of game speed updates upon leaving zed time.
; 2 or 3 seem to provide a good enough result that,
; i.e. it should be hard to notice difference with vanilla game behavior.
; 1 is a smallest possible value, resulting in effectively removing any
; smooting via speed up, simply changing speed from
; the slowest (0.2) to the highest.
; For the reference: on servers with default 30 tick rate there's usually
; about 13 updates total (without this fix).
maxGameSpeedUpdatesAmount=3
; [ADVANCED] Don't change this setting unless you know what you're doing.
; Compatibility setting that allows to keep 'GameInfo' 's 'Tick' event
; from being disabled.
; Useful when running Acedia along with custom 'GameInfo'
; (that isn't 'KFGameType') that relies on 'Tick' event.
; Note, however, that in order to keep this fix working properly,
; it's on you to make sure 'KFGameType.Tick()' logic isn't executed.
disableTick=true
[Acedia.Packages]
useGameModes=false

View File

@ -0,0 +1,7 @@
[hard GameMode]
title={$green Hard difficulty}
difficulty=normal
[hell GameMode]
title={$crimson Hell On Earth}
difficulty=hoe

View File

@ -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"
}

View File

@ -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
{
}

View File

@ -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'
}

View File

@ -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
}

View File

@ -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'.

View File

@ -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'
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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'
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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
{
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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
{
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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')
}

View File

@ -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
}

View File

@ -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'
}

View File

@ -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
}

View 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.")
}

View 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.")
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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
View 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.")
}

View File

@ -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
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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
}

View File

@ -1,6 +1,6 @@
/**
* This actor's role is to add Acedia mutator on listen and dedicated servers.
* Copyright 2019 Anton Tarasenko
* Copyright 2019-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -23,9 +23,8 @@ class StartUp extends Actor;
function PreBeginPlay()
{
super.PreBeginPlay();
if (level != none && level.game != none)
{
level.game.AddMutator(string(class'Acedia'));
if (level != none && level.game != none) {
level.game.AddMutator(string(class'Packages'));
}
Destroy();
}

View 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.")
}