From 61d1c0b8780543bfddbb975c6ad018aeac55bd90 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 6 Jun 2022 04:05:40 +0700 Subject: [PATCH] Add inventory frontend support for Killing Floor --- System | 118 ++ config/AcediaSystem_KF1Frontend.ini | 12 + .../BaseClasses/Frontend/BaseFrontend.uc | 17 + .../BaseClasses/Frontend/EInterface.uc | 16 +- .../Frontend/Templates/ATemplatesComponent.uc | 82 + .../KillingFloor/Frontend/KFFrontend.uc | 1 + .../KF1Frontend/BaseImplementation/EKFAmmo.uc | 464 ++++++ .../BaseImplementation/EKFFlashlightAmmo.uc | 298 ++++ .../BaseImplementation/EKFInventory.uc | 1401 +++++++++++++---- .../BaseImplementation/EKFItemTemplateInfo.uc | 2 +- .../BaseImplementation/EKFMedicAmmo.uc | 285 ++++ .../BaseImplementation/EKFSyringeAmmo.uc | 288 ++++ .../BaseImplementation/EKFUnknownItem.uc | 168 ++ .../BaseImplementation/EKFWeapon.uc | 158 +- sources/Gameplay/KF1Frontend/KF1_Frontend.uc | 3 +- .../Trading/KF1_TemplatesComponent.uc | 215 +++ sources/Players/EPlayer.uc | 2 +- sources/Players/Inventory/EAmmo.uc | 259 +++ sources/Players/Inventory/EInventory.uc | 161 +- sources/Players/Inventory/EItem.uc | 10 +- sources/Players/Inventory/EWeapon.uc | 57 + sources/Text/MutableText.uc | 11 + sources/Types/AcediaActor.uc | 5 + sources/Unreal/InventoryAPI/InventoryAPI.uc | 530 +++++++ .../Unreal/InventoryAPI/InventoryService.uc | 128 ++ sources/Unreal/Tests/TEST_UnrealAPI.uc | 32 +- sources/Unreal/UnrealAPI.uc | 92 +- 27 files changed, 4342 insertions(+), 473 deletions(-) create mode 100644 System create mode 100644 config/AcediaSystem_KF1Frontend.ini create mode 100644 sources/Gameplay/BaseClasses/Frontend/Templates/ATemplatesComponent.uc create mode 100644 sources/Gameplay/KF1Frontend/BaseImplementation/EKFAmmo.uc create mode 100644 sources/Gameplay/KF1Frontend/BaseImplementation/EKFFlashlightAmmo.uc create mode 100644 sources/Gameplay/KF1Frontend/BaseImplementation/EKFMedicAmmo.uc create mode 100644 sources/Gameplay/KF1Frontend/BaseImplementation/EKFSyringeAmmo.uc create mode 100644 sources/Gameplay/KF1Frontend/BaseImplementation/EKFUnknownItem.uc create mode 100644 sources/Gameplay/KF1Frontend/Trading/KF1_TemplatesComponent.uc create mode 100644 sources/Players/Inventory/EAmmo.uc create mode 100644 sources/Players/Inventory/EWeapon.uc create mode 100644 sources/Unreal/InventoryAPI/InventoryAPI.uc create mode 100644 sources/Unreal/InventoryAPI/InventoryService.uc diff --git a/System b/System new file mode 100644 index 0000000..42678aa --- /dev/null +++ b/System @@ -0,0 +1,118 @@ +[Editor.EditorEngine] +EditPackages=Core +EditPackages=Engine +EditPackages=Fire +EditPackages=Editor +EditPackages=UnrealEd +EditPackages=IpDrv +EditPackages=UWeb +EditPackages=GamePlay +EditPackages=UnrealGame +EditPackages=XGame +EditPackages=XInterface +EditPackages=XAdmin +EditPackages=XWebAdmin +EditPackages=GUI2K4 +EditPackages=xVoting +EditPackages=UTV2004c +EditPackages=UTV2004s +EditPackages=ROEffects +EditPackages=ROEngine +EditPackages=ROInterface +EditPackages=Old2k4 +EditPackages=KFMod +EditPackages=KFChar +EditPackages=KFGui +EditPackages=GoodKarma +EditPackages=KFMutators +EditPackages=KFStoryGame +EditPackages=KFStoryUI +EditPackages=SideShowScript +EditPackages=FrightScript +CutdownPackages=Core +CutdownPackages=Editor +CutdownPackages=Engine +CutdownPackages=Fire +CutdownPackages=GamePlay +CutdownPackages=GUI2K4 +CutdownPackages=IpDrv +CutdownPackages=Onslaught +CutdownPackages=UnrealEd +CutdownPackages=UnrealGame +CutdownPackages=UWeb +CutdownPackages=XAdmin +CutdownPackages=XEffects +CutdownPackages=XInterface +CutdownPackages=XPickups +CutdownPackages=XWebAdmin +CutdownPackages=XVoting + + +[Engine.Engine] +RenderDevice=D3D9Drv.D3D9RenderDevice +AudioDevice=ALAudio.ALAudioSubsystem +NetworkDevice=IpDrv.TcpNetDriver +DemoRecordingDevice=Engine.DemoRecDriver +Console=KFMod.KFConsole +GUIController=KFGUI.KFGUIController +StreamPlayer=Engine.StreamInteraction +Language=int +Product=KillingFloor +GameEngine=Engine.GameEngine +EditorEngine=Editor.EditorEngine +DefaultGame=KFMod.KFGameType +DefaultServerGame=KFMod.KFGameType +ViewportManager=WinDrv.WindowsClient +Render=Render.Render +Input=Engine.Input +Canvas=Engine.Canvas +DetectedVideoMemory=0 +ServerReadsStdin=False + + +[Core.System] +PurgeCacheDays=30 +SavePath=..\Save +CachePath=../Cache +CacheExt=.uxx +CacheRecordPath=../System/*.ucl +MusicPath=../Music +SpeechPath=../Speech +Paths=../System/*.u +Paths=../Maps/*.rom +Paths=../TestMaps/*.rom +Paths=../Textures/*.utx +Paths=../Sounds/*.uax +Paths=../Music/*.umx +Paths=../StaticMeshes/*.usx +Paths=../Animations/*.ukx +Paths=../Saves/*.uvx +Paths=../Textures/Old2k4/*.utx +Paths=../Sounds/Old2k4/*.uax../Music/Old2k4/*.umx +Paths=../StaticMeshes/Old2k4/*.usx +Paths=../Animations/Old2k4/*.ukx +Paths=../KarmaData/Old2k4/*.ka +Suppress=DevLoad +Suppress=DevSave +Suppress=DevNetTraffic +Suppress=DevGarbage +Suppress=DevKill +Suppress=DevReplace +Suppress=DevCompile +Suppress=DevBind +Suppress=DevBsp +Suppress=DevNet +Suppress=DevLIPSinc +Suppress=DevKarma +Suppress=RecordCache +Suppress=MapVoteDebug +Suppress=Init +Suppress=MapVote +Suppress=VoiceChat +Suppress=ChatManager +Suppress=Time + + +[ROFirstRun] +ROFirstRun=1094 + diff --git a/config/AcediaSystem_KF1Frontend.ini b/config/AcediaSystem_KF1Frontend.ini new file mode 100644 index 0000000..a7837de --- /dev/null +++ b/config/AcediaSystem_KF1Frontend.ini @@ -0,0 +1,12 @@ +; Every single option in this config should be considered [ADVANCED] +[AcediaCore.KFDualiesTool] +; This array defines what weapons Acedia's Killing Floor frontend considers as +; pairs of single - dual versions. +; If you are adding some custom dual weapons to your server, then they should +; also be added here until a better way is implemented. +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') \ No newline at end of file diff --git a/sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc b/sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc index 83d3624..3f7da48 100644 --- a/sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc +++ b/sources/Gameplay/BaseClasses/Frontend/BaseFrontend.uc @@ -21,6 +21,23 @@ class BaseFrontend extends AcediaObject abstract; +var private config class templatesClass; +var public ATemplatesComponent templates; + +protected function Constructor() +{ + if (templatesClass != none) { + templates = ATemplatesComponent(_.memory.Allocate(templatesClass)); + } +} + +protected function Finalizer() +{ + _.memory.Free(templates); + templates = none; +} + defaultproperties { + templatesClass = none } \ No newline at end of file diff --git a/sources/Gameplay/BaseClasses/Frontend/EInterface.uc b/sources/Gameplay/BaseClasses/Frontend/EInterface.uc index cb580ea..234521c 100644 --- a/sources/Gameplay/BaseClasses/Frontend/EInterface.uc +++ b/sources/Gameplay/BaseClasses/Frontend/EInterface.uc @@ -8,7 +8,7 @@ * once (including those of the same type). Deallocating one such reference * should not affect referred entity in any way and should be treated as simply * getting rid of one of the references. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -42,6 +42,20 @@ public function EInterface Copy() return none; } +/** + * Checks if entity, referred to by the caller `EInterface` supports + * `newInterfaceClass` interface class. + * + * @param newInterfaceClass Class of the `EInterface`, for which method + * should check support by entity, referred to by the caller `EInterface`. + * @return `true` if referred entity supports `newInterfaceClass` and + * `false` otherwise. + */ +public function bool Supports(class newInterfaceClass) +{ + return false; +} + /** * Provides `EInterface` reference of given class `newInterfaceClass` to * the entity, referred to by the caller `EInterface` (if supported). diff --git a/sources/Gameplay/BaseClasses/Frontend/Templates/ATemplatesComponent.uc b/sources/Gameplay/BaseClasses/Frontend/Templates/ATemplatesComponent.uc new file mode 100644 index 0000000..77a8366 --- /dev/null +++ b/sources/Gameplay/BaseClasses/Frontend/Templates/ATemplatesComponent.uc @@ -0,0 +1,82 @@ +/** + * Subset of functionality for dealing with everything related to templates. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class ATemplatesComponent extends AcediaObject + abstract; + +/** + * Returns `true` if list of items named `listName` exists and + * `false` otherwise. + * + * This method is necessary, since `GetItemList()` does not allow to + * distinguish between empty and non-existing item list. + * + * @param listName Name of the list to check for whether it exists. + * @return `true` if list named `listName` exists and `false` otherwise. + * Always returns `false` if `listName` equals `none`. + */ +public function bool ItemListExists(Text listName) +{ + return false; +} + +/** + * Returns array with templates of items belonging to the `listName` list. + * + * All implementations must support: + * 1. "all weapons" / "weapons" (both names + * should refer to the same list) list with templates of all weapons in + * the game; + * 2. "trading weapons" list with names of all weapons available for trade + * in one way or another (even if they are not all tradable in + * all shops / for all players). + * + * @param listName Name of the list to return templates for. + * In case a name of inexistent list is specified - method does nothing. + * @return Array of templates in the list, specified with `listName`. + * All of the `Text`s in the returned array are guaranteed to be `none`. + * When incorrect `listName` is specified - empty array is returned + * (which can also happen if specified list is empty). + */ +public function array GetItemList(Text listName) +{ + local array emptyArray; + return emptyArray; +} + +/** + * Returns array that is listing all available lists of item templates. + * + * All implementations must include "all weapons" and "trading weapons" lists. + * + * @return Array with names of all available lists. + * All of the `Text`s in the returned array are guaranteed to be `none`. + * If a certain list has several names (like "all weapons" / "weapons"), + * only one of these names (guaranteed to always be the same between calls) + * will be included. + */ +public function array GetAvailableLists() +{ + local array emptyArray; + return emptyArray; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc index 75310bd..e7d035f 100644 --- a/sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc +++ b/sources/Gameplay/BaseClasses/KillingFloor/Frontend/KFFrontend.uc @@ -25,6 +25,7 @@ var public ATradingComponent trading; protected function Constructor() { + super.Constructor(); if (tradingClass != none) { trading = ATradingComponent(_.memory.Allocate(tradingClass)); } diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFAmmo.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFAmmo.uc new file mode 100644 index 0000000..776f39a --- /dev/null +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFAmmo.uc @@ -0,0 +1,464 @@ +/** + * Implementation of `EAmmo` for classic Killing Floor weapons that changes + * as little as possible and only on request from another mod, otherwise not + * altering gameplay at all. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class EKFAmmo extends EAmmo; + +var private NativeActorRef ammunitionReference; + +protected function Finalizer() +{ + _.memory.Free(ammunitionReference); + ammunitionReference = none; +} + +/** + * Creates new `EKFAmmo` that refers to the `ammunitionInstance` ammunition. + * + * @param ammunitionInstance Native ammunition instance that new `EKFAmmo` + * will represent. + * @return New `EKFAmmo` that represents given `ammunitionInstance`. + * `none` iff `ammunitionInstance` is either `none` or + * is an unused flash light ammunition + * (has `class'KFMod.FlashlightAmmo'` class). + */ +public final static /*unreal*/ function EKFAmmo Wrap( + Ammunition ammunitionInstance) +{ + local EKFAmmo newReference; + if (ammunitionInstance == none) return none; + // This one is not actually used for anything, so it is not real + if (ammunitionInstance.class == class'KFMod.FlashlightAmmo') return none; + + newReference = EKFAmmo(__().memory.Allocate(class'EKFAmmo')); + newReference.ammunitionReference = __().unreal.ActorRef(ammunitionInstance); + return newReference; +} + +public function EInterface Copy() +{ + local Ammunition ammunitionInstance; + ammunitionInstance = GetNativeInstance(); + return Wrap(ammunitionInstance); +} + +public function bool Supports(class newInterfaceClass) +{ + if (newInterfaceClass == none) return false; + if (newInterfaceClass == class'EItem') return true; + if (newInterfaceClass == class'EAmmo') return true; + if (newInterfaceClass == class'EKFAmmo') return true; + + return false; +} + +public function EInterface As(class newInterfaceClass) +{ + if (!IsExistent()) { + return none; + } + if ( newInterfaceClass == class'EItem' + || newInterfaceClass == class'EAmmo' + || newInterfaceClass == class'EKFAmmo') + { + return Copy(); + } + return none; +} + +public function bool IsExistent() +{ + return (GetNativeInstance() != none); +} + +public function bool SameAs(EInterface other) +{ + local EKFAmmo otherAmmo; + otherAmmo = EKFAmmo(other); + if (otherAmmo == none) { + return false; + } + return (GetNativeInstance() == otherAmmo.GetNativeInstance()); +} + +/** + * Returns `Ammunition` instance represented by the caller `EKFAmmo`. + * + * @return `Ammunition` instance represented by the caller `EKFAmmo`. + */ +public final /*unreal*/ function Ammunition GetNativeInstance() +{ + if (ammunitionReference != none) { + return Ammunition(ammunitionReference.Get()); + } + return none; +} + +public function array GetTags() +{ + local array tagArray; + if (ammunitionReference == none) return tagArray; + if (ammunitionReference.Get() == none) return tagArray; + + tagArray[0] = P("ammo").Copy(); + return tagArray; +} + +public function bool HasTag(Text tagToCheck) +{ + if (tagToCheck == none) return false; + if (tagToCheck.Compare(P("ammo"))) return true; + + return false; +} + +public function Text GetTemplate() +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return none; + } + return _.text.FromString(string(ammunition.class)); +} + +public function Text GetName() +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return none; + } + return _.text.FromString(ammunition.GetHumanReadableName()); +} + +public function bool IsRemovable() +{ + return false; +} + +public function bool IsSellable() +{ + return false; +} + +private function class GetOwnerWeaponPickupClass() +{ + local KFWeapon ownerWeapon; + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return none; + } + ownerWeapon = GetOwnerWeapon(); + if (ownerWeapon != none) { + return class(ownerWeapon.pickupClass); + } + return none; +} + +// Finds a weapons that is corresponding to our ammo. +// We can limit ourselves to returning a single instance, since one weapon +// per ammo type is how Killing Floor does things. +private function KFWeapon GetOwnerWeapon() +{ + local Pawn myOwner; + local KFWeapon nextWeapon; + local Inventory nextInventory; + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) return none; + myOwner = Pawn(ammunition.owner); + if (myOwner == none) return none; + + nextInventory = myOwner.inventory; + while (nextInventory != none) + { + nextWeapon = KFWeapon(nextInventory); + nextInventory = nextInventory.inventory; + if (_.unreal.inventory.GetAmmoClass(nextWeapon, 0) == ammunition.class) + { + return nextWeapon; + } + else if ( _.unreal.inventory.GetAmmoClass(nextWeapon, 1) + == ammunition.class) { + return nextWeapon; + } + } + return none; +} + +/** + * In Killing Floor ammo object itself does not actually have a price, + * instead it is defined inside weapon's `Pickup` class and, therefore, + * cannot be changed for an individual item. Only calculated. + */ +public function bool SetPrice(int newPrice) +{ + return false; +} + +public function int GetPrice() +{ + return GetPriceOf(GetAmount()); +} + +public function int GetTotalPrice() +{ + return GetPriceOf(GetTotalAmount()); +} + +public function int GetPriceOf(int ammoAmount) +{ + local Pawn myOwner; + local int clipSize; + local float clipPrice; + local KFWeapon ownerWeapon; + local KFPlayerReplicationInfo ownerKFPRI; + local class ownerWeaponPickupClass; + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) return 0; + ownerWeapon = GetOwnerWeapon(); + if (ownerWeapon == none) return 0; + ownerWeaponPickupClass = class(ownerWeapon.pickupClass); + if (ownerWeaponPickupClass == none) return 0; + + // Calculate clip price + if ( ownerWeapon.bHasSecondaryAmmo + && ammunition.class != ownerWeapon.fireModeClass[0].default.ammoClass) + { + // Amon Killing Floor's weapons, only M4 203 has a real secondary ammo + clipSize = 1; + } + else { + clipSize = ownerWeapon.default.magCapacity; + } + if( ownerWeapon.PickupClass == class'HuskGunPickup' ) { + clipSize = ownerWeaponPickupClass.default.buyClipSize; + } + clipPrice = ownerWeaponPickupClass.default.ammoCost; + // Calculate clip size + myOwner = Pawn(ammunition.owner); + if (myOwner != none) { + ownerKFPRI = KFPlayerReplicationInfo(myOwner.playerReplicationInfo); + } + if (ownerKFPRI != none) + { + clipPrice *= ownerKFPRI.clientVeteranSkill.static + .GetAmmoCostScaling(ownerKFPRI, ownerWeaponPickupClass); + } + // Calculate price of total ammo + return int(ammoAmount * clipPrice / clipSize); +} + +public function bool SetWeight(int newWeight) +{ + return false; +} + +public function int GetWeight() +{ + return 0; +} + +// Killing Floor weapons do not reduce ammunition when it is loaded it into +// the weapons. This is because each ammo type is only ever used by one weapon, +// so the can simply treat it as part of the weapon and only record how much +// of it is currently in the weapon's magazine. +// This method goes through inventory weapons to find how much ammo was +// already loaded into the weapons. +private function int GetLoadedAmmo() +{ + local KFWeapon ownerWeapon; + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) return 0; + ownerWeapon = GetOwnerWeapon(); + if (ownerWeapon == none) return 0; + // Husk gun does not load ammo at all + if (ownerWeapon.class == class'KFMod.HuskGun') return 0; + + // Most of the Killing Floor weapons do not have a proper separate + // secondary ammo: they either reuse primary ammo (like zed guns or + // hunting shotgun), or they use some pseudo-ammo (like medic guns). + // They only exception is M4 203 that loads itself as soon as + // it fires. Some modded weapons might also be exceptions and/or use + // secondary ammo differently, but we have no way of knowing how + // exactly they are doing it and cannot implement this interface + // for them. + // That is why we only bother with the first fire mode and count + // one loaded ammo for the secondary, just assuming it is M4 203. + // We can also quit as soon as we have found a single weapon that + // uses our ammo, since one weapon per ammo type is how Killing Floor + // does things. + if (_.unreal.inventory.GetAmmoClass(ownerWeapon, 0) == ammunition.class) { + return ownerWeapon.magAmmoRemaining; + } + else if ( _.unreal.inventory.GetAmmoClass(ownerWeapon, 1) + == ammunition.class) + { + return 1; // M4 203 + } + return 0; +} + +public function Add(int amount, optional bool forceAddition) +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return; + } + if (forceAddition) { + ammunition.ammoAmount += amount; + } + else + { + ammunition.ammoAmount = + Min(ammunition.maxAmmo, ammunition.ammoAmount + amount); + } + // Correct possible negative values + if (ammunition.ammoAmount < 0) { + ammunition.ammoAmount = 0; + } + ammunition.netUpdateTime = ammunition.level.timeSeconds - 1; +} + +public function int GetAmount() +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return 0; + } + return Max(0, ammunition.ammoAmount - GetLoadedAmmo()); +} + +public function int GetTotalAmount() +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return 0; + } + return Max(0, ammunition.ammoAmount); +} + +public function SetAmount(int amount, optional bool forceAddition) +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return; + } + if (forceAddition) { + ammunition.ammoAmount = amount; + } + else { + ammunition.ammoAmount = Min(ammunition.maxAmmo, amount); + } + // Correct possible negative values + if (ammunition.ammoAmount < 0) { + ammunition.ammoAmount = 0; + } + ammunition.netUpdateTime = ammunition.level.timeSeconds - 1; +} + +public function int GetMaxAmount() +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return 0; + } + // `Ammunition` does not really support infinite ammo, so return `0` if + // the value is messed up. + return Max(0, ammunition.maxAmmo - GetLoadedAmmo()); +} + +public function int GetMaxTotalAmount() +{ + local Ammunition ammunition; + ammunition = GetNativeInstance(); + if (ammunition == none) { + return 0; + } + // `Ammunition` does not really support infinite ammo, so return `0` if + // the value is messed up. + return Max(0, ammunition.maxAmmo); +} + +/** + * Supports any non-negative ammo value. + */ +public function bool SetMaxAmount( + int newMaxAmmo, + optional bool leaveCurrentAmmo) +{ + local Ammunition ammunition; + // We do not support unlimited ammo values + if (newMaxAmmo < 0) return false; + ammunition = GetNativeInstance(); + if (ammunition == none) return false; + + ammunition.maxAmmo = newMaxAmmo + GetLoadedAmmo(); + if (!leaveCurrentAmmo) { + ammunition.ammoAmount = Min(ammunition.maxAmmo, ammunition.ammoAmount); + } + ammunition.netUpdateTime = ammunition.level.timeSeconds - 1; + return true; +} + +public function bool SetMaxTotalAmount( + int newTotalMaxAmmo, + optional bool leaveCurrentAmmo) +{ + local Ammunition ammunition; + // We do not support unlimited ammo values + if (newTotalMaxAmmo < 0) return false; + ammunition = GetNativeInstance(); + if (ammunition == none) return false; + + ammunition.maxAmmo = newTotalMaxAmmo; + if (!leaveCurrentAmmo) { + ammunition.ammoAmount = Min(ammunition.maxAmmo, ammunition.ammoAmount); + } + ammunition.netUpdateTime = ammunition.level.timeSeconds - 1; + return true; +} + +public function bool HasWeapon() +{ + return (GetOwnerWeapon() != none); +} + +// Killing Floor's ammo should also count ammo already loaded into the magazine +public function Fill() +{ + if (GetMaxTotalAmount() < 0) return; + if (GetAmount() >= GetMaxTotalAmount()) return; + + SetAmount(GetMaxTotalAmount()); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFFlashlightAmmo.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFFlashlightAmmo.uc new file mode 100644 index 0000000..6d24c1d --- /dev/null +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFFlashlightAmmo.uc @@ -0,0 +1,298 @@ +/** + * Implementation of `EAmmo` for Killing Floor's flashlight charge that changes + * as little as possible and only on request from another mod, otherwise not + * altering gameplay at all. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class EKFFlashlightAmmo extends EAmmo; + +var private NativeActorRef pawnReference; + +protected function Finalizer() +{ + _.memory.Free(pawnReference); + pawnReference = none; +} + +/** + * Creates new `EKFFlashlightAmmo` that refers to the `medicWeaponInstance`'s + * medic ammunition. + * + * @param kfHumanPawn Pawn class with flashlight ammo. + * In Killing Floor, "flashlight ammo" is basically just a variable + * inside `KFHumanPawn` instance. + * @return New `EKFFlashlightAmmo` that represents medic ammunition of given + * `kfHumanPawn`. `none` iff `kfHumanPawn` is `none`. + */ +public final static /*unreal*/ function EKFFlashlightAmmo Wrap( + KFHumanPawn kfHumanPawn) +{ + local EKFFlashlightAmmo newReference; + if (kfHumanPawn == none) { + return none; + } + newReference = + EKFFlashlightAmmo(__().memory.Allocate(class'EKFFlashlightAmmo')); + newReference.pawnReference = __().unreal.ActorRef(kfHumanPawn); + return newReference; +} + +public function EInterface Copy() +{ + local KFHumanPawn pawnInstance; + pawnInstance = GetNativeInstance(); + return Wrap(pawnInstance); +} + +public function bool Supports(class newInterfaceClass) +{ + if (newInterfaceClass == none) return false; + if (newInterfaceClass == class'EItem') return true; + if (newInterfaceClass == class'EAmmo') return true; + if (newInterfaceClass == class'EKFFlashlightAmmo') return true; + + return false; +} + +public function EInterface As(class newInterfaceClass) +{ + if (!IsExistent()) { + return none; + } + if ( newInterfaceClass == class'EItem' + || newInterfaceClass == class'EAmmo' + || newInterfaceClass == class'EKFFlashlightAmmo') + { + return Copy(); + } + return none; +} + +public function bool IsExistent() +{ + return (GetNativeInstance() != none); +} + +public function bool SameAs(EInterface other) +{ + local EKFFlashlightAmmo otherAmmo; + otherAmmo = EKFFlashlightAmmo(other); + if (otherAmmo == none) { + return false; + } + return (GetNativeInstance() == otherAmmo.GetNativeInstance()); +} + +/** + * Returns `KFAmmunition` instance represented by the caller `EKFAmmo`. + * + * @return `KFAmmunition` instance represented by the caller `EKFAmmo`. + */ +public final /*unreal*/ function KFHumanPawn GetNativeInstance() +{ + if (pawnReference != none) { + return KFHumanPawn(pawnReference.Get()); + } + return none; +} + +public function array GetTags() +{ + local array tagArray; + if (pawnReference == none) return tagArray; + if (pawnReference.Get() == none) return tagArray; + + tagArray[0] = P("ammo").Copy(); + return tagArray; +} + +public function bool HasTag(Text tagToCheck) +{ + if (tagToCheck == none) return false; + if (tagToCheck.Compare(P("ammo"))) return true; + + return false; +} + +public function Text GetTemplate() +{ + if (IsExistent()) { + return P("flashlight:ammo").Copy(); + } + return none; +} + +public function Text GetName() +{ + if (IsExistent()) { + return P("Flashlight's ammo").Copy(); + } + return none; +} + +public function bool IsRemovable() +{ + return false; +} + +public function bool IsSellable() +{ + return false; +} + +/** + * Medic ammo is free and does not have a price in Killing Floor. + */ +public function bool SetPrice(int newPrice) +{ + return false; +} + +public function int GetPrice() +{ + return 0; +} + +public function int GetTotalPrice() +{ + return 0; +} + +public function int GetPriceOf(int ammoAmount) +{ + return 0; +} + +public function bool SetWeight(int newWeight) +{ + return false; +} + +public function int GetWeight() +{ + return 0; +} + +public function Add(int amount, optional bool forceAddition) +{ + local KFHumanPawn kfHumanPawn; + kfHumanPawn = GetNativeInstance(); + if (kfHumanPawn == none) { + return; + } + if (forceAddition) { + kfHumanPawn.torchBatteryLife += amount; + } + else + { + kfHumanPawn.torchBatteryLife = + Min( kfHumanPawn.default.torchBatteryLife, + kfHumanPawn.torchBatteryLife + amount); + } + // Correct possible negative values + if (kfHumanPawn.torchBatteryLife < 0) { + kfHumanPawn.torchBatteryLife = 0; + } +} + +public function int GetAmount() +{ + local KFHumanPawn kfHumanPawn; + kfHumanPawn = GetNativeInstance(); + if (kfHumanPawn == none) { + return 0; + } + return Max(0, kfHumanPawn.torchBatteryLife); +} + +public function int GetTotalAmount() +{ + return GetAmount(); +} + +public function SetAmount(int amount, optional bool forceAddition) +{ + local KFHumanPawn kfHumanPawn; + kfHumanPawn = GetNativeInstance(); + if (kfHumanPawn == none) { + return; + } + if (forceAddition) { + kfHumanPawn.torchBatteryLife = amount; + } + else + { + kfHumanPawn.torchBatteryLife = + Min(kfHumanPawn.default.torchBatteryLife, amount); + } + // Correct possible negative values + if (kfHumanPawn.torchBatteryLife < 0) { + kfHumanPawn.torchBatteryLife = 0; + } +} + +public function int GetMaxAmount() +{ + local KFHumanPawn kfHumanPawn; + kfHumanPawn = GetNativeInstance(); + if (kfHumanPawn == none) { + return 0; + } + return Max(0, kfHumanPawn.default.torchBatteryLife); +} + +public function int GetMaxTotalAmount() +{ + return GetMaxAmount(); +} + +public function bool SetMaxAmount( + int newMaxAmmo, + optional bool leaveCurrentAmmo) +{ + local KFHumanPawn kfHumanPawn; + // We do not support unlimited ammo values + if (newMaxAmmo < 0) return false; + kfHumanPawn = GetNativeInstance(); + if (kfHumanPawn == none) return false; + + kfHumanPawn.default.torchBatteryLife = newMaxAmmo; + if (!leaveCurrentAmmo) + { + kfHumanPawn.torchBatteryLife = + Min( kfHumanPawn.default.torchBatteryLife, + kfHumanPawn.torchBatteryLife); + } + return true; +} + +public function bool SetMaxTotalAmount( + int newTotalMaxAmmo, + optional bool leaveCurrentAmmo) +{ + return SetMaxAmount(newTotalMaxAmmo, leaveCurrentAmmo); +} + +public function bool HasWeapon() +{ + return (GetNativeInstance() != none); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc index 7524648..731c011 100644 --- a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFInventory.uc @@ -20,16 +20,67 @@ * along with Acedia. If not, see . */ class EKFInventory extends EInventory - config(AcediaSystem_KF1Frontend); + dependson(InventoryAPI); -struct DualiesPair -{ - var class single; - var class dual; -}; +/** + * [reference documentation] + * # `EInventory` implementation for vanilla Killing Floor + * + * ## Supported inventory items + * + * This inventory implementation recognized 3 types of inventory items: + * *weapons*, *ammunition* and special type *unknown*. + * + * ### Weapons + * + * *Weapons* are any inventory derived from `Weapon` inventory class, + * although some features (dual-wielding support and recognizing whether weapon + * can be dropped/removed). For recognizing dual-wielded weapons this class + * relies on `UnrealAPI.InventoryAPI` and its configuration. + * + * Weapons are droppable/removable by default with the only exception of + * weapons derived from `KFWeapon` that have `bKFNeverThrow` set to `true`. + * + * ### Ammunition + * + * *Ammunition* is any `Inventory` derived from `Ammunition` class + * (`EKFAmmo`) plus some extra "artificial" items. "Artificial" here means + * that some ammunition items are not real `Inventory` objects, but rather + * an abstraction about ammo counter inside the weapon: + * + * 1. `EKFMedicAmmo` that stands for the medical charge of Field medic's guns; + * 2. `EKFSyringeAmmo` that stands for healing charge of player's syringe; + * 3. Even `EKFFlashlightAmmo` that stands for the flashlight energy counter + * `torchBatteryLife` inside `KFHumanPawn`. + * + * All their templates are formed as weapon class concatenated with ":ammo" + * suffix (and "flashlight:ammo" for `EKFFlashlightAmmo`), + * e.g. "kfmod.syringe:ammo". + * + * Ammunition is always considered not droppable/removable and cannot be + * added into the inventory by itself, since in Killing Floor it is inherently + * linked to the weapon object. + * + * ### Unknow items + * + * *Unknown* are any `Inventory` instances that cannot be classified as + * either of the above. They can always be added and removed, but never + * dropped. + * + * ## Supported explanations for being unable to add an item. + * + * * "bad reference" - `EItem` that is either `none` or refers to + * now non-existent was passed; + * * "bad template" - supplied template does not exist; + * * "not supported" - adding this type of item to inventory is not supported + * by the API (basically it is ammunition); + * * "conflicting item" - there is an item in the inventory that is in conflict + * with item you are trying to add; + * * "overweight" - adding this item will put player over the available weight + * capacity. + */ -var private EPlayer inventoryOwner; -var private config array dualiesClasses; +var private EPlayer inventoryOwner; protected function Finalizer() { @@ -47,484 +98,1150 @@ public function Initialize(EPlayer player) } } +public function EInterface Copy() +{ + local EKFInventory interfaceCopy; + interfaceCopy = EKFInventory(_.memory.Allocate(class'EKFInventory')); + interfaceCopy.Initialize(inventoryOwner); + return interfaceCopy; +} + +public function bool Supports(class newInterfaceClass) +{ + if (newInterfaceClass == none) return false; + if (newInterfaceClass == class'EInventory') return true; + if (newInterfaceClass == class'EKFInventory') return true; + + return false; +} + +public function EInterface As(class newInterfaceClass) +{ + if (inventoryOwner == none) return none; + if (!IsExistent()) return none; + + if ( newInterfaceClass == class'EInventory' + || newInterfaceClass == class'EKFInventory') + { + return Copy(); + } + return none; +} + +public function bool IsExistent() +{ + return (inventoryOwner != none && inventoryOwner.IsExistent()); +} + +public function bool SameAs(EInterface other) +{ + local EKFInventory otherInventory; + if (inventoryOwner == none) return false; + if (other == none) return false; + otherInventory = EKFInventory(other); + if (otherInventory == none) return false; + + return inventoryOwner.SameAs(otherInventory.inventoryOwner); +} + private function Pawn GetOwnerPawn() { local PlayerController myController; + if (inventoryOwner == none) return none; myController = inventoryOwner.GetController(); - if (myController == none) { + if (myController == none) return none; + + return myController.pawn; +} + +// Wraps `EItem` around passed inventory, based on the appropriate class +private function EItem WrapItem(Inventory nativeItem) +{ + if (nativeItem == none) { return none; } - return myController.pawn; + if (KFWeapon(nativeItem) != none) { + return class'EKFWeapon'.static.Wrap(KFWeapon(nativeItem)); + } + else if (KFAmmunition(nativeItem) != none) { + return class'EKFAmmo'.static.Wrap(KFAmmunition(nativeItem)); + } + return class'EKFUnknownItem'.static.Wrap(nativeItem); } -// TODO: needs more testing -public function bool Add(EItem newItem, optional bool forceAddition) +// Some weapons (medic guns, syringe) store ammo counts in their +// inventory class, this method is supposed to return a wrapper for +// such ammunitions. +private function EItem WrapItemAmmo(Inventory nativeItem) +{ + if (nativeItem == none) { + return none; + } + if (KFMedicGun(nativeItem) != none) { + return class'EKFMedicAmmo'.static.Wrap(KFMedicGun(nativeItem)); + } + else if (Syringe(nativeItem) != none) { + return class'EKFSyringeAmmo'.static.Wrap(Syringe(nativeItem)); + } + return none; +} + +// Seem to be the Killing Floor way to first manually call `Destroyed()` event +// before calling `Destroy()` on an actor. This method does this safely +// (making sure `Actor` reference does not die in a way that will +// crash the game) for an `Actor` wrapped inside `NativeActorRef`. +private function KillRefInventory(NativeActorRef itemRef) +{ + local Actor nativeReference; + local class destroyedClass; + if (itemRef == none) { + return; + } + nativeReference = itemRef.Get(); + if (nativeReference != none) + { + destroyedClass = class(nativeReference.class); + nativeReference.Destroyed(); + } + // Update `nativeReference` actor, in case it got messed up + nativeReference = itemRef.Get(); + if (nativeReference != none) { + nativeReference.Destroy(); + } + if (destroyedClass != none) { + _.unreal.GetKFGameType().WeaponDestroyed(destroyedClass); + } +} + +// Adds an item that this API implementation is not aware about, +// i.e. `Inventory` that must be wrapped as `EKFUnknown`. +private function EItem TryAddUnknownItem(EKFUnknownItem newItem) +{ + local Pawn pawn; + local Inventory nativeInventory; + pawn = GetOwnerPawn(); + if (pawn == none) return none; + nativeInventory = newItem.GetNativeInstance(); + if (nativeInventory == none) return none; + + nativeInventory.GiveTo(pawn); + if (newItem.IsExistent()) { + return newItem; + } + return none; +} + +// Searches `inventoryChain` for a weapon that: +// 1. Has the same root as `inventoryClass` (see `UnrealAPI.InventoryAPI` +// for an explanation of what a "root" is); +// 2. Has specified dual wielding role. +private function KFWeapon GetByRootWithDualRole( + class inventoryClass, + Inventory inventoryChain, + InventoryAPI.DualWieldingRole requiredRole) +{ + local InventoryAPI api; + local class nextWeaponClass; + local class itemRoot, nextRoot; + api = _.unreal.inventory; + itemRoot = api.GetRootPickupClass(inventoryClass); + while (inventoryChain != none) + { + nextWeaponClass = class(inventoryChain.class); + nextRoot = api.GetRootPickupClass(nextWeaponClass); + if ( itemRoot == nextRoot + && api.GetDualWieldingRole(nextWeaponClass) == requiredRole) + { + return KFWeapon(inventoryChain); + } + inventoryChain = inventoryChain.inventory; + } + return none; +} + +/** + * Supports adding weapons and non-ammo (unknown `Inventory` instances added + * by other mods) items. Cannot properly check if unknown item can be added + * and can fail adding an item even if `CanAdd()` succeeded. + */ +public function EItem Add(EItem newItem, optional bool forceAddition) { local Pawn pawn; local EKFWeapon kfWeaponItem; - local KFWeapon kfWeapon; + local KFWeapon nativeWeapon; local class dualClass; - local Inventory collidingItem; - if (!CanAdd(newItem, forceAddition)) return false; - kfWeaponItem = EKFWeapon(newItem); - if (kfWeaponItem == none) return false; + local KFWeapon conflictWeapon; + if (!CanAdd(newItem, forceAddition)) return none; pawn = GetOwnerPawn(); - if (pawn == none) return false; - kfWeapon = kfWeaponItem.GetNativeInstance(); - if (kfWeapon == none) return false; + if (pawn == none) return none; - dualClass = GetDualClass(kfWeapon.class); + kfWeaponItem = EKFWeapon(newItem); + if (kfWeaponItem == none) { + return TryAddUnknownItem(EKFUnknownItem(newItem)); + } + nativeWeapon = kfWeaponItem.GetNativeInstance(); + if (nativeWeapon == none) { + // Dead entity - nothing to add + return none; + } + dualClass = _.unreal.inventory.GetDualClass(nativeWeapon.class); + // The only possible complication here are dual weapons - `newItem` might + // cause addition of completely different weapon. if (dualClass != none) { - collidingItem = FindSkinnedInventory(pawn, kfWeapon.class); - if (collidingItem != none) + conflictWeapon = GetByRootWithDualRole( nativeWeapon.class, + pawn.inventory, DWR_Single); + if (conflictWeapon != none) { - collidingItem.Destroyed(); - collidingItem.Destroy(); + nativeWeapon = KFWeapon(_.unreal.inventory + .MergeWeapons(pawn, dualClass, nativeWeapon, conflictWeapon)); } - kfWeapon.Destroy(); - kfWeapon = KFWeapon(_.memory.Allocate(dualClass)); - if (kfWeapon != none) { - _.unreal.GetKFGameType().WeaponSpawned(kfWeapon); - } - else { - return false; + if (nativeWeapon != none) { + return class'EKFWeapon'.static.Wrap(nativeWeapon); } } - kfWeapon.GiveTo(pawn); - return true; + nativeWeapon.GiveTo(pawn); + return newItem; } -public function bool AddTemplate( +/** + * Supports adding weapons and non-ammo (unknown `Inventory` instances added + * by other mods) items. Cannot properly check if unknown item can be added + * and can fail adding an item even if `CanAddTemplate()` succeeded. + */ +public function EItem AddTemplate( Text newItemTemplate, optional bool forceAddition) { local Pawn pawn; - local KFWeapon newWeapon; - local class newWeaponClass; - local class dualClass; - local KFWeapon collidingWeapon; - local int totalAmmo, magazineAmmo; - if (newItemTemplate == none) return false; - if (!CanAddTemplate(newItemTemplate, forceAddition)) return false; + local EKFUnknownItem newItem; + local KFWeapon newWeapon, collidingWeapon; + local class newInventoryClass; + local class newWeaponClass, dualClass; + if (newItemTemplate == none) return none; + if (!CanAddTemplate(newItemTemplate, forceAddition)) return none; pawn = GetOwnerPawn(); - if (pawn == none) return false; + if (pawn == none) return none; - newWeaponClass = class(_.memory.LoadClass(newItemTemplate)); - if (newWeaponClass != none) { - dualClass = GetDualClass(newWeaponClass); + // Since `CanAddTemplate()` check was passed - `newInventoryClass` is + // either a weapon or some non-ammo inventory. + newInventoryClass = class(_.memory.LoadClass(newItemTemplate)); + newWeaponClass = class(newInventoryClass); + if (newWeaponClass == none) + { + newItem = class'EKFUnknownItem'.static + .Wrap(Inventory(_.memory.Allocate(newInventoryClass))); + if (newItem != none && TryAddUnknownItem(newItem) != none) { + return newItem; + } + _.memory.Free(newItem); + return none; } + // Handle dual pistols merging + dualClass = _.unreal.inventory.GetDualClass(newWeaponClass); if (dualClass != none) { - collidingWeapon = FindSkinnedInventory(pawn, newWeaponClass); - if (collidingWeapon != none) - { - totalAmmo = collidingWeapon.AmmoAmount(0); - magazineAmmo = collidingWeapon.magAmmoRemaining; - collidingWeapon.Destroyed(); - collidingWeapon.Destroy(); + collidingWeapon = GetByRootWithDualRole(newWeaponClass, + pawn.inventory, DWR_Single); + if (collidingWeapon != none) { newWeaponClass = dualClass; } } - newWeapon = KFWeapon(_.memory.Allocate(newWeaponClass)); - if (newWeapon != none) - { - _.unreal.GetKFGameType().WeaponSpawned(newWeapon); - newWeapon.GiveTo(pawn); - if (totalAmmo > 0) { - newWeapon.AddAmmo(totalAmmo, 0); - } - newWeapon.magAmmoRemaining += magazineAmmo; - return true; + // Add regular weapons + newWeapon = KFWeapon(_.unreal.inventory + .MergeWeapons(GetOwnerPawn(), newWeaponClass, collidingWeapon)); + if (newWeapon != none) { + return class'EKFWeapon'.static.Wrap(newWeapon); } - return false; + return none; } -public function bool CanAdd(EItem itemToCheck, optional bool forceAddition) +/** + * Supports adding weapons and non-ammo (unknown `Inventory` instances added + * by other mods) items. Cannot properly check if unknown item can be added + * and can raise "faulty implementation" error in case it cannot. + */ +public function Text CanAddExplain( + EItem itemToCheck, + optional bool forceAddition) { - local EKFWeapon kfWeaponItem; - local KFWeapon kfWeapon; + local EKFWeapon kfWeaponItem; + local EKFUnknownItem kfSomeItem; + local KFWeapon kfWeapon; + if (itemToCheck == none) { + return P("bad reference").Copy(); + } + // We assume all unknown items can be added, since we cannot really + // check anyway + kfSomeItem = EKFUnknownItem(itemToCheck); + if (kfSomeItem != none) + { + if (kfSomeItem.IsExistent()) { + return none; + } + return P("entity was destroyed").Copy(); + } + // If not an `EKFUnknownItem`, then it must be `EKFWeapon` kfWeaponItem = EKFWeapon(itemToCheck); - if (kfWeaponItem == none) return false; // can only add weapons + if (kfWeaponItem == none) { + return P("unsupported item").Copy(); + } kfWeapon = kfWeaponItem.GetNativeInstance(); - if (kfWeapon == none) return false; // dead `EKFWeapon` object - - return CanAddWeaponClass(kfWeapon.class, forceAddition); + if (kfWeapon == none) { + return P("entity was destroyed").Copy(); + } + return CanAddWeaponClassExplain(kfWeapon.class, forceAddition); } -public function bool CanAddTemplate( +/** + * Supports adding weapons and non-ammo (unknown `Inventory` instances added + * by other mods) items. Cannot properly check if unknown item can be added + * and can raise "faulty implementation" error in case it cannot. + */ +public function Text CanAddTemplateExplain( Text itemTemplateToCheck, optional bool forceAddition) { - local class kfWeaponClass; + local class inventoryClass; + local class kfWeaponClass; + if (itemTemplateToCheck == none) { + return P("bad reference").Copy(); + } + if (itemTemplateToCheck.EndsWith(P(":ammo"))) { + return P("not supported").Copy(); + } // Can only add weapons for now - kfWeaponClass = class(_.memory.LoadClass(itemTemplateToCheck)); - return CanAddWeaponClass(kfWeaponClass, forceAddition); -} - -private function bool CanAddWeaponClass( - class kfWeaponClass, - optional bool forceAddition) -{ - local KFPawn kfPawn; - local Inventory collidingItem; - if (kfWeaponClass == none) return false; - kfPawn = KFPawn(GetOwnerPawn()); - if (kfPawn == none) return false; - - if (!forceAddition && !kfPawn.CanCarry(kfWeaponClass.default.weight)) { - return false; + inventoryClass = class(_.memory.LoadClass(itemTemplateToCheck)); + if (inventoryClass == none) { + return P("bad template"); } - if (!forceAddition && HasSameTypeWeapons(kfWeaponClass, kfPawn)) - { - if (GetDualClass(kfWeaponClass) != none) { - collidingItem = FindSkinnedInventory(kfPawn, kfWeaponClass); - } - return (collidingItem != none); + if (class(inventoryClass) != none) { + return P("not supported").Copy(); } - return true; + kfWeaponClass = class(_.memory.LoadClass(itemTemplateToCheck)); + if (kfWeaponClass == none) { + return none; // Neither ammo or a weapon, so `EKFUnknownItem` + } + return CanAddWeaponClassExplain(kfWeaponClass, forceAddition); } -private function bool HasSameTypeWeapons( - class kfWeaponClass, - Pawn pawn) +// Auxiliary method for building "conflicting item:" explanations +private function Text ReportConflictingItem(Inventory conflictingItem) { - local Inventory nextInventory; - local class itemRoot, nextRoot; - nextInventory = pawn.inventory; - itemRoot = GetRootPickupClass(kfWeaponClass); - while (nextInventory != none) - { - nextRoot = GetRootPickupClass(class(nextInventory.class)); - if (itemRoot == nextRoot) { - return true; - } - nextInventory = nextInventory.inventory; + local Text result; + local MutableText builder; + if (conflictingItem == none) { + return P("conflicting item").Copy(); } - return false; + builder = P("conflicting item:") + .MutableCopy() + .AppendString(string(conflictingItem)); + result = builder.Copy(); + _.memory.Free(builder); + return result; } -// For "single" weapons that can have a "dual" version returns class of -// corresponding dual version, for any other -// (including dual weapons themselves) returns `none`. -private final function class GetDualClass(class weapon) +// Checks if a weapon of given class `kfWeaponClass` can be added to +// the inventory. There is two reasons that can prevent it from being added: +// 1. There is a conflict with existing weapon (i.e. already have +// different skin in the inventory); +// 2. It will put player over his weight capacity. +private function Text CanAddWeaponClassExplain( + class kfWeaponClass, + optional bool forceAddition) { - local int i; - local class pickupClass; - local class dualPickupClass; - if (weapon == none) return none; - pickupClass = class(weapon.default.pickupClass); - if (pickupClass == none) return none; + local float additionalWeight; + local class dualVersion; + local KFPawn kfPawn; + local Inventory conflictingWeapon; + local InventoryAPI.DualWieldingRole dualWeildingRole; + if (kfWeaponClass == none) return P("bad template").Copy(); + kfPawn = KFPawn(GetOwnerPawn()); + if (kfPawn == none) return P("internal error:no pawn").Copy(); - for (i = 0; i < dualiesClasses.length; i += 1) + additionalWeight = kfWeaponClass.default.weight; + // Start with checking conflicting weapons, since in case of conflicting + // dual weapons we might need to update `additionalWeight` variable. + conflictingWeapon = + _.unreal.inventory.GetByRoot(kfWeaponClass, kfPawn.inventory); + if (conflictingWeapon != none) { - if (dualiesClasses[i].single == pickupClass) + // `GetByRoot()` is a simple check that thinks handcannon is in + // a conflict with another handcannon, so we need to handle + // dual wieldable weapons differently + dualWeildingRole = _.unreal.inventory.GetDualWieldingRole( + class(conflictingWeapon.class)); + if (dualWeildingRole != DWR_None) { - dualPickupClass = dualiesClasses[i].dual; - if (dualPickupClass != none) { - return class(dualPickupClass.default.inventoryType); + if (HasDualWieldingConflict(kfWeaponClass, kfPawn, forceAddition)) { + return ReportConflictingItem(conflictingWeapon); + } + // Update additional weight + dualVersion = _.unreal.inventory.GetDualClass(kfWeaponClass); + if (dualVersion != none) + { + additionalWeight = + dualVersion.default.weight - additionalWeight; } } + // For non-dual weapons the check easy: we can only force + // conflicting weapons of the different classes into the same inventory + // (e.g. different skins) + else if (!forceAddition || kfWeaponClass == conflictingWeapon.class) { + return ReportConflictingItem(conflictingWeapon); + } + } + // If there were no conflict - just check the weight + if (!forceAddition && !kfPawn.CanCarry(additionalWeight)) { + return P("overweight").Copy(); } return none; } -// Returns inventory type of class `desiredClass` if it exists in -// `pawn`'s inventory. -// Unlike `Pawn`'s `FindInventoryType()` method, this one will find -// inventory type in case it corresponds to the different (weapon's) skin by -// comparing first item of `KFWeaponPickup`'s `variantClasses`. -private function KFWeapon FindSkinnedInventory( +// Decides whether we can add a weapon to the `pawn`'s inventory based on +// the dual-wielding weapons rules +private function bool HasDualWieldingConflict( + class kfWeaponClass, Pawn pawn, - class desiredClass) + bool forceAddition) { - local Inventory nextInv; - local class rootVariant; - local class nextPickupClass, desiredPickupClass; - if (desiredClass == none) return none; - if (pawn == none) return none; - - desiredPickupClass = class(desiredClass.default.pickupClass); - if ( desiredPickupClass != none - && desiredPickupClass.default.variantClasses.length > 0) + local bool addingSingle; + local class dualClass; + if (pawn == none) { + return false; + } + addingSingle = false; + dualClass = _.unreal.inventory.GetDualClass(kfWeaponClass); + if (dualClass == none) { - rootVariant = desiredPickupClass.default.variantClasses[0]; + dualClass = kfWeaponClass; + addingSingle = true; } - for (nextInv = pawn.inventory; nextInv != none; nextInv = nextInv.inventory) + // 1. We can always add pistols if we do not yet have dual version + if (GetByRootWithDualRole(kfWeaponClass, pawn.inventory, DWR_Dual) == none) { - if (nextInv.class == desiredClass) { - return KFWeapon(nextInv); - } - // Variant check - if (rootVariant == none) continue; - nextPickupClass = class(nextInv.pickupClass); - if (nextPickupClass == none) continue; - if (nextPickupClass.default.variantClasses.length <= 0) continue; - if (rootVariant == nextPickupClass.default.variantClasses[0]) { - return KFWeapon(nextInv); - } + return false; } - return none; + // 2. If we do have a dual version, but we are forcing this addition and + // are adding single when there is no other single pistol yet + if ( addingSingle && forceAddition + && _.unreal.inventory.Get(kfWeaponClass, pawn.inventory) == none) + { + return false; + } + // 3. If we do have a dual version, but we are forcing this addition and + // are adding a different skin + if ( forceAddition + && _.unreal.inventory.Get(dualClass, pawn.inventory) == none) + { + return false; + } + return true; } -// Returns a root pickup class. -// For non-dual weapons, root class is defined as either: -// 1. the first variant (reskin), if there are variants for that weapon; -// 2. and as the class itself, if there are no variants. -// For dual weapons (all dual pistols) root class is defined as -// a root of their single version. -// This definition is useful because: -// ~ Vanilla game rules are such that player can only have two weapons -// in the inventory if they have different roots; -// ~ Root is easy to find. -private final function class GetRootPickupClass( - class weapon) -{ - local int i; - local class root; - if (weapon == none) return none; - // Start with a pickup of the given weapons - root = class(weapon.default.pickupClass); - if (root == none) return none; - - // In case it's a dual version - find corresponding single pickup class - // (it's root would be the same). - for (i = 0; i < dualiesClasses.length; i += 1) - { - if (dualiesClasses[i].dual == root) - { - root = dualiesClasses[i].single; - break; - } +// Gets `Inventory` to which `item` corresponds. If there even is one - +// e.g. `EKFFlashlightAmmo` does not correspond to inventory. +private final function Inventory GetItemNativeInstance(EItem item) +{ + if (item == none) { + return none; + } + if (item.class == class'EKFAmmo') { + return EKFAmmo(item).GetNativeInstance(); + } + if (item.class == class'EKFMedicAmmo') { + return EKFMedicAmmo(item).GetNativeInstance(); + } + if (item.class == class'EKFSyringeAmmo') { + return EKFSyringeAmmo(item).GetNativeInstance(); + } + if (item.class == class'EKFWeapon') { + return EKFWeapon(item).GetNativeInstance(); } - // Take either first variant class or the class itself, - - // it's going to be root by definition. - if (root.default.variantClasses.length > 0) { - root = class(root.default.variantClasses[0]); + if (item.class == class'EKFUnknownItem') { + return EKFUnknownItem(item).GetNativeInstance(); } - return root; + return none; } +/** + * Supports removal of weapons and non-ammo (unknown `Inventory` instances + * added by other mods) items. + */ public function bool Remove( EItem itemToRemove, optional bool keepItem, optional bool forceRemoval) { - local bool removedItem; - local float passedTime; - local Pawn pawn; - local Inventory nextInventory; - local EKFWeapon kfWeaponItem; - local KFWeapon kfWeapon; - kfWeaponItem = EKFWeapon(itemToRemove); - if (kfWeaponItem == none) return false; + local bool result; + local Pawn pawn; + local NativeActorRef pawnRef; + local Inventory nativeInstance; + local KFWeapon kfWeapon; + local DynamicArray removalList; + if (EAmmo(itemToRemove) != none) return false; + nativeInstance = GetItemNativeInstance(itemToRemove); + if (nativeInstance == none) return false; pawn = GetOwnerPawn(); - if (pawn == none) return false; - if (pawn.inventory == none) return false; - kfWeapon = kfWeaponItem.GetNativeInstance(); - if (kfWeapon == none) return false; - if (!forceRemoval && kfWeapon.bKFNeverThrow) return false; + if (pawn == none) return false; - passedTime = _.unreal.GetLevel().timeSeconds - 1; - nextInventory = pawn.inventory; - while (nextInventory.inventory != none) - { - if (nextInventory.inventory == kfWeapon) - { - nextInventory.inventory = kfWeapon.inventory; - kfWeapon.inventory = none; - nextInventory.netUpdateTime = passedTime; - kfWeapon.netUpdateTime = passedTime; - kfWeapon.Destroy(); - removedItem = true; - } - else { - nextInventory = nextInventory.inventory; - } + // Do some checks first + kfWeapon = KFWeapon(nativeInstance); + if (!forceRemoval && kfWeapon != none && kfWeapon.bKFNeverThrow) { + return false; } - return removedItem; + if (!_.unreal.inventory.Contains(kfWeapon, pawn.inventory)) { + return false; + } + // This code is an overkill for removing a single item and is not + // really efficient, but it completely relies on methods that + // `RemoveTemplate()` and `RemoveAll()` use, so consistent behavior is + // guaranteed. + // Only optimize this if this method will become + // a bottleneck somewhere. + removalList = _.collections.EmptyDynamicArray(); + removalList.AddItem(_.unreal.ActorRef(nativeInstance), true); + pawnRef = _.unreal.ActorRef(pawn); + result = RemoveInventoryArray( pawnRef, removalList, + keepItem, forceRemoval, true); + _.memory.Free(removalList); + _.memory.Free(pawnRef); + return result; } +/** + * Supports removal of weapons and non-ammo (unknown `Inventory` instances + * added by other mods) items. + */ public function bool RemoveTemplate( - Text itemTemplateToRemove, + Text template, optional bool keepItem, optional bool forceRemoval, optional bool removeAll) +{ + local bool result; + local Pawn pawn; + local NativeActorRef pawnRef; + local DynamicArray removalList; + local class inventoryClass; + local class weaponClass; + if (template == none) return false; + if (template.EndsWith(P(":ammo"))) return false; + inventoryClass = class(_.memory.LoadClass(template)); + if (class(inventoryClass) != none) return false; + if (inventoryClass == none) return false; + pawn = GetOwnerPawn(); + if (pawn == none) return false; + + pawnRef = _.unreal.ActorRef(pawn); + removalList = _.collections.EmptyDynamicArray(); + // All removal works the same - form a "kill list", then remove + // all `Inventory` at once with `RemoveInventoryArray` + AddClassForRemoval(removalList, inventoryClass, forceRemoval, removeAll); + weaponClass = class(inventoryClass); + result = RemoveInventoryArray( + pawnRef, + removalList, + keepItem, + forceRemoval, + _.unreal.inventory.GetDualWieldingRole(weaponClass) == DWR_Dual); + _.memory.Free(removalList); + _.memory.Free(pawnRef); + return result; +} + +// Searches `EKFInventory`'s owner's inventory chain for items of class +// `inventoryClass` and adds them to the `removalArray` (for later removal). +private function AddClassForRemoval( + DynamicArray removalArray, + class inventoryClass, + optional bool forceRemoval, + optional bool removeAll) { local bool canRemoveInventory; - local bool removedItem; - local float passedTime; local Pawn pawn; local Inventory nextInventory; local KFWeapon nextKFWeapon; - local class kfWeaponClass; + local class dualClass; + if (removalArray == none) return; + if (inventoryClass == none) return; pawn = GetOwnerPawn(); - if (pawn == none) return false; - if (pawn.inventory == none) return false; - kfWeaponClass = class(_.memory.LoadClass(itemTemplateToRemove)); - if (kfWeaponClass == none) return false; - if (!forceRemoval && kfWeaponClass.default.bKFNeverThrow) return false; - - passedTime = _.unreal.GetLevel().timeSeconds - 1; + if (pawn == none) return; nextInventory = pawn.inventory; - while (nextInventory.inventory != none) + if (nextInventory == none) return; + + dualClass = + _.unreal.inventory.GetDualClass(class(inventoryClass)); + while (nextInventory != none) { - canRemoveInventory = true; - if (!forceRemoval) - { - nextKFWeapon = KFWeapon(nextInventory.inventory); - if (nextKFWeapon != none && nextKFWeapon.bKFNeverThrow) { - canRemoveInventory = false; - } + // We want to "remove" dual handcannons if removal of single handcannon + // is requested (replacing them with another single handcannon) + canRemoveInventory = (inventoryClass == nextInventory.class) + || (dualClass == nextInventory.class); + nextKFWeapon = KFWeapon(nextInventory); + // Check if weapon is removable + if (canRemoveInventory && nextKFWeapon != none) { + canRemoveInventory = (forceRemoval || !nextKFWeapon.bKFNeverThrow); } - if ( canRemoveInventory - && nextInventory.inventory.class == kfWeaponClass) + if (canRemoveInventory) { - nextInventory.inventory = nextKFWeapon.inventory; - nextKFWeapon.inventory = none; - nextInventory.netUpdateTime = passedTime; - nextKFWeapon.netUpdateTime = passedTime; - nextKFWeapon.Destroy(); - removedItem = true; + removalArray.AddItem(_.unreal.ActorRef(nextInventory), true); if (!removeAll) { - return true; + break; } } - else { - nextInventory = nextInventory.inventory; - } + nextInventory = nextInventory.inventory; } - return removedItem; } +/** + * Supports removal of weapons and non-ammo (unknown `Inventory` instances + * added by other mods) items. Ammo items are removed alongside linked weapons. + */ public function bool RemoveAll( optional bool keepItems, - optional bool forceRemoval) + optional bool forceRemoval, + optional bool includeHidden) { - local int i; + local bool result, canRemoveItem; local Pawn pawn; + local NativeActorRef pawnRef; local KFWeapon kfWeapon; local Inventory nextInventory; - local class destroyedClass; - local array inventoryToRemove; + local DynamicArray inventoryToRemove; pawn = GetOwnerPawn(); - if (pawn == none) return false; - if (pawn.inventory == none) return false; - + if (pawn == none) { + return false; + } + inventoryToRemove = _.collections.EmptyDynamicArray(); nextInventory = pawn.inventory; while (nextInventory != none) { kfWeapon = KFWeapon(nextInventory); - if (kfWeapon == none) - { - nextInventory = nextInventory.inventory; - continue; // TODO: handle non-weapons differently - } - if (forceRemoval || !kfWeapon.bKFNeverThrow) { - inventoryToRemove[inventoryToRemove.length] = nextInventory; + canRemoveItem = kfWeapon != none + && (forceRemoval || !kfWeapon.bKFNeverThrow); + canRemoveItem = canRemoveItem + || (includeHidden && Ammunition(nextInventory) == none); + if (canRemoveItem) { + inventoryToRemove.AddItem(_.unreal.ActorRef(nextInventory), true); } nextInventory = nextInventory.inventory; } - for(i = 0; i < inventoryToRemove.length; i += 1) + pawnRef = _.unreal.ActorRef(pawn); + result = RemoveInventoryArray( pawnRef, inventoryToRemove, + keepItems, forceRemoval, true); + _.memory.Free(inventoryToRemove); + _.memory.Free(pawnRef); + return result; +} + +// `completeRemoval` decides how dual weapons should be treated: +// `true` means all of their parts must be dropped/removed and +// `false` means that only a single half (1 pistol from dual pistols) must be +// dropped/removed. +private function bool RemoveInventoryArray( + NativeActorRef ownerPawnRef, + DynamicArray itemsToRemove, + bool keepItems, + bool forceRemoval, + bool completeRemoval) +{ + local int i; + local bool removedItem, removedEquip; + local Pawn ownerPawn; + local array< class > singleClassesToCleanup; + local NativeActorRef equippedWeapon, nextRef; + if (itemsToRemove == none) return false; + if (ownerPawnRef == none) return false; + ownerPawn = Pawn(ownerPawnRef.Get()); + if (ownerPawn == none) return false; + + equippedWeapon = _.unreal.ActorRef(ownerPawn.weapon); + for(i = 0; i < itemsToRemove.GetLength(); i += 1) { - if (inventoryToRemove[i] == none) { - continue; + // `itemsToRemove` is guaranteed to contain valid `ActorRef`s + nextRef = NativeActorRef(itemsToRemove.GetItem(i)); + removedEquip = removedEquip || equippedWeapon.IsEqual(nextRef); + // If we are dropping (`keepItems == true`) complete dual weapons + // and not just their part (`completeRemoval`), then we will need to + // re-remove single weapons added as a result of drop. + // We have to go through that because we want to employ drop + // methods supplied to us by native classes (that usually only drop + // a single pistol in the pair), instead of simply removing dual + // version and spawning two single pickups. + if (keepItems && completeRemoval) { + AppendSingleClass(singleClassesToCleanup, nextRef); } - destroyedClass = class(inventoryToRemove[i].class); - inventoryToRemove[i].Destroyed(); - inventoryToRemove[i].Destroy(); - _.unreal.GetKFGameType().WeaponDestroyed(destroyedClass); + removedItem = + HandleInventoryRemoval( ownerPawnRef, nextRef, keepItems, + forceRemoval, completeRemoval) + || removedItem; + } + itemsToRemove.Empty(); + for (i = 0; i < singleClassesToCleanup.length; i += 1) + { + AddClassForRemoval(itemsToRemove, singleClassesToCleanup[i], + forceRemoval, false); + } + if (itemsToRemove.GetLength() > 0) + { + RemoveInventoryArray( ownerPawnRef, itemsToRemove, + keepItems, forceRemoval, false); + } + if (removedEquip) { + RepickEquippedWeapon(ownerPawnRef); + } + _.memory.Free(equippedWeapon); + return removedItem; +} + +private function RepickEquippedWeapon(NativeActorRef pawnRef) +{ + local Pawn pawn; + pawn = Pawn(pawnRef.Get()); + if (pawn != none) { + pawn.weapon = none; + } + if (pawn != none && pawn.controller != none) { + pawn.controller.SwitchToBestWeapon(); + } + pawn = Pawn(pawnRef.Get()); + if (pawn != none && pawn.weapon != none) { + pawn.weapon.ClientWeaponSet(false); + } +} + +private function AppendSingleClass( + out array< class > singleClasses, + NativeActorRef nextRef) +{ + local KFWeapon kfWeaponInstance; + local class singleClass; + kfWeaponInstance = KFWeapon(nextRef.Get()); + if (kfWeaponInstance == none) { + return; + } + singleClass = _.unreal.inventory.GetSingleClass(kfWeaponInstance.class); + if (singleClass != none) { + singleClasses[singleClasses.length] = singleClass; + } +} + +// Assumes that `ownerPawnRef != none` and `inventoryRef != none`. +// Removes `Inventory` inside `inventoryRef` in a way appropriate for given +// flags `keepItems`, `forceRemoval` and `completeRemoval`. +private function bool HandleInventoryRemoval( + NativeActorRef ownerPawnRef, + nativeActorRef inventoryRef, + bool keepItems, + bool forceRemoval, + bool completeRemoval) +{ + local Inventory inventory; + local class singleClass; + inventory = Inventory(inventoryRef.Get()); + if (inventory == none) { + return false; + } + // `keepItems` means we have to drop inventory, which is handled by + // the unreal script-provided methods + if (keepItems) { + return DropInventoryItem(inventory); + } + // Reset `completeRemoval` flag if single weapons produced as a result + // of (non-forced) removal of dual version is non-removable. + // Example - dual 9mm. Dual 9mm and be dropped, but a single 9mm + // should not be dropped without being forced to. + if (!forceRemoval && completeRemoval) + { + if (KFWeapon(inventory) != none) + { + singleClass = _.unreal.inventory + .GetSingleClass(class(inventory.class)); + } + if (singleClass != none && singleClass.default.bKFNeverThrow) { + completeRemoval = false; + } + } + // Simply destroy items we want to remove completely, that is if: + // 1. We were told by the flag `completeRemoval`; + // 2. It is not a weapon and, since `Ammunition` inventory should not + // reach this method, `EKFUnknown`. + if (completeRemoval || KFWeapon(inventory) == none) { + KillRefInventory(inventoryRef); + } + else { + DestroyWeaponSingle(ownerPawnRef, inventoryRef); + } + return true; +} + +private function bool DropInventoryItem(Inventory inventoryToDrop) +{ + local Pawn ownerPawn; + local Vector x, y, z; + local Vector tossVelocity; + if (inventoryToDrop == none) return false; + ownerPawn = Pawn(inventoryToDrop.owner); + if (ownerPawn == none) return false; + if (ownerPawn.controller == none) return false; + + tossVelocity = Vector(ownerPawn.controller.GetViewRotation()); + tossVelocity = + tossVelocity * ((ownerPawn.velocity dot tossVelocity) + 150.0) + + Vect(0.0, 0.0, 100.0); + inventoryToDrop.velocity = tossVelocity; + GetAxes(ownerPawn.rotation, x, y, z); + KFWeapon(inventoryToDrop).bCanThrow = true; + inventoryToDrop.DropFrom(ownerPawn.location + + 0.8 * ownerPawn.collisionRadius * x + - 0.5 * ownerPawn.collisionRadius * y); + return true; +} + +// Assumes that `ownerPawnRef != none` and `inventoryToDestroy != none`. +// Destroys a dual version of the weapon, adding a single version in its +// stead (with half the ammo and magazine). +private function DestroyWeaponSingle( + NativeActorRef ownerPawnRef, + NativeActorRef inventoryToDestroy) +{ + local int totalAmmoPrimary, totalAmmoSecondary, magazineAmmo; + local class singleClass; + local KFWeapon kfWeaponToDestroy; + kfWeaponToDestroy = KFWeapon(inventoryToDestroy.Get()); + if (kfWeaponToDestroy == none) { + return; + } + singleClass = _.unreal.inventory.GetSingleClass(kfWeaponToDestroy.class); + if (singleClass != none) + { + totalAmmoPrimary = kfWeaponToDestroy.AmmoAmount(0); + totalAmmoSecondary = kfWeaponToDestroy.AmmoAmount(1); + magazineAmmo = kfWeaponToDestroy.magAmmoRemaining; + } + KillRefInventory(inventoryToDestroy); + if (singleClass != none) + { + _.unreal.inventory + .AddWeaponWithAmmo( Pawn(ownerPawnRef.Get()), singleClass, + totalAmmoPrimary / 2, totalAmmoSecondary / 2, + magazineAmmo / 2, true); } - return (inventoryToRemove.length > 0); } -/** - * Checks whether caller `EInventory` contains given `itemToCheck`. - * - * @param itemToCheck `EItem` we want to check for belonging to the caller - * `EInventory`. - * @result `true` if item does belong to the inventory and `false` otherwise. - */ public function bool Contains(EItem itemToCheck) { + local Pawn itemRelatedPawn; + local Controller inventoryRelatedController; + local Inventory nextInventory, itemInventory; + if (inventoryOwner == none) return false; + if (itemToCheck == none) return false; + + // For flashlight ammo, its `Pawn` must be inventory's owner + if (itemToCheck.class == class'EKFFlashlightAmmo') + { + itemRelatedPawn = EKFFlashlightAmmo(itemToCheck).GetNativeInstance(); + inventoryRelatedController = inventoryOwner.GetController(); + if (inventoryRelatedController != none) { + return itemRelatedPawn == inventoryRelatedController.pawn; + } + return false; + } + // For everything else, its native instance has to be somewhere + // inside inventory + itemInventory = GetItemNativeInstance(itemToCheck); + nextInventory = itemRelatedPawn.inventory; + while (nextInventory != none) + { + if (nextInventory == itemInventory) { + return true; + } + nextInventory = nextInventory.inventory; + } return false; } -/** - * Returns array with all `EItem`s contained inside the caller `EInventory`. - * - * @return Array with all `EItem`s contained inside the caller `EInventory`. - */ +public function bool ContainsTemplate(Text itemTemplateToCheck) +{ + local bool success; + local EItem templateItem; + templateItem = GetTemplateItem(itemTemplateToCheck); + success = (templateItem != none); + _.memory.Free(templateItem); + return success; +} + +private function array FilterNoneReferences(array arrayToFilter) +{ + local int i; + while (i < arrayToFilter.length) + { + if (arrayToFilter[i] == none) { + arrayToFilter.Remove(i, 1); + } + else { + i += 1; + } + } + return arrayToFilter; +} + +// Wraps and returns all items that correspond to at least one given flag. +// This implementation only supports weapons and ammo right now. +// Most of the other item getters are basically just calling this method. +public function array GetItemsByFlags( + bool getWeapon, + bool getAmmo, + bool getRest) +{ + local Pawn pawn; + local Inventory nextInventory; + local array result; + if (!getWeapon && !getAmmo) return result; + pawn = GetOwnerPawn(); + if (pawn == none) return result; + + if (getAmmo) + { + result[result.length] = + class'EKFFlashlightAmmo'.static.Wrap(KFHumanPawn(pawn)); + } + nextInventory = pawn.inventory; + while (nextInventory != none) + { + if (KFWeapon(nextInventory) != none) + { + if (getWeapon) { + result[result.length] = WrapItem(nextInventory); + } + if (getAmmo) { + result[result.length] = WrapItemAmmo(nextInventory); + } + } + // Ammunition never has another, built-in, ammo, so we do not need to + // call `WrapItemAmmo` method + else if (getAmmo && KFAmmunition(nextInventory) != none) { + result[result.length] = WrapItem(nextInventory); + } + else if (getRest) { + result[result.length] = WrapItem(nextInventory); + } + nextInventory = nextInventory.inventory; + } + return FilterNoneReferences(result); +} + +// Same as `GetItemsByFlags()`, but only returns first match. +private function EItem GetItemByFlags( + bool getWeapon, + bool getAmmo, + bool getRest) +{ + local Pawn pawn; + local EItem result; + local Inventory nextInventory; + if (!getWeapon && !getAmmo) return result; + pawn = GetOwnerPawn(); + if (pawn == none) return result; + + if (getAmmo) + { + result = class'EKFFlashlightAmmo'.static.Wrap(KFHumanPawn(pawn)); + if (result != none) { + return result; + } + } + nextInventory = pawn.inventory; + while (nextInventory != none) + { + // From weapon instances we can either wrap a weapon interface or + // ammo interface (for weapons with built-in ammo) + if (KFWeapon(nextInventory) != none) + { + if (getWeapon) { + result = WrapItem(nextInventory); + } + if (getAmmo &&result == none) { + result = WrapItemAmmo(nextInventory); + } + if (result != none) { + return result; + } + } + // Ammunition never has another, built-in, ammo, so we do not need to + // call `WrapItemAmmo` method + if (getAmmo && KFAmmunition(nextInventory) != none) { + result = WrapItem(nextInventory); + } + else if (getRest && result == none) { + result = WrapItem(nextInventory); + } + if (result != none) { + return result; + } + nextInventory = nextInventory.inventory; + } + return none; +} + public function array GetAllItems() { - local array emptyArray; - return emptyArray; + return GetItemsByFlags(true, true, true); +} + +public function array GetItemsSupporting(class interfaceClass) +{ + local bool getWeapon, getAmmo; + getWeapon = ( interfaceClass == class'EWeapon' + || interfaceClass == class'EKFWeapon'); + getAmmo = ( interfaceClass == class'EAmmo' + || interfaceClass == class'EKFAmmo'); + return GetItemsByFlags(getWeapon, getAmmo, false); } -/** - * Returns array with all `EItem`s contained inside the caller `EInventory` - * that has specified tag `tag`. - * - * @param tag Tag, which items we want to get. - * @return Array with all `EItem`s contained inside the caller `EInventory` - * that has specified tag `tag`. - */ public function array GetTagItems(Text tag) { - local array emptyArray; - return emptyArray; + local array emptyArray; + local bool getWeapon, getAmmo; + if (tag == none) { + return emptyArray; + } + getWeapon = P("weapon").Compare(tag) || P("visible").Compare(tag); + getAmmo = P("ammo").Compare(tag); + return GetItemsByFlags(getWeapon, getAmmo, false); } -/** - * Returns `EItem` contained inside the caller `EInventory` that has specified - * tag `tag`. - * - * If several `EItem`s inside caller `EInventory` have specified tag, - * inventory system can pick one arbitrarily (can be based on simple - * convenience of implementation). Returned value does not have to - * be stable (the same after repeated calls). - * - * @param tag Tag, which item we want to get. - * @return `EItem` contained inside the caller `EInventory` that belongs to - * the specified tag `tag`. - */ -public function EItem GetTagItem(Text tag) { return none; } +public function EItem GetTagItem(Text tag) +{ + local bool getWeapon, getAmmo; + if (tag == none) { + return none; + } + getWeapon = P("weapon").Compare(tag); + getAmmo = P("ammo").Compare(tag); + return GetItemByFlags(getWeapon, getAmmo, false); +} -/** - * Returns array with all `EItem`s contained inside the caller `EInventory` - * that originated from the specified template `template`. - * - * @param template Template, that items we want to get originated from. - * @return Array with all `EItem`s contained inside the caller `EInventory` - * that originated from the specified template `template`. - */ public function array GetTemplateItems(Text template) { - local array emptyArray; - return emptyArray; + local Pawn pawn; + local bool getBuiltInAmmo; + local string inventoryClass; + local Inventory nextInventory; + local EItem nextItem; + local array result; + if (template == none) return result; + pawn = GetOwnerPawn(); + if (pawn == none) return result; + if (pawn.inventory == none) return result; + + // As far as Killing Floor is concerned, template == class name + if (template.Compare(P("flashlight:ammo"))) + { + result[0] = class'EKFFlashlightAmmo'.static.Wrap(KFHumanPawn(pawn)); + return result; + } + if (template.EndsWith(P(":ammo"))) + { + // Drop the ":ammo" part (`5` letters long) + inventoryClass = template.ToString(0, template.GetLength() - 5); + getBuiltInAmmo = true; + } + else { + inventoryClass = template.ToString(); + } + nextInventory = pawn.inventory; + while (nextInventory != none) + { + if (inventoryClass ~= string(nextInventory.class)) + { + if (getBuiltInAmmo) { + nextItem = WrapItemAmmo(nextInventory); + } + else { + nextItem = WrapItem(nextInventory); + } + if (nextItem != none) + { + result[result.length] = nextItem; + nextItem = none; + } + } + nextInventory = nextInventory.inventory; + } + return result; +} + +public function EItem GetTemplateItem(Text template) +{ + local Pawn pawn; + local bool getBuiltInAmmo; + local string inventoryClass; + local Inventory nextInventory; + local EItem result; + if (template == none) return result; + pawn = GetOwnerPawn(); + if (pawn == none) return result; + if (pawn.inventory == none) return result; + + // As far as Killing Floor is concerned, template == class name + if (template.Compare(P("flashlight:ammo"))) { + return class'EKFFlashlightAmmo'.static.Wrap(KFHumanPawn(pawn)); + } + if (template.EndsWith(P(":ammo"))) + { + // Drop the ":ammo" part (`5` letters long) + inventoryClass = template.ToString(0, template.GetLength() - 5); + getBuiltInAmmo = true; + } + else { + inventoryClass = template.ToString(); + } + nextInventory = pawn.inventory; + while (nextInventory != none) + { + if (inventoryClass ~= string(nextInventory.class)) + { + if (getBuiltInAmmo) { + result = WrapItemAmmo(nextInventory); + } + else { + result = WrapItem(nextInventory); + } + if (result != none) { + return result; + } + } + nextInventory = nextInventory.inventory; + } + return none; } -/** - * Returns array with all `EItem`s contained inside the caller `EInventory` - * that originated from the specified template `template`. - * - * If several `EItem`s inside caller `EInventory` originated from - * that template, inventory system can pick one arbitrarily (can be based on - * simple convenience of implementation). Returned value does not have to - * be stable (the same after repeated calls). - * - * @param template Template, that item we want to get originated from. - * @return `EItem`s contained inside the caller `EInventory` that originated - * from the specified template `template`. - */ -public function EItem GetTemplateItem(Text template) { return none; } +public function array GetEquippedItems() +{ + local EItem equippedWeapon; + local array result; + equippedWeapon = GetEquippedItem(); + if (equippedWeapon != none) { + result[0] = equippedWeapon; + } + return result; +} + +public function EItem GetEquippedItem() +{ + local Pawn pawn; + local KFWeapon currentWeapon; + pawn = GetOwnerPawn(); + if (pawn == none) return none; + if (pawn.weapon == none) return none; + currentWeapon = KFWeapon(pawn.weapon); + if (currentWeapon == none) return none; + + return class'EKFWeapon'.static.Wrap(currentWeapon); +} defaultproperties { - dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup') - dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup') - dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup') - dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup') - dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup') - dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup') } \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFItemTemplateInfo.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFItemTemplateInfo.uc index a0898da..ccea724 100644 --- a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFItemTemplateInfo.uc +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFItemTemplateInfo.uc @@ -1,5 +1,5 @@ /** - * Implementation of `EKFItemTemplateInfo` for classic Killing Floor items that + * Implementation of `EItemTemplateInfo` for classic Killing Floor items that * changes as little as possible and only on request from another mod, * otherwise not altering gameplay at all. * Copyright 2021 - 2022 Anton Tarasenko diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFMedicAmmo.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFMedicAmmo.uc new file mode 100644 index 0000000..fdd6c72 --- /dev/null +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFMedicAmmo.uc @@ -0,0 +1,285 @@ +/** + * Implementation of `EAmmo` for Killing Floor medic weapons that changes + * as little as possible and only on request from another mod, otherwise not + * altering gameplay at all. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class EKFMedicAmmo extends EAmmo; + +var private NativeActorRef medicWeaponReference; + +protected function Finalizer() +{ + _.memory.Free(medicWeaponReference); + medicWeaponReference = none; +} + +/** + * Creates new `EKFMedicAmmo` that refers to the `medicWeaponInstance`'s + * medic ammunition. + * + * @param medicWeaponInstance Native medic gun, whose medic ammunition + * new `EKFMedicAmmo` will represent. + * @return New `EKFMedicAmmo` that represents medic ammunition of given + * `medicWeaponInstance`. `none` iff `medicWeaponInstance` is `none`. + */ +public final static /*unreal*/ function EKFMedicAmmo Wrap( + KFMedicGun medicWeaponInstance) +{ + local EKFMedicAmmo newReference; + if (medicWeaponInstance == none) { + return none; + } + newReference = EKFMedicAmmo(__().memory.Allocate(class'EKFMedicAmmo')); + newReference.medicWeaponReference = + __().unreal.ActorRef(medicWeaponInstance); + return newReference; +} + +public function EInterface Copy() +{ + local KFMedicGun medicWeaponInstance; + medicWeaponInstance = GetNativeInstance(); + return Wrap(medicWeaponInstance); +} + +public function bool Supports(class newInterfaceClass) +{ + if (newInterfaceClass == none) return false; + if (newInterfaceClass == class'EItem') return true; + if (newInterfaceClass == class'EAmmo') return true; + if (newInterfaceClass == class'EKFMedicAmmo') return true; + + return false; +} + +public function EInterface As(class newInterfaceClass) +{ + if (!IsExistent()) { + return none; + } + if ( newInterfaceClass == class'EItem' + || newInterfaceClass == class'EAmmo' + || newInterfaceClass == class'EKFMedicAmmo') + { + return Copy(); + } + return none; +} + +public function bool IsExistent() +{ + return (GetNativeInstance() != none); +} + +public function bool SameAs(EInterface other) +{ + local EKFMedicAmmo otherAmmo; + otherAmmo = EKFMedicAmmo(other); + if (otherAmmo == none) { + return false; + } + return (GetNativeInstance() == otherAmmo.GetNativeInstance()); +} + +/** + * Returns `KFAmmunition` instance represented by the caller `EKFAmmo`. + * + * @return `KFAmmunition` instance represented by the caller `EKFAmmo`. + */ +public final /*unreal*/ function KFMedicGun GetNativeInstance() +{ + if (medicWeaponReference != none) { + return KFMedicGun(medicWeaponReference.Get()); + } + return none; +} + +public function array GetTags() +{ + local array tagArray; + if (medicWeaponReference == none) return tagArray; + if (medicWeaponReference.Get() == none) return tagArray; + + tagArray[0] = P("ammo").Copy(); + return tagArray; +} + +public function bool HasTag(Text tagToCheck) +{ + if (tagToCheck == none) return false; + if (tagToCheck.Compare(P("ammo"))) return true; + + return false; +} + +public function Text GetTemplate() +{ + local KFMedicGun medicWeapon; + medicWeapon = GetNativeInstance(); + if (medicWeapon == none) { + return none; + } + return _.text.FromString(string(medicWeapon.class) $ ":ammo"); +} + +public function Text GetName() +{ + local KFMedicGun medicWeapon; + medicWeapon = GetNativeInstance(); + if (medicWeapon == none) { + return none; + } + return _.text.FromString(medicWeapon.GetHumanReadableName() $ "'s ammo"); +} + +public function bool IsRemovable() +{ + return false; +} + +public function bool IsSellable() +{ + return false; +} + +/** + * Medic ammo is free and does not have a price in Killing Floor. + */ +public function bool SetPrice(int newPrice) +{ + return false; +} + +public function int GetPrice() +{ + return 0; +} + +public function int GetTotalPrice() +{ + return 0; +} + +public function int GetPriceOf(int ammoAmount) +{ + return 0; +} + +public function bool SetWeight(int newWeight) +{ + return false; +} + +public function int GetWeight() +{ + return 0; +} + +public function Add(int amount, optional bool forceAddition) +{ + local KFMedicGun medicWeapon; + medicWeapon = GetNativeInstance(); + if (medicWeapon == none) { + return; + } + if (forceAddition) { + medicWeapon.healAmmoCharge += amount; + } + else + { + medicWeapon.healAmmoCharge = + Min(medicWeapon.maxAmmoCount, medicWeapon.healAmmoCharge + amount); + } + // Correct possible negative values + if (medicWeapon.healAmmoCharge < 0) { + medicWeapon.healAmmoCharge = 0; + } +} + +public function int GetAmount() +{ + local KFMedicGun medicWeapon; + medicWeapon = GetNativeInstance(); + if (medicWeapon == none) { + return 0; + } + return Max(0, medicWeapon.healAmmoCharge); +} + +public function int GetTotalAmount() +{ + return GetAmount(); +} + +public function SetAmount(int amount, optional bool forceAddition) +{ + local KFMedicGun medicWeapon; + medicWeapon = GetNativeInstance(); + if (medicWeapon == none) { + return; + } + if (forceAddition) { + medicWeapon.healAmmoCharge = amount; + } + else { + medicWeapon.healAmmoCharge = Min(medicWeapon.maxAmmoCount, amount); + } + // Correct possible negative values + if (medicWeapon.healAmmoCharge < 0) { + medicWeapon.healAmmoCharge = 0; + } +} + +public function int GetMaxAmount() +{ + local KFMedicGun medicWeapon; + medicWeapon = GetNativeInstance(); + if (medicWeapon == none) { + return 0; + } + return Max(0, medicWeapon.maxAmmoCount); +} + +public function int GetMaxTotalAmount() +{ + return GetMaxAmount(); +} + +public function bool SetMaxAmount( + int newMaxAmmo, + optional bool leaveCurrentAmmo) +{ + return false; +} + +public function bool SetMaxTotalAmount( + int newTotalMaxAmmo, + optional bool leaveCurrentAmmo) +{ + return false; +} + +public function bool HasWeapon() +{ + return (GetNativeInstance() != none); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFSyringeAmmo.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFSyringeAmmo.uc new file mode 100644 index 0000000..bf34b98 --- /dev/null +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFSyringeAmmo.uc @@ -0,0 +1,288 @@ +/** + * Implementation of `EAmmo` for Killing Floor medical syringe that changes + * as little as possible and only on request from another mod, otherwise not + * altering gameplay at all. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class EKFSyringeAmmo extends EAmmo; + +var private NativeActorRef syringeReference; + +protected function Finalizer() +{ + _.memory.Free(syringeReference); + syringeReference = none; +} + +/** + * Creates new `EKFSyringeAmmo` that refers to the `syringeInstance`'s + * ammunition. + * + * @param syringeInstance Native syringe instance, whose ammunition + * new `EKFSyringeAmmo` will represent. + * @return New `EKFSyringeAmmo` that represents ammunition of given + * `syringeInstance`. `none` iff `syringeInstance` is `none`. + */ +public final static /*unreal*/ function EKFSyringeAmmo Wrap( + Syringe syringeInstance) +{ + local EKFSyringeAmmo newReference; + if (syringeInstance == none) { + return none; + } + newReference = EKFSyringeAmmo(__().memory.Allocate(class'EKFSyringeAmmo')); + newReference.syringeReference = + __().unreal.ActorRef(syringeInstance); + return newReference; +} + +public function EInterface Copy() +{ + local Syringe syringeInstance; + syringeInstance = GetNativeInstance(); + return Wrap(syringeInstance); +} + +public function bool Supports(class newInterfaceClass) +{ + if (newInterfaceClass == none) return false; + if (newInterfaceClass == class'EItem') return true; + if (newInterfaceClass == class'EAmmo') return true; + if (newInterfaceClass == class'EKFSyringeAmmo') return true; + + return false; +} + +public function EInterface As(class newInterfaceClass) +{ + if (!IsExistent()) { + return none; + } + if ( newInterfaceClass == class'EItem' + || newInterfaceClass == class'EAmmo' + || newInterfaceClass == class'EKFSyringeAmmo') + { + return Copy(); + } + return none; +} + +public function bool IsExistent() +{ + return (GetNativeInstance() != none); +} + +public function bool SameAs(EInterface other) +{ + local EKFSyringeAmmo otherAmmo; + otherAmmo = EKFSyringeAmmo(other); + if (otherAmmo == none) { + return false; + } + return (GetNativeInstance() == otherAmmo.GetNativeInstance()); +} + +/** + * Returns `KFAmmunition` instance represented by the caller `EKFAmmo`. + * + * @return `KFAmmunition` instance represented by the caller `EKFAmmo`. + */ +public final /*unreal*/ function Syringe GetNativeInstance() +{ + if (syringeReference != none) { + return Syringe(syringeReference.Get()); + } + return none; +} + +public function array GetTags() +{ + local array tagArray; + if (syringeReference == none) return tagArray; + if (syringeReference.Get() == none) return tagArray; + + tagArray[0] = P("ammo").Copy(); + return tagArray; +} + +public function bool HasTag(Text tagToCheck) +{ + if (tagToCheck == none) return false; + if (tagToCheck.Compare(P("ammo"))) return true; + + return false; +} + +public function Text GetTemplate() +{ + local Syringe syringeInstance; + syringeInstance = GetNativeInstance(); + if (syringeInstance == none) { + return none; + } + return _.text.FromString("kfmod.syringe:ammo"); +} + +public function Text GetName() +{ + local Syringe syringeInstance; + syringeInstance = GetNativeInstance(); + if (syringeInstance == none) { + return none; + } + return _.text.FromString("Syringe's ammo"); +} + +public function bool IsRemovable() +{ + return false; +} + +public function bool IsSellable() +{ + return false; +} + +/** + * Medic ammo is free and does not have a price in Killing Floor. + */ +public function bool SetPrice(int newPrice) +{ + return false; +} + +public function int GetPrice() +{ + return 0; +} + +public function int GetTotalPrice() +{ + return 0; +} + +public function int GetPriceOf(int ammoAmount) +{ + return 0; +} + +public function bool SetWeight(int newWeight) +{ + return false; +} + +public function int GetWeight() +{ + return 0; +} + +public function Add(int amount, optional bool forceAddition) +{ + local Syringe syringeInstance; + syringeInstance = GetNativeInstance(); + if (syringeInstance == none) { + return; + } + if (forceAddition) { + syringeInstance.ammoCharge[0] += amount; + } + else + { + syringeInstance.ammoCharge[0] = + Min(syringeInstance.maxAmmoCount, + syringeInstance.ammoCharge[0] + amount); + } + // Correct possible negative values + if (syringeInstance.ammoCharge[0] < 0) { + syringeInstance.ammoCharge[0] = 0; + } +} + +public function int GetAmount() +{ + local Syringe syringeInstance; + syringeInstance = GetNativeInstance(); + if (syringeInstance == none) { + return 0; + } + return Max(0, syringeInstance.ammoCharge[0]); +} + +public function int GetTotalAmount() +{ + return GetAmount(); +} + +public function SetAmount(int amount, optional bool forceAddition) +{ + local Syringe syringeInstance; + syringeInstance = GetNativeInstance(); + if (syringeInstance == none) { + return; + } + if (forceAddition) { + syringeInstance.ammoCharge[0] = amount; + } + else + { + syringeInstance.ammoCharge[0] = + Min(syringeInstance.maxAmmoCount, amount); + } + // Correct possible negative values + if (syringeInstance.ammoCharge[0] < 0) { + syringeInstance.ammoCharge[0] = 0; + } +} + +public function int GetMaxAmount() +{ + local Syringe syringeInstance; + syringeInstance = GetNativeInstance(); + if (syringeInstance == none) { + return 0; + } + return Max(0, syringeInstance.maxAmmoCount); +} + +public function int GetMaxTotalAmount() +{ + return GetMaxAmount(); +} + +public function bool SetMaxAmount( + int newMaxAmmo, + optional bool leaveCurrentAmmo) +{ + return false; +} + +public function bool SetMaxTotalAmount( + int newTotalMaxAmmo, + optional bool leaveCurrentAmmo) +{ + return false; +} + +public function bool HasWeapon() +{ + return (GetNativeInstance() != none); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFUnknownItem.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFUnknownItem.uc new file mode 100644 index 0000000..c5a1a6a --- /dev/null +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFUnknownItem.uc @@ -0,0 +1,168 @@ +/** + * Dummy implementation for `EItem` interface that can wrap around `Inventory` + * instances that Acedia does not know about - including any non-weapons and + * non-ammo items added by any other mods. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class EKFUnknownItem extends EItem; + +var private NativeActorRef inventoryReference; + +protected function Finalizer() +{ + _.memory.Free(inventoryReference); + inventoryReference = none; +} + +/** + * Creates new `EKFUnknownItem` that refers to the `inventoryInstance`. + * + * @param inventoryInstance Native inventory instance that new + * `EKFUnknownItem` will represent. + * @return New `EKFUnknownItem` that represents given `inventoryInstance`. + * `none` iff `inventoryInstance` is either `none`. + */ +public final static /*unreal*/ function EKFUnknownItem Wrap( + Inventory inventoryInstance) +{ + local EKFUnknownItem newReference; + if (inventoryInstance == none) return none; + if (Ammunition(inventoryInstance) != none) return none; + // This one is not actually used for anything, so it is not real + if (inventoryInstance.class == class'KFMod.FlashlightAmmo') return none; + + newReference = EKFUnknownItem(__().memory.Allocate(class'EKFUnknownItem')); + newReference.inventoryReference = __().unreal.ActorRef(inventoryInstance); + return newReference; +} + +/** + * Returns `Inventory` instance represented by the caller `EKFUnknownItem`. + * + * @return `Inventory` instance represented by the caller `EKFUnknownItem`. + */ +public final /*unreal*/ function Inventory GetNativeInstance() +{ + if (inventoryReference != none) { + return Inventory(inventoryReference.Get()); + } + return none; +} + +public function EInterface Copy() +{ + local Inventory inventoryInstance; + inventoryInstance = GetNativeInstance(); + return Wrap(inventoryInstance); +} + +public function bool Supports(class newInterfaceClass) +{ + if (newInterfaceClass == none) return false; + if (newInterfaceClass == class'EItem') return true; + + return false; +} + +public function EInterface As(class newInterfaceClass) +{ + if (!IsExistent()) { + return none; + } + if (newInterfaceClass == class'EItem') { + return Copy(); + } + return none; +} + +public function bool IsExistent() +{ + return (GetNativeInstance() != none); +} + +public function bool SameAs(EInterface other) +{ + local EKFUnknownItem otherItem; + otherItem = EKFUnknownItem(other); + if (otherItem == none) { + return false; + } + return (GetNativeInstance() == otherItem.GetNativeInstance()); +} + +public function array GetTags() +{ + local array emptyArray; + return emptyArray; +} + +public function bool HasTag(Text tagToCheck) +{ + return false; +} + +public function Text GetTemplate() +{ + local Inventory inventory; + inventory = GetNativeInstance(); + if (inventory == none) { + return none; + } + return _.text.FromString(string(inventory.class)); +} + +public function Text GetName() +{ + local Inventory inventory; + inventory = GetNativeInstance(); + if (inventory == none) { + return none; + } + return _.text.FromString(inventory.GetHumanReadableName()); +} + +public function bool IsRemovable() +{ + return true; +} + +public function bool IsSellable() +{ + return false; +} + +public function bool SetPrice(int newPrice) +{ + return false; +} + +public function int GetPrice() { return 0; } + +public function bool SetWeight(int newWeight) +{ + return false; +} + +public function int GetWeight() +{ + return 0; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFWeapon.uc b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFWeapon.uc index e030b98..ad30565 100644 --- a/sources/Gameplay/KF1Frontend/BaseImplementation/EKFWeapon.uc +++ b/sources/Gameplay/KF1Frontend/BaseImplementation/EKFWeapon.uc @@ -1,5 +1,5 @@ /** - * Implementation of `EItem` for classic Killing Floor weapons that changes + * Implementation of `EWeapon` for classic Killing Floor weapons that changes * as little as possible and only on request from another mod, otherwise not * altering gameplay at all. * Copyright 2021 - 2022 Anton Tarasenko @@ -19,11 +19,12 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class EKFWeapon extends EItem - abstract; +class EKFWeapon extends EWeapon; var private NativeActorRef weaponReference; +var private config array< class > weaponsWithFlashlight; + protected function Finalizer() { _.memory.Free(weaponReference); @@ -40,11 +41,59 @@ protected function Finalizer() public final static /*unreal*/ function EKFWeapon Wrap(KFWeapon weaponInstance) { local EKFWeapon newReference; + if (weaponInstance == none) { + return none; + } newReference = EKFWeapon(__().memory.Allocate(class'EKFWeapon')); newReference.weaponReference = __().unreal.ActorRef(weaponInstance); return newReference; } +public function EInterface Copy() +{ + local KFWeapon weaponInstance; + weaponInstance = GetNativeInstance(); + return Wrap(weaponInstance); +} + +public function bool Supports(class newInterfaceClass) +{ + if (newInterfaceClass == none) return false; + if (newInterfaceClass == class'EWeapon') return true; + if (newInterfaceClass == class'EKFWeapon') return true; + + return false; +} + +public function EInterface As(class newInterfaceClass) +{ + if (!IsExistent()) { + return none; + } + if ( newInterfaceClass == class'EItem' + || newInterfaceClass == class'EWeapon' + || newInterfaceClass == class'EKFWeapon') + { + return Copy(); + } + return none; +} + +public function bool IsExistent() +{ + return (GetNativeInstance() != none); +} + +public function bool SameAs(EInterface other) +{ + local EKFWeapon otherWeapon; + otherWeapon = EKFWeapon(other); + if (otherWeapon == none) { + return false; + } + return (GetNativeInstance() == otherWeapon.GetNativeInstance()); +} + /** * Returns `KFWeapon` instance represented by the caller `EKFWeapon`. * @@ -65,9 +114,19 @@ public function array GetTags() if (weaponReference.Get() == none) return tagArray; tagArray[0] = P("weapon").Copy(); + tagArray[1] = P("visible").Copy(); return tagArray; } +public function bool HasTag(Text tagToCheck) +{ + if (tagToCheck == none) return false; + if (tagToCheck.Compare(P("weapon"))) return true; + if (tagToCheck.Compare(P("visible"))) return true; + + return false; +} + public function Text GetTemplate() { local Weapon weapon; @@ -85,7 +144,7 @@ public function Text GetName() weapon = Weapon(weaponReference.Get()); if (weapon == none) return none; - return _.text.FromString(Locs(weapon.itemName)); + return _.text.FromString(weapon.GetHumanReadableName()); } public function bool IsRemovable() @@ -145,6 +204,97 @@ public function int GetWeight() return int(kfWeapon.weight); } +public function array GetAvailableAmmo() +{ + local EAmmo nextAmmo; + local KFWeapon kfWeapon; + local Inventory nextInventory; + local array result; + local class ammoClass1, ammoClass2; + if (weaponReference == none) return result; + kfWeapon = KFWeapon(weaponReference.Get()); + if (kfWeapon == none) return result; + if (kfWeapon.owner == none) return result; + + ammoClass1 = _.unreal.inventory.GetAmmoClass(kfWeapon, 0); + ammoClass2 = _.unreal.inventory.GetAmmoClass(kfWeapon, 1); + nextInventory = kfWeapon.owner.inventory; + while (nextInventory != none) + { + if ( nextInventory.class == ammoClass1 + || nextInventory.class == ammoClass2) + { + nextAmmo = class'EKFAmmo'.static.Wrap(Ammunition(nextInventory)); + if (nextAmmo != none) { + result[result.length] = nextAmmo; + } + // Reset temporary variable to avoid adding same `EKFAmmo` twice + nextAmmo = none; + } + nextInventory = nextInventory.inventory; + } + result = AddSpecialAmmo(kfWeapon, result); + return result; +} + +private function array AddSpecialAmmo( + KFWeapon kfWeapon, + array ammoCollection) +{ + local EAmmo nextAmmo; + local KFMedicGun kfMedicWeapon; + if (kfWeapon == none) { + return ammoCollection; + } + kfMedicWeapon = KFMedicGun(kfWeapon); + if (kfMedicWeapon != none) + { + nextAmmo = class'EKFMedicAmmo'.static.Wrap(kfMedicWeapon); + if (nextAmmo != none) { + ammoCollection[ammoCollection.length] = nextAmmo; + } + } + if (HasFlashlight(kfWeapon)) + { + nextAmmo = + class'EKFFlashlightAmmo'.static.Wrap(KFHumanPawn(kfWeapon.owner)); + if (nextAmmo != none) { + ammoCollection[ammoCollection.length] = nextAmmo; + } + } + if (kfWeapon.class == class'KFMod.Syringe') + { + nextAmmo = + class'EKFSyringeAmmo'.static.Wrap(Syringe(kfWeapon)); + if (nextAmmo != none) { + ammoCollection[ammoCollection.length] = nextAmmo; + } + } + return ammoCollection; +} + +private function bool HasFlashlight(KFWeapon weapon) +{ + local int i; + if (weapon == none) { + return false; + } + for (i = 0; i < weaponsWithFlashlight.length; i += 1) + { + if (weapon.class == weaponsWithFlashlight[i]) { + return true; + } + } + return false; +} + defaultproperties { + weaponsWithFlashlight(0) = class'Single' + weaponsWithFlashlight(1) = class'Dualies' + weaponsWithFlashlight(2) = class'Shotgun' + weaponsWithFlashlight(3) = class'CamoShotgun' + weaponsWithFlashlight(4) = class'NailGun' + weaponsWithFlashlight(5) = class'BenelliShotgun' + weaponsWithFlashlight(6) = class'GoldenBenelliShotgun' } \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/KF1_Frontend.uc b/sources/Gameplay/KF1Frontend/KF1_Frontend.uc index 4ffbf9f..ad34233 100644 --- a/sources/Gameplay/KF1Frontend/KF1_Frontend.uc +++ b/sources/Gameplay/KF1Frontend/KF1_Frontend.uc @@ -33,5 +33,6 @@ public function EItemTemplateInfo GetItemTemplateInfo(Text templateName) defaultproperties { - tradingClass = class'KF1_TradingComponent' + templatesClass = class'KF1_TemplatesComponent' + tradingClass = class'KF1_TradingComponent' } \ No newline at end of file diff --git a/sources/Gameplay/KF1Frontend/Trading/KF1_TemplatesComponent.uc b/sources/Gameplay/KF1Frontend/Trading/KF1_TemplatesComponent.uc new file mode 100644 index 0000000..0096b84 --- /dev/null +++ b/sources/Gameplay/KF1Frontend/Trading/KF1_TemplatesComponent.uc @@ -0,0 +1,215 @@ +/** + * `ATemplatesComponent`'s implementation for `KF1_Frontend`. + * Lists weapons available at the trader, provides support for per-perk lists, + * derived from the `KFLevelRules`. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class KF1_TemplatesComponent extends ATemplatesComponent; + +var private bool listsAreReady; +// TODO: add tools +var private array availableWeaponLists; +var private array allWeaponsList; +var private array medicWeaponsList; +var private array supportWeaponsList; +var private array sharpshooterWeaponsList; +var private array commandoWeaponsList; +var private array berserkerWeaponsList; +var private array firebugWeaponsList; +var private array demolitionWeaponsList; +var private array neutralWeaponsList; + +protected function Finalizer() +{ + _.memory.FreeMany(allWeaponsList); + _.memory.FreeMany(medicWeaponsList); + _.memory.FreeMany(supportWeaponsList); + _.memory.FreeMany(sharpshooterWeaponsList); + _.memory.FreeMany(commandoWeaponsList); + _.memory.FreeMany(berserkerWeaponsList); + _.memory.FreeMany(firebugWeaponsList); + _.memory.FreeMany(demolitionWeaponsList); + _.memory.FreeMany(neutralWeaponsList); + _.memory.FreeMany(availableWeaponLists); + if (allWeaponsList.length > 0) { + allWeaponsList.length = 0; + } + if (medicWeaponsList.length > 0) { + medicWeaponsList.length = 0; + } + if (supportWeaponsList.length > 0) { + supportWeaponsList.length = 0; + } + if (sharpshooterWeaponsList.length > 0) { + sharpshooterWeaponsList.length = 0; + } + if (commandoWeaponsList.length > 0) { + commandoWeaponsList.length = 0; + } + if (berserkerWeaponsList.length > 0) { + berserkerWeaponsList.length = 0; + } + if (firebugWeaponsList.length > 0) { + firebugWeaponsList.length = 0; + } + if (demolitionWeaponsList.length > 0) { + demolitionWeaponsList.length = 0; + } + if (neutralWeaponsList.length > 0) { + neutralWeaponsList.length = 0; + } + if (availableWeaponLists.length > 0) { + availableWeaponLists.length = 0; + } + listsAreReady = false; +} + +private function BuildKFWeaponLists() +{ + local LevelInfo level; + local KFLevelRules kfLevelRules; + if (listsAreReady) return; + level = _.unreal.GetLevel(); + if (level == none) return; + foreach level.DynamicActors(class'KFMod.KFLevelRules', kfLevelRules) break; + if (kfLevelRules == none) return; + + medicWeaponsList = MakeWeaponList(kfLevelRules.mediItemForSale); + supportWeaponsList = MakeWeaponList(kfLevelRules.suppItemForSale); + sharpshooterWeaponsList = MakeWeaponList(kfLevelRules.shrpItemForSale); + commandoWeaponsList = MakeWeaponList(kfLevelRules.commItemForSale); + berserkerWeaponsList = MakeWeaponList(kfLevelRules.bersItemForSale); + firebugWeaponsList = MakeWeaponList(kfLevelRules.fireItemForSale); + demolitionWeaponsList = MakeWeaponList(kfLevelRules.demoItemForSale); + neutralWeaponsList = MakeWeaponList(kfLevelRules.neutItemForSale); + availableWeaponLists[0] = _.text.FromString("all weapons"); + availableWeaponLists[1] = _.text.FromString("trading weapons"); + availableWeaponLists[2] = _.text.FromString("medic weapons"); + availableWeaponLists[3] = _.text.FromString("support weapons"); + availableWeaponLists[4] = _.text.FromString("sharpshooter weapons"); + availableWeaponLists[5] = _.text.FromString("commando weapons"); + availableWeaponLists[6] = _.text.FromString("firebug weapons"); + availableWeaponLists[7] = _.text.FromString("demolition weapons"); + availableWeaponLists[8] = _.text.FromString("neutral weapons"); + listsAreReady = true; +} + +private function array MakeWeaponList(array< class > shopList) +{ + local int i; + local Text nextTemplate; + local class nextWeaponClass; + local array resultArray; + if (listsAreReady) { + return resultArray; + } + for (i = 0; i < shopList.length; i += 1) + { + if (shopList[i] == none) continue; + nextWeaponClass = class(shopList[i].default.inventoryType); + if (nextWeaponClass == none) continue; + + nextTemplate = _.text.FromString(string(nextWeaponClass)); + resultArray[resultArray.length] = nextTemplate.Copy(); + allWeaponsList[allWeaponsList.length] = nextTemplate; + } + return resultArray; +} + +private function array CopyList(array inputList) +{ + local int i; + local array outputList; + // `inputList` is guaranteed to not contain invalid `Text` objects + for (i = 0; i < inputList.length; i += 1) { + outputList[outputList.length] = inputList[i].Copy(); + } + return outputList; +} + +public function bool ItemListExists(Text listName) +{ + local string listNameAsString; + if (listName == none) return false; + listNameAsString = listName.ToString(); + if (listNameAsString == "weapons") return true; + if (listNameAsString == "all weapons") return true; + if (listNameAsString == "trading weapons") return true; + if (listNameAsString == "medic weapons") return true; + if (listNameAsString == "support weapons") return true; + if (listNameAsString == "sharpshooter weapons") return true; + if (listNameAsString == "commando weapons") return true; + if (listNameAsString == "berserker weapons") return true; + if (listNameAsString == "firebug weapons") return true; + if (listNameAsString == "demolition weapons") return true; + if (listNameAsString == "neutral weapons") return true; + + return false; +} + +public function array GetItemList(Text listName) +{ + local string listNameAsString; + local array emptyArray; + if (listName == none) { + return emptyArray; + } + listNameAsString = listName.ToString(); + BuildKFWeaponLists(); + if ( listNameAsString == "weapons" + || listNameAsString == "all weapons" + || listNameAsString == "trading weapons") + { + return CopyList(allWeaponsList); + } + if (listNameAsString == "medic weapons") { + return CopyList(medicWeaponsList); + } + if (listNameAsString == "support weapons") { + return CopyList(supportWeaponsList); + } + if (listNameAsString == "sharpshooter weapons") { + return CopyList(sharpshooterWeaponsList); + } + if (listNameAsString == "commando weapons") { + return CopyList(commandoWeaponsList); + } + if (listNameAsString == "berserker weapons") { + return CopyList(berserkerWeaponsList); + } + if (listNameAsString == "firebug weapons") { + return CopyList(firebugWeaponsList); + } + if (listNameAsString == "demolition weapons") { + return CopyList(demolitionWeaponsList); + } + if (listNameAsString == "neutral weapons") { + return CopyList(neutralWeaponsList); + } + return emptyArray; +} + +public function array GetAvailableLists() +{ + BuildKFWeaponLists(); + return CopyList(availableWeaponLists); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Players/EPlayer.uc b/sources/Players/EPlayer.uc index 0371271..c14aac4 100644 --- a/sources/Players/EPlayer.uc +++ b/sources/Players/EPlayer.uc @@ -134,7 +134,7 @@ public function bool SameAs(EInterface other) if (other == none) return false; if (controller == none) return false; asPlayer = EPlayer(other); - if (asPlayer != none) return false; + if (asPlayer == none) return false; otherController = asPlayer.controller; if (otherController == none) return false; diff --git a/sources/Players/Inventory/EAmmo.uc b/sources/Players/Inventory/EAmmo.uc new file mode 100644 index 0000000..ae23a42 --- /dev/null +++ b/sources/Players/Inventory/EAmmo.uc @@ -0,0 +1,259 @@ +/** + * Abstract interface that represents ammunition of a certain type. + * Ammunition methods make distinction between "amount" and "total amount": + * * "Amount" is how much of this ammo is stored in this + * inventory item, "max amount" is how much it can store at once. + * These values can be affected by other items in the inventory. + * * "Total amount" is how much ammo of this type player has in his + * inventory in total. + * Neither amounts can ever be negative. + * For Killing Floor "total ammo" corresponds to the amount of ammunition + * associated with a weapon, while "ammo" would correspond to the amount of + * ammo still unleaded into the weapon. This means that "max ammo" becomes + * quite a bizarre value that depends on how full your magazine is. + * For example, if you bought lever action rifle, filled it + * with ammo (80 bullets) and then shot out 6, you will have: + * * "Ammo" == 70 - since you will have that much still unloaded; + * * "Max ammo" == 76 - since with 4 loaded bullets you can only + * have 76 unloaded ones; + * * "Total ammo" == 74 - amount of bullets you can still shoot; + * * "Max total ammo" = 80 - since that is the limit of LAR bullets you + * can carry in total. + * When one loads ammo into the weapon, its "amount" decreases, but its + * "total amount" stays the same. Unless specified otherwise, all the methods + * deal with a regular "amount". + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class EAmmo extends EItem + abstract; + +/** + * Changes amount of ammo inside referred ammunition item by given `amount`. + * + * Negative argument values will decrease it ("adding" a negative amount). + * + * New value cannot go below zero and can only exceed maximum amount + * (@see `GetMaxAmount()` and @see `GetMaxTotalAmount()`) if `forceAddition` + * is also set to `true`. + * If resulting value is to go over the limits - it will be clamped inside + * allowed range. + * + * @param amount How much ammo to add. `0` does nothing, negative + * values decrease the total ammo count. Cannot force total ammo to go into + * negative values. + * @param forceAddition This parameter is only relevant when changing ammo + * amount by `amount` will go over maximum (total) amount that referred + * ammo item can store. Setting this parameter to `true` will allow you to + * add more ammo than caller `EAmmo` normally supports. + * Cannot force total amount below zero. + */ +public function Add(int amount, optional bool forceAddition) {} + +/** + * Returns current price of total ammo inside inventory of the owner of + * referred ammunition item. + * + * In comparison, `EItem`'s method `GetPrice()` returns the price of only + * the ammo inside the referred item. + * @return Current price of total ammo inside inventory of the owner of + * referred ammunition item. + */ +public function int GetTotalPrice() +{ + return 0; +} + +/** + * Returns how much would `ammoAmount` amount of referred ammo item would cost. + * + * @return Price of `ammoAmount` amount of referred ammo item. + */ +public function int GetPriceOf(int ammoAmount) +{ + return 0; +} + +/** + * Returns current amount of ammo inside referred ammunition item. + * + * Guaranteed to not be negative, but can exceed maximum value + * (@see `GetMaxAmount()`). + * + * @return Current amount of ammo inside referred ammunition item. + */ +public function int GetAmount() +{ + return 0; +} + +/** + * Returns current total amount of ammo inside inventory of the owner of + * referred ammunition item. + * + * Guaranteed to not be negative, but can exceed maximum value + * (@see `GetMaxTotalAmount()`). + * + * @return Current amount of ammo inside referred ammunition item. + */ +public function int GetTotalAmount() +{ + return 0; +} + +/** + * Changes amount of ammo inside referred ammunition item. + * + * Negative values will be treated as `0`. Values that exceed + * maximum (total) amount will be automatically reduced to said maximum amount + * (@see `GetMaxAmount()` and @see `GetMaxTotalAmount()`), unless + * `forceAddition` is also set to `true`. + * If resulting value is to go over the limits - it will be clamped inside + * allowed range. + * + * @param amount How much ammo should referred ammunition item have. + * Negative values are treated like `0`. + * @param forceAddition This parameter is only relevant when `amount` is + * higher than maximum (total) amount that referred ammo item can store. + * Setting this parameter to `true` will allow you to add more ammo than + * caller `EAmmo` normally supports. Cannot force total amount below zero. + */ +public function SetAmount(int amount, optional bool forceAddition) {} + +/** + * Returns maximum amount of ammo referred ammunition item supports. + * + * This is not a hard limit and can be bypassed by `SetAmount()` and + * `Add()` methods, meaning that it is possible that + * `GetAmount() > GetMaxAmount()`. + * Treat this value like a limit obtainable through "normal means", that + * can only be exceeded through cheats or special powerups of some kind. + * + * @return Current "soft" max ammo limit of the referred ammunition item. + * Returning negative value means that there is no upper limit. + * Zero is considered a valid value. + */ +public function int GetMaxAmount() +{ + return 0; +} + +/** + * Returns maximum total amount of ammo owner of the referred ammunition item + * can hold. + * + * This is not a hard limit and can be bypassed by `SetAmount()` and + * `Add()` methods, meaning that it is possible that + * `GetTotalAmount() > GetMaxTotalAmount()`. + * Treat this value like a limit obtainable through "normal means", that + * can only be exceeded through cheats or special powerups of some kind. + * + * @return Current "soft" max total ammo limit of the referred ammunition item. + * Returning negative value means that there is no upper limit. + * Zero is considered a valid value. + */ +public function int GetMaxTotalAmount() +{ + return 0; +} + +/** + * Changes maximum amount of ammo referred ammunition item supports. + * + * This is not a hard limit and can be bypassed by `SetAmount()` and + * `Add()` methods, meaning that it is possible that + * `GetAmount() > GetMaxAmount()`. + * Treat this value like a limit obtainable through "normal means", that + * can only be exceeded through cheats or special powerups of some kind. + * + * Referred ammunition item does not have to support this method and is + * allowed to refuse changing maximum ammo value. It can also only support + * certain ranges of values. + * + * @param newMaxAmmo New maximum ammo referred ammunition + * should support. Negative values mean unlimited maximum value. + * @param leaveCurrentAmmo Default value of `false` will result in current + * ammo being updated to not exceed `newMaxAmmo`, while setting this to + * `true` will leave it unchanged. + * + * @return `true` if maximum value was changed and `false` otherwise. + */ +public function bool SetMaxAmount( + int newMaxAmmo, + optional bool leaveCurrentAmmo) +{ + return false; +} + +/** + * Changes maximum total amount of ammo that owner of the referred + * ammunition item supports. + * + * This is not a hard limit and can be bypassed by `SetAmount()` and + * `Add()` methods, meaning that it is possible that + * `GetTotalAmount() > GetMaxTotalAmount()`. + * Treat this value like a limit obtainable through "normal means", that + * can only be exceeded through cheats or special powerups of some kind. + * + * Referred ammunition item does not have to support this method is allowed to + * refuse changing maximum ammo value. It can also only support certain ranges + * of values. + * + * @param newTotalMaxAmmo New maximum total ammo owner of the referred + * ammunition can have. Negative values mean unlimited maximum value. + * @param leaveCurrentAmmo Default value of `false` will result in current + * total ammo being updated to not exceed `newTotalMaxAmmo`, while setting + * this to `true` will leave it unchanged. + * + * @return `true` if maximum value was changed and `false` otherwise. + */ +public function bool SetMaxTotalAmount( + int newTotalMaxAmmo, + optional bool leaveCurrentAmmo) +{ + return false; +} + +/** + * Checks whether the owner of the referred ammo item also has a weapon that + * can be loaded with that ammo. + * + * @return `true` if owner of the referred ammo has a weapon that can be + * loaded with that ammo and `false` otherwise. + */ +public function bool HasWeapon() +{ + return false; +} + +/** + * Maxes out amount of ammo of the referred ammunition item. + * + * Does nothing if current ammo is already at (or higher) than maximum value + * (@see `GetMaxAmount()`). + */ +public function Fill() +{ + if (GetMaxAmount() < 0) return; + if (GetAmount() >= GetMaxAmount()) return; + + SetAmount(GetMaxAmount()); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Players/Inventory/EInventory.uc b/sources/Players/Inventory/EInventory.uc index 74ce303..3f92d01 100644 --- a/sources/Players/Inventory/EInventory.uc +++ b/sources/Players/Inventory/EInventory.uc @@ -21,7 +21,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class EInventory extends AcediaObject +class EInventory extends EInterface abstract; /** @@ -30,7 +30,7 @@ class EInventory extends AcediaObject * This method should not be called manually, unless you implement your own * game interface. * - * Cannot fail for any connected player and can assume it will not be called + * Cannot fail for any connected player and will assume it will not be called * for not connected ones. * * @param player `EPlayer` for which to initialize this inventory. @@ -44,17 +44,23 @@ public function Initialize(EPlayer player) {} * inventory system - it can refuse it. * * @param newItem New item to add to the caller inventory system. + * Can be destroyed as a result of this call, if it gets merged with + * another weapon inside the inventory. * @param forceAddition This parameter is only relevant when `newItem` * cannot be added in the caller inventory system. If it cannot be added * because of the conflict with other items - setting this flag to `true` * allows caller inventory system to get rid of such items to make room for * `newItem`. Removing items is only allowed if it will actually let us add * `newItem`. How removal will be done is up to the implementation. - * @return `true` if `newItem` was added and `false` otherwise. + * @return `EItem` added as a result. Can be different from `newItem` in case + * inventory made it "merge" with another weapon. This can happen, + * for example, if we add a single pistol when inventory already contains + * pistol of the same type. + * `none` if we have failed to add `newItem` to the inventory. */ -public function bool Add(EItem newItem, optional bool forceAddition) +public function EItem Add(EItem newItem, optional bool forceAddition) { - return false; + return none; } /** @@ -72,19 +78,22 @@ public function bool Add(EItem newItem, optional bool forceAddition) * allows caller inventory system to get rid of such items to make room for * new item. Removing items is only allowed if it will actually let us add * new item. How removal will be done is up to the implementation. - * @return `true` if new item was added and `false` otherwise. + * @return Reference to `EItem` interface to the added item entity, + * `none` iff adding item has failed. */ -public function bool AddTemplate( +public function EItem AddTemplate( Text newItemTemplate, optional bool forceAddition) { - return false; + return none; } /** * Checks whether given item `itemToCheck` can be added to the caller * inventory system. * + * See also `CanAddExplain()`. + * * @param itemToCheck Item to check for whether we can add it to * the caller `EInventory`. * @param forceAddition New items can be added with or without @@ -93,15 +102,24 @@ public function bool AddTemplate( * @return `true` if given `itemToCheck` can be added to the caller * inventory system with given flag `forceAddition` and `false` otherwise. */ -public function bool CanAdd(EItem itemToCheck, optional bool forceAddition) +public final function bool CanAdd( + EItem itemToCheck, + optional bool forceAddition) { - return false; + local bool success; + local Text explanation; + explanation = CanAddExplain(itemToCheck, forceAddition); + success = (explanation == none); + _.memory.Free(explanation); + return success; } /** * Checks whether item with given template `itemToCheck` can be added to * the caller inventory system. * + * See also `CanAddTemplateExplain()`. + * * @param itemTemplateToCheck Template of the item to check for whether we can * add it to the caller `EInventory`. * @param forceAddition New items can be added with or without @@ -115,7 +133,58 @@ public function bool CanAddTemplate( Text itemTemplateToCheck, optional bool forceAddition) { - return false; + local bool success; + local Text explanation; + explanation = CanAddTemplateExplain(itemTemplateToCheck, forceAddition); + success = (explanation == none); + _.memory.Free(explanation); + return success; +} + +/** + * Checks whether given item `itemToCheck` can be added to the caller + * inventory system and provides short explanation (dependent on + * implementation) if item cannot be added. + * + * See also `CanAdd()`. + * + * @param itemToCheck Item to check for whether we can add it to + * the caller `EInventory`. + * @param forceAddition New items can be added with or without + * `forceAddition` flag. This parameter allows you to check whether we + * test for addition with or without it. + * @return `none` if given `itemToCheck` can be added to the caller + * inventory system with given flag `forceAddition` and `Text` with + * description of reason why not otherwise. + */ +public function Text CanAddExplain( + EItem itemToCheck, + optional bool forceAddition) +{ + return none; +} + +/** + * Checks whether item with given template `itemToCheck` can be added to + * the caller inventory system and provides short explanation (dependent on + * implementation) if item cannot be added. + * + * See also `CanAddTemplate()`. + * + * @param itemTemplateToCheck Template of the item to check for whether we can + * add it to the caller `EInventory`. + * @param forceAddition New items can be added with or without + * `forceAddition` flag. This parameter allows you to check whether we + * test for addition with or without it. + * @return `none` if given `itemToCheck` can be added to the caller + * inventory system with given flag `forceAddition` and `Text` with + * description of reason why not otherwise. + */ +public function Text CanAddTemplateExplain( + Text itemTemplateToCheck, + optional bool forceAddition) +{ + return none; } /** @@ -137,15 +206,16 @@ public function bool CanAddTemplate( * `EInventory` in the first place). */ public function bool Remove( - EItem itemToRemove, - optional bool keepItem, - optional bool forceRemoval) + EItem itemToRemove, + optional bool keepItem, + optional bool forceRemoval) { return false; } /** - * Removes item of type `itemTemplateToRemove` from the caller `EInventory`. + * Removes item with given template `itemTemplateToRemove` from the caller + * `EInventory`. * * By default removes one arbitrary (can be based on simple convenience of * implementation) item, but optional parameter can make it remove all items @@ -194,13 +264,18 @@ public function bool RemoveTemplate( * @param forceRemoval Set this to `true` if item must be removed * no matter what. Otherwise inventory system can refuse removal of items, * whose `IsRemovable()` returns `false`. + * @param forceRemoval Set this to `true` if even invisible to the player + * items have to be removed. In Killing Floor only weapons are visible to + * the player (ammunition items are considered to be just + * their parameters). * @return `true` if any `EItem` was removed and `false` otherwise * (including the case where no `EItem`s were kept in the caller * `EInventory` in the first place). */ public function bool RemoveAll( optional bool keepItems, - optional bool forceRemoval) + optional bool forceRemoval, + optional bool includeHidden) { return false; } @@ -217,6 +292,20 @@ public function bool Contains(EItem itemToCheck) return false; } +/** + * Checks whether caller `EInventory` contains item with given template + * `itemTemplateToCheck`. + * + * @param itemTemplateToCheck Template we want to check for belonging to + * the caller `EInventory`. + * @result `true` if item with a given template does belong to the inventory + * and `false` otherwise. + */ +public function bool ContainsTemplate(Text itemTemplateToCheck) +{ + return false; +} + /** * Returns array with all `EItem`s contained inside the caller `EInventory`. * @@ -228,6 +317,21 @@ public function array GetAllItems() return emptyArray; } +/** + * Returns array with all `EItem`s contained inside the caller `EInventory` + * that support interface of class `interfaceClass`. + * + * @return Array with all `EItem`s that support interface of + * class `interfaceClass` contained inside the caller `EInventory`. + * Guaranteed to not contain `none` references of interfaces to + * inexistent entities. + */ +public function array GetItemsSupporting(class interfaceClass) +{ + local array emptyArray; + return emptyArray; +} + /** * Returns array with all `EItem`s contained inside the caller `EInventory` * that has specified tag `tag`. @@ -235,6 +339,8 @@ public function array GetAllItems() * @param tag Tag, which items we want to get. * @return Array with all `EItem`s contained inside the caller `EInventory` * that has specified tag `tag`. + * Guaranteed to not contain `none` references of interfaces to + * inexistent entities. */ public function array GetTagItems(Text tag) { @@ -254,6 +360,7 @@ public function array GetTagItems(Text tag) * @param tag Tag, which item we want to get. * @return `EItem` contained inside the caller `EInventory` that belongs to * the specified tag `tag`. + * Guaranteed to not be `none` or refer to non-existent entity. */ public function EItem GetTagItem(Text tag) { return none; } @@ -264,6 +371,8 @@ public function EItem GetTagItem(Text tag) { return none; } * @param template Template, that items we want to get originated from. * @return Array with all `EItem`s contained inside the caller `EInventory` * that originated from the specified template `template`. + * Guaranteed to not contain `none` references or interfaces to + * inexistent entities. */ public function array GetTemplateItems(Text template) { @@ -272,7 +381,7 @@ public function array GetTemplateItems(Text template) } /** - * Returns array with all `EItem`s contained inside the caller `EInventory` + * Returns `EItem`s contained inside the caller `EInventory` * that originated from the specified template `template`. * * If several `EItem`s inside caller `EInventory` originated from @@ -283,9 +392,27 @@ public function array GetTemplateItems(Text template) * @param template Template, that item we want to get originated from. * @return `EItem`s contained inside the caller `EInventory` that originated * from the specified template `template`. + * Guaranteed to not be `none` or refer to non-existent entity. */ public function EItem GetTemplateItem(Text template) { return none; } +/** + * Returns array of caller `EInventory`'s items that are currently equipped by + * its owner player. + * + * @return Array with all `EItem`s contained inside the caller `EInventory` + * that are equipped by its owner. + * Guaranteed to not contain `none` references or interfaces to + * inexistent entities. + */ +public function array GetEquippedItems() +{ + local array emptyArray; + return emptyArray; +} + +public function EItem GetEquippedItem() { return none; } + defaultproperties { } \ No newline at end of file diff --git a/sources/Players/Inventory/EItem.uc b/sources/Players/Inventory/EItem.uc index e95b6ff..1dd09f9 100644 --- a/sources/Players/Inventory/EItem.uc +++ b/sources/Players/Inventory/EItem.uc @@ -30,7 +30,7 @@ * 2. Weight. Accessed by `SetWeight()` / `GetWeight()`. * All of these parameters can be ignored if they are not applicable to * a certain type of item. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -47,7 +47,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class EItem extends AcediaObject +class EItem extends EInterface abstract; /** @@ -63,6 +63,12 @@ public function array GetTags() return emptyArray; } +// TODO: document this +public function bool HasTag(Text tagToCheck) +{ + return false; +} + /** * Returns template caller `EItem` was created from. * diff --git a/sources/Players/Inventory/EWeapon.uc b/sources/Players/Inventory/EWeapon.uc new file mode 100644 index 0000000..11bf8aa --- /dev/null +++ b/sources/Players/Inventory/EWeapon.uc @@ -0,0 +1,57 @@ +/** + * Abstract interface that represents any kind of weapon. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class EWeapon extends EItem + abstract; + +/** + * Returns `EAmmo` for every ammo item that can be used with the caller's + * referred weapon. Method looks for that ammo in the weapon's owner's + * inventory. + * + * @return Array of `EAmmo`s that refer to ammo items suitable for use with + * referred weapon. Every item of the returned array is guaranteed to not + * be `none` and refer to an existent item. + */ +public function array GetAvailableAmmo() +{ + local array emptyArray; + return emptyArray; +} + +/** + * Fills (@see `EAmmo.Fill()` method) `EAmmo` for every ammo item that can be + * used with the caller's referred weapon. Method looks for that ammo in + * the weapon's owner's inventory. + */ +public final function FillAmmo() +{ + local int i; + local array myAmmo; + myAmmo = GetAvailableAmmo(); + for (i = 0; i < myAmmo.length; i += 1) { + myAmmo[i].Fill(); + } + _.memory.FreeMany(myAmmo); +} + + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc index a7794be..821a2d6 100644 --- a/sources/Text/MutableText.uc +++ b/sources/Text/MutableText.uc @@ -100,6 +100,17 @@ public final function MutableText AppendCharacter(Text.Character newCharacter) return MutableText(AppendCodePoint(newCharacter.codePoint)); } +/** + * Adds new line character to the end of the caller `MutableText`. + * + * @return Caller `MutableText` to allow for method chaining. + */ +public final function MutableText AppendNewLine() +{ + AppendCodePoint(CODEPOINT_NEWLINE); + return self; +} + /** * Converts caller `MutableText` instance into lower case. */ diff --git a/sources/Types/AcediaActor.uc b/sources/Types/AcediaActor.uc index 62e537a..b7ae836 100644 --- a/sources/Types/AcediaActor.uc +++ b/sources/Types/AcediaActor.uc @@ -397,4 +397,9 @@ public static function _cleanup() defaultproperties { + RemoteRole = ROLE_None + drawType = DT_None + bCollideActors = false + bCollideWorld = false + bBlockActors = false } \ No newline at end of file diff --git a/sources/Unreal/InventoryAPI/InventoryAPI.uc b/sources/Unreal/InventoryAPI/InventoryAPI.uc new file mode 100644 index 0000000..813ed1d --- /dev/null +++ b/sources/Unreal/InventoryAPI/InventoryAPI.uc @@ -0,0 +1,530 @@ +/** + * Low-level API that provides set of utility methods for working with + * unreal script inventory classes, including some Killing Floor specific + * methods that depend on how its weapons work. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class InventoryAPI extends AcediaObject + config(AcediaSystem); + +/** + * Describes a single-dual weapons class pair. + * For example, `single = class'MK23Pickup'` and + * `dual = class'DualMK23Pickup'`. + */ +struct DualiesPair +{ + var class single; + var class dual; +}; +// All dual pairs that Acedia will recognize +var private const config array dualiesClasses; + +/** + * Describe the role of the weapon regarding a dual wielding. + * All weapons have a dual wielding role, although for most it + * is simply `DWR_None`. + */ +enum DualWieldingRole +{ + // Not a dual weapons and cannot be dual wielded; + // Most weapons are in this category (e.g. lar, ak47, husk cannon, etc.) + DWR_None, + // Not a dual weapon, but can be dual wielded (e.g. single pistols) + DWR_Single, + // A dual weapon, consisted of two single ones (e.g. dual pistols) + DWR_Dual +}; + +/** + * Returns array of single - dual pairs (`DualiesPair`) that defines which + * single weapon class corresponds to which dual class. + * For example, `KFMod.GoldenDeaglePickup` is a single class corresponding + * to the `KFMod.GoldenDualDeaglePickup` dual class. + */ +public function array GetDualiesPairs() +{ + return dualiesClasses; +} + +/** + * Returns dual wielding role of the given class of weapon `weaponClass`. + * See `DualWieldingRole` enum for more details. + * + * @param weaponClass Weapon class to check the role for. + * @return Dual wielding role of the weapon of given class `weaponClass`. + * `DWR_None` in case given `weaponClass` is `none`. + */ +public function DualWieldingRole GetDualWieldingRole( + class weaponClass) +{ + local int i; + local class pickupClass; + if (weaponClass == none) return DWR_None; + pickupClass = class(weaponClass.default.pickupClass); + if (pickupClass == none) return DWR_None; + + for (i = 0; i < dualiesClasses.length; i += 1) + { + if (dualiesClasses[i].single == pickupClass) { + return DWR_Single; + } + if (dualiesClasses[i].dual == pickupClass) { + return DWR_Dual; + } + } + return DWR_None; +} + +/** + * For "dual" weapons (`DWR_Dual`), corresponding of two "single" version + * returns class of corresponding single version, for any other + * (including single weapons themselves) returns `none`. + * + * @param weaponClass Weapon class for which to find matching single class. + * @return Single class that corresponds to the given `weaponClass`, if it is + * classified as `DWR_Dual`. `none` for every other class. + */ +public function class GetSingleClass(class weapon) +{ + local int i; + local class pickupClass; + local class singlePickupClass; + if (weapon == none) return none; + pickupClass = class(weapon.default.pickupClass); + if (pickupClass == none) return none; + + for (i = 0; i < dualiesClasses.length; i += 1) + { + if (dualiesClasses[i].dual == pickupClass) + { + singlePickupClass = dualiesClasses[i].single; + if (singlePickupClass != none) { + return class(singlePickupClass.default.inventoryType); + } + } + } + return none; +} + +/** + * For "single" weapons (`DWR_Single`) that can have a "dual" version returns + * class of corresponding dual version, for any other (including dual weapons + * themselves) returns `none`. + * + * @param weaponClass Weapon class for which to find matching dual class. + * @return Dual class that corresponds to the given `weaponClass`, if it is + * classified as `DWR_Single`. `none` for every other class. + */ +public function class GetDualClass(class weaponClass) +{ + local int i; + local class pickupClass; + local class dualPickupClass; + if (weaponClass == none) return none; + pickupClass = class(weaponClass.default.pickupClass); + if (pickupClass == none) return none; + + for (i = 0; i < dualiesClasses.length; i += 1) + { + if (dualiesClasses[i].single == pickupClass) + { + dualPickupClass = dualiesClasses[i].dual; + if (dualPickupClass != none) { + return class(dualPickupClass.default.inventoryType); + } + } + } + return none; +} + +/** + * Convenience method for finding a first inventory entry of the given + * class `inventoryClass` in the given inventory chain `inventoryChain`. + * + * Inventory is stored as a linked list, where next inventory item is available + * through the `inventory` reference. This method follows this list, starting + * from `inventoryChain` until it finds `Inventory` of the appropriate class + * or reaches the end of the list. + * + * @param inventoryClass Class of the inventory we are interested in. + * @param inventoryChain Inventory chain in which we should search for + * the given class. + * @param acceptChildClass `true` if method should also return any + * `Inventory` of class derived from `inventoryClass` and `false` if + * we want given class specifically (default). + * @return First inventory from `inventoryChain` that matches given + * `inventoryClass` class (whether exactly or as a child class, + * in case `acceptChildClass == true`). + */ +public final function Inventory Get( + class inventoryClass, + Inventory inventoryChain, + optional bool acceptChildClass) +{ + if (inventoryClass == none) { + return none; + } + while (inventoryChain != none) + { + if (inventoryChain.class == inventoryClass) { + return inventoryChain; + } + if ( acceptChildClass + && ClassIsChildOf(inventoryChain.class, inventoryClass)) + { + return inventoryChain; + } + inventoryChain = inventoryChain.inventory; + } + return none; +} + +/** + * Convenience method for finding all inventory entries of the given + * class `inventoryClass` in the given inventory chain `inventoryChain`. + * + * Inventory is stored as a linked list, where next inventory item is available + * through the `inventory` reference. This method follows this list, starting + * from `inventoryChain` until the end of the list. + * + * @param inventoryClass Class of the inventory we are interested in. + * @param inventoryChain Inventory chain in which we should search for + * the given class. + * @param acceptChildClass `true` if method should also return any + * `Inventory` of class derived from `inventoryClass` and `false` if + * we want given class specifically (default). + * @return Array of inventory items from `inventoryChain` that match given + * `inventoryClass` class (whether exactly or as a child class, + * in case `acceptChildClass == true`). + */ +public final function array GetAll( + class inventoryClass, + Inventory inventoryChain, + optional bool acceptChildClass) +{ + local bool shouldAdd; + local array result; + if (inventoryClass == none) { + return result; + } + while (inventoryChain != none) + { + shouldAdd = false; + if (inventoryChain.class == inventoryClass) { + shouldAdd = true; + } + else if (acceptChildClass) { + shouldAdd = ClassIsChildOf(inventoryChain.class, inventoryClass); + } + if (shouldAdd) { + result[result.length] = inventoryChain; + } + inventoryChain = inventoryChain.inventory; + } + return result; +} + +/** + * Checks whether `inventory` is contained in the inventory given by + * `inventoryChain`. + * + * @param inventory Item we are searching for. + * @param inventoryChain Inventory chain in which we should search for + * the given item. + * @return `true` if `inventoryChain` contains `inventory` and + * `false` otherwise. + */ +public final function bool Contains( + Inventory inventory, + Inventory inventoryChain) +{ + while (inventoryChain != none) + { + if (inventoryChain == inventory) { + return true; + } + inventoryChain = inventoryChain.inventory; + } + return false; +} + +/** + * 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: + * 1. Vanilla game rules are such that player can only have two weapons + * in the inventory if they have different roots; + * 2. Root is easy to find. + * + * @param weaponClass Weapon class for which we must find root class. + * @return Root class for the provided `weaponClass` class. + * If `weaponClass` is `none`, method will also return `none`. +*/ +public final function class GetRootPickupClass( + class weaponClass) +{ + local int i; + local class root; + if (weaponClass == none) return none; + // Start with a pickup of the given weapons + root = class(weaponClass.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 != none && root.default.variantClasses.length > 0) { + root = class(root.default.variantClasses[0]); + } + return root; +} + +/** + * Convenience method for finding a first inventory entry with the same root as + * class `inventoryClass` in the given inventory chain `inventoryChain`. + * For information of what a "root" is, see `GetRootPickupClass()`. + * + * Inventory is stored as a linked list, where next inventory item is available + * through the `inventory` reference. This method follows this list, starting + * from `inventoryChain` until it finds `Inventory` of the appropriate class + * or reaches the end of the list. + * + * @param inventoryClass Class of the inventory we are interested in. + * @param inventoryChain Inventory chain in which we should search for + * the given class. + * @return First inventory from `inventoryChain` that has the same root as + * given `inventoryClass` class. + */ +public function KFWeapon GetByRoot( + class inventoryClass, + Inventory inventoryChain) +{ + local class nextWeaponClass; + local class itemRoot, nextRoot; + itemRoot = GetRootPickupClass(inventoryClass); + if (itemRoot == none) { + return none; + } + while (inventoryChain != none) + { + nextWeaponClass = class(inventoryChain.class); + nextRoot = GetRootPickupClass(nextWeaponClass); + if (itemRoot == nextRoot) { + return KFWeapon(inventoryChain); + } + inventoryChain = inventoryChain.inventory; + } + return none; +} + +/** + * Convenience method for finding all inventory entries with the same root as + * class `inventoryClass` in the given inventory chain `inventoryChain`. + * For information of what a "root" is, see `GetRootPickupClass()`. + * + * Inventory is stored as a linked list, where next inventory item is available + * through the `inventory` reference. This method follows this list, starting + * from `inventoryChain` until the end of the list. + * + * @param inventoryClass Class of the inventory we are interested in. + * @param inventoryChain Inventory chain in which we should search for + * the given class. + * @return Array of inventory items from `inventoryChain` that have the same + * root as given `inventoryClass` class. + */ +public function array GetAllByRoot( + class inventoryClass, + Inventory inventoryChain) +{ + local array result; + local KFWeapon nextWeapon; + local class nextWeaponClass; + local class itemRoot, nextRoot; + itemRoot = GetRootPickupClass(inventoryClass); + if (itemRoot == none) { + return result; + } + while (inventoryChain != none) + { + nextWeaponClass = class(inventoryChain.class); + nextRoot = GetRootPickupClass(nextWeaponClass); + if (itemRoot == nextRoot) { + nextWeapon = KFWeapon(inventoryChain); + } + if (nextWeapon != none) + { + result[result.length] = nextWeapon; + nextWeapon = none; + } + inventoryChain = inventoryChain.inventory; + } + return result; +} + +/** + * Returns ammunition class for the given `weapon` weapon, that it uses for + * fire mode numbered `modeNumber`. + * + * @param weapon Weapon for which ammunition class should be found. + * @param modeNumber Fire mode for which ammunition class should be found. + * @return Class of ammunition used for `weapon`'s fire mode, + * numbered `modeNumber`. + * `none` if `weapon` is `none`, fire mode does not exist or + * it is not associated with inventory ammo class. + */ +public function class GetAmmoClass(Weapon weapon, int modeNumber) +{ + local WeaponFire relevantWeaponFire; + if (weapon == none) { + return none; + } + // Just use majestic rjp's hack method `GetFireMode()` to get ammo class + // through a weapon fire + relevantWeaponFire = weapon.GetFireMode(modeNumber); + if (relevantWeaponFire != none) { + return relevantWeaponFire.ammoClass; + } + return none; +} + +/** + * Removes all ammo from the given `weapon`. Assumes weapons has no more than + * two fire modes. + * + * In case given weapon is a child class of `KFWeapon`, also clears its + * magazine counter. + * + * @param weapon Weapon to remove all ammo from. If `none`, + * method does nothing. + */ +public final function ClearAmmo(Weapon weapon) +{ + local InventoryService service; + service = InventoryService(class'InventoryService'.static.Require()); + if (service != none) { + service.ClearAmmo(weapon); + } +} + +/** + * Creates and adds a weapons of the given class to the `pawn` with specified + * amount of ammunition. + * + * @param pawn `Pawn` to which we should add new weapon. + * If `none` - method does nothing. + * @param weaponClassToAdd Class of the weapon we need to add. + * If `none` - method does nothing. + * @param totalAmmoPrimary Ammo to add to the primary fire. + * @param totalAmmoSecondary Ammo to add to the secondary fire. + * @param magazineAmmo Ammo to add to the new weapon's magazine count. + * Only relevant if `weaponClassToAdd` is a child class of `KFWeapon` and + * otherwise ignored. + * @param clearStarterAmmo Newly created weapons usually come with some + * default amount of ammo. Setting this flag to `true` will remove it + * before adding `totalAmmoPrimary`, `totalAmmoSecondary` and + * `magazineAmmo`. + * @return Instance of the newly created weapon. `none` in case of failure or + * if created weapon was destroyed in the process of adding it to + * the `pawn` (can happen as a result of interaction with preexisting + * weapons - e.g. pistol can merge with another one of the same type and + * produce a new weapon). + */ +public function Weapon AddWeaponWithAmmo( + Pawn pawn, + class weaponClassToAdd, + optional int totalAmmoPrimary, + optional int totalAmmoSecondary, + optional int magazineAmmo, + optional bool clearStarterAmmo) +{ + local InventoryService service; + service = InventoryService(class'InventoryService'.static.Require()); + if (service == none) { + return none; + } + return service.AddWeaponWithAmmo( pawn, weaponClassToAdd, + totalAmmoPrimary, totalAmmoSecondary, + magazineAmmo, clearStarterAmmo); +} + +/** + * Auxiliary method for "merging" weapons. Basically acts as + * a `AddWeaponWithAmmo()` - creates and adds a weapons of the given class to + * the `pawn`. But instead of taking numeric parameters to specify starter + * ammunition, copies ammunition counts (adding them together) from two weapons + * (`weaponToMerge1` and `weaponToMerge2`) specified for "merging" + * + * @param pawn `Pawn` to which we should add new merged weapon. + * If `none` - method does nothing. + * @param mergedClass Class of the weapon we need to add as a result + * of "merging". If `none` - method does nothing. + * @param weaponToMerge1 First weapon from which to copy ammunition counts. + * In case it is of a child class of `KFWeapon`, also copies magazine size. + * If `none` - assumes all ammo counts to be zero. + * @param weaponToMerge2 Second weapon from which to copy ammunition counts. + * Completely interchangeable with `weaponToMerge1`. + * @param clearStarterAmmo Newly created weapons usually come with some + * default amount of ammo. Setting this flag to `true` will remove it + * before adding ammunition counts from `weaponToMerge1` and + * `weaponToMerge2`. + * @return Instance of the newly created weapon. `none` in case of failure or + * if created weapon was destroyed in the process of adding it to + * the `pawn` (can happen as a result of interaction with preexisting + * weapons - e.g. pistol can merge with another one of the same type and + * produce a new weapon). + */ +public function Weapon MergeWeapons( + Pawn ownerPawn, + class mergedClass, + optional Weapon weaponToMerge1, + optional Weapon weaponToMerge2, + optional bool clearStarterAmmo) +{ + local InventoryService service; + service = InventoryService(class'InventoryService'.static.Require()); + if (service == none) { + return none; + } + return service.MergeWeapons(ownerPawn, mergedClass, + weaponToMerge1, weaponToMerge2); +} + +defaultproperties +{ + dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup') + dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup') + dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup') + dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup') + dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup') + dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup') +} \ No newline at end of file diff --git a/sources/Unreal/InventoryAPI/InventoryService.uc b/sources/Unreal/InventoryAPI/InventoryService.uc new file mode 100644 index 0000000..9aac329 --- /dev/null +++ b/sources/Unreal/InventoryAPI/InventoryService.uc @@ -0,0 +1,128 @@ +/** + * Service that simply does some of the work of `InventoryService`, since + * working with `Actor`s that can get destroyed in the process is much safer + * inside another `Actor`. + * For description of all methods see `InventoryAPI`. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class InventoryService extends Service; + +public function Weapon AddWeaponWithAmmo( + Pawn pawn, + class weaponClassToAdd, + optional int totalAmmoPrimary, + optional int totalAmmoSecondary, + optional int magazineAmmo, + optional bool clearStarterAmmo) +{ + local Weapon newWeapon; + local KFWeapon newKFWeapon; + if (pawn == none) return none; + newWeapon = Weapon(_.memory.Allocate(weaponClassToAdd)); + if (newWeapon == none) return none; + // It is possible that `newWeapon` can get destroyed somewhere here, + // so add two more checks + _.unreal.GetKFGameType().WeaponSpawned(newWeapon); + if (newWeapon == none) return none; + newWeapon.GiveTo(pawn); + if (newWeapon == none) return none; + + // Update ammo & magazine (if applicable) + if (clearStarterAmmo) { + ClearAmmo(newWeapon); + } + newKFWeapon = KFWeapon(newWeapon); + if (newKFWeapon != none) + { + if (clearStarterAmmo) { + newKFWeapon.magAmmoRemaining = 0; + } + newKFWeapon.magAmmoRemaining += magazineAmmo; + } + if (totalAmmoPrimary > 0) { + newWeapon.AddAmmo(totalAmmoPrimary, 0); + } + if (totalAmmoSecondary > 0) { + newWeapon.AddAmmo(totalAmmoSecondary, 1); + } + return newWeapon; +} + +public function Weapon MergeWeapons( + Pawn pawn, + class mergedClass, + optional Weapon weaponToMerge1, + optional Weapon weaponToMerge2, + optional bool clearStarterAmmo) +{ + local int totalAmmoPrimary, totalAmmoSecondary, magazineAmmo; + local KFWeapon kfWeapon; + if (pawn == none) { + return none; + } + if (weaponToMerge1 != none) + { + kfWeapon = KFWeapon(weaponToMerge1); + if (kfWeapon != none) { + magazineAmmo += kfWeapon.magAmmoRemaining; + } + totalAmmoPrimary += weaponToMerge1.AmmoAmount(0); + totalAmmoSecondary += weaponToMerge1.AmmoAmount(1); + weaponToMerge1.Destroyed(); + if (weaponToMerge1 != none) { + weaponToMerge1.Destroy(); + } + } + if (weaponToMerge2 != none) + { + kfWeapon = KFWeapon(weaponToMerge2); + if (kfWeapon != none) { + magazineAmmo += kfWeapon.magAmmoRemaining; + } + totalAmmoPrimary += weaponToMerge2.AmmoAmount(0); + totalAmmoSecondary += weaponToMerge2.AmmoAmount(1); + weaponToMerge2.Destroyed(); + if (weaponToMerge2 != none) { + weaponToMerge2.Destroy(); + } + } + return AddWeaponWithAmmo( pawn, mergedClass, totalAmmoPrimary, + totalAmmoSecondary, magazineAmmo, + clearStarterAmmo); +} + +public final function ClearAmmo(Weapon weapon) +{ + local float auxiliary, currentAmmoPrimary, currentAmmoSecondary; + local KFWeapon kfWeapon; + if (weapon == none) { + return; + } + weapon.GetAmmoCount(auxiliary, currentAmmoPrimary); + //weapon.GetSecondaryAmmoCount(auxiliary, currentAmmoSecondary); + weapon.AddAmmo(-currentAmmoPrimary, 0); + weapon.AddAmmo(-currentAmmoSecondary, 1); + kfWeapon = KFWeapon(weapon); + if (kfWeapon != none) { + kfWeapon.magAmmoRemaining = 0; + } +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Unreal/Tests/TEST_UnrealAPI.uc b/sources/Unreal/Tests/TEST_UnrealAPI.uc index d4db865..148b812 100644 --- a/sources/Unreal/Tests/TEST_UnrealAPI.uc +++ b/sources/Unreal/Tests/TEST_UnrealAPI.uc @@ -156,43 +156,43 @@ protected static function SubTest_InventoryChainFetchingSingle(Inventory chain) { Issue("Does not find correct first entry inside the inventory chain."); TEST_ExpectTrue( - __().unreal.GetInventoryFrom(class'MockInventoryA', chain) + __().unreal.inventory.Get(class'MockInventoryA', chain) == chain.inventory.inventory); TEST_ExpectTrue( - __().unreal.GetInventoryFrom(class'MockInventoryB', chain) + __().unreal.inventory.Get(class'MockInventoryB', chain) == chain.inventory); TEST_ExpectTrue( - __().unreal.GetInventoryFrom(class'MockInventoryAChild', chain) + __().unreal.inventory.Get(class'MockInventoryAChild', chain) == chain); Issue("Incorrectly finds missing inventory entries."); - TEST_ExpectNone(__().unreal.GetInventoryFrom(none, chain)); - TEST_ExpectNone(__().unreal.GetInventoryFrom(class'Winchester', chain)); + TEST_ExpectNone(__().unreal.inventory.Get(none, chain)); + TEST_ExpectNone(__().unreal.inventory.Get(class'Winchester', chain)); Issue("Does not find correct first entry inside the inventory chain when" @ "allowing for child classes."); TEST_ExpectTrue( - __().unreal.GetInventoryFrom(class'MockInventoryA', chain, true) + __().unreal.inventory.Get(class'MockInventoryA', chain, true) == chain); TEST_ExpectTrue( - __().unreal.GetInventoryFrom(class'MockInventoryB', chain, true) + __().unreal.inventory.Get(class'MockInventoryB', chain, true) == chain.inventory); TEST_ExpectTrue( - __().unreal.GetInventoryFrom(class'MockInventoryAChild', chain, true) + __().unreal.inventory.Get(class'MockInventoryAChild', chain, true) == chain); Issue("Incorrectly finds missing inventory entries when allowing for" @ "child classes."); - TEST_ExpectNone(__().unreal.GetInventoryFrom(none, chain, true)); - TEST_ExpectNone(__().unreal.GetInventoryFrom( class'Winchester', chain, - true)); + TEST_ExpectNone(__().unreal.inventory.Get(none, chain, true)); + TEST_ExpectNone(__().unreal.inventory.Get( class'Winchester', chain, + true)); } protected static function SubTest_InventoryChainFetchingMany(Inventory chain) { local array result; Issue("Does not find correct entries inside the inventory chain."); - result = __().unreal.GetAllInventoryFrom(class'MockInventoryB', chain); + result = __().unreal.inventory.GetAll(class'MockInventoryB', chain); TEST_ExpectTrue(result.length == 2); TEST_ExpectTrue(result[0] == chain.inventory); TEST_ExpectTrue(result[1] == chain.inventory.inventory.inventory.inventory); @@ -200,12 +200,12 @@ protected static function SubTest_InventoryChainFetchingMany(Inventory chain) Issue("Does not find correct entries inside the inventory chain when" @ "allowing for child classes."); result = - __().unreal.GetAllInventoryFrom(class'MockInventoryB', chain, true); + __().unreal.inventory.GetAll(class'MockInventoryB', chain, true); TEST_ExpectTrue(result.length == 2); TEST_ExpectTrue(result[0] == chain.inventory); TEST_ExpectTrue(result[1] == chain.inventory.inventory.inventory.inventory); result = - __().unreal.GetAllInventoryFrom(class'MockInventoryA', chain, true); + __().unreal.inventory.GetAll(class'MockInventoryA', chain, true); TEST_ExpectTrue(result.length == 5); TEST_ExpectTrue(result[0] == chain); TEST_ExpectTrue(result[1] == chain.inventory.inventory); @@ -218,9 +218,9 @@ protected static function SubTest_InventoryChainFetchingMany(Inventory chain) == chain.inventory.inventory.inventory.inventory.inventory.inventory); Issue("Does not return empty array for non-existing inventory class."); - result = __().unreal.GetAllInventoryFrom(class'Winchester', chain); + result = __().unreal.inventory.GetAll(class'Winchester', chain); TEST_ExpectTrue(result.length == 0); - result = __().unreal.GetAllInventoryFrom(class'Winchester', chain, true); + result = __().unreal.inventory.GetAll(class'Winchester', chain, true); TEST_ExpectTrue(result.length == 0); } diff --git a/sources/Unreal/UnrealAPI.uc b/sources/Unreal/UnrealAPI.uc index cb93885..e58e259 100644 --- a/sources/Unreal/UnrealAPI.uc +++ b/sources/Unreal/UnrealAPI.uc @@ -1,7 +1,7 @@ /** * Low-level API that provides set of utility methods for working with * unreal script classes. - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -23,6 +23,7 @@ class UnrealAPI extends AcediaObject; var public MutatorAPI mutator; var public GameRulesAPI gameRules; var public BroadcastAPI broadcasts; +var public InventoryAPI inventory; var private LoggerAPI.Definition fatalNoStalker; @@ -31,6 +32,7 @@ protected function Constructor() mutator = MutatorAPI(_.memory.Allocate(class'MutatorAPI')); gameRules = GameRulesAPI(_.memory.Allocate(class'GameRulesAPI')); broadcasts = BroadcastAPI(_.memory.Allocate(class'BroadcastAPI')); + inventory = InventoryAPI(_.memory.Allocate(class'InventoryAPI')); } public function DropAPI() @@ -38,6 +40,7 @@ public function DropAPI() mutator = none; gameRules = none; broadcasts = none; + inventory = none; } /** @@ -199,93 +202,6 @@ public final function PlayerController GetLocalPlayer() .GetLocalPlayerController(); } -/** - * Convenience method for finding a first inventory entry of the given - * class `inventoryClass` in the given inventory chain `inventoryChain`. - * - * Inventory is stored as a linked list, where next inventory item is available - * through the `inventory` reference. This method follows this list, starting - * from `inventoryChain` until it finds `Inventory` of the appropriate class - * or reaches the end of the list. - * - * @param inventoryClass Class of the inventory we are interested in. - * @param inventoryChain Inventory chain in which we should search for - * the given class. - * @param acceptChildClass `true` if method should also return any - * `Inventory` of class derived from `inventoryClass` and `false` if - * we want given class specifically (default). - * @return First inventory from `inventoryChain` that matches given - * `inventoryClass` class (whether exactly or as a child class, - * in case `acceptChildClass == true`). - */ -public final function Inventory GetInventoryFrom( - class inventoryClass, - Inventory inventoryChain, - optional bool acceptChildClass) -{ - if (inventoryClass == none) { - return none; - } - while (inventoryChain != none) - { - if (inventoryChain.class == inventoryClass) { - return inventoryChain; - } - if ( acceptChildClass - && ClassIsChildOf(inventoryChain.class, inventoryClass)) - { - return inventoryChain; - } - inventoryChain = inventoryChain.inventory; - } - return none; -} - -/** - * Convenience method for finding a all inventory entries of the given - * class `inventoryClass` in the given inventory chain `inventoryChain`. - * - * Inventory is stored as a linked list, where next inventory item is available - * through the `inventory` reference. This method follows this list, starting - * from `inventoryChain` until the end of the list. - * - * @param inventoryClass Class of the inventory we are interested in. - * @param inventoryChain Inventory chain in which we should search for - * the given class. - * @param acceptChildClass `true` if method should also return any - * `Inventory` of class derived from `inventoryClass` and `false` if - * we want given class specifically (default). - * @return Array of inventory items from `inventoryChain` that match given - * `inventoryClass` class (whether exactly or as a child class, - * in case `acceptChildClass == true`). - */ -public final function array GetAllInventoryFrom( - class inventoryClass, - Inventory inventoryChain, - optional bool acceptChildClass) -{ - local bool shouldAdd; - local array result; - if (inventoryClass == none) { - return result; - } - while (inventoryChain != none) - { - shouldAdd = false; - if (inventoryChain.class == inventoryClass) { - shouldAdd = true; - } - else if (acceptChildClass) { - shouldAdd = ClassIsChildOf(inventoryChain.class, inventoryClass); - } - if (shouldAdd) { - result[result.length] = inventoryChain; - } - inventoryChain = inventoryChain.inventory; - } - return result; -} - /** * Creates reference object to store a `Actor` value. *