importing controlled implementation changes from original exp-for-kda branch; assist kill experience rewarded

importing controlled implementation changes from original exp-for-kda branch; code for contributions from prior activity, but will be adjusting to new contribution methods

kill contributions should work; even if they don't, need to put this away for now

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

events for mounting and dismounting of both passenger and cargo; id'ing the installation of an amenity (vehicle or facility); separation of kill/assist experience and support experience calculations; retention of kill record which allows for the calculation of menace

support experience accumulates and is given to the user in gradual provisions

rewarding facility capture through cep; not fully tested yet; math sucks

sort of cep to bep consditions for squad facility capture; bep deposit for ntu silo activity

early reivision for v010; recording ongoing shots fired and landed

restored bep from ntu deposits into resource silos; updating statistics in the database regarding kills and related stats including weapons; updated history management; basic experience calculation changes

all rewarded support events are accounted for

command experience calculations upon facility capture or resecure

corrected database migrations

most of the code for the play or progress system

statistics window updates for exosuits to report kills; killing an llu runner gives cep; moving play or progress functionality to a bang command rather than piggybacking setbr; bep is no longer too high by error
This commit is contained in:
Fate-JH 2023-03-25 23:29:03 -04:00
parent b866aa8a30
commit e9dbd5f259
112 changed files with 5849 additions and 1361 deletions

View file

@ -0,0 +1,22 @@
/* Original: V008__Scoring.sql */
CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats()
RETURNS TRIGGER
AS
$$
DECLARE killerSessionId Int;
DECLARE killerId Int;
DECLARE weaponId Int;
DECLARE out integer;
BEGIN
killerId := NEW.killer_id;
weaponId := NEW.weapon_id;
killerSessionId := proc_sessionnumber_get(killerId);
out := proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, killerSessionId, weaponId);
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;

View file

@ -231,17 +231,17 @@ object VehicleSpawnPadControlTest {
override def SetupNumberPools(): Unit = {} override def SetupNumberPools(): Unit = {}
} }
zone.GUID(guid) zone.GUID(guid)
zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.nanoTime()}") zone.actor = system.spawn(ZoneActor(zone), s"test-zone-${System.currentTimeMillis()}")
// Hack: Wait for the Zone to finish booting, otherwise later tests will fail randomly due to race conditions // Hack: Wait for the Zone to finish booting, otherwise later tests will fail randomly due to race conditions
// with actor probe setting // with actor probe setting
// TODO(chord): Remove when Zone supports notification of booting being complete // TODO(chord): Remove when Zone supports notification of booting being complete
Thread.sleep(5000) Thread.sleep(5000)
vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.nanoTime()}") vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), s"vehicle-control-${System.currentTimeMillis()}")
val pad = VehicleSpawnPad(GlobalDefinitions.mb_pad_creation) val pad = VehicleSpawnPad(GlobalDefinitions.mb_pad_creation)
pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.nanoTime()}") pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.currentTimeMillis()}")
pad.Owner = pad.Owner =
new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building) new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building)
pad.Owner.Faction = faction pad.Owner.Faction = faction

View file

@ -75,12 +75,6 @@ game {
third-party = no third-party = no
} }
# Battle experience rate
bep-rate = 1.0
# Command experience rate
cep-rate = 1.0
# Modify the amount of mending per autorepair tick for facility amenities # Modify the amount of mending per autorepair tick for facility amenities
amenity-autorepair-rate = 1.0 amenity-autorepair-rate = 1.0
@ -223,6 +217,190 @@ game {
# Don't ask. # Don't ask.
doors-can-be-opened-by-med-app-from-this-distance = 5.05 doors-can-be-opened-by-med-app-from-this-distance = 5.05
# How the experience calculates
experience {
# The short contribution time when events are collected and evaluated.
short-contribution-time = 300000
# The long contribution time when events are collected and evaluated
# even factoring the same events from the short contribution time.
# As a result, when comparing the two event lists, similar actors may appear
# but their contributions may be different.
long-contribution-time = 600000
# Battle experience points
# BEP is to be calculated in relation to how valuable a kill is worth.
bep = {
# After all calculations are complete, multiple the result by this value
rate = 1.0
# These numbers are to determine the starting value for a particular kill
base = {
# Black Ops multiplies the base value by this much
bops-multiplier = 10.0
# If the player who died ever utilized a mechanized assault exo-suit
as-max = 250
# The player who died got at least one kill
with-kills = 100
# The player who died was mounted in a vehicle at the time of death
as-mounted = 100
# The player who died after having been in the game world for a while after spawning.
# Dying before this is often called a "spawn kill".
mature = 50
}
}
# Support experience points
# The events from which support experience rises are numerous.
# Calculation is determined by the selection of an "event" that decides how the values are combined.
sep = {
# After all calculations are complete, multiple the result by this value
rate = 1.0
# When using an advanced nanite transport to deposit into the resource silo of a major facility,
# for reaching the maximum amount of a single deposit,
# reward the user with this amount of support experience points.
# Small deposits reward only a percentage of this value.
ntu-silo-deposit-reward = 100
# When the event can not be found, this flat sum is rewarded.
# This should not be treated as a feature.
# It is a bug.
# Check your event label calls.
can-not-find-event-default-value = 15
# The events by which support experience calculation occurs.
# Events can be composed of three parts: a base value, a per-use (shots) value, and an active amount value.
# "Per-use" relies on knowledge from the server about the number of times this exact action occurred before the event.
# "Active amount" relies on knowledge from the server about how much of the changes for this event are still valid.
# Some changes can be undone by other events or other behavior.
#
# name - label by which this event is organized
# base - whole number value
# shots-multiplier - whether use count matters for this event
# - when set to 0.0 (default), it does not
# shots-limit - upper limit of use count
# - cap the count here, if higher
# shots-cutoff - if the use count exceeds this number, the event no longer applies
# - a hard limit that should zero the contribution reward
# - the *-cutoff should probably apply before *-limit, maybe
# shots-nat-log - when set, may the use count to a natural logarithmic curve
# - actually the exponent on the use count before the logarithm
# - similar to shots-limit, but the curve plateaus quickly
# amount-multiplier - whether active amount matters for this event
# - when set to 0.0 (default), it does not
events = [
{
name = "support-heal"
base = 10
shots-multiplier = 5.0
shots-limit = 100
amount-multiplier = 2.0
}
{
name = "support-repair"
base = 10
shots-multiplier = 5.0
shots-limit = 100
}
{
name = "support-repair-terminal"
base = 10
shots-multiplier = 5.0
shots-limit = 100
}
{
name = "support-repair-turret"
base = 10
shots-multiplier = 5.0
shots-limit = 100
}
{
name = "mounted-kill"
base = 25
}
{
name = "router"
base = 15
}
{
name = "hotdrop"
base = 25
}
{
name = "hack"
base = 5
amount-multiplier = 5.0
}
{
name = "ams-resupply"
base = 15
shots-multiplier = 1.0
shots-nat-log = 5.0
}
{
name = "lodestar-repair"
base = 10
shots-multiplier = 1.0
shots-nat-log = 5.0
shots-limit = 100
amount-multiplier = 1.0
}
{
name = "lodestar-rearm"
base = 10
shots-multiplier = 1.0
shots-nat-log = 5.0
}
{
name = "revival"
base = 0
shots-multiplier = 15.0
shots-nat-log = 5.0
shots-cutoff = 10
}
]
}
# Support experience points
cep = {
# After all calculations are complete, multiple the result by this value
rate = 1.0
# When command experience points are rewarded to the lattice link unit carrier,
# modify the original value by this modifier.
llu-carrier-modifier = 0.5
# If a player died while carrying an lattice logic unit,
# award the player who is accredited with the kill command experience as long as the time it had been carried longer than this duration.
# Can set to Duration.Inf to never pass.
llu-slayer-credit-duration = 1 minute
# If a player died while carrying an lattice logic unit,
# and satisfies the carrying duration,
# award the player who is accredited with the kill command experience.
llu-slayer-credit = 200
# The maximum command experience that can be earned in a facility capture based on squad size
maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860]
# When the cep has to be capped for squad size, add a small value to the capped value
# -1 reuses the cep before being capped
squad-size-limit-overflow = -1
# When the cep has to be capped for squad size, calculate a small amount to add to the capped value
squad-size-limit-overflow-multiplier = 0.2
}
}
# The game's official maximum battle rank is 40.
# This is an artificial cap that attempts to stop advancement long before that.
# After becoming this battle rank, battle experience points gain will be locked.
# In our case, we're imposing this because character features can be unstable when above BR24.
max-battle-rank = 24
promotion {
# Whether promotion versus play is offered at battle rank 1.
# Anyone who is currently enrolled in the promotion system remains enrolled during normal game play.
# Relenting on the promotion debt back to battle rank 2 is still possible.
active = true
# This is the maximum battle rank that can be set as part of the promotion system.
max-battle-rank = 13
# How much direct combat contributes to paying back promotion debt.
# Typically, it does not contribute.
battle-experience-points-modifier = 0f
# Don't forget to pay back that debt.
}
} }
anti-cheat { anti-cheat {

View file

@ -137,12 +137,10 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
bcryptedPassword bcryptedPassword
} }
def getAccountLogin(username: String, password: Option[String], token: Option[String]): Unit = { def getAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = {
tokenOpt match {
if (token.isDefined) { case Some(token) => accountLoginWithToken(token)
accountLoginWithToken(token.getOrElse("")) case None => accountLogin(username, passwordOpt.getOrElse(""))
} else {
accountLogin(username, password.getOrElse(""))
} }
} }
@ -164,7 +162,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne
accountOption <- accountsExact.headOption orElse accountsLower.headOption match { accountOption <- accountsExact.headOption orElse accountsLower.headOption match {
// account found // account found
case Some(account) => Future.successful(Some(account)) case Some(account) =>
Future.successful(Some(account))
// create new account // create new account
case None => case None =>

View file

@ -4,10 +4,17 @@ package net.psforever.actors.session
import akka.actor.Cancellable import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy} import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.scoring.{Death, EquipmentStat, KDAStat, Kill} import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, ScoreCard, SupportActivity}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.objects.vital.InGameHistory
import net.psforever.objects.vehicles.MountedWeapons
import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement}
import org.joda.time.{LocalDateTime, Seconds} import org.joda.time.{LocalDateTime, Seconds}
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise} import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@ -36,7 +43,7 @@ import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
import net.psforever.objects.locker.LockerContainer import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry} import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity, Vitality} import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity}
import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars} import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _} import net.psforever.packet.game.{Friend => GameFriend, _}
import net.psforever.persistence import net.psforever.persistence
@ -199,11 +206,17 @@ object AvatarActor {
/** Set total battle experience points */ /** Set total battle experience points */
final case class SetBep(bep: Long) extends Command final case class SetBep(bep: Long) extends Command
/** Advance total battle experience points */
final case class Progress(bep: Long) extends Command
/** Award command experience points */ /** Award command experience points */
final case class AwardCep(bep: Long) extends Command final case class AwardFacilityCaptureBep(bep: Long) extends Command
/** Award command experience points */
final case class AwardCep(cep: Long) extends Command
/** Set total command experience points */ /** Set total command experience points */
final case class SetCep(bep: Long) extends Command final case class SetCep(cep: Long) extends Command
/** Set cosmetics. Only allowed for BR24 or higher. */ /** Set cosmetics. Only allowed for BR24 or higher. */
final case class SetCosmetics(personalStyles: Set[Cosmetic]) extends Command final case class SetCosmetics(personalStyles: Set[Cosmetic]) extends Command
@ -226,6 +239,8 @@ object AvatarActor {
final case class AvatarLoginResponse(avatar: Avatar) final case class AvatarLoginResponse(avatar: Avatar)
final case class SupportExperienceDeposit(bep: Long, delay: Long) extends Command
/** /**
* A player loadout represents all of the items in the player's hands (equipment slots) * A player loadout represents all of the items in the player's hands (equipment slots)
* and all of the items in the player's backpack (inventory) * and all of the items in the player's backpack (inventory)
@ -822,6 +837,110 @@ object AvatarActor {
out.future out.future
} }
def setBepOnly(avatarId: Long, bep: Long): Future[Long] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[Long] = Promise()
val result = ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).update(_.bep -> lift(bep)))
result.onComplete { _ =>
out.completeWith(Future(bep))
}
out.future
}
def loadExperienceDebt(avatarId: Long): Future[Long] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[Long] = Promise()
val result = ctx.run(query[persistence.Progressiondebt].filter(_.avatarId == lift(avatarId)))
result.onComplete {
case Success(debt) if debt.nonEmpty =>
out.completeWith(Future(debt.head.experience))
case _ =>
out.completeWith(Future(0L))
}
out.future
}
def saveExperienceDebt(avatarId: Long, exp: Long): Future[Int] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[Int] = Promise()
val result = ctx.run(query[persistence.Progressiondebt].filter(_.avatarId == lift(avatarId)))
result.onComplete {
case Success(debt) if debt.nonEmpty =>
ctx.run(
query[persistence.Progressiondebt]
.filter(_.avatarId == lift(avatarId))
.update(_.experience -> lift(exp))
)
out.completeWith(Future(1))
case _ =>
out.completeWith(Future(0))
}
out.future
}
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))
.update(_.avatarLoggedIn -> lift(0L))
)
}
def updateToolDischargeFor(avatar: Avatar): Unit = {
updateToolDischargeFor(avatar.id, avatar.scorecard.CurrentLife)
}
def updateToolDischargeFor(avatarId: Long, life: Life): Unit = {
updateToolDischargeFor(avatarId, life.equipmentStats)
}
def updateToolDischargeFor(avatarId: Long, stats: Seq[EquipmentStat]): Unit = {
stats.foreach { stat =>
zones.exp.ToDatabase.reportToolDischarge(avatarId, stat)
}
}
def loadCampaignKdaData(avatarId: Long): Future[ScoreCard] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[ScoreCard] = Promise()
val result = ctx.run(quote {
for {
kdaOut <- query[persistence.Killactivity]
.filter(c =>
(c.killerId == lift(avatarId) || c.victimId == lift(avatarId)) && c.killerId != c.victimId
)
.join(query[persistence.Avatar])
.on({ case (a, b) =>
b.id == a.victimId
})
.map({ case (a, b) =>
(a.killerId, a.victimId, a.victimExosuit, b.factionId)
})
} yield kdaOut
})
result.onComplete {
case Success(res) =>
val card = new ScoreCard()
val (killerEntries, _) = res.partition {
case (killer, _, _, _) => avatarId == killer
}
killerEntries.foreach { case (_, _, exosuit, faction) =>
val statId = StatisticalElement.relatedElement(ExoSuitType(exosuit)).value
card.initStatisticForKill(statId, PlanetSideEmpire(faction))
}
out.completeWith(Future(card))
case _ =>
out.completeWith(Future(new ScoreCard()))
}
out.future
}
def toAvatar(avatar: persistence.Avatar): Avatar = { def toAvatar(avatar: persistence.Avatar): Avatar = {
val bep = avatar.bep val bep = avatar.bep
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) { val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
@ -863,6 +982,9 @@ class AvatarActor(
var _avatar: Option[Avatar] = None var _avatar: Option[Avatar] = None
var saveLockerFunc: () => Unit = storeNewLocker var saveLockerFunc: () => Unit = storeNewLocker
//val topic: ActorRef[Topic.Command[Avatar]] = context.spawnAnonymous(Topic[Avatar]("avatar")) //val topic: ActorRef[Topic.Command[Avatar]] = context.spawnAnonymous(Topic[Avatar]("avatar"))
var supportExperiencePool: Long = 0
var supportExperienceTimer: Cancellable = Default.Cancellable
var experienceDebt: Long = 0L
def avatar: Avatar = _avatar.get def avatar: Avatar = _avatar.get
@ -1024,13 +1146,13 @@ class AvatarActor(
} yield true } yield true
inits.onComplete { inits.onComplete {
case Success(_) => case Success(_) =>
performAvatarLogin(avatarId, account.id, replyTo) performAvatarLoginTest(avatarId, account.id, replyTo)
case Failure(e) => case Failure(e) =>
log.error(e)("db failure") log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6)) sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
} }
} else { } else {
performAvatarLogin(avatarId, account.id, replyTo) performAvatarLoginTest(avatarId, account.id, replyTo)
} }
case Success(_) => case Success(_) =>
//TODO this may not be an actual failure, but don't know what to do //TODO this may not be an actual failure, but don't know what to do
@ -1049,6 +1171,11 @@ class AvatarActor(
buffer.stash(other) buffer.stash(other)
Behaviors.same Behaviors.same
} }
.receiveSignal {
case (_, PostStop) =>
AvatarActor.avatarNoLongerLoggedIn(account.id)
Behaviors.same
}
} }
def postCharacterSelectBehaviour(): Behavior[Command] = { def postCharacterSelectBehaviour(): Behavior[Command] = {
@ -1646,24 +1773,95 @@ class AvatarActor(
updateToolDischarge(stats) updateToolDischarge(stats)
Behaviors.same Behaviors.same
case UpdateKillsDeathsAssists(stat) => case UpdateKillsDeathsAssists(stat: Kill) =>
updateKillsDeathsAssists(stat) 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) =>
val gain = bep - experienceDebt
if (gain > 0L) {
awardSupportExperience(gain, previousDelay = 0L)
} else {
experienceDebt = experienceDebt - bep
}
Behaviors.same Behaviors.same
case AwardBep(bep, modifier) => case AwardBep(bep, modifier) =>
setBep(avatar.bep + bep, modifier) val mod = Config.app.game.promotion.battleExperiencePointsModifier
if (experienceDebt == 0L) {
setBep(avatar.bep + bep, modifier)
} else if (mod > 0f) {
val modifiedBep = (bep.toFloat * Config.app.game.promotion.battleExperiencePointsModifier).toLong
val gain = modifiedBep - experienceDebt
if (gain > 0L) {
setBep(avatar.bep + gain, modifier)
} else {
experienceDebt = experienceDebt - modifiedBep
}
}
Behaviors.same
case AwardFacilityCaptureBep(bep) =>
val gain = bep - experienceDebt
if (gain > 0L) {
setBep(gain, ExperienceType.Normal)
} else {
experienceDebt = experienceDebt - bep
}
Behaviors.same
case SupportExperienceDeposit(bep, delayBy) =>
actuallyAwardSupportExperience(bep, delayBy)
Behaviors.same Behaviors.same
case SetBep(bep) => case SetBep(bep) =>
setBep(bep, ExperienceType.Normal) setBep(bep, ExperienceType.Normal)
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetBattleRank"))
Behaviors.same
case Progress(bep) =>
if ({
val oldBr = BattleRank.withExperience(avatar.bep).value
val newBr = BattleRank.withExperience(bep).value
if (Config.app.game.promotion.active && oldBr == 1 && newBr > 1 && newBr < Config.app.game.promotion.maxBattleRank + 1) {
experienceDebt = bep
if (avatar.cep > 0) {
setCep(0L)
}
true
} else if (experienceDebt > 0 && newBr == 2) {
experienceDebt = 0
true
} else {
false
}
}) {
setBep(bep, ExperienceType.Normal)
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetBattleRank"))
}
Behaviors.same Behaviors.same
case AwardCep(cep) => case AwardCep(cep) =>
setCep(avatar.cep + cep) if (experienceDebt > 0L) {
setCep(avatar.cep + cep)
}
Behaviors.same Behaviors.same
case SetCep(cep) => case SetCep(cep) =>
setCep(cep) setCep(cep)
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetCommandRank"))
Behaviors.same Behaviors.same
case SetCosmetics(cosmetics) => case SetCosmetics(cosmetics) =>
@ -1789,10 +1987,17 @@ class AvatarActor(
} }
.receiveSignal { .receiveSignal {
case (_, PostStop) => case (_, PostStop) =>
AvatarActor.saveAvatarData(avatar)
staminaRegenTimer.cancel() staminaRegenTimer.cancel()
implantTimers.values.foreach(_.cancel()) implantTimers.values.foreach(_.cancel())
supportExperienceTimer.cancel()
if (supportExperiencePool > 0) {
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool)
}
AvatarActor.saveAvatarData(avatar)
saveLockerFunc() saveLockerFunc()
AvatarActor.updateToolDischargeFor(avatar)
AvatarActor.saveExperienceDebt(avatar.id, experienceDebt)
AvatarActor.avatarNoLongerLoggedIn(account.get.id)
Behaviors.same Behaviors.same
} }
} }
@ -1805,9 +2010,26 @@ class AvatarActor(
Future.failed(ex).asInstanceOf[Future[Loadout]] Future.failed(ex).asInstanceOf[Future[Loadout]]
} }
def performAvatarLoginTest(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
import ctx._
val blockLogInIfNot = for {
out <- ctx.run(query[persistence.Account].filter(_.id == lift(accountId)))
} yield out
blockLogInIfNot.onComplete {
case Success(account)
//TODO test against acceptable player factions
if account.exists { _.avatarLoggedIn == 0 } =>
//accept
performAvatarLogin(avatarId, accountId, replyTo)
case _ =>
//refuse
//TODO refuse?
sessionActor ! SessionActor.Quit()
}
}
def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = { def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
import ctx._ import ctx._
val result = for { val result = for {
//log this login //log this login
_ <- ctx.run( _ <- ctx.run(
@ -1819,7 +2041,10 @@ class AvatarActor(
_ <- ctx.run( _ <- ctx.run(
query[persistence.Account] query[persistence.Account]
.filter(_.id == lift(accountId)) .filter(_.id == lift(accountId))
.update(_.lastFactionId -> lift(avatar.faction.id)) .update(
_.lastFactionId -> lift(avatar.faction.id),
_.avatarLoggedIn -> lift(avatarId)
)
) )
//retrieve avatar data //retrieve avatar data
loadouts <- initializeAllLoadouts() loadouts <- initializeAllLoadouts()
@ -1830,9 +2055,11 @@ class AvatarActor(
ignored <- loadIgnoredList(avatarId) ignored <- loadIgnoredList(avatarId)
shortcuts <- loadShortcuts(avatarId) shortcuts <- loadShortcuts(avatarId)
saved <- AvatarActor.loadSavedAvatarData(avatarId) saved <- AvatarActor.loadSavedAvatarData(avatarId)
} yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved) debt <- AvatarActor.loadExperienceDebt(avatarId)
card <- AvatarActor.loadCampaignKdaData(avatarId)
} yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved, debt, card)
result.onComplete { result.onComplete {
case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved)) => case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved, debt, card)) =>
//shortcuts must have a hotbar option for each implant //shortcuts must have a hotbar option for each implant
// val implantShortcuts = shortcutList.filter { // val implantShortcuts = shortcutList.filter {
// case Some(e) => e.purpose == 0 // case Some(e) => e.purpose == 0
@ -1866,11 +2093,13 @@ class AvatarActor(
cooldowns = Cooldowns( cooldowns = Cooldowns(
purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log), purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log) use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
) ),
scorecard = card
) )
) )
// if we need to start stamina regeneration // if we need to start stamina regeneration
tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) } tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) }
experienceDebt = debt
replyTo ! AvatarLoginResponse(avatar) replyTo ! AvatarLoginResponse(avatar)
case Failure(e) => case Failure(e) =>
log.error(e)("db failure") log.error(e)("db failure")
@ -2865,13 +3094,7 @@ class AvatarActor(
def setBep(bep: Long, modifier: ExperienceType): Unit = { def setBep(bep: Long, modifier: ExperienceType): Unit = {
import ctx._ import ctx._
val current = BattleRank.withExperience(avatar.bep).value AvatarActor.setBepOnly(avatar.id, bep).onComplete {
val next = BattleRank.withExperience(bep).value
lazy val br24 = BattleRank.BR24.value
val result = for {
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatar.id)).update(_.bep -> lift(bep)))
} yield r
result.onComplete {
case Success(_) => case Success(_) =>
val sess = session.get val sess = session.get
val zone = sess.zone val zone = sess.zone
@ -2880,6 +3103,9 @@ class AvatarActor(
val player = sess.player val player = sess.player
val pguid = player.GUID val pguid = player.GUID
val localModifier = modifier val localModifier = modifier
val current = BattleRank.withExperience(avatar.bep).value
val next = BattleRank.withExperience(bep).value
val br24 = BattleRank.BR24.value
sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, bep, localModifier)) sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, bep, localModifier))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep)) events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep))
if (current < br24 && next >= br24 || current >= br24 && next < br24) { if (current < br24 && next >= br24 || current >= br24 && next < br24) {
@ -2936,41 +3162,142 @@ class AvatarActor(
} }
} }
def updateKillsDeathsAssists(kdaStat: KDAStat): Unit = { def awardSupportExperience(bep: Long, previousDelay: Long): Unit = {
avatar.scorecard.rate(kdaStat) setBep(avatar.bep + bep, ExperienceType.Support) //todo simplify support testing
val exp = kdaStat.experienceEarned // supportExperiencePool = supportExperiencePool + bep
// avatar.scorecard.rate(bep)
// if (supportExperienceTimer.isCancelled) {
// resetSupportExperienceTimer(previousBep = 0, previousDelay = 0)
// }
}
def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = {
setBep(avatar.bep + bep, ExperienceType.Support)
supportExperiencePool = supportExperiencePool - bep
if (supportExperiencePool > 0) {
resetSupportExperienceTimer(bep, delayBy)
} else {
supportExperiencePool = 0
supportExperienceTimer.cancel()
supportExperienceTimer = Default.Cancellable
}
}
def updateKills(killStat: Kill): Unit = {
val exp = killStat.experienceEarned
val (modifiedExp, msg) = updateExperienceAndType(killStat.experienceEarned)
val output = 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
}).collect {
case mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons =>
player.ContributionFrom(mount)
}
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
)
output.foreach { case (id, entry) =>
val elem = StatisticalElement.fromId(id)
sessionActor ! SessionActor.SendResponse(
AvatarStatisticsMessage(SessionStatistic(
StatisticalCategory.Destroyed,
elem,
entry.tr_s,
entry.nc_s,
entry.vs_s,
entry.ps_s
))
)
}
if (exp > 0L) {
setBep(avatar.bep + exp, msg)
}
}
def updateDeaths(deathStat: Death): Unit = {
AvatarActor.updateToolDischargeFor(avatar)
avatar.scorecard.rate(deathStat)
val _session = session.get val _session = session.get
val zone = _session.zone val zone = _session.zone
val player = _session.player val player = _session.player
kdaStat match { zone.AvatarEvents ! AvatarServiceMessage(
case kill: Kill => player.Name,
val playerSource = PlayerSource(player) AvatarAction.SendResponse(
(kill.info.interaction.cause match { Service.defaultPlayerGUID,
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) } AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard)))
case _ => None )
}).collect { )
case Some(obj: Vitality) => }
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
} def updateAssists(assistStat: Assist): Unit = {
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp) avatar.scorecard.rate(assistStat)
case _: Death => val exp = assistStat.experienceEarned
zone.AvatarEvents ! AvatarServiceMessage( val _session = session.get
player.Name, val avatarId = avatar.id.toLong
AvatarAction.SendResponse( assistStat.weapons.foreach { wrapper =>
Service.defaultPlayerGUID, zones.exp.ToDatabase.reportKillAssistBy(
AvatarStatisticsMessage(DeathStatistic(avatar.scorecard.Lives.size)) avatarId,
) assistStat.victim.CharId,
) wrapper.equipment,
_session.zone.Number,
assistStat.victim.Position,
exp
)
} }
if (exp > 0L) { awardSupportExperience(exp, previousDelay = 0L)
val gameOpts = Config.app.game }
val (msg, modifier): (ExperienceType, Float) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
(ExperienceType.RabbitBall, 1.25f) def updateSupport(supportStat: SupportActivity): Unit = {
} else { val avatarId = avatar.id.toLong
(ExperienceType.Normal, 1f) val target = supportStat.target
} val targetId = target.CharId
setBep(avatar.bep + (exp * modifier * gameOpts.bepRate).toLong, msg) 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.experience.bep
val (modifier, msg) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
(1.25f, ExperienceType.RabbitBall)
} else {
(1f, ExperienceType.Normal)
}
((exp * modifier * gameOpts.rate).toLong, msg)
} }
def updateToolDischarge(stats: EquipmentStat): Unit = { def updateToolDischarge(stats: EquipmentStat): Unit = {
@ -3103,4 +3430,41 @@ class AvatarActor(
} }
output.future output.future
} }
def resetSupportExperienceTimer(previousBep: Long, previousDelay: Long): Unit = {
val bep: Long = if (supportExperiencePool < 10L) {
supportExperiencePool
} else {
val rand = math.random()
val range: Long = if (previousBep < 30L) {
if (rand < 0.3d) {
75L
} else {
215L
}
} else {
if (rand < 0.1d || (previousDelay > 35000L && previousBep > 150L)) {
75L
} else if (rand > 0.9d) {
520L
} else {
125L
}
}
math.min((range * math.random()).toLong, supportExperiencePool)
}
val delay: Long = {
val rand = math.random()
if ((previousBep > 190L || previousDelay > 35000L) && bep < 51L) {
(1000d * rand).toLong
} else {
10000L + (rand * 35000d).toLong
}
}
supportExperienceTimer = context.scheduleOnce(
delay.milliseconds,
context.self,
SupportExperienceDeposit(bep, delay)
)
}
} }

View file

@ -5,6 +5,7 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.ExecutionContextExecutor import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -76,7 +77,7 @@ object ChatActor {
): Unit = { ): Unit = {
if (silos.isEmpty) { if (silos.isEmpty) {
session ! SessionActor.SendResponse( session ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "Server", s"no targets for ntu found with parameters $debugContent", None) ChatMsg(UNK_229, wideContents=true, "Server", s"no targets for ntu found with parameters $debugContent", None)
) )
} }
resources match { resources match {
@ -225,7 +226,7 @@ class ChatActor(
} }
sessionActor ! SessionActor.SetFlying(flying) sessionActor ! SessionActor.SetFlying(flying)
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_FLY, false, recipient, if (flying) "on" else "off", None) ChatMsg(CMT_FLY, wideContents=false, recipient, if (flying) "on" else "off", None)
) )
case (CMT_ANONYMOUS, _, _) => case (CMT_ANONYMOUS, _, _) =>
@ -282,69 +283,43 @@ class ChatActor(
} }
errorMessage match { errorMessage match {
case Some(errorMessage) => case Some(errorMessage) =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, errorMessage))
ChatMsg(
CMT_QUIT,
false,
"",
errorMessage,
None
)
)
case None => case None =>
sessionActor ! SessionActor.Recall() sessionActor ! SessionActor.Recall()
} }
case (CMT_INSTANTACTION, _, _) => case (CMT_INSTANTACTION, _, _) =>
if (session.zoningType == Zoning.Method.Quit) { if (session.zoningType == Zoning.Method.Quit) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "You can't instant action while quitting."))
ChatMsg(CMT_QUIT, false, "", "You can't instant action while quitting.", None)
)
} else if (session.zoningType == Zoning.Method.InstantAction) { } else if (session.zoningType == Zoning.Method.InstantAction) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_instantactionting"))
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_instantactionting", None)
)
} else if (session.zoningType == Zoning.Method.Recall) { } else if (session.zoningType == Zoning.Method.Recall) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg( ChatMsg(CMT_QUIT, "You won't instant action. You already requested to recall to your sanctuary continent")
CMT_QUIT,
false,
"",
"You won't instant action. You already requested to recall to your sanctuary continent",
None
)
) )
} else if (!session.player.isAlive || session.deadState != DeadState.Alive) { } else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
if (session.player.isAlive) { if (session.player.isAlive) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_deconstructing"))
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_deconstructing", None)
)
} else { } else {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_dead"))
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_dead", None)
)
} }
} else if (session.player.VehicleSeated.nonEmpty) { } else if (session.player.VehicleSeated.nonEmpty) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_invehicle"))
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_invehicle", None)
)
} else { } else {
sessionActor ! SessionActor.InstantAction() sessionActor ! SessionActor.InstantAction()
} }
case (CMT_QUIT, _, _) => case (CMT_QUIT, _, _) =>
if (session.zoningType == Zoning.Method.Quit) { if (session.zoningType == Zoning.Method.Quit) {
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_quitting", None)) sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_quitting"))
} else if (!session.player.isAlive || session.deadState != DeadState.Alive) { } else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
if (session.player.isAlive) { if (session.player.isAlive) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_deconstructing"))
ChatMsg(CMT_QUIT, false, "", "@noquit_deconstructing", None)
)
} else { } else {
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_dead", None)) sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_dead"))
} }
} else if (session.player.VehicleSeated.nonEmpty) { } else if (session.player.VehicleSeated.nonEmpty) {
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_invehicle", None)) sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_invehicle"))
} else { } else {
sessionActor ! SessionActor.Quit() sessionActor ! SessionActor.Quit()
} }
@ -514,7 +489,7 @@ class ChatActor(
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg( ChatMsg(
UNK_229, UNK_229,
true, wideContents=true,
"", "",
s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.", s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.",
None None
@ -524,7 +499,7 @@ class ChatActor(
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg( ChatMsg(
UNK_229, UNK_229,
true, wideContents=true,
"", "",
s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.", s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.",
None None
@ -610,11 +585,11 @@ class ChatActor(
) )
} else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) { } else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_45, false, "none", "@notell_target", None) ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_target", None)
) )
} else { } else {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_45, false, "none", "@notell_ignore", None) ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_ignore", None)
) )
} }
@ -663,22 +638,20 @@ class ChatActor(
val popVS = players.count(_.faction == PlanetSideEmpire.VS) val popVS = players.count(_.faction == PlanetSideEmpire.VS)
if (popNC + popTR + popVS == 0) { if (popNC + popTR + popVS == 0) {
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.CMT_WHO, "@Nomatches"))
ChatMsg(ChatMessageType.CMT_WHO, false, "", "@Nomatches", None)
)
} else { } else {
val contName = session.zone.map.name val contName = session.zone.map.name
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None) ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "That command doesn't work for now, but : ", None)
) )
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None) ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "NC online : " + popNC + " on " + contName, None)
) )
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None) ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "TR online : " + popTR + " on " + contName, None)
) )
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None) ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "VS online : " + popVS + " on " + contName, None)
) )
} }
@ -702,16 +675,16 @@ class ChatActor(
} }
(zone, gate, list) match { (zone, gate, list) match {
case (None, None, true) => case (None, None, true) =>
sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, true, "", PointOfInterest.list, None)) sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.list, None))
case (Some(zone), None, true) => case (Some(zone), None, true) =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", PointOfInterest.listWarpgates(zone), None) ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listWarpgates(zone), None)
) )
case (Some(zone), Some(gate), false) => case (Some(zone), Some(gate), false) =>
sessionActor ! SessionActor.SetZone(zone.zonename, gate) sessionActor ! SessionActor.SetZone(zone.zonename, gate)
case (_, None, false) => case (_, None, false) =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", "Gate id not defined (use '/zone <zone> -list')", None) ChatMsg(UNK_229, wideContents=true, "", "Gate id not defined (use '/zone <zone> -list')", None)
) )
case (_, _, _) if buffer.isEmpty || buffer(0).equals("-help") => case (_, _, _) if buffer.isEmpty || buffer(0).equals("-help") =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
@ -740,16 +713,16 @@ class ChatActor(
zone match { zone match {
case Some(zone: PointOfInterest) => case Some(zone: PointOfInterest) =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", PointOfInterest.listAll(zone), None) ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listAll(zone), None)
) )
case _ => ChatMsg(UNK_229, true, "", s"unknown player zone '${session.player.Zone.id}'", None) case _ => ChatMsg(UNK_229, wideContents=true, "", s"unknown player zone '${session.player.Zone.id}'", None)
} }
case (None, Some(waypoint)) if waypoint != "-help" => case (None, Some(waypoint)) if waypoint != "-help" =>
PointOfInterest.getWarpLocation(session.zone.id, waypoint) match { PointOfInterest.getWarpLocation(session.zone.id, waypoint) match {
case Some(location) => sessionActor ! SessionActor.SetPosition(location) case Some(location) => sessionActor ! SessionActor.SetPosition(location)
case None => case None =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", s"unknown location '$waypoint'", None) ChatMsg(UNK_229, wideContents=true, "", s"unknown location '$waypoint'", None)
) )
} }
case _ => case _ =>
@ -759,60 +732,10 @@ class ChatActor(
} }
case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed => case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+") setBattleRank(message, contents, session, AvatarActor.SetBep)
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name =>
rank.toIntOption match {
case Some(rank) => (None, BattleRank.withValueOpt(rank))
case None => (None, None)
}
case (Some(target), Some(rank)) =>
// picking other targets is not supported for now
(None, None)
case (Some(rank), None) =>
rank.toIntOption match {
case Some(rank) => (None, BattleRank.withValueOpt(rank))
case None => (None, None)
}
case _ => (None, None)
}
(target, rank) match {
case (_, Some(rank)) =>
avatarActor ! AvatarActor.SetBep(rank.experience)
sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetBattleRank"))
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
)
}
case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed => case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+") setCommandRank(message, contents, session)
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name =>
rank.toIntOption match {
case Some(rank) => (None, CommandRank.withValueOpt(rank))
case None => (None, None)
}
case (Some(target), Some(rank)) =>
// picking other targets is not supported for now
(None, None)
case (Some(rank), None) =>
rank.toIntOption match {
case Some(rank) => (None, CommandRank.withValueOpt(rank))
case None => (None, None)
}
case _ => (None, None)
}
(target, rank) match {
case (_, Some(rank)) =>
avatarActor ! AvatarActor.SetCep(rank.experience)
sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetCommandRank"))
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
)
}
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed => case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
contents.toIntOption match { contents.toIntOption match {
@ -1031,20 +954,20 @@ class ChatActor(
if (session.player.silenced) { if (session.player.silenced) {
sessionActor ! SessionActor.SetSilenced(false) sessionActor ! SessionActor.SetSilenced(false)
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_229, true, "", "@silence_off", None) ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_off", None)
) )
if (!silenceTimer.isCancelled) silenceTimer.cancel() if (!silenceTimer.isCancelled) silenceTimer.cancel()
} else { } else {
sessionActor ! SessionActor.SetSilenced(true) sessionActor ! SessionActor.SetSilenced(true)
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_229, true, "", "@silence_on", None) ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_on", None)
) )
silenceTimer = context.system.scheduler.scheduleOnce( silenceTimer = context.system.scheduler.scheduleOnce(
time minutes, time minutes,
() => { () => {
sessionActor ! SessionActor.SetSilenced(false) sessionActor ! SessionActor.SetSilenced(false)
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_229, true, "", "@silence_timeout", None) ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_timeout", None)
) )
} }
) )
@ -1186,7 +1109,7 @@ class ChatActor(
if (contents.startsWith("!whitetext ") && gmCommandAllowed) { if (contents.startsWith("!whitetext ") && gmCommandAllowed) {
chatService ! ChatService.Message( chatService ! ChatService.Message(
session, session,
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None), ChatMsg(UNK_227, wideContents=true, "", contents.replace("!whitetext ", ""), None),
ChatChannel.Default() ChatChannel.Default()
) )
true true
@ -1350,6 +1273,13 @@ class ChatActor(
case _ => case _ =>
false false
} }
} else if (contents.startsWith("!progress")) {
if (!session.account.gm && BattleRank.withExperience(session.avatar.bep).value < Config.app.game.promotion.maxBattleRank + 1) {
setBattleRank(message, contents, session, AvatarActor.Progress)
true
} else {
false
}
} else { } else {
false // unknown ! commands are ignored false // unknown ! commands are ignored
} }
@ -1357,4 +1287,66 @@ class ChatActor(
false // unknown ! commands are ignored false // unknown ! commands are ignored
} }
} }
def setBattleRank(
message: ChatMsg,
contents: String,
session: Session,
msgFunc: Long => AvatarActor.Command
): Unit = {
val buffer = contents.toLowerCase.split("\\s+")
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name =>
rank.toIntOption match {
case Some(rank) => (None, BattleRank.withValueOpt(rank))
case None => (None, None)
}
case (Some(_), Some(_)) =>
// picking other targets is not supported for now
(None, None)
case (Some(rank), None) =>
rank.toIntOption match {
case Some(rank) => (None, BattleRank.withValueOpt(rank))
case None => (None, None)
}
case _ => (None, None)
}
(target, rank) match {
case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank =>
avatarActor ! msgFunc(rank.experience)
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
)
}
}
def setCommandRank(message: ChatMsg, contents: String, session: Session): Unit = {
val buffer = contents.toLowerCase.split("\\s+")
val (target, rank) = (buffer.lift(0), buffer.lift(1)) match {
case (Some(target), Some(rank)) if target == session.avatar.name =>
rank.toIntOption match {
case Some(rank) => (None, CommandRank.withValueOpt(rank))
case None => (None, None)
}
case (Some(_), Some(_)) =>
// picking other targets is not supported for now
(None, None)
case (Some(rank), None) =>
rank.toIntOption match {
case Some(rank) => (None, CommandRank.withValueOpt(rank))
case None => (None, None)
}
case _ => (None, None)
}
(target, rank) match {
case (_, Some(rank)) =>
avatarActor ! AvatarActor.SetCep(rank.experience)
//sessionActor ! SessionActor.SendResponse(message.copy(contents = "@AckSuccessSetCommandRank"))
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
)
}
}
} }

View file

@ -3,8 +3,11 @@ package net.psforever.actors.session.support
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.avatar.SpecialCarry
import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.objects.zones.exp
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@ -225,8 +228,6 @@ class SessionAvatarHandlers(
case AvatarResponse.DestroyDisplay(killer, victim, method, unk) case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
if killer.CharId == avatar.id && killer.Faction != victim.Faction => if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk)) sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
//TODO Temporary thing that should go somewhere else and use proper xp values
// avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
case AvatarResponse.Destroy(victim, killer, weapon, pos) => case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer // guid = victim // killer = killer
@ -398,6 +399,80 @@ class SessionAvatarHandlers(
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) => case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda) avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
case AvatarResponse.AwardBep(charId, bep, expType) =>
if (charId == player.CharId) {
avatarActor ! AvatarActor.AwardBep(bep, expType)
}
case AvatarResponse.AwardCep(charId, cep) =>
//if the target player, always award (some) CEP
if (charId == player.CharId) {
avatarActor ! AvatarActor.AwardCep(cep)
}
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
//must be in a squad to earn experience
val cepConfig = Config.app.game.experience.cep
val charId = player.CharId
val squadUI = sessionData.squad.squadUI
val participation = continent
.Building(buildingId)
.map { building =>
building.Participation.PlayerContribution()
}
squadUI
.find { _._1 == charId }
.collect {
case (_, elem) if elem.index == 0 =>
//squad leader earns CEP, modified by squad effort, capped by squad size present during the capture
val squadParticipation = participation match {
case Some(map) => map.filter { case (id, _) => squadUI.contains(id) }
case _ => Map.empty[Long, Float]
}
val maxCepBySquadSize: Long = {
val maxCepList = cepConfig.maximumPerSquadSize
val squadSize: Int = squadParticipation.size
maxCepList.lift(squadSize - 1).getOrElse(squadSize * maxCepList.head).toLong
}
val groupContribution: Float = squadUI
.map { case (id, _) => (id, squadParticipation.getOrElse(id, 0f) / 10f) }
.values
.max
val modifiedExp: Long = (cep.toFloat * groupContribution).toLong
val cappedModifiedExp: Long = math.min(modifiedExp, maxCepBySquadSize)
val finalExp: Long = if (modifiedExp > cappedModifiedExp) {
val overLimitOverflow = if (cepConfig.squadSizeLimitOverflow == -1) {
cep.toFloat
} else {
cepConfig.squadSizeLimitOverflow.toFloat
}
cappedModifiedExp + (overLimitOverflow * cepConfig.squadSizeLimitOverflowMultiplier).toLong
} else {
cappedModifiedExp
}
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, finalExp, expType="cep")
avatarActor ! AvatarActor.AwardCep(finalExp)
Some(finalExp)
case _ =>
//squad member earns BEP based on CEP, modified by personal effort
val individualContribution = {
val contributionList = for {
facilityMap <- participation
if facilityMap.contains(charId)
} yield facilityMap(charId)
if (contributionList.nonEmpty) {
contributionList.max
} else {
0f
}
}
val modifiedExp = (cep * individualContribution).toLong
exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep")
avatarActor ! AvatarActor.AwardFacilityCaptureBep(modifiedExp)
Some(modifiedExp)
}
case AvatarResponse.SendResponse(msg) => case AvatarResponse.SendResponse(msg) =>
sendResponse(msg) sendResponse(msg)
@ -412,16 +487,35 @@ class SessionAvatarHandlers(
case AvatarResponse.Killed(mount) => case AvatarResponse.Killed(mount) =>
//log and chat messages //log and chat messages
val cause = player.LastDamage.flatMap { damage => val cause = player.LastDamage.flatMap { damage =>
damage.interaction.cause match { val interaction = damage.interaction
case cause: ExplodingEntityReason if cause.entity.isInstanceOf[VehicleSpawnPad] => val reason = interaction.cause
val adversarial = interaction.adversarial.map { _.attacker }
reason match {
case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] =>
//also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..." //also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate"))
case _ => () case _ => ()
} }
damage match { adversarial.collect {
case damage if damage.adversarial.nonEmpty => Some(damage.adversarial.get.attacker.Name) case attacker
case damage => Some(s"a ${damage.interaction.cause.getClass.getSimpleName}") if player.Carrying.contains(SpecialCarry.CaptureFlag) &&
attacker.Faction != player.Faction &&
sessionData
.specialItemSlotGuid
.flatMap { continent.GUID }
.collect {
case llu: CaptureFlag =>
System.currentTimeMillis() - llu.LastCollectionTime > Config.app.game.experience.cep.lluSlayerCreditDuration.toMillis
case _ =>
false
}
.contains(true) =>
continent.AvatarEvents ! AvatarServiceMessage(
attacker.Name,
AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
)
} }
adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") }
}.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" } }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" }
log.info(s"${player.Name} has died, killed by $cause") log.info(s"${player.Name} has died, killed by $cause")
if (sessionData.shooting.shotsWhileDead > 0) { if (sessionData.shooting.shotsWhileDead > 0) {
@ -434,6 +528,7 @@ class SessionAvatarHandlers(
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
//player state changes //player state changes
AvatarActor.updateToolDischargeFor(avatar)
player.FreeHand.Equipment.foreach { item => player.FreeHand.Equipment.foreach { item =>
DropEquipmentFromInventory(player)(item) DropEquipmentFromInventory(player)(item)
} }
@ -448,7 +543,6 @@ class SessionAvatarHandlers(
} }
sessionData.playerActionsToCancel() sessionData.playerActionsToCancel()
sessionData.terminals.CancelAllProximityUnits() sessionData.terminals.CancelAllProximityUnits()
sessionData.zoning
AvatarActor.savePlayerLocation(player) AvatarActor.savePlayerLocation(player)
sessionData.zoning.spawn.shiftPosition = Some(player.Position) sessionData.zoning.spawn.shiftPosition = Some(player.Position)

View file

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

View file

@ -2,6 +2,9 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.InGameHistory
import scala.concurrent.duration._ import scala.concurrent.duration._
// //
import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.AvatarActor
@ -160,7 +163,7 @@ class SessionMountHandlers(
s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating" s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
) )
case Mountable.CanMount(obj: PlanetSideGameObject with WeaponTurret, seatNumber, _) => case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}") log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
@ -246,7 +249,7 @@ class SessionMountHandlers(
VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID) VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID)
) )
case Mountable.CanDismount(obj: PlanetSideGameObject with WeaponTurret, seatNum, _) => case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}") log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
DismountAction(tplayer, obj, seatNum) DismountAction(tplayer, obj, seatNum)
@ -276,7 +279,7 @@ class SessionMountHandlers(
* @param obj the mountable object * @param obj the mountable object
* @param seatNum the mount into which the player is mounting * @param seatNum the mount into which the player is mounting
*/ */
def MountingAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
val playerGuid: PlanetSideGUID = tplayer.GUID val playerGuid: PlanetSideGUID = tplayer.GUID
val objGuid: PlanetSideGUID = obj.GUID val objGuid: PlanetSideGUID = obj.GUID
sessionData.playerActionsToCancel() sessionData.playerActionsToCancel()
@ -295,7 +298,7 @@ class SessionMountHandlers(
* @param obj the mountable object * @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking * @param seatNum the mount out of which which the player is disembarking
*/ */
def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
DismountAction(tplayer, obj, seatNum) DismountAction(tplayer, obj, seatNum)
//until vehicles maintain synchronized momentum without a driver //until vehicles maintain synchronized momentum without a driver
obj match { obj match {
@ -332,8 +335,9 @@ class SessionMountHandlers(
* @param obj the mountable object * @param obj the mountable object
* @param seatNum the mount out of which which the player is disembarking * @param seatNum the mount out of which which the player is disembarking
*/ */
def DismountAction(tplayer: Player, obj: PlanetSideGameObject with Mountable, seatNum: Int): Unit = { def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
val playerGuid: PlanetSideGUID = tplayer.GUID val playerGuid: PlanetSideGUID = tplayer.GUID
tplayer.ContributionFrom(obj)
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
val bailType = if (tplayer.BailProtection) { val bailType = if (tplayer.BailProtection) {
BailType.Bailed BailType.Bailed

View file

@ -2,6 +2,9 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.sourcing.AmenitySource
import net.psforever.objects.vital.TerminalUsedActivity
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future import scala.concurrent.Future
// //
@ -31,7 +34,8 @@ class SessionTerminalHandlers(
val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
continent.GUID(terminalGuid) match { continent.GUID(terminalGuid) match {
case Some(term: Terminal) if lastTerminalOrderFulfillment => case Some(term: Terminal) if lastTerminalOrderFulfillment =>
log.info(s"${player.Name} is submitting an order - $transactionType of $itemName") val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
lastTerminalOrderFulfillment = false lastTerminalOrderFulfillment = false
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
term.Actor ! Terminal.Request(player, pkt) term.Actor ! Terminal.Request(player, pkt)
@ -68,8 +72,8 @@ class SessionTerminalHandlers(
order match { order match {
case Terminal.BuyEquipment(item) case Terminal.BuyEquipment(item)
if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty => if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
lastTerminalOrderFulfillment = true
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
lastTerminalOrderFulfillment = true
case Terminal.BuyEquipment(item) => case Terminal.BuyEquipment(item) =>
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
@ -141,6 +145,7 @@ class SessionTerminalHandlers(
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid)) sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
} }
player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
}.orElse { }.orElse {
log.error( log.error(
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it" s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"

View file

@ -2,6 +2,8 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future import scala.concurrent.Future
@ -43,7 +45,8 @@ private[support] class WeaponAndProjectileOperations(
var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start
private[support] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[support] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
private[support] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[support] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
private var ongoingShotsFired: Int = 0 private val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
private val shotsLanded: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
private[support] var shotsWhileDead: Int = 0 private[support] var shotsWhileDead: Int = 0
private val projectiles: Array[Option[Projectile]] = private val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
@ -157,7 +160,7 @@ private[support] class WeaponAndProjectileOperations(
fireStateStopPlayerMessages(item_guid) fireStateStopPlayerMessages(item_guid)
case Some(_) => case Some(_) =>
fireStateStopMountedMessages(item_guid) fireStateStopMountedMessages(item_guid)
case _ => () case _ =>
log.warn(s"ChangeFireState_Stop: can not find $item_guid") log.warn(s"ChangeFireState_Stop: can not find $item_guid")
} }
sessionData.progressBarUpdate.cancel() sessionData.progressBarUpdate.cancel()
@ -275,8 +278,8 @@ private[support] class WeaponAndProjectileOperations(
val LongRangeProjectileInfoMessage(guid, _, _) = pkt val LongRangeProjectileInfoMessage(guid, _, _) = pkt
FindContainedWeapon match { FindContainedWeapon match {
case (Some(_: Vehicle), weapons) case (Some(_: Vehicle), weapons)
if weapons.exists { _.GUID == guid } => ; //now what? if weapons.exists { _.GUID == guid } => () //now what?
case _ => ; case _ => ()
} }
} }
@ -329,13 +332,11 @@ private[support] class WeaponAndProjectileOperations(
_: Vector3, _: Vector3,
hitPos: Vector3 hitPos: Vector3
) => ) =>
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos) match { ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
case Some(resprojectile) => addShotsLanded(resprojectile.cause.attribution, shots = 1)
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) sessionData.handleDealingDamage(target, resprojectile)
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
} }
case _ => ; case _ => ()
} }
case None => case None =>
log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
@ -368,26 +369,22 @@ private[support] class WeaponAndProjectileOperations(
sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target) CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match { ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
case Some(resprojectile) => addShotsLanded(resprojectile.cause.attribution, shots = 1)
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) sessionData.handleDealingDamage(target, resprojectile)
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
} }
case _ => ; case _ => ()
} }
//other victims //other victims
targets.foreach(elem => { targets.foreach(elem => {
sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match { sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match { ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
case Some(resprojectile) => addShotsLanded(resprojectile.cause.attribution, shots = 1)
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) sessionData.handleDealingDamage(target, resprojectile)
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
} }
case _ => ; case _ => ()
} }
}) })
//... //...
@ -413,7 +410,7 @@ private[support] class WeaponAndProjectileOperations(
continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
} }
} }
case None => ; case None => ()
} }
} }
@ -422,13 +419,12 @@ private[support] class WeaponAndProjectileOperations(
sessionData.validObject(victim_guid, decorator = "Lash") match { sessionData.validObject(victim_guid, decorator = "Lash") match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target) CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos) match { ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach {
case Some(resprojectile) => resprojectile =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0)) addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionData.handleDealingDamage(target, resprojectile) sessionData.handleDealingDamage(target, resprojectile)
case None => ;
} }
case _ => ; case _ => ()
} }
} }
@ -524,7 +520,7 @@ private[support] class WeaponAndProjectileOperations(
obj match { obj match {
case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret => case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
turret.Actor ! FacilityTurret.WeaponDischarged() turret.Actor ! FacilityTurret.WeaponDischarged()
case _ => ; case _ => ()
} }
} else { } else {
log.warn( log.warn(
@ -532,7 +528,7 @@ private[support] class WeaponAndProjectileOperations(
) )
} }
case _ => ; case _ => ()
} }
} }
@ -552,7 +548,7 @@ private[support] class WeaponAndProjectileOperations(
case Some(v: Vehicle) => case Some(v: Vehicle) =>
//assert subsystem states //assert subsystem states
v.SubsystemMessages().foreach { sendResponse } v.SubsystemMessages().foreach { sendResponse }
case _ => ; case _ => ()
} }
} }
if (enabledTools.nonEmpty) { if (enabledTools.nonEmpty) {
@ -577,8 +573,9 @@ private[support] class WeaponAndProjectileOperations(
avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina) avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina)
} }
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
tool.Discharge()
prefire += weaponGUID prefire += weaponGUID
ongoingShotsFired = ongoingShotsFired + tool.Discharge() addShotsFired(tool.Definition.ObjectId, tool.AmmoSlot.Chamber)
} }
(o, Some(tool)) (o, Some(tool))
} }
@ -635,7 +632,7 @@ private[support] class WeaponAndProjectileOperations(
continent.GUID(weapon_guid) match { continent.GUID(weapon_guid) match {
case Some(tool: Tool) => case Some(tool: Tool) =>
EmptyMagazine(weapon_guid, tool) EmptyMagazine(weapon_guid, tool)
case _ => ; case _ => ()
} }
} }
@ -719,19 +716,17 @@ private[support] class WeaponAndProjectileOperations(
*/ */
def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
ModifyAmmunition(obj)(box, reloadValue) ModifyAmmunition(obj)(box, reloadValue)
obj.Find(box) match { obj.Find(box).collect { index =>
case Some(index) => continent.VehicleEvents ! VehicleServiceMessage(
continent.VehicleEvents ! VehicleServiceMessage( s"${obj.Actor}",
s"${obj.Actor}", VehicleAction.InventoryState(
VehicleAction.InventoryState( player.GUID,
player.GUID, box,
box, obj.GUID,
obj.GUID, index,
index, box.Definition.Packet.DetailedConstructorData(box).get
box.Definition.Packet.DetailedConstructorData(box).get
)
) )
case None => ; )
} }
} }
@ -849,7 +844,7 @@ private[support] class WeaponAndProjectileOperations(
sessionData.normalItemDrop(player, continent)(previousBox) sessionData.normalItemDrop(player, continent)(previousBox)
} }
AmmoBox.Split(previousBox) match { AmmoBox.Split(previousBox) match {
case Nil | List(_) => ; //done (the former case is technically not possible) case Nil | List(_) => () //done (the former case is technically not possible)
case _ :: toUpdate => case _ :: toUpdate =>
modifyFunc(previousBox, 0) //update to changed capacity value modifyFunc(previousBox, 0) //update to changed capacity value
toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) }) toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) })
@ -1151,7 +1146,6 @@ private[support] class WeaponAndProjectileOperations(
prefire -= itemGuid prefire -= itemGuid
shooting += itemGuid shooting += itemGuid
shootingStart += itemGuid -> System.currentTimeMillis() shootingStart += itemGuid -> System.currentTimeMillis()
ongoingShotsFired = 0
} }
private def fireStateStartChargeMode(tool: Tool): Unit = { private def fireStateStartChargeMode(tool: Tool): Unit = {
@ -1220,11 +1214,10 @@ private[support] class WeaponAndProjectileOperations(
used by ChangeFireStateMessage_Stop handling used by ChangeFireStateMessage_Stop handling
*/ */
private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = {
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId, ongoingShotsFired, 0, 0))
tool.FireMode match { tool.FireMode match {
case _: ChargeFireModeDefinition => case _: ChargeFireModeDefinition =>
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
case _ => ; case _ => ()
} }
if (tool.Magazine == 0) { if (tool.Magazine == 0) {
FireCycleCleanup(tool) FireCycleCleanup(tool)
@ -1365,6 +1358,49 @@ private[support] class WeaponAndProjectileOperations(
) )
} }
private def addShotsFired(weaponId: Int, shots: Int): Unit = {
addShotsToMap(shotsFired, weaponId, shots)
}
private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
addShotsToMap(shotsLanded, weaponId, shots)
}
private def addShotsToMap(map: mutable.HashMap[Int, Int], weaponId: Int, shots: Int): Unit = {
map.put(
weaponId,
map.get(weaponId) match {
case Some(previousShots) => previousShots + shots
case None => shots
}
)
}
private[support] def reportOngoingShots(reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
reportOngoingShots(player.CharId, reportFunc)
}
private[support] def reportOngoingShots(avatarId: Long, reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
//only shots that have been reported as fired count
//if somehow shots had reported as landed but never reported as fired, they are ignored
//these are just raw counts; there's only numeric connection between the entries of fired and of landed
shotsFired.foreach { case (weaponId, fired) =>
val landed = math.min(shotsLanded.getOrElse(weaponId, 0), fired)
reportFunc(avatarId, weaponId, fired, landed)
}
shotsFired.clear()
shotsLanded.clear()
}
//noinspection ScalaUnusedSymbol
private[support] def reportOngoingShotsToAvatar(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = {
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(weaponId, fired, landed, 0, 0))
}
private[support] def reportOngoingShotsToDatabase(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = {
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
}
override protected[session] def stop(): Unit = { override protected[session] def stop(): Unit = {
if (player != null && player.HasGUID) { if (player != null && player.HasGUID) {
(prefire ++ shooting).foreach { guid => (prefire ++ shooting).foreach { guid =>
@ -1373,6 +1409,7 @@ private[support] class WeaponAndProjectileOperations(
fireStateStopMountedMessages(guid) fireStateStopMountedMessages(guid)
} }
projectiles.indices.foreach { projectiles.update(_, None) } projectiles.indices.foreach { projectiles.update(_, None) }
reportOngoingShots(reportOngoingShotsToDatabase)
} }
} }
} }

View file

@ -7,11 +7,14 @@ import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import net.psforever.login.WorldSession import net.psforever.login.WorldSession
import net.psforever.objects.avatar.BattleRank
import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics}
import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.mount.Seat import net.psforever.objects.serverobject.mount.Seat
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, ReconstructionActivity, SpawningActivity} import net.psforever.objects.vital.{InGameHistory, ReconstructionActivity, SpawningActivity}
import net.psforever.packet.game.{CampaignStatistic, MailMessage, SessionStatistic}
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -69,6 +72,30 @@ import net.psforever.zones.Zones
object ZoningOperations { object ZoningOperations {
private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20) private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20)
def reportProgressionSystem(sessionActor: ActorRef): Unit = {
sessionActor ! SessionActor.SendResponse(
MailMessage(
"High Command",
"Progress versus Promotion",
"If you consider yourself as a veteran soldier, despite looking so green, please read this.\n" ++
"You only have this opportunity while you are battle rank 1." ++
"\n\n" ++
"The normal method of rank advancement comes from progress on the battlefield - fighting enemies, helping allies, and capturing facilities. " ++
"\n\n" ++
s"You may, however, rapidly promote yourself to at most battle rank ${Config.app.game.promotion.maxBattleRank}. " ++
"You have access to all of the normal benefits, certification points, implants, etc., of your chosen rank. " ++
"However, that experience that you have skipped will count as PROMOTION DEBT. " ++
"You will not advance any further until you earn that experience back through support activity and engaging in facility capture. " ++
"The amount of experience required and your own effort will determine how long it takes. " ++
"In addition, you will be ineligible of having your command experience be recognized during this time." ++
"\n\n" ++
"If you wish to continue, set your desired battle rank now - use '!progress' followed by a battle rank index. " ++
"If you accept, but it becomes too much of burden, you may ask to revert to battle rank 2 at any time. " ++
"Your normal sense of progress will be restored."
)
)
}
} }
class ZoningOperations( class ZoningOperations(
@ -258,7 +285,7 @@ class ZoningOperations(
obj.GUID, obj.GUID,
Deployable.Icon(obj.Definition.Item), Deployable.Icon(obj.Definition.Item),
obj.Position, obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0)) obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
) )
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
}) })
@ -1657,6 +1684,8 @@ class ZoningOperations(
private[support] var reviveTimer: Cancellable = Default.Cancellable private[support] var reviveTimer: Cancellable = Default.Cancellable
private[support] var respawnTimer: Cancellable = Default.Cancellable private[support] var respawnTimer: Cancellable = Default.Cancellable
private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields
/* packets */ /* packets */
def handleReleaseAvatarRequest(pkt: ReleaseAvatarRequestMessage): Unit = { def handleReleaseAvatarRequest(pkt: ReleaseAvatarRequestMessage): Unit = {
@ -1803,6 +1832,7 @@ class ZoningOperations(
sessionData.persistFunc = UpdatePersistence(from) sessionData.persistFunc = UpdatePersistence(from)
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself //tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
inZone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.TeardownConnection()) inZone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.TeardownConnection())
spawn.switchAvatarStatisticsFieldToRefreshAfterRespawn()
//find and reload previous player //find and reload previous player
( (
inZone.Players.find(p => p.name.equals(name)), inZone.Players.find(p => p.name.equals(name)),
@ -2342,6 +2372,7 @@ class ZoningOperations(
player.avatar = player.avatar.copy(stamina = avatar.maxStamina) player.avatar = player.avatar.copy(stamina = avatar.maxStamina)
avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina) avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina)
avatarActor ! AvatarActor.ResetImplants() avatarActor ! AvatarActor.ResetImplants()
zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
val obj = Player.Respawn(tplayer) val obj = Player.Respawn(tplayer)
DefinitionUtil.applyDefaultLoadout(obj) DefinitionUtil.applyDefaultLoadout(obj)
obj.death_by = tplayer.death_by obj.death_by = tplayer.death_by
@ -2614,10 +2645,11 @@ class ZoningOperations(
LoadZoneAsPlayer(newPlayer, zoneId) LoadZoneAsPlayer(newPlayer, zoneId)
} else { } else {
avatarActor ! AvatarActor.DeactivateActiveImplants() avatarActor ! AvatarActor.DeactivateActiveImplants()
val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, toSpawnPoint) InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, betterSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint) InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneInVehicle(vehicle, pos, ori, zoneId) LoadZoneInVehicle(vehicle, pos, ori, zoneId)
case _ if player.HasGUID => // player is deconstructing self or instant action case _ if player.HasGUID => // player is deconstructing self or instant action
@ -2629,13 +2661,13 @@ class ZoningOperations(
) )
player.Position = pos player.Position = pos
player.Orientation = ori player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint) InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId) LoadZoneAsPlayer(player, zoneId)
case _ => //player is logging in case _ => //player is logging in
player.Position = pos player.Position = pos
player.Orientation = ori player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint) InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId) LoadZoneAsPlayer(player, zoneId)
} }
} }
@ -2803,10 +2835,7 @@ class ZoningOperations(
.foreach { case (_, building) => .foreach { case (_, building) =>
sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0 /*building.BuildingType == StructureType.Facility*/)) sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0 /*building.BuildingType == StructureType.Facility*/))
} }
(0 to 30).foreach(_ => { statisticsPacketFunc()
//TODO 30 for a new character only?
sendResponse(AvatarStatisticsMessage(DeathStatistic(0L)))
})
if (tplayer.ExoSuit == ExoSuitType.MAX) { if (tplayer.ExoSuit == ExoSuitType.MAX) {
sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong)) sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong))
} }
@ -2823,7 +2852,7 @@ class ZoningOperations(
continent.DeployableList continent.DeployableList
.filter(_.OwnerName.contains(name)) .filter(_.OwnerName.contains(name))
.foreach(obj => { .foreach(obj => {
obj.Owner = guid obj.OwnerGuid = guid
drawDeloyableIcon(obj) drawDeloyableIcon(obj)
}) })
drawDeloyableIcon = DontRedrawIcons drawDeloyableIcon = DontRedrawIcons
@ -2831,7 +2860,7 @@ class ZoningOperations(
//assert or transfer vehicle ownership //assert or transfer vehicle ownership
continent.GUID(player.avatar.vehicle) match { continent.GUID(player.avatar.vehicle) match {
case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) => case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
vehicle.Owner = guid vehicle.OwnerGuid = guid
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
s"${tplayer.Faction}", s"${tplayer.Faction}",
VehicleAction.Ownership(guid, vehicle.GUID) VehicleAction.Ownership(guid, vehicle.GUID)
@ -2906,7 +2935,7 @@ class ZoningOperations(
val effortBy = nextSpawnPoint val effortBy = nextSpawnPoint
.collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) } .collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) }
.collect { .collect {
case (_, Some(v: Vehicle)) => continent.GUID(v.Owner) case (_, Some(v: Vehicle)) => continent.GUID(v.OwnerGuid)
case (sp, Some(_: Building)) => Some(sp) case (sp, Some(_: Building)) => Some(sp)
} }
.collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) } .collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) }
@ -2918,11 +2947,15 @@ class ZoningOperations(
SpawningActivity(PlayerSource(player), continent.Number, effortBy) SpawningActivity(PlayerSource(player), continent.Number, effortBy)
} }
}) })
//ride
} }
upstreamMessageCount = 0 upstreamMessageCount = 0
setAvatar = true setAvatar = true
if (
BattleRank.withExperience(tplayer.avatar.bep).value == 1 &&
Config.app.game.promotion.active &&
!account.gm) {
ZoningOperations.reportProgressionSystem(context.self)
}
} }
/** /**
@ -2961,7 +2994,7 @@ class ZoningOperations(
obj.GUID, obj.GUID,
Deployable.Icon(obj.Definition.Item), Deployable.Icon(obj.Definition.Item),
obj.Position, obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0)) obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
) )
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo)) sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
} }
@ -3164,6 +3197,64 @@ class ZoningOperations(
sendResponse(mountPointStatusMessage) sendResponse(mountPointStatusMessage)
} }
/**
* Populate the Character Info window's statistics page during login.
* Always send campaign (historical, total) statistics.
* Set to refresh the statistics fields after each respawn from now on.
*/
private def loginAvatarStatisticsFields(): Unit = {
avatar.scorecard.KillStatistics.foreach { case (id, stat) =>
val campaign = CampaignStatistics(stat)
val elem = StatisticalElement.fromId(id)
sendResponse(AvatarStatisticsMessage(
CampaignStatistic(StatisticalCategory.Destroyed, elem, campaign.tr, campaign.nc, campaign.vs, campaign.ps)
))
}
//originally the client sent a death statistic update in between each change of statistic categories, about 30 times
sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard))))
statisticsPacketFunc = respawnAvatarStatisticsFields
}
/**
* Populate the Character Info window's statistics page after each respawn.
* Check whether to send session-related data, or campaign-related data, or both.
*/
private def respawnAvatarStatisticsFields(): Unit = {
avatar
.scorecard
.KillStatistics
.flatMap { case (id, stat) =>
val campaign = CampaignStatistics(stat)
val session = SessionStatistics(stat)
(StatisticalElement.fromId(id), campaign.total, campaign, session.total, session) match {
case (elem, 0, _, _, session) =>
Seq(SessionStatistic(StatisticalCategory.Destroyed, elem, session.tr, session.nc, session.vs, session.ps))
case (elem, _, campaign, 0, _) =>
Seq(CampaignStatistic(StatisticalCategory.Destroyed, elem, campaign.tr, campaign.nc, campaign.vs, campaign.ps))
case (elem, _, campaign, _, session) =>
Seq(
CampaignStatistic(StatisticalCategory.Destroyed, elem, campaign.tr, campaign.nc, campaign.vs, campaign.ps),
SessionStatistic(StatisticalCategory.Destroyed, elem, session.tr, session.nc, session.vs, session.ps)
)
}
}
.foreach { statistics =>
sendResponse(AvatarStatisticsMessage(statistics))
}
//originally the client sent a death statistic update in between each change of statistic categories, about 30 times
sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard))))
}
/**
* Accessible method to switch population of the Character Info window's statistics page
* from whatever it currently js to after each respawn.
* At the time of "login", only campaign (total, historical) deaths are reported for convenience.
* At the time of "respawn", all fields - campaign and session - should be reported if applicable.
*/
def switchAvatarStatisticsFieldToRefreshAfterRespawn(): Unit = {
statisticsPacketFunc = respawnAvatarStatisticsFields
}
/** /**
* Don't extract the award advancement information from a player character upon respawning or zoning. * Don't extract the award advancement information from a player character upon respawning or zoning.
* You only need to perform that population once at login. * You only need to perform that population once at login.

View file

@ -15,7 +15,7 @@ import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{InGameActivity, InGameHistory} import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.exp.ExperienceCalculator import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
import net.psforever.util.Database._ import net.psforever.util.Database._
import net.psforever.persistence import net.psforever.persistence
@ -87,6 +87,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
private[this] val log = org.log4s.getLogger private[this] val log = org.log4s.getLogger
private val players: mutable.ListBuffer[Player] = mutable.ListBuffer() private val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
private val experience: ActorRef[ExperienceCalculator.Command] = context.spawnAnonymous(ExperienceCalculator(zone)) private val experience: ActorRef[ExperienceCalculator.Command] = context.spawnAnonymous(ExperienceCalculator(zone))
private val supportExperience: ActorRef[SupportExperienceCalculator.Command] = context.spawnAnonymous(SupportExperienceCalculator(zone))
zone.actor = context.self zone.actor = context.self
zone.init(context.toClassic) zone.init(context.toClassic)
@ -154,7 +155,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
experience ! ExperienceCalculator.RewardThisDeath(entity) experience ! ExperienceCalculator.RewardThisDeath(entity)
case RewardOurSupporters(target, history, kill, bep) => case RewardOurSupporters(target, history, kill, bep) =>
() supportExperience ! SupportExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
case ZoneMapUpdate() => case ZoneMapUpdate() =>
zone.Buildings zone.Buildings
@ -162,7 +163,6 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
.values .values
.foreach(_.Actor ! BuildingActor.MapUpdate()) .foreach(_.Actor ! BuildingActor.MapUpdate())
} }
this this
} }
} }

View file

@ -942,8 +942,9 @@ object WorldSession {
result: Boolean result: Boolean
): Unit = { ): Unit = {
if (result) { if (result) {
player.Zone.GUID(guid).collect { player.Zone.GUID(guid).collect { case term: Terminal =>
case term: Terminal => player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction)) player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
player.ContributionFrom(term)
} }
} }
player.Zone.AvatarEvents ! AvatarServiceMessage( player.Zone.AvatarEvents ! AvatarServiceMessage(

View file

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

View file

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

View file

@ -348,7 +348,7 @@ object MineDeployableControl {
jumping = false, jumping = false,
ExoSuitDefinition.Select(exosuit, faction), ExoSuitDefinition.Select(exosuit, faction),
bep = 0, bep = 0,
kills = Nil, progress = PlayerSource.Nobody.progress,
UniquePlayer(charId, name, CharacterSex.Male, mine.Faction) UniquePlayer(charId, name, CharacterSex.Male, mine.Faction)
) )
case None => case None =>

View file

@ -9847,6 +9847,7 @@ object GlobalDefinitions {
resource_silo.Damageable = false resource_silo.Damageable = false
resource_silo.Repairable = false resource_silo.Repairable = false
resource_silo.MaxNtuCapacitor = 1000 resource_silo.MaxNtuCapacitor = 1000
resource_silo.ChargeTime = 105.seconds //from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402)
capture_terminal.Name = "capture_terminal" capture_terminal.Name = "capture_terminal"
capture_terminal.Damageable = false capture_terminal.Damageable = false
@ -9856,7 +9857,7 @@ object GlobalDefinitions {
secondary_capture.Name = "secondary_capture" secondary_capture.Name = "secondary_capture"
secondary_capture.Damageable = false secondary_capture.Damageable = false
secondary_capture.Repairable = false secondary_capture.Repairable = false
secondary_capture.FacilityHackTime = 1.nanosecond secondary_capture.FacilityHackTime = 1.millisecond
vanu_control_console.Name = "vanu_control_console" vanu_control_console.Name = "vanu_control_console"
vanu_control_console.Damageable = false vanu_control_console.Damageable = false

View file

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

View file

@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.serverobject.environment.InteractWithEnvironment import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.serverobject.mount.MountableEntity import net.psforever.objects.serverobject.mount.MountableEntity
import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFromEquipment, Vitality}
import net.psforever.objects.vital.damage.DamageProfile import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -110,6 +110,7 @@ class Player(var avatar: Avatar)
Health = Definition.DefaultHealth Health = Definition.DefaultHealth
Armor = MaxArmor Armor = MaxArmor
Capacitor = 0 Capacitor = 0
avatar.scorecard.respawn()
released = false released = false
} }
isAlive isAlive
@ -124,13 +125,16 @@ class Player(var avatar: Avatar)
def Revive: Boolean = { def Revive: Boolean = {
Destroyed = false Destroyed = false
Health = Definition.DefaultHealth Health = Definition.DefaultHealth
avatar.scorecard.revive()
released = false released = false
true true
} }
def Release: Boolean = { def Release: Boolean = {
released = true if (!released) {
backpack = !isAlive released = true
backpack = !isAlive
}
true true
} }
@ -422,12 +426,13 @@ class Player(var avatar: Avatar)
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value = def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
usingSpecial(state) usingSpecial(state)
//noinspection ScalaUnusedSymbol
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value = private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
SpecialExoSuitDefinition.Mode.Normal SpecialExoSuitDefinition.Mode.Normal
private def UsingAnchorsOrOverdrive( private def UsingAnchorsOrOverdrive(
state: SpecialExoSuitDefinition.Mode.Value state: SpecialExoSuitDefinition.Mode.Value
): SpecialExoSuitDefinition.Mode.Value = { ): SpecialExoSuitDefinition.Mode.Value = {
import SpecialExoSuitDefinition.Mode._ import SpecialExoSuitDefinition.Mode._
val curr = UsingSpecial val curr = UsingSpecial
val next = if (curr == Normal) { val next = if (curr == Normal) {
@ -532,11 +537,14 @@ class Player(var avatar: Avatar)
def Carrying: Option[SpecialCarry] = carrying def Carrying: Option[SpecialCarry] = carrying
//noinspection ScalaUnusedSymbol
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = { def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
Carrying Carrying_=(Some(item))
} }
//noinspection ScalaUnusedSymbol
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = { def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
carrying = item
Carrying Carrying
} }
@ -553,6 +561,14 @@ class Player(var avatar: Avatar)
def DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel] def DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel]
override def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
val earliestEndTime = System.currentTimeMillis() - duration
History.collect {
case heal: HealFromEquipment if heal.amount > 0 && heal.time > earliestEndTime => heal
case repair: RepairFromEquipment if repair.amount > 0 && repair.time > earliestEndTime => repair
}
}
def canEqual(other: Any): Boolean = other.isInstanceOf[Player] def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
override def equals(other: Any): Boolean = override def equals(other: Any): Boolean =
@ -614,12 +630,14 @@ object Player {
if (player.Release) { if (player.Release) {
val obj = new Player(player.avatar) val obj = new Player(player.avatar)
obj.Continent = player.Continent obj.Continent = player.Continent
obj.avatar.scorecard.respawn()
obj obj
} else { } else {
player player
} }
} }
//noinspection ScalaUnusedSymbol
def neverRestrict(player: Player, slot: Int): Boolean = { def neverRestrict(player: Player, slot: Int): Boolean = {
false false
} }

View file

@ -10,6 +10,8 @@ import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.InfantryLoadout import net.psforever.objects.loadouts.InfantryLoadout
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.RevivingActivity
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3} import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
@ -64,7 +66,7 @@ object Players {
val name = target.Name val name = target.Name
val medicName = medic.Name val medicName = medic.Name
log.info(s"$medicName had revived $name") log.info(s"$medicName had revived $name")
//target.History(PlayerRespawn(PlayerSource(target), target.Zone, target.Position, Some(PlayerSource(medic)))) target.LogActivity(RevivingActivity(PlayerSource(target), PlayerSource(medic), target.MaxHealth, item.Definition))
val magazine = item.Discharge(Some(25)) val magazine = item.Discharge(Some(25))
target.Zone.AvatarEvents ! AvatarServiceMessage( target.Zone.AvatarEvents ! AvatarServiceMessage(
medicName, medicName,

View file

@ -3,7 +3,7 @@ package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props} import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory} import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployableCategory}
import net.psforever.objects.definition.DeployableDefinition import net.psforever.objects.definition.{DeployableDefinition, WithShields}
import net.psforever.objects.definition.converter.ShieldGeneratorConverter import net.psforever.objects.definition.converter.ShieldGeneratorConverter
import net.psforever.objects.equipment.{JammableBehavior, JammableUnit} import net.psforever.objects.equipment.{JammableBehavior, JammableUnit}
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable.Target
@ -22,7 +22,8 @@ class ShieldGeneratorDeployable(cdef: ShieldGeneratorDefinition)
with Hackable with Hackable
with JammableUnit with JammableUnit
class ShieldGeneratorDefinition extends DeployableDefinition(240) { class ShieldGeneratorDefinition extends DeployableDefinition(240)
with WithShields {
Packet = new ShieldGeneratorConverter Packet = new ShieldGeneratorConverter
DeployCategory = DeployableCategory.ShieldGenerators DeployCategory = DeployableCategory.ShieldGenerators

View file

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

View file

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

View file

@ -99,7 +99,7 @@ class Tool(private val toolDef: ToolDefinition)
} }
def Discharge(rounds: Option[Int] = None): Int = { def Discharge(rounds: Option[Int] = None): Int = {
lastDischarge = System.nanoTime() lastDischarge = System.currentTimeMillis()
Magazine = FireMode.Discharge(this, rounds) Magazine = FireMode.Discharge(this, rounds)
} }

View file

@ -95,6 +95,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20)) interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var previousFaction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var shields: Int = 0 private var shields: Int = 0
private var decal: Int = 0 private var decal: Int = 0
private var trunkAccess: Option[PlanetSideGUID] = None private var trunkAccess: Option[PlanetSideGUID] = None
@ -128,14 +129,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
} }
def Faction: PlanetSideEmpire.Value = { def Faction: PlanetSideEmpire.Value = {
this.faction
}
override def Faction_=(faction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
this.faction = faction
faction faction
} }
override def Faction_=(toFaction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
//TODO in the future, this may create an issue when the vehicle is originally or is hacked from Black Ops
previousFaction = faction
faction = toFaction
toFaction
}
def PreviousFaction: PlanetSideEmpire.Value = previousFaction
/** How long it takes to jack the vehicle in seconds, based on the hacker's certification level */ /** How long it takes to jack the vehicle in seconds, based on the hacker's certification level */
def JackingDuration: Array[Int] = Definition.JackingDuration def JackingDuration: Array[Int] = Definition.JackingDuration
@ -267,30 +272,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
} }
def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = { def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = {
if (seatNumber == 0) { //valid in almost all cases Vehicles.SeatPermissionGroup(this.Definition, seatNumber)
Some(AccessPermissionGroup.Driver)
} else {
Seat(seatNumber) match {
case Some(_) =>
Definition.controlledWeapons().get(seatNumber) match {
case Some(_) =>
Some(AccessPermissionGroup.Gunner)
case None =>
Some(AccessPermissionGroup.Passenger)
}
case None =>
CargoHold(seatNumber) match {
case Some(_) =>
Some(AccessPermissionGroup.Passenger) //TODO confirm this
case None =>
if (seatNumber >= trunk.Offset && seatNumber < trunk.Offset + trunk.TotalCapacity) {
Some(AccessPermissionGroup.Trunk)
} else {
None
}
}
}
}
} }
def Utilities: Map[Int, Utility] = utilities def Utilities: Map[Int, Utility] = utilities
@ -358,9 +340,9 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
} }
} }
override def DeployTime = Definition.DeployTime override def DeployTime: Int = Definition.DeployTime
override def UndeployTime = Definition.UndeployTime override def UndeployTime: Int = Definition.UndeployTime
def Inventory: GridInventory = trunk def Inventory: GridInventory = trunk
@ -476,7 +458,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
if (trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) { if (trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) {
groupPermissions(3) match { groupPermissions(3) match {
case VehicleLockState.Locked => //only the owner case VehicleLockState.Locked => //only the owner
Owner.isEmpty || (Owner.isDefined && player.GUID == Owner.get) OwnerGuid.isEmpty || (OwnerGuid.isDefined && player.GUID == OwnerGuid.get)
case VehicleLockState.Group => //anyone in the owner's squad or platoon case VehicleLockState.Group => //anyone in the owner's squad or platoon
faction == player.Faction //TODO this is not correct faction == player.Faction //TODO this is not correct
case VehicleLockState.Empire => //anyone of the owner's faction case VehicleLockState.Empire => //anyone of the owner's faction
@ -518,7 +500,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
def PreviousGatingManifest(): Option[VehicleManifest] = previousVehicleGatingManifest def PreviousGatingManifest(): Option[VehicleManifest] = previousVehicleGatingManifest
def DamageModel = Definition.asInstanceOf[DamageResistanceModel] def DamageModel: DamageResistanceModel = Definition.asInstanceOf[DamageResistanceModel]
override def BailProtection_=(protect: Boolean): Boolean = { override def BailProtection_=(protect: Boolean): Boolean = {
!Definition.CanFly && super.BailProtection_=(protect) !Definition.CanFly && super.BailProtection_=(protect)
@ -681,6 +663,6 @@ object Vehicle {
*/ */
def toString(obj: Vehicle): String = { def toString(obj: Vehicle): String = {
val occupancy = obj.Seats.values.count(seat => seat.isOccupied) val occupancy = obj.Seats.values.count(seat => seat.isOccupied)
s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)" s"${obj.Definition.Name}, owned by ${obj.OwnerGuid}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
} }
} }

View file

@ -2,6 +2,7 @@
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.ce.TelepadLike import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
@ -61,7 +62,7 @@ object Vehicles {
* `None`, otherwise * `None`, otherwise
*/ */
def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] = def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] =
vehicle.Zone.GUID(vehicle.Owner) match { vehicle.Zone.GUID(vehicle.OwnerGuid) match {
case Some(player: Player) => case Some(player: Player) =>
if (player.avatar.vehicle.contains(guid)) { if (player.avatar.vehicle.contains(guid)) {
player.avatar.vehicle = None player.avatar.vehicle = None
@ -127,7 +128,7 @@ object Vehicles {
*/ */
def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = { def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = {
val pguid = player.GUID val pguid = player.GUID
if (vehicle.Owner.contains(pguid)) { if (vehicle.OwnerGuid.contains(pguid)) {
vehicle.AssignOwnership(None) vehicle.AssignOwnership(None)
//vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0))) //vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
val vguid = vehicle.GUID val vguid = vehicle.GUID
@ -236,16 +237,14 @@ object Vehicles {
val zone = target.Zone val zone = target.Zone
// Forcefully dismount any cargo // Forcefully dismount any cargo
target.CargoHolds.foreach { case (_, cargoHold) => target.CargoHolds.foreach { case (_, cargoHold) =>
cargoHold.occupant match { cargoHold.occupant.collect {
case Some(cargo: Vehicle) => cargo: Vehicle => cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
case None => ;
} }
} }
// Forcefully dismount all seated occupants from the vehicle // Forcefully dismount all seated occupants from the vehicle
target.Seats.values.foreach(seat => { target.Seats.values.foreach(seat => {
seat.occupant match { seat.occupant.collect {
case Some(tplayer: Player) => tplayer: Player =>
seat.unmount(tplayer) seat.unmount(tplayer)
tplayer.VehicleSeated = None tplayer.VehicleSeated = None
if (tplayer.HasGUID) { if (tplayer.HasGUID) {
@ -254,7 +253,6 @@ object Vehicles {
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID) VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID)
) )
} }
case _ => ;
} }
}) })
// If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed. // If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed.
@ -263,25 +261,17 @@ object Vehicles {
target.Actor ! Vehicle.Deconstruct() target.Actor ! Vehicle.Deconstruct()
} else { // Otherwise handle ownership transfer as normal } else { // Otherwise handle ownership transfer as normal
// Remove ownership of our current vehicle, if we have one // Remove ownership of our current vehicle, if we have one
hacker.avatar.vehicle match { hacker.avatar.vehicle
case Some(guid: PlanetSideGUID) => .flatMap { guid => zone.GUID(guid) }
zone.GUID(guid) match { .collect { case vehicle: Vehicle =>
case Some(vehicle: Vehicle) => Vehicles.Disown(hacker, vehicle)
Vehicles.Disown(hacker, vehicle) }
case _ => ; // Remove ownership of the vehicle from the previous player
} target.OwnerGuid
case _ => ; .flatMap { guid => zone.GUID(guid) }
} .collect { case tplayer: Player =>
target.Owner match { Vehicles.Disown(tplayer, target)
case Some(previousOwnerGuid: PlanetSideGUID) => }
// Remove ownership of the vehicle from the previous player
zone.GUID(previousOwnerGuid) match {
case Some(tplayer: Player) =>
Vehicles.Disown(tplayer, target)
case _ => ; // Vehicle already has no owner
}
case _ => ;
}
// Now take ownership of the jacked vehicle // Now take ownership of the jacked vehicle
target.Actor ! CommonMessages.Hack(hacker, target) target.Actor ! CommonMessages.Hack(hacker, target)
target.Faction = hacker.Faction target.Faction = hacker.Faction
@ -301,16 +291,15 @@ object Vehicles {
// If AMS is deployed, swap it to the new faction // If AMS is deployed, swap it to the new faction
target.Definition match { target.Definition match {
case GlobalDefinitions.router => case GlobalDefinitions.router =>
target.Utility(UtilityType.internal_router_telepad_deployable) match { target.Utility(UtilityType.internal_router_telepad_deployable).collect {
case Some(util: Utility.InternalTelepad) => case util: Utility.InternalTelepad =>
//"power cycle" //"power cycle"
util.Actor ! TelepadLike.Deactivate(util) util.Actor ! TelepadLike.Deactivate(util)
util.Actor ! TelepadLike.Activate(util) util.Actor ! TelepadLike.Activate(util)
case _ => ;
} }
case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed => case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed =>
zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone) zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone)
case _ => ; case _ => ()
} }
} }
@ -411,6 +400,7 @@ object Vehicles {
* *
* @param vehicle the vehicle * @param vehicle the vehicle
*/ */
//noinspection ScalaUnusedSymbol
def BeforeUnloadVehicle(vehicle: Vehicle, zone: Zone): Unit = { def BeforeUnloadVehicle(vehicle: Vehicle, zone: Zone): Unit = {
vehicle.Definition match { vehicle.Definition match {
case GlobalDefinitions.ams => case GlobalDefinitions.ams =>
@ -419,7 +409,7 @@ object Vehicles {
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying) vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case GlobalDefinitions.router => case GlobalDefinitions.router =>
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying) vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case _ => ; case _ => ()
} }
} }
@ -439,4 +429,49 @@ object Vehicles {
val turnAway = if (offset.x >= 0) -90f else 90f val turnAway = if (offset.x >= 0) -90f else 90f
(obj.Position + offset, (shuttleAngle + turnAway) % 360f) (obj.Position + offset, (shuttleAngle + turnAway) % 360f)
} }
/**
* Based on a mounting index, for a certain mount, to what mounting group does this seat belong?
* @param vehicle the vehicle
* @param seatNumber specific seat index
* @return the seat group designation
*/
def SeatPermissionGroup(vehicle: Vehicle, seatNumber: Int): Option[AccessPermissionGroup.Value] = {
SeatPermissionGroup(vehicle.Definition, seatNumber)
}
/**
* Based on a mounting index, for a certain mount, to what mounting group does this seat belong?
* @param definition global vehicle specification
* @param seatNumber specific seat index
* @return the seat group designation
*/
def SeatPermissionGroup(definition: VehicleDefinition, seatNumber: Int): Option[AccessPermissionGroup.Value] = {
if (seatNumber == 0) { //valid in almost all cases
Some(AccessPermissionGroup.Driver)
} else {
definition.Seats
.get(seatNumber)
.map { _ =>
definition.controlledWeapons()
.get(seatNumber)
.map { _ => AccessPermissionGroup.Gunner }
.getOrElse { AccessPermissionGroup.Passenger }
}
.orElse {
definition.Cargo
.get(seatNumber)
.map { _ => AccessPermissionGroup.Passenger }
.orElse {
val offset = definition.TrunkOffset
val size = definition.TrunkSize
if (seatNumber >= offset && seatNumber < offset + size.Width * size.Height) {
Some(AccessPermissionGroup.Trunk)
} else {
None
}
}
}
}
}
} }

View file

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

View file

@ -1,8 +1,8 @@
// Copyright (c) 2023 PSForever // Copyright (c) 2023 PSForever
package net.psforever.objects.avatar.scoring package net.psforever.objects.avatar.scoring
final case class EquipmentStat(objectId: Int, shotsFired: Int, shotsLanded: Int, kills: Int) final case class EquipmentStat(objectId: Int, shotsFired: Int, shotsLanded: Int, kills: Int, assists: Int)
object EquipmentStat { object EquipmentStat {
def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0) def apply(objectId: Int): EquipmentStat = EquipmentStat(objectId, 0, 1, 0, 0)
} }

View file

@ -3,6 +3,7 @@ package net.psforever.objects.avatar.scoring
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.exp.EquipmentUseContextWrapper
import org.joda.time.LocalDateTime import org.joda.time.LocalDateTime
trait KDAStat { trait KDAStat {
@ -10,10 +11,29 @@ trait KDAStat {
val time: LocalDateTime = LocalDateTime.now() 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 def experienceEarned: Long = 0
} }
final case class SupportActivity(
target: PlayerSource,
weapons: Seq[EquipmentUseContextWrapper],
experienceEarned: Long
) extends KDAStat

View file

@ -5,11 +5,15 @@ final case class Life(
kills: Seq[Kill], kills: Seq[Kill],
assists: Seq[Assist], assists: Seq[Assist],
death: Option[Death], death: Option[Death],
equipmentStats: Seq[EquipmentStat] equipmentStats: Seq[EquipmentStat],
supportExperience: Long,
prior: Option[Life]
) )
object Life { object Life {
def apply(): Life = Life(Nil, Nil, None, Nil) def apply(): Life = Life(Nil, Nil, None, Nil, 0, None)
def revive(prior: Life): Life = Life(Nil, Nil, None, Nil, 0, Some(prior))
def bep(life: Life): Long = { def bep(life: Life): Long = {
life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned) life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned)

View file

@ -3,7 +3,7 @@ package net.psforever.objects.avatar.scoring
import net.psforever.objects.GlobalDefinitions import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.types.{PlanetSideEmpire, StatisticalCategory} import net.psforever.types.{PlanetSideEmpire, StatisticalCategory, StatisticalElement}
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.mutable import scala.collection.mutable
@ -18,43 +18,97 @@ class ScoreCard() {
def Lives: Seq[Life] = lives def Lives: Seq[Life] = lives
def AllLives: Seq[Life] = curr +: lives
def Kills: Seq[Kill] = lives.flatMap { _.kills } ++ curr.kills
def KillStatistics: Map[Int, Statistic] = killStatistics.toMap def KillStatistics: Map[Int, Statistic] = killStatistics.toMap
def AssistStatistics: Map[Int, Statistic] = assistStatistics.toMap def AssistStatistics: Map[Int, Statistic] = assistStatistics.toMap
def rate(msg: Any): Unit = { def revive(): Unit = {
curr = Life.revive(curr)
}
def respawn(): Unit = {
val death = curr
curr = Life()
lives = death +: lives
}
def initStatisticForKill(targetId: Int, victimFaction: PlanetSideEmpire.Value): Statistic = {
ScoreCard.initStatisticsFor(killStatistics, targetId, victimFaction)
}
def rate(msg: Any): Seq[(Int, Statistic)] = {
msg match { msg match {
case e: EquipmentStat => case e: EquipmentStat =>
curr = ScoreCard.updateEquipmentStat(curr, e) curr = ScoreCard.updateEquipmentStat(curr, e)
Nil
case k: Kill => case k: Kill =>
curr = curr.copy(kills = k +: curr.kills) curr = curr.copy(kills = k +: curr.kills)
curr = ScoreCard.updateEquipmentStat(curr, EquipmentStat(k.info.interaction.cause.attribution, 0, 0, 1)) //TODO may need to expand these to include other fields later
ScoreCard.updateStatisticsFor(killStatistics, k.info.interaction.cause.attribution, k.victim.Faction) curr = ScoreCard.updateEquipmentStat(curr, EquipmentStat(k.info.interaction.cause.attribution, 0, 0, 1, 0))
val wid = StatisticalElement.relatedElement(k.victim.ExoSuit).value
Seq((wid, ScoreCard.updateStatisticsFor(killStatistics, wid, k.victim.Faction)))
case a: Assist => case a: Assist =>
curr = curr.copy(assists = a +: curr.assists) curr = curr.copy(assists = a +: curr.assists)
val faction = a.victim.Faction val faction = a.victim.Faction
a.weapons.foreach { wid => //TODO may need to expand these to include other fields later
ScoreCard.updateStatisticsFor(assistStatistics, wid, faction) a.weapons.map { weq =>
val wid = weq.equipment
(wid, ScoreCard.updateStatisticsFor(assistStatistics, wid, faction))
} }
case d: Death => case d: Death =>
val expired = curr curr = curr.copy(death = Some(d))
curr = Life() Nil
lives = expired.copy(death = Some(d)) +: lives case value: Long =>
case _ => ; curr = curr.copy(supportExperience = curr.supportExperience + value)
Nil
case _ =>
Nil
} }
} }
} }
object ScoreCard { object ScoreCard {
def reviveCount(card: ScoreCard): Int = {
reviveCount(card.CurrentLife)
}
def reviveCount(life: Life): Int = {
recursiveReviveCount(life, count = 0)
}
def deathCount(card: ScoreCard): Int = {
card.AllLives.foldLeft(0)(_ + deathCount(_))
}
private def deathCount(life: Life): Int = {
life.prior match {
case None => if (life.death.nonEmpty) 1 else 0
case Some(previousLife) => recursiveReviveCount(previousLife, count = 1)
}
}
@tailrec
private def recursiveReviveCount(life: Life, count: Int): Int = {
life.prior match {
case None => count + 1
case Some(previousLife) => recursiveReviveCount(previousLife, count + 1)
}
}
private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = { private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = {
updateEquipmentStat(curr, entry, entry.objectId, entry.kills) updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists)
} }
private def updateEquipmentStat( private def updateEquipmentStat(
curr: Life, curr: Life,
entry: EquipmentStat, entry: EquipmentStat,
objectId: Int, objectId: Int,
killCount: Int killCount: Int,
assists: Int
): Life = { ): Life = {
curr.equipmentStats.indexWhere { a => a.objectId == objectId } match { curr.equipmentStats.indexWhere { a => a.objectId == objectId } match {
case -1 => case -1 =>
@ -72,6 +126,29 @@ object ScoreCard {
} }
} }
@tailrec
private def initStatisticsFor(
statisticMap: mutable.HashMap[Int, Statistic],
objectId: Int,
victimFaction: PlanetSideEmpire.Value
): Statistic = {
statisticMap.get(objectId) match {
case Some(fields) =>
val outEntry = victimFaction match {
case PlanetSideEmpire.TR => fields.copy(tr_c = fields.tr_c + 1)
case PlanetSideEmpire.NC => fields.copy(nc_c = fields.nc_c + 1)
case PlanetSideEmpire.VS => fields.copy(vs_c = fields.vs_c + 1)
case PlanetSideEmpire.NEUTRAL => fields.copy(ps_c = fields.ps_c + 1)
}
statisticMap.put(objectId, outEntry)
outEntry
case _ =>
val out = Statistic(0, 0, 0, 0, 0, 0, 0, 0)
statisticMap.put(objectId, out)
initStatisticsFor(statisticMap, objectId, victimFaction)
}
}
@tailrec @tailrec
private def updateStatisticsFor( private def updateStatisticsFor(
statisticMap: mutable.HashMap[Int, Statistic], statisticMap: mutable.HashMap[Int, Statistic],
@ -81,10 +158,10 @@ object ScoreCard {
statisticMap.get(objectId) match { statisticMap.get(objectId) match {
case Some(fields) => case Some(fields) =>
val outEntry = victimFaction match { val outEntry = victimFaction match {
case PlanetSideEmpire.TR => fields.copy(tr_b = fields.tr_b + 1) case PlanetSideEmpire.TR => fields.copy(tr_s = fields.tr_s + 1)
case PlanetSideEmpire.NC => fields.copy(nc_b = fields.nc_b + 1) case PlanetSideEmpire.NC => fields.copy(nc_s = fields.nc_s + 1)
case PlanetSideEmpire.VS => fields.copy(vs_b = fields.vs_b + 1) case PlanetSideEmpire.VS => fields.copy(vs_s = fields.vs_s + 1)
case PlanetSideEmpire.NEUTRAL => fields.copy(ps_b = fields.ps_b + 1) case PlanetSideEmpire.NEUTRAL => fields.copy(ps_s = fields.ps_s + 1)
} }
outEntry outEntry
case _ => case _ =>

View file

@ -1,4 +1,35 @@
// Copyright (c) 2023 PSForever // Copyright (c) 2023 PSForever
package net.psforever.objects.avatar.scoring package net.psforever.objects.avatar.scoring
final case class Statistic(tr_a: Int, tr_b: Int, nc_a: Int, nc_b: Int, vs_a: Int, vs_b: Int, ps_a: Int, ps_b: Int) /**
* Organizes the eight fields one would find in an `AvatarServiceMessage` statistic field.
* The `_c` fields and the `_s` fields are paired when the values populate the packet
* where `c` stands for "campaign" and `s` stands for "session".
* "Session" values reflect on the UI as the K in K/D
* while "campaign" values reflect on the Character Info window, stats section.
* @param tr_c terran republic campaign stat
* @param tr_s terran republic session stat
* @param nc_c new conglomerate campaign stat
* @param nc_s new conglomerate session stat
* @param vs_c vanu sovereignty campaign stat
* @param vs_s vanu sovereignty session stat
* @param ps_c generic faction campaign stat
* @param ps_s generic faction session stat
*/
final case class Statistic(tr_c: Int, tr_s: Int, nc_c: Int, nc_s: Int, vs_c: Int, vs_s: Int, ps_c: Int, ps_s: Int)
final case class StatisticByContext(tr: Int, nc: Int, vs: Int, ps: Int) {
def total: Int = tr + nc + vs + ps
}
object CampaignStatistics {
def apply(stat: Statistic): StatisticByContext = {
StatisticByContext(stat.tr_c, stat.nc_c, stat.vs_c, stat.ps_c)
}
}
object SessionStatistics {
def apply(stat: Statistic): StatisticByContext = {
StatisticByContext(stat.tr_s, stat.nc_s, stat.vs_s, stat.ps_s)
}
}

View file

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

View file

@ -26,27 +26,8 @@ class VehicleDefinition(objectId: Int)
with VitalityDefinition with VitalityDefinition
with NtuContainerDefinition with NtuContainerDefinition
with ResistanceProfileMutators with ResistanceProfileMutators
with DamageResistanceModel { with DamageResistanceModel
/** ... */ with WithShields {
var shieldUiAttribute: Int = 68
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
private var defaultShields : Option[Int] = None
/** maximum vehicle shields (generally: 20% of health)
* for normal vehicles, offered through amp station facility benefits
* for BFR's, it charges naturally
**/
private var maxShields: Int = 0
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
private var shieldChargeDamageCooldown : Long = 5000L
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
private var shieldChargePeriodicCooldown : Long = 1000L
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
private var autoShieldRecharge : Option[Int] = None
private var autoShieldRechargeSpecial : Option[Int] = None
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
* the drain interval is 250ms which is convenient for us
* we can skip needing to define is explicitly */
private var shieldDrain : Option[Int] = None
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]() private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
private var deployment: Boolean = false private var deployment: Boolean = false
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap() private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
@ -92,63 +73,6 @@ class VehicleDefinition(objectId: Int)
RepairRestoresAt = 1 RepairRestoresAt = 1
registerAs = "vehicles" registerAs = "vehicles"
def DefaultShields: Int = defaultShields.getOrElse(0)
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
def DefaultShields_=(shield: Option[Int]): Int = {
defaultShields = shield
DefaultShields
}
def MaxShields: Int = maxShields
def MaxShields_=(shields: Int): Int = {
maxShields = shields
MaxShields
}
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
shieldChargePeriodicCooldown = cooldown
ShieldPeriodicDelay
}
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
def ShieldDamageDelay_=(cooldown: Long): Long = {
shieldChargeDamageCooldown = cooldown
ShieldDamageDelay
}
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
autoShieldRecharge = charge
ShieldAutoRecharge
}
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
autoShieldRechargeSpecial = charge
ShieldAutoRechargeSpecial
}
def ShieldDrain: Option[Int] = shieldDrain
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
shieldDrain = drain
ShieldDrain
}
def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo def Cargo: mutable.HashMap[Int, CargoDefinition] = cargo
def CanBeOwned: Option[Boolean] = canBeOwned def CanBeOwned: Option[Boolean] = canBeOwned
@ -300,6 +224,7 @@ class VehicleDefinition(objectId: Int)
) )
} }
//noinspection ScalaUnusedSymbol
def Uninitialize(obj: Vehicle, context: ActorContext): Unit = { def Uninitialize(obj: Vehicle, context: ActorContext): Unit = {
obj.Actor ! akka.actor.PoisonPill obj.Actor ! akka.actor.PoisonPill
obj.Actor = Default.Actor obj.Actor = Default.Actor

View file

@ -0,0 +1,81 @@
package net.psforever.objects.definition
trait WithShields {
/** ... */
var shieldUiAttribute: Int = 68
/** how many points of shield the vehicle starts with (should default to 0 if unset through the accessor) */
private var defaultShields : Option[Int] = None
/** maximum vehicle shields (generally: 20% of health)
* for normal vehicles, offered through amp station facility benefits
* for BFR's, it charges naturally
**/
private var maxShields: Int = 0
/** the minimum amount of time that must elapse in between damage and shield charge activities (ms) */
private var shieldChargeDamageCooldown : Long = 5000L
/** the minimum amount of time that must elapse in between distinct shield charge activities (ms) */
private var shieldChargePeriodicCooldown : Long = 1000L
/** if the shield recharges on its own, this value will be non-`None` and indicate by how much */
private var autoShieldRecharge : Option[Int] = None
private var autoShieldRechargeSpecial : Option[Int] = None
/** shield drain is what happens to the shield under special conditions, e.g., bfr flight;
* the drain interval is 250ms which is convenient for us
* we can skip needing to define is explicitly */
private var shieldDrain : Option[Int] = None
def DefaultShields: Int = defaultShields.getOrElse(0)
def DefaultShields_=(shield: Int): Int = DefaultShields_=(Some(shield))
def DefaultShields_=(shield: Option[Int]): Int = {
defaultShields = shield
DefaultShields
}
def MaxShields: Int = maxShields
def MaxShields_=(shields: Int): Int = {
maxShields = shields
MaxShields
}
def ShieldPeriodicDelay : Long = shieldChargePeriodicCooldown
def ShieldPeriodicDelay_=(cooldown: Long): Long = {
shieldChargePeriodicCooldown = cooldown
ShieldPeriodicDelay
}
def ShieldDamageDelay: Long = shieldChargeDamageCooldown
def ShieldDamageDelay_=(cooldown: Long): Long = {
shieldChargeDamageCooldown = cooldown
ShieldDamageDelay
}
def ShieldAutoRecharge: Option[Int] = autoShieldRecharge
def ShieldAutoRecharge_=(charge: Int): Option[Int] = ShieldAutoRecharge_=(Some(charge))
def ShieldAutoRecharge_=(charge: Option[Int]): Option[Int] = {
autoShieldRecharge = charge
ShieldAutoRecharge
}
def ShieldAutoRechargeSpecial: Option[Int] = autoShieldRechargeSpecial.orElse(ShieldAutoRecharge)
def ShieldAutoRechargeSpecial_=(charge: Int): Option[Int] = ShieldAutoRechargeSpecial_=(Some(charge))
def ShieldAutoRechargeSpecial_=(charge: Option[Int]): Option[Int] = {
autoShieldRechargeSpecial = charge
ShieldAutoRechargeSpecial
}
def ShieldDrain: Option[Int] = shieldDrain
def ShieldDrain_=(drain: Int): Option[Int] = ShieldDrain_=(Some(drain))
def ShieldDrain_=(drain: Option[Int]): Option[Int] = {
shieldDrain = drain
ShieldDrain
}
}

View file

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

View file

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

View file

@ -17,7 +17,7 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() {
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L) case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L)
} }
val millisecondsRemaining = TimeUnit.MILLISECONDS.convert(math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.nanoTime), TimeUnit.NANOSECONDS) val millisecondsRemaining = math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.currentTimeMillis())
Success( Success(
CaptureFlagData( CaptureFlagData(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,13 +5,15 @@ import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{Vehicle, Vehicles} import net.psforever.objects.{Vehicle, Vehicles}
import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.objects.vital.resolution.ResolutionCalculations
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.exp.ToDatabase
import net.psforever.packet.game.DamageWithPositionMessage
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.DamageWithPositionMessage
import net.psforever.types.Vector3 import net.psforever.types.Vector3
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -213,6 +215,18 @@ trait DamageableVehicle
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, obj.Definition.shieldUiAttribute, 0) VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, obj.Definition.shieldUiAttribute, 0)
) )
} }
//database entry
cause.adversarial.collect {
case Adversarial(attacker, victim: VehicleSource, implement) =>
ToDatabase.reportMachineDestruction(
attacker.CharId,
victim,
DamageableObject.HackedBy,
DamageableObject.MountedIn.nonEmpty,
implement,
obj.Zone.Number
)
}
//clean up //clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute)) target.Actor ! Vehicle.Deconstruct(Some(1 minute))
DamageableWeaponTurret.DestructionAwareness(obj, cause) DamageableWeaponTurret.DestructionAwareness(obj, cause)

View file

@ -25,14 +25,14 @@ trait Hackable {
def HackedBy_=(agent: Option[Player]): Option[HackInfo] = { def HackedBy_=(agent: Option[Player]): Option[HackInfo] = {
(hackedBy, agent) match { (hackedBy, agent) match {
case (None, Some(actor)) => case (None, Some(actor)) =>
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L)) hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
case (Some(info), Some(actor)) => case (Some(info), Some(actor)) =>
if (actor.Faction == this.Faction) { if (actor.Faction == this.Faction) {
//hack cleared //hack cleared
hackedBy = None hackedBy = None
} else if (actor.Faction != info.hackerFaction) { } else if (actor.Faction != info.hackerFaction) {
//override the hack state with a new hack state if the new user has different faction affiliation //override the hack state with a new hack state if the new user has different faction affiliation
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L)) hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.currentTimeMillis(), 0L))
} }
case (_, None) => case (_, None) =>
hackedBy = None hackedBy = None

View file

@ -8,7 +8,7 @@ import net.psforever.types.{PlanetSideEmpire, Vector3}
/** /**
* Represent a special entity that is carried by the player in certain circumstances. * Represent a special entity that is carried by the player in certain circumstances.
* The entity is not a piece of `Equipment` so it does not go into the holsters, * The entity is not a piece of `Equipment` so it does not go into the holsters,
* doe not into the player's inventory, * does not into the player's inventory,
* and is not carried in or manipulated by the player's hands. * and is not carried in or manipulated by the player's hands.
* The different game elements it simulates are: * The different game elements it simulates are:
* a facility's lattice logic unit (LLU), * a facility's lattice logic unit (LLU),
@ -33,6 +33,7 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
private var target: Building = Building.NoBuilding private var target: Building = Building.NoBuilding
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var carrier: Option[Player] = None private var carrier: Option[Player] = None
private var lastTimeCollected: Long = System.currentTimeMillis()
def Target: Building = target def Target: Building = target
def Target_=(newTarget: Building): Building = { def Target_=(newTarget: Building): Building = {
@ -64,8 +65,11 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
def Carrier: Option[Player] = carrier def Carrier: Option[Player] = carrier
def Carrier_=(newCarrier: Option[Player]) : Option[Player] = { def Carrier_=(newCarrier: Option[Player]) : Option[Player] = {
carrier = newCarrier carrier = newCarrier
lastTimeCollected = System.currentTimeMillis()
carrier carrier
} }
def LastCollectionTime: Long = carrier.map { _ => lastTimeCollected }.getOrElse { System.currentTimeMillis() }
} }
object CaptureFlag { object CaptureFlag {

View file

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

View file

@ -7,11 +7,13 @@ import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.transfer.TransferBehavior import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior} import net.psforever.objects.zones
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.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.util.Config
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -181,6 +183,24 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
if (amount != 0) { if (amount != 0) {
panelAnimationFunc(sender, amount) panelAnimationFunc(sender, amount)
panelAnimationFunc = SkipPanelAnimation 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 =
(Config.app.game.experience.sep.ntuSiloDepositReward.toFloat *
math.floor(amount).toFloat /
math.floor(resourceSilo.MaxNtuCapacitor / resourceSilo.Definition.ChargeTime.toMillis.toFloat)
).toLong
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
owner.name,
AvatarAction.AwardBep(0, deposit, ExperienceType.Normal)
)
zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
}
} }
} }
@ -192,6 +212,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
* @param trigger if positive, activate the animation; * @param trigger if positive, activate the animation;
* if negative or zero, disable the animation * if negative or zero, disable the animation
*/ */
//noinspection ScalaUnusedSymbol
def PanelAnimation(source: ActorRef, trigger: Float): Unit = { def PanelAnimation(source: ActorRef, trigger: Float): Unit = {
val currentlyHas = resourceSilo.NtuCapacitor val currentlyHas = resourceSilo.NtuCapacitor
// do not let the trigger charge go to waste, but also do not let the silo be filled // do not let the trigger charge go to waste, but also do not let the silo be filled

View file

@ -4,12 +4,16 @@ package net.psforever.objects.serverobject.resourcesilo
import net.psforever.objects.NtuContainerDefinition import net.psforever.objects.NtuContainerDefinition
import net.psforever.objects.serverobject.structures.AmenityDefinition import net.psforever.objects.serverobject.structures.AmenityDefinition
import scala.concurrent.duration._
/** /**
* The definition for any `Resource Silo`. * The definition for any `Resource Silo`.
* Object Id 731. * Object Id 731.
*/ */
class ResourceSiloDefinition extends AmenityDefinition(731) class ResourceSiloDefinition extends AmenityDefinition(731)
with NtuContainerDefinition { with NtuContainerDefinition {
var ChargeTime: FiniteDuration = 0.seconds
Name = "resource_silo" Name = "resource_silo"
MaxNtuCapacitor = 1000 MaxNtuCapacitor = 1000
} }

View file

@ -16,11 +16,10 @@ import net.psforever.types._
import scalax.collection.{Graph, GraphEdge} import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import scala.collection.mutable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import scala.concurrent.duration._
class Building( class Building(
private val name: String, private val name: String,
@ -34,9 +33,8 @@ class Building(
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var playersInSOI: List[Player] = List.empty private var playersInSOI: List[Player] = List.empty
private val capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
private var forceDomeActive: Boolean = false private var forceDomeActive: Boolean = false
private var participationFunc: Building.ParticipationLogic = Building.NoParticipation private var participationFunc: ParticipationLogic = NoParticipation
super.Zone_=(zone) super.Zone_=(zone)
super.GUID_=(PlanetSideGUID(building_guid)) //set super.GUID_=(PlanetSideGUID(building_guid)) //set
Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later
@ -51,10 +49,11 @@ class Building(
*/ */
def MapId: Int = map_id def MapId: Int = map_id
def IsCapitol: Boolean = capitols.contains(name) def IsCapitol: Boolean = Building.Capitols.contains(name)
def IsSubCapitol: Boolean = { def IsSubCapitol: Boolean = {
Neighbours match { Neighbours match {
case Some(buildings: Set[Building]) => buildings.exists(x => capitols.contains(x.name)) case Some(buildings: Set[Building]) => buildings.exists(x => Building.Capitols.contains(x.name))
case None => false case None => false
} }
} }
@ -85,13 +84,11 @@ class Building(
box.Actor ! Painbox.Stop() box.Actor ! Painbox.Stop()
} }
} }
participationFunc.Players(building = this, list)
playersInSOI = list playersInSOI = list
participationFunc.TryUpdate()
playersInSOI playersInSOI
} }
def PlayerContribution: Map[Player, Long] = participationFunc.Contribution()
// Get all lattice neighbours // Get all lattice neighbours
def AllNeighbours: Option[Set[Building]] = { def AllNeighbours: Option[Set[Building]] = {
zone.Lattice find this match { zone.Lattice find this match {
@ -186,8 +183,7 @@ class Building(
case Some(obj: CaptureTerminal with Hackable) => case Some(obj: CaptureTerminal with Hackable) =>
obj.HackedBy match { obj.HackedBy match {
case Some(Hackable.HackInfo(p, _, start, length)) => case Some(Hackable.HackInfo(p, _, start, length)) =>
val hack_time_remaining_ms = val hack_time_remaining_ms = math.max(0, start + length - System.currentTimeMillis())
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
(true, p.Faction, hack_time_remaining_ms) (true, p.Faction, hack_time_remaining_ms)
case _ => case _ =>
(false, PlanetSideEmpire.NEUTRAL, 0L) (false, PlanetSideEmpire.NEUTRAL, 0L)
@ -360,43 +356,24 @@ class Building(
override def Amenities_=(obj: Amenity): List[Amenity] = { override def Amenities_=(obj: Amenity): List[Amenity] = {
obj match { obj match {
case _: CaptureTerminal => participationFunc = Building.FacilityHackParticipation case _: CaptureTerminal =>
case _ => ; if (buildingType == StructureType.Facility) {
participationFunc = MajorFacilityHackParticipation(this)
} else if (buildingType == StructureType.Tower) {
participationFunc = TowerHackParticipation(this)
}
case _ => ()
} }
super.Amenities_=(obj) super.Amenities_=(obj)
} }
def Participation: ParticipationLogic = participationFunc
def Definition: BuildingDefinition = buildingDefinition def Definition: BuildingDefinition = buildingDefinition
} }
object Building { object Building {
trait ParticipationLogic { final val Capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
def Players(building: Building, list: List[Player]): Unit = { }
def Contribution(): Map[Player, Long]
}
final case object NoParticipation extends ParticipationLogic {
def Contribution(): Map[Player, Long] = Map.empty[Player, Long]
}
final case object FacilityHackParticipation extends ParticipationLogic {
private var playerContribution: mutable.HashMap[Player, Long] = mutable.HashMap[Player, Long]()
override def Players(building: Building, list: List[Player]): Unit = {
if (list.isEmpty) {
playerContribution.clear()
} else {
val hackTime = (building.CaptureTerminal.get.Definition.FacilityHackTime + 10.minutes).toMillis
val curr = System.currentTimeMillis()
val list2 = list.map { p => (p, curr) }
playerContribution = playerContribution.filterNot { case (p, t) =>
list2.contains(p) || curr - t > hackTime
} ++ list2
}
}
def Contribution(): Map[Player, Long] = playerContribution.toMap
}
final val NoBuilding: Building = final val NoBuilding: Building =
new Building(name = "", 0, map_id = 0, Zone.Nowhere, StructureType.Platform, GlobalDefinitions.building) { new Building(name = "", 0, map_id = 0, Zone.Nowhere, StructureType.Platform, GlobalDefinitions.building) {

View file

@ -0,0 +1,272 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.Player
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.collection.mutable
trait FacilityHackParticipation extends ParticipationLogic {
protected var lastInfoRequest: Long = 0L
protected var infoRequestOverTime: Seq[Long] = Seq[Long]()
/**
key: unique player identifier<br>
values: last player entry, number of times updated, time of last update (POSIX time)
*/
protected var playerContribution: mutable.LongMap[(Player, Int, Long)] = mutable.LongMap[(Player, Int, Long)]()
protected var playerPopulationOverTime: Seq[Map[PlanetSideEmpire.Value, Int]] = Seq[Map[PlanetSideEmpire.Value, Int]]()
def PlayerContribution(timeDelay: Long): Map[Long, Float] = {
playerContribution
.collect {
case (id, (_, d, _)) if d < timeDelay => (id, d.toFloat / timeDelay.toFloat)
case (id, (_, _, _)) => (id, 1.0f)
}
.toMap[Long, Float]
}
protected def updatePlayers(list: List[Player]): Unit = {
val hackTime = building.CaptureTerminal.get.Definition.FacilityHackTime.toMillis
val curr = System.currentTimeMillis()
if (list.isEmpty) {
playerContribution = playerContribution.filterNot { case (_, (_, _, t)) => curr - t > hackTime }
} else {
val (vanguardParticipants, missingParticipants) = {
val uniqueList2 = list.map { _.CharId }
playerContribution
.filterNot { case (_, (_, _, t)) => curr - t > hackTime }
.partition { case (p, _) => uniqueList2.contains(p) }
}
val newParticipaants = list
.filterNot { p =>
playerContribution.exists { case (u, _) => p.CharId == u }
}
playerContribution =
vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++
newParticipaants.map { p => (p.CharId, (p, 1, curr)) } ++
missingParticipants
}
}
/**
* Eliminate participation for players who have no submitted updates within the time period.
* @param list current list of players
* @param now current time (ms)
* @param before how long before the current time beyond which players should be eliminated (ms)
* @see `timeSensitiveFilterAndAppend`
*/
protected def updatePopulationOverTime(list: List[Player], now: Long, before: Long): Unit = {
var populationList = list
val layer = PlanetSideEmpire.values.map { faction =>
val (isFaction, everyoneElse) = populationList.partition(_.Faction == faction)
populationList = everyoneElse
(faction, isFaction.size)
}.toMap[PlanetSideEmpire.Value, Int]
playerPopulationOverTime = timeSensitiveFilterAndAppend(playerPopulationOverTime, layer, now - before)
}
protected def updateTime(now: Long): Unit = {
infoRequestOverTime = timeSensitiveFilterAndAppend(infoRequestOverTime, now, now - 900000L)
}
/**
* Eliminate entries from the primary input list based on time entries in a secondary time record list.
* The time record list must be updated independently.
* @param list input list whose entries are edited against time and then is appended
* @param newEntry new entry of the appropriate type to append to the end of the output list
* @param beforeTime how long before the current time beyond which entries in the input list should be eliminated (ms)
* @tparam T it does not matter what the type is
* @return the modified list
*/
protected def timeSensitiveFilterAndAppend[T](
list: Seq[T],
newEntry: T,
beforeTime: Long
): Seq[T] = {
infoRequestOverTime match {
case Nil => Seq(newEntry)
case _ =>
(infoRequestOverTime.indexWhere { _ >= beforeTime } match {
case -1 => list
case cutOffIndex => list.drop(cutOffIndex)
}) :+ newEntry
}
}
}
object FacilityHackParticipation {
private[participation] def allocateKillsByPlayers(
center: Vector3,
radius: Float,
hackStart: Long,
completionTime: Long,
opposingFaction: PlanetSideEmpire.Value,
contributionVictor: Iterable[(Player, Int, Long)],
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
val killMapFunc: Iterable[(Player, Int, Long)] => Iterable[(UniquePlayer, Float, Seq[Kill])] = {
killsEarnedPerPlayerDuringHack(center.xy, radius * radius, hackStart, hackStart + completionTime, opposingFaction)
}
killMapFunc(contributionVictor)
}
private[participation] def calculateExperienceFromKills(
killMapValues: Iterable[(UniquePlayer, Float, Seq[Kill])],
contributionOpposingSize: Int
): Long = {
val totalExperienceFromKills = killMapValues
.flatMap { _._3.map { _.experienceEarned } }
.sum
.toFloat
(totalExperienceFromKills * contributionOpposingSize.toFloat * 0.1d).toLong
}
private[participation] def killsEarnedPerPlayerDuringHack(
centerXY: Vector3,
distanceSq: Float,
start: Long,
end: Long,
faction: PlanetSideEmpire.Value
)
(
list: Iterable[(Player, Int, Long)]
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
val duration = end - start
list.map { case (p, d, _) =>
val killList = p.avatar.scorecard.Kills.filter { k =>
val killTime = k.info.interaction.hitTime
k.victim.Faction == faction &&
start < killTime &&
killTime <= end &&
Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
}
(PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList)
}
}
private[participation] def diffHeatForFactionMap(
data: mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]],
faction: PlanetSideEmpire.Value
): Map[Vector3, Seq[Long]] = {
var lastHeatAmount: Long = 0
var outList: Seq[Long] = Seq[Long]()
data.map { case (key, map) =>
map(faction) match {
case Nil => ()
case value :: Nil =>
outList = outList :+ value
case value :: list =>
lastHeatAmount = value
list.foreach { heat =>
if (heat < lastHeatAmount) {
lastHeatAmount = heat
outList = outList :+ heat
} else {
outList = outList :+ (heat - lastHeatAmount)
lastHeatAmount = heat
}
}
}
(key, outList)
}.toMap[Vector3, Seq[Long]]
}
private[participation] def heatMapComparison(
victorData: Iterable[Seq[Long]],
opposedData: Iterable[Seq[Long]]
): Float = {
var dataCount: Int = 0
var dataSum: Float = 0
if (victorData.size == opposedData.size) {
val seq1 = victorData.toSeq
val seq2 = opposedData.toSeq
seq1.indices.foreach { outerIndex =>
val list1 = seq1(outerIndex)
val list2 = seq2(outerIndex)
if (list1.size == list2.size) {
val indices1 = list1.indices
dataCount = dataCount + indices1.size
indices1.foreach { innerIndex =>
val value1 = list1(innerIndex)
val value2 = list2(innerIndex)
if (value1 * value2 == 0) {
dataCount -= 1
} else if (value1 > value2) {
dataSum = dataSum - value2.toFloat / value1.toFloat
} else {
dataSum = dataSum + value2.toFloat / value1.toFloat
}
}
}
}
}
if (dataSum != 0) {
math.max(0.15f, math.min(2f, dataSum / dataCount.toFloat))
} else {
1f //can't do anything; multiplier should not affect values
}
}
/**
* na
* @param populationNumbers list of the population updates
* @param gradingRule the rule whereby population numbers are transformed into percentage bonus
* @param layers from largest groupings of percentages from applying the above rule, average the values from this many groups
* @return the modifier value
*/
private[participation] def populationProgressModifier(
populationNumbers: Seq[Int],
gradingRule: Int=>Float,
layers: Int
): Float = {
val gradedPopulation = populationNumbers
.map { gradingRule }
.groupBy(x => x)
.values
.toSeq
.sortBy(_.size)
.take(layers)
.flatten
gradedPopulation.sum / gradedPopulation.size.toFloat
}
private[participation] def populationBalanceModifier(
victorPopulationNumbers: Seq[Int],
opposingPopulationNumbers: Seq[Int],
healthyPercentage: Float
): Float = {
val rate = for {
victorPop <- victorPopulationNumbers
opposePop <- opposingPopulationNumbers
out = if (
(opposePop + victorPop < 8) ||
(opposePop < victorPop && opposePop * healthyPercentage > victorPop) ||
(opposePop > victorPop && victorPop * healthyPercentage > opposePop)
) {
1f //balanced enough population
} else {
opposePop / victorPop.toFloat
}
if true
} yield out
rate.sum / rate.size.toFloat
}
private[participation] def competitionBonus(
victorSize: Long,
opposingSize: Long,
steamrollPercentage: Float,
steamrollBonus: Long,
overwhelmingOddsPercentage: Float,
overwhelmingOddsBonus: Long
): Long = {
if (opposingSize * steamrollPercentage < victorSize.toFloat) {
0L //steamroll by the victor
} else if (victorSize * overwhelmingOddsPercentage <= opposingSize.toFloat) {
overwhelmingOddsBonus + opposingSize + victorSize //victory against overwhelming odds
} else {
steamrollBonus * opposingSize //still a battle
}
}
}

View file

@ -0,0 +1,294 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.{Building, StructureType}
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.objects.zones.{HotSpotInfo, ZoneHotSpotProjector}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
import net.psforever.util.Config
import akka.pattern.ask
import akka.util.Timeout
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
final case class MajorFacilityHackParticipation(building: Building) extends FacilityHackParticipation {
private implicit val timeout: Timeout = 10.seconds
private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]()
def TryUpdate(): Unit = {
val list = building.PlayersInSOI
updatePlayers(list)
val now = System.currentTimeMillis()
if (now - lastInfoRequest > 60000L) {
updatePopulationOverTime(list, now, before = 900000L)
updateHotSpotInfoOverTime()
updateTime(now)
}
lastInfoRequest = now
}
private def updateHotSpotInfoOnly(): Future[ZoneHotSpotProjector.ExposedHeat] = {
ask(
building.Zone.Activity,
ZoneHotSpotProjector.ExposeHeatForRegion(building.Position, building.Definition.SOIRadius.toFloat)
).mapTo[ZoneHotSpotProjector.ExposedHeat]
}
private def updateHotSpotInfoOverTime(): Future[ZoneHotSpotProjector.ExposedHeat] = {
import net.psforever.objects.zones.ZoneHotSpotProjector
import scala.concurrent.Promise
import scala.util.Success
val requestLayers: Promise[ZoneHotSpotProjector.ExposedHeat] = Promise[ZoneHotSpotProjector.ExposedHeat]()
val request = updateHotSpotInfoOnly()
requestLayers.completeWith(request)
request.onComplete {
case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
hotSpotLayersOverTime = timeSensitiveFilterAndAppend(hotSpotLayersOverTime, activity, System.currentTimeMillis() - 900000L)
case _ =>
requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(Vector3.Zero, 0, Nil)))
}
requestLayers.future
}
def RewardFacilityCapture(
defenderFaction: PlanetSideEmpire.Value,
attackingFaction: PlanetSideEmpire.Value,
hacker: PlayerSource,
hackTime: Long,
completionTime: Long,
isResecured: Boolean
): Unit = {
val curr = System.currentTimeMillis()
val hackStart = curr - completionTime
val socketOpt = building.GetFlagSocket
val (victorFaction, opposingFaction, hasFlag, flagCarrier) = if (!isResecured) {
val carrier = socketOpt.flatMap(_.previousFlag).flatMap(_.Carrier)
(attackingFaction, defenderFaction, socketOpt.nonEmpty, carrier)
} else {
(defenderFaction, attackingFaction, socketOpt.nonEmpty, None)
}
val (contributionVictor, contributionOpposing, _) = {
val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction }
(a.values, b.values, c.values)
}
val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) {
//setup for ...
val populationIndices = playerPopulationOverTime.indices
val allFactions = PlanetSideEmpire.values.filterNot { _ == PlanetSideEmpire.NEUTRAL }.toSeq
val (victorPopulationByLayer, opposingPopulationByLayer) = {
val individualPopulationByLayer = allFactions.map { f =>
(f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
}
val contributionOpposingSize = contributionOpposing.size
val killsByPlayersNotInTower = eliminateClosestTowerFromParticipating(
building,
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
hackStart,
completionTime,
opposingFaction,
contributionOpposing
)
)
//1) experience from killing opposingFaction across duration of hack
//The kills that occurred in the facility's attached field tower's sphere of influence have been eliminated from consideration.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
killsByPlayersNotInTower,
contributionOpposingSize
)
//2) peak population modifier
//Large facility battles should be well-rewarded.
val populationModifier = FacilityHackParticipation.populationProgressModifier(
opposingPopulationByLayer,
{ pop =>
if (pop > 75) 0.9f
else if (pop > 59) 0.6f
else if (pop > 29) 0.55f
else if (pop > 25) 0.5f
else 0.45f
},
4
)
//3) competition multiplier
val competitionMultiplier: Float = {
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.5f
)
//compensate for heat
val regionHeatMapProgression = {
/*
transform the different layers of the facility heat map timeline into a progressing timeline of regional hotspot information;
where the grouping are of simultaneous hotspots,
the letter indicates a unique hotspot,
and the number an identifier between related hotspots:
((A-1, B-2, C-3), (D-1, E-2, F-3), (G-1, H-2, I-3)) ... (1->(A, D, G), 2->(B, E, H), 3->(C, F, I))
*/
val finalMap = mutable.HashMap[Vector3, Map[PlanetSideEmpire.Value, Seq[Long]]]()
.addAll(
hotSpotLayersOverTime.take(1).flatMap { entry =>
entry.map { f => (f.DisplayLocation, Map.empty[PlanetSideEmpire.Value, Seq[Long]]) }
}
)
//note: this pre-seeding of keys allows us to skip a getOrElse call in the foldLeft
hotSpotLayersOverTime.foldLeft(finalMap) { (map, list) =>
list.foreach { entry =>
val key = entry.DisplayLocation
val newValues = entry.Activity.map { case (f, e) => (f, e.Heat.toLong) }
val combinedValues = map(key).map { case (f, e) => (f, e :+ newValues(f)) }
map.put(key, combinedValues)
}
map
}.toMap
finalMap //explicit for no good reason
}
val heatMapModifier = FacilityHackParticipation.heatMapComparison(
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, victorFaction).values,
FacilityHackParticipation.diffHeatForFactionMap(regionHeatMapProgression, opposingFaction).values
)
heatMapModifier * populationBalanceModifier
}
//4) hack time modifier
//Captured major facilities without a lattice link unit and resecured major facilities with a lattice link unit
// incur the full hack time if the module is not transported to a friendly facility
//Captured major facilities with a lattice link unit and resecure major facilities without a lattice link uit
// will incur an abbreviated duration
val overallTimeMultiplier: Float = {
if (
building.Faction == PlanetSideEmpire.NEUTRAL ||
building.NtuLevel == 0 ||
building.Generator.map { _.Condition }.contains(PlanetSideGeneratorState.Destroyed)
) { //the facility ran out of nanites or power during the hack or became neutral
0f
} else if (hasFlag) {
if (completionTime >= hackTime) { //hack timed out without llu delivery
0.25f
} else if (isResecured) {
0.5f + (if (hackTime <= completionTime * 0.3f) {
completionTime.toFloat / hackTime.toFloat
} else if (hackTime >= completionTime * 0.6f) {
(hackTime - completionTime).toFloat / hackTime.toFloat
} else {
0f
})
} else {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
}
} else {
if (isResecured) {
0.5f + (hackTime - completionTime).toFloat / (2f * hackTime)
} else {
0.5f
}
}
}
//5. individual contribution factors - by time
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 600000 && math.abs(completionTime - t) < 5000 =>
(a, 0.45f)
case (a, (_, d, _)) if d >= 600000 =>
(a, 0.25f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f * (0.5f + (d.toFloat / 600000f)))
case (a, (_, _, _)) =>
(a, 0.15f)
}
//6. competition bonus
//This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 5L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 15L
)
//7. calculate overall command experience points
val finalCep: Long = math.ceil(
math.max(0L, baseExperienceFromFacilityCapture) *
populationModifier *
competitionMultiplier *
overallTimeMultiplier *
Config.app.game.experience.cep.rate + competitionBonus
).toLong
//8. reward participants
//Classically, only players in the SOI are rewarded, and the llu runner too
val hackerId = hacker.CharId
val events = building.Zone.AvatarEvents
val playersInSoi = building.PlayersInSOI
if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
}
playersInSoi
.filter { player => player.Faction == victorFaction && player.CharId != hackerId }
.foreach { player =>
val charId = player.CharId
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
val outputValue = (finalCep * contributionMultiplier).toLong
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, outputValue))
}
flagCarrier.collect {
case player if !isResecured =>
val charId: Long = player.CharId
val finalModifiedCep: Long = {
val durationPoints: Long = (hackTime - completionTime) / 1500L
val betterDurationPoints: Long = if (durationPoints >= 200L) {
durationPoints
} else {
200L + durationPoints
}
math.min(
betterDurationPoints,
(finalCep * Config.app.game.experience.cep.lluCarrierModifier).toLong
)
}
ToDatabase.reportFacilityCapture(
charId,
building.Zone.Number,
building.GUID.guid,
finalModifiedCep,
expType="llu"
)
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep))
}
}
}
private def eliminateClosestTowerFromParticipating(
building: Building,
list: Iterable[(UniquePlayer, Float, Seq[Kill])]
): Iterable[(UniquePlayer, Float, Seq[Kill])] = {
val buildingPosition = building.Position.xy
building
.Zone
.Buildings
.values
.filter { building => building.BuildingType == StructureType.Tower }
.minByOption { tower => Vector3.DistanceSquared(buildingPosition, tower.Position.xy) }
.map { tower =>
val towerPosition = tower.Position.xy
val towerRadius = math.pow(tower.Definition.SOIRadius.toDouble * 0.7d, 2d).toFloat
list
.map { case (p, f, kills) =>
val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) <= towerRadius }
(p, f, filteredKills)
}
.filter { case (_, _, kills) => kills.nonEmpty }
}
.getOrElse(list)
}
}

View file

@ -0,0 +1,20 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.types.PlanetSideEmpire
case object NoParticipation extends ParticipationLogic {
def building: Building = Building.NoBuilding
def TryUpdate(): Unit = { /* nothing here */ }
def RewardFacilityCapture(
defenderFaction: PlanetSideEmpire.Value,
attackingFaction: PlanetSideEmpire.Value,
hacker: PlayerSource,
hackTime: Long,
completionTime: Long,
isResecured: Boolean
): Unit = { /* nothing here */ }
override def PlayerContribution(timeDelay: Long): Map[Long, Float] = Map.empty[Long, Float]
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.types.PlanetSideEmpire
//noinspection ScalaUnusedSymbol
trait ParticipationLogic {
def building: Building
def TryUpdate(): Unit
/**
* na
* @param defenderFaction those attempting to stop the hack
* the `terminal` (above) and facility originally belonged to this empire
* @param attackingFaction those attempting to progress the hack;
* the `hacker` (below) belongs to this empire
* @param hacker the player who hacked the capture terminal (above)
* @param hackTime how long the over-all facility hack allows or requires
* @param completionTime how long the facility hacking process lasted
* @param isResecured whether `defendingFaction` or the `attackingFaction` succeeded;
* the latter is called a "capture",
* while the former is a "resecure"
*/
def RewardFacilityCapture(
defenderFaction: PlanetSideEmpire.Value,
attackingFaction: PlanetSideEmpire.Value,
hacker: PlayerSource,
hackTime: Long,
completionTime: Long,
isResecured: Boolean
): Unit
def PlayerContribution(timeDelay: Long = 600): Map[Long, Float]
}

View file

@ -0,0 +1,163 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config
final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation {
def TryUpdate(): Unit = {
val list = building.PlayersInSOI
updatePlayers(building.PlayersInSOI)
val now = System.currentTimeMillis()
if (now - lastInfoRequest > 60000L) {
updatePopulationOverTime(list, now, before = 300000L)
}
}
def RewardFacilityCapture(
defenderFaction: PlanetSideEmpire.Value,
attackingFaction: PlanetSideEmpire.Value,
hacker: PlayerSource,
hackTime: Long,
completionTime: Long,
isResecured: Boolean
): Unit = {
val (victorFaction, opposingFaction) = if (!isResecured) {
(attackingFaction, defenderFaction)
} else {
(defenderFaction, attackingFaction)
}
val (contributionVictor, contributionOpposing, _) = {
//TODO this is only to preserve a semblance of the original return type; fix this output
val (a, b1) = playerContribution.partition { case (_, (p, _, _)) => p.Faction == victorFaction }
val (b, c) = b1.partition { case (_, (p, _, _)) => p.Faction == opposingFaction }
(a.values, b.values, c.values)
}
val contributionVictorSize = contributionVictor.size
if (contributionVictorSize > 0) {
//setup for ...
import scala.concurrent.duration._
val curr = System.currentTimeMillis()
val contributionOpposingSize = contributionOpposing.size
val populationIndices = playerPopulationOverTime.indices
val allFactions = PlanetSideEmpire.values.filterNot {
_ == PlanetSideEmpire.NEUTRAL
}.toSeq
val (victorPopulationByLayer, opposingPopulationByLayer) = {
val individualPopulationByLayer = allFactions.map { f =>
(f, populationIndices.indices.map { i => playerPopulationOverTime(i)(f) })
}.toMap[PlanetSideEmpire.Value, Seq[Int]]
(individualPopulationByLayer(victorFaction), individualPopulationByLayer(opposingFaction))
}
val soiPlayers = building.PlayersInSOI
//1) experience from killing opposingFaction
//Because the hack duration of towers is instantaneous, the prior period of five minutes is artificially selected.
val baseExperienceFromFacilityCapture: Long = FacilityHackParticipation.calculateExperienceFromKills(
FacilityHackParticipation.allocateKillsByPlayers(
building.Position,
building.Definition.SOIRadius.toFloat,
curr - 5.minutes.toMillis,
curr,
opposingFaction,
contributionVictor
),
contributionOpposingSize
)
//2) peak population modifier
//Towers should not be regarded as major battles.
//As the population rises, the rewards decrease (dramatically).
val populationModifier = FacilityHackParticipation.populationProgressModifier(
victorPopulationByLayer,
{ pop =>
if (pop > 80) 0f
else if (pop > 39) (80 - pop).toFloat * 0.01f
else if (pop > 25) 0.5f
else if (pop > 19) 0.55f
else if (pop > 9) 0.6f
else if (pop > 5) 0.75f
else 1f
},
2
)
//3) competition multiplier
val competitionMultiplier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.25f
)
//4a. individual contribution factors - by time
//Once again, an arbitrary five minute period.
val contributionPerPlayerByTime = playerContribution.collect {
case (a, (_, d, t)) if d >= 300000 && math.abs(completionTime - t) < 5000 =>
(a, 0.45f)
case (a, (_, d, _)) if d >= 300000 =>
(a, 0.25f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f * (0.5f + (d.toFloat / 300000f)))
case (a, (_, _, _)) =>
(a, 0.15f)
}
//4b. individual contribution factors - by distance to goal (secondary_capture)
//Because the hack duration of towers is instantaneous, distance from terminal is a more important factor
val contributionPerPlayerByDistanceFromGoal = {
var minDistance: Float = Float.PositiveInfinity
val location = building
.CaptureTerminal
.map { terminal => terminal.Position }
.getOrElse { hacker.Position }
soiPlayers
.map { p =>
val distance = Vector3.Distance(p.Position, location)
minDistance = math.min(minDistance, distance)
(p.CharId, distance)
}
.map { case (id, distance) =>
(id, math.max(0.15f, minDistance / distance))
}
}.toMap[Long, Float]
//5) token competition bonus
//This value will probably suck, and that's fine.
val competitionBonus: Long = FacilityHackParticipation.competitionBonus(
contributionVictorSize,
contributionOpposingSize,
steamrollPercentage = 1.25f,
steamrollBonus = 2L,
overwhelmingOddsPercentage = 0.5f,
overwhelmingOddsBonus = 30L
)
//6. calculate overall command experience points
val finalCep: Long = math.ceil(
baseExperienceFromFacilityCapture *
populationModifier *
competitionMultiplier *
Config.app.game.experience.cep.rate + competitionBonus
).toLong
//7. reward participants
//Classically, only players in the SOI are rewarded
val events = building.Zone.AvatarEvents
soiPlayers
.filter { player =>
player.Faction == victorFaction && player.CharId != hacker.CharId
}
.foreach { player =>
val charId = player.CharId
val contributionTimeMultiplier = contributionPerPlayerByTime.getOrElse(charId, 0.5f)
val contributionDistanceMultiplier = contributionPerPlayerByDistanceFromGoal.getOrElse(charId, 0.5f)
val outputValue = (finalCep * contributionTimeMultiplier * contributionDistanceMultiplier).toLong
events ! AvatarServiceMessage(
player.Name,
AvatarAction.AwardCep(0, outputValue)
)
}
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
}
playerContribution.clear()
playerPopulationOverTime.reverse match {
case entry :: _ => playerPopulationOverTime = Seq(entry)
}
}
}

View file

@ -17,7 +17,7 @@ import net.psforever.objects.serverobject.damage.DamageableAmenity
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity} import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity}
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.vital.{HealFromTerm, RepairFromTerm, Vitality} import net.psforever.objects.vital.{HealFromTerminal, RepairFromTerminal, Vitality}
import net.psforever.objects.zones.ZoneAware import net.psforever.objects.zones.ZoneAware
import net.psforever.packet.game.InventoryStateMessage import net.psforever.packet.game.InventoryStateMessage
import net.psforever.services.Service import net.psforever.services.Service
@ -291,7 +291,7 @@ object ProximityTerminalControl {
healAmount healAmount
} }
target.Health = health + finalHealthAmount target.Health = health + finalHealthAmount
target.LogActivity(HealFromTerm(AmenitySource(terminal), finalHealthAmount)) target.LogActivity(HealFromTerminal(AmenitySource(terminal), finalHealthAmount))
updateFunc(target) updateFunc(target)
target.Health == maxHealth target.Health == maxHealth
} else { } else {
@ -338,7 +338,7 @@ object ProximityTerminalControl {
repairAmount repairAmount
} }
target.Armor = armor + finalRepairAmount target.Armor = armor + finalRepairAmount
target.LogActivity(RepairFromTerm(AmenitySource(terminal), finalRepairAmount)) target.LogActivity(RepairFromTerminal(AmenitySource(terminal), finalRepairAmount))
val zone = target.Zone val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
zone.id, zone.id,

View file

@ -101,7 +101,7 @@ class FacilityTurretControl(turret: FacilityTurret)
turret.ControlledWeapon(wepNumber = 1).foreach { turret.ControlledWeapon(wepNumber = 1).foreach {
case weapon: Tool => case weapon: Tool =>
// recharge when last shot fired 3s delay, +1, 200ms interval // recharge when last shot fired 3s delay, +1, 200ms interval
if (weapon.Magazine < weapon.MaxMagazine && System.nanoTime() - weapon.LastDischarge > 3000000000L) { if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
weapon.Magazine += 1 weapon.Magazine += 1
val seat = turret.Seat(0).get val seat = turret.Seat(0).get
seat.occupant match { seat.occupant match {

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret package net.psforever.objects.serverobject.turret
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition} import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets} import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
import net.psforever.objects.vital.resistance.ResistanceProfileMutators import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -14,7 +14,8 @@ import scala.collection.mutable
trait TurretDefinition trait TurretDefinition
extends MountableWeaponsDefinition extends MountableWeaponsDefinition
with ResistanceProfileMutators with ResistanceProfileMutators
with DamageResistanceModel { with DamageResistanceModel
with WithShields {
odef: ObjectDefinition => odef: ObjectDefinition =>
Turrets(odef.ObjectId) //let throw NoSuchElementException Turrets(odef.ObjectId) //let throw NoSuchElementException
/* key - upgrade, value - weapon definition */ /* key - upgrade, value - weapon definition */

View file

@ -17,6 +17,7 @@ final case class AmenitySource(
health: Int, health: Int,
Orientation: Vector3, Orientation: Vector3,
occupants: List[SourceEntry], occupants: List[SourceEntry],
installation: SourceEntry,
hacked: Option[HackInfo], hacked: Option[HackInfo],
unique: UniqueAmenity unique: UniqueAmenity
) extends SourceWithHealthEntry { ) extends SourceWithHealthEntry {
@ -54,6 +55,7 @@ object AmenitySource {
health, health,
obj.Orientation, obj.Orientation,
Nil, Nil,
SourceEntry(obj.Owner),
hackData, hackData,
sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position) sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
) )

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing package net.psforever.objects.sourcing
import net.psforever.objects.avatar.scoring.Life
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition} import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
@ -28,7 +29,7 @@ final case class PlayerSource(
jumping: Boolean, jumping: Boolean,
Modifiers: ResistanceProfile, Modifiers: ResistanceProfile,
bep: Long, bep: Long,
kills: Seq[Any], progress: Life,
unique: UniquePlayer unique: UniquePlayer
) extends SourceWithHealthEntry { ) extends SourceWithHealthEntry {
override def Name: String = unique.name override def Name: String = unique.name
@ -46,6 +47,7 @@ object PlayerSource {
val exosuit = p.ExoSuit val exosuit = p.ExoSuit
val faction = p.Faction val faction = p.Faction
val seatedEntity = mountableAndSeat(p) val seatedEntity = mountableAndSeat(p)
val avatar = p.avatar
PlayerSource( PlayerSource(
p.Definition, p.Definition,
exosuit, exosuit,
@ -58,13 +60,17 @@ object PlayerSource {
p.Crouching, p.Crouching,
p.Jumping, p.Jumping,
ExoSuitDefinition.Select(exosuit, faction), ExoSuitDefinition.Select(exosuit, faction),
p.avatar.bep, avatar.bep,
kills = Nil, progress = avatar.scorecard.CurrentLife,
UniquePlayer(p.CharId, p.Name, p.Sex, faction) UniquePlayer(p.CharId, p.Name, p.Sex, faction)
) )
} }
def apply(name: String, faction: PlanetSideEmpire.Value, position: Vector3): PlayerSource = { def apply(name: String, faction: PlanetSideEmpire.Value, position: Vector3): PlayerSource = {
this(UniquePlayer(0L, name, CharacterSex.Male, faction), position)
}
def apply(unique: UniquePlayer, position: Vector3): PlayerSource = {
new PlayerSource( new PlayerSource(
GlobalDefinitions.avatar, GlobalDefinitions.avatar,
ExoSuitType.Standard, ExoSuitType.Standard,
@ -78,8 +84,8 @@ object PlayerSource {
jumping = false, jumping = false,
GlobalDefinitions.Standard, GlobalDefinitions.Standard,
bep = 0L, bep = 0L,
kills = Nil, progress = tokenLife,
UniquePlayer(0L, name, CharacterSex.Male, faction) unique
) )
} }
@ -116,6 +122,7 @@ object PlayerSource {
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = { def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
val exosuit = player.ExoSuit val exosuit = player.ExoSuit
val faction = player.Faction val faction = player.Faction
val avatar = player.avatar
PlayerSource( PlayerSource(
player.Definition, player.Definition,
exosuit, exosuit,
@ -128,12 +135,25 @@ object PlayerSource {
player.Crouching, player.Crouching,
player.Jumping, player.Jumping,
ExoSuitDefinition.Select(exosuit, faction), ExoSuitDefinition.Select(exosuit, faction),
player.avatar.bep, avatar.bep,
kills = Nil, progress = tokenLife,
UniquePlayer(player.CharId, player.Name, player.Sex, faction) UniquePlayer(player.CharId, player.Name, player.Sex, faction)
) )
} }
/**
* Produce a copy of a normal player source entity
* but the `seatedIn` field is overrode to point at the specified vehicle and seat number.<br>
* Don't think too much about it.
* @param player `SourceEntry` for a player
* @param source `SourceEntry` for the aforementioned mountable entity
* @param seatNumber the attributed seating index in which the player is mounted in `source`
* @return a `PlayerSource` entity
*/
def inSeat(player: PlayerSource, source: SourceEntry, seatNumber: Int): PlayerSource = {
player.copy(seatedIn = Some((source, seatNumber)))
}
/** /**
* "Nobody is my name: Nobody they call me * "Nobody is my name: Nobody they call me
* my mother and my father and all my other companions * my mother and my father and all my other companions
@ -142,4 +162,9 @@ object PlayerSource {
* the others first: this will be my guest-gift to you. * the others first: this will be my guest-gift to you.
*/ */
final val Nobody = PlayerSource("Nobody", PlanetSideEmpire.NEUTRAL, Vector3.Zero) final val Nobody = PlayerSource("Nobody", PlanetSideEmpire.NEUTRAL, Vector3.Zero)
/**
* Used to dummy the statistics value for shallow player source entities.
*/
private val tokenLife: Life = Life()
} }

View file

@ -17,14 +17,15 @@ final case class VehicleSource(
Orientation: Vector3, Orientation: Vector3,
Velocity: Option[Vector3], Velocity: Option[Vector3],
deployed: DriveState.Value, deployed: DriveState.Value,
owner: Option[UniquePlayer],
occupants: List[SourceEntry], occupants: List[SourceEntry],
Modifiers: ResistanceProfile, Modifiers: ResistanceProfile,
unique: UniqueVehicle unique: UniqueVehicle
) extends SourceWithHealthEntry with SourceWithShieldsEntry { ) extends SourceWithHealthEntry with SourceWithShieldsEntry {
def Name: String = SourceEntry.NameFormat(Definition.Name) def Name: String = SourceEntry.NameFormat(Definition.Name)
def Health: Int = health def Health: Int = health
def Shields: Int = shields def Shields: Int = shields
def total: Int = health + shields def total: Int = health + shields
} }
object VehicleSource { object VehicleSource {
@ -42,6 +43,7 @@ object VehicleSource {
obj.Orientation, obj.Orientation,
obj.Velocity, obj.Velocity,
obj.DeploymentState, obj.DeploymentState,
None,
Nil, Nil,
obj.Definition.asInstanceOf[ResistanceProfile], obj.Definition.asInstanceOf[ResistanceProfile],
UniqueVehicle( UniqueVehicle(
@ -52,13 +54,15 @@ object VehicleSource {
obj.OriginalOwnerName.getOrElse("none") obj.OriginalOwnerName.getOrElse("none")
) )
) )
vehicle.copy(occupants = { //shallow information that references the existing source entry
obj.Seats.map { case (seatNumber, seat) => vehicle.copy(
owner = obj.Owners,
occupants = obj.Seats.map { case (seatNumber, seat) =>
seat.occupant match { seat.occupant match {
case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber) //shallow case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber)
case _ => PlayerSource.Nobody case _ => PlayerSource.Nobody
} }
}.toList }.toList
}) )
} }
} }

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020 PSForever // Copyright (c) 2020 PSForever
package net.psforever.objects.vehicles package net.psforever.objects.vehicles
import akka.actor.ActorRef import akka.actor.{ActorRef, Cancellable}
import net.psforever.actors.commands.NtuCommand import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.BuildingActor import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.deploy.Deployment
@ -13,17 +13,18 @@ import net.psforever.types.DriveState
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.serverobject.transfer.TransferContainer.TransferMaterial
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior { trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
var panelAnimationFunc: () => Unit = NoCharge var panelAnimationFunc: () => Unit = NoCharge
var ntuChargingTick = Default.Cancellable var ntuChargingTick: Cancellable = Default.Cancellable
findChargeTargetFunc = Vehicles.FindANTChargingSource findChargeTargetFunc = Vehicles.FindANTChargingSource
findDischargeTargetFunc = Vehicles.FindANTDischargingTarget findDischargeTargetFunc = Vehicles.FindANTDischargingTarget
def TransferMaterial = Ntu.Nanites def TransferMaterial: TransferMaterial = Ntu.Nanites
def ChargeTransferObject: Vehicle with NtuContainer def ChargeTransferObject: Vehicle with NtuContainer
@ -186,8 +187,10 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
val chargeToDeposit = if (min == 0) { val chargeToDeposit = if (min == 0) {
transferTarget match { transferTarget match {
case Some(silo: ResourceSilo) => case Some(silo: ResourceSilo) =>
// Silos would charge from 0-100% in roughly 105s on live (~20%-100% https://youtu.be/veOWToR2nSk?t=1402) scala.math.min(
scala.math.min(scala.math.min(silo.MaxNtuCapacitor / 105, chargeable.NtuCapacitor), max) scala.math.min(silo.MaxNtuCapacitor / silo.Definition.ChargeTime.toMillis.toFloat, chargeable.NtuCapacitor),
max
)
case _ => case _ =>
0 0
} }

View file

@ -5,12 +5,9 @@ import akka.actor.{Actor, Cancellable}
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.packet.game.{ import net.psforever.objects.sourcing.VehicleSource
CargoMountPointStatusMessage, import net.psforever.objects.vital.VehicleCargoMountActivity
ObjectAttachMessage, import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage}
ObjectDetachMessage,
PlanetsideAttributeMessage
}
import net.psforever.types.{BailType, CargoStatus, PlanetSideGUID, Vector3} import net.psforever.types.{BailType, CargoStatus, PlanetSideGUID, Vector3}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.Service import net.psforever.services.Service
@ -35,14 +32,12 @@ trait CarrierBehavior {
cargoDismountTimer.cancel() cargoDismountTimer.cancel()
val obj = CarrierObject val obj = CarrierObject
val zone = obj.Zone val zone = obj.Zone
zone.GUID(isMounting) match { zone.GUID(isMounting).collect {
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID) case v : Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
case _ => ;
} }
isMounting = None isMounting = None
zone.GUID(isDismounting) match { zone.GUID(isDismounting).collect {
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID) case v : Vehicle => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
case _ => ;
} }
isDismounting = None isDismounting = None
} }
@ -89,9 +84,8 @@ trait CarrierBehavior {
) )
} }
else { else {
obj.Zone.GUID(isMounting) match { obj.Zone.GUID(isMounting).collect {
case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID) case v: Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
case _ => ;
} }
isMounting = None isMounting = None
} }
@ -115,10 +109,9 @@ trait CarrierBehavior {
kicked = false kicked = false
) )
case _ => case _ =>
obj.CargoHold(mountPoint) match { obj.CargoHold(mountPoint).collect {
case Some(hold) if hold.isOccupied && hold.occupant.get.GUID == cargo_guid => case hold if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
hold.unmount(hold.occupant.get) CarrierBehavior.CargoDismountAction(obj, hold.occupant.get, hold, BailType.Normal)
case _ => ;
} }
false false
} }
@ -135,17 +128,15 @@ trait CarrierBehavior {
CarrierBehavior.CheckCargoDismount(cargo_guid, mountPoint, iteration + 1, bailed) CarrierBehavior.CheckCargoDismount(cargo_guid, mountPoint, iteration + 1, bailed)
) )
} else { } else {
zone.GUID(isDismounting.getOrElse(cargo_guid)) match { zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
case Some(cargo: Vehicle) => case cargo: Vehicle =>
cargo.Actor ! CargoBehavior.EndCargoDismounting(guid) cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
case _ => ;
} }
isDismounting = None isDismounting = None
} }
} else { } else {
zone.GUID(isDismounting.getOrElse(cargo_guid)) match { zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
case Some(cargo: Vehicle) => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid) case cargo: Vehicle => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
case _ => ;
} }
isDismounting = None isDismounting = None
} }
@ -213,10 +204,8 @@ object CarrierBehavior {
if (distance <= 64) { if (distance <= 64) {
//cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it //cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it
log.debug(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance") log.debug(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance")
hold.mount(cargo) CargoMountAction(carrier, cargo, hold, carrierGUID)
cargo.MountedIn = carrierGUID
cargo.Velocity = None cargo.Velocity = None
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
zone.VehicleEvents ! VehicleServiceMessage( zone.VehicleEvents ! VehicleServiceMessage(
s"${cargo.Actor}", s"${cargo.Actor}",
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)) VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))
@ -358,9 +347,7 @@ object CarrierBehavior {
//obviously, don't do this //obviously, don't do this
} else if (iteration > 40) { } else if (iteration > 40) {
//cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold //cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold
hold.mount(cargo) CargoMountAction(carrier, cargo, hold, carrierGUID)
cargo.MountedIn = carrierGUID
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
CargoMountBehaviorForAll(carrier, cargo, mountPoint) CargoMountBehaviorForAll(carrier, cargo, mountPoint)
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo) zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
false false
@ -441,9 +428,10 @@ object CarrierBehavior {
val zone = carrier.Zone val zone = carrier.Zone
carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match { carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match {
case Some((mountPoint, hold)) => case Some((mountPoint, hold)) =>
cargo.MountedIn = None CarrierBehavior.CargoDismountAction(
hold.unmount( carrier,
cargo, cargo,
hold,
if (bailed) BailType.Bailed else if (kicked) BailType.Kicked else BailType.Normal if (bailed) BailType.Bailed else if (kicked) BailType.Kicked else BailType.Normal
) )
val driverOpt = cargo.Seats(0).occupant val driverOpt = cargo.Seats(0).occupant
@ -532,7 +520,7 @@ object CarrierBehavior {
targetGUID: PlanetSideGUID targetGUID: PlanetSideGUID
): Unit = { ): Unit = {
target match { target match {
case Some(_: Vehicle) => ; case Some(_: Vehicle) => ()
case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle") case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle")
case None => log.error(s"$decorator target $targetGUID has gone missing") case None => log.error(s"$decorator target $targetGUID has gone missing")
} }
@ -653,4 +641,62 @@ object CarrierBehavior {
) )
msgs msgs
} }
/**
* na
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param hold na
* @param carrierGuid the ferrying vehicle's unique identifier
*/
private def CargoMountAction(
carrier: Vehicle,
cargo: Vehicle,
hold: Cargo,
carrierGuid: PlanetSideGUID): Unit = {
hold.mount(cargo)
cargo.MountedIn = carrierGuid
val event = VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number)
cargo.LogActivity(event)
cargo.Seats
.filterNot(_._1 == 0) /*ignore driver*/
.values
.collect {
case seat if seat.isOccupied =>
seat.occupants.foreach { player =>
player.LogActivity(event)
}
}
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGuid)
}
/**
* na
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param hold na
* @param bailType na
*/
private def CargoDismountAction(
carrier: Vehicle,
cargo: Vehicle,
hold: Cargo,
bailType: BailType.Value
): Unit = {
cargo.MountedIn = None
hold.unmount(cargo, bailType)
val event = VehicleCargoMountActivity(VehicleSource(carrier), VehicleSource(cargo), carrier.Zone.Number)
cargo.LogActivity(event)
cargo.Seats
.filterNot(_._1 == 0) /*ignore driver*/
.values
.collect {
case seat if seat.isOccupied =>
seat.occupants.foreach { player =>
player.LogActivity(event)
player.ContributionFrom(cargo)
player.ContributionFrom(carrier)
}
}
}
} }

View file

@ -170,8 +170,8 @@ class BfrControl(vehicle: Vehicle)
specialArmWeaponEquipManagement(item, slot, handiness) specialArmWeaponEquipManagement(item, slot, handiness)
} }
override def dismountCleanup(seatBeingDismounted: Int): Unit = { override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = {
super.dismountCleanup(seatBeingDismounted) super.dismountCleanup(seatBeingDismounted, player)
if (!vehicle.Seats.values.exists(_.isOccupied)) { if (!vehicle.Seats.values.exists(_.isOccupied)) {
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
case Some(subsys) => case Some(subsys) =>

View file

@ -34,9 +34,9 @@ class DeployingVehicleControl(vehicle: Vehicle)
case msg : Deployment.TryUndeploy => case msg : Deployment.TryUndeploy =>
deployBehavior.apply(msg) deployBehavior.apply(msg)
case msg @ Mountable.TryDismount(_, seat_num, _) => case msg @ Mountable.TryDismount(player, seat_num, _) =>
dismountBehavior.apply(msg) dismountBehavior.apply(msg)
dismountCleanup(seat_num) dismountCleanup(seat_num, player)
} }
/** /**

View file

@ -20,10 +20,10 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.repair.RepairableVehicle import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.sourcing.{SourceEntry, VehicleSource} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge} import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.SuicideReason import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones._ import net.psforever.objects.zones._
@ -128,9 +128,9 @@ class VehicleControl(vehicle: Vehicle)
mountBehavior.apply(msg) mountBehavior.apply(msg)
mountCleanup(mount_point, player) mountCleanup(mount_point, player)
case msg @ Mountable.TryDismount(_, seat_num, _) => case msg @ Mountable.TryDismount(player, seat_num, _) =>
dismountBehavior.apply(msg) dismountBehavior.apply(msg)
dismountCleanup(seat_num) dismountCleanup(seat_num, player)
case CommonMessages.ChargeShields(amount, motivator) => case CommonMessages.ChargeShields(amount, motivator) =>
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }) chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) })
@ -237,9 +237,9 @@ class VehicleControl(vehicle: Vehicle)
def commonDisabledBehavior: Receive = checkBehavior def commonDisabledBehavior: Receive = checkBehavior
.orElse { .orElse {
case msg @ Mountable.TryDismount(_, seat_num, _) => case msg @ Mountable.TryDismount(user, seat_num, _) =>
dismountBehavior.apply(msg) dismountBehavior.apply(msg)
dismountCleanup(seat_num) dismountCleanup(seat_num, user)
case Vehicle.Deconstruct(time) => case Vehicle.Deconstruct(time) =>
time match { time match {
@ -286,7 +286,7 @@ class VehicleControl(vehicle: Vehicle)
val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger) val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger)
val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire)
(if (seatGroup == AccessPermissionGroup.Driver) { (if (seatGroup == AccessPermissionGroup.Driver) {
vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked vehicle.OwnerGuid.contains(user.GUID) || vehicle.OwnerGuid.isEmpty || permission != VehicleLockState.Locked
} else { } else {
permission != VehicleLockState.Locked permission != VehicleLockState.Locked
}) && }) &&
@ -297,6 +297,8 @@ class VehicleControl(vehicle: Vehicle)
val obj = MountableObject val obj = MountableObject
obj.PassengerInSeat(user) match { obj.PassengerInSeat(user) match {
case Some(seatNumber) => case Some(seatNumber) =>
val vsrc = VehicleSource(vehicle)
user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
//if the driver mount, change ownership if that is permissible for this vehicle //if the driver mount, change ownership if that is permissible for this vehicle
if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) { if (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) {
//whatever vehicle was previously owned //whatever vehicle was previously owned
@ -325,7 +327,7 @@ class VehicleControl(vehicle: Vehicle)
vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user) vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user)
} }
def dismountCleanup(seatBeingDismounted: Int): Unit = { def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = {
val obj = MountableObject val obj = MountableObject
// Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
if (!obj.Seats(0).isOccupied) { if (!obj.Seats(0).isOccupied) {
@ -340,6 +342,7 @@ class VehicleControl(vehicle: Vehicle)
) )
} }
if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated if (!obj.Seats(seatBeingDismounted).isOccupied) { //seat was vacated
user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number))
//we were only owning the vehicle while we sat in its driver seat //we were only owning the vehicle while we sat in its driver seat
val canBeOwned = obj.Definition.CanBeOwned val canBeOwned = obj.Definition.CanBeOwned
if (canBeOwned.contains(false) && seatBeingDismounted == 0) { if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
@ -348,7 +351,7 @@ class VehicleControl(vehicle: Vehicle)
//are we already decaying? are we unowned? is no one seated anywhere? //are we already decaying? are we unowned? is no one seated anywhere?
if (!decaying && if (!decaying &&
obj.Definition.undergoesDecay && obj.Definition.undergoesDecay &&
obj.Owner.isEmpty && obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) { obj.Seats.values.forall(!_.isOccupied)) {
decaying = true decaying = true
decayTimer = context.system.scheduler.scheduleOnce( decayTimer = context.system.scheduler.scheduleOnce(
@ -418,7 +421,7 @@ class VehicleControl(vehicle: Vehicle)
Vehicles.Disown(obj.GUID, obj) Vehicles.Disown(obj.GUID, obj)
if (!decaying && if (!decaying &&
obj.Definition.undergoesDecay && obj.Definition.undergoesDecay &&
obj.Owner.isEmpty && obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) { obj.Seats.values.forall(!_.isOccupied)) {
decaying = true decaying = true
decayTimer = context.system.scheduler.scheduleOnce( decayTimer = context.system.scheduler.scheduleOnce(
@ -890,7 +893,7 @@ object VehicleControl {
/** /**
* Determine if a given activity entry would invalidate the act of charging vehicle shields this tick. * Determine if a given activity entry would invalidate the act of charging vehicle shields this tick.
* @param now the current time (in nanoseconds) * @param now the current time (in milliseconds)
* @param act a `VitalsActivity` entry to test * @param act a `VitalsActivity` entry to test
* @return `true`, if the shield charge would be blocked; * @return `true`, if the shield charge would be blocked;
* `false`, otherwise * `false`, otherwise

View file

@ -4,12 +4,15 @@ package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition} import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{AmenitySource, ObjectSource, PlayerSource, SourceEntry, SourceWithHealthEntry, VehicleSource} import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason} import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType} import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
import net.psforever.util.Config
import scala.collection.mutable
/* root */ /* root */
@ -18,7 +21,21 @@ import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
* Must keep track of the time (ms) the activity occurred. * Must keep track of the time (ms) the activity occurred.
*/ */
trait InGameActivity { trait InGameActivity {
val time: Long = System.currentTimeMillis() private var _time: Long = System.currentTimeMillis()
def time: Long = _time
}
object InGameActivity {
def ShareTime(benefactor: InGameActivity, donor: InGameActivity): InGameActivity = {
benefactor._time = donor.time
benefactor
}
def SetTime(benefactor: InGameActivity, time: Long): InGameActivity = {
benefactor._time = time
benefactor
}
} }
/* normal history */ /* normal history */
@ -28,13 +45,64 @@ trait InGameActivity {
*/ */
trait GeneralActivity extends InGameActivity trait GeneralActivity extends InGameActivity
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity trait SupportActivityCausedByAnother {
def user: PlayerSource
def amount: Int
}
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
extends GeneralActivity
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) extends GeneralActivity final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
extends GeneralActivity
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) extends GeneralActivity final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amount: Int, equipment: EquipmentDefinition)
extends GeneralActivity with SupportActivityCausedByAnother
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
extends GeneralActivity
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
extends GeneralActivity
sealed trait VehicleMountChange extends GeneralActivity {
def vehicle: VehicleSource
def zoneNumber: Int
}
sealed trait VehiclePassengerMountChange extends VehicleMountChange {
def player: PlayerSource
}
sealed trait VehicleCargoMountChange extends VehicleMountChange {
def cargo: VehicleSource
}
final case class VehicleMountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int)
extends VehiclePassengerMountChange
final case class VehicleDismountActivity(
vehicle: VehicleSource,
player: PlayerSource,
zoneNumber: Int,
pairedEvent: Option[VehicleMountActivity] = None
) extends VehiclePassengerMountChange
final case class VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
extends VehicleCargoMountChange
final case class VehicleCargoDismountActivity(
vehicle: VehicleSource,
cargo: VehicleSource,
zoneNumber: Int,
pairedEvent: Option[VehicleCargoMountActivity] = None
) extends VehicleCargoMountChange
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
extends GeneralActivity {
val start: Long = entries.headOption.map { _.time }.getOrElse(System.currentTimeMillis())
val end: Long = entries.lastOption.map { _.time }.getOrElse(start)
}
/* vitals history */ /* vitals history */
@ -65,9 +133,8 @@ trait DamagingActivity extends VitalsActivity {
def health: Int = { def health: Int = {
(data.targetBefore, data.targetAfter) match { (data.targetBefore, data.targetAfter) match {
case (pb: PlayerSource, pa: PlayerSource) if pb.ExoSuit == ExoSuitType.MAX => pb.total - pa.total case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health case _ => 0
case _ => 0
} }
} }
} }
@ -76,9 +143,9 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
extends HealingActivity extends HealingActivity
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int) final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends HealingActivity extends HealingActivity with SupportActivityCausedByAnother
final case class HealFromTerm(term: AmenitySource, amount: Int) final case class HealFromTerminal(term: AmenitySource, amount: Int)
extends HealingActivity extends HealingActivity
final case class HealFromImplant(implant: ImplantType, amount: Int) final case class HealFromImplant(implant: ImplantType, amount: Int)
@ -91,9 +158,9 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
extends RepairingActivity() extends RepairingActivity()
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int) final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends RepairingActivity extends RepairingActivity with SupportActivityCausedByAnother
final case class RepairFromTerm(term: AmenitySource, amount: Int) extends RepairingActivity final case class RepairFromTerminal(term: AmenitySource, amount: Int) extends RepairingActivity
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int) final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
extends RepairingActivity extends RepairingActivity
@ -158,11 +225,34 @@ trait InGameHistory {
/** /**
* An in-game event must be recorded. * An in-game event must be recorded.
* Add new entry to the list (for recent activity). * Add new entry to the list (for recent activity).
* Special handling must be conducted for certain events.
* @param action the fully-informed entry * @param action the fully-informed entry
* @return the list of previous changes to this entity * @return the list of previous changes to this entity
*/ */
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = { def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
action match { action match {
case Some(act: VehicleDismountActivity) =>
history
.findLast(_.isInstanceOf[VehicleMountActivity])
.collect {
case event: VehicleMountActivity if event.vehicle.unique == act.vehicle.unique =>
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
}
.orElse {
history = history :+ act
None
}
case Some(act: VehicleCargoDismountActivity) =>
history
.findLast(_.isInstanceOf[VehicleCargoMountActivity])
.collect {
case event: VehicleCargoMountActivity if event.vehicle.unique == act.vehicle.unique =>
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
}
.orElse {
history = history :+ act
None
}
case Some(act) => case Some(act) =>
history = history :+ act history = history :+ act
case None => () case None => ()
@ -188,7 +278,7 @@ trait InGameHistory {
LogActivity(DamageFromPainbox(result)) LogActivity(DamageFromPainbox(result))
case _: EnvironmentReason => case _: EnvironmentReason =>
LogActivity(DamageFromEnvironment(result)) LogActivity(DamageFromEnvironment(result))
case _ => ; case _ =>
LogActivity(DamageFrom(result)) LogActivity(DamageFrom(result))
if(result.adversarial.nonEmpty) { if(result.adversarial.nonEmpty) {
lastDamage = Some(result) lastDamage = Some(result)
@ -209,10 +299,55 @@ trait InGameHistory {
} }
} }
/**
* activity that comes from another entity used for scoring;<br>
* key - unique reference to that entity; value - history from that entity
*/
private val contributionInheritance: mutable.HashMap[SourceUniqueness, Contribution] =
mutable.HashMap[SourceUniqueness, Contribution]()
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
if (target eq this) {
None
} else {
val uniqueTarget = SourceEntry(target).unique
(target.GetContribution(), contributionInheritance.get(uniqueTarget)) match {
case (Some(in), Some(curr)) =>
val end = curr.end
val contribution = Contribution(uniqueTarget, curr.entries ++ in.filter(_.time > end))
contributionInheritance.put(uniqueTarget, contribution)
Some(contribution)
case (Some(in), _) =>
val contribution = Contribution(uniqueTarget, in)
contributionInheritance.put(uniqueTarget, contribution)
Some(contribution)
case (None, _) =>
None
}
}
}
def GetContribution(): Option[List[InGameActivity]] = {
Option(GetContributionDuringPeriod(History, duration = Config.app.game.experience.longContributionTime))
}
def GetContributionDuringPeriod(list: List[InGameActivity], duration: Long): List[InGameActivity] = {
val earliestEndTime = System.currentTimeMillis() - duration
list.collect {
case event: DamagingActivity if event.health > 0 && event.time > earliestEndTime => event
case event: RepairingActivity if event.amount > 0 && event.time > earliestEndTime => event
}
}
def HistoryAndContributions(): List[InGameActivity] = {
History ++ contributionInheritance.values.toList
}
def ClearHistory(): List[InGameActivity] = { def ClearHistory(): List[InGameActivity] = {
lastDamage = None lastDamage = None
val out = history val out = history
history = List.empty history = List.empty
contributionInheritance.clear()
out out
} }
} }
@ -221,14 +356,14 @@ object InGameHistory {
def SpawnReconstructionActivity( def SpawnReconstructionActivity(
obj: PlanetSideGameObject with FactionAffinity with InGameHistory, obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
zoneNumber: Int, zoneNumber: Int,
unit: Option[SourceEntry] unit: Option[PlanetSideGameObject with FactionAffinity with InGameHistory]
): Unit = { ): Unit = {
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists { val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
_.isInstanceOf[SpawningActivity]
}) { val event: GeneralActivity = if (obj.History.isEmpty) {
ReconstructionActivity(ObjectSource(obj), zoneNumber, unit) SpawningActivity(SourceEntry(obj), zoneNumber, toUnitSource)
} else { } else {
SpawningActivity(ObjectSource(obj), zoneNumber, unit) ReconstructionActivity(SourceEntry(obj), zoneNumber, toUnitSource)
} }
if (obj.History.lastOption match { if (obj.History.lastOption match {
case Some(evt: SpawningActivity) => evt != event case Some(evt: SpawningActivity) => evt != event
@ -236,6 +371,13 @@ object InGameHistory {
case _ => true case _ => true
}) { }) {
obj.LogActivity(event) obj.LogActivity(event)
unit.foreach { o => obj.ContributionFrom(o) }
} }
} }
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
target
.GetContribution()
.collect { case events => Contribution(SourceEntry(target).unique, events) }
}
} }

View file

@ -102,7 +102,7 @@ class ActivityReport {
* @return the time * @return the time
*/ */
def Duration_=(time: FiniteDuration): FiniteDuration = { def Duration_=(time: FiniteDuration): FiniteDuration = {
Duration_=(time.toNanos) Duration_=(time.toMillis)
} }
/** /**
@ -112,8 +112,8 @@ class ActivityReport {
* @return the time * @return the time
*/ */
def Duration_=(time: Long): FiniteDuration = { def Duration_=(time: Long): FiniteDuration = {
if (time > duration.toNanos) { if (time > duration.toMillis) {
duration = FiniteDuration(time, "nanoseconds") duration = FiniteDuration(time, "milliseconds")
Renew Renew
} }
Duration Duration
@ -177,7 +177,7 @@ class ActivityReport {
* @return the current time * @return the current time
*/ */
def Renew: Long = { def Renew: Long = {
val t = System.nanoTime val t = System.currentTimeMillis()
firstReport = firstReport.orElse(Some(t)) firstReport = firstReport.orElse(Some(t))
lastReport = Some(t) lastReport = Some(t)
t t
@ -191,6 +191,6 @@ class ActivityReport {
heat = 0 heat = 0
firstReport = None firstReport = None
lastReport = None lastReport = None
duration = FiniteDuration(0, "nanoseconds") duration = FiniteDuration(0, "milliseconds")
} }
} }

View file

@ -165,7 +165,7 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki
val attackerFaction = attacker.Faction val attackerFaction = attacker.Faction
val noPriorHotSpots = hotspots.isEmpty val noPriorHotSpots = hotspots.isEmpty
val duration = zone.HotSpotTimeFunction(defender, attacker) val duration = zone.HotSpotTimeFunction(defender, attacker)
if (duration.toNanos > 0) { if (duration.toMillis > 0) {
val hotspot = TryHotSpot(zone.HotSpotCoordinateFunction(location)) val hotspot = TryHotSpot(zone.HotSpotCoordinateFunction(location))
trace( trace(
s"updating activity status for ${zone.id} hotspot x=${hotspot.DisplayLocation.x} y=${hotspot.DisplayLocation.y}" s"updating activity status for ${zone.id} hotspot x=${hotspot.DisplayLocation.x} y=${hotspot.DisplayLocation.y}"
@ -191,11 +191,11 @@ class ZoneHotSpotProjector(zone: Zone, hotspots: ListBuffer[HotSpotInfo], blanki
case ZoneHotSpotProjector.BlankingPhase() | Zone.HotSpot.Cleanup() => case ZoneHotSpotProjector.BlankingPhase() | Zone.HotSpot.Cleanup() =>
blanking.cancel() blanking.cancel()
val curr: Long = System.nanoTime val curr: Long = System.currentTimeMillis()
//blanking dated activity reports //blanking dated activity reports
val changed = hotspots.flatMap(spot => { val changed = hotspots.flatMap(spot => {
spot.Activity.collect { spot.Activity.collect {
case (b, a: ActivityReport) if a.LastReport + a.Duration.toNanos <= curr => case (b, a: ActivityReport) if a.LastReport + a.Duration.toMillis <= curr =>
a.Clear() //this faction has no more activity in this sector a.Clear() //this faction has no more activity in this sector
(b, spot) (b, spot)
} }

View file

@ -0,0 +1,38 @@
// 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
}
sealed abstract class NoIntermediateUseContextWrapper(override val value: Int)
extends EquipmentUseContextWrapper(value) {
def intermediate: Int = 0
}
final case class NoUse() extends NoIntermediateUseContextWrapper(value = -1) {
def equipment: Int = 0
}
final case class DamageWith(equipment: Int) extends NoIntermediateUseContextWrapper(value = 0)
final case class Destroyed(equipment: Int) extends NoIntermediateUseContextWrapper(value = 1)
final case class ReviveAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 4)
final case class AmenityDestroyed(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 10)
final case class DriverKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 12)
final case class GunnerKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 13)
final case class PassengerKilled(equipment: Int) extends NoIntermediateUseContextWrapper(value = 14)
final case class CargoDestroyed(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 15)
final case class DriverAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 18)
final case class HealKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 20)
final case class ReviveKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 21)
final case class RepairKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 22)
final case class AmsRespawnKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 23)
final case class HotDropKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 24)
final case class HackKillAssist(equipment: Int, intermediate: Int) extends EquipmentUseContextWrapper(value = 25)
final case class LodestarRearmKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 26)
final case class AmsResupplyKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 27)
final case class RouterKillAssist(equipment: Int) extends NoIntermediateUseContextWrapper(value = 28)

View file

@ -4,17 +4,11 @@ package net.psforever.objects.zones.exp
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, SupervisorStrategy} import akka.actor.typed.{Behavior, SupervisorStrategy}
import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.avatar.scoring.{Assist, Death, Kill}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, SpawningActivity} import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult} import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import scala.collection.mutable
import scala.concurrent.duration._
object ExperienceCalculator { object ExperienceCalculator {
def apply(zone: Zone): Behavior[Command] = def apply(zone: Zone): Behavior[Command] =
@ -32,38 +26,6 @@ object ExperienceCalculator {
RewardThisDeath(SourceEntry(obj), obj.LastDamage, obj.History) RewardThisDeath(SourceEntry(obj), obj.LastDamage, obj.History)
} }
} }
def calculateExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
val lifespan = (history.headOption, history.lastOption) match {
case (Some(spawn), Some(death)) => death.time - spawn.time
case _ => 0L
}
val wasEverAMax = victim.ExoSuit == ExoSuitType.MAX || history.exists {
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
case _ => false
}
val base = if (wasEverAMax) { //shamed
250L
} else if (victim.Seated || victim.kills.nonEmpty) {
100L
} else if (lifespan > 15000L) {
50L
} else {
1L
}
if (base > 1) {
//black ops modifier
//TODO x10
base
} else {
base
}
}
} }
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone) class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
@ -74,263 +36,10 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
def onMessage(msg: Command): Behavior[Command] = { def onMessage(msg: Command): Behavior[Command] = {
msg match { msg match {
case RewardThisDeath(victim: PlayerSource, lastDamage, history) => case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
rewardThisPlayerDeath( KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
victim,
lastDamage, case _ => ()
limitHistoryToThisLife(history.toList)
)
case _ =>
()
} }
Behaviors.same Behaviors.same
} }
def rewardThisPlayerDeath(
victim: PlayerSource,
lastDamage: Option[DamageResult],
history: List[InGameActivity]
): Unit = {
val everyone = determineKiller(lastDamage, history) match {
case Some((result, killer: PlayerSource)) =>
val assists = collectAssistsForPlayer(victim, history, Some(killer))
val fullBep = KillDeathAssists.calculateExperience(killer, victim, history)
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, history.last.time - history.head.time, fullBep)) +: hitSquad
case _ =>
val assists = collectAssistsForPlayer(victim, history, None)
val fullBep = ExperienceCalculator.calculateExperience(victim, history)
val hitSquad = assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, history.last.time - history.head.time, fullBep)) +: hitSquad
}
val events = zone.AvatarEvents
everyone.foreach { case (p, kda) =>
events ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
}
}
def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
val spawnIndex = history.indexWhere {
case SpawningActivity(_, _, _) => true
case _ => false
}
val endIndex = history.lastIndexWhere {
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
case _ => false
}
if (spawnIndex == -1 || endIndex == -1) {
Nil //throw VitalsHistoryException(history.head, "vitals history does not contain expected conditions")
// } else
// if (spawnIndex == -1) {
// Nil //throw VitalsHistoryException(history.head, "vitals history does not contain initial spawn conditions")
// } else if (endIndex == -1) {
// Nil //throw VitalsHistoryException(history.last, "vitals history does not contain end of life conditions")
} else {
history.slice(spawnIndex, endIndex)
}
}
def determineKiller(lastDamageActivity: Option[DamageResult], history: List[InGameActivity]): Option[(DamageResult, SourceEntry)] = {
val now = System.currentTimeMillis()
val compareTimeMillis = 10.seconds.toMillis
lastDamageActivity
.collect { case dam if now - dam.interaction.hitTime < compareTimeMillis => dam }
.flatMap { dam => Some(dam, dam.adversarial) }
.orElse {
history.collect { case damage: DamagingActivity
if now - damage.time < compareTimeMillis && damage.data.adversarial.nonEmpty =>
damage.data
}
.flatMap { dam => Some(dam, dam.adversarial) }.lastOption
}
.collect { case (dam, Some(adv)) => (dam, adv.attacker) }
}
private[exp] def collectAssistsForPlayer(
victim: PlayerSource,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
// val cardinalSin = victim.ExoSuit == ExoSuitType.MAX || history.exists {
// case SpawningActivity(p: PlayerSource,_,_) => p.ExoSuit == ExoSuitType.MAX
// case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
// case _ => false
// }
val initialHealth = history
.headOption
.collect { case SpawningActivity(p: PlayerSource,_,_) => p.health } match {
case Some(value) => value.toFloat
case _ => 100f
}
val healthAssists = collectHealthAssists(
victim,
history,
initialHealth,
allocateContributors(healthDamageContributors)
)
healthAssists.remove(0L)
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
healthAssists.values
}
private def allocateContributors(
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
)
(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value
): mutable.LongMap[ContributionStats] = {
/** players who have contributed to this death, and how much they have contributed<br>
* key - character identifier,
* value - (player, damage, total damage, number of shots) */
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
tallyFunc(history, faction, participants)
participants
}
private def healthDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/** damage as it is measured in order (with heal-countered damage eliminated)<br>
* key - character identifier,
* value - current damage contribution */
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.tail.foreach {
case d: DamagingActivity if d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
case _: RepairingActivity => ()
case h: HealingActivity =>
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
case _ => ()
}
inOrder
}
private def collectHealthAssists(
victim: SourceEntry,
history: List[InGameActivity],
topHealth: Float,
func: (List[InGameActivity], PlanetSideEmpire.Value)=>mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStatsOutput] = {
val healthAssists = func(history, victim.Faction)
.filterNot { case (_, kda) => kda.amount <= 0 }
.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.weapon_id }, kda.amount / topHealth))
}
healthAssists.remove(victim.CharId)
healthAssists
}
private def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
activity.data.adversarial match {
case Some(Adversarial(attacker: PlayerSource, _, _))
if attacker.Faction != faction =>
val whoId = attacker.CharId
val wepid = activity.data.interaction.cause.attribution
val time = activity.time
val updatedEntry = participants.get(whoId) match {
case Some(mod) =>
//previous attacker, just add to entry
val firstWeapon = mod.weapons.head
val weapons = if (firstWeapon.weapon_id == wepid) {
firstWeapon.copy(amount = firstWeapon.amount + amount, shots = firstWeapon.shots + 1, time = time) +: mod.weapons.tail
} else {
WeaponStats(wepid, amount, 1, time) +: mod.weapons
}
mod.copy(
amount = mod.amount + amount,
weapons = weapons,
totalDamage = mod.totalDamage + amount,
shots = mod.shots + 1,
time = activity.time
)
case None =>
//new attacker, new entry
ContributionStats(
attacker,
Seq(WeaponStats(wepid, amount, 1, time)),
amount,
amount,
1,
time
)
}
participants.put(whoId, updatedEntry)
order.indexWhere({ case (id, _) => id == whoId }) match {
case 0 =>
//ongoing attack by same player
val entry = order.head
(entry._1, entry._2 + amount) +: order.tail
case _ =>
//different player than immediate prior attacker
(whoId, amount) +: order
}
case _ =>
//damage that does not lead to contribution
order.headOption match {
case Some((id, dam)) =>
if (id == 0L) {
(0L, dam + amount) +: order.tail //pool
} else {
(0L, amount) +: order //new
}
case None =>
order
}
}
}
private def contributeWithRecoveryActivity(
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
var amt = amount
var count = 0
var newOrder: Seq[(Long, Int)] = Nil
order.takeWhile { entry =>
val (id, total) = entry
if (id > 0 && total > 0) {
val part = participants(id)
if (amount > total) {
//drop this entry
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
amt = amt - total
} else {
//edit around the inclusion of this entry
val newTotal = total - amt
val trimmedWeapons = {
var index = -1
var weaponSum = 0
val pweapons = part.weapons
while (weaponSum < amt) {
index += 1
weaponSum = weaponSum + pweapons(index).amount
}
pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)
}
newOrder = (id, newTotal) +: newOrder
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
amt = 0
}
}
count += 1
amt > 0
}
newOrder ++ order.drop(count)
}
} }

View file

@ -0,0 +1,642 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import net.psforever.objects.avatar.scoring.{Assist, Death, KDAStat, Kill}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, RepairingActivity, RevivingActivity, SpawningActivity}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
import scala.annotation.tailrec
import scala.collection.mutable
import scala.concurrent.duration._
/**
* One player will interact using any number of weapons they possess
* that will affect a different player - the target.
* A kill is counted as the last interaction that affects a target so as to drop their health to zero.
* An assist is counted as every other interaction that affects the target up until the kill interaction
* in a similar way to the kill interaction.
* @see `ContributionStats`
* @see `ContributionStatsOutput`
* @see `DamagingActivity`
* @see `HealingActivity`
* @see `InGameActivity`
* @see `InGameHistory`
* @see `PlayerSource`
* @see `RepairingActivity`
* @see `SourceEntry`
*/
object KillAssists {
/**
* Primary landing point for calculating the rewards given for player death.
* Rewards in the form of "battle experience points" are given:
* to the player held responsible for the other player's death - the killer;
* all players whose efforts managed to deal damage to the player who died prior to the killer - assists.
* @param victim player that died
* @param lastDamage purported as the in-game activity that resulted in the player dying
* @param history chronology of activity the game considers noteworthy;
* `lastDamage` should be within this chronology
* @param eventBus where to send the results of the experience determination(s)
* @see `ActorRef`
* @see `AvatarAction.UpdateKillsDeathsAssists`
* @see `AvatarServiceMessage`
* @see `DamageResult`
* @see `rewardThisPlayerDeath`
*/
private[exp] def rewardThisPlayerDeath(
victim: PlayerSource,
lastDamage: Option[DamageResult],
history: Iterable[InGameActivity],
eventBus: ActorRef
): Unit = {
rewardThisPlayerDeath(victim, lastDamage, history).foreach { case (p, kda) =>
eventBus ! AvatarServiceMessage(p.Name, AvatarAction.UpdateKillsDeathsAssists(p.CharId, kda))
}
}
/**
* Primary innards of the functionality of calculating the rewards given for player death.
* @param victim player that died
* @param lastDamage purported as the in-game activity that resulted in the player dying
* @param history chronology of activity the game considers noteworthy;
* `lastDamage` should be within this chronology
* @return na
* @see `Assist`
* @see `calculateExperience`
* @see `collectKillAssistsForPlayer`
* @see `DamageResult`
* @see `Death`
* @see `KDAStat`
* @see `limitHistoryToThisLife`
* @see `Support.baseExperience`
*/
private def rewardThisPlayerDeath(
victim: PlayerSource,
lastDamage: Option[DamageResult],
history: Iterable[InGameActivity],
): Seq[(PlayerSource, KDAStat)] = {
val truncatedHistory = limitHistoryToThisLife(history.toList)
determineKiller(lastDamage, truncatedHistory) match {
case Some((result, killer: PlayerSource)) =>
val assists = collectKillAssistsForPlayer(victim, truncatedHistory, Some(killer))
val fullBep = calculateExperience(killer, victim, truncatedHistory)
val hitSquad = (killer, Kill(victim, result, fullBep)) +: assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
case _ =>
val assists = collectKillAssistsForPlayer(victim, truncatedHistory, None)
val fullBep = Support.baseExperience(victim, truncatedHistory)
val hitSquad = assists.map {
case ContributionStatsOutput(p, w, r) => (p, Assist(victim, w, r, (fullBep * r).toLong))
}.toSeq
(victim, Death(hitSquad.map { _._1 }, truncatedHistory.last.time - truncatedHistory.head.time, fullBep)) +: hitSquad
}
}
/**
* Limit the chronology of in-game activity between an starting activity and a concluding activity for a player character.
* The starting activity is signalled by one or two particular events.
* The concluding activity is a condition of one of many common events.
* All of the activities logged in between count.
* @param history chronology of activity the game considers noteworthy
* @return chronology of activity the game considers noteworthy, but truncated
*/
private def limitHistoryToThisLife(history: List[InGameActivity]): List[InGameActivity] = {
val spawnIndex = history.lastIndexWhere {
case _: SpawningActivity => true
case _: RevivingActivity => true
case _ => false
}
val endIndex = history.lastIndexWhere {
case damage: DamagingActivity => damage.data.targetAfter.asInstanceOf[PlayerSource].Health == 0
case _ => false
}
if (spawnIndex == -1 || endIndex == -1 || spawnIndex > endIndex) {
Nil
} else {
history.slice(spawnIndex, endIndex)
}
}
/**
* Determine the player who is the origin/owner of the bullet that reduced health to zero.
* @param lastDamageActivity damage result that purports the player who is the killer
* @param history chronology of activity the game considers noteworthy;
* referenced in the case that the suggested `DamageResult` is not suitable to determine a player
* @return player associated
* @see `limitHistoryToThisLife`
*/
private[exp] def determineKiller(
lastDamageActivity: Option[DamageResult],
history: List[InGameActivity]
): Option[(DamageResult, SourceEntry)] = {
val now = System.currentTimeMillis()
val compareTimeMillis = 10.seconds.toMillis
lastDamageActivity
.collect { case dam
if now - dam.interaction.hitTime < compareTimeMillis && dam.adversarial.nonEmpty =>
(dam, dam.adversarial.get.attacker)
}
.orElse {
limitHistoryToThisLife(history)
.lastOption
.collect { case dam: DamagingActivity =>
val res = dam.data
(res, res.adversarial.get.attacker)
}
}
}
/**
* "Menace" is a crude measurement of how much consistent destructive power a player has been demonstrating.
* Within the last ten kills, the rate of the player's killing speed is measured.
* The measurement - a "streak" in modern lingo - is transformed into the form of an `Integer` for simplicity.
* @param player the player
* @param mercy a time value that can be used to continue a missed streak
* @return an integer between 0 and 7;
* 0 is no kills,
* 1 is some kills,
* 2-7 is a menace score;
* there is no particular meaning behind different menace scores ascribed by this function
* but the range allows for progressive distinction
* @see `qualifiedTimeDifferences`
* @see `takeWhileLess`
*/
private[exp] def calculateMenace(player: PlayerSource, mercy: Long = 5000L): Int = {
val maxDelayDiff: Long = 45000L
val minDelayDiff: Long = 20000L
val allKills = player.progress.kills
//the very first kill must have been within the max delay (but does not count towards menace)
if (allKills.headOption.exists { System.currentTimeMillis() - _.time.toDate.getTime < maxDelayDiff}) {
allKills match {
case _ :: kills if kills.size > 3 =>
val (continuations, restsBetweenKills) =
qualifiedTimeDifferences(
kills.map(_.time.toDate.getTime).iterator,
maxValidDiffCount = 10,
maxDelayDiff,
minDelayDiff
)
.partition(_ > minDelayDiff)
math.max(
1,
math.floor(math.sqrt(
math.max(0, takeWhileLess(restsBetweenKills, testValue = 20000L, mercy).size - 1) + /*max=8*/
math.max(0, takeWhileLess(restsBetweenKills, testValue = 10000L, mercy).size - 5) * 3 + /*max=12*/
math.max(0, takeWhileLess(restsBetweenKills, testValue = 5000L, mercy = 1000L).size - 4) * 7 /*max=35*/
) - continuations.size)
).toInt
case _ =>
1
}
} else {
0
}
}
/**
* Take a list of times
* and produce a list of delays between those entries less than a maximum time delay.
* These are considered "qualifying".
* Count a certain number of time delays that fall within a minimum threshold
* and stop when that minimum count is achieved.
* These are considered "valid".
* The final product should be a new list of the successive delays from the first list
* containing both qualified and valid entries,
* stopping at either the first unqualified delay or the last valid delay or at exhaustion of the original list.
* @param iter unfiltered list of times (ms)
* @param maxValidDiffCount maximum number of valid entries in the final list of time differences;
* see `validTimeEntryCount`
* @param maxDiff exclusive amount of time allowed between qualifying entries;
* include any time difference within this delay;
* these entries are "qualifying" but are not "valid"
* @param minDiff inclusive amount of time difference allowed between valid entries;
* include time differences in this delay
* these entries are "valid" and should increment the counter `validTimeEntryCount`
* @return list of qualifying time differences (ms)
*/
/*
Parameters governed by recursion:
@param diffList ongoing list of qualifying time differences (ms)
@param diffExtensionList accumulation of entries greater than the `minTimeEntryDiff`
but less that the `minTimeEntryDiff`;
holds qualifying time differences
that will be included before the next valid time difference
@param validDiffCount currently number of valid time entries in the qualified time list;
see `maxValidTimeEntryCount`
@param previousTime previous qualifying entry time;
by default, current time (ms)
*/
@tailrec
private def qualifiedTimeDifferences(
iter: Iterator[Long],
maxValidDiffCount: Int,
maxDiff: Long,
minDiff: Long,
diffList: Seq[Long] = Nil,
diffExtensionList: Seq[Long] = Nil,
validDiffCount: Int = 0,
previousTime: Long = System.currentTimeMillis()
): Iterable[Long] = {
if (iter.hasNext && validDiffCount < maxValidDiffCount) {
val nextTime = iter.next()
val delay = previousTime - nextTime
if (delay < maxDiff) {
if (delay <= minDiff) {
qualifiedTimeDifferences(
iter,
maxValidDiffCount,
maxDiff,
minDiff,
diffList ++ (diffExtensionList :+ delay),
Nil,
validDiffCount + 1,
nextTime
)
} else {
qualifiedTimeDifferences(
iter,
maxValidDiffCount,
maxDiff,
minDiff,
diffList,
diffExtensionList :+ delay,
validDiffCount,
nextTime
)
}
} else {
diffList
}
} else {
diffList
}
}
/**
* From a list of values, isolate all values less than than a test value.
* @param list list of values
* @param testValue test value that all valid values must be less than
* @param mercy initial mercy value that values may be tested for being less than the test value
* @return list of values less than the test value, including mercy
*/
private def takeWhileLess(list: Iterable[Long], testValue: Long, mercy: Long): Iterable[Long] = {
var onGoingMercy: Long = mercy
list.filter { value =>
if (value < testValue) {
true
} else if (value - onGoingMercy < testValue) {
//mercy is reduced every time it is utilized to find a valid value
onGoingMercy = math.ceil(onGoingMercy * 0.8f).toLong
true
} else {
false
}
}
}
/**
* Modify a base experience value to consider additional reasons for points.
* @param killer player that delivers the interaction that reduces health to zero
* @param victim player to which the final interaction has reduced health to zero
* @param history chronology of activity the game considers noteworthy
* @return the value of the kill in what the game called "battle experience points"
* @see `BattleRank.withExperience`
* @see `Support.baseExperience`
*/
private def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
//base value (the kill experience before modifiers)
val base = Support.baseExperience(victim, history)
if (base > 1) {
//include battle rank disparity modifier
val battleRankDisparity = {
import net.psforever.objects.avatar.BattleRank
val killerLevel = BattleRank.withExperience(killer.bep).value
val victimLevel = BattleRank.withExperience(victim.bep).value
if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
if (killerLevel < 7) {
6 * victimLevel + 10
} else if (killerLevel < 12) {
(12 - killerLevel) * victimLevel + 10
} else if (killerLevel < 25) {
25 + victimLevel - killerLevel
} else {
25
}
} else {
math.floor(-0.15f * base - killerLevel + victimLevel).toLong
}
}
val baseWithDisparity: Long = base + battleRankDisparity
val killCount: Long = victim.progress.kills.size
if (battleRankDisparity > 0) {
//include menace modifier
val pureMenace = calculateMenace(victim)
baseWithDisparity + (killCount * (1f + pureMenace.toFloat / 10f)).toLong
} else {
math.max(baseWithDisparity, killCount)
}
} else {
base
}
}
/**
* Evaluate chronological in-game activity within a scope of history and
* isolate the interactions that lead to one player dying.
* Factor in interactions that would have the dying player attempt to resist death, if only for a short while longer.
* @param victim player to which the final interaction has reduced health to zero
* @param history chronology of activity the game considers noteworthy
* @param killerOpt optional player that delivers the interaction that reduces the `victim's` health to zero
* @return summary of the interaction in terms of players, equipment activity, and experience
* @see `armorDamageContributors`
* @see `collectKillAssists`
* @see `healthDamageContributors`
* @see `Support.allocateContributors`
* @see `Support.onlyOriginalAssistEntries`
*/
private def collectKillAssistsForPlayer(
victim: PlayerSource,
history: List[InGameActivity],
killerOpt: Option[PlayerSource]
): Iterable[ContributionStatsOutput] = {
val healthAssists = collectKillAssists(
victim,
history,
Support.allocateContributors(healthDamageContributors)
)
healthAssists.remove(0L)
healthAssists.remove(victim.CharId)
killerOpt.map { killer => healthAssists.remove(killer.CharId) }
if (Support.wasEverAMax(victim, history)) {
val armorAssists = collectKillAssists(
victim,
history,
Support.allocateContributors(armorDamageContributors)
)
armorAssists.remove(0L)
armorAssists.remove(victim.CharId)
killerOpt.map { killer => armorAssists.remove(killer.CharId) }
Support.onlyOriginalAssistEntries(healthAssists, armorAssists)
} else {
healthAssists.values
}
}
/**
* Analyze history based on a discriminating function and format the output.
* @param victim player to which the final interaction has reduced health to zero
* @param history chronology of activity the game considers noteworthy
* @param func mechanism for discerning particular interactions and building a narrative around their history;
* tallies all activity by a certain player using certain equipment
* @return summary of the interaction in terms of players, equipment activity, and experience
*/
private def collectKillAssists(
victim: SourceEntry,
history: List[InGameActivity],
func: (List[InGameActivity], PlanetSideEmpire.Value) => mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStatsOutput] = {
val assists = func(history, victim.Faction).filterNot { case (_, kda) => kda.amount <= 0 }
val total = assists.values.foldLeft(0f)(_ + _.total)
val output = assists.map { case (id, kda) =>
(id, ContributionStatsOutput(kda.player, kda.weapons.map { _.equipment }, kda.amount / total))
}
output.remove(victim.CharId)
output
}
/**
* In relation to a target player's health,
* build a secondary chronology of how the health value is affected per interaction and
* maintain a quantitative record of that activity in relation to the other players and their equipment.
* @param history chronology of activity the game considers noteworthy
* @param faction empire to target
* @param participants quantitative record of activity in relation to the other players and their equipment
* @return chronology of how the health value is affected per interaction
* @see `contributeWithDamagingActivity`
* @see `contributeWithRecoveryActivity`
* @see `RevivingActivity`
*/
private def healthDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/*
damage as it is measured in order (with heal-countered damage eliminated)<br>
key - character identifier,
value - current damage contribution
*/
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.health, participants, inOrder)
case r: RevivingActivity =>
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
case h: HealingActivity =>
inOrder = contributeWithRecoveryActivity(h.amount, participants, inOrder)
case _ => ()
}
inOrder
}
/**
* In relation to a target player's armor,
* build a secondary chronology of how the armor value is affected per interaction and
* maintain a quantitative record of that activity in relation to the other players and their equipment.
* @param history chronology of activity the game considers noteworthy
* @param faction empire to target
* @param participants quantitative record of activity in relation to the other players and their equipment
* @return chronology of how the armor value is affected per interaction
* @see `contributeWithDamagingActivity`
* @see `contributeWithRecoveryActivity`
*/
private def armorDamageContributors(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
participants: mutable.LongMap[ContributionStats]
): Seq[(Long, Int)] = {
/*
damage as it is measured in order (with heal-countered damage eliminated)<br>
key - character identifier,
value - current damage contribution
*/
var inOrder: Seq[(Long, Int)] = Seq[(Long, Int)]()
history.foreach {
case d: DamagingActivity if d.amount - d.health > 0 =>
inOrder = contributeWithDamagingActivity(d, faction, d.amount - d.health, participants, inOrder)
case r: RepairingActivity =>
inOrder = contributeWithRecoveryActivity(r.amount, participants, inOrder)
case _ => ()
}
inOrder
}
/**
* Analyze damaging activity for quantitative records.
* @param activity a particular in-game activity that negative affects a player's health
* @param faction empire to target
* @param amount value
* @param participants quantitative record of activity in relation to the other players and their equipment
* @param order chronology of how the armor value is affected per interaction
* @return chronology of how the armor value is affected per interaction
*/
private def contributeWithDamagingActivity(
activity: DamagingActivity,
faction: PlanetSideEmpire.Value,
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
val data = activity.data
val playerOpt = data.adversarial.collect { case Adversarial(p: PlayerSource, _,_) => p }
contributeWithDamagingActivity(
playerOpt,
data.interaction.cause.attribution,
faction,
amount,
activity.time,
participants,
order
)
}
/**
* Analyze damaging activity for quantitative records.
* @param userOpt optional player for the quantitative record
* @param wepid weapon for the quantitative record
* @param faction empire to target
* @param amount value
* @param participants quantitative record of activity in relation to the other players and their equipment
* @param order chronology of how the armor value is affected per interaction
* @return chronology of how the armor value is affected per interaction
*/
private[exp] def contributeWithDamagingActivity(
userOpt: Option[PlayerSource],
wepid: Int,
faction: PlanetSideEmpire.Value,
amount: Int,
time: Long,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
userOpt match {
case Some(user)
if user.Faction != faction =>
val whoId = user.CharId
val percentage = amount / user.Definition.MaxHealth.toFloat
val updatedEntry = participants.get(whoId) match {
case Some(mod) =>
//previous attacker, just add to entry
val firstWeapon = mod.weapons.head
val newEntry = DamageWith(wepid)
val weapons = if (firstWeapon.equipment == newEntry) {
firstWeapon.copy(
amount = firstWeapon.amount + amount,
shots = firstWeapon.shots + 1,
time = time,
contributions = firstWeapon.contributions + percentage
) +: mod.weapons.tail
} else {
WeaponStats(newEntry, amount, 1, time, percentage) +: mod.weapons
}
mod.copy(
amount = mod.amount + amount,
weapons = weapons,
total = mod.total + amount,
shots = mod.shots + 1,
time = time
)
case None =>
//new attacker, new entry
ContributionStats(
user,
Seq(WeaponStats(DamageWith(wepid), amount, 1, time, percentage)),
amount,
amount,
1,
time
)
}
participants.put(whoId, updatedEntry)
order.indexWhere({ case (id, _) => id == whoId }) match {
case 0 =>
//ongoing attack by same player
val entry = order.head
(entry._1, entry._2 + amount) +: order.tail
case _ =>
//different player than immediate prior attacker
(whoId, amount) +: order
}
case _ =>
//damage that does not lead to contribution
order.headOption match {
case Some((id, dam)) =>
if (id == 0L) {
(0L, dam + amount) +: order.tail //pool
} else {
(0L, amount) +: order //new
}
case None =>
order
}
}
}
/**
* Analyze recovery activity for quantitative records.
* @param amount value
* @param participants quantitative record of activity in relation to the other players and their equipment
* @param order chronology of how the armor value is affected per interaction
* @return chronology of how the armor value is affected per interaction
*/
private[exp] def contributeWithRecoveryActivity(
amount: Int,
participants: mutable.LongMap[ContributionStats],
order: Seq[(Long, Int)]
): Seq[(Long, Int)] = {
var amt = amount
var count = 0
var newOrder: Seq[(Long, Int)] = Nil
order.takeWhile { entry =>
val (id, total) = entry
if (id > 0 && total > 0) {
val part = participants(id)
if (amount > total) {
//drop this entry
participants.put(id, part.copy(amount = 0, weapons = Nil)) //just in case
amt = amt - total
} else {
//edit around the inclusion of this entry
val newTotal = total - amt
val trimmedWeapons = {
var index = -1
var weaponSum = 0
val pweapons = part.weapons
while (weaponSum < amt) {
index += 1
weaponSum = weaponSum + pweapons(index).amount
}
(pweapons(index).copy(amount = weaponSum - amt) +: pweapons.slice(index+1, pweapons.size)) ++
pweapons.slice(0, index).map(_.copy(amount = 0))
}
newOrder = (id, newTotal) +: newOrder
participants.put(id, part.copy(amount = part.amount - amount, weapons = trimmedWeapons))
amt = 0
}
}
count += 1
amt > 0
}
newOrder ++ order.drop(count)
}
}

View file

@ -0,0 +1,930 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.avatar.scoring.{Kill, SupportActivity}
import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource}
import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import net.psforever.util.Config
import scala.collection.mutable
/**
* Kills and assists consider the target, in an exchange of projectiles from the weapons of players towards the target.
* Contributions consider actions of other allied players towards the player who is the source of the projectiles.
* These actions are generally positive for the player.
* @see `Contribution`
* @see `ContributionStats`
* @see `ContributionStatsOutput`
* @see `DamagingActivity`
* @see `GlobalDefinitions`
* @see `HealingActivity`
* @see `InGameActivity`
* @see `InGameHistory`
* @see `Kill`
* @see `PlayerSource`
* @see `RepairingActivity`
* @see `SourceEntry`
* @see `SourceUniqueness`
* @see `VehicleSource`
*/
object KillContributions {
/** the object type ids of various game elements that are recognized for "stat recovery" */
final val RecoveryItems: Seq[Int] = {
import net.psforever.objects.GlobalDefinitions._
Seq(
bank,
nano_dispenser,
medicalapplicator,
order_terminal,
order_terminala,
order_terminalb,
medical_terminal,
adv_med_terminal,
bfr_rearm_terminal,
multivehicle_rearm_terminal,
lodestar_repair_terminal
).collect { _.ObjectId }
} //TODO currently includes things that are not typical equipment but things that express contribution
/** cached for empty collection returns; please do not add anything to it */
private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats]
/**
* Primary landing point for calculating the rewards given for helping one player kill another player.
* Rewards in the form of "support experience points" are given
* to all allied players that have somehow been involved with the player who killed another player.
* @param target player that delivers the interaction that killed another player;
* history is purportedly composed of events that have happened to this player within a time frame
* @param history chronology of activity the game considers noteworthy
* @param kill the in-game event that maintains information about the other player's death;
* originates from prior statistical management normally
* @param bep battle experience points to be referenced for support experience points conversion
* @param eventBus where to send the results of the experience determination(s)
* @see `ActorRef`
* @see `AvatarAction.UpdateKillsDeathsAssists`
* @see `AvatarServiceMessage`
* @see `rewardTheseSupporters`
* @see `SupportActivity`
*/
private[exp] def rewardTheseSupporters(
target: PlayerSource,
history: Iterable[InGameActivity],
kill: Kill,
bep: Long,
eventBus: ActorRef
): Unit = {
val victim = kill.victim
//take the output and transform that into contribution distribution data
rewardTheseSupporters(target, history, kill, bep)
.foreach { case (charId, ContributionStatsOutput(player, weapons, exp)) =>
eventBus ! AvatarServiceMessage(
player.Name,
AvatarAction.UpdateKillsDeathsAssists(charId, SupportActivity(victim, weapons, exp.toLong))
)
}
}
/**
* Primary innards for calculating the rewards given for helping one player kill another player.
* @param target player that delivers the interaction that killed another player;
* history is purportedly composed of events that have happened to this player within a time frame
* @param history chronology of activity the game considers noteworthy
* @param kill the in-game event that maintains information about the other player's death;
* originates from prior statistical management normally
* @param bep battle experience points to be referenced for support experience points conversion
* returns list of user unique identifiers and
* a summary of the interaction in terms of players, equipment activity, and experience
* @see `ActorRef`
* @see `additionalContributionSources`
* @see `AvatarAction.UpdateKillsDeathsAssists`
* @see `AvatarServiceMessage`
* @see `CombinedHealthAndArmorContributionProcess`
* @see `composeContributionOutput`
* @see `initialScoring`
* @see `KillAssists.calculateMenace`
* @see `limitHistoryToThisLife`
* @see `rewardTheseSupporters`
* @see `SupportActivity`
*/
private[exp] def rewardTheseSupporters(
target: PlayerSource,
history: Iterable[InGameActivity],
kill: Kill,
bep: Long
): Iterable[(Long, ContributionStatsOutput)] = {
val faction = target.Faction
/*
divide into applicable time periods;
these two periods represent passes over the in-game history to evaluate statistic modification events;
the short time period should stand on its own, but should also be represented in the long time period;
more players should be rewarded if one qualifies for the longer time period's evaluation
*/
val (contributions, (longHistory, shortHistory)) = {
val killTime = kill.time.toDate.getTime
val shortPeriod = killTime - Config.app.game.experience.shortContributionTime
val (contrib, onlyHistory) = history.partition { _.isInstanceOf[Contribution] }
(
contrib
.collect { case Contribution(unique, entries) => (unique, entries) }
.toMap[SourceUniqueness, List[InGameActivity]],
limitHistoryToThisLife(onlyHistory.toList, killTime).partition { _.time < shortPeriod }
)
}
//events that are older than 5 minutes are enough to prove one has been alive that long
val empty = mutable.ListBuffer[SourceUniqueness]()
empty.addOne(target.unique)
val otherContributionCalculations = additionalContributionSources(faction, kill, contributions)(_, _, _)
if (longHistory.nonEmpty && KillAssists.calculateMenace(target) > 3) {
//long and short history
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,
initialScoring(longContributionProcess.output(), bep.toFloat),
empty
)
val shortContributionEntries = otherContributionCalculations(
shortHistory,
initialScoring(shortContributionProcess.output(), bep.toFloat),
empty
)
longContributionEntries.remove(target.CharId)
longContributionEntries.remove(kill.victim.CharId)
shortContributionEntries.remove(target.CharId)
shortContributionEntries.remove(kill.victim.CharId)
//combine
(longContributionEntries ++ shortContributionEntries)
.toSeq
.distinctBy(_._2.player.unique)
.flatMap { case (_, stats) =>
composeContributionOutput(stats.player.CharId, shortContributionEntries, longContributionEntries, bep)
}
} else {
//short history only
val contributionProcess = new CombinedHealthAndArmorContributionProcess(faction, contributions, Nil)
contributionProcess.submit(shortHistory)
val contributionEntries = otherContributionCalculations(
shortHistory,
initialScoring(contributionProcess.output(), bep.toFloat),
empty
)
contributionEntries.remove(target.CharId)
contributionEntries.remove(kill.victim.CharId)
contributionEntries
.flatMap { case (_, stats) =>
composeContributionOutput(stats.player.CharId, contributionEntries, contributionEntries, bep)
}
}
}
/**
* Only historical activity that falls within the valid period matters.<br>
* Unlike an expected case where the history would be bound by being spawned and being killed, respectively,
* this imposes only the long contribution time limit on events since the latest entry;
* and, it may stop some time after the otherwise closest activity for being spawned.
* @param history the original history
* @param eventTime from which time to start counting backwards
* @return the potentially truncated history
*/
private def limitHistoryToThisLife(history: List[InGameActivity], eventTime: Long): List[InGameActivity] = {
limitHistoryToThisLife(history, eventTime, eventTime - Config.app.game.experience.longContributionTime)
}
/**
* Only historical activity that falls within the valid period matters.
* @param history the original history
* @param eventTime from which time to start counting backwards
* @param startTime after which time to start counting forwards
* @return the potentially truncated history
*/
private def limitHistoryToThisLife(
history: List[InGameActivity],
eventTime: Long,
startTime: Long
): List[InGameActivity] = {
history.filter { event => event.time <= eventTime && event.time >= startTime }
}
/**
* Manipulate contribution scores that have been evaluated up to this point
* for a fixed combination of users and different implements
* by replacing the score using a flat predictable numerical evaluation.
* @param existingParticipants quantitative record of activity in relation to the other players and their equipment
* @param bep battle experience point
* @return quantitative record of activity in relation to the other players and their equipment
*/
private def initialScoring(
existingParticipants: mutable.LongMap[ContributionStats],
bep: Float
): mutable.LongMap[ContributionStats] = {
//the scoring up to this point should be rate based, but is not perfectly useful for us
existingParticipants.map { case (id, stat) =>
val newWeaponStats = stat.weapons.map { weaponStat =>
weaponStat.copy(contributions = 10f + weaponStat.shots.toFloat + 0.05f * bep)
}
existingParticipants.put(id, stat.copy(weapons = newWeaponStats))
}
existingParticipants
}
/**
* na
* @param faction empire to target
* @param kill the in-game event that maintains information about the other player's death;
* originates from prior statistical management normally
* @param contributions na
* @param history chronology of activity the game considers noteworthy
* @param existingParticipants quantitative record of activity in relation to the other players and their equipment
* @param excludedTargets do not repeat analysis on entities associated with these tokens
* @return quantitative record of activity in relation to the other players and their equipment
* @see `contributeWithRevivalActivity`
* @see `contributeWithTerminalActivity`
* @see `contributeWithVehicleTransportActivity`
* @see `contributeWithVehicleCargoTransportActivity`
* @see `contributeWithKillWhileMountedActivity`
*/
private def additionalContributionSources(
faction: PlanetSideEmpire.Value,
kill: Kill,
contributions: Map[SourceUniqueness, List[InGameActivity]]
)
(
history: List[InGameActivity],
existingParticipants: mutable.LongMap[ContributionStats],
excludedTargets: mutable.ListBuffer[SourceUniqueness]
): mutable.LongMap[ContributionStats] = {
contributeWithRevivalActivity(history, existingParticipants)
contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants)
contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants)
contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants)
existingParticipants.remove(0)
existingParticipants
}
/**
* Gather and reward specific in-game equipment use activity.<br>
* If the player who performed the killing interaction is mounted in something,
* determine if the mount is has been effected by previous in-game interactions
* that resulted in positive stat maintenance or development.
* Also, reward the owner, if an owner exists, for providing the mount.
* @param kill the in-game event that maintains information about the other player's death
* @param faction empire to target
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @param out quantitative record of activity in relation to the other players and their equipment
* @see `combineStatsInto`
* @see `extractContributionsForMachineByTarget`
*/
private def contributeWithKillWhileMountedActivity(
kill: Kill,
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
out: mutable.LongMap[ContributionStats]
): Unit = {
val eventTime = kill.time.toDate.getTime
(kill
.info
.interaction
.cause match {
case p: ProjectileReason => p.projectile.mounted_in.map { case (_, src) => Some((src, p.projectile.owner)) }
case _ => None
})
.collect {
case Some((mount: VehicleSource, attacker: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
mount.owner
.collect {
case owner if owner == attacker.unique =>
//owner is gunner; reward only repairs
excludedTargets.addOne(owner)
owner
case owner =>
//gunner is different from owner; reward driver and repairs
excludedTargets.addOne(owner)
excludedTargets.addOne(attacker.unique)
val time = kill.time.toDate.getTime
val weaponStat = Support.calculateSupportExperience(
event = "mounted-kill",
WeaponStats(DriverAssist(mount.Definition.ObjectId), 1, 1, time, 1f)
)
combineStatsInto(
out,
(
owner.charId,
ContributionStats(
PlayerSource(owner, mount.Position),
Seq(weaponStat),
1,
1,
1,
time
)
)
)
owner
}
combineStatsInto(
out,
extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair")
)
case Some((mount: TurretSource, _: PlayerSource)) if !excludedTargets.contains(mount.unique) =>
combineStatsInto(
out,
extractContributionsForMachineByTarget(mount, faction, eventTime, contributions, excludedTargets, eventOutputType="support-repair-turret")
)
}
}
/**
* Gather and reward specific in-game equipment use activity.<br>
* na
* @param kill the in-game event that maintains information about the other player's death
* @param history chronology of activity the game considers noteworthy
* @param faction empire to target
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @param out quantitative record of activity in relation to the other players and their equipment
* @see `combineStatsInto`
* @see `extractContributionsForMachineByTarget`
*/
private def contributeWithVehicleTransportActivity(
kill: Kill,
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
out: mutable.LongMap[ContributionStats]
): Unit = {
/*
collect the dismount activity of all vehicles from which this player is not the owner
make certain all dismount activity can be paired with a mounting activity
certain other qualifications of the prior mounting must be met before the support bonus applies
*/
val dismountActivity = history
.collect {
case out: VehicleDismountActivity
if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
}
.collect {
case (in: VehicleMountActivity, out: VehicleDismountActivity)
if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.player.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || {
val inTime = in.time
val outTime = out.time
out.player.progress.kills.exists { death =>
val deathTime = death.info.interaction.hitTime
inTime < deathTime && deathTime <= outTime
}
} || {
val sameZone = in.zoneNumber == out.zoneNumber
val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
val distanceMoved = {
val killLocation = kill.info.adversarial
.collect { adversarial => adversarial.attacker.Position.xy }
.getOrElse(Vector3.Zero)
Vector3.DistanceSquared(killLocation, out.player.Position.xy)
}
val timeSpent = out.time - in.time
distanceMoved < 5625f /* 75m */ &&
(timeSpent >= 210000L /* 3:30 */ ||
(sameZone && (distanceTransported > 160000f /* 400m */ ||
distanceTransported > 10000f /* 100m */ && timeSpent >= 60000L /* 1:00m */)) ||
(!sameZone && (distanceTransported > 10000f /* 100m */ || timeSpent >= 120000L /* 2:00 */ )))
}) =>
out
}
//apply
dismountActivity
.groupBy { _.vehicle }
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
val (equipmentUseContext, equipmentUseEvent) = mount.Definition match {
case v @ GlobalDefinitions.router =>
(RouterKillAssist(v.ObjectId), "router")
case v =>
(HotDropKillAssist(v.ObjectId, 0), "hotdrop")
}
val size = dismountsFromVehicle.size
val time = dismountsFromVehicle.maxBy(_.time).time
val weaponStat = Support.calculateSupportExperience(
equipmentUseEvent,
WeaponStats(equipmentUseContext, size, size, time, 1f)
)
combineStatsInto(
out,
(
promotedOwner.CharId,
ContributionStats(promotedOwner, Seq(weaponStat), size, size, size, time)
)
)
contributions.get(mount.unique).collect {
case list =>
val mountHistory = dismountsFromVehicle
.flatMap { event =>
val eventTime = event.time
val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime)
}
.distinctBy(_.time)
combineStatsInto(
out,
extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
)
}
}
}
/**
* Gather and reward specific in-game equipment use activity.<br>
* na
* @param kill the in-game event that maintains information about the other player's death
* @param faction empire to target
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @param out quantitative record of activity in relation to the other players and their equipment
* @see `combineStatsInto`
* @see `extractContributionsForMachineByTarget`
*/
private def contributeWithVehicleCargoTransportActivity(
kill: Kill,
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
out: mutable.LongMap[ContributionStats]
): Unit = {
/*
collect the dismount activity of all vehicles from which this player is not the owner
make certain all dismount activity can be paired with a mounting activity
certain other qualifications of the prior mounting must be met before the support bonus applies
*/
val dismountActivity = history
.collect {
case out: VehicleCargoDismountActivity
if out.vehicle.owner.nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
}
.collect {
case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
if in.vehicle.unique == out.vehicle.unique &&
out.vehicle.Faction == out.cargo.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || {
val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
val distanceMoved = {
val killLocation = kill.info.adversarial
.collect { adversarial => adversarial.attacker.Position.xy }
.getOrElse(Vector3.Zero)
Vector3.DistanceSquared(killLocation, out.cargo.Position.xy)
}
val timeSpent = out.time - in.time
distanceMoved < 5625f /* 75m */ &&
(timeSpent >= 210000 /* 3:30 */ || distanceTransported > 360000f /* 600m */)
}) =>
out
}
//apply
dismountActivity
.groupBy { _.cargo }
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
val mountId = mount.Definition.ObjectId
dismountsFromVehicle
.groupBy(_.vehicle)
.map { case (vehicle, events) =>
val size = events.size
val time = events.maxBy(_.time).time
val weaponStat = Support.calculateSupportExperience(
event = "hotdrop",
WeaponStats(HotDropKillAssist(vehicle.Definition.ObjectId, mountId), size, size, time, 1f)
)
(vehicle, vehicle.owner, Seq(weaponStat))
}
.collect { case (vehicle, Some(owner), statContext) =>
combineStatsInto(
out,
(
owner.charId,
ContributionStats(promotedOwner, statContext, 1, 1, 1, statContext.head.time)
)
)
contributions.get(mount.unique).collect {
case list =>
val mountHistory = dismountsFromVehicle
.flatMap { event =>
val eventTime = event.time
val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime)
}
.distinctBy(_.time)
combineStatsInto(
out,
extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair")
)
}
contributions.get(vehicle.unique).collect {
case list =>
val carrierHistory = dismountsFromVehicle
.flatMap { event =>
val eventTime = event.time
val startTime = event.pairedEvent.get.time - Config.app.game.experience.longContributionTime
limitHistoryToThisLife(list, eventTime, startTime)
}
.distinctBy(_.time)
combineStatsInto(
out,
extractContributionsForMachineByTarget(vehicle, faction, carrierHistory, contributions, excludedTargets, eventOutputType="support-repair")
)
}
}
}
}
/**
* Gather and reward specific in-game equipment use activity.<br>
* na
* @param faction empire to target
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @param out quantitative record of activity in relation to the other players and their equipment
* @see `AmsResupplyKillAssist`
* @see `BuildingSource`
* @see `combineStatsInto`
* @see `contributeWithTerminalActivity`
* @see `extractContributionsForMachineByTarget`
* @see `HackKillAssist`
* @see `HealFromTerminal`
* @see `LodestarRearmKillAssist`
* @see `RepairFromTerminal`
* @see `RepairKillAssist`
* @see `TerminalUsedActivity`
*/
private def contributeWithTerminalActivity(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
out: mutable.LongMap[ContributionStats]
): Unit = {
history
.collect {
case h: HealFromTerminal => (h.term, h)
case r: RepairFromTerminal => (r.term, r)
case t: TerminalUsedActivity => (t.terminal, t)
}
.groupBy(_._1.unique)
.map {
case (_, events1) =>
val (termThings1, _) = events1.unzip
val hackContext = HackKillAssist(GlobalDefinitions.remote_electronics_kit.ObjectId, termThings1.head.Definition.ObjectId)
if (termThings1.exists(t => t.Faction != faction && t.hacked.nonEmpty)) {
/*
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
*/
events1
.collect { case out @ (t, _) if t.hacked.nonEmpty => out }
.groupBy { case (t, _) => t.hacked.get.player.unique }
.foreach { case (_, events2) =>
val (termThings2, events3) = events2.unzip
val hacker = termThings2.head.hacked.get.player
val size = events3.size
val time = events3.maxBy(_.time).time
val weaponStats = Support.calculateSupportExperience(
event = "hack",
WeaponStats(hackContext, size, size, time, 1f)
)
combineStatsInto(
out,
(
hacker.CharId,
ContributionStats(
hacker,
Seq(weaponStats),
size,
size,
size,
time
)
)
)
}
} else if (termThings1.exists(_.Faction == faction)) {
//faction-aligned terminal
val (_, events2) = events1.unzip
val eventTime = events2.maxBy(_.time).time
val startTime = events2.minBy(_.time).time - Config.app.game.experience.longContributionTime
val termThingsHead = termThings1.head
val (equipmentUseContext, equipmentUseEvent, installationEvent, target) = termThingsHead.installation match {
case v: VehicleSource =>
termThingsHead.Definition match {
case GlobalDefinitions.order_terminala =>
(AmsResupplyKillAssist(GlobalDefinitions.order_terminala.ObjectId), "ams-resupply", "support-repair", Some(v))
case GlobalDefinitions.order_terminalb =>
(AmsResupplyKillAssist(GlobalDefinitions.order_terminalb.ObjectId), "ams-resupply", "support-repair", Some(v))
case GlobalDefinitions.lodestar_repair_terminal =>
(RepairKillAssist(GlobalDefinitions.lodestar_repair_terminal.ObjectId, v.Definition.ObjectId), "lodestar-repair", "support-repair", Some(v))
case GlobalDefinitions.bfr_rearm_terminal =>
(LodestarRearmKillAssist(GlobalDefinitions.bfr_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
case GlobalDefinitions.multivehicle_rearm_terminal =>
(LodestarRearmKillAssist(GlobalDefinitions.multivehicle_rearm_terminal.ObjectId), "lodestar-rearm", "support-repair", Some(v))
case _ =>
(NoUse(), "", "", None)
}
case _: BuildingSource =>
(NoUse(), "", "support-repair-terminal", Some(termThingsHead))
case _ =>
(NoUse(), "", "", None)
}
target.map { src =>
combineStatsInto(
out,
extractContributionsForMachineByTarget(src, faction, eventTime, startTime, contributions, excludedTargets, installationEvent)
)
}
events1
.map { case (a, b) => (a.installation, b) }
.collect { case (installation: VehicleSource, evt) if installation.owner.nonEmpty => (installation, evt) }
.groupBy(_._1.owner.get)
.collect { case (owner, list) =>
val (installations, events2) = list.unzip
val size = events2.size
val time = events2.maxBy(_.time).time
val weaponStats = Support.calculateSupportExperience(
equipmentUseEvent,
WeaponStats(equipmentUseContext, size, size, time, 1f)
)
combineStatsInto(
out,
(
owner.charId,
ContributionStats(
PlayerSource(owner, installations.head.Position),
Seq(weaponStats),
size,
size,
size,
time
)
)
)
}
}
None
}
}
/**
* Gather and reward specific in-game equipment use activity.<br>
* na
* @param history chronology of activity the game considers noteworthy
* @param out quantitative record of activity in relation to the other players and their equipment
* @see `combineStatsInto`
* @see `ReviveKillAssist`
*/
private def contributeWithRevivalActivity(
history: List[InGameActivity],
out: mutable.LongMap[ContributionStats]
): Unit = {
history
.collect { case rev: RevivingActivity => rev }
.groupBy(_.user.CharId)
.map { case (id, revivesByThisPlayer) =>
val user = revivesByThisPlayer.head.user
revivesByThisPlayer
.groupBy(_.equipment)
.map { case (definition, events) =>
val eventSize = events.size
val objectId = definition.ObjectId
val time = events.maxBy(_.time).time
combineStatsInto(
out,
(
id,
ContributionStats(
user,
Seq({
Support.calculateSupportExperience(
event = "revival",
WeaponStats(ReviveKillAssist(objectId), 1, eventSize, time, 1f)
)
}),
eventSize,
eventSize,
eventSize,
time
)
)
)
}
}
}
/**
* na
* Mainly produces repair events.
* @param target entity external to the subject of the kill
* @param faction empire to target
* @param time na
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @return quantitative record of activity in relation to the other players and their equipment
*/
private def extractContributionsForMachineByTarget(
target: SourceEntry,
faction: PlanetSideEmpire.Value,
time: Long,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
eventOutputType: String
): mutable.LongMap[ContributionStats] = {
val start: Long = time - Config.app.game.experience.longContributionTime
extractContributionsForMachineByTarget(target, faction, time, start, contributions, excludedTargets, eventOutputType)
}
/**
* na
* Mainly produces repair events.
* @param target entity external to the subject of the kill
* @param faction empire to target
* @param eventTime na
* @param startTime na
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @return quantitative record of activity in relation to the other players and their equipment
* @see `limitHistoryToThisLife`
*/
private def extractContributionsForMachineByTarget(
target: SourceEntry,
faction: PlanetSideEmpire.Value,
eventTime: Long,
startTime: Long,
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
eventOutputType: String
): mutable.LongMap[ContributionStats] = {
val unique = target.unique
val history = limitHistoryToThisLife(contributions.getOrElse(unique, List()), eventTime, startTime)
extractContributionsForMachineByTarget(target, faction, history, contributions, excludedTargets, eventOutputType)
}
/**
* na
* Mainly produces repair events.
* @param target entity external to the subject of the kill
* @param faction empire to target
* @param history na
* @param contributions mapping between external entities
* the target has interacted with in the form of in-game activity
* and history related to the time period in which the interaction ocurred
* @param excludedTargets if a potential target is listed here already, skip processing it
* @return quantitative record of activity in relation to the other players and their equipment
* @see `cullContributorImplements`
* @see `emptyMap`
* @see `MachineRecoveryExperienceContributionProcess`
*/
private def extractContributionsForMachineByTarget(
target: SourceEntry,
faction: PlanetSideEmpire.Value,
history: List[InGameActivity],
contributions: Map[SourceUniqueness, List[InGameActivity]],
excludedTargets: mutable.ListBuffer[SourceUniqueness],
eventOutputType: String
): mutable.LongMap[ContributionStats] = {
val unique = target.unique
if (!excludedTargets.contains(unique) && history.nonEmpty) {
excludedTargets.addOne(unique)
val process = new MachineRecoveryExperienceContributionProcess(faction, contributions, eventOutputType, excludedTargets)
process.submit(history)
cullContributorImplements(process.output())
} else {
emptyMap
}
}
/**
* na
* @param main quantitative record of activity in relation to the other players and their equipment
* @param transferFrom quantitative record of activity in relation to the other players and their equipment
* @return quantitative record of activity in relation to the other players and their equipment
* @see `combineStatsInto`
*/
private def combineStatsInto(
main: mutable.LongMap[ContributionStats],
transferFrom: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
transferFrom.foreach { (entry: (Long, ContributionStats)) => combineStatsInto(main, entry) }
main
}
/**
* na
* @param main quantitative record of activity in relation to the other players and their equipment
* @param entry two value tuple representing:
* a player's unique identifier,
* and a quantitative record of activity in relation to the other players and their equipment
* @see `Support.combineWeaponStats`
*/
private def combineStatsInto(main: mutable.LongMap[ContributionStats], entry: (Long, ContributionStats)): Unit = {
val (id, sampleStats) = entry
main.get(id) match {
case Some(foundStats) =>
main.put(id, foundStats.copy(weapons = Support.combineWeaponStats(foundStats.weapons, sampleStats.weapons)))
case None =>
main.put(id, sampleStats)
}
}
/**
* Filter quantitative records based on the presence of specific equipment used for statistic recovery.
* @param input quantitative record of activity in relation to the other players and their equipment
* @return quantitative record of activity in relation to the other players and their equipment
* @see `RecoveryItems`
*/
private[exp] def cullContributorImplements(
input: mutable.LongMap[ContributionStats]
): mutable.LongMap[ContributionStats] = {
input.collect { case (id, entry) =>
(id, entry.copy(weapons = entry.weapons.filter { stats => RecoveryItems.contains(stats.equipment.equipment) }))
}.filter { case (_, entry) =>
entry.weapons.nonEmpty
}
}
/**
* na
* @param charId the unique identifier being targeted
* @param shortPeriod quantitative record of activity in relation to the other players and their equipment
* @param longPeriod quantitative record of activity in relation to the other players and their equipment
* @param max maximum value for the third output value
* @return two value tuple representing:
* a player's unique identifier,
* and a summary of the interaction in terms of players, equipment activity, and experience
* @see `composeContributionOutput`
*/
private def composeContributionOutput(
charId: Long,
shortPeriod: mutable.LongMap[ContributionStats],
longPeriod: mutable.LongMap[ContributionStats],
max: Long
): Option[(Long, ContributionStatsOutput)] = {
composeContributionOutput(charId, longPeriod, modifier=0.8f, max)
.orElse { composeContributionOutput(charId, shortPeriod, modifier=1f, max) }
.collect {
case (player, weaponIds, experience) =>
(charId, ContributionStatsOutput(player, weaponIds, experience))
}
}
/**
* na
* @param charId the unique identifier being targeted
* @param stats quantitative record of activity in relation to the other players and their equipment
* @param modifier modifier value for the potential third output value
* @param max maximum value for the third output value
* @return three value tuple representing:
* player,
* the context in which certain equipment is being used,
* and a final value for the awarded support experience points
*/
private def composeContributionOutput(
charId: Long,
stats: mutable.LongMap[ContributionStats],
modifier: Float,
max: Long
): Option[(PlayerSource, Seq[EquipmentUseContextWrapper], Float)] = {
stats
.get(charId)
.collect {
case entry =>
val (weapons, contributions) = entry.weapons.map { entry => (entry.equipment, entry.contributions) }.unzip
(
entry.player,
weapons.distinct,
modifier * math.floor(math.min(contributions.foldLeft(0f)(_ + _), max.toFloat)).toFloat
)
}
}
}

View file

@ -1,40 +0,0 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.InGameActivity
object KillDeathAssists {
private[exp] def calculateExperience(
killer: PlayerSource,
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
//base value (the kill experience before modifiers)
val base = ExperienceCalculator.calculateExperience(victim, history)
if (base > 1) {
//battle rank disparity modifiers
val battleRankDisparity = {
import net.psforever.objects.avatar.BattleRank
val killerLevel = BattleRank.withExperience(killer.bep).value
val victimLevel = BattleRank.withExperience(victim.bep).value
if (victimLevel > killerLevel || killerLevel - victimLevel < 6) {
if (killerLevel < 7) {
6 * victimLevel + 10
} else if (killerLevel < 12) {
(12 - killerLevel) * victimLevel + 10
} else if (killerLevel < 25) {
25 + victimLevel - killerLevel
} else {
25
}
} else {
math.floor(-0.15f * base - killerLevel + victimLevel).toLong
}
}
math.max(1, base + battleRankDisparity)
} else {
base
}
}
}

View file

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

View file

@ -0,0 +1,239 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.{InGameActivity, ReconstructionActivity, RepairFromExoSuitChange, SpawningActivity}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import net.psforever.util.Config
import scala.collection.mutable
/**
* Functions to assist experience calculation and history manipulation and analysis.
*/
object Support {
private val sep = Config.app.game.experience.sep
/**
* Calculate a base experience value to consider additional reasons for points.
* @param victim player to which a final interaction has reduced health to zero
* @param history chronology of activity the game considers noteworthy
* @return the value of the kill in what the game called "battle experience points"
* @see `Support.wasEverAMax`
*/
private[exp] def baseExperience(
victim: PlayerSource,
history: Iterable[InGameActivity]
): Long = {
val lifespan = (history.headOption, history.lastOption) match {
case (Some(spawn), Some(death)) => death.time - spawn.time
case _ => 0L
}
val base = if (Support.wasEverAMax(victim, history)) {
Config.app.game.experience.bep.base.asMax
} else if (victim.progress.kills.nonEmpty) {
Config.app.game.experience.bep.base.withKills
} else if (victim.Seated) {
Config.app.game.experience.bep.base.asMounted
} else if (lifespan > 15000L) {
Config.app.game.experience.bep.base.mature
} else {
1L
}
if (base > 1) {
//black ops modifier
base// * Config.app.game.experience.bep.base.bopsMultiplier
} else {
base
}
}
/**
* Combine two quantitative records into one, maintaining only the original entries.
* @param first one quantitative record
* @param second another quantitative record
* @param combiner mechanism for determining how to combine quantitative records;
* defaults to an additive combiner with a small multiplier value
* @return the combined quantitative records
* @see `defaultAdditiveOutputCombiner`
* @see `onlyOriginalAssistEntriesIterable`
*/
private[exp] def onlyOriginalAssistEntries(
first: mutable.LongMap[ContributionStatsOutput],
second: mutable.LongMap[ContributionStatsOutput],
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
onlyOriginalAssistEntriesIterable(first.values, second.values, combiner)
}
/**
* Combine two quantitative records into one, maintaining only the original entries.
* @param first one quantitative record
* @param second another quantitative record
* @param combiner mechanism for determining how to combine quantitative records;
* defaults to an additive combiner with a small multiplier value
* @return the combined quantitative records
* @see `defaultAdditiveOutputCombiner`
*/
private[exp] def onlyOriginalAssistEntriesIterable(
first: Iterable[ContributionStatsOutput],
second: Iterable[ContributionStatsOutput],
combiner: (ContributionStatsOutput, ContributionStatsOutput)=>ContributionStatsOutput =
defaultAdditiveOutputCombiner(multiplier = 0.05f)
): Iterable[ContributionStatsOutput] = {
if (second.isEmpty) {
first
} else if (first.isEmpty) {
second
} else {
//overlap discriminated by percentage
val shared: mutable.LongMap[ContributionStatsOutput] = mutable.LongMap[ContributionStatsOutput]()
for {
h @ ContributionStatsOutput(hid, _, _) <- first
a @ ContributionStatsOutput(aid, _, _) <- second
out = combiner(h, a)
id = out.player.CharId
if hid == aid && shared.put(id, out).isEmpty
} yield ()
val sharedKeys = shared.keys
(first ++ second).filterNot { case ContributionStatsOutput(id, _, _) => sharedKeys.exists(_ == id.CharId) } ++ shared.values
}
}
/**
* Combine two quantitative records into one, maintaining only the original entries.
* @param multiplier adjust the combined
* @param first one quantitative record
* @param second another quantitative record
* @return the combined quantitative records
*/
private def defaultAdditiveOutputCombiner(
multiplier: Float
)
(
first: ContributionStatsOutput,
second: ContributionStatsOutput
): ContributionStatsOutput = {
if (first.percentage < second.percentage)
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)
}
/**
* Take two sequences of equipment statistics
* and combine both lists where overlap of the same equipment use is added together per field.
* If one sequence comtains more elements of the same type of equipment use,
* the additional entries may become lost.
* @param first statistics in relation to equipment
* @param second statistics in relation to equipment
* @return statistics in relation to equipment
*/
private[exp] def combineWeaponStats(
first: Seq[WeaponStats],
second: Seq[WeaponStats]
): Seq[WeaponStats] = {
val (firstInSecond, firstAlone) = first.partition(firstStat => second.exists(_.equipment == firstStat.equipment))
val (secondInFirst, secondAlone) = second.partition(secondStat => firstInSecond.exists(_.equipment == secondStat.equipment))
val combined = firstInSecond.flatMap { firstStat =>
secondInFirst
.filter(_.equipment == firstStat.equipment)
.map { secondStat =>
firstStat.copy(
shots = firstStat.shots + secondStat.shots,
amount = firstStat.amount + secondStat.amount,
contributions = firstStat.contributions + secondStat.contributions,
time = math.max(firstStat.time, secondStat.time)
)
}
}
firstAlone ++ secondAlone ++ combined
}
/**
* Run a function against history, targeting a certain faction.
* @param tallyFunc the history analysis function
* @param history chronology of activity the game considers noteworthy
* @param faction empire to target
* @return quantitative record of activity in relation to the other players and their equipment
*/
private[exp] def allocateContributors(
tallyFunc: (List[InGameActivity], PlanetSideEmpire.Value, mutable.LongMap[ContributionStats]) => Any
)
(
history: List[InGameActivity],
faction: PlanetSideEmpire.Value
): mutable.LongMap[ContributionStats] = {
/*
players who have contributed to this death, and how much they have contributed<br>
key - character identifier,
value - (player, damage, total damage, number of shots)
*/
val participants: mutable.LongMap[ContributionStats] = mutable.LongMap[ContributionStats]()
tallyFunc(history, faction, participants)
participants
}
/**
* You better not fail this purity test.
* @param player player being tested
* @param history chronology of activity the game considers noteworthy;
* allegedly associated with this player
* @return `true`, if the player has ever committed a great shame;
* `false`, otherwise ... and it better be
*/
private[exp] def wasEverAMax(player: PlayerSource, history: Iterable[InGameActivity]): Boolean = {
player.ExoSuit == ExoSuitType.MAX || history.exists {
case SpawningActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
case ReconstructionActivity(p: PlayerSource, _, _) => p.ExoSuit == ExoSuitType.MAX
case RepairFromExoSuitChange(suit, _) => suit == ExoSuitType.MAX
case _ => false
}
}
/**
* Take a weapon statistics entry and calculate the support experience value resulting from this support event.
* The complete formula is:<br><br>
* `base + shots-multplier * ln(shots^exp + 2) + amount-multiplier * amount`<br><br>
* ... where the middle field can be truncated into:<br><br>
* `shots-multplier * shots`<br><br>
* ... without the natural logarithm exponent defined.
* Limits can be applied to the number of shots and/or to the amount,
* which will either zero the calculations or cap the results.
* @param event identification for the event calculation parameters
* @param weaponStat base weapon stat entry to be modified
* @param canNotFindEventDefaultValue custom default value
* @return weapon stat entry with a modified for the experience
*/
private[exp] def calculateSupportExperience(
event: String,
weaponStat: WeaponStats,
canNotFindEventDefaultValue: Option[Float] = None
): WeaponStats = {
val rewards: Float = sep.events
.find(evt => event.equals(evt.name))
.map { event =>
val shots = weaponStat.shots
val shotsMultiplier = event.shotsMultiplier
if (shotsMultiplier > 0f && shots < event.shotsCutoff) {
val modifiedShotsReward: Float = {
val partialShots = math.min(event.shotsLimit, shots).toFloat
shotsMultiplier * (if (event.shotsNatLog > 0f) {
math.log(math.pow(partialShots, event.shotsNatLog) + 2d).toFloat
} else {
partialShots
})
}
val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat
event.base + modifiedShotsReward + modifiedAmountReward
} else {
0f
}
}
.getOrElse(
canNotFindEventDefaultValue.getOrElse(sep.canNotFindEventDefaultValue.toFloat)
)
weaponStat.copy(contributions = rewards)
}
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, SupervisorStrategy}
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.Zone
object SupportExperienceCalculator {
def apply(zone: Zone): Behavior[Command] =
Behaviors.supervise[Command] {
Behaviors.setup(context => new SupportExperienceCalculator(context, zone))
}.onFailure[Exception](SupervisorStrategy.restart)
sealed trait Command
final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command
object RewardOurSupporters {
def apply(obj: PlanetSideGameObject with FactionAffinity with InGameHistory, kill: Kill): RewardOurSupporters = {
RewardOurSupporters(SourceEntry(obj), obj.History, kill, kill.experienceEarned)
}
}
}
class SupportExperienceCalculator(context: ActorContext[SupportExperienceCalculator.Command], zone: Zone)
extends AbstractBehavior[SupportExperienceCalculator.Command](context) {
import SupportExperienceCalculator._
def onMessage(msg: Command): Behavior[Command] = {
msg match {
case RewardOurSupporters(target: PlayerSource, history, kill, bep) =>
KillContributions.rewardTheseSupporters(target, history, kill, bep, zone.AvatarEvents)
case _ => ()
}
Behaviors.same
}
}

View file

@ -0,0 +1,217 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.zones.exp
import scala.concurrent.ExecutionContext.Implicits.global
import net.psforever.objects.avatar.scoring.EquipmentStat
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.persistence
import net.psforever.types.Vector3
import net.psforever.util.Database.ctx
import net.psforever.util.Database.ctx._
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,
victimExoSuitId: Int,
victimMounted: Int,
weaponId: Int,
zoneId: Int,
position: Vector3,
exp: Long
): Unit = {
ctx.run(query[persistence.Killactivity]
.insert(
_.victimId -> lift(victimId),
_.killerId -> lift(killerId),
_.victimExosuit -> lift(victimExoSuitId),
_.victimMounted -> lift(victimMounted),
_.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 `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.Killactivity]
.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 = {
ctx.run(query[persistence.Weaponstatsession]
.insert(
_.avatarId -> lift(avatarId),
_.weaponId -> lift(stats.objectId),
_.shotsFired -> lift(stats.shotsFired),
_.shotsLanded -> lift(stats.shotsLanded),
_.kills -> lift(0),
_.assists -> lift(0),
_.sessionId -> lift(-1L)
)
.onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)(
(t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired),
(t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded)
)
)
}
/**
* 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,
hackState: Option[HackInfo],
isCargo: Boolean,
weaponId: Int,
zoneNumber: Int
): Unit = {
import net.psforever.util.Database.ctx
import net.psforever.util.Database.ctx._
val normalFaction = machine.Faction.id
val hackedToFaction = hackState.map { _.player.Faction.id }.getOrElse(normalFaction)
val machinePosition = machine.Position
ctx.run(query[persistence.Machinedestroyed]
.insert(
_.avatarId -> lift(avatarId),
_.weaponId -> lift(weaponId),
_.machineType -> lift(machine.Definition.ObjectId),
_.machineFaction -> lift(normalFaction),
_.hackedFaction -> lift(hackedToFaction),
_.asCargo -> lift(isCargo),
_.zoneNum -> lift(zoneNumber),
_.px -> lift((machinePosition.x * 1000).toInt),
_.py -> lift((machinePosition.y * 1000).toInt),
_.pz -> lift((machinePosition.z * 1000).toInt)
)
)
}
/**
* Insert an entry into the database's `ntuactivity` table.
* This table monitors experience earned through NTU silo operations and
* first time event entity interactions (zone and building set to 0).
*/
def reportNtuActivity(
avatarId: Long,
zoneId: Int,
buildingId: Int,
experience: Long
): Unit = {
ctx.run(query[persistence.Ntuactivity]
.insert(
_.avatarId -> lift(avatarId),
_.zoneId -> lift(zoneId),
_.buildingId -> lift(buildingId),
_.exp -> lift(experience)
)
.onConflictUpdate(_.avatarId, _.zoneId, _.buildingId)(
(t, e) => t.exp -> (t.exp + e.exp)
)
)
}
/**
* Insert an entry into the database's `kdasession` table
* to specifically update the revive counter for the current session.
*/
def reportRespawns(
avatarId: Long,
reviveCount: Int
): Unit = {
ctx.run(query[persistence.Kdasession]
.insert(
_.avatarId -> lift(avatarId),
_.revives -> lift(reviveCount),
_.sessionId -> lift(-1)
)
)
}
/**
* Insert an entry into the database's `buildingCapture` table.
*/
def reportFacilityCapture(
avatarId: Long,
zoneId: Int,
buildingId: Int,
exp: Long,
expType: String
): Unit = {
ctx.run(query[persistence.Buildingcapture]
.insert(
_.avatarId -> lift(avatarId),
_.zoneId -> lift(zoneId),
_.buildingId -> lift(buildingId),
_.exp -> lift(exp),
_.expType -> lift(expType)
)
)
}
}

View file

@ -1,10 +0,0 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp
import net.psforever.objects.vital.InGameActivity
final case class VitalsHistoryException(
head: InGameActivity, //InGameActivity might be more suitable?
private val message: String = "",
private val cause: Throwable = None.orNull
) extends Exception(message, cause)

View file

@ -0,0 +1,55 @@
// 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
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 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 _ => ()
}
}
}

View file

@ -0,0 +1,90 @@
// 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, Support, 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 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) =>
var totalShots: Int = 0
var totalAmount: Int = 0
var mostRecentTime: Long = 0
val groupedWeapons = entry.weapons
.groupBy(_.equipment)
.map {
case (weaponContext, weaponEntries) =>
val specificEntries = weaponEntries.filter(_.equipment == weaponContext)
val amount = specificEntries.foldLeft(0)(_ + _.amount)
totalAmount = totalAmount + amount
val shots = specificEntries.foldLeft(0)(_ + _.shots)
totalShots = totalShots + shots
val time = specificEntries.maxBy(_.time).time
mostRecentTime = math.max(mostRecentTime, time)
Support.calculateSupportExperience(
event = "support-heal",
WeaponStats(weaponContext, amount, shots, time, 1f)
)
}
.toSeq
(id, entry.copy(
weapons = groupedWeapons,
amount = totalAmount,
total = math.max(entry.total, totalAmount),
shots = totalShots,
time = mostRecentTime
))
}
}
}

View file

@ -0,0 +1,68 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.zones.exp.rec
import net.psforever.objects.sourcing.SourceUniqueness
import net.psforever.objects.vital.{DamagingActivity, HealFromEquipment, HealingActivity, InGameActivity, RevivingActivity}
import net.psforever.types.PlanetSideEmpire
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 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 _ => ()
}
}
}

View file

@ -0,0 +1,77 @@
// 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.objects.zones.exp.{ContributionStats, Support, WeaponStats}
import net.psforever.types.PlanetSideEmpire
import scala.collection.mutable
class MachineRecoveryExperienceContributionProcess(
private val faction : PlanetSideEmpire.Value,
private val contributions: Map[SourceUniqueness, List[InGameActivity]],
eventOutputType: String,
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 =>
val (damage, recovery) = RecoveryExperienceContribution.contributeWithDamagingActivity(
d,
d.health,
damageParticipants,
participants,
damageInOrder,
recoveryInOrder
)
damageInOrder = damage
recoveryInOrder = recovery
case r: RepairFromEquipment if !excludedTargets.contains(r.user.unique) =>
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 _ => ()
}
}
override def output(): mutable.LongMap[ContributionStats] = {
super.output().map { case (id, stats) =>
val weps = stats.weapons
.groupBy(_.equipment)
.map { case (wrapper, entries) =>
val size = entries.size
val newTime = entries.maxBy(_.time).time
Support.calculateSupportExperience(
eventOutputType,
WeaponStats(wrapper, size, entries.foldLeft(0)(_ + _.amount), newTime, 1f)
)
}
.toSeq
(id, stats.copy(weapons = weps))
}
}
}

View file

@ -0,0 +1,282 @@
// 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
if (users.nonEmpty) {
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)
}
}

View file

@ -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()
}
}

View file

@ -14,6 +14,7 @@ import scala.annotation.switch
* na * na
*/ */
sealed abstract class Statistic(val code: Int) sealed abstract class Statistic(val code: Int)
/** /**
* na * na
*/ */
@ -38,12 +39,18 @@ sealed case class IntermediateStatistic(
* @param fields four pairs of values that add together to produce the first columns on the statistics spreadsheet; * @param fields four pairs of values that add together to produce the first columns on the statistics spreadsheet;
* organized as TR, NC, VS, BO (PS) * organized as TR, NC, VS, BO (PS)
*/ */
final case class InitStatistic( final case class CampaignStatistic(
category: StatisticalCategory, category: StatisticalCategory,
unk: StatisticalElement, unk: StatisticalElement,
fields: List[Long] fields: List[Long]
) extends Statistic(code = 0) with ComplexStatistic ) extends Statistic(code = 0) with ComplexStatistic
object CampaignStatistic {
def apply(cat: StatisticalCategory, stat: StatisticalElement, tr: Long, nc: Long, vs: Long, bo: Long): CampaignStatistic = {
CampaignStatistic(cat, stat, List(tr, 0, nc, 0, vs, 0, bo, 0))
}
}
/** /**
* *
* @param category na * @param category na
@ -51,12 +58,18 @@ final case class InitStatistic(
* @param fields four pairs of values that add together to produce the first column(s) on the statistics spreadsheet; * @param fields four pairs of values that add together to produce the first column(s) on the statistics spreadsheet;
* organized as TR, NC, VS, BO (PS) * organized as TR, NC, VS, BO (PS)
*/ */
final case class UpdateStatistic( final case class SessionStatistic(
category: StatisticalCategory, category: StatisticalCategory,
unk: StatisticalElement, unk: StatisticalElement,
fields: List[Long] fields: List[Long]
) extends Statistic(code = 1) with ComplexStatistic ) extends Statistic(code = 1) with ComplexStatistic
object SessionStatistic {
def apply(cat: StatisticalCategory, stat: StatisticalElement, tr: Long, nc: Long, vs: Long, bo: Long): SessionStatistic = {
SessionStatistic(cat, stat, List(0, tr, 0, nc, 0, vs, 0, bo))
}
}
/** /**
* *
* @param deaths how badly you suck, quantitatively analyzed * @param deaths how badly you suck, quantitatively analyzed
@ -103,10 +116,10 @@ object AvatarStatisticsMessage extends Marshallable[AvatarStatisticsMessage] {
*/ */
private val initCodec: Codec[Statistic] = complexCodec.exmap[Statistic]( private val initCodec: Codec[Statistic] = complexCodec.exmap[Statistic](
{ {
case IntermediateStatistic(a, b, c) => Successful(InitStatistic(a, b, c)) case IntermediateStatistic(a, b, c) => Successful(CampaignStatistic(a, b, c))
}, },
{ {
case InitStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c)) case CampaignStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c))
case _ => Failure(Err("expected initializing statistic, but found something else")) case _ => Failure(Err("expected initializing statistic, but found something else"))
} }
) )
@ -115,10 +128,10 @@ object AvatarStatisticsMessage extends Marshallable[AvatarStatisticsMessage] {
*/ */
private val updateCodec: Codec[Statistic] = complexCodec.exmap[Statistic]( private val updateCodec: Codec[Statistic] = complexCodec.exmap[Statistic](
{ {
case IntermediateStatistic(a, b, c) => Successful(UpdateStatistic(a, b, c)) case IntermediateStatistic(a, b, c) => Successful(SessionStatistic(a, b, c))
}, },
{ {
case UpdateStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c)) case SessionStatistic(a, b, c) => Successful(IntermediateStatistic(a, b, c))
case _ => Failure(Err("expected updating statistic, but found something else")) case _ => Failure(Err("expected updating statistic, but found something else"))
} }
) )

View file

@ -15,7 +15,8 @@ import scodec.codecs._
* At the moment, it only seems possible to receive and read mail from the server. * At the moment, it only seems possible to receive and read mail from the server.
* @param sender the name of the player who sent the mail * @param sender the name of the player who sent the mail
* @param subject the subject * @param subject the subject
* @param message the message * @param message the message;
* line breaks use `\n`
*/ */
final case class MailMessage(sender: String, subject: String, message: String) extends PlanetSideGamePacket { final case class MailMessage(sender: String, subject: String, message: String) extends PlanetSideGamePacket {
type Packet = MailMessage type Packet = MailMessage

View file

@ -12,6 +12,8 @@ case class Account(
inactive: Boolean = false, inactive: Boolean = false,
gm: Boolean = false, gm: Boolean = false,
lastFactionId: Int = 3, lastFactionId: Int = 3,
avatarLoggedIn: Long,
sessionId: Long,
token: Option[String], token: Option[String],
tokenCreated: Option[LocalDateTime] tokenCreated: Option[LocalDateTime]
) )

View file

@ -0,0 +1,95 @@
// Copyright (c) 2023 PSForever
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()
)
case class Buildingcapture(
index: Int,
avatarId: Long,
zoneId: Int,
buildingId: Int,
exp: Long,
expType: String,
timestamp: LocalDateTime = LocalDateTime.now()
)
case class Kdasession (
avatarId: Long,
sessionId: Int,
kills: Int,
deaths: Int,
assists: Int,
revives: Int
)
case class Killactivity(
index: Int,
killerId: Long,
victimId: Long,
victimExosuit: Int,
victimMounted: Int, //object type id * 10 + seat type
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()
)
case class Machinedestroyed(
index: Int,
avatarId: Long,
weaponId: Int,
machineType: Int,
machineFaction: Int,
hackedFaction: Int,
asCargo: Boolean,
zoneNum: Int,
px: Int, //Position.x * 1000
py: Int, //Position.y * 1000
pz: Int, //Position.z * 1000
timestamp: LocalDateTime = LocalDateTime.now()
)
case class Ntuactivity (
avatarId: Long,
zoneId: Int,
buildingId: Int,
exp: Long
)
case class Supportactivity(
index: Int,
userId: Long,
targetId: Long,
targetExosuit: Int,
interactionType: Int,
implementType: Int,
intermediateType: Int,
exp: Long,
timestamp: LocalDateTime = LocalDateTime.now()
)
case class Weaponstatsession(
avatarId: Long,
weaponId: Int,
shotsFired: Int,
shotsLanded: Int,
kills: Int,
assists: Int,
sessionId: Long
)

View file

@ -0,0 +1,7 @@
// Copyright (c) 2023 PSForever
package net.psforever.persistence
case class Progressiondebt(
avatarId:Long,
experience: Long
)

View file

@ -82,7 +82,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
entryManagementBehaviors entryManagementBehaviors
.orElse { .orElse {
case RemoverActor.AddTask(obj, zone, duration) => case RemoverActor.AddTask(obj, zone, duration) =>
val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toNanos) val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toMillis)
if (InclusionTest(entry) && !secondHeap.exists(test => sameEntryComparator.Test(test, entry))) { if (InclusionTest(entry) && !secondHeap.exists(test => sameEntryComparator.Test(test, entry))) {
InitialJob(entry) InitialJob(entry)
if (entry.duration == 0) { if (entry.duration == 0) {
@ -120,7 +120,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
case RemoverActor.StartDelete() => case RemoverActor.StartDelete() =>
firstTask.cancel() firstTask.cancel()
secondTask.cancel() secondTask.cancel()
val now: Long = System.nanoTime val now: Long = System.currentTimeMillis()
val (in, out) = firstHeap.partition(entry => { val (in, out) = firstHeap.partition(entry => {
now - entry.time >= entry.duration now - entry.time >= entry.duration
}) })
@ -229,19 +229,19 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
* this new entry is always set to last for the duration of the second pool * this new entry is always set to last for the duration of the second pool
*/ */
private def RepackageEntry(entry: RemoverActor.Entry): RemoverActor.Entry = { private def RepackageEntry(entry: RemoverActor.Entry): RemoverActor.Entry = {
RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toNanos) RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toMillis)
} }
/** /**
* Common function to reset the first task's delayed execution. * Common function to reset the first task's delayed execution.
* Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool. * Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool.
* @param now the time (in nanoseconds); * @param now the time (in milliseconds);
* defaults to the current time (in nanoseconds) * defaults to the current time (in milliseconds)
*/ */
def RetimeFirstTask(now: Long = System.nanoTime): Unit = { def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = {
firstTask.cancel() firstTask.cancel()
if (firstHeap.nonEmpty) { if (firstHeap.nonEmpty) {
val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)) nanoseconds val short_timeout: FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete()) firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete())
} }
@ -274,14 +274,14 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
/** /**
* Default time for entries waiting in the first list. * Default time for entries waiting in the first list.
* Override. * Override.
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds) * @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
*/ */
def FirstStandardDuration: FiniteDuration def FirstStandardDuration: FiniteDuration
/** /**
* Default time for entries waiting in the second list. * Default time for entries waiting in the second list.
* Override. * Override.
* @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds) * @return the time as a `FiniteDuration` object (to be later transformed into milliseconds)
*/ */
def SecondStandardDuration: FiniteDuration def SecondStandardDuration: FiniteDuration
@ -322,7 +322,7 @@ object RemoverActor extends SupportActorCaseConversions {
* Internally, all entries have a "time created" field. * Internally, all entries have a "time created" field.
* @param _obj the target * @param _obj the target
* @param _zone the zone in which this target is registered * @param _zone the zone in which this target is registered
* @param _duration how much longer the target will exist in its current state (in nanoseconds) * @param _duration how much longer the target will exist in its current state (in milliseconds)
*/ */
case class Entry(_obj: PlanetSideGameObject, _zone: Zone, _duration: Long) case class Entry(_obj: PlanetSideGameObject, _zone: Zone, _duration: Long)
extends SupportActor.Entry(_obj, _zone, _duration) extends SupportActor.Entry(_obj, _zone, _duration)
@ -332,7 +332,7 @@ object RemoverActor extends SupportActorCaseConversions {
* @see `FirstStandardDuration` * @see `FirstStandardDuration`
* @param obj the target * @param obj the target
* @param zone the zone in which this target is registered * @param zone the zone in which this target is registered
* @param duration how much longer the target will exist in its current state (in nanoseconds); * @param duration how much longer the target will exist in its current state (in milliseconds);
* a default time duration is provided by implementation * a default time duration is provided by implementation
*/ */
case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None) case class AddTask(obj: PlanetSideGameObject, zone: Zone, duration: Option[FiniteDuration] = None)

View file

@ -438,7 +438,34 @@ class AvatarService(zone: Zone) extends Actor {
) )
) )
case _ => ; case AvatarAction.AwardBep(charId, bep, expType) =>
AvatarEvents.publish(
AvatarServiceResponse(
s"/$forChannel/Avatar",
Service.defaultPlayerGUID,
AvatarResponse.AwardBep(charId, bep, expType)
)
)
case AvatarAction.AwardCep(charId, bep) =>
AvatarEvents.publish(
AvatarServiceResponse(
s"/$forChannel/Avatar",
Service.defaultPlayerGUID,
AvatarResponse.AwardCep(charId, bep)
)
)
case AvatarAction.FacilityCaptureRewards(building_id, zone_number, exp) =>
AvatarEvents.publish(
AvatarServiceResponse(
s"/$forChannel/Avatar",
Service.defaultPlayerGUID,
AvatarResponse.FacilityCaptureRewards(building_id, zone_number, exp)
)
)
case _ => ()
} }
//message to Undertaker //message to Undertaker

View file

@ -12,7 +12,7 @@ import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent} import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3} import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
@ -156,6 +156,9 @@ object AvatarAction {
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Action final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Action
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Action
final case class AwardCep(charId: Long, bep: Long) extends Action
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action
final case class TeardownConnection() extends Action final case class TeardownConnection() extends Action
// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action

View file

@ -11,7 +11,7 @@ import net.psforever.objects.sourcing.SourceEntry
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.objectcreate.ConstructorData
import net.psforever.packet.game.ObjectCreateMessage import net.psforever.packet.game.ObjectCreateMessage
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3} import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.services.GenericEventBusMsg import net.psforever.services.GenericEventBusMsg
final case class AvatarServiceResponse( final case class AvatarServiceResponse(
@ -127,4 +127,7 @@ object AvatarResponse {
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Response
final case class AwardCep(charId: Long, bep: Long) extends Response
final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response
} }

View file

@ -49,8 +49,7 @@ class DoorCloseActor() extends Actor {
}) })
if (openDoors.nonEmpty) { if (openDoors.nonEmpty) {
val short_timeout: FiniteDuration = val short_timeout: FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)).milliseconds
math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)) nanoseconds
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors()) doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
} }
@ -67,7 +66,7 @@ class DoorCloseActor() extends Actor {
* and newer entries are always added to the end of the main `List`, * and newer entries are always added to the end of the main `List`,
* processing in order is always correct. * processing in order is always correct.
* @param list the `List` of entries to divide * @param list the `List` of entries to divide
* @param now the time right now (in nanoseconds) * @param now the time right now (in milliseconds)
* @see `List.partition` * @see `List.partition`
* @return a `Tuple` of two `Lists`, whose qualifications are explained above * @return a `Tuple` of two `Lists`, whose qualifications are explained above
*/ */
@ -84,7 +83,7 @@ class DoorCloseActor() extends Actor {
* a `List` of elements that have exceeded the time limit, * a `List` of elements that have exceeded the time limit,
* and a `List` of elements that still satisfy the time limit. * and a `List` of elements that still satisfy the time limit.
* @param iter the `Iterator` of entries to divide * @param iter the `Iterator` of entries to divide
* @param now the time right now (in nanoseconds) * @param now the time right now (in milliseconds)
* @param index a persistent record of the index where list division should occur; * @param index a persistent record of the index where list division should occur;
* defaults to 0 * defaults to 0
* @return the index where division will occur * @return the index where division will occur

View file

@ -9,19 +9,16 @@ import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.{Default, Player} import net.psforever.objects.Default
import net.psforever.packet.game.{GenericAction, PlanetsideAttributeEnum} import net.psforever.packet.game.{GenericAction, PlanetsideAttributeEnum}
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.ZoneHotSpotProjector
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
import net.psforever.util.Config
import java.util.concurrent.TimeUnit
import scala.collection.mutable
import scala.concurrent.duration.{FiniteDuration, _} import scala.concurrent.duration.{FiniteDuration, _}
import scala.util.{Random, Success} import scala.util.Random
/** /**
* Responsible for handling the aspects related to hacking control consoles and capturing bases. * Responsible for handling the aspects related to hacking control consoles and capturing bases.
@ -40,7 +37,7 @@ class HackCaptureActor extends Actor {
val duration = target.Definition.FacilityHackTime val duration = target.Definition.FacilityHackTime
target.HackedBy match { target.HackedBy match {
case Some(hackInfo) => case Some(hackInfo) =>
target.HackedBy = hackInfo.Duration(duration.toNanos) target.HackedBy = hackInfo.Duration(duration.toMillis)
case None => case None =>
log.error(s"Initial $target hack information is missing") log.error(s"Initial $target hack information is missing")
} }
@ -59,8 +56,8 @@ class HackCaptureActor extends Actor {
case HackCaptureActor.ProcessCompleteHacks() => case HackCaptureActor.ProcessCompleteHacks() =>
log.trace("Processing complete hacks") log.trace("Processing complete hacks")
clearTrigger.cancel() clearTrigger.cancel()
val now: Long = System.nanoTime val now: Long = System.currentTimeMillis()
val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toNanos) val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toMillis)
hackedObjects = stillHacked hackedObjects = stillHacked
finishedHacks.foreach { entry => finishedHacks.foreach { entry =>
val terminal = entry.target val terminal = entry.target
@ -79,14 +76,9 @@ class HackCaptureActor extends Actor {
case _ => case _ =>
// Timed hack finished (or neutral LLU base with no neighbour as timed hack), capture the base // Timed hack finished (or neutral LLU base with no neighbour as timed hack), capture the base
val hackTime = terminal.Definition.FacilityHackTime.toMillis
HackCompleted(terminal, hackedByFaction) HackCompleted(terminal, hackedByFaction)
HackCaptureActor.RewardFacilityCaptureParticipants( building.Participation.RewardFacilityCapture(terminal.Faction, hackedByFaction, hacker, hackTime, hackTime, isResecured = false)
building,
terminal,
hacker,
now - entry.hack_timestamp,
isResecured = false
)
} }
} }
// If there's hacked objects left in the list restart the timer with the shortest hack time left // If there's hacked objects left in the list restart the timer with the shortest hack time left
@ -94,6 +86,7 @@ class HackCaptureActor extends Actor {
case HackCaptureActor.ResecureCaptureTerminal(target, _, hacker) => case HackCaptureActor.ResecureCaptureTerminal(target, _, hacker) =>
val (results, remainder) = hackedObjects.partition(x => x.target eq target) val (results, remainder) = hackedObjects.partition(x => x.target eq target)
val faction = GetHackingFaction(target).getOrElse(target.Faction)
target.HackedBy = None target.HackedBy = None
hackedObjects = remainder hackedObjects = remainder
val building = target.Owner.asInstanceOf[Building] val building = target.Owner.asInstanceOf[Building]
@ -102,13 +95,7 @@ class HackCaptureActor extends Actor {
case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured) case flag: CaptureFlag => target.Zone.LocalEvents ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.Resecured)
} }
NotifyHackStateChange(target, isResecured = true) NotifyHackStateChange(target, isResecured = true)
// HackCaptureActor.RewardFacilityCaptureParticipants( building.Participation.RewardFacilityCapture(target.Faction, faction, hacker, target.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - results.head.hack_timestamp, isResecured = true)
// building,
// target,
// hacker,
// System.currentTimeMillis() - results.head.hack_timestamp,
// isResecured = true
// )
// Restart the timer in case the object we just removed was the next one scheduled // Restart the timer in case the object we just removed was the next one scheduled
RestartTimer() RestartTimer()
@ -124,13 +111,7 @@ class HackCaptureActor extends Actor {
val hackedByFaction = hackInfo.hackerFaction val hackedByFaction = hackInfo.hackerFaction
hackedObjects = hackedObjects.filterNot(x => x == entry) hackedObjects = hackedObjects.filterNot(x => x == entry)
HackCompleted(terminal, hackedByFaction) HackCompleted(terminal, hackedByFaction)
// HackCaptureActor.RewardFacilityCaptureParticipants( building.Participation.RewardFacilityCapture(terminal.Faction, hacker.Faction, hacker, terminal.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - entry.hack_timestamp, isResecured = false)
// building,
// terminal,
// hacker,
// System.currentTimeMillis() - entry.hack_timestamp,
// isResecured = false
// )
entry.target.Actor ! CommonMessages.ClearHack() entry.target.Actor ! CommonMessages.ClearHack()
flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag) flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag)
// If there's hacked objects left in the list restart the timer with the shortest hack time left // If there's hacked objects left in the list restart the timer with the shortest hack time left
@ -151,6 +132,9 @@ class HackCaptureActor extends Actor {
case owner: Building if owner.IsCtfBase => Some((owner, owner.GetFlag, owner.Neighbours(hackingFaction))) case owner: Building if owner.IsCtfBase => Some((owner, owner.GetFlag, owner.Neighbours(hackingFaction)))
case _ => None case _ => None
}) match { }) match {
case None =>
//not an error; this is just not a ctf facility
false
case Some((owner, None, Some(neighbours))) if neighbours.nonEmpty => case Some((owner, None, Some(neighbours))) if neighbours.nonEmpty =>
log.info(s"An LLU is being spawned for facility ${owner.Name} by $hackingFaction") log.info(s"An LLU is being spawned for facility ${owner.Name} by $hackingFaction")
spawnCaptureFlag(neighbours, terminal, hackingFaction) spawnCaptureFlag(neighbours, terminal, hackingFaction)
@ -231,9 +215,8 @@ class HackCaptureActor extends Actor {
private def RestartTimer(): Unit = { private def RestartTimer(): Unit = {
if (hackedObjects.nonEmpty) { if (hackedObjects.nonEmpty) {
val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.nanoTime())) val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.currentTimeMillis()))
val short_timeout: FiniteDuration = val short_timeout: FiniteDuration = math.max(1, hackEntry.duration.toMillis - (System.currentTimeMillis() - hackEntry.hack_timestamp)).milliseconds
math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)).nanoseconds
log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds") log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds")
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks()) clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks())
@ -247,7 +230,7 @@ object HackCaptureActor {
zone: Zone, zone: Zone,
unk1: Long, unk1: Long,
unk2: Long, unk2: Long,
startTime: Long = System.nanoTime() startTime: Long = System.currentTimeMillis()
) )
final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource) final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource)
@ -274,8 +257,7 @@ object HackCaptureActor {
17039360L 17039360L
case Some(Hackable.HackInfo(p, _, start, length)) => case Some(Hackable.HackInfo(p, _, start, length)) =>
// See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated // See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated
val hackTimeRemainingMS = val hackTimeRemainingMS = math.max(0, start + length - System.currentTimeMillis())
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
val startNum = p.Faction match { val startNum = p.Faction match {
case PlanetSideEmpire.TR => 0x10000 case PlanetSideEmpire.TR => 0x10000
case PlanetSideEmpire.NC => 0x20000 case PlanetSideEmpire.NC => 0x20000
@ -291,149 +273,12 @@ object HackCaptureActor {
entry1: HackCaptureActor.HackEntry, entry1: HackCaptureActor.HackEntry,
entry2: HackCaptureActor.HackEntry entry2: HackCaptureActor.HackEntry
): HackCaptureActor.HackEntry = { ): HackCaptureActor.HackEntry = {
val entry1TimeLeft = entry1.duration.toNanos - (now - entry1.hack_timestamp) val entry1TimeLeft = entry1.duration.toMillis - (now - entry1.hack_timestamp)
val entry2TimeLeft = entry2.duration.toNanos - (now - entry2.hack_timestamp) val entry2TimeLeft = entry2.duration.toMillis - (now - entry2.hack_timestamp)
if (entry1TimeLeft < entry2TimeLeft) { if (entry1TimeLeft < entry2TimeLeft) {
entry1 entry1
} else { } else {
entry2 entry2
} }
} }
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
private implicit val timeout: Timeout = Timeout(5.seconds)
private def RewardFacilityCaptureParticipants(
building: Building,
terminal: CaptureTerminal,
hacker: PlayerSource,
time: Long,
isResecured: Boolean
): Unit = {
val faction: PlanetSideEmpire.Value = terminal.Faction
val (contributionVictor, contributionAgainst) = building.PlayerContribution.keys.partition { _.Faction == faction }
val contributionVictorSize = contributionVictor.size
val flagCarrier = if (!isResecured) {
building.GetFlagSocket.flatMap(_.previousFlag).flatMap(_.Carrier)
} else {
None
}
val request = ask(building.Zone.Activity, ZoneHotSpotProjector.ExposeHeatForRegion(building.Position, building.Definition.SOIRadius.toFloat))
request.onComplete {
case Success(ZoneHotSpotProjector.ExposedHeat(_, _, activity)) =>
val (heatVictor, heatAgainst) = {
val reports = activity.map { _.Activity }
val allHeat: List[Long] = reports.map { a => a.values.foldLeft(0L)(_ + _.Heat) }
val _rewardedHeat: List[Long] = reports.flatMap { rep => rep.get(faction).map { _.Heat.toLong } }
val _enemyHeat = allHeat.indices.map { index =>
val allHeatValue = allHeat(index)
val rewardedHeatValue = _rewardedHeat(index)
allHeatValue - rewardedHeatValue
}
(_rewardedHeat, _enemyHeat.toList)
}
val heatVictorSum: Long = heatVictor.sum[Long]
val heatAgainstSum: Long = heatAgainst.sum[Long]
if (contributionVictorSize > 0) {
val contributionRate = if (heatVictorSum * heatAgainstSum != 0) {
math.log(heatVictorSum * contributionVictorSize / heatAgainstSum.toFloat).toFloat
} else {
contributionAgainst.size / contributionVictorSize.toFloat
}
RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, contributionRate)
}
case _ =>
RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, victorContributionRate = 1.0f)
}
request.recover {
_ => RewardFacilityCaptureParticipants(building, terminal, faction, hacker, building.PlayersInSOI, flagCarrier, isResecured, time, victorContributionRate = 1.0f)
}
}
private def RewardFacilityCaptureParticipants(
building: Building,
terminal: CaptureTerminal,
faction: PlanetSideEmpire.Value,
hacker: PlayerSource,
targets: List[Player],
flagCarrier: Option[Player],
isResecured: Boolean,
hackTime: Long,
victorContributionRate: Float
): Unit = {
val contribution = building.PlayerContribution
val (contributionVictor, contributionAgainst) = contribution.keys.partition { _.Faction == faction }
val contributionVictorSize = contributionVictor.size
val contributionAgainstSize = contributionAgainst.size
val (contributionByTime, contributionByTimePartitioned) = {
val curr = System.currentTimeMillis()
val interval = 300000
val range: Seq[Long] = {
val htime = hackTime.toInt
(
if (htime < 60000) {
Seq(htime, interval + htime, 2 * interval + htime)
} else if (htime <= interval) {
Seq(60000, htime, interval + htime, 2 * interval + htime)
} else {
(60000 +: (interval to htime by interval)) ++ Seq(interval + htime, 2 * interval + htime)
}
).map { _.toLong }
}
val playerMap = Array.fill[mutable.ListBuffer[Player]](range.size)(mutable.ListBuffer.empty)
contribution.foreach { case (p, t) =>
playerMap(range.lastIndexWhere(time => curr - t <= time)).addOne(p)
}
(playerMap, playerMap.map { _.partition(_.Faction == faction) })
}
val contributionByTimeSize = contributionByTime.length
val base: Long = 50L
val overallPopulationBonus = {
contributionByTime.map { _.size }.sum * contributionByTimeSize +
contributionByTime.zipWithIndex.map { case (lst, index) =>
((contributionByTimeSize - index) * lst.size *
{
val lists = contributionByTimePartitioned(index)
lists._2.size / math.max(lists._1.size, 1).toFloat
}).toLong
}.sum
}
val competitionBonus: Long = if (contributionAgainstSize * 1.5f < contributionVictorSize.toFloat) {
//steamroll by the victor
25L * (contributionVictorSize - contributionAgainstSize)
} else if (contributionVictorSize * 1.5f <= contributionAgainstSize.toFloat) {
//victory against overwhelming odds
500L + 50L * contribution.keys.size
} else {
//still a battle
10L * math.min(contributionAgainstSize, contributionVictorSize)
}
val timeMultiplier: Float = {
val buildingHackTimeMilli = terminal.Definition.FacilityHackTime.toMillis.toFloat
1f + (if (isResecured) {
(buildingHackTimeMilli - hackTime) / buildingHackTimeMilli
} else {
0f
})
}
val finalCep: Long = ((base + overallPopulationBonus + competitionBonus) * timeMultiplier * Config.app.game.cepRate).toLong
//reward participant(s)
// targets
// .filter { player =>
// player.Faction == faction && !player.Name.equals(hacker.Name)
// }
// .foreach { player =>
// events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(0, finalCep))
// }
// events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
// flagCarrier match {
// case Some(player) => events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(player.CharId, finalCep / 2))
// case None => ;
// }
}
} }

Some files were not shown because too many files have changed in this diff Show more