Experience for KDA [Prep-work] (#1024)

* extensive modifications to source entry shorthand

* moving 11 files changes 55 other files

* added score card classes; upgraded packet classes

* I decided to import over everything

* proliferation of in-game activity messages, especially for spawning activity; removed defaults for activities; fixed (most?) building tests

* upkeep on the LLU's managing service, as well as the facility hack management service, in response to a potential bug

* a facility that changes faction affiliation while it is the destination of an LLU delivery will cancel that LLU delivery

* fixed crash due to boomer trigger overriding position of ace, without the ace being properly cleaned up on the client of the bomber; fixed issue with the boomer trigger going missing

* flipped the first two FDU deployable settings so they match the correct fire modes; corrected a stack overflow situation with the sourcing entities

* action, but no response

* condensed parameters on avatar class

* as always, fixing tests

* quickly, loose ends tied
This commit is contained in:
Fate-JH 2023-02-14 00:09:28 -05:00 committed by GitHub
parent 40cf783f18
commit 779054fef9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
153 changed files with 3772 additions and 2100 deletions

View file

@ -46,6 +46,7 @@ lazy val psforeverSettings = Seq(
"com.typesafe.akka" %% "akka-stream" % "2.6.17",
"com.typesafe.akka" %% "akka-testkit" % "2.6.17" % "test",
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.17",
"com.typesafe.akka" %% "akka-actor-testkit-typed" % "2.6.17" % "test",
"com.typesafe.akka" %% "akka-slf4j" % "2.6.17",
"com.typesafe.akka" %% "akka-cluster-typed" % "2.6.17",
"com.typesafe.akka" %% "akka-coordination" % "2.6.17",

View file

@ -6,7 +6,7 @@ import akka.testkit.TestProbe
import base.FreedContextActorTest
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.CommonMessages
@ -14,6 +14,7 @@ import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.resourcesilo.{ResourceSilo, ResourceSiloControl}
import net.psforever.objects.serverobject.structures.{AutoRepairStats, Building, StructureType}
import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal, TerminalControl}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.damage.DamageProfile

View file

@ -7,11 +7,12 @@ import base.FreedContextActorTest
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.structures.{AutoRepairStats, Building, StructureType}
import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal, TerminalControl}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.damage.DamageProfile

View file

@ -297,7 +297,7 @@ class MiddlewareActor(
send(ServerStart(nonce, serverNonce), None, None)
cryptoSetup()
case (Unknown30(nonce), _) =>
case (Unknown30(_), _) =>
/*
Unknown30 is used to reuse an existing crypto session when switching from login to world
When not handling it, it appears that the client will fall back to using ClientStart

View file

@ -5,8 +5,15 @@ import java.util.concurrent.atomic.AtomicInteger
import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity}
import net.psforever.objects.avatar.{ProgressDecoration, SpecialCarry}
import net.psforever.objects.avatar.scoring.{Death, EquipmentStat, KDAStat, Kill}
import net.psforever.objects.sourcing.SourceWithHealthEntry
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.{DamagingActivity, HealingActivity, SpawningActivity, Vitality}
import net.psforever.packet.game.objectcreate.BasicCharacterData
import net.psforever.types.ExperienceType
import org.joda.time.{LocalDateTime, Seconds}
import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success}
@ -32,8 +39,8 @@ import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
import net.psforever.objects._
import net.psforever.objects.ballistics.PlayerSource
import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.HealFromImplant
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _}
@ -188,7 +195,7 @@ object AvatarActor {
final case class SuspendStaminaRegeneration(duration: FiniteDuration) extends Command
/** Award battle experience points */
final case class AwardBep(bep: Long) extends Command
final case class AwardBep(bep: Long, modifier: ExperienceType) extends Command
/** Set total battle experience points */
final case class SetBep(bep: Long) extends Command
@ -214,6 +221,8 @@ object AvatarActor {
final case class RemoveShortcut(slot: Int) extends Command
final case class UpdateToolDischarge(stat: EquipmentStat) extends Command
final case class AvatarResponse(avatar: Avatar)
final case class AvatarLoginResponse(avatar: Avatar)
@ -618,22 +627,28 @@ object AvatarActor {
*/
def finalSavePlayerData(player: Player): Future[Int] = {
val health = (
player.History.find(_.isInstanceOf[DamagingActivity]),
player.History.find(_.isInstanceOf[HealingActivity])
player.History.findLast(_.isInstanceOf[DamagingActivity]),
player.History.collect { case h: HealingActivity => h }
) match {
case (Some(damage), Some(heal)) =>
case (Some(damage: DamagingActivity), heals) if heals.nonEmpty =>
val health = damage.data.targetAfter.asInstanceOf[PlayerSource].health
//between damage and potential healing, which came last?
if (damage.time < heal.time) {
heal.asInstanceOf[HealingActivity].amount % player.MaxHealth
if (damage.time < heals.last.time) {
health + heals.map { _.amount }.sum
} else {
damage.asInstanceOf[DamagingActivity].data.targetAfter.asInstanceOf[PlayerSource].health
health
}
case (Some(damage: DamagingActivity), _) =>
damage.data.targetAfter.asInstanceOf[PlayerSource].health
case (None, heals) if heals.nonEmpty =>
player.History.headOption match {
case Some(es: SpawningActivity) =>
es.src.asInstanceOf[SourceWithHealthEntry].health + heals.map { _.amount }.sum
case _ =>
player.Health
}
case (Some(damage), None) =>
damage.asInstanceOf[DamagingActivity].data.targetAfter.asInstanceOf[PlayerSource].health
case (None, Some(heal)) =>
heal.asInstanceOf[HealingActivity].amount % player.MaxHealth
case _ =>
player.MaxHealth
player.Health
}
savePlayerData(player, health)
}
@ -772,6 +787,21 @@ object AvatarActor {
}
out.future
}
def toAvatar(avatar: persistence.Avatar): Avatar =
Avatar(
avatar.id,
BasicCharacterData(
avatar.name,
PlanetSideEmpire(avatar.factionId),
CharacterSex.valuesToEntriesMap(avatar.genderId),
avatar.headId,
CharacterVoice(avatar.voiceId)
),
avatar.bep,
avatar.cep,
decoration = ProgressDecoration(cosmetics = avatar.cosmetics.map(c => Cosmetic.valuesFromObjectCreateValue(c)))
)
}
class AvatarActor(
@ -937,7 +967,7 @@ class AvatarActor(
case Success(characters) =>
characters.headOption match {
case Some(character) =>
avatar = character.toAvatar
avatar = AvatarActor.toAvatar(character)
replyTo ! AvatarResponse(avatar)
case None =>
log.error(s"selected character $charId not found")
@ -1497,7 +1527,7 @@ class AvatarActor(
val events = zone.AvatarEvents
val guid = player.GUID
val newHealth = player.Health = originalHealth + 1
player.History(HealFromImplant(PlayerSource(player), 1, implantType))
player.LogActivity(HealFromImplant(implantType, 1))
events ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth)
@ -1595,67 +1625,24 @@ class AvatarActor(
initializeImplants()
Behaviors.same
case AwardBep(bep) =>
context.self ! SetBep(avatar.bep + bep)
case UpdateToolDischarge(stats) =>
updateToolDischarge(stats)
Behaviors.same
case AwardBep(bep, modifier) =>
setBep(avatar.bep + bep, modifier)
Behaviors.same
case SetBep(bep) =>
import ctx._
val result = for {
_ <-
if (BattleRank.withExperience(bep).value < BattleRank.BR24.value) setCosmetics(Set())
else Future.successful(())
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatar.id)).update(_.bep -> lift(bep)))
} yield r
result.onComplete {
case Success(_) =>
sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(session.get.player.GUID, bep, 0))
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.PlanetsideAttributeToAll(session.get.player.GUID, 17, bep)
)
// when the level is reduced, take away any implants over the implant slot limit
val implants = avatar.implants.zipWithIndex.map {
case (implant, index) =>
if (index >= BattleRank.withExperience(bep).implantSlots && implant.isDefined) {
ctx.run(
query[persistence.Implant]
.filter(_.name == lift(implant.get.definition.Name))
.filter(_.avatarId == lift(avatar.id))
.delete
)
.onComplete {
case Success(_) =>
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, index, 0)
)
case Failure(exception) => log.error(exception)("db failure")
}
None
} else {
implant
}
}
avatar = avatar.copy(bep = bep, implants = implants)
case Failure(exception) => log.error(exception)("db failure")
}
setBep(bep, ExperienceType.Normal)
Behaviors.same
case AwardCep(cep) =>
context.self ! SetCep(avatar.cep + cep)
setCep(avatar.cep + cep)
Behaviors.same
case SetCep(cep) =>
import ctx._
ctx.run(query[persistence.Avatar].filter(_.id == lift(avatar.id)).update(_.cep -> lift(cep))).onComplete {
case Success(_) =>
avatar = avatar.copy(cep = cep)
session.get.zone.AvatarEvents ! AvatarServiceMessage(
session.get.zone.id,
AvatarAction.PlanetsideAttributeToAll(session.get.player.GUID, 18, cep)
)
case Failure(exception) => log.error(exception)("db failure")
}
setCep(cep)
Behaviors.same
case SetCosmetics(cosmetics) =>
@ -1690,8 +1677,8 @@ class AvatarActor(
//short-circuit if the shortcut already exists at the given location
val isMacroShortcut = shortcut.isInstanceOf[Shortcut.Macro]
val isDifferentShortcut = !(targetShortcut match {
case Some(target) => AvatarShortcut.equals(shortcut, target)
case _ => false
case Some(target: AvatarShortcut) => AvatarShortcut.equals(shortcut, target)
case _ => false
})
if (isDifferentShortcut) {
if (!isMacroShortcut && avatar.shortcuts.flatten.exists {
@ -1701,7 +1688,7 @@ class AvatarActor(
if (shortcut.isInstanceOf[Shortcut.Implant]) {
//duplicate implant
targetShortcut match {
case Some(existingShortcut) =>
case Some(existingShortcut: AvatarShortcut) =>
//redraw redundant shortcut slot with existing shortcut
sessionActor ! SessionActor.SendResponse(
CreateShortcutMessage(session.get.player.GUID, slot + 1, Some(AvatarShortcut.convert(existingShortcut)))
@ -2116,7 +2103,7 @@ class AvatarActor(
avatars.filter(!_.deleted) foreach { a =>
val secondsSinceLastLogin = Seconds.secondsBetween(a.lastLogin, LocalDateTime.now()).getSeconds
val avatar = a.toAvatar
val avatar = AvatarActor.toAvatar(a)
val player = new Player(avatar)
player.ExoSuit = ExoSuitType.Reinforced
@ -2356,11 +2343,12 @@ class AvatarActor(
}
def refreshLoadouts(loadouts: Iterable[(Option[Loadout], Int)]): Unit = {
val guid = session.get.player.GUID
loadouts
.map {
case (Some(loadout: InfantryLoadout), index) =>
FavoritesMessage.Infantry(
session.get.player.GUID,
guid,
index,
loadout.label,
InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)
@ -2368,14 +2356,14 @@ class AvatarActor(
case (Some(loadout: VehicleLoadout), index)
if GlobalDefinitions.isBattleFrameVehicle(loadout.vehicle_definition) =>
FavoritesMessage.Battleframe(
session.get.player.GUID,
guid,
index - 15,
loadout.label,
VehicleLoadout.DetermineBattleframeSubtype(loadout.vehicle_definition)
)
case (Some(loadout: VehicleLoadout), index) =>
FavoritesMessage.Vehicle(
session.get.player.GUID,
guid,
index - 10,
loadout.label
)
@ -2389,7 +2377,7 @@ class AvatarActor(
}
FavoritesMessage(
mtype,
session.get.player.GUID,
guid,
lineNo,
"",
0
@ -2812,4 +2800,110 @@ class AvatarActor(
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.RemoveIgnoredPlayer, GameFriend(name)))
sessionActor ! SessionActor.CharSaved
}
def setBep(bep: Long, modifier: ExperienceType): Unit = {
import ctx._
val result = for {
_ <-
if (BattleRank.withExperience(bep).value < BattleRank.BR24.value) setCosmetics(Set())
else Future.successful(())
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatar.id)).update(_.bep -> lift(bep)))
} yield r
result.onComplete {
case Success(_) =>
val sess = session.get
val zone = sess.zone
val pguid = sess.player.GUID
val localModifier = modifier
sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, bep, localModifier))
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep)
)
// when the level is reduced, take away any implants over the implant slot limit
val implants = avatar.implants.zipWithIndex.map {
case (implant, index) =>
if (index >= BattleRank.withExperience(bep).implantSlots && implant.isDefined) {
ctx.run(
query[persistence.Implant]
.filter(_.name == lift(implant.get.definition.Name))
.filter(_.avatarId == lift(avatar.id))
.delete
)
.onComplete {
case Success(_) =>
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(pguid, ImplantAction.Remove, index, 0)
)
case Failure(exception) =>
log.error(exception)("db failure")
}
None
} else {
implant
}
}
avatar = avatar.copy(bep = bep, implants = implants)
case Failure(exception) =>
log.error(exception)("db failure")
}
}
def setCep(cep: Long): Unit = {
import ctx._
ctx.run(query[persistence.Avatar].filter(_.id == lift(avatar.id)).update(_.cep -> lift(cep))).onComplete {
case Success(_) =>
val sess = session.get
val zone = sess.zone
avatar = avatar.copy(cep = cep)
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(sess.player.GUID, 18, cep)
)
case Failure(exception) =>
log.error(exception)("db failure")
}
}
def updateKillsDeathsAssists(kdaStat: KDAStat): Unit = {
avatar.scorecard.rate(kdaStat)
val exp = kdaStat.experienceEarned
val _session = session.get
val zone = _session.zone
val player = _session.player
kdaStat match {
case kill: Kill =>
val _ = PlayerSource(player)
(kill.info.interaction.cause match {
case pr: ProjectileReason => pr.projectile.mounted_in.map { a => zone.GUID(a._1) }
case _ => None
}) match {
case Some(Some(_: Vitality)) =>
//zone.actor ! ZoneActor.RewardOurSupporters(playerSource, obj.History, kill, exp)
case _ => ;
}
//zone.actor ! ZoneActor.RewardOurSupporters(playerSource, player.History, kill, exp)
case _: Death =>
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarStatisticsMessage(DeathStatistic(avatar.scorecard.Lives.size))
)
)
}
if (exp > 0L) {
val gameOpts = Config.app.game
val (msg, modifier): (ExperienceType, Float) = if (player.Carrying.contains(SpecialCarry.RabbitBall)) {
(ExperienceType.RabbitBall, 1.25f)
} else {
(ExperienceType.Normal, 1f)
}
setBep(avatar.bep + (exp * modifier * gameOpts.bepRate).toLong, msg)
}
}
def updateToolDischarge(stats: EquipmentStat): Unit = {
avatar.scorecard.rate(stats)
}
}

View file

@ -10,7 +10,7 @@ import net.psforever.objects.definition.ImplantDefinition
import net.psforever.packet.game.{CreateShortcutMessage, Shortcut}
import net.psforever.packet.game.objectcreate.DrawnSlot
import net.psforever.types.ChatMessageType.{CMT_GMOPEN, UNK_227}
import net.psforever.types.ImplantType
import net.psforever.types.{ExperienceType, ImplantType}
import scala.collection.mutable
import scala.concurrent.ExecutionContextExecutor
@ -820,7 +820,7 @@ class ChatActor(
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
contents.toIntOption match {
case Some(bep) => avatarActor ! AvatarActor.AwardBep(bep)
case Some(bep) => avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Normal)
case None =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_ADDBATTLEEXPERIENCE_usage")

View file

@ -29,6 +29,8 @@ class SessionAvatarHandlers(
chatActor: typed.ActorRef[ChatActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
private[support] var lastSeenStreamMessage: Array[Long] = Array.fill[Long](65535)(elem=0L)
/**
* na
*
@ -117,7 +119,7 @@ class SessionAvatarHandlers(
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
// TODO Temporary thing that should go somewhere else and use proper xp values
if (killer.CharId == avatar.id && killer.Faction != victim.Faction) {
avatarActor ! AvatarActor.AwardBep((1000 * Config.app.game.bepRate).toLong)
avatarActor ! AvatarActor.AwardBep((1000 * Config.app.game.bepRate).toLong, ExperienceType.Normal)
avatarActor ! AvatarActor.AwardCep((100 * Config.app.game.cepRate).toLong)
}
@ -287,7 +289,7 @@ class SessionAvatarHandlers(
val r2 = 2 + r.nextInt(4000).toFloat
(Vector3(r2, r2, r1), 0L, 0f)
} else {
val before = player.lastSeenStreamMessage(guid.guid)
val before = lastSeenStreamMessage(guid.guid)
val dist = Vector3.DistanceSquared(player.Position, pos)
(pos, now - before, dist)
}
@ -307,7 +309,7 @@ class SessionAvatarHandlers(
is_cloaking
)
)
player.lastSeenStreamMessage(guid.guid) = now
lastSeenStreamMessage(guid.guid) = now
}
}

View file

@ -3,6 +3,8 @@ package net.psforever.actors.session.support
import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, OneForOneStrategy, SupervisorStrategy, typed}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
@ -940,18 +942,22 @@ class SessionData(
def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = {
val FacilityBenefitShieldChargeRequestMessage(_) = pkt
continent.GUID(player.VehicleSeated) match {
case Some(obj) if obj.Destroyed => () //vehicle will try to charge even if destroyed
case Some(obj: Vehicle) =>
obj.Actor ! Vehicle.ChargeShields(15)
case Some(_: TurretDeployable) => () //TODO the turret will charge a shield in some circumstances
case None if player.VehicleSeated.nonEmpty =>
log.error(
s"FacilityBenefitShieldChargeRequest: ${player.Name} is seated in a vehicle that can not be found in zone ${continent.id}"
)
case _ =>
log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is seated in an imaginary vehicle")
}
val vehicleGuid = player.VehicleSeated
continent
.GUID(vehicleGuid)
.foreach {
case obj: Vehicle if !obj.Destroyed => //vehicle will try to charge even if destroyed
obj.Actor ! CommonMessages.ChargeShields(
15,
Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius))
)
case _ if vehicleGuid.nonEmpty =>
log.warn(
s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find vehicle ${vehicleGuid.get.guid} in zone ${continent.id}"
)
case _ =>
log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is not seated in a vehicle")
}
}
def handleBattleplan(pkt: BattleplanMessage): Unit = {
@ -2284,7 +2290,7 @@ class SessionData(
* @param tplayer the player to be killed
*/
def suicide(tplayer: Player): Unit = {
tplayer.History(PlayerSuicide(PlayerSource(tplayer)))
tplayer.LogActivity(PlayerSuicide(PlayerSource(tplayer)))
tplayer.Actor ! Player.Die()
}

View file

@ -128,20 +128,17 @@ class SessionLocalHandlers(
llu.Definition.Packet.ConstructorData(llu).get
)
)
sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk = 20, 0.8000001f))
case LocalResponse.LluDespawned(llu) =>
sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, llu.Position, unk = 20, 0.8000001f))
sendResponse(ObjectDeleteMessage(llu.GUID, 0))
case LocalResponse.LluDespawned(lluGuid, position) =>
sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk = 20, 0.8000001f))
sendResponse(ObjectDeleteMessage(lluGuid, 0))
// If the player was holding the LLU, remove it from their tracked special item slot
sessionData.specialItemSlotGuid match {
case Some(guid) =>
if (guid == llu.GUID) {
sessionData.specialItemSlotGuid = None
player.Carrying = None
}
case _ => ;
case Some(guid) if guid == lluGuid =>
sessionData.specialItemSlotGuid = None
player.Carrying = None
case _ => ()
}
case LocalResponse.ObjectDelete(object_guid, unk) =>

View file

@ -132,6 +132,11 @@ class SessionVehicleHandlers(
sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat))
}
case VehicleResponse.ObjectDelete(itemGuid) =>
if (tplayer_guid != guid) {
sendResponse(ObjectDeleteMessage(itemGuid, 0))
}
case VehicleResponse.Ownership(vehicleGuid) =>
if (tplayer_guid == guid) { // Only the player that owns this vehicle needs the ownership packet
avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid))

View file

@ -2,6 +2,8 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.avatar.scoring.EquipmentStat
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
@ -9,7 +11,7 @@ import scala.concurrent.duration._
//
import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor}
import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
import net.psforever.objects.ballistics.{PlayerSource, Projectile, ProjectileQuality, SourceEntry}
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
import net.psforever.objects.entity.SimpleWorldEntity
import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, EquipmentSize, FireModeSwitch}
import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow}
@ -24,7 +26,8 @@ import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.{Zone, ZoneProjectile}
import net.psforever.objects._
import net.psforever.packet.game.{AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponLazeTargetPositionMessage, _}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.packet.game._
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{ExoSuitType, PlanetSideGUID, Vector3}
@ -40,6 +43,7 @@ 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[support] var shotsWhileDead: Int = 0
private val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
@ -112,6 +116,7 @@ private[support] class WeaponAndProjectileOperations(
prefire -= item_guid
shooting += item_guid
shootingStart += item_guid -> System.currentTimeMillis()
ongoingShotsFired = 0
//special case - suppress the decimator's alternate fire mode, by projectile
if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) {
continent.AvatarEvents ! AvatarServiceMessage(
@ -140,6 +145,7 @@ private[support] class WeaponAndProjectileOperations(
prefire -= item_guid
shooting += item_guid
shootingStart += item_guid -> System.currentTimeMillis()
ongoingShotsFired = 0
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.ChangeFireState_Start(player.GUID, item_guid)
@ -168,8 +174,10 @@ private[support] class WeaponAndProjectileOperations(
continent.id,
AvatarAction.ChangeFireState_Start(pguid, item_guid)
)
ongoingShotsFired = ongoingShotsFired + tool.Discharge()
shootingStart += item_guid -> (System.currentTimeMillis() - 1L)
}
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(tool.Definition.ObjectId,ongoingShotsFired,0,0))
tool.FireMode match {
case _: ChargeFireModeDefinition =>
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
@ -410,6 +418,7 @@ private[support] class WeaponAndProjectileOperations(
) =>
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 => ;
}
@ -447,8 +456,9 @@ private[support] class WeaponAndProjectileOperations(
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, target.Position, target)
ResolveProjectileInteraction(projectile, resolution1, target, target.Position) match {
case Some(_projectile) =>
sessionData.handleDealingDamage(target, _projectile)
case Some(resprojectile) =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
}
case _ => ;
@ -459,8 +469,9 @@ private[support] class WeaponAndProjectileOperations(
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos) match {
case Some(_projectile) =>
sessionData.handleDealingDamage(target, _projectile)
case Some(resprojectile) =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
}
case _ => ;
@ -499,8 +510,9 @@ private[support] class WeaponAndProjectileOperations(
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(projectile) =>
sessionData.handleDealingDamage(target, projectile)
case Some(resprojectile) =>
avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(resprojectile.cause.attribution,0,1,0))
sessionData.handleDealingDamage(target, resprojectile)
case None => ;
}
case _ => ;
@ -554,17 +566,29 @@ private[support] class WeaponAndProjectileOperations(
val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position)
if (distanceToOwner <= acceptableDistanceToOwner) {
val projectile_info = tool.Projectile
val projectile =
Projectile(
projectile_info,
tool.Definition,
tool.FireMode,
PlayerSource(player),
attribution,
shotOrigin,
angle,
shotVelocity
)
val wguid = weaponGUID.guid
val mountedIn = (continent.turretToWeapon
.find { case (guid, _) => guid == wguid } match {
case Some((_, turretGuid)) => Some((
turretGuid,
continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
))
case _ => None
}) match {
case Some((guid, Some(entity))) => Some((guid, entity))
case _ => None
}
val projectile = new Projectile(
projectile_info,
tool.Definition,
tool.FireMode,
mountedIn,
PlayerSource(player),
attribution,
shotOrigin,
angle,
shotVelocity
)
val initialQuality = tool.FireMode match {
case mode: ChargeFireModeDefinition =>
ProjectileQuality.Modified(
@ -641,7 +665,7 @@ private[support] class WeaponAndProjectileOperations(
}
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
prefire += weaponGUID
tool.Discharge()
ongoingShotsFired = ongoingShotsFired + tool.Discharge()
}
(o, Some(tool))
}

View file

@ -6,6 +6,10 @@ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import akka.pattern.ask
import akka.util.Timeout
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, ReconstructionActivity, SpawningActivity}
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
@ -37,11 +41,12 @@ import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vehicles._
import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning}
import net.psforever.objects._
import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectCreateMessageParent, PlacementData}
import net.psforever.packet.game.{BeginZoningMessage, DroppodLaunchRequestMessage, ReleaseAvatarRequestMessage, SpawnRequestMessage, WarpgateRequest}
import net.psforever.packet.game.{AvatarAwardMessage, AvatarSearchCriteriaMessage, AvatarStatisticsMessage, AwardCompletion, BindPlayerMessage, BindStatus, CargoMountPointStatusMessage, ChangeShortcutBankMessage, ChatChannel, CreateShortcutMessage, DroppodFreefallingMessage, LoadMapMessage, ObjectCreateDetailedMessage, ObjectDeleteMessage, PlanetsideStringAttributeMessage, PlayerStateShiftMessage, SetChatFilterMessage, SetCurrentAvatarMessage, ShiftState, Statistics}
import net.psforever.packet.game.{AvatarAwardMessage, AvatarSearchCriteriaMessage, AvatarStatisticsMessage, AwardCompletion, BindPlayerMessage, BindStatus, CargoMountPointStatusMessage, ChangeShortcutBankMessage, ChatChannel, CreateShortcutMessage, DroppodFreefallingMessage, LoadMapMessage, ObjectCreateDetailedMessage, ObjectDeleteMessage, PlanetsideStringAttributeMessage, PlayerStateShiftMessage, SetChatFilterMessage, SetCurrentAvatarMessage, ShiftState}
import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, ChatMsg, ContinentalLockUpdateMessage, DeadState, DensityLevelUpdateMessage, DeployRequestMessage, DeployableInfo, DeployableObjectsInfoMessage, DeploymentAction, DisconnectMessage, DroppodError, DroppodLaunchResponseMessage, FriendsResponse, GenericObjectActionMessage, GenericObjectStateMsg, HotSpotUpdateMessage, ObjectAttachMessage, ObjectCreateMessage, PlanetsideAttributeEnum, PlanetsideAttributeMessage, PropertyOverrideMessage, ReplicationStreamMessage, SetEmpireMessage, TimeOfDayMessage, TriggerEffectMessage, ZoneForcedCavernConnectionsMessage, ZoneInfoMessage, ZoneLockInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo}
import net.psforever.packet.game.{BeginZoningMessage, DroppodLaunchRequestMessage, ReleaseAvatarRequestMessage, SpawnRequestMessage, WarpgateRequest}
import net.psforever.packet.game.DeathStatistic
import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectCreateMessageParent, PlacementData}
import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.packet.{PlanetSideGamePacket, game}
import net.psforever.persistence.Savedplayer
import net.psforever.services.RemoverActor
@ -828,6 +833,10 @@ class ZoningOperations(
spawn.LoadZonePhysicalSpawnPoint(zoneId, position, Vector3.Zero, 0 seconds, None)
case _ => // not seated as the driver, in which case we can't move
}
case _ if !player.isAlive =>
Player.Respawn(player)
player.Health = 1
spawn.LoadZonePhysicalSpawnPoint(continent.id, position, Vector3.z(player.Orientation.z), 0.seconds, None)
case None =>
spawn.deadState = DeadState.Release // cancel movement updates
player.Position = position
@ -1162,7 +1171,7 @@ class ZoningOperations(
*/
def LoadZoneAsPlayer(targetPlayer: Player, zoneId: String): Unit = {
log.debug(s"LoadZoneAsPlayer: ${targetPlayer.avatar.name} loading into $zoneId")
if (!zoneReload && zoneId == continent.id) {
if (!zoneReload && zoneId.equals(continent.id)) {
if (player.isBackpack) { // important! test the actor-wide player ref, not the parameter
// respawning from unregistered player
TaskWorkflow.execute(sessionData.registerAvatar(targetPlayer))
@ -1436,6 +1445,10 @@ class ZoningOperations(
Deployables.Disown(continent, avatar, context.self)
spawn.drawDeloyableIcon = spawn.RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone
sessionData.squad.squadSetup = sessionData.squad.ZoneChangeSquadSetup
val lastSeen = sessionData.avatarResponse.lastSeenStreamMessage
lastSeen.indices.foreach { index =>
lastSeen(index) = 0
}
}
/**
@ -2542,17 +2555,26 @@ class ZoningOperations(
nextSpawnPoint = physSpawnPoint
shiftPosition = Some(pos)
shiftOrientation = Some(ori)
val toZoneNumber = if (continent.id.equals(zoneId)) {
continent.Number
} else {
Zones.zones.find { _.id.equals(zoneId) }.orElse(Some(Zone.Nowhere)).get.Number
}
val toSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
respawnTimer = context.system.scheduler.scheduleOnce(respawnTime) {
if (player.isBackpack) { // if the player is dead, he is handled as dead infantry, even if he died in a vehicle
// new player is spawning
val newPlayer = RespawnClone(player)
newPlayer.Position = pos
newPlayer.Orientation = ori
newPlayer.LogActivity(SpawningActivity(PlayerSource(newPlayer), toZoneNumber, toSpawnPoint))
LoadZoneAsPlayer(newPlayer, zoneId)
} else {
avatarActor ! AvatarActor.DeactivateActiveImplants()
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)
LoadZoneInVehicle(vehicle, pos, ori, zoneId)
case _ if player.HasGUID => // player is deconstructing self or instant action
@ -2564,11 +2586,13 @@ class ZoningOperations(
)
player.Position = pos
player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
case _ => //player is logging in
player.Position = pos
player.Orientation = ori
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, toSpawnPoint)
LoadZoneAsPlayer(player, zoneId)
}
}
@ -2698,7 +2722,6 @@ class ZoningOperations(
}
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
//TODO if Medkit does not have shortcut, add to a free slot or write over slot 64
avatar.shortcuts
.zipWithIndex
.collect { case (Some(shortcut), index) =>
@ -2739,7 +2762,7 @@ class ZoningOperations(
}
(0 to 30).foreach(_ => {
//TODO 30 for a new character only?
sendResponse(AvatarStatisticsMessage(2, Statistics(0L)))
sendResponse(AvatarStatisticsMessage(DeathStatistic(0L)))
})
if (tplayer.ExoSuit == ExoSuitType.MAX) {
sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong))
@ -2787,13 +2810,13 @@ class ZoningOperations(
)
)
case (Some(vehicle), Some(0)) =>
//driver; summon any passengers and cargo vehicles left behind on previous continent
//driver of vehicle
if (vehicle.Jammed) {
//TODO something better than just canceling?
vehicle.Actor ! JammableUnit.ClearJammeredStatus()
vehicle.Actor ! JammableUnit.ClearJammeredSound()
}
//positive shield strength
// positive shield strength
if (vehicle.Definition.MaxShields > 0) {
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, vehicle.Definition.shieldUiAttribute, vehicle.Shields))
}
@ -2805,6 +2828,11 @@ class ZoningOperations(
if (vehicle.Definition.MaxCapacitor > 0) {
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 113, vehicle.Capacitor))
}
// vehicle entering zone
if (vehicle.History.headOption.exists { _.isInstanceOf[SpawningActivity] }) {
vehicle.LogActivity(ReconstructionActivity(VehicleSource(vehicle), continent.Number, None))
}
// summon any passengers and cargo vehicles left behind on previous continent
LoadZoneTransferPassengerMessages(
guid,
continent.id,
@ -2825,12 +2853,31 @@ class ZoningOperations(
} else if (originalDeadState == DeadState.Dead || player.Health == 0) {
//killed during spawn setup or possibly a relog into a corpse (by accident?)
player.Actor ! Player.Die()
} else {
AvatarActor.savePlayerData(player)
sessionData.displayCharSavedMsgThenRenewTimer(
Config.app.game.savedMsg.short.fixed,
Config.app.game.savedMsg.short.variable
)
//player
val effortBy = nextSpawnPoint
.collect { case sp: SpawnTube => (sp, continent.GUID(sp.Owner.GUID)) }
.collect {
case (_, Some(v: Vehicle)) => continent.GUID(v.Owner)
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
}
AvatarActor.savePlayerData(player)
sessionData.displayCharSavedMsgThenRenewTimer(
Config.app.game.savedMsg.short.fixed,
Config.app.game.savedMsg.short.variable
)
upstreamMessageCount = 0
setAvatar = true
}

View file

@ -188,6 +188,10 @@ class BuildingActor(
def setup(details: BuildingControlDetails): Behavior[Command] = {
Behaviors.receiveMessage {
case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings))
if listings.isEmpty =>
Behaviors.same
case ReceptionistListing(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
switchToBehavior(details.copy(interstellarCluster = listings.head))

View file

@ -2,7 +2,6 @@ package net.psforever.actors.zone
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.ce.Deployable
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
@ -11,12 +10,13 @@ import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
import net.psforever.objects.{ConstructionItem, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import scala.collection.mutable.ListBuffer
import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.util.Database._
import net.psforever.persistence
import scala.collection.mutable
import scala.util.{Failure, Success}
import scala.concurrent.ExecutionContext.Implicits.global
@ -26,7 +26,7 @@ object ZoneActor {
.supervise[Command] {
Behaviors.setup(context => new ZoneActor(context, zone))
}
.onFailure[Exception](SupervisorStrategy.restart)
.onFailure[Exception](SupervisorStrategy.resume)
sealed trait Command
@ -73,13 +73,13 @@ object ZoneActor {
}
class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
extends AbstractBehavior[ZoneActor.Command](context) {
extends AbstractBehavior[ZoneActor.Command](context) {
import ZoneActor._
import ctx._
private[this] val log = org.log4s.getLogger
val players: ListBuffer[Player] = ListBuffer()
val players: mutable.ListBuffer[Player] = mutable.ListBuffer()
zone.actor = context.self
zone.init(context.toClassic)
@ -102,7 +102,7 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
case Failure(e) => log.error(e.getMessage)
}
override def onMessage(msg: Command): Behavior[Command] = {
def onMessage(msg: Command): Behavior[Command] = {
msg match {
case GetZone(replyTo) =>
replyTo ! ZoneResponse(zone)

View file

@ -38,17 +38,21 @@ case object CavernFacilityLogic
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
entity match {
case terminal: CaptureTerminal =>
val building = details.building
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
details.building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
data match {
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
case _ => log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
})
data match {
case Some(isResecured: Boolean) =>
//pass hack information to amenities
building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
})
case _ =>
log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
details.building.HackableAmenities.foreach(amenity => {
building.HackableAmenities.foreach(amenity => {
if (amenity.HackedBy.isDefined) {
details.building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
}
})
// No map update needed - will be sent by `HackCaptureActor` when required

View file

@ -38,17 +38,21 @@ case object FacilityLogic
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
entity match {
case terminal: CaptureTerminal =>
val building = details.building
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
details.building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
data match {
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
case _ => log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
})
data match {
case Some(isResecured: Boolean) =>
//pass hack information to amenities
building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
})
case _ =>
log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
details.building.HackableAmenities.foreach(amenity => {
building.HackableAmenities.foreach(amenity => {
if (amenity.HackedBy.isDefined) {
details.building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id, LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
}
})
// No map update needed - will be sent by `HackCaptureActor` when required

View file

@ -9,6 +9,7 @@ import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneAct
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.services.{InterstellarClusterService, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
@ -164,17 +165,21 @@ case object MajorFacilityLogic
details.building.Zone.actor ! ZoneActor.ZoneMapUpdate()
}
case terminal: CaptureTerminal =>
val building = details.building
// Notify amenities that listen for CC hack state changes, e.g. wall turrets to dismount seated players
details.building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
data match {
case Some(isResecured: Boolean) => amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
case _ => log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
})
data match {
case Some(isResecured: Boolean) =>
//pass hack information to amenities
building.Amenities.filter(x => x.isInstanceOf[CaptureTerminalAware]).foreach(amenity => {
amenity.Actor ! CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured)
})
case _ =>
log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
// When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
details.building.HackableAmenities.foreach(amenity => {
building.HackableAmenities.foreach(amenity => {
if (amenity.HackedBy.isDefined) {
details.building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
}
})
// No map update needed - will be sent by `HackCaptureActor` when required
@ -330,6 +335,14 @@ case object MajorFacilityLogic
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
alignForceDomeStatus(details)
val bldg = details.building
//the presence of the flag means that we are involved in an ongoing llu hack
(bldg.GetFlag, bldg.CaptureTerminal) match {
case (Some(flag), Some(terminal)) if (flag.Target eq building) && flag.Faction != building.Faction =>
//our hack destination may have been compromised and the hack needs to be cancelled
bldg.Zone.LocalEvents ! LocalServiceMessage("", LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody))
case _ => ()
}
Behaviors.same
}

View file

@ -9,7 +9,10 @@ import net.psforever.objects.guid._
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.sourcing.AmenitySource
import net.psforever.objects.vital.TerminalUsedActivity
import net.psforever.objects.zones.Zone
import net.psforever.types.{ExoSuitType, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.services.Service
@ -31,7 +34,8 @@ object WorldSession {
* @return 1 for `true`; 0 for `false`
*/
implicit def boolToInt(b: Boolean): Int = if (b) 1 else 0
private implicit val timeout = new Timeout(5000 milliseconds)
private implicit val timeout: Timeout = new Timeout(5000 milliseconds)
/**
* Use this for placing equipment that has already been registered into a container,
@ -185,7 +189,6 @@ object WorldSession {
val localZone = obj.Zone
TaskBundle(
new StraightforwardTask() {
private val localContainer = obj
private val localItem = item
private val localSlot = slot
private val localFunc: (Equipment, Int) => Future[Any] = PutEquipmentInInventorySlot(obj)
@ -556,10 +559,12 @@ object WorldSession {
val tile = item.Definition.Tile
destination.Inventory.CheckCollisionsVar(dest, tile.Width, tile.Height)
} match {
case Success(Nil) =>
case Success(Nil)
if ContainableBehavior.PermitEquipmentStow(destination, item, dest) =>
//no swap item
(true, None)
case Success(List(swapEntry: InventoryItem)) =>
case Success(List(swapEntry: InventoryItem))
if ContainableBehavior.PermitEquipmentStow(destination, item, dest) =>
//the swap item is to be registered to the source's zone
(true, Some(swapEntry.obj.GUID))
case _ =>
@ -568,13 +573,13 @@ object WorldSession {
}
if (performSwap) {
def moveItemTaskFunc(toSlot: Int): Task = new StraightforwardTask() {
val localGUID = swapItemGUID //the swap item's original GUID, if any swap item
val localChannel = toChannel
val localSource = source
val localDestination = destination
val localItem = item
val localDestSlot = dest
val localSrcSlot = toSlot
val localGUID: Option[PlanetSideGUID] = swapItemGUID //the swap item's original GUID, if any swap item
val localChannel: String = toChannel
val localSource: PlanetSideServerObject with Container = source
val localDestination: PlanetSideServerObject with Container = destination
val localItem: Equipment = item
val localDestSlot: Int = dest
val localSrcSlot: Int = toSlot
val localMoveOnComplete: Try[Any] => Unit = {
case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) =>
//swapItem is not registered right now, we can not drop the item without re-registering it
@ -673,13 +678,13 @@ object WorldSession {
}
if (performSwap) {
def moveItemTaskFunc(toSlot: Int): Task = new StraightforwardTask() {
val localGUID = swapItemGUID //the swap item's original GUID, if any swap item
val localChannel = toChannel
val localSource = source
val localDestination = destination
val localItem = item
val localDestSlot = dest
val localSrcSlot = toSlot
val localGUID: Option[PlanetSideGUID] = swapItemGUID //the swap item's original GUID, if any swap item
val localChannel: String = toChannel
val localSource: PlanetSideServerObject with Container = source
val localDestination: PlanetSideServerObject with Container = destination
val localItem: Equipment = item
val localDestSlot: Int = dest
val localSrcSlot: Int = toSlot
val localMoveOnComplete: Try[Any] => Unit = {
case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) =>
//swapItem is not registered right now, we can not drop the item without re-registering it
@ -936,6 +941,11 @@ object WorldSession {
def TerminalResult(guid: PlanetSideGUID, player: Player, transaction: TransactionType.Value)(
result: Boolean
): Unit = {
if (result) {
player.Zone.GUID(guid).collect {
case term: Terminal => player.LogActivity(TerminalUsedActivity(AmenitySource(term), transaction))
}
}
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(guid, transaction, result)

View file

@ -2,10 +2,10 @@
package net.psforever.objects
import akka.actor.{ActorContext, Props}
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.etc.TriggerUsedReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.Zone
@ -35,7 +35,7 @@ class BoomerDeployable(cdef: ExplosiveDeployableDefinition)
}
class BoomerDeployableDefinition(private val objectId: Int) extends ExplosiveDeployableDefinition(objectId) {
override def Initialize(obj: Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext): Unit = {
obj.Actor =
context.actorOf(Props(classOf[BoomerDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
@ -97,7 +97,7 @@ class BoomerDeployableControl(mine: BoomerDeployable)
}
zone.AvatarEvents! AvatarServiceMessage(
zone.id,
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, trigger.GUID)
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, guid)
)
TaskWorkflow.execute(GUIDTask.unregisterObject(zone.GUID, trigger))
case None => ;

View file

@ -53,7 +53,7 @@ private class DefinitionWrappedInVitality(definition: ObjectDefinition)
case p: ProjectileDefinition => p
case _ => GlobalDefinitions.no_projectile
}
Name = { definition.Name }
DefaultHealth = 1 //just cuz
}

View file

@ -2,7 +2,6 @@
package net.psforever.objects
import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.objects.ballistics.{DeployableSource, PlayerSource, SourceEntry}
import net.psforever.objects.ce._
import net.psforever.objects.definition.{DeployableDefinition, ExoSuitDefinition}
import net.psforever.objects.definition.converter.SmallDeployableConverter
@ -12,13 +11,14 @@ import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity}
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, SourceEntry, UniquePlayer}
import net.psforever.objects.vital.etc.TrippedMineReason
import net.psforever.objects.vital.resolution.ResolutionCalculations.Output
import net.psforever.objects.vital.{SimpleResolutions, Vitality}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.Zone
import net.psforever.types.{ExoSuitType, Vector3}
import net.psforever.types.{CharacterSex, ExoSuitType, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -127,7 +127,7 @@ object ExplosiveDeployableControl {
* @param damage na
*/
def DamageResolution(target: ExplosiveDeployable, cause: DamageResult, damage: Int): Unit = {
target.History(cause)
target.LogActivity(cause)
if (cause.interaction.cause.source.SympatheticExplosion) {
explodes(target, cause)
DestructionAwareness(target, cause)
@ -324,32 +324,32 @@ object MineDeployableControl {
val deployableSource = DeployableSource(mine)
val blame = mine.OwnerName match {
case Some(name) =>
val(charId, exosuit, seated): (Long, ExoSuitType.Value, Boolean) = mine.Zone
val(charId, exosuit, seatedIn): (Long, ExoSuitType.Value, Option[(SourceEntry, Int)]) = mine.Zone
.LivePlayers
.find { _.Name.equals(name) } match {
case Some(player) =>
//if the owner is alive in the same zone as the mine, use data from their body to create the source
(player.CharId, player.ExoSuit, player.VehicleSeated.nonEmpty)
(player.CharId, player.ExoSuit, PlayerSource.mountableAndSeat(player))
case None =>
//if the owner is as dead as a corpse or is not in the same zone as the mine, use defaults
(0L, ExoSuitType.Standard, false)
(0L, ExoSuitType.Standard, None)
}
val faction = mine.Faction
PlayerSource(
name,
charId,
GlobalDefinitions.avatar,
mine.Faction,
exosuit,
seated,
100,
100,
seatedIn,
health = 100,
armor = 0,
mine.Position,
Vector3.Zero,
None,
crouching = false,
jumping = false,
ExoSuitDefinition.Select(exosuit, faction)
ExoSuitDefinition.Select(exosuit, faction),
bep = 0,
kills = Nil,
UniquePlayer(charId, name, CharacterSex.Male, mine.Faction)
)
case None =>
//credit where credit is due

View file

@ -5647,10 +5647,10 @@ object GlobalDefinitions {
advanced_ace.Name = "advanced_ace"
advanced_ace.Size = EquipmentSize.Rifle
advanced_ace.Modes += new ConstructionFireMode {
Item(DeployedItem.portable_manned_turret, Set(Certification.AssaultEngineering))
Item(DeployedItem.tank_traps, Set(Certification.FortificationEngineering))
}
advanced_ace.Modes += new ConstructionFireMode {
Item(DeployedItem.tank_traps, Set(Certification.FortificationEngineering))
Item(DeployedItem.portable_manned_turret, Set(Certification.AssaultEngineering))
}
advanced_ace.Modes += new ConstructionFireMode {
Item(DeployedItem.deployable_shield_generator, Set(Certification.AssaultEngineering))
@ -9841,14 +9841,17 @@ object GlobalDefinitions {
capture_terminal.Name = "capture_terminal"
capture_terminal.Damageable = false
capture_terminal.Repairable = false
capture_terminal.FacilityHackTime = 15.minutes
secondary_capture.Name = "secondary_capture"
secondary_capture.Damageable = false
secondary_capture.Repairable = false
secondary_capture.FacilityHackTime = 1.nanosecond
vanu_control_console.Name = "vanu_control_console"
vanu_control_console.Damageable = false
vanu_control_console.Repairable = false
vanu_control_console.FacilityHackTime = 10.minutes
lodestar_repair_terminal.Name = "lodestar_repair_terminal"
lodestar_repair_terminal.Interval = 1000

View file

@ -4,8 +4,9 @@ package net.psforever.objects
import net.psforever.types.PlanetSideGUID
trait OwnableByPlayer {
private var owner: Option[PlanetSideGUID] = None
private var ownerName: Option[String] = None
private var owner: Option[PlanetSideGUID] = None
private var ownerName: Option[String] = None
private var originalOwnerName: Option[String] = None
def Owner: Option[PlanetSideGUID] = owner
@ -33,24 +34,27 @@ trait OwnableByPlayer {
owner match {
case Some(_) =>
ownerName = owner
originalOwnerName = originalOwnerName.orElse(owner)
case None =>
ownerName = None
}
OwnerName
}
def OriginalOwnerName: Option[String] = originalOwnerName
/**
* na
* @param player na
* @return na
*/
* na
* @param player na
* @return na
*/
def AssignOwnership(player: Player): OwnableByPlayer = AssignOwnership(Some(player))
/**
* na
* @param playerOpt na
* @return na
*/
* na
* @param playerOpt na
* @return na
*/
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
playerOpt match {
case Some(player) =>

View file

@ -72,7 +72,6 @@ class Player(var avatar: Avatar)
var spectator: Boolean = false
var silenced: Boolean = false
var death_by: Int = 0
var lastSeenStreamMessage: Array[Long] = Array.fill[Long](65535)(0L)
var lastShotSeq_time: Int = -1
/** From PlanetsideAttributeMessage */

View file

@ -60,12 +60,14 @@ object Players {
* @param medic the name of the player doing the reviving
* @param item the tool being used to revive the target player
*/
def FinishRevivingPlayer(target: Player, medic: String, item: Tool)(): Unit = {
def FinishRevivingPlayer(target: Player, medic: Player, item: Tool)(): Unit = {
val name = target.Name
log.info(s"$medic had revived $name")
val medicName = medic.Name
log.info(s"$medicName had revived $name")
//target.History(PlayerRespawn(PlayerSource(target), target.Zone, target.Position, Some(PlayerSource(medic))))
val magazine = item.Discharge(Some(25))
target.Zone.AvatarEvents ! AvatarServiceMessage(
medic,
medicName,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine)

View file

@ -89,7 +89,7 @@ class ShieldGeneratorControl(gen: ShieldGeneratorDeployable)
val damageToShields = originalShields - shields
val damage = damageToHealth + damageToShields
if (WillAffectTarget(target, damage, cause)) {
target.History(cause)
target.LogActivity(cause)
DamageLog(
target,
s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields"

View file

@ -1,11 +1,11 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.equipment.{EffectTarget, TargetValidation}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.etc.EmpReason

View file

@ -590,13 +590,6 @@ object Vehicle {
*/
final case class Reactivate()
/**
* A request has been made to charge this vehicle's shields.
* @see `FacilityBenefitShieldChargeRequestMessage`
* @param amount the number of points to charge
*/
final case class ChargeShields(amount: Int)
/**
* Following a successful shield charge tick, display the results of the update.
* @see `FacilityBenefitShieldChargeRequestMessage`

View file

@ -2,13 +2,14 @@
package net.psforever.objects.avatar
import net.psforever.actors.session.AvatarActor
import net.psforever.objects.avatar.scoring.ScoreCard
import net.psforever.objects.definition.{AvatarDefinition, BasicDefinition}
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot}
import net.psforever.objects.inventory.LocallyRegisteredInventory
import net.psforever.objects.loadouts.{Loadout, SquadLoadout}
import net.psforever.objects.locker.{LockerContainer, LockerEquipment}
import net.psforever.objects.{GlobalDefinitions, OffhandEquipmentSlot}
import net.psforever.packet.game.objectcreate.RibbonBars
import net.psforever.packet.game.objectcreate.{BasicCharacterData, RibbonBars}
import net.psforever.types._
import org.joda.time.{Duration, LocalDateTime, Seconds}
@ -74,6 +75,10 @@ object Avatar {
GlobalDefinitions.super_staminakit -> 5.minutes // Temporary - Default value is 20 minutes
)
def apply(charId: Int, name: String, faction: PlanetSideEmpire.Value, sex: CharacterSex, head: Int, voice: CharacterVoice.Value): Avatar = {
Avatar(charId, BasicCharacterData(name, faction, sex, head, voice))
}
def makeLocker(): LockerContainer = {
new LockerContainer({
val inv = new LocallyRegisteredInventory(numbers = 40150 until 40450) // TODO var bad
@ -113,11 +118,7 @@ case class MemberLists(
case class Avatar(
/** unique identifier corresponding to a database table row index */
id: Int,
name: String,
faction: PlanetSideEmpire.Value,
sex: CharacterSex,
head: Int,
voice: CharacterVoice.Value,
basic: BasicCharacterData,
bep: Long = 0,
cep: Long = 0,
stamina: Int = 100,
@ -132,7 +133,8 @@ case class Avatar(
decoration: ProgressDecoration = ProgressDecoration(),
loadouts: Loadouts = Loadouts(),
cooldowns: Cooldowns = Cooldowns(),
people: MemberLists = MemberLists()
people: MemberLists = MemberLists(),
scorecard: ScoreCard = new ScoreCard()
) {
assert(bep >= 0)
assert(cep >= 0)
@ -140,6 +142,16 @@ case class Avatar(
val br: BattleRank = BattleRank.withExperience(bep)
val cr: CommandRank = CommandRank.withExperience(cep)
def name: String = basic.name
def faction: PlanetSideEmpire.Value = basic.faction
def sex: CharacterSex = basic.sex
def head: Int = basic.head
def voice: CharacterVoice.Value = basic.voice
private def cooldown(
times: Map[String, LocalDateTime],
cooldowns: Map[BasicDefinition, FiniteDuration],

View file

@ -4,8 +4,7 @@ package net.psforever.objects.avatar
import akka.actor.{Actor, ActorRef, Props, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
import net.psforever.objects.{Player, _}
import net.psforever.objects.ballistics.PlayerSource
import net.psforever.objects._
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.DeployAnimation
import net.psforever.objects.definition.converter.OCM
@ -33,6 +32,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.vital.collision.CollisionReason
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason}
@ -50,21 +50,21 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
with AggravatedBehavior
with AuraEffectBehavior
with RespondsToZoneEnvironment {
def JammableObject = player
def JammableObject: Player = player
def DamageableObject = player
def DamageableObject: Player = player
def ContainerObject = player
def ContainerObject: Player = player
def AggravatedObject = player
def AggravatedObject: Player = player
def AuraTargetObject = player
def AuraTargetObject: Player = player
ApplicableEffect(Aura.Plasma)
ApplicableEffect(Aura.Napalm)
ApplicableEffect(Aura.Comet)
ApplicableEffect(Aura.Fire)
def InteractiveObject = player
def InteractiveObject: Player = player
SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
SetInteraction(EnvironmentAttribute.Death, doInteractingWithDeath)
@ -105,8 +105,18 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case Player.Die(Some(reason)) =>
if (player.isAlive) {
//primary death
PerformDamage(player, reason.calculate())
suicide()
val health = player.Health
val psource = PlayerSource(player)
player.Health = 0
HandleDamage(
player,
DamageResult(psource, psource.copy(health = 0), reason),
health,
damageToArmor = 0,
damageToStamina = 0,
damageToCapacitor = 0
)
damageLog.info(s"${player.Name}-infantry: dead by explicit reason - ${reason.cause.resolution}")
}
case Player.Die(None) =>
@ -138,12 +148,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
)
)
events ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth))
player.History(
player.LogActivity(
HealFromEquipment(
PlayerSource(player),
PlayerSource(user),
newHealth - originalHealth,
GlobalDefinitions.medicalapplicator
GlobalDefinitions.medicalapplicator,
newHealth - originalHealth
)
)
}
@ -172,7 +181,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
) {
sender() ! CommonMessages.Progress(
4,
Players.FinishRevivingPlayer(player, user.Name, item),
Players.FinishRevivingPlayer(player, user, item),
Players.RevivingTickAction(player, user, item)
)
}
@ -202,12 +211,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
)
)
events ! AvatarServiceMessage(zone.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, player.Armor))
player.History(
player.LogActivity(
RepairFromEquipment(
PlayerSource(player),
PlayerSource(user),
newArmor - originalArmor,
GlobalDefinitions.bank
GlobalDefinitions.bank,
newArmor - originalArmor
)
)
}
@ -242,7 +250,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (player.Health == player.MaxHealth) {
(None, 0, 0, "@HealComplete")
} else {
player.History(HealFromKit(PlayerSource(player), 25, kdef))
player.LogActivity(HealFromKit(kdef, 25))
player.Health = player.Health + 25
(Some(index), 0, player.Health, "")
}
@ -250,7 +258,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (player.Health == player.MaxHealth) {
(None, 0, 0, "@HealComplete")
} else {
player.History(HealFromKit(PlayerSource(player), 100, kdef))
player.LogActivity(HealFromKit(kdef, 100))
player.Health = player.Health + 100
(Some(index), 0, player.Health, "")
}
@ -258,7 +266,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (player.Armor == player.MaxArmor) {
(None, 0, 0, "Armor at maximum - No repairing required.")
} else {
player.History(RepairFromKit(PlayerSource(player), 200, kdef))
player.LogActivity(RepairFromKit(kdef, 200))
player.Armor = player.Armor + 200
(Some(index), 4, player.Armor, "")
}
@ -426,13 +434,14 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val originalArmor = player.Armor
player.ExoSuit = nextSuit
val toMaxArmor = player.MaxArmor
val toArmor =
val toArmor = {
if (originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), nextSuit))
player.LogActivity(RepairFromExoSuitChange(nextSuit, toMaxArmor - player.Armor))
player.Armor = toMaxArmor
} else {
player.Armor = originalArmor
}
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
@ -488,9 +497,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
)
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, true)
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result=true)
)
case _ => assert(false, msg.toString)
case _ => assert(assertion=false, msg.toString)
}
case Zone.Ground.ItemOnGround(item, _, _) =>
@ -528,8 +537,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val trigger = new BoomerTrigger
trigger.Companion = obj.GUID
obj.Trigger = trigger
//TODO sufficiently delete the tool
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.ObjectDelete(player.GUID, tool.GUID))
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, tool.GUID))
TaskWorkflow.execute(GUIDTask.unregisterEquipment(zone.GUID, tool))
player.Find(tool) match {
case Some(index) if player.VisibleSlots.contains(index) =>
@ -593,19 +601,18 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val originalSubtype = Loadout.DetermineSubtype(player)
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) =>
false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
}
else {
true
})
(if (exosuit == ExoSuitType.MAX) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) =>
false
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
} else {
true
})
if (requestToChangeArmor && allowedToChangeArmor) {
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
@ -614,13 +621,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
val originalArmor = player.Armor
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
val toMaxArmor = player.MaxArmor
val toArmor = if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
player.History(HealFromExoSuitChange(PlayerSource(player), exosuit))
player.Armor = toMaxArmor
}
else {
player.Armor = originalArmor
val toArmor = toMaxArmor
if (originalArmor != toMaxArmor) {
player.LogActivity(RepairFromExoSuitChange(exosuit, toMaxArmor - originalArmor))
}
player.Armor = toMaxArmor
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
@ -804,7 +809,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
})) {
//activate second wind
player.Health += 25
player.History(HealFromImplant(PlayerSource(player), 25, ImplantType.SecondWind))
player.LogActivity(HealFromImplant(ImplantType.SecondWind, 25))
avatarActor ! AvatarActor.ResetImplant(ImplantType.SecondWind)
avatarActor ! AvatarActor.RestoreStamina(25)
}
@ -844,7 +849,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
cause.interaction.cause.source.Aggravated.nonEmpty
}
//log historical event (always)
target.History(cause)
target.LogActivity(cause)
//stat changes
if (damageToCapacitor > 0) {
events ! AvatarServiceMessage(
@ -959,7 +964,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
avatarActor ! AvatarActor.DeinitializeImplants()
//log historical event
target.History(cause)
target.LogActivity(cause)
//log message
cause.adversarial match {
case Some(a) =>
@ -1015,7 +1020,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
nameChannel,
AvatarAction.SendResponse(
Service.defaultPlayerGUID,
AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, target.Faction, true)
AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, target.Faction, unk5=true)
)
)
//TODO other methods of death?

View file

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

View file

@ -0,0 +1,19 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.avatar.scoring
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.interaction.DamageResult
import org.joda.time.LocalDateTime
trait KDAStat {
def experienceEarned: Long
val time: LocalDateTime = LocalDateTime.now()
}
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 Death(assailant: Seq[PlayerSource], timeAlive: Long, bep: Long) extends KDAStat {
def experienceEarned: Long = 0
}

View file

@ -0,0 +1,17 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.avatar.scoring
final case class Life(
kills: Seq[Kill],
assists: Seq[Assist],
death: Option[Death],
equipmentStats: Seq[EquipmentStat]
)
object Life {
def apply(): Life = Life(Nil, Nil, None, Nil)
def bep(life: Life): Long = {
life.kills.foldLeft(0L)(_ + _.experienceEarned) + life.assists.foldLeft(0L)(_ + _.experienceEarned)
}
}

View file

@ -0,0 +1,149 @@
// Copyright (c) 2023 PSForever
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 scala.annotation.tailrec
import scala.collection.mutable
class ScoreCard() {
private var curr: Life = Life()
private var lives: Seq[Life] = Seq()
private val killStatistics: mutable.HashMap[Int, Statistic] = mutable.HashMap[Int, Statistic]()
private val assistStatistics: mutable.HashMap[Int, Statistic] = mutable.HashMap[Int, Statistic]()
def CurrentLife: Life = curr
def Lives: Seq[Life] = lives
def KillStatistics: Map[Int, Statistic] = killStatistics.toMap
def AssistStatistics: Map[Int, Statistic] = assistStatistics.toMap
def rate(msg: Any): Unit = {
msg match {
case e: EquipmentStat =>
curr = ScoreCard.updateEquipmentStat(curr, e)
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)
case a: Assist =>
curr = curr.copy(assists = a +: curr.assists)
val faction = a.victim.Faction
a.weapons.foreach { wid =>
ScoreCard.updateStatisticsFor(assistStatistics, wid, faction)
}
case d: Death =>
val expired = curr
curr = Life()
lives = expired.copy(death = Some(d)) +: lives
case _ => ;
}
}
}
object ScoreCard {
private def updateEquipmentStat(curr: Life, entry: EquipmentStat): Life = {
updateEquipmentStat(curr, entry, entry.objectId, entry.kills)
}
private def updateEquipmentStat(
curr: Life,
entry: EquipmentStat,
objectId: Int,
killCount: Int
): Life = {
curr.equipmentStats.indexWhere { a => a.objectId == objectId } match {
case -1 =>
curr.copy(equipmentStats = entry +: curr.equipmentStats)
case index =>
val stats = curr.equipmentStats
val old = stats(index)
curr.copy(
equipmentStats = (stats.take(index) :+ old.copy(
shotsFired = old.shotsFired + entry.shotsFired,
shotsLanded = old.shotsLanded + entry.shotsLanded,
kills = old.kills + killCount
)) ++ stats.drop(index+1)
)
}
}
@tailrec
private def updateStatisticsFor(
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_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)
}
outEntry
case _ =>
val out = Statistic(0, 0, 0, 0, 0, 0, 0, 0)
statisticMap.put(objectId, out)
updateStatisticsFor(statisticMap, objectId, victimFaction)
}
}
def weaponObjectIdMap(objectId: Int): Int = {
objectId match {
//aphelion
case 81 | 82 => 80
case 90 | 92 => 88
case 94 | 95 => 93
case 102 | 104 => 100
case 107 | 109 => 105
//colossus
case 183 | 184 => 182
case 187 | 189 => 185
case 192 | 194 => 190
case 202 | 203 => 201
case 206 | 208 => 204
//cycler
case 234 | 235 | 236 => 233
//peregrine
case 634 | 635 => 633
case 638 | 640 => 636
case 646 | 648 => 644
case 650 | 651 => 649
case 660 | 662 => 658
//eh
case _ => objectId
}
}
def rewardKillGetCategories(victim: SourceEntry): Seq[StatisticalCategory] = {
victim match {
case p: PlayerSource =>
p.seatedIn match {
case Some((v: VehicleSource, seat: Int)) =>
val seatCategory = if (seat == 0) {
StatisticalCategory.DriverKilled
} else {
v.Definition.controlledWeapons().get(seat) match {
case Some(_) => StatisticalCategory.GunnerKilled
case None => StatisticalCategory.PassengerKilled
}
}
if (GlobalDefinitions.isFlightVehicle(v.Definition)) {
Seq(StatisticalCategory.Dogfighter, seatCategory)
} else {
Seq(seatCategory)
}
case _ =>
Seq(StatisticalCategory.Destroyed)
}
case _ =>
Seq(StatisticalCategory.Destroyed)
}
}
}

View file

@ -0,0 +1,4 @@
// 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)

View file

@ -1,49 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.{DeployableDefinition, ObjectDefinition}
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class DeployableSource(
obj_def: ObjectDefinition with DeployableDefinition,
faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
owner: SourceEntry,
position: Vector3,
orientation: Vector3
) extends SourceEntry {
override def Name = obj_def.Descriptor
override def Faction = faction
def Definition: ObjectDefinition with DeployableDefinition = obj_def
def Health = health
def Shields = shields
def OwnerName = owner.Name
def Position = position
def Orientation = orientation
def Velocity = None
def Modifiers = obj_def.asInstanceOf[ResistanceProfile]
}
object DeployableSource {
def apply(obj: Deployable): DeployableSource = {
val ownerName = obj.OwnerName
val ownerSource = (obj.Zone.LivePlayers ++ obj.Zone.Corpses)
.find { p => ownerName.contains(p.Name) }
match {
case Some(p) => SourceEntry(p)
case _ => SourceEntry.None
}
DeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
ownerSource,
obj.Position,
obj.Orientation
)
}
}

View file

@ -2,6 +2,7 @@
package net.psforever.objects.ballistics
import net.psforever.objects.Player
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
import net.psforever.objects.vital.etc.RadiationReason

View file

@ -1,85 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class ObjectSource(
obj: PlanetSideGameObject,
faction: PlanetSideEmpire.Value,
position: Vector3,
orientation: Vector3,
velocity: Option[Vector3]
) extends SourceEntry {
private val definition = obj.Definition match {
case vital : VitalityDefinition => vital
case genericDefinition => NonvitalDefinition(genericDefinition)
}
private val modifiers = definition match {
case nonvital : NonvitalDefinition => nonvital
case _ => ObjectSource.FixedResistances
}
override def Name = SourceEntry.NameFormat(obj.Definition.Name)
override def Faction = faction
def Definition = definition
def Position = position
def Orientation = orientation
def Velocity = velocity
def Modifiers = modifiers
}
object ObjectSource {
final val FixedResistances = new ResistanceProfileMutators() { }
def apply(obj: PlanetSideGameObject): ObjectSource = {
ObjectSource(
obj,
obj match {
case aligned: FactionAffinity => aligned.Faction
case _ => PlanetSideEmpire.NEUTRAL
},
obj.Position,
obj.Orientation,
obj.Velocity
)
}
}
/**
* A wrapper for a definition that does not represent a `Vitality` object.
* @param definition the original definition
*/
class NonvitalDefinition(private val definition : ObjectDefinition)
extends ObjectDefinition(definition.ObjectId)
with ResistanceProfileMutators
with VitalityDefinition {
Name = { definition.Name }
Packet = { definition.Packet }
def canEqual(a: Any) : Boolean = a.isInstanceOf[definition.type]
override def equals(that: Any): Boolean = definition.equals(that)
override def hashCode: Int = definition.hashCode
}
object NonvitalDefinition {
//single point of contact for all wrapped definitions
private val storage: scala.collection.mutable.LongMap[NonvitalDefinition] =
new scala.collection.mutable.LongMap[NonvitalDefinition]()
def apply(definition : ObjectDefinition) : NonvitalDefinition = {
storage.get(definition.ObjectId) match {
case Some(existing) =>
existing
case None =>
val out = new NonvitalDefinition(definition)
storage += definition.ObjectId.toLong -> out
out
}
}
}

View file

@ -1,58 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.Player
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{ExoSuitType, PlanetSideEmpire, Vector3}
final case class PlayerSource(
name: String,
char_id: Long,
obj_def: AvatarDefinition,
faction: PlanetSideEmpire.Value,
exosuit: ExoSuitType.Value,
seated: Boolean,
health: Int,
armor: Int,
position: Vector3,
orientation: Vector3,
velocity: Option[Vector3],
crouching: Boolean,
jumping: Boolean,
modifiers: ResistanceProfile
) extends SourceEntry {
override def Name = name
override def Faction = faction
override def CharId = char_id
def Definition = obj_def
def ExoSuit = exosuit
def Seated = seated
def Health = health
def Armor = armor
def Position = position
def Orientation = orientation
def Velocity = velocity
def Modifiers = modifiers
}
object PlayerSource {
def apply(tplayer: Player): PlayerSource = {
PlayerSource(
tplayer.Name,
tplayer.CharId,
tplayer.Definition,
tplayer.Faction,
tplayer.ExoSuit,
tplayer.VehicleSeated.nonEmpty,
tplayer.Health,
tplayer.Armor,
tplayer.Position,
tplayer.Orientation,
tplayer.Velocity,
tplayer.Crouching,
tplayer.Jumping,
ExoSuitDefinition.Select(tplayer.ExoSuit, tplayer.Faction)
)
}
}

View file

@ -1,8 +1,10 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import java.util.concurrent.atomic.AtomicLong
import net.psforever.objects.sourcing.SourceEntry
import java.util.concurrent.atomic.AtomicLong
//
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ProjectileDefinition, ToolDefinition}
import net.psforever.objects.entity.SimpleWorldEntity
@ -13,42 +15,44 @@ import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.types.Vector3
/**
* A summation of weapon discharge.
* @see `ProjectileDefinition`
* @see `ToolDefinition`
* @see `FireModeDefinition`
* @see `SourceEntry`
* @see `PlayerSource`
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile;
* most often a player (`PlayerSource`)
* @param attribute_to an object ID that refers to the method of death that would be reported;
* usually the same as `tool_def.ObjectId`;
* if not, then it is a type of vehicle (and owner should have a positive `seated` field)
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @param shot_velocity the initial velocity coordinates of the projectile according to its world directions
* @param quality na
* @param id an exclusive identifier for this projectile;
* normally generated internally, but can be manually set (for modifying a continuous projectile reference)
* @param fire_time when the weapon discharged was recorded;
* defaults to `System.currentTimeMillis()`
*/
* A summation of weapon discharge.
* @see `ProjectileDefinition`
* @see `ToolDefinition`
* @see `FireModeDefinition`
* @see `SourceEntry`
* @see `PlayerSource`
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param mounted_in na
* @param owner the agency that caused the weapon to produce this projectile;
* most often a player (`PlayerSource`)
* @param attribute_to an object ID that refers to the method of death that would be reported;
* usually the same as `tool_def.ObjectId`;
* if not, then it is a type of vehicle (and owner should have a positive `seated` field)
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @param shot_velocity the initial velocity coordinates of the projectile according to its world directions
* @param quality na
* @param id an exclusive identifier for this projectile;
* normally generated internally, but can be manually set (for modifying a continuous projectile reference)
* @param fire_time when the weapon discharged was recorded;
* defaults to `System.currentTimeMillis()`
*/
final case class Projectile(
profile: ProjectileDefinition,
tool_def: ToolDefinition,
fire_mode: FireModeDefinition,
owner: SourceEntry,
attribute_to: Int,
shot_origin: Vector3,
shot_angle: Vector3,
shot_velocity: Option[Vector3],
quality: ProjectileQuality = ProjectileQuality.Normal,
id: Long = Projectile.idGenerator.getAndIncrement(),
fire_time: Long = System.currentTimeMillis()
) extends PlanetSideGameObject
profile: ProjectileDefinition,
tool_def: ToolDefinition,
fire_mode: FireModeDefinition,
mounted_in: Option[(Int, SourceEntry)],
owner: SourceEntry,
attribute_to: Int,
shot_origin: Vector3,
shot_angle: Vector3,
shot_velocity: Option[Vector3],
quality: ProjectileQuality = ProjectileQuality.Normal,
id: Long = Projectile.idGenerator.getAndIncrement(),
fire_time: Long = System.currentTimeMillis()
) extends PlanetSideGameObject
with BlockMapEntity {
Position = shot_origin
Orientation = shot_angle
@ -66,18 +70,19 @@ final case class Projectile(
private var resolved: DamageResolution.Value = DamageResolution.Unresolved
/**
* Create a copy of this projectile with all the same information
* save for the quality.
* Used mainly for aggravated damage.
* It is important to note that the new projectile shares the (otherwise) exclusive id of the original.
* @param value the new quality
* @return a new `Projectile` entity
*/
* Create a copy of this projectile with all the same information
* save for the quality.
* Used mainly for aggravated damage.
* It is important to note that the new projectile shares the (otherwise) exclusive id of the original.
* @param value the new quality
* @return a new `Projectile` entity
*/
def quality(value: ProjectileQuality): Projectile = {
val projectile = Projectile(
val projectile = new Projectile(
profile,
tool_def,
fire_mode,
mounted_in,
owner,
attribute_to,
shot_origin,
@ -93,8 +98,8 @@ final case class Projectile(
}
/**
* Mark the projectile as being "encountered" or "managed" at least once.
*/
* Mark the projectile as being "encountered" or "managed" at least once.
*/
def Resolve(): Unit = {
resolved = DamageResolution.Resolved
}
@ -107,7 +112,7 @@ final case class Projectile(
def isMiss: Boolean = resolved == DamageResolution.Missed
def Definition = profile
def Definition: ProjectileDefinition = profile
}
object Projectile {
@ -116,22 +121,22 @@ object Projectile {
final val baseUID: Int = 40100
/** all clients progress through 40100 to 40124 normally, skipping only for long-lived projectiles
* 40125 to 40149 are being reserved as a guard against undetected overflow
*/
* 40125 to 40149 are being reserved as a guard against undetected overflow
*/
final val rangeUID: Int = 40150
private val idGenerator: AtomicLong = new AtomicLong
/**
* Overloaded constructor for an `Unresolved` projectile.
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object
*/
* Overloaded constructor for an `Unresolved` projectile.
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object
*/
def apply(
profile: ProjectileDefinition,
tool_def: ToolDefinition,
@ -140,20 +145,20 @@ object Projectile {
shot_origin: Vector3,
shot_angle: Vector3
): Projectile = {
Projectile(profile, tool_def, fire_mode, SourceEntry(owner), tool_def.ObjectId, shot_origin, shot_angle, None)
Projectile(profile, tool_def, fire_mode, None, SourceEntry(owner), tool_def.ObjectId, shot_origin, shot_angle, None)
}
/**
* Overloaded constructor for an `Unresolved` projectile.
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile
* @param attribute_to an object ID that refers to the method of death that would be reported
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object
*/
* Overloaded constructor for an `Unresolved` projectile.
* @param profile an explanation of the damage that can be performed by this discharge
* @param tool_def the weapon that caused this discharge
* @param fire_mode the current fire mode of the tool used
* @param owner the agency that caused the weapon to produce this projectile
* @param attribute_to an object ID that refers to the method of death that would be reported
* @param shot_origin where the projectile started
* @param shot_angle in which direction the projectile was aimed when it was discharged
* @return the `Projectile` object
*/
def apply(
profile: ProjectileDefinition,
tool_def: ToolDefinition,
@ -163,7 +168,7 @@ object Projectile {
shot_origin: Vector3,
shot_angle: Vector3
): Projectile = {
Projectile(profile, tool_def, fire_mode, SourceEntry(owner), attribute_to, shot_origin, shot_angle, None)
Projectile(profile, tool_def, fire_mode, None, SourceEntry(owner), attribute_to, shot_origin, shot_angle, None)
}
def apply(
@ -175,6 +180,6 @@ object Projectile {
shot_origin: Vector3,
shot_angle: Vector3
): Projectile = {
Projectile(profile, tool_def, fire_mode, owner, attribute_to, shot_origin, shot_angle, None)
Projectile(profile, tool_def, fire_mode, None, owner, attribute_to, shot_origin, shot_angle, None)
}
}

View file

@ -1,49 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle}
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
trait SourceEntry extends WorldEntity {
def Name: String = ""
def Definition: ObjectDefinition with VitalityDefinition
def CharId: Long = 0L
def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
def Position_=(pos: Vector3) = Position
def Orientation_=(pos: Vector3) = Position
def Velocity_=(pos: Option[Vector3]) = Velocity
def Modifiers: ResistanceProfile
}
object SourceEntry {
final val None = new SourceEntry() {
def Definition = null
def Position = Vector3.Zero
def Orientation = Vector3.Zero
def Velocity = Some(Vector3.Zero)
def Modifiers = null
}
def apply(target: PlanetSideGameObject with FactionAffinity): SourceEntry = {
target match {
case obj: Player => PlayerSource(obj)
case obj: Vehicle => VehicleSource(obj)
case obj: Deployable => DeployableSource(obj)
case _ => ObjectSource(target)
}
}
def NameFormat(name: String): String = {
name
.replace("_", " ")
.split(" ")
.map(_.capitalize)
.mkString(" ")
}
}

View file

@ -1,50 +0,0 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.ballistics
import net.psforever.objects.Vehicle
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class VehicleSource(
obj_def: VehicleDefinition,
faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
position: Vector3,
orientation: Vector3,
velocity: Option[Vector3],
occupants: List[SourceEntry],
modifiers: ResistanceProfile
) extends SourceEntry {
override def Name = SourceEntry.NameFormat(obj_def.Name)
override def Faction = faction
def Definition: VehicleDefinition = obj_def
def Health = health
def Shields = shields
def Position = position
def Orientation = orientation
def Velocity = velocity
def Modifiers = modifiers
}
object VehicleSource {
def apply(obj: Vehicle): VehicleSource = {
VehicleSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
obj.Position,
obj.Orientation,
obj.Velocity,
obj.Seats.values.map { seat =>
seat.occupant match {
case Some(p) => PlayerSource(p)
case _ => SourceEntry.None
}
}.toList,
obj.Definition.asInstanceOf[ResistanceProfile]
)
}
}

View file

@ -65,7 +65,7 @@ object AvatarConverter {
def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = {
val alt_model_flag: Boolean = obj.isBackpack
val aa: Int => CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, obj.Voice),
obj.avatar.basic,
CommonFieldData(
obj.Faction,
bops = false,

View file

@ -3,6 +3,7 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.packet.game.objectcreate.{CaptureFlagData, PlacementData}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
@ -13,7 +14,7 @@ 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("", PlanetSideGUID(0), PlanetSideEmpire.NEUTRAL, Vector3.Zero, 0L, 0L)
case _ => Hackable.HackInfo(PlayerSource("", PlanetSideEmpire.NEUTRAL, Vector3.Zero), PlanetSideGUID(0), 0L, 0L)
}
val millisecondsRemaining = TimeUnit.MILLISECONDS.convert(math.max(0, hackInfo.hackStartTime + hackInfo.hackDuration - System.nanoTime), TimeUnit.NANOSECONDS)

View file

@ -39,7 +39,7 @@ class CharacterSelectConverter extends AvatarConverter {
*/
private def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = {
val aa: Int => CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, CharacterVoice.Mute),
obj.avatar.basic,
CommonFieldData(
obj.Faction,
bops = false,

View file

@ -30,7 +30,7 @@ class CorpseConverter extends AvatarConverter {
*/
private def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = {
val aa: Int => CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(obj.Name, obj.Faction, CharacterSex.Male, 0, CharacterVoice.Mute),
obj.avatar.basic,
CommonFieldData(
obj.Faction,
bops = false,

View file

@ -2,10 +2,10 @@
package net.psforever.objects.equipment
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.{GlobalDefinitions, Tool, Vehicle}
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.objects.vital.RepairFromArmorSiphon
import net.psforever.objects.vital.etc.{ArmorSiphonModifiers, ArmorSiphonReason}
import net.psforever.objects.vital.interaction.DamageInteraction
@ -76,7 +76,7 @@ object ArmorSiphonBehavior {
if before < obj.MaxHealth =>
val after = obj.Health += amount
if(before < after) {
obj.History(RepairFromArmorSiphon(asr.siphon.Definition, before - after))
obj.LogActivity(RepairFromArmorSiphon(asr.siphon.Definition, VehicleSource(obj), before - after))
val zone = obj.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,

View file

@ -39,22 +39,38 @@ object EffectTarget {
false
}
/**
* To repair at this silo, the vehicle:
* can not be a flight vehicle,
* must have some health already, but does not have all its health,
* and can not have taken damage in the last five seconds.
*/
def RepairSilo(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle =>
!GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && !v.History.takeWhile(System.currentTimeMillis() - _.time <= 5000L).exists(_.isInstanceOf[DamagingActivity])
case _ =>
false
case v: Vehicle => !GlobalDefinitions.isFlightVehicle(v.Definition) && CommonRepairConditions(v)
case _ => false
}
/**
* To repair at this landing pad, the vehicle:
* be a flight vehicle,
* must have some health already, but does not have all its health,
* and can not have taken damage in the last five seconds.
*/
def PadLanding(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle =>
GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && v.Health < v.MaxHealth && !v.History.takeWhile(System.currentTimeMillis() - _.time <= 5000L).exists(_.isInstanceOf[DamagingActivity])
case _ =>
false
case v: Vehicle => GlobalDefinitions.isFlightVehicle(v.Definition) && CommonRepairConditions(v)
case _ => false
}
private def CommonRepairConditions(v: Vehicle): Boolean = {
v.Health > 0 && v.Health < v.MaxHealth &&
v.History.findLast { entry => entry.isInstanceOf[DamagingActivity] }.exists {
case entry if System.currentTimeMillis() - entry.time < 5000L => true
case _ => false
}
}
def Player(target: PlanetSideGameObject): Boolean =
target match {
case p: Player =>

View file

@ -1,8 +1,9 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import net.psforever.objects.ballistics.{PlayerSource, Projectile, SourceEntry}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.geometry.d3._
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player}
import net.psforever.types.{ExoSuitType, Vector3}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject
import net.psforever.objects.Player
import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.serverobject.hackable.Hackable
//temporary location for these messages
@ -23,4 +23,15 @@ object CommonMessages {
final case class Progress(delta: Float, completionAction: () => Unit, tickAction: Float => Boolean) {
assert(delta > 0, s"progress activity change value must be positive number - $delta")
}
/**
* A request has been made to charge this entity's shields.
* @see `FacilityBenefitShieldChargeRequestMessage`
* @param amount the number of points to charge
* @param motivator the element that caused the shield to charge;
* allowed to be `None`;
* most often, a `Building`;
* if the vehicle instigated its own charge (battleframe robotics), specify that
*/
final case class ChargeShields(amount: Int, motivator: Option[PlanetSideGameObject])
}

View file

@ -4,6 +4,7 @@ package net.psforever.objects.serverobject.damage
import akka.actor.{Actor, Cancellable}
import net.psforever.objects.ballistics._
import net.psforever.objects.serverobject.aura.Aura
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.base._
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}

View file

@ -16,7 +16,6 @@ trait DamageableAmenity extends DamageableEntity {
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
DamageableAmenity.DestructionAwareness(target, cause)
target.ClearHistory()
}
}

View file

@ -56,7 +56,7 @@ trait DamageableEntity extends Damageable {
val health = target.Health
val damage = originalHealth - health
if (WillAffectTarget(target, damage, cause)) {
target.History(cause)
target.LogActivity(cause)
DamageLog(target, s"BEFORE=$originalHealth, AFTER=$health, CHANGE=$damage")
HandleDamage(target, cause, damage)
} else {

View file

@ -2,8 +2,8 @@
package net.psforever.objects.serverobject.damage
import net.psforever.objects.Player
import net.psforever.objects.ballistics.{PlayerSource, SourceEntry}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.packet.game.DamageWithPositionMessage
import net.psforever.services.Service

View file

@ -49,7 +49,7 @@ trait DamageableVehicle
//bfrs undergo a shiver spell before exploding
val obj = DamageableObject
obj.Health = 0
obj.History(cause)
obj.LogActivity(cause)
DestructionAwareness(obj, cause)
}
@ -74,7 +74,7 @@ trait DamageableVehicle
val damageToHealth = originalHealth - health
val damageToShields = originalShields - shields
if (WillAffectTarget(target, damageToHealth + damageToShields, cause)) {
target.History(cause)
target.LogActivity(cause)
DamageLog(
target,
s"BEFORE=$originalHealth/$originalShields, AFTER=$health/$shields, CHANGE=$damageToHealth/$damageToShields"
@ -130,7 +130,7 @@ trait DamageableVehicle
if (obj.MountedIn.nonEmpty) {
//log historical event
target.History(cause)
target.LogActivity(cause)
}
//damage
if (Damageable.CanDamageOrJammer(target, totalDamage, cause.interaction)) {
@ -214,7 +214,6 @@ trait DamageableVehicle
}
//clean up
target.Actor ! Vehicle.Deconstruct(Some(1 minute))
target.ClearHistory()
DamageableWeaponTurret.DestructionAwareness(obj, cause)
case _ => ;
}

View file

@ -51,7 +51,7 @@ trait DamageableWeaponTurret
}
//log historical event
target.History(cause)
target.LogActivity(cause)
//damage
if (Damageable.CanDamageOrJammer(target, damageToHealth, cause.interaction)) {
//jammering

View file

@ -30,10 +30,10 @@ class GeneratorControl(gen: Generator)
with DamageableEntity
with RepairableEntity
with AmenityAutoRepair {
def FactionObject = gen
def DamageableObject = gen
def RepairableObject = gen
def AutoRepairObject = gen
def FactionObject: Generator = gen
def DamageableObject: Generator = gen
def RepairableObject: Generator = gen
def AutoRepairObject: Generator = gen
/** flagged to explode after some time */
var imminentExplosion: Boolean = false
/** explode when this timer completes */
@ -123,9 +123,9 @@ class GeneratorControl(gen: Generator)
imminentExplosion = false
//hate on everything nearby
Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc)
gen.ClearHistory()
case GeneratorControl.Restored() =>
gen.ClearHistory()
GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Online))
case _ => ;
@ -153,7 +153,6 @@ class GeneratorControl(gen: Generator)
imminentExplosion = false
gen.Condition = PlanetSideGeneratorState.Destroyed
GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Destroyed))
gen.ClearHistory()
case _ =>
}

View file

@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.hackable
import net.psforever.objects.Player
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.packet.game.TriggeredSound
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
@ -24,14 +25,14 @@ trait Hackable {
def HackedBy_=(agent: Option[Player]): Option[HackInfo] = {
(hackedBy, agent) match {
case (None, Some(actor)) =>
hackedBy = Some(HackInfo(actor.Name, actor.GUID, actor.Faction, actor.Position, System.nanoTime, 0L))
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L))
case (Some(info), Some(actor)) =>
if (actor.Faction == this.Faction) {
//hack cleared
hackedBy = None
} else if (actor.Faction != info.hackerFaction) {
//override the hack state with a new hack state if the new user has different faction affiliation
hackedBy = Some(HackInfo(actor.Name, actor.GUID, actor.Faction, actor.Position, System.nanoTime, 0L))
hackedBy = Some(HackInfo(PlayerSource(actor), actor.GUID, System.nanoTime, 0L))
}
case (_, None) =>
hackedBy = None
@ -67,30 +68,19 @@ trait Hackable {
hackDuration = arr
arr
}
// private var hackable : Option[Boolean] = None
// def Hackable : Boolean = hackable.getOrElse(Definition.Hackable)
//
// def Hackable_=(state : Boolean) : Boolean = Hackable_=(Some(state))
//
// def Hackable_=(state : Option[Boolean]) : Boolean = {
// hackable = state
// Hackable
// }
//
// def Definition : HackableDefinition
}
object Hackable {
final case class HackInfo(
hackerName: String,
hackerGUID: PlanetSideGUID,
hackerFaction: PlanetSideEmpire.Value,
hackerPos: Vector3,
hackStartTime: Long,
hackDuration: Long
) {
def Duration(time: Long): HackInfo =
HackInfo(hackerName, hackerGUID, hackerFaction, hackerPos, hackStartTime, time)
player: PlayerSource,
hackerGUID: PlanetSideGUID,
hackStartTime: Long,
hackDuration: Long
) {
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)
}
}

View file

@ -1,3 +1,4 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.serverobject.llu
import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building}
@ -5,9 +6,28 @@ import net.psforever.objects.{GlobalDefinitions, Player}
import net.psforever.types.{PlanetSideEmpire, Vector3}
/**
* This object represents the LLU that gets spawned at a LLU socket when a LLU control console is hacked
*/
class CaptureFlag(tDef: CaptureFlagDefinition) extends Amenity {
* 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,
* 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),
* the cavern modules,
* and the rabbit ball (special game mode).<br>
* <br>
* For the lattice logic unit, when a facility is set to generate an LLU upon hack,
* and an adjacent facility on the lattice provides an accommodating faction connection,
* the unit gets spawned at the LLU socket within the hacked facility.
* The LLU socket actually doesn't do anything but keep track of the spawned flag and provide a location.
* It associates with the faction of the hacker and, carried by other players of the same faction only,
* must be brought to the control console of a designated facility that is owned by the faction of the hacking empire.
* If the hack is cancelled through a resecure, the LLU despawns.
* If the facility is counter-hacked, the active LLU despawns and a new LLU is spawned in the socket.
* Other empires can not interact with the LLU while it is dropped on the ground and
* vehicles will be warned and then deconstructed if they linges too long near a dropped LLU.
* The LLU can not be submerged in water or it will despawn and the hack will cancel.
*/
class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity {
def Definition : CaptureFlagDefinition = tDef
private var target: Building = Building.NoBuilding
@ -15,28 +35,35 @@ class CaptureFlag(tDef: CaptureFlagDefinition) extends Amenity {
private var carrier: Option[Player] = None
def Target: Building = target
def Target_=(new_target: Building): Building = {
target = new_target
def Target_=(newTarget: Building): Building = {
target = newTarget
target
}
// Since a LLU belongs to a base, but needs to be picked up by the enemy faction we need to be able to override the faction that owns the LLU to the hacker faction
/**
* Since a LLU belongs to a base,
* but needs to be picked up by the enemy faction,
* override the faction that owns the LLU to display the hacker faction.
*/
override def Faction: PlanetSideEmpire.Value = faction
override def Faction_=(new_faction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
faction = new_faction
override def Faction_=(newFaction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = {
faction = newFaction
faction
}
// When the flag is carried by a player, the position returned should be that of the carrier not the flag
/**
* When the flag is carried by a player, the position returned should be that of the carrier not the flag.
* @return the position of the carrier, if there is a player carrying the flag, or the flag itself
*/
override def Position: Vector3 = if (Carrier.nonEmpty) {
carrier.get.Position
} else {
Entity.Position
super.Position
}
def Carrier: Option[Player] = carrier
def Carrier_=(new_carrier: Option[Player]) : Option[Player] = {
carrier = new_carrier
def Carrier_=(newCarrier: Option[Player]) : Option[Player] = {
carrier = newCarrier
carrier
}
}
@ -53,7 +80,6 @@ object CaptureFlag {
obj.Target = target
obj.Owner = owner
obj.Faction = faction
obj
}
}

View file

@ -13,6 +13,7 @@ import net.psforever.types.Vector3
*/
class CaptureFlagSocket(tDef: CaptureFlagSocketDefinition)
extends Amenity {
private var lastFlag: Option[CaptureFlag] = None
private var spawnedCaptureFlag: Option[CaptureFlag] = None
def captureFlag: Option[CaptureFlag] = spawnedCaptureFlag
@ -20,10 +21,19 @@ class CaptureFlagSocket(tDef: CaptureFlagSocketDefinition)
def captureFlag_=(flag: CaptureFlag): Option[CaptureFlag] = captureFlag_=(Some(flag))
def captureFlag_=(flag: Option[CaptureFlag]): Option[CaptureFlag] = {
lastFlag = flag.orElse(lastFlag)
spawnedCaptureFlag = flag
captureFlag
}
def previousFlag: Option[CaptureFlag] = lastFlag
def clearOldFlagData(): Unit = {
if (spawnedCaptureFlag.isEmpty) {
lastFlag = None
}
}
def Definition : CaptureFlagSocketDefinition = tDef
}

View file

@ -3,13 +3,13 @@ package net.psforever.objects.serverobject.pad.process
import akka.actor.Props
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.etc.{ExplodingEntityReason, VehicleSpawnReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone
import scala.concurrent.ExecutionContext.Implicits.global

View file

@ -1,13 +1,14 @@
package net.psforever.objects.serverobject.painbox
import akka.actor.Cancellable
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.etc.PainboxReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.{Default, GlobalDefinitions, Player}
import net.psforever.services.Service
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
@ -58,7 +59,7 @@ class PainboxControl(painbox: Painbox) extends PoweredAmenityControl {
}
var commonBehavior: Receive = {
case "startup" =>
case Service.Startup() =>
if (!disabled && domain.midpoint == Vector3.Zero) {
initialStartup()
}

View file

@ -9,6 +9,7 @@ import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.{Default, NtuContainer, NtuStorageBehavior}
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.structures.{Amenity, AutoRepairStats, Building}
import net.psforever.objects.vital.RepairFromAmenityAutoRepair
import net.psforever.util.Config
import scala.concurrent.duration._
@ -96,6 +97,7 @@ trait AmenityAutoRepair
wholeRepairAmount + wholeOverflow
}
PerformRepairs(obj, finalRepairAmount)
obj.LogActivity(RepairFromAmenityAutoRepair(finalRepairAmount))
val currentTime = System.currentTimeMillis()
val taskTime = currentTime - autoRepairQueueTask.getOrElse(currentTime)
autoRepairQueueTask = Some(0L)
@ -148,7 +150,8 @@ trait AmenityAutoRepair
* or if the current process has stalled.
*/
private def startAutoRepairIfStopped(): Unit = {
if(autoRepairQueueTask.isEmpty || stallDetection(stallTime = 15000L)) {
val stallTime: Long = 15000L
if(autoRepairQueueTask.isEmpty || stallDetection(stallTime)) {
trySetupAutoRepairInitial()
}
}

View file

@ -3,6 +3,8 @@ package net.psforever.objects.serverobject.repair
import net.psforever.objects.Tool
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.sourcing.{SourceEntry, SourceWithHealthEntry}
import net.psforever.objects.vital.{DamagingActivity, RepairFromEquipment, SpawningActivity}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
/**
@ -23,7 +25,7 @@ trait RepairableAmenity extends RepairableEntity {
object RepairableAmenity {
/**
* A resotred `Amenity` target dispatches two messages to chance its model and operational states.
* A restored `Amenity` target dispatches two messages to chance its model and operational states.
* These `PlanetSideAttributeMessage` attributes are the same as reported during zone load client configuration.
* @see `AvatarAction.PlanetsideAttributeToAll`
* @see `AvatarServiceMessage`
@ -37,5 +39,27 @@ object RepairableAmenity {
val targetGUID = target.GUID
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 50, 0))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(targetGUID, 51, 0))
RestorationOfHistory(target)
}
/**
* The vitality change history will be forgotten as this entity, once destroyed, has been rebuilt.
* For the purpose of inheritance of experience due to interaction,
* the users who made an effort to repair the entity in its resurgence.
* @param target na
*/
def RestorationOfHistory(target: Repairable.Target): Unit = {
val list = target.ClearHistory()
val effort = list.slice(
list.lastIndexWhere {
case dam: DamagingActivity => dam.data.targetAfter.asInstanceOf[SourceWithHealthEntry].health == 0
case _ => false
},
list.size
).collect {
case entry: RepairFromEquipment => Some(entry.user)
case _ => None
}.flatten.distinctBy(_.Name)
target.LogActivity(SpawningActivity(SourceEntry(target), target.Zone.Number, effort.headOption))
}
}

View file

@ -1,6 +1,8 @@
//Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.repair
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.vital.RepairFromEquipment
import net.psforever.objects.{Player, Tool}
import net.psforever.packet.game.{InventoryStateMessage, RepairMessage}
import net.psforever.types.{PlanetSideEmpire, Vector3}
@ -92,6 +94,13 @@ trait RepairableEntity extends Repairable {
InventoryStateMessage(item.AmmoSlot.Box.GUID, item.GUID, magazine.toLong)
)
)
target.LogActivity(
RepairFromEquipment(
PlayerSource(player),
item.Definition,
repairValue
)
)
PerformRepairs(target, repairValue)
} else {
originalHealth

View file

@ -16,6 +16,7 @@ trait RepairableWeaponTurret extends RepairableEntity {
override def Restoration(target: Repairable.Target): Unit = {
super.Restoration(target)
RepairableAmenity.RestorationOfHistory(target)
RepairableWeaponTurret.Restoration(RepairableObject)
}
}

View file

@ -1,8 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.structures
import java.util.concurrent.TimeUnit
import akka.actor.ActorContext
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.{GlobalDefinitions, NtuContainer, Player}
@ -20,32 +18,37 @@ import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
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,
private val building_guid: Int,
private val map_id: Int,
private val zone: Zone,
private val buildingType: StructureType,
private val buildingDefinition: BuildingDefinition
) extends AmenityOwner
private val name: String,
private val building_guid: Int,
private val map_id: Int,
private val zone: Zone,
private val buildingType: StructureType,
private val buildingDefinition: BuildingDefinition
) extends AmenityOwner
with BlockMapEntity {
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
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
override def toString = name
override def toString: String = name
def Name: String = name
/**
* The map_id is the identifier number used in BuildingInfoUpdateMessage. This is the index that the building appears in the MPO file starting from index 1
* The GUID is the identifier number used in SetEmpireMessage / Facility hacking / PlanetSideAttributeMessage.
*/
* The map_id is the identifier number used in BuildingInfoUpdateMessage. This is the index that the building appears in the MPO file starting from index 1
* The GUID is the identifier number used in SetEmpireMessage / Facility hacking / PlanetSideAttributeMessage.
*/
def MapId: Int = map_id
def IsCapitol: Boolean = capitols.contains(name)
@ -82,10 +85,13 @@ class Building(
box.Actor ! Painbox.Stop()
}
}
participationFunc.Players(building = this, list)
playersInSOI = list
playersInSOI
}
def PlayerContribution: Map[Player, Long] = participationFunc.Contribution()
// Get all lattice neighbours
def AllNeighbours: Option[Set[Building]] = {
zone.Lattice find this match {
@ -139,7 +145,11 @@ class Building(
case _ => false
}
def GetFlagSocket: Option[CaptureFlagSocket] = this.Amenities.find(_.Definition == GlobalDefinitions.llm_socket).asInstanceOf[Option[CaptureFlagSocket]]
def GetFlagSocket: Option[CaptureFlagSocket] = {
this.Amenities
.find(_.Definition == GlobalDefinitions.llm_socket)
.map(_.asInstanceOf[CaptureFlagSocket])
}
def GetFlag: Option[CaptureFlag] = {
GetFlagSocket match {
case Some(socket) => socket.captureFlag
@ -175,10 +185,10 @@ 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(_, _, hfaction, _, start, length)) =>
case Some(Hackable.HackInfo(p, _, start, length)) =>
val hack_time_remaining_ms =
TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
(true, hfaction, hack_time_remaining_ms)
(true, p.Faction, hack_time_remaining_ms)
case _ =>
(false, PlanetSideEmpire.NEUTRAL, 0L)
}
@ -199,8 +209,8 @@ class Building(
}
val cavernBenefit: Set[CavernBenefit] = if (
generatorState != PlanetSideGeneratorState.Destroyed &&
faction != PlanetSideEmpire.NEUTRAL &&
connectedCavern().nonEmpty
faction != PlanetSideEmpire.NEUTRAL &&
connectedCavern().nonEmpty
) {
Set(CavernBenefit.VehicleModule, CavernBenefit.EquipmentModule)
} else {
@ -233,22 +243,28 @@ class Building(
}
def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = {
val genState = Generator match {
case Some(obj) => obj.Condition != PlanetSideGeneratorState.Destroyed
val baseDownState = (NtuSource match {
case Some(ntu) => ntu.NtuCapacitor < 1f
case _ => false
}
if (genState || Faction == PlanetSideEmpire.NEUTRAL) {
}) ||
(Generator match {
case Some(obj) => obj.Condition == PlanetSideGeneratorState.Destroyed
case _ => false
}) ||
Faction == PlanetSideEmpire.NEUTRAL
if (baseDownState) {
false
} else {
// Check this Building is on the lattice first
zone.Lattice find this match {
case Some(_) =>
val faction = Faction
val subGraph = Zone.Lattice filter (
(b : Building) =>
b.Faction == this.Faction &&
!b.CaptureTerminalIsHacked &&
b.NtuLevel > 0 &&
(b.Generator.isEmpty || b.Generator.get.Condition != PlanetSideGeneratorState.Destroyed)
(b: Building) =>
b.Faction == faction &&
!b.CaptureTerminalIsHacked &&
b.NtuLevel > 0 &&
(b.Generator.isEmpty || b.Generator.get.Condition != PlanetSideGeneratorState.Destroyed)
)
findLatticeBenefit(wantedBenefit, subGraph)
case None =>
@ -282,7 +298,10 @@ class Building(
case Some(obj) => obj.Condition
case _ => PlanetSideGeneratorState.Normal
}
if (genState == PlanetSideGeneratorState.Destroyed || Faction == PlanetSideEmpire.NEUTRAL) {
if (genState == PlanetSideGeneratorState.Destroyed ||
Faction == PlanetSideEmpire.NEUTRAL ||
CaptureTerminalIsHacked
) {
Set(LatticeBenefit.None)
} else {
friendlyFunctionalNeighborhood().map { _.Definition.LatticeLinkBenefit }
@ -296,10 +315,10 @@ class Building(
while (currBuilding.nonEmpty) {
val building = currBuilding.head
val neighborsToAdd = if (!visitedNeighbors.contains(building.MapId)
&& (building match { case _ : WarpGate => false; case _ => true })
&& !building.CaptureTerminalIsHacked
&& building.NtuLevel > 0
&& (building.Generator match {
&& (building match { case _ : WarpGate => false; case _ => true })
&& !building.CaptureTerminalIsHacked
&& building.NtuLevel > 0
&& (building.Generator match {
case Some(o) => o.Condition != PlanetSideGeneratorState.Destroyed
case _ => true
})
@ -321,14 +340,14 @@ class Building(
}
/**
* Starting from an overworld zone facility,
* find a lattice connected cavern facility that is the same faction as this starting building.
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
* do not let the search escape the current zone into another.
* If we start in a cavern zone, do not continue a fruitless search;
* just fail.
* @return the discovered faction-aligned cavern facility
*/
* Starting from an overworld zone facility,
* find a lattice connected cavern facility that is the same faction as this starting building.
* Except for the necessary examination of the major facility on the other side of a warp gate pair,
* do not let the search escape the current zone into another.
* If we start in a cavern zone, do not continue a fruitless search;
* just fail.
* @return the discovered faction-aligned cavern facility
*/
def connectedCavern(): Option[Building] = net.psforever.objects.zones.Zone.findConnectedCavernFacility(building = this)
def BuildingType: StructureType = buildingType
@ -339,10 +358,46 @@ class Building(
override def Continent_=(zone: String): String = Continent //building never leaves zone after being set in constructor
override def Amenities_=(obj: Amenity): List[Amenity] = {
obj match {
case _: CaptureTerminal => participationFunc = Building.FacilityHackParticipation
case _ => ;
}
super.Amenities_=(obj)
}
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 NoBuilding: Building =
new Building(name = "", 0, map_id = 0, Zone.Nowhere, StructureType.Platform, GlobalDefinitions.building) {
override def Faction_=(faction: PlanetSideEmpire.Value): PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
@ -355,11 +410,11 @@ object Building {
}
def Structure(
buildingType: StructureType,
location: Vector3,
rotation: Vector3,
definition: BuildingDefinition
)(name: String, guid: Int, map_id: Int, zone: Zone, context: ActorContext): Building = {
buildingType: StructureType,
location: Vector3,
rotation: Vector3,
definition: BuildingDefinition
)(name: String, guid: Int, map_id: Int, zone: Zone, context: ActorContext): Building = {
val obj = new Building(name, guid, map_id, zone, buildingType, definition)
obj.Position = location
obj.Orientation = rotation
@ -368,9 +423,9 @@ object Building {
}
def Structure(
buildingType: StructureType,
location: Vector3
)(name: String, guid: Int, map_id: Int, zone: Zone, context: ActorContext): Building = {
buildingType: StructureType,
location: Vector3
)(name: String, guid: Int, map_id: Int, zone: Zone, context: ActorContext): Building = {
val obj = new Building(name, guid, map_id, zone, buildingType, GlobalDefinitions.building)
obj.Position = location
obj.Actor = context.spawn(BuildingActor(zone, obj), s"$map_id-$buildingType-building").toClassic
@ -378,18 +433,18 @@ object Building {
}
def Structure(
buildingType: StructureType
)(name: String, guid: Int, map_id: Int, zone: Zone, context: ActorContext): Building = {
buildingType: StructureType
)(name: String, guid: Int, map_id: Int, zone: Zone, context: ActorContext): Building = {
val obj = new Building(name, guid, map_id, zone, buildingType, GlobalDefinitions.building)
obj.Actor = context.spawn(BuildingActor(zone, obj), s"$map_id-$buildingType-building").toClassic
obj
}
def Structure(
buildingType: StructureType,
buildingDefinition: BuildingDefinition,
location: Vector3
)(name: String, guid: Int, id: Int, zone: Zone, context: ActorContext): Building = {
buildingType: StructureType,
buildingDefinition: BuildingDefinition,
location: Vector3
)(name: String, guid: Int, id: Int, zone: Zone, context: ActorContext): Building = {
val obj = new Building(name, guid, id, zone, buildingType, buildingDefinition)
obj.Position = location
obj.Actor = context.spawn(BuildingActor(zone, obj), s"$id-$buildingType-building").toClassic

View file

@ -1,57 +0,0 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.terminals
import net.psforever.objects.Player
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.util.{Failure, Success}
object CaptureTerminals {
private val log = org.log4s.getLogger("CaptureTerminals")
/**
* The process of hacking an object is completed.
* Pass the message onto the hackable object and onto the local events system.
* @param target the `Hackable` object that has been hacked
* @param unk na;
* used by `HackMessage` as `unk5`
* @see `HackMessage`
*/
//TODO add params here depending on which params in HackMessage are important
def FinishHackingCaptureConsole(target: CaptureTerminal, hackingPlayer: Player, unk: Long)(): Unit = {
import akka.pattern.ask
import scala.concurrent.duration._
log.info(s"${hackingPlayer.toString} hacked a ${target.Definition.Name}")
// Wait for the target actor to set the HackedBy property
import scala.concurrent.ExecutionContext.Implicits.global
ask(target.Actor, CommonMessages.Hack(hackingPlayer, target))(1 second).mapTo[Boolean].onComplete {
case Success(_) =>
target.Zone.LocalEvents ! LocalServiceMessage(
target.Zone.id,
LocalAction.TriggerSound(hackingPlayer.GUID, target.HackSound, hackingPlayer.Position, 30, 0.49803925f)
)
val isResecured = hackingPlayer.Faction == target.Faction
if (isResecured) {
// Resecure the CC
target.Zone.LocalEvents ! LocalServiceMessage(
target.Zone.id,
LocalAction.ResecureCaptureTerminal(
target
)
)
}
else {
// Start the CC hack timer
target.Zone.LocalEvents ! LocalServiceMessage(
target.Zone.id,
LocalAction.StartCaptureTerminalHack(
target
)
)
}
case Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}")
}
}
}

View file

@ -2,8 +2,13 @@
package net.psforever.objects.serverobject.terminals
import akka.actor.{ActorRef, Cancellable}
import net.psforever.objects.sourcing.AmenitySource
import org.log4s.Logger
import scala.collection.mutable
import scala.concurrent.duration._
//
import net.psforever.objects._
import net.psforever.objects.ballistics.{PlayerSource, VehicleSource}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
@ -12,39 +17,37 @@ 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}
import net.psforever.objects.vital.{HealFromTerm, RepairFromTerm, Vitality}
import net.psforever.objects.zones.ZoneAware
import net.psforever.packet.game.InventoryStateMessage
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.collection.mutable
import scala.concurrent.duration._
/**
* An `Actor` that handles messages being dispatched to a specific `ProximityTerminal`.
* Although this "terminal" itself does not accept the same messages as a normal `Terminal` object,
* it returns the same type of messages - wrapped in a `TerminalMessage` - to the `sender`.
* @param term the proximity unit (terminal)
*/
* An `Actor` that handles messages being dispatched to a specific `ProximityTerminal`.
* Although this "terminal" itself does not accept the same messages as a normal `Terminal` object,
* it returns the same type of messages - wrapped in a `TerminalMessage` - to the `sender`.
* @param term the proximity unit (terminal)
*/
class ProximityTerminalControl(term: Terminal with ProximityUnit)
extends PoweredAmenityControl
extends PoweredAmenityControl
with FactionAffinityBehavior.Check
with HackableBehavior.GenericHackable
with DamageableAmenity
with RepairableAmenity
with AmenityAutoRepair {
def FactionObject = term
def HackableObject = term
def TerminalObject = term
def DamageableObject = term
def RepairableObject = term
def AutoRepairObject = term
def FactionObject: Terminal with ProximityUnit = term
def HackableObject: Terminal with ProximityUnit = term
def TerminalObject: Terminal with ProximityUnit = term
def DamageableObject: Terminal with ProximityUnit = term
def RepairableObject: Terminal with ProximityUnit = term
def AutoRepairObject: Terminal with ProximityUnit = term
var terminalAction: Cancellable = Default.Cancellable
val callbacks: mutable.ListBuffer[ActorRef] = new mutable.ListBuffer[ActorRef]()
val log = org.log4s.getLogger
val log: Logger = org.log4s.getLogger
val commonBehavior: Receive = checkBehavior
.orElse(takesDamage)
@ -63,7 +66,7 @@ class ProximityTerminalControl(term: Terminal with ProximityUnit)
.orElse(hackableBehavior)
.orElse {
case CommonMessages.Use(player, Some(item: SimpleItem))
if item.Definition == GlobalDefinitions.remote_electronics_kit =>
if item.Definition == GlobalDefinitions.remote_electronics_kit =>
//TODO setup certifications check
term.Owner match {
case b: Building if (b.Faction != player.Faction || b.CaptureTerminalIsHacked) && term.HackedBy.isEmpty =>
@ -214,16 +217,16 @@ object ProximityTerminalControl {
private case class TerminalAction()
/**
* Determine which functionality to pursue by a generic proximity-functional unit given the target for its activity.
* @see `VehicleService:receive, ProximityUnit.Action`
* @param terminal the proximity-based unit
* @param target the object being affected by the unit
*/
* Determine which functionality to pursue by a generic proximity-functional unit given the target for its activity.
* @see `VehicleService:receive, ProximityUnit.Action`
* @param terminal the proximity-based unit
* @param target the object being affected by the unit
*/
def selectAndTryProximityUnitBehavior(
callback: ActorRef,
terminal: Terminal with ProximityUnit,
target: PlanetSideGameObject
): Boolean = {
callback: ActorRef,
terminal: Terminal with ProximityUnit,
target: PlanetSideGameObject
): Boolean = {
(terminal.Definition, target) match {
case (_: MedicalTerminalDefinition, p: Player) => HealthAndArmorTerminal(terminal, p)
case (_: WeaponRechargeTerminalDefinition, p: Player) => WeaponRechargeTerminal(terminal, p)
@ -234,110 +237,131 @@ object ProximityTerminalControl {
}
/**
* When standing on the platform of a(n advanced) medical terminal,
* restore the player's health and armor points (when they need their health and armor points restored).
* If the player is both fully healed and fully repaired, stop using the terminal.
* @param unit the medical terminal
* @param target the player being healed
*/
* When standing on the platform of a(n advanced) medical terminal,
* restore the player's health and armor points (when they need their health and armor points restored).
* If the player is both fully healed and fully repaired, stop using the terminal.
* @param unit the medical terminal
* @param target the player being healed
*/
def HealthAndArmorTerminal(unit: Terminal with ProximityUnit, target: Player): Boolean = {
val medDef = unit.Definition.asInstanceOf[MedicalTerminalDefinition]
val healAmount = medDef.HealAmount
val healthFull: Boolean = if (healAmount != 0 && target.Health < target.MaxHealth) {
target.History(HealFromTerm(PlayerSource(target), healAmount, 0, medDef))
HealAction(target, healAmount)
} else {
true
}
val repairAmount = medDef.ArmorAmount
val armorFull: Boolean = if (repairAmount != 0 && target.Armor < target.MaxArmor) {
target.History(HealFromTerm(PlayerSource(target), 0, repairAmount, medDef))
ArmorRepairAction(target, repairAmount)
} else {
true
}
healthFull && armorFull
val medDef = unit.Definition.asInstanceOf[MedicalTerminalDefinition]
val fullHeal = HealAction(unit, target, medDef.HealAmount, PlayerHealthCallback)
val fullRepair = ArmorRepairAction(unit, target, medDef.ArmorAmount)
fullHeal && fullRepair
}
/**
* Restore, at most, a specific amount of health points on a player.
* Send messages to connected client and to events system.
* @param tplayer the player
* @param healValue the amount to heal;
* 10 by default
* @return whether the player can be repaired for any more health points
*/
def HealAction(tplayer: Player, healValue: Int = 10): Boolean = {
tplayer.Health = tplayer.Health + healValue
val zone = tplayer.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 0, tplayer.Health)
)
tplayer.Health == tplayer.MaxHealth
}
/**
* Restore, at most, a specific amount of personal armor points on a player.
* Send messages to connected client and to events system.
* @param tplayer the player
* @param repairValue the amount to repair;
* 10 by default
* @return whether the player can be repaired for any more armor points
*/
def ArmorRepairAction(tplayer: Player, repairValue: Int = 10): Boolean = {
tplayer.Armor = tplayer.Armor + repairValue
val zone = tplayer.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 4, tplayer.Armor)
)
tplayer.Armor == tplayer.MaxArmor
}
/**
* When driving a vehicle close to a rearm/repair silo,
* restore the vehicle's health points.
* If the vehicle is fully repaired, stop using the terminal.
* @param unit the terminal
* @param target the vehicle being repaired
*/
* When driving a vehicle close to a rearm/repair silo,
* restore the vehicle's health points.
* If the vehicle is fully repaired, stop using the terminal.
* @param unit the terminal
* @param target the vehicle being repaired
*/
def VehicleRepairTerminal(unit: Terminal with ProximityUnit, target: Vehicle): Boolean = {
val medDef = unit.Definition.asInstanceOf[MedicalTerminalDefinition]
val healAmount = medDef.HealAmount
val maxHealth = target.MaxHealth
val noMoreHeal = if (!target.Destroyed && unit.Validate(target)) {
//repair vehicle
if (healAmount > 0 && target.Health < maxHealth) {
target.Health = target.Health + healAmount
target.History(RepairFromTerm(VehicleSource(target), healAmount, medDef))
val zone = target.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 0, target.Health)
)
target.Health == maxHealth
} else {
unit.Definition match {
case medDef: MedicalTerminalDefinition if !target.Destroyed && unit.Validate(target) =>
HealAction(unit, target, medDef.HealAmount, VehicleHealthCallback)
case _ =>
true
}
} else {
true
}
noMoreHeal
}
/**
* When standing in a friendly SOI whose facility is under the influence of an Ancient Weapon Module benefit,
* and the player is in possession of Ancient weaponnry whose magazine is not full,
* restore some ammunition to its magazine.
* If no valid weapons are discovered or the discovered valid weapons have full magazines, stop using the terminal.
* @param unit the terminal
* @param target the player with weapons being recharged
*/
* Restore, at most, a specific amount of health points on a player.
* Send messages to connected client and to events system.
* @param terminal na
* @param target that which will accept the health
* @param healAmount health value to be given to the target
* @param updateFunc callback to update the UI
* @return whether the target can be healed any further
*/
def HealAction(
terminal: Terminal,
target: PlanetSideGameObject with Vitality with ZoneAware,
healAmount: Int,
updateFunc: PlanetSideGameObject with Vitality with ZoneAware=>Unit
): Boolean = {
val health = target.Health
val maxHealth = target.MaxHealth
val nextHealth = health + healAmount
if (healAmount != 0 && health < maxHealth) {
val finalHealthAmount = if (nextHealth > maxHealth) {
nextHealth - maxHealth
} else {
healAmount
}
target.Health = health + finalHealthAmount
target.LogActivity(HealFromTerm(AmenitySource(terminal), finalHealthAmount))
updateFunc(target)
target.Health == maxHealth
} else {
true
}
}
def PlayerHealthCallback(target: PlanetSideGameObject with Vitality with ZoneAware): Unit = {
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(target.GUID, 0, target.Health)
)
}
def VehicleHealthCallback(target: PlanetSideGameObject with Vitality with ZoneAware): Unit = {
val zone = target.Zone
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, target.GUID, 0, target.Health)
)
}
/**
* Restore, at most, a specific amount of personal armor points on a player.
* Send messages to connected client and to events system.
* @param terminal na
* @param target that which will accept the repair
* @param repairAmount armor value to be given to the target
* @return whether the target can be repaired any further
*/
def ArmorRepairAction(
terminal: Terminal,
target: Player,
repairAmount: Int
): Boolean = {
val armor = target.Armor
val maxArmor = target.MaxArmor
val nextArmor = armor + repairAmount
if (repairAmount != 0 && armor < maxArmor) {
val finalRepairAmount = if (nextArmor > maxArmor) {
nextArmor - maxArmor
} else {
repairAmount
}
target.Armor = armor + finalRepairAmount
target.LogActivity(RepairFromTerm(AmenitySource(terminal), finalRepairAmount))
val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlanetsideAttributeToAll(target.GUID, 4, target.Armor)
)
target.Armor == maxArmor
} else {
true
}
}
/**
* When standing in a friendly SOI whose facility is under the influence of an Ancient Weapon Module benefit,
* and the player is in possession of Ancient weaponnry whose magazine is not full,
* restore some ammunition to its magazine.
* If no valid weapons are discovered or the discovered valid weapons have full magazines, stop using the terminal.
* @param unit the terminal
* @param target the player with weapons being recharged
*/
def WeaponRechargeTerminal(unit: Terminal with ProximityUnit, target: Player): Boolean = {
val result = WeaponsBeingRechargedWithSomeAmmunition(
unit.Definition.asInstanceOf[WeaponRechargeTerminalDefinition].AmmoAmount,
target.Holsters().map { _.Equipment }.flatten.toIterable ++ target.Inventory.Items.map { _.obj }
target.Holsters().flatMap { _.Equipment }.toIterable ++ target.Inventory.Items.map { _.obj }
)
val events = unit.Zone.AvatarEvents
val channel = target.Name
@ -349,17 +373,17 @@ object ProximityTerminalControl {
)
}
}
!result.unzip._2.flatten.exists { slot => slot.Magazine < slot.MaxMagazine() }
!result.flatMap { _._2 }.exists { slot => slot.Magazine < slot.MaxMagazine() }
}
/**
* When driving close to a rearm/repair silo whose facility is under the influence of an Ancient Weapon Module benefit,
* and the vehicle is an Ancient vehicle with mounted weaponry whose magazine(s) is not full,
* restore some ammunition to the magazine(s).
* If no valid weapons are discovered or the discovered valid weapons have full magazines, stop using the terminal.
* @param unit the terminal
* @param target the vehicle with weapons being recharged
*/
* When driving close to a rearm/repair silo whose facility is under the influence of an Ancient Weapon Module benefit,
* and the vehicle is an Ancient vehicle with mounted weaponry whose magazine(s) is not full,
* restore some ammunition to the magazine(s).
* If no valid weapons are discovered or the discovered valid weapons have full magazines, stop using the terminal.
* @param unit the terminal
* @param target the vehicle with weapons being recharged
*/
def WeaponRechargeTerminal(unit: Terminal with ProximityUnit, target: Vehicle): Boolean = {
val result = WeaponsBeingRechargedWithSomeAmmunition(
unit.Definition.asInstanceOf[WeaponRechargeTerminalDefinition].AmmoAmount,
@ -375,17 +399,17 @@ object ProximityTerminalControl {
)
}
}
!result.unzip._2.flatten.exists { slot => slot.Magazine < slot.MaxMagazine() }
!result.flatMap { _._2 }.exists { slot => slot.Magazine < slot.MaxMagazine() }
}
/**
* Collect all weapons with magazines that need to have ammunition reloaded,
* and reload some ammunition into them.
* @param ammoAdded the amount of ammo to be added to a weapon
* @param equipment the equipment being considered;
* weapons whose ammo will be increased will be isolated
* @return na
*/
* Collect all weapons with magazines that need to have ammunition reloaded,
* and reload some ammunition into them.
* @param ammoAdded the amount of ammo to be added to a weapon
* @param equipment the equipment being considered;
* weapons whose ammo will be increased will be isolated
* @return na
*/
def WeaponsBeingRechargedWithSomeAmmunition(
ammoAdded: Int,
equipment: Iterable[Equipment]
@ -399,12 +423,12 @@ object ProximityTerminalControl {
}
/**
* Collect all magazines from this weapon that need to have ammunition reloaded,
* and reload some ammunition into them.
* @param ammoAdded the amount of ammo to be added to a weapon
* @param slots the vehicle with weapons being recharged
* @return ammunition slots that were affected
*/
* Collect all magazines from this weapon that need to have ammunition reloaded,
* and reload some ammunition into them.
* @param ammoAdded the amount of ammo to be added to a weapon
* @param slots the vehicle with weapons being recharged
* @return ammunition slots that were affected
*/
def WeaponAmmoRecharge(
ammoAdded: Int,
slots: List[Tool.FireModeSlot]

View file

@ -1,12 +1,15 @@
package net.psforever.objects.serverobject.terminals.capture
import net.psforever.objects.serverobject.structures.AmenityDefinition
import scala.concurrent.duration.{Duration, FiniteDuration}
class CaptureTerminalDefinition(objectId: Int) extends AmenityDefinition(objectId) {
Name = objectId match {
case 158 => "capture_terminal"
case 751 => "secondary_capture"
case 930 => "vanu_control_console"
case _ => throw new IllegalArgumentException("Not a valid capture terminal object id")
private var hackTime: FiniteDuration = Duration.Zero
def FacilityHackTime: FiniteDuration = hackTime
def FacilityHackTime_=(time: FiniteDuration): FiniteDuration = {
hackTime = time
FacilityHackTime
}
}

View file

@ -1,13 +1,16 @@
package net.psforever.objects.serverobject.terminals.capture
import akka.util.Timeout
import net.psforever.objects.Player
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.util.{Failure, Success}
object CaptureTerminals {
object CaptureTerminals {import scala.concurrent.duration._
private val log = org.log4s.getLogger("CaptureTerminals")
private implicit val timeout: Timeout = 1.second
/**
* The process of hacking an object is completed.
@ -22,39 +25,34 @@ object CaptureTerminals {
def FinishHackingCaptureConsole(target: CaptureTerminal, hackingPlayer: Player, unk: Long)(): Unit = {
import akka.pattern.ask
import scala.concurrent.duration._
log.info(s"${hackingPlayer.toString} hacked a ${target.Definition.Name}")
// Wait for the target actor to set the HackedBy property
import scala.concurrent.ExecutionContext.Implicits.global
ask(target.Actor, CommonMessages.Hack(hackingPlayer, target))(1 second).mapTo[Boolean].onComplete {
ask(target.Actor, CommonMessages.Hack(hackingPlayer, target)).mapTo[Boolean].onComplete {
case Success(_) =>
target.Zone.LocalEvents ! LocalServiceMessage(
target.Zone.id,
val zone = target.Zone
val zoneid = zone.id
val events = zone.LocalEvents
val isResecured = hackingPlayer.Faction == target.Faction
events ! LocalServiceMessage(
zoneid,
LocalAction.TriggerSound(hackingPlayer.GUID, target.HackSound, hackingPlayer.Position, 30, 0.49803925f)
)
val isResecured = hackingPlayer.Faction == target.Faction
if (isResecured) {
// Resecure the CC
target.Zone.LocalEvents ! LocalServiceMessage(
target.Zone.id,
LocalAction.ResecureCaptureTerminal(
target
)
events ! LocalServiceMessage(
zoneid,
LocalAction.ResecureCaptureTerminal(target, PlayerSource(hackingPlayer))
)
} else {
// Start the CC hack timer
target.Zone.LocalEvents ! LocalServiceMessage(
target.Zone.id,
LocalAction.StartCaptureTerminalHack(
target
)
events ! LocalServiceMessage(
zoneid,
LocalAction.StartCaptureTerminalHack(target)
)
}
case Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}")
case Failure(_) =>
log.warn(s"Hack message failed on target guid: ${target.GUID}")
}
}
}

View file

@ -6,7 +6,7 @@ import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity, DamageableMountable}
import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior}
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableEntity}
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity, RepairableEntity}
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
@ -27,13 +27,13 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
with RepairableEntity
with AmenityAutoRepair
with CaptureTerminalAwareBehavior {
def MountableObject = mech
def HackableObject = mech
def FactionObject = mech
def DamageableObject = mech
def RepairableObject = mech
def AutoRepairObject = mech
def CaptureTerminalAwareObject = mech
def MountableObject: ImplantTerminalMech = mech
def HackableObject: ImplantTerminalMech = mech
def FactionObject: ImplantTerminalMech = mech
def DamageableObject: ImplantTerminalMech = mech
def RepairableObject: ImplantTerminalMech = mech
def AutoRepairObject: ImplantTerminalMech = mech
def CaptureTerminalAwareObject: ImplantTerminalMech = mech
def commonBehavior: Receive =
checkBehavior
@ -98,7 +98,6 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
DamageableMountable.DestructionAwareness(DamageableObject, cause)
target.ClearHistory()
}
override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = {
@ -136,4 +135,9 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech)
def powerTurnOnCallback(): Unit = {
tryAutoRepair()
}
override def Restoration(obj: Target): Unit = {
super.Restoration(obj)
RepairableAmenity.RestorationOfHistory(obj)
}
}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
import akka.actor.Cancellable
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
@ -37,16 +38,16 @@ class FacilityTurretControl(turret: FacilityTurret)
with AmenityAutoRepair
with JammableMountedWeapons
with CaptureTerminalAwareBehavior {
def FactionObject = turret
def MountableObject = turret
def JammableObject = turret
def DamageableObject = turret
def RepairableObject = turret
def AutoRepairObject = turret
def CaptureTerminalAwareObject = turret
def FactionObject: FacilityTurret = turret
def MountableObject: FacilityTurret = turret
def JammableObject: FacilityTurret = turret
def DamageableObject: FacilityTurret = turret
def RepairableObject: FacilityTurret = turret
def AutoRepairObject: FacilityTurret = turret
def CaptureTerminalAwareObject: FacilityTurret = turret
// Used for timing ammo recharge for vanu turrets in caves
var weaponAmmoRechargeTimer = Default.Cancellable
var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable
override def postStop(): Unit = {
super.postStop()
@ -186,7 +187,7 @@ class FacilityTurretControl(turret: FacilityTurret)
seat.unmount(player)
player.VehicleSeated = None
if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, false, guid))
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2=false, guid))
}
case None => ;
}

View file

@ -0,0 +1,67 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.sourcing
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class AmenitySource(
private val objdef: ObjectDefinition,
Faction: PlanetSideEmpire.Value,
health: Int,
Orientation: Vector3,
occupants: List[SourceEntry],
hacked: Option[HackInfo],
unique: UniqueAmenity
) extends SourceWithHealthEntry {
private val definition = objdef match {
case vital: VitalityDefinition => vital
case genericDefinition => NonvitalDefinition(genericDefinition)
}
private val modifiers = definition match {
case nonvital: NonvitalDefinition => nonvital
case _ => ObjectSource.FixedResistances
}
def Name: String = SourceEntry.NameFormat(definition.Descriptor)
def Definition: ObjectDefinition with VitalityDefinition = definition
def Health: Int = health
def total: Int = health
def Modifiers: ResistanceProfile = modifiers
def Position: Vector3 = unique.position
def Velocity: Option[Vector3] = None
}
object AmenitySource {
def apply(obj: Amenity): AmenitySource = {
val health: Int = obj match {
case o: Vitality => o.Health
case _ => 1
}
val hackData = obj match {
case o: Hackable => o.HackedBy
case _ => None
}
val amenity = AmenitySource(
obj.Definition,
obj.Faction,
health,
obj.Orientation,
Nil,
hackData,
sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
)
amenity.copy(occupants = obj match {
case o: Mountable =>
o.Seats.values.flatMap { _.occupants }.map { p => PlayerSource.inSeat(p, o, amenity) }.toList
case _ =>
Nil
})
}
}

View file

@ -0,0 +1,41 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.structures.{Building, BuildingDefinition}
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{LatticeBenefit, PlanetSideEmpire, PlanetSideGUID, Vector3}
final case class UniqueBuilding(
zone_number: Int,
building_guid: PlanetSideGUID
) extends SourceUniqueness
final case class BuildingSource(
private val obj_def: BuildingDefinition,
Faction: PlanetSideEmpire.Value,
Position: Vector3,
Orientation: Vector3,
benefits: Set[LatticeBenefit],
unique: UniqueBuilding
) extends SourceEntry {
private val definition = NonvitalDefinition(obj_def)
def Name: String = SourceEntry.NameFormat(Definition.Name)
def Definition: ObjectDefinition with VitalityDefinition = definition
def Modifiers: ResistanceProfile = ObjectSource.FixedResistances
def Velocity: Option[Vector3] = None
}
object BuildingSource {
def apply(b: Building): BuildingSource = {
BuildingSource(
b.Definition,
b.Faction,
b.Position,
b.Orientation,
b.latticeConnectedFacilityBenefits(),
UniqueBuilding(b.Zone.Number, b.GUID)
)
}
}

View file

@ -0,0 +1,64 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.{DeployableDefinition, ObjectDefinition}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class DeployableSource(
Definition: ObjectDefinition with DeployableDefinition,
Faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
owner: SourceEntry,
Position: Vector3,
Orientation: Vector3,
occupants: List[SourceEntry],
unique: UniqueDeployable
) extends SourceWithHealthEntry {
def Name: String = Definition.Descriptor
def Health: Int = health
def Shields: Int = shields
def OwnerName: String = owner.Name
def Velocity: Option[Vector3] = None
def Modifiers: ResistanceProfile = Definition.asInstanceOf[ResistanceProfile]
def total: Int = health + shields
}
object DeployableSource {
def apply(obj: Deployable): DeployableSource = {
val ownerName = obj.OwnerName
val ownerSource = (obj.Zone.LivePlayers ++ obj.Zone.Corpses)
.find { p => ownerName.contains(p.Name) }
match {
case Some(p) => SourceEntry(p)
case _ => SourceEntry.None
}
val occupants = obj match {
case o: Mountable =>
o.Seats.values.flatMap { _.occupants }.map { PlayerSource(_) }.toList
case _ =>
Nil
}
DeployableSource(
obj.Definition,
obj.Faction,
obj.Health,
obj.Shields,
ownerSource,
obj.Position,
obj.Orientation,
occupants,
UniqueDeployable(
obj.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
obj.OriginalOwnerName.getOrElse("none")
)
)
}
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2017-2023 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
/**
* A wrapper for a definition that does not represent a `Vitality` object
* but needs to look like one internally to satisfy type requirements.
* @param definition the original definition
*/
class NonvitalDefinition(private val definition : ObjectDefinition)
extends ObjectDefinition(definition.ObjectId)
with ResistanceProfileMutators
with VitalityDefinition {
Name = definition.Name
Packet = definition.Packet
def canEqual(a: Any) : Boolean = a.isInstanceOf[definition.type]
override def equals(that: Any): Boolean = definition.equals(that)
override def hashCode: Int = definition.hashCode
}
object NonvitalDefinition {
//single point of contact for all wrapped definitions
private val storage: scala.collection.mutable.LongMap[NonvitalDefinition] =
new scala.collection.mutable.LongMap[NonvitalDefinition]()
def apply(definition : ObjectDefinition) : NonvitalDefinition = {
storage.get(definition.ObjectId) match {
case Some(existing) =>
existing
case None =>
val out = new NonvitalDefinition(definition)
storage += definition.ObjectId.toLong -> out
out
}
}
}

View file

@ -0,0 +1,50 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.{ResistanceProfile, ResistanceProfileMutators}
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class UniqueObject(objectId: Int) extends SourceUniqueness
final case class ObjectSource(
private val obj_def: ObjectDefinition,
Faction: PlanetSideEmpire.Value,
Position: Vector3,
Orientation: Vector3,
Velocity: Option[Vector3],
unique: UniqueObject
) extends SourceEntry {
private val definition = obj_def match {
case vital : VitalityDefinition => vital
case genericDefinition => NonvitalDefinition(genericDefinition)
}
private val modifiers = definition match {
case nonvital : NonvitalDefinition => nonvital
case _ => ObjectSource.FixedResistances
}
def Name: String = SourceEntry.NameFormat(Definition.Name)
def Definition: ObjectDefinition with VitalityDefinition = definition
def Modifiers: ResistanceProfile = modifiers
}
object ObjectSource {
final val FixedResistances = new ResistanceProfileMutators() { }
def apply(obj: PlanetSideGameObject): ObjectSource = {
ObjectSource(
obj.Definition,
obj match {
case aligned: FactionAffinity => aligned.Faction
case _ => PlanetSideEmpire.NEUTRAL
},
obj.Position,
obj.Orientation,
obj.Velocity,
UniqueObject(obj.Definition.ObjectId)
)
}
}

View file

@ -0,0 +1,135 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player}
import net.psforever.types.{CharacterSex, ExoSuitType, PlanetSideEmpire, Vector3}
final case class UniquePlayer(
charId: Long,
name: String,
sex: CharacterSex,
faction: PlanetSideEmpire.Value
) extends SourceUniqueness
final case class PlayerSource(
Definition: AvatarDefinition,
ExoSuit: ExoSuitType.Value,
seatedIn: Option[(SourceEntry, Int)],
health: Int,
armor: Int,
Position: Vector3,
Orientation: Vector3,
Velocity: Option[Vector3],
crouching: Boolean,
jumping: Boolean,
Modifiers: ResistanceProfile,
bep: Long,
kills: Seq[Any],
unique: UniquePlayer
) extends SourceWithHealthEntry {
override def Name: String = unique.name
override def Faction: PlanetSideEmpire.Value = unique.faction
override def CharId: Long = unique.charId
def Seated: Boolean = seatedIn.nonEmpty
def Health: Int = health
def Armor: Int = armor
def total: Int = health + armor
}
object PlayerSource {
def apply(p: Player): PlayerSource = {
val exosuit = p.ExoSuit
val faction = p.Faction
val seatedEntity = mountableAndSeat(p)
PlayerSource(
p.Definition,
exosuit,
seatedEntity,
p.Health,
p.Armor,
p.Position,
p.Orientation,
p.Velocity,
p.Crouching,
p.Jumping,
ExoSuitDefinition.Select(exosuit, faction),
p.avatar.bep,
kills = Nil,
UniquePlayer(p.CharId, p.Name, p.Sex, faction)
)
}
def apply(name: String, faction: PlanetSideEmpire.Value, position: Vector3): PlayerSource = {
new PlayerSource(
GlobalDefinitions.avatar,
ExoSuitType.Standard,
seatedIn = None,
health = 100,
armor = 0,
position,
Orientation = Vector3.Zero,
Velocity = None,
crouching = false,
jumping = false,
GlobalDefinitions.Standard,
bep = 0L,
kills = Nil,
UniquePlayer(0L, name, CharacterSex.Male, faction)
)
}
def mountableAndSeat(player: Player): Option[(SourceEntry, Int)] = {
player.Zone.GUID(player.VehicleSeated) match {
case Some(thing: PlanetSideGameObject with Mountable with FactionAffinity) =>
Some((SourceEntry(thing), thing.PassengerInSeat(player).get))
case _ =>
None
}
}
/**
* Produce a mostly normal player source entity
* but the `seatedIn` field is just a shallow copy of the mountable information.
* Said "shallow copy" will not reflect that the player is an occupant of the mountable entity
* even if this function is entirely for the purpose of establishing that the player is an occupant of the mountable entity.<br>
* Don't think too much about it.
* @param player player
* @param mount mountable entity in which the player should be seated
* @param source a `SourceEntry` for the aforementioned mountable entity
* @return a `PlayerSource` entity
*/
def inSeat(player: Player, mount: Mountable, source: SourceEntry): PlayerSource = {
val exosuit = player.ExoSuit
val faction = player.Faction
PlayerSource(
player.Definition,
exosuit,
Some((source, mount.PassengerInSeat(player).get)),
player.Health,
player.Armor,
player.Position,
player.Orientation,
player.Velocity,
player.Crouching,
player.Jumping,
ExoSuitDefinition.Select(exosuit, faction),
player.avatar.bep,
kills = Nil,
UniquePlayer(player.CharId, player.Name, player.Sex, faction)
)
}
/**
* "Nobody is my name: Nobody they call me
* my mother and my father and all my other companions
* Thus I spoke but he immediately replied to me with a ruthless spirit:
* I shall kill Nobody last of all, after his companions,
* the others first: this will be my guest-gift to you.
*/
final val Nobody = PlayerSource("Nobody", PlanetSideEmpire.NEUTRAL, Vector3.Zero)
}

View file

@ -0,0 +1,71 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
import net.psforever.types.{PlanetSideEmpire, Vector3}
trait SourceUniqueness
trait SourceEntry {
def Name: String
def Definition: ObjectDefinition with VitalityDefinition
def CharId: Long = 0L
def Faction: PlanetSideEmpire.Value
def Position: Vector3
def Orientation: Vector3
def Velocity: Option[Vector3]
def Modifiers: ResistanceProfile
def unique: SourceUniqueness
}
trait SourceWithHealthEntry extends SourceEntry {
def health: Int
def total: Int
}
trait SourceWithShieldsEntry extends SourceWithHealthEntry {
def shields: Int
}
object SourceEntry {
final protected val nonUnique: SourceUniqueness = new SourceUniqueness() { }
final val None = new SourceEntry() {
def Name: String = "none"
def Definition: ObjectDefinition with VitalityDefinition = null
def Faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
def Position: Vector3 = Vector3.Zero
def Orientation: Vector3 = Vector3.Zero
def Velocity: Option[Vector3] = Some(Vector3.Zero)
def Modifiers: ResistanceProfile = null
def unique: SourceUniqueness = nonUnique
}
def apply(target: PlanetSideGameObject with FactionAffinity): SourceEntry = {
target match {
case obj: Player => PlayerSource(obj)
case obj: Vehicle => VehicleSource(obj)
case obj: FacilityTurret => TurretSource(obj)
case obj: Amenity => AmenitySource(obj)
case obj: TurretDeployable => TurretSource(obj)
case obj: Deployable => DeployableSource(obj)
case obj: Building => BuildingSource(obj)
case _ => ObjectSource(target)
}
}
def NameFormat(name: String): String = {
name
.replace("_", " ")
.split(" ")
.map(_.capitalize)
.mkString(" ")
}
}

View file

@ -0,0 +1,65 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
import net.psforever.objects.vital.VitalityDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.{PlanetSideGameObject, TurretDeployable}
import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class TurretSource(
Definition: ObjectDefinition with VitalityDefinition,
Faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
Position: Vector3,
Orientation: Vector3,
occupants: List[SourceEntry],
unique: SourceUniqueness
) extends SourceWithHealthEntry with SourceWithShieldsEntry {
def Name: String = SourceEntry.NameFormat(Definition.Descriptor)
def Health: Int = health
def Shields: Int = shields
def Velocity: Option[Vector3] = None
def Modifiers: ResistanceProfile = Definition.asInstanceOf[ResistanceProfile]
def total: Int = health + shields
}
object TurretSource {
def apply(obj: PlanetSideGameObject with WeaponTurret): TurretSource = {
val position = obj.Position
val identifer = obj match {
case o: TurretDeployable =>
UniqueDeployable(
o.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
o.OriginalOwnerName.getOrElse("none")
)
case o: FacilityTurret =>
UniqueAmenity(o.Zone.Number, o.GUID, position)
case o =>
throw new IllegalArgumentException(s"was given ${o.Actor.toString()} when only wanted to model turrets")
}
val turret = TurretSource(
obj.Definition.asInstanceOf[ObjectDefinition with VitalityDefinition],
obj.Faction,
obj.Health,
shields = 0, //TODO implement later
position,
obj.Orientation,
Nil,
identifer
)
turret.copy(occupants = obj match {
case o: Mountable =>
o.Seats.values.flatMap { _.occupants }.map { p => PlayerSource.inSeat(p, o, turret) }.toList
case _ =>
Nil
})
}
}

View file

@ -0,0 +1,10 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
import net.psforever.types.{PlanetSideGUID, Vector3}
final case class UniqueAmenity(
zoneNumber: Int,
guid: PlanetSideGUID,
position: Vector3
) extends SourceUniqueness

View file

@ -0,0 +1,7 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
final case class UniqueDeployable(
spawnTime: Long,
originalOwnerName: String
) extends SourceUniqueness

View file

@ -0,0 +1,64 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.Vehicle
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.types.{DriveState, PlanetSideEmpire, Vector3}
final case class UniqueVehicle(spawnTime: Long, originalOwnerName: String) extends SourceUniqueness
final case class VehicleSource(
Definition: VehicleDefinition,
Faction: PlanetSideEmpire.Value,
health: Int,
shields: Int,
Position: Vector3,
Orientation: Vector3,
Velocity: Option[Vector3],
deployed: DriveState.Value,
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
}
object VehicleSource {
def apply(obj: Vehicle): VehicleSource = {
val faction = obj.HackedBy match {
case Some(o) => o.player.Faction
case _ => obj.Faction
}
val vehicle = VehicleSource(
obj.Definition,
faction,
obj.Health,
obj.Shields,
obj.Position,
obj.Orientation,
obj.Velocity,
obj.DeploymentState,
Nil,
obj.Definition.asInstanceOf[ResistanceProfile],
UniqueVehicle(
obj.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
obj.OriginalOwnerName.getOrElse("none")
)
)
vehicle.copy(occupants = {
obj.Seats.values.map { seat =>
seat.occupant match {
case Some(p) => PlayerSource.inSeat(p, obj, vehicle) //shallow
case _ => PlayerSource.Nobody
}
}.toList
})
}
}

View file

@ -2,7 +2,8 @@
package net.psforever.objects.vehicles
import net.psforever.objects.Vehicle
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality, SourceEntry}
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
import net.psforever.objects.vital.etc.RadiationReason

View file

@ -3,7 +3,6 @@ package net.psforever.objects.vehicles.control
import akka.actor.Cancellable
import net.psforever.objects._
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment._
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
@ -11,8 +10,9 @@ import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObjec
import net.psforever.objects.serverobject.containable.ContainableBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.transfer.TransferBehavior
import net.psforever.objects.sourcing.{SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.VehicleShieldCharge
import net.psforever.objects.vital.ShieldCharge
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
@ -37,12 +37,12 @@ class BfrControl(vehicle: Vehicle)
* `Cancellable.alreadyCancelled` indicates a permanant cessation of charging activity (vehicle destruction) */
var shieldCharge: Cancellable = Default.Cancellable
def SiphoningObject = vehicle
def SiphoningObject: Vehicle = vehicle
def ChargeTransferObject = vehicle
def ChargeTransferObject: Vehicle = vehicle
if (vehicle.Shields < vehicle.MaxShields) {
chargeShields(amount = 0) //start charging if starts as uncharged
chargeShields(amount = 0, Some(VehicleSource(vehicle))) //start charging if starts as uncharged
}
override def postStop(): Unit = {
@ -107,7 +107,7 @@ class BfrControl(vehicle: Vehicle)
shieldCharge = context.system.scheduler.scheduleOnce(
delay = vehicle.Definition.ShieldDamageDelay milliseconds,
self,
Vehicle.ChargeShields(0)
CommonMessages.ChargeShields(0, Some(vehicle))
)
}
}
@ -242,7 +242,7 @@ class BfrControl(vehicle: Vehicle)
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory)
val afterWeapons = weapons
.map { item => item.start += 1; item }
(culledWeaponMounts(pairedArmSubsys.unzip._2), afterWeapons, stow)
(culledWeaponMounts(pairedArmSubsys.map { _._2 }), afterWeapons, stow)
} else if(vWeapons.size == 2 && GlobalDefinitions.isBattleFrameGunnerVehicle(definition)) {
//battleframe is a flight variant but loadout spec is for gunner variant
// remap the hands, shave the gunner mount from the spec, and refit the trunk
@ -312,7 +312,7 @@ class BfrControl(vehicle: Vehicle)
)
}
override def chargeShields(amount: Int): Unit = {
override def chargeShields(amount: Int, motivator: Option[SourceEntry]): Unit = {
chargeShieldsOnly(amount)
shieldCharge(vehicle.Shields, vehicle.Definition, delay = 0) //continue charge?
}
@ -328,7 +328,7 @@ class BfrControl(vehicle: Vehicle)
}).getOrElse(amount) * vehicle.SubsystemStatusMultiplier(sys = "BattleframeShieldGenerator.RechargeRate")).toInt)
vehicle.Shields = before + chargeAmount
val after = vehicle.Shields
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), after - before))
vehicle.LogActivity(ShieldCharge(after - before, Some(VehicleSource(vehicle))))
showShieldCharge()
if (before == 0 && after > 0) {
enableShield()
@ -346,7 +346,7 @@ class BfrControl(vehicle: Vehicle)
shieldCharge = context.system.scheduler.scheduleOnce(
delay = definition.ShieldPeriodicDelay + delay milliseconds,
self,
Vehicle.ChargeShields(0)
CommonMessages.ChargeShields(0, Some(vehicle))
)
} else {
shieldCharge = Default.Cancellable

View file

@ -4,7 +4,6 @@ package net.psforever.objects.vehicles.control
import akka.actor.Cancellable
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects._
import net.psforever.objects.ballistics.VehicleSource
import net.psforever.objects.definition.VehicleDefinition
import net.psforever.objects.definition.converter.OCM
import net.psforever.objects.entity.WorldEntity
@ -20,9 +19,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.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.{DamagingActivity, VehicleShieldCharge, VitalsActivity}
import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones._
@ -60,23 +60,15 @@ class VehicleControl(vehicle: Vehicle)
//make control actors belonging to utilities when making control actor belonging to vehicle
vehicle.Utilities.foreach { case (_, util) => util.Setup }
def MountableObject = vehicle
def JammableObject = vehicle
def FactionObject = vehicle
def DamageableObject = vehicle
def SiphonableObject = vehicle
def RepairableObject = vehicle
def ContainerObject = vehicle
def InteractiveObject = vehicle
def CargoObject = vehicle
def MountableObject: Vehicle = vehicle
def JammableObject: Vehicle = vehicle
def FactionObject: Vehicle = vehicle
def DamageableObject: Vehicle = vehicle
def SiphonableObject: Vehicle = vehicle
def RepairableObject: Vehicle = vehicle
def ContainerObject: Vehicle = vehicle
def InteractiveObject: Vehicle = vehicle
def CargoObject: Vehicle = vehicle
SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
@ -136,8 +128,8 @@ class VehicleControl(vehicle: Vehicle)
dismountBehavior.apply(msg)
dismountCleanup(seat_num)
case Vehicle.ChargeShields(amount) =>
chargeShields(amount)
case CommonMessages.ChargeShields(amount, motivator) =>
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) })
case Vehicle.UpdateZoneInteractionProgressUI(player) =>
updateZoneInteractionProgressUI(player)
@ -406,6 +398,7 @@ class VehicleControl(vehicle: Vehicle)
TaskWorkflow.execute(GUIDTask.unregisterVehicle(zone.GUID, vehicle))
//banished to the shadow realm
vehicle.Position = Vector3.Zero
vehicle.ClearHistory()
//queue final deletion
decayTimer = context.system.scheduler.scheduleOnce(5 seconds, self, VehicleControl.Deletion())
}
@ -559,14 +552,14 @@ class VehicleControl(vehicle: Vehicle)
//make certain vehicles don't charge shields too quickly
def canChargeShields: Boolean = {
val func: VitalsActivity => Boolean = VehicleControl.LastShieldChargeOrDamage(System.currentTimeMillis(), vehicle.Definition)
val func: InGameActivity => Boolean = VehicleControl.LastShieldChargeOrDamage(System.currentTimeMillis(), vehicle.Definition)
vehicle.Health > 0 && vehicle.Shields < vehicle.MaxShields &&
!vehicle.History.exists(func)
vehicle.History.findLast(func).isEmpty
}
def chargeShields(amount: Int): Unit = {
def chargeShields(amount: Int, motivator: Option[SourceEntry]): Unit = {
if (canChargeShields) {
vehicle.History(VehicleShieldCharge(VehicleSource(vehicle), amount))
vehicle.LogActivity(ShieldCharge(amount, motivator))
vehicle.Shields = vehicle.Shields + amount
vehicle.Zone.VehicleEvents ! VehicleServiceMessage(
s"${vehicle.Actor}",
@ -874,7 +867,7 @@ class VehicleControl(vehicle: Vehicle)
}
object VehicleControl {
import net.psforever.objects.vital.{VehicleShieldCharge, VitalsActivity}
import net.psforever.objects.vital.{ShieldCharge}
private case class PrepareForDeletion()
@ -893,10 +886,10 @@ object VehicleControl {
* @return `true`, if the shield charge would be blocked;
* `false`, otherwise
*/
def LastShieldChargeOrDamage(now: Long, vdef: VehicleDefinition)(act: VitalsActivity): Boolean = {
def LastShieldChargeOrDamage(now: Long, vdef: VehicleDefinition)(act: InGameActivity): Boolean = {
act match {
case dact: DamagingActivity => now - dact.time < vdef.ShieldDamageDelay //damage delays next charge
case vsc: VehicleShieldCharge => now - vsc.time < vdef.ShieldPeriodicDelay //previous charge delays next
case vsc: ShieldCharge => now - vsc.time < vdef.ShieldPeriodicDelay //previous charge delays next
case _ => false
}
}

View file

@ -0,0 +1,241 @@
// Copyright (c) 2020 PSForever
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.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}
/* root */
/**
* The root of all chronological activity.
* Must keep track of the time (ms) the activity occurred.
*/
trait InGameActivity {
val time: Long = System.currentTimeMillis()
}
/* normal history */
/**
* A generic classification of activity.
*/
trait GeneralActivity extends InGameActivity
final case class SpawningActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity
final case class ReconstructionActivity(src: SourceEntry, zoneNumber: Int, unit: Option[SourceEntry]) extends GeneralActivity
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) extends GeneralActivity
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) extends GeneralActivity
/* vitals history */
/**
* A vital entity can be hurt or damaged or healed or repaired (HDHR).<br>
* Shields are not included in the definition of what is a "vital statistic",
* and that includes Infantry shields due to the Personal Shield implant
* and MAX shields due to being a New Conglomerate soldier.
*/
trait VitalsActivity extends InGameActivity {
def amount: Int
}
trait HealingActivity extends VitalsActivity
trait RepairingActivity extends VitalsActivity
trait DamagingActivity extends VitalsActivity {
override val time: Long = data.interaction.hitTime
def data: DamageResult
def amount: Int = {
(data.targetBefore, data.targetAfter) match {
case (pb: SourceWithHealthEntry, pa: SourceWithHealthEntry) => pb.total - pa.total
case _ => 0
}
}
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
}
}
}
final case class HealFromKit(kit_def: KitDefinition, amount: Int)
extends HealingActivity
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends HealingActivity
final case class HealFromTerm(term: AmenitySource, amount: Int)
extends HealingActivity
final case class HealFromImplant(implant: ImplantType, amount: Int)
extends HealingActivity
final case class RepairFromExoSuitChange(exosuit: ExoSuitType.Value, amount: Int)
extends RepairingActivity
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
extends RepairingActivity()
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends RepairingActivity
final case class RepairFromTerm(term: AmenitySource, amount: Int) extends RepairingActivity
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
extends RepairingActivity
final case class RepairFromAmenityAutoRepair(amount: Int)
extends RepairingActivity
final case class DamageFrom(data: DamageResult)
extends DamagingActivity
final case class DamageFromProjectile(data: DamageResult)
extends DamagingActivity
final case class DamageFromPainbox(data: DamageResult)
extends DamagingActivity
final case class DamageFromEnvironment(data: DamageResult)
extends DamagingActivity
final case class PlayerSuicide(player: PlayerSource)
extends DamagingActivity {
private lazy val result = {
val out = DamageResult(
player,
player.copy(health = 0),
DamageInteraction(player, SuicideReason(), player.Position)
)
out
}
def data: DamageResult = result
}
final case class DamageFromExplodingEntity(data: DamageResult)
extends DamagingActivity
trait InGameHistory {
/** a list of important events that have occurred in chronological order */
private var history: List[InGameActivity] = List.empty[InGameActivity]
/** the last source of damage that cna be used to indicate blame for damage */
private var lastDamage: Option[DamageResult] = None
def History: List[InGameActivity] = history
/**
* Only the changes to vitality statistics.
* @return a list of the chronologically-consistent vitality events
*/
def VitalsHistory(): List[VitalsActivity] = History.collect {
case event: VitalsActivity => event
}
/**
* An in-game event must be recorded.
* Add new entry to the front of the list (for recent activity).
* @param action the fully-informed entry
* @return the list of previous changes to this entity
*/
def LogActivity(action: InGameActivity): List[InGameActivity] = LogActivity(Some(action))
/**
* An in-game event must be recorded.
* Add new entry to the front of the list (for recent activity).
* @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) =>
history = act +: history
case None => ()
}
history
}
/**
* Very common example of a `VitalsActivity` event involving damage.
* They are repackaged before submission and are often tagged for specific blame.
* @param result the fully-informed entry
* @return the list of previous changes to this object's vital statistics
*/
def LogActivity(result: DamageResult): List[InGameActivity] = {
result.interaction.cause match {
case _: ProjectileReason =>
LogActivity(DamageFromProjectile(result))
lastDamage = Some(result)
case _: ExplodingEntityReason =>
LogActivity(DamageFromExplodingEntity(result))
lastDamage = Some(result)
case _: PainboxReason =>
LogActivity(DamageFromPainbox(result))
case _: EnvironmentReason =>
LogActivity(DamageFromEnvironment(result))
case _ => ;
LogActivity(DamageFrom(result))
if(result.adversarial.nonEmpty) {
lastDamage = Some(result)
}
}
History
}
def LastDamage: Option[DamageResult] = lastDamage
/**
* Find, specifically, the last instance of a weapon discharge that caused damage.
* @return information about the discharge
*/
def LastShot: Option[DamageResult] = {
History.findLast({ p => p.isInstanceOf[DamageFromProjectile] }).map {
case entry: DamageFromProjectile => entry.data
}
}
def ClearHistory(): List[InGameActivity] = {
lastDamage = None
val out = history
history = List.empty
out
}
}
object InGameHistory {
def SpawnReconstructionActivity(
obj: PlanetSideGameObject with FactionAffinity with InGameHistory,
zoneNumber: Int,
unit: Option[SourceEntry]
): Unit = {
val event: GeneralActivity = if (obj.History.nonEmpty || obj.History.headOption.exists {
_.isInstanceOf[SpawningActivity]
}) {
ReconstructionActivity(ObjectSource(obj), zoneNumber, unit)
} else {
SpawningActivity(ObjectSource(obj), zoneNumber, unit)
}
if (obj.History.lastOption match {
case Some(evt: SpawningActivity) => evt != event
case Some(evt: ReconstructionActivity) => evt != event
case _ => true
}) {
obj.LogActivity(event)
}
}
}

View file

@ -8,7 +8,7 @@ import net.psforever.objects.vital.resolution.{DamageAndResistance, ResolutionCa
* The amount of HDHR is controlled by the damage model of this vital object reacting to stimulus.
* The damage model is provided.
*/
trait Vitality extends VitalsHistory {
trait Vitality extends InGameHistory {
private var health: Int = Definition.DefaultHealth
private var defaultHealth: Option[Int] = None
private var maxHealth: Option[Int] = None

View file

@ -1,237 +0,0 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital
import net.psforever.objects.Player
import net.psforever.objects.ballistics.{PlayerSource, VehicleSource}
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.terminals.TerminalDefinition
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}
trait VitalsActivity {
def time: Long
}
trait HealingActivity extends VitalsActivity {
def amount: Int
val time: Long = System.currentTimeMillis()
}
trait DamagingActivity extends VitalsActivity {
def time: Long = data.interaction.hitTime
def data: DamageResult
}
final case class HealFromKit(kit_def: KitDefinition, amount: Int)
extends HealingActivity
final case class HealFromEquipment(
user: PlayerSource,
equipment_def: EquipmentDefinition,
amount: Int
) extends HealingActivity
final case class HealFromTerm(term_def: TerminalDefinition, health: Int, armor: Int)
extends HealingActivity {
def amount: Int = health + armor
}
final case class HealFromImplant(implant: ImplantType, health: Int)
extends HealingActivity {
def amount: Int = health
}
final case class HealFromExoSuitChange(exosuit: ExoSuitType.Value)
extends HealingActivity {
def amount: Int = 0
}
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
extends HealingActivity()
final case class RepairFromEquipment(
user: PlayerSource,
equipment_def: EquipmentDefinition,
amount: Int
) extends HealingActivity
final case class RepairFromTerm(term_def: TerminalDefinition, amount: Int)
extends HealingActivity
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, amount: Int)
extends HealingActivity
final case class VehicleShieldCharge(amount: Int)
extends HealingActivity //TODO facility
final case class DamageFrom(data: DamageResult)
extends DamagingActivity
final case class DamageFromProjectile(data: DamageResult)
extends DamagingActivity
final case class DamageFromPainbox(data: DamageResult)
extends DamagingActivity
final case class DamageFromEnvironment(data: DamageResult)
extends DamagingActivity
final case class PlayerSuicide(player: PlayerSource)
extends DamagingActivity {
private lazy val result = {
val out = DamageResult(
player,
player.copy(health = 0),
DamageInteraction(player, SuicideReason(), player.Position)
)
out
}
def data: DamageResult = result
}
final case class DamageFromExplodingEntity(data: DamageResult)
extends DamagingActivity
/**
* A vital object can be hurt or damaged or healed or repaired (HDHR).
* A history of the previous changes in vital statistics of the underlying object is recorded
* in reverse chronological order.
*/
trait VitalsHistory {
/** a reverse-order list of chronological events that have occurred to these vital statistics */
private var vitalsHistory: List[VitalsActivity] = List.empty[VitalsActivity]
private var lastDamage: Option[DamageResult] = None
def History: List[VitalsActivity] = vitalsHistory
/**
* A `VitalsActivity` event must be recorded.
* Add new entry to the front of the list (for recent activity).
* @param action the fully-informed entry
* @return the list of previous changes to this object's vital statistics
*/
def History(action: VitalsActivity): List[VitalsActivity] = History(Some(action))
/**
* A `VitalsActivity` event must be recorded.
* Add new entry to the front of the list (for recent activity).
* @param action the fully-informed entry
* @return the list of previous changes to this object's vital statistics
*/
def History(action: Option[VitalsActivity]): List[VitalsActivity] = {
action match {
case Some(act) =>
vitalsHistory = act +: vitalsHistory
case None => ;
}
vitalsHistory
}
/**
* Very common example of a `VitalsActivity` event involving damage.
* @param result the fully-informed entry
* @return the list of previous changes to this object's vital statistics
*/
def History(result: DamageResult): List[VitalsActivity] = {
result.interaction.cause match {
case _: ProjectileReason =>
vitalsHistory = DamageFromProjectile(result) +: vitalsHistory
lastDamage = Some(result)
case _: ExplodingEntityReason =>
vitalsHistory = DamageFromExplodingEntity(result) +: vitalsHistory
lastDamage = Some(result)
case _: PainboxReason =>
vitalsHistory = DamageFromPainbox(result) +: vitalsHistory
case _: EnvironmentReason =>
vitalsHistory = DamageFromEnvironment(result) +: vitalsHistory
case _ => ;
vitalsHistory = DamageFrom(result) +: vitalsHistory
if(result.adversarial.nonEmpty) {
lastDamage = Some(result)
}
}
vitalsHistory
}
def LastDamage: Option[DamageResult] = lastDamage
/**
* Find, specifically, the last instance of a weapon discharge vital statistics change.
* @return information about the discharge
*/
def LastShot: Option[DamageResult] = {
vitalsHistory.find({ p => p.isInstanceOf[DamageFromProjectile] }) match {
case Some(entry: DamageFromProjectile) =>
Some(entry.data)
case _ =>
None
}
}
def ClearHistory(): List[VitalsActivity] = {
val out = vitalsHistory
vitalsHistory = List.empty[VitalsActivity]
out
}
}
//deprecated overrides
object HealFromKit {
def apply(Target: PlayerSource, amount: Int, kit_def: KitDefinition): HealFromKit =
HealFromKit(kit_def, amount)
}
object HealFromEquipment {
def apply(
Target: PlayerSource,
user: PlayerSource,
amount: Int,
equipment_def: EquipmentDefinition
): HealFromEquipment =
HealFromEquipment(user, equipment_def, amount)
}
object HealFromTerm {
def apply(Target: PlayerSource, health: Int, armor: Int, term_def: TerminalDefinition): HealFromTerm =
HealFromTerm(term_def, health, armor)
}
object HealFromImplant {
def apply(Target: PlayerSource, amount: Int, implant: ImplantType): HealFromImplant =
HealFromImplant(implant, amount)
}
object HealFromExoSuitChange {
def apply(Target: PlayerSource, exosuit: ExoSuitType.Value): HealFromExoSuitChange =
HealFromExoSuitChange(exosuit)
}
object RepairFromKit {
def apply(Target: PlayerSource, amount: Int, kit_def: KitDefinition): RepairFromKit =
RepairFromKit(kit_def, amount)
}
object RepairFromEquipment {
def apply(
Target: PlayerSource,
user: PlayerSource,
amount: Int,
equipment_def: EquipmentDefinition
) : RepairFromEquipment =
RepairFromEquipment(user, equipment_def, amount)
}
object RepairFromTerm {
def apply(Target: VehicleSource, amount: Int, term_def: TerminalDefinition): RepairFromTerm =
RepairFromTerm(term_def, amount)
}
object VehicleShieldCharge {
def apply(Target: VehicleSource, amount: Int): VehicleShieldCharge =
VehicleShieldCharge(amount)
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.base
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.prop.DamageProperties

View file

@ -1,8 +1,8 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vital.collision
import net.psforever.objects.ballistics.{DeployableSource, PlayerSource, VehicleSource}
import net.psforever.objects.definition.ExoSuitDefinition
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, VehicleSource}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.types.Vector3

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.collision
import net.psforever.objects.ballistics.{DeployableSource, SourceEntry, VehicleSource}
import net.psforever.objects.sourcing.{DeployableSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution, DamageType}
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.DamageAndResistance

View file

@ -2,7 +2,7 @@
package net.psforever.objects.vital.damage
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.ballistics._
import net.psforever.objects.sourcing.VehicleSource
import net.psforever.objects.vital.base.{DamageModifiers, DamageReason}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.prop.DamageWithPosition

View file

@ -1,9 +1,9 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.environment
import net.psforever.objects.ballistics.PlayerSource
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.serverobject.environment.EnvironmentAttribute
import net.psforever.objects.sourcing.PlayerSource
/**
* The deeper you move into lava, the greater the amount of health you burn through.

View file

@ -1,8 +1,8 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.environment
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.environment.{EnvironmentAttribute, PieceOfEnvironment}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.prop.DamageProperties

View file

@ -1,8 +1,8 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.{GlobalDefinitions, Tool, Vehicle}
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.prop.DamageWithPosition

View file

@ -2,8 +2,8 @@
package net.psforever.objects.vital.etc
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.prop.DamageWithPosition

View file

@ -2,9 +2,9 @@
package net.psforever.objects.vital.etc
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}

View file

@ -1,8 +1,8 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.damage.DamageCalculations

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