diff --git a/server/src/main/resources/db/migration/V010__Scoring2.sql b/server/src/main/resources/db/migration/V010__Scoring2.sql index a85aa1435..14f013560 100644 --- a/server/src/main/resources/db/migration/V010__Scoring2.sql +++ b/server/src/main/resources/db/migration/V010__Scoring2.sql @@ -1,6 +1,6 @@ /* changes to objects from V008__Scoring.sql */ ALTER TABLE killactivity -ADD COLUMN victim_mounted INT NO NULL DEFAULT 0; +ADD COLUMN victim_mounted INT NOT NULL DEFAULT 0; DROP PROCEDURE IF EXISTS proc_sessionnumber_initAndOrIncrease; @@ -254,7 +254,7 @@ END; $$ LANGUAGE plpgsql; /* new objects */ -CREATE OR REPLACE TABLE machinedestroyed ( +CREATE TABLE IF NOT EXISTS machinedestroyed ( "index" SERIAL PRIMARY KEY NOT NULL, "avatar_id" INT NOT NULL REFERENCES avatar (id), "weapon_id" INT NOT NULL, @@ -269,29 +269,44 @@ CREATE OR REPLACE TABLE machinedestroyed ( "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -/* -expectations: -heal player with medapp: no intermediate_type, implement_type is the medapp (no, yes) -revive player with medapp: intermediate_type is 121, implement_type is the medapp (yes, yes) -repair max with bank: no intermediate_type, implement_type is the bank (no, yes) -repair foo with gluegun: intermediate_type is the foo object type, implement_type is the gluegun (yes, yes) -bail from vehicle: intermediate_type is the vehicle object type, no implement_type (yes, no) -cargo from vehicle: intermediate_type is the vehicle object type, implement_type is the cargo vehicle object type (yes, yes) -damage from vehicle (collision): no intermediate_type, implement_type is vehicle object type (no, yes) -damage from vehicle (explosion): intermediate_type is vehicle object type, implement_type is intermediate_type (yes, yes) -always ignore target_id = user_id -*/ -CREATE OR REPLACE TABLE supportactivity ( +CREATE TABLE IF NOT EXISTS "assistactivity" ( + "index" SERIAL PRIMARY KEY NOT NULL, + "victim_id" INT NOT NULL REFERENCES avatar (id), + "attacker_id" INT NOT NULL REFERENCES avatar (id), + "weapon_id" SMALLINT NOT NULL, + "zone_id" SMALLINT NOT NULL, + "px" INT NOT NULL, + "py" INT NOT NULL, + "pz" INT NOT NULL, + "exp" INT NOT NULL, + "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS supportactivity ( "index" SERIAL PRIMARY KEY NOT NULL, "user_id" INT NOT NULL REFERENCES avatar (id), -- player that provides the support "target_id" INT NOT NULL REFERENCES avatar (id), -- benefactor of the support - "interaction_type" SMALLINT NOT NULL, -- description of support + "target_exosuit" SMALLINT NOT NULL, -- benefactor's exo-suit + "interaction_type" SMALLINT NOT NULL, -- classification of support "intermediate_type" INT DEFAULT 0, -- through what medium user_id supports target_id "implement_type" INT DEFAULT 0, -- tool utilized by user_id to support target_id, potentially via interaction with intermediate_type - "bep" INT NOT NULL, + "exp" INT NOT NULL, "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE IF NOT EXISTS respawnsession ( + "avatar_id" INT NOT NULL REFERENCES avatar (id), + "session_id" INT NOT NULL, + "respawn_count" INT NOT NULL DEFAULT 0, + UNIQUE(avatar_id, session_id) +); + +CREATE TABLE IF NOT EXISTS respawn ( + "avatar_id" INT NOT NULL REFERENCES avatar (id), + "respawn_count" INT NOT NULL DEFAULT 0, + UNIQUE(avatar_id) +); + /* Before inserting a value into the weaponstatsession table to session id -1, correct the session id to the most current session id, @@ -333,3 +348,33 @@ ON weaponstatsession FOR EACH ROW WHEN (NEW.session_id = -1) EXECUTE FUNCTION fn_weaponstatsession_beforeInsert(); + +/* +A kill assist activity causes a major update to weapon stats: +the weapon that was used in the activity has the kills count for the killer updated/increased. +*/ +CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats() +RETURNS TRIGGER +AS +$$ +DECLARE killerSessionId Int; +DECLARE killerId Int; +DECLARE weaponId Int; +BEGIN + killerId := NEW.killer_id; + weaponId := NEW.weapon_id; + SELECT proc_sessionnumber_get(killerId, killerSessionId); + BEGIN + UPDATE weaponstatsession + SET assists = assists + 1 + WHERE avatar_id = killerId AND session_id = killerSessionId AND weapon_id = weaponId; + END; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER psf_assistactivity_updateRelatedStats +AFTER INSERT +ON killactivity +FOR EACH ROW +EXECUTE FUNCTION fn_assistactivity_updateRelatedStats(); diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 1ba3637b6..b1ce43916 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -7,7 +7,7 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy} import java.util.concurrent.atomic.AtomicInteger 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, SupportActivity} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.sourcing.VehicleSource import net.psforever.objects.vital.InGameHistory @@ -843,6 +843,7 @@ object AvatarActor { def avatarNoLongerLoggedIn(accountId: Long): Unit = { import ctx._ + import scala.concurrent.ExecutionContext.Implicits.global //linter says unused but compiler says otherwise ctx.run( query[persistence.Account] .filter(_.id == lift(accountId)) @@ -1681,16 +1682,24 @@ class AvatarActor( updateToolDischarge(stats) Behaviors.same - case UpdateKillsDeathsAssists(stat) => - updateKillsDeathsAssists(stat) + case UpdateKillsDeathsAssists(stat: Kill) => + updateKills(stat) + Behaviors.same + + case UpdateKillsDeathsAssists(stat: Assist) => + updateAssists(stat) + Behaviors.same + + case UpdateKillsDeathsAssists(stat: Death) => + updateDeaths(stat) + Behaviors.same + + case UpdateKillsDeathsAssists(stat: SupportActivity) => + updateSupport(stat) Behaviors.same case AwardBep(bep, ExperienceType.Support) => - supportExperiencePool = supportExperiencePool + bep - avatar.scorecard.rate(bep) - if (supportExperienceTimer.isCancelled) { - resetSupportExperienceTimer(previousBep = 0, previousDelay = 0) - } + awardSupportExperience(bep, previousDelay = 0L) Behaviors.same case AwardBep(bep, modifier) => @@ -1877,7 +1886,8 @@ class AvatarActor( performAvatarLogin(avatarId, accountId, replyTo) case _ => //refuse - //TODO refuse + //TODO refuse? + sessionActor ! SessionActor.Quit() } } @@ -3011,63 +3021,114 @@ class AvatarActor( } } - def updateKillsDeathsAssists(kdaStat: KDAStat): Unit = { - avatar.scorecard.rate(kdaStat) - val exp = kdaStat.experienceEarned + def awardSupportExperience(bep: Long, previousDelay: Long): Unit = { + supportExperiencePool = supportExperiencePool + bep + avatar.scorecard.rate(bep) + if (supportExperienceTimer.isCancelled) { + resetSupportExperienceTimer(previousBep = 0, previousDelay = 0) + } + } + + def updateKills(killStat: Kill): Unit = { + val exp = killStat.experienceEarned + val (modifiedExp, msg) = updateExperienceAndType(killStat.experienceEarned) + avatar.scorecard.rate(killStat.copy(experienceEarned = modifiedExp)) + val _session = session.get + val zone = _session.zone + val player = _session.player + val playerSource = PlayerSource(player) + val historyTranscript = (killStat.info.interaction.cause match { + case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile + case _ => None + }) match { + case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons) => + player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount).toList + case _ => + player.HistoryAndContributions() + } + zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, killStat, exp) + val target = killStat.info.targetAfter.asInstanceOf[PlayerSource] + val targetMounted = target.seatedIn.map { case (v: VehicleSource, seat) => + val definition = v.Definition + definition.ObjectId * 10 + Vehicles.SeatPermissionGroup(definition, seat).map { _.id }.getOrElse(0) + }.getOrElse(0) + zones.exp.ToDatabase.reportKillBy( + avatar.id.toLong, + target.CharId, + target.ExoSuit.id, + targetMounted, + killStat.info.interaction.cause.attribution, + player.Zone.Number, + target.Position, + modifiedExp + ) + if (exp > 0L) { + setBep(avatar.bep + exp, msg) + } + } + + def updateDeaths(deathStat: Death): Unit = { + avatar.scorecard.rate(deathStat) val _session = session.get val zone = _session.zone val player = _session.player - kdaStat match { - case kill: Kill => - val playerSource = PlayerSource(player) - val historyTranscript = (kill.info.interaction.cause match { - case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile - case _ => None - }) match { - case Some(mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons) => - player.HistoryAndContributions() ++ InGameHistory.ContributionFrom(mount).toList - case _ => - player.HistoryAndContributions() - } - zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, kill, exp) - val target = kill.info.targetAfter.asInstanceOf[PlayerSource] - val targetMounted = target.seatedIn.map { case (v: VehicleSource, seat) => - val definition = v.Definition - definition.ObjectId * 10 + Vehicles.SeatPermissionGroup(definition, seat).map { _.id }.getOrElse(0) - }.getOrElse(0) - zones.exp.ToDatabase.reportKillBy( - avatar.id.toLong, - target.CharId, - target.ExoSuit.id, - targetMounted, - kill.info.interaction.cause.attribution, - player.Zone.Number, - target.Position, - kill.experienceEarned - ) - case assist: Assist => - val avatarId = avatar.id.toLong - assist.weapons.foreach { - zones.exp.ToDatabase.reportAssistKills(avatarId, _, assists = 1) - } - case _: Death => - zone.AvatarEvents ! AvatarServiceMessage( - player.Name, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - AvatarStatisticsMessage(DeathStatistic(avatar.scorecard.Lives.size)) - ) - ) + zone.AvatarEvents ! AvatarServiceMessage( + player.Name, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + AvatarStatisticsMessage(DeathStatistic(avatar.scorecard.Lives.size)) + ) + ) + } + + def updateAssists(assistStat: Assist): Unit = { + avatar.scorecard.rate(assistStat) + val exp = assistStat.experienceEarned + val _session = session.get + val avatarId = avatar.id.toLong + assistStat.weapons.foreach { wrapper => + zones.exp.ToDatabase.reportKillAssistBy( + avatarId, + assistStat.victim.CharId, + wrapper.equipment, + _session.zone.Number, + assistStat.victim.Position, + exp + ) } - if (exp > 0L) { - val gameOpts = Config.app.game - val (msg, modifier): (ExperienceType, Float) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) { - (ExperienceType.RabbitBall, 1.25f) - } else { - (ExperienceType.Normal, 1f) - } - setBep(avatar.bep + (exp * modifier * gameOpts.bepRate).toLong, msg) + awardSupportExperience(exp, previousDelay = 0L) + } + + def updateSupport(supportStat: SupportActivity): Unit = { + val avatarId = avatar.id.toLong + val target = supportStat.target + val targetId = target.CharId + val targetExosuit = target.ExoSuit.id + val exp = supportStat.experienceEarned + supportStat.weapons.foreach { entry => + zones.exp.ToDatabase.reportSupportBy( + avatarId, + targetId, + targetExosuit, + entry.value, + entry.intermediate, + entry.equipment, + exp + ) } + awardSupportExperience(exp, previousDelay = 0L) + } + + def updateExperienceAndType(exp: Long): (Long, ExperienceType) = { + val _session = session.get + val player = _session.player + val gameOpts = Config.app.game + val (modifier, msg) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) { + (1.25f, ExperienceType.RabbitBall) + } else { + (1f, ExperienceType.Normal) + } + ((exp * modifier * gameOpts.bepRate).toLong, msg) } def updateToolDischarge(stats: EquipmentStat): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala index d1584ff42..279bc78d0 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -282,7 +282,6 @@ class SessionMountHandlers( def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { val playerGuid: PlanetSideGUID = tplayer.GUID val objGuid: PlanetSideGUID = obj.GUID - tplayer.ContributionFrom(obj) sessionData.playerActionsToCancel() avatarActor ! AvatarActor.DeactivateActiveImplants() avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/KDAStat.scala b/src/main/scala/net/psforever/objects/avatar/scoring/KDAStat.scala index cf4117bfd..fffa0b7d9 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/KDAStat.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/KDAStat.scala @@ -3,6 +3,7 @@ package net.psforever.objects.avatar.scoring import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.zones.exp.EquipmentUseContextWrapper import org.joda.time.LocalDateTime trait KDAStat { @@ -10,10 +11,29 @@ trait KDAStat { val time: LocalDateTime = LocalDateTime.now() } -final case class Kill(victim: PlayerSource, info: DamageResult, experienceEarned: Long) extends KDAStat +final case class Kill( + victim: PlayerSource, + info: DamageResult, + experienceEarned: Long + ) extends KDAStat -final case class Assist(victim: PlayerSource, weapons: Seq[Int], damageInflictedPercentage: Float, experienceEarned: Long) extends KDAStat +final case class Assist( + victim: PlayerSource, + weapons: Seq[EquipmentUseContextWrapper], + damageInflictedPercentage: Float, + experienceEarned: Long + ) extends KDAStat -final case class Death(assailant: Seq[PlayerSource], timeAlive: Long, bep: Long) extends KDAStat { +final case class Death( + assailant: Seq[PlayerSource], + timeAlive: Long, + bep: Long + ) extends KDAStat { def experienceEarned: Long = 0 } + +final case class SupportActivity( + target: PlayerSource, + weapons: Seq[EquipmentUseContextWrapper], + experienceEarned: Long + ) extends KDAStat diff --git a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala index 456362a07..8a0a29407 100644 --- a/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala +++ b/src/main/scala/net/psforever/objects/avatar/scoring/ScoreCard.scala @@ -36,7 +36,7 @@ class ScoreCard() { curr = curr.copy(assists = a +: curr.assists) val faction = a.victim.Faction a.weapons.foreach { wid => - ScoreCard.updateStatisticsFor(assistStatistics, wid, faction) + ScoreCard.updateStatisticsFor(assistStatistics, wid.equipment, faction) } case d: Death => val expired = curr diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index 96bca9257..0758f502f 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -7,8 +7,8 @@ import net.psforever.actors.zone.BuildingActor import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.transfer.TransferBehavior import net.psforever.objects.serverobject.structures.Building -import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior} -import net.psforever.types.PlanetSideEmpire +import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior, Vehicle} +import net.psforever.types.{ExperienceType, PlanetSideEmpire} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -181,6 +181,19 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) if (amount != 0) { panelAnimationFunc(sender, amount) panelAnimationFunc = SkipPanelAnimation + (src match { + case v: Vehicle => Some(v) + case _ => None + }) + .map { v => (v, v.Owners) } + .collect { case (vehicle, Some(owner)) => + //experience is reported as normal + val deposit: Long = 100L * math.floor(amount).toLong / math.floor(resourceSilo.MaxNtuCapacitor / 105f).toLong + vehicle.Zone.AvatarEvents ! AvatarServiceMessage( + owner.name, + AvatarAction.AwardBep(0, deposit, ExperienceType.Normal) + ) + } } } @@ -192,6 +205,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) * @param trigger if positive, activate the animation; * if negative or zero, disable the animation */ + //noinspection ScalaUnusedSymbol def PanelAnimation(source: ActorRef, trigger: Float): Unit = { val currentlyHas = resourceSilo.NtuCapacitor // do not let the trigger charge go to waste, but also do not let the silo be filled diff --git a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala index 9b0cb0f0f..b6e40a1b5 100644 --- a/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/AntTransferBehavior.scala @@ -9,12 +9,11 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.transfer.{TransferBehavior, TransferContainer} import net.psforever.objects.{NtuContainer, _} -import net.psforever.types.{DriveState, ExperienceType} +import net.psforever.types.DriveState import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import akka.actor.typed.scaladsl.adapter._ import net.psforever.objects.serverobject.transfer.TransferContainer.TransferMaterial -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -140,10 +139,6 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior { chargeable.NtuCapacitor -= chargeToDeposit UpdateNtuUI(chargeable) Ntu.Grant(chargeable, chargeToDeposit) - vehicle.Zone.AvatarEvents ! AvatarServiceMessage( - vehicle.OwnerName.getOrElse(""), - AvatarAction.AwardBep(0, 100L, ExperienceType.Normal) - ) } /** Stopping */ diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index 2ba1f801d..320415bd8 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -77,8 +77,8 @@ final case class VehicleCargoDismountActivity(vehicle: VehicleSource, cargo: Veh final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity]) extends GeneralActivity { - val start: Long = entries.last.time - val end: Long = entries.head.time + val start: Long = entries.headOption.map { _.time }.getOrElse(System.currentTimeMillis()) + val end: Long = entries.lastOption.map { _.time }.getOrElse(start) } /* vitals history */ @@ -261,17 +261,18 @@ trait InGameHistory { mutable.HashMap[SourceUniqueness, Contribution]() def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = { - if (target ne this) { - InGameHistory.ContributionFrom(target) match { - case out @ Some(in @ Contribution(src, _)) => - contributionInheritance.put(src, in) - out + if (target eq this) { + None + } else { + target.GetContribution() match { + case Some(in) => + val contribution = Contribution(SourceEntry(target).unique, in) + contributionInheritance.put(contribution.src, contribution) + Some(contribution) case None => contributionInheritance.remove(SourceEntry(target).unique) None } - } else { - None } } @@ -307,12 +308,11 @@ object InGameHistory { unit: Option[PlanetSideGameObject with FactionAffinity with InGameHistory] ): Unit = { val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) } - val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists { - _.isInstanceOf[SpawningActivity] - }) { - ReconstructionActivity(ObjectSource(obj), zoneNumber, toUnitSource) - } else { + + val event: GeneralActivity = if (obj.History.isEmpty) { SpawningActivity(ObjectSource(obj), zoneNumber, toUnitSource) + } else { + ReconstructionActivity(ObjectSource(obj), zoneNumber, toUnitSource) } if (obj.History.lastOption match { case Some(evt: SpawningActivity) => evt != event diff --git a/src/main/scala/net/psforever/objects/zones/exp/EquipmentUseContextWrapper.scala b/src/main/scala/net/psforever/objects/zones/exp/EquipmentUseContextWrapper.scala new file mode 100644 index 000000000..cb7bedfce --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/EquipmentUseContextWrapper.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp + +import enumeratum.values.IntEnumEntry + +sealed abstract class EquipmentUseContextWrapper(val value: Int) extends IntEnumEntry { + def equipment: Int + def intermediate: Int = 0 +} + +final case class NoUse(equipment: Int) extends EquipmentUseContextWrapper(value = -1) + +final case class DamageWith(equipment: Int) extends EquipmentUseContextWrapper(value = 0) + +final case class Destroyed(equipment: Int) extends EquipmentUseContextWrapper(value = 1) +final case class ReviveAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 4) +final case class AmenityDestroyed(equipment: Int) extends EquipmentUseContextWrapper(value = 10) +final case class DriverKilled(equipment: Int) extends EquipmentUseContextWrapper(value = 12) +final case class GunnerKilled(equipment: Int) extends EquipmentUseContextWrapper(value = 13) +final case class PassengerKilled(equipment: Int) extends EquipmentUseContextWrapper(value = 14) +final case class CargoDestroyed(equipment: Int) extends EquipmentUseContextWrapper(value = 15) +final case class DriverAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 18) +final case class HealKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 20) +final case class ReviveKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 21) +final case class RepairKillAssist(equipment: Int, override val intermediate: Int) extends EquipmentUseContextWrapper(value = 22) +final case class AmsRespawnKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 23) +final case class HotDropKillAssist(equipment: Int, override val intermediate: Int) extends EquipmentUseContextWrapper(value = 24) +final case class HackKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 25) +final case class LodestarRearmKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 26) +final case class AmsResupplyKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 27) +final case class RouterKillAssist(equipment: Int) extends EquipmentUseContextWrapper(value = 28) diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala index 028d1cf82..4ba367fc3 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillAssists.scala @@ -23,7 +23,7 @@ object KillAssists { case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0 case _ => false } - if (spawnIndex == -1 || endIndex == -1) { + if (spawnIndex == -1 || endIndex == -1 || spawnIndex > endIndex) { Nil } else { history.slice(spawnIndex, endIndex) @@ -193,7 +193,7 @@ object KillAssists { val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 } val total = assists.values.foldLeft(0f)(_ + _.total) val output = assists.map { case (id, kda) => - (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.equipment_id }, kda.amount / total)) + (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.equipment }, kda.amount / total)) } output.remove(victim.CharId) output @@ -277,7 +277,8 @@ object KillAssists { case Some(mod) => //previous attacker, just add to entry val firstWeapon = mod.weapons.head - val weapons = if (firstWeapon.equipment_id == wepid) { + val newEntry = DamageWith(wepid) + val weapons = if (firstWeapon.equipment == newEntry) { firstWeapon.copy( amount = firstWeapon.amount + amount, shots = firstWeapon.shots + 1, @@ -285,7 +286,7 @@ object KillAssists { contributions = firstWeapon.contributions + percentage ) +: mod.weapons.tail } else { - WeaponStats(wepid, amount, 1, time, percentage) +: mod.weapons + WeaponStats(newEntry, amount, 1, time, percentage) +: mod.weapons } mod.copy( amount = mod.amount + amount, @@ -298,7 +299,7 @@ object KillAssists { //new attacker, new entry ContributionStats( user, - Seq(WeaponStats(wepid, amount, 1, time, percentage)), + Seq(WeaponStats(DamageWith(wepid), amount, 1, time, percentage)), amount, amount, 1, diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala index 2e736a6b1..748c3f520 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala @@ -3,11 +3,12 @@ package net.psforever.objects.zones.exp import akka.actor.ActorRef import net.psforever.objects.GlobalDefinitions -import net.psforever.objects.avatar.scoring.{Assist, Kill} +import net.psforever.objects.avatar.scoring.{Kill, SupportActivity} import net.psforever.objects.serverobject.hackable.Hackable.HackInfo import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, VehicleSource} import net.psforever.objects.vital.{Contribution, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleCargoMountChange, VehicleDismountActivity, VehicleMountActivity, VehiclePassengerMountChange} import net.psforever.objects.vital.projectile.ProjectileReason +import net.psforever.objects.zones.exp.rec.{ArmorRecoveryExperienceContributionProcess, CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.{PlanetSideEmpire, Vector3} @@ -20,15 +21,16 @@ object KillContributions { bank, nano_dispenser, medicalapplicator, + order_terminal, + order_terminala, + order_terminalb, medical_terminal, adv_med_terminal, - crystals_health_a, - crystals_health_b, bfr_rearm_terminal, multivehicle_rearm_terminal, lodestar_repair_terminal ).collect { _.ObjectId } - } + } //TODO currently includes things that are not typical items but are used for expressing contribution implements private[exp] def rewardTheseSupporters( target: PlayerSource, @@ -40,27 +42,27 @@ object KillContributions { //setup val killTime = kill.time.toDate.getTime val faction = target.Faction - val limitedHistory = limitHistoryToThisLife(history.toList, killTime) //divide by applicable time periods (long=10minutes, short=5minutes) val shortPeriod = killTime - 300000L val (contributions, (longHistory, shortHistory)) = { - val (contrib, onlyHistory) = limitedHistory.partition { _.isInstanceOf[Contribution] } + val (contrib, onlyHistory) = history.partition { _.isInstanceOf[Contribution] } ( contrib .collect { case Contribution(unique, entries) => (unique, entries) } .toMap[SourceUniqueness, List[InGameActivity]], - onlyHistory.partition { _.time > shortPeriod } + limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time > shortPeriod } ) } //sort by applicable time periods, as long as the longer period is represented by activity - val otherContributionCalculations = contributionScoringAndCulling(faction, kill, contributions, bep)(_, _) + val empty = mutable.ListBuffer[SourceUniqueness]() + val otherContributionCalculations = contributionScoringAndCulling(faction, kill, contributions, bep)(_, _, _) val finalContributions = if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 2) { 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()) + val longContributionEntries = otherContributionCalculations(longHistory, longContributionProcess.output(), empty) + val shortContributionEntries = otherContributionCalculations(shortHistory, shortContributionProcess.output(), empty) longContributionEntries.remove(target.CharId) longContributionEntries.remove(kill.victim.CharId) shortContributionEntries.remove(target.CharId) @@ -74,7 +76,7 @@ object KillContributions { } else { val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil) contributionProcess.submit(shortHistory) - val contributionEntries = otherContributionCalculations(shortHistory, contributionProcess.output()) + val contributionEntries = otherContributionCalculations(shortHistory, contributionProcess.output(), empty) contributionEntries.remove(target.CharId) contributionEntries.remove(kill.victim.CharId) contributionEntries @@ -87,7 +89,7 @@ object KillContributions { finalContributions.foreach { case (charId, ContributionStatsOutput(player, weapons, exp)) => eventBus ! AvatarServiceMessage( player.Name, - AvatarAction.UpdateKillsDeathsAssists(charId, Assist(victim, weapons, 1f, exp.toLong)) + AvatarAction.UpdateKillsDeathsAssists(charId, SupportActivity(victim, weapons, exp.toLong)) ) } } @@ -116,7 +118,8 @@ object KillContributions { ) ( history: List[InGameActivity], - contributionEntries: mutable.LongMap[ContributionStats] + contributionEntries: mutable.LongMap[ContributionStats], + excludedTargets: mutable.ListBuffer[SourceUniqueness] ): mutable.LongMap[ContributionStats] = { contributionEntries.map { case (id, stat) => val newWeaponStats = stat.weapons.map { weaponStat => @@ -124,11 +127,11 @@ object KillContributions { } contributionEntries.put(id, stat.copy(weapons = newWeaponStats)) } - contributeWithKillWhileMountedActivity(faction, kill, history, contributionEntries) + contributeWithKillWhileMountedActivity(faction, kill, history, contributionEntries, excludedTargets) contributeWithRevivalActivity(history, contributionEntries) contributeWithVehicleTransportActivity(history, contributionEntries) contributeWithVehicleCargoTransportActivity(history, contributionEntries) - contributeWithTerminalActivity(faction, history, contributions, contributionEntries) + contributeWithTerminalActivity(faction, history, contributions, contributionEntries, excludedTargets) contributionEntries.remove(0) contributionEntries } @@ -137,7 +140,8 @@ object KillContributions { faction: PlanetSideEmpire.Value, kill: Kill, history: List[InGameActivity], - participants: mutable.LongMap[ContributionStats] + participants: mutable.LongMap[ContributionStats], + excludedTargets: mutable.ListBuffer[SourceUniqueness] ): List[InGameActivity] = { (kill .info @@ -147,7 +151,7 @@ object KillContributions { case _ => None }) .collect { - case mount: VehicleSource => + case mount: VehicleSource if !excludedTargets.contains(mount.unique) => //repairs val contributions = history .collect { case Contribution(unique, entries) if mount.unique == unique => (unique, entries) } @@ -155,7 +159,7 @@ object KillContributions { contributions .foreach { case (_, localHistory) => - val process = new ArmorRecoveryExperienceContributionProcess(faction, contributions) + val process = new ArmorRecoveryExperienceContributionProcess(faction, contributions, excludedTargets :+ mount.unique) process.submit(localHistory) cullContributorImplements(process.output()).foreach { case (id, stat) => @@ -179,14 +183,21 @@ object KillContributions { } } //vehicle owner - extractContributionsForEntityByUser(faction, mount, contributions, participants) + extractContributionsForEntityByUser(faction, mount, contributions, participants, excludedTargets) 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) + ContributionStats( + owner, + Seq(WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 10f)), + 1, + 1, + 1, + time + ) ) } } @@ -236,7 +247,15 @@ object KillContributions { val numberOfDismounts = dismountsFromVehicle.size contributeWithCombinedActivity( owner.CharId, - dismountsFromVehicle.map { act => WeaponStats(act.vehicle.Definition.ObjectId, 0, numberOfDismounts, act.time, 15f) }, + dismountsFromVehicle.map { act => + val statContext = act.vehicle.Definition match { + case v @ GlobalDefinitions.router => + RouterKillAssist(v.ObjectId) + case v => + HotDropKillAssist(v.ObjectId, 0) + } + WeaponStats(statContext, 0, numberOfDismounts, act.time, 15f) + }, dismountsFromVehicle.head.vehicle.owner.get, amount = 0, total = 0, @@ -280,7 +299,15 @@ object KillContributions { val numberOfDismounts = dismountsFromVehicle.size contributeWithCombinedActivity( owner.CharId, - dismountsFromVehicle.map { act => WeaponStats(act.vehicle.Definition.ObjectId, 0, numberOfDismounts, act.time, 15f) }, + dismountsFromVehicle.map { act => + WeaponStats( + HotDropKillAssist(act.vehicle.Definition.ObjectId, act.cargo.Definition.ObjectId), + 0, + numberOfDismounts, + act.time, + 15f + ) + }, dismountsFromVehicle.head.vehicle.owner.get, amount = 0, total = 0, @@ -303,7 +330,9 @@ object KillContributions { val numberOfRevives = revivesByThisPlayer.size contributeWithCombinedActivity( id, - revivesByThisPlayer.map { stat => WeaponStats(stat.equipment.ObjectId, 100, 1, stat.time, 25f) }, + revivesByThisPlayer.map { stat => + WeaponStats(ReviveKillAssist(stat.equipment.ObjectId), 100, 1, stat.time, 25f) + }, revivesByThisPlayer.head.user, amount = 100 * numberOfRevives, total = 100 * numberOfRevives, @@ -319,41 +348,83 @@ object KillContributions { faction: PlanetSideEmpire.Value, history: List[InGameActivity], contributions: Map[SourceUniqueness, List[InGameActivity]], - participants: mutable.LongMap[ContributionStats] + participants: mutable.LongMap[ContributionStats], + excludedTargets: mutable.ListBuffer[SourceUniqueness] ): List[InGameActivity] = { contributeWithTerminalActivity( history.collect { case t: TerminalUsedActivity => (t, t.terminal, t.terminal.hacked) }, faction, contributions, - participants + participants, + excludedTargets ) } private[exp] def contributeWithTerminalActivity( data: Seq[(InGameActivity, AmenitySource, Option[HackInfo])], faction: PlanetSideEmpire.Value, - contributions: Map[SourceUniqueness, List[InGameActivity]], - participants: mutable.LongMap[ContributionStats] + contributions: Map[SourceUniqueness, List[InGameActivity]], + participants: mutable.LongMap[ContributionStats], + excludedTargets: mutable.ListBuffer[SourceUniqueness] ): List[InGameActivity] = { data.collect { case (t, terminal, Some(info)) if terminal.Faction != faction => + /* + if the terminal has been hacked, + and the original terminal does not align with our own faction, + then the support must be reported as a hack; + if we are the same faction as the terminal, then the hacked condition is irrelevant + */ participants.getOrElseUpdate( info.player.CharId, - ContributionStats(info.player, Seq(WeaponStats(terminal.Definition.ObjectId, 0, 1, t.time, 10f)), 0, 0, 1, t.time) + ContributionStats( + info.player, + Seq(WeaponStats(HackKillAssist(terminal.Definition.ObjectId), 0, 1, t.time, 10f)), + 0, + 0, + 1, + t.time + ) ) t case (t, terminal, _) => - terminal.installation match { + val (equipmentUseContext, ownerOpt) = terminal.installation match { case v: VehicleSource => - v.owner.collect { - owner => - participants.getOrElseUpdate( - owner.CharId, - ContributionStats(owner, Seq(WeaponStats(terminal.Definition.ObjectId, 0, 1, t.time, 10f)), 0, 0, 1, t.time) - ) + terminal.Definition match { + case GlobalDefinitions.order_terminala => + extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets) + (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner) + case GlobalDefinitions.order_terminalb => + extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets) + (AmsResupplyKillAssist(terminal.Definition.ObjectId), v.owner) + case GlobalDefinitions.lodestar_repair_terminal => + extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets) + (RepairKillAssist(terminal.Definition.ObjectId, v.Definition.ObjectId), v.owner) + case GlobalDefinitions.bfr_rearm_terminal => + extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets) + (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner) + case GlobalDefinitions.multivehicle_rearm_terminal => + extractContributionsForEntityByUser(faction, v, contributions, participants, excludedTargets) + (LodestarRearmKillAssist(terminal.Definition.ObjectId), v.owner) + case _ => + (NoUse(0), None) } case _ => - extractContributionsForEntityByUser(faction, terminal, contributions, participants) + (NoUse(0), None) + } + ownerOpt.collect { + owner => + participants.getOrElseUpdate( + owner.CharId, + ContributionStats( + owner, + Seq(WeaponStats(equipmentUseContext, 0, 1, t.time, 10f)), + 0, + 0, + 1, + t.time + ) + ) } t }.toList @@ -363,22 +434,27 @@ object KillContributions { faction: PlanetSideEmpire.Value, target: SourceEntry, contributions: Map[SourceUniqueness, List[InGameActivity]], - contributionsBy: mutable.LongMap[ContributionStats] + contributionsBy: mutable.LongMap[ContributionStats], + excludedTargets: mutable.ListBuffer[SourceUniqueness] ): List[InGameActivity] = { - val shortHistory = contributions.getOrElse(target.unique, Nil) - val process = new ArmorRecoveryExperienceContributionProcess(faction, contributions) - process.submit(shortHistory) - cullContributorImplements(process.output()).foreach { - case (id, stats) => contributionsBy.getOrElseUpdate(id, stats) + if (!excludedTargets.contains(target.unique)) { + val shortHistory = contributions.getOrElse(target.unique, Nil) + val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, excludedTargets :+ target.unique) + process.submit(shortHistory) + cullContributorImplements(process.output()).foreach { + case (id, stats) => contributionsBy.getOrElseUpdate(id, stats) + } + shortHistory + } else { + Nil } - 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) })) + (id, entry.copy(weapons = entry.weapons.filter { stats => KillContributions.RecoveryItems.contains(stats.equipment.equipment) })) //TODO bad test; fix later }.filter { case (_, entry) => entry.weapons.nonEmpty } @@ -398,7 +474,7 @@ object KillContributions { case Some(stats) => val oldWeapons = stats.weapons val newWeapons = newStats.map { weapon => - oldWeapons.find { _.equipment_id == weapon.equipment_id } match { + oldWeapons.find { _.equipment == weapon.equipment } match { case Some(foundEntry) => weapon.copy( amount = weapon.amount + foundEntry.amount, @@ -429,7 +505,7 @@ object KillContributions { val weapons = entry.weapons ( entry.player, - weapons.filter { _.amount == 0 }.map { _.equipment_id }, + weapons.filter { _.amount == 0 }.map { _.equipment }, (0.9f * math.min(weapons.foldLeft(0f)(_ + _.contributions), bep.toFloat)).toLong ) } @@ -441,13 +517,13 @@ object KillContributions { val weapons = entry.weapons ( entry.player, - weapons.map { _.equipment_id }, + weapons.map { _.equipment }, math.min(weapons.foldLeft(0f)(_ + _.contributions).toLong, bep) ) } } .orElse { - Some((PlayerSource.Nobody, Seq(0), 0L)) + Some((PlayerSource.Nobody, Seq(), 0L)) } .collect { case (player, weaponIds, experience) if experience > 0 => diff --git a/src/main/scala/net/psforever/objects/zones/exp/RecoveryExperienceContribution.scala b/src/main/scala/net/psforever/objects/zones/exp/RecoveryExperienceContribution.scala deleted file mode 100644 index bae389793..000000000 --- a/src/main/scala/net/psforever/objects/zones/exp/RecoveryExperienceContribution.scala +++ /dev/null @@ -1,449 +0,0 @@ -// 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 - )) - } - } -} diff --git a/src/main/scala/net/psforever/objects/zones/exp/Stats.scala b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala index 758939a47..ef5b1b70f 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Stats.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Stats.scala @@ -4,14 +4,14 @@ package net.psforever.objects.zones.exp import net.psforever.objects.sourcing.PlayerSource sealed trait ItemUseStats { - def equipment_id: Int + def equipment: EquipmentUseContextWrapper def shots: Int def time: Long def contributions: Float } private case class WeaponStats( - equipment_id: Int, + equipment: EquipmentUseContextWrapper, amount: Int, shots: Int, time: Long, @@ -19,7 +19,7 @@ private case class WeaponStats( ) extends ItemUseStats private case class EquipmentStats( - equipment_id: Int, + equipment: EquipmentUseContextWrapper, shots: Int, time: Long, contributions: Float @@ -36,6 +36,6 @@ private[exp] case class ContributionStats( sealed case class ContributionStatsOutput( player: PlayerSource, - implements: Seq[Int], + implements: Seq[EquipmentUseContextWrapper], percentage: Float ) diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala index 2f1b7bd99..3f6f82fb6 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -87,31 +87,11 @@ object Support { second: ContributionStatsOutput ): ContributionStatsOutput = { if (first.percentage < second.percentage) - second.copy( - implements = (second.implements ++ first.implements).distinct, - percentage = first.percentage + second.implements.size * multiplier - ) + second.copy(implements = (second.implements ++ first.implements).distinct, percentage = first.percentage + second.implements.size * multiplier) else - first.copy( - implements = (first.implements ++ second.implements).distinct, - percentage = second.percentage + second.implements.size * multiplier - ) + first.copy(implements = (first.implements ++ second.implements).distinct, percentage = second.percentage + second.implements.size * multiplier) } -// private[exp] def collectKillAssists( -// victim: SourceEntry, -// history: List[InGameActivity], -// func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats] -// ): mutable.LongMap[ContributionStatsOutput] = { -// val healthAssists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 } -// val qualifiedHealing = healthAssists.values.foldLeft(0)(_ + _.total) -// val healthAssistsOutput = healthAssists.map { case (id, kda) => -// (id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / qualifiedHealing)) -// } -// healthAssistsOutput.remove(victim.CharId) -// healthAssistsOutput -// } - private[exp] def collectHealingSupportAssists( target: SourceEntry, time: JodaLocalDateTime, @@ -145,9 +125,12 @@ object Support { history: List[InGameActivity] ): mutable.LongMap[ContributionStatsOutput] = { collectSupportContributions( - target, time, history.collect { case repairs: RepairFromEquipment + target, + time, + history.collect { case repairs: RepairFromEquipment if repairs.amount > 0 && repairs.user.unique != target.unique => (repairs, repairs.equipment_def.ObjectId) - }, mapContributionPointsByPercentage + }, + mapContributionPointsByPercentage ) } @@ -183,7 +166,7 @@ object Support { if target.CharId != player.CharId && faction == player.Faction => //accessed a hacked terminal termsUsed.put(guid.toLong, Some(terminal)) - addTerminalContributionEntry(credit, player, Seq(GlobalDefinitions.remote_electronics_kit.ObjectId), percentage = 1f) + addTerminalContributionEntry(credit, player, Seq(HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId)), percentage = 1f) case _ if terminal.Faction == faction => //accessed a faction-friendly terminal; check log for repair history @@ -193,7 +176,8 @@ object Support { .History .collect { case a: RepairFromEquipment => val user = a.user - addTerminalContributionEntry(credit, user, Seq(a.equipment_def.ObjectId), percentage = 0.5f) + //TODO might be wrong intermediate + addTerminalContributionEntry(credit, user, Seq(RepairKillAssist(a.equipment_def.ObjectId, target.Definition.ObjectId)), percentage = 0.5f) } case _ => //what is this? @@ -208,7 +192,7 @@ object Support { private def addTerminalContributionEntry( contributions: mutable.LongMap[ContributionStatsOutput], player: PlayerSource, - implements: Seq[Int], + implements: Seq[EquipmentUseContextWrapper], percentage: Float ): Unit = { val charId = player.CharId @@ -216,10 +200,7 @@ object Support { case Some(entry) if percentage > entry.percentage => contributions.put(charId, entry.copy(percentage = percentage)) case Some(entry) => - contributions.put(charId, entry.copy( - implements = (entry.implements ++ implements).distinct, - percentage = entry.percentage + 0.05f - )) + contributions.put(charId, entry.copy(implements = (entry.implements ++ implements).distinct, percentage = entry.percentage + 0.05f)) case None => contributions.put(charId, ContributionStatsOutput(player, implements, percentage)) } @@ -262,7 +243,7 @@ object Support { )) case None => //the contribution percentage allocated will always be 1.0f and should be overwritten later - contributions.put(charId, ContributionStats(user, Seq(WeaponStats(defaultTool, amount, 1, h.time, 1f)), amount, amount, 1, h.time)) + contributions.put(charId, ContributionStats(user, Seq(WeaponStats(NoUse(defaultTool), amount, 1, h.time, 1f)), amount, amount, 1, h.time)) } } contributions @@ -354,7 +335,7 @@ object Support { 0.5f } } - (charId, ContributionStatsOutput(user, contribution.weapons.map { _.equipment_id }, value)) + (charId, ContributionStatsOutput(user, contribution.weapons.map { _.equipment }, value)) } //noinspection ScalaUnusedSymbol @@ -366,7 +347,7 @@ object Support { charId: Long, contribution: ContributionStats, ): (Long, ContributionStatsOutput) = { - (charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.equipment_id }, compareList.size.toFloat)) + (charId, ContributionStatsOutput(contribution.player, contribution.weapons.map { _.equipment }, compareList.size.toFloat)) } private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = { diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala index 9bd90e813..9252de9fc 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala @@ -14,6 +14,10 @@ import net.psforever.util.Database.ctx._ import scala.util.Success object ToDatabase { + /** + * Insert an entry into the database's `killactivity` table. + * One player just died and some other player is at fault. + */ def reportKillBy( killerId: Long, victimId: Long, @@ -40,6 +44,66 @@ object ToDatabase { ) } + /** + * Insert an entry into the database's `assistactivity` table. + * One player just died and some other player tried to take credit. + * (They are actually an accomplice.) + */ + def reportKillAssistBy( + avatarId: Long, + victimId: Long, + weaponId: Int, + zoneId: Int, + position: Vector3, + exp: Long + ): Unit = { + ctx.run(query[persistence.Assistactivity] + .insert( + _.killerId -> lift(avatarId), + _.victimId -> lift(victimId), + _.weaponId -> lift(weaponId), + _.zoneId -> lift(zoneId), + _.px -> lift((position.x * 1000).toInt), + _.py -> lift((position.y * 1000).toInt), + _.pz -> lift((position.z * 1000).toInt), + _.exp -> lift(exp) + ) + ) + } + + /** + * Insert an entry into the database's `supportactivity` table. + * One player did something for some other player and + * that other player was able to kill a third player. + */ + def reportSupportBy( + user: Long, + target: Long, + exosuit: Int, + interaction: Int, + intermediate: Int, + implement: Int, + experience: Long + ): Unit = { + ctx.run(query[persistence.Supportactivity] + .insert( + _.userId -> lift(user), + _.targetId -> lift(target), + _.targetExosuit -> lift(exosuit), + _.interactionType -> lift(interaction), + _.implementType -> lift(implement), + _.intermediateType -> lift(intermediate), + _.exp -> lift(experience) + ) + ) + } + + /** + * Attempt to update the database's `weaponstatsession` table and, + * if no existing entries can be found, + * insert a new entry into the table. + * Shots fired. + */ def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Unit = { val result = for { res <- ctx.run( @@ -69,34 +133,11 @@ object ToDatabase { } } - def reportAssistKills(avatarId: Long, weaponId: Int, assists: Int): Unit = { - val result = for { - res <- ctx.run( - query[persistence.Weaponstatsession] - .filter(_.avatarId == lift(avatarId)) - .filter(_.weaponId == lift(weaponId)) - .update( - _.assists -> lift(assists) - ) - ) - } yield res - result.onComplete { - case Success(rowCount) if rowCount.longValue > 0 => () - case _ => - ctx.run(query[persistence.Weaponstatsession] - .insert( - _.avatarId -> lift(avatarId), - _.weaponId -> lift(weaponId), - _.assists -> lift(assists), - _.shotsFired -> lift(0), - _.shotsLanded -> lift(0), - _.kills -> lift(0), - _.sessionId -> lift(-1L) - ) - ) - } - } - + /** + * Insert an entry into the database's `machinedestroyed` table. + * Just as stated, something that was not a player was destroyed. + * Valid entity types include: vehicles, amenities, and various turrets. + */ def reportMachineDestruction( avatarId: Long, machine: VehicleSource, @@ -108,15 +149,15 @@ object ToDatabase { import net.psforever.util.Database.ctx import net.psforever.util.Database.ctx._ val normalFaction = machine.Faction.id - val hackedFaction = hackState.map { _.player.Faction }.getOrElse(normalFaction) + val hackedToFaction = hackState.map { _.player.Faction.id }.getOrElse(normalFaction) val machinePosition = machine.Position - ctx.run(query[persistence.Machinedestroyedinstance] + ctx.run(query[persistence.Machinedestroyed] .insert( _.avatarId -> lift(avatarId), _.weaponId -> lift(weaponId), _.machineType -> lift(machine.Definition.ObjectId), _.machineFaction -> lift(normalFaction), - _.hackedFaction -> lift(hackedFaction), + _.hackedFaction -> lift(hackedToFaction), _.asCargo -> lift(isCargo), _.zoneNum -> lift(zoneNumber), _.px -> lift((machinePosition.x * 1000).toInt), diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/ArmorRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/ArmorRecoveryExperienceContributionProcess.scala new file mode 100644 index 000000000..cf4034255 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/ArmorRecoveryExperienceContributionProcess.scala @@ -0,0 +1,84 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp.rec + +import net.psforever.objects.sourcing.SourceUniqueness +import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairFromTerminal, RepairingActivity, SupportActivityCausedByAnother} +import net.psforever.objects.zones.exp.KillContributions +import net.psforever.types.PlanetSideEmpire + +import scala.collection.mutable + +class ArmorRecoveryExperienceContributionProcess( + private val faction : PlanetSideEmpire.Value, + private val contributions: Map[SourceUniqueness, List[InGameActivity]], + private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer() + ) 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, + excludedTargets + ) + .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 _ => () + } + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala new file mode 100644 index 000000000..3ec36dacd --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/CombinedHealthAndArmorContributionProcess.scala @@ -0,0 +1,82 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp.rec + +import net.psforever.objects.sourcing.SourceUniqueness +import net.psforever.objects.vital.InGameActivity +import net.psforever.objects.zones.exp.{ContributionStats, KillContributions, WeaponStats} +import net.psforever.types.PlanetSideEmpire + +import scala.collection.mutable + +class CombinedHealthAndArmorContributionProcess( + private val faction : PlanetSideEmpire.Value, + private val contributions: Map[SourceUniqueness, List[InGameActivity]], + otherSubmissions: Seq[RecoveryExperienceContribution] + ) extends RecoveryExperienceContribution { + private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer() + private val process: Seq[RecoveryExperienceContributionProcess] = Seq( + new HealthRecoveryExperienceContributionProcess(faction, contributions, excludedTargets), + new ArmorRecoveryExperienceContributionProcess(faction, contributions, excludedTargets) + ) + + 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) + .map { + case (weaponId, weaponEntries) => + val specificEntries = weaponEntries.filter(_.equipment == 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 + )) + } + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/HealthRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/HealthRecoveryExperienceContributionProcess.scala new file mode 100644 index 000000000..4b5edcd67 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/HealthRecoveryExperienceContributionProcess.scala @@ -0,0 +1,97 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp.rec + +import net.psforever.objects.sourcing.SourceUniqueness +import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, HealFromTerminal, HealingActivity, InGameActivity, RevivingActivity, SupportActivityCausedByAnother} +import net.psforever.objects.zones.exp.KillContributions +import net.psforever.types.PlanetSideEmpire + +import scala.collection.mutable + +private class HealthRecoveryExperienceContributionProcess( + private val faction : PlanetSideEmpire.Value, + private val contributions: Map[SourceUniqueness, List[InGameActivity]], + private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer() + ) 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, + excludedTargets + ) + .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 _ => () + } + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala new file mode 100644 index 000000000..0b211ddfe --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/MachineRecoveryExperienceContributionProcess.scala @@ -0,0 +1,59 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp.rec + +import net.psforever.objects.sourcing.SourceUniqueness +import net.psforever.objects.vital.{DamagingActivity, InGameActivity, RepairFromEquipment, RepairingActivity} +import net.psforever.types.PlanetSideEmpire + +import scala.collection.mutable + +//noinspection ScalaUnusedSymbol +class MachineRecoveryExperienceContributionProcess( + private val faction : PlanetSideEmpire.Value, + private val contributions: Map[SourceUniqueness, List[InGameActivity]], + private val excludedTargets: mutable.ListBuffer[SourceUniqueness] = mutable.ListBuffer() + ) 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 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 _ => () + } + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContribution.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContribution.scala new file mode 100644 index 000000000..bc932856d --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContribution.scala @@ -0,0 +1,280 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp.rec + +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.vital._ +import net.psforever.objects.vital.interaction.{Adversarial, DamageResult} +import net.psforever.objects.zones.exp.{ContributionStats, HealKillAssist, WeaponStats} +import net.psforever.types.PlanetSideEmpire + +import scala.collection.mutable + +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, user) + 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(HealKillAssist(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.equipment == 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) + } +} diff --git a/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContributionProcess.scala b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContributionProcess.scala new file mode 100644 index 000000000..7394bba68 --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/exp/rec/RecoveryExperienceContributionProcess.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.zones.exp.rec + +import net.psforever.objects.sourcing.{PlayerSource, SourceUniqueness} +import net.psforever.objects.vital.InGameActivity +import net.psforever.objects.zones.exp.ContributionStats +import net.psforever.types.PlanetSideEmpire + +import scala.collection.mutable + +//noinspection ScalaUnusedSymbol +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() + } +} diff --git a/src/main/scala/net/psforever/persistence/Assistactivity.scala b/src/main/scala/net/psforever/persistence/Assistactivity.scala new file mode 100644 index 000000000..9d59ad53c --- /dev/null +++ b/src/main/scala/net/psforever/persistence/Assistactivity.scala @@ -0,0 +1,16 @@ +package net.psforever.persistence + +import org.joda.time.LocalDateTime + +case class Assistactivity( + index: Int, + killerId: Long, + victimId: Long, + weaponId: Int, + zoneId: Int, + px: Int, //Position.x * 1000 + py: Int, //Position.y * 1000 + pz: Int, //Position.z * 1000 + exp: Long, + timestamp: LocalDateTime = LocalDateTime.now() + ) diff --git a/src/main/scala/net/psforever/persistence/Killactivity.scala b/src/main/scala/net/psforever/persistence/Killactivity.scala index d1448c1f4..2eb6a1f7e 100644 --- a/src/main/scala/net/psforever/persistence/Killactivity.scala +++ b/src/main/scala/net/psforever/persistence/Killactivity.scala @@ -1,6 +1,8 @@ // Copyright (c) 2022 PSForever package net.psforever.persistence +import org.joda.time.LocalDateTime + case class Killactivity( index: Int, killerId: Long, @@ -12,5 +14,6 @@ case class Killactivity( px: Int, //Position.x * 1000 py: Int, //Position.y * 1000 pz: Int, //Position.z * 1000 - exp: Long + exp: Long, + timestamp: LocalDateTime = LocalDateTime.now() ) diff --git a/src/main/scala/net/psforever/persistence/Machinedestroyedinstance.scala b/src/main/scala/net/psforever/persistence/Machinedestroyed.scala similarity index 95% rename from src/main/scala/net/psforever/persistence/Machinedestroyedinstance.scala rename to src/main/scala/net/psforever/persistence/Machinedestroyed.scala index b876a2b5b..c25e4eac1 100644 --- a/src/main/scala/net/psforever/persistence/Machinedestroyedinstance.scala +++ b/src/main/scala/net/psforever/persistence/Machinedestroyed.scala @@ -3,7 +3,7 @@ package net.psforever.persistence import org.joda.time.LocalDateTime -case class Machinedestroyedinstance( +case class Machinedestroyed( index: Int, avatarId: Long, weaponId: Int, diff --git a/src/main/scala/net/psforever/persistence/Supportactivity.scala b/src/main/scala/net/psforever/persistence/Supportactivity.scala new file mode 100644 index 000000000..d131adaec --- /dev/null +++ b/src/main/scala/net/psforever/persistence/Supportactivity.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2022 PSForever +package net.psforever.persistence + +import org.joda.time.LocalDateTime + +case class Supportactivity( + index: Int, + userId: Long, + targetId: Long, + targetExosuit: Int, + interactionType: Int, + implementType: Int, + intermediateType: Int, + exp: Long, + timestamp: LocalDateTime = LocalDateTime.now() + ) diff --git a/src/main/scala/net/psforever/types/Statistics.scala b/src/main/scala/net/psforever/types/Statistics.scala index 27272ea67..5020b1015 100644 --- a/src/main/scala/net/psforever/types/Statistics.scala +++ b/src/main/scala/net/psforever/types/Statistics.scala @@ -64,16 +64,16 @@ object StatisticalCategory extends IntEnum[StatisticalCategory] { Seq(Phantasm, ImplantTerminalMech, Droppod, SpitfireAA, SpitfireCloaked, SpitfireTurret, TankTraps) ++ driverOnlyVehicles ++ gunnerVehicles ++ mannedTurretElements ++ exosuitElements, Seq( - // Chaingun12mm, Chaingun15mm, Cannon20mm, Deliverer20mm, DropshipL20mm, Cannon75mm, Lightning75mm, AdvancedMissileLauncherT, AMS, AnniversaryGun, AnniversaryGunA, AnniversaryGunB, ANT, Sunderer, ApcBallGunL, ApcBallGunR, ApcTr, ApcNc, ApcVs, ApcWeaponSystemA, ApcWeaponSystemB, ApcWeaponSystemC, ApcWeaponSystemCNc, ApcWeaponSystemCTr, ApcWeaponSystemCVs, ApcWeaponSystemD, ApcWeaponSystemDNc, ApcWeaponSystemDTr, ApcWeaponSystemDVs, Aphelion, AphelionArmorSiphon, AphelionFlight, AphelionGunner, AphelionImmolationCannon, AphelionLaser, AphelionNtuSiphon, AphelionPlasmaCloud, AphelionPlasmaRocketPod, - // AphelionPpa, AphelionStarfire, AuroraWeaponSystemA, AuroraWeaponSystemB, Battlewagon, BattlewagonWeaponSystemA, BattlewagonWeaponSystemB, BattlewagonWeaponSystemC, BattlewagonWeaponSystemD, Infantry, Raider, Beamer, BoltDriver, Boomer, Chainblade, ChaingunP, Colossus, ColossusArmorSiphon, ColossusBurster, ColossusChaingun, ColossusClusterBombPod, ColossusDual100mmCannons, ColossusFlight, - // ColossusGunner, ColossusNtuSiphon, ColossusTankCannon, Cycler, CyclerV2, CyclerV3, CyclerV4, Dropship, DropshipRearTurret, Dynomite, EnergyGunNc, EnergyGunTr, EnergyGunVs, Flail, FlailWeapon, Flamethrower, - // Flechette, FluxCannonThresher, Fluxpod, Forceblade, FragGrenade, Fury, FragmentationGrenade, FuryWeaponSystemA, GalaxyGunship, GalaxyGunshipCannon, GalaxyGunshipGun, GalaxyGunshipTailgun, Gauss, GaussCannon, GrenadeLauncherMarauder, HeMine, HeavyRailBeamMagrider, HeavySniper, Hellfire, - // Hunterseeker, Ilc9, Isp, JammerGrenade, Katana, Knife, Lancer, Lasher, Liberator, Liberator25mmCannon, LiberatorBombBay, LiberatorWeaponSystem, Lightgunship, LightgunshipWeapon20mm, LightgunshipWeaponRocket, LightgunshipWeaponSystem, Lightning, LightningWeaponSystem, Lodestar, Maelstrom, Magcutter, Magrider, PhalanxTurret, - // MedicalApplicator, MediumTransport, MediumTransportWeaponSystemA, MediumTransportWeaponSystemB, MineSweeper, MiniChaingun, Mosquito, NchevFalcon, NchevScattercannon, NchevSparrow, Oicw, - // OrbitalStrikeBig, OrbitalStrikeSmall, ParticleBeamMagrider, PelletGun, Peregrine, PeregrineArmorSiphon, PeregrineDualMachineGun, PeregrineDualRocketPods, PeregrineFlight, PeregrineGunner, PeregrineMechhammer, PeregrineNtuSiphon, PeregrineParticleCannon, PeregrineSparrow, PhalanxAvcombo, PhalanxFlakcombo, PhalanxSglHevgatcan, Phantasm, Phantasm12mmMachinegun, Phoenix, PlasmaGrenade, Prowler, ProwlerWeaponSystemA, - // ProwlerWeaponSystemB, Pulsar, PulsedParticleAccelerator, Punisher, QuadAssault, QuadAssaultWeaponSystem, QuadStealth, RShotgun, Radiator, Repeater, Rocklet, RotaryChaingunMosquito, Router, RouterTelepadDeployable, Scythe, SixShooter, Skyguard, SkyguardWeaponSystem, - // Spiker, SpitfireAA, SpitfireCloaked, SpitfireTurret, Striker, Suppressor, Switchblade, ThreeManHeavyBuggy, Thumper, Thunderer, ThundererWeaponSystemA, ThundererWeaponSystemB, TrhevBurster, TrhevDualcycler, TrhevPounder, TwoManAssaultBuggy, TwoManHeavyBuggy, - // TwoManHoverBuggy, Vanguard, VanguardWeapon150mm, VanguardWeapon20mm, VanguardWeaponSystem, VanuModule, VanuSentryTurretWeapon, VanuModuleBeam, VshevComet, VshevQuasar, VshevStarfire, Vulture, VultureBombBay, VultureNoseWeaponSystem, VultureTailCannon, Wasp, WaspWeaponSystem, Winchester + Chaingun12mm, Chaingun15mm, Cannon20mm, Deliverer20mm, DropshipL20mm, Cannon75mm, Lightning75mm, AdvancedMissileLauncherT, AMS, AnniversaryGun, AnniversaryGunA, AnniversaryGunB, ANT, Sunderer, ApcBallGunL, ApcBallGunR, ApcTr, ApcNc, ApcVs, ApcWeaponSystemA, ApcWeaponSystemB, ApcWeaponSystemC, ApcWeaponSystemCNc, ApcWeaponSystemCTr, ApcWeaponSystemCVs, ApcWeaponSystemD, ApcWeaponSystemDNc, ApcWeaponSystemDTr, ApcWeaponSystemDVs, Aphelion, AphelionArmorSiphon, AphelionFlight, AphelionGunner, AphelionImmolationCannon, AphelionLaser, AphelionNtuSiphon, AphelionPlasmaCloud, AphelionPlasmaRocketPod, + AphelionPpa, AphelionStarfire, AuroraWeaponSystemA, AuroraWeaponSystemB, Battlewagon, BattlewagonWeaponSystemA, BattlewagonWeaponSystemB, BattlewagonWeaponSystemC, BattlewagonWeaponSystemD, Infantry, Raider, Beamer, BoltDriver, Boomer, Chainblade, ChaingunP, Colossus, ColossusArmorSiphon, ColossusBurster, ColossusChaingun, ColossusClusterBombPod, ColossusDual100mmCannons, ColossusFlight, + ColossusGunner, ColossusNtuSiphon, ColossusTankCannon, Cycler, CyclerV2, CyclerV3, CyclerV4, Dropship, DropshipRearTurret, Dynomite, EnergyGunNc, EnergyGunTr, EnergyGunVs, Flail, FlailWeapon, Flamethrower, + Flechette, FluxCannonThresher, Fluxpod, Forceblade, FragGrenade, Fury, FragmentationGrenade, FuryWeaponSystemA, GalaxyGunship, GalaxyGunshipCannon, GalaxyGunshipGun, GalaxyGunshipTailgun, Gauss, GaussCannon, GrenadeLauncherMarauder, HeMine, HeavyRailBeamMagrider, HeavySniper, Hellfire, + Hunterseeker, Ilc9, Isp, JammerGrenade, Katana, Knife, Lancer, Lasher, Liberator, Liberator25mmCannon, LiberatorBombBay, LiberatorWeaponSystem, Lightgunship, LightgunshipWeapon20mm, LightgunshipWeaponRocket, LightgunshipWeaponSystem, Lightning, LightningWeaponSystem, Lodestar, Maelstrom, Magcutter, Magrider, PhalanxTurret, + MedicalApplicator, MediumTransport, MediumTransportWeaponSystemA, MediumTransportWeaponSystemB, MineSweeper, MiniChaingun, Mosquito, NchevFalcon, NchevScattercannon, NchevSparrow, Oicw, + OrbitalStrikeBig, OrbitalStrikeSmall, ParticleBeamMagrider, PelletGun, Peregrine, PeregrineArmorSiphon, PeregrineDualMachineGun, PeregrineDualRocketPods, PeregrineFlight, PeregrineGunner, PeregrineMechhammer, PeregrineNtuSiphon, PeregrineParticleCannon, PeregrineSparrow, PhalanxAvcombo, PhalanxFlakcombo, PhalanxSglHevgatcan, Phantasm, Phantasm12mmMachinegun, Phoenix, PlasmaGrenade, Prowler, ProwlerWeaponSystemA, + ProwlerWeaponSystemB, Pulsar, PulsedParticleAccelerator, Punisher, QuadAssault, QuadAssaultWeaponSystem, QuadStealth, RShotgun, Radiator, Repeater, Rocklet, RotaryChaingunMosquito, Router, RouterTelepadDeployable, Scythe, SixShooter, Skyguard, SkyguardWeaponSystem, + Spiker, SpitfireAA, SpitfireCloaked, SpitfireTurret, Striker, Suppressor, Switchblade, ThreeManHeavyBuggy, Thumper, Thunderer, ThundererWeaponSystemA, ThundererWeaponSystemB, TrhevBurster, TrhevDualcycler, TrhevPounder, TwoManAssaultBuggy, TwoManHeavyBuggy, + TwoManHoverBuggy, Vanguard, VanguardWeapon150mm, VanguardWeapon20mm, VanguardWeaponSystem, VanuModule, VanuSentryTurretWeapon, VanuModuleBeam, VshevComet, VshevQuasar, VshevStarfire, Vulture, VultureBombBay, VultureNoseWeaponSystem, VultureTailCannon, Wasp, WaspWeaponSystem, Winchester ), Seq(Facilities, Redoubt, Tower, VanuControlPoint, VanuVehicleStation), exosuitElements,