From 6399963e687448a7e3aaa52a4e49475b94950a60 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sun, 21 Apr 2019 08:22:14 -0400 Subject: [PATCH] Exclude Equipment from Loadouts (#249) * routines for eliminating equipment and configurations from loadout availability depending on the terminal type * fix to FavoritesMessage use of exo-suit type and subtype causing entries to identify incorrectly; standard is the fallback exo-suit type should a player try to load a suit type they no longer have certed * factored subtype value into exo-suit fallback selection * when in a vehicle, and accessing a terminal, the item purchased will attempt to be placed in the vehicle's trunk before testing the player's free hand; the item will not go into the player's backpack --- .../psforever/objects/GlobalDefinitions.scala | 2 + .../definition/ExoSuitDefinition.scala | 1 + .../objects/loadouts/InfantryLoadout.scala | 17 ++- .../terminals/OrderTerminalDefinition.scala | 58 +++++++-- .../src/main/scala/WorldSessionActor.scala | 117 +++++++++++++++--- 5 files changed, 164 insertions(+), 31 deletions(-) diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index c4f9973bd..d52d183bc 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -5803,6 +5803,7 @@ object GlobalDefinitions { order_terminala.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons) order_terminala.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) order_terminala.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() + order_terminala.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX order_terminala.SellEquipmentByDefault = true order_terminalb.Name = "order_terminalb" @@ -5811,6 +5812,7 @@ object GlobalDefinitions { order_terminalb.Tab += 2 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.supportAmmunition ++ EquipmentTerminalDefinition.supportWeapons) order_terminalb.Tab += 3 -> OrderTerminalDefinition.EquipmentPage(EquipmentTerminalDefinition.vehicleAmmunition) order_terminalb.Tab += 4 -> OrderTerminalDefinition.InfantryLoadoutPage() + order_terminalb.Tab(4).asInstanceOf[OrderTerminalDefinition.InfantryLoadoutPage].Exclude = ExoSuitType.MAX order_terminalb.SellEquipmentByDefault = true cert_terminal.Name = "cert_terminal" diff --git a/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala index 7cc43ad4c..f2d68a2e0 100644 --- a/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/ExoSuitDefinition.scala @@ -94,6 +94,7 @@ class SpecialExoSuitDefinition(private val suitType : ExoSuitType.Value) extends override def Use : ExoSuitDefinition = { val obj = new SpecialExoSuitDefinition(SuitType) + obj.Permissions = Permissions obj.MaxArmor = MaxArmor obj.InventoryScale = InventoryScale obj.InventoryOffset = InventoryOffset diff --git a/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala index 5277f4bab..0f9986e29 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.loadouts -import net.psforever.types.ExoSuitType +import net.psforever.types.{CertificationType, ExoSuitType} /** * A blueprint of a player's uniform, their holster items, and their inventory items, saved in a specific state. @@ -99,4 +99,19 @@ object InfantryLoadout { case ExoSuitType.Infiltration => 7 } } + + /** + * Assuming the exo-suit is a mechanized assault type, + * use the subtype to determine what certifications would be valid for permitted access to that specific exo-suit. + * The "C" does not stand for "certification." + * @see `CertificationType` + * @param subtype the numeric subtype + * @return a `Set` of all certifications that would grant access to the mechanized assault exo-suit subtype + */ + def DetermineSubtypeC(subtype : Int) : Set[CertificationType.Value] = subtype match { + case 1 => Set(CertificationType.AIMAX, CertificationType.UniMAX) + case 2 => Set(CertificationType.AVMAX, CertificationType.UniMAX) + case 3 => Set(CertificationType.AAMAX, CertificationType.UniMAX) + case _ => Set.empty[CertificationType.Value] + } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala index a53cb97ec..4784d8de3 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala @@ -136,7 +136,7 @@ object OrderTerminalDefinition { Terminal.BuyExosuit(suit, subtype) case _ => items.get(msg.item_name) match { - case Some(item : (()=>Equipment)) => + case Some(item) => Terminal.BuyEquipment(item()) case _ => Terminal.NoDeal() @@ -180,7 +180,7 @@ object OrderTerminalDefinition { final case class EquipmentPage(stock : Map[String, ()=>Equipment]) extends Tab { override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { stock.get(msg.item_name) match { - case Some(item : (()=>Equipment)) => + case Some(item) => Terminal.BuyEquipment(item()) case _ => Terminal.NoDeal() @@ -216,23 +216,56 @@ object OrderTerminalDefinition { } } + /** + * The base class for "loadout" type tabs. + * Defines logic for enumerating items and entities that should be eliminated from being loaded. + * The method for filtering those excluded items, if applicable, + * and management of the resulting loadout object + * is the responsibility of the specific tab that is instantiated. + */ + abstract class LoadoutTab extends Tab { + private var contraband : Seq[Any] = Nil + + def Exclude : Seq[Any] = contraband + + def Exclude_=(equipment : Any) : Seq[Any] = { + contraband = Seq(equipment) + Exclude + } + + def Exclude_=(equipmentList : Seq[Any]) : Seq[Any] = { + contraband = equipmentList + Exclude + } + } + /** * The tab used to select which custom loadout the player is using. * Player loadouts are defined by an exo-suit to be worn by the player * and equipment in the holsters and the inventory. * In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts; * no extra information specific to this page is necessary. + * If an exo-suit type is considered excluded, the whole loadout is blocked. + * If the exclusion is written as a `Tuple` object `(A, B)`, + * `A` will be expected as an exo-suit type, and `B` will be expected as its subtype, + * and the pair must both match to block the whole loadout. + * If any of the player's inventory is considered excluded, only those items will be filtered. * @see `ExoSuitType` * @see `Equipment` * @see `InfantryLoadout` * @see `Loadout` */ - final case class InfantryLoadoutPage() extends Tab { + //TODO block equipment by blocking ammunition type + final case class InfantryLoadoutPage() extends LoadoutTab { override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { player.LoadLoadout(msg.unk1) match { - case Some(loadout : InfantryLoadout) => - val holsters = loadout.visible_slots.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) - val inventory = loadout.inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + case Some(loadout : InfantryLoadout) if !Exclude.contains(loadout.exosuit) && !Exclude.contains((loadout.exosuit, loadout.subtype)) => + val holsters = loadout.visible_slots + .map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + .filterNot { entry => Exclude.contains(entry.obj.Definition) } + val inventory = loadout.inventory + .map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + .filterNot { entry => Exclude.contains(entry.obj.Definition) } Terminal.InfantryLoadout(loadout.exosuit, loadout.subtype, holsters, inventory) case _ => Terminal.NoDeal() @@ -246,16 +279,21 @@ object OrderTerminalDefinition { * and equipment in the trunk. * In this case, the reference to the player that is a parameter of the functions maintains information about the loadouts; * no extra information specific to this page is necessary. + * If a vehicle type (by definition) is considered excluded, the whole loadout is blocked. + * If any of the vehicle's inventory is considered excluded, only those items will be filtered. * @see `Equipment` * @see `Loadout` * @see `VehicleLoadout` */ - final case class VehicleLoadoutPage() extends Tab { + final case class VehicleLoadoutPage() extends LoadoutTab { override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { player.LoadLoadout(msg.unk1 + 10) match { - case Some(loadout : VehicleLoadout) => - val weapons = loadout.visible_slots.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) - val inventory = loadout.inventory.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + case Some(loadout : VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) => + val weapons = loadout.visible_slots + .map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + val inventory = loadout.inventory + .map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) }) + .filterNot { entry => Exclude.contains(entry.obj.Definition) } Terminal.VehicleLoadout(loadout.vehicle_definition, weapons, inventory) case _ => Terminal.NoDeal() diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 7e7f3a683..61b7f6b01 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1535,13 +1535,34 @@ class WorldSessionActor extends Actor with MDCContextAware { lastTerminalOrderFulfillment = true case Terminal.BuyEquipment(item) => - tplayer.Fit(item) match { - case Some(index) => - item.Faction = tplayer.Faction - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) - taskResolver ! PutEquipmentInSlot(tplayer, item, index) - case None => - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) + continent.GUID(tplayer.VehicleSeated) match { + //vehicle trunk + case Some(vehicle : Vehicle) => + vehicle.Fit(item) match { + case Some(index) => + item.Faction = tplayer.Faction + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + taskResolver ! StowNewEquipmentInVehicle(vehicle)(index, item) + case None => //player free hand? + tplayer.FreeHand.Equipment match { + case None => + item.Faction = tplayer.Faction + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + taskResolver ! PutEquipmentInSlot(tplayer, item, Player.FreeHandSlot) + case Some(_) => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) + } + } + //player backpack or free hand + case _ => + tplayer.Fit(item) match { + case Some(index) => + item.Faction = tplayer.Faction + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) + taskResolver ! PutEquipmentInSlot(tplayer, item, index) + case None => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false)) + } } lastTerminalOrderFulfillment = true @@ -1559,24 +1580,42 @@ class WorldSessionActor extends Actor with MDCContextAware { case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}") - //TODO check exo-suit permissions sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true)) //prepare lists of valid objects val beforeFreeHand = tplayer.FreeHand.Equipment val dropPred = DropPredicate(tplayer) val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred) val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred) - val (_, afterHolsters) = holsters.partition(dropPred) //dropped items are forgotten - val (_, afterInventory) = inventory.partition(dropPred) //dropped items are forgotten - //change suit (clear inventory and change holster sizes; holsters must be empty before this point) tplayer.FreeHand.Equipment = None //terminal and inventory will close, so prematurely dropping should be fine + //sanitize exo-suit for change val originalSuit = player.ExoSuit val originalSubtype = Loadout.DetermineSubtype(tplayer) + val fallbackSuit = ExoSuitType.Standard + val fallbackSubtype = 0 + //a loadout with a prohibited exo-suit type will result in a fallback exo-suit type + val (nextSuit : ExoSuitType.Value, nextSubtype : Int) = + if(ExoSuitDefinition.Select(exosuit).Permissions match { + case Nil => + true + case permissions if subtype != 0 => + val certs = tplayer.Certifications + certs.intersect(permissions.toSet).nonEmpty && + certs.intersect(InfantryLoadout.DetermineSubtypeC(subtype)).nonEmpty + case permissions => + tplayer.Certifications.intersect(permissions.toSet).nonEmpty + }) { + (exosuit, subtype) + } + else { + log.warn(s"$tplayer no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead") + (fallbackSuit, fallbackSubtype) + } + //update suit interally (holsters must be empty before this point) val originalArmor = player.Armor - tplayer.ExoSuit = exosuit + tplayer.ExoSuit = nextSuit val toMaxArmor = tplayer.MaxArmor - if(originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) { - tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit)) + if(originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) { + tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), nextSuit)) tplayer.Armor = toMaxArmor sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, toMaxArmor)) avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, toMaxArmor)) @@ -1590,6 +1629,44 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true)) avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, tplayer.LastDrawnSlot)) } + //a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped + //dropped items can be forgotten safely + val (afterHolsters, afterInventory) = if(nextSuit == exosuit) { + ( + holsters.filterNot(dropPred), + inventory.filterNot(dropPred) + ) + } + else { + val newSuitDef = ExoSuitDefinition.Select(nextSuit) + val (afterInventory, extra) = GridInventory.recoverInventory( + inventory.filterNot(dropPred), + tplayer.Inventory + ) + val afterHolsters = { + val preservedHolsters = if(exosuit == ExoSuitType.MAX) { + holsters.filter(_.start == 4) //melee slot perservation + } + else { + holsters + .filterNot(dropPred) + .collect { + case item @ InventoryItem(obj, index) if newSuitDef.Holster(index) == obj.Size => item + } + } + val size = newSuitDef.Holsters.size + val indexMap = preservedHolsters.map { entry => entry.start } + preservedHolsters ++ (extra.map { obj => + tplayer.Fit(obj) match { + case Some(index : Int) if index < size && !indexMap.contains(index) => + InventoryItem(obj, index) + case _ => + InventoryItem(obj, -1) + } + }).filterNot(entry => entry.start == -1) + } + (afterHolsters, afterInventory) + } //delete everything (not dropped) beforeHolsters.foreach({ elem => avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID)) @@ -1599,9 +1676,9 @@ class WorldSessionActor extends Actor with MDCContextAware { taskResolver ! GUIDTask.UnregisterEquipment(elem.obj)(continent.GUID) }) //report change - sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype)) - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype)) - if(exosuit == ExoSuitType.MAX) { + sendResponse(ArmorChangedMessage(tplayer.GUID, nextSuit, nextSubtype)) + avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, nextSuit, nextSubtype)) + if(nextSuit == ExoSuitType.MAX) { val (maxWeapons, otherWeapons) = afterHolsters.partition(entry => { entry.obj.Size == EquipmentSize.Max }) taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, maxWeapons.head.obj, 0))) otherWeapons @@ -2622,7 +2699,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val (inf, veh) = avatar.Loadouts.partition { case (index, _) => index < 10 } inf.foreach { case (index, loadout : InfantryLoadout) => - sendResponse(FavoritesMessage(LoadoutType.Infantry, guid, index, loadout.label, loadout.exosuit.id + loadout.subtype)) + sendResponse(FavoritesMessage(LoadoutType.Infantry, guid, index, loadout.label, InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype))) } veh.foreach { case (index, loadout : VehicleLoadout) => @@ -5622,7 +5699,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @see `ChangeAmmoMessage` * @param obj the `Container` object * @param index an index in `obj`'s inventory - * @param item an `AmmoBox` + * @param item the `Equipment` item * @return a `TaskResolver.GiveTask` chain that executes the action */ def StowNewEquipment(obj : PlanetSideGameObject with Container)(index : Int, item : Equipment) : TaskResolver.GiveTask = { @@ -5637,7 +5714,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @see `ChangeAmmoMessage` * @param obj the `Container` object * @param index an index in `obj`'s inventory - * @param item an `AmmoBox` + * @param item the `Equipment` item * @return a `TaskResolver.GiveTask` chain that executes the action */ def StowNewEquipmentInVehicle(obj : Vehicle)(index : Int, item : Equipment) : TaskResolver.GiveTask = {