Merge pull request #1138 from Fate-JH/exp-for-kda-3

Blood Money
This commit is contained in:
Fate-JH 2023-11-28 20:05:19 -05:00 committed by GitHub
commit 6bf58a7738
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 6709 additions and 1321 deletions

View file

@ -0,0 +1,95 @@
/* 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;
/* New */
ALTER TABLE "progressiondebt"
ADD COLUMN IF NOT EXISTS "max_experience" INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "enroll_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS "clear_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
/*
Upon indoctrinating a player into the progression system,
update the peak experience for the battle rank for future reference
and record when the player asked for this enhanced rank promotion.
*/
CREATE OR REPLACE FUNCTION fn_progressiondebt_updateEnrollment()
RETURNS TRIGGER
AS
$$
DECLARE avatarId Int;
DECLARE oldExp Int;
DECLARE newExp Int;
BEGIN
avatarId := NEW.avatar_id;
newExp := NEW.experience;
oldExp := OLD.experience;
BEGIN
IF (oldExp = 0 AND newExp > 0) THEN
UPDATE progressiondebt
SET experience = newExp, max_experience = newExp, enroll_time = CURRENT_TIMESTAMP
WHERE avatar_id = avatarId;
END IF;
END;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER psf_progressiondebt_updateEnrollment
BEFORE UPDATE
ON progressiondebt
FOR EACH ROW
EXECUTE PROCEDURE fn_progressiondebt_updateEnrollment();
/*
Upon unlisting a player from the progression system,
update the time when the player has completed his tensure.
*/
CREATE OR REPLACE FUNCTION fn_progressiondebt_updateClearTime()
RETURNS TRIGGER
AS
$$
DECLARE avatarId Int;
DECLARE oldExp Int;
DECLARE newExp Int;
DECLARE newMaxExp Int;
BEGIN
avatarId := NEW.avatar_id;
newExp := NEW.experience;
oldExp := OLD.experience;
newMaxExp := NEW.max_experience;
BEGIN
IF (oldExp > newExp AND newExp = 0) THEN
UPDATE progressiondebt
SET experience = 0, max_experience = newMaxExp, clear_time = CURRENT_TIMESTAMP
WHERE avatar_id = avatarId;
END IF;
END;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER psf_progressiondebt_updateClearTime
BEFORE UPDATE
ON progressiondebt
FOR EACH ROW
EXECUTE PROCEDURE fn_progressiondebt_updateClearTime();

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,192 @@ 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-min - lower limit of use count
# - minimum amount of shots required before applying multiplier
# shots-max - 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
# shots-multiplier - whether use count matters for this event
# - when set to 0.0 (default), it does not
# 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-max = 100
amount-multiplier = 2.0
}
{
name = "support-repair"
base = 10
shots-multiplier = 5.0
shots-max = 100
}
{
name = "support-repair-terminal"
base = 10
shots-multiplier = 5.0
shots-max = 100
}
{
name = "support-repair-turret"
base = 10
shots-multiplier = 5.0
shots-max = 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
}
{
name = "lodestar-repair"
base = 10
shots-multiplier = 1.0
shots-max = 100
amount-multiplier = 1.0
}
{
name = "lodestar-rearm"
base = 10
shots-multiplier = 1.0
}
{
name = "revival"
base = 0
shots-multiplier = 15.0
shots-cutoff = 10
}
]
}
# Command experience points
cep = {
# After all calculations are complete, multiply 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 = 30 seconds
# 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
# This is that 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 the reset battle rank is still possible.
active = true
# This battle rank and any battle ranks of ordinal decrement that allow opt-in to the progression system.
broadcast-battle-rank = 1
# This is the minimum battle rank that can be set as part of the promotion system.
# Used to escape debt and return to normal play.
reset-battle-rank = 5
# 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
support-experience-points-modifier = 3f
capture-experience-points-modifier = 1f
# 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 =>

File diff suppressed because it is too large Load diff

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,59 +732,17 @@ 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")
)
if (!setBattleRank(contents, session, AvatarActor.SetBep)) {
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
)
}
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")
)
if (!setCommandRank(contents, session)) {
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
)
}
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
@ -1031,20 +962,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 +1117,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
@ -1201,8 +1132,8 @@ class ChatActor(
true
} else if (contents.startsWith("!list")) {
val zone = contents.split(" ").lift(1) match {
case None =>
val zone = dropFirstWord(contents).split("\\s+").headOption match {
case Some("") | None =>
Some(session.zone)
case Some(id) =>
Zones.zones.find(_.id == id)
@ -1247,8 +1178,8 @@ class ChatActor(
true
} else if (contents.startsWith("!ntu") && gmCommandAllowed) {
val buffer = contents.toLowerCase.split("\\s+")
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
val buffer = dropFirstWord(contents).split("\\s+")
val (facility, customNtuValue) = (buffer.headOption, buffer.lift(1)) match {
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
case _ => (None, None)
@ -1279,8 +1210,8 @@ class ChatActor(
true
} else if (contents.startsWith("!zonerotate") && gmCommandAllowed) {
val buffer = contents.toLowerCase.split("\\s+")
cluster ! InterstellarClusterService.CavernRotation(buffer.lift(1) match {
val buffer = dropFirstWord(contents).split("\\s+")
cluster ! InterstellarClusterService.CavernRotation(buffer.headOption match {
case Some("-list") | Some("-l") =>
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
case _ =>
@ -1301,8 +1232,8 @@ class ChatActor(
} else if (contents.startsWith("!macro")) {
val avatar = session.avatar
val args = contents.split(" ").filter(_ != "")
(args.lift(1), args.lift(2)) match {
val args = dropFirstWord(contents).split(" ").filter(_ != "")
(args.headOption, args.lift(1)) match {
case (Some(cmd), other) =>
cmd.toLowerCase() match {
case "medkit" =>
@ -1350,6 +1281,17 @@ class ChatActor(
case _ =>
false
}
} else if (contents.startsWith("!progress")) {
val ourRank = BattleRank.withExperience(session.avatar.bep).value
if (!session.account.gm &&
(ourRank <= Config.app.game.promotion.broadcastBattleRank ||
ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
setBattleRank(dropFirstWord(contents), session, AvatarActor.Progress)
true
} else {
setBattleRank(contents="1", session, AvatarActor.Progress)
false
}
} else {
false // unknown ! commands are ignored
}
@ -1357,4 +1299,75 @@ class ChatActor(
false // unknown ! commands are ignored
}
}
private def dropFirstWord(str: String): String = {
val noExtraSpaces = str.replaceAll("\\s+", " ").toLowerCase.trim
noExtraSpaces.indexOf(" ") match {
case -1 => ""
case beforeFirstBlank => noExtraSpaces.drop(beforeFirstBlank + 1)
}
}
def setBattleRank(
contents: String,
session: Session,
msgFunc: Long => AvatarActor.Command
): Boolean = {
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("-h"), _) | (Some("-help"), _) =>
(None, Some(BattleRank.BR1))
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)
true
case _ =>
false
}
}
def setCommandRank(
contents: String,
session: Session
): Boolean = {
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)
true
case _ =>
false
}
}
}

View file

@ -81,6 +81,8 @@ object SessionActor {
final case class UpdateIgnoredPlayers(msg: FriendsResponse) extends Command
final case class AvatarLoadingSync(step: Int) extends Command
final case object CharSaved extends Command
private[session] case object CharSavedMsg extends Command
@ -255,6 +257,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case SessionActor.SetConnectionState(state) =>
sessionFuncs.connectionState = state
case SessionActor.AvatarLoadingSync(state) =>
sessionFuncs.zoning.spawn.handleAvatarLoadingSync(state)
/* uncommon messages (utility, or once in a while) */
case SessionActor.AvatarAwardMessageBundle(pkts, delay) =>
sessionFuncs.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay)

View file

@ -5,6 +5,7 @@ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, typed}
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
@ -224,10 +225,7 @@ class SessionAvatarHandlers(
case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
// TODO Temporary thing that should go somewhere else and use proper xp values
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
avatarActor ! AvatarActor.AwardBep((1000 * Config.app.game.bepRate).toLong, ExperienceType.Normal)
avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
// guid = victim // killer = killer
@ -396,6 +394,24 @@ class SessionAvatarHandlers(
sessionData.kitToBeUsed = None
sendResponse(ChatMsg(ChatMessageType.UNK_225, msg))
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
case AvatarResponse.AwardBep(charId, bep, expType) =>
//if the target player, always award (some) BEP
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) =>
facilityCaptureRewards(buildingId, zoneNumber, cep)
case AvatarResponse.SendResponse(msg) =>
sendResponse(msg)
@ -410,16 +426,16 @@ 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.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) {
@ -432,6 +448,7 @@ class SessionAvatarHandlers(
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
//player state changes
AvatarActor.updateToolDischargeFor(avatar)
player.FreeHand.Equipment.foreach { item =>
DropEquipmentFromInventory(player)(item)
}
@ -446,7 +463,6 @@ class SessionAvatarHandlers(
}
sessionData.playerActionsToCancel()
sessionData.terminals.CancelAllProximityUnits()
sessionData.zoning
AvatarActor.savePlayerLocation(player)
sessionData.zoning.spawn.shiftPosition = Some(player.Position)
@ -590,6 +606,71 @@ class SessionAvatarHandlers(
)
)
}
private def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = {
//TODO squad services deactivated, participation trophy rewards for now - 11-20-2023
//must be in a squad to earn experience
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 =>
val cepConfig = Config.app.game.experience.cep
//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 * (math.random().toFloat % 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)
}
}
}
object SessionAvatarHandlers {

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")
@ -1379,7 +1379,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")
@ -1582,6 +1582,24 @@ class SessionData(
case Some(llu: CaptureFlag) => Some((llu, llu.Carrier))
case _ => None
}) match {
case Some((llu, Some(carrier: Player)))
if carrier.GUID == player.GUID && !player.isAlive =>
player.LastDamage.foreach { damage =>
damage
.interaction
.adversarial
.map { _.attacker }
.collect {
case attacker
if attacker.Faction != player.Faction &&
System.currentTimeMillis() - llu.LastCollectionTime >= Config.app.game.experience.cep.lluSlayerCreditDuration.toMillis =>
continent.AvatarEvents ! AvatarServiceMessage(
attacker.Name,
AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
)
}
}
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
case Some((llu, Some(carrier: Player))) if carrier.GUID == player.GUID =>
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
case Some((_, Some(carrier: Player))) =>
@ -2461,9 +2479,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
) {
@ -2476,6 +2494,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
@ -162,7 +165,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))
@ -248,7 +251,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)
@ -278,7 +281,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()
@ -297,7 +300,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 {
@ -334,8 +337,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.objects.vital.{InGameHistory, IncarnationActivity, 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" ++
s"You only have this opportunity while you are less than or equal to battle rank ${Config.app.game.promotion.broadcastBattleRank}." ++
"\n\n" ++
"The normal method of rank advancement comes from 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. " ++
s"If you accept, but it becomes too much of burden, you may ask to revert to battle rank ${Config.app.game.promotion.resetBattleRank} 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))
})
@ -602,22 +629,7 @@ class ZoningOperations(
ServiceManager.serviceManager ! Lookup("propertyOverrideManager")
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
(
FriendsResponse.packetSequence(
MemberAction.InitializeFriendList,
avatar.people.friend
.map { f =>
game.Friend(f.name, AvatarActor.onlineIfNotIgnoredEitherWay(avatar, f.name))
}
) ++
//ignored list (no one ever online)
FriendsResponse.packetSequence(
MemberAction.InitializeIgnoreList,
avatar.people.ignored.map { f => game.Friend(f.name) }
)
).foreach {
sendResponse
}
spawn.initializeFriendsAndIgnoredLists()
//the following subscriptions last until character switch/logout
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
@ -1657,6 +1669,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 +1817,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 +2357,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 +2630,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 +2646,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)
}
}
@ -2765,16 +2782,7 @@ class ZoningOperations(
}
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
avatar.shortcuts
.zipWithIndex
.collect { case (Some(shortcut), index) =>
sendResponse(CreateShortcutMessage(
guid,
index + 1,
Some(AvatarShortcut.convert(shortcut))
))
}
sendResponse(ChangeShortcutBankMessage(guid, 0))
initializeShortcutsAndBank(guid)
//Favorites lists
avatarActor ! AvatarActor.InitialRefreshLoadouts()
@ -2803,10 +2811,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 +2828,7 @@ class ZoningOperations(
continent.DeployableList
.filter(_.OwnerName.contains(name))
.foreach(obj => {
obj.Owner = guid
obj.OwnerGuid = guid
drawDeloyableIcon(obj)
})
drawDeloyableIcon = DontRedrawIcons
@ -2831,7 +2836,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,23 +2911,35 @@ 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)) }
.flatten
player.LogActivity({
if (player.History.headOption.exists { _.isInstanceOf[SpawningActivity] }) {
ReconstructionActivity(PlayerSource(player), continent.Number, effortBy)
} else {
SpawningActivity(PlayerSource(player), continent.Number, effortBy)
}
})
//ride
val lastEntryOpt = player.History.lastOption
if (lastEntryOpt.exists { !_.isInstanceOf[IncarnationActivity] }) {
player.LogActivity({
lastEntryOpt match {
case Some(_) =>
ReconstructionActivity(PlayerSource(player), continent.Number, effortBy)
case None =>
SpawningActivity(PlayerSource(player), continent.Number, effortBy)
}
})
}
}
upstreamMessageCount = 0
setAvatar = true
if (
!account.gm && /* gm's are excluded */
Config.app.game.promotion.active && /* play versus progress system must be active */
BattleRank.withExperience(tplayer.avatar.bep).value <= Config.app.game.promotion.broadcastBattleRank && /* must be below a certain battle rank */
avatar.scorecard.Lives.isEmpty && /* first life after login */
avatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */
player.History.size == 1 /* did nothing but come into existence */
) {
ZoningOperations.reportProgressionSystem(context.self)
}
}
/**
@ -2935,6 +2952,59 @@ class ZoningOperations(
HandleSetCurrentAvatar(tplayer)
}
/**
* Respond to feedback of how the avatar's data is being handled
* in a way that properly reflects the state of the server at the moment.
* @param state indicator for the progress of the avatar
*/
def handleAvatarLoadingSync(state: Int): Unit = {
if (state == 2 && zoneLoaded.contains(true)) {
initializeFriendsAndIgnoredLists()
initializeShortcutsAndBank(player.GUID)
avatarActor ! AvatarActor.RefreshPurchaseTimes()
loginAvatarStatisticsFields()
avatarActor ! AvatarActor.InitialRefreshLoadouts()
}
}
/**
* Set up and dispatch a list of `FriendsResponse` packets related to both formal friends and ignored players.
*/
def initializeFriendsAndIgnoredLists(): Unit = {
(
FriendsResponse.packetSequence(
MemberAction.InitializeFriendList,
avatar.people.friend
.map { f =>
game.Friend(f.name, AvatarActor.onlineIfNotIgnoredEitherWay(avatar, f.name))
}
) ++
//ignored list (no one ever online)
FriendsResponse.packetSequence(
MemberAction.InitializeIgnoreList,
avatar.people.ignored.map { f => game.Friend(f.name) }
)
).foreach {
sendResponse
}
}
/**
* Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet.
*/
def initializeShortcutsAndBank(guid: PlanetSideGUID): Unit = {
avatar.shortcuts
.zipWithIndex
.collect { case (Some(shortcut), index) =>
sendResponse(CreateShortcutMessage(
guid,
index + 1,
Some(AvatarShortcut.convert(shortcut))
))
}
sendResponse(ChangeShortcutBankMessage(guid, 0))
}
/**
* Draw the icon for this deployable object.<br>
* <br>
@ -2961,7 +3031,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 +3234,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 is 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

@ -7,12 +7,15 @@ import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic
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, SupportExperienceCalculator}
import net.psforever.util.Database._
import net.psforever.persistence
@ -70,6 +73,9 @@ object ZoneActor {
// Once they do, we won't need this anymore
final case class ZoneMapUpdate() extends Command
final case class RewardThisDeath(entity: PlanetSideGameObject with FactionAffinity with InGameHistory) extends Command
final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command
}
class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
@ -79,7 +85,9 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
import ctx._
private[this] val log = org.log4s.getLogger
val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
private val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
private val experience: ActorRef[ExperienceCalculator.Command] = context.spawnAnonymous(ExperienceCalculator(zone))
private val supportExperience: ActorRef[SupportExperienceCalculator.Command] = context.spawnAnonymous(SupportExperienceCalculator(zone))
zone.actor = context.self
zone.init(context.toClassic)
@ -143,13 +151,18 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
case HotSpotActivity(defender, attacker, location) =>
zone.Activity ! Zone.HotSpot.Activity(defender, attacker, location)
case RewardThisDeath(entity) =>
experience ! ExperienceCalculator.RewardThisDeath(entity)
case RewardOurSupporters(target, history, kill, bep) =>
supportExperience ! SupportExperienceCalculator.RewardOurSupporters(target, history, kill, bep)
case ZoneMapUpdate() =>
zone.Buildings
.filter(_._2.BuildingType == StructureType.Facility)
.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

@ -9918,6 +9918,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
@ -9927,7 +9928,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

@ -3,6 +3,7 @@ package net.psforever.objects.avatar
import akka.actor.{Actor, ActorRef, Props, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.zone.ZoneActor
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
import net.psforever.objects._
import net.psforever.objects.ce.Deployable
@ -32,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}
@ -355,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)
@ -365,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}")
@ -507,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, _, _) =>
@ -1052,6 +1062,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case _ =>
events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
zone.actor ! ZoneActor.RewardThisDeath(player)
}
def suicide() : Unit = {

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,11 +158,12 @@ 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)
}
statisticMap.put(objectId, outEntry)
outEntry
case _ =>
val out = Statistic(0, 0, 0, 0, 0, 0, 0, 0)

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

@ -14,10 +14,10 @@ class CaptureFlagConverter extends ObjectCreateConverter[CaptureFlag]() {
override def ConstructorData(obj : CaptureFlag) : Try[CaptureFlagData] = {
val hackInfo = obj.Owner.asInstanceOf[Building].CaptureTerminal.get.HackedBy match {
case Some(hackInfo) => hackInfo
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L)
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L, obj.Faction)
}
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, Faction))
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, Faction))
}
case (_, None) =>
hackedBy = None
@ -73,14 +73,15 @@ trait Hackable {
object Hackable {
final case class HackInfo(
player: PlayerSource,
hackerGUID: PlanetSideGUID,
hackStartTime: Long,
hackDuration: Long
hackerGUID: PlanetSideGUID,
hackStartTime: Long,
hackDuration: Long,
originalFaction: PlanetSideEmpire.Value
) {
def hackerName: String = player.Name
def hackerFaction: PlanetSideEmpire.Value = player.Faction
def hackerPos: Vector3 = player.Position
def Duration(time: Long): HackInfo = HackInfo(player, hackerGUID, hackStartTime, time)
def Duration(time: Long): HackInfo = HackInfo(player, hackerGUID, hackStartTime, time, originalFaction)
}
}

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

@ -27,7 +27,7 @@ class IFFLockControl(lock: IFFLock)
.orElse {
case CommonMessages.Use(player, Some(item: SimpleItem))
if item.Definition == GlobalDefinitions.remote_electronics_kit =>
if (lock.Faction != player.Faction && lock.HackedBy.isEmpty) {
if (lock.Faction != player.Faction) {
sender() ! CommonMessages.Progress(
GenericHackables.GetHackSpeed(player, lock),
GenericHackables.FinishHacking(lock, player, 1114636288L),
@ -42,8 +42,7 @@ class IFFLockControl(lock: IFFLock)
} else {
val log = org.log4s.getLogger
log.warn(s"IFF lock is being hacked by ${player.Faction}, but don't know how to handle this state:")
log.warn(s"Lock - Faction=${lock.Faction}, HackedBy=${lock.HackedBy}")
log.warn(s"Player - Faction=${player.Faction}")
log.warn(s"Lock - Faction=${lock.Faction}, HackedBy=${lock.HackedBy.map{_.player}}")
}
case IFFLock.DoorOpenRequest(target, door, replyTo) =>

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,23 @@ 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 *
amount * resourceSilo.Definition.ChargeTime.toSeconds.toFloat / resourceSilo.MaxNtuCapacitor
).toLong
vehicle.Zone.AvatarEvents ! AvatarServiceMessage(
owner.name,
AvatarAction.AwardBep(owner.charId, deposit, ExperienceType.Normal)
)
zones.exp.ToDatabase.reportNtuActivity(owner.charId, resourceSilo.Zone.Number, resourceSilo.Owner.GUID.guid, deposit)
}
}
}
@ -192,6 +211,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 {
@ -185,9 +182,8 @@ class Building(
val (hacking, hackingFaction, hackTime): (Boolean, PlanetSideEmpire.Value, Long) = CaptureTerminal match {
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)
case Some(Hackable.HackInfo(p, _, start, length, _)) =>
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,276 @@
// 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 PlayerContributionRaw: Map[Long, (Player, Int, Long)] = playerContribution.toMap
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 (dataCount != 0) {
math.max(0.2f, math.min(2f, dataSum / dataCount.toFloat))
} else {
0.5f
}
}
/**
* 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,
ordering: Ordering[Int] = Ordering[Int]
): Float = {
val gradedPopulation = populationNumbers
.map { gradingRule }
.groupBy(x => x)
.values
.toSeq
.sortBy(_.size)(ordering)
.take(layers)
.flatten
gradedPopulation.sum / gradedPopulation.size.toFloat
}
private[participation] def populationBalanceModifier(
victorPopulationNumbers: Seq[Int],
opposingPopulationNumbers: Seq[Int],
healthyPercentage: Float,
maxRatio: Float = 1f
): 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
math.max(0f, math.min(rate.sum / rate.size.toFloat, maxRatio))
}
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,328 @@
// 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, 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(building.Position.xy, building.Definition.SOIRadius, Nil)))
// }
requestLayers.completeWith(Future(ZoneHotSpotProjector.ExposedHeat(building.Position.xy, building.Definition.SOIRadius.toFloat, Nil)))
requestLayers.future
}
def RewardFacilityCapture(
defenderFaction: PlanetSideEmpire.Value,
attackingFaction: PlanetSideEmpire.Value,
hacker: PlayerSource,
hackTime: Long,
completionTime: Long,
isResecured: Boolean
): Unit = {
//has the facility ran out of nanites during the hack
if (building.NtuLevel > 0) {
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
)
val events = building.Zone.AvatarEvents
val buildingId = building.GUID.guid
val zoneNumber = building.Zone.Number
val playersInSoi = building.PlayersInSOI.filter {
_.Faction == victorFaction
}
if (baseExperienceFromFacilityCapture > 0) {
//2) population modifier
//The value of the first should grow as population grows.
//This is an intentionally imperfect counterbalance to that growth.
val populationModifier = FacilityHackParticipation.populationProgressModifier(
opposingPopulationByLayer,
{ pop =>
if (pop > 75) 0.5f
else if (pop > 59) 0.6f
else if (pop > 29) 0.7f
else if (pop > 19) 0.75f
else 0.8f
},
4
)
//3) competition multiplier
val competitionMultiplier: Float = {
val populationBalanceModifier: Float = FacilityHackParticipation.populationBalanceModifier(
victorPopulationByLayer,
opposingPopulationByLayer,
healthyPercentage = 1.5f,
maxRatio = 2.0f
)
//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.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 unit
// will incur an abbreviated duration
val overallTimeMultiplier: Float = {
if (hasFlag) {
if (completionTime >= hackTime) { //hack timed out without llu delivery
0.5f
} 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.65f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.25f + (d.toFloat / 1800000f))
case (a, (_, _, _)) =>
(a, 0.25f)
}
//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
//terminal hacker (always cep)
if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) {
ToDatabase.reportFacilityCapture(
hackerId,
zoneNumber,
buildingId,
finalCep,
expType = "cep"
)
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep))
}
//bystanders (cep if squad leader, bep otherwise)
playersInSoi
.filterNot { _.CharId == hackerId }
.foreach { player =>
val charId = player.CharId
val contributionMultiplier = contributionPerPlayerByTime.getOrElse(charId, 1f)
val outputValue = (finalCep * contributionMultiplier).toLong
events ! AvatarServiceMessage(player.Name, AvatarAction.FacilityCaptureRewards(buildingId, zoneNumber, outputValue))
}
//flag carrier (won't be in soi, but earns cep from capture)
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,
zoneNumber,
buildingId,
finalModifiedCep,
expType = "llu"
)
events ! AvatarServiceMessage(player.Name, AvatarAction.AwardCep(charId, finalModifiedCep))
}
} else {
//no need to calculate a fancy score
val hackerId = hacker.CharId
val hackerScore = List((hackerId, 0L, "cep"))
ToDatabase.reportFacilityCaptureInBulk(
if (isResecured) {
hackerScore
} else {
val flagCarrierScore = flagCarrier.map (p => List((p.CharId, 0L, "llu"))).getOrElse(Nil)
if (playersInSoi.exists(_.CharId == hackerId) && !flagCarrierScore.exists { case (charId, _,_) => charId == hackerId }) {
hackerScore ++ flagCarrierScore
} else {
flagCarrierScore
}
} ++ playersInSoi.filterNot { p => p.CharId == hackerId }.map(p => (p.CharId, 0L, "bep")),
zoneNumber,
buildingId
)
}
}
}
}
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,23 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.Player
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 PlayerContributionRaw: Map[Long, (Player, Int, Long)] = Map.empty[Long, (Player, Int, Long)]
override def PlayerContribution(timeDelay: Long): Map[Long, Float] = Map.empty[Long, Float]
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.Player
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 PlayerContributionRaw: Map[Long, (Player, Int, Long)]
def PlayerContribution(timeDelay: Long = 600): Map[Long, Float]
}

View file

@ -0,0 +1,180 @@
// 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.objects.zones.exp.ToDatabase
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) {
//early setup ...
import scala.concurrent.duration._
val curr = System.currentTimeMillis()
val soiPlayers = building.PlayersInSOI.filter { _.Faction == victorFaction }
val contributionOpposingSize = contributionOpposing.size
val events = building.Zone.AvatarEvents
val buildingId = building.GUID.guid
val zoneNumber = building.Zone.Number
val hackerId = hacker.CharId
//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
)
//based on this math, the optimal number of enemy for experience gain is 20
//max value of: 1000 * pop * max(0, (40 - pop)) * 0.1
if (baseExperienceFromFacilityCapture > 0) {
//more setup ...
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))
}
//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 > 40) 0.075f
else if (pop > 8) (40 - pop).toFloat * 0.1f
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.75f)
case (a, (_, d, t)) if math.abs(completionTime - t) < 5000 =>
(a, 0.15f + (d.toFloat / 600000f))
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.25f, 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
//terminal hacker (always cep)
events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hacker.CharId, finalCep))
ToDatabase.reportFacilityCapture(
hackerId,
zoneNumber,
buildingId,
finalCep,
expType = "cep"
)
//bystanders (cep if squad leader, bep otherwise)
soiPlayers
.filterNot(_.CharId == hackerId)
.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.FacilityCaptureRewards(buildingId, zoneNumber, outputValue)
)
}
} else {
//no need to calculate a fancy score
ToDatabase.reportFacilityCaptureInBulk(
(hackerId, 0L, "cep") +: soiPlayers.filterNot(_.CharId == hackerId).map(p => (p.CharId, 0L, "bep")),
zoneNumber,
buildingId
)
}
}
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

@ -12,7 +12,7 @@ class CaptureTerminal(private val idef: CaptureTerminalDefinition) extends Ameni
override def toString: String = {
val guid = if (HasGUID) {
s" ${Continent}-${GUID.guid}"
s" $Continent-${GUID.guid}"
} else {
""
}

View file

@ -103,7 +103,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
@ -132,6 +133,7 @@ trait AntTransferBehavior extends TransferBehavior with NtuStorageBehavior {
ActivatePanelsForChargingEvent(ChargeTransferObject)
}
//noinspection ScalaUnusedSymbol
def WithdrawAndTransmit(vehicle: Vehicle, maxRequested: Float): Any = {
val chargeable = ChargeTransferObject
var chargeToDeposit = Math.min(Math.min(chargeable.NtuCapacitor, 100), maxRequested)
@ -186,8 +188,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.toSeconds.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, SpawningActivity}
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones._
@ -143,9 +143,9 @@ class VehicleControl(vehicle: Vehicle)
}) =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num))
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) })
@ -252,9 +252,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 {
@ -301,7 +301,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
}) &&
@ -312,6 +312,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
@ -340,7 +342,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) {
@ -355,6 +357,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) {
@ -363,7 +366,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(
@ -433,7 +436,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(
@ -905,7 +908,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,66 @@ 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
trait IncarnationActivity extends GeneralActivity
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) extends GeneralActivity
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
extends IncarnationActivity
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) extends GeneralActivity
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry])
extends IncarnationActivity
final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amount: Int, equipment: EquipmentDefinition)
extends IncarnationActivity 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 +135,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 +145,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 +160,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
@ -148,7 +217,7 @@ trait InGameHistory {
/**
* An in-game event must be recorded.
* Add new entry to the front of the list (for recent activity).
* Add new entry to the list (for recent activity).
* @param action the fully-informed entry
* @return the list of previous changes to this entity
*/
@ -157,14 +226,37 @@ trait InGameHistory {
/**
* An in-game event must be recorded.
* Add new entry to the front of the list (for recent activity).
* Add new entry to the list (for recent activity).
* Special handling must be conducted for certain events.
* @param action the fully-informed entry
* @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 = act +: history
history = history :+ act
case None => ()
}
history
@ -188,7 +280,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 +301,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 +358,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 +373,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

@ -0,0 +1,45 @@
// 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.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
object ExperienceCalculator {
def apply(zone: Zone): Behavior[Command] =
Behaviors.supervise[Command] {
Behaviors.setup(context => new ExperienceCalculator(context, zone))
}.onFailure[Exception](SupervisorStrategy.restart)
sealed trait Command
final case class RewardThisDeath(victim: SourceEntry, lastDamage: Option[DamageResult], history: Iterable[InGameActivity])
extends ExperienceCalculator.Command
object RewardThisDeath {
def apply(obj: PlanetSideGameObject with FactionAffinity with InGameHistory): RewardThisDeath = {
RewardThisDeath(SourceEntry(obj), obj.LastDamage, obj.History)
}
}
}
class ExperienceCalculator(context: ActorContext[ExperienceCalculator.Command], zone: Zone)
extends AbstractBehavior[ExperienceCalculator.Command](context) {
import ExperienceCalculator._
def onMessage(msg: Command): Behavior[Command] = {
msg match {
case RewardThisDeath(victim: PlayerSource, lastDamage, history) =>
KillAssists.rewardThisPlayerDeath(victim, lastDamage, history, zone.AvatarEvents)
case _ => ()
}
Behaviors.same
}
}

View file

@ -0,0 +1,636 @@
// 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)
lazy val base = Support.baseExperience(victim, history)
if (killer.Faction == victim.Faction || killer.unique == victim.unique) {
0L
} else if (base > 1) {
//include battle rank disparity modifier
val battleRankDisparity: Long = {
import net.psforever.objects.avatar.BattleRank
val killerLevel = BattleRank.withExperience(killer.bep).value
val victimLevel = BattleRank.withExperience(victim.bep).value
val victimMinusKiller = victimLevel - killerLevel
if (victimMinusKiller > -1) {
victimMinusKiller * 10 + victimLevel
} else {
val bothLevels = killerLevel + victimLevel
val pointFive = (base.toFloat * 0.25f).toInt
-1 * (if (bothLevels >= base) {
pointFive
} else {
math.min(bothLevels, pointFive)
})
}
}.toLong
//include menace modifier
base + battleRankDisparity + (victim.progress.kills.size.toFloat * (1f + calculateMenace(victim).toFloat / 10f)).toLong
} 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

@ -0,0 +1,41 @@
// Copyright (c) 2023 PSForever
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(
equipment: EquipmentUseContextWrapper,
amount: Int,
shots: Int,
time: Long,
contributions: Float
) extends ItemUseStats
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[EquipmentUseContextWrapper],
percentage: Float
)

View file

@ -0,0 +1,235 @@
// 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 shotsMax = event.shotsMax
val shotsMultiplier = event.shotsMultiplier
if (shotsMultiplier > 0f && shots < event.shotsCutoff) {
val modifiedShotsReward: Float =
shotsMultiplier * math.log(math.min(shotsMax, shots).toDouble + 2d).toFloat
val modifiedAmountReward: Float =
event.amountMultiplier * weaponStat.amount.toFloat
event.base.toFloat + 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,232 @@
// 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)
)
)
}
/**
* Insert multiple entries into the database's `buildingCapture` table as a single transaction.
*/
def reportFacilityCaptureInBulk(
avatarIdAndExp: List[(Long, Long, String)],
zoneId: Int,
buildingId: Int
): Unit = {
ctx.run(quote { liftQuery(
avatarIdAndExp.map { case (avatarId, exp, expType) =>
persistence.Buildingcapture(-1, avatarId, zoneId, buildingId, exp, expType)
}
)}.foreach(e => query[persistence.Buildingcapture].insertValue(e)))
}
}

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

@ -2,7 +2,8 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.bits.BitVector
import scodec.{Attempt, Codec}
import scodec.codecs._
/**
@ -12,21 +13,33 @@ import scodec.codecs._
* It merely generates the message:<br>
* `"You have been awarded x experience points."`<br>
* ... where `x` is the number of experience points that have been promised.
* If the `Boolean` parameter is `true`, `x` will be equal to the number provided followed by the word "Command."
* If the `Boolean` parameter is `false`, `x` will be represented as an obvious blank space character.
* (Yes, it prints to the events chat like that.)
* @param exp the number of (Command) experience points earned
* @param unk defaults to `true` for effect;
* if `false`, the number of experience points in the message will be blanked
* @param cmd if `true`, the message will be tailored for "Command" experience;
* if `false`, the number of experience points and the "Command" flair will be blanked
*/
final case class ExperienceAddedMessage(exp: Int, unk: Boolean = true) extends PlanetSideGamePacket {
final case class ExperienceAddedMessage(exp: Int, cmd: Boolean) extends PlanetSideGamePacket {
type Packet = ExperienceAddedMessage
def opcode = GamePacketOpcode.ExperienceAddedMessage
def encode = ExperienceAddedMessage.encode(this)
def opcode: GamePacketOpcode.Value = GamePacketOpcode.ExperienceAddedMessage
def encode: Attempt[BitVector] = ExperienceAddedMessage.encode(this)
}
object ExperienceAddedMessage extends Marshallable[ExperienceAddedMessage] {
/**
* Produce a packet whose message to the event chat is
* "You have been awarded experience points."
* @return `ExperienceAddedMessage` packet
*/
def apply(): ExperienceAddedMessage = ExperienceAddedMessage(0, cmd = false)
/**
* Produce a packet whose message to the event chat is
* "You have been awarded 'exp' Command experience points."
* @param exp the number of Command experience points earned
* @return `ExperienceAddedMessage` packet
*/
def apply(exp: Int): ExperienceAddedMessage = ExperienceAddedMessage(exp, cmd = true)
implicit val codec: Codec[ExperienceAddedMessage] = (
("exp" | uintL(15)) :: ("unk" | bool)
("exp" | uintL(bits = 15)) :: ("unk" | bool)
).as[ExperienceAddedMessage]
}

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,12 @@
// Copyright (c) 2023 PSForever
package net.psforever.persistence
import org.joda.time.LocalDateTime
case class Progressiondebt(
avatarId:Long,
experience: Long,
maxExperience: Long = -1,
enrollTime: LocalDateTime = LocalDateTime.now(),
clearTime: LocalDateTime = LocalDateTime.now()
)

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

@ -429,7 +429,43 @@ class AvatarService(zone: Zone) extends Actor {
)
)
case _ => ;
case AvatarAction.UpdateKillsDeathsAssists(charId, stat) =>
AvatarEvents.publish(
AvatarServiceResponse(
s"/$forChannel/Avatar",
Service.defaultPlayerGUID,
AvatarResponse.UpdateKillsDeathsAssists(charId, stat)
)
)
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

@ -2,6 +2,7 @@
package net.psforever.services.avatar
import net.psforever.objects.Player
import net.psforever.objects.avatar.scoring.KDAStat
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
@ -11,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
@ -154,6 +155,11 @@ object AvatarAction {
final case class UseKit(kit_guid: PlanetSideGUID, kit_objid: Int) extends Action
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Action
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Action
final case class 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
// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action

View file

@ -2,6 +2,7 @@
package net.psforever.services.avatar
import net.psforever.objects.Player
import net.psforever.objects.avatar.scoring.KDAStat
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.InventoryItem
@ -10,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(
@ -124,4 +125,9 @@ object AvatarResponse {
// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
final case class UseKit(kit_guid: PlanetSideGUID, kit_objid: Int) extends Response
final case class KitNotUsed(kit_guid: PlanetSideGUID, msg: String) extends Response
final case class UpdateKillsDeathsAssists(charId: Long, kda: KDAStat) extends Response
final case class 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
}

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