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 = {}
}
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
// with actor probe setting
// TODO(chord): Remove when Zone supports notification of booting being complete
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)
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 =
new Building("Building", building_guid = 0, map_id = 0, zone, StructureType.Building, GlobalDefinitions.building)
pad.Owner.Faction = faction

View file

@ -75,12 +75,6 @@ game {
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
amenity-autorepair-rate = 1.0
@ -223,6 +217,190 @@ game {
# Don't ask.
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 {

View file

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

View file

@ -4,10 +4,17 @@ package net.psforever.actors.session
import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import java.util.concurrent.atomic.AtomicInteger
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.scoring.{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 scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
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.sourcing.{PlayerSource,SourceWithHealthEntry}
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.{Friend => GameFriend, _}
import net.psforever.persistence
@ -199,11 +206,17 @@ object AvatarActor {
/** Set total battle experience points */
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 */
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 */
final case class SetCep(bep: Long) extends Command
final case class SetCep(cep: Long) extends Command
/** Set cosmetics. Only allowed for BR24 or higher. */
final case class SetCosmetics(personalStyles: Set[Cosmetic]) extends Command
@ -226,6 +239,8 @@ object AvatarActor {
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)
* and all of the items in the player's backpack (inventory)
@ -822,6 +837,110 @@ object AvatarActor {
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 = {
val bep = avatar.bep
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
@ -863,6 +982,9 @@ class AvatarActor(
var _avatar: Option[Avatar] = None
var saveLockerFunc: () => Unit = storeNewLocker
//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
@ -1024,13 +1146,13 @@ class AvatarActor(
} yield true
inits.onComplete {
case Success(_) =>
performAvatarLogin(avatarId, account.id, replyTo)
performAvatarLoginTest(avatarId, account.id, replyTo)
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
}
} else {
performAvatarLogin(avatarId, account.id, replyTo)
performAvatarLoginTest(avatarId, account.id, replyTo)
}
case Success(_) =>
//TODO this may not be an actual failure, but don't know what to do
@ -1049,6 +1171,11 @@ class AvatarActor(
buffer.stash(other)
Behaviors.same
}
.receiveSignal {
case (_, PostStop) =>
AvatarActor.avatarNoLongerLoggedIn(account.id)
Behaviors.same
}
}
def postCharacterSelectBehaviour(): Behavior[Command] = {
@ -1646,24 +1773,95 @@ class AvatarActor(
updateToolDischarge(stats)
Behaviors.same
case UpdateKillsDeathsAssists(stat) =>
updateKillsDeathsAssists(stat)
case UpdateKillsDeathsAssists(stat: Kill) =>
updateKills(stat)
Behaviors.same
case UpdateKillsDeathsAssists(stat: Assist) =>
updateAssists(stat)
Behaviors.same
case UpdateKillsDeathsAssists(stat: Death) =>
updateDeaths(stat)
Behaviors.same
case UpdateKillsDeathsAssists(stat: SupportActivity) =>
updateSupport(stat)
Behaviors.same
case AwardBep(bep, ExperienceType.Support) =>
val gain = bep - experienceDebt
if (gain > 0L) {
awardSupportExperience(gain, previousDelay = 0L)
} else {
experienceDebt = experienceDebt - bep
}
Behaviors.same
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
case SetBep(bep) =>
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
case AwardCep(cep) =>
setCep(avatar.cep + cep)
if (experienceDebt > 0L) {
setCep(avatar.cep + cep)
}
Behaviors.same
case SetCep(cep) =>
setCep(cep)
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_229, "@AckSuccessSetCommandRank"))
Behaviors.same
case SetCosmetics(cosmetics) =>
@ -1789,10 +1987,17 @@ class AvatarActor(
}
.receiveSignal {
case (_, PostStop) =>
AvatarActor.saveAvatarData(avatar)
staminaRegenTimer.cancel()
implantTimers.values.foreach(_.cancel())
supportExperienceTimer.cancel()
if (supportExperiencePool > 0) {
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool)
}
AvatarActor.saveAvatarData(avatar)
saveLockerFunc()
AvatarActor.updateToolDischargeFor(avatar)
AvatarActor.saveExperienceDebt(avatar.id, experienceDebt)
AvatarActor.avatarNoLongerLoggedIn(account.get.id)
Behaviors.same
}
}
@ -1805,9 +2010,26 @@ class AvatarActor(
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 = {
import ctx._
val result = for {
//log this login
_ <- ctx.run(
@ -1819,7 +2041,10 @@ class AvatarActor(
_ <- ctx.run(
query[persistence.Account]
.filter(_.id == lift(accountId))
.update(_.lastFactionId -> lift(avatar.faction.id))
.update(
_.lastFactionId -> lift(avatar.faction.id),
_.avatarLoggedIn -> lift(avatarId)
)
)
//retrieve avatar data
loadouts <- initializeAllLoadouts()
@ -1830,9 +2055,11 @@ class AvatarActor(
ignored <- loadIgnoredList(avatarId)
shortcuts <- loadShortcuts(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 {
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
// val implantShortcuts = shortcutList.filter {
// case Some(e) => e.purpose == 0
@ -1866,11 +2093,13 @@ class AvatarActor(
cooldowns = Cooldowns(
purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
)
),
scorecard = card
)
)
// if we need to start stamina regeneration
tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) }
experienceDebt = debt
replyTo ! AvatarLoginResponse(avatar)
case Failure(e) =>
log.error(e)("db failure")
@ -2865,13 +3094,7 @@ class AvatarActor(
def setBep(bep: Long, modifier: ExperienceType): Unit = {
import ctx._
val current = BattleRank.withExperience(avatar.bep).value
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 {
AvatarActor.setBepOnly(avatar.id, bep).onComplete {
case Success(_) =>
val sess = session.get
val zone = sess.zone
@ -2880,6 +3103,9 @@ class AvatarActor(
val player = sess.player
val pguid = player.GUID
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))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep))
if (current < br24 && next >= br24 || current >= br24 && next < br24) {
@ -2936,41 +3162,142 @@ class AvatarActor(
}
}
def updateKillsDeathsAssists(kdaStat: KDAStat): Unit = {
avatar.scorecard.rate(kdaStat)
val exp = kdaStat.experienceEarned
def awardSupportExperience(bep: Long, previousDelay: Long): Unit = {
setBep(avatar.bep + bep, ExperienceType.Support) //todo simplify support testing
// 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 zone = _session.zone
val player = _session.player
kdaStat match {
case kill: Kill =>
val playerSource = PlayerSource(player)
(kill.info.interaction.cause match {
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
case _ => None
}).collect {
case Some(obj: Vitality) =>
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
}
zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp)
case _: Death =>
zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarStatisticsMessage(DeathStatistic(avatar.scorecard.Lives.size))
)
)
zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard)))
)
)
}
def updateAssists(assistStat: Assist): Unit = {
avatar.scorecard.rate(assistStat)
val exp = assistStat.experienceEarned
val _session = session.get
val avatarId = avatar.id.toLong
assistStat.weapons.foreach { wrapper =>
zones.exp.ToDatabase.reportKillAssistBy(
avatarId,
assistStat.victim.CharId,
wrapper.equipment,
_session.zone.Number,
assistStat.victim.Position,
exp
)
}
if (exp > 0L) {
val gameOpts = Config.app.game
val (msg, modifier): (ExperienceType, Float) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
(ExperienceType.RabbitBall, 1.25f)
} else {
(ExperienceType.Normal, 1f)
}
setBep(avatar.bep + (exp * modifier * gameOpts.bepRate).toLong, msg)
awardSupportExperience(exp, previousDelay = 0L)
}
def updateSupport(supportStat: SupportActivity): Unit = {
val avatarId = avatar.id.toLong
val target = supportStat.target
val targetId = target.CharId
val targetExosuit = target.ExoSuit.id
val exp = supportStat.experienceEarned
supportStat.weapons.foreach { entry =>
zones.exp.ToDatabase.reportSupportBy(
avatarId,
targetId,
targetExosuit,
entry.value,
entry.intermediate,
entry.equipment,
exp
)
}
awardSupportExperience(exp, previousDelay = 0L)
}
def updateExperienceAndType(exp: Long): (Long, ExperienceType) = {
val _session = session.get
val player = _session.player
val gameOpts = Config.app.game.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 = {
@ -3103,4 +3430,41 @@ class AvatarActor(
}
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.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.scaladsl.adapter._
import scala.collection.mutable
import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration._
@ -76,7 +77,7 @@ object ChatActor {
): Unit = {
if (silos.isEmpty) {
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 {
@ -225,7 +226,7 @@ class ChatActor(
}
sessionActor ! SessionActor.SetFlying(flying)
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, _, _) =>
@ -282,69 +283,43 @@ class ChatActor(
}
errorMessage match {
case Some(errorMessage) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_QUIT,
false,
"",
errorMessage,
None
)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, errorMessage))
case None =>
sessionActor ! SessionActor.Recall()
}
case (CMT_INSTANTACTION, _, _) =>
if (session.zoningType == Zoning.Method.Quit) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "You can't instant action while quitting.", None)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "You can't instant action while quitting."))
} else if (session.zoningType == Zoning.Method.InstantAction) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_instantactionting", None)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_instantactionting"))
} else if (session.zoningType == Zoning.Method.Recall) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_QUIT,
false,
"",
"You won't instant action. You already requested to recall to your sanctuary continent",
None
)
ChatMsg(CMT_QUIT, "You won't instant action. You already requested to recall to your sanctuary continent")
)
} else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
if (session.player.isAlive) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_deconstructing", None)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_deconstructing"))
} else {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_dead", None)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_dead"))
}
} else if (session.player.VehicleSeated.nonEmpty) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noinstantaction_invehicle", None)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_invehicle"))
} else {
sessionActor ! SessionActor.InstantAction()
}
case (CMT_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) {
if (session.player.isAlive) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(CMT_QUIT, false, "", "@noquit_deconstructing", None)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_deconstructing"))
} 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) {
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_invehicle", None))
sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_invehicle"))
} else {
sessionActor ! SessionActor.Quit()
}
@ -514,7 +489,7 @@ class ChatActor(
sessionActor ! SessionActor.SendResponse(
ChatMsg(
UNK_229,
true,
wideContents=true,
"",
s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.",
None
@ -524,7 +499,7 @@ class ChatActor(
sessionActor ! SessionActor.SendResponse(
ChatMsg(
UNK_229,
true,
wideContents=true,
"",
s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.",
None
@ -610,11 +585,11 @@ class ChatActor(
)
} else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.UNK_45, false, "none", "@notell_target", None)
ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_target", None)
)
} else {
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)
if (popNC + popTR + popVS == 0) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, false, "", "@Nomatches", None)
)
sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.CMT_WHO, "@Nomatches"))
} else {
val contName = session.zone.map.name
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(
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(
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(
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 {
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) =>
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) =>
sessionActor ! SessionActor.SetZone(zone.zonename, gate)
case (_, None, false) =>
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") =>
sessionActor ! SessionActor.SendResponse(
@ -740,16 +713,16 @@ class ChatActor(
zone match {
case Some(zone: PointOfInterest) =>
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" =>
PointOfInterest.getWarpLocation(session.zone.id, waypoint) match {
case Some(location) => sessionActor ! SessionActor.SetPosition(location)
case None =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", s"unknown location '$waypoint'", None)
ChatMsg(UNK_229, wideContents=true, "", s"unknown location '$waypoint'", None)
)
}
case _ =>
@ -759,60 +732,10 @@ class ChatActor(
}
case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
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(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")
)
}
setBattleRank(message, contents, session, AvatarActor.SetBep)
case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
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(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")
)
}
setCommandRank(message, contents, session)
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
contents.toIntOption match {
@ -1031,20 +954,20 @@ class ChatActor(
if (session.player.silenced) {
sessionActor ! SessionActor.SetSilenced(false)
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()
} else {
sessionActor ! SessionActor.SetSilenced(true)
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(
time minutes,
() => {
sessionActor ! SessionActor.SetSilenced(false)
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) {
chatService ! ChatService.Message(
session,
ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None),
ChatMsg(UNK_227, wideContents=true, "", contents.replace("!whitetext ", ""), None),
ChatChannel.Default()
)
true
@ -1350,6 +1273,13 @@ class ChatActor(
case _ =>
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 {
false // unknown ! commands are ignored
}
@ -1357,4 +1287,66 @@ class ChatActor(
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.{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.services.Service
import net.psforever.objects.zones.exp
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
@ -225,8 +228,6 @@ class SessionAvatarHandlers(
case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
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) =>
// guid = victim // killer = killer
@ -398,6 +399,80 @@ class SessionAvatarHandlers(
case AvatarResponse.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) =>
sendResponse(msg)
@ -412,16 +487,35 @@ class SessionAvatarHandlers(
case AvatarResponse.Killed(mount) =>
//log and chat messages
val cause = player.LastDamage.flatMap { damage =>
damage.interaction.cause match {
case cause: ExplodingEntityReason if cause.entity.isInstanceOf[VehicleSpawnPad] =>
val interaction = damage.interaction
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 ..."
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate"))
case _ => ()
}
damage match {
case damage if damage.adversarial.nonEmpty => Some(damage.adversarial.get.attacker.Name)
case damage => Some(s"a ${damage.interaction.cause.getClass.getSimpleName}")
adversarial.collect {
case attacker
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)" }
log.info(s"${player.Name} has died, killed by $cause")
if (sessionData.shooting.shotsWhileDead > 0) {
@ -434,6 +528,7 @@ class SessionAvatarHandlers(
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
//player state changes
AvatarActor.updateToolDischargeFor(avatar)
player.FreeHand.Equipment.foreach { item =>
DropEquipmentFromInventory(player)(item)
}
@ -448,7 +543,6 @@ class SessionAvatarHandlers(
}
sessionData.playerActionsToCancel()
sessionData.terminals.CancelAllProximityUnits()
sessionData.zoning
AvatarActor.savePlayerLocation(player)
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.{ActorContext, ActorRef, Cancellable, typed}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
import scala.collection.mutable
@ -411,10 +411,10 @@ class SessionData(
/* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */
if (
(session.account.gm ||
(player.avatar.vehicle.contains(objectGuid) && vehicle.Owner.contains(player.GUID)) ||
(player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) ||
(player.Faction == vehicle.Faction &&
(vehicle.Definition.CanBeOwned.nonEmpty &&
(vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))) &&
(vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) &&
(vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied))
) {
vehicle.Actor ! Vehicle.Deconstruct()
@ -442,7 +442,7 @@ class SessionData(
}
case Some(obj: Deployable) =>
if (session.account.gm || obj.Owner.isEmpty || obj.Owner.contains(player.GUID) || obj.Destroyed) {
if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) {
obj.Actor ! Deployable.Deconstruct()
} else {
log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it")
@ -1377,7 +1377,7 @@ class SessionData(
//access to trunk
if (
obj.AccessingTrunk.isEmpty &&
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid
.contains(player.GUID))
) {
log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk")
@ -2458,9 +2458,9 @@ class SessionData(
src: PlanetSideGameObject with TelepadLike,
dest: PlanetSideGameObject with TelepadLike
): Unit = {
val time = System.nanoTime
val time = System.currentTimeMillis()
if (
time - recentTeleportAttempt > (2 seconds).toNanos && router.DeploymentState == DriveState.Deployed &&
time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
internalTelepad.Active &&
remoteTelepad.Active
) {
@ -2473,6 +2473,11 @@ class SessionData(
continent.id,
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 {
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
}

View file

@ -2,6 +2,9 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.InGameHistory
import scala.concurrent.duration._
//
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"
)
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")
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
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)
)
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}")
DismountAction(tplayer, obj, seatNum)
@ -276,7 +279,7 @@ class SessionMountHandlers(
* @param obj the mountable object
* @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 objGuid: PlanetSideGUID = obj.GUID
sessionData.playerActionsToCancel()
@ -295,7 +298,7 @@ class SessionMountHandlers(
* @param obj the mountable object
* @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)
//until vehicles maintain synchronized momentum without a driver
obj match {
@ -332,8 +335,9 @@ class SessionMountHandlers(
* @param obj the mountable object
* @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
tplayer.ContributionFrom(obj)
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
val bailType = if (tplayer.BailProtection) {
BailType.Bailed

View file

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

View file

@ -2,6 +2,8 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
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
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 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 val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
@ -157,7 +160,7 @@ private[support] class WeaponAndProjectileOperations(
fireStateStopPlayerMessages(item_guid)
case Some(_) =>
fireStateStopMountedMessages(item_guid)
case _ => ()
case _ =>
log.warn(s"ChangeFireState_Stop: can not find $item_guid")
}
sessionData.progressBarUpdate.cancel()
@ -275,8 +278,8 @@ private[support] class WeaponAndProjectileOperations(
val LongRangeProjectileInfoMessage(guid, _, _) = pkt
FindContainedWeapon match {
case (Some(_: Vehicle), weapons)
if weapons.exists { _.GUID == guid } => ; //now what?
case _ => ;
if weapons.exists { _.GUID == guid } => () //now what?
case _ => ()
}
}
@ -329,13 +332,11 @@ private[support] class WeaponAndProjectileOperations(
_: Vector3,
hitPos: Vector3
) =>
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos) match {
case Some(resprojectile) =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionData.handleDealingDamage(target, resprojectile)
}
case _ => ;
case _ => ()
}
case None =>
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 {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match {
case Some(resprojectile) =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionData.handleDealingDamage(target, resprojectile)
}
case _ => ;
case _ => ()
}
//other victims
targets.foreach(elem => {
sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match {
case Some(resprojectile) =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionData.handleDealingDamage(target, resprojectile)
}
case _ => ;
case _ => ()
}
})
//...
@ -413,7 +410,7 @@ private[support] class WeaponAndProjectileOperations(
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 {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos) match {
case Some(resprojectile) =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach {
resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
}
case _ => ;
case _ => ()
}
}
@ -524,7 +520,7 @@ private[support] class WeaponAndProjectileOperations(
obj match {
case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
turret.Actor ! FacilityTurret.WeaponDischarged()
case _ => ;
case _ => ()
}
} else {
log.warn(
@ -532,7 +528,7 @@ private[support] class WeaponAndProjectileOperations(
)
}
case _ => ;
case _ => ()
}
}
@ -552,7 +548,7 @@ private[support] class WeaponAndProjectileOperations(
case Some(v: Vehicle) =>
//assert subsystem states
v.SubsystemMessages().foreach { sendResponse }
case _ => ;
case _ => ()
}
}
if (enabledTools.nonEmpty) {
@ -577,8 +573,9 @@ private[support] class WeaponAndProjectileOperations(
avatarActor ! AvatarActor.ConsumeStamina(avatar.stamina)
}
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
tool.Discharge()
prefire += weaponGUID
ongoingShotsFired = ongoingShotsFired + tool.Discharge()
addShotsFired(tool.Definition.ObjectId, tool.AmmoSlot.Chamber)
}
(o, Some(tool))
}
@ -635,7 +632,7 @@ private[support] class WeaponAndProjectileOperations(
continent.GUID(weapon_guid) match {
case Some(tool: 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 = {
ModifyAmmunition(obj)(box, reloadValue)
obj.Find(box) match {
case Some(index) =>
continent.VehicleEvents ! VehicleServiceMessage(
s"${obj.Actor}",
VehicleAction.InventoryState(
player.GUID,
box,
obj.GUID,
index,
box.Definition.Packet.DetailedConstructorData(box).get
)
obj.Find(box).collect { index =>
continent.VehicleEvents ! VehicleServiceMessage(
s"${obj.Actor}",
VehicleAction.InventoryState(
player.GUID,
box,
obj.GUID,
index,
box.Definition.Packet.DetailedConstructorData(box).get
)
case None => ;
)
}
}
@ -849,7 +844,7 @@ private[support] class WeaponAndProjectileOperations(
sessionData.normalItemDrop(player, continent)(previousBox)
}
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 =>
modifyFunc(previousBox, 0) //update to changed capacity value
toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) })
@ -1151,7 +1146,6 @@ private[support] class WeaponAndProjectileOperations(
prefire -= itemGuid
shooting += itemGuid
shootingStart += itemGuid -> System.currentTimeMillis()
ongoingShotsFired = 0
}
private def fireStateStartChargeMode(tool: Tool): Unit = {
@ -1220,11 +1214,10 @@ private[support] class WeaponAndProjectileOperations(
used by ChangeFireStateMessage_Stop handling
*/
private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = {
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId, ongoingShotsFired, 0, 0))
tool.FireMode match {
case _: ChargeFireModeDefinition =>
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
case _ => ;
case _ => ()
}
if (tool.Magazine == 0) {
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 = {
if (player != null && player.HasGUID) {
(prefire ++ shooting).foreach { guid =>
@ -1373,6 +1409,7 @@ private[support] class WeaponAndProjectileOperations(
fireStateStopMountedMessages(guid)
}
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.util.Timeout
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.serverobject.mount.Seat
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, ReconstructionActivity, SpawningActivity}
import net.psforever.packet.game.{CampaignStatistic, MailMessage, SessionStatistic}
import scala.collection.mutable
import scala.concurrent.duration._
@ -69,6 +72,30 @@ import net.psforever.zones.Zones
object ZoningOperations {
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(
@ -258,7 +285,7 @@ class ZoningOperations(
obj.GUID,
Deployable.Icon(obj.Definition.Item),
obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0))
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
)
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
})
@ -1657,6 +1684,8 @@ class ZoningOperations(
private[support] var reviveTimer: Cancellable = Default.Cancellable
private[support] var respawnTimer: Cancellable = Default.Cancellable
private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields
/* packets */
def handleReleaseAvatarRequest(pkt: ReleaseAvatarRequestMessage): Unit = {
@ -1803,6 +1832,7 @@ class ZoningOperations(
sessionData.persistFunc = UpdatePersistence(from)
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
inZone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.TeardownConnection())
spawn.switchAvatarStatisticsFieldToRefreshAfterRespawn()
//find and reload previous player
(
inZone.Players.find(p => p.name.equals(name)),
@ -2342,6 +2372,7 @@ class ZoningOperations(
player.avatar = player.avatar.copy(stamina = avatar.maxStamina)
avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina)
avatarActor ! AvatarActor.ResetImplants()
zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
val obj = Player.Respawn(tplayer)
DefinitionUtil.applyDefaultLoadout(obj)
obj.death_by = tplayer.death_by
@ -2614,10 +2645,11 @@ class ZoningOperations(
LoadZoneAsPlayer(newPlayer, zoneId)
} else {
avatarActor ! AvatarActor.DeactivateActiveImplants()
val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(vehicle, toZoneNumber, betterSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneInVehicle(vehicle, pos, ori, zoneId)
case _ if player.HasGUID => // player is deconstructing self or instant action
@ -2629,13 +2661,13 @@ class ZoningOperations(
)
player.Position = pos
player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
case _ => //player is logging in
player.Position = pos
player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
}
}
@ -2803,10 +2835,7 @@ class ZoningOperations(
.foreach { case (_, building) =>
sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0 /*building.BuildingType == StructureType.Facility*/))
}
(0 to 30).foreach(_ => {
//TODO 30 for a new character only?
sendResponse(AvatarStatisticsMessage(DeathStatistic(0L)))
})
statisticsPacketFunc()
if (tplayer.ExoSuit == ExoSuitType.MAX) {
sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong))
}
@ -2823,7 +2852,7 @@ class ZoningOperations(
continent.DeployableList
.filter(_.OwnerName.contains(name))
.foreach(obj => {
obj.Owner = guid
obj.OwnerGuid = guid
drawDeloyableIcon(obj)
})
drawDeloyableIcon = DontRedrawIcons
@ -2831,7 +2860,7 @@ class ZoningOperations(
//assert or transfer vehicle ownership
continent.GUID(player.avatar.vehicle) match {
case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
vehicle.Owner = guid
vehicle.OwnerGuid = guid
continent.VehicleEvents ! VehicleServiceMessage(
s"${tplayer.Faction}",
VehicleAction.Ownership(guid, vehicle.GUID)
@ -2906,7 +2935,7 @@ class ZoningOperations(
val effortBy = nextSpawnPoint
.collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) }
.collect {
case (_, Some(v: Vehicle)) => continent.GUID(v.Owner)
case (_, Some(v: Vehicle)) => continent.GUID(v.OwnerGuid)
case (sp, Some(_: Building)) => Some(sp)
}
.collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) }
@ -2918,11 +2947,15 @@ class ZoningOperations(
SpawningActivity(PlayerSource(player), continent.Number, effortBy)
}
})
//ride
}
upstreamMessageCount = 0
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,
Deployable.Icon(obj.Definition.Item),
obj.Position,
obj.Owner.getOrElse(PlanetSideGUID(0))
obj.OwnerGuid.getOrElse(PlanetSideGUID(0))
)
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
}
@ -3164,6 +3197,64 @@ class ZoningOperations(
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.
* 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.sourcing.SourceEntry
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.persistence
@ -87,6 +87,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
private[this] val log = org.log4s.getLogger
private val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
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.init(context.toClassic)
@ -154,7 +155,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
experience ! ExperienceCalculator.RewardThisDeath(entity)
case RewardOurSupporters(target, history, kill, bep) =>
()
supportExperience ! SupportExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
case ZoneMapUpdate() =>
zone.Buildings
@ -162,7 +163,6 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
.values
.foreach(_.Actor ! BuildingActor.MapUpdate())
}
this
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -9847,6 +9847,7 @@ object GlobalDefinitions {
resource_silo.Damageable = false
resource_silo.Repairable = false
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.Damageable = false
@ -9856,7 +9857,7 @@ object GlobalDefinitions {
secondary_capture.Name = "secondary_capture"
secondary_capture.Damageable = 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.Damageable = false

View file

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

View file

@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.aura.AuraContainer
import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.serverobject.mount.MountableEntity
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.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -110,6 +110,7 @@ class Player(var avatar: Avatar)
Health = Definition.DefaultHealth
Armor = MaxArmor
Capacitor = 0
avatar.scorecard.respawn()
released = false
}
isAlive
@ -124,13 +125,16 @@ class Player(var avatar: Avatar)
def Revive: Boolean = {
Destroyed = false
Health = Definition.DefaultHealth
avatar.scorecard.revive()
released = false
true
}
def Release: Boolean = {
released = true
backpack = !isAlive
if (!released) {
released = true
backpack = !isAlive
}
true
}
@ -422,12 +426,13 @@ class Player(var avatar: Avatar)
def UsingSpecial_=(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
usingSpecial(state)
//noinspection ScalaUnusedSymbol
private def DefaultUsingSpecial(state: SpecialExoSuitDefinition.Mode.Value): SpecialExoSuitDefinition.Mode.Value =
SpecialExoSuitDefinition.Mode.Normal
private def UsingAnchorsOrOverdrive(
state: SpecialExoSuitDefinition.Mode.Value
): SpecialExoSuitDefinition.Mode.Value = {
state: SpecialExoSuitDefinition.Mode.Value
): SpecialExoSuitDefinition.Mode.Value = {
import SpecialExoSuitDefinition.Mode._
val curr = UsingSpecial
val next = if (curr == Normal) {
@ -532,11 +537,14 @@ class Player(var avatar: Avatar)
def Carrying: Option[SpecialCarry] = carrying
//noinspection ScalaUnusedSymbol
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
Carrying
Carrying_=(Some(item))
}
//noinspection ScalaUnusedSymbol
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
carrying = item
Carrying
}
@ -553,6 +561,14 @@ class Player(var avatar: Avatar)
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]
override def equals(other: Any): Boolean =
@ -614,12 +630,14 @@ object Player {
if (player.Release) {
val obj = new Player(player.avatar)
obj.Continent = player.Continent
obj.avatar.scorecard.respawn()
obj
} else {
player
}
}
//noinspection ScalaUnusedSymbol
def neverRestrict(player: Player, slot: Int): Boolean = {
false
}

View file

@ -10,6 +10,8 @@ import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.inventory.InventoryItem
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.packet.game._
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
@ -64,7 +66,7 @@ object Players {
val name = target.Name
val medicName = medic.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))
target.Zone.AvatarEvents ! AvatarServiceMessage(
medicName,

View file

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

View file

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

View file

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

View file

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

View file

@ -95,6 +95,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var previousFaction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var shields: Int = 0
private var decal: Int = 0
private var trunkAccess: Option[PlanetSideGUID] = None
@ -128,14 +129,18 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
}
def Faction: PlanetSideEmpire.Value = {
this.faction
}
override def Faction_=(faction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
this.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 */
def JackingDuration: Array[Int] = Definition.JackingDuration
@ -267,30 +272,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
}
def SeatPermissionGroup(seatNumber: Int): Option[AccessPermissionGroup.Value] = {
if (seatNumber == 0) { //valid in almost all cases
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
}
}
}
}
Vehicles.SeatPermissionGroup(this.Definition, seatNumber)
}
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
@ -476,7 +458,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
if (trunkAccess.isEmpty || trunkAccess.contains(player.GUID)) {
groupPermissions(3) match {
case VehicleLockState.Locked => //only the owner
Owner.isEmpty || (Owner.isDefined && player.GUID == Owner.get)
OwnerGuid.isEmpty || (OwnerGuid.isDefined && player.GUID == OwnerGuid.get)
case VehicleLockState.Group => //anyone in the owner's squad or platoon
faction == player.Faction //TODO this is not correct
case VehicleLockState.Empire => //anyone of the owner's faction
@ -518,7 +500,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
def PreviousGatingManifest(): Option[VehicleManifest] = previousVehicleGatingManifest
def DamageModel = Definition.asInstanceOf[DamageResistanceModel]
def DamageModel: DamageResistanceModel = Definition.asInstanceOf[DamageResistanceModel]
override def BailProtection_=(protect: Boolean): Boolean = {
!Definition.CanFly && super.BailProtection_=(protect)
@ -681,6 +663,6 @@ object Vehicle {
*/
def toString(obj: Vehicle): String = {
val occupancy = obj.Seats.values.count(seat => seat.isOccupied)
s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
s"${obj.Definition.Name}, owned by ${obj.OwnerGuid}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)"
}
}

View file

@ -2,6 +2,7 @@
package net.psforever.objects
import net.psforever.objects.ce.TelepadLike
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
@ -61,7 +62,7 @@ object Vehicles {
* `None`, otherwise
*/
def Disown(guid: PlanetSideGUID, vehicle: Vehicle): Option[Vehicle] =
vehicle.Zone.GUID(vehicle.Owner) match {
vehicle.Zone.GUID(vehicle.OwnerGuid) match {
case Some(player: Player) =>
if (player.avatar.vehicle.contains(guid)) {
player.avatar.vehicle = None
@ -127,7 +128,7 @@ object Vehicles {
*/
def Disown(player: Player, vehicle: Vehicle): Option[Vehicle] = {
val pguid = player.GUID
if (vehicle.Owner.contains(pguid)) {
if (vehicle.OwnerGuid.contains(pguid)) {
vehicle.AssignOwnership(None)
//vehicle.Zone.VehicleEvents ! VehicleServiceMessage(player.Name, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
val vguid = vehicle.GUID
@ -236,16 +237,14 @@ object Vehicles {
val zone = target.Zone
// Forcefully dismount any cargo
target.CargoHolds.foreach { case (_, cargoHold) =>
cargoHold.occupant match {
case Some(cargo: Vehicle) =>
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
case None => ;
cargoHold.occupant.collect {
cargo: Vehicle => cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed = false)
}
}
// Forcefully dismount all seated occupants from the vehicle
target.Seats.values.foreach(seat => {
seat.occupant match {
case Some(tplayer: Player) =>
seat.occupant.collect {
tplayer: Player =>
seat.unmount(tplayer)
tplayer.VehicleSeated = None
if (tplayer.HasGUID) {
@ -254,7 +253,6 @@ object Vehicles {
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.
@ -263,25 +261,17 @@ object Vehicles {
target.Actor ! Vehicle.Deconstruct()
} else { // Otherwise handle ownership transfer as normal
// Remove ownership of our current vehicle, if we have one
hacker.avatar.vehicle match {
case Some(guid: PlanetSideGUID) =>
zone.GUID(guid) match {
case Some(vehicle: Vehicle) =>
Vehicles.Disown(hacker, vehicle)
case _ => ;
}
case _ => ;
}
target.Owner match {
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 _ => ;
}
hacker.avatar.vehicle
.flatMap { guid => zone.GUID(guid) }
.collect { case vehicle: Vehicle =>
Vehicles.Disown(hacker, vehicle)
}
// Remove ownership of the vehicle from the previous player
target.OwnerGuid
.flatMap { guid => zone.GUID(guid) }
.collect { case tplayer: Player =>
Vehicles.Disown(tplayer, target)
}
// Now take ownership of the jacked vehicle
target.Actor ! CommonMessages.Hack(hacker, target)
target.Faction = hacker.Faction
@ -301,16 +291,15 @@ object Vehicles {
// If AMS is deployed, swap it to the new faction
target.Definition match {
case GlobalDefinitions.router =>
target.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util: Utility.InternalTelepad) =>
target.Utility(UtilityType.internal_router_telepad_deployable).collect {
case util: Utility.InternalTelepad =>
//"power cycle"
util.Actor ! TelepadLike.Deactivate(util)
util.Actor ! TelepadLike.Activate(util)
case _ => ;
}
case GlobalDefinitions.ams if target.DeploymentState == DriveState.Deployed =>
zone.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(zone)
case _ => ;
case _ => ()
}
}
@ -411,6 +400,7 @@ object Vehicles {
*
* @param vehicle the vehicle
*/
//noinspection ScalaUnusedSymbol
def BeforeUnloadVehicle(vehicle: Vehicle, zone: Zone): Unit = {
vehicle.Definition match {
case GlobalDefinitions.ams =>
@ -419,7 +409,7 @@ object Vehicles {
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case GlobalDefinitions.router =>
vehicle.Actor ! Deployment.TryUndeploy(DriveState.Undeploying)
case _ => ;
case _ => ()
}
}
@ -439,4 +429,49 @@ object Vehicles {
val turnAway = if (offset.x >= 0) -90f else 90f
(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.repair.Repairable
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.sourcing.{AmenitySource, PlayerSource}
import net.psforever.objects.vital.collision.CollisionReason
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
@ -356,6 +356,12 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}
case Terminal.TerminalMessage(_, msg, order) =>
lazy val terminalUsedAction = {
player.Zone.GUID(msg.terminal_guid).collect {
case t: Terminal =>
player.LogActivity(TerminalUsedActivity(AmenitySource(t), msg.transaction_type))
}
}
order match {
case Terminal.BuyExosuit(exosuit, subtype) =>
val result = setExoSuit(exosuit, subtype)
@ -366,6 +372,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
)
terminalUsedAction
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}")
@ -508,7 +515,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result=true)
)
case _ => assert(assertion=false, msg.toString)
terminalUsedAction
case _ =>
assert(assertion=false, msg.toString)
}
case Zone.Ground.ItemOnGround(item, _, _) =>

View file

@ -1,8 +1,8 @@
// Copyright (c) 2023 PSForever
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 {
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.vital.interaction.DamageResult
import net.psforever.objects.zones.exp.EquipmentUseContextWrapper
import org.joda.time.LocalDateTime
trait KDAStat {
@ -10,10 +11,29 @@ trait KDAStat {
val time: LocalDateTime = LocalDateTime.now()
}
final case class Kill(victim: PlayerSource, info: DamageResult, experienceEarned: Long) extends KDAStat
final case class Kill(
victim: PlayerSource,
info: DamageResult,
experienceEarned: Long
) extends KDAStat
final case class Assist(victim: PlayerSource, weapons: Seq[Int], damageInflictedPercentage: Float, experienceEarned: Long) extends KDAStat
final case class Assist(
victim: PlayerSource,
weapons: Seq[EquipmentUseContextWrapper],
damageInflictedPercentage: Float,
experienceEarned: Long
) extends KDAStat
final case class Death(assailant: Seq[PlayerSource], timeAlive: Long, bep: Long) extends KDAStat {
final case class Death(
assailant: Seq[PlayerSource],
timeAlive: Long,
bep: Long
) extends KDAStat {
def experienceEarned: Long = 0
}
final case class SupportActivity(
target: PlayerSource,
weapons: Seq[EquipmentUseContextWrapper],
experienceEarned: Long
) extends KDAStat

View file

@ -5,11 +5,15 @@ final case class Life(
kills: Seq[Kill],
assists: Seq[Assist],
death: Option[Death],
equipmentStats: Seq[EquipmentStat]
equipmentStats: Seq[EquipmentStat],
supportExperience: Long,
prior: Option[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 = {
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.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.types.{PlanetSideEmpire, StatisticalCategory}
import net.psforever.types.{PlanetSideEmpire, StatisticalCategory, StatisticalElement}
import scala.annotation.tailrec
import scala.collection.mutable
@ -18,43 +18,97 @@ class ScoreCard() {
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 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 {
case e: EquipmentStat =>
curr = ScoreCard.updateEquipmentStat(curr, e)
Nil
case k: Kill =>
curr = curr.copy(kills = k +: curr.kills)
curr = ScoreCard.updateEquipmentStat(curr, EquipmentStat(k.info.interaction.cause.attribution, 0, 0, 1))
ScoreCard.updateStatisticsFor(killStatistics, k.info.interaction.cause.attribution, k.victim.Faction)
//TODO may need to expand these to include other fields later
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 =>
curr = curr.copy(assists = a +: curr.assists)
val faction = a.victim.Faction
a.weapons.foreach { wid =>
ScoreCard.updateStatisticsFor(assistStatistics, wid, faction)
//TODO may need to expand these to include other fields later
a.weapons.map { weq =>
val wid = weq.equipment
(wid, ScoreCard.updateStatisticsFor(assistStatistics, wid, faction))
}
case d: Death =>
val expired = curr
curr = Life()
lives = expired.copy(death = Some(d)) +: lives
case _ => ;
curr = curr.copy(death = Some(d))
Nil
case value: Long =>
curr = curr.copy(supportExperience = curr.supportExperience + value)
Nil
case _ =>
Nil
}
}
}
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 = {
updateEquipmentStat(curr, entry, entry.objectId, entry.kills)
updateEquipmentStat(curr, entry, entry.objectId, entry.kills, entry.assists)
}
private def updateEquipmentStat(
curr: Life,
entry: EquipmentStat,
objectId: Int,
killCount: Int
killCount: Int,
assists: Int
): Life = {
curr.equipmentStats.indexWhere { a => a.objectId == objectId } match {
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
private def updateStatisticsFor(
statisticMap: mutable.HashMap[Int, Statistic],
@ -81,10 +158,10 @@ object ScoreCard {
statisticMap.get(objectId) match {
case Some(fields) =>
val outEntry = victimFaction match {
case PlanetSideEmpire.TR => fields.copy(tr_b = fields.tr_b + 1)
case PlanetSideEmpire.NC => fields.copy(nc_b = fields.nc_b + 1)
case PlanetSideEmpire.VS => fields.copy(vs_b = fields.vs_b + 1)
case PlanetSideEmpire.NEUTRAL => fields.copy(ps_b = fields.ps_b + 1)
case PlanetSideEmpire.TR => fields.copy(tr_s = fields.tr_s + 1)
case PlanetSideEmpire.NC => fields.copy(nc_s = fields.nc_s + 1)
case PlanetSideEmpire.VS => fields.copy(vs_s = fields.vs_s + 1)
case PlanetSideEmpire.NEUTRAL => fields.copy(ps_s = fields.ps_s + 1)
}
outEntry
case _ =>

View file

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

View file

@ -26,27 +26,8 @@ class VehicleDefinition(objectId: Int)
with VitalityDefinition
with NtuContainerDefinition
with ResistanceProfileMutators
with DamageResistanceModel {
/** ... */
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
with DamageResistanceModel
with WithShields {
private val cargo: mutable.HashMap[Int, CargoDefinition] = mutable.HashMap[Int, CargoDefinition]()
private var deployment: Boolean = false
private val utilities: mutable.HashMap[Int, UtilityType.Value] = mutable.HashMap()
@ -92,63 +73,6 @@ class VehicleDefinition(objectId: Int)
RepairRestoresAt = 1
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 CanBeOwned: Option[Boolean] = canBeOwned
@ -300,6 +224,7 @@ class VehicleDefinition(objectId: Int)
)
}
//noinspection ScalaUnusedSymbol
def Uninitialize(obj: Vehicle, context: ActorContext): Unit = {
obj.Actor ! akka.actor.PoisonPill
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,
v4 = None,
v5 = None,
obj.Owner match {
obj.OwnerGuid match {
case Some(owner) => owner
case None => PlanetSideGUID(0)
}

View file

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

View file

@ -17,7 +17,7 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() {
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(
CaptureFlagData(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,13 +5,15 @@ import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{Vehicle, Vehicles}
import net.psforever.objects.equipment.JammableUnit
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.interaction.DamageResult
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
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.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.DamageWithPositionMessage
import net.psforever.types.Vector3
import scala.concurrent.duration._
@ -213,6 +215,18 @@ trait DamageableVehicle
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
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
DamageableWeaponTurret.DestructionAwareness(obj, cause)

View file

@ -25,14 +25,14 @@ trait Hackable {
def HackedBy_=(agent: Option[Player]): Option[HackInfo] = {
(hackedBy, agent) match {
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)) =>
if (actor.Faction == this.Faction) {
//hack cleared
hackedBy = None
} else if (actor.Faction != info.hackerFaction) {
//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) =>
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.
* 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.
* The different game elements it simulates are:
* 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 faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var carrier: Option[Player] = None
private var lastTimeCollected: Long = System.currentTimeMillis()
def Target: Building = target
def Target_=(newTarget: Building): Building = {
@ -64,8 +65,11 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
def Carrier: Option[Player] = carrier
def Carrier_=(newCarrier: Option[Player]) : Option[Player] = {
carrier = newCarrier
lastTimeCollected = System.currentTimeMillis()
carrier
}
def LastCollectionTime: Long = carrier.map { _ => lastTimeCollected }.getOrElse { System.currentTimeMillis() }
}
object CaptureFlag {

View file

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

View file

@ -7,11 +7,13 @@ import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior}
import net.psforever.types.PlanetSideEmpire
import net.psforever.objects.zones
import net.psforever.objects.{GlobalDefinitions, Ntu, NtuContainer, NtuStorageBehavior, Vehicle}
import net.psforever.types.{ExperienceType, PlanetSideEmpire}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.util.Config
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@ -181,6 +183,24 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
if (amount != 0) {
panelAnimationFunc(sender, amount)
panelAnimationFunc = SkipPanelAnimation
(src match {
case v: Vehicle => Some(v)
case _ => None
})
.map { v => (v, v.Owners) }
.collect { case (vehicle, Some(owner)) =>
//experience is reported as normal
val deposit: Long =
(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;
* if negative or zero, disable the animation
*/
//noinspection ScalaUnusedSymbol
def PanelAnimation(source: ActorRef, trigger: Float): Unit = {
val currentlyHas = resourceSilo.NtuCapacitor
// do not let the trigger charge go to waste, but also do not let the silo be filled

View file

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

View file

@ -16,11 +16,10 @@ import net.psforever.types._
import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._
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 scala.collection.mutable
import java.util.concurrent.TimeUnit
import scala.concurrent.duration._
class Building(
private val name: String,
@ -34,9 +33,8 @@ class Building(
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var playersInSOI: List[Player] = List.empty
private val capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
private var forceDomeActive: Boolean = false
private var participationFunc: Building.ParticipationLogic = Building.NoParticipation
private var participationFunc: ParticipationLogic = NoParticipation
super.Zone_=(zone)
super.GUID_=(PlanetSideGUID(building_guid)) //set
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 IsCapitol: Boolean = capitols.contains(name)
def IsCapitol: Boolean = Building.Capitols.contains(name)
def IsSubCapitol: Boolean = {
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
}
}
@ -85,13 +84,11 @@ class Building(
box.Actor ! Painbox.Stop()
}
}
participationFunc.Players(building = this, list)
playersInSOI = list
participationFunc.TryUpdate()
playersInSOI
}
def PlayerContribution: Map[Player, Long] = participationFunc.Contribution()
// Get all lattice neighbours
def AllNeighbours: Option[Set[Building]] = {
zone.Lattice find this match {
@ -186,8 +183,7 @@ class Building(
case Some(obj: CaptureTerminal with Hackable) =>
obj.HackedBy match {
case Some(Hackable.HackInfo(p, _, start, length)) =>
val hack_time_remaining_ms =
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
val hack_time_remaining_ms = math.max(0, start + length - System.currentTimeMillis())
(true, p.Faction, hack_time_remaining_ms)
case _ =>
(false, PlanetSideEmpire.NEUTRAL, 0L)
@ -360,43 +356,24 @@ class Building(
override def Amenities_=(obj: Amenity): List[Amenity] = {
obj match {
case _: CaptureTerminal => participationFunc = Building.FacilityHackParticipation
case _ => ;
case _: CaptureTerminal =>
if (buildingType == StructureType.Facility) {
participationFunc = MajorFacilityHackParticipation(this)
} else if (buildingType == StructureType.Tower) {
participationFunc = TowerHackParticipation(this)
}
case _ => ()
}
super.Amenities_=(obj)
}
def Participation: ParticipationLogic = participationFunc
def Definition: BuildingDefinition = buildingDefinition
}
object Building {
trait ParticipationLogic {
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 Capitols = List("Thoth", "Voltan", "Neit", "Anguta", "Eisa", "Verica")
final val NoBuilding: 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.repair.{AmenityAutoRepair, RepairableAmenity}
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.packet.game.InventoryStateMessage
import net.psforever.services.Service
@ -291,7 +291,7 @@ object ProximityTerminalControl {
healAmount
}
target.Health = health + finalHealthAmount
target.LogActivity(HealFromTerm(AmenitySource(terminal), finalHealthAmount))
target.LogActivity(HealFromTerminal(AmenitySource(terminal), finalHealthAmount))
updateFunc(target)
target.Health == maxHealth
} else {
@ -338,7 +338,7 @@ object ProximityTerminalControl {
repairAmount
}
target.Armor = armor + finalRepairAmount
target.LogActivity(RepairFromTerm(AmenitySource(terminal), finalRepairAmount))
target.LogActivity(RepairFromTerminal(AmenitySource(terminal), finalRepairAmount))
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,

View file

@ -101,7 +101,7 @@ class FacilityTurretControl(turret: FacilityTurret)
turret.ControlledWeapon(wepNumber = 1).foreach {
case weapon: Tool =>
// 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
val seat = turret.Seat(0).get
seat.occupant match {

View file

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

View file

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

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.avatar.scoring.Life
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.Mountable
@ -28,7 +29,7 @@ final case class PlayerSource(
jumping: Boolean,
Modifiers: ResistanceProfile,
bep: Long,
kills: Seq[Any],
progress: Life,
unique: UniquePlayer
) extends SourceWithHealthEntry {
override def Name: String = unique.name
@ -46,6 +47,7 @@ object PlayerSource {
val exosuit = p.ExoSuit
val faction = p.Faction
val seatedEntity = mountableAndSeat(p)
val avatar = p.avatar
PlayerSource(
p.Definition,
exosuit,
@ -58,13 +60,17 @@ object PlayerSource {
p.Crouching,
p.Jumping,
ExoSuitDefinition.Select(exosuit, faction),
p.avatar.bep,
kills = Nil,
avatar.bep,
progress = avatar.scorecard.CurrentLife,
UniquePlayer(p.CharId, p.Name, p.Sex, faction)
)
}
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(
GlobalDefinitions.avatar,
ExoSuitType.Standard,
@ -78,8 +84,8 @@ object PlayerSource {
jumping = false,
GlobalDefinitions.Standard,
bep = 0L,
kills = Nil,
UniquePlayer(0L, name, CharacterSex.Male, faction)
progress = tokenLife,
unique
)
}
@ -116,6 +122,7 @@ object PlayerSource {
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
val exosuit = player.ExoSuit
val faction = player.Faction
val avatar = player.avatar
PlayerSource(
player.Definition,
exosuit,
@ -128,12 +135,25 @@ object PlayerSource {
player.Crouching,
player.Jumping,
ExoSuitDefinition.Select(exosuit, faction),
player.avatar.bep,
kills = Nil,
avatar.bep,
progress = tokenLife,
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
* 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.
*/
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,
Velocity: Option[Vector3],
deployed: DriveState.Value,
owner: Option[UniquePlayer],
occupants: List[SourceEntry],
Modifiers: ResistanceProfile,
unique: UniqueVehicle
) extends SourceWithHealthEntry with SourceWithShieldsEntry {
def Name: String = SourceEntry.NameFormat(Definition.Name)
def Health: Int = health
def Shields: Int = shields
def total: Int = health + shields
def Name: String = SourceEntry.NameFormat(Definition.Name)
def Health: Int = health
def Shields: Int = shields
def total: Int = health + shields
}
object VehicleSource {
@ -42,6 +43,7 @@ object VehicleSource {
obj.Orientation,
obj.Velocity,
obj.DeploymentState,
None,
Nil,
obj.Definition.asInstanceOf[ResistanceProfile],
UniqueVehicle(
@ -52,13 +54,15 @@ object VehicleSource {
obj.OriginalOwnerName.getOrElse("none")
)
)
vehicle.copy(occupants = {
obj.Seats.map { case (seatNumber, seat) =>
//shallow information that references the existing source entry
vehicle.copy(
owner = obj.Owners,
occupants = obj.Seats.map { case (seatNumber, seat) =>
seat.occupant match {
case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber) //shallow
case Some(p) => PlayerSource.inSeat(p, vehicle, seatNumber)
case _ => PlayerSource.Nobody
}
}.toList
})
)
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vehicles
import akka.actor.ActorRef
import akka.actor.{ActorRef, Cancellable}
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.BuildingActor
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.vehicle.{VehicleAction, VehicleServiceMessage}
import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.serverobject.transfer.TransferContainer.TransferMaterial
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
var panelAnimationFunc: () => Unit = NoCharge
var ntuChargingTick = Default.Cancellable
var ntuChargingTick: Cancellable = Default.Cancellable
findChargeTargetFunc = Vehicles.FindANTChargingSource
findDischargeTargetFunc = Vehicles.FindANTDischargingTarget
def TransferMaterial = Ntu.Nanites
def TransferMaterial: TransferMaterial = Ntu.Nanites
def ChargeTransferObject: Vehicle with NtuContainer
@ -186,8 +187,10 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
val chargeToDeposit = if (min == 0) {
transferTarget match {
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(silo.MaxNtuCapacitor / 105, chargeable.NtuCapacitor), max)
scala.math.min(
scala.math.min(silo.MaxNtuCapacitor / silo.Definition.ChargeTime.toMillis.toFloat, chargeable.NtuCapacitor),
max
)
case _ =>
0
}

View file

@ -5,12 +5,9 @@ import akka.actor.{Actor, Cancellable}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.zones.Zone
import net.psforever.objects._
import net.psforever.packet.game.{
CargoMountPointStatusMessage,
ObjectAttachMessage,
ObjectDetachMessage,
PlanetsideAttributeMessage
}
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.objects.vital.VehicleCargoMountActivity
import net.psforever.packet.game.{CargoMountPointStatusMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage}
import net.psforever.types.{BailType, CargoStatus, PlanetSideGUID, Vector3}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.Service
@ -35,14 +32,12 @@ trait CarrierBehavior {
cargoDismountTimer.cancel()
val obj = CarrierObject
val zone = obj.Zone
zone.GUID(isMounting) match {
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
case _ => ;
zone.GUID(isMounting).collect {
case v : Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
}
isMounting = None
zone.GUID(isDismounting) match {
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
case _ => ;
zone.GUID(isDismounting).collect {
case v : Vehicle => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
}
isDismounting = None
}
@ -89,9 +84,8 @@ trait CarrierBehavior {
)
}
else {
obj.Zone.GUID(isMounting) match {
case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
case _ => ;
obj.Zone.GUID(isMounting).collect {
case v: Vehicle => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
}
isMounting = None
}
@ -115,10 +109,9 @@ trait CarrierBehavior {
kicked = false
)
case _ =>
obj.CargoHold(mountPoint) match {
case Some(hold) if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
hold.unmount(hold.occupant.get)
case _ => ;
obj.CargoHold(mountPoint).collect {
case hold if hold.isOccupied && hold.occupant.get.GUID == cargo_guid =>
CarrierBehavior.CargoDismountAction(obj, hold.occupant.get, hold, BailType.Normal)
}
false
}
@ -135,17 +128,15 @@ trait CarrierBehavior {
CarrierBehavior.CheckCargoDismount(cargo_guid, mountPoint, iteration + 1, bailed)
)
} else {
zone.GUID(isDismounting.getOrElse(cargo_guid)) match {
case Some(cargo: Vehicle) =>
zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
case cargo: Vehicle =>
cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
case _ => ;
}
isDismounting = None
}
} else {
zone.GUID(isDismounting.getOrElse(cargo_guid)) match {
case Some(cargo: Vehicle) => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
case _ => ;
zone.GUID(isDismounting.getOrElse(cargo_guid)).collect {
case cargo: Vehicle => cargo.Actor ! CargoBehavior.EndCargoDismounting(guid)
}
isDismounting = None
}
@ -213,10 +204,8 @@ object CarrierBehavior {
if (distance <= 64) {
//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")
hold.mount(cargo)
cargo.MountedIn = carrierGUID
CargoMountAction(carrier, cargo, hold, carrierGUID)
cargo.Velocity = None
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
zone.VehicleEvents ! VehicleServiceMessage(
s"${cargo.Actor}",
VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health))
@ -358,9 +347,7 @@ object CarrierBehavior {
//obviously, don't do this
} else if (iteration > 40) {
//cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold
hold.mount(cargo)
cargo.MountedIn = carrierGUID
cargo.Actor ! CargoBehavior.EndCargoMounting(carrierGUID)
CargoMountAction(carrier, cargo, hold, carrierGUID)
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
zone.actor ! ZoneActor.RemoveFromBlockMap(cargo)
false
@ -441,9 +428,10 @@ object CarrierBehavior {
val zone = carrier.Zone
carrier.CargoHolds.find({ case (_, hold) => hold.occupant.contains(cargo) }) match {
case Some((mountPoint, hold)) =>
cargo.MountedIn = None
hold.unmount(
CarrierBehavior.CargoDismountAction(
carrier,
cargo,
hold,
if (bailed) BailType.Bailed else if (kicked) BailType.Kicked else BailType.Normal
)
val driverOpt = cargo.Seats(0).occupant
@ -532,7 +520,7 @@ object CarrierBehavior {
targetGUID: PlanetSideGUID
): Unit = {
target match {
case Some(_: Vehicle) => ;
case Some(_: 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")
}
@ -653,4 +641,62 @@ object CarrierBehavior {
)
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)
}
override def dismountCleanup(seatBeingDismounted: Int): Unit = {
super.dismountCleanup(seatBeingDismounted)
override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = {
super.dismountCleanup(seatBeingDismounted, player)
if (!vehicle.Seats.values.exists(_.isOccupied)) {
vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
case Some(subsys) =>

View file

@ -34,9 +34,9 @@ class DeployingVehicleControl(vehicle: Vehicle)
case msg : Deployment.TryUndeploy =>
deployBehavior.apply(msg)
case msg @ Mountable.TryDismount(_, seat_num, _) =>
case msg @ Mountable.TryDismount(player, seat_num, _) =>
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.repair.RepairableVehicle
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.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.etc.SuicideReason
import net.psforever.objects.zones._
@ -128,9 +128,9 @@ class VehicleControl(vehicle: Vehicle)
mountBehavior.apply(msg)
mountCleanup(mount_point, player)
case msg @ Mountable.TryDismount(_, seat_num, _) =>
case msg @ Mountable.TryDismount(player, seat_num, _) =>
dismountBehavior.apply(msg)
dismountCleanup(seat_num)
dismountCleanup(seat_num, player)
case CommonMessages.ChargeShields(amount, motivator) =>
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) })
@ -237,9 +237,9 @@ class VehicleControl(vehicle: Vehicle)
def commonDisabledBehavior: Receive = checkBehavior
.orElse {
case msg @ Mountable.TryDismount(_, seat_num, _) =>
case msg @ Mountable.TryDismount(user, seat_num, _) =>
dismountBehavior.apply(msg)
dismountCleanup(seat_num)
dismountCleanup(seat_num, user)
case Vehicle.Deconstruct(time) =>
time match {
@ -286,7 +286,7 @@ class VehicleControl(vehicle: Vehicle)
val seatGroup = vehicle.SeatPermissionGroup(seatNumber).getOrElse(AccessPermissionGroup.Passenger)
val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire)
(if (seatGroup == AccessPermissionGroup.Driver) {
vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked
vehicle.OwnerGuid.contains(user.GUID) || vehicle.OwnerGuid.isEmpty || permission != VehicleLockState.Locked
} else {
permission != VehicleLockState.Locked
}) &&
@ -297,6 +297,8 @@ class VehicleControl(vehicle: Vehicle)
val obj = MountableObject
obj.PassengerInSeat(user) match {
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 (seatNumber == 0 && !obj.OwnerName.contains(user.Name) && obj.Definition.CanBeOwned.nonEmpty) {
//whatever vehicle was previously owned
@ -325,7 +327,7 @@ class VehicleControl(vehicle: Vehicle)
vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user)
}
def dismountCleanup(seatBeingDismounted: Int): Unit = {
def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = {
val obj = MountableObject
// Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
if (!obj.Seats(0).isOccupied) {
@ -340,6 +342,7 @@ class VehicleControl(vehicle: Vehicle)
)
}
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
val canBeOwned = obj.Definition.CanBeOwned
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?
if (!decaying &&
obj.Definition.undergoesDecay &&
obj.Owner.isEmpty &&
obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) {
decaying = true
decayTimer = context.system.scheduler.scheduleOnce(
@ -418,7 +421,7 @@ class VehicleControl(vehicle: Vehicle)
Vehicles.Disown(obj.GUID, obj)
if (!decaying &&
obj.Definition.undergoesDecay &&
obj.Owner.isEmpty &&
obj.OwnerGuid.isEmpty &&
obj.Seats.values.forall(!_.isOccupied)) {
decaying = true
decayTimer = context.system.scheduler.scheduleOnce(
@ -890,7 +893,7 @@ object VehicleControl {
/**
* 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
* @return `true`, if the shield charge would be blocked;
* `false`, otherwise

View file

@ -4,12 +4,15 @@ package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
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.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
import net.psforever.util.Config
import scala.collection.mutable
/* root */
@ -18,7 +21,21 @@ import net.psforever.types.{ExoSuitType, ImplantType, TransactionType}
* Must keep track of the time (ms) the activity occurred.
*/
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 */
@ -28,13 +45,64 @@ trait 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 */
@ -65,9 +133,8 @@ trait DamagingActivity extends VitalsActivity {
def health: Int = {
(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 _ => 0
case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.health - pa.health
case _ => 0
}
}
}
@ -76,9 +143,9 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
extends HealingActivity
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
final case class HealFromImplant(implant: ImplantType, amount: Int)
@ -91,9 +158,9 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
extends RepairingActivity()
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)
extends RepairingActivity
@ -158,11 +225,34 @@ trait InGameHistory {
/**
* An in-game event must be recorded.
* Add new entry to the list (for recent activity).
* Special handling must be conducted for certain events.
* @param action the fully-informed entry
* @return the list of previous changes to this entity
*/
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
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) =>
history = history :+ act
case None => ()
@ -188,7 +278,7 @@ trait InGameHistory {
LogActivity(DamageFromPainbox(result))
case _: EnvironmentReason =>
LogActivity(DamageFromEnvironment(result))
case _ => ;
case _ =>
LogActivity(DamageFrom(result))
if(result.adversarial.nonEmpty) {
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] = {
lastDamage = None
val out = history
history = List.empty
contributionInheritance.clear()
out
}
}
@ -221,14 +356,14 @@ object InGameHistory {
def SpawnReconstructionActivity(
obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
zoneNumber: Int,
unit: Option[SourceEntry]
unit: Option[PlanetSideGameObject with FactionAffinity with InGameHistory]
): Unit = {
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists {
_.isInstanceOf[SpawningActivity]
}) {
ReconstructionActivity(ObjectSource(obj), zoneNumber, unit)
val toUnitSource = unit.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
val event: GeneralActivity = if (obj.History.isEmpty) {
SpawningActivity(SourceEntry(obj), zoneNumber, toUnitSource)
} else {
SpawningActivity(ObjectSource(obj), zoneNumber, unit)
ReconstructionActivity(SourceEntry(obj), zoneNumber, toUnitSource)
}
if (obj.History.lastOption match {
case Some(evt: SpawningActivity) => evt != event
@ -236,6 +371,13 @@ object InGameHistory {
case _ => true
}) {
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
*/
def Duration_=(time: FiniteDuration): FiniteDuration = {
Duration_=(time.toNanos)
Duration_=(time.toMillis)
}
/**
@ -112,8 +112,8 @@ class ActivityReport {
* @return the time
*/
def Duration_=(time: Long): FiniteDuration = {
if (time > duration.toNanos) {
duration = FiniteDuration(time, "nanoseconds")
if (time > duration.toMillis) {
duration = FiniteDuration(time, "milliseconds")
Renew
}
Duration
@ -177,7 +177,7 @@ class ActivityReport {
* @return the current time
*/
def Renew: Long = {
val t = System.nanoTime
val t = System.currentTimeMillis()
firstReport = firstReport.orElse(Some(t))
lastReport = Some(t)
t
@ -191,6 +191,6 @@ class ActivityReport {
heat = 0
firstReport = 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 noPriorHotSpots = hotspots.isEmpty
val duration = zone.HotSpotTimeFunction(defender, attacker)
if (duration.toNanos > 0) {
if (duration.toMillis > 0) {
val hotspot = TryHotSpot(zone.HotSpotCoordinateFunction(location))
trace(
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() =>
blanking.cancel()
val curr: Long = System.nanoTime
val curr: Long = System.currentTimeMillis()
//blanking dated activity reports
val changed = hotspots.flatMap(spot => {
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
(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.{Behavior, SupervisorStrategy}
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.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, InGameActivity, InGameHistory, ReconstructionActivity, RepairFromExoSuitChange, RepairingActivity, SpawningActivity}
import net.psforever.objects.vital.interaction.{Adversarial, DamageResult}
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.vital.interaction.DamageResult
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 {
def apply(zone: Zone): Behavior[Command] =
@ -32,38 +26,6 @@ object ExperienceCalculator {
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)
@ -74,263 +36,10 @@ class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command],
def onMessage(msg: Command): Behavior[Command] = {
msg match {
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
rewardThisPlayerDeath(
victim,
lastDamage,
limitHistoryToThisLife(history.toList)
)
case _ =>
()
KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
case _ => ()
}
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
sealed trait ItemUseStats {
def equipment: EquipmentUseContextWrapper
def shots: Int
def time: Long
def contributions: Float
}
private case class WeaponStats(
weapon_id: Int,
equipment: EquipmentUseContextWrapper,
amount: Int,
shots: Int,
time: Long
)
time: Long,
contributions: Float
) extends ItemUseStats
private case class ContributionStats(
player: PlayerSource,
weapons: Seq[WeaponStats],
amount: Int,
totalDamage: Int,
shots: Int,
time: Long
)
private case class EquipmentStats(
equipment: EquipmentUseContextWrapper,
shots: Int,
time: Long,
contributions: Float
) extends ItemUseStats
private[exp] case class ContributionStats(
player: PlayerSource,
weapons: Seq[WeaponStats],
amount: Int,
total: Int,
shots: Int,
time: Long
)
sealed case class ContributionStatsOutput(
player: PlayerSource,
implements: Seq[Int],
implements: Seq[EquipmentUseContextWrapper],
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
*/
sealed abstract class Statistic(val code: Int)
/**
* 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;
* organized as TR, NC, VS, BO (PS)
*/
final case class InitStatistic(
final case class CampaignStatistic(
category: StatisticalCategory,
unk: StatisticalElement,
fields: List[Long]
) 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
@ -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;
* organized as TR, NC, VS, BO (PS)
*/
final case class UpdateStatistic(
final case class SessionStatistic(
category: StatisticalCategory,
unk: StatisticalElement,
fields: List[Long]
) 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
@ -103,10 +116,10 @@ object AvatarStatisticsMessage extends Marshallable[AvatarStatisticsMessage] {
*/
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"))
}
)
@ -115,10 +128,10 @@ object AvatarStatisticsMessage extends Marshallable[AvatarStatisticsMessage] {
*/
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"))
}
)

View file

@ -15,7 +15,8 @@ import scodec.codecs._
* 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 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 {
type Packet = MailMessage

View file

@ -12,6 +12,8 @@ case class Account(
inactive: Boolean = false,
gm: Boolean = false,
lastFactionId: Int = 3,
avatarLoggedIn: Long,
sessionId: Long,
token: Option[String],
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
.orElse {
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))) {
InitialJob(entry)
if (entry.duration == 0) {
@ -120,7 +120,7 @@ abstract class RemoverActor() extends SupportActor[RemoverActor.Entry] {
case RemoverActor.StartDelete() =>
firstTask.cancel()
secondTask.cancel()
val now: Long = System.nanoTime
val now: Long = System.currentTimeMillis()
val (in, out) = firstHeap.partition(entry => {
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
*/
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.
* 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);
* defaults to the current time (in nanoseconds)
* @param now the time (in milliseconds);
* defaults to the current time (in milliseconds)
*/
def RetimeFirstTask(now: Long = System.nanoTime): Unit = {
def RetimeFirstTask(now: Long = System.currentTimeMillis()): Unit = {
firstTask.cancel()
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
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.
* 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
/**
* Default time for entries waiting in the second list.
* 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
@ -322,7 +322,7 @@ object RemoverActor extends SupportActorCaseConversions {
* Internally, all entries have a "time created" field.
* @param _obj the target
* @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)
extends SupportActor.Entry(_obj, _zone, _duration)
@ -332,7 +332,7 @@ object RemoverActor extends SupportActorCaseConversions {
* @see `FirstStandardDuration`
* @param obj the target
* @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
*/
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

View file

@ -12,7 +12,7 @@ import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket
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
@ -156,6 +156,9 @@ object AvatarAction {
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) 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 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.game.objectcreate.ConstructorData
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
final case class AvatarServiceResponse(
@ -127,4 +127,7 @@ object AvatarResponse {
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) 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) {
val short_timeout: FiniteDuration =
math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)) nanoseconds
val short_timeout: FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - openDoors.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global
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`,
* processing in order is always correct.
* @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`
* @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,
* and a `List` of elements that still satisfy the time limit.
* @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;
* defaults to 0
* @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.terminals.capture.CaptureTerminal
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.objects.sourcing.PlayerSource
import net.psforever.objects.zones.ZoneHotSpotProjector
import net.psforever.services.Service
import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
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.util.{Random, Success}
import scala.util.Random
/**
* 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
target.HackedBy match {
case Some(hackInfo) =>
target.HackedBy = hackInfo.Duration(duration.toNanos)
target.HackedBy = hackInfo.Duration(duration.toMillis)
case None =>
log.error(s"Initial $target hack information is missing")
}
@ -59,8 +56,8 @@ class HackCaptureActor extends Actor {
case HackCaptureActor.ProcessCompleteHacks() =>
log.trace("Processing complete hacks")
clearTrigger.cancel()
val now: Long = System.nanoTime
val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toNanos)
val now: Long = System.currentTimeMillis()
val (stillHacked, finishedHacks) = hackedObjects.partition(x => now - x.hack_timestamp < x.duration.toMillis)
hackedObjects = stillHacked
finishedHacks.foreach { entry =>
val terminal = entry.target
@ -79,14 +76,9 @@ class HackCaptureActor extends Actor {
case _ =>
// 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)
HackCaptureActor.RewardFacilityCaptureParticipants(
building,
terminal,
hacker,
now - entry.hack_timestamp,
isResecured = false
)
building.Participation.RewardFacilityCapture(terminal.Faction, hackedByFaction, hacker, hackTime, hackTime, isResecured = false)
}
}
// 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) =>
val (results, remainder) = hackedObjects.partition(x => x.target eq target)
val faction = GetHackingFaction(target).getOrElse(target.Faction)
target.HackedBy = None
hackedObjects = remainder
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)
}
NotifyHackStateChange(target, isResecured = true)
// HackCaptureActor.RewardFacilityCaptureParticipants(
// building,
// target,
// hacker,
// System.currentTimeMillis() - results.head.hack_timestamp,
// isResecured = true
// )
building.Participation.RewardFacilityCapture(target.Faction, faction, hacker, target.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - results.head.hack_timestamp, isResecured = true)
// Restart the timer in case the object we just removed was the next one scheduled
RestartTimer()
@ -124,13 +111,7 @@ class HackCaptureActor extends Actor {
val hackedByFaction = hackInfo.hackerFaction
hackedObjects = hackedObjects.filterNot(x => x == entry)
HackCompleted(terminal, hackedByFaction)
// HackCaptureActor.RewardFacilityCaptureParticipants(
// building,
// terminal,
// hacker,
// System.currentTimeMillis() - entry.hack_timestamp,
// isResecured = false
// )
building.Participation.RewardFacilityCapture(terminal.Faction, hacker.Faction, hacker, terminal.Definition.FacilityHackTime.toMillis, System.currentTimeMillis() - entry.hack_timestamp, isResecured = false)
entry.target.Actor ! CommonMessages.ClearHack()
flag.Zone.LocalEvents ! CaptureFlagManager.Captured(flag)
// 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 _ => None
}) match {
case None =>
//not an error; this is just not a ctf facility
false
case Some((owner, None, Some(neighbours))) if neighbours.nonEmpty =>
log.info(s"An LLU is being spawned for facility ${owner.Name} by $hackingFaction")
spawnCaptureFlag(neighbours, terminal, hackingFaction)
@ -231,9 +215,8 @@ class HackCaptureActor extends Actor {
private def RestartTimer(): Unit = {
if (hackedObjects.nonEmpty) {
val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.nanoTime()))
val short_timeout: FiniteDuration =
math.max(1, hackEntry.duration.toNanos - (System.nanoTime - hackEntry.hack_timestamp)).nanoseconds
val hackEntry = hackedObjects.reduceLeft(HackCaptureActor.minTimeLeft(System.currentTimeMillis()))
val short_timeout: FiniteDuration = math.max(1, hackEntry.duration.toMillis - (System.currentTimeMillis() - hackEntry.hack_timestamp)).milliseconds
log.trace(s"RestartTimer: still items left in hacked objects list. Checking again in ${short_timeout.toSeconds} seconds")
import scala.concurrent.ExecutionContext.Implicits.global
clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks())
@ -247,7 +230,7 @@ object HackCaptureActor {
zone: Zone,
unk1: Long,
unk2: Long,
startTime: Long = System.nanoTime()
startTime: Long = System.currentTimeMillis()
)
final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource)
@ -274,8 +257,7 @@ object HackCaptureActor {
17039360L
case Some(Hackable.HackInfo(p, _, start, length)) =>
// See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated
val hackTimeRemainingMS =
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
val hackTimeRemainingMS = math.max(0, start + length - System.currentTimeMillis())
val startNum = p.Faction match {
case PlanetSideEmpire.TR => 0x10000
case PlanetSideEmpire.NC => 0x20000
@ -291,149 +273,12 @@ object HackCaptureActor {
entry1: HackCaptureActor.HackEntry,
entry2: HackCaptureActor.HackEntry
): HackCaptureActor.HackEntry = {
val entry1TimeLeft = entry1.duration.toNanos - (now - entry1.hack_timestamp)
val entry2TimeLeft = entry2.duration.toNanos - (now - entry2.hack_timestamp)
val entry1TimeLeft = entry1.duration.toMillis - (now - entry1.hack_timestamp)
val entry2TimeLeft = entry2.duration.toMillis - (now - entry2.hack_timestamp)
if (entry1TimeLeft < entry2TimeLeft) {
entry1
} else {
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