extensivwe changes to the way OwnableByPlayer manages owner user information due to uniqueness, that changes a lot of vehicle and deployable code; fleshing out experience calculation procedure for future testing

This commit is contained in:
Fate-JH 2023-06-15 09:31:51 -04:00
parent 45d2ea544c
commit da6ea21147
38 changed files with 1089 additions and 1013 deletions

View file

@ -10,6 +10,7 @@ import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.InGameHistory
import net.psforever.objects.vehicles.MountedWeapons
import org.joda.time.{LocalDateTime, Seconds}
import scala.collection.mutable
@ -2949,14 +2950,16 @@ class AvatarActor(
kdaStat match {
case kill: Kill =>
val playerSource = PlayerSource(player)
(kill.info.interaction.cause match {
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
val historyTranscript = (kill.info.interaction.cause match {
case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
case _ => None
}).collect {
case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory) =>
player.ContributionFrom(mount)
}) match {
case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons) =>
player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount)
case None =>
player.HistoryAndContributions()
}
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.HistoryAndContributions(), kill, exp)
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, kill, exp)
case _: Assist =>
()
case _: Death =>

View file

@ -3,7 +3,7 @@ package net.psforever.actors.session.support
import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
import scala.collection.mutable
@ -411,10 +411,10 @@ class SessionData(
/* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */
if (
(session.account.gm ||
(player.avatar.vehicle.contains(objectGuid) && vehicle.Owner.contains(player.GUID)) ||
(player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) ||
(player.Faction == vehicle.Faction &&
(vehicle.Definition.CanBeOwned.nonEmpty &&
(vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))) &&
(vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) &&
(vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied))
) {
vehicle.Actor ! Vehicle.Deconstruct()
@ -442,7 +442,7 @@ class SessionData(
}
case Some(obj: Deployable) =>
if (session.account.gm || obj.Owner.isEmpty || obj.Owner.contains(player.GUID) || obj.Destroyed) {
if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) {
obj.Actor ! Deployable.Deconstruct()
} else {
log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it")
@ -1377,7 +1377,7 @@ class SessionData(
//access to trunk
if (
obj.AccessingTrunk.isEmpty &&
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid
.contains(player.GUID))
) {
log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk")
@ -2473,6 +2473,7 @@ class SessionData(
continent.id,
LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
)
player.LogActivity(VehicleDismountActivity(VehicleSource(router), PlayerSource(player)))
} else {
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
}

View file

@ -2,6 +2,9 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.sourcing.AmenitySource
import net.psforever.objects.vital.TerminalUsedActivity
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
//
@ -68,8 +71,8 @@ class SessionTerminalHandlers(
order match {
case Terminal.BuyEquipment(item)
if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
lastTerminalOrderFulfillment = true
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
lastTerminalOrderFulfillment = true
case Terminal.BuyEquipment(item) =>
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
@ -141,6 +144,7 @@ class SessionTerminalHandlers(
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
}
player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
}.orElse {
log.error(
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"

View file

@ -258,7 +258,7 @@ class ZoningOperations(
obj.GUID,
Deployable.Icon(obj.Definition.Item),
obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0))
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
)
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
})
@ -2824,7 +2824,7 @@ class ZoningOperations(
continent.DeployableList
.filter(_.OwnerName.contains(name))
.foreach(obj => {
obj.Owner = guid
obj.OwnerGuid = guid
drawDeloyableIcon(obj)
})
drawDeloyableIcon = DontRedrawIcons
@ -2832,7 +2832,7 @@ class ZoningOperations(
//assert or transfer vehicle ownership
continent.GUID(player.avatar.vehicle) match {
case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
vehicle.Owner = guid
vehicle.OwnerGuid = guid
continent.VehicleEvents ! VehicleServiceMessage(
s"${tplayer.Faction}",
VehicleAction.Ownership(guid, vehicle.GUID)
@ -2907,7 +2907,7 @@ class ZoningOperations(
val effortBy = nextSpawnPoint
.collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) }
.collect {
case (_, Some(v: Vehicle)) => continent.GUID(v.Owner)
case (_, Some(v: Vehicle)) => continent.GUID(v.OwnerGuid)
case (sp, Some(_: Building)) => Some(sp)
}
.collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) }
@ -2962,7 +2962,7 @@ class ZoningOperations(
obj.GUID,
Deployable.Icon(obj.Definition.Item),
obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0))
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
)
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
}

View file

@ -72,7 +72,9 @@ class BoomerDeployableControl(mine: BoomerDeployable)
override def loseOwnership(faction: PlanetSideEmpire.Value): Unit = {
super.loseOwnership(PlanetSideEmpire.NEUTRAL)
mine.OwnerName = None
val guid = mine.OwnerGuid
mine.AssignOwnership(None)
mine.OwnerGuid = guid
}
override def gainOwnership(player: Player): Unit = {

View file

@ -89,8 +89,7 @@ object Deployables {
.foreach { p =>
p.Actor ! Player.LoseDeployable(target)
}
target.Owner = None
target.OwnerName = None
target.AssignOwnership(None)
}
events ! LocalServiceMessage(
s"${target.Faction}",
@ -119,7 +118,7 @@ object Deployables {
.collect {
case Some(obj: Deployable) =>
obj.Actor ! Deployable.Ownership(None)
obj.Owner = None //fast-forward the effect
obj.OwnerGuid = None //fast-forward the effect
obj
}
}

View file

@ -1,45 +1,33 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.types.PlanetSideGUID
trait OwnableByPlayer {
private var owner: Option[PlanetSideGUID] = None
private var ownerName: Option[String] = None
private var originalOwnerName: Option[String] = None
private var owner: Option[UniquePlayer] = None
private var ownerGuid: Option[PlanetSideGUID] = None
private var originalOwnerName: Option[String] = None
def Owner: Option[PlanetSideGUID] = owner
def Owners: Option[UniquePlayer] = owner
def Owner_=(owner: PlanetSideGUID): Option[PlanetSideGUID] = Owner_=(Some(owner))
def OwnerGuid: Option[PlanetSideGUID] = ownerGuid
def Owner_=(owner: Player): Option[PlanetSideGUID] = Owner_=(Some(owner.GUID))
def OwnerGuid_=(owner: PlanetSideGUID): Option[PlanetSideGUID] = OwnerGuid_=(Some(owner))
def Owner_=(owner: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
def OwnerGuid_=(owner: Player): Option[PlanetSideGUID] = OwnerGuid_=(Some(owner.GUID))
def OwnerGuid_=(owner: Option[PlanetSideGUID]): Option[PlanetSideGUID] = {
owner match {
case Some(_) =>
this.owner = owner
ownerGuid = owner
case None =>
this.owner = None
ownerGuid = None
}
Owner
OwnerGuid
}
def OwnerName: Option[String] = ownerName
def OwnerName_=(owner: String): Option[String] = OwnerName_=(Some(owner))
def OwnerName_=(owner: Player): Option[String] = OwnerName_=(Some(owner.Name))
def OwnerName_=(owner: Option[String]): Option[String] = {
owner match {
case Some(_) =>
ownerName = owner
originalOwnerName = originalOwnerName.orElse(owner)
case None =>
ownerName = None
}
OwnerName
}
def OwnerName: Option[String] = owner.map { _.name }
def OriginalOwnerName: Option[String] = originalOwnerName
@ -56,14 +44,30 @@ trait OwnableByPlayer {
* @return na
*/
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
playerOpt match {
case Some(player) =>
Owner = player
OwnerName = player
case None =>
Owner = None
OwnerName = None
(originalOwnerName, playerOpt) match {
case (None, Some(player)) =>
owner = Some(PlayerSource(player).unique)
originalOwnerName = originalOwnerName.orElse { Some(player.Name) }
OwnerGuid = player
case (_, Some(player)) =>
owner = Some(PlayerSource(player).unique)
OwnerGuid = player
case (_, None) =>
owner = None
OwnerGuid = None
}
this
}
/**
* na
* @param ownable na
* @return na
*/
def AssignOwnership(ownable: OwnableByPlayer): OwnableByPlayer = {
owner = ownable.owner
originalOwnerName = originalOwnerName.orElse { ownable.originalOwnerName }
OwnerGuid = ownable.OwnerGuid
this
}
}

View file

@ -557,16 +557,16 @@ class Player(var avatar: Avatar)
def DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel]
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
override def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = {
val start = ending - duration
override def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
val earliestEndTime = System.currentTimeMillis() - duration
History.collect {
case heal: HealFromEquipment if heal.time <= ending && heal.time > start => heal
case repair: RepairFromEquipment if repair.time <= ending && repair.time > start => repair
case heal: HealFromEquipment if heal.amount > 0 && heal.time > earliestEndTime => heal
case repair: RepairFromEquipment if repair.amount > 0 && repair.time > earliestEndTime => repair
}
}
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
override def equals(other: Any): Boolean =
other match {
case that: Player =>

View file

@ -112,11 +112,10 @@ object SpecialEmp {
faction: PlanetSideEmpire.Value
): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = {
distanceCheck(new PlanetSideServerObject with OwnableByPlayer {
Owner = Some(owner.GUID)
OwnerName = owner match {
case p: Player => p.Name
case o: OwnableByPlayer => o.OwnerName.getOrElse("")
case _ => ""
owner match {
case p: Player => AssignOwnership(p)
case o: OwnableByPlayer => AssignOwnership(o)
case _ => OwnerGuid_=(Some(owner.GUID))
}
Position = position
def Faction = faction

View file

@ -103,8 +103,7 @@ class TelepadDeployableControl(tpad: TelepadDeployable)
override def startOwnerlessDecay(): Unit = {
//telepads do not decay when they become ownerless
//telepad decay is tied to their lifecycle with routers
tpad.Owner = None
tpad.OwnerName = None
tpad.AssignOwnership(None)
}
override def finalizeDeployable(callback: ActorRef): Unit = {

View file

@ -476,7 +476,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
if (trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) {
groupPermissions(3) match {
case VehicleLockState.Locked => //only the owner
Owner.isEmpty || (Owner.isDefined && player.GUID == Owner.get)
OwnerGuid.isEmpty || (OwnerGuid.isDefined && player.GUID == OwnerGuid.get)
case VehicleLockState.Group => //anyone in the owner's squad or platoon
faction == player.Faction //TODO this is not correct
case VehicleLockState.Empire => //anyone of the owner's faction
@ -681,6 +681,6 @@ object Vehicle {
*/
def toString(obj: Vehicle): String = {
val occupancy = obj.Seats.values.count(seat => seat.isOccupied)
s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
s"${obj.Definition.Name}, owned by ${obj.OwnerGuid}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
}
}

View file

@ -61,7 +61,7 @@ object Vehicles {
* `None`, otherwise
*/
def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] =
vehicle.Zone.GUID(vehicle.Owner) match {
vehicle.Zone.GUID(vehicle.OwnerGuid) match {
case Some(player: Player) =>
if (player.avatar.vehicle.contains(guid)) {
player.avatar.vehicle = None
@ -127,7 +127,7 @@ object Vehicles {
*/
def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = {
val pguid = player.GUID
if (vehicle.Owner.contains(pguid)) {
if (vehicle.OwnerGuid.contains(pguid)) {
vehicle.AssignOwnership(None)
//vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
val vguid = vehicle.GUID
@ -272,7 +272,7 @@ object Vehicles {
}
case _ => ;
}
target.Owner match {
target.OwnerGuid match {
case Some(previousOwnerGuid: PlanetSideGUID) =>
// Remove ownership of the vehicle from the previous player
zone.GUID(previousOwnerGuid) match {

View file

@ -33,7 +33,7 @@ import net.psforever.objects.locker.LockerContainerControl
import net.psforever.objects.serverobject.environment._
import net.psforever.objects.serverobject.repair.Repairable
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource}
import net.psforever.objects.vital.collision.CollisionReason
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
@ -356,6 +356,12 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}
case Terminal.TerminalMessage(_, msg, order) =>
lazy val terminalUsedAction = {
player.Zone.GUID(msg.terminal_guid).collect {
case t: Terminal =>
player.LogActivity(TerminalUsedActivity(AmenitySource(t), msg.transaction_type))
}
}
order match {
case Terminal.BuyExosuit(exosuit, subtype) =>
val result = setExoSuit(exosuit, subtype)
@ -366,6 +372,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
)
terminalUsedAction
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}")
@ -508,7 +515,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result=true)
)
case _ => assert(assertion=false, msg.toString)
terminalUsedAction
case _ =>
assert(assertion=false, msg.toString)
}
case Zone.Ground.ItemOnGround(item, _, _) =>

View file

@ -66,16 +66,16 @@ trait DeployableBehavior {
finalizeDeployable(callback)
case Deployable.Ownership(None)
if DeployableObject.Owner.nonEmpty =>
if DeployableObject.OwnerGuid.nonEmpty =>
val obj = DeployableObject
if (constructed.contains(true)) {
loseOwnership(obj.Faction)
} else {
obj.Owner = None
obj.OwnerGuid = None
}
case Deployable.Ownership(Some(player))
if !DeployableObject.Destroyed && DeployableObject.Owner.isEmpty =>
if !DeployableObject.Destroyed && DeployableObject.OwnerGuid.isEmpty =>
if (constructed.contains(true)) {
gainOwnership(player)
} else {
@ -132,12 +132,12 @@ trait DeployableBehavior {
def startOwnerlessDecay(): Unit = {
val obj = DeployableObject
if (obj.Owner.nonEmpty && decay.isCancelled) {
if (obj.OwnerGuid.nonEmpty && decay.isCancelled) {
//without an owner, this deployable should begin to decay and will deconstruct later
import scala.concurrent.ExecutionContext.Implicits.global
decay = context.system.scheduler.scheduleOnce(Deployable.decay, self, Deployable.Deconstruct())
}
obj.Owner = None //OwnerName should remain set
obj.OwnerGuid = None //OwnerName should remain set
}
/**
@ -163,7 +163,7 @@ trait DeployableBehavior {
val guid = obj.GUID
val zone = obj.Zone
val originalFaction = obj.Faction
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, obj.Owner.get)
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, obj.OwnerGuid.get)
if (originalFaction != toFaction) {
obj.Faction = toFaction
val localEvents = zone.LocalEvents
@ -199,7 +199,7 @@ trait DeployableBehavior {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.TriggerEffectLocation(
obj.Owner.getOrElse(Service.defaultPlayerGUID),
obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID),
"spawn_object_effect",
obj.Position,
obj.Orientation
@ -234,7 +234,7 @@ trait DeployableBehavior {
val obj = DeployableObject
val zone = obj.Zone
val localEvents = zone.LocalEvents
val owner = obj.Owner.getOrElse(Service.defaultPlayerGUID)
val owner = obj.OwnerGuid.getOrElse(Service.defaultPlayerGUID)
obj.OwnerName match {
case Some(_) =>
case None =>
@ -306,7 +306,9 @@ trait DeployableBehavior {
if (!obj.Destroyed) {
Deployables.AnnounceDestroyDeployable(obj)
}
obj.OwnerName = None
val guid = obj.OwnerGuid
obj.AssignOwnership(None)
obj.OwnerGuid = guid
}
}

View file

@ -28,7 +28,7 @@ class BattleFrameFlightConverter extends ObjectCreateConverter[Vehicle]() {
jammered = false,
v4 = None,
v5 = None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -28,7 +28,7 @@ class BattleFrameRoboticsConverter extends ObjectCreateConverter[Vehicle]() {
jammered = obj.Jammed,
v4 = None,
v5 = None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -27,7 +27,7 @@ class DroppodConverter extends ObjectCreateConverter[Vehicle]() {
jammered = obj.Jammed,
v4 = Some(false),
v5 = None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -26,7 +26,7 @@ class FieldTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
jammered = obj.Jammed,
Some(false),
None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -24,7 +24,7 @@ class ShieldGeneratorConverter extends ObjectCreateConverter[ShieldGeneratorDepl
jammered = obj.Jammed,
None,
None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -25,7 +25,7 @@ class SmallDeployableConverter extends ObjectCreateConverter[Deployable]() {
},
Some(false),
None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -26,7 +26,7 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
jammered = obj.Jammed,
Some(true),
None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -24,7 +24,7 @@ class TRAPConverter extends ObjectCreateConverter[TrapDeployable]() {
false,
Some(true),
None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -28,7 +28,7 @@ class TelepadDeployableConverter extends ObjectCreateConverter[TelepadDeployable
false,
None,
Some(router.guid),
obj.Owner.getOrElse(PlanetSideGUID(0))
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
),
unk1 = 87,
unk2 = 12

View file

@ -27,7 +27,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
jammered = obj.Jammed,
v4 = Some(false),
v5 = None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

@ -1,15 +1,17 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.pad
import akka.actor.{Cancellable, Props}
import akka.actor.{ActorRef, Cancellable, OneForOneStrategy, Props}
import net.psforever.objects.avatar.SpecialCarry
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.pad.process.{VehicleSpawnControlBase, VehicleSpawnControlConcealPlayer}
import net.psforever.objects.sourcing.AmenitySource
import net.psforever.objects.vital.TerminalUsedActivity
import net.psforever.objects.zones.{Zone, ZoneAware, Zoning}
import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.Vector3
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
import scala.annotation.tailrec
import scala.concurrent.ExecutionContext.Implicits.global
@ -38,38 +40,39 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
with FactionAffinityBehavior.Check {
/** a reminder sent to future customers */
var periodicReminder: Cancellable = Default.Cancellable
private var periodicReminder: Cancellable = Default.Cancellable
/** repeatedly test whether queued orders are valid */
var queueManagement: Cancellable = Default.Cancellable
private var queueManagement: Cancellable = Default.Cancellable
/** a list of vehicle orders that have been submitted for this spawn pad */
var orders: List[VehicleSpawnPad.VehicleOrder] = List.empty[VehicleSpawnPad.VehicleOrder]
private var orders: List[VehicleSpawnPad.VehicleOrder] = List.empty[VehicleSpawnPad.VehicleOrder]
/** the current vehicle order being acted upon;
* used as a guard condition to control order processing rate
*/
var trackedOrder: Option[VehicleSpawnControl.Order] = None
private var trackedOrder: Option[VehicleSpawnControl.Order] = None
/** how to process either the first order or every subsequent order */
var handleOrderFunc: VehicleSpawnPad.VehicleOrder => Unit = NewTasking
private var handleOrderFunc: VehicleSpawnPad.VehicleOrder => Unit = NewTasking
def LogId = ""
/**
* The first chained action of the vehicle spawning process.
*/
val concealPlayer =
private val concealPlayer: ActorRef =
context.actorOf(Props(classOf[VehicleSpawnControlConcealPlayer], pad), s"${context.parent.path.name}-conceal")
def FactionObject: FactionAffinity = pad
import akka.actor.SupervisorStrategy._
override val supervisorStrategy = {
import akka.actor.OneForOneStrategy
override val supervisorStrategy: OneForOneStrategy = {
OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 10 seconds) {
case _: akka.actor.ActorKilledException => Restart
case _ => Resume
case _ =>
log.warn(s"vehicle spawn pad restarted${trackedOrder.map { o => s"; an unfulfilled order for ${o.driver.Name} will be expunged" }.getOrElse("")}")
Restart
}
}
@ -85,21 +88,18 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
try {
handleOrderFunc(msg)
} catch {
case _: AssertionError => ; //ehhh
case e: Exception => //something unexpected
e.printStackTrace()
case _: AssertionError => () //ehhh
case e: Exception => e.printStackTrace() //something unexpected
}
case VehicleSpawnControl.ProcessControl.OrderCancelled =>
trackedOrder match {
case Some(entry)
if sender() == concealPlayer =>
trackedOrder.collect {
case entry if sender() == concealPlayer =>
CancelOrder(
entry,
VehicleSpawnControl.validateOrderCredentials(pad, entry.driver, entry.vehicle)
.orElse(Some("@SVCP_RemovedFromVehicleQueue_Generic"))
)
case _ => ;
}
trackedOrder = None //guard off
SelectOrder()
@ -120,37 +120,40 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
During this time, a periodic message about the spawn pad being blocked will be broadcast to the order queue.
*/
case VehicleSpawnControl.ProcessControl.Reminder =>
trackedOrder match {
case Some(entry) =>
if (periodicReminder.isCancelled) {
trace(s"the pad has become blocked by a ${entry.vehicle.Definition.Name} in its current order")
periodicReminder = context.system.scheduler.scheduleWithFixedDelay(
VehicleSpawnControl.periodicReminderTestDelay,
VehicleSpawnControl.periodicReminderTestDelay,
self,
VehicleSpawnControl.ProcessControl.Reminder
)
} else {
BlockedReminder(entry, orders)
}
case None => ;
trackedOrder
.collect {
case entry =>
if (periodicReminder.isCancelled) {
trace(s"the pad has become blocked by a ${entry.vehicle.Definition.Name} in its current order")
periodicReminder = context.system.scheduler.scheduleWithFixedDelay(
VehicleSpawnControl.periodicReminderTestDelay,
VehicleSpawnControl.periodicReminderTestDelay,
self,
VehicleSpawnControl.ProcessControl.Reminder
)
} else {
BlockedReminder(entry, orders)
}
trackedOrder
}
.orElse {
periodicReminder.cancel()
}
None
}
case VehicleSpawnControl.ProcessControl.Flush =>
periodicReminder.cancel()
orders.foreach { CancelOrder(_, Some("@SVCP_RemovedFromVehicleQueue_Generic")) }
orders = Nil
trackedOrder match {
case Some(entry) => CancelOrder(entry, Some("@SVCP_RemovedFromVehicleQueue_Generic"))
case None => ;
trackedOrder.foreach {
entry => CancelOrder(entry, Some("@SVCP_RemovedFromVehicleQueue_Generic"))
}
trackedOrder = None
handleOrderFunc = NewTasking
pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) //cautious animation reset
concealPlayer ! akka.actor.Kill //should cause the actor to restart, which will abort any trapped messages
self ! akka.actor.Kill //should cause the actor to restart, which will abort any trapped messages
case _ => ;
case _ => ()
}
/**
@ -158,7 +161,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* All orders accepted in the meantime will be queued and a note about priority will be issued.
* @param order the order being accepted
*/
def NewTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
private def NewTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
handleOrderFunc = QueuedTasking
ProcessOrder(Some(order))
}
@ -168,7 +171,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* all orders accepted in the meantime will be queued and a note about priority will be issued.
* @param order the order being accepted
*/
def QueuedTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
private def QueuedTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
val name = order.player.Name
if (trackedOrder match {
case Some(tracked) =>
@ -219,14 +222,14 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
/**
* Select the next available queued order and begin processing it.
*/
def SelectOrder(): Unit = ProcessOrder(SelectFirstOrder())
private def SelectOrder(): Unit = ProcessOrder(SelectFirstOrder())
/**
* Select the next-available queued order if there is no current order being fulfilled.
* If the queue has been exhausted, set functionality to prepare to accept the next order as a "first order."
* @return the next-available order
*/
def SelectFirstOrder(): Option[VehicleSpawnPad.VehicleOrder] = {
private def SelectFirstOrder(): Option[VehicleSpawnPad.VehicleOrder] = {
trackedOrder match {
case None =>
val (completeOrder, remainingOrders): (Option[VehicleSpawnPad.VehicleOrder], List[VehicleSpawnPad.VehicleOrder]) =
@ -255,14 +258,12 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* @param order the order being accepted;
* `None`, if no order found or submitted
*/
def ProcessOrder(order: Option[VehicleSpawnPad.VehicleOrder]): Unit = {
private def ProcessOrder(order: Option[VehicleSpawnPad.VehicleOrder]): Unit = {
periodicReminder.cancel()
order match {
case Some(_order) =>
order.collect {
case VehicleSpawnPad.VehicleOrder(driver, vehicle, terminal) =>
val size = orders.size + 1
val driver = _order.player
val name = driver.Name
val vehicle = _order.vehicle
val newOrder = VehicleSpawnControl.Order(driver, vehicle)
recursiveOrderReminder(orders.iterator, size)
trace(s"processing next order - a ${vehicle.Definition.Name} for $name")
@ -273,7 +274,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
)
trackedOrder = Some(newOrder) //guard on
context.system.scheduler.scheduleOnce(2000 milliseconds, concealPlayer, newOrder)
case None => ;
driver.LogActivity(TerminalUsedActivity(AmenitySource(terminal), TransactionType.Buy))
}
}
@ -282,7 +283,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* either start a periodic examination of those credentials until the queue has been emptied or
* cancel a running periodic examination if the queue is already empty.
*/
def queueManagementTask(): Unit = {
private def queueManagementTask(): Unit = {
if (orders.nonEmpty) {
orders = orderCredentialsCheck(orders).toList
if (queueManagement.isCancelled) {
@ -306,7 +307,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* @param recipients the original list of orders
* @return the list of still-acceptable orders
*/
def orderCredentialsCheck(recipients: Iterable[VehicleSpawnPad.VehicleOrder]): Iterable[VehicleSpawnPad.VehicleOrder] = {
private def orderCredentialsCheck(recipients: Iterable[VehicleSpawnPad.VehicleOrder]): Iterable[VehicleSpawnPad.VehicleOrder] = {
recipients
.map { order =>
(order, VehicleSpawnControl.validateOrderCredentials(order.terminal, order.player, order.vehicle))
@ -328,10 +329,10 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* @param blockedOrder the previous order whose vehicle is blocking the spawn pad from operating
* @param recipients all of the other customers who will be receiving the message
*/
def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnPad.VehicleOrder]): Unit = {
private def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnPad.VehicleOrder]): Unit = {
val user = blockedOrder.vehicle
.Seats(0).occupant
.orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner))
.orElse(pad.Zone.GUID(blockedOrder.vehicle.OwnerGuid))
.orElse(pad.Zone.GUID(blockedOrder.DriverGUID))
val relevantRecipients: Iterator[VehicleSpawnPad.VehicleOrder] = user match {
case Some(p: Player) if !p.HasGUID =>
@ -358,14 +359,14 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* Cancel this vehicle order and inform the person who made it, if possible.
* @param entry the order being cancelled
*/
def CancelOrder(entry: VehicleSpawnControl.Order, msg: Option[String]): Unit = {
private def CancelOrder(entry: VehicleSpawnControl.Order, msg: Option[String]): Unit = {
CancelOrder(entry.vehicle, entry.driver, msg)
}
/**
* Cancel this vehicle order and inform the person who made it, if possible.
* @param entry the order being cancelled
*/
def CancelOrder(entry: VehicleSpawnPad.VehicleOrder, msg: Option[String]): Unit = {
private def CancelOrder(entry: VehicleSpawnPad.VehicleOrder, msg: Option[String]): Unit = {
CancelOrder(entry.vehicle, entry.player, msg)
}
/**
@ -373,7 +374,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* @param vehicle the vehicle from the order being cancelled
* @param player the player who would driver the vehicle from the order being cancelled
*/
def CancelOrder(vehicle: Vehicle, player: Player, msg: Option[String]): Unit = {
private def CancelOrder(vehicle: Vehicle, player: Player, msg: Option[String]): Unit = {
if (vehicle.Seats.values.count(_.isOccupied) == 0) {
VehicleSpawnControl.DisposeSpawnedVehicle(vehicle, player, pad.Zone)
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(player.Name, VehicleSpawnPad.Reminders.Cancelled, msg)
@ -436,8 +437,8 @@ object VehicleSpawnControl {
final case class Order(driver: Player, vehicle: Vehicle) {
assert(driver.HasGUID, s"when ordering a vehicle, the prospective driver ${driver.Name} does not have a GUID")
assert(vehicle.HasGUID, s"when ordering a vehicle, the ${vehicle.Definition.Name} does not have a GUID")
val DriverGUID = driver.GUID
val time = System.currentTimeMillis()
val DriverGUID: PlanetSideGUID = driver.GUID
val time: Long = System.currentTimeMillis()
}
/**
@ -502,7 +503,7 @@ object VehicleSpawnControl {
* @param player the player who would own the vehicle being disposed
* @param zone the zone in which the vehicle is registered (should be located)
*/
def DisposeSpawnedVehicle(vehicle: Vehicle, player: Player, zone: Zone): Unit = {
private def DisposeSpawnedVehicle(vehicle: Vehicle, player: Player, zone: Zone): Unit = {
DisposeVehicle(vehicle, zone)
zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(player.GUID)
}

View file

@ -17,6 +17,7 @@ final case class VehicleSource(
Orientation: Vector3,
Velocity: Option[Vector3],
deployed: DriveState.Value,
owner: Option[PlayerSource],
occupants: List[SourceEntry],
Modifiers: ResistanceProfile,
unique: UniqueVehicle
@ -42,6 +43,7 @@ object VehicleSource {
obj.Orientation,
obj.Velocity,
obj.DeploymentState,
None,
Nil,
obj.Definition.asInstanceOf[ResistanceProfile],
UniqueVehicle(

View file

@ -286,7 +286,7 @@ class VehicleControl(vehicle: Vehicle)
val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger)
val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire)
(if (seatGroup == AccessPermissionGroup.Driver) {
vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked
vehicle.OwnerGuid.contains(user.GUID) || vehicle.OwnerGuid.isEmpty || permission != VehicleLockState.Locked
} else {
permission != VehicleLockState.Locked
}) &&
@ -348,7 +348,7 @@ class VehicleControl(vehicle: Vehicle)
//are we already decaying? are we unowned? is no one seated anywhere?
if (!decaying &&
obj.Definition.undergoesDecay &&
obj.Owner.isEmpty &&
obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) {
decaying = true
decayTimer = context.system.scheduler.scheduleOnce(
@ -418,7 +418,7 @@ class VehicleControl(vehicle: Vehicle)
Vehicles.Disown(obj.GUID, obj)
if (!decaying &&
obj.Definition.undergoesDecay &&
obj.Owner.isEmpty &&
obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) {
decaying = true
decayTimer = context.system.scheduler.scheduleOnce(

View file

@ -50,9 +50,13 @@ final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
extends GeneralActivity
final case class Contribution(unique: SourceEntry, entries: List[InGameActivity])
final case class VehicleDismountActivity(vehicle: VehicleSource, player: PlayerSource)
extends GeneralActivity
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
extends GeneralActivity {
override val time: Long = entries.maxBy(_.time).time
val start: Long = entries.last.time
val end: Long = entries.head.time
}
/* vitals history */
@ -183,6 +187,9 @@ trait InGameHistory {
action match {
case Some(act) =>
history = history :+ act
if (IsContributionEvent(act)) {
previousContributionInheritance = None
}
case None => ()
}
history
@ -206,7 +213,7 @@ trait InGameHistory {
LogActivity(DamageFromPainbox(result))
case _: EnvironmentReason =>
LogActivity(DamageFromEnvironment(result))
case _ => ;
case _ =>
LogActivity(DamageFrom(result))
if(result.adversarial.nonEmpty) {
lastDamage = Some(result)
@ -227,70 +234,79 @@ trait InGameHistory {
}
}
private val contributionInheritance: mutable.HashMap[SourceUniqueness, Seq[Contribution]] =
mutable.HashMap[SourceUniqueness, Seq[Contribution]]()
private var previousContributionInheritance: Option[List[InGameActivity]] = None
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Boolean = {
private val contributionInheritance: mutable.HashMap[SourceUniqueness, Contribution] =
mutable.HashMap[SourceUniqueness, Contribution]()
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
if (target ne this) {
val events = target.GetContribution()
val nonEmpty = events.nonEmpty
if (nonEmpty) {
val source = SourceEntry(target)
contributionInheritance.get(source.unique) match {
case Some(previousContributions) =>
val uniqueEvents = for {
curr <- events
if !previousContributions.filter(_ == curr).exists(_.time == curr.time)
} yield curr
contributionInheritance.put(source.unique, previousContributions :+ Contribution(source, uniqueEvents))
case None =>
contributionInheritance.put(source.unique, Seq(Contribution(source, events)))
}
InGameHistory.ContributionFrom(target) match {
case out @ Some(in @ Contribution(src, _)) =>
contributionInheritance.put(src, in)
out
case None =>
contributionInheritance.remove(SourceEntry(target).unique)
None
}
nonEmpty
} else {
false
None
}
}
def RemoveContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Iterable[Contribution] = {
contributionInheritance.remove(SourceEntry(target).unique).getOrElse(Nil)
def GetContribution(): Option[List[InGameActivity]] = {
previousContributionInheritance
.collect {
case out @ events if events.head.time > System.currentTimeMillis() - 600000L =>
Some(out)
case events =>
val newEvents = GetContributionDuringPeriod(events, duration = 600000L)
if (newEvents.isEmpty) {
previousContributionInheritance = None
None
} else {
previousContributionInheritance = Some(newEvents)
Some(newEvents)
}
}
.flatten
.orElse {
val events = GetContributionDuringPeriod(History, duration = 600000L)
if (events.nonEmpty) {
previousContributionInheritance = Some(events)
Some(events)
} else {
None
}
}
}
def GetContribution(): List[InGameActivity] = {
GetContributionDuringPeriod(System.currentTimeMillis(), duration = 600000)
def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
val earliestEndTime = System.currentTimeMillis() - duration
list.collect {
case event: DamagingActivity if event.health > 0 && event.time > earliestEndTime => event
case event: RepairingActivity if event.amount > 0 && event.time > earliestEndTime => event
}
}
def GetContribution(ending: Long): List[InGameActivity] = {
GetContributionDuringPeriod(ending, duration = 600000)
}
def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = {
val start = ending - duration
History.collect { case repair: RepairFromEquipment
if repair.time <= ending && repair.time > start => repair
def IsContributionEvent(event: InGameActivity): Boolean = {
event match {
case _: RepairingActivity => true
case _: DamagingActivity => true
case _ => false
}
}
def HistoryAndContributions(): List[InGameActivity] = {
val ending = System.currentTimeMillis()
val start = ending - 600000
contributionInheritance.foreach { case (obj, list) =>
val filtered = list.filter { event => event.time <= ending && event.time > start }
if (filtered.isEmpty) {
contributionInheritance.remove(obj)
} else if (filtered.size != list.size) {
contributionInheritance.update(obj, filtered)
}
}
val contributions = contributionInheritance.flatMap { case (_, list) => list }
(History ++ contributions).sortBy(_.time)
History ++ contributionInheritance.values.toList
}
def ClearHistory(): List[InGameActivity] = {
lastDamage = None
val out = history
history = List.empty
previousContributionInheritance = None
contributionInheritance.clear()
out
}
}
@ -318,4 +334,10 @@ object InGameHistory {
unit.foreach { o => obj.ContributionFrom(o) }
}
}
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
target
.GetContribution()
.collect { case events => Contribution(SourceEntry(target).unique, events) }
}
}

View file

@ -5,7 +5,7 @@ import akka.actor.ActorRef
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity, SpawningActivity}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
@ -13,6 +13,28 @@ import scala.collection.mutable
import scala.concurrent.duration._
object KillAssists {
private def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
val spawnIndex = history.lastIndexWhere {
case _: SpawningActivity => true
case _: RevivingActivity => true
case _ => false
}
val endIndex = history.lastIndexWhere {
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
case _ => false
}
if (spawnIndex == -1 || endIndex == -1) {
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
// } else
// if (spawnIndex == -1) {
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
// } else if (endIndex == -1) {
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
} else {
history.slice(spawnIndex, endIndex)
}
}
private[exp] def determineKiller(
lastDamageActivity: Option[DamageResult],
history: List[InGameActivity]
@ -71,7 +93,7 @@ object KillAssists {
history: Iterable[InGameActivity],
eventBus: ActorRef
): Unit = {
val shortHistory = Support.limitHistoryToThisLife(history.toList)
val shortHistory = limitHistoryToThisLife(history.toList)
val everyone = determineKiller(lastDamage, shortHistory) match {
case Some((result, killer: PlayerSource)) =>
val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer))
@ -130,7 +152,7 @@ object KillAssists {
val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val total = assists.values.foldLeft(0f)(_ + _.total)
val output = assists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / total))
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.equipment_id }, kda.amount / total))
}
output.remove(victim.CharId)
output
@ -214,7 +236,7 @@ object KillAssists {
case Some(mod) =>
//previous attacker, just add to entry
val firstWeapon = mod.weapons.head
val weapons = if (firstWeapon.weapon_id == wepid) {
val weapons = if (firstWeapon.equipment_id == wepid) {
firstWeapon.copy(
amount = firstWeapon.amount + amount,
shots = firstWeapon.shots + 1,

View file

@ -2,29 +2,28 @@
package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import java.util.Date
import net.psforever.objects.avatar.scoring.{Assist, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceUniqueness}
import net.psforever.objects.vital.{Contribution, DamagingActivity, HealFromEquipment, HealFromTerminal, HealingActivity, InGameActivity, RepairFromEquipment, RepairFromExoSuitChange, RepairFromTerminal, RepairingActivity, RevivingActivity, SupportActivityCausedByAnother}
import net.psforever.objects.vital.interaction.Adversarial
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, VehicleSource}
import net.psforever.objects.vital.{Contribution, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleDismountActivity}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
import org.joda.time.Instant
import org.joda.time.{LocalDateTime => JodaLocalDateTime}
import scala.collection.mutable
object KillContributions {
private lazy val recoveryItems: Seq[Int] = {
final val RecoveryItems: Seq[Int] = {
import net.psforever.objects.GlobalDefinitions._
Seq(
bank.ObjectId,
nano_dispenser.ObjectId,
medicalapplicator.ObjectId,
medical_terminal.ObjectId,
adv_med_terminal.ObjectId
)
bank,
nano_dispenser,
medicalapplicator,
medical_terminal,
adv_med_terminal,
crystals_health_a,
crystals_health_b
).collect { _.ObjectId }
}
private[exp] def rewardTheseSupporters(
@ -34,465 +33,326 @@ object KillContributions {
bep: Long,
eventBus: ActorRef
): Unit = {
val killTime = kill.time
//setup
val killTime = kill.time.toDate.getTime
val faction = target.Faction
val shortHistory = Support.limitHistoryToThisLife(history.toList)
//direct heal assists and repair assists
/*
can be simplified by ignoring resolved damage and countered healing
and just extracting the HealingActivity and DamagingActivity events,
but the contribution percentage calculations will need to be adjusted
*/
val healthAssists = cullContributorImplements(allocateContributors(healthRecoveryContributors)(shortHistory, faction))
val armorAssists = cullContributorImplements(allocateContributors(armorRecoveryContributors)(shortHistory, faction))
//combined
val allAssists = healthAssists.map { case out @ (id, healthEntry) =>
armorAssists.get(id) match {
case Some(armorEntry) =>
(id, healthEntry.copy(weapons = healthEntry.weapons ++ armorEntry.weapons))
case None =>
out
}
}
//revival
contributeWithRevivalActivity(shortHistory, allAssists)
allAssists.remove(0)
allAssists.remove(target.CharId)
val limitedHistory = limitHistoryToThisLife(history.toList, killTime)
//divide by applicable time periods (long=10minutes, short=5minutes)
val longEvents = findTimeApplicableActivities(allAssists.values.toSeq, killTime, timeOffset = 600)
val shortEvents = findTimeApplicableActivities(longEvents, killTime, timeOffset = 300)
val victim = kill.victim
//convert to output format
longEvents
.map { stats =>
val (_, ContributionStatsOutput(p, w, r)) = mapContributionPointsByPercentage(total = 100f, shortEvents)(stats.player.CharId, stats)
(p, Assist(victim, w, r, (bep * r).toLong))
}
.foreach { case (player, kda) =>
eventBus ! AvatarServiceMessage(player.Name, AvatarAction.UpdateKillsDeathsAssists(player.CharId, kda))
}
}
private def allocateContributors(
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Seq[(Long, Int)]
)
(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value
): mutable.LongMap[ContributionStats] = {
/** players who have contributed to this death, and how much they have contributed<br>
* key - character identifier,
* value - (player, damage, total damage, number of shots) */
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
tallyFunc(history, faction, participants)
participants
}
private def cullContributorImplements(
input: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
input.collect { case (id, entry) =>
(id, entry.copy(weapons = entry.weapons.filter { stats => recoveryItems.contains(stats.weapon_id) }))
}.filter { case (_, entry) =>
entry.weapons.nonEmpty
val shortPeriod = killTime - 300000L
val (contributions, (longHistory, shortHistory)) = {
val (contrib, onlyHistory) = limitedHistory.partition { _.isInstanceOf[Contribution] }
(
contrib
.collect { case Contribution(unique, entries) => (unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]],
onlyHistory.partition { _.time > shortPeriod }
)
}
}
private def healthRecoveryContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
val contributions = history
.collect { case Contribution(unique, entries) => (unique.unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]]
val contributionsBy: mutable.LongMap[List[InGameActivity]] =
mutable.LongMap[List[InGameActivity]]()
history.foreach {
case d: DamagingActivity if d.health > 0 =>
val (damage, recovery) = contributeWithDamagingActivity(d, faction, d.health, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case h: HealFromEquipment =>
val (damage, recovery) = contributeWithRecoveryActivity(h.user, h.equipment_def.ObjectId, faction, h.amount, h.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case ht: HealFromTerminal =>
val time = ht.time
val users = cacheContributionsForEntityByUser(ht.term.unique, contributions, contributionsBy)
.collect { case entry: SupportActivityCausedByAnother
if entry.time <= time && entry.time > time - 300000 => entry //short support activity time
}
.groupBy(_.user.unique)
.map(_._2.head.user)
.toSeq
val (damage, recovery) = contributeWithSupportRecoveryActivity(users, ht.term.Definition.ObjectId, faction, ht.amount, time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case h: HealingActivity =>
val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, h.amount, h.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RevivingActivity =>
val (damage, recovery) = contributeWithRecoveryActivity(r.equipment.ObjectId, faction, r.amount, r.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case _ => ()
}
recoveryInOrder
}
private def armorRecoveryContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
val contributions = history
.collect { case Contribution(unique, entries) => (unique.unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]]
val contributionsBy: mutable.LongMap[List[InGameActivity]] =
mutable.LongMap[List[InGameActivity]]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
val (damage, recovery) = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RepairFromEquipment =>
val (damage, recovery) = contributeWithRecoveryActivity(r.user, r.equipment_def.ObjectId, faction, r.amount, r.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case rt: RepairFromTerminal => ()
val time = rt.time
val users = cacheContributionsForEntityByUser(rt.term.unique, contributions, contributionsBy)
.collect { case entry: SupportActivityCausedByAnother
if entry.time <= time && entry.time > time - 300000 => entry //short support activity time
}
.groupBy(_.user.unique)
.map(_._2.head.user)
.toSeq
val (damage, recovery) = contributeWithSupportRecoveryActivity(users, rt.term.Definition.ObjectId, faction, rt.amount, time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case rxc: RepairFromExoSuitChange =>
//TODO use cacheContributionsForEntityByUser; what out what order terminal was used
val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, rxc.amount, rxc.time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RepairingActivity =>
val (damage, recovery) = contributeWithRecoveryActivity(wepid = 0, faction, r.amount, r.time, participants, damageInOrder,recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case _ => ()
}
recoveryInOrder
}
def cacheContributionsForEntityByUser(
target: SourceUniqueness,
contributions: Map[SourceUniqueness, List[InGameActivity]],
contributionsBy: mutable.LongMap[List[InGameActivity]]
): List[InGameActivity] = {
contributions.get(target) match {
case Some(list) if list.nonEmpty =>
list
.collect { case r: RepairFromEquipment => r }
.groupBy(_.user.unique)
.flatMap { case (_, activities) =>
val charId = activities.head.user.CharId
contributionsBy.get(charId) match {
case Some(outList) =>
outList
case None =>
contributionsBy.put(charId, activities).getOrElse(Nil)
}
}.toList
case _ =>
Nil
}
}
private def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
amount: Int,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
//mark entries from the recovery list to truncate
val data = activity.data
val time = activity.time
val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _, _) => p }
var lastCharId = 0L
var lastValue = 0
var ramt = amount
var rindex = -1
val riter = recoveryOrder.iterator
while (riter.hasNext && ramt > 0) {
val (id, value) = riter.next()
if (value > 0) {
val entry = participants(id)
val weapons = entry.weapons
lastCharId = id
lastValue = value
if (value > ramt) {
participants.put(
id,
entry.copy(
weapons = weapons.head.copy(amount = weapons.head.amount - ramt, time = time) +: weapons.tail,
amount = entry.total - ramt,
time = time
)
)
ramt = 0
lastValue = lastValue - value
} else {
participants.put(
id,
entry.copy(
weapons = weapons.tail :+ weapons.head.copy(amount = 0, time = time),
amount = entry.total - ramt,
time = time
)
)
ramt = ramt - value
rindex += 1
lastValue = 0
}
}
rindex += 1
}
//damage calculations
val newDamageOrder = KillAssists.contributeWithDamagingActivity(
playerOpt,
data.interaction.cause.attribution,
faction,
amount,
time,
participants,
damageOrder
)
//re-combine output list(s)
val leftovers = if (lastValue > 0) {
Seq((lastCharId, lastValue))
//sort by applicable time periods, as long as the longer period is represented by activity
val otherContributionCalculations = contributionScoringAndCulling(faction, kill, Seq(target.CharId), contributions, bep)(_, _)
val finalContributions = if (longHistory.isEmpty) {
val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
contributionProcess.submit(shortHistory)
val contributionEntries = otherContributionCalculations(shortHistory, contributionProcess.output())
contributionEntries
.keys
.map { composeContributionOutput(_, contributionEntries, contributionEntries, bep) }
} else {
Nil
val longContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
val shortContributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Seq(longContributionProcess))
longContributionProcess.submit(longHistory)
shortContributionProcess.submit(shortHistory)
val longContributionEntries = otherContributionCalculations(longHistory, longContributionProcess.output())
val shortContributionEntries = otherContributionCalculations(shortHistory, shortContributionProcess.output())
(longContributionEntries.keys ++ shortContributionEntries.keys)
.toSeq
.distinct
.map { composeContributionOutput(_, shortContributionEntries, longContributionEntries, bep) }
}
//take the output and transform that into contribution distribution data
val victim = kill.victim
finalContributions.foreach { case (charId, ContributionStatsOutput(player, weapons, exp)) =>
eventBus ! AvatarServiceMessage(
player.Name,
AvatarAction.UpdateKillsDeathsAssists(charId, Assist(victim, weapons, 1f, exp.toLong))
)
}
(newDamageOrder, leftovers ++ recoveryOrder.slice(rindex, recoveryOrder.size) ++ recoveryOrder.take(rindex).map { case (id, _) => (id, 0) })
}
private def contributeWithRecoveryActivity(
user: PlayerSource,
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
contributeWithRecoveryActivity(user, user.CharId, wepid, faction, amount, time, participants, damageOrder, recoveryOrder)
/**
* Only historical activity that falls within the valid period matters.<br>
* Unlike an expected case where the history would be bound by being spawned and being killed, respectively,
* this imposes only the long contribution time limit on events since the latest entry;
* and, it may stop some time after the otherwise closest activity for being spawned.
* @param history the original history
* @param eventTime from which time to start counting backwards
* @return the potentially truncated history
*/
private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = {
val longLimit: Long = eventTime - 600000L
history.collect {
case event if event.time < eventTime && event.time >= longLimit => event
}
}
private def contributeWithRecoveryActivity(
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
contributeWithRecoveryActivity(PlayerSource.Nobody, charId = 0, wepid, faction, amount, time, participants, damageOrder, recoveryOrder)
private def contributionScoringAndCulling(
faction: PlanetSideEmpire.Value,
kill: Kill,
excludeTargets: Seq[Long],
contributions: Map[SourceUniqueness, List[InGameActivity]],
bep: Long
)
(
history: List[InGameActivity],
contributionEntries: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
contributionEntries.map { case (id, stat) =>
val newWeaponStats = stat.weapons.map { weaponStat =>
weaponStat.copy(contributions = (weaponStat.shots + (bep * weaponStat.amount / stat.total)).toFloat)
}
contributionEntries.put(id, stat.copy(weapons = newWeaponStats))
}
contributeWithKillWhileMountedActivity(faction, kill, history, contributionEntries)
contributeWithVehicleTransportActivity(history, contributionEntries)
contributeWithRevivalActivity(history, contributionEntries)
contributeWithTerminalActivity(faction, history, contributions, contributionEntries)
contributionEntries.remove(0)
excludeTargets.foreach { contributionEntries.remove }
contributionEntries
}
private def contributeWithRecoveryActivity(
user: PlayerSource,
charId: Long,
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
//recovery calculations
val newOrder = KillAssists.contributeWithRecoveryActivity(amount, participants, damageOrder)
//detect potential instances of and remove approximate value for friendly-fire recovery (that doesn't count)
val qualityRecoveryAmount = {
val (entries, count) = {
val value = damageOrder.size - newOrder.size
if (value > 0) {
if (newOrder.head == damageOrder.lift(value).getOrElse(newOrder.head)) {
(damageOrder.take(value), value)
} else {
(damageOrder.take(value + 1), value + 1)
}
} else {
if (newOrder.headOption != damageOrder.headOption) {
(damageOrder.headOption.toList, 1)
} else {
(Nil, 1)
}
}
private def contributeWithKillWhileMountedActivity(
faction: PlanetSideEmpire.Value,
kill: Kill,
history: List[InGameActivity],
participants: mutable.LongMap[ContributionStats]
): List[InGameActivity] = {
(kill
.info
.interaction
.cause match {
case p: ProjectileReason => p.projectile.mounted_in.map { _._2 }
case _ => None
})
.collect {
case mount: VehicleSource =>
//repairs
val contributions = history
.collect { case Contribution(unique, entries) if mount.unique == unique => (unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]]
contributions
.foreach {
case (_, localHistory) =>
val process = new ArmorRecoveryExperienceContributionProcess(faction, contributions)
process.submit(localHistory)
cullContributorImplements(process.output()).foreach {
case (id, stat) =>
val killExperience = kill.experienceEarned
val newWeapons = stat.weapons.map { weapon =>
val shots = weapon.shots
val amount = weapon.amount
val exp = math.min(0.5d * killExperience, shots + math.ceil(math.log(amount))).toFloat
weapon.copy(contributions = exp)
}.toList
contributeWithCombinedActivity(
id,
newWeapons,
stat.player,
stat.amount,
stat.total,
stat.shots,
stat.time,
participants
)
}
}
//vehicle owner
extractContributionsForEntityByUser(faction, mount, contributions, participants)
mount.owner
.collect {
case owner =>
val time = kill.time.toDate.getTime
participants.getOrElseUpdate(
owner.CharId,
ContributionStats(owner, Seq(WeaponStats(0, 1, 1, time, 10f)), 1, 1, 1, time)
)
}
}
if (entries.nonEmpty) {
//approximate the value as a percentage
amount * entries.count { case (id, _) => participants.get(id).collect { _.player.Faction != faction }.getOrElse(false) } / count
} else {
amount
}
}
if (qualityRecoveryAmount > 0) {
val updatedEntry = participants.get(charId) match {
case Some(entry) =>
//existing entry
entry.copy(
weapons = WeaponStats(wepid, qualityRecoveryAmount, 1, time, 1f) +: entry.weapons,
amount = entry.amount + qualityRecoveryAmount,
total = entry.total + qualityRecoveryAmount,
time = time
)
case None =>
//new entry
ContributionStats(user, Seq(WeaponStats(wepid, qualityRecoveryAmount, 1, time, 1f)), qualityRecoveryAmount, qualityRecoveryAmount, 1, time)
}
participants.put(charId, updatedEntry)
}
//re-combine output list(s) (bridge entry for output recovery list)
(newOrder, (charId, amount) +: recoveryOrder)
history
}
private def contributeWithSupportRecoveryActivity(
users: Seq[PlayerSource],
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
var outputDamageOrder = damageOrder
var outputRecoveryOrder = recoveryOrder
users.zip {
val numberOfUsers = users.size
val out = Array.fill(numberOfUsers)(numberOfUsers / amount)
(0 to numberOfUsers % amount).foreach {
out(_) += 1
private def contributeWithVehicleTransportActivity(
history: List[InGameActivity],
participants: mutable.LongMap[ContributionStats]
): List[InGameActivity] = {
val shortHistory = history.collect { case out: VehicleDismountActivity => out }
shortHistory
.groupBy { a => a.vehicle.owner }
.foreach { case (Some(owner), dismountsFromVehicle) =>
val numberOfDismounts = dismountsFromVehicle.size
contributeWithCombinedActivity(
owner.CharId,
dismountsFromVehicle.map { act => WeaponStats(act.vehicle.Definition.ObjectId, 1, 1, act.time, 15f) },
dismountsFromVehicle.head.vehicle.owner.get,
numberOfDismounts,
numberOfDismounts,
numberOfDismounts,
dismountsFromVehicle.maxBy(_.time).time,
participants
)
}
out
}.foreach { case (user, subAmount) =>
val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, participants, outputDamageOrder, outputRecoveryOrder)
outputDamageOrder = a
outputRecoveryOrder = b
}
(outputDamageOrder, outputRecoveryOrder)
shortHistory
}
private def contributeWithRevivalActivity(
history: List[InGameActivity],
participants: mutable.LongMap[ContributionStats]
): Unit = {
history
.collect { case rev: RevivingActivity => rev }
): List[InGameActivity] = {
val shortHistory = history.collect { case rev: RevivingActivity => rev }
shortHistory
.groupBy(_.user.CharId)
.foreach { case (id, revivesByThisPlayer) =>
val numberOfRevives = revivesByThisPlayer.size
val latestTime = revivesByThisPlayer.maxBy(_.time).time
val newStats = revivesByThisPlayer.map { stat => WeaponStats(stat.equipment.ObjectId, 100, 1, stat.time, 0.25f) }
val newStat = WeaponStats(revivesByThisPlayer.head.equipment.ObjectId, 100 * numberOfRevives, numberOfRevives, latestTime, 0.25f)
participants.get(id) match {
case Some(stats) =>
participants.put(id, stats.copy(weapons = stats.weapons ++ newStats))
case None =>
participants.put(id, ContributionStats(
revivesByThisPlayer.head.user,
newStats,
newStat.amount,
newStat.shots,
revivesByThisPlayer.size,
latestTime
))
}
contributeWithCombinedActivity(
id,
revivesByThisPlayer.map { stat => WeaponStats(stat.equipment.ObjectId, 100, 1, stat.time, 25f) },
revivesByThisPlayer.head.user,
amount = 100 * numberOfRevives,
total = 100 * numberOfRevives,
numberOfRevives,
revivesByThisPlayer.maxBy(_.time).time,
participants
)
}
shortHistory
}
private def findTimeApplicableActivities(
activity: Seq[ContributionStats],
killLastTime: JodaLocalDateTime,
timeOffset: Int //s
): Seq[ContributionStats] = {
val dateTimeConverter: Date=>JodaLocalDateTime = JodaLocalDateTime.fromDateFields
val milliToInstant: Long=>org.joda.time.Instant = Instant.ofEpochMilli
val targetTime = killLastTime.minusSeconds(timeOffset)
//find all activities that occurred before the kill time but after the offset
activity.collect { entry =>
val weapons = entry.weapons.filter { stat =>
val activityTime = dateTimeConverter(milliToInstant(stat.time).toDate)
(activityTime.isBefore(killLastTime) || activityTime.equals(killLastTime)) && targetTime.isAfter(activityTime)
}
.groupBy(_.weapon_id)
.collect { case (id, stats) =>
WeaponStats(
id,
stats.foldLeft(0)(_ + _.amount),
stats.foldLeft(0)(_ + _.shots),
stats.maxBy(_.time).time,
stats.foldLeft(0f)(_ + _.contributions)
)
}
if (weapons.nonEmpty) {
val totalMod = entry.total / entry.amount.toFloat
val recoveryAmount = weapons.foldLeft(0)(_ + _.amount)
val recoveryShots = weapons.foldLeft(0)(_ + _.shots)
val recoveryTime = weapons.map { _.time }.max
Some(ContributionStats(entry.player, weapons.toSeq, recoveryAmount, (recoveryAmount * totalMod).toInt, recoveryShots, recoveryTime))
} else {
None
}
}.flatten
private def contributeWithTerminalActivity(
faction: PlanetSideEmpire.Value,
history: List[InGameActivity],
contributions: Map[SourceUniqueness, List[InGameActivity]],
participants: mutable.LongMap[ContributionStats]
): List[InGameActivity] = {
contributeWithTerminalActivity(
history.collect { case t: TerminalUsedActivity => (t, t.terminal, t.terminal.hacked) },
faction,
contributions,
participants
)
}
private def mapContributionPointsByPercentage(
total: Float,
compareList: Iterable[ContributionStats]
)
(
charId: Long,
contribution: ContributionStats,
): (Long, ContributionStatsOutput) = {
val user = contribution.player
val unique = user.unique
val points = contribution.amount
val value = if (points < 75) {
//a small contribution means the lower time limit
if (compareList.exists { a => a.player.unique == unique }) {
math.max(0.2f, points / total)
} else {
0f
}
} else {
//large contribution is always okay
if (points > 299) {
1.0f
} else if (points > 100) {
0.75f
} else {
0.5f
}
private[exp] def contributeWithTerminalActivity(
data: Seq[(InGameActivity, AmenitySource, Option[HackInfo])],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
participants: mutable.LongMap[ContributionStats]
): List[InGameActivity] = {
data.collect {
case (t, terminal, Some(info)) if terminal.Faction != faction =>
participants.getOrElseUpdate(
info.player.CharId,
ContributionStats(info.player, Seq(WeaponStats(0, 1, 1, t.time, 10f)), 1, 1, 1, t.time)
)
t
case (t, terminal, _) =>
extractContributionsForEntityByUser(faction, terminal, contributions, participants)
t
}.toList
}
private def extractContributionsForEntityByUser(
faction: PlanetSideEmpire.Value,
target: SourceEntry,
contributions: Map[SourceUniqueness, List[InGameActivity]],
contributionsBy: mutable.LongMap[ContributionStats]
): List[InGameActivity] = {
val shortHistory = contributions.getOrElse(target.unique, Nil)
val process = new ArmorRecoveryExperienceContributionProcess(faction, contributions)
process.submit(shortHistory)
cullContributorImplements(process.output()).foreach {
case (id, stats) => contributionsBy.getOrElseUpdate(id, stats)
}
(charId, ContributionStatsOutput(user, contribution.weapons.map { _.weapon_id }, value))
shortHistory
}
private[exp] def cullContributorImplements(
input: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
input.collect { case (id, entry) =>
(id, entry.copy(weapons = entry.weapons.filter { stats => KillContributions.RecoveryItems.contains(stats.equipment_id) }))
}.filter { case (_, entry) =>
entry.weapons.nonEmpty
}
}
private def contributeWithCombinedActivity(
id: Long,
newStats: List[WeaponStats],
user: PlayerSource,
amount: Int,
total: Int,
shots: Int,
time: Long,
participants: mutable.LongMap[ContributionStats]
): Unit = {
participants.get(id) match {
case Some(stats) =>
val oldWeapons = stats.weapons
val newWeapons = newStats.map { weapon =>
oldWeapons.find { _.equipment_id == weapon.equipment_id } match {
case Some(foundEntry) =>
weapon.copy(
amount = weapon.amount + foundEntry.amount,
shots = weapon.shots + foundEntry.shots,
contributions = weapon.contributions + foundEntry.contributions
)
case None =>
weapon
}
}
participants.put(id, stats.copy(weapons = newWeapons))
case None =>
participants.put(id, ContributionStats(user, newStats, amount, total, shots, time))
}
}
private def composeContributionOutput(
charId: Long,
shortPeriod: mutable.LongMap[ContributionStats],
longPeriod: mutable.LongMap[ContributionStats],
bep: Long
): (Long, ContributionStatsOutput) = {
shortPeriod
.get(charId)
.collect {
case entry =>
val weapons = entry.weapons
(
entry.player,
weapons.map { _.equipment_id },
math.min(weapons.foldLeft(0f)(_ + _.contributions).toLong, bep)
)
}
.orElse {
longPeriod
.get(charId)
.collect {
case entry =>
val weapons = entry.weapons
(
entry.player,
weapons.filter { _.amount == 0 }.map { _.equipment_id },
(0.9f * math.min(weapons.foldLeft(0f)(_ + _.contributions), bep.toFloat)).toLong
)
}
}
.orElse {
Some((PlayerSource.Nobody, Seq(0), 0L))
}
.collect {
case (player, weaponIds, experience) =>
(charId, ContributionStatsOutput(player, weaponIds, experience.toFloat))
}
.get
}
}

View file

@ -1,295 +0,0 @@
// Copyright (c) NOT VERSIONED PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.definition.WithShields
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceWithHealthEntry, SourceWithShieldsEntry}
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairingActivity, ShieldCharge, SpawningActivity}
import net.psforever.objects.vital.interaction.Adversarial
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
object KillDeathAssists2 {
private def collectKillAssists(
victim: SourceEntry,
history: List[InGameActivity],
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStatsOutput] = {
val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val total = assists.values.foldLeft(0f)(_ + _.total)
val output = assists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / total))
}
output.remove(victim.CharId)
output
}
private[exp] def collectAssistsForEntity(
target: SourceWithHealthEntry,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
val (_, maxShields) = maximumEntityHealthAndShields(history)
val healthAssists = collectKillAssists(
target,
history,
Support.allocateContributors(entityHealthDamageContributors)
)
if (maxShields > 0) {
val shieldAssists = collectShieldAssists(target.asInstanceOf[SourceWithShieldsEntry], history)
killerOpt.collect {
case killer =>
healthAssists.remove(killer.CharId)
shieldAssists.remove(killer.CharId)
}
Support.onlyOriginalAssistEntries(healthAssists, shieldAssists)
} else {
killerOpt.collect {
case killer =>
healthAssists.remove(killer.CharId)
}
healthAssists.values
}
}
private[exp] def collectShieldAssists(
victim: SourceWithShieldsEntry,
history: List[InGameActivity],
): mutable.LongMap[ContributionStatsOutput] = {
val initialShieldAssists = Support.allocateContributors(entityShieldDamageContributors)(history, victim.Faction)
.filterNot { case (_, kda) => kda.amount <= 0 }
val fullFloatShield = initialShieldAssists.values.foldLeft(0)(_ + _.total).toFloat
val shieldAssists = initialShieldAssists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, math.min(kda.total / fullFloatShield, 0.4f)))
}
shieldAssists.remove(victim.CharId)
shieldAssists
}
private def entityShieldDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.tail.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
case v: ShieldCharge if v.amount > 0 =>
inOrder = contributeWithRecoveryActivity(v.amount, participants, inOrder)
case _ => ()
}
inOrder
}
private def maximumEntityHealthAndShields(history: List[InGameActivity]): (Int, Int) = {
var (health, shields) = (0, 0)
history.collect {
case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
health = math.max(target.Definition.MaxHealth, health)
shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
health = math.max(target.Definition.MaxHealth, health)
case d: DamagingActivity =>
d.data.targetBefore match {
case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
health = math.max(target.Definition.MaxHealth, health)
shields = math.max(target.Definition.asInstanceOf[WithShields].MaxShields, shields)
case target: SourceWithHealthEntry =>
health = math.max(target.health, health)
case _ => ()
}
}
(health, shields)
}
// private def initialEntityHealthAndShields(history: List[InGameActivity]): (Int, Int, PlanetSideEmpire.Value) = {
// history.collectFirst {
// case SpawningActivity(target: SourceWithHealthEntry with SourceWithShieldsEntry, _, _) =>
// (target.health, target.shields, target.Faction)
// case SpawningActivity(target: SourceWithHealthEntry, _, _) =>
// (target.health, 0, target.Faction)
// case d: DamagingActivity =>
// d.data.targetBefore match {
// case target: SourceWithHealthEntry with SourceWithShieldsEntry =>
// (target.health, target.shields, target.Faction)
// case target: SourceWithHealthEntry =>
// (target.health, 0, target.Faction)
// case _ =>
// (0, 0, PlanetSideEmpire.NEUTRAL)
// }
// }.getOrElse((0, 0, PlanetSideEmpire.NEUTRAL))
// }
private def armorDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
case r: RepairingActivity =>
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
case _ => ()
}
inOrder
}
private[exp] def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
val data = activity.data
val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _,_) => p }
contributeWithDamagingActivity(
playerOpt,
data.interaction.cause.attribution,
faction,
amount,
activity.time,
participants,
order
)
}
private[exp] def contributeWithDamagingActivity(
userOpt: Option[PlayerSource],
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
userOpt match {
case Some(user)
if user.Faction != faction =>
val whoId = user.CharId
val percentage = amount / user.Definition.MaxHealth.toFloat
val updatedEntry = participants.get(whoId) match {
case Some(mod) =>
//previous attacker, just add to entry
val firstWeapon = mod.weapons.head
val weapons = if (firstWeapon.weapon_id == wepid) {
firstWeapon.copy(
amount = firstWeapon.amount + amount,
shots = firstWeapon.shots + 1,
time = time,
contributions = firstWeapon.contributions + percentage
) +: mod.weapons.tail
} else {
WeaponStats(wepid, amount, 1, time, percentage) +: mod.weapons
}
mod.copy(
amount = mod.amount + amount,
weapons = weapons,
total = mod.total + amount,
shots = mod.shots + 1,
time = time
)
case None =>
//new attacker, new entry
ContributionStats(
user,
Seq(WeaponStats(wepid, amount, 1, time, percentage)),
amount,
amount,
1,
time
)
}
participants.put(whoId, updatedEntry)
order.indexWhere({ case (id, _) => id == whoId }) match {
case 0 =>
//ongoing attack by same player
val entry = order.head
(entry._1, entry._2 + amount) +: order.tail
case _ =>
//different player than immediate prior attacker
(whoId, amount) +: order
}
case _ =>
//damage that does not lead to contribution
order.headOption match {
case Some((id, dam)) =>
if (id == 0L) {
(0L, dam + amount) +: order.tail //pool
} else {
(0L, amount) +: order //new
}
case None =>
order
}
}
}
private def entityHealthDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.collect {
case d: DamagingActivity if d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
case r: RepairingActivity if r.amount > 0 =>
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
}
inOrder
}
private[exp] def contributeWithRecoveryActivity(
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
var amt = amount
var count = 0
var newOrder: Seq[(Long, Int)] = Nil
order.takeWhile { entry =>
val (id, total) = entry
if (id > 0 && total > 0) {
val part = participants(id)
if (amount > total) {
//drop this entry
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
amt = amt - total
} else {
//edit around the inclusion of this entry
val newTotal = total - amt
val trimmedWeapons = {
var index = -1
var weaponSum = 0
val pweapons = part.weapons
while (weaponSum < amt) {
index += 1
weaponSum = weaponSum + pweapons(index).amount
}
pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
}
newOrder = (id, newTotal) +: newOrder
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
amt = 0
}
}
count += 1
amt > 0
}
newOrder ++ order.drop(count)
}
}

View file

@ -0,0 +1,449 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.{PlayerSource, SourceUniqueness}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, HealFromTerminal, HealingActivity, InGameActivity, RepairFromEquipment, RepairFromTerminal, RepairingActivity, RevivingActivity, SupportActivityCausedByAnother}
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
sealed trait RecoveryExperienceContribution {
def submit(history: List[InGameActivity]): Unit
def output(): mutable.LongMap[ContributionStats]
def clear(): Unit
}
object RecoveryExperienceContribution {
private[exp] def contributeWithDamagingActivity(
activity: DamagingActivity,
amount: Int,
damageParticipants: mutable.LongMap[PlayerSource],
recoveryParticipants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
//mark entries from the ordered recovery list to truncate
val data: DamageResult = activity.data
val time: Long = activity.time
var lastCharId: Long = 0L
var lastValue: Int = 0
var ramt: Int = amount
var rindex: Int = 0
val riter = recoveryOrder.iterator
while (riter.hasNext && ramt > 0) {
val (id, value) = riter.next()
if (value > 0) {
/*
if the amount on the previous recovery node is positive, reduce it by the damage value for that user's last used equipment
keep traversing recovery nodes, and lobbing them off, until the recovery amount is zero
if the user can not be found having an entry, skip the update but lob off the recovery progress node all the same
if the amount is zero, do not check any further recovery progress nodes
*/
recoveryParticipants
.get(id)
.foreach { entry =>
val weapons = entry.weapons
lastCharId = id
lastValue = value
if (value > ramt) {
//take from the value on the last-used equipment, at the front of the list
recoveryParticipants.put(
id,
entry.copy(
weapons = weapons.head.copy(amount = math.max(0, weapons.head.amount - ramt), time = time) +: weapons.tail,
amount = math.max(0, entry.amount - ramt),
time = time
)
)
ramt = 0
lastValue = lastValue - value
} else {
//take from the value on the last-used equipment, at the front of the list
//move that entry to the end of the list
recoveryParticipants.put(
id,
entry.copy(
weapons = weapons.tail :+ weapons.head.copy(amount = 0, time = time),
amount = math.max(0, entry.amount - ramt),
time = time
)
)
ramt = ramt - value
rindex += 1
lastValue = 0
}
}
rindex += 1
}
}
//damage order and damage contribution entry
val newDamageEntry = data
.adversarial
.collect { case Adversarial(p: PlayerSource, _, _) => (p, damageParticipants.get(p.CharId)) }
.collect {
case (player, Some(PlayerSource.Nobody)) =>
damageParticipants.put(player.CharId, player)
Some(player)
case (player, Some(_)) =>
damageParticipants.getOrElseUpdate(player.CharId, player)
Some(player)
}
.collect {
case Some(player) => (player.CharId, amount) //for damageOrder
}
.orElse {
Some((0L, amount)) //for damageOrder
}
//re-combine output list(s)
val leftovers = if (lastValue > 0) {
Seq((lastCharId, lastValue))
} else {
Nil
}
(newDamageEntry.toList ++ damageOrder, leftovers ++ recoveryOrder.slice(rindex, recoveryOrder.size) ++ recoveryOrder.take(rindex).map { case (id, _) => (id, 0) })
}
private[exp] def contributeWithRecoveryActivity(
user: PlayerSource,
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
damageParticipants: mutable.LongMap[PlayerSource],
recoveryParticipants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
contributeWithRecoveryActivity(user, user.CharId, wepid, faction, amount, time, damageParticipants, recoveryParticipants, damageOrder, recoveryOrder)
}
private[exp] def contributeWithRecoveryActivity(
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
damageParticipants: mutable.LongMap[PlayerSource],
recoveryParticipants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
contributeWithRecoveryActivity(PlayerSource.Nobody, charId = 0, wepid, faction, amount, time, damageParticipants, recoveryParticipants, damageOrder, recoveryOrder)
}
private[exp] def contributeWithRecoveryActivity(
user: PlayerSource,
charId: Long,
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
damageParticipants: mutable.LongMap[PlayerSource],
recoveryParticipants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
//mark entries from the ordered damage list to truncate
val damageEntries = damageOrder.iterator
var amtToReduce: Int = amount
var amtToGain: Int = 0
var lastValue: Int = -1
var damageRemoveCount: Int = 0
var damageRemainder: Seq[(Long, Int)] = Nil
//keep reducing previous damage until recovery amount is depleted, or no more damage entries remain, or the last damage entry was depleted already
while (damageEntries.hasNext && amtToReduce > 0 && lastValue != 0) {
val (id, value) = damageEntries.next()
lastValue = value
if (value > 0) {
damageParticipants
.get(id)
.collect {
case player if player.Faction != faction =>
//if previous attacker was an enemy, the recovery counts towards contribution
if (value > amtToReduce) {
damageRemainder = Seq((id, value - amtToReduce))
amtToGain = amtToGain + amtToReduce
amtToReduce = 0
} else {
amtToGain = amtToGain + value
amtToReduce = amtToReduce - value
}
Some(player)
case player =>
//if the previous attacker was friendly fire, the recovery doesn't count towards contribution
if (value > amtToReduce) {
damageRemainder = Seq((id, value - amtToReduce))
amtToReduce = 0
} else {
amtToReduce = amtToReduce - value
}
Some(player)
}
.orElse {
//if we couldn't find an entry, just give the contribution to the user anyway
damageParticipants.put(id, PlayerSource.Nobody)
if (value > amtToReduce) {
damageRemainder = Seq((id, value - amtToReduce))
amtToGain = amtToGain + amtToReduce
amtToReduce = 0
} else {
amtToGain = amtToGain + value
amtToReduce = amtToReduce - value
}
None
}
//keep track of entries whose damage was depleted
damageRemoveCount += 1
}
}
amtToGain = amtToGain + amtToReduce //if early termination, gives leftovers as gain
if (amtToGain > 0) {
val newWeaponStats = WeaponStats(wepid, amtToGain, 1, time, 1f)
//try: add first contribution entry
//then: add accumulation of last weapon entry to contribution entry
//last: add new weapon entry to contribution entry
recoveryParticipants
.getOrElseUpdate(
charId,
ContributionStats(user, Seq(newWeaponStats), amtToGain, amtToGain, 1, time)
) match {
case entry if entry.weapons.size > 1 =>
if (entry.weapons.head.equipment_id == wepid) {
val head = entry.weapons.head
recoveryParticipants.put(
charId,
entry.copy(
weapons = head.copy(amount = head.amount + amtToGain, shots = head.shots + 1, time = time) +: entry.weapons.tail,
amount = entry.amount + amtToGain,
total = entry.total + amtToGain,
shots = entry.shots + 1,
time = time
)
)
} else {
recoveryParticipants.put(
charId,
entry.copy(
weapons = newWeaponStats +: entry.weapons,
amount = entry.amount + amtToGain,
total = entry.total + amtToGain,
shots = entry.shots + 1,
time = time
)
)
}
case _ => ()
//not technically possible
}
}
val newRecoveryEntry = if (amtToGain == 0) {
Seq((0L, amount))
} else if (amtToGain < amount) {
Seq((0L, amount - amtToGain), (charId, amtToGain))
} else {
Seq((charId, amount))
}
(
damageRemainder ++ damageOrder.drop(damageRemoveCount) ++ damageOrder.take(damageRemoveCount).map { case (id, _) => (id, 0) },
newRecoveryEntry ++ recoveryOrder
)
}
private[exp] def contributeWithSupportRecoveryActivity(
users: Seq[PlayerSource],
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
damageOrder: Seq[(Long, Int)],
recoveryOrder: Seq[(Long, Int)]
): (Seq[(Long, Int)], Seq[(Long, Int)]) = {
var outputDamageOrder = damageOrder
var outputRecoveryOrder = recoveryOrder
val damageParticipants: mutable.LongMap[PlayerSource] = mutable.LongMap[PlayerSource]()
users.zip {
val numberOfUsers = users.size
val out = Array.fill(numberOfUsers)(numberOfUsers / amount)
(0 to numberOfUsers % amount).foreach {
out(_) += 1
}
out
}.foreach { case (user, subAmount) =>
val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, damageParticipants, participants, outputDamageOrder, outputRecoveryOrder)
outputDamageOrder = a
outputRecoveryOrder = b
}
(outputDamageOrder, outputRecoveryOrder)
}
}
//noinspection ScalaUnusedSymbol
private abstract class RecoveryExperienceContributionProcess(
faction : PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]]
) extends RecoveryExperienceContribution {
protected var damageInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
protected var recoveryInOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
protected val contributionsBy: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
protected val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
protected val damageParticipants: mutable.LongMap[PlayerSource] = mutable.LongMap[PlayerSource]()
def submit(history: List[InGameActivity]): Unit
def output(): mutable.LongMap[ContributionStats] = {
val output = participants.map { a => a }
clear()
output
}
def clear(): Unit = {
damageInOrder = Nil
recoveryInOrder = Nil
contributionsBy.clear()
participants.clear()
damageParticipants.clear()
}
}
private class HealthRecoveryExperienceContributionProcess(
private val faction : PlanetSideEmpire.Value,
private val contributions: Map[SourceUniqueness, List[InGameActivity]]
) extends RecoveryExperienceContributionProcess(faction, contributions) {
def submit(history: List[InGameActivity]): Unit = {
history.foreach {
case d: DamagingActivity if d.health > 0 =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithDamagingActivity(d, d.health, damageParticipants, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case ht: HealFromTerminal =>
val time = ht.time
val users = KillContributions.contributeWithTerminalActivity(Seq((ht, ht.term, ht.term.hacked)), faction, contributions, contributionsBy)
.collect { case entry: SupportActivityCausedByAnother => entry }
.groupBy(_.user.unique)
.map(_._2.head.user)
.toSeq
val (damage, recovery) = RecoveryExperienceContribution.contributeWithSupportRecoveryActivity(users, ht.term.Definition.ObjectId, faction, ht.amount, time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case h: HealFromEquipment =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(h.user, h.equipment_def.ObjectId, faction, h.amount, h.time, damageParticipants, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case h: HealingActivity =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(wepid = 0, faction, h.amount, h.time, damageParticipants, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RevivingActivity =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(r.equipment.ObjectId, faction, r.amount, r.time, damageParticipants, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case _ => ()
}
}
}
private class ArmorRecoveryExperienceContributionProcess(
private val faction : PlanetSideEmpire.Value,
private val contributions: Map[SourceUniqueness, List[InGameActivity]]
) extends RecoveryExperienceContributionProcess(faction, contributions) {
def submit(history: List[InGameActivity]): Unit = {
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithDamagingActivity(d, d.amount - d.health, damageParticipants, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case rt: RepairFromTerminal =>
val time = rt.time
val users = KillContributions.contributeWithTerminalActivity(Seq((rt, rt.term, rt.term.hacked)), faction, contributions, contributionsBy)
.collect { case entry: SupportActivityCausedByAnother => entry }
.groupBy(_.user.unique)
.map(_._2.head.user)
.toSeq
val (damage, recovery) = RecoveryExperienceContribution.contributeWithSupportRecoveryActivity(users, rt.term.Definition.ObjectId, faction, rt.amount, time, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RepairFromEquipment =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(r.user, r.equipment_def.ObjectId, faction, r.amount, r.time, damageParticipants, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case r: RepairingActivity =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithRecoveryActivity(wepid = 0, faction, r.amount, r.time, damageParticipants, participants, damageInOrder, recoveryInOrder)
damageInOrder = damage
recoveryInOrder = recovery
case _ => ()
}
}
}
private class CombinedHealthAndArmorContributionProcess(
private val faction : PlanetSideEmpire.Value,
private val contributions: Map[SourceUniqueness, List[InGameActivity]],
otherSubmissions: Seq[RecoveryExperienceContribution]
) extends RecoveryExperienceContribution {
private val process: Seq[RecoveryExperienceContributionProcess] = Seq(
new HealthRecoveryExperienceContributionProcess(faction, contributions),
new ArmorRecoveryExperienceContributionProcess(faction, contributions)
)
def submit(history: List[InGameActivity]): Unit = {
for (elem <- process ++ otherSubmissions) { elem.submit(history) }
}
def output(): mutable.LongMap[ContributionStats] = {
val output = combineRecoveryContributions(
KillContributions.cullContributorImplements(process.head.output()),
KillContributions.cullContributorImplements(process(1).output())
)
clear()
output
}
def clear(): Unit = {
process.foreach ( _.clear() )
}
private def combineRecoveryContributions(
healthAssists: mutable.LongMap[ContributionStats],
armorAssists: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
healthAssists
.map {
case out@(id, healthEntry) =>
armorAssists.get(id) match {
case Some(armorEntry) =>
//healthAssists && armorAssists
(id, healthEntry.copy(weapons = healthEntry.weapons ++ armorEntry.weapons))
case None =>
//healthAssists only
out
}
}
.addAll {
//armorAssists only
val healthKeys = healthAssists.keys.toSeq
armorAssists.filter { case (id, _) => !healthKeys.contains(id) }
}
.map {
case (id, entry) =>
val groupedWeapons = entry.weapons
.groupBy(_.equipment_id)
.map {
case (weaponId, weaponEntries) =>
val specificEntries = weaponEntries.filter(_.equipment_id == weaponId)
val amount = specificEntries.foldLeft(0)(_ + _.amount)
val shots = specificEntries.foldLeft(0)(_ + _.shots)
WeaponStats(weaponId, amount, shots, specificEntries.maxBy(_.time).time, 1f)
}
.toSeq
(id, ContributionStats(
player = entry.player,
weapons = groupedWeapons,
amount = entry.amount + entry.amount,
total = entry.total + entry.total,
shots = groupedWeapons.foldLeft(0)(_ + _.shots),
time = groupedWeapons.maxBy(_.time).time
))
}
}
}

View file

@ -3,22 +3,36 @@ package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource
sealed trait ItemUseStats {
def equipment_id: Int
def shots: Int
def time: Long
def contributions: Float
}
private case class WeaponStats(
weapon_id: Int,
equipment_id: Int,
amount: Int,
shots: Int,
time: Long,
contributions: Float
)
) extends ItemUseStats
private case class ContributionStats(
player: PlayerSource,
weapons: Seq[WeaponStats],
amount: Int,
total: Int,
shots: Int,
time: Long
)
private case class EquipmentStats(
equipment_id: Int,
shots: Int,
time: Long,
contributions: Float
) extends ItemUseStats
private[exp] case class ContributionStats(
player: PlayerSource,
weapons: Seq[WeaponStats],
amount: Int,
total: Int,
shots: Int,
time: Long
)
sealed case class ContributionStatsOutput(
player: PlayerSource,

View file

@ -8,7 +8,7 @@ import org.joda.time.{Instant, LocalDateTime => JodaLocalDateTime}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry}
import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, InGameActivity, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, ReconstructionActivity, RepairFromEquipment, RepairFromExoSuitChange, RevivingActivity, SpawningActivity, SupportActivityCausedByAnother, TerminalUsedActivity}
import net.psforever.objects.zones.Zone
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
import net.psforever.zones.Zones
@ -18,28 +18,6 @@ import scala.collection.mutable
object Support {
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother
private[exp] def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
val spawnIndex = history.lastIndexWhere {
case _: SpawningActivity => true
case _: RevivingActivity => true
case _ => false
}
val endIndex = history.lastIndexWhere {
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
case _ => false
}
if (spawnIndex == -1 || endIndex == -1) {
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
// } else
// if (spawnIndex == -1) {
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
// } else if (endIndex == -1) {
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
} else {
history.slice(spawnIndex, endIndex)
}
}
private[exp] def onlyOriginalAssistEntries(
first: mutable.LongMap[ContributionStatsOutput],
second: mutable.LongMap[ContributionStatsOutput],
@ -349,7 +327,7 @@ object Support {
0.5f
}
}
(charId, ContributionStatsOutput(user, contribution.weapons.map { _.weapon_id }, value))
(charId, ContributionStatsOutput(user, contribution.weapons.map { _.equipment_id }, value))
}
//noinspection ScalaUnusedSymbol
@ -361,7 +339,7 @@ object Support {
charId: Long,
contribution: ContributionStats,
): (Long, ContributionStatsOutput) = {
(charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.weapon_id }, compareList.size.toFloat))
(charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.equipment_id }, compareList.size.toFloat))
}
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {

View file

@ -564,7 +564,7 @@ class ConverterTest extends Specification {
obj.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90)
obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001)
obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 1
obj.Definition.Packet.ConstructorData(obj) match {
case Success(pkt) =>
@ -596,7 +596,7 @@ class ConverterTest extends Specification {
obj.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90)
obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001)
obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 0
obj.Definition.Packet.ConstructorData(obj) match {
case Success(pkt) =>
@ -628,7 +628,7 @@ class ConverterTest extends Specification {
obj.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90)
//obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001)
obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 1
obj.Definition.Packet.ConstructorData(obj).isFailure mustEqual true
@ -641,7 +641,7 @@ class ConverterTest extends Specification {
obj.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90)
obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001)
obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 1
obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true
}

View file

@ -34,16 +34,17 @@ class DeployableTest extends Specification {
"Deployable" should {
"know its owner by GUID" in {
val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine)
obj.Owner.isEmpty mustEqual true
obj.Owner = PlanetSideGUID(10)
obj.Owner.contains(PlanetSideGUID(10)) mustEqual true
obj.OwnerGuid.isEmpty mustEqual true
obj.OwnerGuid = PlanetSideGUID(10)
obj.OwnerGuid.contains(PlanetSideGUID(10)) mustEqual true
}
"know its owner by GUID" in {
val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine)
obj.OwnerName.isEmpty mustEqual true
obj.OwnerName = "TestCharacter"
obj.OwnerName.contains("TestCharacter") mustEqual true
// val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine)
// obj.OwnerName.isEmpty mustEqual true
// obj.OwnerName = "TestCharacter"
// obj.OwnerName.contains("TestCharacter") mustEqual true
ko
}
"know its faction allegiance" in {
@ -339,8 +340,8 @@ class ExplosiveDeployableJammerTest extends ActorTest {
guid.register(player2, 4)
guid.register(weapon, 5)
j_mine.Zone = zone
j_mine.Owner = player2
j_mine.OwnerName = player2.Name
j_mine.OwnerGuid = player2
//j_mine.OwnerName = player2.Name
j_mine.Faction = PlanetSideEmpire.NC
j_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], j_mine), "j-mine-control")
@ -422,8 +423,8 @@ class ExplosiveDeployableJammerExplodeTest extends ActorTest {
guid.register(player2, 4)
guid.register(weapon, 5)
h_mine.Zone = zone
h_mine.Owner = player2
h_mine.OwnerName = player2.Name
h_mine.OwnerGuid = player2
//h_mine.OwnerName = player2.Name
h_mine.Faction = PlanetSideEmpire.NC
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control")
zone.blockMap.addTo(player1)
@ -528,8 +529,8 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
guid.register(player2, 4)
guid.register(weapon, 5)
h_mine.Zone = zone
h_mine.Owner = player2
h_mine.OwnerName = player2.Name
h_mine.OwnerGuid = player2
//h_mine.OwnerName = player2.Name
h_mine.Faction = PlanetSideEmpire.NC
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control")

View file

@ -310,7 +310,7 @@ class VehicleControlMountingBlockedExosuitTest extends ActorTest {
// Reset to allow further driver mount mounting tests
vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref)
probe.receiveOne(500 milliseconds) //discard
vehicle.Owner = None //ensure
vehicle.OwnerGuid = None //ensure
vehicle.OwnerName = None //ensure
vehicle.Actor.tell(Mountable.TryMount(player3, 1), probe.ref) // Agile in driver mount allowing all except MAX
VehicleControlTest.checkCanMount(probe, "Agile in driver mount allowing all except MAX")
@ -369,7 +369,7 @@ class VehicleControlMountingDriverSeatTest extends ActorTest {
"allow players to sit in the driver mount, even if it is locked, if the vehicle is unowned" in {
assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked
assert(vehicle.Seats(0).occupant.isEmpty)
assert(vehicle.Owner.isEmpty)
assert(vehicle.OwnerGuid.isEmpty)
vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref)
VehicleControlTest.checkCanMount(probe, "")
assert(vehicle.Seats(0).occupant.nonEmpty)
@ -397,7 +397,7 @@ class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest {
"block players that are not the current owner from sitting in the driver mount (locked)" in {
assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked
assert(vehicle.Seats(0).occupant.isEmpty)
vehicle.Owner = player1.GUID
vehicle.OwnerGuid = player1.GUID
vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref)
VehicleControlTest.checkCanMount(probe, "")
@ -434,7 +434,7 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest {
vehicle.PermissionGroup(0, 3) //passenger group -> empire
assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire
assert(vehicle.Seats(0).occupant.isEmpty)
vehicle.Owner = player1.GUID //owner set
vehicle.OwnerGuid = player1.GUID //owner set
vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref)
VehicleControlTest.checkCanMount(probe, "")

View file

@ -121,7 +121,7 @@ class VehicleTest extends Specification {
"construct (detailed)" in {
val fury_vehicle = Vehicle(GlobalDefinitions.fury)
fury_vehicle.Owner.isEmpty mustEqual true
fury_vehicle.OwnerGuid.isEmpty mustEqual true
fury_vehicle.Seats.size mustEqual 1
fury_vehicle.Seats(0).definition.restriction mustEqual NoMax
fury_vehicle.Seats(0).isOccupied mustEqual false
@ -148,30 +148,30 @@ class VehicleTest extends Specification {
"can be owned by a player" in {
val fury_vehicle = Vehicle(GlobalDefinitions.fury)
fury_vehicle.Owner.isDefined mustEqual false
fury_vehicle.OwnerGuid.isDefined mustEqual false
val player1 = Player(avatar1)
player1.GUID = PlanetSideGUID(1)
fury_vehicle.Owner = player1
fury_vehicle.Owner.isDefined mustEqual true
fury_vehicle.Owner.contains(PlanetSideGUID(1)) mustEqual true
fury_vehicle.OwnerGuid = player1
fury_vehicle.OwnerGuid.isDefined mustEqual true
fury_vehicle.OwnerGuid.contains(PlanetSideGUID(1)) mustEqual true
}
"ownership depends on who last was granted it" in {
val fury_vehicle = Vehicle(GlobalDefinitions.fury)
fury_vehicle.Owner.isDefined mustEqual false
fury_vehicle.OwnerGuid.isDefined mustEqual false
val player1 = Player(avatar1)
player1.GUID = PlanetSideGUID(1)
fury_vehicle.Owner = player1
fury_vehicle.Owner.isDefined mustEqual true
fury_vehicle.Owner.contains(PlanetSideGUID(1)) mustEqual true
fury_vehicle.OwnerGuid = player1
fury_vehicle.OwnerGuid.isDefined mustEqual true
fury_vehicle.OwnerGuid.contains(PlanetSideGUID(1)) mustEqual true
val player2 = Player(avatar2)
player2.GUID = PlanetSideGUID(2)
fury_vehicle.Owner = player2
fury_vehicle.Owner.isDefined mustEqual true
fury_vehicle.Owner.contains(PlanetSideGUID(2)) mustEqual true
fury_vehicle.OwnerGuid = player2
fury_vehicle.OwnerGuid.isDefined mustEqual true
fury_vehicle.OwnerGuid.contains(PlanetSideGUID(2)) mustEqual true
}
"can use mount point to get mount number" in {