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

View file

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

View file

@ -2,6 +2,9 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} 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.ExecutionContext.Implicits.global
import scala.concurrent.Future import scala.concurrent.Future
// //
@ -68,8 +71,8 @@ class SessionTerminalHandlers(
order match { order match {
case Terminal.BuyEquipment(item) case Terminal.BuyEquipment(item)
if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty => if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
lastTerminalOrderFulfillment = true
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
lastTerminalOrderFulfillment = true
case Terminal.BuyEquipment(item) => case Terminal.BuyEquipment(item) =>
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
@ -141,6 +144,7 @@ class SessionTerminalHandlers(
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid)) sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
} }
player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
}.orElse { }.orElse {
log.error( 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" 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, obj.GUID,
Deployable.Icon(obj.Definition.Item), Deployable.Icon(obj.Definition.Item),
obj.Position, obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0)) obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
) )
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
}) })
@ -2824,7 +2824,7 @@ class ZoningOperations(
continent.DeployableList continent.DeployableList
.filter(_.OwnerName.contains(name)) .filter(_.OwnerName.contains(name))
.foreach(obj => { .foreach(obj => {
obj.Owner = guid obj.OwnerGuid = guid
drawDeloyableIcon(obj) drawDeloyableIcon(obj)
}) })
drawDeloyableIcon = DontRedrawIcons drawDeloyableIcon = DontRedrawIcons
@ -2832,7 +2832,7 @@ class ZoningOperations(
//assert or transfer vehicle ownership //assert or transfer vehicle ownership
continent.GUID(player.avatar.vehicle) match { continent.GUID(player.avatar.vehicle) match {
case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) => case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
vehicle.Owner = guid vehicle.OwnerGuid = guid
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
s"${tplayer.Faction}", s"${tplayer.Faction}",
VehicleAction.Ownership(guid, vehicle.GUID) VehicleAction.Ownership(guid, vehicle.GUID)
@ -2907,7 +2907,7 @@ class ZoningOperations(
val effortBy = nextSpawnPoint val effortBy = nextSpawnPoint
.collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) } .collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) }
.collect { .collect {
case (_, Some(v: Vehicle)) => continent.GUID(v.Owner) case (_, Some(v: Vehicle)) => continent.GUID(v.OwnerGuid)
case (sp, Some(_: Building)) => Some(sp) case (sp, Some(_: Building)) => Some(sp)
} }
.collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) } .collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) }
@ -2962,7 +2962,7 @@ class ZoningOperations(
obj.GUID, obj.GUID,
Deployable.Icon(obj.Definition.Item), Deployable.Icon(obj.Definition.Item),
obj.Position, obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0)) obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
) )
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
} }

View file

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

View file

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

View file

@ -1,45 +1,33 @@
// Copyright (c) 2019 PSForever // Copyright (c) 2019 PSForever
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.types.PlanetSideGUID import net.psforever.types.PlanetSideGUID
trait OwnableByPlayer { trait OwnableByPlayer {
private var owner: Option[PlanetSideGUID] = None private var owner: Option[UniquePlayer] = None
private var ownerName: Option[String] = None private var ownerGuid: Option[PlanetSideGUID] = None
private var originalOwnerName: Option[String] = 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 { owner match {
case Some(_) => case Some(_) =>
this.owner = owner ownerGuid = owner
case None => case None =>
this.owner = None ownerGuid = None
} }
Owner OwnerGuid
} }
def OwnerName: Option[String] = ownerName def OwnerName: Option[String] = owner.map { _.name }
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 OriginalOwnerName: Option[String] = originalOwnerName def OriginalOwnerName: Option[String] = originalOwnerName
@ -56,14 +44,30 @@ trait OwnableByPlayer {
* @return na * @return na
*/ */
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = { def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
playerOpt match { (originalOwnerName, playerOpt) match {
case Some(player) => case (None, Some(player)) =>
Owner = player owner = Some(PlayerSource(player).unique)
OwnerName = player originalOwnerName = originalOwnerName.orElse { Some(player.Name) }
case None => OwnerGuid = player
Owner = None case (_, Some(player)) =>
OwnerName = None owner = Some(PlayerSource(player).unique)
OwnerGuid = player
case (_, None) =>
owner = None
OwnerGuid = None
} }
this 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 DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel]
def canEqual(other: Any): Boolean = other.isInstanceOf[Player] override def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
val earliestEndTime = System.currentTimeMillis() - duration
override def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = {
val start = ending - duration
History.collect { History.collect {
case heal: HealFromEquipment if heal.time <= ending && heal.time > start => heal case heal: HealFromEquipment if heal.amount > 0 && heal.time > earliestEndTime => heal
case repair: RepairFromEquipment if repair.time <= ending && repair.time > start => repair 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 = override def equals(other: Any): Boolean =
other match { other match {
case that: Player => case that: Player =>

View file

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

View file

@ -103,8 +103,7 @@ class TelepadDeployableControl(tpad: TelepadDeployable)
override def startOwnerlessDecay(): Unit = { override def startOwnerlessDecay(): Unit = {
//telepads do not decay when they become ownerless //telepads do not decay when they become ownerless
//telepad decay is tied to their lifecycle with routers //telepad decay is tied to their lifecycle with routers
tpad.Owner = None tpad.AssignOwnership(None)
tpad.OwnerName = None
} }
override def finalizeDeployable(callback: ActorRef): Unit = { 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)) { if (trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) {
groupPermissions(3) match { groupPermissions(3) match {
case VehicleLockState.Locked => //only the owner 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 case VehicleLockState.Group => //anyone in the owner's squad or platoon
faction == player.Faction //TODO this is not correct faction == player.Faction //TODO this is not correct
case VehicleLockState.Empire => //anyone of the owner's faction case VehicleLockState.Empire => //anyone of the owner's faction
@ -681,6 +681,6 @@ object Vehicle {
*/ */
def toString(obj: Vehicle): String = { def toString(obj: Vehicle): String = {
val occupancy = obj.Seats.values.count(seat => seat.isOccupied) 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 * `None`, otherwise
*/ */
def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] = def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] =
vehicle.Zone.GUID(vehicle.Owner) match { vehicle.Zone.GUID(vehicle.OwnerGuid) match {
case Some(player: Player) => case Some(player: Player) =>
if (player.avatar.vehicle.contains(guid)) { if (player.avatar.vehicle.contains(guid)) {
player.avatar.vehicle = None player.avatar.vehicle = None
@ -127,7 +127,7 @@ object Vehicles {
*/ */
def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = { def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = {
val pguid = player.GUID val pguid = player.GUID
if (vehicle.Owner.contains(pguid)) { if (vehicle.OwnerGuid.contains(pguid)) {
vehicle.AssignOwnership(None) vehicle.AssignOwnership(None)
//vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0))) //vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
val vguid = vehicle.GUID val vguid = vehicle.GUID
@ -272,7 +272,7 @@ object Vehicles {
} }
case _ => ; case _ => ;
} }
target.Owner match { target.OwnerGuid match {
case Some(previousOwnerGuid: PlanetSideGUID) => case Some(previousOwnerGuid: PlanetSideGUID) =>
// Remove ownership of the vehicle from the previous player // Remove ownership of the vehicle from the previous player
zone.GUID(previousOwnerGuid) match { 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.environment._
import net.psforever.objects.serverobject.repair.Repairable import net.psforever.objects.serverobject.repair.Repairable
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad 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.collision.CollisionReason
import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} 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) => 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 { order match {
case Terminal.BuyExosuit(exosuit, subtype) => case Terminal.BuyExosuit(exosuit, subtype) =>
val result = setExoSuit(exosuit, subtype) val result = setExoSuit(exosuit, subtype)
@ -366,6 +372,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Name, player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result) AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
) )
terminalUsedAction
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}") 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, player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result=true) 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, _, _) => case Zone.Ground.ItemOnGround(item, _, _) =>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -286,7 +286,7 @@ class VehicleControl(vehicle: Vehicle)
val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger) val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger)
val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire)
(if (seatGroup == AccessPermissionGroup.Driver) { (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 { } else {
permission != VehicleLockState.Locked permission != VehicleLockState.Locked
}) && }) &&
@ -348,7 +348,7 @@ class VehicleControl(vehicle: Vehicle)
//are we already decaying? are we unowned? is no one seated anywhere? //are we already decaying? are we unowned? is no one seated anywhere?
if (!decaying && if (!decaying &&
obj.Definition.undergoesDecay && obj.Definition.undergoesDecay &&
obj.Owner.isEmpty && obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) { obj.Seats.values.forall(!_.isOccupied)) {
decaying = true decaying = true
decayTimer = context.system.scheduler.scheduleOnce( decayTimer = context.system.scheduler.scheduleOnce(
@ -418,7 +418,7 @@ class VehicleControl(vehicle: Vehicle)
Vehicles.Disown(obj.GUID, obj) Vehicles.Disown(obj.GUID, obj)
if (!decaying && if (!decaying &&
obj.Definition.undergoesDecay && obj.Definition.undergoesDecay &&
obj.Owner.isEmpty && obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) { obj.Seats.values.forall(!_.isOccupied)) {
decaying = true decaying = true
decayTimer = context.system.scheduler.scheduleOnce( 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) final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
extends GeneralActivity 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 { extends GeneralActivity {
override val time: Long = entries.maxBy(_.time).time val start: Long = entries.last.time
val end: Long = entries.head.time
} }
/* vitals history */ /* vitals history */
@ -183,6 +187,9 @@ trait InGameHistory {
action match { action match {
case Some(act) => case Some(act) =>
history = history :+ act history = history :+ act
if (IsContributionEvent(act)) {
previousContributionInheritance = None
}
case None => () case None => ()
} }
history history
@ -206,7 +213,7 @@ trait InGameHistory {
LogActivity(DamageFromPainbox(result)) LogActivity(DamageFromPainbox(result))
case _: EnvironmentReason => case _: EnvironmentReason =>
LogActivity(DamageFromEnvironment(result)) LogActivity(DamageFromEnvironment(result))
case _ => ; case _ =>
LogActivity(DamageFrom(result)) LogActivity(DamageFrom(result))
if(result.adversarial.nonEmpty) { if(result.adversarial.nonEmpty) {
lastDamage = Some(result) lastDamage = Some(result)
@ -227,70 +234,79 @@ trait InGameHistory {
} }
} }
private val contributionInheritance: mutable.HashMap[SourceUniqueness, Seq[Contribution]] = private var previousContributionInheritance: Option[List[InGameActivity]] = None
mutable.HashMap[SourceUniqueness, Seq[Contribution]]()
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) { if (target ne this) {
val events = target.GetContribution() InGameHistory.ContributionFrom(target) match {
val nonEmpty = events.nonEmpty case out @ Some(in @ Contribution(src, _)) =>
if (nonEmpty) { contributionInheritance.put(src, in)
val source = SourceEntry(target) out
contributionInheritance.get(source.unique) match { case None =>
case Some(previousContributions) => contributionInheritance.remove(SourceEntry(target).unique)
val uniqueEvents = for { None
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)))
}
} }
nonEmpty
} else { } else {
false None
} }
} }
def RemoveContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Iterable[Contribution] = { def GetContribution(): Option[List[InGameActivity]] = {
contributionInheritance.remove(SourceEntry(target).unique).getOrElse(Nil) 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] = { def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
GetContributionDuringPeriod(System.currentTimeMillis(), duration = 600000) 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] = { def IsContributionEvent(event: InGameActivity): Boolean = {
GetContributionDuringPeriod(ending, duration = 600000) event match {
} case _: RepairingActivity => true
case _: DamagingActivity => true
def GetContributionDuringPeriod(ending: Long, duration: Long): List[InGameActivity] = { case _ => false
val start = ending - duration
History.collect { case repair: RepairFromEquipment
if repair.time <= ending && repair.time > start => repair
} }
} }
def HistoryAndContributions(): List[InGameActivity] = { def HistoryAndContributions(): List[InGameActivity] = {
val ending = System.currentTimeMillis() History ++ contributionInheritance.values.toList
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)
} }
def ClearHistory(): List[InGameActivity] = { def ClearHistory(): List[InGameActivity] = {
lastDamage = None lastDamage = None
val out = history val out = history
history = List.empty history = List.empty
previousContributionInheritance = None
contributionInheritance.clear()
out out
} }
} }
@ -318,4 +334,10 @@ object InGameHistory {
unit.foreach { o => obj.ContributionFrom(o) } 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.avatar.scoring.{Assist, Death, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult} 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.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
@ -13,6 +13,28 @@ import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
object KillAssists { 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( private[exp] def determineKiller(
lastDamageActivity: Option[DamageResult], lastDamageActivity: Option[DamageResult],
history: List[InGameActivity] history: List[InGameActivity]
@ -71,7 +93,7 @@ object KillAssists {
history: Iterable[InGameActivity], history: Iterable[InGameActivity],
eventBus: ActorRef eventBus: ActorRef
): Unit = { ): Unit = {
val shortHistory = Support.limitHistoryToThisLife(history.toList) val shortHistory = limitHistoryToThisLife(history.toList)
val everyone = determineKiller(lastDamage, shortHistory) match { val everyone = determineKiller(lastDamage, shortHistory) match {
case Some((result, killer: PlayerSource)) => case Some((result, killer: PlayerSource)) =>
val assists = collectKillAssistsForPlayer(victim, shortHistory, Some(killer)) 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 assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val total = assists.values.foldLeft(0f)(_ + _.total) val total = assists.values.foldLeft(0f)(_ + _.total)
val output = assists.map { case (id, kda) => 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.remove(victim.CharId)
output output
@ -214,7 +236,7 @@ object KillAssists {
case Some(mod) => case Some(mod) =>
//previous attacker, just add to entry //previous attacker, just add to entry
val firstWeapon = mod.weapons.head val firstWeapon = mod.weapons.head
val weapons = if (firstWeapon.weapon_id == wepid) { val weapons = if (firstWeapon.equipment_id == wepid) {
firstWeapon.copy( firstWeapon.copy(
amount = firstWeapon.amount + amount, amount = firstWeapon.amount + amount,
shots = firstWeapon.shots + 1, shots = firstWeapon.shots + 1,

View file

@ -2,29 +2,28 @@
package net.psforever.objects.zones.exp package net.psforever.objects.zones.exp
import akka.actor.ActorRef import akka.actor.ActorRef
import java.util.Date
import net.psforever.objects.avatar.scoring.{Assist, Kill} import net.psforever.objects.avatar.scoring.{Assist, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceUniqueness} import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.vital.{Contribution, DamagingActivity, HealFromEquipment, HealFromTerminal, HealingActivity, InGameActivity, RepairFromEquipment, RepairFromExoSuitChange, RepairFromTerminal, RepairingActivity, RevivingActivity, SupportActivityCausedByAnother} import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, VehicleSource}
import net.psforever.objects.vital.interaction.Adversarial 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.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
import org.joda.time.Instant
import org.joda.time.{LocalDateTime => JodaLocalDateTime}
import scala.collection.mutable import scala.collection.mutable
object KillContributions { object KillContributions {
private lazy val recoveryItems: Seq[Int] = { final val RecoveryItems: Seq[Int] = {
import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.GlobalDefinitions._
Seq( Seq(
bank.ObjectId, bank,
nano_dispenser.ObjectId, nano_dispenser,
medicalapplicator.ObjectId, medicalapplicator,
medical_terminal.ObjectId, medical_terminal,
adv_med_terminal.ObjectId adv_med_terminal,
) crystals_health_a,
crystals_health_b
).collect { _.ObjectId }
} }
private[exp] def rewardTheseSupporters( private[exp] def rewardTheseSupporters(
@ -34,465 +33,326 @@ object KillContributions {
bep: Long, bep: Long,
eventBus: ActorRef eventBus: ActorRef
): Unit = { ): Unit = {
val killTime = kill.time //setup
val killTime = kill.time.toDate.getTime
val faction = target.Faction val faction = target.Faction
val shortHistory = Support.limitHistoryToThisLife(history.toList) val limitedHistory = limitHistoryToThisLife(history.toList, killTime)
//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)
//divide by applicable time periods (long=10minutes, short=5minutes) //divide by applicable time periods (long=10minutes, short=5minutes)
val longEvents = findTimeApplicableActivities(allAssists.values.toSeq, killTime, timeOffset = 600) val shortPeriod = killTime - 300000L
val shortEvents = findTimeApplicableActivities(longEvents, killTime, timeOffset = 300) val (contributions, (longHistory, shortHistory)) = {
val victim = kill.victim val (contrib, onlyHistory) = limitedHistory.partition { _.isInstanceOf[Contribution] }
//convert to output format (
longEvents contrib
.map { stats => .collect { case Contribution(unique, entries) => (unique, entries) }
val (_, ContributionStatsOutput(p, w, r)) = mapContributionPointsByPercentage(total = 100f, shortEvents)(stats.player.CharId, stats) .toMap[SourceUniqueness, List[InGameActivity]],
(p, Assist(victim, w, r, (bep * r).toLong)) onlyHistory.partition { _.time > shortPeriod }
} )
.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
} }
} //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)(_, _)
private def healthRecoveryContributors( val finalContributions = if (longHistory.isEmpty) {
history: List[InGameActivity], val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
faction: PlanetSideEmpire.Value, contributionProcess.submit(shortHistory)
participants: mutable.LongMap[ContributionStats] val contributionEntries = otherContributionCalculations(shortHistory, contributionProcess.output())
): Seq[(Long, Int)] = { contributionEntries
/** damage as it is measured in order (with heal-countered damage eliminated)<br> .keys
* key - character identifier, .map { composeContributionOutput(_, contributionEntries, contributionEntries, bep) }
* 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))
} else { } 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, * Only historical activity that falls within the valid period matters.<br>
wepid: Int, * Unlike an expected case where the history would be bound by being spawned and being killed, respectively,
faction: PlanetSideEmpire.Value, * this imposes only the long contribution time limit on events since the latest entry;
amount: Int, * and, it may stop some time after the otherwise closest activity for being spawned.
time: Long, * @param history the original history
participants: mutable.LongMap[ContributionStats], * @param eventTime from which time to start counting backwards
damageOrder: Seq[(Long, Int)], * @return the potentially truncated history
recoveryOrder: Seq[(Long, Int)] */
): (Seq[(Long, Int)], Seq[(Long, Int)]) = { private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = {
contributeWithRecoveryActivity(user, user.CharId, wepid, faction, amount, time, participants, damageOrder, recoveryOrder) val longLimit: Long = eventTime - 600000L
history.collect {
case event if event.time < eventTime && event.time >= longLimit => event
}
} }
private def contributeWithRecoveryActivity( private def contributionScoringAndCulling(
wepid: Int, faction: PlanetSideEmpire.Value,
faction: PlanetSideEmpire.Value, kill: Kill,
amount: Int, excludeTargets: Seq[Long],
time: Long, contributions: Map[SourceUniqueness, List[InGameActivity]],
participants: mutable.LongMap[ContributionStats], bep: Long
damageOrder: Seq[(Long, Int)], )
recoveryOrder: Seq[(Long, Int)] (
): (Seq[(Long, Int)], Seq[(Long, Int)]) = { history: List[InGameActivity],
contributeWithRecoveryActivity(PlayerSource.Nobody, charId = 0, wepid, faction, amount, time, participants, damageOrder, recoveryOrder) 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( private def contributeWithKillWhileMountedActivity(
user: PlayerSource, faction: PlanetSideEmpire.Value,
charId: Long, kill: Kill,
wepid: Int, history: List[InGameActivity],
faction: PlanetSideEmpire.Value, participants: mutable.LongMap[ContributionStats]
amount: Int, ): List[InGameActivity] = {
time: Long, (kill
participants: mutable.LongMap[ContributionStats], .info
damageOrder: Seq[(Long, Int)], .interaction
recoveryOrder: Seq[(Long, Int)] .cause match {
): (Seq[(Long, Int)], Seq[(Long, Int)]) = { case p: ProjectileReason => p.projectile.mounted_in.map { _._2 }
//recovery calculations case _ => None
val newOrder = KillAssists.contributeWithRecoveryActivity(amount, participants, damageOrder) })
//detect potential instances of and remove approximate value for friendly-fire recovery (that doesn't count) .collect {
val qualityRecoveryAmount = { case mount: VehicleSource =>
val (entries, count) = { //repairs
val value = damageOrder.size - newOrder.size val contributions = history
if (value > 0) { .collect { case Contribution(unique, entries) if mount.unique == unique => (unique, entries) }
if (newOrder.head == damageOrder.lift(value).getOrElse(newOrder.head)) { .toMap[SourceUniqueness, List[InGameActivity]]
(damageOrder.take(value), value) contributions
} else { .foreach {
(damageOrder.take(value + 1), value + 1) case (_, localHistory) =>
} val process = new ArmorRecoveryExperienceContributionProcess(faction, contributions)
} else { process.submit(localHistory)
if (newOrder.headOption != damageOrder.headOption) { cullContributorImplements(process.output()).foreach {
(damageOrder.headOption.toList, 1) case (id, stat) =>
} else { val killExperience = kill.experienceEarned
(Nil, 1) 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) { history
//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)
} }
private def contributeWithSupportRecoveryActivity( private def contributeWithVehicleTransportActivity(
users: Seq[PlayerSource], history: List[InGameActivity],
wepid: Int, participants: mutable.LongMap[ContributionStats]
faction: PlanetSideEmpire.Value, ): List[InGameActivity] = {
amount: Int, val shortHistory = history.collect { case out: VehicleDismountActivity => out }
time: Long, shortHistory
participants: mutable.LongMap[ContributionStats], .groupBy { a => a.vehicle.owner }
damageOrder: Seq[(Long, Int)], .foreach { case (Some(owner), dismountsFromVehicle) =>
recoveryOrder: Seq[(Long, Int)] val numberOfDismounts = dismountsFromVehicle.size
): (Seq[(Long, Int)], Seq[(Long, Int)]) = { contributeWithCombinedActivity(
var outputDamageOrder = damageOrder owner.CharId,
var outputRecoveryOrder = recoveryOrder dismountsFromVehicle.map { act => WeaponStats(act.vehicle.Definition.ObjectId, 1, 1, act.time, 15f) },
users.zip { dismountsFromVehicle.head.vehicle.owner.get,
val numberOfUsers = users.size numberOfDismounts,
val out = Array.fill(numberOfUsers)(numberOfUsers / amount) numberOfDismounts,
(0 to numberOfUsers % amount).foreach { numberOfDismounts,
out(_) += 1 dismountsFromVehicle.maxBy(_.time).time,
participants
)
} }
out shortHistory
}.foreach { case (user, subAmount) =>
val (a, b) = contributeWithRecoveryActivity(user, user.CharId, wepid, faction, subAmount, time, participants, outputDamageOrder, outputRecoveryOrder)
outputDamageOrder = a
outputRecoveryOrder = b
}
(outputDamageOrder, outputRecoveryOrder)
} }
private def contributeWithRevivalActivity( private def contributeWithRevivalActivity(
history: List[InGameActivity], history: List[InGameActivity],
participants: mutable.LongMap[ContributionStats] participants: mutable.LongMap[ContributionStats]
): Unit = { ): List[InGameActivity] = {
history val shortHistory = history.collect { case rev: RevivingActivity => rev }
.collect { case rev: RevivingActivity => rev } shortHistory
.groupBy(_.user.CharId) .groupBy(_.user.CharId)
.foreach { case (id, revivesByThisPlayer) => .foreach { case (id, revivesByThisPlayer) =>
val numberOfRevives = revivesByThisPlayer.size val numberOfRevives = revivesByThisPlayer.size
val latestTime = revivesByThisPlayer.maxBy(_.time).time contributeWithCombinedActivity(
val newStats = revivesByThisPlayer.map { stat => WeaponStats(stat.equipment.ObjectId, 100, 1, stat.time, 0.25f) } id,
val newStat = WeaponStats(revivesByThisPlayer.head.equipment.ObjectId, 100 * numberOfRevives, numberOfRevives, latestTime, 0.25f) revivesByThisPlayer.map { stat => WeaponStats(stat.equipment.ObjectId, 100, 1, stat.time, 25f) },
participants.get(id) match { revivesByThisPlayer.head.user,
case Some(stats) => amount = 100 * numberOfRevives,
participants.put(id, stats.copy(weapons = stats.weapons ++ newStats)) total = 100 * numberOfRevives,
case None => numberOfRevives,
participants.put(id, ContributionStats( revivesByThisPlayer.maxBy(_.time).time,
revivesByThisPlayer.head.user, participants
newStats, )
newStat.amount,
newStat.shots,
revivesByThisPlayer.size,
latestTime
))
}
} }
shortHistory
} }
private def findTimeApplicableActivities( private def contributeWithTerminalActivity(
activity: Seq[ContributionStats], faction: PlanetSideEmpire.Value,
killLastTime: JodaLocalDateTime, history: List[InGameActivity],
timeOffset: Int //s contributions: Map[SourceUniqueness, List[InGameActivity]],
): Seq[ContributionStats] = { participants: mutable.LongMap[ContributionStats]
val dateTimeConverter: Date=>JodaLocalDateTime = JodaLocalDateTime.fromDateFields ): List[InGameActivity] = {
val milliToInstant: Long=>org.joda.time.Instant = Instant.ofEpochMilli contributeWithTerminalActivity(
val targetTime = killLastTime.minusSeconds(timeOffset) history.collect { case t: TerminalUsedActivity => (t, t.terminal, t.terminal.hacked) },
//find all activities that occurred before the kill time but after the offset faction,
activity.collect { entry => contributions,
val weapons = entry.weapons.filter { stat => participants
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 mapContributionPointsByPercentage( private[exp] def contributeWithTerminalActivity(
total: Float, data: Seq[(InGameActivity, AmenitySource, Option[HackInfo])],
compareList: Iterable[ContributionStats] faction: PlanetSideEmpire.Value,
) contributions: Map[SourceUniqueness, List[InGameActivity]],
( participants: mutable.LongMap[ContributionStats]
charId: Long, ): List[InGameActivity] = {
contribution: ContributionStats, data.collect {
): (Long, ContributionStatsOutput) = { case (t, terminal, Some(info)) if terminal.Faction != faction =>
val user = contribution.player participants.getOrElseUpdate(
val unique = user.unique info.player.CharId,
val points = contribution.amount ContributionStats(info.player, Seq(WeaponStats(0, 1, 1, t.time, 10f)), 1, 1, 1, t.time)
val value = if (points < 75) { )
//a small contribution means the lower time limit t
if (compareList.exists { a => a.player.unique == unique }) { case (t, terminal, _) =>
math.max(0.2f, points / total) extractContributionsForEntityByUser(faction, terminal, contributions, participants)
} else { t
0f }.toList
} }
} else {
//large contribution is always okay private def extractContributionsForEntityByUser(
if (points > 299) { faction: PlanetSideEmpire.Value,
1.0f target: SourceEntry,
} else if (points > 100) { contributions: Map[SourceUniqueness, List[InGameActivity]],
0.75f contributionsBy: mutable.LongMap[ContributionStats]
} else { ): List[InGameActivity] = {
0.5f 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 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( private case class WeaponStats(
weapon_id: Int, equipment_id: Int,
amount: Int, amount: Int,
shots: Int, shots: Int,
time: Long, time: Long,
contributions: Float contributions: Float
) ) extends ItemUseStats
private case class ContributionStats( private case class EquipmentStats(
player: PlayerSource, equipment_id: Int,
weapons: Seq[WeaponStats], shots: Int,
amount: Int, time: Long,
total: Int, contributions: Float
shots: Int, ) extends ItemUseStats
time: Long
) private[exp] case class ContributionStats(
player: PlayerSource,
weapons: Seq[WeaponStats],
amount: Int,
total: Int,
shots: Int,
time: Long
)
sealed case class ContributionStatsOutput( sealed case class ContributionStatsOutput(
player: PlayerSource, 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.hackable.Hackable
import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry} 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.objects.zones.Zone
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType} import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
import net.psforever.zones.Zones import net.psforever.zones.Zones
@ -18,28 +18,6 @@ import scala.collection.mutable
object Support { object Support {
private type SupportActivity = InGameActivity with SupportActivityCausedByAnother 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( private[exp] def onlyOriginalAssistEntries(
first: mutable.LongMap[ContributionStatsOutput], first: mutable.LongMap[ContributionStatsOutput],
second: mutable.LongMap[ContributionStatsOutput], second: mutable.LongMap[ContributionStatsOutput],
@ -349,7 +327,7 @@ object Support {
0.5f 0.5f
} }
} }
(charId, ContributionStatsOutput(user, contribution.weapons.map { _.weapon_id }, value)) (charId, ContributionStatsOutput(user, contribution.weapons.map { _.equipment_id }, value))
} }
//noinspection ScalaUnusedSymbol //noinspection ScalaUnusedSymbol
@ -361,7 +339,7 @@ object Support {
charId: Long, charId: Long,
contribution: ContributionStats, contribution: ContributionStats,
): (Long, ContributionStatsOutput) = { ): (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 = { 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.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90) obj.GUID = PlanetSideGUID(90)
obj.Router = PlanetSideGUID(1001) obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001) obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 1 obj.Health = 1
obj.Definition.Packet.ConstructorData(obj) match { obj.Definition.Packet.ConstructorData(obj) match {
case Success(pkt) => case Success(pkt) =>
@ -596,7 +596,7 @@ class ConverterTest extends Specification {
obj.Faction = PlanetSideEmpire.TR obj.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90) obj.GUID = PlanetSideGUID(90)
obj.Router = PlanetSideGUID(1001) obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001) obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 0 obj.Health = 0
obj.Definition.Packet.ConstructorData(obj) match { obj.Definition.Packet.ConstructorData(obj) match {
case Success(pkt) => case Success(pkt) =>
@ -628,7 +628,7 @@ class ConverterTest extends Specification {
obj.Faction = PlanetSideEmpire.TR obj.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90) obj.GUID = PlanetSideGUID(90)
//obj.Router = PlanetSideGUID(1001) //obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001) obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 1 obj.Health = 1
obj.Definition.Packet.ConstructorData(obj).isFailure mustEqual true obj.Definition.Packet.ConstructorData(obj).isFailure mustEqual true
@ -641,7 +641,7 @@ class ConverterTest extends Specification {
obj.Faction = PlanetSideEmpire.TR obj.Faction = PlanetSideEmpire.TR
obj.GUID = PlanetSideGUID(90) obj.GUID = PlanetSideGUID(90)
obj.Router = PlanetSideGUID(1001) obj.Router = PlanetSideGUID(1001)
obj.Owner = PlanetSideGUID(5001) obj.OwnerGuid = PlanetSideGUID(5001)
obj.Health = 1 obj.Health = 1
obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true obj.Definition.Packet.DetailedConstructorData(obj).isFailure mustEqual true
} }

View file

@ -34,16 +34,17 @@ class DeployableTest extends Specification {
"Deployable" should { "Deployable" should {
"know its owner by GUID" in { "know its owner by GUID" in {
val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine)
obj.Owner.isEmpty mustEqual true obj.OwnerGuid.isEmpty mustEqual true
obj.Owner = PlanetSideGUID(10) obj.OwnerGuid = PlanetSideGUID(10)
obj.Owner.contains(PlanetSideGUID(10)) mustEqual true obj.OwnerGuid.contains(PlanetSideGUID(10)) mustEqual true
} }
"know its owner by GUID" in { "know its owner by GUID" in {
val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine) // val obj = new ExplosiveDeployable(GlobalDefinitions.he_mine)
obj.OwnerName.isEmpty mustEqual true // obj.OwnerName.isEmpty mustEqual true
obj.OwnerName = "TestCharacter" // obj.OwnerName = "TestCharacter"
obj.OwnerName.contains("TestCharacter") mustEqual true // obj.OwnerName.contains("TestCharacter") mustEqual true
ko
} }
"know its faction allegiance" in { "know its faction allegiance" in {
@ -339,8 +340,8 @@ class ExplosiveDeployableJammerTest extends ActorTest {
guid.register(player2, 4) guid.register(player2, 4)
guid.register(weapon, 5) guid.register(weapon, 5)
j_mine.Zone = zone j_mine.Zone = zone
j_mine.Owner = player2 j_mine.OwnerGuid = player2
j_mine.OwnerName = player2.Name //j_mine.OwnerName = player2.Name
j_mine.Faction = PlanetSideEmpire.NC j_mine.Faction = PlanetSideEmpire.NC
j_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], j_mine), "j-mine-control") 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(player2, 4)
guid.register(weapon, 5) guid.register(weapon, 5)
h_mine.Zone = zone h_mine.Zone = zone
h_mine.Owner = player2 h_mine.OwnerGuid = player2
h_mine.OwnerName = player2.Name //h_mine.OwnerName = player2.Name
h_mine.Faction = PlanetSideEmpire.NC h_mine.Faction = PlanetSideEmpire.NC
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control") h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control")
zone.blockMap.addTo(player1) zone.blockMap.addTo(player1)
@ -528,8 +529,8 @@ class ExplosiveDeployableDestructionTest extends ActorTest {
guid.register(player2, 4) guid.register(player2, 4)
guid.register(weapon, 5) guid.register(weapon, 5)
h_mine.Zone = zone h_mine.Zone = zone
h_mine.Owner = player2 h_mine.OwnerGuid = player2
h_mine.OwnerName = player2.Name //h_mine.OwnerName = player2.Name
h_mine.Faction = PlanetSideEmpire.NC h_mine.Faction = PlanetSideEmpire.NC
h_mine.Actor = system.actorOf(Props(classOf[MineDeployableControl], h_mine), "h-mine-control") 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 // Reset to allow further driver mount mounting tests
vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref) vehicle.Actor.tell(Mountable.TryDismount(player1, 0), probe.ref)
probe.receiveOne(500 milliseconds) //discard probe.receiveOne(500 milliseconds) //discard
vehicle.Owner = None //ensure vehicle.OwnerGuid = None //ensure
vehicle.OwnerName = None //ensure vehicle.OwnerName = None //ensure
vehicle.Actor.tell(Mountable.TryMount(player3, 1), probe.ref) // Agile in driver mount allowing all except MAX 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") 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 { "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.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked
assert(vehicle.Seats(0).occupant.isEmpty) assert(vehicle.Seats(0).occupant.isEmpty)
assert(vehicle.Owner.isEmpty) assert(vehicle.OwnerGuid.isEmpty)
vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref)
VehicleControlTest.checkCanMount(probe, "") VehicleControlTest.checkCanMount(probe, "")
assert(vehicle.Seats(0).occupant.nonEmpty) 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 { "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.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked
assert(vehicle.Seats(0).occupant.isEmpty) assert(vehicle.Seats(0).occupant.isEmpty)
vehicle.Owner = player1.GUID vehicle.OwnerGuid = player1.GUID
vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref) vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref)
VehicleControlTest.checkCanMount(probe, "") VehicleControlTest.checkCanMount(probe, "")
@ -434,7 +434,7 @@ class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest {
vehicle.PermissionGroup(0, 3) //passenger group -> empire vehicle.PermissionGroup(0, 3) //passenger group -> empire
assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire
assert(vehicle.Seats(0).occupant.isEmpty) 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) vehicle.Actor.tell(Mountable.TryMount(player1, 1), probe.ref)
VehicleControlTest.checkCanMount(probe, "") VehicleControlTest.checkCanMount(probe, "")

View file

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