removed unnecessary handling from spectator mode; safety rails added to ValidObject, so oit doesn't get too silly; turrets will no longer end the world for somebody

This commit is contained in:
Fate-JH 2024-08-28 23:55:43 -04:00
parent a4b7e77d9d
commit 4cbcf3a5e4
3 changed files with 15 additions and 476 deletions

View file

@ -554,7 +554,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
}
.orElse {
//occasionally, something that is not technically a turret's natural target may be attacked
sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target")
continent.GUID(targetGuid) //AIDamage/Attacker
.collect {
case target: PlanetSideServerObject with FactionAffinity with Vitality =>
sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker")

View file

@ -1,138 +1,28 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.AvatarActor
import akka.actor.ActorContext
import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations}
import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory}
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
import net.psforever.objects.definition.ProjectileDefinition
import net.psforever.objects.equipment.{ChargeFireModeDefinition, EquipmentSize}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.equipment.ChargeFireModeDefinition
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.doors.InteriorDoorPassage
import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, DummyExplodingEntity, GlobalDefinitions, OwnableByPlayer, PlanetSideGameObject, SpecialEmp, Tool}
import net.psforever.objects.serverobject.interior.Sidedness
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
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.OicwLilBuddyReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.{Zone, ZoneProjectile}
import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage}
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, GlobalDefinitions, PlanetSideGameObject, Tool}
import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.util.Config
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import net.psforever.types.PlanetSideGUID
object WeaponAndProjectileLogic {
def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = {
new WeaponAndProjectileLogic(ops, ops.context)
}
/**
* Does a line segment line intersect with a sphere?<br>
* This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
* @param start first point of the line segment
* @param end second point of the line segment
* @param center center of the sphere
* @param radius radius of the sphere
* @return list of all points of intersection, if any
* @see `Vector3.DistanceSquared`
* @see `Vector3.MagnitudeSquared`
*/
private def quickLineSphereIntersectionPoints(
start: Vector3,
end: Vector3,
center: Vector3,
radius: Float
): Iterable[Vector3] = {
/*
Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
*/
val Vector3(cx, cy, cz) = center
val Vector3(sx, sy, sz) = start
val vector = end - start
//speed our way through a quadratic equation
val (a, b) = {
val Vector3(dx, dy, dz) = vector
(
dx * dx + dy * dy + dz * dz,
2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
)
}
val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
val result = b * b - 4 * a * c
if (result < 0f) {
//negative, no intersection
Seq()
} else if (result < 0.00001f) {
//zero-ish, one intersection point
Seq(start - vector * (b / (2f * a)))
} else {
//positive, two intersection points
val sqrt = math.sqrt(result).toFloat
val endStart = vector / (2f * a)
Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
}.filter(p => Vector3.DistanceSquared(start, p) <= a)
}
/**
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
* The main difference from "normal" server-side explosion
* is that the owner of the projectile must be clarified explicitly.
* @see `Zone::serverSideDamage`
* @param zone where the explosion is taking place
* (`source` contains the coordinate location)
* @param source a game object that represents the source of the explosion
* @param owner who or what to accredit damage from the explosion to;
* clarifies a normal `SourceEntry(source)` accreditation
*/
private def detonateLittleBuddy(
zone: Zone,
source: PlanetSideGameObject with FactionAffinity with Vitality,
proxy: Projectile,
owner: SourceEntry
)(): Unit = {
Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position))
}
/**
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
* The main difference from "normal" server-side explosion
* is that the owner of the projectile must be clarified explicitly.
* The sub-projectiles will be the product of a normal projectile rather than a standard game object
* so a custom `source` entity must wrap around it and fulfill the requirements of the field.
* @see `Zone::explosionDamage`
* @param owner who or what to accredit damage from the explosion to
* @param explosionPosition where the explosion will be positioned in the game world
* @param source a game object that represents the source of the explosion
* @param target a game object that is affected by the explosion
* @return a `DamageInteraction` object
*/
private def littleBuddyExplosionDamage(
owner: SourceEntry,
projectileId: Long,
explosionPosition: Vector3
)
(
source: PlanetSideGameObject with FactionAffinity with Vitality,
target: PlanetSideGameObject with FactionAffinity with Vitality
): DamageInteraction = {
DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition)
}
}
class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions {
def sessionLogic: SessionData = ops.sessionLogic
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
//private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
/* packets */
@ -227,126 +117,11 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
def handleDirectHit(pkt: HitMessage): Unit = { /* intentionally blank */ }
def handleSplashHit(pkt: SplashHitMessage): Unit = {
val SplashHitMessage(
_,
projectile_guid,
explosion_pos,
direct_victim_uid,
_,
projectile_vel,
_,
targets
) = pkt
ops.FindProjectileEntry(projectile_guid) match {
case Some(projectile) =>
val profile = projectile.profile
projectile.Velocity = projectile_vel
val (resolution1, resolution2) = profile.Aggravated match {
case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
(DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash)
case _ =>
(DamageResolution.Splash, DamageResolution.Splash)
}
//direct_victim_uid
sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionLogic.handleDealingDamage(target, resprojectile)
}
case _ => ()
}
//other victims
targets.foreach(elem => {
sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionLogic.handleDealingDamage(target, resprojectile)
}
case _ => ()
}
})
//...
HandleDamageProxy(projectile, projectile_guid, explosion_pos)
if (
projectile.profile.HasJammedEffectDuration ||
projectile.profile.JammerProjectile ||
projectile.profile.SympatheticExplosion
) {
//can also substitute 'projectile.profile' for 'SpecialEmp.emp'
Zone.serverSideDamage(
continent,
player,
SpecialEmp.emp,
SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
SpecialEmp.findAllBoomers(profile.DamageRadius)
)
}
if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
//cleanup
if (projectile.HasGUID) {
continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
}
}
case None => ()
}
}
def handleSplashHit(pkt: SplashHitMessage): Unit = { /* intentionally blank */ }
def handleLashHit(pkt: LashMessage): Unit = { /* intentionally blank */ }
def handleAIDamage(pkt: AIDamage): Unit = {
val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
(continent.GUID(player.VehicleSeated) match {
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
if tobj.GUID == targetGuid &&
tobj.OwnerGuid.contains(player.GUID) =>
//deployable turrets
Some(tobj)
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
if tobj.GUID == targetGuid &&
tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
//facility turrets, etc.
Some(tobj)
case _
if player.GUID == targetGuid =>
//player avatars
Some(player)
case _ =>
None
}).collect {
case target: AutomatedTurret.Target =>
sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
.collect {
case turret: AutomatedTurret if turret.Target.isEmpty =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
Some(target)
case turret: AutomatedTurret =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
Some(target)
}
}
.orElse {
//occasionally, something that is not technically a turret's natural target may be attacked
sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target")
.collect {
case target: PlanetSideServerObject with FactionAffinity with Vitality =>
sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker")
.collect {
case turret: AutomatedTurret if turret.Target.nonEmpty =>
//the turret must be shooting at something (else) first
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
}
Some(target)
}
}
}
def handleAIDamage(pkt: AIDamage): Unit = { /* intentionally blank */ }
/* support code */
@ -402,167 +177,6 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity))
}
private def CheckForHitPositionDiscrepancy(
projectile_guid: PlanetSideGUID,
hitPos: Vector3,
target: PlanetSideGameObject with FactionAffinity with Vitality
): Unit = {
val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position)
if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) {
// If the target position on the server does not match the position where the projectile landed within reason there may be foul play
log.warn(
s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect"
)
}
}
/**
* na
* @param projectile the projectile object
* @param resolution the resolution status to promote the projectile
* @return a copy of the projectile
*/
private def ResolveProjectileInteraction(
projectile: Projectile,
resolution: DamageResolution.Value,
target: PlanetSideGameObject with FactionAffinity with Vitality,
pos: Vector3
): Option[DamageInteraction] = {
if (projectile.isMiss) {
log.warn("expected projectile was already counted as a missed shot; can not resolve any further")
None
} else {
val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player))
if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) {
avatarActor ! AvatarActor.ConsumeStamina(10)
}
Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos))
}
}
/**
* Take a projectile that was introduced into the game world and
* determine if it generates a secondary damage projectile or
* an method of damage causation that requires additional management.
* @param projectile the projectile
* @param pguid the client-local projectile identifier
* @param hitPos the game world position where the projectile is being recorded
* @return a for all affected targets, a combination of projectiles, projectile location, and the target's location;
* nothing if no targets were affected
*/
private def HandleDamageProxy(
projectile: Projectile,
pguid: PlanetSideGUID,
hitPos: Vector3
): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = {
GlobalDefinitions.getDamageProxy(projectile, hitPos) match {
case Nil =>
Nil
case list if list.isEmpty =>
Nil
case list =>
HandleDamageProxySetupLittleBuddy(list, hitPos)
UpdateProjectileSidednessAfterHit(projectile, hitPos)
val projectileSide = projectile.WhichSide
list.flatMap { proxy =>
if (proxy.profile.ExistsOnRemoteClients) {
proxy.Position = hitPos
proxy.WhichSide = projectileSide
continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy)
Nil
} else if (proxy.tool_def == GlobalDefinitions.maelstrom) {
//server-side maelstrom grenade target selection
val radius = proxy.profile.LashRadius * proxy.profile.LashRadius
val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList })
.filter { target =>
Vector3.DistanceSquared(target.Position, hitPos) <= radius
}
//chainlash is separated from the actual damage application for convenience
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.SendResponse(
PlanetSideGUID(0),
ChainLashMessage(
hitPos,
projectile.profile.ObjectId,
targets.map { _.GUID }
)
)
)
targets.map { target =>
CheckForHitPositionDiscrepancy(pguid, hitPos, target)
(target, proxy, hitPos, target.Position)
}
} else {
Nil
}
}
}
}
private def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = {
val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw }
val size: Int = listOfLittleBuddies.size
if (size > 0) {
val desiredDownwardsProjectiles: Int = 2
val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down
val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out
val z: Float = player.Orientation.z //player's standing direction
val north: Vector3 = Vector3(0,1,0) //map North
val speed: Float = 144f //speed (packet discovered)
val dist: Float = 25 //distance (client defined)
val downwardsAngle: Float = -85f
val flaredAngle: Float = -70f
//angle of separation for downwards, degrees from vertical for flared out
val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) {
(360f / firstHalf, downwardsAngle)
} else {
(0f, 0f)
}
val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) {
(360f / secondHalf, flaredAngle)
} else {
(0f, 0f)
}
val smallRotOffset: Float = z + 90f
val largeRotOffset: Float = z + math.random().toFloat * 45f
val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat)
//downwards projectiles
var i: Int = 0
listOfLittleBuddies.take(firstHalf).foreach { proxy =>
val facing = (smallRotOffset + smallStep * i.toFloat) % 360
val dir = north.Rx(smallAngle).Rz(facing)
proxy.Position = detonationPosition + dir.xy + verticalCorrection
proxy.Velocity = dir * speed
proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing)
HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
i += 1
}
//flared out projectiles
i = 0
listOfLittleBuddies.drop(firstHalf).foreach { proxy =>
val facing = (largeRotOffset + largeStep * i.toFloat) % 360
val dir = north.Rx(largeAngle).Rz(facing)
proxy.Position = detonationPosition + dir
proxy.Velocity = dir * speed
proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing)
HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
i += 1
}
true
} else {
false
}
}
private def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = {
//explosion
val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction)
obj.Position = obj.Position + orientation * distance
val explosionFunc: ()=>Unit = WeaponAndProjectileLogic.detonateLittleBuddy(continent, obj, proxy, proxy.owner)
context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() }
}
private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = {
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
@ -601,81 +215,4 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
fireStateStopUpdateChargeAndCleanup(tool)
ops.fireStateStopMountedMessages(itemGuid)
}
//noinspection SameParameterValue
private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
ops.addShotsToMap(ops.shotsLanded, weaponId, shots)
}
private def CompileAutomatedTurretDamageData(
turret: AutomatedTurret,
owner: SourceEntry,
projectileTypeId: Long
): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
turret.Weapons
.values
.flatMap { _.Equipment }
.collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
.find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
}
private def HandleAIDamage(
target: PlanetSideServerObject with FactionAffinity with Vitality,
results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
): Unit = {
results.collect {
case (obj, tool, owner, projectileInfo) =>
val angle = Vector3.Unit(target.Position - obj.Position)
val proj = new Projectile(
projectileInfo,
tool.Definition,
tool.FireMode,
None,
owner,
obj.Definition.ObjectId,
obj.Position + Vector3.z(value = 1f),
angle,
Some(angle * projectileInfo.FinalVelocity)
)
val hitPos = target.Position + Vector3.z(value = 1f)
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionLogic.handleDealingDamage(target, resprojectile)
}
}
}
private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = {
val origin = projectile.Position
val distance = Vector3.Magnitude(hitPosition - origin)
continent.blockMap
.sector(hitPosition, distance)
.environmentList
.collect { case o: InteriorDoorPassage =>
val door = o.door
val intersectTest = WeaponAndProjectileLogic.quickLineSphereIntersectionPoints(
origin,
hitPosition,
door.Position,
door.Definition.UseRadius + 0.1f
)
(door, intersectTest)
}
.collect { case (door, intersectionTest) if intersectionTest.nonEmpty =>
(door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest)
}
.minByOption { case (_, dist, _) => dist }
.foreach { case (door, _, intersects) =>
val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) {
Sidedness.OutsideOf
} else {
Sidedness.InsideOf
}
projectile.WhichSide = if (intersects.size == 1) {
Sidedness.InBetweenSides(door, strictly)
} else {
strictly
}
}
}
}

View file

@ -238,8 +238,9 @@ class SessionData(
case Some(_: LocalLockerItem) =>
player.avatar.locker.Inventory.hasItem(guid) match {
case out @ Some(_) =>
case out @ Some(thing) =>
contextSafeEntity = guid
oldRefsMap.put(guid, thing.Definition.Name)
out
case None if contextSafeEntity == guid =>
//safeguard
@ -263,9 +264,10 @@ class SessionData(
None
case out @ Some(obj) if obj.HasGUID =>
oldRefsMap.put(guid, obj.Definition.Name)
out
case None if !id.contains(PlanetSideGUID(0)) =>
case None if guid != PlanetSideGUID(0) && guid != player.GUID && !player.VehicleSeated.contains(guid) =>
//delete stale entity reference from client
//deleting guid=0 will cause BAD things to happen
log.error(s"$elevatedDecorator: ${player.Name} has an invalid reference to $hint with GUID $guid in zone ${continent.id}")